diff --git a/.editorconfig b/.editorconfig index 65651a6c..7fb304be 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,12 @@ root = true + [*] indent_style = space -indent_size = 4 \ No newline at end of file +indent_size = 4 + +[*.cs] +charset = utf-8-bom +trim_trailing_whitespace = true +insert_final_newline = true +# IDE0032: Use auto property +dotnet_style_prefer_auto_properties = true:none diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..4973482b --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: ['https://www.paypal.com/donate/?business=G9PDX5KLGKBA2&no_recurring=0¤cy_code=USD'] diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml new file mode 100644 index 00000000..fd5ee070 --- /dev/null +++ b/.github/workflows/nuget-publish.yml @@ -0,0 +1,88 @@ +name: Publish NuGet Package + +on: + workflow_dispatch: + +jobs: + publish-release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore + + - name: Build LiteNetLib + run: dotnet build LiteNetLib/LiteNetLib.csproj --configuration Release + + - name: Check version and pack if changed + id: check-pack + run: | + echo "Checking for version changes..." + + PROJECT_PATH="LiteNetLib/LiteNetLib.csproj" + PACKAGE_NAME="litenetlib" + + # Extract version from project file + LOCAL_VERSION=$(grep -o '[^<]*' "$PROJECT_PATH" | sed 's/\(.*\)<\/PackageVersion>/\1/') + + if [ -z "$LOCAL_VERSION" ]; then + echo "No tag found in $PROJECT_PATH" + echo "has_packages=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Get the latest version from NuGet.org + echo "Checking NuGet.org for $PACKAGE_NAME..." + NUGET_VERSION=$(curl -s "https://api.nuget.org/v3-flatcontainer/$PACKAGE_NAME/index.json" | grep -o '"[^"]*"' | sed 's/"//g' | sort -V | tail -n 1) + + echo "Local version: $LOCAL_VERSION" + echo "NuGet version: $NUGET_VERSION" + + # Compare versions + if [ "$LOCAL_VERSION" != "$NUGET_VERSION" ]; then + echo "Version changed - will pack and publish" + mkdir -p ./nupkgs + dotnet pack "$PROJECT_PATH" --configuration Release --no-build --output ./nupkgs + echo "has_packages=true" >> $GITHUB_OUTPUT + else + echo "Version unchanged - skipping" + echo "has_packages=false" >> $GITHUB_OUTPUT + fi + + - name: Push to NuGet.org + if: steps.check-pack.outputs.has_packages == 'true' + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + echo "Publishing to NuGet.org..." + for package in ./nupkgs/*.nupkg; do + if [ -f "$package" ]; then + echo "Publishing $package..." + dotnet nuget push "$package" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate || { + echo "Failed to publish $package" + exit 1 + } + fi + done + + - name: Upload package as artifact + if: steps.check-pack.outputs.has_packages == 'true' + uses: actions/upload-artifact@v4 + with: + name: nuget-package + path: ./nupkgs/*.nupkg + retention-days: 7 + + - name: No package to publish + if: steps.check-pack.outputs.has_packages == 'false' + run: echo "Version unchanged - nothing to publish" diff --git a/.gitignore b/.gitignore index dbffa72f..3efd0e52 100644 --- a/.gitignore +++ b/.gitignore @@ -160,6 +160,8 @@ publish/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config +!LiteNetLibSampleUnity/Packages/* + # Windows Azure Build Output csx/ *.build.csdef diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 99393544..00000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: csharp -solution: LiteNetLib.sln -install: - - nuget restore LiteNetLib.sln -script: - - xbuild /p:Configuration=Release /target:"LiteNetLib;LibSample" LiteNetLib.sln -notifications: - email: false \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt index a2f4003a..ec1bddf3 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Ruslan Pyrch +Copyright (c) 2025 Ruslan Pyrch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/LNL.png b/LNL.png new file mode 100644 index 00000000..33aedfff Binary files /dev/null and b/LNL.png differ diff --git a/LNL.svg b/LNL.svg new file mode 100644 index 00000000..3a8ef232 --- /dev/null +++ b/LNL.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/LibSample/AesEncryptLayer.cs b/LibSample/AesEncryptLayer.cs new file mode 100644 index 00000000..1df94508 --- /dev/null +++ b/LibSample/AesEncryptLayer.cs @@ -0,0 +1,110 @@ +using LiteNetLib.Layers; +using System; +using System.Net; +using System.Security.Cryptography; + +namespace LibSample +{ + /// + /// Uses AES encryption in CBC mode. Make sure you handle your key properly. + /// GCHandle.Alloc(key, GCHandleType.Pinned) to avoid your key being moved around different memory segments. + /// ZeroMemory(gch.AddrOfPinnedObject(), key.Length); to erase it when you are done. + /// Speed varies greatly depending on hardware encryption support. + /// Why encrypt: http://ithare.com/udp-for-games-security-encryption-and-ddos-protection/ + /// + public class AesEncryptLayer : PacketLayerBase + { + public const int KeySize = 256; + public const int BlockSize = 128; + public const int KeySizeInBytes = KeySize / 8; + public const int BlockSizeInBytes = BlockSize / 8; + + private readonly Aes _aes; + private ICryptoTransform _encryptor; + private byte[] cipherBuffer = new byte[1500]; //Max possible UDP packet size + private ICryptoTransform _decryptor; + private byte[] ivBuffer = new byte[BlockSizeInBytes]; + + /// + /// Should be safe against eavesdropping, but is vulnerable to tampering + /// Needs a HMAC on top of the encrypted content to be fully safe + /// + /// + /// + public AesEncryptLayer(byte[] key) : base(BlockSizeInBytes * 2) + { + if (key.Length != KeySizeInBytes) throw new NotSupportedException("EncryptLayer only supports keysize " + KeySize); + + //Switch this with AesGCM for better performance, requires .NET Core 3.0 or Standard 2.1 + _aes = Aes.Create(); + _aes.KeySize = KeySize; + _aes.BlockSize = BlockSize; + _aes.Key = key; + _aes.Mode = CipherMode.CBC; + _aes.Padding = PaddingMode.PKCS7; + + _encryptor = _aes.CreateEncryptor(); + _decryptor = _aes.CreateDecryptor(); + } + + public override void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int length) + { + //Can't copy directly to _aes.IV. It won't work for some reason. + Buffer.BlockCopy(data, 0, ivBuffer, 0, ivBuffer.Length); + //_aes.IV = ivBuffer; + _decryptor = _aes.CreateDecryptor(_aes.Key, ivBuffer); + + //int currentRead = ivBuffer.Length; + //int currentWrite = 0; + + //TransformBlocks(_decryptor, data, length, ref currentRead, ref currentWrite); + byte[] lastBytes = _decryptor.TransformFinalBlock(data, BlockSizeInBytes, length - BlockSizeInBytes); + + data = lastBytes; + length = lastBytes.Length; + } + + public override void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) + { + //Some Unity platforms may need these (and will be slower + generate garbage) + if (!_encryptor.CanReuseTransform) + { + _aes.GenerateIV(); + _encryptor = _aes.CreateEncryptor(); + } + + //Copy IV in plaintext to output, this is standard practice + _aes.IV.CopyTo(cipherBuffer, 0); + + int currentRead = offset; + int currentWrite = _aes.IV.Length; + byte[] lastBytes = _encryptor.TransformFinalBlock(data, currentRead, length - offset); + lastBytes.CopyTo(cipherBuffer, currentWrite); + //TransformBlocks(_encryptor, data, length, ref currentRead, ref currentWrite); + + data = cipherBuffer; + offset = 0; + length = lastBytes.Length + BlockSizeInBytes; + } + + private void TransformBlocks(ICryptoTransform transform, byte[] input, int inputLength, ref int currentRead, ref int currentWrite) + { + //This loop produces a invalid padding exception + //I'm leaving it here as a start point in case others need support for + //Platforms wheere !transfom.CanTransformMultipleBlocks + if (!transform.CanTransformMultipleBlocks) + { + while (inputLength - currentRead > BlockSizeInBytes) + { + int encryptedCount = transform.TransformBlock(input, currentRead, BlockSizeInBytes, cipherBuffer, currentWrite); + currentRead += encryptedCount; + currentWrite += encryptedCount; + } + } + + byte[] lastBytes = transform.TransformFinalBlock(input, currentRead, inputLength - currentRead); + lastBytes.CopyTo(cipherBuffer, currentWrite); + currentWrite += lastBytes.Length; + } + } +} diff --git a/LibSample/AesEncryptionTest.cs b/LibSample/AesEncryptionTest.cs new file mode 100644 index 00000000..e46e249d --- /dev/null +++ b/LibSample/AesEncryptionTest.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text; + +namespace LibSample +{ + class AesEncryptionTest : IExample + { + public void Run() => AesLayerEncryptDecrypt(); + + private void AesLayerEncryptDecrypt() + { + IPEndPoint emptyEndPoint = null; + + var keyGen = RandomNumberGenerator.Create(); + byte[] key = new byte[AesEncryptLayer.KeySizeInBytes]; + keyGen.GetBytes(key); + const string testData = "This text is long enough to need multiple blocks to encrypt"; + + var outboudLayer = new AesEncryptLayer(key); + byte[] outbound = Encoding.ASCII.GetBytes(testData); + int lengthOfPacket = outbound.Length; + int start = 0; + int length = outbound.Length; + outboudLayer.ProcessOutBoundPacket(ref emptyEndPoint, ref outbound, ref start, ref length); + + int minLenth = lengthOfPacket + AesEncryptLayer.BlockSizeInBytes; + int maxLength = lengthOfPacket + outboudLayer.ExtraPacketSizeForLayer; + if (length < minLenth || length > maxLength) + { + throw new Exception("Packet length out of bounds"); + } + + var inboundLayer = new AesEncryptLayer(key); + //Copy array so we dont read and write to same array + byte[] inboundData = new byte[outbound.Length]; + outbound.CopyTo(inboundData, 0); + inboundLayer.ProcessInboundPacket(ref emptyEndPoint, ref inboundData, ref length); + + Console.WriteLine(Encoding.ASCII.GetString(inboundData, 0, length)); + byte[] expectedPlaintext = Encoding.ASCII.GetBytes(testData); + + var isEqualLength = expectedPlaintext.Length == length; + var areContentEqual = expectedPlaintext.SequenceEqual(inboundData); + if (isEqualLength && areContentEqual) + { + Console.WriteLine("Test complete"); + } + else + { + throw new Exception("Test failed, decrypted data not equal to original"); + } + } + } +} diff --git a/LibSample/BroadcastTest.cs b/LibSample/BroadcastTest.cs index 2b6973fd..6720a3e3 100644 --- a/LibSample/BroadcastTest.cs +++ b/LibSample/BroadcastTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Threading; @@ -7,7 +8,7 @@ namespace LibSample { - class BroadcastTest + class BroadcastTest : IExample { private class ClientListener : INetEventListener { @@ -15,7 +16,7 @@ private class ClientListener : INetEventListener public void OnPeerConnected(NetPeer peer) { - Console.WriteLine("[Client {0}] connected to: {1}:{2}", Client.LocalPort, peer.EndPoint.Address, peer.EndPoint.Port); + Console.WriteLine("[Client {0}] connected to: {1}:{2}", Client.LocalPort, peer.Address, peer.Port); } public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) @@ -28,7 +29,7 @@ public void OnNetworkError(IPEndPoint endPoint, SocketError error) Console.WriteLine("[Client] error! " + error); } - public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) + public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod) { } @@ -50,7 +51,7 @@ public void OnNetworkLatencyUpdate(NetPeer peer, int latency) public void OnConnectionRequest(ConnectionRequest request) { - request.AcceptIfKey("key"); + request.Reject(); } } @@ -58,19 +59,21 @@ private class ServerListener : INetEventListener { public NetManager Server; + private readonly List _peersList = new List(); + public void OnPeerConnected(NetPeer peer) { - Console.WriteLine("[Server] Peer connected: " + peer.EndPoint); - var peers = Server.GetPeers(ConnectionState.Connected); - foreach (var netPeer in peers) + Console.WriteLine("[Server] Peer connected: " + peer); + Server.GetConnectedPeers(_peersList); + foreach (var netPeer in _peersList) { - Console.WriteLine("ConnectedPeersList: id={0}, ep={1}", netPeer.Id, netPeer.EndPoint); + Console.WriteLine("ConnectedPeersList: id={0}, ep={1}", netPeer.Id, netPeer); } } public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { - Console.WriteLine("[Server] Peer disconnected: " + peer.EndPoint + ", reason: " + disconnectInfo.Reason); + Console.WriteLine("[Server] Peer disconnected: " + peer + ", reason: " + disconnectInfo.Reason); } public void OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) @@ -78,7 +81,7 @@ public void OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) Console.WriteLine("[Server] error: " + socketErrorCode); } - public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) + public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { } @@ -86,9 +89,9 @@ public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMetho public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { Console.WriteLine("[Server] ReceiveUnconnected {0}. From: {1}. Data: {2}", messageType, remoteEndPoint, reader.GetString(100)); - NetDataWriter wrtier = new NetDataWriter(); - wrtier.Put("SERVER DISCOVERY RESPONSE"); - Server.SendUnconnectedMessage(wrtier, remoteEndPoint); + NetDataWriter writer = new NetDataWriter(); + writer.Put("SERVER DISCOVERY RESPONSE"); + Server.SendUnconnectedMessage(writer, remoteEndPoint); } public void OnNetworkLatencyUpdate(NetPeer peer, int latency) @@ -98,7 +101,7 @@ public void OnNetworkLatencyUpdate(NetPeer peer, int latency) public void OnConnectionRequest(ConnectionRequest request) { - + request.AcceptIfKey("key"); } } @@ -112,8 +115,11 @@ public void Run() //Server _serverListener = new ServerListener(); - NetManager server = new NetManager(_serverListener); - server.BroadcastReceiveEnabled = true; + NetManager server = new NetManager(_serverListener) + { + BroadcastReceiveEnabled = true, + IPv6Enabled = true + }; if (!server.Start(9050)) { Console.WriteLine("Server start failed"); @@ -125,10 +131,14 @@ public void Run() //Client _clientListener1 = new ClientListener(); - NetManager client1 = new NetManager(_clientListener1); + NetManager client1 = new NetManager(_clientListener1) + { + UnconnectedMessagesEnabled = true, + SimulateLatency = true, + SimulationMaxLatency = 1500, + IPv6Enabled = true + }; _clientListener1.Client = client1; - client1.SimulateLatency = true; - client1.SimulationMaxLatency = 1500; if (!client1.Start()) { Console.WriteLine("Client1 start failed"); @@ -137,10 +147,15 @@ public void Run() } _clientListener2 = new ClientListener(); - NetManager client2 = new NetManager(_clientListener2); + NetManager client2 = new NetManager(_clientListener2) + { + UnconnectedMessagesEnabled = true, + SimulateLatency = true, + SimulationMaxLatency = 1500, + IPv6Enabled = true + }; + _clientListener2.Client = client2; - client2.SimulateLatency = true; - client2.SimulationMaxLatency = 1500; client2.Start(); //Send broadcast diff --git a/LibSample/EchoMessagesTest.cs b/LibSample/EchoMessagesTest.cs index 5adb0755..7a8dc9b9 100644 --- a/LibSample/EchoMessagesTest.cs +++ b/LibSample/EchoMessagesTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Net.Sockets; using System.Threading; @@ -7,7 +7,7 @@ namespace LibSample { - class EchoMessagesTest + class EchoMessagesTest : IExample { private static int _messagesReceivedCount = 0; @@ -15,7 +15,7 @@ private class ClientListener : INetEventListener { public void OnPeerConnected(NetPeer peer) { - Console.WriteLine("[Client] connected to: {0}:{1}", peer.EndPoint.Address, peer.EndPoint.Port); + Console.WriteLine("[Client] connected to: {0}:{1}", peer.Address, peer.Port); NetDataWriter dataWriter = new NetDataWriter(); for (int i = 0; i < 5; i++) @@ -63,13 +63,13 @@ public void OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) Console.WriteLine("[Client] error! " + socketErrorCode); } - public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) + public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { if (reader.AvailableBytes == 13218) { - Console.WriteLine("[{0}] TestFrag: {1}, {2}", - peer.NetManager.LocalPort, - reader.RawData[reader.UserDataOffset], + Console.WriteLine("[{0}] TestFrag: {1}, {2}", + peer.NetManager.LocalPort, + reader.RawData[reader.UserDataOffset], reader.RawData[reader.UserDataOffset + 13217]); } else @@ -88,7 +88,7 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead public void OnNetworkLatencyUpdate(NetPeer peer, int latency) { - + } public void OnConnectionRequest(ConnectionRequest request) @@ -103,17 +103,17 @@ private class ServerListener : INetEventListener public void OnPeerConnected(NetPeer peer) { - Console.WriteLine("[Server] Peer connected: " + peer.EndPoint); - var peers = Server.GetPeers(ConnectionState.Connected); - foreach (var netPeer in peers) + Console.WriteLine("[Server] Peer connected: " + peer); + foreach (var netPeer in Server) { - Console.WriteLine("ConnectedPeersList: id={0}, ep={1}", netPeer.Id, netPeer.EndPoint); + if(netPeer.ConnectionState == ConnectionState.Connected) + Console.WriteLine("ConnectedPeersList: id={0}, ep={1}", netPeer.Id, netPeer); } } public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { - Console.WriteLine("[Server] Peer disconnected: " + peer.EndPoint + ", reason: " + disconnectInfo.Reason); + Console.WriteLine("[Server] Peer disconnected: " + peer + ", reason: " + disconnectInfo.Reason); } public void OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) @@ -121,7 +121,7 @@ public void OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) Console.WriteLine("[Server] error: " + socketErrorCode); } - public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) + public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { //echo peer.Send(reader.GetRemainingBytes(), deliveryMethod); @@ -165,6 +165,7 @@ public void Run() _serverListener = new ServerListener(); NetManager server = new NetManager(_serverListener); + if (!server.Start(Port)) { Console.WriteLine("Server start failed"); @@ -194,7 +195,7 @@ public void Run() //SimulateLatency = true, SimulationMaxLatency = 1500 }; - + client2.Start(); client2.Connect("::1", Port, "gamekey"); @@ -210,10 +211,10 @@ public void Run() client2.Stop(); server.Stop(); Console.ReadKey(); - Console.WriteLine("ServStats:\n BytesReceived: {0}\n PacketsReceived: {1}\n BytesSent: {2}\n PacketsSent: {3}", - server.Statistics.BytesReceived, - server.Statistics.PacketsReceived, - server.Statistics.BytesSent, + Console.WriteLine("ServStats:\n BytesReceived: {0}\n PacketsReceived: {1}\n BytesSent: {2}\n PacketsSent: {3}", + server.Statistics.BytesReceived, + server.Statistics.PacketsReceived, + server.Statistics.BytesSent, server.Statistics.PacketsSent); Console.WriteLine("Client1Stats:\n BytesReceived: {0}\n PacketsReceived: {1}\n BytesSent: {2}\n PacketsSent: {3}", client1.Statistics.BytesReceived, diff --git a/LibSample/GithubSample.cs b/LibSample/GithubSample.cs index 7b570195..0a680449 100644 --- a/LibSample/GithubSample.cs +++ b/LibSample/GithubSample.cs @@ -15,7 +15,7 @@ public void Server() listener.ConnectionRequestEvent += request => { - if(server.PeersCount < 10 /* max connections */) + if(server.ConnectedPeersCount < 10 /* max connections */) request.AcceptIfKey("SomeConnectionKey"); else request.Reject(); @@ -23,10 +23,10 @@ public void Server() listener.PeerConnectedEvent += peer => { - Console.WriteLine("We got connection: {0}", peer.EndPoint); // Show peer ip - NetDataWriter writer = new NetDataWriter(); // Create writer class - writer.Put("Hello client!"); // Put some string - peer.Send(writer, DeliveryMethod.ReliableOrdered); // Send with reliability + Console.WriteLine("We got connection: {0}", peer); // Show peer ip + NetDataWriter writer = new NetDataWriter(); // Create writer class + writer.Put("Hello client!"); // Put some string + peer.Send(writer, DeliveryMethod.ReliableOrdered); // Send with reliability }; while (!Console.KeyAvailable) @@ -44,7 +44,7 @@ public void Client() NetManager client = new NetManager(listener); client.Start(); client.Connect("localhost" /* host ip or name */, 9050 /* port */, "SomeConnectionKey" /* text key or NetDataWriter */); - listener.NetworkReceiveEvent += (fromPeer, dataReader, deliveryMethod) => + listener.NetworkReceiveEvent += (fromPeer, dataReader, channel, deliveryMethod) => { Console.WriteLine("We got: {0}", dataReader.GetString(100 /* max length of string */)); dataReader.Recycle(); diff --git a/LibSample/HolePunchServerTest.cs b/LibSample/HolePunchServerTest.cs index 0a4f81a0..d7564d56 100644 --- a/LibSample/HolePunchServerTest.cs +++ b/LibSample/HolePunchServerTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Net; using System.Threading; @@ -8,13 +8,13 @@ namespace LibSample { class WaitPeer { - public IPEndPoint InternalAddr { get; private set; } - public IPEndPoint ExternalAddr { get; private set; } + public IPEndPoint InternalAddr { get; } + public IPEndPoint ExternalAddr { get; } public DateTime RefreshTime { get; private set; } public void Refresh() { - RefreshTime = DateTime.Now; + RefreshTime = DateTime.UtcNow; } public WaitPeer(IPEndPoint internalAddr, IPEndPoint externalAddr) @@ -25,7 +25,7 @@ public WaitPeer(IPEndPoint internalAddr, IPEndPoint externalAddr) } } - class HolePunchServerTest : INatPunchListener + class HolePunchServerTest : IExample, INatPunchListener { private const int ServerPort = 50010; private const string ConnectionKey = "test_key"; @@ -39,8 +39,7 @@ class HolePunchServerTest : INatPunchListener void INatPunchListener.OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) { - WaitPeer wpeer; - if (_waitingPeers.TryGetValue(token, out wpeer)) + if (_waitingPeers.TryGetValue(token, out var wpeer)) { if (wpeer.InternalAddr.Equals(localEndPoint) && wpeer.ExternalAddr.Equals(remoteEndPoint)) @@ -77,7 +76,7 @@ void INatPunchListener.OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndP } } - void INatPunchListener.OnNatIntroductionSuccess(IPEndPoint targetEndPoint, string token) + void INatPunchListener.OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) { //Ignore we are server } @@ -85,22 +84,22 @@ void INatPunchListener.OnNatIntroductionSuccess(IPEndPoint targetEndPoint, strin public void Run() { Console.WriteLine("=== HolePunch Test ==="); - - EventBasedNetListener netListener = new EventBasedNetListener(); + + EventBasedNetListener clientListener = new EventBasedNetListener(); EventBasedNatPunchListener natPunchListener1 = new EventBasedNatPunchListener(); EventBasedNatPunchListener natPunchListener2 = new EventBasedNatPunchListener(); - netListener.PeerConnectedEvent += peer => + clientListener.PeerConnectedEvent += peer => { - Console.WriteLine("PeerConnected: " + peer.EndPoint.ToString()); + Console.WriteLine("PeerConnected: " + peer); }; - netListener.ConnectionRequestEvent += request => + clientListener.ConnectionRequestEvent += request => { request.AcceptIfKey(ConnectionKey); }; - netListener.PeerDisconnectedEvent += (peer, disconnectInfo) => + clientListener.PeerDisconnectedEvent += (peer, disconnectInfo) => { Console.WriteLine("PeerDisconnected: " + disconnectInfo.Reason); if (disconnectInfo.AdditionalData.AvailableBytes > 0) @@ -109,35 +108,44 @@ public void Run() } }; - natPunchListener1.NatIntroductionSuccess += (point, token) => + natPunchListener1.NatIntroductionSuccess += (point, addrType, token) => { var peer = _c1.Connect(point, ConnectionKey); - Console.WriteLine("Success C1. Connecting to C2: {0}, connection created: {1}", point, peer != null); + Console.WriteLine($"NatIntroductionSuccess C1. Connecting to C2: {point}, type: {addrType}, connection created: {peer != null}"); }; - natPunchListener2.NatIntroductionSuccess += (point, token) => + natPunchListener2.NatIntroductionSuccess += (point, addrType, token) => { var peer = _c2.Connect(point, ConnectionKey); - Console.WriteLine("Success C2. Connecting to C1: {0}, connection created: {1}", point, peer != null); + Console.WriteLine($"NatIntroductionSuccess C2. Connecting to C1: {point}, type: {addrType}, connection created: {peer != null}"); }; - _c1 = new NetManager(netListener); - _c1.NatPunchEnabled = true; + _c1 = new NetManager(clientListener) + { + IPv6Enabled = true, + NatPunchEnabled = true + }; _c1.NatPunchModule.Init(natPunchListener1); _c1.Start(); - _c2 = new NetManager(netListener); - _c2.NatPunchEnabled = true; + _c2 = new NetManager(clientListener) + { + IPv6Enabled = true, + NatPunchEnabled = true + }; _c2.NatPunchModule.Init(natPunchListener2); _c2.Start(); - _puncher = new NetManager(netListener); + _puncher = new NetManager(clientListener) + { + IPv6Enabled = true, + NatPunchEnabled = true + }; _puncher.Start(ServerPort); - _puncher.NatPunchEnabled = true; _puncher.NatPunchModule.Init(this); - _c1.NatPunchModule.SendNatIntroduceRequest(NetUtils.MakeEndPoint("::1", ServerPort), "token1"); - _c2.NatPunchModule.SendNatIntroduceRequest(NetUtils.MakeEndPoint("::1", ServerPort), "token1"); + _c1.NatPunchModule.SendNatIntroduceRequest("localhost", ServerPort, "token1"); + _c2.NatPunchModule.SendNatIntroduceRequest("localhost", ServerPort, "token1"); // keep going until ESCAPE is pressed Console.WriteLine("Press ESC to quit"); @@ -158,8 +166,8 @@ public void Run() _c1.Stop(); } } - - DateTime nowTime = DateTime.Now; + + DateTime nowTime = DateTime.UtcNow; _c1.NatPunchModule.PollEvents(); _c2.NatPunchModule.PollEvents(); diff --git a/LibSample/IExample.cs b/LibSample/IExample.cs new file mode 100644 index 00000000..2ed2afdc --- /dev/null +++ b/LibSample/IExample.cs @@ -0,0 +1,7 @@ +namespace LibSample +{ + public interface IExample + { + void Run(); + } +} diff --git a/LibSample/LibSample.csproj b/LibSample/LibSample.csproj index a2a5c6fc..f1d24079 100644 --- a/LibSample/LibSample.csproj +++ b/LibSample/LibSample.csproj @@ -1,66 +1,12 @@ - - - + + - Debug - AnyCPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699} Exe - Properties - LibSample - LibSample - v4.5 - 512 - true - + net8.0;net9.0 - - AnyCPU - true - full - false - bin\Debug\ - TRACE;DEBUG - prompt - 4 - false - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - - - - - - - - - - - - + - - {2f98f12c-b72a-48fe-a7e5-6d8b0c5abacb} - LiteNetLib - + - - - \ No newline at end of file + + diff --git a/LibSample/NtpTest.cs b/LibSample/NtpTest.cs new file mode 100644 index 00000000..8c7f73f7 --- /dev/null +++ b/LibSample/NtpTest.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using LiteNetLib; + +namespace LibSample +{ + public class NtpTest : IExample + { + public void Run() + { + RequestNtpService("0.pool.ntp.org"); + } + + private void RequestNtpService(string ntpService) + { + Console.WriteLine($"Request time from \"{ntpService}\" service"); + var ntpComplete = false; + EventBasedNetListener listener = new EventBasedNetListener(); + listener.NtpResponseEvent += ntpPacket => + { + if (ntpPacket != null) + Console.WriteLine("[MAIN] NTP time test offset: " + ntpPacket.CorrectionOffset); + else + Console.WriteLine("[MAIN] NTP time error"); + ntpComplete = true; + }; + + NetManager nm = new NetManager(listener); + nm.Start(); + nm.CreateNtpRequest(ntpService); + + while (!ntpComplete) + { + Thread.Yield(); + } + } + } +} diff --git a/LibSample/PacketProcessorExample.cs b/LibSample/PacketProcessorExample.cs index 57da9c1a..46ad36ee 100644 --- a/LibSample/PacketProcessorExample.cs +++ b/LibSample/PacketProcessorExample.cs @@ -1,51 +1,87 @@ using System; +using System.Collections.Generic; using System.Threading; using LiteNetLib; using LiteNetLib.Utils; namespace LibSample { + struct CustomStruct : INetSerializable + { + public int X; + public int Y; + + public void Serialize(NetDataWriter writer) + { + writer.Put(X); + writer.Put(Y); + } + + public void Deserialize(NetDataReader reader) + { + X = reader.GetInt(); + Y = reader.GetInt(); + } + } + class ArgumentsForLogin { public string UserId { get; set; } public string Password { get; set; } public int SomeInt { get; set; } + public List SomeList { get; set; } } - class PacketProcessorExample + class PacketProcessorExample : IExample { private readonly NetPacketProcessor _netPacketProcessor = new NetPacketProcessor(); private NetManager _client; private NetManager _server; + private NetDataWriter _writer = new NetDataWriter(); public void Run() { //setup netpacketprocessor + _netPacketProcessor.RegisterNestedType(); _netPacketProcessor.SubscribeReusable(Method1); //setup events EventBasedNetListener clientListener = new EventBasedNetListener(); EventBasedNetListener serverListener = new EventBasedNetListener(); - serverListener.ConnectionRequestEvent += request => request.AcceptIfKey("key"); + serverListener.ConnectionRequestEvent += request => + { + request.AcceptIfKey("key"); + }; serverListener.NetworkReceiveEvent += - (peer, reader, method) => _netPacketProcessor.ReadAllPackets(reader, peer); + (peer, reader, channel, method) => + { + _netPacketProcessor.ReadAllPackets(reader, peer); + }; + clientListener.PeerConnectedEvent += peer => + { + //send after connect + var testList = new List + { + new CustomStruct {X = 1, Y = -1}, + new CustomStruct {X = 5, Y = -28}, + new CustomStruct {X = -114, Y = 65535} + }; + _writer.Reset(); + _netPacketProcessor.Write(_writer, new ArgumentsForLogin { Password = "pass", SomeInt = 5, UserId = "someUser", SomeList = testList }); + peer.Send(_writer, DeliveryMethod.ReliableOrdered); + }; //start client/server _client = new NetManager(clientListener); _server = new NetManager(serverListener); _client.Start(); _server.Start(9050); - var clientPeer = _client.Connect("localhost", 9050, "key"); - - //send - _netPacketProcessor.Send( - clientPeer, - new ArgumentsForLogin { Password = "pass", SomeInt = 5, UserId = "someUser"}, - DeliveryMethod.ReliableOrdered); + _client.Connect("localhost", 9050, "key"); while (!Console.KeyAvailable) { _server.PollEvents(); + _client.PollEvents(); Thread.Sleep(10); } _client.Stop(); @@ -55,6 +91,11 @@ public void Run() void Method1(ArgumentsForLogin args, NetPeer peer) { Console.WriteLine("Received: \n {0}\n {1}\n {2}", args.UserId, args.Password, args.SomeInt); + Console.WriteLine("List count: " + args.SomeList.Count); + for (int i = 0; i < args.SomeList.Count; i++) + { + Console.WriteLine($" X: {args.SomeList[i].X}, Y: {args.SomeList[i].Y}"); + } } } } diff --git a/LibSample/Program.cs b/LibSample/Program.cs index 80d00aee..87149fad 100644 --- a/LibSample/Program.cs +++ b/LibSample/Program.cs @@ -1,32 +1,106 @@ -using System; -using LiteNetLib.Utils; +using System; +using System.Text; namespace LibSample { - class Program + public static class Program { + private static readonly IExample[] Examples = { + new EchoMessagesTest(), + new HolePunchServerTest(), + new BroadcastTest(), + new SerializerBenchmark(), + new SpeedBench(), + new PacketProcessorExample(), + new AesEncryptionTest(), + new NtpTest(), + }; + static void Main(string[] args) { - //Test ntp - NtpRequest ntpRequest = null; - ntpRequest = NtpRequest.Create("pool.ntp.org", ntpPacket => + AppendExampleMenu(MenuStringBuilder); + WriteAndClean(MenuStringBuilder); + + do { - ntpRequest.Close(); - if (ntpPacket != null) - Console.WriteLine("[MAIN] NTP time test offset: " + ntpPacket.CorrectionOffset); + Console.Write("Write command: "); + var input = Console.ReadLine(); + + if (input != null) + { + var lcInput = input.ToLower(); + + if (lcInput == "help" || lcInput == "h") + { + AppendFullHelpMenu(MenuStringBuilder); + WriteAndClean(MenuStringBuilder); + continue; + } + + if (lcInput == "quit" || lcInput == "exit" || lcInput == "q" || lcInput == "e") + { + break; + } + + if (int.TryParse(input, out var optionKey)) + { + if (optionKey < 0 || optionKey >= Examples.Length) + { + PrintInvalidCommand(input); + continue; + } + + ((IExample)Activator.CreateInstance(Examples[optionKey].GetType())).Run(); + } + else + { + PrintInvalidCommand(input); + } + } else - Console.WriteLine("[MAIN] NTP time error"); - }); - ntpRequest.Send(); - - new EchoMessagesTest().Run(); - //new HolePunchServerTest().Run(); - //new BroadcastTest().Run(); - //new BenchmarkTest.TestHost().Run(); - //new SerializerBenchmark().Run(); - //new SpeedBench().Run(); - //new PacketProcessorExample().Run(); + { + PrintInvalidCommand(string.Empty); + } + } while (true); + } + + private static void PrintInvalidCommand(string invalidInput) + { + AppendInvalidCommand(MenuStringBuilder, invalidInput); + WriteAndClean(MenuStringBuilder); + } + + private static readonly StringBuilder MenuStringBuilder = new StringBuilder(); + + private static void WriteAndClean(StringBuilder sb) + { + Console.WriteLine(sb.ToString()); + sb.Clear(); + } + + private static void AppendInvalidCommand(StringBuilder sb, string invalidInput) + { + sb.Append("Invalid input \""); + sb.Append(string.IsNullOrWhiteSpace(invalidInput) ? "[Whitespace/Empty Line]" : invalidInput); + sb.AppendLine("\" command. Write \"help\" command for more information."); + } + + private static void AppendFullHelpMenu(StringBuilder sb) + { + sb.AppendLine(); + sb.AppendLine("\"help/h\" - write helper text for this console menu."); + sb.AppendLine("\"exit/e/quit/q\" - close app"); + AppendExampleMenu(sb); + sb.AppendLine(); + } + + private static void AppendExampleMenu(StringBuilder sb) + { + for (var i = 0; i < Examples.Length; i++) + { + var example = Examples[i]; + sb.AppendLine($"\"{i}\" - Example of {example.GetType().Name}"); + } } } } - diff --git a/LibSample/Properties/AssemblyInfo.cs b/LibSample/Properties/AssemblyInfo.cs deleted file mode 100644 index 9c9f10b8..00000000 --- a/LibSample/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("LibSample")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("LibSample")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("ef232cbe-04c8-4fd9-ae26-2f7ebbe81699")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/LibSample/SerializerBenchmark.cs b/LibSample/SerializerBenchmark.cs index f9ea1f49..31034ebb 100644 --- a/LibSample/SerializerBenchmark.cs +++ b/LibSample/SerializerBenchmark.cs @@ -7,8 +7,10 @@ namespace LibSample { - class SerializerBenchmark + class SerializerBenchmark : IExample { + const int LoopLength = 100000; + [Serializable] //Just for test binary formatter private struct SampleNetSerializable : INetSerializable { @@ -90,14 +92,45 @@ public static SomeVector2 Deserialize(NetDataReader reader) } } + private void NetSerializerTest(NetSerializer serializer, NetDataWriter netDataWriter, Stopwatch stopwatch, SamplePacket samplePacket) + { + netDataWriter.Reset(); + stopwatch.Restart(); + for (int i = 0; i < LoopLength; i++) + serializer.Serialize(netDataWriter, samplePacket); + stopwatch.Stop(); + Console.WriteLine($"NetSerializer time: {stopwatch.ElapsedMilliseconds} ms, size: { netDataWriter.Length / LoopLength} bytes"); + } + + private void DataWriterTest(NetDataWriter netDataWriter, Stopwatch stopwatch, SamplePacket samplePacket) + { + netDataWriter.Reset(); + stopwatch.Restart(); + for (int i = 0; i < LoopLength; i++) + { + netDataWriter.Put(samplePacket.SomeString); + netDataWriter.Put(samplePacket.SomeFloat); + netDataWriter.PutArray(samplePacket.SomeIntArray); + SomeVector2.Serialize(netDataWriter, samplePacket.SomeVector2); + netDataWriter.Put((ushort)samplePacket.SomeVectors.Length); + for (int j = 0; j < samplePacket.SomeVectors.Length; j++) + { + SomeVector2.Serialize(netDataWriter, samplePacket.SomeVectors[j]); + } + netDataWriter.Put(samplePacket.EmptyString); + netDataWriter.Put(samplePacket.TestObj); + } + stopwatch.Stop(); + Console.WriteLine($"Raw time: {stopwatch.ElapsedMilliseconds} ms, size: { netDataWriter.Length / LoopLength} bytes"); + } + public void Run() { Console.WriteLine("=== Serializer benchmark ==="); - - const int LoopLength = 100000; + + //Test serializer performance Stopwatch stopwatch = new Stopwatch(); - BinaryFormatter binaryFormatter = new BinaryFormatter(); MemoryStream memoryStream = new MemoryStream(); NetDataWriter netDataWriter = new NetDataWriter(); @@ -114,7 +147,7 @@ public void Run() samplePacket.SomeVectors[i] = new SomeVector2(i, i); } - NetSerializer netSerializer = new NetSerializer(); + var netSerializer = new NetSerializer(); netSerializer.RegisterNestedType(); netSerializer.RegisterNestedType( SomeVector2.Serialize, SomeVector2.Deserialize ); @@ -124,55 +157,16 @@ public void Run() double c = Math.Sin(i); } - //Test binary formatter - stopwatch.Start(); - for (int i = 0; i < LoopLength; i++) - { - binaryFormatter.Serialize(memoryStream, samplePacket); - } - stopwatch.Stop(); - Console.WriteLine("BinaryFormatter time: " + stopwatch.ElapsedMilliseconds + " ms"); + DataWriterTest(netDataWriter, stopwatch, samplePacket); + NetSerializerTest(netSerializer, netDataWriter, stopwatch, samplePacket); - //Test NetSerializer - stopwatch.Restart(); - for (int i = 0; i < LoopLength; i++) - { - netSerializer.Serialize(netDataWriter, samplePacket); - } - stopwatch.Stop(); - Console.WriteLine("NetSerializer first run time: " + stopwatch.ElapsedMilliseconds + " ms"); + DataWriterTest(netDataWriter, stopwatch, samplePacket); + NetSerializerTest(netSerializer, netDataWriter, stopwatch, samplePacket); - //Test NetSerializer - netDataWriter.Reset(); - stopwatch.Restart(); - for (int i = 0; i < LoopLength; i++) - { - netSerializer.Serialize(netDataWriter, samplePacket); - } - stopwatch.Stop(); - Console.WriteLine("NetSerializer second run time: " + stopwatch.ElapsedMilliseconds + " ms"); + DataWriterTest(netDataWriter, stopwatch, samplePacket); + NetSerializerTest(netSerializer, netDataWriter, stopwatch, samplePacket); - //Test RAW - netDataWriter.Reset(); - stopwatch.Restart(); - for (int i = 0; i < LoopLength; i++) - { - netDataWriter.Put(samplePacket.SomeFloat); - netDataWriter.Put(samplePacket.SomeString); - netDataWriter.PutArray(samplePacket.SomeIntArray); - netDataWriter.Put(samplePacket.SomeVector2.X); - netDataWriter.Put(samplePacket.SomeVector2.Y); - netDataWriter.Put(samplePacket.SomeVectors.Length); - for (int j = 0; j < samplePacket.SomeVectors.Length; j++) - { - netDataWriter.Put(samplePacket.SomeVectors[j].X); - netDataWriter.Put(samplePacket.SomeVectors[j].Y); - } - netDataWriter.Put(samplePacket.EmptyString); - netDataWriter.Put(samplePacket.TestObj.Value); - } - stopwatch.Stop(); - Console.WriteLine("DataWriter (raw put method calls) time: " + stopwatch.ElapsedMilliseconds + " ms"); + Console.ReadKey(); } } } diff --git a/LibSample/SpeedBench.cs b/LibSample/SpeedBench.cs index e34512ec..09dcb5f4 100644 --- a/LibSample/SpeedBench.cs +++ b/LibSample/SpeedBench.cs @@ -8,32 +8,34 @@ namespace LibSample { - internal class SpeedBench + internal class SpeedBench : IExample { - public class Server : INetEventListener + public class Server : INetEventListener, IDisposable { public int ReliableReceived; public int UnreliableReceived; readonly NetManager _server; + public NetStatistics Stats => _server.Statistics; + public Server() { - _server = new NetManager(this); - _server.AutoRecycle = true; - _server.UpdateTime = 1; - _server.SimulatePacketLoss = false; - _server.SimulationPacketLossChance = 20; + _server = new NetManager(this) + { + AutoRecycle = true, + UpdateTime = 1, + SimulatePacketLoss = false, + SimulationPacketLossChance = 20, + EnableStatistics = true, + UnsyncedEvents = true + }; _server.Start(9050); } - public void PollEvents() - { - _server.PollEvents(); - } - void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) { + Console.WriteLine($"Server: error: {socketErrorCode}"); } void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) @@ -45,7 +47,7 @@ public void OnConnectionRequest(ConnectionRequest request) request.AcceptIfKey("ConnKey"); } - void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) + void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { var isReliable = reader.GetBool(); var data = reader.GetString(); @@ -67,15 +69,21 @@ void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, Ne void INetEventListener.OnPeerConnected(NetPeer peer) { - + Console.WriteLine($"Server: client connected: {peer}"); } void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { + Console.WriteLine($"Server: client disconnected: {disconnectInfo.Reason}"); + } + + public void Dispose() + { + _server.Stop(); } } - public class Client : INetEventListener + public class Client : INetEventListener, IDisposable { public int ReliableSent; public int UnreliableSent; @@ -84,20 +92,20 @@ public class Client : INetEventListener readonly NetDataWriter _writer; NetPeer _peer; - public NetStatistics Stats - { - get { return _client.Statistics; } - } + public NetStatistics Stats => _client.Statistics; public Client() { _writer = new NetDataWriter(); - _client = new NetManager(this); - _client.UnsyncedEvents = true; - _client.AutoRecycle = true; - _client.SimulatePacketLoss = false; - _client.SimulationPacketLossChance = 20; + _client = new NetManager(this) + { + UnsyncedEvents = true, + AutoRecycle = true, + SimulatePacketLoss = false, + SimulationPacketLossChance = 20, + EnableStatistics = true + }; _client.Start(); } @@ -106,7 +114,6 @@ public void SendUnreliable(string pData) _writer.Reset(); _writer.Put(false); _writer.Put(pData); - _peer.Send(_writer, DeliveryMethod.Unreliable); UnreliableSent++; } @@ -116,7 +123,6 @@ public void SendReliable(string pData) _writer.Reset(); _writer.Put(true); _writer.Put(pData); - _peer.Send(_writer, DeliveryMethod.ReliableOrdered); ReliableSent++; } @@ -128,6 +134,7 @@ public void Connect() void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) { + Console.WriteLine($"Client: error: {socketErrorCode}"); } void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) @@ -136,10 +143,10 @@ void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) public void OnConnectionRequest(ConnectionRequest request) { - request.AcceptIfKey("ConnKey"); + request.RejectForce(); } - void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) + void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { } @@ -151,17 +158,35 @@ void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, Ne void INetEventListener.OnPeerConnected(NetPeer peer) { + Console.WriteLine("Client: Connected"); + for (int i = 0; i < MAX_LOOP_COUNT; i++) + { + for (int ui = 0; ui < UNRELIABLE_MESSAGES_PER_LOOP; ui++) + SendUnreliable(DATA); + + for (int ri = 0; ri < RELIABLE_MESSAGES_PER_LOOP; ri++) + SendReliable(DATA); + } + + int dataSize = MAX_LOOP_COUNT * Encoding.UTF8.GetByteCount(DATA) * (UNRELIABLE_MESSAGES_PER_LOOP + RELIABLE_MESSAGES_PER_LOOP); + Console.WriteLine("DataSize: {0}b, {1}kb, {2}mb", dataSize, dataSize / 1024, dataSize / 1024 / 1024); } void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { + Console.WriteLine($"Client: Disconnected {disconnectInfo.Reason}"); + } + + public void Dispose() + { + _client.Stop(); } } private const string DATA = "The quick brown fox jumps over the lazy dog"; - private static int MAX_LOOP_COUNT = 750; - private static int UNRELIABLE_MESSAGES_PER_LOOP = 1000; - private static int RELIABLE_MESSAGES_PER_LOOP = 350; - private static bool CLIENT_RUNNING = true; + private const int MAX_LOOP_COUNT = 750; + private const int UNRELIABLE_MESSAGES_PER_LOOP = 1000; + private const int RELIABLE_MESSAGES_PER_LOOP = 350; + private static volatile bool CLIENT_RUNNING = true; public void Run() { @@ -171,46 +196,33 @@ public void Run() Thread clientThread = new Thread(StartClient); clientThread.Start(); Console.WriteLine("Processing..."); - Console.ReadKey(); + serverThread.Join(); + clientThread.Join(); + Console.WriteLine("Test is end."); } private static void StartServer() { - Server s = new Server(); - - while (CLIENT_RUNNING) + using (Server s = new Server()) { - s.PollEvents(); - Thread.Sleep(1); - } + while (CLIENT_RUNNING) + Thread.Sleep(100); - Thread.Sleep(10000); - s.PollEvents(); - Console.WriteLine("SERVER RECEIVED -> Reliable: " + s.ReliableReceived + ", Unreliable: " + s.UnreliableReceived); + Console.WriteLine("SERVER RECEIVED -> Reliable: " + s.ReliableReceived + ", Unreliable: " + s.UnreliableReceived); + Console.WriteLine("SERVER STATS:\n" + s.Stats); + } } private static void StartClient() { - Client c = new Client(); - c.Connect(); - - for (int i = 0; i < MAX_LOOP_COUNT; i++) + using (Client c = new Client()) { - //for (int ui = 0; ui < UNRELIABLE_MESSAGES_PER_LOOP; ui++) - // c.SendUnreliable(DATA); - - for (int ri = 0; ri < RELIABLE_MESSAGES_PER_LOOP; ri++) - c.SendReliable(DATA); + c.Connect(); + Thread.Sleep(10000); + CLIENT_RUNNING = false; + Console.WriteLine("CLIENT SENT -> Reliable: " + c.ReliableSent + ", Unreliable: " + c.UnreliableSent); + Console.WriteLine("CLIENT STATS:\n" + c.Stats); } - - int dataSize = MAX_LOOP_COUNT * Encoding.UTF8.GetByteCount(DATA) * (UNRELIABLE_MESSAGES_PER_LOOP + RELIABLE_MESSAGES_PER_LOOP); - Console.WriteLine("DataSize: {0}b, {1}kb, {2}mb", dataSize, dataSize/1024, dataSize/1024/1024); - - Thread.Sleep(10000); - CLIENT_RUNNING = false; - - Console.WriteLine("CLIENT SENT -> Reliable: " + c.ReliableSent + ", Unreliable: " + c.UnreliableSent); - Console.WriteLine("CLIENT STATS:\n" + c.Stats); } } } diff --git a/LiteNetLib.Tests/CRC32LayerTest.cs b/LiteNetLib.Tests/CRC32LayerTest.cs new file mode 100644 index 00000000..c6b716e6 --- /dev/null +++ b/LiteNetLib.Tests/CRC32LayerTest.cs @@ -0,0 +1,89 @@ +using LiteNetLib.Layers; +using LiteNetLib.Utils; +using NUnit.Framework; +using System; +using System.Net; + +namespace LiteNetLib.Tests +{ + [TestFixture] + public class CRC32LayerTest + { + private Crc32cLayer _crc32Layer; + private IPEndPoint _dummyEndpoint; + + [SetUp] + public void Setup() + { + NetDebug.Logger = null; + _crc32Layer = new Crc32cLayer(); + _dummyEndpoint = new IPEndPoint(IPAddress.Loopback, 23456); + } + + [Test] + public void ReturnsDataWithoutChecksum() + { + byte[] packet = GetTestPacketWithCrc(); + + int length = packet.Length; + _crc32Layer.ProcessInboundPacket(ref _dummyEndpoint, ref packet, ref length); + + Assert.That(length, Is.EqualTo(packet.Length - CRC32C.ChecksumSize)); + } + + [Test] + public void ReturnsNilCountForBadChecksum() + { + byte[] packet = GetTestPacketWithCrc(); + + //Fake a change to the data to cause data/crc missmatch + packet[4] = 0; + + int length = packet.Length; + _crc32Layer.ProcessInboundPacket(ref _dummyEndpoint, ref packet, ref length); + + Assert.That(length, Is.EqualTo(0)); + } + + [Test] + public void ReturnsNilCountForTooShortMessage() + { + byte[] packet = new byte[2]; + + int length = packet.Length; + + _crc32Layer.ProcessInboundPacket(ref _dummyEndpoint, ref packet, ref length); + + Assert.That(length, Is.EqualTo(0)); + } + + [Test] + public void CanSendAndReceiveSameMessage() + { + byte[] message = GetTestMessageBytes(); + //Process outbound adds bytes, so we need a larger array + byte[] package = new byte[message.Length + CRC32C.ChecksumSize]; + + Buffer.BlockCopy(message, 0, package, 0, message.Length); + + int offset = 0; + int length = message.Length; + _crc32Layer.ProcessOutBoundPacket(ref _dummyEndpoint, ref package, ref offset, ref length); + _crc32Layer.ProcessInboundPacket(ref _dummyEndpoint, ref package, ref length); + } + + private static byte[] GetTestPacketWithCrc() + { + byte[] testMsg = GetTestMessageBytes(); + uint crc32 = CRC32C.Compute(testMsg, 0, testMsg.Length); + + byte[] packet = new byte[testMsg.Length + CRC32C.ChecksumSize]; + Buffer.BlockCopy(testMsg, 0, packet, 0, testMsg.Length); + FastBitConverter.GetBytes(packet, testMsg.Length, crc32); + return packet; + } + + private static byte[] GetTestMessageBytes() + => System.Text.Encoding.ASCII.GetBytes("This is a test string with some length"); + } +} diff --git a/LiteNetLib.Tests/CommunicationTest.cs b/LiteNetLib.Tests/CommunicationTest.cs new file mode 100644 index 00000000..fcc8224f --- /dev/null +++ b/LiteNetLib.Tests/CommunicationTest.cs @@ -0,0 +1,751 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using LiteNetLib.Layers; +using LiteNetLib.Tests.TestUtility; +using LiteNetLib.Utils; + +using NUnit.Framework; + +namespace LiteNetLib.Tests +{ + [TestFixture] + [Category("Communication")] + public class CommunicationTest + { + const int TestTimeout = 4000; + [SetUp] + public void Init() + { + NetDebug.Logger = new LibErrorChecker(); + ManagerStack = new NetManagerStack(DefaultAppKey, DefaultPort); + } + + [TearDown] + public void TearDown() + { + ManagerStack?.Dispose(); + } + + private static readonly int DefaultPort = TestPorts.ForFramework(9050); + private static readonly int ReconnectPort = TestPorts.ForFramework(10123); + private static readonly int ReuseClientPort = TestPorts.ForFramework(9049); + private const string DefaultAppKey = "test_server"; + private static readonly byte[] DefaultAppKeyBytes = [12, 0, 116, 101, 115, 116, 95, 115, 101, 114, 118, 101, 114]; + + public NetManagerStack ManagerStack { get; set; } + + [Test, CancelAfter(TestTimeout)] + public void ConnectionByIpV4() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void P2PConnect() + { + var client1 = ManagerStack.Client(1); + var client2 = ManagerStack.Client(2); + + client1.Connect("127.0.0.1", client2.LocalPort, DefaultAppKey); + client2.Connect("127.0.0.1", client1.LocalPort, DefaultAppKey); + + while (client1.ConnectedPeersCount != 1 || client2.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + client1.PollEvents(); + client2.PollEvents(); + } + + Assert.That(client1.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client2.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void P2PConnectWithSpan() + { + var client1 = ManagerStack.Client(1); + var client2 = ManagerStack.Client(2); + + IPEndPoint endPoint1 = new IPEndPoint(IPAddress.Loopback, client2.LocalPort); + IPEndPoint endPoint2 = new IPEndPoint(IPAddress.Loopback, client1.LocalPort); + client1.Connect(endPoint1, DefaultAppKeyBytes.AsSpan()); + client2.Connect(endPoint2, DefaultAppKeyBytes.AsSpan()); + + while (client1.ConnectedPeersCount != 1 || client2.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + client1.PollEvents(); + client2.PollEvents(); + } + + Assert.That(client1.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client2.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void ConnectionByIpV4Unsynced() + { + var server = ManagerStack.Server(1); + server.UnsyncedEvents = true; + var client = ManagerStack.Client(1); + client.UnsyncedEvents = true; + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void DeliveryTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + bool msgDelivered = false; + bool msgReceived = false; + const int testSize = 250 * 1024; + ManagerStack.ClientListener(1).DeliveryEvent += (peer, obj) => + { + Assert.That((int)obj, Is.EqualTo(5)); + msgDelivered = true; + }; + ManagerStack.ClientListener(1).PeerConnectedEvent += peer => + { + int testData = 5; + byte[] arr = new byte[testSize]; + arr[0] = 196; + arr[7000] = 32; + arr[12499] = 200; + arr[testSize - 1] = 254; + peer.SendWithDeliveryEvent(arr, 0, DeliveryMethod.ReliableUnordered, testData); + }; + ManagerStack.ServerListener(1).NetworkReceiveEvent += (peer, reader, channel, method) => + { + Assert.That(reader.UserDataSize, Is.EqualTo(testSize)); + Assert.That(reader.RawData[reader.UserDataOffset], Is.EqualTo(196)); + Assert.That(reader.RawData[reader.UserDataOffset + 7000], Is.EqualTo(32)); + Assert.That(reader.RawData[reader.UserDataOffset + 12499], Is.EqualTo(200)); + Assert.That(reader.RawData[reader.UserDataOffset + testSize - 1], Is.EqualTo(254)); + msgReceived = true; + }; + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1 || !msgDelivered || !msgReceived) + { + Thread.Sleep(15); + server.PollEvents(); + client.PollEvents(); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void PeerNotFoundTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + DisconnectInfo? disconnectInfo = null; + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => disconnectInfo = info; + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + server.Stop(false); + server.Start(DefaultPort); + while (client.ConnectedPeersCount == 1) + { + Thread.Sleep(15); + } + client.PollEvents(); + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(disconnectInfo.HasValue, Is.True); + Assert.That(disconnectInfo.Value.Reason, Is.EqualTo(DisconnectReason.PeerNotFound)); + } + + [Test, CancelAfter(10000)] + public void ConnectionFailedTest() + { + NetManager client = ManagerStack.Client(1); + + var result = false; + DisconnectInfo disconnectInfo = default; + + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + result = true; + disconnectInfo = info; + }; + + client.Connect("127.0.0.2", DefaultPort, DefaultAppKey); + + while (!result) + { + Thread.Sleep(15); + client.PollEvents(); + } + + Assert.That(result, Is.True); + Assert.That(disconnectInfo.Reason, Is.EqualTo(DisconnectReason.ConnectionFailed)); + } + + [Test, CancelAfter(10000)] + public void NetPeerDisconnectTimeout() + { + NetManager client = ManagerStack.Client(1); + NetManager server = ManagerStack.Server(1); + + //Default 5 sec timeout for local network is too mach, set 1 for test + server.DisconnectTimeout = 1000; + + NetPeer clientServerPeer = client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (clientServerPeer.ConnectionState != ConnectionState.Connected) + { + Thread.Sleep(15); + server.PollEvents(); + client.PollEvents(); + } + + Assert.That(clientServerPeer.ConnectionState, Is.EqualTo(ConnectionState.Connected)); + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(peer, Is.EqualTo(clientServerPeer)); + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.Timeout)); + }; + + server.Stop(); + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + while (client.ConnectedPeersCount == 1) + { + Thread.Sleep(15); + } + } + + [Test] + public void ReconnectTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + int connectCount = 0; + bool reconnected = false; + ManagerStack.ServerListener(1).PeerConnectedEvent += peer => + { + if (connectCount == 0) + { + byte[] data = {1,2,3,4,5,6,7,8,9}; + for (int i = 0; i < 1000; i++) + { + peer.Send(data, DeliveryMethod.ReliableOrdered); + } + } + connectCount++; + }; + + client.Stop(); + client.Start(ReconnectPort); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (connectCount < 2) + { + if (connectCount == 1 && !reconnected) + { + client.Stop(); + Thread.Sleep(500); + client.Start(ReconnectPort); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + reconnected = true; + } + client.PollEvents(); + server.PollEvents(); + Thread.Sleep(15); + } + Assert.That(connectCount, Is.EqualTo(2)); + } + + [Test] + public void RejectTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + bool rejectReceived = false; + + ManagerStack.ServerListener(1).ClearConnectionRequestEvent(); + ManagerStack.ServerListener(1).ConnectionRequestEvent += request => + { + request.Reject(Encoding.UTF8.GetBytes("reject_test")); + }; + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.ConnectionRejected)); + Assert.That(Encoding.UTF8.GetString(info.AdditionalData.GetRemainingBytes()), Is.EqualTo("reject_test")); + rejectReceived = true; + }; + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (!rejectReceived) + { + client.PollEvents(); + server.PollEvents(); + Thread.Sleep(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + } + + [Test] + public void RejectForceTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + bool rejectReceived = false; + + ManagerStack.ServerListener(1).ClearConnectionRequestEvent(); + ManagerStack.ServerListener(1).ConnectionRequestEvent += request => + { + request.RejectForce(Encoding.UTF8.GetBytes("reject_test")); + }; + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.ConnectionRejected)); + Assert.That(Encoding.UTF8.GetString(info.AdditionalData.GetRemainingBytes()), Is.EqualTo("reject_test")); + rejectReceived = true; + }; + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (!rejectReceived) + { + client.PollEvents(); + server.PollEvents(); + Thread.Sleep(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + } + + [Test, CancelAfter(10000)] + public void NetPeerDisconnectAll() + { + NetManager client = ManagerStack.Client(1); + NetManager client2 = ManagerStack.Client(2); + NetManager server = ManagerStack.Server(1); + + NetPeer clientServerPeer = client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + client2.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (clientServerPeer.ConnectionState != ConnectionState.Connected) + { + Thread.Sleep(15); + server.PollEvents(); + client.PollEvents(); + client2.PollEvents(); + } + + Assert.That(clientServerPeer.ConnectionState, Is.EqualTo(ConnectionState.Connected)); + Assert.That(server.GetPeersCount(ConnectionState.Connected), Is.EqualTo(2)); + + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + byte[] bytes = info.AdditionalData.GetRemainingBytes(); + Assert.That(bytes, Is.EqualTo(new byte[] { 1, 2, 3, 4 }).AsCollection); + Assert.That(peer, Is.EqualTo(clientServerPeer)); + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.RemoteConnectionClose)); + }; + + server.DisconnectAll(new byte[]{1, 2, 3, 4}, 0, 4); + + Assert.That(server.GetPeersCount(ConnectionState.Connected), Is.EqualTo(0)); + + while (client.GetPeersCount(ConnectionState.Connected) != 0) + { + Thread.Sleep(15); + client.PollEvents(); + server.PollEvents(); + } + + //Wait for client 'ShutdownOk' response + Thread.Sleep(100); + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(clientServerPeer.ConnectionState, Is.EqualTo(ConnectionState.Disconnected)); + } + + [Test, CancelAfter(TestTimeout)] + public void DisconnectFromServerTest() + { + NetManager server = ManagerStack.Server(1); + NetManager client = ManagerStack.Client(1); + var clientDisconnected = false; + var serverDisconnected = false; + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => { clientDisconnected = true; }; + ManagerStack.ServerListener(1).PeerDisconnectedEvent += (peer, info) => { serverDisconnected = true; }; + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + while (server.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + + server.DisconnectPeer(server.FirstPeer); + + while (!(clientDisconnected && serverDisconnected)) + { + Thread.Sleep(15); + client.PollEvents(); + server.PollEvents(); + } + + Assert.That(clientDisconnected, Is.True); + Assert.That(serverDisconnected, Is.True); + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + } + + [Test, CancelAfter(5000)] + public void EncryptTest() + { + EventBasedNetListener srvListener = new EventBasedNetListener(); + EventBasedNetListener cliListener = new EventBasedNetListener(); + NetManager srv = new NetManager(srvListener, new XorEncryptLayer("secret_key")); + NetManager cli = new NetManager(cliListener, new XorEncryptLayer("secret_key")); + srv.Start(DefaultPort + 1); + cli.Start(); + + srvListener.ConnectionRequestEvent += request => { request.AcceptIfKey(DefaultAppKey); }; + cli.Connect("127.0.0.1", DefaultPort + 1, DefaultAppKey); + + while (srv.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + srv.PollEvents(); + } + Thread.Sleep(200); + Assert.That(srv.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(cli.ConnectedPeersCount, Is.EqualTo(1)); + cli.Stop(); + srv.Stop(); + } + + [Test, CancelAfter(5000)] + public void ConnectAfterDisconnectWithSamePort() + { + NetManager server = ManagerStack.Server(1); + + EventBasedNetListener listener = new EventBasedNetListener(); + NetManager client = new NetManager(listener, new Crc32cLayer()); + Assert.That(client.Start(ReuseClientPort), Is.True); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + while (server.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + client.Stop(); + + var connected = false; + listener.PeerConnectedEvent += (peer) => + { + connected = true; + }; + Assert.That(client.Start(ReuseClientPort), Is.True); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (!connected) + { + Thread.Sleep(15); + server.PollEvents(); + client.PollEvents(); + } + + Assert.That(connected, Is.True); + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + client.Stop(); + } + + [Test, CancelAfter(TestTimeout)] + public void DisconnectFromClientTest() + { + NetManager server = ManagerStack.Server(1); + NetManager client = ManagerStack.Client(1); + var clientDisconnected = false; + var serverDisconnected = false; + + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.DisconnectPeerCalled)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + clientDisconnected = true; + }; + ManagerStack.ServerListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.RemoteConnectionClose)); + serverDisconnected = true; + }; + + NetPeer serverPeer = client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + while (server.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + + //User server peer from client + serverPeer.Disconnect(); + + while (!(clientDisconnected && serverDisconnected)) + { + Thread.Sleep(15); + client.PollEvents(); + server.PollEvents(); + } + + Assert.That(clientDisconnected, Is.True); + Assert.That(serverDisconnected, Is.True); + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + } + + [Test, CancelAfter(10000)] + public void ChannelsTest() + { + const int channelsCount = 64; + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + server.ChannelsCount = channelsCount; + client.ChannelsCount = channelsCount; + + NetDataWriter writer = new NetDataWriter(); + var methods = new[] + { + DeliveryMethod.Unreliable, + DeliveryMethod.Sequenced, + DeliveryMethod.ReliableOrdered, + DeliveryMethod.ReliableSequenced, + DeliveryMethod.ReliableUnordered + }; + + int messagesReceived = 0; + ManagerStack.ClientListener(1).PeerConnectedEvent += peer => + { + for (int i = 0; i < channelsCount; i++) + { + foreach (var deliveryMethod in methods) + { + writer.Reset(); + writer.Put((byte) deliveryMethod); + if (deliveryMethod == DeliveryMethod.ReliableOrdered || + deliveryMethod == DeliveryMethod.ReliableUnordered) + writer.Put(new byte[506]); + peer.Send(writer, (byte) i, deliveryMethod); + } + } + }; + ManagerStack.ServerListener(1).NetworkReceiveEvent += (peer, reader, channel, method) => + { + Assert.That((DeliveryMethod)reader.GetByte(), Is.EqualTo(method)); + messagesReceived++; + }; + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (messagesReceived != methods.Length*channelsCount) + { + server.PollEvents(); + client.PollEvents(); + Thread.Sleep(15); + } + + Assert.That(messagesReceived, Is.EqualTo(methods.Length * channelsCount)); + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void ConnectionByIpV6() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + client.Connect("::1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(10000)] + public void DiscoveryBroadcastTest() + { + var server = ManagerStack.Server(1); + var clientCount = 10; + + server.BroadcastReceiveEnabled = true; + + var writer = new NetDataWriter(); + writer.Put("Client request"); + + ManagerStack.ServerListener(1).NetworkReceiveUnconnectedEvent += (point, reader, type) => + { + if (type == UnconnectedMessageType.Broadcast) + { + var serverWriter = new NetDataWriter(); + serverWriter.Put("Server response"); + server.SendUnconnectedMessage(serverWriter, point); + } + }; + + for (ushort i = 1; i <= clientCount; i++) + { + var cache = i; + ManagerStack.Client(i).UnconnectedMessagesEnabled = true; + ManagerStack.ClientListener(i).NetworkReceiveUnconnectedEvent += (point, reader, type) => + { + if (point.AddressFamily == AddressFamily.InterNetworkV6) + return; + Assert.That(type, Is.EqualTo(UnconnectedMessageType.BasicMessage)); + Assert.That(reader.GetString(), Is.EqualTo("Server response")); + ManagerStack.Client(cache).Connect(point, DefaultAppKey); + }; + } + + ManagerStack.ClientForeach((i, manager, l) => manager.SendBroadcast(writer, DefaultPort)); + + while (server.ConnectedPeersCount < clientCount) + { + server.PollEvents(); + ManagerStack.ClientForeach((i, manager, l) => manager.PollEvents()); + + Thread.Sleep(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(clientCount)); + ManagerStack.ClientForeach( + (i, manager, l) => + { + Assert.That(manager.ConnectedPeersCount, Is.EqualTo(1)); + }); + } + + [Test] + public void HelperManagerStackTest() + { + Assert.That(ManagerStack.Client(1), Is.SameAs(ManagerStack.Client(1))); + Assert.That(ManagerStack.Client(1), Is.Not.SameAs(ManagerStack.Client(2))); + Assert.That(ManagerStack.Client(2), Is.SameAs(ManagerStack.Client(2))); + + Assert.That(ManagerStack.Server(1), Is.SameAs(ManagerStack.Server(1))); + Assert.That(ManagerStack.Server(1), Is.Not.SameAs(ManagerStack.Client(1))); + Assert.That(ManagerStack.Server(1), Is.Not.SameAs(ManagerStack.Client(2))); + } + + [Test, CancelAfter(TestTimeout)] + public void ManualMode() + { + var serverListener = new EventBasedNetListener(); + var server = new NetManager(serverListener, new Crc32cLayer()); + + serverListener.ConnectionRequestEvent += request => request.AcceptIfKey(DefaultAppKey); + + var client = ManagerStack.Client(1); + Assert.That(server.StartInManualMode(DefaultPort), Is.True); + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + server.ManualUpdate(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + server.Stop(); + } + + [Test, CancelAfter(TestTimeout)] + public void SendRawDataToAll() + { + var clientCount = 10; + + var server = ManagerStack.Server(1); + + for (ushort i = 1; i <= clientCount; i++) + { + ManagerStack.Client(i).Connect("127.0.0.1", DefaultPort, DefaultAppKey); + } + + while (server.ConnectedPeersCount < clientCount) + { + Thread.Sleep(15); + server.PollEvents(); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(clientCount)); + Thread.Sleep(100); + ManagerStack.ClientForeach((i, manager, l) => Assert.That(manager.ConnectedPeersCount, Is.EqualTo(1))); + + var dataStack = new Stack(clientCount); + + ManagerStack.ClientForeach( + (i, manager, l) => l.NetworkReceiveEvent += (peer, reader, channel, type) => dataStack.Push(reader.GetRemainingBytes())); + + var data = Encoding.Default.GetBytes("TextForTest"); + server.SendToAll(data, DeliveryMethod.ReliableUnordered); + + while (dataStack.Count < clientCount) + { + ManagerStack.ClientForeach((i, manager, l) => manager.PollEvents()); + + Thread.Sleep(10); + } + + Assert.That(dataStack.Count, Is.EqualTo(clientCount)); + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(clientCount)); + for (ushort i = 1; i <= clientCount; i++) + { + Assert.That(ManagerStack.Client(i).ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(data, Is.EqualTo(dataStack.Pop()).AsCollection); + } + } + } +} diff --git a/LiteNetLib.Tests/ConnectionTest.cs b/LiteNetLib.Tests/ConnectionTest.cs deleted file mode 100644 index 3c7c8fa7..00000000 --- a/LiteNetLib.Tests/ConnectionTest.cs +++ /dev/null @@ -1,502 +0,0 @@ -using System.Collections.Generic; -using System.Net.Sockets; -using System.Text; -using System.Threading; - -using LiteNetLib.Tests.TestUtility; -using LiteNetLib.Utils; - -using NUnit.Framework; - -namespace LiteNetLib.Tests -{ - [TestFixture] - [Category("Communication")] - public class CommunicationTest - { - [SetUp] - public void Init() - { - ManagerStack = new NetManagerStack(DefaultAppKey, DefaultPort); - } - - [TearDown] - public void TearDown() - { - ManagerStack?.Dispose(); - } - - private const int DefaultPort = 9050; - private const string DefaultAppKey = "test_server"; - - public NetManagerStack ManagerStack { get; set; } - - [Test, MaxTime(2000)] - public void ConnectionByIpV4() - { - var server = ManagerStack.Server(1); - var client = ManagerStack.Client(1); - client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - - while (server.PeersCount != 1 || client.PeersCount != 1) - { - Thread.Sleep(15); - server.PollEvents(); - } - - Assert.AreEqual(1, server.PeersCount); - Assert.AreEqual(1, client.PeersCount); - } - - [Test, MaxTime(2000)] - public void PeerNotFoundTest() - { - var server = ManagerStack.Server(1); - var client = ManagerStack.Client(1); - DisconnectInfo? disconnectInfo = null; - ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => disconnectInfo = info; - client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - - while (server.PeersCount != 1 || client.PeersCount != 1) - { - Thread.Sleep(15); - server.PollEvents(); - } - server.Stop(false); - server.Start(DefaultPort); - while (client.PeersCount == 1) - { - Thread.Sleep(15); - } - client.PollEvents(); - - Assert.AreEqual(0, server.PeersCount); - Assert.AreEqual(0, client.PeersCount); - Assert.IsTrue(disconnectInfo.HasValue); - Assert.AreEqual(DisconnectReason.RemoteConnectionClose, disconnectInfo.Value.Reason); - } - - [Test, MaxTime(10000)] - public void ConnectionFailedTest() - { - NetManager client = ManagerStack.Client(1); - - var result = false; - DisconnectInfo disconnectInfo = default(DisconnectInfo); - - ManagerStack.ClientListener(1).PeerConnectedEvent += peer => - { - result = true; - }; - ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => - { - result = true; - disconnectInfo = info; - }; - - client.Connect("127.0.0.2", DefaultPort, DefaultAppKey); - - while (!result) - { - Thread.Sleep(15); - client.PollEvents(); - } - - Assert.True(result); - Assert.AreEqual(DisconnectReason.ConnectionFailed, disconnectInfo.Reason); - } - - [Test, MaxTime(10000)] - public void NetPeerDisconnectTimeout() - { - NetManager client = ManagerStack.Client(1); - NetManager server = ManagerStack.Server(1); - - //Default 5 sec timeout for local network is too mach, set 1 for test - server.DisconnectTimeout = 1000; - - NetPeer clientServerPeer = client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - - while (clientServerPeer.ConnectionState != ConnectionState.Connected) - { - Thread.Sleep(15); - server.PollEvents(); - client.PollEvents(); - } - - Assert.AreEqual(ConnectionState.Connected, clientServerPeer.ConnectionState); - Assert.True(server.PeersCount == 1); - - ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => - { - Assert.AreEqual(clientServerPeer, peer); - Assert.AreEqual(DisconnectReason.Timeout, info.Reason); - }; - - server.Stop(); - - Assert.True(server.PeersCount == 0); - while (client.PeersCount == 1) - { - Thread.Sleep(15); - } - } - - [Test] - public void ReconnectTest() - { - var server = ManagerStack.Server(1); - var client = ManagerStack.Client(1); - int connectCount = 0; - bool reconnected = false; - ManagerStack.ServerListener(1).PeerConnectedEvent += peer => - { - if (connectCount == 0) - { - byte[] data = {1,2,3,4,5,6,7,8,9}; - for (int i = 0; i < 1000; i++) - { - peer.Send(data, DeliveryMethod.ReliableOrdered); - } - } - connectCount++; - }; - - client.Stop(); - client.Start(10123); - client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - - while (connectCount < 2) - { - if (connectCount == 1 && !reconnected) - { - client.Stop(); - Thread.Sleep(500); - client.Start(10123); - client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - reconnected = true; - } - client.PollEvents(); - server.PollEvents(); - Thread.Sleep(15); - } - Assert.AreEqual(2, connectCount); - } - - [Test] - public void RejectTest() - { - var server = ManagerStack.Server(1); - var client = ManagerStack.Client(1); - bool rejectReceived = false; - - ManagerStack.ServerListener(1).ClearConnectionRequestEvent(); - ManagerStack.ServerListener(1).ConnectionRequestEvent += request => - { - request.Reject(Encoding.UTF8.GetBytes("reject_test")); - }; - ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => - { - Assert.AreEqual(true, info.Reason == DisconnectReason.ConnectionRejected); - Assert.AreEqual("reject_test", Encoding.UTF8.GetString(info.AdditionalData.GetRemainingBytes())); - rejectReceived = true; - }; - - client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - - while (!rejectReceived) - { - client.PollEvents(); - server.PollEvents(); - Thread.Sleep(15); - } - - Assert.AreEqual(0, server.PeersCount); - Assert.AreEqual(0, client.PeersCount); - } - - [Test, MaxTime(10000)] - public void NetPeerDisconnectAll() - { - NetManager client = ManagerStack.Client(1); - NetManager client2 = ManagerStack.Client(2); - NetManager server = ManagerStack.Server(1); - - NetPeer clientServerPeer = client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - client2.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - - while (clientServerPeer.ConnectionState != ConnectionState.Connected) - { - Thread.Sleep(15); - server.PollEvents(); - client.PollEvents(); - client2.PollEvents(); - } - - Assert.AreEqual(ConnectionState.Connected, clientServerPeer.ConnectionState); - Assert.AreEqual(2, server.GetPeersCount(ConnectionState.Connected)); - - ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => - { - byte[] bytes = info.AdditionalData.GetRemainingBytes(); - Assert.AreEqual(new byte[] { 1, 2, 3, 4 }, bytes); - Assert.AreEqual(clientServerPeer, peer); - Assert.AreEqual(DisconnectReason.RemoteConnectionClose, info.Reason); - }; - - server.DisconnectAll(new byte[]{1, 2, 3, 4}, 0, 4); - - Assert.AreEqual(0, server.GetPeersCount(ConnectionState.Connected)); - - while (client.GetPeersCount(ConnectionState.Connected) != 0) - { - Thread.Sleep(15); - client.PollEvents(); - server.PollEvents(); - } - - //Wait for client 'ShutdownOk' response - Thread.Sleep(100); - - Assert.AreEqual(0, server.PeersCount); - Assert.AreEqual(ConnectionState.Disconnected, clientServerPeer.ConnectionState); - } - - [Test, MaxTime(2000)] - public void DisconnectFromServerTest() - { - NetManager server = ManagerStack.Server(1); - NetManager client = ManagerStack.Client(1); - var clientDisconnected = false; - var serverDisconnected = false; - ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => { clientDisconnected = true; }; - ManagerStack.ServerListener(1).PeerDisconnectedEvent += (peer, info) => { serverDisconnected = true; }; - - client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - while (server.PeersCount != 1) - { - Thread.Sleep(15); - server.PollEvents(); - } - - server.DisconnectPeer(server.FirstPeer); - - while (!(clientDisconnected && serverDisconnected)) - { - Thread.Sleep(15); - client.PollEvents(); - server.PollEvents(); - } - - // Wait that server remove disconnected peers - Thread.Sleep(100); - - Assert.True(clientDisconnected); - Assert.True(serverDisconnected); - Assert.AreEqual(0, server.PeersCount); - Assert.AreEqual(0, client.PeersCount); - } - - [Test, MaxTime(5000)] - public void ConnectAfterDisconnectWithSamePort() - { - NetManager server = ManagerStack.Server(1); - - EventBasedNetListener listener = new EventBasedNetListener(); - NetManager client = new NetManager(listener); - Assert.True(client.Start(9049)); - client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - while (server.PeersCount != 1) - { - Thread.Sleep(15); - server.PollEvents(); - } - client.Stop(); - - var connected = false; - listener.PeerConnectedEvent += (peer) => - { - connected = true; - }; - Assert.True(client.Start(9049)); - client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - - while (!connected) - { - Thread.Sleep(15); - server.PollEvents(); - client.PollEvents(); - } - - Assert.True(connected); - Assert.AreEqual(1, server.PeersCount); - Assert.AreEqual(1, client.PeersCount); - } - - [Test, MaxTime(2000)] - public void DisconnectFromClientTest() - { - NetManager server = ManagerStack.Server(1); - NetManager client = ManagerStack.Client(1); - var clientDisconnected = false; - var serverDisconnected = false; - - ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => { clientDisconnected = true; }; - ManagerStack.ServerListener(1).PeerDisconnectedEvent += (peer, info) => { serverDisconnected = true; }; - - NetPeer serverPeer = client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); - while (server.PeersCount != 1) - { - Thread.Sleep(15); - server.PollEvents(); - } - - //User server peer from client - serverPeer.Disconnect(); - - while (!(clientDisconnected && serverDisconnected)) - { - Thread.Sleep(15); - client.PollEvents(); - server.PollEvents(); - } - - // Wait that server remove disconnected peers - Thread.Sleep(100); - - Assert.True(clientDisconnected); - Assert.True(serverDisconnected); - Assert.AreEqual(0, server.PeersCount); - Assert.AreEqual(0, client.PeersCount); - } - - [Test, MaxTime(2000)] - public void ConnectionByIpV6() - { - var server = ManagerStack.Server(1); - var client = ManagerStack.Client(1); - client.Connect("::1", DefaultPort, DefaultAppKey); - - while (server.PeersCount != 1 || client.PeersCount != 1) - { - Thread.Sleep(15); - server.PollEvents(); - } - - Assert.AreEqual(1, server.PeersCount); - Assert.AreEqual(1, client.PeersCount); - } - - [Test, MaxTime(2000)] - public void DiscoveryBroadcastTest() - { - var server = ManagerStack.Server(1); - var clientCount = 10; - - server.BroadcastReceiveEnabled = true; - - var writer = new NetDataWriter(); - writer.Put("Client request"); - - ManagerStack.ServerListener(1).NetworkReceiveUnconnectedEvent += (point, reader, type) => - { - if (type == UnconnectedMessageType.Broadcast) - { - var serverWriter = new NetDataWriter(); - serverWriter.Put("Server response"); - server.SendUnconnectedMessage(serverWriter, point); - } - }; - - for (ushort i = 1; i <= clientCount; i++) - { - var cache = i; - ManagerStack.Client(i).UnconnectedMessagesEnabled = true; - ManagerStack.ClientListener(i).NetworkReceiveUnconnectedEvent += (point, reader, type) => - { - if (point.AddressFamily == AddressFamily.InterNetworkV6) - return; - Assert.AreEqual(type, UnconnectedMessageType.BasicMessage); - Assert.AreEqual("Server response", reader.GetString()); - ManagerStack.Client(cache).Connect(point, DefaultAppKey); - }; - } - - ManagerStack.ClientForeach((i, manager, l) => manager.SendBroadcast(writer, DefaultPort)); - - while (server.PeersCount < clientCount) - { - server.PollEvents(); - ManagerStack.ClientForeach((i, manager, l) => manager.PollEvents()); - - Thread.Sleep(15); - } - - Assert.AreEqual(clientCount, server.PeersCount); - ManagerStack.ClientForeach( - (i, manager, l) => - { - Assert.AreEqual(manager.PeersCount, 1); - }); - } - - [Test] - public void HelperManagerStackTest() - { - Assert.AreSame(ManagerStack.Client(1), ManagerStack.Client(1)); - Assert.AreNotSame(ManagerStack.Client(1), ManagerStack.Client(2)); - Assert.AreSame(ManagerStack.Client(2), ManagerStack.Client(2)); - - Assert.AreSame(ManagerStack.Server(1), ManagerStack.Server(1)); - Assert.AreNotSame(ManagerStack.Server(1), ManagerStack.Client(1)); - Assert.AreNotSame(ManagerStack.Server(1), ManagerStack.Client(2)); - } - - [Test] - public void SendRawDataToAll() - { - var clientCount = 10; - - var server = ManagerStack.Server(1); - - for (ushort i = 1; i <= clientCount; i++) - { - ManagerStack.Client(i).Connect("127.0.0.1", DefaultPort, DefaultAppKey); - } - - while (server.PeersCount < clientCount) - { - Thread.Sleep(15); - server.PollEvents(); - } - - Assert.AreEqual(server.PeersCount, clientCount); - Thread.Sleep(100); - ManagerStack.ClientForeach((i, manager, l) => Assert.AreEqual(manager.PeersCount, 1)); - - var dataStack = new Stack(clientCount); - - ManagerStack.ClientForeach( - (i, manager, l) => l.NetworkReceiveEvent += (peer, reader, type) => dataStack.Push(reader.GetRemainingBytes())); - - var data = Encoding.Default.GetBytes("TextForTest"); - server.SendToAll(data, DeliveryMethod.ReliableUnordered); - - while (dataStack.Count < clientCount) - { - ManagerStack.ClientForeach((i, manager, l) => manager.PollEvents()); - - Thread.Sleep(10); - } - - Assert.AreEqual(dataStack.Count, clientCount); - - Assert.AreEqual(server.PeersCount, clientCount); - for (ushort i = 1; i <= clientCount; i++) - { - Assert.AreEqual(ManagerStack.Client(i).PeersCount, 1); - Assert.That(data, Is.EqualTo(dataStack.Pop()).AsCollection); - } - } - } -} \ No newline at end of file diff --git a/LiteNetLib.Tests/LibErrorChecker.cs b/LiteNetLib.Tests/LibErrorChecker.cs new file mode 100644 index 00000000..d99538a7 --- /dev/null +++ b/LiteNetLib.Tests/LibErrorChecker.cs @@ -0,0 +1,13 @@ +using System; +using NUnit.Framework; + +namespace LiteNetLib.Tests; + +class LibErrorChecker : INetLogger +{ + public void WriteNet(NetLogLevel level, string str, params object[] args) + { + if(level == NetLogLevel.Error || level == NetLogLevel.Warning) + Assert.Fail(string.Format(str, args)); + } +} diff --git a/LiteNetLib.Tests/LiteCommunicationTest.cs b/LiteNetLib.Tests/LiteCommunicationTest.cs new file mode 100644 index 00000000..608d655d --- /dev/null +++ b/LiteNetLib.Tests/LiteCommunicationTest.cs @@ -0,0 +1,697 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using LiteNetLib.Layers; +using LiteNetLib.Tests.TestUtility; +using LiteNetLib.Utils; + +using NUnit.Framework; + +namespace LiteNetLib.Tests +{ + [TestFixture] + [Category("Communication")] + public class LiteCommunicationTest + { + const int TestTimeout = 4000; + [SetUp] + public void Init() + { + NetDebug.Logger = new LibErrorChecker(); + ManagerStack = new LiteNetManagerStack(DefaultAppKey, DefaultPort); + } + + [TearDown] + public void TearDown() + { + ManagerStack?.Dispose(); + } + + private static readonly int DefaultPort = TestPorts.ForFramework(9050); + private static readonly int ReconnectPort = TestPorts.ForFramework(10123); + private static readonly int ReuseClientPort = TestPorts.ForFramework(9049); + private const string DefaultAppKey = "test_server"; + private static readonly byte[] DefaultAppKeyBytes = [12, 0, 116, 101, 115, 116, 95, 115, 101, 114, 118, 101, 114]; + + public LiteNetManagerStack ManagerStack { get; set; } + + [Test, CancelAfter(TestTimeout)] + public void ConnectionByIpV4() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void P2PConnect() + { + var client1 = ManagerStack.Client(1); + var client2 = ManagerStack.Client(2); + + client1.Connect("127.0.0.1", client2.LocalPort, DefaultAppKey); + client2.Connect("127.0.0.1", client1.LocalPort, DefaultAppKey); + + while (client1.ConnectedPeersCount != 1 || client2.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + client1.PollEvents(); + client2.PollEvents(); + } + + Assert.That(client1.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client2.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void P2PConnectWithSpan() + { + var client1 = ManagerStack.Client(1); + var client2 = ManagerStack.Client(2); + + IPEndPoint endPoint1 = new IPEndPoint(IPAddress.Loopback, client2.LocalPort); + IPEndPoint endPoint2 = new IPEndPoint(IPAddress.Loopback, client1.LocalPort); + client1.Connect(endPoint1, DefaultAppKeyBytes.AsSpan()); + client2.Connect(endPoint2, DefaultAppKeyBytes.AsSpan()); + + while (client1.ConnectedPeersCount != 1 || client2.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + client1.PollEvents(); + client2.PollEvents(); + } + + Assert.That(client1.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client2.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void ConnectionByIpV4Unsynced() + { + var server = ManagerStack.Server(1); + server.UnsyncedEvents = true; + var client = ManagerStack.Client(1); + client.UnsyncedEvents = true; + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void DeliveryTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + bool msgDelivered = false; + bool msgReceived = false; + const int testSize = 250 * 1024; + ManagerStack.ClientListener(1).DeliveryEvent += (peer, obj) => + { + Assert.That((int)obj, Is.EqualTo(5)); + msgDelivered = true; + }; + ManagerStack.ClientListener(1).PeerConnectedEvent += peer => + { + int testData = 5; + byte[] arr = new byte[testSize]; + arr[0] = 196; + arr[7000] = 32; + arr[12499] = 200; + arr[testSize - 1] = 254; + peer.SendWithDeliveryEvent(arr, DeliveryMethod.ReliableUnordered, testData); + }; + ManagerStack.ServerListener(1).NetworkReceiveEvent += (peer, reader, method) => + { + Assert.That(reader.UserDataSize, Is.EqualTo(testSize)); + Assert.That(reader.RawData[reader.UserDataOffset], Is.EqualTo(196)); + Assert.That(reader.RawData[reader.UserDataOffset + 7000], Is.EqualTo(32)); + Assert.That(reader.RawData[reader.UserDataOffset + 12499], Is.EqualTo(200)); + Assert.That(reader.RawData[reader.UserDataOffset + testSize - 1], Is.EqualTo(254)); + msgReceived = true; + }; + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1 || !msgDelivered || !msgReceived) + { + Thread.Sleep(15); + server.PollEvents(); + client.PollEvents(); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(TestTimeout)] + public void PeerNotFoundTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + DisconnectInfo? disconnectInfo = null; + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => disconnectInfo = info; + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + server.Stop(false); + server.Start(DefaultPort); + while (client.ConnectedPeersCount == 1) + { + Thread.Sleep(15); + } + client.PollEvents(); + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(disconnectInfo.HasValue, Is.True); + Assert.That(disconnectInfo.Value.Reason, Is.EqualTo(DisconnectReason.PeerNotFound)); + } + + [Test, CancelAfter(10000)] + public void ConnectionFailedTest() + { + var client = ManagerStack.Client(1); + + var result = false; + DisconnectInfo disconnectInfo = default; + + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + result = true; + disconnectInfo = info; + }; + + client.Connect("127.0.0.2", DefaultPort, DefaultAppKey); + + while (!result) + { + Thread.Sleep(15); + client.PollEvents(); + } + + Assert.That(result, Is.True); + Assert.That(disconnectInfo.Reason, Is.EqualTo(DisconnectReason.ConnectionFailed)); + } + + [Test, CancelAfter(10000)] + public void NetPeerDisconnectTimeout() + { + var client = ManagerStack.Client(1); + var server = ManagerStack.Server(1); + + //Default 5 sec timeout for local network is too mach, set 1 for test + server.DisconnectTimeout = 1000; + + var clientServerPeer = client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (clientServerPeer.ConnectionState != ConnectionState.Connected) + { + Thread.Sleep(15); + server.PollEvents(); + client.PollEvents(); + } + + Assert.That(clientServerPeer.ConnectionState, Is.EqualTo(ConnectionState.Connected)); + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(peer, Is.EqualTo(clientServerPeer)); + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.Timeout)); + }; + + server.Stop(); + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + while (client.ConnectedPeersCount == 1) + { + Thread.Sleep(15); + } + } + + [Test] + public void ReconnectTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + int connectCount = 0; + bool reconnected = false; + ManagerStack.ServerListener(1).PeerConnectedEvent += peer => + { + if (connectCount == 0) + { + byte[] data = {1,2,3,4,5,6,7,8,9}; + for (int i = 0; i < 1000; i++) + { + peer.Send(data, DeliveryMethod.ReliableOrdered); + } + } + connectCount++; + }; + + client.Stop(); + client.Start(ReconnectPort); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (connectCount < 2) + { + if (connectCount == 1 && !reconnected) + { + client.Stop(); + Thread.Sleep(500); + client.Start(ReconnectPort); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + reconnected = true; + } + client.PollEvents(); + server.PollEvents(); + Thread.Sleep(15); + } + Assert.That(connectCount, Is.EqualTo(2)); + } + + [Test] + public void RejectTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + bool rejectReceived = false; + + ManagerStack.ServerListener(1).ClearConnectionRequestEvent(); + ManagerStack.ServerListener(1).ConnectionRequestEvent += request => + { + request.Reject(Encoding.UTF8.GetBytes("reject_test")); + }; + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.ConnectionRejected)); + Assert.That(Encoding.UTF8.GetString(info.AdditionalData.GetRemainingBytes()), Is.EqualTo("reject_test")); + rejectReceived = true; + }; + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (!rejectReceived) + { + client.PollEvents(); + server.PollEvents(); + Thread.Sleep(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + } + + [Test] + public void RejectForceTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + bool rejectReceived = false; + + ManagerStack.ServerListener(1).ClearConnectionRequestEvent(); + ManagerStack.ServerListener(1).ConnectionRequestEvent += request => + { + request.RejectForce(Encoding.UTF8.GetBytes("reject_test")); + }; + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.ConnectionRejected)); + Assert.That(Encoding.UTF8.GetString(info.AdditionalData.GetRemainingBytes()), Is.EqualTo("reject_test")); + rejectReceived = true; + }; + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (!rejectReceived) + { + client.PollEvents(); + server.PollEvents(); + Thread.Sleep(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + } + + [Test, CancelAfter(10000)] + public void NetPeerDisconnectAll() + { + var client = ManagerStack.Client(1); + var client2 = ManagerStack.Client(2); + var server = ManagerStack.Server(1); + + var clientServerPeer = client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + client2.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (clientServerPeer.ConnectionState != ConnectionState.Connected) + { + Thread.Sleep(15); + server.PollEvents(); + client.PollEvents(); + client2.PollEvents(); + } + + Assert.That(clientServerPeer.ConnectionState, Is.EqualTo(ConnectionState.Connected)); + Assert.That(server.GetPeersCount(ConnectionState.Connected), Is.EqualTo(2)); + + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + byte[] bytes = info.AdditionalData.GetRemainingBytes(); + Assert.That(bytes, Is.EqualTo(new byte[] { 1, 2, 3, 4 }).AsCollection); + Assert.That(peer, Is.EqualTo(clientServerPeer)); + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.RemoteConnectionClose)); + }; + + server.DisconnectAll(new byte[]{1, 2, 3, 4}, 0, 4); + + Assert.That(server.GetPeersCount(ConnectionState.Connected), Is.EqualTo(0)); + + while (client.GetPeersCount(ConnectionState.Connected) != 0) + { + Thread.Sleep(15); + client.PollEvents(); + server.PollEvents(); + } + + //Wait for client 'ShutdownOk' response + Thread.Sleep(100); + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(clientServerPeer.ConnectionState, Is.EqualTo(ConnectionState.Disconnected)); + } + + [Test, CancelAfter(TestTimeout)] + public void DisconnectFromServerTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + var clientDisconnected = false; + var serverDisconnected = false; + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => { clientDisconnected = true; }; + ManagerStack.ServerListener(1).PeerDisconnectedEvent += (peer, info) => { serverDisconnected = true; }; + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + while (server.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + + server.DisconnectPeer(server.FirstPeer); + + while (!(clientDisconnected && serverDisconnected)) + { + Thread.Sleep(15); + client.PollEvents(); + server.PollEvents(); + } + + Assert.That(clientDisconnected, Is.True); + Assert.That(serverDisconnected, Is.True); + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + } + + [Test, CancelAfter(5000)] + public void EncryptTest() + { + var srvListener = new EventBasedLiteNetListener(); + var cliListener = new EventBasedLiteNetListener(); + var srv = new LiteNetManager(srvListener, new XorEncryptLayer("secret_key")); + var cli = new LiteNetManager(cliListener, new XorEncryptLayer("secret_key")); + srv.Start(DefaultPort + 1); + cli.Start(); + + srvListener.ConnectionRequestEvent += request => { request.AcceptIfKey(DefaultAppKey); }; + cli.Connect("127.0.0.1", DefaultPort + 1, DefaultAppKey); + + while (srv.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + srv.PollEvents(); + } + Thread.Sleep(200); + Assert.That(srv.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(cli.ConnectedPeersCount, Is.EqualTo(1)); + cli.Stop(); + srv.Stop(); + } + + [Test, CancelAfter(5000)] + public void ConnectAfterDisconnectWithSamePort() + { + var server = ManagerStack.Server(1); + + var listener = new EventBasedLiteNetListener(); + var client = new LiteNetManager(listener, new Crc32cLayer()); + Assert.That(client.Start(ReuseClientPort), Is.True); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + while (server.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + client.Stop(); + + var connected = false; + listener.PeerConnectedEvent += (peer) => + { + connected = true; + }; + Assert.That(client.Start(ReuseClientPort), Is.True); + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (!connected) + { + Thread.Sleep(15); + server.PollEvents(); + client.PollEvents(); + } + + Assert.That(connected, Is.True); + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + client.Stop(); + } + + [Test, CancelAfter(TestTimeout)] + public void DisconnectFromClientTest() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + var clientDisconnected = false; + var serverDisconnected = false; + + ManagerStack.ClientListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.DisconnectPeerCalled)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + clientDisconnected = true; + }; + ManagerStack.ServerListener(1).PeerDisconnectedEvent += (peer, info) => + { + Assert.That(info.Reason, Is.EqualTo(DisconnectReason.RemoteConnectionClose)); + serverDisconnected = true; + }; + + var serverPeer = client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + while (server.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + + //User server peer from client + serverPeer.Disconnect(); + + while (!(clientDisconnected && serverDisconnected)) + { + Thread.Sleep(15); + client.PollEvents(); + server.PollEvents(); + } + + Assert.That(clientDisconnected, Is.True); + Assert.That(serverDisconnected, Is.True); + Assert.That(server.ConnectedPeersCount, Is.EqualTo(0)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(0)); + } + + [Test, CancelAfter(TestTimeout)] + public void ConnectionByIpV6() + { + var server = ManagerStack.Server(1); + var client = ManagerStack.Client(1); + client.Connect("::1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + } + + [Test, CancelAfter(10000)] + public void DiscoveryBroadcastTest() + { + var server = ManagerStack.Server(1); + var clientCount = 10; + + server.BroadcastReceiveEnabled = true; + + var writer = new NetDataWriter(); + writer.Put("Client request"); + + ManagerStack.ServerListener(1).NetworkReceiveUnconnectedEvent += (point, reader, type) => + { + if (type == UnconnectedMessageType.Broadcast) + { + var serverWriter = new NetDataWriter(); + serverWriter.Put("Server response"); + server.SendUnconnectedMessage(serverWriter, point); + } + }; + + for (ushort i = 1; i <= clientCount; i++) + { + var cache = i; + ManagerStack.Client(i).UnconnectedMessagesEnabled = true; + ManagerStack.ClientListener(i).NetworkReceiveUnconnectedEvent += (point, reader, type) => + { + if (point.AddressFamily == AddressFamily.InterNetworkV6) + return; + Assert.That(type, Is.EqualTo(UnconnectedMessageType.BasicMessage)); + Assert.That(reader.GetString(), Is.EqualTo("Server response")); + ManagerStack.Client(cache).Connect(point, DefaultAppKey); + }; + } + + ManagerStack.ClientForeach((i, manager, l) => manager.SendBroadcast(writer, DefaultPort)); + + while (server.ConnectedPeersCount < clientCount) + { + server.PollEvents(); + ManagerStack.ClientForeach((i, manager, l) => manager.PollEvents()); + + Thread.Sleep(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(clientCount)); + ManagerStack.ClientForeach( + (i, manager, l) => + { + Assert.That(manager.ConnectedPeersCount, Is.EqualTo(1)); + }); + } + + [Test] + public void HelperManagerStackTest() + { + Assert.That(ManagerStack.Client(1), Is.SameAs(ManagerStack.Client(1))); + Assert.That(ManagerStack.Client(1), Is.Not.SameAs(ManagerStack.Client(2))); + Assert.That(ManagerStack.Client(2), Is.SameAs(ManagerStack.Client(2))); + + Assert.That(ManagerStack.Server(1), Is.SameAs(ManagerStack.Server(1))); + Assert.That(ManagerStack.Server(1), Is.Not.SameAs(ManagerStack.Client(1))); + Assert.That(ManagerStack.Server(1), Is.Not.SameAs(ManagerStack.Client(2))); + } + + [Test, CancelAfter(TestTimeout)] + public void ManualMode() + { + var serverListener = new EventBasedLiteNetListener(); + var server = new LiteNetManager(serverListener, new Crc32cLayer()); + + serverListener.ConnectionRequestEvent += request => request.AcceptIfKey(DefaultAppKey); + + var client = ManagerStack.Client(1); + Assert.That(server.StartInManualMode(DefaultPort), Is.True); + + client.Connect("127.0.0.1", DefaultPort, DefaultAppKey); + + while (server.ConnectedPeersCount != 1 || client.ConnectedPeersCount != 1) + { + Thread.Sleep(15); + server.PollEvents(); + server.ManualUpdate(15); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(client.ConnectedPeersCount, Is.EqualTo(1)); + server.Stop(); + } + + [Test, CancelAfter(TestTimeout)] + public void SendRawDataToAll() + { + var clientCount = 10; + + var server = ManagerStack.Server(1); + + for (ushort i = 1; i <= clientCount; i++) + { + ManagerStack.Client(i).Connect("127.0.0.1", DefaultPort, DefaultAppKey); + } + + while (server.ConnectedPeersCount < clientCount) + { + Thread.Sleep(15); + server.PollEvents(); + } + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(clientCount)); + Thread.Sleep(100); + ManagerStack.ClientForeach((i, manager, l) => Assert.That(manager.ConnectedPeersCount, Is.EqualTo(1))); + + var dataStack = new Stack(clientCount); + + ManagerStack.ClientForeach( + (i, manager, l) => l.NetworkReceiveEvent += (peer, reader, type) => dataStack.Push(reader.GetRemainingBytes())); + + var data = Encoding.Default.GetBytes("TextForTest"); + server.SendToAll(data, DeliveryMethod.ReliableUnordered); + + while (dataStack.Count < clientCount) + { + ManagerStack.ClientForeach((i, manager, l) => manager.PollEvents()); + + Thread.Sleep(10); + } + + Assert.That(dataStack.Count, Is.EqualTo(clientCount)); + + Assert.That(server.ConnectedPeersCount, Is.EqualTo(clientCount)); + for (ushort i = 1; i <= clientCount; i++) + { + Assert.That(ManagerStack.Client(i).ConnectedPeersCount, Is.EqualTo(1)); + Assert.That(data, Is.EqualTo(dataStack.Pop()).AsCollection); + } + } + } +} diff --git a/LiteNetLib.Tests/LiteNetLib.Tests.csproj b/LiteNetLib.Tests/LiteNetLib.Tests.csproj index 6492c448..b0bb097e 100644 --- a/LiteNetLib.Tests/LiteNetLib.Tests.csproj +++ b/LiteNetLib.Tests/LiteNetLib.Tests.csproj @@ -2,23 +2,22 @@ Library - net45;netcoreapp2.0 + net8.0;net9.0 - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - diff --git a/LiteNetLib.Tests/NetSerializerTest.cs b/LiteNetLib.Tests/NetSerializerTest.cs index 989cde12..a8bd036a 100644 --- a/LiteNetLib.Tests/NetSerializerTest.cs +++ b/LiteNetLib.Tests/NetSerializerTest.cs @@ -1,4 +1,8 @@ -using LiteNetLib.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using LiteNetLib.Utils; using NUnit.Framework; @@ -14,21 +18,31 @@ public void Init() _samplePacket = new SamplePacket { SomeFloat = 3.42f, - SomeIntArray = new[] {6, 5, 4}, + SomeIntArray = new[] { 6, 5, 4 }, SomeString = "Test String", + SomeGuid = Guid.NewGuid(), SomeVector2 = new SomeVector2(4, 5), - SomeVectors = new[] {new SomeVector2(1, 2), new SomeVector2(3, 4)}, + SomeVectors = new[] { new SomeVector2(1, 2), new SomeVector2(3, 4) }, + SomeFastVector3 = new SomeFastVector3(0.21f, 100.25f, -400.0f), + SomeFastVectors = new[] { new SomeFastVector3(1.0f, 2.0f, 3.0f), new SomeFastVector3(4.0f, 5.0f, 6.0f) }, SomeEnum = TestEnum.B, SomeByteArray = new byte[] { 255, 1, 0 }, - TestObj = new SampleNetSerializable {Value = 5}, - TestArray = new [] { new SampleNetSerializable { Value = 6 }, new SampleNetSerializable { Value = 15 } }, - SampleClassArray = new[] { new SampleClass { Value = 6 }, new SampleClass { Value = 15 } } + TestObj = new SampleNetSerializable { Value = 5 }, + TestArray = new[] { new SampleNetSerializable { Value = 6 }, new SampleNetSerializable { Value = 15 } }, + SampleClassArray = new[] + { FillTestArray(new SampleClass { Value = 6, TestEnum = TestEnum.C }), FillTestArray(new SampleClass { Value = 15, TestEnum = TestEnum.B }) }, + SampleClassList = new List + { FillTestArray(new SampleClass { Value = 1 }), FillTestArray(new SampleClass { Value = 5 }) }, + VectorList = new List { new SomeVector2(-1, -2), new SomeVector2(700, 800) }, + FastVectorList = new List { new SomeFastVector3(-100.0f, -200.0f, -300.0f), new SomeFastVector3(100.0f, 200.0f, 300.0f) }, + IgnoreMe = 1337 }; _packetProcessor = new NetPacketProcessor(); _packetProcessor.RegisterNestedType(); _packetProcessor.RegisterNestedType(() => new SampleClass()); _packetProcessor.RegisterNestedType(SomeVector2.Serialize, SomeVector2.Deserialize); + _packetProcessor.RegisterNestedType(SomeFastVector3.Serialize, SomeFastVector3.Deserialize); } private SamplePacket _samplePacket; @@ -60,6 +74,30 @@ public static SomeVector2 Deserialize(NetDataReader reader) } } + private readonly struct SomeFastVector3 + { + public readonly float X; + public readonly float Y; + public readonly float Z; + + public SomeFastVector3(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + public static void Serialize(NetDataWriter writer, SomeFastVector3 vector) + { + writer.PutUnmanaged(vector); + } + + public static SomeFastVector3 Deserialize(NetDataReader reader) + { + return reader.GetUnmanaged(); + } + } + private struct SampleNetSerializable : INetSerializable { public int Value; @@ -78,20 +116,28 @@ public void Deserialize(NetDataReader reader) private class SampleClass : INetSerializable { public int Value; + public ChildClass[] TestArray = Array.Empty(); + public TestEnum TestEnum; public void Serialize(NetDataWriter writer) { writer.Put(Value); + writer.PutArray(TestArray); + writer.PutEnum(TestEnum); } public void Deserialize(NetDataReader reader) { Value = reader.GetInt(); + TestArray = reader.GetArray(); + TestEnum = reader.GetEnum(); } public override bool Equals(object obj) { - return ((SampleClass)obj).Value == Value; + var other = (SampleClass)obj; + return other.Value == Value && (TestArray is null && other.TestArray is null || TestArray != null && + other.TestArray != null && TestArray.SequenceEqual(other.TestArray)) && other.TestEnum == TestEnum; } public override int GetHashCode() @@ -100,6 +146,31 @@ public override int GetHashCode() } } + private class ChildClass : INetSerializable + { + public int Value; + + public void Serialize(NetDataWriter writer) + { + writer.Put(Value); + } + + public void Deserialize(NetDataReader reader) + { + Value = reader.GetInt(); + } + + public override bool Equals(object obj) + { + return ((ChildClass)obj).Value == Value; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + } + private enum TestEnum { A = 1, @@ -114,12 +185,20 @@ private class SamplePacket public int[] SomeIntArray { get; set; } public byte[] SomeByteArray { get; set; } public string SomeString { get; set; } + public Guid SomeGuid { get; set; } public SomeVector2 SomeVector2 { get; set; } public SomeVector2[] SomeVectors { get; set; } + public SomeFastVector3 SomeFastVector3 { get; set; } + public SomeFastVector3[] SomeFastVectors { get; set; } public TestEnum SomeEnum { get; set; } public SampleNetSerializable TestObj { get; set; } public SampleNetSerializable[] TestArray { get; set; } public SampleClass[] SampleClassArray { get; set; } + public List SampleClassList { get; set; } + public List VectorList { get; set; } + public List FastVectorList { get; set; } + [IgnoreDataMember] + public int IgnoreMe { get; set; } } private static bool AreSame(string s1, string s2) @@ -131,13 +210,22 @@ private static bool AreSame(string s1, string s2) return s1 == s2; } + private static SampleClass FillTestArray(SampleClass obj) + { + var random = new Random(); + obj.TestArray = new ChildClass[random.Next(10)]; + for (int i = 0; i < obj.TestArray.Length; i++) + obj.TestArray[i] = new ChildClass { Value = random.Next() }; + return obj; + } + [Test, MaxTime(2000)] public void CustomPackageTest() { var writer = new NetDataWriter(); _packetProcessor.Write(writer, _samplePacket); - var reader = new NetDataReader(writer.CopyData()); + var reader = new NetDataReader(writer); SamplePacket readPackage = null; _packetProcessor.SubscribeReusable( @@ -148,18 +236,54 @@ public void CustomPackageTest() _packetProcessor.ReadAllPackets(reader); - Assert.NotNull(readPackage); - Assert.IsTrue(AreSame(_samplePacket.EmptyString, readPackage.EmptyString)); - Assert.AreEqual(_samplePacket.SomeFloat, readPackage.SomeFloat); - Assert.AreEqual(_samplePacket.SomeIntArray, readPackage.SomeIntArray); - Assert.IsTrue(AreSame(_samplePacket.SomeString, readPackage.SomeString)); - Assert.AreEqual(_samplePacket.SomeVector2, readPackage.SomeVector2); - Assert.AreEqual(_samplePacket.SomeVectors, readPackage.SomeVectors); - Assert.AreEqual(_samplePacket.SomeEnum, readPackage.SomeEnum); - Assert.AreEqual(_samplePacket.TestObj.Value, readPackage.TestObj.Value); - Assert.AreEqual(_samplePacket.TestArray, readPackage.TestArray); - Assert.AreEqual(_samplePacket.SomeByteArray, readPackage.SomeByteArray); - Assert.AreEqual(_samplePacket.SampleClassArray, readPackage.SampleClassArray); + Assert.That(readPackage, Is.Not.Null); + Assert.That(AreSame(_samplePacket.EmptyString, readPackage.EmptyString), Is.True); + Assert.That(readPackage.SomeFloat, Is.EqualTo(_samplePacket.SomeFloat)); + Assert.That(readPackage.SomeIntArray, Is.EqualTo(_samplePacket.SomeIntArray).AsCollection); + Assert.That(AreSame(_samplePacket.SomeString, readPackage.SomeString), Is.True); + Assert.That(readPackage.SomeGuid, Is.EqualTo(_samplePacket.SomeGuid)); + Assert.That(readPackage.SomeVector2, Is.EqualTo(_samplePacket.SomeVector2)); + Assert.That(readPackage.SomeVectors, Is.EqualTo(_samplePacket.SomeVectors).AsCollection); + Assert.That(readPackage.SomeFastVector3, Is.EqualTo(_samplePacket.SomeFastVector3)); + Assert.That(readPackage.SomeFastVectors, Is.EqualTo(_samplePacket.SomeFastVectors).AsCollection); + Assert.That(readPackage.SomeEnum, Is.EqualTo(_samplePacket.SomeEnum)); + Assert.That(readPackage.TestObj.Value, Is.EqualTo(_samplePacket.TestObj.Value)); + Assert.That(readPackage.TestArray, Is.EqualTo(_samplePacket.TestArray).AsCollection); + Assert.That(readPackage.SomeByteArray, Is.EqualTo(_samplePacket.SomeByteArray).AsCollection); + Assert.That(readPackage.SampleClassArray, Is.EqualTo(_samplePacket.SampleClassArray).AsCollection); + Assert.That(readPackage.IgnoreMe, Is.EqualTo(0)); // expect 0 because it should be ignored + Assert.That(readPackage.SampleClassList, Is.EqualTo(_samplePacket.SampleClassList).AsCollection); + Assert.That(readPackage.VectorList, Is.EqualTo(_samplePacket.VectorList).AsCollection); + Assert.That(readPackage.FastVectorList, Is.EqualTo(_samplePacket.FastVectorList).AsCollection); + + //remove test + _samplePacket.SampleClassList.RemoveAt(0); + _samplePacket.SampleClassArray = new []{new SampleClass {Value = 1}}; + _samplePacket.VectorList.RemoveAt(0); + _samplePacket.FastVectorList.RemoveAt(0); + + writer.Reset(); + _packetProcessor.Write(writer, _samplePacket); + reader.SetSource(writer); + _packetProcessor.ReadAllPackets(reader); + + Assert.That(readPackage.SampleClassArray, Is.EqualTo(_samplePacket.SampleClassArray).AsCollection); + Assert.That(readPackage.SampleClassList, Is.EqualTo(_samplePacket.SampleClassList).AsCollection); + + //add test + _samplePacket.SampleClassList.Add(new SampleClass { Value = 152 }); + _samplePacket.SampleClassList.Add(new SampleClass { Value = 154 }); + _samplePacket.SampleClassArray = new[] { new SampleClass { Value = 1 }, new SampleClass { Value = 2 }, new SampleClass { Value = 3 } }; + _samplePacket.VectorList.Add(new SomeVector2(500,600)); + _samplePacket.FastVectorList.Add(new SomeFastVector3(500.0f, 600.0f, 700.0f)); + + writer.Reset(); + _packetProcessor.Write(writer, _samplePacket); + reader.SetSource(writer); + _packetProcessor.ReadAllPackets(reader); + + Assert.That(readPackage.SampleClassArray, Is.EqualTo(_samplePacket.SampleClassArray).AsCollection); + Assert.That(readPackage.SampleClassList, Is.EqualTo(_samplePacket.SampleClassList).AsCollection); } } -} \ No newline at end of file +} diff --git a/LiteNetLib.Tests/ReaderWriterSimpleDataTest.cs b/LiteNetLib.Tests/ReaderWriterSimpleDataTest.cs index e1185eda..03725915 100644 --- a/LiteNetLib.Tests/ReaderWriterSimpleDataTest.cs +++ b/LiteNetLib.Tests/ReaderWriterSimpleDataTest.cs @@ -1,6 +1,8 @@ using LiteNetLib.Utils; using NUnit.Framework; +using System; +using System.Net; namespace LiteNetLib.Tests { @@ -14,10 +16,10 @@ public void WriteReadBool() var ndw = new NetDataWriter(); ndw.Put(true); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readBool = ndr.GetBool(); - Assert.AreEqual(readBool, true); + Assert.That(readBool, Is.True); } [Test] @@ -26,7 +28,7 @@ public void WriteReadBoolArray() var ndw = new NetDataWriter(); ndw.PutArray(new[] {true, false, true, false, false}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readBoolArray = ndr.GetBoolArray(); Assert.That(new[] {true, false, true, false, false}, Is.EqualTo(readBoolArray).AsCollection); @@ -38,10 +40,10 @@ public void WriteReadByte() var ndw = new NetDataWriter(); ndw.Put((byte) 8); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readByte = ndr.GetByte(); - Assert.AreEqual(readByte, (byte) 8); + Assert.That(readByte, Is.EqualTo((byte) 8)); } [Test] @@ -50,7 +52,7 @@ public void WriteReadByteArray() var ndw = new NetDataWriter(); ndw.Put(new byte[] {1, 2, 4, 8, 16, byte.MaxValue, byte.MinValue}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readByteArray = new byte[7]; ndr.GetBytes(readByteArray, 7); @@ -59,16 +61,36 @@ public void WriteReadByteArray() Is.EqualTo(readByteArray).AsCollection); } +#if NET5_0_OR_GREATER + [Test] + public void WriteReadByteSpan() + { + Span tempBytes = new byte[] { 1, 2, 4, 8 }; + var ndw = new NetDataWriter(); + ndw.Put(tempBytes); + Span anotherTempBytes = new byte[] { 16, byte.MaxValue, byte.MinValue }; + ndw.Put(anotherTempBytes); + + var ndr = new NetDataReader(ndw); + var readByteArray = new byte[7]; + ndr.GetBytes(readByteArray, 7); + + Assert.That( + new byte[] { 1, 2, 4, 8, 16, byte.MaxValue, byte.MinValue }, + Is.EqualTo(readByteArray).AsCollection); + } +#endif + [Test] public void WriteReadDouble() { var ndw = new NetDataWriter(); ndw.Put(3.1415); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readDouble = ndr.GetDouble(); - Assert.AreEqual(readDouble, 3.1415); + Assert.That(readDouble, Is.EqualTo(3.1415)); } [Test] @@ -77,7 +99,7 @@ public void WriteReadDoubleArray() var ndw = new NetDataWriter(); ndw.PutArray(new[] {1.1, 2.2, 3.3, 4.4, double.MaxValue, double.MinValue}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readDoubleArray = ndr.GetDoubleArray(); Assert.That( @@ -91,10 +113,10 @@ public void WriteReadFloat() var ndw = new NetDataWriter(); ndw.Put(3.1415f); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readFloat = ndr.GetFloat(); - Assert.AreEqual(readFloat, 3.1415f); + Assert.That(readFloat, Is.EqualTo(3.1415f)); } [Test] @@ -103,7 +125,7 @@ public void WriteReadFloatArray() var ndw = new NetDataWriter(); ndw.PutArray(new[] {1.1f, 2.2f, 3.3f, 4.4f, float.MaxValue, float.MinValue}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readFloatArray = ndr.GetFloatArray(); Assert.That( @@ -117,10 +139,10 @@ public void WriteReadInt() var ndw = new NetDataWriter(); ndw.Put(32); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readInt = ndr.GetInt(); - Assert.AreEqual(readInt, 32); + Assert.That(readInt, Is.EqualTo(32)); } [Test] @@ -129,7 +151,7 @@ public void WriteReadIntArray() var ndw = new NetDataWriter(); ndw.PutArray(new[] {1, 2, 3, 4, 5, 6, 7, int.MaxValue, int.MinValue}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readIntArray = ndr.GetIntArray(); Assert.That(new[] {1, 2, 3, 4, 5, 6, 7, int.MaxValue, int.MinValue}, Is.EqualTo(readIntArray).AsCollection); @@ -141,10 +163,10 @@ public void WriteReadLong() var ndw = new NetDataWriter(); ndw.Put(64L); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readLong = ndr.GetLong(); - Assert.AreEqual(readLong, 64L); + Assert.That(readLong, Is.EqualTo(64L)); } [Test] @@ -153,7 +175,7 @@ public void WriteReadLongArray() var ndw = new NetDataWriter(); ndw.PutArray(new[] {1L, 2L, 3L, 4L, long.MaxValue, long.MinValue}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readLongArray = ndr.GetLongArray(); Assert.That(new[] {1L, 2L, 3L, 4L, long.MaxValue, long.MinValue}, Is.EqualTo(readLongArray).AsCollection); @@ -165,10 +187,10 @@ public void WriteReadNetEndPoint() var ndw = new NetDataWriter(); ndw.Put(NetUtils.MakeEndPoint("127.0.0.1", 7777)); - var ndr = new NetDataReader(ndw.Data); - var readNetEndPoint = ndr.GetNetEndPoint(); + var ndr = new NetDataReader(ndw); + var readNetEndPoint = ndr.GetIPEndPoint(); - Assert.AreEqual(readNetEndPoint, NetUtils.MakeEndPoint("127.0.0.1", 7777)); + Assert.That(readNetEndPoint, Is.EqualTo(NetUtils.MakeEndPoint("127.0.0.1", 7777))); } [Test] @@ -177,10 +199,10 @@ public void WriteReadSByte() var ndw = new NetDataWriter(); ndw.Put((sbyte) 8); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readSByte = ndr.GetSByte(); - Assert.AreEqual(readSByte, (sbyte) 8); + Assert.That(readSByte, Is.EqualTo((sbyte) 8)); } [Test] @@ -189,10 +211,10 @@ public void WriteReadShort() var ndw = new NetDataWriter(); ndw.Put((short) 16); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readShort = ndr.GetShort(); - Assert.AreEqual(readShort, (short) 16); + Assert.That(readShort, Is.EqualTo((short) 16)); } [Test] @@ -201,7 +223,7 @@ public void WriteReadShortArray() var ndw = new NetDataWriter(); ndw.PutArray(new short[] {1, 2, 3, 4, 5, 6, short.MaxValue, short.MinValue}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readShortArray = ndr.GetShortArray(); Assert.That( @@ -215,10 +237,10 @@ public void WriteReadString() var ndw = new NetDataWriter(); ndw.Put("String", 10); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readString = ndr.GetString(10); - Assert.AreEqual(readString, "String"); + Assert.That(readString, Is.EqualTo("String")); } [Test] @@ -227,7 +249,7 @@ public void WriteReadStringArray() var ndw = new NetDataWriter(); ndw.PutArray(new[] {"First", "Second", "Third", "Fourth"}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readStringArray = ndr.GetStringArray(10); Assert.That(new[] {"First", "Second", "Third", "Fourth"}, Is.EqualTo(readStringArray).AsCollection); @@ -239,10 +261,10 @@ public void WriteReadUInt() var ndw = new NetDataWriter(); ndw.Put(34U); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readUInt = ndr.GetUInt(); - Assert.AreEqual(readUInt, 34U); + Assert.That(readUInt, Is.EqualTo(34U)); } [Test] @@ -251,7 +273,7 @@ public void WriteReadUIntArray() var ndw = new NetDataWriter(); ndw.PutArray(new[] {1U, 2U, 3U, 4U, 5U, 6U, uint.MaxValue, uint.MinValue}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readUIntArray = ndr.GetUIntArray(); Assert.That( @@ -265,10 +287,10 @@ public void WriteReadULong() var ndw = new NetDataWriter(); ndw.Put(64UL); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readULong = ndr.GetULong(); - Assert.AreEqual(readULong, 64UL); + Assert.That(readULong, Is.EqualTo(64UL)); } [Test] @@ -277,7 +299,7 @@ public void WriteReadULongArray() var ndw = new NetDataWriter(); ndw.PutArray(new[] {1UL, 2UL, 3UL, 4UL, 5UL, ulong.MaxValue, ulong.MinValue}); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readULongArray = ndr.GetULongArray(); Assert.That( @@ -291,10 +313,28 @@ public void WriteReadUShort() var ndw = new NetDataWriter(); ndw.Put((ushort) 16); - var ndr = new NetDataReader(ndw.Data); + var ndr = new NetDataReader(ndw); var readUShort = ndr.GetUShort(); - Assert.AreEqual(readUShort, (ushort) 16); + Assert.That(readUShort, Is.EqualTo((ushort) 16)); + } + + [Test] + public void WriteReadIPEndPoint() + { + var ndw = new NetDataWriter(); + var ipep = new IPEndPoint(IPAddress.Broadcast, 12345); + var ipep6 = new IPEndPoint(IPAddress.IPv6Loopback, 12345); + ndw.Put(ipep); + ndw.Put(ipep6); + + var ndr = new NetDataReader(ndw); + var readIpep = ndr.GetIPEndPoint(); + var readIpep6 = ndr.GetIPEndPoint(); + + Assert.That(readIpep, Is.EqualTo(ipep)); + Assert.That(readIpep6, Is.EqualTo(ipep6)); + Assert.That(ndr.AvailableBytes, Is.EqualTo(0)); } } -} \ No newline at end of file +} diff --git a/LiteNetLib.Tests/TestUtility/NetManagerStack.cs b/LiteNetLib.Tests/TestUtility/NetManagerStack.cs index 36e23026..4ade127c 100644 --- a/LiteNetLib.Tests/TestUtility/NetManagerStack.cs +++ b/LiteNetLib.Tests/TestUtility/NetManagerStack.cs @@ -1,41 +1,39 @@ using System; using System.Collections.Generic; using System.Linq; - +using LiteNetLib.Layers; using NUnit.Framework; namespace LiteNetLib.Tests.TestUtility { - public class NetManagerStack : IDisposable + public abstract class NetManagerStack : IDisposable where TManager : LiteNetManager { private struct NetContainer { - public readonly NetManager Manager; - public readonly EventBasedNetListener Listener; + public readonly TManager Manager; + public readonly TListener Listener; - public NetContainer(NetManager netManager, EventBasedNetListener listener) + public NetContainer(TManager netManager, TListener listener) { Manager = netManager; Listener = listener; } } - private readonly string _appKey; + protected readonly string AppKey; private readonly int _serverPort; - private readonly HashSet _clientIds = new HashSet(); - private readonly HashSet _serverIds = new HashSet(); - - private readonly Dictionary _managers = - new Dictionary(); + private readonly HashSet _clientIds = new(); + private readonly HashSet _serverIds = new(); + private readonly Dictionary _managers = new(); public NetManagerStack(string appKey, int serverPort) { - _appKey = appKey; + AppKey = appKey; _serverPort = serverPort; } - public void ClientForeach(Action action) + public void ClientForeach(Action action) { foreach (var id in _clientIds) { @@ -44,34 +42,34 @@ public void ClientForeach(Action acti } } - public void ServerForeach(Action action) + public void ServerForeach(Action action) { - foreach (var id in _clientIds) + foreach (var id in _serverIds) { var tuple = GetNetworkManager(id, false); action(id, tuple.Manager, tuple.Listener); } } - public NetManager Client(ushort id) + public TManager Client(ushort id) { _clientIds.Add(id); return GetNetworkManager(id, true).Manager; } - public EventBasedNetListener ClientListener(ushort id) + public TListener ClientListener(ushort id) { _clientIds.Add(id); return GetNetworkManager(id, true).Listener; } - public NetManager Server(ushort id) + public TManager Server(ushort id) { _serverIds.Add(id); return GetNetworkManager(id, false).Manager; } - public EventBasedNetListener ServerListener(ushort id) + public TListener ServerListener(ushort id) { _serverIds.Add(id); return GetNetworkManager(id, false).Listener; @@ -80,43 +78,31 @@ public EventBasedNetListener ServerListener(ushort id) public void Dispose() { foreach (var manager in _managers.Values.Select(v => v.Manager)) - { manager.Stop(); - } } + protected abstract (TManager,TListener) CreateNetworkManager(); + private NetContainer GetNetworkManager(ushort id, bool isClient) { - NetContainer container; if (id == 0) { Assert.Fail("Id cannot be 0"); } var key = isClient ? id : (uint) id << 16; - if (!_managers.TryGetValue(key, out container)) + if (!_managers.TryGetValue(key, out var container)) { - var listener = new EventBasedNetListener(); - listener.ConnectionRequestEvent += request => - { - request.AcceptIfKey(_appKey); - }; - NetManager netManager; + var (netManager, listener) = CreateNetworkManager(); if (isClient) { - netManager = new NetManager(listener); if (!netManager.Start()) - { Assert.Fail($"Client {id} start failed"); - } } else { - netManager = new NetManager(listener); if (!netManager.Start(_serverPort)) - { Assert.Fail($"Server {id} on port{_serverPort} start failed"); - } } container = new NetContainer(netManager, listener); @@ -126,4 +112,32 @@ private NetContainer GetNetworkManager(ushort id, bool isClient) return container; } } -} \ No newline at end of file + + public class LiteNetManagerStack : NetManagerStack + { + public LiteNetManagerStack(string appKey, int serverPort) : base(appKey, serverPort) + { + } + + protected override (LiteNetManager, EventBasedLiteNetListener) CreateNetworkManager() + { + var listener = new EventBasedLiteNetListener(); + listener.ConnectionRequestEvent += request =>request.AcceptIfKey(AppKey); + return (new LiteNetManager(listener, new Crc32cLayer()), listener); + } + } + + public class NetManagerStack : NetManagerStack + { + public NetManagerStack(string appKey, int serverPort) : base(appKey, serverPort) + { + } + + protected override (NetManager, EventBasedNetListener) CreateNetworkManager() + { + var listener = new EventBasedNetListener(); + listener.ConnectionRequestEvent += request =>request.AcceptIfKey(AppKey); + return (new NetManager(listener, new Crc32cLayer()), listener); + } + } +} diff --git a/LiteNetLib.Tests/TestUtility/TestPorts.cs b/LiteNetLib.Tests/TestUtility/TestPorts.cs new file mode 100644 index 00000000..70b63934 --- /dev/null +++ b/LiteNetLib.Tests/TestUtility/TestPorts.cs @@ -0,0 +1,32 @@ +using System; +using System.Runtime.Versioning; + +namespace LiteNetLib.Tests.TestUtility +{ + internal static class TestPorts + { + private const int BaselineFrameworkMajor = 8; + private const int FrameworkPortBlockSize = 1000; + + public static int ForFramework(int basePort) + { + var frameworkAttribute = (TargetFrameworkAttribute)Attribute.GetCustomAttribute( + typeof(TestPorts).Assembly, + typeof(TargetFrameworkAttribute)); + + if (frameworkAttribute == null) + return basePort; + + const string versionMarker = "Version=v"; + var versionStart = frameworkAttribute.FrameworkName.IndexOf(versionMarker, StringComparison.Ordinal); + if (versionStart < 0) + return basePort; + + var versionText = frameworkAttribute.FrameworkName.Substring(versionStart + versionMarker.Length); + if (!Version.TryParse(versionText, out var version)) + return basePort; + + return basePort + Math.Max(0, version.Major - BaselineFrameworkMajor) * FrameworkPortBlockSize; + } + } +} diff --git a/LiteNetLib.Tests/packages.config b/LiteNetLib.Tests/packages.config deleted file mode 100644 index b68e40f9..00000000 --- a/LiteNetLib.Tests/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/LiteNetLib.Trim.Dummy/LiteNetLib.Trim.Dummy.csproj b/LiteNetLib.Trim.Dummy/LiteNetLib.Trim.Dummy.csproj new file mode 100644 index 00000000..8fe4051b --- /dev/null +++ b/LiteNetLib.Trim.Dummy/LiteNetLib.Trim.Dummy.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + \ No newline at end of file diff --git a/LiteNetLib.Trim.Dummy/Program.cs b/LiteNetLib.Trim.Dummy/Program.cs new file mode 100644 index 00000000..b49dd580 --- /dev/null +++ b/LiteNetLib.Trim.Dummy/Program.cs @@ -0,0 +1,3 @@ +// I'm a dummy for assembly trimming since the built-in analyzer isn't perfect yet. +// Please build me with `dotnet publish -c Release -r win-x64` (or similar) and test for warnings. +Console.WriteLine("Hello, World!"); diff --git a/LiteNetLib.nuspec b/LiteNetLib.nuspec deleted file mode 100644 index 9ae8c981..00000000 --- a/LiteNetLib.nuspec +++ /dev/null @@ -1,34 +0,0 @@ - - - - LiteNetLib - LiteNetLib - 0.8.3 - Ruslan Pyrch - RevenantX - https://github.com/RevenantX/LiteNetLib/blob/master/LICENSE.txt - https://github.com/RevenantX/LiteNetLib - false - Lite reliable UDP library for .NET, Mono, and .NET Core. - Copyright 2017 Ruslan Pyrch - udp reliable-udp network - https://github.com/RevenantX/LiteNetLib/releases/tag/v0.8.3 - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/LiteNetLib.sln b/LiteNetLib.sln index e0396dce..853c8778 100644 --- a/LiteNetLib.sln +++ b/LiteNetLib.sln @@ -1,10 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2024 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34031.279 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibSample", "LibSample\LibSample.csproj", "{EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiteNetLib.Tests", "LiteNetLib.Tests\LiteNetLib.Tests.csproj", "{6B591E33-E71F-4A37-A08D-B56DD47EDE3F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiteNetLib", "LiteNetLib\LiteNetLib.csproj", "{2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}" @@ -14,34 +12,26 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibSample", "LibSample\LibSample.csproj", "{6499F9B4-0814-42EE-90A5-119AEA07E7EE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiteNetLib.Trim.Dummy", "LiteNetLib.Trim.Dummy\LiteNetLib.Trim.Dummy.csproj", "{5903804F-8AB4-4AF7-9152-BED0561E9C7D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|ARM = Debug|ARM Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 + DebugUnsafe|Any CPU = DebugUnsafe|Any CPU + DebugUnsafe|ARM = DebugUnsafe|ARM + DebugUnsafe|x64 = DebugUnsafe|x64 + DebugUnsafe|x86 = DebugUnsafe|x86 Release|Any CPU = Release|Any CPU Release|ARM = Release|ARM Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Debug|ARM.ActiveCfg = Debug|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Debug|ARM.Build.0 = Debug|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Debug|x64.ActiveCfg = Debug|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Debug|x64.Build.0 = Debug|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Debug|x86.ActiveCfg = Debug|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Debug|x86.Build.0 = Debug|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Release|Any CPU.Build.0 = Release|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Release|ARM.ActiveCfg = Release|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Release|ARM.Build.0 = Release|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Release|x64.ActiveCfg = Release|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Release|x64.Build.0 = Release|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Release|x86.ActiveCfg = Release|Any CPU - {EF232CBE-04C8-4FD9-AE26-2F7EBBE81699}.Release|x86.Build.0 = Release|Any CPU {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.Debug|ARM.ActiveCfg = Debug|Any CPU @@ -50,6 +40,14 @@ Global {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.Debug|x64.Build.0 = Debug|Any CPU {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.Debug|x86.ActiveCfg = Debug|Any CPU {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.Debug|x86.Build.0 = Debug|Any CPU + {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.DebugUnsafe|Any CPU.ActiveCfg = Debug|Any CPU + {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.DebugUnsafe|Any CPU.Build.0 = Debug|Any CPU + {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.DebugUnsafe|ARM.ActiveCfg = Debug|Any CPU + {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.DebugUnsafe|ARM.Build.0 = Debug|Any CPU + {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.DebugUnsafe|x64.ActiveCfg = Debug|Any CPU + {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.DebugUnsafe|x64.Build.0 = Debug|Any CPU + {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.DebugUnsafe|x86.ActiveCfg = Debug|Any CPU + {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.DebugUnsafe|x86.Build.0 = Debug|Any CPU {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.Release|Any CPU.Build.0 = Release|Any CPU {6B591E33-E71F-4A37-A08D-B56DD47EDE3F}.Release|ARM.ActiveCfg = Release|Any CPU @@ -66,6 +64,14 @@ Global {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.Debug|x64.Build.0 = Debug|Any CPU {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.Debug|x86.ActiveCfg = Debug|Any CPU {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.Debug|x86.Build.0 = Debug|Any CPU + {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.DebugUnsafe|Any CPU.ActiveCfg = DebugUnsafe|Any CPU + {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.DebugUnsafe|Any CPU.Build.0 = DebugUnsafe|Any CPU + {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.DebugUnsafe|ARM.ActiveCfg = DebugUnsafe|Any CPU + {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.DebugUnsafe|ARM.Build.0 = DebugUnsafe|Any CPU + {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.DebugUnsafe|x64.ActiveCfg = DebugUnsafe|Any CPU + {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.DebugUnsafe|x64.Build.0 = DebugUnsafe|Any CPU + {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.DebugUnsafe|x86.ActiveCfg = DebugUnsafe|Any CPU + {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.DebugUnsafe|x86.Build.0 = DebugUnsafe|Any CPU {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.Release|Any CPU.Build.0 = Release|Any CPU {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.Release|ARM.ActiveCfg = Release|Any CPU @@ -74,6 +80,54 @@ Global {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.Release|x64.Build.0 = Release|Any CPU {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.Release|x86.ActiveCfg = Release|Any CPU {2F98F12C-B72A-48FE-A7E5-6D8B0C5ABACB}.Release|x86.Build.0 = Release|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Debug|ARM.ActiveCfg = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Debug|ARM.Build.0 = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Debug|x64.Build.0 = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Debug|x86.Build.0 = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.DebugUnsafe|Any CPU.ActiveCfg = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.DebugUnsafe|Any CPU.Build.0 = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.DebugUnsafe|ARM.ActiveCfg = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.DebugUnsafe|ARM.Build.0 = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.DebugUnsafe|x64.ActiveCfg = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.DebugUnsafe|x64.Build.0 = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.DebugUnsafe|x86.ActiveCfg = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.DebugUnsafe|x86.Build.0 = Debug|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Release|Any CPU.Build.0 = Release|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Release|ARM.ActiveCfg = Release|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Release|ARM.Build.0 = Release|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Release|x64.ActiveCfg = Release|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Release|x64.Build.0 = Release|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Release|x86.ActiveCfg = Release|Any CPU + {6499F9B4-0814-42EE-90A5-119AEA07E7EE}.Release|x86.Build.0 = Release|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Debug|ARM.ActiveCfg = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Debug|ARM.Build.0 = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Debug|x64.ActiveCfg = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Debug|x64.Build.0 = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Debug|x86.ActiveCfg = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Debug|x86.Build.0 = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.DebugUnsafe|Any CPU.ActiveCfg = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.DebugUnsafe|Any CPU.Build.0 = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.DebugUnsafe|ARM.ActiveCfg = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.DebugUnsafe|ARM.Build.0 = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.DebugUnsafe|x64.ActiveCfg = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.DebugUnsafe|x64.Build.0 = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.DebugUnsafe|x86.ActiveCfg = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.DebugUnsafe|x86.Build.0 = Debug|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Release|Any CPU.Build.0 = Release|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Release|ARM.ActiveCfg = Release|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Release|ARM.Build.0 = Release|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Release|x64.ActiveCfg = Release|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Release|x64.Build.0 = Release|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Release|x86.ActiveCfg = Release|Any CPU + {5903804F-8AB4-4AF7-9152-BED0561E9C7D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LiteNetLib/BaseChannel.cs b/LiteNetLib/BaseChannel.cs index c734413f..1dd969c5 100644 --- a/LiteNetLib/BaseChannel.cs +++ b/LiteNetLib/BaseChannel.cs @@ -1,31 +1,83 @@ using System.Collections.Generic; +using System.Threading; namespace LiteNetLib { + /// + /// Base class for reliable and sequenced communication channels.
+ /// Handles the queuing and scheduling of outgoing packets. + ///
internal abstract class BaseChannel { - public BaseChannel Next; - protected readonly NetPeer Peer; - protected readonly Queue OutgoingQueue; + /// + /// The peer associated with this channel. + /// + protected readonly LiteNetPeer Peer; + /// + /// Queue containing packets waiting to be sent over the network. + /// + protected readonly Queue OutgoingQueue = new Queue(NetConstants.DefaultWindowSize); + private int _isAddedToPeerChannelSendQueue; - protected BaseChannel(NetPeer peer) - { + /// + /// Gets the number of packets currently residing in the outgoing queue. + /// + public int PacketsInQueue => OutgoingQueue.Count; + + /// + /// Initializes a new instance of the class. + /// + /// The peer that owns this channel. + protected BaseChannel(LiteNetPeer peer) => Peer = peer; - OutgoingQueue = new Queue(64); + + /// + /// Adds a packet to the outgoing queue and notifies the peer to schedule a send update. + /// + /// The packet to be enqueued. + public void AddToQueue(NetPacket packet) + { + lock (OutgoingQueue) + { + OutgoingQueue.Enqueue(packet); + } + AddToPeerChannelSendQueue(); } - public int PacketsInQueue + /// + /// Thread-safely marks this channel as having pending data and adds it to the peer's update list. + /// + protected void AddToPeerChannelSendQueue() { - get { return OutgoingQueue.Count; } + if (Interlocked.CompareExchange(ref _isAddedToPeerChannelSendQueue, 1, 0) == 0) + Peer.AddToReliableChannelSendQueue(this); } - public void AddToQueue(NetPacket packet) + /// + /// Attempts to send packets from the queue. If the queue becomes empty or throttled, + /// it resets the update flag. + /// + /// if there are still packets remaining to be sent in future updates. + public bool SendAndCheckQueue() { - lock (OutgoingQueue) - OutgoingQueue.Enqueue(packet); + bool hasPacketsToSend = SendNextPackets(); + if (!hasPacketsToSend) + Interlocked.Exchange(ref _isAddedToPeerChannelSendQueue, 0); + + return hasPacketsToSend; } - public abstract void SendNextPackets(); + /// + /// Abstract method to implement the specific logic for sending packets (e.g., windowing for reliable). + /// + /// if packets were sent and the channel should remain in the send queue. + public abstract bool SendNextPackets(); + + /// + /// Abstract method to handle an incoming packet received on this specific channel. + /// + /// The received packet. + /// if the packet was processed successfully. public abstract bool ProcessPacket(NetPacket packet); } } diff --git a/LiteNetLib/ConnectionRequest.cs b/LiteNetLib/ConnectionRequest.cs index 8bdbc7d4..8fb15cf5 100644 --- a/LiteNetLib/ConnectionRequest.cs +++ b/LiteNetLib/ConnectionRequest.cs @@ -4,76 +4,80 @@ namespace LiteNetLib { - public enum ConnectionRequestType - { - Incoming, - PeerToPeer - } - internal enum ConnectionRequestResult { None, Accept, - Reject + Reject, + RejectForce } - internal interface IConnectionRequestListener + public class LiteConnectionRequest { - void OnConnectionSolved(ConnectionRequest request, byte[] rejectData, int start, int length); - } - - public class ConnectionRequest - { - private readonly IConnectionRequestListener _listener; + private readonly LiteNetManager _listener; private int _used; - public IPEndPoint RemoteEndPoint { get { return Peer.EndPoint; } } - public readonly NetDataReader Data; - public ConnectionRequestType Type { get; private set; } + /// + /// Data sent by the remote peer in the connection request. + /// + public NetDataReader Data => InternalPacket.Data; internal ConnectionRequestResult Result { get; private set; } - internal readonly long ConnectionId; - internal readonly byte ConnectionNumber; - internal readonly NetPeer Peer; + internal NetConnectRequestPacket InternalPacket; + + /// + /// The remote endpoint (IP and Port) of the peer requesting the connection. + /// + public readonly IPEndPoint RemoteEndPoint; - private bool TryActivate() + internal void UpdateRequest(NetConnectRequestPacket connectRequest) { - return Interlocked.CompareExchange(ref _used, 1, 0) == 0; + //old request + if (connectRequest.ConnectionTime < InternalPacket.ConnectionTime) + return; + + if (connectRequest.ConnectionTime == InternalPacket.ConnectionTime && + connectRequest.ConnectionNumber == InternalPacket.ConnectionNumber) + return; + + InternalPacket = connectRequest; } - internal ConnectionRequest( - long connectionId, - byte connectionNumber, - ConnectionRequestType type, - NetDataReader netDataReader, - NetPeer peer, - IConnectionRequestListener listener) + private bool TryActivate() => + Interlocked.CompareExchange(ref _used, 1, 0) == 0; + + internal LiteConnectionRequest(IPEndPoint remoteEndPoint, NetConnectRequestPacket requestPacket, LiteNetManager listener) { - ConnectionId = connectionId; - ConnectionNumber = connectionNumber; - Type = type; - Peer = peer; - Data = netDataReader; + InternalPacket = requestPacket; + RemoteEndPoint = remoteEndPoint; _listener = listener; } - public NetPeer AcceptIfKey(string key) + /// + /// Accepts the connection if the first string in the matches the provided key. + /// + /// + /// This is a helper method for simple password/key validation. + /// If the key does not match or data is invalid, the connection is automatically rejected. + /// + /// The required string key to match. + /// A new if the key matches and connection is accepted; otherwise, null. + public LiteNetPeer AcceptIfKey(string key) { if (!TryActivate()) return null; try { if (Data.GetString() == key) - { Result = ConnectionRequestResult.Accept; - _listener.OnConnectionSolved(this, null, 0, 0); - return Peer; - } } catch { NetDebug.WriteError("[AC] Invalid incoming data"); } + if (Result == ConnectionRequestResult.Accept) + return _listener.OnConnectionSolved(this, null, 0, 0); + Result = ConnectionRequestResult.Reject; _listener.OnConnectionSolved(this, null, 0, 0); return null; @@ -83,36 +87,102 @@ public NetPeer AcceptIfKey(string key) /// Accept connection and get new NetPeer as result /// /// Connected NetPeer - public NetPeer Accept() + public LiteNetPeer Accept() { if (!TryActivate()) return null; Result = ConnectionRequestResult.Accept; - _listener.OnConnectionSolved(this, null, 0, 0); - return Peer; + return _listener.OnConnectionSolved(this, null, 0, 0); } - public void Reject(byte[] rejectData, int start, int length) + /// + /// Rejects the connection request. + /// + /// Optional user data to send along with the rejection packet. + /// Offset in the array. + /// Length of the data to be sent from the array. + /// + /// If , immediately removes the request, if is not a reject packet is also sent.
+ /// If , creates a temporary peer that sends rejection packets and lingers in memory until a timeout occurs to handle late-arriving packets. + /// + public void Reject(byte[] rejectData, int start, int length, bool force) { if (!TryActivate()) return; - Result = ConnectionRequestResult.Reject; + Result = force ? ConnectionRequestResult.RejectForce : ConnectionRequestResult.Reject; _listener.OnConnectionSolved(this, rejectData, start, length); } - public void Reject() - { - Reject(null, 0, 0); - } + /// + /// Rejects the connection reliably. Creates a temporary peer to handle packet loss. + /// + /// Data to send with the rejection. + /// Offset in the array. + /// Length of the data to be sent. + public void Reject(byte[] rejectData, int start, int length) => + Reject(rejectData, start, length, false); - public void Reject(byte[] rejectData) - { - Reject(rejectData, 0, rejectData.Length); - } + /// + /// Rejects the connection immediately without reliability. + /// Minimizes resource usage by not creating an internal peer. + /// + /// Data to send with the rejection. + /// Offset in the array. + /// Length of the data to be sent. + public void RejectForce(byte[] rejectData, int start, int length) => + Reject(rejectData, start, length, true); + + /// + /// Rejects the connection immediately without sending any packet. + /// + public void RejectForce() => + Reject(null, 0, 0, true); + + /// + /// Rejects the connection immediately without reliability. + /// + /// Data to send with the rejection. + public void RejectForce(byte[] rejectData) => + Reject(rejectData, 0, rejectData.Length, true); + + /// + /// Rejects the connection immediately without reliability using data from a . + /// + /// Writer containing the data to send. + public void RejectForce(NetDataWriter rejectData) => + Reject(rejectData.Data, 0, rejectData.Length, true); - public void Reject(NetDataWriter rejectData) + /// + /// Rejects the connection reliably without additional data. + /// + public void Reject() => + Reject(null, 0, 0, false); + + /// + /// Rejects the connection reliably. + /// + /// Data to send with the rejection. + public void Reject(byte[] rejectData) => + Reject(rejectData, 0, rejectData.Length, false); + + /// + /// Rejects the connection reliably using data from a . + /// + /// Writer containing the data to send. + public void Reject(NetDataWriter rejectData) => + Reject(rejectData.Data, 0, rejectData.Length, false); + } + + public class ConnectionRequest : LiteConnectionRequest + { + internal ConnectionRequest(IPEndPoint remoteEndPoint, NetConnectRequestPacket requestPacket, LiteNetManager listener) : base(remoteEndPoint, requestPacket, listener) { - Reject(rejectData.Data, 0, rejectData.Length); } + + /// + public new NetPeer AcceptIfKey(string key) => (NetPeer)base.AcceptIfKey(key); + + /// + public new NetPeer Accept() => (NetPeer)base.Accept(); } } diff --git a/LiteNetLib/INetEventListener.cs b/LiteNetLib/INetEventListener.cs index 7589041a..17891c6e 100644 --- a/LiteNetLib/INetEventListener.cs +++ b/LiteNetLib/INetEventListener.cs @@ -1,5 +1,6 @@ -using System.Net; +using System.Net; using System.Net.Sockets; +using LiteNetLib.Utils; namespace LiteNetLib { @@ -17,14 +18,39 @@ public enum UnconnectedMessageType /// public enum DisconnectReason { + /// + /// Connection to host failed + /// ConnectionFailed, + + /// + /// Timeout + /// Timeout, + HostUnreachable, + NetworkUnreachable, + + /// + /// Remote host disconnected peer + /// RemoteConnectionClose, + + /// + /// Disconnect called locally + /// DisconnectPeerCalled, + + /// + /// Connection rejected by remote host + /// ConnectionRejected, + InvalidProtocol, - UnknownHost + UnknownHost, + Reconnect, + PeerToPeerConnection, + PeerNotFound } /// @@ -48,6 +74,9 @@ public struct DisconnectInfo public NetPacketReader AdditionalData; } + /// + /// Interface for implementing own INetEventListener. This is a bit faster than use EventBasedListener + /// public interface INetEventListener { /// @@ -75,15 +104,16 @@ public interface INetEventListener /// /// From peer /// DataReader containing all received data + /// Number of channel at which packet arrived /// Type of received packet - void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod); + void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod); /// /// Received unconnected message /// /// From address (IP and Port) /// Message data - /// Message type (simple, discovery request or responce) + /// Message type (simple, discovery request or response) void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType); /// @@ -98,102 +128,397 @@ public interface INetEventListener /// /// Request information (EndPoint, internal id, additional data) void OnConnectionRequest(ConnectionRequest request); + + /// + /// On reliable message delivered + /// + /// + /// + void OnMessageDelivered(NetPeer peer, object userData) { } + + /// + /// Ntp response + /// + /// + void OnNtpResponse(NtpPacket packet) { } + + /// + /// Called when peer address changed (when AllowPeerAddressChange is enabled) + /// + /// Peer that changed address (with new address) + /// previous IP + void OnPeerAddressChanged(NetPeer peer, IPEndPoint previousAddress) { } + } + + /// + /// Interface for implementing own ILiteNetEventListener. This is a bit faster than use EventBasedListener + /// + public interface ILiteNetEventListener + { + /// + /// New remote peer connected to host, or client connected to remote host + /// + /// Connected peer object + void OnPeerConnected(LiteNetPeer peer); + + /// + /// Peer disconnected + /// + /// disconnected peer + /// additional info about reason, errorCode or data received with disconnect message + void OnPeerDisconnected(LiteNetPeer peer, DisconnectInfo disconnectInfo); + + /// + /// Network error (on send or receive) + /// + /// From endPoint (can be null) + /// Socket error + void OnNetworkError(IPEndPoint endPoint, SocketError socketError) { } + + /// + /// Received some data + /// + /// From peer + /// DataReader containing all received data + /// Type of received packet + void OnNetworkReceive(LiteNetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod); + + /// + /// Received unconnected message + /// + /// From address (IP and Port) + /// Message data + /// Message type (simple, discovery request or response) + void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { } + + /// + /// Latency information updated + /// + /// Peer with updated latency + /// latency value in milliseconds + void OnNetworkLatencyUpdate(LiteNetPeer peer, int latency) { } + + /// + /// On peer connection requested + /// + /// Request information (EndPoint, internal id, additional data) + void OnConnectionRequest(LiteConnectionRequest request); + + /// + /// Called when peer address changed (when AllowPeerAddressChange is enabled) + /// + /// Peer that changed address (with new address) + /// previous IP + void OnPeerAddressChanged(LiteNetPeer peer, IPEndPoint previousAddress) { } + + /// + /// On reliable message delivered + /// + /// + /// + void OnMessageDelivered(LiteNetPeer peer, object userData) { } } + /// + /// Simple event based listener for simple setups and benchmarks + /// public class EventBasedNetListener : INetEventListener { + /// + /// Delegate for the event that occurs when a new peer has successfully connected. + /// + /// The connected peer. public delegate void OnPeerConnected(NetPeer peer); + /// + /// Delegate for the event that occurs when a peer disconnects or the connection is lost. + /// + /// The disconnected peer. + /// Information regarding the reason and data associated with the disconnection. public delegate void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo); + /// + /// Delegate for the event that occurs when a network error is detected in the underlying socket. + /// + /// The endpoint associated with the error. + /// The specific socket error code. public delegate void OnNetworkError(IPEndPoint endPoint, SocketError socketError); - public delegate void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod); + /// + /// Delegate for the event that occurs when data is received from a connected peer. + /// + /// The peer that sent the data. + /// The reader containing the received payload. + /// The channel on which the data was received. + /// The delivery method used for this packet. + public delegate void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod); + /// + /// Delegate for the event that occurs when a message is received from an unconnected endpoint. + /// + /// The endpoint that sent the message. + /// The reader containing the received payload. + /// The type of unconnected message (e.g., Discovery or UnconnectedData). public delegate void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType); + /// + /// Delegate for the event that occurs when the round-trip time (RTT) to a peer is updated. + /// + /// The peer whose latency was updated. + /// The new latency value in milliseconds. public delegate void OnNetworkLatencyUpdate(NetPeer peer, int latency); - + /// + /// Delegate for the event that occurs when a new connection request is received. + /// + /// The connection request object used to accept or reject the connection. public delegate void OnConnectionRequest(ConnectionRequest request); + /// + /// Delegate for the event that occurs when a reliable packet is successfully delivered or acknowledged. + /// + /// The peer that received the packet. + /// The custom user data that was attached to the sent packet. + public delegate void OnDeliveryEvent(NetPeer peer, object userData); + /// + /// Delegate for the event that occurs when an NTP response is received from a time server. + /// + /// The NTP packet containing time information. + public delegate void OnNtpResponseEvent(NtpPacket packet); + /// + /// Delegate for the event that occurs when a peer's remote address changes (roaming). + /// + /// The peer whose address changed. + /// The previous IP endpoint of the peer. + public delegate void OnPeerAddressChangedEvent(NetPeer peer, IPEndPoint previousAddress); + + /// + /// Occurs when a new peer has successfully connected. + /// + public event OnPeerConnected PeerConnectedEvent; + /// + /// Occurs when a peer disconnects or the connection is lost. + /// + public event OnPeerDisconnected PeerDisconnectedEvent; + /// + /// Occurs when a network error is detected in the underlying socket. + /// + public event OnNetworkError NetworkErrorEvent; + /// + /// Occurs when data is received from a connected peer. + /// + public event OnNetworkReceive NetworkReceiveEvent; + /// + /// Occurs when a message is received from an unconnected endpoint. + /// + public event OnNetworkReceiveUnconnected NetworkReceiveUnconnectedEvent; + /// + /// Occurs when the round-trip time (RTT) to a peer is updated. + /// + public event OnNetworkLatencyUpdate NetworkLatencyUpdateEvent; + /// + /// Occurs when a new connection request is received. + /// + public event OnConnectionRequest ConnectionRequestEvent; + /// + /// Occurs when a reliable packet is successfully delivered or acknowledged. + /// + public event OnDeliveryEvent DeliveryEvent; + /// + /// Occurs when an NTP response is received. + /// + public event OnNtpResponseEvent NtpResponseEvent; + /// + /// Occurs when a peer's remote address changes. + /// + public event OnPeerAddressChangedEvent PeerAddressChangedEvent; + + /// Clears all subscribers from . + public void ClearPeerConnectedEvent() => PeerConnectedEvent = null; + /// Clears all subscribers from . + public void ClearPeerDisconnectedEvent() => PeerDisconnectedEvent = null; + /// Clears all subscribers from . + public void ClearNetworkErrorEvent() => NetworkErrorEvent = null; + /// Clears all subscribers from . + public void ClearNetworkReceiveEvent() => NetworkReceiveEvent = null; + /// Clears all subscribers from . + public void ClearNetworkReceiveUnconnectedEvent() => NetworkReceiveUnconnectedEvent = null; + /// Clears all subscribers from . + public void ClearNetworkLatencyUpdateEvent() => NetworkLatencyUpdateEvent = null; + /// Clears all subscribers from . + public void ClearConnectionRequestEvent() => ConnectionRequestEvent = null; + /// Clears all subscribers from . + public void ClearDeliveryEvent() => DeliveryEvent = null; + /// Clears all subscribers from . + public void ClearNtpResponseEvent() => NtpResponseEvent = null; + /// Clears all subscribers from . + public void ClearPeerAddressChangedEvent() => PeerAddressChangedEvent = null; + + void INetEventListener.OnPeerConnected(NetPeer peer) => + PeerConnectedEvent?.Invoke(peer); + + void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) => + PeerDisconnectedEvent?.Invoke(peer, disconnectInfo); + + void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) => + NetworkErrorEvent?.Invoke(endPoint, socketErrorCode); + + void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) => + NetworkReceiveEvent?.Invoke(peer, reader, channelNumber, deliveryMethod); + + void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) => + NetworkReceiveUnconnectedEvent?.Invoke(remoteEndPoint, reader, messageType); + + void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) => + NetworkLatencyUpdateEvent?.Invoke(peer, latency); + + void INetEventListener.OnConnectionRequest(ConnectionRequest request) => + ConnectionRequestEvent?.Invoke(request); + + void INetEventListener.OnMessageDelivered(NetPeer peer, object userData) => + DeliveryEvent?.Invoke(peer, userData); + + void INetEventListener.OnNtpResponse(NtpPacket packet) => + NtpResponseEvent?.Invoke(packet); + + void INetEventListener.OnPeerAddressChanged(NetPeer peer, IPEndPoint previousAddress) => + PeerAddressChangedEvent?.Invoke(peer, previousAddress); + } + + /// + /// Simple event based listener for simple setups and benchmarks + /// + public class EventBasedLiteNetListener : ILiteNetEventListener + { + /// + /// Delegate for the event that occurs when a new peer has successfully connected. + /// + /// The connected peer. + public delegate void OnPeerConnected(LiteNetPeer peer); + /// + /// Delegate for the event that occurs when a peer disconnects or the connection is lost. + /// + /// The disconnected peer. + /// Information regarding the reason and data associated with the disconnection. + public delegate void OnPeerDisconnected(LiteNetPeer peer, DisconnectInfo disconnectInfo); + /// + /// Delegate for the event that occurs when a network error is detected in the underlying socket. + /// + /// The endpoint associated with the error. + /// The specific socket error code. + public delegate void OnNetworkError(IPEndPoint endPoint, SocketError socketError); + /// + /// Delegate for the event that occurs when data is received from a connected peer. + /// + /// The peer that sent the data. + /// The reader containing the received payload. + /// The delivery method used for this packet. + public delegate void OnNetworkReceive(LiteNetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod); + /// + /// Delegate for the event that occurs when a message is received from an unconnected endpoint. + /// + /// The endpoint that sent the message. + /// The reader containing the received payload. + /// The type of unconnected message (e.g., Discovery or UnconnectedData). + public delegate void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType); + /// + /// Delegate for the event that occurs when the round-trip time (RTT) to a peer is updated. + /// + /// The peer whose latency was updated. + /// The new latency value in milliseconds. + public delegate void OnNetworkLatencyUpdate(LiteNetPeer peer, int latency); + /// + /// Delegate for the event that occurs when a new connection request is received. + /// + /// The connection request object used to accept or reject the connection. + public delegate void OnConnectionRequest(LiteConnectionRequest request); + /// + /// Delegate for the event that occurs when a reliable packet is successfully delivered or acknowledged. + /// + /// The peer that received the packet. + /// The custom user data that was attached to the sent packet. + public delegate void OnDeliveryEvent(LiteNetPeer peer, object userData); + /// + /// Delegate for the event that occurs when a peer's remote address changes (roaming). + /// + /// The peer whose address changed. + /// The previous IP endpoint of the peer. + public delegate void OnPeerAddressChangedEvent(LiteNetPeer peer, IPEndPoint previousAddress); + /// + /// Occurs when a new peer has successfully connected. + /// public event OnPeerConnected PeerConnectedEvent; + /// + /// Occurs when a peer disconnects or the connection is lost. + /// public event OnPeerDisconnected PeerDisconnectedEvent; + /// + /// Occurs when a network error is detected in the underlying socket. + /// public event OnNetworkError NetworkErrorEvent; + /// + /// Occurs when data is received from a connected peer. + /// public event OnNetworkReceive NetworkReceiveEvent; + /// + /// Occurs when a message is received from an unconnected endpoint. + /// public event OnNetworkReceiveUnconnected NetworkReceiveUnconnectedEvent; + /// + /// Occurs when the round-trip time (RTT) to a peer is updated. + /// public event OnNetworkLatencyUpdate NetworkLatencyUpdateEvent; + /// + /// Occurs when a new connection request is received. + /// public event OnConnectionRequest ConnectionRequestEvent; + /// + /// Occurs when a reliable packet is successfully delivered or acknowledged. + /// + public event OnDeliveryEvent DeliveryEvent; + /// + /// Occurs when a peer's remote address changes. + /// + public event OnPeerAddressChangedEvent PeerAddressChangedEvent; + + /// Clears all subscribers from . + public void ClearPeerConnectedEvent() => PeerConnectedEvent = null; + /// Clears all subscribers from . + public void ClearPeerDisconnectedEvent() => PeerDisconnectedEvent = null; + /// Clears all subscribers from . + public void ClearNetworkErrorEvent() => NetworkErrorEvent = null; + /// Clears all subscribers from . + public void ClearNetworkReceiveEvent() => NetworkReceiveEvent = null; + /// Clears all subscribers from . + public void ClearNetworkReceiveUnconnectedEvent() => NetworkReceiveUnconnectedEvent = null; + /// Clears all subscribers from . + public void ClearNetworkLatencyUpdateEvent() => NetworkLatencyUpdateEvent = null; + /// Clears all subscribers from . + public void ClearConnectionRequestEvent() => ConnectionRequestEvent = null; + /// Clears all subscribers from . + public void ClearDeliveryEvent() => DeliveryEvent = null; + /// Clears all subscribers from . + public void ClearPeerAddressChangedEvent() => PeerAddressChangedEvent = null; + + void ILiteNetEventListener.OnPeerConnected(LiteNetPeer peer) => + PeerConnectedEvent?.Invoke(peer); + + void ILiteNetEventListener.OnPeerDisconnected(LiteNetPeer peer, DisconnectInfo disconnectInfo) => + PeerDisconnectedEvent?.Invoke(peer, disconnectInfo); + + void ILiteNetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) => + NetworkErrorEvent?.Invoke(endPoint, socketErrorCode); + + void ILiteNetEventListener.OnNetworkReceive(LiteNetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) => + NetworkReceiveEvent?.Invoke(peer, reader, deliveryMethod); + + void ILiteNetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) => + NetworkReceiveUnconnectedEvent?.Invoke(remoteEndPoint, reader, messageType); + + void ILiteNetEventListener.OnNetworkLatencyUpdate(LiteNetPeer peer, int latency) => + NetworkLatencyUpdateEvent?.Invoke(peer, latency); + + void ILiteNetEventListener.OnConnectionRequest(LiteConnectionRequest request) => + ConnectionRequestEvent?.Invoke(request); + + void ILiteNetEventListener.OnMessageDelivered(LiteNetPeer peer, object userData) => + DeliveryEvent?.Invoke(peer, userData); - public void ClearPeerConnectedEvent() - { - PeerConnectedEvent = null; - } - - public void ClearPeerDisconnectedEvent() - { - PeerDisconnectedEvent = null; - } - - public void ClearNetworkErrorEvent() - { - NetworkErrorEvent = null; - } - - public void ClearNetworkReceiveEvent() - { - NetworkReceiveEvent = null; - } - - public void ClearNetworkReceiveUnconnectedEvent() - { - NetworkReceiveUnconnectedEvent = null; - } - - public void ClearNetworkLatencyUpdateEvent() - { - NetworkLatencyUpdateEvent = null; - } - - public void ClearConnectionRequestEvent() - { - ConnectionRequestEvent = null; - } - - void INetEventListener.OnPeerConnected(NetPeer peer) - { - if (PeerConnectedEvent != null) - PeerConnectedEvent(peer); - } - - void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) - { - if (PeerDisconnectedEvent != null) - PeerDisconnectedEvent(peer, disconnectInfo); - } - - void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) - { - if (NetworkErrorEvent != null) - NetworkErrorEvent(endPoint, socketErrorCode); - } - - void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) - { - if (NetworkReceiveEvent != null) - NetworkReceiveEvent(peer, reader, deliveryMethod); - } - - void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) - { - if (NetworkReceiveUnconnectedEvent != null) - NetworkReceiveUnconnectedEvent(remoteEndPoint, reader, messageType); - } - - void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) - { - if (NetworkLatencyUpdateEvent != null) - NetworkLatencyUpdateEvent(peer, latency); - } - - void INetEventListener.OnConnectionRequest(ConnectionRequest request) - { - if (ConnectionRequestEvent != null) - ConnectionRequestEvent(request); - } + void ILiteNetEventListener.OnPeerAddressChanged(LiteNetPeer peer, IPEndPoint previousAddress) => + PeerAddressChangedEvent?.Invoke(peer, previousAddress); } } diff --git a/LiteNetLib/InternalPackets.cs b/LiteNetLib/InternalPackets.cs new file mode 100644 index 00000000..6cd9a8db --- /dev/null +++ b/LiteNetLib/InternalPackets.cs @@ -0,0 +1,131 @@ +using System; +using System.Net; +using LiteNetLib.Utils; + +namespace LiteNetLib +{ + public sealed class NetConnectRequestPacket + { + public const int HeaderSize = 18; + public readonly long ConnectionTime; + public byte ConnectionNumber; + public readonly byte[] TargetAddress; + public readonly NetDataReader Data; + public readonly int PeerId; + + private NetConnectRequestPacket(long connectionTime, byte connectionNumber, int localId, byte[] targetAddress, NetDataReader data) + { + ConnectionTime = connectionTime; + ConnectionNumber = connectionNumber; + TargetAddress = targetAddress; + Data = data; + PeerId = localId; + } + + internal static int GetProtocolId(NetPacket packet) => + BitConverter.ToInt32(packet.RawData, 1); + + internal static NetConnectRequestPacket FromData(NetPacket packet) + { + if (packet.ConnectionNumber >= NetConstants.MaxConnectionNumber) + return null; + + //Getting connection time for peer + long connectionTime = BitConverter.ToInt64(packet.RawData, 5); + + //Get peer id + int peerId = BitConverter.ToInt32(packet.RawData, 13); + + //Get target address + int addrSize = packet.RawData[HeaderSize-1]; + if (addrSize != 16 && addrSize != 28) + return null; + byte[] addressBytes = new byte[addrSize]; + Buffer.BlockCopy(packet.RawData, HeaderSize, addressBytes, 0, addrSize); + + // Read data and create request + var reader = new NetDataReader(null, 0, 0); + if (packet.Size > HeaderSize+addrSize) + reader.SetSource(packet.RawData, HeaderSize + addrSize, packet.Size); + + return new NetConnectRequestPacket(connectionTime, packet.ConnectionNumber, peerId, addressBytes, reader); + } + + internal static NetPacket Make(ReadOnlySpan connectData, SocketAddress addressBytes, long connectTime, int localId) + { + //Make initial packet + var packet = new NetPacket(PacketProperty.ConnectRequest, connectData.Length+addressBytes.Size); + + //Add data + FastBitConverter.GetBytes(packet.RawData, 1, NetConstants.ProtocolId); + FastBitConverter.GetBytes(packet.RawData, 5, connectTime); + FastBitConverter.GetBytes(packet.RawData, 13, localId); + packet.RawData[HeaderSize - 1] = (byte)addressBytes.Size; + for (int i = 0; i < addressBytes.Size; i++) + packet.RawData[HeaderSize + i] = addressBytes[i]; + connectData.CopyTo(packet.RawData.AsSpan(HeaderSize + addressBytes.Size)); + return packet; + } + } + + internal sealed class NetConnectAcceptPacket + { + public const int Size = 15; + public readonly long ConnectionTime; + public readonly byte ConnectionNumber; + public readonly int PeerId; + public readonly bool PeerNetworkChanged; + + private NetConnectAcceptPacket(long connectionTime, byte connectionNumber, int peerId, bool peerNetworkChanged) + { + ConnectionTime = connectionTime; + ConnectionNumber = connectionNumber; + PeerId = peerId; + PeerNetworkChanged = peerNetworkChanged; + } + + public static NetConnectAcceptPacket FromData(NetPacket packet) + { + if (packet.Size != Size) + return null; + + long connectionId = BitConverter.ToInt64(packet.RawData, 1); + + //check connect num + byte connectionNumber = packet.RawData[9]; + if (connectionNumber >= NetConstants.MaxConnectionNumber) + return null; + + //check reused flag + byte isReused = packet.RawData[10]; + if (isReused > 1) + return null; + + //get remote peer id + int peerId = BitConverter.ToInt32(packet.RawData, 11); + if (peerId < 0) + return null; + + return new NetConnectAcceptPacket(connectionId, connectionNumber, peerId, isReused == 1); + } + + public static NetPacket Make(long connectTime, byte connectNum, int localPeerId) + { + var packet = new NetPacket(PacketProperty.ConnectAccept, 0); + FastBitConverter.GetBytes(packet.RawData, 1, connectTime); + packet.RawData[9] = connectNum; + FastBitConverter.GetBytes(packet.RawData, 11, localPeerId); + return packet; + } + + public static NetPacket MakeNetworkChanged(LiteNetPeer peer) + { + var packet = new NetPacket(PacketProperty.PeerNotFound, Size-1); + FastBitConverter.GetBytes(packet.RawData, 1, peer.ConnectTime); + packet.RawData[9] = peer.ConnectionNum; + packet.RawData[10] = 1; + FastBitConverter.GetBytes(packet.RawData, 11, peer.RemoteId); + return packet; + } + } +} diff --git a/LiteNetLib/Layers/Crc32cLayer.cs b/LiteNetLib/Layers/Crc32cLayer.cs new file mode 100644 index 00000000..dc07fe19 --- /dev/null +++ b/LiteNetLib/Layers/Crc32cLayer.cs @@ -0,0 +1,41 @@ +using LiteNetLib.Utils; +using System; +using System.Net; + +namespace LiteNetLib.Layers +{ + public sealed class Crc32cLayer : PacketLayerBase + { + public Crc32cLayer() : base(CRC32C.ChecksumSize) + { + + } + + public override void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int length) + { + if (length < NetConstants.HeaderSize + CRC32C.ChecksumSize) + { + NetDebug.WriteError("[NM] DataReceived size: bad!"); + //Set length to 0 to have netManager drop the packet. + length = 0; + return; + } + + int checksumPoint = length - CRC32C.ChecksumSize; + if (CRC32C.Compute(data, 0, checksumPoint) != BitConverter.ToUInt32(data, checksumPoint)) + { + NetDebug.Write("[NM] DataReceived checksum: bad!"); + //Set length to 0 to have netManager drop the packet. + length = 0; + return; + } + length -= CRC32C.ChecksumSize; + } + + public override void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) + { + FastBitConverter.GetBytes(data, length, CRC32C.Compute(data, offset, length)); + length += CRC32C.ChecksumSize; + } + } +} diff --git a/LiteNetLib/Layers/PacketLayerBase.cs b/LiteNetLib/Layers/PacketLayerBase.cs new file mode 100644 index 00000000..f2e4c881 --- /dev/null +++ b/LiteNetLib/Layers/PacketLayerBase.cs @@ -0,0 +1,17 @@ +using System.Net; + +namespace LiteNetLib.Layers +{ + public abstract class PacketLayerBase + { + public readonly int ExtraPacketSizeForLayer; + + protected PacketLayerBase(int extraPacketSizeForLayer) + { + ExtraPacketSizeForLayer = extraPacketSizeForLayer; + } + + public abstract void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int length); + public abstract void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length); + } +} diff --git a/LiteNetLib/Layers/XorEncryptLayer.cs b/LiteNetLib/Layers/XorEncryptLayer.cs new file mode 100644 index 00000000..a3a130bf --- /dev/null +++ b/LiteNetLib/Layers/XorEncryptLayer.cs @@ -0,0 +1,59 @@ +using System; +using System.Net; +using System.Text; + +namespace LiteNetLib.Layers +{ + public class XorEncryptLayer : PacketLayerBase + { + private byte[] _byteKey; + + public XorEncryptLayer() : base(0) + { + + } + + public XorEncryptLayer(byte[] key) : this() + { + SetKey(key); + } + + public XorEncryptLayer(string key) : this() + { + SetKey(key); + } + + public void SetKey(string key) + { + _byteKey = Encoding.UTF8.GetBytes(key); + } + + public void SetKey(byte[] key) + { + if (_byteKey == null || _byteKey.Length != key.Length) + _byteKey = new byte[key.Length]; + Buffer.BlockCopy(key, 0, _byteKey, 0, key.Length); + } + + public override void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int length) + { + if (_byteKey == null) + return; + for (int i = 0; i < length; i++) + { + data[i] = (byte)(data[i] ^ _byteKey[i % _byteKey.Length]); + } + } + + public override void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) + { + if (_byteKey == null) + return; + int cur = offset; + for (int i = 0; i < length; i++, cur++) + { + data[cur] = (byte)(data[cur] ^ _byteKey[i % _byteKey.Length]); + } + } + } +} diff --git a/LiteNetLib/LiteNetLib.asmdef b/LiteNetLib/LiteNetLib.asmdef new file mode 100644 index 00000000..530c72ed --- /dev/null +++ b/LiteNetLib/LiteNetLib.asmdef @@ -0,0 +1,13 @@ +{ + "name": "LiteNetLib", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/LiteNetLib/LiteNetLib.csproj b/LiteNetLib/LiteNetLib.csproj index 67493698..fa18ce06 100644 --- a/LiteNetLib/LiteNetLib.csproj +++ b/LiteNetLib/LiteNetLib.csproj @@ -3,27 +3,58 @@ LiteNetLib LiteNetLib - 4 - net35;netstandard2.0;netcoreapp2.0 + net8.0;netstandard2.1 + true + Library + 8 + + true + 1701;1702;1705;1591 + 2.1.3 + Lite reliable UDP library for Mono and .NET + true + true + 2.1.3 - + TRACE;DEBUG - 1701;1702;1705;1591 - 4 - + TRACE - bin\Release\net35\LiteNetLib.xml - 1701;1702;1705;1591 - - - - - - + + $(DefineConstants);SIMULATE_NETWORK + + + + true + $(DefineConstants);LITENETLIB_UNSAFE + udp reliable-udp network + https://github.com/RevenantX/LiteNetLib/releases/tag/2.1.3 + git + https://github.com/RevenantX/LiteNetLib + https://github.com/RevenantX/LiteNetLib + MIT + True + Ruslan Pyrch + Copyright 2026 Ruslan Pyrch + Lite reliable UDP library for .NET, Mono, and .NET Core + LNL.png + README.md + + + + + True + \ + + + True + \ + + \ No newline at end of file diff --git a/LiteNetLib/LiteNetManager.HashSet.cs b/LiteNetLib/LiteNetManager.HashSet.cs new file mode 100644 index 00000000..11fdd114 --- /dev/null +++ b/LiteNetLib/LiteNetManager.HashSet.cs @@ -0,0 +1,323 @@ +using System; +using System.Net; +using System.Threading; + +namespace LiteNetLib +{ + //minimal hashset class from dotnet with some optimizations + public partial class LiteNetManager + { + private const int MaxPrimeArrayLength = 0x7FFFFFC3; + private const int HashPrime = 101; + private const int Lower31BitMask = 0x7FFFFFFF; + private static readonly int[] Primes = + { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, + 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, + 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, + 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, + 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 + }; + + private static int HashSetGetPrime(int min) + { + foreach (int prime in Primes) + { + if (prime >= min) + return prime; + } + + // Outside of our predefined table. Compute the hard way. + for (int i = (min | 1); i < int.MaxValue; i += 2) + { + if (IsPrime(i) && ((i - 1) % HashPrime != 0)) + return i; + } + return min; + + bool IsPrime(int candidate) + { + if ((candidate & 1) != 0) + { + int limit = (int)Math.Sqrt(candidate); + for (int divisor = 3; divisor <= limit; divisor += 2) + { + if (candidate % divisor == 0) + return false; + } + return true; + } + return candidate == 2; + } + } + + private struct Slot + { + internal int HashCode; + internal int Next; + internal LiteNetPeer Value; + } + + private int[] _buckets; + private Slot[] _slots; + private int _count; + private int _lastIndex; + private int _freeList = -1; + private LiteNetPeer[] _peersArray = new LiteNetPeer[32]; + + protected readonly ReaderWriterLockSlim _peersLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); + protected volatile LiteNetPeer _headPeer; + + private void ClearPeerSet() + { + _peersLock.EnterWriteLock(); + _headPeer = null; + if (_lastIndex > 0) + { + Array.Clear(_slots, 0, _lastIndex); + Array.Clear(_buckets, 0, _buckets.Length); + _lastIndex = 0; + _count = 0; + _freeList = -1; + } + _peersArray = new LiteNetPeer[32]; + _peersLock.ExitWriteLock(); + } + + protected bool ContainsPeer(LiteNetPeer item) + { + if (item == null) + { + NetDebug.WriteError($"Contains peer null: {item}"); + return false; + } + if (_buckets != null) + { + int hashCode = item.GetHashCode() & Lower31BitMask; + for (int i = _buckets[hashCode % _buckets.Length] - 1; i >= 0; i = _slots[i].Next) + { + if (_slots[i].HashCode == hashCode && _slots[i].Value.Equals(item)) + return true; + } + } + return false; + } + + /// + /// Gets peer by peer id + /// + /// id of peer + /// Peer if peer with id exist, otherwise null + public LiteNetPeer GetPeerById(int id) + { + return id >= 0 && id < _peersArray.Length ? _peersArray[id] : null; + } + + /// + /// Gets peer by peer id + /// + /// id of peer + /// resulting peer + /// if peer with id exist; otherwise + public bool TryGetPeerById(int id, out LiteNetPeer peer) + { + peer = GetPeerById(id); + return peer != null; + } + + private void AddPeer(LiteNetPeer peer) + { + if (peer == null) + { + NetDebug.WriteError($"Add peer null: {peer}"); + return; + } + _peersLock.EnterWriteLock(); + if (_headPeer != null) + { + peer.NextPeer = _headPeer; + _headPeer.PrevPeer = peer; + } + _headPeer = peer; + AddPeerToSet(peer); + if (peer.Id >= _peersArray.Length) + { + int newSize = _peersArray.Length * 2; + while (peer.Id >= newSize) + newSize *= 2; + Array.Resize(ref _peersArray, newSize); + } + _peersArray[peer.Id] = peer; + _peersLock.ExitWriteLock(); + } + + private void RemovePeer(LiteNetPeer peer, bool enableWriteLock) + { + if(enableWriteLock) + _peersLock.EnterWriteLock(); + if (!RemovePeerFromSet(peer)) + { + if(enableWriteLock) + _peersLock.ExitWriteLock(); + return; + } + if (peer == _headPeer) + _headPeer = peer.NextPeer; + + if (peer.PrevPeer != null) + peer.PrevPeer.NextPeer = peer.NextPeer; + if (peer.NextPeer != null) + peer.NextPeer.PrevPeer = peer.PrevPeer; + peer.PrevPeer = null; + + _peersArray[peer.Id] = null; + _peerIds.Enqueue(peer.Id); + + if(enableWriteLock) + _peersLock.ExitWriteLock(); + } + + protected bool RemovePeerFromSet(LiteNetPeer peer) + { + if (_buckets == null || peer == null) + return false; + int hashCode = peer.GetHashCode() & Lower31BitMask; + int bucket = hashCode % _buckets.Length; + int last = -1; + for (int i = _buckets[bucket] - 1; i >= 0; last = i, i = _slots[i].Next) + { + if (_slots[i].HashCode == hashCode && _slots[i].Value.Equals(peer)) + { + if (last < 0) + _buckets[bucket] = _slots[i].Next + 1; + else + _slots[last].Next = _slots[i].Next; + _slots[i].HashCode = -1; + _slots[i].Value = null; + _slots[i].Next = _freeList; + + _count--; + if (_count == 0) + { + _lastIndex = 0; + _freeList = -1; + } + else + { + _freeList = i; + } + return true; + } + } + return false; + } + + private bool TryGetPeer(IPEndPoint endPoint, out LiteNetPeer actualValue) + { + if (_buckets != null) + { +#if NET8_0_OR_GREATER + //can be NetPeer or IPEndPoint + int hashCode = (UseNativeSockets ? endPoint.GetHashCode() : endPoint.Serialize().GetHashCode()) & Lower31BitMask; +#else + int hashCode = endPoint.GetHashCode() & Lower31BitMask; +#endif + _peersLock.EnterReadLock(); + for (int i = _buckets[hashCode % _buckets.Length] - 1; i >= 0; i = _slots[i].Next) + { + if (_slots[i].HashCode == hashCode && _slots[i].Value.Equals(endPoint)) + { + actualValue = _slots[i].Value; + _peersLock.ExitReadLock(); + return true; + } + } + _peersLock.ExitReadLock(); + } + actualValue = null; + return false; + } + + //only used for NET8 + private bool TryGetPeer(SocketAddress saddr, out LiteNetPeer actualValue) + { + if (_buckets != null) + { + int hashCode = saddr.GetHashCode() & Lower31BitMask; + _peersLock.EnterReadLock(); + for (int i = _buckets[hashCode % _buckets.Length] - 1; i >= 0; i = _slots[i].Next) + { + if (_slots[i].HashCode == hashCode && _slots[i].Value.Serialize().Equals(saddr)) + { + actualValue = _slots[i].Value; + _peersLock.ExitReadLock(); + return true; + } + } + _peersLock.ExitReadLock(); + } + actualValue = null; + return false; + } + + protected bool AddPeerToSet(LiteNetPeer value) + { + if (_buckets == null) + { + int size = HashSetGetPrime(0); + _buckets = new int[size]; + _slots = new Slot[size]; + } + + int hashCode = value.GetHashCode() & Lower31BitMask; + int bucket = hashCode % _buckets.Length; + for (int i = _buckets[hashCode % _buckets.Length] - 1; i >= 0; i = _slots[i].Next) + { + if (_slots[i].HashCode == hashCode && _slots[i].Value.Equals(value)) + return false; + } + + int index; + if (_freeList >= 0) + { + index = _freeList; + _freeList = _slots[index].Next; + } + else + { + if (_lastIndex == _slots.Length) + { + //increase capacity + int newSize = 2 * _count; + newSize = (uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > _count + ? MaxPrimeArrayLength + : HashSetGetPrime(newSize); + + // Able to increase capacity; copy elements to larger array and rehash + Slot[] newSlots = new Slot[newSize]; + Array.Copy(_slots, 0, newSlots, 0, _lastIndex); + _buckets = new int[newSize]; + for (int i = 0; i < _lastIndex; i++) + { + int b = newSlots[i].HashCode % newSize; + newSlots[i].Next = _buckets[b] - 1; + _buckets[b] = i + 1; + } + _slots = newSlots; + // this will change during resize + bucket = hashCode % _buckets.Length; + } + index = _lastIndex; + _lastIndex++; + } + _slots[index].HashCode = hashCode; + _slots[index].Value = value; + _slots[index].Next = _buckets[bucket] - 1; + _buckets[bucket] = index + 1; + _count++; + + return true; + } + } + +} diff --git a/LiteNetLib/LiteNetManager.PacketPool.cs b/LiteNetLib/LiteNetManager.PacketPool.cs new file mode 100644 index 00000000..fd8520f6 --- /dev/null +++ b/LiteNetLib/LiteNetManager.PacketPool.cs @@ -0,0 +1,82 @@ +using System; + +namespace LiteNetLib +{ + public partial class LiteNetManager + { + private NetPacket _poolHead; + private int _poolCount; + private readonly object _poolLock = new object(); + + /// + /// Maximum packet pool size (increase if you have tons of packets sending) + /// + public int PacketPoolSize = 1000; + + public int PoolCount => _poolCount; + + private NetPacket PoolGetWithData(PacketProperty property, byte[] data, int start, int length) + { + int headerSize = NetPacket.GetHeaderSize(property); + NetPacket packet = PoolGetPacket(length + headerSize); + packet.Property = property; + Buffer.BlockCopy(data, start, packet.RawData, headerSize, length); + return packet; + } + + //Get packet with size + private NetPacket PoolGetWithProperty(PacketProperty property, int size) + { + NetPacket packet = PoolGetPacket(size + NetPacket.GetHeaderSize(property)); + packet.Property = property; + return packet; + } + + private NetPacket PoolGetWithProperty(PacketProperty property) + { + NetPacket packet = PoolGetPacket(NetPacket.GetHeaderSize(property)); + packet.Property = property; + return packet; + } + + internal NetPacket PoolGetPacket(int size) + { + if (size > NetConstants.MaxPacketSize) + return new NetPacket(size); + + NetPacket packet; + lock (_poolLock) + { + packet = _poolHead; + if (packet == null) + return new NetPacket(size); + + _poolHead = _poolHead.Next; + _poolCount--; + } + + packet.Size = size; + if (packet.RawData.Length < size) + packet.RawData = new byte[size]; + return packet; + } + + internal void PoolRecycle(NetPacket packet) + { + if (packet.RawData.Length > NetConstants.MaxPacketSize || _poolCount >= PacketPoolSize) + { + //Don't pool big packets. Save memory + return; + } + + //Clean fragmented flag + packet.RawData[0] = 0; + lock (_poolLock) + { + packet.Next = _poolHead; + _poolHead = packet; + _poolCount++; + } + } + } +} diff --git a/LiteNetLib/LiteNetManager.Socket.cs b/LiteNetLib/LiteNetManager.Socket.cs new file mode 100644 index 00000000..2f1e35a3 --- /dev/null +++ b/LiteNetLib/LiteNetManager.Socket.cs @@ -0,0 +1,736 @@ +#if UNITY_2018_3_OR_NEWER +#define UNITY_SOCKET_FIX +#endif +using System.Runtime.InteropServices; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using LiteNetLib.Utils; + +namespace LiteNetLib +{ + public partial class LiteNetManager + { + protected Socket _udpSocketv4; + private Socket _udpSocketv6; + private Thread _receiveThread; + private IPEndPoint _bufferEndPointv4; + private IPEndPoint _bufferEndPointv6; +#if UNITY_SOCKET_FIX + private PausedSocketFix _pausedSocketFix; + private bool _useSocketFix; +#endif + +#if NET8_0_OR_GREATER + private readonly SocketAddress _sockAddrCacheV4 = new SocketAddress(AddressFamily.InterNetwork); + private readonly SocketAddress _sockAddrCacheV6 = new SocketAddress(AddressFamily.InterNetworkV6); +#endif + + private const int SioUdpConnreset = -1744830452; //SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12 + private static readonly IPAddress MulticastAddressV6 = IPAddress.Parse("ff02::1"); + public static readonly bool IPv6Support; + + // special case in iOS (and possibly android that should be resolved in unity) + internal bool NotConnected; + + /// + /// Poll timeout in microseconds. Increasing can slightly increase performance in cost of slow NetManager.Stop(Socket.Close) + /// + public int ReceivePollingTime = 50000; //0.05 second + + public short Ttl + { + get + { +#if UNITY_SWITCH + return 0; +#else + return _udpSocketv4.Ttl; +#endif + } + internal set + { +#if !UNITY_SWITCH + _udpSocketv4.Ttl = value; +#endif + } + } + + static LiteNetManager() + { +#if DISABLE_IPV6 + IPv6Support = false; +#elif !UNITY_2019_1_OR_NEWER && !UNITY_2018_4_OR_NEWER && (!UNITY_EDITOR && ENABLE_IL2CPP) + string version = UnityEngine.Application.unityVersion; + IPv6Support = Socket.OSSupportsIPv6 && int.Parse(version.Remove(version.IndexOf('f')).Split('.')[2]) >= 6; +#else + IPv6Support = Socket.OSSupportsIPv6; +#endif + } + + private bool ProcessError(SocketException ex) + { + switch (ex.SocketErrorCode) + { + case SocketError.NotConnected: + NotConnected = true; + return true; + case SocketError.Interrupted: + case SocketError.NotSocket: + case SocketError.OperationAborted: + return true; + case SocketError.ConnectionReset: + case SocketError.MessageSize: + case SocketError.TimedOut: + case SocketError.NetworkReset: + case SocketError.WouldBlock: + //NetDebug.Write($"[R]Ignored error: {(int)ex.SocketErrorCode} - {ex}"); + break; + default: + NetDebug.WriteError($"[R]Error code: {(int)ex.SocketErrorCode} - {ex}"); + CreateEvent(NetEvent.EType.Error, errorCode: ex.SocketErrorCode); + break; + } + return false; + } + + private void ManualReceive(Socket socket, EndPoint bufferEndPoint) + { + //Reading data + try + { + int count = MaxPacketPerManualReceive; + while (socket.Available > 0 && (count > 0 || MaxPacketPerManualReceive == 0)) + { + if(ReceiveFrom(socket, ref bufferEndPoint) > 0) + count--; + else + break; + } + } + catch (SocketException ex) + { + ProcessError(ex); + } + catch (ObjectDisposedException) + { + + } + catch (Exception e) + { + //protects socket receive thread + NetDebug.WriteError("[NM] SocketReceiveThread error: " + e); + } + } + + private void NativeReceiveLogic() + { + IntPtr socketHandle4 = _udpSocketv4.Handle; + IntPtr socketHandle6 = _udpSocketv6?.Handle ?? IntPtr.Zero; + byte[] addrBuffer4 = new byte[NativeSocket.IPv4AddrSize]; + byte[] addrBuffer6 = new byte[NativeSocket.IPv6AddrSize]; + var tempEndPoint = new IPEndPoint(IPAddress.Any, 0); + var selectReadList = new List(2); + var socketv4 = _udpSocketv4; + var socketV6 = _udpSocketv6; + var packet = PoolGetPacket(NetConstants.MaxPacketSize); + + while (_isRunning) + { + try + { + if (socketV6 == null) + { + if (NativeReceiveFrom(socketHandle4, addrBuffer4) == false) + return; + continue; + } + bool messageReceived = false; + if (socketv4.Available != 0 || selectReadList.Contains(socketv4)) + { + if (NativeReceiveFrom(socketHandle4, addrBuffer4) == false) + return; + messageReceived = true; + } + if (socketV6.Available != 0 || selectReadList.Contains(socketV6)) + { + if (NativeReceiveFrom(socketHandle6, addrBuffer6) == false) + return; + messageReceived = true; + } + + selectReadList.Clear(); + + if (messageReceived) + continue; + + selectReadList.Add(socketv4); + selectReadList.Add(socketV6); + + Socket.Select(selectReadList, null, null, ReceivePollingTime); + } + catch (SocketException ex) + { + if (ProcessError(ex)) + return; + } + catch (ObjectDisposedException) + { + //socket closed + return; + } + catch (ThreadAbortException) + { + //thread closed + return; + } + catch (Exception e) + { + //protects socket receive thread + NetDebug.WriteError("[NM] SocketReceiveThread error: " + e); + } + } + + bool NativeReceiveFrom(IntPtr s, byte[] address) + { + int addrSize = address.Length; + packet.Size = NativeSocket.RecvFrom(s, packet.RawData, NetConstants.MaxPacketSize, address, ref addrSize); + if (packet.Size == 0) + return true; //socket closed or empty packet + + if (packet.Size == -1) + { + //Linux timeout EAGAIN + return ProcessError(new SocketException((int)NativeSocket.GetSocketError())) == false; + } + + //NetDebug.WriteForce($"[R]Received data from {endPoint}, result: {packet.Size}"); + //refresh temp Addr/Port + short family = (short)((address[1] << 8) | address[0]); + tempEndPoint.Port = (ushort)((address[2] << 8) | address[3]); + if ((NativeSocket.UnixMode && family == NativeSocket.AF_INET6) || (!NativeSocket.UnixMode && (AddressFamily)family == AddressFamily.InterNetworkV6)) + { + uint scope = unchecked((uint)( + (address[27] << 24) + + (address[26] << 16) + + (address[25] << 8) + + (address[24]))); + tempEndPoint.Address = new IPAddress(new ReadOnlySpan(address, 8, 16), scope); + } + else //IPv4 + { + long ipv4Addr = unchecked((uint)((address[4] & 0x000000FF) | + (address[5] << 8 & 0x0000FF00) | + (address[6] << 16 & 0x00FF0000) | + (address[7] << 24))); + tempEndPoint.Address = new IPAddress(ipv4Addr); + } + + if (TryGetPeer(tempEndPoint, out var peer)) + { + //use cached native ep + OnMessageReceived(packet, peer); + } + else + { + OnMessageReceived(packet, tempEndPoint); + tempEndPoint = new IPEndPoint(IPAddress.Any, 0); + } + packet = PoolGetPacket(NetConstants.MaxPacketSize); + return true; + } + } + + private int ReceiveFrom(Socket s, ref EndPoint bufferEndPoint) + { + var packet = PoolGetPacket(NetConstants.MaxPacketSize); +#if NET8_0_OR_GREATER + var sockAddr = s.AddressFamily == AddressFamily.InterNetwork ? _sockAddrCacheV4 : _sockAddrCacheV6; + packet.Size = s.ReceiveFrom(new Span(packet.RawData, 0, NetConstants.MaxPacketSize), SocketFlags.None, sockAddr); + OnMessageReceived(packet, TryGetPeer(sockAddr, out var peer) ? peer : (IPEndPoint)bufferEndPoint.Create(sockAddr)); +#else + packet.Size = s.ReceiveFrom(packet.RawData, 0, NetConstants.MaxPacketSize, SocketFlags.None, ref bufferEndPoint); + OnMessageReceived(packet, (IPEndPoint)bufferEndPoint); +#endif + return packet.Size; + } + + private void ReceiveLogic() + { + EndPoint bufferEndPoint4 = new IPEndPoint(IPAddress.Any, 0); + EndPoint bufferEndPoint6 = new IPEndPoint(IPAddress.IPv6Any, 0); + var selectReadList = new List(2); + var socketv4 = _udpSocketv4; + var socketV6 = _udpSocketv6; + + while (_isRunning) + { + //Reading data + try + { + if (socketV6 == null) + { + if (socketv4.Available == 0 && !socketv4.Poll(ReceivePollingTime, SelectMode.SelectRead)) + continue; + ReceiveFrom(socketv4, ref bufferEndPoint4); + } + else + { + bool messageReceived = false; + if (socketv4.Available != 0 || selectReadList.Contains(socketv4)) + { + ReceiveFrom(socketv4, ref bufferEndPoint4); + messageReceived = true; + } + if (socketV6.Available != 0 || selectReadList.Contains(socketV6)) + { + ReceiveFrom(socketV6, ref bufferEndPoint6); + messageReceived = true; + } + + selectReadList.Clear(); + + if (messageReceived) + continue; + + selectReadList.Add(socketv4); + selectReadList.Add(socketV6); + Socket.Select(selectReadList, null, null, ReceivePollingTime); + } + //NetDebug.Write(NetLogLevel.Trace, $"[R]Received data from {bufferEndPoint}, result: {packet.Size}"); + } + catch (SocketException ex) + { + if (ProcessError(ex)) + return; + } + catch (ObjectDisposedException) + { + //socket closed + return; + } + catch (ThreadAbortException) + { + //thread closed + return; + } + catch (Exception e) + { + //protects socket receive thread + NetDebug.WriteError("[NM] SocketReceiveThread error: " + e); + } + } + } + + /// + /// Start logic thread and listening on selected port + /// + /// bind to specific ipv4 address + /// bind to specific ipv6 address + /// port to listen + /// + /// When , disables internal background threads.
+ /// You must manually call and .

+ /// Can be used when e.g. 10,000 instances are running on the same machine, reducing the amount of threads used. + /// + public bool Start(IPAddress addressIPv4, IPAddress addressIPv6, int port, bool manualMode) + { + if (IsRunning && NotConnected == false) + return false; + + NotConnected = false; + _manualMode = manualMode; + UseNativeSockets = UseNativeSockets && NativeSocket.IsSupported; + _udpSocketv4 = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + if (!BindSocket(_udpSocketv4, new IPEndPoint(addressIPv4, port))) + return false; + + LocalPort = ((IPEndPoint)_udpSocketv4.LocalEndPoint).Port; + +#if UNITY_SOCKET_FIX + if (_useSocketFix && _pausedSocketFix == null) + _pausedSocketFix = new PausedSocketFix(this, addressIPv4, addressIPv6, port, manualMode); +#endif + + _isRunning = true; + if (_manualMode) + { + _bufferEndPointv4 = new IPEndPoint(IPAddress.Any, 0); + } + + //Check IPv6 support + if (IPv6Support && IPv6Enabled) + { + _udpSocketv6 = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); + //Use one port for two sockets + if (BindSocket(_udpSocketv6, new IPEndPoint(addressIPv6, LocalPort))) + { + if (_manualMode) + _bufferEndPointv6 = new IPEndPoint(IPAddress.IPv6Any, 0); + } + else + { + _udpSocketv6 = null; + } + } + + if (!manualMode) + { + ThreadStart ts = ReceiveLogic; + if (UseNativeSockets) + ts = NativeReceiveLogic; + _receiveThread = new Thread(ts) + { + Name = $"ReceiveThread({LocalPort})", + IsBackground = true + }; + _receiveThread.Start(); + if (_logicThread == null) + { + _logicThread = new Thread(UpdateLogic) { Name = "LogicThread", IsBackground = true }; + _logicThread.Start(); + } + } + + return true; + } + + private bool BindSocket(Socket socket, IPEndPoint ep) + { + //Setup socket + socket.ReceiveTimeout = 500; + socket.SendTimeout = 500; + socket.ReceiveBufferSize = NetConstants.SocketBufferSize; + socket.SendBufferSize = NetConstants.SocketBufferSize; + socket.Blocking = true; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + try + { + socket.IOControl(SioUdpConnreset, new byte[] { 0 }, null); + } + catch + { + //ignored + } + } + + try + { + socket.ExclusiveAddressUse = !ReuseAddress; + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, ReuseAddress); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontRoute, DontRoute); + } + catch + { + //Unity with IL2CPP throws an exception here, it doesn't matter in most cases so just ignore it + } + if (ep.AddressFamily == AddressFamily.InterNetwork) + { + Ttl = NetConstants.SocketTTL; + + try { socket.EnableBroadcast = true; } + catch (SocketException e) + { + NetDebug.WriteError($"[B]Broadcast error: {e.SocketErrorCode}"); + } + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + try { socket.DontFragment = true; } + catch (SocketException e) + { + NetDebug.WriteError($"[B]DontFragment error: {e.SocketErrorCode}"); + } + } + } + //Bind + try + { + socket.Bind(ep); + NetDebug.Write(NetLogLevel.Trace, $"[B]Successfully binded to port: {((IPEndPoint)socket.LocalEndPoint).Port}, AF: {socket.AddressFamily}"); + + //join multicast + if (ep.AddressFamily == AddressFamily.InterNetworkV6) + { + try + { +#if !UNITY_SOCKET_FIX + socket.SetSocketOption( + SocketOptionLevel.IPv6, + SocketOptionName.AddMembership, + new IPv6MulticastOption(MulticastAddressV6)); +#endif + } + catch (Exception) + { + // Unity3d throws exception - ignored + } + } + } + catch (SocketException bindException) + { + switch (bindException.SocketErrorCode) + { + //IPv6 bind fix + case SocketError.AddressAlreadyInUse: + if (socket.AddressFamily == AddressFamily.InterNetworkV6) + { + try + { + //Set IPv6Only + socket.DualMode = false; + socket.Bind(ep); + } + catch (SocketException ex) + { + //because its fixed in 2018_3 + NetDebug.WriteError($"[B]Bind exception: {ex}, errorCode: {ex.SocketErrorCode}"); + return false; + } + return true; + } + break; + //hack for iOS (Unity3D) + case SocketError.AddressFamilyNotSupported: + return true; + } + NetDebug.WriteError($"[B]Bind exception: {bindException}, errorCode: {bindException.SocketErrorCode}"); + return false; + } + return true; + } + + internal int SendRawAndRecycle(NetPacket packet, IPEndPoint remoteEndPoint) + { + int result = SendRaw(packet.RawData, 0, packet.Size, remoteEndPoint); + PoolRecycle(packet); + return result; + } + + internal int SendRaw(NetPacket packet, IPEndPoint remoteEndPoint) => + SendRaw(packet.RawData, 0, packet.Size, remoteEndPoint); + + internal int SendRaw(byte[] message, int start, int length, IPEndPoint remoteEndPoint) + { + if (!_isRunning) + return 0; + + NetPacket expandedPacket = null; + if (_extraPacketLayer != null) + { + expandedPacket = PoolGetPacket(length + _extraPacketLayer.ExtraPacketSizeForLayer); + Buffer.BlockCopy(message, start, expandedPacket.RawData, 0, length); + start = 0; + _extraPacketLayer.ProcessOutBoundPacket(ref remoteEndPoint, ref expandedPacket.RawData, ref start, ref length); + message = expandedPacket.RawData; + } + +#if DEBUG || SIMULATE_NETWORK + if (HandleSimulateOutboundPacketLoss()) + { + if (expandedPacket != null) + PoolRecycle(expandedPacket); + return 0; // Simulate successful send to avoid triggering error handling + } + + if (HandleSimulateOutboundLatency(message, start, length, remoteEndPoint)) + { + if (expandedPacket != null) + PoolRecycle(expandedPacket); + return length; // Simulate successful send + } +#endif + + return SendRawCoreWithCleanup(message, start, length, remoteEndPoint, expandedPacket); + } + + private int SendRawCoreWithCleanup(byte[] message, int start, int length, IPEndPoint remoteEndPoint, NetPacket expandedPacket) + { + try + { + return SendRawCore(message, start, length, remoteEndPoint); + } + finally + { + if (expandedPacket != null) + PoolRecycle(expandedPacket); + } + } + + // Core socket sending logic without simulation - used by both SendRaw and delayed packet processing + internal int SendRawCore(byte[] message, int start, int length, IPEndPoint remoteEndPoint) + { + if (!_isRunning) + return 0; + + var socket = _udpSocketv4; + if (remoteEndPoint.AddressFamily == AddressFamily.InterNetworkV6 && IPv6Support) + { + socket = _udpSocketv6; + if (socket == null) + return 0; + } + + int result; + try + { + if (UseNativeSockets && remoteEndPoint is LiteNetPeer peer) + { + unsafe + { + fixed (byte* dataWithOffset = &message[start]) + result = NativeSocket.SendTo(socket.Handle, dataWithOffset, length, peer.NativeAddress, peer.NativeAddress.Length); + } + if (result == -1) + throw NativeSocket.GetSocketException(); + } + else + { +#if NET8_0_OR_GREATER + result = socket.SendTo(new ReadOnlySpan(message, start, length), SocketFlags.None, remoteEndPoint.Serialize()); +#else + result = socket.SendTo(message, start, length, SocketFlags.None, remoteEndPoint); +#endif + } + //NetDebug.WriteForce("[S]Send packet to {0}, result: {1}", remoteEndPoint, result); + } + catch (SocketException ex) + { + switch (ex.SocketErrorCode) + { + case SocketError.NoBufferSpaceAvailable: + case SocketError.Interrupted: + return 0; + case SocketError.MessageSize: + NetDebug.Write(NetLogLevel.Trace, $"[SRD] 10040, datalen: {length}"); + return 0; + + case SocketError.HostUnreachable: + case SocketError.NetworkUnreachable: + if (DisconnectOnUnreachable && remoteEndPoint is LiteNetPeer peer) + { + DisconnectPeerForce( + peer, + ex.SocketErrorCode == SocketError.HostUnreachable + ? DisconnectReason.HostUnreachable + : DisconnectReason.NetworkUnreachable, + ex.SocketErrorCode, + null); + } + + CreateEvent(NetEvent.EType.Error, remoteEndPoint: remoteEndPoint, errorCode: ex.SocketErrorCode); + return -1; + + case SocketError.Shutdown: + CreateEvent(NetEvent.EType.Error, remoteEndPoint: remoteEndPoint, errorCode: ex.SocketErrorCode); + return -1; + + default: + NetDebug.WriteError($"[S] {ex}"); + return -1; + } + } + catch (Exception ex) + { + NetDebug.WriteError($"[S] {ex}"); + return 0; + } + + if (result <= 0) + return 0; + + if (EnableStatistics) + { + Statistics.IncrementPacketsSent(); + Statistics.AddBytesSent(length); + } + + return result; + } + + public bool SendBroadcast(NetDataWriter writer, int port) => + SendBroadcast(writer.Data, 0, writer.Length, port); + + public bool SendBroadcast(byte[] data, int port) => + SendBroadcast(data, 0, data.Length, port); + + public bool SendBroadcast(byte[] data, int start, int length, int port) + { + if (!IsRunning) + return false; + + NetPacket packet; + if (_extraPacketLayer != null) + { + var headerSize = NetPacket.GetHeaderSize(PacketProperty.Broadcast); + packet = PoolGetPacket(headerSize + length + _extraPacketLayer.ExtraPacketSizeForLayer); + packet.Property = PacketProperty.Broadcast; + Buffer.BlockCopy(data, start, packet.RawData, headerSize, length); + var checksumComputeStart = 0; + int preCrcLength = length + headerSize; + IPEndPoint emptyEp = null; + _extraPacketLayer.ProcessOutBoundPacket(ref emptyEp, ref packet.RawData, ref checksumComputeStart, ref preCrcLength); + } + else + { + packet = PoolGetWithData(PacketProperty.Broadcast, data, start, length); + } + + bool broadcastSuccess = false; + bool multicastSuccess = false; + try + { + broadcastSuccess = _udpSocketv4.SendTo( + packet.RawData, + 0, + packet.Size, + SocketFlags.None, + new IPEndPoint(IPAddress.Broadcast, port)) > 0; + + if (_udpSocketv6 != null) + { + multicastSuccess = _udpSocketv6.SendTo( + packet.RawData, + 0, + packet.Size, + SocketFlags.None, + new IPEndPoint(MulticastAddressV6, port)) > 0; + } + } + catch (SocketException ex) + { + if (ex.SocketErrorCode == SocketError.HostUnreachable) + return broadcastSuccess; + NetDebug.WriteError($"[S][MCAST] {ex}"); + return broadcastSuccess; + } + catch (Exception ex) + { + NetDebug.WriteError($"[S][MCAST] {ex}"); + return broadcastSuccess; + } + finally + { + PoolRecycle(packet); + } + + return broadcastSuccess || multicastSuccess; + } + + private void CloseSocket() + { + _isRunning = false; + if (_receiveThread != null && _receiveThread != Thread.CurrentThread) + _receiveThread.Join(); + _receiveThread = null; + _udpSocketv4?.Close(); + _udpSocketv6?.Close(); + _udpSocketv4 = null; + _udpSocketv6 = null; + } + } +} diff --git a/LiteNetLib/LiteNetManager.cs b/LiteNetLib/LiteNetManager.cs new file mode 100644 index 00000000..2903e8d8 --- /dev/null +++ b/LiteNetLib/LiteNetManager.cs @@ -0,0 +1,1670 @@ +#if UNITY_2018_3_OR_NEWER +#define UNITY_SOCKET_FIX +#endif +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using LiteNetLib.Layers; +using LiteNetLib.Utils; + +namespace LiteNetLib +{ + /// + /// Main class for all network operations. Can be used as client and/or server. + /// + public partial class LiteNetManager : IEnumerable + { + public struct NetPeerEnumerator : IEnumerator where T : LiteNetPeer + { + private readonly T _initialPeer; + private T _p; + + public NetPeerEnumerator(T p) + { + _initialPeer = p; + _p = null; + } + + public void Dispose() { } + + public bool MoveNext() + { + _p = _p == null ? _initialPeer : (T)_p.NextPeer; + return _p != null; + } + + public void Reset() => + throw new NotSupportedException(); + + public T Current => _p; + object IEnumerator.Current => _p; + } + + private struct IncomingData + { + public NetPacket Data; + public IPEndPoint EndPoint; + public DateTime TimeWhenGet; + } + private readonly List _pingSimulationList = new List(); + +#if DEBUG || SIMULATE_NETWORK + private struct OutboundDelayedPacket + { + public byte[] Data; + public int Start; + public int Length; + public IPEndPoint EndPoint; + public DateTime TimeWhenSend; + } + private readonly List _outboundSimulationList = new List(); +#endif + + private readonly Random _randomGenerator = new Random(); + private const int MinLatencyThreshold = 5; + + private Thread _logicThread; + private bool _manualMode; + private readonly AutoResetEvent _updateTriggerEvent = new AutoResetEvent(true); + + private NetEvent _pendingEventHead; + private NetEvent _pendingEventTail; + + private NetEvent _netEventPoolHead; + private readonly ILiteNetEventListener _netEventListener; + + private readonly Dictionary _requestsDict = new Dictionary(); + + private long _connectedPeersCount; + private readonly PacketLayerBase _extraPacketLayer; + private int _lastPeerId; + private ConcurrentQueue _peerIds = new ConcurrentQueue(); + + private readonly object _eventLock = new object(); + private volatile bool _isRunning; + + /// + /// Used with and to tag packets that + /// need to be dropped. Only relevant when DEBUG is defined. + /// + private bool _dropPacket; + + //config section + /// + /// Enable messages receiving without connection. (with SendUnconnectedMessage method) + /// + public bool UnconnectedMessagesEnabled = false; + + /// + /// Enable nat punch messages + /// + public bool NatPunchEnabled = false; + + /// + /// Library logic update and send period in milliseconds + /// Lowest values in Windows doesn't change much because of Thread.Sleep precision + /// To more frequent sends (or sends tied to your game logic) use + /// + public int UpdateTime = 15; + + /// + /// Interval for latency detection and checking connection (in milliseconds) + /// + public int PingInterval = 1000; + + /// + /// If NetManager doesn't receive any packet from remote peer during this time (in milliseconds) then connection will be closed + /// (including library internal keepalive packets) + /// + public int DisconnectTimeout = 5000; + + /// + /// Simulate packet loss by dropping random amount of packets. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) + /// + public bool SimulatePacketLoss = false; + + /// + /// Simulate latency by holding packets for random time. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) + /// + public bool SimulateLatency = false; + + /// + /// Chance of packet loss when simulation enabled. value in percents (1 - 100). + /// + public int SimulationPacketLossChance = 10; + + /// + /// Minimum simulated round-trip latency (in milliseconds). Actual latency applied per direction is half of this value. + /// + public int SimulationMinLatency = 30; + + /// + /// Maximum simulated round-trip latency (in milliseconds). Actual latency applied per direction is half of this value. + /// + public int SimulationMaxLatency = 100; + + /// + /// Events automatically will be called without PollEvents method from another thread + /// + public bool UnsyncedEvents = false; + + /// + /// If true - receive event will be called from "receive" thread immediately otherwise on PollEvents call + /// + public bool UnsyncedReceiveEvent = false; + + /// + /// If true - delivery event will be called from "receive" thread immediately otherwise on PollEvents call + /// + public bool UnsyncedDeliveryEvent = false; + + /// + /// Allows receive broadcast packets + /// + public bool BroadcastReceiveEnabled = false; + + /// + /// Delay between initial connection attempts (in milliseconds) + /// + public int ReconnectDelay = 500; + + /// + /// Maximum connection attempts before client stops and call disconnect event. + /// + public int MaxConnectAttempts = 10; + + /// + /// Enables socket option "ReuseAddress" for specific purposes + /// + public bool ReuseAddress = false; + + /// + /// UDP Only Socket Option + /// Normally IP sockets send packets of data through routers and gateways until they reach the final destination. + /// If the DontRoute flag is set to True, then data will be delivered on the local subnet only. + /// + public bool DontRoute = false; + + /// + /// Statistics of all connections + /// + public readonly NetStatistics Statistics = new NetStatistics(); + + /// + /// Toggles the collection of network statistics for the instance and all known peers + /// + public bool EnableStatistics = false; + + /// + /// Max fragmented packets size for reliable channels - that equals to data of size fragments count * (MTU-reliable header size) + /// + public ushort MaxFragmentsCount = ushort.MaxValue; + + /// + /// NatPunchModule for NAT hole punching operations + /// + public NatPunchModule NatPunchModule => _natPunchModule.Value; + + private readonly Lazy _natPunchModule; + + /// + /// Returns true if socket listening and update thread is running + /// + public bool IsRunning => _isRunning; + + /// + /// Local EndPoint (host and port) + /// + public int LocalPort { get; private set; } + + /// + /// Automatically recycle NetPacketReader after OnReceive event + /// + public bool AutoRecycle; + + /// + /// IPv6 support + /// + public bool IPv6Enabled = true; + + /// + /// Override MTU for all new peers registered in this NetManager, will ignores MTU Discovery! + /// + public int MtuOverride = 0; + + /// + /// Automatically discovery mtu starting from. Use at own risk because some routers can break MTU detection + /// and connection in result + /// + public bool MtuDiscovery = false; + + /// + /// First peer. Useful for Client mode + /// + public LiteNetPeer FirstPeer => _headPeer; + + /// + /// Experimental feature mostly for servers. Only for Windows/Linux + /// use direct socket calls for send/receive to drastically increase speed and reduce GC pressure + /// + public bool UseNativeSockets = false; + + /// + /// Disconnect peers if HostUnreachable or NetworkUnreachable spawned (old behaviour 0.9.x was true) + /// + public bool DisconnectOnUnreachable = false; + + /// + /// Allows peer change it's ip (lte to wifi, wifi to lte, etc). Use only on server + /// + public bool AllowPeerAddressChange = false; + + /// + /// Returns connected peers count + /// + public int ConnectedPeersCount => (int)Interlocked.Read(ref _connectedPeersCount); + + /// + /// Maximum packets that can be received per one ManualReceive (PollEvents when started using StartInManualMode) + /// 0 - infinite - but this can cause some big delays there is too many incoming packets + /// + public int MaxPacketPerManualReceive = 256; + + /// + /// Gets the additional size in bytes required by the active . + /// + /// + /// This value is used by to calculate the available MTU for user data.
+ /// If a packet layer is active (e.g., for encryption or CRC), this returns the overhead added to every packet. + /// Returns 0 if no packet layer is assigned. + ///
+ public int ExtraPacketSizeForLayer => _extraPacketLayer?.ExtraPacketSizeForLayer ?? 0; + + /// + /// NetManager constructor + /// + /// Network events listener (also can implement IDeliveryEventListener) + /// Extra processing of packages, like CRC checksum or encryption. All connected NetManagers must have same layer. +#if UNITY_SOCKET_FIX + public LiteNetManager(ILiteNetEventListener listener, PacketLayerBase extraPacketLayer = null, bool useSocketFix = true) + { + _useSocketFix = useSocketFix; +#else + public LiteNetManager(ILiteNetEventListener listener, PacketLayerBase extraPacketLayer = null) + { +#endif + _netEventListener = listener; + _natPunchModule = new Lazy(() => new NatPunchModule(this)); + _extraPacketLayer = extraPacketLayer; + } + + internal void ConnectionLatencyUpdated(LiteNetPeer fromPeer, int latency) => + CreateEvent(NetEvent.EType.ConnectionLatencyUpdated, fromPeer, latency: latency); + + internal void MessageDelivered(LiteNetPeer fromPeer, object userData) => + CreateEvent(NetEvent.EType.MessageDelivered, fromPeer, userData: userData); + + internal void DisconnectPeerForce(LiteNetPeer peer, + DisconnectReason reason, + SocketError socketErrorCode, + NetPacket eventData) => + DisconnectPeer(peer, reason, socketErrorCode, true, null, 0, 0, eventData); + + /// + /// Disconnects a peer and handles internal state cleanup. + /// + /// The peer to disconnect. + /// The reason for disconnection provided to the event listener. + /// The error code from the underlying socket, if any. + /// + /// If , immediately sets state to without sending a notification.
+ /// If , sends unreliable disconnect packets until and sets state to . + /// Peer will linger until to ignore late-arriving packets from the old session. + /// + /// Optional custom data to include in the disconnect packet. + /// Offset in the array. + /// Number of bytes to send from the array. + /// Internal packet data associated with the disconnect event. + private void DisconnectPeer( + LiteNetPeer peer, + DisconnectReason reason, + SocketError socketErrorCode, + bool force, + byte[] data, + int start, + int count, + NetPacket eventData) + { + var shutdownResult = peer.Shutdown(data, start, count, force); + if (shutdownResult == ShutdownResult.None) + return; + if (shutdownResult == ShutdownResult.WasConnected) + Interlocked.Decrement(ref _connectedPeersCount); + CreateEvent( + NetEvent.EType.Disconnect, + peer, + errorCode: socketErrorCode, + disconnectReason: reason, + readerSource: eventData); + } + + private void CreateEvent( + NetEvent.EType type, + LiteNetPeer peer = null, + IPEndPoint remoteEndPoint = null, + SocketError errorCode = 0, + int latency = 0, + DisconnectReason disconnectReason = DisconnectReason.ConnectionFailed, + LiteConnectionRequest connectionRequest = null, + DeliveryMethod deliveryMethod = DeliveryMethod.Unreliable, + byte channelNumber = 0, + NetPacket readerSource = null, + object userData = null) + { + NetEvent evt; + bool unsyncEvent = UnsyncedEvents; + + if (type == NetEvent.EType.Connect) + Interlocked.Increment(ref _connectedPeersCount); + else if (type == NetEvent.EType.MessageDelivered) + unsyncEvent = UnsyncedDeliveryEvent; + + lock (_eventLock) + { + evt = _netEventPoolHead; + if (evt == null) + evt = new NetEvent(this); + else + _netEventPoolHead = evt.Next; + } + + evt.Next = null; + evt.Type = type; + evt.DataReader.SetSource(readerSource, readerSource?.HeaderSize ?? 0); + evt.Peer = peer; + evt.RemoteEndPoint = remoteEndPoint; + evt.Latency = latency; + evt.ErrorCode = errorCode; + evt.DisconnectReason = disconnectReason; + evt.ConnectionRequest = connectionRequest; + evt.DeliveryMethod = deliveryMethod; + evt.ChannelNumber = channelNumber; + evt.UserData = userData; + + if (unsyncEvent || _manualMode) + { + ProcessEvent(evt); + } + else + { + lock (_eventLock) + { + if (_pendingEventTail == null) + _pendingEventHead = evt; + else + _pendingEventTail.Next = evt; + _pendingEventTail = evt; + } + } + } + + protected virtual void ProcessEvent(NetEvent evt) + { + NetDebug.Write("[NM] Processing event: " + evt.Type); + bool emptyData = evt.DataReader.IsNull; + switch (evt.Type) + { + case NetEvent.EType.Connect: + _netEventListener.OnPeerConnected(evt.Peer); + break; + case NetEvent.EType.Disconnect: + var info = new DisconnectInfo + { + Reason = evt.DisconnectReason, + AdditionalData = evt.DataReader, + SocketErrorCode = evt.ErrorCode + }; + _netEventListener.OnPeerDisconnected(evt.Peer, info); + break; + case NetEvent.EType.Receive: + _netEventListener.OnNetworkReceive(evt.Peer, evt.DataReader, evt.DeliveryMethod); + break; + case NetEvent.EType.ReceiveUnconnected: + _netEventListener.OnNetworkReceiveUnconnected(evt.RemoteEndPoint, evt.DataReader, UnconnectedMessageType.BasicMessage); + break; + case NetEvent.EType.Broadcast: + _netEventListener.OnNetworkReceiveUnconnected(evt.RemoteEndPoint, evt.DataReader, UnconnectedMessageType.Broadcast); + break; + case NetEvent.EType.Error: + _netEventListener.OnNetworkError(evt.RemoteEndPoint, evt.ErrorCode); + break; + case NetEvent.EType.ConnectionLatencyUpdated: + _netEventListener.OnNetworkLatencyUpdate(evt.Peer, evt.Latency); + break; + case NetEvent.EType.ConnectionRequest: + _netEventListener.OnConnectionRequest(evt.ConnectionRequest); + break; + case NetEvent.EType.MessageDelivered: + _netEventListener.OnMessageDelivered(evt.Peer, evt.UserData); + break; + case NetEvent.EType.PeerAddressChanged: + _peersLock.EnterUpgradeableReadLock(); + IPEndPoint previousAddress = null; + if (ContainsPeer(evt.Peer)) + { + _peersLock.EnterWriteLock(); + RemovePeerFromSet(evt.Peer); + previousAddress = new IPEndPoint(evt.Peer.Address, evt.Peer.Port); + evt.Peer.FinishEndPointChange(evt.RemoteEndPoint); + AddPeerToSet(evt.Peer); + _peersLock.ExitWriteLock(); + } + _peersLock.ExitUpgradeableReadLock(); + if (previousAddress != null) + _netEventListener.OnPeerAddressChanged(evt.Peer, previousAddress); + break; + } + //Recycle if not message + if (emptyData) + RecycleEvent(evt); + else if (AutoRecycle) + evt.DataReader.RecycleInternal(); + } + + internal void RecycleEvent(NetEvent evt) + { + evt.Peer = null; + evt.ErrorCode = 0; + evt.RemoteEndPoint = null; + evt.ConnectionRequest = null; + lock (_eventLock) + { + evt.Next = _netEventPoolHead; + _netEventPoolHead = evt; + } + } + + protected virtual void ProcessNtpRequests(float elapsedMilliseconds) + { + //not used in lite version + } + + //Update function + private void UpdateLogic() + { + var peersToRemove = new List(); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + while (_isRunning) + { + try + { + ProcessDelayedPackets(); + float elapsed = (float)(stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0); + elapsed = elapsed <= 0.0f ? 0.001f : elapsed; + stopwatch.Restart(); + + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + { + if (netPeer.ConnectionState == ConnectionState.Disconnected && + netPeer.TimeSinceLastPacket > DisconnectTimeout) + { + peersToRemove.Add(netPeer); + } + else + { + netPeer.Update(elapsed); + } + } + + if (peersToRemove.Count > 0) + { + _peersLock.EnterWriteLock(); + for (int i = 0; i < peersToRemove.Count; i++) + RemovePeer(peersToRemove[i], false); + _peersLock.ExitWriteLock(); + peersToRemove.Clear(); + } + + ProcessNtpRequests(elapsed); + + int sleepTime = UpdateTime - (int)stopwatch.ElapsedMilliseconds; + if (sleepTime > 0) + _updateTriggerEvent.WaitOne(sleepTime); + } + catch (ThreadAbortException) + { + return; + } + catch (Exception e) + { + NetDebug.WriteError("[NM] LogicThread error: " + e); + } + } + stopwatch.Stop(); + } + + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] + private void ProcessDelayedPackets() + { + if (!SimulateLatency) + return; + + var time = DateTime.UtcNow; + lock (_pingSimulationList) + { + for (int i = 0; i < _pingSimulationList.Count; i++) + { + var incomingData = _pingSimulationList[i]; + if (incomingData.TimeWhenGet <= time) + { + HandleMessageReceived(incomingData.Data, incomingData.EndPoint); + _pingSimulationList.RemoveAt(i); + i--; + } + } + } + +#if DEBUG || SIMULATE_NETWORK + lock (_outboundSimulationList) + { + for (int i = 0; i < _outboundSimulationList.Count; i++) + { + var outboundData = _outboundSimulationList[i]; + if (outboundData.TimeWhenSend <= time) + { + // Send the delayed packet directly to socket layer bypassing simulation + SendRawCore(outboundData.Data, outboundData.Start, outboundData.Length, outboundData.EndPoint); + _outboundSimulationList.RemoveAt(i); + i--; + } + } + } +#endif + } + + + /// + /// Updates internal peer states, handles timeouts, processes NTP requests and sends buffered packets. + /// + /// Time passed since the last update frame. + /// + /// Must be called continuously from the main loop if was set to . + /// + public void ManualUpdate(float elapsedMilliseconds) + { + if (!_manualMode) + return; + + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + { + if (netPeer.ConnectionState == ConnectionState.Disconnected && netPeer.TimeSinceLastPacket > DisconnectTimeout) + { + RemovePeer(netPeer, false); + } + else + { + netPeer.Update(elapsedMilliseconds); + } + } + ProcessNtpRequests(elapsedMilliseconds); + } + + //connect to + protected virtual LiteNetPeer CreateOutgoingPeer(IPEndPoint remoteEndPoint, int id, byte connectNum, ReadOnlySpan connectData) => + new LiteNetPeer(this, remoteEndPoint, id, connectNum, connectData); + + //accept + protected virtual LiteNetPeer CreateIncomingPeer(LiteConnectionRequest request, int id) => + new LiteNetPeer(this, request, id); + + //reject + protected virtual LiteNetPeer CreateRejectPeer(IPEndPoint remoteEndPoint, int id) => + new LiteNetPeer(this, remoteEndPoint, id); + + protected virtual LiteConnectionRequest CreateConnectionRequest(IPEndPoint remoteEndPoint, NetConnectRequestPacket requestPacket) => + new LiteConnectionRequest(remoteEndPoint, requestPacket, this); + + internal LiteNetPeer OnConnectionSolved(LiteConnectionRequest request, byte[] rejectData, int start, int length) + { + LiteNetPeer netPeer = null; + + if (request.Result == ConnectionRequestResult.RejectForce) + { + NetDebug.Write(NetLogLevel.Trace, "[NM] Peer connect reject force."); + if (rejectData != null && length > 0) + { + var shutdownPacket = PoolGetWithProperty(PacketProperty.Disconnect, length); + shutdownPacket.ConnectionNumber = request.InternalPacket.ConnectionNumber; + FastBitConverter.GetBytes(shutdownPacket.RawData, 1, request.InternalPacket.ConnectionTime); + if (shutdownPacket.Size >= NetConstants.PossibleMtu[0]) + NetDebug.WriteError("[Peer] Disconnect additional data size more than MTU!"); + else + Buffer.BlockCopy(rejectData, start, shutdownPacket.RawData, 9, length); + SendRawAndRecycle(shutdownPacket, request.RemoteEndPoint); + } + lock (_requestsDict) + _requestsDict.Remove(request.RemoteEndPoint); + } + else lock (_requestsDict) + { + if (TryGetPeer(request.RemoteEndPoint, out netPeer)) + { + //already have peer + } + else if (request.Result == ConnectionRequestResult.Reject) + { + netPeer = CreateRejectPeer(request.RemoteEndPoint, GetNextPeerId()); + netPeer.Reject(request.InternalPacket, rejectData, start, length); + AddPeer(netPeer); + NetDebug.Write(NetLogLevel.Trace, "[NM] Peer connect reject."); + } + else //Accept + { + netPeer = CreateIncomingPeer(request, GetNextPeerId()); + AddPeer(netPeer); + CreateEvent(NetEvent.EType.Connect, netPeer); + NetDebug.Write(NetLogLevel.Trace, $"[NM] Received peer connection Id: {netPeer.ConnectTime}, EP: {netPeer}"); + } + _requestsDict.Remove(request.RemoteEndPoint); + } + + return netPeer; + } + + private int GetNextPeerId() => + _peerIds.TryDequeue(out int id) ? id : _lastPeerId++; + + private void ProcessConnectRequest( + IPEndPoint remoteEndPoint, + LiteNetPeer netPeer, + NetConnectRequestPacket connRequest) + { + //if we have peer + if (netPeer != null) + { + var processResult = netPeer.ProcessConnectRequest(connRequest); + NetDebug.Write($"ConnectRequest LastId: {netPeer.ConnectTime}, NewId: {connRequest.ConnectionTime}, EP: {remoteEndPoint}, Result: {processResult}"); + + switch (processResult) + { + case ConnectRequestResult.Reconnection: + DisconnectPeerForce(netPeer, DisconnectReason.Reconnect, 0, null); + RemovePeer(netPeer, true); + //go to new connection + break; + case ConnectRequestResult.NewConnection: + RemovePeer(netPeer, true); + //go to new connection + break; + case ConnectRequestResult.P2PLose: + DisconnectPeerForce(netPeer, DisconnectReason.PeerToPeerConnection, 0, null); + RemovePeer(netPeer, true); + //go to new connection + break; + default: + //no operations needed + return; + } + //ConnectRequestResult.NewConnection + //Set next connection number + if (processResult != ConnectRequestResult.P2PLose) + connRequest.ConnectionNumber = (byte)((netPeer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); + //To reconnect peer + } + else + { + NetDebug.Write($"ConnectRequest Id: {connRequest.ConnectionTime}, EP: {remoteEndPoint}"); + } + + LiteConnectionRequest req; + lock (_requestsDict) + { + if (_requestsDict.TryGetValue(remoteEndPoint, out req)) + { + req.UpdateRequest(connRequest); + return; + } + req = CreateConnectionRequest(remoteEndPoint, connRequest); + _requestsDict.Add(remoteEndPoint, req); + } + NetDebug.Write($"[NM] Creating request event: {connRequest.ConnectionTime}"); + CreateEvent(NetEvent.EType.ConnectionRequest, connectionRequest: req); + } + + private void OnMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) + { + if (packet.Size == 0) + { + PoolRecycle(packet); + return; + } + + _dropPacket = false; + HandleSimulateLatency(packet, remoteEndPoint); + HandleSimulatePacketLoss(); + if (_dropPacket) + { + return; + } + + // ProcessEvents + HandleMessageReceived(packet, remoteEndPoint); + } + + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] + private void HandleSimulateLatency(NetPacket packet, IPEndPoint remoteEndPoint) + { + if (!SimulateLatency) + { + return; + } + + int roundTripLatency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); + int inboundLatency = roundTripLatency / 2; + if (inboundLatency > MinLatencyThreshold) + { + lock (_pingSimulationList) + { + _pingSimulationList.Add(new IncomingData + { + Data = packet, + EndPoint = remoteEndPoint, + TimeWhenGet = DateTime.UtcNow.AddMilliseconds(inboundLatency) + }); + } + // hold packet + _dropPacket = true; + } + } + + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] + private void HandleSimulatePacketLoss() + { + if (SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance) + { + _dropPacket = true; + } + } + +#if DEBUG || SIMULATE_NETWORK + private bool HandleSimulateOutboundLatency(byte[] data, int start, int length, IPEndPoint remoteEndPoint) + { + if (!SimulateLatency) + { + return false; + } + + int roundTripLatency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); + int outboundLatency = roundTripLatency / 2; + if (outboundLatency > MinLatencyThreshold) + { + // Create a copy of the data to avoid issues with recycled packets + byte[] dataCopy = new byte[length]; + Array.Copy(data, start, dataCopy, 0, length); + + lock (_outboundSimulationList) + { + _outboundSimulationList.Add(new OutboundDelayedPacket + { + Data = dataCopy, + Start = 0, + Length = length, + EndPoint = remoteEndPoint, + TimeWhenSend = DateTime.UtcNow.AddMilliseconds(outboundLatency) + }); + } + + return true; + } + return false; + } +#endif + +#if DEBUG || SIMULATE_NETWORK + private bool HandleSimulateOutboundPacketLoss() + { + bool shouldDrop = SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance; + return shouldDrop; + } +#endif + + internal virtual bool CustomMessageHandle(NetPacket packet, IPEndPoint remoteEndPoint) => + false; + + private void HandleMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) + { + var originalPacketSize = packet.Size; + if (EnableStatistics) + { + Statistics.IncrementPacketsReceived(); + Statistics.AddBytesReceived(originalPacketSize); + } + + if (CustomMessageHandle(packet, remoteEndPoint)) + return; + + if (_extraPacketLayer != null) + { + _extraPacketLayer.ProcessInboundPacket(ref remoteEndPoint, ref packet.RawData, ref packet.Size); + if (packet.Size == 0) + return; + } + + if (!packet.Verify()) + { + NetDebug.WriteError("[NM] DataReceived: bad!"); + PoolRecycle(packet); + return; + } + + switch (packet.Property) + { + //special case connect request + case PacketProperty.ConnectRequest: + if (NetConnectRequestPacket.GetProtocolId(packet) != NetConstants.ProtocolId) + { + SendRawAndRecycle(PoolGetWithProperty(PacketProperty.InvalidProtocol), remoteEndPoint); + return; + } + break; + //unconnected messages + case PacketProperty.Broadcast: + if (!BroadcastReceiveEnabled) + return; + CreateEvent(NetEvent.EType.Broadcast, remoteEndPoint: remoteEndPoint, readerSource: packet); + return; + case PacketProperty.UnconnectedMessage: + if (!UnconnectedMessagesEnabled) + return; + CreateEvent(NetEvent.EType.ReceiveUnconnected, remoteEndPoint: remoteEndPoint, readerSource: packet); + return; + case PacketProperty.NatMessage: + if (NatPunchEnabled) + NatPunchModule.ProcessMessage(remoteEndPoint, packet); + return; + } + + //Check normal packets + bool peerFound = remoteEndPoint is LiteNetPeer netPeer || TryGetPeer(remoteEndPoint, out netPeer); + + if (peerFound && EnableStatistics) + { + netPeer.Statistics.IncrementPacketsReceived(); + netPeer.Statistics.AddBytesReceived(originalPacketSize); + } + + switch (packet.Property) + { + case PacketProperty.ConnectRequest: + var connRequest = NetConnectRequestPacket.FromData(packet); + if (connRequest != null) + ProcessConnectRequest(remoteEndPoint, netPeer, connRequest); + break; + case PacketProperty.PeerNotFound: + if (peerFound) //local + { + if (netPeer.ConnectionState != ConnectionState.Connected) + return; + if (packet.Size == 1) + { + //first reply + //send NetworkChanged packet + netPeer.ResetMtu(); + SendRaw(NetConnectAcceptPacket.MakeNetworkChanged(netPeer), remoteEndPoint); + NetDebug.Write($"PeerNotFound sending connection info: {remoteEndPoint}"); + } + else if (packet.Size == 2 && packet.RawData[1] == 1) + { + //second reply + DisconnectPeerForce(netPeer, DisconnectReason.PeerNotFound, 0, null); + } + } + else if (packet.Size > 1) //remote + { + //check if this is old peer + bool isOldPeer = false; + + if (AllowPeerAddressChange) + { + NetDebug.Write($"[NM] Looks like address change: {packet.Size}"); + var remoteData = NetConnectAcceptPacket.FromData(packet); + if (remoteData != null && + remoteData.PeerNetworkChanged && + remoteData.PeerId < _peersArray.Length) + { + _peersLock.EnterUpgradeableReadLock(); + var peer = _peersArray[remoteData.PeerId]; + _peersLock.ExitUpgradeableReadLock(); + if (peer != null && + peer.ConnectTime == remoteData.ConnectionTime && + peer.ConnectionNum == remoteData.ConnectionNumber) + { + if (peer.ConnectionState == ConnectionState.Connected) + { + peer.InitiateEndPointChange(); + CreateEvent(NetEvent.EType.PeerAddressChanged, peer, remoteEndPoint); + NetDebug.Write("[NM] PeerNotFound change address of remote peer"); + } + isOldPeer = true; + } + } + } + + PoolRecycle(packet); + + //else peer really not found + if (!isOldPeer) + { + var secondResponse = PoolGetWithProperty(PacketProperty.PeerNotFound, 1); + secondResponse.RawData[1] = 1; + SendRawAndRecycle(secondResponse, remoteEndPoint); + } + } + break; + case PacketProperty.InvalidProtocol: + if (peerFound && netPeer.ConnectionState == ConnectionState.Outgoing) + DisconnectPeerForce(netPeer, DisconnectReason.InvalidProtocol, 0, null); + break; + case PacketProperty.Disconnect: + if (peerFound) + { + var disconnectResult = netPeer.ProcessDisconnect(packet); + if (disconnectResult == DisconnectResult.None) + { + PoolRecycle(packet); + return; + } + DisconnectPeerForce( + netPeer, + disconnectResult == DisconnectResult.Disconnect + ? DisconnectReason.RemoteConnectionClose + : DisconnectReason.ConnectionRejected, + 0, packet); + } + else + { + PoolRecycle(packet); + } + //Send shutdown + SendRawAndRecycle(PoolGetWithProperty(PacketProperty.ShutdownOk), remoteEndPoint); + break; + case PacketProperty.ConnectAccept: + if (!peerFound) + return; + var connAccept = NetConnectAcceptPacket.FromData(packet); + if (connAccept != null && netPeer.ProcessConnectAccept(connAccept)) + CreateEvent(NetEvent.EType.Connect, netPeer); + break; + default: + if (peerFound) + netPeer.ProcessPacket(packet); + else + SendRawAndRecycle(PoolGetWithProperty(PacketProperty.PeerNotFound), remoteEndPoint); + break; + } + } + + internal void CreateReceiveEvent(NetPacket packet, DeliveryMethod method, byte channelNumber, int headerSize, LiteNetPeer fromPeer) + { + NetEvent evt; + + if (UnsyncedEvents || UnsyncedReceiveEvent || _manualMode) + { + lock (_eventLock) + { + evt = _netEventPoolHead; + if (evt == null) + evt = new NetEvent(this); + else + _netEventPoolHead = evt.Next; + } + evt.Next = null; + evt.Type = NetEvent.EType.Receive; + evt.DataReader.SetSource(packet, headerSize); + evt.Peer = fromPeer; + evt.DeliveryMethod = method; + evt.ChannelNumber = channelNumber; + ProcessEvent(evt); + } + else + { + lock (_eventLock) + { + evt = _netEventPoolHead; + if (evt == null) + evt = new NetEvent(this); + else + _netEventPoolHead = evt.Next; + + evt.Next = null; + evt.Type = NetEvent.EType.Receive; + evt.DataReader.SetSource(packet, headerSize); + evt.Peer = fromPeer; + evt.DeliveryMethod = method; + evt.ChannelNumber = channelNumber; + + if (_pendingEventTail == null) + _pendingEventHead = evt; + else + _pendingEventTail.Next = evt; + _pendingEventTail = evt; + } + } + } + + /// + /// Send data to all connected peers (channel - 0) + /// + /// DataWriter with data + /// Send options (reliable, unreliable, etc.) + public void SendToAll(NetDataWriter writer, DeliveryMethod options) => + SendToAll(writer.Data, 0, writer.Length, options); + + /// + /// Send data to all connected peers (channel - 0) + /// + /// Data + /// Send options (reliable, unreliable, etc.) + public void SendToAll(byte[] data, DeliveryMethod options) => + SendToAll(data, 0, data.Length, options); + + /// + /// Send data to all connected peers (channel - 0) + /// + /// Data + /// Start of data + /// Length of data + /// Send options (reliable, unreliable, etc.) + public void SendToAll(byte[] data, int start, int length, DeliveryMethod options) + { + try + { + _peersLock.EnterReadLock(); + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + netPeer.Send(data, start, length, options); + } + finally + { + _peersLock.ExitReadLock(); + } + } + + /// + /// Send data to all connected peers (channel - 0) + /// + /// DataWriter with data + /// Send options (reliable, unreliable, etc.) + /// Excluded peer + public void SendToAll(NetDataWriter writer, DeliveryMethod options, LiteNetPeer excludePeer) => + SendToAll(writer.Data, 0, writer.Length, options, excludePeer); + + /// + /// Send data to all connected peers (channel - 0) + /// + /// Data + /// Send options (reliable, unreliable, etc.) + /// Excluded peer + public void SendToAll(byte[] data, DeliveryMethod options, LiteNetPeer excludePeer) => + SendToAll(data, 0, data.Length, options, excludePeer); + + /// + /// Send data to all connected peers + /// + /// Data + /// Start of data + /// Length of data + /// Send options (reliable, unreliable, etc.) + /// Excluded peer + public void SendToAll(byte[] data, int start, int length, DeliveryMethod options, LiteNetPeer excludePeer) + { + try + { + _peersLock.EnterReadLock(); + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + { + if (netPeer != excludePeer) + netPeer.Send(data, start, length, options); + } + } + finally + { + _peersLock.ExitReadLock(); + } + } + + /// + /// Send data to all connected peers (channel - 0) + /// + /// Data + /// Send options (reliable, unreliable, etc.) + public void SendToAll(ReadOnlySpan data, DeliveryMethod options) => + SendToAll(data, options, null); + + /// + /// Send data to all connected peers (channel - 0) + /// + /// Data + /// Send options (reliable, unreliable, etc.) + /// Excluded peer + public void SendToAll(ReadOnlySpan data, DeliveryMethod options, LiteNetPeer excludePeer) + { + try + { + _peersLock.EnterReadLock(); + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + { + if (netPeer != excludePeer) + netPeer.Send(data, options); + } + } + finally + { + _peersLock.ExitReadLock(); + } + } + + /// + /// Send message without connection + /// + /// Raw data + /// Packet destination + /// Operation result + public bool SendUnconnectedMessage(ReadOnlySpan message, IPEndPoint remoteEndPoint) + { + int headerSize = NetPacket.GetHeaderSize(PacketProperty.UnconnectedMessage); + var packet = PoolGetPacket(message.Length + headerSize); + packet.Property = PacketProperty.UnconnectedMessage; + message.CopyTo(new Span(packet.RawData, headerSize, message.Length)); + return SendRawAndRecycle(packet, remoteEndPoint) > 0; + } + + /// + /// Start logic thread and listening on available port + /// + public bool Start() => + Start(0); + + /// + /// Start logic thread and listening on selected port + /// + /// bind to specific ipv4 address + /// bind to specific ipv6 address + /// port to listen + public bool Start(IPAddress addressIPv4, IPAddress addressIPv6, int port) => + Start(addressIPv4, addressIPv6, port, false); + + /// + /// Start logic thread and listening on selected port + /// + /// bind to specific ipv4 address + /// bind to specific ipv6 address + /// port to listen + public bool Start(string addressIPv4, string addressIPv6, int port) + { + IPAddress ipv4 = NetUtils.ResolveAddress(addressIPv4); + IPAddress ipv6 = NetUtils.ResolveAddress(addressIPv6); + return Start(ipv4, ipv6, port); + } + + /// + /// Start logic thread and listening on selected port + /// + /// port to listen + public bool Start(int port) => + Start(IPAddress.Any, IPAddress.IPv6Any, port); + + /// + /// Start in manual mode and listening on selected port + /// In this mode you should use ManualReceive (without PollEvents) for receive packets + /// and ManualUpdate(...) for update and send packets + /// This mode useful mostly for single-threaded servers + /// + /// bind to specific ipv4 address + /// bind to specific ipv6 address + /// port to listen + public bool StartInManualMode(IPAddress addressIPv4, IPAddress addressIPv6, int port) => + Start(addressIPv4, addressIPv6, port, true); + + /// + /// Start in manual mode and listening on selected port + /// In this mode you should use ManualReceive (without PollEvents) for receive packets + /// and ManualUpdate(...) for update and send packets + /// This mode useful mostly for single-threaded servers + /// + /// bind to specific ipv4 address + /// bind to specific ipv6 address + /// port to listen + public bool StartInManualMode(string addressIPv4, string addressIPv6, int port) + { + IPAddress ipv4 = NetUtils.ResolveAddress(addressIPv4); + IPAddress ipv6 = NetUtils.ResolveAddress(addressIPv6); + return StartInManualMode(ipv4, ipv6, port); + } + + /// + /// Start in manual mode and listening on selected port + /// In this mode you should use ManualReceive (without PollEvents) for receive packets + /// and ManualUpdate(...) for update and send packets + /// This mode useful mostly for single-threaded servers + /// + /// port to listen + public bool StartInManualMode(int port) => + StartInManualMode(IPAddress.Any, IPAddress.IPv6Any, port); + + /// + /// Send message without connection + /// + /// Raw data + /// Packet destination + /// Operation result + public bool SendUnconnectedMessage(byte[] message, IPEndPoint remoteEndPoint) => + SendUnconnectedMessage(message, 0, message.Length, remoteEndPoint); + + /// + /// Send message without connection. WARNING This method allocates a new IPEndPoint object and + /// synchronously makes a DNS request. If you're calling this method every frame it will be + /// much faster to just cache the IPEndPoint. + /// + /// Data serializer + /// Packet destination IP or hostname + /// Packet destination port + /// Operation result + public bool SendUnconnectedMessage(NetDataWriter writer, string address, int port) => + SendUnconnectedMessage(writer.Data, 0, writer.Length, NetUtils.MakeEndPoint(address, port)); + + /// + /// Send message without connection + /// + /// Data serializer + /// Packet destination + /// Operation result + public bool SendUnconnectedMessage(NetDataWriter writer, IPEndPoint remoteEndPoint) => + SendUnconnectedMessage(writer.Data, 0, writer.Length, remoteEndPoint); + + /// + /// Send message without connection + /// + /// Raw data + /// data start + /// data length + /// Packet destination + /// Operation result + public bool SendUnconnectedMessage(byte[] message, int start, int length, IPEndPoint remoteEndPoint) + { + //No need for CRC here, SendRaw does that + NetPacket packet = PoolGetWithData(PacketProperty.UnconnectedMessage, message, start, length); + return SendRawAndRecycle(packet, remoteEndPoint) > 0; + } + + /// + /// Triggers update and send logic immediately (works asynchronously) + /// + public void TriggerUpdate() => + _updateTriggerEvent.Set(); + + /// + /// Synchronizes arrived events from the background thread to your main-thread/ + /// + /// + /// Must be called continuously from the main loop if was set to to receive data from the UDP sockets. + /// + public void PollEvents() + { + if (_manualMode) + { + if (_udpSocketv4 != null) + ManualReceive(_udpSocketv4, _bufferEndPointv4); + if (_udpSocketv6 != null && _udpSocketv6 != _udpSocketv4) + ManualReceive(_udpSocketv6, _bufferEndPointv6); + ProcessDelayedPackets(); + return; + } + if (UnsyncedEvents) + return; + NetEvent pendingEvent; + lock (_eventLock) + { + pendingEvent = _pendingEventHead; + _pendingEventHead = null; + _pendingEventTail = null; + } + + while (pendingEvent != null) + { + var next = pendingEvent.Next; + ProcessEvent(pendingEvent); + pendingEvent = next; + } + } + + /// + /// Connect to remote host + /// + /// Server IP or hostname + /// Server Port + /// Connection key + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public LiteNetPeer Connect(string address, int port, string key) => + Connect(address, port, NetDataWriter.FromString(key)); + + /// + /// Connect to remote host + /// + /// Server IP or hostname + /// Server Port + /// Additional data for remote peer + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public LiteNetPeer Connect(string address, int port, NetDataWriter connectionData) + { + IPEndPoint ep; + try + { + ep = NetUtils.MakeEndPoint(address, port); + } + catch + { + CreateEvent(NetEvent.EType.Disconnect, disconnectReason: DisconnectReason.UnknownHost); + return null; + } + return Connect(ep, connectionData); + } + + /// + /// Connect to remote host + /// + /// Server end point (ip and port) + /// Connection key + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public LiteNetPeer Connect(IPEndPoint target, string key) => + Connect(target, NetDataWriter.FromString(key)); + + /// + /// Connect to remote host + /// + /// Server end point (ip and port) + /// Additional data for remote peer + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public LiteNetPeer Connect(IPEndPoint target, NetDataWriter connectionData) + { + if (!_isRunning) + throw new InvalidOperationException("Client is not running"); + + lock (_requestsDict) + { + if (_requestsDict.ContainsKey(target)) + return null; + + byte connectionNumber = 0; + if (TryGetPeer(target, out var peer)) + { + switch (peer.ConnectionState) + { + //just return already connected peer + case ConnectionState.Connected: + case ConnectionState.Outgoing: + return peer; + } + //else reconnect + connectionNumber = (byte)((peer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); + RemovePeer(peer, true); + } + + //Create reliable connection + //And send connection request + peer = CreateOutgoingPeer(target, GetNextPeerId(), connectionNumber, connectionData.AsReadOnlySpan()); + AddPeer(peer); + return peer; + } + } + + /// + /// Connect to remote host + /// + /// Server end point (ip and port) + /// Additional data for remote peer + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public LiteNetPeer Connect(IPEndPoint target, ReadOnlySpan connectionData) + { + if (!_isRunning) + throw new InvalidOperationException("Client is not running"); + + lock (_requestsDict) + { + if (_requestsDict.ContainsKey(target)) + return null; + + byte connectionNumber = 0; + if (TryGetPeer(target, out var peer)) + { + switch (peer.ConnectionState) + { + //just return already connected peer + case ConnectionState.Connected: + case ConnectionState.Outgoing: + return peer; + } + //else reconnect + connectionNumber = (byte)((peer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); + RemovePeer(peer, true); + } + + //Create reliable connection + //And send connection request + peer = CreateOutgoingPeer(target, GetNextPeerId(), connectionNumber, connectionData); + AddPeer(peer); + return peer; + } + } + + /// + /// Force closes connection and stop all threads. + /// + public void Stop() => + Stop(true); + + /// + /// Force closes connection and stop all threads. + /// + /// Send disconnect messages + public void Stop(bool sendDisconnectMessages) + { + if (!_isRunning) + return; + NetDebug.Write("[NM] Stop"); + + //Send last disconnect + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + netPeer.Shutdown(null, 0, 0, !sendDisconnectMessages); + + //Stop + CloseSocket(); + +#if UNITY_SOCKET_FIX + if (_useSocketFix) + { + _pausedSocketFix.Deinitialize(); + _pausedSocketFix = null; + } +#endif + + _updateTriggerEvent.Set(); + if (!_manualMode) + { + _logicThread.Join(); + _logicThread = null; + } + + //clear peers + ClearPeerSet(); + _peerIds = new ConcurrentQueue(); + _lastPeerId = 0; + + ClearPingSimulationList(); + ClearOutboundSimulationList(); + + _connectedPeersCount = 0; + _pendingEventHead = null; + _pendingEventTail = null; + } + + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] + private void ClearPingSimulationList() + { + lock (_pingSimulationList) + _pingSimulationList.Clear(); + } + + [Conditional("DEBUG"), Conditional("SIMULATE_NETWORK")] + private void ClearOutboundSimulationList() + { +#if DEBUG || SIMULATE_NETWORK + lock (_outboundSimulationList) + _outboundSimulationList.Clear(); +#endif + } + + /// + /// Return peers count with connection state + /// + /// peer connection state (you can use as bit flags) + /// peers count + public int GetPeersCount(ConnectionState peerState) + { + int count = 0; + _peersLock.EnterReadLock(); + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + { + if ((netPeer.ConnectionState & peerState) != 0) + count++; + } + _peersLock.ExitReadLock(); + return count; + } + + /// + /// Get copy of peers (without allocations) + /// + /// List that will contain result + /// State of peers + public void GetPeers(List peers, ConnectionState peerState) + { + peers.Clear(); + _peersLock.EnterReadLock(); + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + { + if ((netPeer.ConnectionState & peerState) != 0) + peers.Add(netPeer); + } + _peersLock.ExitReadLock(); + } + + /// + /// Get copy of connected peers (without allocations) + /// + /// List that will contain result + public void GetConnectedPeers(List peers) => + GetPeers(peers, ConnectionState.Connected); + + /// + /// Disconnect all peers without any additional data + /// + public void DisconnectAll() => + DisconnectAll(null, 0, 0); + + /// + /// Disconnect all peers with shutdown message + /// + /// Data to send (must be less or equal MTU) + /// Data start + /// Data count + public void DisconnectAll(byte[] data, int start, int count) + { + //Send disconnect packets + _peersLock.EnterReadLock(); + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + { + DisconnectPeer( + netPeer, + DisconnectReason.DisconnectPeerCalled, + 0, + false, + data, + start, + count, + null); + } + _peersLock.ExitReadLock(); + } + + /// + /// Immediately disconnect peer from server without additional data + /// + /// peer to disconnect + public void DisconnectPeerForce(LiteNetPeer peer) => + DisconnectPeerForce(peer, DisconnectReason.DisconnectPeerCalled, 0, null); + + /// + /// Disconnect peer from server + /// + /// peer to disconnect + public void DisconnectPeer(LiteNetPeer peer) => + DisconnectPeer(peer, null, 0, 0); + + /// + /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) + /// + /// peer to disconnect + /// additional data + public void DisconnectPeer(LiteNetPeer peer, byte[] data) => + DisconnectPeer(peer, data, 0, data.Length); + + /// + /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) + /// + /// peer to disconnect + /// additional data + public void DisconnectPeer(LiteNetPeer peer, NetDataWriter writer) => + DisconnectPeer(peer, writer.Data, 0, writer.Length); + + /// + /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) + /// + /// peer to disconnect + /// additional data + /// data start + /// data length + public void DisconnectPeer(LiteNetPeer peer, byte[] data, int start, int count) + { + DisconnectPeer( + peer, + DisconnectReason.DisconnectPeerCalled, + 0, + false, + data, + start, + count, + null); + } + + public NetPeerEnumerator GetEnumerator() => + new NetPeerEnumerator(_headPeer); + + IEnumerator IEnumerable.GetEnumerator() => + new NetPeerEnumerator(_headPeer); + + IEnumerator IEnumerable.GetEnumerator() => + new NetPeerEnumerator(_headPeer); + } +} diff --git a/LiteNetLib/LiteNetPeer.cs b/LiteNetLib/LiteNetPeer.cs new file mode 100644 index 00000000..78ba2e9d --- /dev/null +++ b/LiteNetLib/LiteNetPeer.cs @@ -0,0 +1,1356 @@ +#if DEBUG +#define STATS_ENABLED +#endif +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Runtime.CompilerServices; +using System.Threading; +using LiteNetLib.Utils; + +namespace LiteNetLib +{ + /// + /// Peer connection state + /// + [Flags] + public enum ConnectionState : byte + { + Outgoing = 1 << 1, + Connected = 1 << 2, + ShutdownRequested = 1 << 3, + Disconnected = 1 << 4, + EndPointChange = 1 << 5, + Any = Outgoing | Connected | ShutdownRequested | EndPointChange + } + + internal enum ConnectRequestResult + { + None, + P2PLose, //when peer connecting + Reconnection, //when peer was connected + NewConnection //when peer was disconnected + } + + internal enum DisconnectResult + { + None, + Reject, + Disconnect + } + + internal enum ShutdownResult + { + None, + Success, + WasConnected + } + + /// + /// Network peer. Main purpose is sending messages to specific peer. + /// + public class LiteNetPeer : IPEndPoint + { + //Ping and RTT + private int _rtt; + private int _avgRtt; + private int _rttCount; + private float _resendDelay = 27.0f; + private float _pingSendTimer; + private float _rttResetTimer; + private readonly Stopwatch _pingTimer = new Stopwatch(); + private float _timeSinceLastPacket; + private long _remoteDelta; + + //Common + private readonly object _shutdownLock = new object(); + + internal volatile LiteNetPeer NextPeer; + internal LiteNetPeer PrevPeer; + + internal byte ConnectionNum + { + get => _connectNum; + private set + { + _connectNum = value; + _mergeData.ConnectionNumber = value; + _pingPacket.ConnectionNumber = value; + _pongPacket.ConnectionNumber = value; + } + } + + //Channels + private NetPacket[] _unreliableSecondQueue; + private NetPacket[] _unreliableChannel; + private int _unreliablePendingCount; + private readonly object _unreliableChannelLock = new object(); + + //MTU + private int _mtu; + private int _mtuIdx; + private bool _finishMtu; + private float _mtuCheckTimer; + private int _mtuCheckAttempts; + private const int MtuCheckDelay = 1000; + private const int MaxMtuCheckAttempts = 4; + private readonly object _mtuMutex = new object(); + + //Fragment + private sealed class IncomingFragments + { + public readonly NetPacket[] Fragments; + public readonly ushort TotalFragments; + public readonly byte ChannelId; + + public int ReceivedCount; + public int TotalSize; + + public IncomingFragments(ushort totalFragments, byte channelId) + { + TotalFragments = totalFragments; + ChannelId = channelId; + Fragments = new NetPacket[totalFragments]; + } + + public void RecycleAll(LiteNetManager netManager) + { + for (int i = 0; i < Fragments.Length; i++) + { + NetPacket packet = Fragments[i]; + if (packet != null) + { + netManager.PoolRecycle(packet); + Fragments[i] = null; + } + } + } + } + private int _fragmentId; + private readonly Dictionary _holdedFragments; + private readonly Dictionary _deliveredFragments; + + //Merging + private readonly NetPacket _mergeData; + private int _mergePos; + private int _mergeCount; + + //Connection + private int _connectAttempts; + private float _connectTimer; + private long _connectTime; + private byte _connectNum; + private ConnectionState _connectionState; + private NetPacket _shutdownPacket; + private const int ShutdownDelay = 300; + private float _shutdownTimer; + private readonly NetPacket _pingPacket; + private readonly NetPacket _pongPacket; + private readonly NetPacket _connectRequestPacket; + private readonly NetPacket _connectAcceptPacket; + + /// + /// Peer parent NetManager + /// + public readonly LiteNetManager NetManager; + + /// + /// Current connection state + /// + public ConnectionState ConnectionState => _connectionState; + + /// + /// Connection time for internal purposes + /// + internal long ConnectTime => _connectTime; + + /// + /// Peer id can be used as key in your dictionary of peers + /// + public readonly int Id; + + /// + /// Id assigned from server + /// + public int RemoteId { get; private set; } + + /// + /// Current one-way ping (RTT/2) in milliseconds + /// + public int Ping => _avgRtt/2; + + /// + /// Round trip time in milliseconds + /// + public int RoundTripTime => _avgRtt; + + /// + /// Current MTU - Maximum Transfer Unit ( maximum udp packet size without fragmentation ) + /// + public int Mtu => _mtu; + + /// + /// Delta with remote time in ticks (not accurate) + /// positive - remote time > our time + /// + public long RemoteTimeDelta => _remoteDelta; + + /// + /// Remote UTC time (not accurate) + /// + public DateTime RemoteUtcTime => new DateTime(DateTime.UtcNow.Ticks + _remoteDelta); + + /// + /// Time since last packet received (including internal library packets) in milliseconds + /// + public float TimeSinceLastPacket => _timeSinceLastPacket; + + /// + /// Fixed part of the resend delay + /// + public float ResendFixedDelay = 25.0f; + + /// + /// Multiplication factor of Rtt in the resend delay calculation + /// + public float ResendRttMultiplier = 2.1f; + + internal float ResendDelay => _resendDelay; + + /// + /// Application defined object containing data about the connection + /// + public object Tag; + + /// + /// Statistics of peer connection + /// + public readonly NetStatistics Statistics; + + private SocketAddress _cachedSocketAddr; + private int _cachedHashCode; + private ReliableChannel _reliableChannel; + private ReliableChannel _reliableUnorderedChannel; + private SequencedChannel _sequencedChannel; + + internal byte[] NativeAddress; + + protected virtual int ChannelsCount => 1; + + /// + /// IPEndPoint serialize + /// + /// SocketAddress + public override SocketAddress Serialize() => + _cachedSocketAddr; + + public override int GetHashCode() => + //uses SocketAddress hash in NET8 and IPEndPoint hash for NativeSockets and previous NET versions + _cachedHashCode; + + //incoming connection constructor + internal LiteNetPeer(LiteNetManager netManager, IPEndPoint remoteEndPoint, int id) : base(remoteEndPoint.Address, remoteEndPoint.Port) + { + Id = id; + Statistics = new NetStatistics(); + NetManager = netManager; + + _cachedSocketAddr = base.Serialize(); + if (NetManager.UseNativeSockets) + { + NativeAddress = new byte[_cachedSocketAddr.Size]; + for (int i = 0; i < _cachedSocketAddr.Size; i++) + NativeAddress[i] = _cachedSocketAddr[i]; + } +#if NET8_0_OR_GREATER + _cachedHashCode = NetManager.UseNativeSockets ? base.GetHashCode() : _cachedSocketAddr.GetHashCode(); +#else + _cachedHashCode = base.GetHashCode(); +#endif + + ResetMtu(); + + _connectionState = ConnectionState.Connected; + _mergeData = new NetPacket(PacketProperty.Merged, NetConstants.MaxPacketSize); + _pongPacket = new NetPacket(PacketProperty.Pong, 0); + _pingPacket = new NetPacket(PacketProperty.Ping, 0) {Sequence = 1}; + + _unreliableSecondQueue = new NetPacket[8]; + _unreliableChannel = new NetPacket[8]; + _holdedFragments = new Dictionary(); + _deliveredFragments = new Dictionary(); + } + + internal void InitiateEndPointChange() + { + ResetMtu(); + _connectionState = ConnectionState.EndPointChange; + } + + internal void FinishEndPointChange(IPEndPoint newEndPoint) + { + if (_connectionState != ConnectionState.EndPointChange) + return; + _connectionState = ConnectionState.Connected; + + Address = newEndPoint.Address; + Port = newEndPoint.Port; + + _cachedSocketAddr = base.Serialize(); + if (NetManager.UseNativeSockets) + { + NativeAddress = new byte[_cachedSocketAddr.Size]; + for (int i = 0; i < _cachedSocketAddr.Size; i++) + NativeAddress[i] = _cachedSocketAddr[i]; + } +#if NET8_0_OR_GREATER + _cachedHashCode = NetManager.UseNativeSockets ? base.GetHashCode() : _cachedSocketAddr.GetHashCode(); +#else + _cachedHashCode = base.GetHashCode(); +#endif + } + + internal void ResetMtu() + { + //finish if discovery disabled + _finishMtu = !NetManager.MtuDiscovery; + if (NetManager.MtuOverride > 0) + OverrideMtu(NetManager.MtuOverride); + else + SetMtu(0); + } + + private void SetMtu(int mtuIdx) + { + _mtuIdx = mtuIdx; + _mtu = NetConstants.PossibleMtu[mtuIdx] - NetManager.ExtraPacketSizeForLayer; + } + + private void OverrideMtu(int mtuValue) + { + _mtu = mtuValue; + _finishMtu = true; + } + + /// + /// Returns packets count in queue for reliable channel 0 + /// + /// type of channel ReliableOrdered or ReliableUnordered + /// packets count in channel queue + public int GetPacketsCountInReliableQueue(bool ordered) => + (ordered ? _reliableChannel : _reliableUnorderedChannel)?.PacketsInQueue ?? 0; + + /// + /// Create temporary packet (maximum size MTU - headerSize) to send later without additional copies + /// + /// Delivery method (reliable, unreliable, etc.) + /// PooledPacket that you can use to write data starting from UserDataOffset + public PooledPacket CreatePacketFromPool(DeliveryMethod deliveryMethod) + { + //multithreaded variable + int mtu = _mtu; + var packet = NetManager.PoolGetPacket(mtu); + if (deliveryMethod == DeliveryMethod.Unreliable) + { + packet.Property = PacketProperty.Unreliable; + return new PooledPacket(packet, mtu, 0); + } + else + { + packet.Property = PacketProperty.Channeled; + return new PooledPacket(packet, mtu, (byte)deliveryMethod); + } + } + + /// + /// Sends pooled packet without data copy + /// + /// packet to send + /// size of user data you want to send + public void SendPooledPacket(PooledPacket packet, int userDataSize) + { + if (_connectionState != ConnectionState.Connected) + return; + packet._packet.Size = packet.UserDataOffset + userDataSize; + if (packet._packet.Property == PacketProperty.Channeled) + { + CreateChannel(packet._channelNumber).AddToQueue(packet._packet); + } + else + { + lock (_unreliableChannelLock) + { + if (_unreliablePendingCount == _unreliableChannel.Length) + Array.Resize(ref _unreliableChannel, _unreliablePendingCount*2); + _unreliableChannel[_unreliablePendingCount++] = packet._packet; + } + } + } + + internal virtual BaseChannel CreateChannel(byte channelNumber) + { + switch ((DeliveryMethod)channelNumber) + { + case DeliveryMethod.ReliableOrdered: + case DeliveryMethod.ReliableSequenced: + return _reliableChannel ??= new ReliableChannel(this, true, (int)DeliveryMethod.ReliableOrdered); + + case DeliveryMethod.ReliableUnordered: + return _reliableUnorderedChannel ??= new ReliableChannel(this, false, channelNumber); + + case DeliveryMethod.Sequenced: + return _sequencedChannel ??= new SequencedChannel(this, true, channelNumber); + + default: + throw new Exception("Invalid channel type"); + } + } + + //"Connect to" constructor + internal LiteNetPeer(LiteNetManager netManager, IPEndPoint remoteEndPoint, int id, byte connectNum, ReadOnlySpan connectData) + : this(netManager, remoteEndPoint, id) + { + _connectTime = DateTime.UtcNow.Ticks; + _connectionState = ConnectionState.Outgoing; + ConnectionNum = connectNum; + + //Make initial packet + _connectRequestPacket = NetConnectRequestPacket.Make(connectData, remoteEndPoint.Serialize(), _connectTime, id); + _connectRequestPacket.ConnectionNumber = connectNum; + + //Send request + NetManager.SendRaw(_connectRequestPacket, this); + + NetDebug.Write(NetLogLevel.Trace, $"[CC] ConnectId: {_connectTime}, ConnectNum: {connectNum}"); + } + + //"Accept" incoming constructor + internal LiteNetPeer(LiteNetManager netManager, LiteConnectionRequest request, int id) + : this(netManager, request.RemoteEndPoint, id) + { + _connectTime = request.InternalPacket.ConnectionTime; + ConnectionNum = request.InternalPacket.ConnectionNumber; + RemoteId = request.InternalPacket.PeerId; + + //Make initial packet + _connectAcceptPacket = NetConnectAcceptPacket.Make(_connectTime, ConnectionNum, id); + + //Make Connected + _connectionState = ConnectionState.Connected; + + //Send + NetManager.SendRaw(_connectAcceptPacket, this); + + NetDebug.Write(NetLogLevel.Trace, $"[CC] ConnectId: {_connectTime}"); + } + + //Reject + internal void Reject(NetConnectRequestPacket requestData, byte[] data, int start, int length) + { + _connectTime = requestData.ConnectionTime; + _connectNum = requestData.ConnectionNumber; + Shutdown(data, start, length, false); + } + + internal bool ProcessConnectAccept(NetConnectAcceptPacket packet) + { + if (_connectionState != ConnectionState.Outgoing) + return false; + + //check connection id + if (packet.ConnectionTime != _connectTime) + { + NetDebug.Write(NetLogLevel.Trace, $"[NC] Invalid connectId: {packet.ConnectionTime} != our({_connectTime})"); + return false; + } + //check connect num + ConnectionNum = packet.ConnectionNumber; + RemoteId = packet.PeerId; + + NetDebug.Write(NetLogLevel.Trace, "[NC] Received connection accept"); + Interlocked.Exchange(ref _timeSinceLastPacket, 0); + _connectionState = ConnectionState.Connected; + return true; + } + + /// + /// Gets maximum size of packet that will be not fragmented. + /// + /// Type of packet that you want send + /// size in bytes + public int GetMaxSinglePacketSize(DeliveryMethod options) => + _mtu - NetPacket.GetHeaderSize(options == DeliveryMethod.Unreliable ? PacketProperty.Unreliable : PacketProperty.Channeled); + + /// + /// Send data to peer with delivery event called + /// + /// Data + /// Delivery method (reliable, unreliable, etc.) + /// User data that will be received in DeliveryEvent + /// + /// If you trying to send unreliable packet type + /// + public void SendWithDeliveryEvent(byte[] data, DeliveryMethod deliveryMethod, object userData) + { + if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) + throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); + SendInternal(new ReadOnlySpan(data, 0, data.Length), 0, deliveryMethod, userData); + } + + /// + /// Send data to peer with delivery event called + /// + /// Data + /// Start of data + /// Length of data + /// Delivery method (reliable, unreliable, etc.) + /// User data that will be received in DeliveryEvent + /// + /// If you trying to send unreliable packet type + /// + public void SendWithDeliveryEvent(byte[] data, int start, int length, DeliveryMethod deliveryMethod, object userData) + { + if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) + throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); + SendInternal(new ReadOnlySpan(data, start, length), 0, deliveryMethod, userData); + } + + /// + /// Send data to peer with delivery event called + /// + /// Data + /// Delivery method (reliable, unreliable, etc.) + /// User data that will be received in DeliveryEvent + /// + /// If you trying to send unreliable packet type + /// + public void SendWithDeliveryEvent(NetDataWriter dataWriter, DeliveryMethod deliveryMethod, object userData) + { + if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) + throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); + SendInternal(dataWriter.AsReadOnlySpan(), 0, deliveryMethod, userData); + } + + /// + /// Send data to peer (channel - 0) + /// + /// Data + /// Send options (reliable, unreliable, etc.) + /// + /// If size exceeds maximum limit: + /// MTU - headerSize bytes for Unreliable + /// Fragment count exceeded ushort.MaxValue + /// + public void Send(byte[] data, DeliveryMethod deliveryMethod) => + SendInternal(new ReadOnlySpan(data, 0, data.Length), 0, deliveryMethod, null); + + /// + /// Send data to peer (channel - 0) + /// + /// DataWriter with data + /// Send options (reliable, unreliable, etc.) + /// + /// If size exceeds maximum limit: + /// MTU - headerSize bytes for Unreliable + /// Fragment count exceeded ushort.MaxValue + /// + public void Send(NetDataWriter dataWriter, DeliveryMethod deliveryMethod) => + SendInternal(dataWriter.AsReadOnlySpan(), 0, deliveryMethod, null); + + /// + /// Send data to peer (channel - 0) + /// + /// Data + /// Start of data + /// Length of data + /// Send options (reliable, unreliable, etc.) + /// + /// If size exceeds maximum limit: + /// MTU - headerSize bytes for Unreliable + /// Fragment count exceeded ushort.MaxValue + /// + public void Send(byte[] data, int start, int length, DeliveryMethod options) => + SendInternal(new ReadOnlySpan(data, start, length), 0, options, null); + + /// + /// Send data to peer with delivery event called + /// + /// Data + /// Delivery method (reliable, unreliable, etc.) + /// User data that will be received in DeliveryEvent + /// + /// If you trying to send unreliable packet type + /// + public void SendWithDeliveryEvent(ReadOnlySpan data, DeliveryMethod deliveryMethod, object userData) + { + if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) + throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); + SendInternal(data, 0, deliveryMethod, userData); + } + + /// + /// Send data to peer (channel - 0) + /// + /// Data + /// Send options (reliable, unreliable, etc.) + /// + /// If size exceeds maximum limit: + /// MTU - headerSize bytes for Unreliable + /// Fragment count exceeded ushort.MaxValue + /// + public void Send(ReadOnlySpan data, DeliveryMethod deliveryMethod) => + SendInternal(data, 0, deliveryMethod, null); + + protected void SendInternal( + ReadOnlySpan data, + byte channelNumber, + DeliveryMethod deliveryMethod, + object userData) + { + if (_connectionState != ConnectionState.Connected || channelNumber >= ChannelsCount) + return; + + //Select channel + PacketProperty property; + BaseChannel channel = null; + + if (deliveryMethod == DeliveryMethod.Unreliable) + { + property = PacketProperty.Unreliable; + } + else + { + property = PacketProperty.Channeled; + channel = CreateChannel((byte)(channelNumber * NetConstants.ChannelTypeCount + (byte)deliveryMethod)); + } + + //Prepare + NetDebug.Write("[RS]Packet: " + property); + + //Check fragmentation + int headerSize = NetPacket.GetHeaderSize(property); + //Save mtu for multithread + int mtu = _mtu; + int length = data.Length; + if (length + headerSize > mtu) + { + //if cannot be fragmented + if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) + throw new TooBigPacketException("Unreliable or ReliableSequenced packet size exceeded maximum of " + (mtu - headerSize) + " bytes, Check allowed size by GetMaxSinglePacketSize()"); + + int packetFullSize = mtu - headerSize; + int packetDataSize = packetFullSize - NetConstants.FragmentHeaderSize; + int totalPackets = length / packetDataSize + (length % packetDataSize == 0 ? 0 : 1); + + if (totalPackets > NetManager.MaxFragmentsCount) + throw new TooBigPacketException("Data was split in " + totalPackets + " fragments, which exceeds " + NetManager.MaxFragmentsCount); + + ushort currentFragmentId = (ushort)Interlocked.Increment(ref _fragmentId); + + for (ushort partIdx = 0; partIdx < totalPackets; partIdx++) + { + int sendLength = length > packetDataSize ? packetDataSize : length; + + NetPacket p = NetManager.PoolGetPacket(headerSize + sendLength + NetConstants.FragmentHeaderSize); + p.Property = property; + p.UserData = userData; + p.FragmentId = currentFragmentId; + p.FragmentPart = partIdx; + p.FragmentsTotal = (ushort)totalPackets; + p.MarkFragmented(); + + data.Slice(partIdx * packetDataSize, sendLength).CopyTo(new Span(p.RawData, NetConstants.FragmentedHeaderTotalSize, sendLength)); + channel.AddToQueue(p); + + length -= sendLength; + } + return; + } + + //Else just send + NetPacket packet = NetManager.PoolGetPacket(headerSize + length); + packet.Property = property; + data.CopyTo(new Span(packet.RawData, headerSize, length)); + packet.UserData = userData; + + if (channel == null) //unreliable + { + lock (_unreliableChannelLock) + { + if (_unreliablePendingCount == _unreliableChannel.Length) + Array.Resize(ref _unreliableChannel, _unreliablePendingCount*2); + _unreliableChannel[_unreliablePendingCount++] = packet; + } + } + else + { + channel.AddToQueue(packet); + } + } + + public void Disconnect(byte[] data) => + NetManager.DisconnectPeer(this, data); + + public void Disconnect(NetDataWriter writer) => + NetManager.DisconnectPeer(this, writer); + + public void Disconnect(byte[] data, int start, int count) => + NetManager.DisconnectPeer(this, data, start, count); + + public void Disconnect() => + NetManager.DisconnectPeer(this); + + internal DisconnectResult ProcessDisconnect(NetPacket packet) + { + if ((_connectionState == ConnectionState.Connected || _connectionState == ConnectionState.Outgoing) && + packet.Size >= 9 && + BitConverter.ToInt64(packet.RawData, 1) == _connectTime && + packet.ConnectionNumber == _connectNum) + { + return _connectionState == ConnectionState.Connected + ? DisconnectResult.Disconnect + : DisconnectResult.Reject; + } + return DisconnectResult.None; + } + + internal virtual void AddToReliableChannelSendQueue(BaseChannel channel) + { + + } + + /// + /// Internally handles the shutdown process for this peer. + /// + /// Optional data to include in the unreliable disconnect packet. + /// Offset in the array. + /// Length of the data to send. + /// + /// If , immediately sets state to without sending a notification.
+ /// If , sends unreliable disconnect packets until a timeout occurs and sets state to + /// Queued reliable packets are bypassed and dropped immediately. + /// + /// A indicating the state change transition. + internal ShutdownResult Shutdown(byte[] data, int start, int length, bool force) + { + lock (_shutdownLock) + { + //trying to shutdown already disconnected + if (_connectionState == ConnectionState.Disconnected || + _connectionState == ConnectionState.ShutdownRequested) + { + return ShutdownResult.None; + } + + var result = _connectionState == ConnectionState.Connected + ? ShutdownResult.WasConnected + : ShutdownResult.Success; + + //don't send anything + if (force) + { + _connectionState = ConnectionState.Disconnected; + return result; + } + + //reset time for reconnect protection + Interlocked.Exchange(ref _timeSinceLastPacket, 0); + + //send shutdown packet + _shutdownPacket = new NetPacket(PacketProperty.Disconnect, length) {ConnectionNumber = _connectNum}; + FastBitConverter.GetBytes(_shutdownPacket.RawData, 1, _connectTime); + if (_shutdownPacket.Size >= _mtu) + { + //Drop additional data + NetDebug.WriteError("[Peer] Disconnect additional data size more than MTU - 8!"); + } + else if (data != null && length > 0) + { + Buffer.BlockCopy(data, start, _shutdownPacket.RawData, 9, length); + } + _connectionState = ConnectionState.ShutdownRequested; + NetDebug.Write("[Peer] Send disconnect"); + NetManager.SendRaw(_shutdownPacket, this); + return result; + } + } + + private void UpdateRoundTripTime(int roundTripTime) + { + _rtt += roundTripTime; + _rttCount++; + _avgRtt = _rtt/_rttCount; + _resendDelay = ResendFixedDelay + _avgRtt * ResendRttMultiplier; + } + + internal void AddReliablePacket(DeliveryMethod method, NetPacket p) + { + if (p.IsFragmented) + { + if (p.FragmentsTotal == 0 || p.FragmentsTotal > NetManager.MaxFragmentsCount) + { + NetManager.PoolRecycle(p); + NetDebug.WriteError($"Invalid FragmentsTotal: {p.FragmentsTotal}"); + return; + } + + if (p.FragmentPart >= p.FragmentsTotal) + { + NetManager.PoolRecycle(p); + NetDebug.WriteError($"FragmentPart {p.FragmentPart} >= FragmentsTotal {p.FragmentsTotal}"); + return; + } + + NetDebug.Write($"Fragment. Id: {p.FragmentId}, Part: {p.FragmentPart}, Total: {p.FragmentsTotal}"); + + //Get needed array from dictionary + ushort packetFragId = p.FragmentId; + byte packetChannelId = p.ChannelId; + if (!_holdedFragments.TryGetValue(packetFragId, out var incomingFragments)) + { + //Holded fragments limit reached + if (_holdedFragments.Count >= NetConstants.MaxFragmentsInWindow * ChannelsCount * NetConstants.FragmentedChannelsCount) + { + NetManager.PoolRecycle(p); + //NetDebug.WriteError($"Holded fragments limit reached ({_holdedFragments.Count}/{(NetConstants.DefaultWindowSize / 2) * ChannelsCount * NetConstants.FragmentedChannelsCount}). Dropping fragment id: {packetFragId}"); + return; + } + + incomingFragments = new IncomingFragments(p.FragmentsTotal, p.ChannelId); + _holdedFragments.Add(packetFragId, incomingFragments); + } + else if (p.FragmentsTotal != incomingFragments.TotalFragments || p.ChannelId != incomingFragments.ChannelId) + { + NetManager.PoolRecycle(p); + NetDebug.WriteError("Fragment metadata mismatch"); + return; + } + + //Cache + var fragments = incomingFragments.Fragments; + + //Error check + if (fragments[p.FragmentPart] != null) + { + NetManager.PoolRecycle(p); + NetDebug.WriteError("Invalid fragment packet"); + return; + } + + //Fill array + fragments[p.FragmentPart] = p; + + //Increase received fragments count + incomingFragments.ReceivedCount++; + + //Increase total size + incomingFragments.TotalSize += p.Size - NetConstants.FragmentedHeaderTotalSize; + + //Check for finish + if (incomingFragments.ReceivedCount != incomingFragments.TotalFragments) + return; + + //Just simple packet + NetPacket resultingPacket = NetManager.PoolGetPacket(incomingFragments.TotalSize); + + void AbortReassembly(string error) + { + _holdedFragments.Remove(packetFragId); + incomingFragments.RecycleAll(NetManager); + NetManager.PoolRecycle(resultingPacket); + NetDebug.WriteError(error); + } + + int pos = 0; + for (ushort i = 0; i < incomingFragments.TotalFragments; i++) + { + NetPacket fragment = fragments[i]; + if (fragment == null) + { + AbortReassembly($"Fragment {i} missing during reassembly"); + return; + } + + if (fragment.Size > fragment.RawData.Length) + { + AbortReassembly($"Fragment error size: {fragment.Size} > fragment.RawData.Length: {fragment.RawData.Length}"); + return; + } + + int writtenSize = fragment.Size - NetConstants.FragmentedHeaderTotalSize; + if (pos + writtenSize > resultingPacket.RawData.Length) + { + AbortReassembly($"Fragment error pos: {pos + writtenSize} >= resultPacketSize: {resultingPacket.RawData.Length}"); + return; + } + + //Create resulting big packet + Buffer.BlockCopy( + fragment.RawData, + NetConstants.FragmentedHeaderTotalSize, + resultingPacket.RawData, + pos, + writtenSize); + + pos += writtenSize; + + //Free memory + NetManager.PoolRecycle(fragment); + fragments[i] = null; + } + + //Clear memory + _holdedFragments.Remove(packetFragId); + + //Send to process + NetManager.CreateReceiveEvent(resultingPacket, method, (byte)(packetChannelId / NetConstants.ChannelTypeCount), 0, this); + } + else //Just simple packet + { + NetManager.CreateReceiveEvent(p, method, (byte)(p.ChannelId / NetConstants.ChannelTypeCount), NetConstants.ChanneledHeaderSize, this); + } + } + + private void ProcessMtuPacket(NetPacket packet) + { + //header + int + if (packet.Size < NetConstants.PossibleMtu[0]) + return; + + //first stage check (mtu check and mtu ok) + int receivedMtu = BitConverter.ToInt32(packet.RawData, 1); + int endMtuCheck = BitConverter.ToInt32(packet.RawData, packet.Size - 4); + if (receivedMtu != packet.Size || receivedMtu != endMtuCheck || receivedMtu > NetConstants.MaxPacketSize) + { + NetDebug.WriteError($"[MTU] Broken packet. RMTU {receivedMtu}, EMTU {endMtuCheck}, PSIZE {packet.Size}"); + return; + } + + if (packet.Property == PacketProperty.MtuCheck) + { + _mtuCheckAttempts = 0; + NetDebug.Write("[MTU] check. send back: " + receivedMtu); + packet.Property = PacketProperty.MtuOk; + NetManager.SendRawAndRecycle(packet, this); + } + else if(receivedMtu > _mtu && !_finishMtu) //MtuOk + { + //invalid packet + if (receivedMtu != NetConstants.PossibleMtu[_mtuIdx + 1] - NetManager.ExtraPacketSizeForLayer) + return; + + lock (_mtuMutex) + { + SetMtu(_mtuIdx+1); + } + //if maxed - finish. + if (_mtuIdx == NetConstants.PossibleMtu.Length - 1) + _finishMtu = true; + NetManager.PoolRecycle(packet); + NetDebug.Write("[MTU] ok. Increase to: " + _mtu); + } + } + + private void UpdateMtuLogic(float deltaTime) + { + if (_finishMtu) + return; + + _mtuCheckTimer += deltaTime; + if (_mtuCheckTimer < MtuCheckDelay) + return; + + _mtuCheckTimer = 0; + _mtuCheckAttempts++; + if (_mtuCheckAttempts >= MaxMtuCheckAttempts) + { + _finishMtu = true; + return; + } + + lock (_mtuMutex) + { + if (_mtuIdx >= NetConstants.PossibleMtu.Length - 1) + return; + + //Send increased packet + int newMtu = NetConstants.PossibleMtu[_mtuIdx + 1] - NetManager.ExtraPacketSizeForLayer; + var p = NetManager.PoolGetPacket(newMtu); + p.Property = PacketProperty.MtuCheck; + FastBitConverter.GetBytes(p.RawData, 1, newMtu); //place into start + FastBitConverter.GetBytes(p.RawData, p.Size - 4, newMtu);//and end of packet + + //Must check result for MTU fix + if (NetManager.SendRawAndRecycle(p, this) <= 0) + _finishMtu = true; + } + } + + /// + /// Evaluates incoming connection requests against the current peer state to supply Reconnect Protection. + /// + /// The incoming connection request packet. + /// A directing how the manager should handle the request. + /// + /// If the state is , the peer lingers to ignore older connection requests + /// (where the packet timestamp is smaller than internal ), ensuring older connections are ignored. + /// + internal ConnectRequestResult ProcessConnectRequest(NetConnectRequestPacket connRequest) + { + //current or new request + switch (_connectionState) + { + //P2P case + case ConnectionState.Outgoing: + //fast check + if (connRequest.ConnectionTime < _connectTime) + { + return ConnectRequestResult.P2PLose; + } + //slow rare case check + if (connRequest.ConnectionTime == _connectTime) + { + var localBytes = connRequest.TargetAddress; + for (int i = _cachedSocketAddr.Size-1; i >= 0; i--) + { + byte rb = _cachedSocketAddr[i]; + if (rb == localBytes[i]) + continue; + if (rb < localBytes[i]) + return ConnectRequestResult.P2PLose; + } + } + break; + + case ConnectionState.Connected: + //Old connect request + if (connRequest.ConnectionTime == _connectTime) + { + //just reply accept + NetManager.SendRaw(_connectAcceptPacket, this); + } + //New connect request + else if (connRequest.ConnectionTime > _connectTime) + { + return ConnectRequestResult.Reconnection; + } + break; + + case ConnectionState.Disconnected: + case ConnectionState.ShutdownRequested: + if (connRequest.ConnectionTime >= _connectTime) + return ConnectRequestResult.NewConnection; + break; + } + return ConnectRequestResult.None; + } + + internal virtual void ProcessChanneled(NetPacket packet) + { + if (packet.ChannelId >= NetConstants.ChannelTypeCount || + packet.ChannelId == (int)DeliveryMethod.ReliableSequenced) + { + NetManager.PoolRecycle(packet); + return; + } + + if (!CreateChannel(packet.ChannelId).ProcessPacket(packet)) + NetManager.PoolRecycle(packet); + } + + //Process incoming packet + internal void ProcessPacket(NetPacket packet) + { + //not initialized + if (_connectionState == ConnectionState.Outgoing || _connectionState == ConnectionState.Disconnected) + { + NetManager.PoolRecycle(packet); + return; + } + if (packet.Property == PacketProperty.ShutdownOk) + { + if (_connectionState == ConnectionState.ShutdownRequested) + _connectionState = ConnectionState.Disconnected; + NetManager.PoolRecycle(packet); + return; + } + if (packet.ConnectionNumber != _connectNum) + { + NetDebug.Write(NetLogLevel.Trace, "[RR]Old packet"); + NetManager.PoolRecycle(packet); + return; + } + Interlocked.Exchange(ref _timeSinceLastPacket, 0); + + NetDebug.Write($"[RR]PacketProperty: {packet.Property}"); + switch (packet.Property) + { + case PacketProperty.Merged: + int pos = NetConstants.HeaderSize; + while (pos < packet.Size) + { + ushort size = BitConverter.ToUInt16(packet.RawData, pos); + if (size == 0) + break; + + pos += 2; + if (packet.Size - pos < size) + break; + + NetPacket mergedPacket = NetManager.PoolGetPacket(size); + Buffer.BlockCopy(packet.RawData, pos, mergedPacket.RawData, 0, size); + mergedPacket.Size = size; + + if (!mergedPacket.Verify()) + break; + + pos += size; + ProcessPacket(mergedPacket); + } + NetManager.PoolRecycle(packet); + break; + //If we get ping, send pong + case PacketProperty.Ping: + if (NetUtils.RelativeSequenceNumber(packet.Sequence, _pongPacket.Sequence) > 0) + { + NetDebug.Write("[PP]Ping receive, send pong"); + FastBitConverter.GetBytes(_pongPacket.RawData, 3, DateTime.UtcNow.Ticks); + _pongPacket.Sequence = packet.Sequence; + NetManager.SendRaw(_pongPacket, this); + } + NetManager.PoolRecycle(packet); + break; + + //If we get pong, calculate ping time and rtt + case PacketProperty.Pong: + if (packet.Sequence == _pingPacket.Sequence) + { + _pingTimer.Stop(); + int elapsedMs = (int)_pingTimer.ElapsedMilliseconds; + _remoteDelta = BitConverter.ToInt64(packet.RawData, 3) + (elapsedMs * TimeSpan.TicksPerMillisecond ) / 2 - DateTime.UtcNow.Ticks; + UpdateRoundTripTime(elapsedMs); + NetManager.ConnectionLatencyUpdated(this, elapsedMs / 2); + NetDebug.Write($"[PP]Ping: {packet.Sequence} - {elapsedMs} - {_remoteDelta}"); + } + NetManager.PoolRecycle(packet); + break; + + case PacketProperty.Ack: + case PacketProperty.Channeled: + case PacketProperty.ReliableMerged: + ProcessChanneled(packet); + break; + + //Simple packet without acks + case PacketProperty.Unreliable: + NetManager.CreateReceiveEvent(packet, DeliveryMethod.Unreliable, 0, NetConstants.HeaderSize, this); + return; + + case PacketProperty.MtuCheck: + case PacketProperty.MtuOk: + ProcessMtuPacket(packet); + break; + + default: + NetDebug.WriteError("Error! Unexpected packet type: " + packet.Property); + break; + } + } + + private void SendMerged() + { + if (_mergeCount == 0) + return; + int bytesSent; + if (_mergeCount > 1) + { + NetDebug.Write("[P]Send merged: " + _mergePos + ", count: " + _mergeCount); + bytesSent = NetManager.SendRaw(_mergeData.RawData, 0, NetConstants.HeaderSize + _mergePos, this); + } + else + { + //Send without length information and merging + bytesSent = NetManager.SendRaw(_mergeData.RawData, NetConstants.HeaderSize + 2, _mergePos - 2, this); + } + + if (NetManager.EnableStatistics) + { + Statistics.IncrementPacketsSent(); + Statistics.AddBytesSent(bytesSent); + } + + _mergePos = 0; + _mergeCount = 0; + } + + internal void SendUserData(NetPacket packet) + { + packet.ConnectionNumber = _connectNum; + int mergedPacketSize = NetConstants.HeaderSize + packet.Size + 2; + const int sizeTreshold = 20; + if (mergedPacketSize + sizeTreshold >= _mtu) + { + NetDebug.Write(NetLogLevel.Trace, "[P]SendingPacket: " + packet.Property); + int bytesSent = NetManager.SendRaw(packet, this); + + if (NetManager.EnableStatistics) + { + Statistics.IncrementPacketsSent(); + Statistics.AddBytesSent(bytesSent); + } + + return; + } + if (_mergePos + mergedPacketSize > _mtu) + SendMerged(); + + FastBitConverter.GetBytes(_mergeData.RawData, _mergePos + NetConstants.HeaderSize, (ushort)packet.Size); + Buffer.BlockCopy(packet.RawData, 0, _mergeData.RawData, _mergePos + NetConstants.HeaderSize + 2, packet.Size); + _mergePos += packet.Size + 2; + _mergeCount++; + //DebugWriteForce("Merged: " + _mergePos + "/" + (_mtu - 2) + ", count: " + _mergeCount); + } + + protected virtual void UpdateChannels() + { + _reliableChannel?.SendNextPackets(); + _reliableUnorderedChannel?.SendNextPackets(); + _sequencedChannel?.SendNextPackets(); + } + + internal void Update(float deltaTime) + { + Interlocked.Exchange(ref _timeSinceLastPacket, _timeSinceLastPacket + deltaTime); + switch (_connectionState) + { + case ConnectionState.Connected: + if (_timeSinceLastPacket > NetManager.DisconnectTimeout) + { + NetDebug.Write($"[UPDATE] Disconnect by timeout: {_timeSinceLastPacket} > {NetManager.DisconnectTimeout}"); + NetManager.DisconnectPeerForce(this, DisconnectReason.Timeout, 0, null); + return; + } + break; + + case ConnectionState.ShutdownRequested: + if (_timeSinceLastPacket > NetManager.DisconnectTimeout) + { + _connectionState = ConnectionState.Disconnected; + } + else + { + _shutdownTimer += deltaTime; + if (_shutdownTimer >= ShutdownDelay) + { + _shutdownTimer = 0; + NetManager.SendRaw(_shutdownPacket, this); + } + } + return; + + case ConnectionState.Outgoing: + _connectTimer += deltaTime; + if (_connectTimer > NetManager.ReconnectDelay) + { + _connectTimer = 0; + _connectAttempts++; + if (_connectAttempts > NetManager.MaxConnectAttempts) + { + NetManager.DisconnectPeerForce(this, DisconnectReason.ConnectionFailed, 0, null); + return; + } + + //else send connect again + NetManager.SendRaw(_connectRequestPacket, this); + } + return; + + case ConnectionState.Disconnected: + return; + } + + //Send ping + _pingSendTimer += deltaTime; + if (_pingSendTimer >= NetManager.PingInterval) + { + NetDebug.Write("[PP] Send ping..."); + //reset timer + _pingSendTimer = 0; + //send ping + _pingPacket.Sequence++; + //ping timeout + if (_pingTimer.IsRunning) + UpdateRoundTripTime((int)_pingTimer.ElapsedMilliseconds); + _pingTimer.Restart(); + NetManager.SendRaw(_pingPacket, this); + } + + //RTT - round trip time + _rttResetTimer += deltaTime; + if (_rttResetTimer >= NetManager.PingInterval * 3) + { + _rttResetTimer = 0; + _rtt = _avgRtt; + _rttCount = 1; + } + + UpdateMtuLogic(deltaTime); + + UpdateChannels(); + + if (_unreliablePendingCount > 0) + { + int unreliableCount; + lock (_unreliableChannelLock) + { + (_unreliableChannel, _unreliableSecondQueue) = (_unreliableSecondQueue, _unreliableChannel); + unreliableCount = _unreliablePendingCount; + _unreliablePendingCount = 0; + } + for (int i = 0; i < unreliableCount; i++) + { + var packet = _unreliableSecondQueue[i]; + SendUserData(packet); + NetManager.PoolRecycle(packet); + } + } + + SendMerged(); + } + + //For reliable channel + internal void RecycleAndDeliver(NetPacket packet) + { + if (packet.UserData is MergedPacketUserData mergedUserData) + { + for (int i = 0; i < mergedUserData.Items.Length; i++) + NetManager.MessageDelivered(this, mergedUserData.Items[i]); + packet.UserData = null; + } + else if (packet.UserData != null) + { + if (packet.IsFragmented) + { + _deliveredFragments.TryGetValue(packet.FragmentId, out ushort fragCount); + fragCount++; + if (fragCount == packet.FragmentsTotal) + { + NetManager.MessageDelivered(this, packet.UserData); + _deliveredFragments.Remove(packet.FragmentId); + } + else + { + _deliveredFragments[packet.FragmentId] = fragCount; + } + } + else + { + NetManager.MessageDelivered(this, packet.UserData); + } + packet.UserData = null; + } + NetManager.PoolRecycle(packet); + } + } +} diff --git a/LiteNetLib/NatPunchModule.cs b/LiteNetLib/NatPunchModule.cs index a49146ed..3259384b 100644 --- a/LiteNetLib/NatPunchModule.cs +++ b/LiteNetLib/NatPunchModule.cs @@ -1,23 +1,71 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; using LiteNetLib.Utils; -//Some code parts taken from lidgren-network-gen3 namespace LiteNetLib { + /// + /// Specifies the type of network address discovered during NAT punchthrough. + /// + public enum NatAddressType + { + /// + /// Address within the local area network (LAN). + /// + Internal, + /// + /// Publicly accessible address on the wide area network (WAN). + /// + External + } + + /// + /// Interface for handling events related to NAT punchthrough and introduction. + /// public interface INatPunchListener { + /// + /// Called when a NAT introduction request is received from the mediator server. + /// + /// The local endpoint of the client requesting connection. + /// The remote endpoint of the client requesting connection. + /// Custom data token associated with the request. void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token); - void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, string token); + + /// + /// Called when NAT punchthrough is successful and a direct connection can be established. + /// + /// The resolved endpoint of the remote peer. + /// The type of address (Internal or External) that succeeded. + /// Custom data token associated with the request. + void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token); } + /// + /// An implementation of that maps callbacks to events. + /// public class EventBasedNatPunchListener : INatPunchListener { + /// + /// Delegate for NAT introduction request events. + /// public delegate void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token); - public delegate void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, string token); + /// + /// Delegate for NAT introduction success events. + /// + public delegate void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token); + + /// + /// Event triggered when a NAT introduction request is received. + /// public event OnNatIntroductionRequest NatIntroductionRequest; + + /// + /// Event triggered when NAT punchthrough is successfully completed. + /// public event OnNatIntroductionSuccess NatIntroductionSuccess; void INatPunchListener.OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) @@ -26,10 +74,10 @@ void INatPunchListener.OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndP NatIntroductionRequest(localEndPoint, remoteEndPoint, token); } - void INatPunchListener.OnNatIntroductionSuccess(IPEndPoint targetEndPoint, string token) + void INatPunchListener.OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) { if (NatIntroductionSuccess != null) - NatIntroductionSuccess(targetEndPoint, token); + NatIntroductionSuccess(targetEndPoint, type, token); } } @@ -48,30 +96,95 @@ struct RequestEventData struct SuccessEventData { public IPEndPoint TargetEndPoint; + public NatAddressType Type; public string Token; } - private readonly NetSocket _socket; - private readonly Queue _requestEvents; - private readonly Queue _successEvents; - private const byte HostByte = 1; - private const byte ClientByte = 0; - public const int MaxTokenLength = 256; + class NatIntroduceRequestPacket + { + public IPEndPoint Internal { [Preserve] get; [Preserve] set; } + public string Token { [Preserve] get; [Preserve] set; } + } + class NatIntroduceResponsePacket + { + public IPEndPoint Internal { [Preserve] get; [Preserve] set; } + public IPEndPoint External { [Preserve] get; [Preserve] set; } + public string Token { [Preserve] get; [Preserve] set; } + } + + class NatPunchPacket + { + public string Token { [Preserve] get; [Preserve] set; } + public bool IsExternal { [Preserve] get; [Preserve] set; } + } + + private readonly LiteNetManager _socket; + private readonly ConcurrentQueue _requestEvents = new ConcurrentQueue(); + private readonly ConcurrentQueue _successEvents = new ConcurrentQueue(); + private readonly NetDataReader _cacheReader = new NetDataReader(); + private readonly NetDataWriter _cacheWriter = new NetDataWriter(); + private readonly NetPacketProcessor _netPacketProcessor = new NetPacketProcessor(MaxTokenLength); private INatPunchListener _natPunchListener; + /// + /// Maximum allowed length for the NAT introduction token string. + /// + public const int MaxTokenLength = 256; - internal NatPunchModule(NetSocket socket) + /// + /// Events automatically will be called without PollEvents method from another thread + /// + public bool UnsyncedEvents = false; + + internal NatPunchModule(LiteNetManager socket) { _socket = socket; - _requestEvents = new Queue(); - _successEvents = new Queue(); + _netPacketProcessor.SubscribeReusable(OnNatIntroductionResponse); + _netPacketProcessor.SubscribeReusable(OnNatIntroductionRequest); + _netPacketProcessor.SubscribeReusable(OnNatPunch); + } + + internal void ProcessMessage(IPEndPoint senderEndPoint, NetPacket packet) + { + lock (_cacheReader) + { + _cacheReader.SetSource(packet.RawData, NetConstants.HeaderSize, packet.Size); + _netPacketProcessor.ReadAllPackets(_cacheReader, senderEndPoint); + } } + /// + /// Initializes the NAT punch module with a listener to handle punchthrough events. + /// + /// The listener implementation that will receive NAT events. public void Init(INatPunchListener listener) { _natPunchListener = listener; } + private void Send< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>(T packet, IPEndPoint target) where T : class, new() + { + _cacheWriter.Reset(); + _cacheWriter.Put((byte)PacketProperty.NatMessage); + _netPacketProcessor.Write(_cacheWriter, packet); + _socket.SendRaw(_cacheWriter.Data, 0, _cacheWriter.Length, target); + } + + /// + /// Sends NAT introduction packets to both the host and the client to facilitate punchthrough. + /// + /// + /// This is typically called by a mediator (e.g. a master server). + /// + /// Internal (LAN) endpoint of the host. + /// External (WAN) endpoint of the host. + /// Internal (LAN) endpoint of the client. + /// External (WAN) endpoint of the client. + /// Custom token or data to include in the introduction. public void NatIntroduce( IPEndPoint hostInternal, IPEndPoint hostExternal, @@ -79,160 +192,149 @@ public void NatIntroduce( IPEndPoint clientExternal, string additionalInfo) { - NetDataWriter dw = new NetDataWriter(); - - //First packet (server) - //send to client - dw.Put((byte)PacketProperty.NatIntroduction); - dw.Put(ClientByte); - dw.Put(hostInternal); - dw.Put(hostExternal); - dw.Put(additionalInfo, MaxTokenLength); - SocketError errorCode = 0; - _socket.SendTo(dw.Data, 0, dw.Length, clientExternal, ref errorCode); - - //Second packet (client) - //send to server - dw.Reset(); - dw.Put((byte)PacketProperty.NatIntroduction); - dw.Put(HostByte); - dw.Put(clientInternal); - dw.Put(clientExternal); - dw.Put(additionalInfo, MaxTokenLength); - _socket.SendTo(dw.Data, 0, dw.Length, hostExternal, ref errorCode); + var req = new NatIntroduceResponsePacket + { + Token = additionalInfo + }; + + //First packet (server) send to client + req.Internal = hostInternal; + req.External = hostExternal; + Send(req, clientExternal); + + //Second packet (client) send to server + req.Internal = clientInternal; + req.External = clientExternal; + Send(req, hostExternal); } + /// + /// Triggers queued NAT punchthrough events (Success or Request) on the provided . + /// + /// + /// This should be called from the main thread if is . + /// public void PollEvents() { - if (_natPunchListener == null) + if (UnsyncedEvents) + return; + + if (_natPunchListener == null || (_successEvents.IsEmpty && _requestEvents.IsEmpty)) return; - lock (_successEvents) + + while (_successEvents.TryDequeue(out var evt)) { - while (_successEvents.Count > 0) - { - var evt = _successEvents.Dequeue(); - _natPunchListener.OnNatIntroductionSuccess(evt.TargetEndPoint, evt.Token); - } + _natPunchListener.OnNatIntroductionSuccess( + evt.TargetEndPoint, + evt.Type, + evt.Token); } - lock (_requestEvents) + + while (_requestEvents.TryDequeue(out var evt)) { - while (_requestEvents.Count > 0) - { - var evt = _requestEvents.Dequeue(); - _natPunchListener.OnNatIntroductionRequest(evt.LocalEndPoint, evt.RemoteEndPoint, evt.Token); - } + _natPunchListener.OnNatIntroductionRequest(evt.LocalEndPoint, evt.RemoteEndPoint, evt.Token); } } + /// + /// Sends a request to the Master Server to introduce this peer to another peer. + /// + /// The hostname or IP of the Master Server. + /// The port of the Master Server. + /// Custom token to identify the connection or room. + public void SendNatIntroduceRequest(string host, int port, string additionalInfo) + { + SendNatIntroduceRequest(NetUtils.MakeEndPoint(host, port), additionalInfo); + } + + /// + /// Sends a request to the Master Server to introduce this peer to another peer. + /// + /// The endpoint of the Master Server. + /// Custom token to identify the connection or room. public void SendNatIntroduceRequest(IPEndPoint masterServerEndPoint, string additionalInfo) { //prepare outgoing data - NetDataWriter dw = new NetDataWriter(); string networkIp = NetUtils.GetLocalIp(LocalAddrType.IPv4); - if (string.IsNullOrEmpty(networkIp)) + if (string.IsNullOrEmpty(networkIp) || masterServerEndPoint.AddressFamily == AddressFamily.InterNetworkV6) { networkIp = NetUtils.GetLocalIp(LocalAddrType.IPv6); } - IPEndPoint localEndPoint = NetUtils.MakeEndPoint(networkIp, _socket.LocalPort); - dw.Put((byte)PacketProperty.NatIntroductionRequest); - dw.Put(localEndPoint); - dw.Put(additionalInfo, MaxTokenLength); - //prepare packet - SocketError errorCode = 0; - _socket.SendTo(dw.Data, 0, dw.Length, masterServerEndPoint, ref errorCode); + Send( + new NatIntroduceRequestPacket + { + Internal = NetUtils.MakeEndPoint(networkIp, _socket.LocalPort), + Token = additionalInfo + }, + masterServerEndPoint); } - private void HandleNatPunch(IPEndPoint senderEndPoint, NetDataReader dr) + //We got request and must introduce + private void OnNatIntroductionRequest(NatIntroduceRequestPacket req, IPEndPoint senderEndPoint) { - byte fromHostByte = dr.GetByte(); - if (fromHostByte != HostByte && fromHostByte != ClientByte) + if (UnsyncedEvents) { - //garbage - return; + _natPunchListener.OnNatIntroductionRequest( + req.Internal, + senderEndPoint, + req.Token); } - - //Read info - string additionalInfo = dr.GetString(MaxTokenLength); - NetDebug.Write(NetLogLevel.Trace, "[NAT] punch received from {0} - additional info: {1}", senderEndPoint, additionalInfo); - - //Release punch success to client; enabling him to Connect() to msg.Sender if token is ok - lock (_successEvents) + else { - _successEvents.Enqueue(new SuccessEventData { TargetEndPoint = senderEndPoint, Token = additionalInfo }); + _requestEvents.Enqueue(new RequestEventData + { + LocalEndPoint = req.Internal, + RemoteEndPoint = senderEndPoint, + Token = req.Token + }); } } - private void HandleNatIntroduction(NetDataReader dr) + //We got introduce and must punch + private void OnNatIntroductionResponse(NatIntroduceResponsePacket req) { - // read intro - byte hostByte = dr.GetByte(); - IPEndPoint remoteInternal = dr.GetNetEndPoint(); - IPEndPoint remoteExternal = dr.GetNetEndPoint(); - string token = dr.GetString(MaxTokenLength); - - NetDebug.Write(NetLogLevel.Trace, "[NAT] introduction received; we are designated " + (hostByte == HostByte ? "host" : "client")); - NetDataWriter writer = new NetDataWriter(); + NetDebug.Write(NetLogLevel.Trace, "[NAT] introduction received"); // send internal punch - writer.Put((byte)PacketProperty.NatPunchMessage); - writer.Put(hostByte); - writer.Put(token); - SocketError errorCode = 0; - _socket.SendTo(writer.Data, 0, writer.Length, remoteInternal, ref errorCode); - NetDebug.Write(NetLogLevel.Trace, "[NAT] internal punch sent to " + remoteInternal); + var punchPacket = new NatPunchPacket {Token = req.Token}; + Send(punchPacket, req.Internal); + NetDebug.Write(NetLogLevel.Trace, $"[NAT] internal punch sent to {req.Internal}"); + + // hack for some routers + _socket.Ttl = 2; + _socket.SendRaw(new[] { (byte)PacketProperty.Empty }, 0, 1, req.External); // send external punch - writer.Reset(); - writer.Put((byte)PacketProperty.NatPunchMessage); - writer.Put(hostByte); - writer.Put(token); - if (hostByte == HostByte) - { - _socket.Ttl = 2; - _socket.SendTo(writer.Data, 0, writer.Length, remoteExternal, ref errorCode); - _socket.Ttl = NetConstants.SocketTTL; - } - else - { - _socket.SendTo(writer.Data, 0, writer.Length, remoteExternal, ref errorCode); - } - - NetDebug.Write(NetLogLevel.Trace, "[NAT] external punch sent to " + remoteExternal); + _socket.Ttl = NetConstants.SocketTTL; + punchPacket.IsExternal = true; + Send(punchPacket, req.External); + NetDebug.Write(NetLogLevel.Trace, $"[NAT] external punch sent to {req.External}"); } - private void HandleNatIntroductionRequest(IPEndPoint senderEndPoint, NetDataReader dr) + //We got punch and can connect + private void OnNatPunch(NatPunchPacket req, IPEndPoint senderEndPoint) { - IPEndPoint localEp = dr.GetNetEndPoint(); - string token = dr.GetString(MaxTokenLength); - lock (_requestEvents) + //Read info + NetDebug.Write(NetLogLevel.Trace, $"[NAT] punch received from {senderEndPoint} - additional info: {req.Token}"); + + //Release punch success to client; enabling him to Connect() to Sender if token is ok + if(UnsyncedEvents) { - _requestEvents.Enqueue(new RequestEventData - { - LocalEndPoint = localEp, - RemoteEndPoint = senderEndPoint, - Token = token - }); + _natPunchListener.OnNatIntroductionSuccess( + senderEndPoint, + req.IsExternal ? NatAddressType.External : NatAddressType.Internal, + req.Token + ); } - } - - internal void ProcessMessage(IPEndPoint senderEndPoint, NetPacket packet) - { - var dr = new NetDataReader(packet.RawData, NetConstants.HeaderSize, packet.Size); - switch (packet.Property) + else { - case PacketProperty.NatIntroductionRequest: - //We got request and must introduce - HandleNatIntroductionRequest(senderEndPoint, dr); - break; - case PacketProperty.NatIntroduction: - //We got introduce and must punch - HandleNatIntroduction(dr); - break; - case PacketProperty.NatPunchMessage: - //We got punch and can connect - HandleNatPunch(senderEndPoint, dr); - break; + _successEvents.Enqueue(new SuccessEventData + { + TargetEndPoint = senderEndPoint, + Type = req.IsExternal ? NatAddressType.External : NatAddressType.Internal, + Token = req.Token + }); } } } diff --git a/LiteNetLib/NativeSocket.cs b/LiteNetLib/NativeSocket.cs new file mode 100644 index 00000000..84ecedc4 --- /dev/null +++ b/LiteNetLib/NativeSocket.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LiteNetLib +{ + internal static class NativeSocket + { + static unsafe class WinSock + { + private const string LibName = "ws2_32.dll"; + + [DllImport(LibName, SetLastError = true)] + public static extern int recvfrom( + IntPtr socketHandle, + [In, Out] byte[] pinnedBuffer, + [In] int len, + [In] SocketFlags socketFlags, + [Out] byte[] socketAddress, + [In, Out] ref int socketAddressSize); + + [DllImport(LibName, SetLastError = true)] + internal static extern int sendto( + IntPtr socketHandle, + byte* pinnedBuffer, + [In] int len, + [In] SocketFlags socketFlags, + [In] byte[] socketAddress, + [In] int socketAddressSize); + } + + static unsafe class UnixSock + { + private const string LibName = "libc"; + + [DllImport(LibName, SetLastError = true)] + public static extern int recvfrom( + IntPtr socketHandle, + [In, Out] byte[] pinnedBuffer, + [In] int len, + [In] SocketFlags socketFlags, + [Out] byte[] socketAddress, + [In, Out] ref int socketAddressSize); + + [DllImport(LibName, SetLastError = true)] + internal static extern int sendto( + IntPtr socketHandle, + byte* pinnedBuffer, + [In] int len, + [In] SocketFlags socketFlags, + [In] byte[] socketAddress, + [In] int socketAddressSize); + } + + /// + /// Indicates whether the native socket optimizations are supported on the current platform. + /// + public static readonly bool IsSupported = false; + /// + /// Indicates whether the current environment requires Unix-style native socket calls. + /// + public static readonly bool UnixMode = false; + + /// + /// The size of the native sockaddr_in structure for IPv4. + /// + public const int IPv4AddrSize = 16; + /// + /// The size of the native sockaddr_in6 structure for IPv6. + /// + public const int IPv6AddrSize = 28; + /// + /// Native Address Family constant for IPv4 (AF_INET). + /// + public const int AF_INET = 2; + /// + /// Native Address Family constant for IPv6 (AF_INET6). + /// + public const int AF_INET6 = 10; + + private static readonly Dictionary NativeErrorToSocketError = new Dictionary + { + { 13, SocketError.AccessDenied }, //EACCES + { 98, SocketError.AddressAlreadyInUse }, //EADDRINUSE + { 99, SocketError.AddressNotAvailable }, //EADDRNOTAVAIL + { 97, SocketError.AddressFamilyNotSupported }, //EAFNOSUPPORT + { 11, SocketError.WouldBlock }, //EAGAIN + { 114, SocketError.AlreadyInProgress }, //EALREADY + { 9, SocketError.OperationAborted }, //EBADF + { 125, SocketError.OperationAborted }, //ECANCELED + { 103, SocketError.ConnectionAborted }, //ECONNABORTED + { 111, SocketError.ConnectionRefused }, //ECONNREFUSED + { 104, SocketError.ConnectionReset }, //ECONNRESET + { 89, SocketError.DestinationAddressRequired }, //EDESTADDRREQ + { 14, SocketError.Fault }, //EFAULT + { 112, SocketError.HostDown }, //EHOSTDOWN + { 6, SocketError.HostNotFound }, //ENXIO + { 113, SocketError.HostUnreachable }, //EHOSTUNREACH + { 115, SocketError.InProgress }, //EINPROGRESS + { 4, SocketError.Interrupted }, //EINTR + { 22, SocketError.InvalidArgument }, //EINVAL + { 106, SocketError.IsConnected }, //EISCONN + { 24, SocketError.TooManyOpenSockets }, //EMFILE + { 90, SocketError.MessageSize }, //EMSGSIZE + { 100, SocketError.NetworkDown }, //ENETDOWN + { 102, SocketError.NetworkReset }, //ENETRESET + { 101, SocketError.NetworkUnreachable }, //ENETUNREACH + { 23, SocketError.TooManyOpenSockets }, //ENFILE + { 105, SocketError.NoBufferSpaceAvailable }, //ENOBUFS + { 61, SocketError.NoData }, //ENODATA + { 2, SocketError.AddressNotAvailable }, //ENOENT + { 92, SocketError.ProtocolOption }, //ENOPROTOOPT + { 107, SocketError.NotConnected }, //ENOTCONN + { 88, SocketError.NotSocket }, //ENOTSOCK + { 3440, SocketError.OperationNotSupported }, //ENOTSUP + { 1, SocketError.AccessDenied }, //EPERM + { 32, SocketError.Shutdown }, //EPIPE + { 96, SocketError.ProtocolFamilyNotSupported }, //EPFNOSUPPORT + { 93, SocketError.ProtocolNotSupported }, //EPROTONOSUPPORT + { 91, SocketError.ProtocolType }, //EPROTOTYPE + { 94, SocketError.SocketNotSupported }, //ESOCKTNOSUPPORT + { 108, SocketError.Disconnecting }, //ESHUTDOWN + { 110, SocketError.TimedOut }, //ETIMEDOUT + { 0, SocketError.Success } + }; + + static NativeSocket() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + IsSupported = true; + UnixMode = true; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + IsSupported = true; + } + } + + /// + /// Receives a datagram from the specified socket handle using native OS calls. + /// + /// The OS handle for the socket. + /// A pinned byte array to receive the data. + /// The number of s to receive. + /// A pinned byte array to store the source address (sockaddr). + /// The size of the structure. + /// The number of s received, or a negative value on error. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RecvFrom( + IntPtr socketHandle, + byte[] pinnedBuffer, + int len, + byte[] socketAddress, + ref int socketAddressSize) => + UnixMode + ? UnixSock.recvfrom(socketHandle, pinnedBuffer, len, 0, socketAddress, ref socketAddressSize) + : WinSock.recvfrom(socketHandle, pinnedBuffer, len, 0, socketAddress, ref socketAddressSize); + + /// + /// Sends a datagram to the specified socket handle using native OS calls. + /// + /// The OS handle for the socket. + /// A pointer to the pinned memory containing data to send. + /// The number of s to send. + /// A pinned byte array containing the destination address (sockaddr). + /// The size of the structure. + /// The number of s sent, or a negative value on error. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int SendTo( + IntPtr socketHandle, + byte* pinnedBuffer, + int len, + byte[] socketAddress, + int socketAddressSize) => + UnixMode + ? UnixSock.sendto(socketHandle, pinnedBuffer, len, 0, socketAddress, socketAddressSize) + : WinSock.sendto(socketHandle, pinnedBuffer, len, 0, socketAddress, socketAddressSize); + + /// + /// Retrieves the last OS-specific socket error and translates it to . + /// + /// The translated . + public static SocketError GetSocketError() + { + int error = Marshal.GetLastWin32Error(); + if (UnixMode) + return NativeErrorToSocketError.TryGetValue(error, out var err) + ? err + : SocketError.SocketError; + return (SocketError)error; + } + + /// + /// Retrieves the last OS-specific socket error and encapsulates it in a . + /// + /// A representing the last native error. + public static SocketException GetSocketException() + { + int error = Marshal.GetLastWin32Error(); + if (UnixMode) + return NativeErrorToSocketError.TryGetValue(error, out var err) + ? new SocketException((int)err) + : new SocketException((int)SocketError.SocketError); + return new SocketException(error); + } + + /// + /// Converts the of an endpoint to the corresponding native constant. + /// + /// The endpoint to evaluate. + /// The native address family identifier. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short GetNativeAddressFamily(IPEndPoint remoteEndPoint) => + UnixMode + ? (short)(remoteEndPoint.AddressFamily == AddressFamily.InterNetwork ? AF_INET : AF_INET6) + : (short)remoteEndPoint.AddressFamily; + } +} diff --git a/LiteNetLib/NetConstants.cs b/LiteNetLib/NetConstants.cs index 26bfc476..947cc865 100644 --- a/LiteNetLib/NetConstants.cs +++ b/LiteNetLib/NetConstants.cs @@ -1,34 +1,35 @@ -namespace LiteNetLib +namespace LiteNetLib { /// /// Sending method type /// - public enum DeliveryMethod + public enum DeliveryMethod : byte { /// - /// Unreliable. Packets can be dropped, duplicated or arrive without order + /// Unreliable. Packets can be dropped, can be duplicated, can arrive without order. /// - Unreliable, + Unreliable = 4, /// - /// Reliable. All packets will be sent and received, but without order + /// Reliable. Packets won't be dropped, won't be duplicated, can arrive without order. /// - ReliableUnordered, + ReliableUnordered = 0, /// - /// Unreliable. Packets can be dropped, but never duplicated and arrive in order + /// Unreliable. Packets can be dropped, won't be duplicated, will arrive in order. /// - Sequenced, + Sequenced = 1, /// - /// Reliable and ordered. All packets will be sent and received in order + /// Reliable and ordered. Packets won't be dropped, won't be duplicated, will arrive in order. /// - ReliableOrdered, + ReliableOrdered = 2, /// - /// Reliable only last packet + /// Reliable only last packet. Packets can be dropped (except the last one), won't be duplicated, will arrive in order. + /// Cannot be fragmented /// - ReliableSequenced + ReliableSequenced = 3 } /// @@ -36,47 +37,59 @@ public enum DeliveryMethod /// public static class NetConstants { - internal static byte ChannelNumberToId(DeliveryMethod method, byte channelNumber, byte channelsCount) - { - int multiplier = 0; - switch (method) - { - case DeliveryMethod.Sequenced: multiplier = 1; break; - case DeliveryMethod.ReliableOrdered: multiplier = 2; break; - case DeliveryMethod.ReliableSequenced: multiplier = 3; break; - } - return (byte)(channelNumber + multiplier * channelsCount); - } - - internal static DeliveryMethod ChannelIdToDeliveryMethod(byte channelId, byte channelsCount) - { - switch (channelId / channelsCount) - { - case 1: return DeliveryMethod.Sequenced; - case 2: return DeliveryMethod.ReliableOrdered; - case 3: return DeliveryMethod.ReliableSequenced; - } - return DeliveryMethod.ReliableUnordered; - } - - //can be tuned + /// + /// Default window size for reliable channels (number of packets). + /// public const int DefaultWindowSize = 64; - public const int SocketBufferSize = 1024 * 1024; //1mb + /// + /// Size of the underlying UDP socket receive and send buffers in bytes.
+ /// Default is 1MB. + ///
+ public const int SocketBufferSize = 1024 * 1024; + /// + /// Time To Live (TTL) for the UDP packets. + /// public const int SocketTTL = 255; + /// + /// Size of the base packet header (PacketProperty) in s. + /// public const int HeaderSize = 1; + /// + /// Size of the header for sequenced or reliable messages in s.
+ /// Includes , Sequence, and ChannelId. + ///
public const int ChanneledHeaderSize = 4; + /// + /// Additional header size required for fragmented packets in s.
+ /// Includes FragmentId, FragmentPart, and FragmentsTotal. + ///
public const int FragmentHeaderSize = 6; + /// + /// Total header size for a fragmented channeled packet in s.
+ /// Combines and . + ///
+ public const int FragmentedHeaderTotalSize = ChanneledHeaderSize + FragmentHeaderSize; + /// + /// Maximum possible sequence number before wrapping back to zero. + /// public const ushort MaxSequence = 32768; + /// + /// Half of the , used for sequence comparison and wrap-around logic. + /// public const ushort HalfMaxSequence = MaxSequence / 2; //protocol - internal const int ProtocolId = 9; + internal const int ProtocolId = 13; internal const int MaxUdpHeaderSize = 68; + internal const int ChannelTypeCount = 4; + internal const int FragmentedChannelsCount = 2; + internal const int MaxFragmentsInWindow = DefaultWindowSize / 2; internal static readonly int[] PossibleMtu = { - 576 - MaxUdpHeaderSize, //minimal + //576 - MaxUdpHeaderSize minimal (RFC 1191) + 1024, //most games standard 1232 - MaxUdpHeaderSize, 1460 - MaxUdpHeaderSize, //google cloud 1472 - MaxUdpHeaderSize, //VPN @@ -84,11 +97,28 @@ internal static DeliveryMethod ChannelIdToDeliveryMethod(byte channelId, byte ch 1500 - MaxUdpHeaderSize //Ethernet II (RFC 1191) }; - internal static readonly int MaxPacketSize = PossibleMtu[PossibleMtu.Length - 1]; + /// + /// The starting Maximum Transmission Unit (MTU) used for new connections before path MTU discovery. + /// + public static readonly int InitialMtu = PossibleMtu[0]; + /// + /// Maximum possible packet size allowed by the library based on the largest supported MTU. + /// + public static readonly int MaxPacketSize = PossibleMtu[PossibleMtu.Length - 1]; + /// + /// Maximum payload size for a single unreliable packet in s.
+ /// Calculated as - . + ///
+ public static readonly int MaxUnreliableDataSize = MaxPacketSize - HeaderSize; - //peer specific + /// + /// Maximum possible value for . + /// + /// + /// This value is used to distinguish between different connection instances from the same .
+ /// It allows the receiver to identify and discard packets belonging to previous connection attempts that may arrive + /// late due to network jitter, even if they originate from the same address and port.
+ ///
public const byte MaxConnectionNumber = 4; - - public const int PacketPoolSize = 1000; } } diff --git a/LiteNetLib/NetDebug.cs b/LiteNetLib/NetDebug.cs index bebd8d1e..44cb6f3e 100644 --- a/LiteNetLib/NetDebug.cs +++ b/LiteNetLib/NetDebug.cs @@ -3,6 +3,20 @@ namespace LiteNetLib { + public class InvalidPacketException : ArgumentException + { + public InvalidPacketException(string message) : base(message) + { + } + } + + public class TooBigPacketException : InvalidPacketException + { + public TooBigPacketException(string message) : base(message) + { + } + } + public enum NetLogLevel { Warning, @@ -33,7 +47,7 @@ private static void WriteLogic(NetLogLevel logLevel, string str, params object[] { if (Logger == null) { -#if UNITY_4 || UNITY_5 || UNITY_5_3_OR_NEWER +#if UNITY_5_3_OR_NEWER UnityEngine.Debug.Log(string.Format(str, args)); #else Console.WriteLine(str, args); @@ -47,32 +61,32 @@ private static void WriteLogic(NetLogLevel logLevel, string str, params object[] } [Conditional("DEBUG_MESSAGES")] - internal static void Write(string str, params object[] args) + internal static void Write(string str) { - WriteLogic(NetLogLevel.Trace, str, args); + WriteLogic(NetLogLevel.Trace, str); } [Conditional("DEBUG_MESSAGES")] - internal static void Write(NetLogLevel level, string str, params object[] args) + internal static void Write(NetLogLevel level, string str) { - WriteLogic(level, str, args); + WriteLogic(level, str); } [Conditional("DEBUG_MESSAGES"), Conditional("DEBUG")] - internal static void WriteForce(string str, params object[] args) + internal static void WriteForce(string str) { - WriteLogic(NetLogLevel.Trace, str, args); + WriteLogic(NetLogLevel.Trace, str); } [Conditional("DEBUG_MESSAGES"), Conditional("DEBUG")] - internal static void WriteForce(NetLogLevel level, string str, params object[] args) + internal static void WriteForce(NetLogLevel level, string str) { - WriteLogic(level, str, args); + WriteLogic(level, str); } - internal static void WriteError(string str, params object[] args) + internal static void WriteError(string str) { - WriteLogic(NetLogLevel.Error, str, args); + WriteLogic(NetLogLevel.Error, str); } } } diff --git a/LiteNetLib/NetEvent.cs b/LiteNetLib/NetEvent.cs new file mode 100644 index 00000000..7b701e69 --- /dev/null +++ b/LiteNetLib/NetEvent.cs @@ -0,0 +1,107 @@ +using System.Net; +using System.Net.Sockets; + +namespace LiteNetLib +{ + /// + /// Internally used event type + /// + public sealed class NetEvent + { + /// + /// Reference to the next event in the pool or event queue. + /// + public NetEvent Next; + + /// + /// Specifies the category of the network event. + /// + public enum EType + { + /// New peer connected. + Connect, + /// Peer disconnected. + Disconnect, + /// Data received from a connected peer. + Receive, + /// Unconnected message received. + ReceiveUnconnected, + /// Socket or internal protocol error occurred. + Error, + /// Round-trip time (RTT) for a peer has been updated. + ConnectionLatencyUpdated, + /// Broadcast message received. + Broadcast, + /// Incoming connection request from a new peer. + ConnectionRequest, + /// Reliable message was successfully delivered to the remote peer. + MessageDelivered, + /// The IP address or port of an existing peer has changed (e.g., roaming). + PeerAddressChanged + } + + /// + /// The type of network event that occurred. + /// + public EType Type; + + /// + /// The peer associated with this event. for unconnected events. + /// + public LiteNetPeer Peer; + + /// + /// The remote endpoint (IP and Port) from which the event originated. + /// + public IPEndPoint RemoteEndPoint; + + /// + /// Optional user data associated with a connection request or disconnect. + /// + public object UserData; + + /// + /// The updated latency value in milliseconds. Only valid when is . + /// + public int Latency; + + /// + /// The specific socket error. Only valid when is . + /// + public SocketError ErrorCode; + + /// + /// The reason for a peer's disconnection. Only valid when is . + /// + public DisconnectReason DisconnectReason; + + /// + /// Information about an incoming connection. Only valid when is . + /// + public LiteConnectionRequest ConnectionRequest; + + /// + /// The delivery method used for the received packet. Only valid when is . + /// + public DeliveryMethod DeliveryMethod; + + /// + /// The channel on which the packet was received. + /// + public byte ChannelNumber; + + /// + /// A reader for accessing the payload of received data, broadcast, or unconnected messages. + /// + public readonly NetPacketReader DataReader; + + /// + /// Initializes a new instance of the class. + /// + /// The that owns the packet pool and buffers for this event. + public NetEvent(LiteNetManager manager) + { + DataReader = new NetPacketReader(manager, this); + } + } +} diff --git a/LiteNetLib/NetExceptions.cs b/LiteNetLib/NetExceptions.cs deleted file mode 100644 index db999a64..00000000 --- a/LiteNetLib/NetExceptions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -namespace LiteNetLib -{ - public class InvalidPacketException: ArgumentException - { - public InvalidPacketException() - { - } - - public InvalidPacketException(string message): base(message) - { - } - - public InvalidPacketException(string message, Exception innerException): base(message, innerException) - { - } - } - - public class TooBigPacketException : InvalidPacketException - { - public TooBigPacketException() - { - } - - public TooBigPacketException(string message) : base(message) - { - } - - public TooBigPacketException(string message, Exception innerException) : base(message, innerException) - { - } - } -} \ No newline at end of file diff --git a/LiteNetLib/NetManager.cs b/LiteNetLib/NetManager.cs index 5c73a3c2..fc02a659 100644 --- a/LiteNetLib/NetManager.cs +++ b/LiteNetLib/NetManager.cs @@ -1,483 +1,154 @@ -#if DEBUG -#define STATS_ENABLED -#endif - -using System; -using System.Collections; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Net; -using System.Net.Sockets; -using System.Threading; +using LiteNetLib.Layers; using LiteNetLib.Utils; namespace LiteNetLib { - public class NetPacketReader : NetDataReader - { - private NetPacket _packet; - private readonly NetManager _manager; - private readonly NetEvent _evt; - - internal NetPacketReader(NetManager manager, NetEvent evt) - { - _manager = manager; - _evt = evt; - } - - internal void SetSource(NetPacket packet) - { - if (packet == null) - return; - _packet = packet; - SetSource(packet.RawData, packet.GetHeaderSize(), packet.Size); - } - - public void Recycle() - { - Clear(); - if (_packet != null) - _manager.NetPacketPool.Recycle(_packet); - _packet = null; - _manager.RecycleEvent(_evt); - } - } - - internal sealed class NetEvent - { - public enum EType - { - Connect, - Disconnect, - Receive, - ReceiveUnconnected, - Error, - ConnectionLatencyUpdated, - Broadcast, - ConnectionRequest - } - public EType Type; - - public NetPeer Peer; - public IPEndPoint RemoteEndPoint; - public int Latency; - public SocketError ErrorCode; - public DisconnectReason DisconnectReason; - public ConnectionRequest ConnectionRequest; - public DeliveryMethod DeliveryMethod; - public readonly NetPacketReader DataReader; - - public NetEvent(NetManager manager) - { - DataReader = new NetPacketReader(manager, this); - } - } - /// - /// Main class for all network operations. Can be used as client and/or server. + /// More feature rich network manager with adjustable channels count /// - public class NetManager : INetSocketListener, IConnectionRequestListener, IEnumerable + public class NetManager : LiteNetManager, IEnumerable { - private class IPEndPointComparer : IEqualityComparer - { - public bool Equals(IPEndPoint x, IPEndPoint y) - { - return x.Equals(y); - } - - public int GetHashCode(IPEndPoint obj) - { - return obj.GetHashCode(); - } - } - -#if DEBUG - private struct IncomingData - { - public byte[] Data; - public IPEndPoint EndPoint; - public DateTime TimeWhenGet; - } - private readonly List _pingSimulationList = new List(); - private readonly Random _randomGenerator = new Random(); - private const int MinLatencyThreshold = 5; -#endif - - private readonly NetSocket _socket; - private Thread _logicThread; - - private readonly Queue _netEventsQueue; - private readonly Stack _netEventsPool; private readonly INetEventListener _netEventListener; - - private readonly Dictionary _peersDict; - private readonly ReaderWriterLockSlim _peersLock; - private volatile NetPeer _headPeer; - private volatile int _connectedPeersCount; - private readonly List _connectedPeerListCache; - private int _lastPeerId; - private readonly Queue _peerIds; private byte _channelsCount = 1; - - internal readonly NetPacketPool NetPacketPool; - - //config section - /// - /// Enable messages receiving without connection. (with SendUnconnectedMessage method) - /// - public bool UnconnectedMessagesEnabled = false; - - /// - /// Enable nat punch messages - /// - public bool NatPunchEnabled = false; - - /// - /// Library logic update and send period in milliseconds - /// - public int UpdateTime = 15; - - /// - /// Interval for latency detection and checking connection - /// - public int PingInterval = 1000; - - /// - /// If NetManager doesn't receive any packet from remote peer during this time then connection will be closed - /// (including library internal keepalive packets) - /// - public int DisconnectTimeout = 5000; - - /// - /// Simulate packet loss by dropping random amout of packets. (Works only in DEBUG mode) - /// - public bool SimulatePacketLoss = false; - - /// - /// Simulate latency by holding packets for random time. (Works only in DEBUG mode) - /// - public bool SimulateLatency = false; - - /// - /// Chance of packet loss when simulation enabled. value in percents (1 - 100). - /// - public int SimulationPacketLossChance = 10; - - /// - /// Minimum simulated latency - /// - public int SimulationMinLatency = 30; - - /// - /// Maximum simulated latency - /// - public int SimulationMaxLatency = 100; - - /// - /// Experimental feature. Events automatically will be called without PollEvents method from another thread - /// - public bool UnsyncedEvents = false; - - /// - /// Allows receive broadcast packets - /// - public bool BroadcastReceiveEnabled = false; - - /// - /// Delay betwen initial connection attempts - /// - public int ReconnectDelay = 500; - - /// - /// Maximum connection attempts before client stops and call disconnect event. - /// - public int MaxConnectAttempts = 10; - - /// - /// Enables socket option "ReuseAddress" for specific purposes - /// - public bool ReuseAddress = false; - - /// - /// Statistics of all connections - /// - public readonly NetStatistics Statistics; - - //modules - /// - /// NatPunchModule for NAT hole punching operations - /// - public readonly NatPunchModule NatPunchModule; - - /// - /// Returns true if socket listening and update thread is running - /// - public bool IsRunning { get; private set; } - - /// - /// Local EndPoint (host and port) - /// - public int LocalPort { get { return _socket.LocalPort; } } - - /// - /// Automatically recycle NetPacketReader after OnReceive event - /// - public bool AutoRecycle; - - /// - /// IPv6 support - /// - public bool IPv6Enabled = true; - - /// - /// First peer. Useful for Client mode - /// - public NetPeer FirstPeer - { - get { return _headPeer; } - } + private readonly ConcurrentDictionary _ntpRequests = new ConcurrentDictionary(); /// /// QoS channel count per message type (value must be between 1 and 64 channels) /// public byte ChannelsCount { - get { return _channelsCount; } + get => _channelsCount; set { - if(value < 1 || value > 64) + if (value < 1 || value > 64) throw new ArgumentException("Channels count must be between 1 and 64"); _channelsCount = value; } } /// - /// Returns connected peers list (with internal cached list) + /// First peer. Useful for Client mode /// - public List ConnectedPeerList - { - get - { - _connectedPeerListCache.Clear(); - for(var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if ((netPeer.ConnectionState & ConnectionState.Connected) != 0) - _connectedPeerListCache.Add(netPeer); - } - return _connectedPeerListCache; - } - } + public new NetPeer FirstPeer => (NetPeer)_headPeer; /// - /// Returns connected peers count + /// Get copy of peers (without allocations) /// - public int PeersCount { get { return _connectedPeersCount; } } - - private bool TryGetPeer(IPEndPoint endPoint, out NetPeer peer) + /// List that will contain result + /// State of peers + public void GetPeers(List peers, ConnectionState peerState) { + peers.Clear(); _peersLock.EnterReadLock(); - bool result = _peersDict.TryGetValue(endPoint, out peer); - _peersLock.ExitReadLock(); - return result; - } - - private NetPeer TryAddPeer(NetPeer peer) - { - _peersLock.EnterUpgradeableReadLock(); - NetPeer existingPeer; - if (_peersDict.TryGetValue(peer.EndPoint, out existingPeer)) - { - //return back unused peerId - lock (_peerIds) - _peerIds.Enqueue(peer.Id); - - _peersLock.ExitUpgradeableReadLock(); - return existingPeer; - } - _peersLock.EnterWriteLock(); - if (_headPeer != null) + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) { - peer.NextPeer = _headPeer; - _headPeer.PrevPeer = peer; + if ((netPeer.ConnectionState & peerState) != 0) + peers.Add((NetPeer)netPeer); } - _headPeer = peer; - _peersDict.Add(peer.EndPoint, peer); - _peersLock.ExitWriteLock(); - _peersLock.ExitUpgradeableReadLock(); - return peer; + _peersLock.ExitReadLock(); } - private void RemovePeer(NetPeer peer) - { - _peersLock.EnterWriteLock(); - RemovePeerInternal(peer); - _peersLock.ExitWriteLock(); - } + /// + /// Get copy of connected peers (without allocations) + /// + /// List that will contain result + public void GetConnectedPeers(List peers) => + GetPeers(peers, ConnectionState.Connected); - private void RemovePeerInternal(NetPeer peer) - { - if (!_peersDict.Remove(peer.EndPoint)) - return; - if (peer == _headPeer) - _headPeer = peer.NextPeer; - if (peer.PrevPeer != null) - peer.PrevPeer.NextPeer = peer.NextPeer; - if (peer.NextPeer != null) - peer.NextPeer.PrevPeer = peer.PrevPeer; - peer.PrevPeer = null; - peer.NextPeer = null; - lock(_peerIds) - _peerIds.Enqueue(peer.Id); - } + public NetManager(INetEventListener listener, PacketLayerBase extraPacketLayer = null) : base(null, extraPacketLayer) => + _netEventListener = listener; /// - /// NetManager constructor + /// Create the requests for NTP server /// - /// Network events listener - public NetManager(INetEventListener listener) - { - _socket = new NetSocket(this); - _netEventListener = listener; - _netEventsQueue = new Queue(); - _netEventsPool = new Stack(); - NetPacketPool = new NetPacketPool(); - NatPunchModule = new NatPunchModule(_socket); - Statistics = new NetStatistics(); - _connectedPeerListCache = new List(); - _peersDict = new Dictionary(new IPEndPointComparer()); - _peersLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - _peerIds = new Queue(); - } + /// NTP Server address. + public void CreateNtpRequest(IPEndPoint endPoint) => + _ntpRequests.TryAdd(endPoint, new NtpRequest(endPoint)); - internal void ConnectionLatencyUpdated(NetPeer fromPeer, int latency) + /// + /// Create the requests for NTP server + /// + /// NTP Server address. + /// port + public void CreateNtpRequest(string ntpServerAddress, int port) { - CreateEvent(NetEvent.EType.ConnectionLatencyUpdated, fromPeer, latency: latency); + var endPoint = NetUtils.MakeEndPoint(ntpServerAddress, port); + _ntpRequests.TryAdd(endPoint, new NtpRequest(endPoint)); } - internal int SendRawAndRecycle(NetPacket packet, IPEndPoint remoteEndPoint) + /// + /// Create the requests for NTP server (default port) + /// + /// NTP Server address. + public void CreateNtpRequest(string ntpServerAddress) { - var result = SendRaw(packet.RawData, 0, packet.Size, remoteEndPoint); - NetPacketPool.Recycle(packet); - return result; + var endPoint = NetUtils.MakeEndPoint(ntpServerAddress, NtpRequest.DefaultPort); + _ntpRequests.TryAdd(endPoint, new NtpRequest(endPoint)); } - internal int SendRaw(NetPacket packet, IPEndPoint remoteEndPoint) + internal override bool CustomMessageHandle(NetPacket packet, IPEndPoint remoteEndPoint) { - return SendRaw(packet.RawData, 0, packet.Size, remoteEndPoint); - } + if (_ntpRequests.Count > 0 && _ntpRequests.TryGetValue(remoteEndPoint, out _)) + { + if (packet.Size < 48) + { + NetDebug.Write(NetLogLevel.Trace, $"NTP response too short: {packet.Size}"); + return true; + } - internal int SendRaw(byte[] message, int start, int length, IPEndPoint remoteEndPoint) - { - if (!IsRunning) - return 0; + byte[] copiedData = new byte[packet.Size]; + Buffer.BlockCopy(packet.RawData, 0, copiedData, 0, packet.Size); + NtpPacket ntpPacket = NtpPacket.FromServerResponse(copiedData, DateTime.UtcNow); + try + { + ntpPacket.ValidateReply(); + } + catch (InvalidOperationException ex) + { + NetDebug.Write(NetLogLevel.Trace, $"NTP response error: {ex.Message}"); + ntpPacket = null; + } - SocketError errorCode = 0; - int result = _socket.SendTo(message, start, length, remoteEndPoint, ref errorCode); - NetPeer fromPeer; - switch (errorCode) - { - case SocketError.MessageSize: - NetDebug.Write(NetLogLevel.Trace, "[SRD] 10040, datalen: {0}", length); - return -1; - case SocketError.HostUnreachable: - if (TryGetPeer(remoteEndPoint, out fromPeer)) - DisconnectPeerForce(fromPeer, DisconnectReason.HostUnreachable, errorCode, null); - CreateEvent(NetEvent.EType.Error, remoteEndPoint: remoteEndPoint, errorCode: errorCode); - return -1; - case SocketError.ConnectionReset: //connection reset (connection closed) - if (TryGetPeer(remoteEndPoint, out fromPeer)) - DisconnectPeerForce(fromPeer, DisconnectReason.RemoteConnectionClose, errorCode, null); - return -1; + if (ntpPacket != null) + { + _ntpRequests.TryRemove(remoteEndPoint, out _); + _netEventListener.OnNtpResponse(ntpPacket); + } + return true; } - if (result <= 0) - return 0; -#if STATS_ENABLED - Statistics.PacketsSent++; - Statistics.BytesSent += (uint)length; -#endif - return result; - } - internal void DisconnectPeerForce(NetPeer peer, - DisconnectReason reason, - SocketError socketErrorCode, - NetPacket eventData) - { - DisconnectPeer(peer, reason, socketErrorCode, true, null, 0, 0, eventData); + return false; } - private void DisconnectPeer( - NetPeer peer, - DisconnectReason reason, - SocketError socketErrorCode, - bool force, - byte[] data, - int start, - int count, - NetPacket eventData) - { - bool wasConnected = peer.ConnectionState == ConnectionState.Connected; - if (!peer.Shutdown(data, start, count, force)) - return; - if(wasConnected) - _connectedPeersCount--; - CreateEvent( - NetEvent.EType.Disconnect, - peer, - errorCode: socketErrorCode, - disconnectReason: reason, - readerSource: eventData); - } + //connect to + protected override LiteNetPeer CreateOutgoingPeer(IPEndPoint remoteEndPoint, int id, byte connectNum, ReadOnlySpan connectData) => + new NetPeer(this, remoteEndPoint, id, connectNum, connectData); - private void CreateEvent( - NetEvent.EType type, - NetPeer peer = null, - IPEndPoint remoteEndPoint = null, - SocketError errorCode = 0, - int latency = 0, - DisconnectReason disconnectReason = DisconnectReason.ConnectionFailed, - ConnectionRequest connectionRequest = null, - DeliveryMethod deliveryMethod = DeliveryMethod.Unreliable, - NetPacket readerSource = null) - { - NetEvent evt = null; - if (type == NetEvent.EType.Connect) - _connectedPeersCount++; - lock (_netEventsPool) - { - if (_netEventsPool.Count > 0) - evt = _netEventsPool.Pop(); - } - if(evt == null) - evt = new NetEvent(this); - evt.Type = type; - evt.DataReader.SetSource(readerSource); - evt.Peer = peer; - evt.RemoteEndPoint = remoteEndPoint; - evt.Latency = latency; - evt.ErrorCode = errorCode; - evt.DisconnectReason = disconnectReason; - evt.ConnectionRequest = connectionRequest; - evt.DeliveryMethod = deliveryMethod; - if (UnsyncedEvents) - { - ProcessEvent(evt); - } - else - { - lock (_netEventsQueue) - _netEventsQueue.Enqueue(evt); - } - } + //accept + protected override LiteNetPeer CreateIncomingPeer(LiteConnectionRequest request, int id) => + new NetPeer(this, request, id); + + //reject + protected override LiteNetPeer CreateRejectPeer(IPEndPoint remoteEndPoint, int id) => + new NetPeer(this, remoteEndPoint, id); + + //connection request when you use Accept for getting NetPeer instead of LiteNetPeer + protected override LiteConnectionRequest CreateConnectionRequest(IPEndPoint remoteEndPoint, NetConnectRequestPacket requestPacket) => + new ConnectionRequest(remoteEndPoint, requestPacket, this); - private void ProcessEvent(NetEvent evt) + protected override void ProcessEvent(NetEvent evt) { NetDebug.Write("[NM] Processing event: " + evt.Type); bool emptyData = evt.DataReader.IsNull; + var netPeer = evt.Peer as NetPeer; switch (evt.Type) { case NetEvent.EType.Connect: - _netEventListener.OnPeerConnected(evt.Peer); + _netEventListener.OnPeerConnected(netPeer); break; case NetEvent.EType.Disconnect: var info = new DisconnectInfo @@ -486,10 +157,10 @@ private void ProcessEvent(NetEvent evt) AdditionalData = evt.DataReader, SocketErrorCode = evt.ErrorCode }; - _netEventListener.OnPeerDisconnected(evt.Peer, info); + _netEventListener.OnPeerDisconnected(netPeer, info); break; case NetEvent.EType.Receive: - _netEventListener.OnNetworkReceive(evt.Peer, evt.DataReader, evt.DeliveryMethod); + _netEventListener.OnNetworkReceive(netPeer, evt.DataReader, evt.ChannelNumber, evt.DeliveryMethod); break; case NetEvent.EType.ReceiveUnconnected: _netEventListener.OnNetworkReceiveUnconnected(evt.RemoteEndPoint, evt.DataReader, UnconnectedMessageType.BasicMessage); @@ -501,411 +172,60 @@ private void ProcessEvent(NetEvent evt) _netEventListener.OnNetworkError(evt.RemoteEndPoint, evt.ErrorCode); break; case NetEvent.EType.ConnectionLatencyUpdated: - _netEventListener.OnNetworkLatencyUpdate(evt.Peer, evt.Latency); + _netEventListener.OnNetworkLatencyUpdate(netPeer, evt.Latency); break; case NetEvent.EType.ConnectionRequest: - _netEventListener.OnConnectionRequest(evt.ConnectionRequest); - break; - } - //Recycle if not message - if (emptyData) - RecycleEvent(evt); - else if (AutoRecycle) - evt.DataReader.Recycle(); - } - - internal void RecycleEvent(NetEvent evt) - { - evt.Peer = null; - evt.ErrorCode = 0; - evt.RemoteEndPoint = null; - evt.ConnectionRequest = null; - lock (_netEventsPool) - _netEventsPool.Push(evt); - } - - //Update function - private void UpdateLogic() - { - var peersToRemove = new List(); - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - while (IsRunning) - { -#if DEBUG - if (SimulateLatency) - { - var time = DateTime.UtcNow; - lock (_pingSimulationList) - { - for (int i = 0; i < _pingSimulationList.Count; i++) - { - var incomingData = _pingSimulationList[i]; - if (incomingData.TimeWhenGet <= time) - { - DataReceived(incomingData.Data, incomingData.Data.Length, incomingData.EndPoint); - _pingSimulationList.RemoveAt(i); - i--; - } - } - } - } -#endif - -#if STATS_ENABLED - ulong totalPacketLoss = 0; -#endif - int elapsed = (int)stopwatch.ElapsedMilliseconds; - if (elapsed <= 0) - elapsed = 1; - for(var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if (netPeer.ConnectionState == ConnectionState.Disconnected && netPeer.TimeSinceLastPacket > DisconnectTimeout) - { - peersToRemove.Add(netPeer); - } - else - { - netPeer.Update(elapsed); -#if STATS_ENABLED - totalPacketLoss += netPeer.Statistics.PacketLoss; -#endif - } - } - if (peersToRemove.Count > 0) - { - _peersLock.EnterWriteLock(); - for (int i = 0; i < peersToRemove.Count; i++) - RemovePeerInternal(peersToRemove[i]); - _peersLock.ExitWriteLock(); - peersToRemove.Clear(); - } -#if STATS_ENABLED - Statistics.PacketLoss = totalPacketLoss; -#endif - int sleepTime = UpdateTime - (int)(stopwatch.ElapsedMilliseconds - elapsed); - stopwatch.Reset(); - stopwatch.Start(); - if (sleepTime > 0) - Thread.Sleep(sleepTime); - } - stopwatch.Stop(); - } - - void INetSocketListener.OnMessageReceived(byte[] data, int length, SocketError errorCode, IPEndPoint remoteEndPoint) - { - if (errorCode != 0) - { - CreateEvent(NetEvent.EType.Error, errorCode: errorCode); - NetDebug.WriteError("[NM] Receive error: {0}", errorCode); - return; - } -#if DEBUG - if (SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance) - { - //drop packet - return; - } - if (SimulateLatency) - { - int latency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); - if (latency > MinLatencyThreshold) - { - byte[] holdedData = new byte[length]; - Buffer.BlockCopy(data, 0, holdedData, 0, length); - - lock (_pingSimulationList) - { - _pingSimulationList.Add(new IncomingData - { - Data = holdedData, - EndPoint = remoteEndPoint, - TimeWhenGet = DateTime.UtcNow.AddMilliseconds(latency) - }); - } - //hold packet - return; - } - } -#endif - try - { - //ProcessEvents - DataReceived(data, length, remoteEndPoint); - } - catch(Exception e) - { - //protects socket receive thread - NetDebug.WriteError("[NM] SocketReceiveThread error: " + e ); - } - } - - void IConnectionRequestListener.OnConnectionSolved(ConnectionRequest request, byte[] rejectData, int start, int length) - { - if (request.Result == ConnectionRequestResult.Reject) - { - NetDebug.Write(NetLogLevel.Trace, "[NM] Peer connect reject."); - request.Peer.Reject(request.ConnectionId, request.ConnectionNumber, rejectData, start, length); - } - else - { - //Accept - request.Peer.Accept(request.ConnectionId, request.ConnectionNumber); - //TODO: sync - //Add event - CreateEvent(NetEvent.EType.Connect, request.Peer); - - NetDebug.Write(NetLogLevel.Trace, "[NM] Received peer connection Id: {0}, EP: {1}", - request.Peer.ConnectTime, request.Peer.EndPoint); - } - } - - private int GetNextPeerId() - { - lock (_peerIds) - { - if (_peerIds.Count == 0) - return _lastPeerId++; - return _peerIds.Dequeue(); - } - } - - private void ProcessConnectRequest( - IPEndPoint remoteEndPoint, - NetPeer netPeer, - NetConnectRequestPacket connRequest) - { - byte connectionNumber = connRequest.ConnectionNumber; - - //if we have peer - if (netPeer != null) - { - NetDebug.Write("ConnectRequest LastId: {0}, NewId: {1}, EP: {2}", netPeer.ConnectTime, connRequest.ConnectionTime, remoteEndPoint); - var processResult = netPeer.ProcessConnectRequest(connRequest); - switch (processResult) - { - case ConnectRequestResult.Reconnection: - DisconnectPeerForce(netPeer, DisconnectReason.RemoteConnectionClose, 0, null); - RemovePeer(netPeer); - //go to new connection - break; - case ConnectRequestResult.NewConnection: - RemovePeer(netPeer); - //go to new connection - break; - case ConnectRequestResult.P2PConnection: - CreateEvent( - NetEvent.EType.ConnectionRequest, - connectionRequest: new ConnectionRequest( - netPeer.ConnectTime, - connectionNumber, - ConnectionRequestType.PeerToPeer, - connRequest.Data, - netPeer, - this) - ); - return; - default: - //no operations needed - return; - } - //ConnectRequestResult.NewConnection - //Set next connection number - connectionNumber = (byte)((netPeer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); - //To reconnect peer - } - else - { - NetDebug.Write("ConnectRequest Id: {0}, EP: {1}", connRequest.ConnectionTime, remoteEndPoint); - } - //Add new peer and craete ConnectRequest event - NetDebug.Write("[NM] Creating request event: " + connRequest.ConnectionTime); - netPeer = new NetPeer(this, remoteEndPoint, GetNextPeerId()); - if (TryAddPeer(netPeer) == netPeer) - { - CreateEvent(NetEvent.EType.ConnectionRequest, connectionRequest: new ConnectionRequest( - connRequest.ConnectionTime, - connectionNumber, - ConnectionRequestType.Incoming, - connRequest.Data, - netPeer, - this)); - } - } - - private void DataReceived(byte[] reusableBuffer, int count, IPEndPoint remoteEndPoint) - { -#if STATS_ENABLED - Statistics.PacketsReceived++; - Statistics.BytesReceived += (uint) count; -#endif - //Try read packet - NetPacket packet = NetPacketPool.GetPacket(count, false); - if (!packet.FromBytes(reusableBuffer, 0, count)) - { - NetPacketPool.Recycle(packet); - NetDebug.WriteError("[NM] DataReceived: bad!"); - return; - } - - //get peer - //Check normal packets - NetPeer netPeer; - //old packets protection - bool peerFound = TryGetPeer(remoteEndPoint, out netPeer); - - //Check unconnected - switch (packet.Property) - { - case PacketProperty.PeerNotFound: - if (peerFound) - { - if (netPeer.ConnectionState != ConnectionState.Connected) - return; - if (packet.Size == 1) - { - //first reply - var p = NetPacketPool.GetWithProperty(PacketProperty.PeerNotFound, 9); - p.RawData[1] = 0; - FastBitConverter.GetBytes(p.RawData, 2, netPeer.ConnectTime); - SendRawAndRecycle(p, remoteEndPoint); - NetDebug.Write("PeerNotFound sending connectId: {0}", netPeer.ConnectTime); - } - else if (packet.Size == 10 && packet.RawData[1] == 1 && BitConverter.ToInt64(packet.RawData, 2) == netPeer.ConnectTime) - { - //second reply - NetDebug.Write("PeerNotFound received our connectId: {0}", netPeer.ConnectTime); - DisconnectPeerForce(netPeer, DisconnectReason.RemoteConnectionClose, 0, null); - } - } - else if (packet.Size == 10 && packet.RawData[1] == 0) - { - //send reply back - packet.RawData[1] = 1; - SendRawAndRecycle(packet, remoteEndPoint); - } - break; - case PacketProperty.Broadcast: - if (!BroadcastReceiveEnabled) - break; - CreateEvent(NetEvent.EType.Broadcast, remoteEndPoint: remoteEndPoint, readerSource: packet); - break; - - case PacketProperty.UnconnectedMessage: - if (!UnconnectedMessagesEnabled) - break; - CreateEvent(NetEvent.EType.ReceiveUnconnected, remoteEndPoint: remoteEndPoint, readerSource: packet); - break; - - case PacketProperty.NatIntroduction: - case PacketProperty.NatIntroductionRequest: - case PacketProperty.NatPunchMessage: - if (NatPunchEnabled) - NatPunchModule.ProcessMessage(remoteEndPoint, packet); - break; - case PacketProperty.InvalidProtocol: - if (peerFound && netPeer.ConnectionState == ConnectionState.Outcoming) - DisconnectPeerForce(netPeer, DisconnectReason.InvalidProtocol, 0, null); - break; - case PacketProperty.Disconnect: - if (peerFound) - { - var disconnectResult = netPeer.ProcessDisconnect(packet); - if (disconnectResult == DisconnectResult.None) - { - NetPacketPool.Recycle(packet); - return; - } - DisconnectPeerForce( - netPeer, - disconnectResult == DisconnectResult.Disconnect - ? DisconnectReason.RemoteConnectionClose - : DisconnectReason.ConnectionRejected, - 0, packet); - } - else - { - NetPacketPool.Recycle(packet); - } - //Send shutdown - SendRawAndRecycle(NetPacketPool.GetWithProperty(PacketProperty.ShutdownOk, 0), remoteEndPoint); + _netEventListener.OnConnectionRequest((ConnectionRequest)evt.ConnectionRequest); break; - - case PacketProperty.ConnectAccept: - var connAccept = NetConnectAcceptPacket.FromData(packet); - if (connAccept != null && peerFound && netPeer.ProcessConnectAccept(connAccept)) - CreateEvent(NetEvent.EType.Connect, netPeer); + case NetEvent.EType.MessageDelivered: + _netEventListener.OnMessageDelivered(netPeer, evt.UserData); break; - case PacketProperty.ConnectRequest: - if (NetConnectRequestPacket.GetProtocolId(packet) != NetConstants.ProtocolId) + case NetEvent.EType.PeerAddressChanged: + _peersLock.EnterUpgradeableReadLock(); + IPEndPoint previousAddress = null; + if (ContainsPeer(evt.Peer)) { - SendRawAndRecycle(NetPacketPool.GetWithProperty(PacketProperty.InvalidProtocol, 0), remoteEndPoint); - break; + _peersLock.EnterWriteLock(); + RemovePeerFromSet(evt.Peer); + previousAddress = new IPEndPoint(evt.Peer.Address, evt.Peer.Port); + evt.Peer.FinishEndPointChange(evt.RemoteEndPoint); + AddPeerToSet(evt.Peer); + _peersLock.ExitWriteLock(); } - var connRequest = NetConnectRequestPacket.FromData(packet); - if (connRequest != null) - ProcessConnectRequest(remoteEndPoint, netPeer, connRequest); - break; - default: - if(peerFound) - netPeer.ProcessPacket(packet); - else - SendRawAndRecycle(NetPacketPool.GetWithProperty(PacketProperty.PeerNotFound, 0), remoteEndPoint); + _peersLock.ExitUpgradeableReadLock(); + if (previousAddress != null) + _netEventListener.OnPeerAddressChanged(netPeer, previousAddress); break; } + //Recycle if not message + if (emptyData) + RecycleEvent(evt); + else if (AutoRecycle) + evt.DataReader.RecycleInternal(); } - internal void ReceiveFromPeer(NetPacket packet, IPEndPoint remoteEndPoint) + protected override void ProcessNtpRequests(float elapsedMilliseconds) { - NetPeer fromPeer; - if (!TryGetPeer(remoteEndPoint, out fromPeer)) + if (_ntpRequests.IsEmpty) return; - NetDebug.Write(NetLogLevel.Trace, "[NM] Received message"); - DeliveryMethod deliveryMethod; - switch (packet.Property) + List requestsToRemove = null; + foreach (var ntpRequest in _ntpRequests) { - default: //PacketProperty.Unreliable - deliveryMethod = DeliveryMethod.Unreliable; - break; - case PacketProperty.Channeled: - deliveryMethod = NetConstants.ChannelIdToDeliveryMethod(packet.ChannelId, _channelsCount); - break; + ntpRequest.Value.Send(_udpSocketv4, elapsedMilliseconds); + if (ntpRequest.Value.NeedToKill) + { + if (requestsToRemove == null) + requestsToRemove = new List(); + requestsToRemove.Add(ntpRequest.Key); + } } - CreateEvent(NetEvent.EType.Receive, fromPeer, deliveryMethod: deliveryMethod, readerSource: packet); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// DataWriter with data - /// Send options (reliable, unreliable, etc.) - public void SendToAll(NetDataWriter writer, DeliveryMethod options) - { - SendToAll(writer.Data, 0, writer.Length, options); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, DeliveryMethod options) - { - SendToAll(data, 0, data.Length, options); - } - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Start of data - /// Length of data - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, int start, int length, DeliveryMethod options) - { - SendToAll(data, start, length, 0, options); + if (requestsToRemove != null) + { + foreach (var ipEndPoint in requestsToRemove) + _ntpRequests.TryRemove(ipEndPoint, out _); + } } /// @@ -914,10 +234,8 @@ public void SendToAll(byte[] data, int start, int length, DeliveryMethod options /// DataWriter with data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) - public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options) - { + public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options) => SendToAll(writer.Data, 0, writer.Length, channelNumber, options); - } /// /// Send data to all connected peers @@ -925,70 +243,75 @@ public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod o /// Data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options) - { + public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options) => SendToAll(data, 0, data.Length, channelNumber, options); - } /// /// Send data to all connected peers /// - /// Data - /// Start of data - /// Length of data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options) - { - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - netPeer.Send(data, start, length, channelNumber, options); - } - - /// - /// Send data to all connected peers (channel - 0) - /// /// DataWriter with data + /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) /// Excluded peer - public void SendToAll(NetDataWriter writer, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(writer.Data, 0, writer.Length, 0, options, excludePeer); - } + public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options, LiteNetPeer excludePeer) => + SendToAll(writer.Data, 0, writer.Length, channelNumber, options, excludePeer); /// - /// Send data to all connected peers (channel - 0) + /// Send data to all connected peers /// /// Data + /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) /// Excluded peer - public void SendToAll(byte[] data, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(data, 0, data.Length, 0, options, excludePeer); - } + public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options, LiteNetPeer excludePeer) => + SendToAll(data, 0, data.Length, channelNumber, options, excludePeer); /// - /// Send data to all connected peers (channel - 0) + /// Send data to all connected peers /// /// Data /// Start of data /// Length of data + /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) /// Excluded peer - public void SendToAll(byte[] data, int start, int length, DeliveryMethod options, NetPeer excludePeer) + public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options, LiteNetPeer excludePeer) { - SendToAll(data, start, length, 0, options, excludePeer); + try + { + _peersLock.EnterReadLock(); + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + { + if (netPeer != excludePeer) + ((NetPeer)netPeer).Send(data, start, length, channelNumber, options); + } + } + finally + { + _peersLock.ExitReadLock(); + } } /// /// Send data to all connected peers /// - /// DataWriter with data + /// Data + /// Start of data + /// Length of data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) + public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options) { - SendToAll(writer.Data, 0, writer.Length, channelNumber, options, excludePeer); + try + { + _peersLock.EnterReadLock(); + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) + ((NetPeer)netPeer).Send(data, start, length, channelNumber, options); + } + finally + { + _peersLock.ExitReadLock(); + } } /// @@ -997,165 +320,30 @@ public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod o /// Data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(data, 0, data.Length, channelNumber, options, excludePeer); - } - + public void SendToAll(ReadOnlySpan data, byte channelNumber, DeliveryMethod options) => + SendToAll(data, channelNumber, options, null); /// /// Send data to all connected peers /// /// Data - /// Start of data - /// Length of data /// Number of channel (from 0 to channelsCount - 1) /// Send options (reliable, unreliable, etc.) /// Excluded peer - public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) - { - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if (netPeer != excludePeer) - netPeer.Send(data, start, length, channelNumber, options); - } - } - - /// - /// Start logic thread and listening on available port - /// - public bool Start() - { - return Start(0); - } - - /// - /// Start logic thread and listening on selected port - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool Start(IPAddress addressIPv4, IPAddress addressIPv6, int port) - { - if (IsRunning) - return false; - if (!_socket.Bind(addressIPv4, addressIPv6, port, ReuseAddress, IPv6Enabled)) - return false; - IsRunning = true; - _logicThread = new Thread(UpdateLogic) { Name = "LogicThread", IsBackground = true }; - _logicThread.Start(); - return true; - } - - /// - /// Start logic thread and listening on selected port - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool Start(string addressIPv4, string addressIPv6, int port) - { - IPAddress ipv4 = NetUtils.ResolveAddress(addressIPv4); - IPAddress ipv6 = NetUtils.ResolveAddress(addressIPv6); - return Start(ipv4, ipv6, port); - } - - /// - /// Start logic thread and listening on selected port - /// - /// port to listen - public bool Start(int port) - { - return Start(IPAddress.Any, IPAddress.IPv6Any, port); - } - - /// - /// Send message without connection - /// - /// Raw data - /// Packet destination - /// Operation result - public bool SendUnconnectedMessage(byte[] message, IPEndPoint remoteEndPoint) - { - return SendUnconnectedMessage(message, 0, message.Length, remoteEndPoint); - } - - /// - /// Send message without connection - /// - /// Data serializer - /// Packet destination - /// Operation result - public bool SendUnconnectedMessage(NetDataWriter writer, IPEndPoint remoteEndPoint) - { - return SendUnconnectedMessage(writer.Data, 0, writer.Length, remoteEndPoint); - } - - /// - /// Send message without connection - /// - /// Raw data - /// data start - /// data length - /// Packet destination - /// Operation result - public bool SendUnconnectedMessage(byte[] message, int start, int length, IPEndPoint remoteEndPoint) - { - if (!IsRunning) - return false; - var packet = NetPacketPool.GetWithData(PacketProperty.UnconnectedMessage, message, start, length); - bool result = SendRawAndRecycle(packet, remoteEndPoint) > 0; - return result; - } - - public bool SendBroadcast(NetDataWriter writer, int port) - { - return SendBroadcast(writer.Data, 0, writer.Length, port); - } - - public bool SendBroadcast(byte[] data, int port) - { - return SendBroadcast(data, 0, data.Length, port); - } - - public bool SendBroadcast(byte[] data, int start, int length, int port) - { - if (!IsRunning) - return false; - var packet = NetPacketPool.GetWithData(PacketProperty.Broadcast, data, start, length); - bool result = _socket.SendBroadcast(packet.RawData, 0, packet.Size, port); - NetPacketPool.Recycle(packet); - return result; - } - - /// - /// Flush all queued packets of all peers - /// - public void Flush() - { - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - netPeer.Flush(); - } - - /// - /// Receive all pending events. Call this in game update code - /// - public void PollEvents() + public void SendToAll(ReadOnlySpan data, byte channelNumber, DeliveryMethod options, LiteNetPeer excludePeer) { - if (UnsyncedEvents) - return; - while (true) + try { - NetEvent evt; - lock (_netEventsQueue) + _peersLock.EnterReadLock(); + for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) { - if (_netEventsQueue.Count > 0) - evt = _netEventsQueue.Dequeue(); - else - return; + if (netPeer != excludePeer) + ((NetPeer)netPeer).Send(data, channelNumber, options); } - ProcessEvent(evt); + } + finally + { + _peersLock.ExitReadLock(); } } @@ -1165,12 +353,10 @@ public void PollEvents() /// Server IP or hostname /// Server Port /// Connection key - /// New NetPeer if new connection, Old NetPeer if already connected - /// Manager is not running. Call - public NetPeer Connect(string address, int port, string key) - { - return Connect(address, port, NetDataWriter.FromString(key)); - } + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public new NetPeer Connect(string address, int port, string key) => + Connect(address, port, NetDataWriter.FromString(key)); /// /// Connect to remote host @@ -1178,266 +364,45 @@ public NetPeer Connect(string address, int port, string key) /// Server IP or hostname /// Server Port /// Additional data for remote peer - /// New NetPeer if new connection, Old NetPeer if already connected - /// Manager is not running. Call - public NetPeer Connect(string address, int port, NetDataWriter connectionData) - { - IPEndPoint ep; - try - { - ep = NetUtils.MakeEndPoint(address, port); - } - catch - { - CreateEvent(NetEvent.EType.Disconnect, disconnectReason: DisconnectReason.UnknownHost); - return null; - } - return Connect(ep, connectionData); - } + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public new NetPeer Connect(string address, int port, NetDataWriter connectionData) => + (NetPeer)base.Connect(address, port, connectionData); /// /// Connect to remote host /// /// Server end point (ip and port) /// Connection key - /// New NetPeer if new connection, Old NetPeer if already connected - /// Manager is not running. Call - public NetPeer Connect(IPEndPoint target, string key) - { - return Connect(target, NetDataWriter.FromString(key)); - } + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public new NetPeer Connect(IPEndPoint target, string key) => + (NetPeer)base.Connect(target, key); /// /// Connect to remote host /// /// Server end point (ip and port) /// Additional data for remote peer - /// New NetPeer if new connection, Old NetPeer if already connected - /// Manager is not running. Call - public NetPeer Connect(IPEndPoint target, NetDataWriter connectionData) - { - if (!IsRunning) - throw new InvalidOperationException("Client is not running"); - - NetPeer peer; - byte connectionNumber = 0; - if (TryGetPeer(target, out peer)) - { - switch (peer.ConnectionState) - { - //just return already connected peer - case ConnectionState.Connected: - case ConnectionState.Outcoming: - case ConnectionState.Incoming: - return peer; - } - //else reconnect - connectionNumber = (byte)((peer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); - RemovePeer(peer); - } - //Create reliable connection - //And send connection request - return TryAddPeer(new NetPeer(this, target, GetNextPeerId(), connectionNumber, connectionData)); - } - - /// - /// Force closes connection and stop all threads. - /// - public void Stop() - { - Stop(true); - } - - /// - /// Force closes connection and stop all threads. - /// - /// Send disconnect messages - public void Stop(bool sendDisconnectMessages) - { - if (!IsRunning) - return; - NetDebug.Write("[NM] Stop"); - - //Send last disconnect - for(var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - netPeer.Shutdown(null, 0, 0, !sendDisconnectMessages); - - //For working send - IsRunning = false; - - //Stop - _logicThread.Join(); - _logicThread = null; - _socket.Close(); - - //clear peers - _peersLock.EnterWriteLock(); - _headPeer = null; - _peersDict.Clear(); - _peersLock.ExitWriteLock(); -#if DEBUG - lock (_pingSimulationList) - _pingSimulationList.Clear(); -#endif - _connectedPeersCount = 0; - lock(_netEventsQueue) - _netEventsQueue.Clear(); - } - - /// - /// Return peers count with connection state - /// - /// peer connection state (you can use as bit flags) - /// peers count - public int GetPeersCount(ConnectionState peerState) - { - int count = 0; - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if ((netPeer.ConnectionState & peerState) != 0) - count++; - } - return count; - } - - /// - /// Get copy of current connected peers (slow! use GetPeersNonAlloc for best performance) - /// - /// Array with connected peers - [Obsolete("Use GetPeers(ConnectionState peerState)")] - public NetPeer[] GetPeers() - { - return GetPeers(ConnectionState.Connected | ConnectionState.Outcoming); - } - - /// - /// Get copy of current connected peers (slow! use GetPeersNonAlloc for best performance) - /// - /// Array with connected peers - public NetPeer[] GetPeers(ConnectionState peerState) - { - List peersList = new List(); - GetPeersNonAlloc(peersList, peerState); - return peersList.ToArray(); - } - - /// - /// Get copy of peers (without allocations) - /// - /// List that will contain result - /// State of peers - public void GetPeersNonAlloc(List peers, ConnectionState peerState) - { - peers.Clear(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if ((netPeer.ConnectionState & peerState) != 0) - peers.Add(netPeer); - } - } - - /// - /// Disconnect all peers without any additional data - /// - public void DisconnectAll() - { - DisconnectAll(null, 0, 0); - } - - /// - /// Disconnect all peers with shutdown message - /// - /// Data to send (must be less or equal MTU) - /// Data start - /// Data count - public void DisconnectAll(byte[] data, int start, int count) - { - //Send disconnect packets - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - DisconnectPeer( - netPeer, - DisconnectReason.DisconnectPeerCalled, - 0, - false, - data, - start, - count, - null); - } - } - - /// - /// Immediately disconnect peer from server without additional data - /// - /// peer to disconnect - public void DisconnectPeerForce(NetPeer peer) - { - DisconnectPeerForce(peer, DisconnectReason.DisconnectPeerCalled, 0, null); - } - - /// - /// Disconnect peer from server - /// - /// peer to disconnect - public void DisconnectPeer(NetPeer peer) - { - DisconnectPeer(peer, null, 0, 0); - } - - /// - /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - /// - /// peer to disconnect - /// additional data - public void DisconnectPeer(NetPeer peer, byte[] data) - { - DisconnectPeer(peer, data, 0, data.Length); - } + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public new NetPeer Connect(IPEndPoint target, NetDataWriter connectionData) => + (NetPeer)base.Connect(target, connectionData); /// - /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - /// - /// peer to disconnect - /// additional data - public void DisconnectPeer(NetPeer peer, NetDataWriter writer) - { - DisconnectPeer(peer, writer.Data, 0, writer.Length); - } - - /// - /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) + /// Connect to remote host /// - /// peer to disconnect - /// additional data - /// data start - /// data length - public void DisconnectPeer(NetPeer peer, byte[] data, int start, int count) - { - DisconnectPeer( - peer, - DisconnectReason.DisconnectPeerCalled, - 0, - false, - data, - start, - count, - null); - } + /// Server end point (ip and port) + /// Additional data for remote peer + /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting + /// Manager is not running. Call + public new NetPeer Connect(IPEndPoint target, ReadOnlySpan connectionData) => + (NetPeer)base.Connect(target, connectionData); - public IEnumerator GetEnumerator() - { - var peer = _headPeer; - while (peer != null) - { - yield return peer; - peer = peer.NextPeer; - } - } + public new NetPeerEnumerator GetEnumerator() => + new NetPeerEnumerator((NetPeer)_headPeer); - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => + new NetPeerEnumerator((NetPeer)_headPeer); } } diff --git a/LiteNetLib/NetPacket.cs b/LiteNetLib/NetPacket.cs index 2ff8e270..d0ecdd6a 100644 --- a/LiteNetLib/NetPacket.cs +++ b/LiteNetLib/NetPacket.cs @@ -1,4 +1,4 @@ -using System; +using System; using LiteNetLib.Utils; namespace LiteNetLib @@ -7,6 +7,7 @@ internal enum PacketProperty : byte { Unreliable, Channeled, + ReliableMerged, Ack, Ping, Pong, @@ -14,238 +15,207 @@ internal enum PacketProperty : byte ConnectAccept, Disconnect, UnconnectedMessage, - NatIntroductionRequest, - NatIntroduction, - NatPunchMessage, MtuCheck, MtuOk, Broadcast, Merged, ShutdownOk, PeerNotFound, - InvalidProtocol + InvalidProtocol, + NatMessage, + Empty, + Total } internal sealed class NetPacket { - private static readonly int LastProperty = Enum.GetValues(typeof(PacketProperty)).Length; - //Header + private static readonly int PropertiesCount = (int)PacketProperty.Total; + private static readonly int[] HeaderSizes; + + static NetPacket() + { + HeaderSizes = NetUtils.AllocatePinnedUninitializedArray(PropertiesCount); + for (int i = 0; i < HeaderSizes.Length; i++) + { + switch ((PacketProperty)i) + { + case PacketProperty.Channeled: + case PacketProperty.Ack: + case PacketProperty.ReliableMerged: + HeaderSizes[i] = NetConstants.ChanneledHeaderSize; + break; + case PacketProperty.Ping: + HeaderSizes[i] = NetConstants.HeaderSize + 2; + break; + case PacketProperty.ConnectRequest: + HeaderSizes[i] = NetConnectRequestPacket.HeaderSize; + break; + case PacketProperty.ConnectAccept: + HeaderSizes[i] = NetConnectAcceptPacket.Size; + break; + case PacketProperty.Disconnect: + HeaderSizes[i] = NetConstants.HeaderSize + 8; + break; + case PacketProperty.Pong: + HeaderSizes[i] = NetConstants.HeaderSize + 10; + break; + default: + HeaderSizes[i] = NetConstants.HeaderSize; + break; + } + } + } + + /// + /// Gets or sets the packet property (type). + /// Stored in the first 5 bits of the first byte (0x1F mask). + /// public PacketProperty Property { - get { return (PacketProperty)(RawData[0] & 0x1F); } - set { RawData[0] = (byte)((RawData[0] & 0xE0) | (byte)value); } + get => (PacketProperty)(RawData[0] & 0x1F); + set => RawData[0] = (byte)((RawData[0] & 0xE0) | (byte)value); } + /// + /// Gets or sets the connection number used to distinguish between multiple connection instances from the same endpoint. + /// Stored in bits 6 and 7 of the first byte (0x60 mask). + /// + /// + /// Used to discard packets from previous connections made in the same frame/time. + /// public byte ConnectionNumber { - get { return (byte)((RawData[0] & 0x60) >> 5); } - set { RawData[0] = (byte) ((RawData[0] & 0x9F) | (value << 5)); } + get => (byte)((RawData[0] & 0x60) >> 5); + set => RawData[0] = (byte)((RawData[0] & 0x9F) | (value << 5)); } + /// + /// Gets or sets the sequence number of the packet. + /// Located at offset 1 in . + /// public ushort Sequence { - get { return BitConverter.ToUInt16(RawData, 1); } - set { FastBitConverter.GetBytes(RawData, 1, value); } + get => BitConverter.ToUInt16(RawData, 1); + set => FastBitConverter.GetBytes(RawData, 1, value); } - public bool IsFragmented - { - get { return (RawData[0] & 0x80) != 0; } - } + /// + /// Returns if the fragmentation bit (the highest bit of the first byte) is set. + /// + public bool IsFragmented => (RawData[0] & 0x80) != 0; - public void MarkFragmented() - { - RawData[0] |= 0x80; //set first bit - } + /// + /// Sets the fragmentation bit (0x80) in the packet header. + /// + public void MarkFragmented() => RawData[0] |= 0x80; + /// + /// Gets or sets the channel identifier. + /// Located at offset 3 in . + /// public byte ChannelId { - get { return RawData[3]; } - set { RawData[3] = value; } + get => RawData[3]; + set => RawData[3] = value; } + /// + /// Gets or sets the unique identifier for a fragmented message. + /// Located at offset 4 in . + /// public ushort FragmentId { - get { return BitConverter.ToUInt16(RawData, 4); } - set { FastBitConverter.GetBytes(RawData, 4, value); } + get => BitConverter.ToUInt16(RawData, 4); + set => FastBitConverter.GetBytes(RawData, 4, value); } + /// + /// Gets or sets the index of the current fragment part. + /// Located at offset 6 in . + /// public ushort FragmentPart { - get { return BitConverter.ToUInt16(RawData, 6); } - set { FastBitConverter.GetBytes(RawData, 6, value); } + get => BitConverter.ToUInt16(RawData, 6); + set => FastBitConverter.GetBytes(RawData, 6, value); } + /// + /// Gets or sets the total number of fragments in the message. + /// Located at offset 8 in . + /// public ushort FragmentsTotal { - get { return BitConverter.ToUInt16(RawData, 8); } - set { FastBitConverter.GetBytes(RawData, 8, value); } + get => BitConverter.ToUInt16(RawData, 8); + set => FastBitConverter.GetBytes(RawData, 8, value); } - //Data + /// + /// The raw array containing the packet header and payload. + /// public byte[] RawData; + + /// + /// The actual size of the data in , including headers. + /// public int Size; + /// + /// Custom user data associated with this packet. Used for delivery notifications. + /// + public object UserData; + + /// + /// Reference to the next packet in the NetPacketPool. + /// + public NetPacket Next; + + /// + /// Initializes a new instance of the class with a specific buffer size. + /// + /// Total size of the packet including headers. public NetPacket(int size) { RawData = new byte[size]; Size = size; } - public NetPacket(PacketProperty property, int size) + /// + /// Initializes a new instance of the class, calculating the required size based on the property. + /// + /// The type of packet to create. + /// Size of the user data payload. + public NetPacket(PacketProperty property, int payloadSize) { - size += GetHeaderSize(property); - RawData = new byte[size]; + int totalSize = payloadSize + GetHeaderSize(property); + RawData = new byte[totalSize]; Property = property; - Size = size; + Size = totalSize; } - public void Realloc(int toSize, bool clear) - { - Size = toSize; - if (RawData.Length < toSize) - { - RawData = new byte[toSize]; - return; - } - if (clear) //clear not reallocated - Array.Clear(RawData, 0, toSize); - } + /// + /// Gets the fixed header size for a specific . + /// + /// The packet type. + /// Header size in bytes. + public static int GetHeaderSize(PacketProperty property) => HeaderSizes[(int)property]; - public static int GetHeaderSize(PacketProperty property) - { - switch (property) - { - case PacketProperty.Channeled: - case PacketProperty.Ack: - return NetConstants.ChanneledHeaderSize; - case PacketProperty.Ping: - return NetConstants.HeaderSize + 2; - case PacketProperty.ConnectRequest: - return NetConnectRequestPacket.HeaderSize; - case PacketProperty.ConnectAccept: - return NetConnectAcceptPacket.Size; - case PacketProperty.Disconnect: - return NetConstants.HeaderSize + 8; - case PacketProperty.Pong: - return NetConstants.HeaderSize + 10; - default: - return NetConstants.HeaderSize; - } - } + /// + /// Gets the header size of the current packet based on its property bits. + /// + public int HeaderSize => HeaderSizes[RawData[0] & 0x1F]; - public int GetHeaderSize() + /// + /// Performs a basic check on the packet header and size. + /// + /// if the packet property is valid and the size is sufficient for the headers. + public bool Verify() { - return GetHeaderSize(Property); - } - - //Packet contstructor from byte array - public bool FromBytes(byte[] data, int start, int packetSize) - { - //Reading property - byte property = (byte)(data[start] & 0x1F); - bool fragmented = (data[start] & 0x80) != 0; - int headerSize = GetHeaderSize((PacketProperty) property); - - if (property > LastProperty || packetSize < headerSize || - (fragmented && packetSize < headerSize + NetConstants.FragmentHeaderSize)) - { + byte property = (byte)(RawData[0] & 0x1F); + if (property >= PropertiesCount) return false; - } - - Buffer.BlockCopy(data, start, RawData, 0, packetSize); - Size = packetSize; - return true; - } - } - - internal sealed class NetConnectRequestPacket - { - public const int HeaderSize = 13; - public readonly long ConnectionTime; - public readonly byte ConnectionNumber; - public readonly NetDataReader Data; - - private NetConnectRequestPacket(long connectionId, byte connectionNumber, NetDataReader data) - { - ConnectionTime = connectionId; - ConnectionNumber = connectionNumber; - Data = data; - } - - public static int GetProtocolId(NetPacket packet) - { - return BitConverter.ToInt32(packet.RawData, 1); - } - - public static NetConnectRequestPacket FromData(NetPacket packet) - { - if (packet.ConnectionNumber >= NetConstants.MaxConnectionNumber) - return null; - - //Getting new id for peer - long connectionId = BitConverter.ToInt64(packet.RawData, 5); - - // Read data and create request - var reader = new NetDataReader(null, 0, 0); - if (packet.Size > HeaderSize) - reader.SetSource(packet.RawData, HeaderSize, packet.Size); - - return new NetConnectRequestPacket(connectionId, packet.ConnectionNumber, reader); - } - - public static NetPacket Make(NetDataWriter connectData, long connectId) - { - //Make initial packet - var packet = new NetPacket(PacketProperty.ConnectRequest, connectData.Length); - - //Add data - FastBitConverter.GetBytes(packet.RawData, 1, NetConstants.ProtocolId); - FastBitConverter.GetBytes(packet.RawData, 5, connectId); - Buffer.BlockCopy(connectData.Data, 0, packet.RawData, HeaderSize, connectData.Length); - return packet; - } - } - - internal sealed class NetConnectAcceptPacket - { - public const int Size = 11; - public readonly long ConnectionId; - public readonly byte ConnectionNumber; - public readonly bool IsReusedPeer; - - private NetConnectAcceptPacket(long connectionId, byte connectionNumber, bool isReusedPeer) - { - ConnectionId = connectionId; - ConnectionNumber = connectionNumber; - IsReusedPeer = isReusedPeer; - } - - public static NetConnectAcceptPacket FromData(NetPacket packet) - { - if (packet.Size > Size) - return null; - - long connectionId = BitConverter.ToInt64(packet.RawData, 1); - //check connect num - byte connectionNumber = packet.RawData[9]; - if (connectionNumber >= NetConstants.MaxConnectionNumber) - return null; - //check reused flag - byte isReused = packet.RawData[10]; - if (isReused > 1) - return null; - - return new NetConnectAcceptPacket(connectionId, connectionNumber, isReused == 1); - } - - public static NetPacket Make(long connectId, byte connectNum, bool reusedPeer) - { - var packet = new NetPacket(PacketProperty.ConnectAccept, 0); - FastBitConverter.GetBytes(packet.RawData, 1, connectId); - packet.RawData[9] = connectNum; - packet.RawData[10] = (byte)(reusedPeer ? 1 : 0); - return packet; + int headerSize = HeaderSizes[property]; + bool fragmented = (RawData[0] & 0x80) != 0; + return Size >= headerSize && (!fragmented || Size >= headerSize + NetConstants.FragmentHeaderSize); } } } diff --git a/LiteNetLib/NetPacketPool.cs b/LiteNetLib/NetPacketPool.cs deleted file mode 100644 index 57a39996..00000000 --- a/LiteNetLib/NetPacketPool.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Threading; - -namespace LiteNetLib -{ - internal sealed class NetPacketPool - { - private readonly NetPacket[] _pool = new NetPacket[NetConstants.PacketPoolSize]; - private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - private int _count; - - public NetPacket GetWithData(PacketProperty property, byte[] data, int start, int length) - { - var packet = GetWithProperty(property, length); - Buffer.BlockCopy(data, start, packet.RawData, NetPacket.GetHeaderSize(property), length); - return packet; - } - - public NetPacket GetPacket(int size, bool clear) - { - NetPacket packet = null; - if (size <= NetConstants.MaxPacketSize) - { - _lock.EnterUpgradeableReadLock(); - if (_count > 0) - { - _lock.EnterWriteLock(); - _count--; - packet = _pool[_count]; - _pool[_count] = null; - _lock.ExitWriteLock(); - } - _lock.ExitUpgradeableReadLock(); - } - if (packet == null) - { - //allocate new packet - packet = new NetPacket(size); - } - else - { - //reallocate packet data if packet not fits - packet.Realloc(size, clear); - } - return packet; - } - - //Get packet with size - public NetPacket GetWithProperty(PacketProperty property, int size) - { - size += NetPacket.GetHeaderSize(property); - NetPacket packet = GetPacket(size, true); - packet.Property = property; - return packet; - } - - public void Recycle(NetPacket packet) - { - if (packet.RawData.Length > NetConstants.MaxPacketSize) - { - //Dont pool big packets. Save memory - return; - } - - //Clean fragmented flag - packet.RawData[0] = 0; - - _lock.EnterUpgradeableReadLock(); - if (_count == NetConstants.PacketPoolSize) - { - _lock.ExitUpgradeableReadLock(); - return; - } - _lock.EnterWriteLock(); - _pool[_count] = packet; - _count++; - _lock.ExitWriteLock(); - _lock.ExitUpgradeableReadLock(); - } - } -} diff --git a/LiteNetLib/NetPacketReader.cs b/LiteNetLib/NetPacketReader.cs new file mode 100644 index 00000000..2098dde2 --- /dev/null +++ b/LiteNetLib/NetPacketReader.cs @@ -0,0 +1,44 @@ +using LiteNetLib.Utils; + +namespace LiteNetLib +{ + public sealed class NetPacketReader : NetDataReader + { + private NetPacket _packet; + private readonly LiteNetManager _manager; + private readonly NetEvent _evt; + + internal NetPacketReader(LiteNetManager manager, NetEvent evt) + { + _manager = manager; + _evt = evt; + } + + internal void SetSource(NetPacket packet, int headerSize) + { + if (packet == null) + return; + _packet = packet; + _data = packet.RawData; + _position = headerSize; + _offset = headerSize; + _dataSize = packet.Size; + } + + internal void RecycleInternal() + { + Clear(); + if (_packet != null) + _manager.PoolRecycle(_packet); + _packet = null; + _manager.RecycleEvent(_evt); + } + + public void Recycle() + { + if (_manager.AutoRecycle) + return; + RecycleInternal(); + } + } +} diff --git a/LiteNetLib/NetPeer.cs b/LiteNetLib/NetPeer.cs index 951b4450..3be8844e 100644 --- a/LiteNetLib/NetPeer.cs +++ b/LiteNetLib/NetPeer.cs @@ -1,353 +1,141 @@ -#if DEBUG -#define STATS_ENABLED -#endif -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System; +using System.Collections.Concurrent; using System.Net; +using System.Threading; using LiteNetLib.Utils; namespace LiteNetLib { /// - /// Peer connection state + /// Improved LiteNetPeer with full multi-channel support /// - [Flags] - public enum ConnectionState : byte + public class NetPeer : LiteNetPeer { - Incoming = 1 << 1, - Outcoming = 1 << 2, - Connected = 1 << 3, - ShutdownRequested = 1 << 4, - Disconnected = 1 << 5, - Any = Incoming | Outcoming | Connected | ShutdownRequested - } - - internal enum ConnectRequestResult - { - None, - P2PConnection, //when peer connecting - Reconnection, //when peer was connected - NewConnection //when peer was disconnected - } - - internal enum DisconnectResult - { - None, - Reject, - Disconnect - } - - /// - /// Network peer. Main purpose is sending messages to specific peer. - /// - public class NetPeer - { - //Ping and RTT - private int _rtt; - private int _avgRtt; - private int _rttCount; - private double _resendDelay = 27.0; - private int _pingSendTimer; - private int _rttResetTimer; - private readonly Stopwatch _pingTimer = new Stopwatch(); - private int _timeSinceLastPacket; - private long _remoteDelta; - - //Common - private readonly IPEndPoint _remoteEndPoint; - private readonly NetManager _netManager; - private readonly NetPacketPool _packetPool; - private readonly object _flushLock = new object(); - private readonly object _sendLock = new object(); - private readonly object _shutdownLock = new object(); - - internal NetPeer NextPeer; - internal NetPeer PrevPeer; - - internal byte ConnectionNum - { - get { return _connectNum; } - private set - { - _connectNum = value; - _mergeData.ConnectionNumber = value; - _pingPacket.ConnectionNumber = value; - _pongPacket.ConnectionNumber = value; - } - } - - //Channels - private readonly SimpleChannel _unreliableChannel; + private readonly ConcurrentQueue _channelSendQueue = new ConcurrentQueue(); private readonly BaseChannel[] _channels; - private BaseChannel _headChannel; - private readonly byte _channelsCount; - private readonly int _channelsTotalCount; - //MTU - private int _mtu = NetConstants.PossibleMtu[0]; - private int _mtuIdx; - private bool _finishMtu; - private int _mtuCheckTimer; - private int _mtuCheckAttempts; - private const int MtuCheckDelay = 1000; - private const int MaxMtuCheckAttempts = 4; - private readonly object _mtuMutex = new object(); + protected override int ChannelsCount => ((NetManager)NetManager).ChannelsCount; - //Fragment - private class IncomingFragments + internal NetPeer(NetManager netManager, IPEndPoint remoteEndPoint, int id) : base(netManager, remoteEndPoint, id) { - public NetPacket[] Fragments; - public int ReceivedCount; - public int TotalSize; - } - private ushort _fragmentId; - private readonly Dictionary _holdedFragments; - - //Merging - private readonly NetPacket _mergeData; - private int _mergePos; - private int _mergeCount; - - //Connection - private int _connectAttempts; - private int _connectTimer; - private long _connectTime; - private byte _connectNum; - private ConnectionState _connectionState; - private NetPacket _shutdownPacket; - private const int ShutdownDelay = 300; - private int _shutdownTimer; - private readonly NetPacket _pingPacket; - private readonly NetPacket _pongPacket; - private readonly NetPacket _connectRequestPacket; - private NetPacket _connectAcceptPacket; - /// - /// Current connection state - /// - public ConnectionState ConnectionState { get { return _connectionState; } } - - /// - /// Connection time for internal purposes - /// - internal long ConnectTime { get { return _connectTime; } } - - /// - /// Peer id can be used as key in your dictionary of peers - /// - public readonly int Id; - - /// - /// Peer ip address and port - /// - public IPEndPoint EndPoint { get { return _remoteEndPoint; } } - - /// - /// Current ping in milliseconds - /// - public int Ping { get { return _avgRtt/2; } } - - /// - /// Current MTU - Maximum Transfer Unit ( maximum udp packet size without fragmentation ) - /// - public int Mtu { get { return _mtu; } } + } - /// - /// Delta with remote time in ticks (not accurate) - /// positive - remote time > our time - /// - public long RemoteTimeDelta + internal NetPeer(NetManager netManager, IPEndPoint remoteEndPoint, int id, byte connectNum, ReadOnlySpan connectData) : base(netManager, remoteEndPoint, id, connectNum, connectData) { - get { return _remoteDelta; } + _channels = new BaseChannel[netManager.ChannelsCount * NetConstants.ChannelTypeCount]; } - /// - /// Remote UTC time (not accurate) - /// - public DateTime RemoteUtcTime + internal NetPeer(NetManager netManager, LiteConnectionRequest request, int id) : base(netManager, request, id) { - get { return new DateTime(DateTime.UtcNow.Ticks + _remoteDelta); } + _channels = new BaseChannel[netManager.ChannelsCount * NetConstants.ChannelTypeCount]; } /// - /// Time since last packet received (including internal library packets) - /// - public int TimeSinceLastPacket { get { return _timeSinceLastPacket; } } - - /// - /// Peer parent NetManager + /// Send data to peer /// - public NetManager NetManager { get { return _netManager; } } - - internal double ResendDelay { get { return _resendDelay; } } - - /// - /// Application defined object containing data about the connection - /// - public object Tag; + /// DataWriter with data + /// Number of channel (from 0 to channelsCount - 1) + /// Send options (reliable, unreliable, etc.) + /// + /// If size exceeds maximum limit: + /// MTU - headerSize bytes for Unreliable + /// Fragment count exceeded ushort.MaxValue + /// + public void Send(NetDataWriter dataWriter, byte channelNumber, DeliveryMethod deliveryMethod) => + SendInternal(dataWriter.AsReadOnlySpan(), channelNumber, deliveryMethod, null); /// - /// Statistics of peer connection + /// Send data to peer with delivery event called /// - public readonly NetStatistics Statistics; - - //incoming connection constructor - internal NetPeer(NetManager netManager, IPEndPoint remoteEndPoint, int id) - { - Id = id; - Statistics = new NetStatistics(); - _packetPool = netManager.NetPacketPool; - _netManager = netManager; - _remoteEndPoint = remoteEndPoint; - _connectionState = ConnectionState.Incoming; - _mergeData = new NetPacket(PacketProperty.Merged, NetConstants.MaxPacketSize); - _pongPacket = new NetPacket(PacketProperty.Pong, 0); - _pingPacket = new NetPacket(PacketProperty.Ping, 0) {Sequence = 1}; - - _unreliableChannel = new SimpleChannel(this); - _headChannel = _unreliableChannel; - _holdedFragments = new Dictionary(); - - _channelsCount = netManager.ChannelsCount; - _channelsTotalCount = (byte)(_channelsCount * 4); - _channels = new BaseChannel[_channelsTotalCount]; - } - - private BaseChannel CreateChannel(byte idx) - { - BaseChannel newChannel = _channels[idx]; - if (newChannel != null) - return newChannel; - switch (NetConstants.ChannelIdToDeliveryMethod(idx, _channelsCount)) - { - case DeliveryMethod.ReliableUnordered: - newChannel = new ReliableChannel(this, false, idx); - break; - case DeliveryMethod.Sequenced: - newChannel = new SequencedChannel(this, false, idx); - break; - case DeliveryMethod.ReliableOrdered: - newChannel = new ReliableChannel(this, true, idx); - break; - case DeliveryMethod.ReliableSequenced: - newChannel = new SequencedChannel(this, true, idx); - break; - } - _channels[idx] = newChannel; - newChannel.Next = _headChannel; - _headChannel = newChannel; - return newChannel; - } - - //"Connect to" constructor - internal NetPeer(NetManager netManager, IPEndPoint remoteEndPoint, int id, byte connectNum, NetDataWriter connectData) - : this(netManager, remoteEndPoint, id) - { - _connectTime = DateTime.UtcNow.Ticks; - _connectionState = ConnectionState.Outcoming; - ConnectionNum = connectNum; - - //Make initial packet - _connectRequestPacket = NetConnectRequestPacket.Make(connectData, _connectTime); - _connectRequestPacket.ConnectionNumber = connectNum; - - //Send request - _netManager.SendRaw(_connectRequestPacket, _remoteEndPoint); - - NetDebug.Write(NetLogLevel.Trace, "[CC] ConnectId: {0}, ConnectNum: {1}", _connectTime, connectNum); - } - - //"Accept" incoming constructor - internal void Accept(long connectId, byte connectNum) - { - _connectTime = connectId; - _connectionState = ConnectionState.Connected; - ConnectionNum = connectNum; - - //Make initial packet - _connectAcceptPacket = NetConnectAcceptPacket.Make(_connectTime, connectNum, false); - //Send - _netManager.SendRaw(_connectAcceptPacket, _remoteEndPoint); - - NetDebug.Write(NetLogLevel.Trace, "[CC] ConnectId: {0}", _connectTime); - } - - internal bool ProcessConnectAccept(NetConnectAcceptPacket packet) + /// Data + /// Number of channel (from 0 to channelsCount - 1) + /// Delivery method (reliable, unreliable, etc.) + /// User data that will be received in DeliveryEvent + /// + /// If you trying to send unreliable packet type + /// + public void SendWithDeliveryEvent(byte[] data, byte channelNumber, DeliveryMethod deliveryMethod, object userData) { - if (_connectionState != ConnectionState.Outcoming) - return false; - - //check connection id - if (packet.ConnectionId != _connectTime) - { - NetDebug.Write(NetLogLevel.Trace, "[NC] Invalid connectId: {0}", _connectTime); - return false; - } - //check connect num - ConnectionNum = packet.ConnectionNumber; - - NetDebug.Write(NetLogLevel.Trace, "[NC] Received connection accept"); - _timeSinceLastPacket = 0; - _connectionState = ConnectionState.Connected; - return true; + if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) + throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); + SendInternal(new ReadOnlySpan(data, 0, data.Length), channelNumber, deliveryMethod, userData); } /// - /// Gets maximum size of packet that will be not fragmented. + /// Send data to peer with delivery event called /// - /// Type of packet that you want send - /// size in bytes - public int GetMaxSinglePacketSize(DeliveryMethod options) + /// Data + /// Start of data + /// Length of data + /// Number of channel (from 0 to channelsCount - 1) + /// Delivery method (reliable, unreliable, etc.) + /// User data that will be received in DeliveryEvent + /// + /// If you trying to send unreliable packet type + /// + public void SendWithDeliveryEvent(byte[] data, int start, int length, byte channelNumber, DeliveryMethod deliveryMethod, object userData) { - return _mtu - NetPacket.GetHeaderSize(options == DeliveryMethod.Unreliable ? PacketProperty.Unreliable : PacketProperty.Channeled); + if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) + throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); + SendInternal(new ReadOnlySpan(data, start, length), channelNumber, deliveryMethod, userData); } /// - /// Send data to peer (channel - 0) + /// Send data to peer with delivery event called /// - /// Data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue + /// Data + /// Number of channel (from 0 to channelsCount - 1) + /// Delivery method (reliable, unreliable, etc.) + /// User data that will be received in DeliveryEvent + /// + /// If you trying to send unreliable packet type /// - public void Send(byte[] data, DeliveryMethod options) + public void SendWithDeliveryEvent(NetDataWriter dataWriter, byte channelNumber, DeliveryMethod deliveryMethod, object userData) { - Send(data, 0, data.Length, 0, options); + if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) + throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); + SendInternal(dataWriter.AsReadOnlySpan(), channelNumber, deliveryMethod, userData); } /// - /// Send data to peer (channel - 0) + /// Send data to peer with delivery event called /// - /// DataWriter with data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue + /// Data + /// Number of channel (from 0 to channelsCount - 1) + /// Delivery method (reliable, unreliable, etc.) + /// User data that will be received in DeliveryEvent + /// + /// If you trying to send unreliable packet type /// - public void Send(NetDataWriter dataWriter, DeliveryMethod options) + public void SendWithDeliveryEvent(ReadOnlySpan data, byte channelNumber, DeliveryMethod deliveryMethod, object userData) { - Send(dataWriter.Data, 0, dataWriter.Length, 0, options); + if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) + throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); + SendInternal(data, channelNumber, deliveryMethod, userData); } /// - /// Send data to peer (channel - 0) + /// Create temporary packet (maximum size MTU - headerSize) to send later without additional copies /// - /// Data - /// Start of data - /// Length of data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(byte[] data, int start, int length, DeliveryMethod options) + /// Delivery method (reliable, unreliable, etc.) + /// Number of channel (from 0 to channelsCount - 1) + /// PooledPacket that you can use to write data starting from UserDataOffset + public PooledPacket CreatePacketFromPool(DeliveryMethod deliveryMethod, byte channelNumber) { - Send(data, start, length, 0, options); + //multithreaded variable + int mtu = Mtu; + var packet = NetManager.PoolGetPacket(mtu); + if (deliveryMethod == DeliveryMethod.Unreliable) + { + packet.Property = PacketProperty.Unreliable; + return new PooledPacket(packet, mtu, 0); + } + else + { + packet.Property = PacketProperty.Channeled; + return new PooledPacket(packet, mtu, (byte)(channelNumber * NetConstants.ChannelTypeCount + (byte)deliveryMethod)); + } } /// @@ -355,32 +143,28 @@ public void Send(byte[] data, int start, int length, DeliveryMethod options) /// /// Data /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) + /// Send options (reliable, unreliable, etc.) /// /// If size exceeds maximum limit: /// MTU - headerSize bytes for Unreliable /// Fragment count exceeded ushort.MaxValue /// - public void Send(byte[] data, byte channelNumber, DeliveryMethod options) - { - Send(data, 0, data.Length, channelNumber, options); - } + public void Send(byte[] data, byte channelNumber, DeliveryMethod deliveryMethod) => + SendInternal(new ReadOnlySpan(data, 0, data.Length), channelNumber, deliveryMethod, null); /// /// Send data to peer /// - /// DataWriter with data + /// Data /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) + /// Send options (reliable, unreliable, etc.) /// /// If size exceeds maximum limit: /// MTU - headerSize bytes for Unreliable /// Fragment count exceeded ushort.MaxValue /// - public void Send(NetDataWriter dataWriter, byte channelNumber, DeliveryMethod options) - { - Send(dataWriter.Data, 0, dataWriter.Length, channelNumber, options); - } + public void Send(ReadOnlySpan data, byte channelNumber, DeliveryMethod deliveryMethod) => + SendInternal(data, channelNumber, deliveryMethod, null); /// /// Send data to peer @@ -389,644 +173,95 @@ public void Send(NetDataWriter dataWriter, byte channelNumber, DeliveryMethod op /// Start of data /// Length of data /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) + /// Delivery method (reliable, unreliable, etc.) /// /// If size exceeds maximum limit: /// MTU - headerSize bytes for Unreliable /// Fragment count exceeded ushort.MaxValue /// - public void Send(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options) - { - if (_connectionState == ConnectionState.ShutdownRequested || - _connectionState == ConnectionState.Disconnected) - return; - if (channelNumber >= _channelsTotalCount) - return; - - //Select channel - PacketProperty property; - BaseChannel channel; - - if (options == DeliveryMethod.Unreliable) - { - property = PacketProperty.Unreliable; - channel = _unreliableChannel; - } - else - { - property = PacketProperty.Channeled; - channel = CreateChannel(NetConstants.ChannelNumberToId(options, channelNumber, _channelsCount)); - } - - //Prepare - NetDebug.Write("[RS]Packet: " + property); - - //Check fragmentation - int headerSize = NetPacket.GetHeaderSize(property); - //Save mtu for multithread - int mtu = _mtu; - if (length + headerSize > mtu) - { - //if cannot be fragmented - if (options != DeliveryMethod.ReliableOrdered && options != DeliveryMethod.ReliableUnordered) - throw new TooBigPacketException("Unreliable packet size exceeded maximum of " + (_mtu - headerSize) + " bytes"); - - int packetFullSize = mtu - headerSize; - int packetDataSize = packetFullSize - NetConstants.FragmentHeaderSize; - - int fullPacketsCount = length / packetDataSize; - int lastPacketSize = length % packetDataSize; - int totalPackets = fullPacketsCount + (lastPacketSize == 0 ? 0 : 1); - - NetDebug.Write("FragmentSend:\n" + - " MTU: {0}\n" + - " headerSize: {1}\n" + - " packetFullSize: {2}\n" + - " packetDataSize: {3}\n" + - " fullPacketsCount: {4}\n" + - " lastPacketSize: {5}\n" + - " totalPackets: {6}", - mtu, headerSize, packetFullSize, packetDataSize, fullPacketsCount, lastPacketSize, totalPackets); + public void Send(byte[] data, int start, int length, byte channelNumber, DeliveryMethod deliveryMethod) => + SendInternal(new ReadOnlySpan(data, start, length), channelNumber, deliveryMethod, null); - if (totalPackets > ushort.MaxValue) - { - throw new TooBigPacketException("Data was split in " + totalPackets + " fragments, which exceeds " + ushort.MaxValue); - } - - int dataOffset = headerSize + NetConstants.FragmentHeaderSize; - - lock (_sendLock) - { - for (ushort i = 0; i < fullPacketsCount; i++) - { - NetPacket p = _packetPool.GetWithProperty(property, packetFullSize); - p.FragmentId = _fragmentId; - p.FragmentPart = i; - p.FragmentsTotal = (ushort)totalPackets; - p.MarkFragmented(); - Buffer.BlockCopy(data, i * packetDataSize, p.RawData, dataOffset, packetDataSize); - channel.AddToQueue(p); - } - if (lastPacketSize > 0) - { - NetPacket p = _packetPool.GetWithProperty(property, lastPacketSize + NetConstants.FragmentHeaderSize); - p.FragmentId = _fragmentId; - p.FragmentPart = (ushort)fullPacketsCount; //last - p.FragmentsTotal = (ushort)totalPackets; - p.MarkFragmented(); - Buffer.BlockCopy(data, fullPacketsCount * packetDataSize, p.RawData, dataOffset, lastPacketSize); - channel.AddToQueue(p); - } - _fragmentId++; - } - return; - } - - //Else just send - NetPacket packet = _packetPool.GetWithData(property, data, start, length); - channel.AddToQueue(packet); - } - - public void Disconnect(byte[] data) - { - _netManager.DisconnectPeer(this, data); - } - - public void Disconnect(NetDataWriter writer) - { - _netManager.DisconnectPeer(this, writer); - } - - public void Disconnect(byte[] data, int start, int count) - { - _netManager.DisconnectPeer(this, data, start, count); - } - - public void Disconnect() - { - _netManager.DisconnectPeer(this); - } - - internal DisconnectResult ProcessDisconnect(NetPacket packet) - { - if ((_connectionState == ConnectionState.Connected || _connectionState == ConnectionState.Outcoming) && - packet.Size >= 9 && - BitConverter.ToInt64(packet.RawData, 1) == _connectTime && - packet.ConnectionNumber == _connectNum) - { - return _connectionState == ConnectionState.Connected - ? DisconnectResult.Disconnect - : DisconnectResult.Reject; - } - return DisconnectResult.None; - } - - internal void Reject(long connectionId, byte connectionNumber, byte[] data, int start, int length) - { - _connectTime = connectionId; - _connectNum = connectionNumber; - Shutdown(data, start, length, false); - } - - internal bool Shutdown(byte[] data, int start, int length, bool force) - { - lock (_shutdownLock) - { - //trying to shutdown already disconnected - if (_connectionState == ConnectionState.Disconnected || - _connectionState == ConnectionState.ShutdownRequested) - { - return false; - } - - //don't send anything - if (force) - { - _connectionState = ConnectionState.Disconnected; - return true; - } - - //reset time for reconnect protection - _timeSinceLastPacket = 0; - - //send shutdown packet - _shutdownPacket = new NetPacket(PacketProperty.Disconnect, length); - _shutdownPacket.ConnectionNumber = _connectNum; - FastBitConverter.GetBytes(_shutdownPacket.RawData, 1, _connectTime); - if (_shutdownPacket.Size >= _mtu) - { - //Drop additional data - NetDebug.WriteError("[Peer] Disconnect additional data size more than MTU - 8!"); - } - else if (data != null && length > 0) - { - Buffer.BlockCopy(data, start, _shutdownPacket.RawData, 9, length); - } - _connectionState = ConnectionState.ShutdownRequested; - NetDebug.Write("[Peer] Send disconnect"); - _netManager.SendRaw(_shutdownPacket, _remoteEndPoint); - return true; - } - } - - private void UpdateRoundTripTime(int roundTripTime) + /// + /// Returns packets count in queue for reliable channel + /// + /// number of channel 0-63 + /// type of channel ReliableOrdered or ReliableUnordered + /// packets count in channel queue + public int GetPacketsCountInReliableQueue(byte channelNumber, bool ordered) { - _rtt += roundTripTime; - _rttCount++; - _avgRtt = _rtt/_rttCount; - _resendDelay = 25.0 + _avgRtt * 2.1; // 25 ms + double rtt + int idx = channelNumber * NetConstants.ChannelTypeCount + + (byte) (ordered ? DeliveryMethod.ReliableOrdered : DeliveryMethod.ReliableUnordered); + return ((ReliableChannel)_channels[idx])?.PacketsInQueue ?? 0; } - internal void AddIncomingPacket(NetPacket p) - { - if (p.IsFragmented) - { - NetDebug.Write("Fragment. Id: {0}, Part: {1}, Total: {2}", p.FragmentId, p.FragmentPart, p.FragmentsTotal); - //Get needed array from dictionary - ushort packetFragId = p.FragmentId; - IncomingFragments incomingFragments; - if (!_holdedFragments.TryGetValue(packetFragId, out incomingFragments)) - { - incomingFragments = new IncomingFragments - { - Fragments = new NetPacket[p.FragmentsTotal] - }; - _holdedFragments.Add(packetFragId, incomingFragments); - } - - //Cache - var fragments = incomingFragments.Fragments; - - //Error check - if (p.FragmentPart >= fragments.Length || fragments[p.FragmentPart] != null) - { - _packetPool.Recycle(p); - NetDebug.WriteError("Invalid fragment packet"); - return; - } - //Fill array - fragments[p.FragmentPart] = p; - - //Increase received fragments count - incomingFragments.ReceivedCount++; - - //Increase total size - int dataOffset = p.GetHeaderSize() + NetConstants.FragmentHeaderSize; - incomingFragments.TotalSize += p.Size - dataOffset; - - //Check for finish - if (incomingFragments.ReceivedCount != fragments.Length) - return; - - NetDebug.Write("Received all fragments!"); - NetPacket resultingPacket = _packetPool.GetWithProperty( p.Property, incomingFragments.TotalSize ); - - int resultingPacketOffset = resultingPacket.GetHeaderSize(); - int firstFragmentSize = fragments[0].Size - dataOffset; - for (int i = 0; i < incomingFragments.ReceivedCount; i++) - { - //Create resulting big packet - int fragmentSize = fragments[i].Size - dataOffset; - Buffer.BlockCopy( - fragments[i].RawData, - dataOffset, - resultingPacket.RawData, - resultingPacketOffset + firstFragmentSize * i, - fragmentSize); - - //Free memory - _packetPool.Recycle(fragments[i]); - fragments[i] = null; - } - - //Send to process - _netManager.ReceiveFromPeer(resultingPacket, _remoteEndPoint); - - //Clear memory - _holdedFragments.Remove(packetFragId); - } - else //Just simple packet - { - _netManager.ReceiveFromPeer(p, _remoteEndPoint); - } - } + /// + /// Returns packets count in queue for reliable channel 0 + /// + /// type of channel ReliableOrdered or ReliableUnordered + /// packets count in channel queue + public new int GetPacketsCountInReliableQueue(bool ordered) => + GetPacketsCountInReliableQueue(0, ordered); - private void ProcessMtuPacket(NetPacket packet) + protected override void UpdateChannels() { - //header + int - if (packet.Size < NetConstants.PossibleMtu[0]) - return; - - //first stage check (mtu check and mtu ok) - int receivedMtu = BitConverter.ToInt32(packet.RawData, 1); - int endMtuCheck = BitConverter.ToInt32(packet.RawData, packet.Size - 4); - if (receivedMtu != packet.Size || receivedMtu != endMtuCheck || receivedMtu > NetConstants.MaxPacketSize) - { - NetDebug.WriteError("[MTU] Broken packet. RMTU {0}, EMTU {1}, PSIZE {2}", receivedMtu, endMtuCheck, packet.Size); + //Pending send + if (_channelSendQueue.IsEmpty) return; - } - if (packet.Property == PacketProperty.MtuCheck) + int count = _channelSendQueue.Count; + while (count-- > 0) { - _mtuCheckAttempts = 0; - NetDebug.Write("[MTU] check. send back: " + receivedMtu); - packet.Property = PacketProperty.MtuOk; - _netManager.SendRawAndRecycle(packet, _remoteEndPoint); - } - else if(receivedMtu > _mtu && !_finishMtu) //MtuOk - { - //invalid packet - if (receivedMtu != NetConstants.PossibleMtu[_mtuIdx + 1]) - return; - - lock (_mtuMutex) + if (!_channelSendQueue.TryDequeue(out var channel)) + break; + if (channel.SendAndCheckQueue()) { - _mtuIdx++; - _mtu = receivedMtu; + // still has something to send, re-add it to the send queue + _channelSendQueue.Enqueue(channel); } - //if maxed - finish. - if (_mtuIdx == NetConstants.PossibleMtu.Length - 1) - _finishMtu = true; - - NetDebug.Write("[MTU] ok. Increase to: " + _mtu); } } - private void UpdateMtuLogic(int deltaTime) + internal override void ProcessChanneled(NetPacket packet) { - if (_finishMtu) - return; - - _mtuCheckTimer += deltaTime; - if (_mtuCheckTimer < MtuCheckDelay) - return; - - _mtuCheckTimer = 0; - _mtuCheckAttempts++; - if (_mtuCheckAttempts >= MaxMtuCheckAttempts) + if (packet.ChannelId >= _channels.Length) { - _finishMtu = true; + NetManager.PoolRecycle(packet); return; } - - lock (_mtuMutex) - { - if (_mtuIdx >= NetConstants.PossibleMtu.Length - 1) - return; - - //Send increased packet - int newMtu = NetConstants.PossibleMtu[_mtuIdx + 1]; - var p = _packetPool.GetWithProperty(PacketProperty.MtuCheck, newMtu - NetConstants.HeaderSize); - FastBitConverter.GetBytes(p.RawData, 1, newMtu); //place into start - FastBitConverter.GetBytes(p.RawData, p.Size - 4, newMtu);//and end of packet - - //Must check result for MTU fix - if (_netManager.SendRawAndRecycle(p, _remoteEndPoint) <= 0) - _finishMtu = true; - } + var channel = _channels[packet.ChannelId] ?? (packet.Property == PacketProperty.Ack ? null : CreateChannel(packet.ChannelId)); + if (channel != null && !channel.ProcessPacket(packet)) + NetManager.PoolRecycle(packet); } - internal ConnectRequestResult ProcessConnectRequest(NetConnectRequestPacket connRequest) - { - //current or new request - switch (_connectionState) - { - //P2P case or just ID update - case ConnectionState.Outcoming: - case ConnectionState.Incoming: - //change connect id if newer - if (connRequest.ConnectionTime >= _connectTime) - { - //Change connect id - _connectTime = connRequest.ConnectionTime; - ConnectionNum = connRequest.ConnectionNumber; - } - return _connectionState == ConnectionState.Outcoming - ? ConnectRequestResult.P2PConnection - : ConnectRequestResult.None; - - case ConnectionState.Connected: - //Old connect request - if (connRequest.ConnectionTime == _connectTime) - { - //just reply accept - _netManager.SendRaw(_connectAcceptPacket, _remoteEndPoint); - } - //New connect request - else if (connRequest.ConnectionTime > _connectTime) - { - return ConnectRequestResult.Reconnection; - } - break; + internal override void AddToReliableChannelSendQueue(BaseChannel channel) => + _channelSendQueue.Enqueue(channel); - case ConnectionState.Disconnected: - case ConnectionState.ShutdownRequested: - if (connRequest.ConnectionTime >= _connectTime) - { - return ConnectRequestResult.NewConnection; - } - break; - } - return ConnectRequestResult.None; - } - - //Process incoming packet - internal void ProcessPacket(NetPacket packet) + internal override BaseChannel CreateChannel(byte idx) { - //not initialized - if (_connectionState == ConnectionState.Incoming) - { - _packetPool.Recycle(packet); - return; - } - if (packet.ConnectionNumber != _connectNum && packet.Property != PacketProperty.ShutdownOk) //without connectionNum - { - NetDebug.Write(NetLogLevel.Trace, "[RR]Old packet"); - _packetPool.Recycle(packet); - return; - } - _timeSinceLastPacket = 0; - - NetDebug.Write("[RR]PacketProperty: {0}", packet.Property); - switch (packet.Property) + var newChannel = _channels[idx]; + if (newChannel != null) + return newChannel; + switch ((DeliveryMethod)(idx % NetConstants.ChannelTypeCount)) { - case PacketProperty.Merged: - int pos = NetConstants.HeaderSize; - while (pos < packet.Size) - { - ushort size = BitConverter.ToUInt16(packet.RawData, pos); - pos += 2; - NetPacket mergedPacket = _packetPool.GetPacket(size, false); - if (!mergedPacket.FromBytes(packet.RawData, pos, size)) - { - _packetPool.Recycle(packet); - break; - } - pos += size; - ProcessPacket(mergedPacket); - } - break; - //If we get ping, send pong - case PacketProperty.Ping: - if (NetUtils.RelativeSequenceNumber(packet.Sequence, _pongPacket.Sequence) > 0) - { - NetDebug.Write("[PP]Ping receive, send pong"); - FastBitConverter.GetBytes(_pongPacket.RawData, 3, DateTime.UtcNow.Ticks); - _pongPacket.Sequence = packet.Sequence; - _netManager.SendRaw(_pongPacket, _remoteEndPoint); - } - _packetPool.Recycle(packet); - break; - - //If we get pong, calculate ping time and rtt - case PacketProperty.Pong: - if (packet.Sequence == _pingPacket.Sequence) - { - _pingTimer.Stop(); - int elapsedMs = (int)_pingTimer.ElapsedMilliseconds; - _remoteDelta = BitConverter.ToInt64(packet.RawData, 3) + (elapsedMs * TimeSpan.TicksPerMillisecond ) / 2 - DateTime.UtcNow.Ticks; - UpdateRoundTripTime(elapsedMs); - _netManager.ConnectionLatencyUpdated(this, elapsedMs / 2); - NetDebug.Write("[PP]Ping: {0} - {1} - {2}", packet.Sequence, elapsedMs, _remoteDelta); - } - _packetPool.Recycle(packet); - break; - - case PacketProperty.Ack: - case PacketProperty.Channeled: - if (packet.ChannelId > _channelsTotalCount) - { - _packetPool.Recycle(packet); - break; - } - var channel = _channels[packet.ChannelId] ?? (packet.Property == PacketProperty.Ack ? null : CreateChannel(packet.ChannelId)); - if (channel != null) - { - if (!channel.ProcessPacket(packet)) - _packetPool.Recycle(packet); - } + case DeliveryMethod.ReliableUnordered: + newChannel = new ReliableChannel(this, false, idx); break; - - //Simple packet without acks - case PacketProperty.Unreliable: - AddIncomingPacket(packet); - return; - - case PacketProperty.MtuCheck: - case PacketProperty.MtuOk: - ProcessMtuPacket(packet); + case DeliveryMethod.Sequenced: + newChannel = new SequencedChannel(this, false, idx); break; - - case PacketProperty.ShutdownOk: - if(_connectionState == ConnectionState.ShutdownRequested) - _connectionState = ConnectionState.Disconnected; - _packetPool.Recycle(packet); - break; - - default: - NetDebug.WriteError("Error! Unexpected packet type: " + packet.Property); + case DeliveryMethod.ReliableOrdered: + newChannel = new ReliableChannel(this, true, idx); break; - } - } - - private void SendMerged() - { - if (_mergeCount == 0) - return; - int bytesSent; - if (_mergeCount > 1) - { - NetDebug.Write("[P]Send merged: " + _mergePos + ", count: " + _mergeCount); - bytesSent = _netManager.SendRaw(_mergeData.RawData, 0, NetConstants.HeaderSize + _mergePos, _remoteEndPoint); - } - else - { - //Send without length information and merging - bytesSent = _netManager.SendRaw(_mergeData.RawData, NetConstants.HeaderSize + 2, _mergePos - 2, _remoteEndPoint); - } -#if STATS_ENABLED - Statistics.PacketsSent++; - Statistics.BytesSent += (ulong)bytesSent; -#endif - _mergePos = 0; - _mergeCount = 0; - } - - internal void SendUserData(NetPacket packet) - { - packet.ConnectionNumber = _connectNum; - int mergedPacketSize = NetConstants.HeaderSize + packet.Size + 2; - const int sizeTreshold = 20; - if (mergedPacketSize + sizeTreshold >= _mtu) - { - NetDebug.Write(NetLogLevel.Trace, "[P]SendingPacket: " + packet.Property); - int bytesSent = _netManager.SendRaw(packet, _remoteEndPoint); -#if STATS_ENABLED - Statistics.PacketsSent++; - Statistics.BytesSent += (ulong)bytesSent; -#endif - return; - } - if (_mergePos + mergedPacketSize > _mtu) - SendMerged(); - - FastBitConverter.GetBytes(_mergeData.RawData, _mergePos + NetConstants.HeaderSize, (ushort)packet.Size); - Buffer.BlockCopy(packet.RawData, 0, _mergeData.RawData, _mergePos + NetConstants.HeaderSize + 2, packet.Size); - _mergePos += packet.Size + 2; - _mergeCount++; - //DebugWriteForce("Merged: " + _mergePos + "/" + (_mtu - 2) + ", count: " + _mergeCount); - } - - /// - /// Flush all queued packets - /// - public void Flush() - { - if (_connectionState != ConnectionState.Connected) - return; - lock (_flushLock) - { - BaseChannel currentChannel = _headChannel; - while (currentChannel != null) - { - currentChannel.SendNextPackets(); - currentChannel = currentChannel.Next; - } - SendMerged(); - } - } - - internal void Update(int deltaTime) - { - _timeSinceLastPacket += deltaTime; - switch (_connectionState) - { - case ConnectionState.Connected: - if (_timeSinceLastPacket > _netManager.DisconnectTimeout) - { - NetDebug.Write( - "[UPDATE] Disconnect by timeout: {0} > {1}", - _timeSinceLastPacket, - _netManager.DisconnectTimeout); - _netManager.DisconnectPeerForce(this, DisconnectReason.Timeout, 0, null); - return; - } + case DeliveryMethod.ReliableSequenced: + newChannel = new SequencedChannel(this, true, idx); break; - - case ConnectionState.ShutdownRequested: - if (_timeSinceLastPacket > _netManager.DisconnectTimeout) - { - _connectionState = ConnectionState.Disconnected; - } - else - { - _shutdownTimer += deltaTime; - if (_shutdownTimer >= ShutdownDelay) - { - _shutdownTimer = 0; - _netManager.SendRaw(_shutdownPacket, _remoteEndPoint); - } - } - return; - - case ConnectionState.Outcoming: - _connectTimer += deltaTime; - if (_connectTimer > _netManager.ReconnectDelay) - { - _connectTimer = 0; - _connectAttempts++; - if (_connectAttempts > _netManager.MaxConnectAttempts) - { - _netManager.DisconnectPeerForce(this, DisconnectReason.ConnectionFailed, 0, null); - return; - } - - //else send connect again - _netManager.SendRaw(_connectRequestPacket, _remoteEndPoint); - } - return; - - case ConnectionState.Disconnected: - case ConnectionState.Incoming: - return; - } - - //Send ping - _pingSendTimer += deltaTime; - if (_pingSendTimer >= _netManager.PingInterval) - { - NetDebug.Write("[PP] Send ping..."); - //reset timer - _pingSendTimer = 0; - //send ping - _pingPacket.Sequence++; - //ping timeout - if (_pingTimer.IsRunning) - UpdateRoundTripTime((int)_pingTimer.ElapsedMilliseconds); - _pingTimer.Reset(); - _pingTimer.Start(); - _netManager.SendRaw(_pingPacket, _remoteEndPoint); } + var prevChannel = Interlocked.CompareExchange(ref _channels[idx], newChannel, null); + if (prevChannel != null) + return prevChannel; - //RTT - round trip time - _rttResetTimer += deltaTime; - if (_rttResetTimer >= _netManager.PingInterval * 3) - { - _rttResetTimer = 0; - _rtt = _avgRtt; - _rttCount = 1; - } - - UpdateMtuLogic(deltaTime); - - //Pending send - Flush(); - } - - //For channels - internal void Recycle(NetPacket packet) - { - _packetPool.Recycle(packet); + return newChannel; } } } diff --git a/LiteNetLib/NetSocket.cs b/LiteNetLib/NetSocket.cs deleted file mode 100644 index 0b1e6db6..00000000 --- a/LiteNetLib/NetSocket.cs +++ /dev/null @@ -1,324 +0,0 @@ -#if UNITY_4 || UNITY_5 || UNITY_5_3_OR_NEWER -#define UNITY -#endif -#if NETSTANDARD2_0 || NETCOREAPP2_0 -using System.Runtime.InteropServices; -#endif - -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading; - -namespace LiteNetLib -{ - internal interface INetSocketListener - { - void OnMessageReceived(byte[] data, int length, SocketError errorCode, IPEndPoint remoteEndPoint); - } - - internal sealed class NetSocket - { - public const int ReceivePollingTime = 1000000; //1 second - private Socket _udpSocketv4; - private Socket _udpSocketv6; - private Thread _threadv4; - private Thread _threadv6; - private volatile bool _running; - private readonly INetSocketListener _listener; - private static readonly IPAddress MulticastAddressV6 = IPAddress.Parse("FF02:0:0:0:0:0:0:1"); - internal static readonly bool IPv6Support; - - public int LocalPort { get; private set; } - - public short Ttl - { - get { return _udpSocketv4.Ttl; } - set { _udpSocketv4.Ttl = value; } - } - - static NetSocket() - { -#if DISABLE_IPV6 || (!UNITY_EDITOR && ENABLE_IL2CPP && !UNITY_2018_3_OR_NEWER) - IPv6Support = false; -#elif !UNITY_2019_1_OR_NEWER && !UNITY_2018_4_OR_NEWER && (!UNITY_EDITOR && ENABLE_IL2CPP && UNITY_2018_3_OR_NEWER) - string version = UnityEngine.Application.unityVersion; - IPv6Support = Socket.OSSupportsIPv6 && int.Parse(version.Remove(version.IndexOf('f')).Split('.')[2]) >= 6; -#elif UNITY_2018_2_OR_NEWER - IPv6Support = Socket.OSSupportsIPv6; -#elif UNITY -#pragma warning disable 618 - IPv6Support = Socket.SupportsIPv6; -#pragma warning restore 618 -#else - IPv6Support = Socket.OSSupportsIPv6; -#endif - } - - public NetSocket(INetSocketListener listener) - { - _listener = listener; - } - - private void ReceiveLogic(object state) - { - Socket socket = (Socket)state; - EndPoint bufferEndPoint = new IPEndPoint(socket.AddressFamily == AddressFamily.InterNetwork ? IPAddress.Any : IPAddress.IPv6Any, 0); - byte[] receiveBuffer = new byte[NetConstants.MaxPacketSize]; - - while (_running) - { - int result; - - //Reading data - try - { - if (socket.Available == 0 && !socket.Poll(ReceivePollingTime, SelectMode.SelectRead)) - continue; - result = socket.ReceiveFrom(receiveBuffer, 0, receiveBuffer.Length, SocketFlags.None, - ref bufferEndPoint); - } - catch (SocketException ex) - { - switch (ex.SocketErrorCode) - { - case SocketError.Interrupted: - case SocketError.NotSocket: - return; - case SocketError.ConnectionReset: - case SocketError.MessageSize: - case SocketError.TimedOut: - NetDebug.Write(NetLogLevel.Trace, "[R]Ignored error: {0} - {1}", - (int) ex.SocketErrorCode, ex.ToString()); - break; - default: - NetDebug.WriteError("[R]Error code: {0} - {1}", (int) ex.SocketErrorCode, - ex.ToString()); - _listener.OnMessageReceived(null, 0, ex.SocketErrorCode, (IPEndPoint) bufferEndPoint); - break; - } - - continue; - } - catch (ObjectDisposedException) - { - return; - } - - //All ok! - NetDebug.Write(NetLogLevel.Trace, "[R]Received data from {0}, result: {1}", bufferEndPoint.ToString(), result); - _listener.OnMessageReceived(receiveBuffer, result, 0, (IPEndPoint)bufferEndPoint); - } - } - - public bool Bind(IPAddress addressIPv4, IPAddress addressIPv6, int port, bool reuseAddress, bool ipv6) - { - _udpSocketv4 = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - if (!BindSocket(_udpSocketv4, new IPEndPoint(addressIPv4, port), reuseAddress)) - return false; - LocalPort = ((IPEndPoint) _udpSocketv4.LocalEndPoint).Port; - _running = true; - _threadv4 = new Thread(ReceiveLogic); - _threadv4.Name = "SocketThreadv4(" + LocalPort + ")"; - _threadv4.IsBackground = true; - _threadv4.Start(_udpSocketv4); - - //Check IPv6 support - if (!IPv6Support || !ipv6) - return true; - - _udpSocketv6 = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - //Use one port for two sockets - if (BindSocket(_udpSocketv6, new IPEndPoint(addressIPv6, LocalPort), reuseAddress)) - { - try - { -#if !UNITY - _udpSocketv6.SetSocketOption( - SocketOptionLevel.IPv6, - SocketOptionName.AddMembership, - new IPv6MulticastOption(MulticastAddressV6)); -#endif - } - catch(Exception) - { - // Unity3d throws exception - ignored - } - - _threadv6 = new Thread(ReceiveLogic); - _threadv6.Name = "SocketThreadv6(" + LocalPort + ")"; - _threadv6.IsBackground = true; - _threadv6.Start(_udpSocketv6); - } - - return true; - } - - private bool BindSocket(Socket socket, IPEndPoint ep, bool reuseAddress) - { - //Setup socket - socket.ReceiveTimeout = 500; - socket.SendTimeout = 500; - socket.ReceiveBufferSize = NetConstants.SocketBufferSize; - socket.SendBufferSize = NetConstants.SocketBufferSize; - try - { - socket.ExclusiveAddressUse = !reuseAddress; - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, reuseAddress); - } - catch - { - //Unity with IL2CPP throws an exception here, it doesn't matter in most cases so just ignore it - } - if (socket.AddressFamily == AddressFamily.InterNetwork) - { - socket.Ttl = NetConstants.SocketTTL; - -#if NETSTANDARD2_0 || NETCOREAPP2_0 - if(!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) -#endif - try { socket.DontFragment = true; } - catch (SocketException e) - { - NetDebug.WriteError("[B]DontFragment error: {0}", e.SocketErrorCode); - } - - try { socket.EnableBroadcast = true; } - catch (SocketException e) - { - NetDebug.WriteError("[B]Broadcast error: {0}", e.SocketErrorCode); - } - } - - //Bind - try - { - socket.Bind(ep); - NetDebug.Write(NetLogLevel.Trace, "[B]Successfully binded to port: {0}", ((IPEndPoint)socket.LocalEndPoint).Port); - } - catch (SocketException bindException) - { - switch (bindException.SocketErrorCode) - { - //IPv6 bind fix - case SocketError.AddressAlreadyInUse: - if (socket.AddressFamily == AddressFamily.InterNetworkV6) - { - try - { - socket.SetSocketOption(SocketOptionLevel.IPv6, (SocketOptionName)27, true); - socket.Bind(ep); - } - catch (SocketException ex) - { - NetDebug.WriteError("[B]Bind exception: {0}, errorCode: {1}", ex.ToString(), ex.SocketErrorCode); - return false; - } - return true; - } - break; - //hack for iOS (Unity3D) - case SocketError.AddressFamilyNotSupported: - return true; - } - NetDebug.WriteError("[B]Bind exception: {0}, errorCode: {1}", bindException.ToString(), bindException.SocketErrorCode); - return false; - } - return true; - } - - public bool SendBroadcast(byte[] data, int offset, int size, int port) - { - bool broadcastSuccess = false; - bool multicastSuccess = false; - try - { - broadcastSuccess = _udpSocketv4.SendTo( - data, - offset, - size, - SocketFlags.None, - new IPEndPoint(IPAddress.Broadcast, port)) > 0; - - if (_udpSocketv6 != null) - { - multicastSuccess = _udpSocketv6.SendTo( - data, - offset, - size, - SocketFlags.None, - new IPEndPoint(MulticastAddressV6, port)) > 0; - } - } - catch (Exception ex) - { - NetDebug.WriteError("[S][MCAST]" + ex); - return broadcastSuccess; - } - return broadcastSuccess || multicastSuccess; - } - - public int SendTo(byte[] data, int offset, int size, IPEndPoint remoteEndPoint, ref SocketError errorCode) - { - try - { - var socket = _udpSocketv4; - if (remoteEndPoint.AddressFamily == AddressFamily.InterNetworkV6 && IPv6Support) - socket = _udpSocketv6; - int result = socket.SendTo(data, offset, size, SocketFlags.None, remoteEndPoint); - NetDebug.Write(NetLogLevel.Trace, "[S]Send packet to {0}, result: {1}", remoteEndPoint, result); - return result; - } - catch (SocketException ex) - { - switch (ex.SocketErrorCode) - { - case SocketError.NoBufferSpaceAvailable: - case SocketError.Interrupted: - return 0; - case SocketError.MessageSize: //do nothing - break; - default: - NetDebug.WriteError("[S]" + ex); - break; - } - errorCode = ex.SocketErrorCode; - return -1; - } - catch (Exception ex) - { - NetDebug.WriteError("[S]" + ex); - return -1; - } - } - - public void Close() - { - _running = false; - // first close sockets - if (_udpSocketv4 != null) - { - _udpSocketv4.Close(); - _udpSocketv4 = null; - } - if (_udpSocketv6 != null) - { - _udpSocketv6.Close(); - _udpSocketv6 = null; - } - // then join threads - if (_threadv4 != null) - { - if (_threadv4 != Thread.CurrentThread) - _threadv4.Join(); - _threadv4 = null; - } - if (_threadv6 != null) - { - if (_threadv6 != Thread.CurrentThread) - _threadv6.Join(); - _threadv6 = null; - } - } - } -} diff --git a/LiteNetLib/NetStatistics.cs b/LiteNetLib/NetStatistics.cs index e763e7ad..e2d0304c 100644 --- a/LiteNetLib/NetStatistics.cs +++ b/LiteNetLib/NetStatistics.cs @@ -1,28 +1,112 @@ -namespace LiteNetLib +using System.Threading; + +namespace LiteNetLib { + /// + /// Thread-safe counter for network statistics including sent/received packets, bytes, and packet loss. + /// public sealed class NetStatistics { - public ulong PacketsSent; - public ulong PacketsReceived; - public ulong BytesSent; - public ulong BytesReceived; - public ulong PacketLoss; - public ulong PacketLossPercent + private long _packetsSent; + private long _packetsReceived; + private long _bytesSent; + private long _bytesReceived; + private long _packetLoss; + + /// + /// Total number of packets sent. + /// + public long PacketsSent => Interlocked.Read(ref _packetsSent); + + /// + /// Total number of packets received. + /// + public long PacketsReceived => Interlocked.Read(ref _packetsReceived); + + /// + /// Total number of bytes sent. + /// + public long BytesSent => Interlocked.Read(ref _bytesSent); + + /// + /// Total number of bytes received. + /// + public long BytesReceived => Interlocked.Read(ref _bytesReceived); + + /// + /// Total number of packets lost during transmission. + /// + public long PacketLoss => Interlocked.Read(ref _packetLoss); + + /// + /// Percentage of sent packets that were lost. + /// Calculated as (PacketLoss * 100) / PacketsSent. + /// + public long PacketLossPercent { - get { return PacketsSent == 0 ? 0 : PacketLoss * 100 / PacketsSent; } - } + get + { + long sent = PacketsSent, loss = PacketLoss; - public ulong SequencedPacketLoss; + return sent == 0 ? 0 : loss * 100 / sent; + } + } + /// + /// Resets all statistical counters to zero. + /// public void Reset() { - PacketsSent = 0; - PacketsReceived = 0; - BytesSent = 0; - BytesReceived = 0; - PacketLoss = 0; + Interlocked.Exchange(ref _packetsSent, 0); + Interlocked.Exchange(ref _packetsReceived, 0); + Interlocked.Exchange(ref _bytesSent, 0); + Interlocked.Exchange(ref _bytesReceived, 0); + Interlocked.Exchange(ref _packetLoss, 0); } + /// + /// Increments the count of sent packets by one. + /// + public void IncrementPacketsSent() => + Interlocked.Increment(ref _packetsSent); + + /// + /// Increments the count of received packets by one. + /// + public void IncrementPacketsReceived() => + Interlocked.Increment(ref _packetsReceived); + + /// + /// Adds a specific amount to the total bytes sent. + /// + /// Number of bytes to add. + public void AddBytesSent(long bytesSent) => + Interlocked.Add(ref _bytesSent, bytesSent); + + /// + /// Adds a specific amount to the total bytes received. + /// + /// Number of bytes to add. + public void AddBytesReceived(long bytesReceived) => + Interlocked.Add(ref _bytesReceived, bytesReceived); + + /// + /// Increments the count of lost packets by one. + /// + public void IncrementPacketLoss() => + Interlocked.Increment(ref _packetLoss); + + /// + /// Adds a specific amount to the total packet loss count. + /// + /// Number of lost packets to add. + public void AddPacketLoss(long packetLoss) => + Interlocked.Add(ref _packetLoss, packetLoss); + + /// + /// Returns a string representation of the current network statistics. + /// + /// A formatted string containing bytes received/sent, packets received/sent, and loss information. public override string ToString() { return diff --git a/LiteNetLib/NetUtils.cs b/LiteNetLib/NetUtils.cs index 510d8fee..593c2f2a 100644 --- a/LiteNetLib/NetUtils.cs +++ b/LiteNetLib/NetUtils.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Sockets; using System.Net.NetworkInformation; @@ -22,20 +23,35 @@ public enum LocalAddrType /// public static class NetUtils { - public static IPEndPoint MakeEndPoint(string hostStr, int port) - { - return new IPEndPoint(ResolveAddress(hostStr), port); - } + private static readonly NetworkSorter NetworkSorter = new NetworkSorter(); + + /// + /// Creates an from a host string and a port. + /// + /// The host name or IP address string to resolve. + /// The port number for the endpoint. + /// A new instance. + public static IPEndPoint MakeEndPoint(string hostStr, int port) => + new IPEndPoint(ResolveAddress(hostStr), port); + /// + /// Resolves a host string into an . + /// + /// + /// This method handles "localhost" specifically, attempts to parse the string as a direct IP, + /// and falls back to DNS resolution. It prioritizes IPv6 if is enabled. + /// + /// The host name or IP address string (e.g., "127.0.0.1", "localhost", or "google.com"). + /// The resolved . + /// Thrown when the address cannot be resolved or is invalid. public static IPAddress ResolveAddress(string hostStr) { if(hostStr == "localhost") return IPAddress.Loopback; - - IPAddress ipAddress; - if (!IPAddress.TryParse(hostStr, out ipAddress)) + + if (!IPAddress.TryParse(hostStr, out var ipAddress)) { - if (NetSocket.IPv6Support) + if (LiteNetManager.IPv6Support) ipAddress = ResolveAddress(hostStr, AddressFamily.InterNetworkV6); if (ipAddress == null) ipAddress = ResolveAddress(hostStr, AddressFamily.InterNetwork); @@ -46,9 +62,15 @@ public static IPAddress ResolveAddress(string hostStr) return ipAddress; } - private static IPAddress ResolveAddress(string hostStr, AddressFamily addressFamily) + /// + /// Resolves a host string using DNS for a specific . + /// + /// The host name to resolve via DNS. + /// The preferred address family (e.g., or ). + /// The first matching the family, or if no match is found. + public static IPAddress ResolveAddress(string hostStr, AddressFamily addressFamily) { - IPAddress[] addresses = ResolveAddresses(hostStr); + IPAddress[] addresses = Dns.GetHostEntry(hostStr).AddressList; foreach (IPAddress ip in addresses) { if (ip.AddressFamily == addressFamily) @@ -59,23 +81,11 @@ private static IPAddress ResolveAddress(string hostStr, AddressFamily addressFam return null; } - private static IPAddress[] ResolveAddresses(string hostStr) - { -#if NETSTANDARD2_0 || NETCOREAPP2_0 - var hostTask = Dns.GetHostEntryAsync(hostStr); - hostTask.GetAwaiter().GetResult(); - var host = hostTask.Result; -#else - var host = Dns.GetHostEntry(hostStr); -#endif - return host.AddressList; - } - /// /// Get all local ip addresses /// /// type of address (IPv4, IPv6 or both) - /// List with all local ip adresses + /// List with all local ip addresses public static List GetLocalIpList(LocalAddrType addrType) { List targetList = new List(); @@ -88,16 +98,21 @@ public static List GetLocalIpList(LocalAddrType addrType) /// /// result list /// type of address (IPv4, IPv6 or both) - public static void GetLocalIpList(List targetList, LocalAddrType addrType) + public static void GetLocalIpList(IList targetList, LocalAddrType addrType) { bool ipv4 = (addrType & LocalAddrType.IPv4) == LocalAddrType.IPv4; bool ipv6 = (addrType & LocalAddrType.IPv6) == LocalAddrType.IPv6; try { - foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) + // Sort networks interfaces so it prefer Wifi over Cellular networks + // Most cellulars networks seems to be incompatible with NAT Punch + var networks = NetworkInterface.GetAllNetworkInterfaces(); + Array.Sort(networks, NetworkSorter); + + foreach (NetworkInterface ni in networks) { //Skip loopback and disabled network interfaces - if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback || + if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback || ni.OperationalStatus != OperationalStatus.Up) continue; @@ -115,23 +130,24 @@ public static void GetLocalIpList(List targetList, LocalAddrType addrTyp targetList.Add(address.ToString()); } } + + //Fallback mode (unity android) + if (targetList.Count == 0) + { + IPAddress[] addresses = Dns.GetHostEntry(Dns.GetHostName()).AddressList; + foreach (IPAddress ip in addresses) + { + if((ipv4 && ip.AddressFamily == AddressFamily.InterNetwork) || + (ipv6 && ip.AddressFamily == AddressFamily.InterNetworkV6)) + targetList.Add(ip.ToString()); + } + } } catch { //ignored } - //Fallback mode (unity android) - if (targetList.Count == 0) - { - IPAddress[] addresses = ResolveAddresses(Dns.GetHostName()); - foreach (IPAddress ip in addresses) - { - if((ipv4 && ip.AddressFamily == AddressFamily.InterNetwork) || - (ipv6 && ip.AddressFamily == AddressFamily.InterNetworkV6)) - targetList.Add(ip.ToString()); - } - } if (targetList.Count == 0) { if(ipv4) @@ -162,7 +178,7 @@ public static string GetLocalIp(LocalAddrType addrType) // =========================================== internal static void PrintInterfaceInfos() { - NetDebug.WriteForce(NetLogLevel.Info, "IPv6Support: {0}", NetSocket.IPv6Support); + NetDebug.WriteForce(NetLogLevel.Info, $"IPv6Support: { LiteNetManager.IPv6Support}"); try { foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) @@ -174,24 +190,67 @@ internal static void PrintInterfaceInfos() { NetDebug.WriteForce( NetLogLevel.Info, - "Interface: {0}, Type: {1}, Ip: {2}, OpStatus: {3}", - ni.Name, - ni.NetworkInterfaceType.ToString(), - ip.Address.ToString(), - ni.OperationalStatus.ToString()); + $"Interface: {ni.Name}, Type: {ni.NetworkInterfaceType}, Ip: {ip.Address}, OpStatus: {ni.OperationalStatus}"); } } } } catch (Exception e) { - NetDebug.WriteForce(NetLogLevel.Info, "Error while getting interface infos: {0}", e.ToString()); + NetDebug.WriteForce(NetLogLevel.Info, $"Error while getting interface infos: {e}"); } } - internal static int RelativeSequenceNumber(int number, int expected) + internal static int RelativeSequenceNumber(int number, int expected) => + (number - expected + NetConstants.MaxSequence + NetConstants.HalfMaxSequence) % NetConstants.MaxSequence - NetConstants.HalfMaxSequence; + + internal static T[] AllocatePinnedUninitializedArray(int count) where T : unmanaged { - return (number - expected + NetConstants.MaxSequence + NetConstants.HalfMaxSequence) % NetConstants.MaxSequence - NetConstants.HalfMaxSequence; +#if NET5_0_OR_GREATER || NET5_0 + return GC.AllocateUninitializedArray(count, true); +#else + return new T[count]; +#endif + } + } + + // Pick the most obvious choice for the local IP + // Ethernet > Wifi > Others > Cellular + internal class NetworkSorter : IComparer + { + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public int Compare(NetworkInterface a, NetworkInterface b) + { + var isCellularA = a.NetworkInterfaceType == NetworkInterfaceType.Wman || + a.NetworkInterfaceType == NetworkInterfaceType.Wwanpp || + a.NetworkInterfaceType == NetworkInterfaceType.Wwanpp2; + + var isCellularB = b.NetworkInterfaceType == NetworkInterfaceType.Wman || + b.NetworkInterfaceType == NetworkInterfaceType.Wwanpp || + b.NetworkInterfaceType == NetworkInterfaceType.Wwanpp2; + + var isWifiA = a.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; + var isWifiB = b.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; + + var isEthernetA = a.NetworkInterfaceType == NetworkInterfaceType.Ethernet || + a.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || + a.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet || + a.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || + a.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT; + + var isEthernetB = b.NetworkInterfaceType == NetworkInterfaceType.Ethernet || + b.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || + b.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet || + b.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || + b.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT; + + var isOtherA = !isCellularA && !isWifiA && !isEthernetA; + var isOtherB = !isCellularB && !isWifiB && !isEthernetB; + + var priorityA = isEthernetA ? 3 : isWifiA ? 2 : isOtherA ? 1 : 0; + var priorityB = isEthernetB ? 3 : isWifiB ? 2 : isOtherB ? 1 : 0; + + return priorityA > priorityB ? -1 : priorityA < priorityB ? 1 : 0; } } } diff --git a/LiteNetLib/PausedSocketFix.cs b/LiteNetLib/PausedSocketFix.cs new file mode 100644 index 00000000..a8947348 --- /dev/null +++ b/LiteNetLib/PausedSocketFix.cs @@ -0,0 +1,67 @@ +#if UNITY_2018_3_OR_NEWER +using System.Net; +using UnityEngine; + +namespace LiteNetLib +{ + public class PausedSocketFix + { + private readonly LiteNetManager _netManager; + private readonly IPAddress _ipv4; + private readonly IPAddress _ipv6; + private readonly int _port; + private readonly bool _manualMode; + private bool _initialized; + + public PausedSocketFix(LiteNetManager netManager, IPAddress ipv4, IPAddress ipv6, int port, bool manualMode) + { + _netManager = netManager; + _ipv4 = ipv4; + _ipv6 = ipv6; + _port = port; + _manualMode = manualMode; + Application.focusChanged += Application_focusChanged; + Application.quitting += Deinitialize; + _initialized = true; + } + + public void Deinitialize() + { + if (_initialized) + { + Application.focusChanged -= Application_focusChanged; + Application.quitting -= Deinitialize; + } + + if (_netManager.IsRunning) + { + _netManager.Stop(); + } + _initialized = false; + } + + private void Application_focusChanged(bool focused) + { + //If coming back into focus see if a reconnect is needed. + if (focused) + { + //try reconnect + if (!_initialized) + return; + //Was intentionally disconnected at some point. + if (!_netManager.IsRunning) + return; + //Socket is in working state. + if (_netManager.NotConnected == false) + return; + + //Socket isn't running but should be. Try to start again. + if (!_netManager.Start(_ipv4, _ipv6, _port, _manualMode)) + { + NetDebug.WriteError($"[S] Cannot restore connection. Ipv4 {_ipv4}, Ipv6 {_ipv6}, Port {_port}, ManualMode {_manualMode}"); + } + } + } + } +} +#endif diff --git a/LiteNetLib/PooledPacket.cs b/LiteNetLib/PooledPacket.cs new file mode 100644 index 00000000..927b0335 --- /dev/null +++ b/LiteNetLib/PooledPacket.cs @@ -0,0 +1,32 @@ +namespace LiteNetLib +{ + public readonly ref struct PooledPacket + { + internal readonly NetPacket _packet; + internal readonly byte _channelNumber; + + /// + /// Maximum data size that you can put into such packet + /// + public readonly int MaxUserDataSize; + + /// + /// Offset for user data when writing to Data array + /// + public readonly int UserDataOffset; + + /// + /// Raw packet data. Do not modify header! Use UserDataOffset as start point for your data + /// + public byte[] Data => _packet.RawData; + + internal PooledPacket(NetPacket packet, int maxDataSize, byte channelNumber) + { + _packet = packet; + UserDataOffset = _packet.HeaderSize; + _packet.Size = UserDataOffset; + MaxUserDataSize = maxDataSize - UserDataOffset; + _channelNumber = channelNumber; + } + } +} diff --git a/LiteNetLib/ReliableChannel.cs b/LiteNetLib/ReliableChannel.cs index 08252dd8..8aa49851 100644 --- a/LiteNetLib/ReliableChannel.cs +++ b/LiteNetLib/ReliableChannel.cs @@ -1,19 +1,33 @@ -using System; +using System; +using System.Collections.Generic; +using LiteNetLib.Utils; namespace LiteNetLib { + internal sealed class MergedPacketUserData + { + public readonly object[] Items; + + public MergedPacketUserData(object[] items) + { + Items = items; + } + } + internal sealed class ReliableChannel : BaseChannel { + [ThreadStatic] + private static List _mergedPacketUserDataList; + private const int MergeHeaderSize = 2; + private const int MergeSizeThreshold = 20; + private struct PendingPacket { private NetPacket _packet; private long _timeStamp; private bool _isSent; - public override string ToString() - { - return _packet == null ? "Empty" : _packet.Sequence.ToString(); - } + public override string ToString() => _packet == null ? "Empty" : _packet.Sequence.ToString(); public void Init(NetPacket packet) { @@ -21,28 +35,33 @@ public void Init(NetPacket packet) _isSent = false; } - public void TrySend(long currentTime, NetPeer peer) + //Returns true if there is a pending packet inside + public bool TrySend(long currentTime, LiteNetPeer peer) { if (_packet == null) - return; + return false; + if (_isSent) //check send time { double resendDelay = peer.ResendDelay * TimeSpan.TicksPerMillisecond; double packetHoldTime = currentTime - _timeStamp; if (packetHoldTime < resendDelay) - return; - NetDebug.Write("[RC]Resend: {0} > {1}", (int)packetHoldTime, resendDelay); + return true; + NetDebug.Write($"[RC]Resend: {packetHoldTime} > {resendDelay}"); } _timeStamp = currentTime; _isSent = true; peer.SendUserData(_packet); + return true; } - public bool Clear(NetPeer peer) + public bool IsEmpty => _packet == null; + + public bool Clear(LiteNetPeer peer) { if (_packet != null) { - peer.Recycle(_packet); + peer.RecycleAndDeliver(_packet); _packet = null; return true; } @@ -62,12 +81,13 @@ public bool Clear(NetPeer peer) private bool _mustSendAcks; + private readonly DeliveryMethod _deliveryMethod; private readonly bool _ordered; private readonly int _windowSize; private const int BitsInByte = 8; private readonly byte _id; - public ReliableChannel(NetPeer peer, bool ordered, byte id) : base(peer) + public ReliableChannel(LiteNetPeer peer, bool ordered, byte id) : base(peer) { _id = id; _windowSize = NetConstants.DefaultWindowSize; @@ -77,9 +97,15 @@ public ReliableChannel(NetPeer peer, bool ordered, byte id) : base(peer) _pendingPackets[i] = new PendingPacket(); if (_ordered) + { + _deliveryMethod = DeliveryMethod.ReliableOrdered; _receivedPackets = new NetPacket[_windowSize]; + } else + { + _deliveryMethod = DeliveryMethod.ReliableUnordered; _earlyReceived = new bool[_windowSize]; + } _localWindowStart = 0; _localSeqence = 0; @@ -88,6 +114,98 @@ public ReliableChannel(NetPeer peer, bool ordered, byte id) : base(peer) _outgoingAcks = new NetPacket(PacketProperty.Ack, (_windowSize - 1) / BitsInByte + 2) {ChannelId = id}; } + private NetPacket GetNextOutgoingPacket() + { + int maxPayloadSize = Peer.Mtu - NetConstants.ChanneledHeaderSize; + NetPacket mergedPacket = null; + int mergePos = 0; + + List userDataList = null; + + while (OutgoingQueue.Count > 0) + { + var packet = OutgoingQueue.Peek(); + if (packet.IsFragmented) + break; + + int payloadSize = packet.Size - NetConstants.ChanneledHeaderSize; + int newSize = mergePos + MergeHeaderSize + payloadSize; + if (newSize + MergeSizeThreshold > maxPayloadSize && mergePos > 0) + break; + if (newSize > maxPayloadSize) + break; + + if (mergedPacket == null) + { + mergedPacket = Peer.NetManager.PoolGetPacket(Peer.Mtu); + mergedPacket.Property = PacketProperty.ReliableMerged; + userDataList = _mergedPacketUserDataList; + if (userDataList == null) + { + userDataList = new List(); + _mergedPacketUserDataList = userDataList; + } + else + { + userDataList.Clear(); + } + } + + FastBitConverter.GetBytes(mergedPacket.RawData, NetConstants.ChanneledHeaderSize + mergePos, (ushort)payloadSize); + Buffer.BlockCopy(packet.RawData, NetConstants.ChanneledHeaderSize, mergedPacket.RawData, NetConstants.ChanneledHeaderSize + mergePos + MergeHeaderSize, payloadSize); + mergePos += payloadSize + MergeHeaderSize; + + if (packet.UserData != null) + { + userDataList.Add(packet.UserData); + packet.UserData = null; + } + + Peer.NetManager.PoolRecycle(OutgoingQueue.Dequeue()); + } + + if (mergedPacket == null) + return OutgoingQueue.Dequeue(); + + mergedPacket.Size = NetConstants.ChanneledHeaderSize + mergePos; + if (userDataList.Count > 0) + mergedPacket.UserData = new MergedPacketUserData(userDataList.ToArray()); + + return mergedPacket; + } + + private void ProcessIncomingPacket(NetPacket packet) + { + if (packet.Property == PacketProperty.ReliableMerged) + { + //ProcessMerged + int pos = NetConstants.ChanneledHeaderSize; + while (pos + MergeHeaderSize <= packet.Size) + { + ushort size = BitConverter.ToUInt16(packet.RawData, pos); + pos += MergeHeaderSize; + if (size == 0 || pos + size > packet.Size) + { + NetDebug.Write("[RR]Merged packet corrupted"); + break; + } + + NetPacket mergedPacket = Peer.NetManager.PoolGetPacket(NetConstants.ChanneledHeaderSize + size); + mergedPacket.Property = PacketProperty.Channeled; + mergedPacket.ChannelId = packet.ChannelId; + Buffer.BlockCopy(packet.RawData, pos, mergedPacket.RawData, NetConstants.ChanneledHeaderSize, size); + pos += size; + + Peer.AddReliablePacket(_deliveryMethod, mergedPacket); + } + Peer.NetManager.PoolRecycle(packet); + } + else + { + Peer.AddReliablePacket(_deliveryMethod, packet); + } + } + //ProcessAck in packet private void ProcessAck(NetPacket packet) { @@ -105,7 +223,7 @@ private void ProcessAck(NetPacket packet) return; } - //check relevance + //check relevance if (windowRel >= _windowSize) { NetDebug.Write("[PA]Old acks"); @@ -122,7 +240,7 @@ private void ProcessAck(NetPacket packet) int rel = NetUtils.RelativeSequenceNumber(pendingSeq, ackWindowStart); if (rel >= _windowSize) { - NetDebug.Write("[PA]REL: " + rel); + //NetDebug.Write($"[PA]REL: {rel}"); break; } @@ -131,28 +249,31 @@ private void ProcessAck(NetPacket packet) int currentBit = pendingIdx % BitsInByte; if ((acksData[currentByte] & (1 << currentBit)) == 0) { -#if STATS_ENABLED || DEBUG - Peer.Statistics.PacketLoss++; -#endif + if (Peer.NetManager.EnableStatistics && !_pendingPackets[pendingIdx].IsEmpty) + { + Peer.Statistics.IncrementPacketLoss(); + Peer.NetManager.Statistics.IncrementPacketLoss(); + } + //Skip false ack - NetDebug.Write("[PA]False ack: {0}", pendingSeq); + //NetDebug.Write($"[PA]False ack: {pendingSeq}"); continue; } if (pendingSeq == _localWindowStart) { - //Move window + //Move window _localWindowStart = (_localWindowStart + 1) % NetConstants.MaxSequence; } //clear packet if (_pendingPackets[pendingIdx].Clear(Peer)) - NetDebug.Write("[PA]Removing reliableInOrder ack: {0} - true", pendingSeq); + NetDebug.Write($"[PA]Removing reliableInOrder ack: {pendingSeq} - true"); } } } - public override void SendNextPackets() + public override bool SendNextPackets() { if (_mustSendAcks) { @@ -163,6 +284,8 @@ public override void SendNextPackets() } long currentTime = DateTime.UtcNow.Ticks; + bool hasPendingPackets = false; + lock (_pendingPackets) { //get packets from queue @@ -174,17 +297,24 @@ public override void SendNextPackets() if (relate >= _windowSize) break; - var netPacket = OutgoingQueue.Dequeue(); + var netPacket = GetNextOutgoingPacket(); netPacket.Sequence = (ushort) _localSeqence; netPacket.ChannelId = _id; _pendingPackets[_localSeqence % _windowSize].Init(netPacket); _localSeqence = (_localSeqence + 1) % NetConstants.MaxSequence; } } + //send for (int pendingSeq = _localWindowStart; pendingSeq != _localSeqence; pendingSeq = (pendingSeq + 1) % NetConstants.MaxSequence) - _pendingPackets[pendingSeq % _windowSize].TrySend(currentTime, Peer); + { + // Please note: TrySend is invoked on a mutable struct, it's important to not extract it into a variable here + if (_pendingPackets[pendingSeq % _windowSize].TrySend(currentTime, Peer)) + hasPendingPackets = true; + } } + + return hasPendingPackets || _mustSendAcks || OutgoingQueue.Count > 0; } //Process incoming packet @@ -251,12 +381,15 @@ public override bool ProcessPacket(NetPacket packet) //Final stage - process valid packet //trigger acks send _mustSendAcks = true; + ackIdx = seq % _windowSize; ackByte = NetConstants.ChanneledHeaderSize + ackIdx / BitsInByte; ackBit = ackIdx % BitsInByte; if ((_outgoingAcks.RawData[ackByte] & (1 << ackBit)) != 0) { NetDebug.Write("[RR]ReliableInOrder duplicate"); + //because _mustSendAcks == true + AddToPeerChannelSendQueue(); return false; } @@ -264,11 +397,13 @@ public override bool ProcessPacket(NetPacket packet) _outgoingAcks.RawData[ackByte] |= (byte) (1 << ackBit); } + AddToPeerChannelSendQueue(); + //detailed check if (seq == _remoteSequence) { NetDebug.Write("[RR]ReliableInOrder packet succes"); - Peer.AddIncomingPacket(packet); + ProcessIncomingPacket(packet); _remoteSequence = (_remoteSequence + 1) % NetConstants.MaxSequence; if (_ordered) @@ -276,9 +411,9 @@ public override bool ProcessPacket(NetPacket packet) NetPacket p; while ((p = _receivedPackets[_remoteSequence % _windowSize]) != null) { - //process holded packet + //process holden packet _receivedPackets[_remoteSequence % _windowSize] = null; - Peer.AddIncomingPacket(p); + ProcessIncomingPacket(p); _remoteSequence = (_remoteSequence + 1) % NetConstants.MaxSequence; } } @@ -294,7 +429,7 @@ public override bool ProcessPacket(NetPacket packet) return true; } - //holded packet + //holden packet if (_ordered) { _receivedPackets[ackIdx] = packet; @@ -302,9 +437,9 @@ public override bool ProcessPacket(NetPacket packet) else { _earlyReceived[ackIdx] = true; - Peer.AddIncomingPacket(packet); + ProcessIncomingPacket(packet); } return true; } } -} \ No newline at end of file +} diff --git a/LiteNetLib/SequencedChannel.cs b/LiteNetLib/SequencedChannel.cs index f013b9e2..4bd1d778 100644 --- a/LiteNetLib/SequencedChannel.cs +++ b/LiteNetLib/SequencedChannel.cs @@ -1,3 +1,5 @@ +using System; + namespace LiteNetLib { internal sealed class SequencedChannel : BaseChannel @@ -9,8 +11,9 @@ internal sealed class SequencedChannel : BaseChannel private readonly NetPacket _ackPacket; private bool _mustSendAck; private readonly byte _id; + private long _lastPacketSendTime; - public SequencedChannel(NetPeer peer, bool reliable, byte id) : base(peer) + public SequencedChannel(LiteNetPeer peer, bool reliable, byte id) : base(peer) { _id = id; _reliable = reliable; @@ -18,13 +21,21 @@ public SequencedChannel(NetPeer peer, bool reliable, byte id) : base(peer) _ackPacket = new NetPacket(PacketProperty.Ack, 0) {ChannelId = id}; } - public override void SendNextPackets() + public override bool SendNextPackets() { if (_reliable && OutgoingQueue.Count == 0) { - var packet = _lastPacket; - if(packet != null) - Peer.SendUserData(packet); + long currentTime = DateTime.UtcNow.Ticks; + long packetHoldTime = currentTime - _lastPacketSendTime; + if (packetHoldTime >= Peer.ResendDelay * TimeSpan.TicksPerMillisecond) + { + var packet = _lastPacket; + if (packet != null) + { + _lastPacketSendTime = currentTime; + Peer.SendUserData(packet); + } + } } else { @@ -39,9 +50,14 @@ public override void SendNextPackets() Peer.SendUserData(packet); if (_reliable && OutgoingQueue.Count == 0) + { + _lastPacketSendTime = DateTime.UtcNow.Ticks; _lastPacket = packet; + } else - Peer.Recycle(packet); + { + Peer.NetManager.PoolRecycle(packet); + } } } } @@ -52,10 +68,14 @@ public override void SendNextPackets() _ackPacket.Sequence = _remoteSequence; Peer.SendUserData(_ackPacket); } + + return _lastPacket != null; } public override bool ProcessPacket(NetPacket packet) { + if (packet.IsFragmented) + return false; if (packet.Property == PacketProperty.Ack) { if (_reliable && _lastPacket != null && packet.Sequence == _lastPacket.Sequence) @@ -66,12 +86,28 @@ public override bool ProcessPacket(NetPacket packet) bool packetProcessed = false; if (packet.Sequence < NetConstants.MaxSequence && relative > 0) { - Peer.Statistics.PacketLoss += (ulong)(relative - 1); + if (Peer.NetManager.EnableStatistics) + { + Peer.Statistics.AddPacketLoss(relative - 1); + Peer.NetManager.Statistics.AddPacketLoss(relative - 1); + } + _remoteSequence = packet.Sequence; - Peer.AddIncomingPacket(packet); + Peer.NetManager.CreateReceiveEvent( + packet, + _reliable ? DeliveryMethod.ReliableSequenced : DeliveryMethod.Sequenced, + (byte)(packet.ChannelId / NetConstants.ChannelTypeCount), + NetConstants.ChanneledHeaderSize, + Peer); packetProcessed = true; } - _mustSendAck = true; + + if (_reliable) + { + _mustSendAck = true; + AddToPeerChannelSendQueue(); + } + return packetProcessed; } } diff --git a/LiteNetLib/SimpleChannel.cs b/LiteNetLib/SimpleChannel.cs deleted file mode 100644 index 005ccfad..00000000 --- a/LiteNetLib/SimpleChannel.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace LiteNetLib -{ - internal sealed class SimpleChannel : BaseChannel - { - public SimpleChannel(NetPeer peer) : base(peer) - { - - } - - public override void SendNextPackets() - { - lock (OutgoingQueue) - { - while (OutgoingQueue.Count > 0) - { - NetPacket packet = OutgoingQueue.Dequeue(); - Peer.SendUserData(packet); - Peer.Recycle(packet); - } - } - } - - public override bool ProcessPacket(NetPacket packet) - { - return false; - } - } -} diff --git a/LiteNetLib/Trimming.cs b/LiteNetLib/Trimming.cs new file mode 100644 index 00000000..6c575b65 --- /dev/null +++ b/LiteNetLib/Trimming.cs @@ -0,0 +1,12 @@ +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; + +namespace LiteNetLib +{ + internal static class Trimming + { + internal const DynamicallyAccessedMemberTypes SerializerMemberTypes = PublicProperties | NonPublicProperties; + } +} +#endif diff --git a/LiteNetLib/Utils/CRC32C.cs b/LiteNetLib/Utils/CRC32C.cs new file mode 100644 index 00000000..7e85680c --- /dev/null +++ b/LiteNetLib/Utils/CRC32C.cs @@ -0,0 +1,150 @@ +#if NETCOREAPP3_0_OR_GREATER || NETCOREAPP3_1 || NET5_0 +using System; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; +#endif +#if NET5_0_OR_GREATER || NET5_0 +using System.Runtime.Intrinsics.Arm; +#endif + +namespace LiteNetLib.Utils +{ + //Implementation from Crc32.NET + public static class CRC32C + { + public const int ChecksumSize = 4; + private const uint Poly = 0x82F63B78u; + private static readonly uint[] Table; + + static CRC32C() + { +#if NETCOREAPP3_0_OR_GREATER || NETCOREAPP3_1 || NET5_0 + if (Sse42.IsSupported) + return; +#endif +#if NET5_0_OR_GREATER || NET5_0 + if (Crc32.IsSupported) + return; +#endif + Table = NetUtils.AllocatePinnedUninitializedArray(16 * 256); + for (uint i = 0; i < 256; i++) + { + uint res = i; + for (int t = 0; t < 16; t++) + { + for (int k = 0; k < 8; k++) + res = (res & 1) == 1 ? Poly ^ (res >> 1) : (res >> 1); + Table[t * 256 + i] = res; + } + } + } + + /// + /// Compute CRC32C for data + /// + /// input data + /// offset + /// length + /// CRC32C checksum + public static uint Compute(byte[] input, int offset, int length) + { + uint crcLocal = uint.MaxValue; +#if NETCOREAPP3_0_OR_GREATER || NETCOREAPP3_1 || NET5_0 + if (Sse42.IsSupported) + { + var data = new ReadOnlySpan(input, offset, length); + int processed = 0; + if (Sse42.X64.IsSupported && data.Length > sizeof(ulong)) + { + processed = data.Length / sizeof(ulong) * sizeof(ulong); + var ulongs = MemoryMarshal.Cast(data.Slice(0, processed)); + ulong crclong = crcLocal; + for (int i = 0; i < ulongs.Length; i++) + { + crclong = Sse42.X64.Crc32(crclong, ulongs[i]); + } + + crcLocal = (uint)crclong; + } + else if (data.Length > sizeof(uint)) + { + processed = data.Length / sizeof(uint) * sizeof(uint); + var uints = MemoryMarshal.Cast(data.Slice(0, processed)); + for (int i = 0; i < uints.Length; i++) + { + crcLocal = Sse42.Crc32(crcLocal, uints[i]); + } + } + + for (int i = processed; i < data.Length; i++) + { + crcLocal = Sse42.Crc32(crcLocal, data[i]); + } + + return crcLocal ^ uint.MaxValue; + } +#endif +#if NET5_0_OR_GREATER || NET5_0 + if (Crc32.IsSupported) + { + var data = new ReadOnlySpan(input, offset, length); + int processed = 0; + if (Crc32.Arm64.IsSupported && data.Length > sizeof(ulong)) + { + processed = data.Length / sizeof(ulong) * sizeof(ulong); + var ulongs = MemoryMarshal.Cast(data.Slice(0, processed)); + for (int i = 0; i < ulongs.Length; i++) + { + crcLocal = Crc32.Arm64.ComputeCrc32C(crcLocal, ulongs[i]); + } + } + else if (data.Length > sizeof(uint)) + { + processed = data.Length / sizeof(uint) * sizeof(uint); + var uints = MemoryMarshal.Cast(data.Slice(0, processed)); + for (int i = 0; i < uints.Length; i++) + { + crcLocal = Crc32.ComputeCrc32C(crcLocal, uints[i]); + } + } + + for (int i = processed; i < data.Length; i++) + { + crcLocal = Crc32.ComputeCrc32C(crcLocal, data[i]); + } + + return crcLocal ^ uint.MaxValue; + } +#endif + while (length >= 16) + { + var a = Table[(3 * 256) + input[offset + 12]] + ^ Table[(2 * 256) + input[offset + 13]] + ^ Table[(1 * 256) + input[offset + 14]] + ^ Table[(0 * 256) + input[offset + 15]]; + + var b = Table[(7 * 256) + input[offset + 8]] + ^ Table[(6 * 256) + input[offset + 9]] + ^ Table[(5 * 256) + input[offset + 10]] + ^ Table[(4 * 256) + input[offset + 11]]; + + var c = Table[(11 * 256) + input[offset + 4]] + ^ Table[(10 * 256) + input[offset + 5]] + ^ Table[(9 * 256) + input[offset + 6]] + ^ Table[(8 * 256) + input[offset + 7]]; + + var d = Table[(15 * 256) + ((byte)crcLocal ^ input[offset])] + ^ Table[(14 * 256) + ((byte)(crcLocal >> 8) ^ input[offset + 1])] + ^ Table[(13 * 256) + ((byte)(crcLocal >> 16) ^ input[offset + 2])] + ^ Table[(12 * 256) + ((crcLocal >> 24) ^ input[offset + 3])]; + + crcLocal = d ^ c ^ b ^ a; + offset += 16; + length -= 16; + } + while (--length >= 0) + crcLocal = Table[(byte)(crcLocal ^ input[offset++])] ^ crcLocal >> 8; + return crcLocal ^ uint.MaxValue; + } + } +} diff --git a/LiteNetLib/Utils/FastBitConverter.cs b/LiteNetLib/Utils/FastBitConverter.cs index 3693a027..202cf134 100644 --- a/LiteNetLib/Utils/FastBitConverter.cs +++ b/LiteNetLib/Utils/FastBitConverter.cs @@ -1,118 +1,53 @@ -using System.Runtime.InteropServices; +using System; +using System.Runtime.CompilerServices; +#if UNITY_ANDROID +using Unity.Collections.LowLevel.Unsafe; +#endif namespace LiteNetLib.Utils { public static class FastBitConverter { - [StructLayout(LayoutKind.Explicit)] - private struct ConverterHelperDouble - { - [FieldOffset(0)] - public ulong Along; - - [FieldOffset(0)] - public double Adouble; - } - - [StructLayout(LayoutKind.Explicit)] - private struct ConverterHelperFloat - { - [FieldOffset(0)] - public int Aint; - - [FieldOffset(0)] - public float Afloat; - } - - private static void WriteLittleEndian(byte[] buffer, int offset, ulong data) - { -#if BIGENDIAN - buffer[offset + 7] = (byte)(data); - buffer[offset + 6] = (byte)(data >> 8); - buffer[offset + 5] = (byte)(data >> 16); - buffer[offset + 4] = (byte)(data >> 24); - buffer[offset + 3] = (byte)(data >> 32); - buffer[offset + 2] = (byte)(data >> 40); - buffer[offset + 1] = (byte)(data >> 48); - buffer[offset ] = (byte)(data >> 56); -#else - buffer[offset] = (byte)(data); - buffer[offset + 1] = (byte)(data >> 8); - buffer[offset + 2] = (byte)(data >> 16); - buffer[offset + 3] = (byte)(data >> 24); - buffer[offset + 4] = (byte)(data >> 32); - buffer[offset + 5] = (byte)(data >> 40); - buffer[offset + 6] = (byte)(data >> 48); - buffer[offset + 7] = (byte)(data >> 56); -#endif - } - - private static void WriteLittleEndian(byte[] buffer, int offset, int data) - { -#if BIGENDIAN - buffer[offset + 3] = (byte)(data); - buffer[offset + 2] = (byte)(data >> 8); - buffer[offset + 1] = (byte)(data >> 16); - buffer[offset ] = (byte)(data >> 24); + /// + /// Converts a value of type into a byte array starting at the specified index. + /// + /// The type of the value to convert. Must be an unmanaged/blittable type. + /// The destination byte array. + /// The zero-based index in at which to begin writing. + /// The value to be converted and written. + /// + /// Thrown when the array is too small to contain the value at the given . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void GetBytes(byte[] bytes, int startIndex, T value) where T : unmanaged + { + int size = sizeof(T); + if (bytes.Length < startIndex + size) + ThrowIndexOutOfRangeException(); +#if NET8_0_OR_GREATER + Unsafe.WriteUnaligned(ref bytes[startIndex], value); #else - buffer[offset] = (byte)(data); - buffer[offset + 1] = (byte)(data >> 8); - buffer[offset + 2] = (byte)(data >> 16); - buffer[offset + 3] = (byte)(data >> 24); + fixed (byte* ptr = &bytes[startIndex]) + { + #if UNITY_ANDROID + // On some android systems, assigning *(T*)ptr throws a NRE if + // the ptr isn't aligned (i.e. if Position is 1,2,3,5, etc.). + // Here we have to use memcpy. + // + // => we can't get a pointer of a struct in C# without + // marshalling allocations + // => instead, we stack allocate an array of type T and use that + // => stackalloc avoids GC and is very fast. it only works for + // value types, but all blittable types are anyway. + T* valueBuffer = stackalloc T[1] { value }; + UnsafeUtility.MemCpy(ptr, valueBuffer, size); + #else + *(T*)ptr = value; + #endif + } #endif } - public static void WriteLittleEndian(byte[] buffer, int offset, short data) - { -#if BIGENDIAN - buffer[offset + 1] = (byte)(data); - buffer[offset ] = (byte)(data >> 8); -#else - buffer[offset] = (byte)(data); - buffer[offset + 1] = (byte)(data >> 8); -#endif - } - - public static void GetBytes(byte[] bytes, int startIndex, double value) - { - ConverterHelperDouble ch = new ConverterHelperDouble { Adouble = value }; - WriteLittleEndian(bytes, startIndex, ch.Along); - } - - public static void GetBytes(byte[] bytes, int startIndex, float value) - { - ConverterHelperFloat ch = new ConverterHelperFloat { Afloat = value }; - WriteLittleEndian(bytes, startIndex, ch.Aint); - } - - public static void GetBytes(byte[] bytes, int startIndex, short value) - { - WriteLittleEndian(bytes, startIndex, value); - } - - public static void GetBytes(byte[] bytes, int startIndex, ushort value) - { - WriteLittleEndian(bytes, startIndex, (short)value); - } - - public static void GetBytes(byte[] bytes, int startIndex, int value) - { - WriteLittleEndian(bytes, startIndex, value); - } - - public static void GetBytes(byte[] bytes, int startIndex, uint value) - { - WriteLittleEndian(bytes, startIndex, (int)value); - } - - public static void GetBytes(byte[] bytes, int startIndex, long value) - { - WriteLittleEndian(bytes, startIndex, (ulong)value); - } - - public static void GetBytes(byte[] bytes, int startIndex, ulong value) - { - WriteLittleEndian(bytes, startIndex, value); - } + private static void ThrowIndexOutOfRangeException() => throw new IndexOutOfRangeException(); } } diff --git a/LiteNetLib/Utils/INetSerializable.cs b/LiteNetLib/Utils/INetSerializable.cs index 92f14bee..423e0468 100644 --- a/LiteNetLib/Utils/INetSerializable.cs +++ b/LiteNetLib/Utils/INetSerializable.cs @@ -1,8 +1,23 @@ namespace LiteNetLib.Utils { + /// + /// Interface for implementing custom data serialization for network transmission. + /// + /// + /// This is the most efficient way to send complex objects as it avoids reflection. + /// public interface INetSerializable { + /// + /// Writes the object data into the provided . + /// + /// The writer to pack data into. void Serialize(NetDataWriter writer); + + /// + /// Reads the object data from the provided . + /// + /// The reader to extract data from. void Deserialize(NetDataReader reader); } } diff --git a/LiteNetLib/Utils/NetDataReader.cs b/LiteNetLib/Utils/NetDataReader.cs index 3960aa58..2e230009 100644 --- a/LiteNetLib/Utils/NetDataReader.cs +++ b/LiteNetLib/Utils/NetDataReader.cs @@ -1,6 +1,8 @@ -using System; +using System; using System.Net; -using System.Text; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace LiteNetLib.Utils { @@ -9,48 +11,125 @@ public class NetDataReader protected byte[] _data; protected int _position; protected int _dataSize; - private int _offset; + protected int _offset; + private const int IPv4Size = 4; + private const int IPv6Size = 16; + private const int GuidSize = 16; + + /// + /// Gets the internal array containing the raw network data. + /// public byte[] RawData { - get { return _data; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _data; } + /// + /// Gets the total size of the buffer. + /// public int RawDataSize { - get { return _dataSize; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _dataSize; } + /// + /// Gets the starting offset of the user payload within the . + /// public int UserDataOffset { - get { return _offset; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _offset; } + /// + /// Gets the size of the user payload excluding the initial . + /// public int UserDataSize { - get { return _dataSize - _offset; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _dataSize - _offset; } + /// + /// Gets a value indicating whether the internal data buffer is . + /// public bool IsNull { - get { return _data == null; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _data == null; } + /// + /// Gets the current read position within the buffer. + /// public int Position { - get { return _position; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _position; } + /// + /// Gets a value indicating whether the has reached the end of the data. + /// public bool EndOfData { - get { return _position == _dataSize; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _position == _dataSize; } + /// + /// Gets the number of s remaining to be read. + /// public int AvailableBytes { - get { return _dataSize - _position; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _dataSize - _position; } + /// + /// Verifies that the buffer has at least s available to read. + /// + /// The number of s required. + /// Thrown if exceeds or is negative. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureAvailable(int count) + { + int available = _dataSize - _position; + if (count < 0 || available < 0 || count > available) + ThrowNotEnoughData(count); + } + + /// + /// Throws an indicating that there is not enough data to read. + /// + /// The number of bytes that were attempted to be read. + /// Always thrown. + [MethodImpl(MethodImplOptions.NoInlining)] + private void ThrowNotEnoughData(int count) + { + throw new InvalidOperationException( + $"Not enough data to read {count} byte(s). Position={_position}, DataSize={_dataSize}"); + } + + /// + /// Advances the by the specified . + /// + /// The number of s to skip. + public void SkipBytes(int count) => _position += count; + + /// + /// Sets the current read to a specific index. + /// + /// The index to move the to. + public void SetPosition(int position) => _position = position; + + /// + /// Reinitializes the reader using data from a . + /// + /// The source . public void SetSource(NetDataWriter dataWriter) { _data = dataWriter.Data; @@ -59,6 +138,10 @@ public void SetSource(NetDataWriter dataWriter) _dataSize = dataWriter.Length; } + /// + /// Reinitializes the reader using a array. + /// + /// The source array. public void SetSource(byte[] source) { _data = source; @@ -67,14 +150,12 @@ public void SetSource(byte[] source) _dataSize = source.Length; } - public void SetSource(byte[] source, int offset) - { - _data = source; - _position = offset; - _offset = offset; - _dataSize = source.Length; - } - + /// + /// Reinitializes the reader using a segment of a array. + /// + /// The source array. + /// The starting index for reading. + /// The total number of s available to read from the . public void SetSource(byte[] source, int offset, int maxSize) { _data = source; @@ -83,620 +164,866 @@ public void SetSource(byte[] source, int offset, int maxSize) _dataSize = maxSize; } - public NetDataReader() - { + public NetDataReader() { } - } - - public NetDataReader(byte[] source) - { - SetSource(source); - } + public NetDataReader(NetDataWriter writer) => SetSource(writer); - public NetDataReader(byte[] source, int offset) - { - SetSource(source, offset); - } + public NetDataReader(byte[] source) => SetSource(source); - public NetDataReader(byte[] source, int offset, int maxSize) - { - SetSource(source, offset, maxSize); - } + public NetDataReader(byte[] source, int offset, int maxSize) => SetSource(source, offset, maxSize); #region GetMethods - public IPEndPoint GetNetEndPoint() - { - string host = GetString(1000); - int port = GetInt(); - return NetUtils.MakeEndPoint(host, port); - } - public byte GetByte() + /// + /// Deserializes a that implements . + /// + /// A type implementing . + /// The deserialized output. + public void Get(out T result) where T : struct, INetSerializable { - byte res = _data[_position]; - _position += 1; - return res; + result = default; + result.Deserialize(this); } - public sbyte GetSByte() + /// + /// Deserializes a that implements using a provided constructor. + /// + /// A type implementing . + /// The deserialized instance output. + /// A factory used to instantiate the . + public void Get(out T result, Func constructor) where T : class, INetSerializable { - var b = (sbyte)_data[_position]; - _position++; - return b; + result = constructor(); + result.Deserialize(this); } - public bool[] GetBoolArray() - { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new bool[size]; - for (int i = 0; i < size; i++) - { - arr[i] = GetBool(); - } - return arr; - } + /// + /// Deserializes an and assigns it to the parameter. + /// + /// The deserialized output. + public void Get(out IPEndPoint result) => result = GetIPEndPoint(); - public ushort[] GetUShortArray() + /// + /// Reads an from the current . + /// + /// The deserialized . + /// + /// Reads a to determine the (0 for IPv4, 1 for IPv6), + /// followed by the address bytes and a 2-byte port. + /// + public IPEndPoint GetIPEndPoint() { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new ushort[size]; - for (int i = 0; i < size; i++) - { - arr[i] = GetUShort(); - } - return arr; - } + bool isIPv4 = GetByte() == 0; - public short[] GetShortArray() - { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new short[size]; - for (int i = 0; i < size; i++) - { - arr[i] = GetShort(); - } - return arr; - } + int size = isIPv4 ? IPv4Size : IPv6Size; + EnsureAvailable(size); - public long[] GetLongArray() - { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new long[size]; - for (int i = 0; i < size; i++) - { - arr[i] = GetLong(); - } - return arr; + IPAddress address = new IPAddress(new ReadOnlySpan(_data, _position, size)); + _position += size; + return new IPEndPoint(address, GetUShort()); } - public ulong[] GetULongArray() - { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new ulong[size]; - for (int i = 0; i < size; i++) - { - arr[i] = GetULong(); - } - return arr; - } + /// Reads a and assigns it to . + public void Get(out byte result) => result = GetByte(); + + /// Reads an and assigns it to . + public void Get(out sbyte result) => result = GetSByte(); + + /// Reads a and assigns it to . + public void Get(out bool result) => result = GetBool(); + + /// Reads a and assigns it to . + public void Get(out char result) => result = GetChar(); + + /// Reads a and assigns it to . + public void Get(out ushort result) => result = GetUShort(); - public int[] GetIntArray() + /// Reads a and assigns it to . + public void Get(out short result) => result = GetShort(); + + /// Reads a and assigns it to . + public void Get(out ulong result) => result = GetULong(); + + /// Reads a and assigns it to . + public void Get(out long result) => result = GetLong(); + + /// Reads a and assigns it to . + public void Get(out uint result) => result = GetUInt(); + + /// Reads an and assigns it to . + public void Get(out int result) => result = GetInt(); + + /// Reads a and assigns it to . + public void Get(out double result) => result = GetDouble(); + + /// Reads a and assigns it to . + public void Get(out float result) => result = GetFloat(); + + /// Reads a and assigns it to . + public void Get(out string result) => result = GetString(); + + /// Reads a with a length limit and assigns it to . + public void Get(out string result, int maxLength) => result = GetString(maxLength); + + /// Reads a and assigns it to . + public void Get(out Guid result) => result = GetGuid(); + + /// + /// Reads an array of unmanaged values prefixed by a length. + /// + /// An unmanaged type. + /// A new array of type . + public unsafe T[] GetUnmanagedArray() where T : unmanaged { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new int[size]; - for (int i = 0; i < size; i++) - { - arr[i] = GetInt(); - } - return arr; + ushort length = GetUShort(); + + int byteLength = checked(length * sizeof(T)); + EnsureAvailable(byteLength); + + ReadOnlySpan slice = _data.AsSpan(_position, byteLength); + T[] result = MemoryMarshal.Cast(slice) + .ToArray(); + _position += byteLength; + return result; } - public uint[] GetUIntArray() + /// + /// Reads an array of values by performing a direct memory copy. + /// + /// The element type. + /// The size of a single element in s. + /// A new array of type . + public T[] GetArray(ushort size) { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new uint[size]; - for (int i = 0; i < size; i++) - { - arr[i] = GetUInt(); - } - return arr; + ushort length = GetUShort(); + + int byteLength = checked(length * size); + EnsureAvailable(byteLength); + + T[] result = new T[length]; + if (byteLength > 0) + Buffer.BlockCopy(_data, _position, result, 0, byteLength); + + _position += byteLength; + return result; } - public float[] GetFloatArray() + /// + /// Reads an array of objects implementing . + /// + /// A type with a parameterless constructor implementing . + /// A new array of type . + public T[] GetArray() where T : INetSerializable, new() { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new float[size]; - for (int i = 0; i < size; i++) + ushort length = GetUShort(); + T[] result = new T[length]; + for (int i = 0; i < length; i++) { - arr[i] = GetFloat(); + var item = new T(); + item.Deserialize(this); + result[i] = item; } - return arr; + return result; } - public double[] GetDoubleArray() + /// + /// Reads an array of objects implementing using a specific constructor. + /// + /// A type implementing . + /// The factory used to create instances. + /// A new array of type . + public T[] GetArray(Func constructor) where T : class, INetSerializable { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new double[size]; - for (int i = 0; i < size; i++) - { - arr[i] = GetDouble(); - } - return arr; + ushort length = GetUShort(); + T[] result = new T[length]; + for (int i = 0; i < length; i++) + Get(out result[i], constructor); + return result; } + /// Reads an array of values. + public bool[] GetBoolArray() => GetUnmanagedArray(); + + /// Reads an array of values. + public ushort[] GetUShortArray() => GetUnmanagedArray(); + + /// Reads an array of values. + public short[] GetShortArray() => GetUnmanagedArray(); + + /// Reads an array of values. + public int[] GetIntArray() => GetUnmanagedArray(); + + /// Reads an array of values. + public uint[] GetUIntArray() => GetUnmanagedArray(); + + /// Reads an array of values. + public float[] GetFloatArray() => GetUnmanagedArray(); + + /// Reads an array of values. + public double[] GetDoubleArray() => GetUnmanagedArray(); + + /// Reads an array of values. + public long[] GetLongArray() => GetUnmanagedArray(); + + /// Reads an array of values. + public ulong[] GetULongArray() => GetUnmanagedArray(); + + /// + /// Reads an array of values. + /// + /// A new array. + /// + /// Reads a 2-byte length header followed by each element. + /// public string[] GetStringArray() { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new string[size]; - for (int i = 0; i < size; i++) + ushort length = GetUShort(); + EnsureAvailable(checked(length * sizeof(ushort))); // 2 bytes (ushort) for string length + + string[] result = new string[length]; + for (int i = 0; i < length; i++) { - arr[i] = GetString(); + result[i] = GetString(); } - return arr; + + return result; } + /// + /// Reads an array of values with a maximum character limit per element. + /// + /// The maximum number of characters allowed per . + /// A new array. + /// + /// Strings exceeding are returned as . + /// public string[] GetStringArray(int maxStringLength) { - ushort size = BitConverter.ToUInt16(_data, _position); - _position += 2; - var arr = new string[size]; - for (int i = 0; i < size; i++) + ushort length = GetUShort(); + EnsureAvailable(checked(length * sizeof(ushort))); // 2 bytes (ushort) for string length + + string[] result = new string[length]; + for (int i = 0; i < length; i++) { - arr[i] = GetString(maxStringLength); + result[i] = GetString(maxStringLength); } - return arr; - } - - public bool GetBool() - { - bool res = _data[_position] > 0; - _position += 1; - return res; - } - public char GetChar() - { - char result = BitConverter.ToChar(_data, _position); - _position += 2; return result; } - public ushort GetUShort() + /// Reads the next from the buffer. + public byte GetByte() { - ushort result = BitConverter.ToUInt16(_data, _position); - _position += 2; - return result; + EnsureAvailable(1); + return _data[_position++]; } - public short GetShort() + /// Reads the next from the buffer. + public sbyte GetSByte() => (sbyte)GetByte(); + + /// Reads a value from the current position. + /// if the byte is 1; otherwise, . + public bool GetBool() => GetByte() == 1; + + /// Reads a value as a 2-byte . + public char GetChar() => GetUnmanaged(); + + /// Reads a value using unmanaged memory access. + public ushort GetUShort() => GetUnmanaged(); + + /// Reads a value using unmanaged memory access. + public short GetShort() => GetUnmanaged(); + + /// Reads a value using unmanaged memory access. + public long GetLong() => GetUnmanaged(); + + /// Reads a value using unmanaged memory access. + public ulong GetULong() => GetUnmanaged(); + + /// Reads an value using unmanaged memory access. + public int GetInt() => GetUnmanaged(); + + /// Reads a value using unmanaged memory access. + public uint GetUInt() => GetUnmanaged(); + + /// Reads a value using unmanaged memory access. + public float GetFloat() => GetUnmanaged(); + + /// Reads a value using unmanaged memory access. + public double GetDouble() => GetUnmanaged(); + + /// + /// Reads a with a maximum character limit. + /// + /// The maximum allowed character count. + /// The deserialized , or if the character count exceeds . + /// + /// Note that limits the number of characters, not the total size in s. + /// + public string GetString(int maxLength) { - short result = BitConverter.ToInt16(_data, _position); - _position += 2; + ushort size = GetUShort(); + if (size == 0) + return string.Empty; + + int actualSize = size - 1; + EnsureAvailable(actualSize); + + string result = maxLength > 0 && + NetDataWriter.uTF8Encoding.GetCharCount(_data, _position, actualSize) > maxLength + ? string.Empty + : NetDataWriter.uTF8Encoding.GetString(_data, _position, actualSize); + + _position += actualSize; return result; } - public long GetLong() + /// + /// Reads a from the current position. + /// + /// The deserialized . + public string GetString() { - long result = BitConverter.ToInt64(_data, _position); - _position += 8; + ushort size = GetUShort(); + if (size == 0) + return string.Empty; + + int actualSize = size - 1; + EnsureAvailable(actualSize); + + string result = NetDataWriter.uTF8Encoding.GetString(_data, _position, actualSize); + _position += actualSize; return result; } - public ulong GetULong() + /// + /// Reads a prefixed with a 4-byte length header. + /// + /// The deserialized . + public string GetLargeString() { - ulong result = BitConverter.ToUInt64(_data, _position); - _position += 8; + int size = GetInt(); + if (size <= 0) + return string.Empty; + + EnsureAvailable(size); + + string result = NetDataWriter.uTF8Encoding.GetString(_data, _position, size); + _position += size; return result; } - public int GetInt() + /// + /// Reads a 16-byte . + /// + /// The deserialized . + public Guid GetGuid() { - int result = BitConverter.ToInt32(_data, _position); - _position += 4; + EnsureAvailable(GuidSize); + var result = new Guid(_data.AsSpan(_position, GuidSize)); + _position += GuidSize; return result; } - public uint GetUInt() + /// + /// Gets an of s from the current position. + /// + /// The number of s to include in the segment. + /// An wrapping the internal buffer. + public ArraySegment GetBytesSegment(int count) { - uint result = BitConverter.ToUInt32(_data, _position); - _position += 4; - return result; + EnsureAvailable(count); + ArraySegment segment = new ArraySegment(_data, _position, count); + _position += count; + return segment; } - public float GetFloat() + /// + /// Gets an containing all remaining s. + /// + /// An from the current position to the end of the data. + public ArraySegment GetRemainingBytesSegment() { - float result = BitConverter.ToSingle(_data, _position); - _position += 4; - return result; + ArraySegment segment = new ArraySegment(_data, _position, AvailableBytes); + _position = _dataSize; + return segment; } - public double GetDouble() + /// + /// Returns a of s containing all remaining data. + /// + /// A from the current to the end of the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan GetRemainingBytesSpan() { - double result = BitConverter.ToDouble(_data, _position); - _position += 8; + var result = new ReadOnlySpan(_data, _position, AvailableBytes); + _position = _dataSize; return result; } - public string GetString(int maxLength) + /// + /// Returns a of s containing all remaining data. + /// + /// A from the current to the end of the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyMemory GetRemainingBytesMemory() { - int bytesCount = GetInt(); - if (bytesCount <= 0 || bytesCount > maxLength*2) - { - return string.Empty; - } - - int charCount = Encoding.UTF8.GetCharCount(_data, _position, bytesCount); - if (charCount > maxLength) - { - return string.Empty; - } - - string result = Encoding.UTF8.GetString(_data, _position, bytesCount); - _position += bytesCount; + var result = new ReadOnlyMemory(_data, _position, AvailableBytes); + _position = _dataSize; return result; } - public string GetString() + /// + /// Reads all remaining s and returns them as a new array. + /// + /// A new array containing the remaining data. + /// + /// This method performs a heap allocation and advances the to the end of the data. + /// + public byte[] GetRemainingBytes() { - int bytesCount = GetInt(); - if (bytesCount <= 0) - { - return string.Empty; - } + int size = _dataSize - _position; + if (size == 0) + return Array.Empty(); - string result = Encoding.UTF8.GetString(_data, _position, bytesCount); - _position += bytesCount; + byte[] result = new byte[size]; + Buffer.BlockCopy(_data, _position, result, 0, size); + _position = _dataSize; return result; } - public ArraySegment GetRemainingBytesSegment() + /// + /// Deserializes a that implements . + /// + /// A type implementing . + /// The deserialized . + public T Get() where T : struct, INetSerializable { - ArraySegment segment = new ArraySegment(_data, _position, AvailableBytes); - _position = _data.Length; - return segment; - } - - public T Get() where T : INetSerializable, new() - { - var obj = new T(); - obj.Deserialize(this); - return obj; + Get(out T result); + return result; } - public byte[] GetRemainingBytes() + /// + /// Deserializes a that implements using a provided constructor. + /// + /// A type implementing . + /// The factory used to instantiate the . + /// A new instance of . + public T Get(Func constructor) where T : class, INetSerializable { - byte[] outgoingData = new byte[AvailableBytes]; - Buffer.BlockCopy(_data, _position, outgoingData, 0, AvailableBytes); - _position = _data.Length; - return outgoingData; + Get(out T result, constructor); + return result; } + /// + /// Copies a specified number of s into a destination array at a specific offset. + /// + /// The array to copy data into. + /// The starting index in the array. + /// The number of s to read. public void GetBytes(byte[] destination, int start, int count) { + EnsureAvailable(count); Buffer.BlockCopy(_data, _position, destination, start, count); _position += count; } + /// + /// Copies a specified number of s into a destination array starting at index 0. + /// + /// The array to copy data into. + /// The number of s to read. public void GetBytes(byte[] destination, int count) { + EnsureAvailable(count); Buffer.BlockCopy(_data, _position, destination, 0, count); _position += count; } - - public sbyte[] GetSBytesWithLength() - { - int length = GetInt(); - sbyte[] outgoingData = new sbyte[length]; - Buffer.BlockCopy(_data, _position, outgoingData, 0, length); - _position += length; - return outgoingData; - } - - public byte[] GetBytesWithLength() - { - int length = GetInt(); - byte[] outgoingData = new byte[length]; - Buffer.BlockCopy(_data, _position, outgoingData, 0, length); - _position += length; - return outgoingData; + + /// + /// Reads an array prefixed with its length. + /// + /// A new array. + public sbyte[] GetSBytesWithLength() => GetUnmanagedArray(); + + /// + /// Reads a array prefixed with its length. + /// + /// A new array. + public byte[] GetBytesWithLength() => GetUnmanagedArray(); + + /// + /// Reads a value of type from the internal byte buffer at the current position, + /// advancing the position by the size of . + /// + /// An unmanaged value type to read from the buffer. + /// The value of type read from the buffer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe T GetUnmanaged() where T : unmanaged + { + var size = sizeof(T); + EnsureAvailable(size); + +#if NET8_0_OR_GREATER + var value = Unsafe.ReadUnaligned(ref _data[_position]); +#else + T value; + fixed (byte* ptr = &_data[_position]) + { + value = *(T*)ptr; + } +#endif + + _position += size; + return value; + } + + /// + /// Reads a nullable value of type from the internal byte buffer at the current position, + /// first reading a indicating whether the value is present, + /// and then reading the value itself if it exists.
+ /// Advances the position by 1 byte for the presence flag plus the size of if the value is present. + ///
+ /// An unmanaged value type to read from the buffer. + /// + /// The nullable value of type read from the buffer. + /// Returns if the presence flag indicates no value. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T? GetNullableUnmanaged() where T : unmanaged + { + var hasValue = GetBool(); + if (!hasValue) + { + return null; + } + + return GetUnmanaged(); } + + /// + /// Reads an enum value of type from the internal data buffer at the current position.
+ /// Advances the position by the size of . + ///
+ /// An unmanaged enum type to read. + /// The enum value read from the buffer. + public unsafe T GetEnum() where T : unmanaged, Enum => GetUnmanaged(); + #endregion #region PeekMethods - public byte PeekByte() - { - return _data[_position]; - } + /// Reads the at the current position without advancing the . + public byte PeekByte() => _data[_position]; - public sbyte PeekSByte() - { - return (sbyte)_data[_position]; - } + /// Reads the at the current position without advancing the . + public sbyte PeekSByte() => (sbyte)PeekByte(); - public bool PeekBool() - { - return _data[_position] > 0; - } + /// Reads the at the current position without advancing the . + public bool PeekBool() => PeekByte() == 1; - public char PeekChar() - { - return BitConverter.ToChar(_data, _position); - } + /// Reads the at the current position without advancing the . + public char PeekChar() => PeekUnmanaged(); - public ushort PeekUShort() - { - return BitConverter.ToUInt16(_data, _position); - } + /// Reads the at the current position without advancing the . + public ushort PeekUShort() => PeekUnmanaged(); - public short PeekShort() - { - return BitConverter.ToInt16(_data, _position); - } + /// Reads the at the current position without advancing the . + public short PeekShort() => PeekUnmanaged(); - public long PeekLong() - { - return BitConverter.ToInt64(_data, _position); - } + /// Reads the at the current position without advancing the . + public long PeekLong() => PeekUnmanaged(); - public ulong PeekULong() - { - return BitConverter.ToUInt64(_data, _position); - } + /// Reads the at the current position without advancing the . + public ulong PeekULong() => PeekUnmanaged(); - public int PeekInt() - { - return BitConverter.ToInt32(_data, _position); - } + /// Reads the at the current position without advancing the . + public int PeekInt() => PeekUnmanaged(); - public uint PeekUInt() - { - return BitConverter.ToUInt32(_data, _position); - } + /// Reads the at the current position without advancing the . + public uint PeekUInt() => PeekUnmanaged(); - public float PeekFloat() - { - return BitConverter.ToSingle(_data, _position); - } + /// Reads the at the current position without advancing the . + public float PeekFloat() => PeekUnmanaged(); - public double PeekDouble() - { - return BitConverter.ToDouble(_data, _position); - } + /// Reads the at the current position without advancing the . + public double PeekDouble() => PeekUnmanaged(); + /// + /// Reads a with a character limit without advancing the . + /// + /// Maximum allowed character count. + /// Strings exceeding are returned as . public string PeekString(int maxLength) { - int bytesCount = BitConverter.ToInt32(_data, _position); - if (bytesCount <= 0 || bytesCount > maxLength * 2) - { - return string.Empty; - } - - int charCount = Encoding.UTF8.GetCharCount(_data, _position + 4, bytesCount); - if (charCount > maxLength) - { + ushort size = PeekUShort(); + if (size == 0) return string.Empty; - } - string result = Encoding.UTF8.GetString(_data, _position + 4, bytesCount); - return result; + int actualSize = size - 1; + return (maxLength > 0 && NetDataWriter.uTF8Encoding.GetCharCount(_data, _position + sizeof(ushort), actualSize) > maxLength) + ? string.Empty + : NetDataWriter.uTF8Encoding.GetString(_data, _position + sizeof(ushort), actualSize); } + /// + /// Reads a without advancing the . + /// public string PeekString() { - int bytesCount = BitConverter.ToInt32(_data, _position); - if (bytesCount <= 0) - { + ushort size = PeekUShort(); + if (size == 0) return string.Empty; - } - string result = Encoding.UTF8.GetString(_data, _position + 4, bytesCount); - return result; + int actualSize = size - 1; + return NetDataWriter.uTF8Encoding.GetString(_data, _position + sizeof(ushort), actualSize); } - #endregion - #region TryGetMethods - public bool TryGetByte(out byte result) + /// + /// Reads an unmanaged value of type at the current position without advancing the . + /// + /// An unmanaged value type. + /// The value read from the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe T PeekUnmanaged() where T : unmanaged { - if (AvailableBytes >= 1) +#if NET8_0_OR_GREATER + return Unsafe.ReadUnaligned(ref _data[_position]); +#else + T value; + fixed (byte* ptr = &_data[_position]) { - result = GetByte(); - return true; + value = *(T*)ptr; } - result = 0; - return false; + return value; +#endif } - public bool TryGetSByte(out sbyte result) + /// + /// Returns a of s containing all remaining data without advancing the reader . + /// + /// A from the current to the end of the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan PeekRemainingBytesSpan() { - if (AvailableBytes >= 1) - { - result = GetSByte(); - return true; - } - result = 0; - return false; + return new ReadOnlySpan(_data, _position, AvailableBytes); } - public bool TryGetBool(out bool result) + /// + /// Returns a of s containing all remaining data without advancing the reader . + /// + /// A from the current to the end of the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyMemory PeekRemainingBytesMemory() { - if (AvailableBytes >= 1) - { - result = GetBool(); - return true; - } - result = false; - return false; + return new ReadOnlyMemory(_data, _position, AvailableBytes); } - public bool TryGetChar(out char result) + /// + /// Reads all remaining s and returns them as a new array without advancing the reader . + /// + /// A new array containing the remaining data. + /// + /// This method performs a heap allocation by copying the data into a new array. + /// + public byte[] PeekRemainingBytes() { - if (AvailableBytes >= 2) - { - result = GetChar(); - return true; - } - result = '\0'; - return false; - } + int size = _dataSize - _position; + if (size == 0) + return Array.Empty(); - public bool TryGetShort(out short result) - { - if (AvailableBytes >= 2) - { - result = GetShort(); - return true; - } - result = 0; - return false; + byte[] result = new byte[size]; + Buffer.BlockCopy(_data, _position, result, 0, size); + return result; } - public bool TryGetUShort(out ushort result) - { - if (AvailableBytes >= 2) - { - result = GetUShort(); - return true; - } - result = 0; - return false; - } + #endregion - public bool TryGetInt(out int result) - { - if (AvailableBytes >= 4) - { - result = GetInt(); - return true; - } - result = 0; - return false; - } + #region TryGetMethods - public bool TryGetUInt(out uint result) + /// Attempts to read a without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetByte(out byte result) => TryGetUnmanaged(out result); + + /// Attempts to read an without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetSByte(out sbyte result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or if failed. + /// if enough data was available; otherwise, . + public bool TryGetBool(out bool result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or '\0' if failed. + /// if enough data was available; otherwise, . + public bool TryGetChar(out char result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetShort(out short result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetUShort(out ushort result) => TryGetUnmanaged(out result); + + /// Attempts to read an without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetInt(out int result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetUInt(out uint result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetLong(out long result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetULong(out ulong result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetFloat(out float result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or 0 if failed. + /// if enough data was available; otherwise, . + public bool TryGetDouble(out double result) => TryGetUnmanaged(out result); + + /// Attempts to read a without throwing an exception. + /// The deserialized , or if failed. + /// if enough data was available; otherwise, . + public bool TryGetString(out string result) { - if (AvailableBytes >= 4) + if (AvailableBytes < sizeof(ushort)) { - result = GetUInt(); - return true; + result = null; + return false; } - result = 0; - return false; - } - public bool TryGetLong(out long result) - { - if (AvailableBytes >= 8) - { - result = GetLong(); - return true; - } - result = 0; - return false; - } + ushort size = PeekUShort(); + int actualSize = size == 0 ? 0 : size - 1; - public bool TryGetULong(out ulong result) - { - if (AvailableBytes >= 8) + if (AvailableBytes < sizeof(ushort) + actualSize) { - result = GetULong(); - return true; + result = null; + return false; } - result = 0; - return false; + + result = GetString(); + return true; } - public bool TryGetFloat(out float result) + /// Attempts to read a array without throwing an exception. + /// The deserialized array, or if failed. + /// if enough data was available; otherwise, . + public bool TryGetStringArray(out string[] result) { - if (AvailableBytes >= 4) + if (AvailableBytes < sizeof(ushort)) { - result = GetFloat(); - return true; + result = null; + return false; } - result = 0; - return false; - } - public bool TryGetDouble(out double result) - { - if (AvailableBytes >= 8) + int startPosition = _position; + + ushort length = GetUShort(); + if (AvailableBytes < checked(length * sizeof(ushort))) // 2 bytes (ushort) for string length { - result = GetDouble(); - return true; + _position = startPosition; // Roll back to the original position + result = null; + return false; } - result = 0; - return false; - } - public bool TryGetString(out string result) - { - if (AvailableBytes >= 4) + string[] values = new string[length]; + for (int i = 0; i < length; i++) { - var bytesCount = PeekInt(); - if (AvailableBytes >= bytesCount + 4) + if (!TryGetString(out values[i])) { - result = GetString(); - return true; + _position = startPosition; // Roll back to the original position + result = null; + return false; } } - result = null; - return false; + + result = values; + return true; } - public bool TryGetStringArray(out string[] result) + /// Attempts to read a array with a length header without throwing an exception. + /// The deserialized array, or if failed. + /// if enough data was available; otherwise, . + public bool TryGetBytesWithLength(out byte[] result) { - ushort size; - if (!TryGetUShort(out size)) + if (AvailableBytes < sizeof(ushort)) { result = null; return false; } - result = new string[size]; - for (int i = 0; i < size; i++) + ushort length = PeekUShort(); + if (AvailableBytes < sizeof(ushort) + length) { - if (!TryGetString(out result[i])) - { - result = null; - return false; - } + result = null; + return false; } + result = GetBytesWithLength(); return true; } - public bool TryGetBytesWithLength(out byte[] result) + /// + /// Attempts to read a value of type from the internal byte buffer at the current position, + /// advancing the position by the size of if successful. + /// + /// An unmanaged value type to read from the buffer. + /// When this method returns, contains the value read from the buffer, or the default value if the read failed. + /// if enough data was available to read the value; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool TryGetUnmanaged(out T result) where T : unmanaged { - if (AvailableBytes >= 4) + int size = sizeof(T); + if (AvailableBytes < size) { - var length = PeekInt(); - if (AvailableBytes >= length + 4) - { - result = GetBytesWithLength(); - return true; - } + result = default; + return false; } - result = null; - return false; + +#if NET8_0_OR_GREATER + result = Unsafe.ReadUnaligned(ref _data[_position]); +#else + fixed (byte* ptr = &_data[_position]) + { + result = *(T*)ptr; + } +#endif + + _position += size; + return true; } + #endregion + /// Clears the reader state and releases the reference to the internal buffer. public void Clear() { _position = 0; + _offset = 0; _dataSize = 0; _data = null; } } } - diff --git a/LiteNetLib/Utils/NetDataWriter.cs b/LiteNetLib/Utils/NetDataWriter.cs index 27268cb7..24f09b8d 100644 --- a/LiteNetLib/Utils/NetDataWriter.cs +++ b/LiteNetLib/Utils/NetDataWriter.cs @@ -1,5 +1,8 @@ -using System; +using System; using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; namespace LiteNetLib.Utils @@ -11,19 +14,59 @@ public class NetDataWriter private const int InitialSize = 64; private readonly bool _autoResize; + private const int IPv4Size = 4; + private const int IPv6Size = 16; + private const int GuidSize = 16; + + /// + /// Gets the total capacity of the internal buffer. + /// + /// The length of the underlying array. public int Capacity { - get { return _data.Length; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _data.Length; } - public NetDataWriter() : this(true, InitialSize) + /// + /// Gets the underlying array used by this writer. + /// + /// The internal array. + /// + /// Accessing this directly allows for external manipulation but bypasses bounds checking. + /// + public byte[] Data { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _data; } - public NetDataWriter(bool autoResize) : this(autoResize, InitialSize) + /// + /// Gets the current number of s written to the buffer. + /// + /// The current . + public int Length { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _position; } + /// + /// Returns a representing the currently used portion of the internal buffer. + /// + /// A from index 0 to . + /// + /// Provides a high-performance, zero-allocation view of the data. + /// The span becomes invalid if the internal buffer is resized or if changes. + /// + public ReadOnlySpan AsReadOnlySpan() => new ReadOnlySpan(_data, 0, _position); + + internal static readonly UTF8Encoding uTF8Encoding = new UTF8Encoding(false, true); + + public NetDataWriter() : this(true, InitialSize) { } + + public NetDataWriter(bool autoResize) : this(autoResize, InitialSize) { } + public NetDataWriter(bool autoResize, int initialSize) { _data = new byte[initialSize]; @@ -43,7 +86,7 @@ public static NetDataWriter FromBytes(byte[] bytes, bool copy) netDataWriter.Put(bytes); return netDataWriter; } - return new NetDataWriter(true, 0) {_data = bytes}; + return new NetDataWriter(true, 0) {_data = bytes, _position = bytes.Length}; } /// @@ -54,11 +97,26 @@ public static NetDataWriter FromBytes(byte[] bytes, bool copy) /// Length of array public static NetDataWriter FromBytes(byte[] bytes, int offset, int length) { - var netDataWriter = new NetDataWriter(true, bytes.Length); + var netDataWriter = new NetDataWriter(true, length); netDataWriter.Put(bytes, offset, length); return netDataWriter; } + /// + /// Creates NetDataWriter from the given . + /// + public static NetDataWriter FromBytes(Span bytes) + { + var netDataWriter = new NetDataWriter(true, bytes.Length); + netDataWriter.Put(bytes); + return netDataWriter; + } + + /// + /// Creates a new and serializes a into it. + /// + /// The to serialize. + /// A new instance containing the serialized . public static NetDataWriter FromString(string value) { var netDataWriter = new NetDataWriter(); @@ -66,28 +124,66 @@ public static NetDataWriter FromString(string value) return netDataWriter; } + /// + /// Ensures the internal buffer is at least . + /// + /// The required minimum size of the buffer. + /// + /// If an allocation is necessary, the buffer grows to either + /// or doubles its current size, whichever is larger. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResizeIfNeed(int newSize) { - int len = _data.Length; - if (len < newSize) + if (_data.Length < newSize) + { + Array.Resize(ref _data, Math.Max(newSize, _data.Length * 2)); + } + } + + /// + /// Ensures the internal buffer can accommodate more s. + /// + /// The number of additional s to fit. + /// + /// This checks against the current . If the capacity is insufficient, + /// the buffer grows to either the required size or doubles its current size. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void EnsureFit(int additionalSize) + { + if (_data.Length < _position + additionalSize) { - while (len < newSize) - len *= 2; - Array.Resize(ref _data, len); + Array.Resize(ref _data, Math.Max(_position + additionalSize, _data.Length * 2)); } } + + /// + /// Resets the to 0 and ensures the internal buffer has at least the specified . + /// + /// The minimum capacity required for the internal buffer. + /// + /// If the current buffer is smaller than , will allocate a larger array. + /// public void Reset(int size) { ResizeIfNeed(size); _position = 0; } - public void Reset() - { - _position = 0; - } + /// + /// Resets the to 0, effectively clearing the buffer for reuse. + /// + public void Reset() => _position = 0; + /// + /// Creates a array containing the current data from the internal buffer. + /// + /// A new array of length . + /// + /// This method performs a heap allocation and copies the data using . + /// public byte[] CopyData() { byte[] resultData = new byte[_position]; @@ -95,277 +191,371 @@ public byte[] CopyData() return resultData; } - public byte[] Data + /// + /// Sets position of NetDataWriter to rewrite previous values + /// + /// new byte position + /// previous position of data writer + public int SetPosition(int position) { - get { return _data; } + int prevPosition = _position; + _position = position; + return prevPosition; } - public int Length - { - get { return _position; } - } + /// + /// Serializes a value. + /// + /// The value to write. + public void Put(float value) => PutUnmanaged(value); - public void Put(float value) - { - if (_autoResize) - ResizeIfNeed(_position + 4); - FastBitConverter.GetBytes(_data, _position, value); - _position += 4; - } + /// + /// Serializes a value. + /// + /// The value to write. + public void Put(double value) => PutUnmanaged(value); - public void Put(double value) - { - if (_autoResize) - ResizeIfNeed(_position + 8); - FastBitConverter.GetBytes(_data, _position, value); - _position += 8; - } + /// + /// Serializes a value. + /// + /// The value to write. + public void Put(long value) => PutUnmanaged(value); - public void Put(long value) - { - if (_autoResize) - ResizeIfNeed(_position + 8); - FastBitConverter.GetBytes(_data, _position, value); - _position += 8; - } + /// + /// Serializes a value. + /// + /// The value to write. + public void Put(ulong value) => PutUnmanaged(value); - public void Put(ulong value) - { - if (_autoResize) - ResizeIfNeed(_position + 8); - FastBitConverter.GetBytes(_data, _position, value); - _position += 8; - } + /// + /// Serializes an value. + /// + /// The value to write. + public void Put(int value) => PutUnmanaged(value); - public void Put(int value) - { - if (_autoResize) - ResizeIfNeed(_position + 4); - FastBitConverter.GetBytes(_data, _position, value); - _position += 4; - } + /// + /// Serializes a value. + /// + /// The value to write. + public void Put(uint value) => PutUnmanaged(value); - public void Put(uint value) - { - if (_autoResize) - ResizeIfNeed(_position + 4); - FastBitConverter.GetBytes(_data, _position, value); - _position += 4; - } + /// + /// Serializes a value as a . + /// + /// The value to write. + public void Put(char value) => PutUnmanaged(value); - public void Put(char value) - { - if (_autoResize) - ResizeIfNeed(_position + 2); - FastBitConverter.GetBytes(_data, _position, value); - _position += 2; - } + /// + /// Serializes a value. + /// + /// The value to write. + public void Put(ushort value) => PutUnmanaged(value); - public void Put(ushort value) - { - if (_autoResize) - ResizeIfNeed(_position + 2); - FastBitConverter.GetBytes(_data, _position, value); - _position += 2; - } + /// + /// Serializes a value. + /// + /// The value to write. + public void Put(short value) => PutUnmanaged(value); - public void Put(short value) - { - if (_autoResize) - ResizeIfNeed(_position + 2); - FastBitConverter.GetBytes(_data, _position, value); - _position += 2; - } + /// + /// Serializes an value. + /// + /// The value to write. + public void Put(sbyte value) => PutUnmanaged(value); - public void Put(sbyte value) - { - if (_autoResize) - ResizeIfNeed(_position + 1); - _data[_position] = (byte)value; - _position++; - } + /// + /// Serializes a value. + /// + /// The value to write. + public void Put(byte value) => PutUnmanaged(value); - public void Put(byte value) + /// + /// Serializes a value. + /// + /// The value to write. + public void Put(Guid value) { if (_autoResize) - ResizeIfNeed(_position + 1); - _data[_position] = value; - _position++; + ResizeIfNeed(_position + GuidSize); + value.TryWriteBytes(_data.AsSpan(_position)); + _position += GuidSize; } + /// + /// Serializes a segment of a array. + /// + /// The source array. + /// The starting index in the source array. + /// The number of s to write. public void Put(byte[] data, int offset, int length) { - if (_autoResize) - ResizeIfNeed(_position + length); - Buffer.BlockCopy(data, offset, _data, _position, length); - _position += length; + Put(data.AsSpan(offset, length)); } + /// + /// Serializes an entire array. + /// + /// The source array. public void Put(byte[] data) + { + Put(data.AsSpan()); + } + + /// + /// Serializes a of s to the internal buffer. + /// + /// The span of data to write. + public void Put(ReadOnlySpan data) { if (_autoResize) ResizeIfNeed(_position + data.Length); - Buffer.BlockCopy(data, 0, _data, _position, data.Length); + data.CopyTo(_data.AsSpan(_position)); _position += data.Length; } - - public void PutSBytesWithLength(sbyte[] data, int offset, int length) + + /// + /// Serializes a segment of an array prefixed with its length. + /// + /// The source array. + /// The starting index in the source array. + /// The number of elements to write. + public void PutSBytesWithLength(sbyte[] data, int offset, ushort length) { if (_autoResize) - ResizeIfNeed(_position + length + 4); + ResizeIfNeed(_position + 2 + length); + FastBitConverter.GetBytes(_data, _position, length); - Buffer.BlockCopy(data, offset, _data, _position + 4, length); - _position += length + 4; - } - - public void PutSBytesWithLength(sbyte[] data) - { - if (_autoResize) - ResizeIfNeed(_position + data.Length + 4); - FastBitConverter.GetBytes(_data, _position, data.Length); - Buffer.BlockCopy(data, 0, _data, _position + 4, data.Length); - _position += data.Length + 4; + _position += 2; + + if (length > 0) + { + ReadOnlySpan source = data.AsSpan(offset, length); + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + + sourceBytes.CopyTo(_data.AsSpan(_position)); + _position += length; + } } - public void PutBytesWithLength(byte[] data, int offset, int length) + /// + /// Serializes an array prefixed with its length. + /// + /// The source array. + public void PutSBytesWithLength(sbyte[] data) => PutArray(data, 1); + + /// + /// Serializes a segment of a array prefixed with its length. + /// + /// The source array. + /// The starting index in the source array. + /// The number of s to write. + public void PutBytesWithLength(byte[] data, int offset, ushort length) { if (_autoResize) - ResizeIfNeed(_position + length + 4); + ResizeIfNeed(_position + 2 + length); + FastBitConverter.GetBytes(_data, _position, length); - Buffer.BlockCopy(data, offset, _data, _position + 4, length); - _position += length + 4; - } + _position += 2; - public void PutBytesWithLength(byte[] data) - { - if (_autoResize) - ResizeIfNeed(_position + data.Length + 4); - FastBitConverter.GetBytes(_data, _position, data.Length); - Buffer.BlockCopy(data, 0, _data, _position + 4, data.Length); - _position += data.Length + 4; + if (length > 0) + { + data.AsSpan(offset, length).CopyTo(_data.AsSpan(_position)); + _position += length; + } } - public void Put(bool value) - { - if (_autoResize) - ResizeIfNeed(_position + 1); - _data[_position] = (byte)(value ? 1 : 0); - _position++; - } + /// + /// Serializes a array prefixed with its length. + /// + /// The source array. + public void PutBytesWithLength(byte[] data) => PutArray(data, 1); - public void PutArray(float[] value) - { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; - if (_autoResize) - ResizeIfNeed(_position + len * 4 + 2); - Put(len); - for (int i = 0; i < len; i++) - Put(value[i]); - } + /// + /// Serializes a value as a single . + /// + /// The value to write. + public void Put(bool value) => Put((byte)(value ? 1 : 0)); - public void PutArray(double[] value) - { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; + /// + /// Serializes an prefixed with a 2-byte length. + /// + /// The source array to serialize. + /// The size of a single element in s. + /// + /// If the array is , a length of 0 is written.
+ /// The total payload size is calculated as length * sz. + ///
+ public void PutArray(Array arr, int sz) + { + ushort length = arr == null ? (ushort)0 : (ushort)arr.Length; + sz *= length; if (_autoResize) - ResizeIfNeed(_position + len * 8 + 2); - Put(len); - for (int i = 0; i < len; i++) - Put(value[i]); + ResizeIfNeed(_position + sz + 2); + FastBitConverter.GetBytes(_data, _position, length); + if (arr != null) + Buffer.BlockCopy(arr, 0, _data, _position + 2, sz); + _position += sz + 2; } - public void PutArray(long[] value) + /// + /// Serializes an array of unmanaged values. + /// + /// The unmanaged type of the array elements. + /// The array to serialize. + public void PutUnmanagedArray(T[] arr) where T : unmanaged { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; - if (_autoResize) - ResizeIfNeed(_position + len * 8 + 2); - Put(len); - for (int i = 0; i < len; i++) - Put(value[i]); + PutSpan(arr.AsSpan()); } - public void PutArray(ulong[] value) + /// + /// Serializes a of unmanaged values to the internal buffer. + /// + /// The unmanaged type of the span elements. + /// The span of data to write. + /// + /// Writes a 2-byte length header followed by the raw binary data.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void PutSpan(Span span) where T : unmanaged { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; - if (_autoResize) - ResizeIfNeed(_position + len * 8 + 2); - Put(len); - for (int i = 0; i < len; i++) - Put(value[i]); - } + var length = (ushort)span.Length; + var byteLength = length * sizeof(T); - public void PutArray(int[] value) - { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; if (_autoResize) - ResizeIfNeed(_position + len * 4 + 2); - Put(len); - for (int i = 0; i < len; i++) - Put(value[i]); - } + ResizeIfNeed(_position + byteLength + 2); - public void PutArray(uint[] value) - { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; - if (_autoResize) - ResizeIfNeed(_position + len * 4 + 2); - Put(len); - for (int i = 0; i < len; i++) - Put(value[i]); - } + FastBitConverter.GetBytes(_data, _position, length); + _position += 2; - public void PutArray(ushort[] value) - { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; - if (_autoResize) - ResizeIfNeed(_position + len * 2 + 2); - Put(len); - for (int i = 0; i < len; i++) - Put(value[i]); + if (length > 0) + { + var sourceBytes = MemoryMarshal.AsBytes(span); + sourceBytes.CopyTo(_data.AsSpan(_position)); + _position += byteLength; + } } - public void PutArray(short[] value) - { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; - if (_autoResize) - ResizeIfNeed(_position + len * 2 + 2); - Put(len); - for (int i = 0; i < len; i++) - Put(value[i]); - } + /// + /// Serializes an array of unmanaged values to the internal buffer. + /// + public void PutArray(float[] value) => PutUnmanagedArray(value); + + /// + public void PutArray(double[] value) => PutUnmanagedArray(value); + + /// + public void PutArray(long[] value) => PutUnmanagedArray(value); - public void PutArray(bool[] value) + /// + public void PutArray(ulong[] value) => PutUnmanagedArray(value); + + /// + public void PutArray(int[] value) => PutUnmanagedArray(value); + + /// + public void PutArray(uint[] value) => PutUnmanagedArray(value); + + /// + public void PutArray(ushort[] value) => PutUnmanagedArray(value); + + /// + public void PutArray(short[] value) => PutUnmanagedArray(value); + + /// + public void PutArray(bool[] value) => PutUnmanagedArray(value); + + /// + /// Serializes an array of values. + /// + /// The array of elements to write. + /// + /// Writes a 2-byte length header followed by each element. + /// + public void PutArray(string[] value) { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; - if (_autoResize) - ResizeIfNeed(_position + len + 2); - Put(len); - for (int i = 0; i < len; i++) + ushort strArrayLength = value == null ? (ushort)0 : (ushort)value.Length; + Put(strArrayLength); + for (int i = 0; i < strArrayLength; i++) Put(value[i]); } - public void PutArray(string[] value) + /// + /// Serializes an array of values with a maximum length constraint per element. + /// + /// The array of elements to write. + /// The maximum allowed length for each individual . + public void PutArray(string[] value, int strMaxLength) { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; - Put(len); - for (int i = 0; i < len; i++) - Put(value[i]); + ushort strArrayLength = value == null ? (ushort)0 : (ushort)value.Length; + Put(strArrayLength); + for (int i = 0; i < strArrayLength; i++) + Put(value[i], strMaxLength); } - public void PutArray(string[] value, int maxLength) + /// + /// Serializes an array of objects implementing . + /// + /// A type that implements and has a parameterless constructor. + /// The array of objects to serialize. + public void PutArray(T[] value) where T : INetSerializable, new() { - ushort len = value == null ? (ushort)0 : (ushort)value.Length; - Put(len); - for (int i = 0; i < len; i++) - Put(value[i], maxLength); + ushort strArrayLength = (ushort)(value?.Length ?? 0); + Put(strArrayLength); + for (int i = 0; i < strArrayLength; i++) + value[i].Serialize(this); } + /// + /// Serializes an . + /// + /// The network endpoint to write. + /// Thrown when the is not or . + /// + /// Writes a (0 for IPv4, 1 for IPv6), followed by the address bytes and a 2-byte port. + /// public void Put(IPEndPoint endPoint) { - Put(endPoint.Address.ToString()); - Put(endPoint.Port); + int addressSize; + byte familyFlag; + + if (endPoint.AddressFamily == AddressFamily.InterNetwork) + { + addressSize = IPv4Size; + familyFlag = 0; + } + else if (endPoint.AddressFamily == AddressFamily.InterNetworkV6) + { + addressSize = IPv6Size; + familyFlag = 1; + } + else + { + throw new ArgumentException($"Unsupported address family: {endPoint.AddressFamily}"); + } + + if (_autoResize) + ResizeIfNeed(_position + 1 + addressSize + 2); + + _data[_position++] = familyFlag; + + Span destination = _data.AsSpan(_position, addressSize); + if (!endPoint.Address.TryWriteBytes(destination, out int written)) + throw new ArgumentException("Failed to write IP bytes."); + + _position += written; + + Put((ushort)endPoint.Port); } - public void Put(string value) + /// + /// Serializes a using a 4-byte length header. + /// + /// The to write. + /// + /// Recommended for strings that may exceed the 65535 byte limit of standard length headers.
+ /// Uses . + ///
+ public void PutLargeString(string value) { if (string.IsNullOrEmpty(value)) { @@ -373,43 +563,119 @@ public void Put(string value) return; } - //put bytes count - int bytesCount = Encoding.UTF8.GetByteCount(value); + int size = uTF8Encoding.GetByteCount(value); + Put(size); + if (_autoResize) - ResizeIfNeed(_position + bytesCount + 4); - Put(bytesCount); + ResizeIfNeed(_position + size); - //put string - Encoding.UTF8.GetBytes(value, 0, value.Length, _data, _position); - _position += bytesCount; + uTF8Encoding.GetBytes(value, 0, value.Length, _data, _position); + _position += size; } - public void Put(string value, int maxLength) + /// + /// Serializes a string using a 2-byte length header. + /// + /// The string to write to the buffer. + /// + /// The maximum number of characters to write. If the string is longer, it will be truncated.
+ /// A value of 0 indicates no limit. + /// + /// + /// Note that limits the number of characters, not the total size in s.
+ /// Uses . + ///
+ public void Put(string value, int maxLength = 0) { if (string.IsNullOrEmpty(value)) { - Put(0); + Put((ushort)0); return; } - int length = value.Length > maxLength ? maxLength : value.Length; - //calculate max count - int bytesCount = Encoding.UTF8.GetByteCount(value); + ReadOnlySpan source = value.AsSpan(); + if (maxLength > 0 && source.Length > maxLength) + { + source = source.Slice(0, maxLength); + } + + int maxSize = uTF8Encoding.GetMaxByteCount(source.Length); if (_autoResize) - ResizeIfNeed(_position + bytesCount + 4); + ResizeIfNeed(_position + maxSize + sizeof(ushort)); + int size = uTF8Encoding.GetBytes(source, _data.AsSpan(_position + sizeof(ushort))); + if (size == 0) + { + Put((ushort)0); + return; + } + Put(checked((ushort)(size + 1))); + _position += size; + } - //put bytes count - Put(bytesCount); + /// + /// Writes a value of type into the internal byte buffer at the current position, + /// advancing the position by the size of . + /// + /// An unmanaged value type to write into the buffer. + /// The value to write into the buffer. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void PutUnmanaged(T value) where T : unmanaged + { + int size = sizeof(T); + if (_autoResize) + ResizeIfNeed(_position + size); + FastBitConverter.GetBytes(_data, _position, value); + _position += size; + } - //put string - Encoding.UTF8.GetBytes(value, 0, length, _data, _position); + /// + /// Writes a nullable value of type into the internal byte buffer at the current position, + /// first writing a indicating whether the value is present, + /// and then writing the value itself if it exists.
Advances the position by 1 byte for the presence flag plus + /// the size of if the value is present. + ///
+ /// An unmanaged value type to write into the buffer. + /// The nullable value to write into the buffer. If , only a flag is written. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PutNullableUnmanaged(T? value) where T : unmanaged + { + bool hasValue = value.HasValue; + Put(hasValue); + if (!hasValue) + { + return; + } - _position += bytesCount; + PutUnmanaged(value.Value); } - public void Put(T obj) where T : INetSerializable + /// + /// Writes an enum value of type to the internal data buffer at the current position.
+ /// Automatically resizes the buffer if is enabled. + /// Advances the position by the size of . + ///
+ /// An unmanaged enum type to write. + /// The enum value to write. + public unsafe void PutEnum(T value) where T : unmanaged, Enum { - obj.Serialize(this); + var size = sizeof(T); + if (_autoResize) + { + ResizeIfNeed(_position + size); + } + + FastBitConverter.GetBytes(_data, _position, value); + _position += size; } + + /// + /// Serializes an object implementing . + /// + /// A type that implements the interface. + /// The object instance to serialize. + /// + /// This method calls the method on the provided . + /// + public void Put(T obj) where T : INetSerializable => obj.Serialize(this); } } diff --git a/LiteNetLib/Utils/NetPacketProcessor.cs b/LiteNetLib/Utils/NetPacketProcessor.cs index 852b0f6a..e3d8d981 100644 --- a/LiteNetLib/Utils/NetPacketProcessor.cs +++ b/LiteNetLib/Utils/NetPacketProcessor.cs @@ -1,16 +1,32 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace LiteNetLib.Utils { public class NetPacketProcessor { + private static class HashCache + { + public static readonly ulong Id; + + //FNV-1 64 bit hash + static HashCache() + { + ulong hash = 14695981039346656037UL; //offset + string typeName = typeof(T).ToString(); + for (var i = 0; i < typeName.Length; i++) + { + hash ^= typeName[i]; + hash *= 1099511628211UL; //prime + } + Id = hash; + } + } + protected delegate void SubscribeDelegate(NetDataReader reader, object userData); - private readonly Dictionary _hashCache = new Dictionary(); - private readonly char[] _hashBuffer = new char[1024]; private readonly NetSerializer _netSerializer; private readonly Dictionary _callbacks = new Dictionary(); - private readonly NetDataWriter _netDataWriter = new NetDataWriter(); public NetPacketProcessor() { @@ -22,50 +38,33 @@ public NetPacketProcessor(int maxStringLength) _netSerializer = new NetSerializer(maxStringLength); } - //FNV-1 64 bit hash - protected virtual ulong GetHash(Type type) + protected virtual ulong GetHash() { - ulong hash; - string typeName = type.FullName; - if (_hashCache.TryGetValue(typeName, out hash)) - { - return hash; - } - hash = 14695981039346656037UL; //offset - typeName.CopyTo(0, _hashBuffer, 0, typeName.Length); - for (var i = 0; i < typeName.Length; i++) - { - hash = hash ^ _hashBuffer[i]; - hash *= 1099511628211UL; //prime - } - _hashCache.Add(typeName, hash); - return hash; + return HashCache.Id; } protected virtual SubscribeDelegate GetCallbackFromData(NetDataReader reader) { - var hash = reader.GetULong(); - SubscribeDelegate action; - if (!_callbacks.TryGetValue(hash, out action)) + ulong hash = reader.GetULong(); + if (!_callbacks.TryGetValue(hash, out var action)) { throw new ParseException("Undefined packet in NetDataReader"); } return action; } - protected virtual void WriteHash(Type type, NetDataWriter writer) + protected virtual void WriteHash(NetDataWriter writer) { - writer.Put(GetHash(type)); + writer.Put(GetHash()); } /// /// Register nested property type /// /// INetSerializable structure - /// True - if register successful, false - if type already registered - public bool RegisterNestedType() where T : struct, INetSerializable + public void RegisterNestedType() where T : struct, INetSerializable { - return _netSerializer.RegisterNestedType(); + _netSerializer.RegisterNestedType(); } /// @@ -73,20 +72,18 @@ public bool RegisterNestedType() where T : struct, INetSerializable /// /// /// - /// True - if register successful, false - if type already registered - public bool RegisterNestedType(Action writeDelegate, Func readDelegate) + public void RegisterNestedType(Action writeDelegate, Func readDelegate) { - return _netSerializer.RegisterNestedType(writeDelegate, readDelegate); + _netSerializer.RegisterNestedType(writeDelegate, readDelegate); } /// /// Register nested property type /// /// INetSerializable class - /// True - if register successful, false - if type already registered - public bool RegisterNestedType(Func constructor) where T : class, INetSerializable + public void RegisterNestedType(Func constructor) where T : class, INetSerializable { - return _netSerializer.RegisterNestedType(constructor); + _netSerializer.RegisterNestedType(constructor); } /// @@ -96,9 +93,7 @@ public bool RegisterNestedType(Func constructor) where T : class, INetSeri public void ReadAllPackets(NetDataReader reader) { while (reader.AvailableBytes > 0) - { ReadPacket(reader); - } } /// @@ -110,9 +105,7 @@ public void ReadAllPackets(NetDataReader reader) public void ReadAllPackets(NetDataReader reader, object userData) { while (reader.AvailableBytes > 0) - { ReadPacket(reader, userData); - } } /// @@ -125,62 +118,22 @@ public void ReadPacket(NetDataReader reader) ReadPacket(reader, null); } - public void Send(NetPeer peer, T packet, DeliveryMethod options) where T : class, new() - { - _netDataWriter.Reset(); - Write(_netDataWriter, packet); - peer.Send(_netDataWriter, options); - } - - public void SendNetSerializable(NetPeer peer, T packet, DeliveryMethod options) where T : INetSerializable - { - _netDataWriter.Reset(); - WriteNetSerializable(_netDataWriter, packet); - peer.Send(_netDataWriter, options); - } - - public void Send(NetManager manager, T packet, DeliveryMethod options) where T : class, new() + public void Write< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>(NetDataWriter writer, T packet) where T : class, new() { - _netDataWriter.Reset(); - Write(_netDataWriter, packet); - manager.SendToAll(_netDataWriter, options); - } - - public void SendNetSerializable(NetManager manager, T packet, DeliveryMethod options) where T : INetSerializable - { - _netDataWriter.Reset(); - WriteNetSerializable(_netDataWriter, packet); - manager.SendToAll(_netDataWriter, options); - } - - public void Write(NetDataWriter writer, T packet) where T : class, new() - { - WriteHash(typeof(T), writer); + WriteHash(writer); _netSerializer.Serialize(writer, packet); } - public void WriteNetSerializable(NetDataWriter writer, T packet) where T : INetSerializable + public void WriteNetSerializable(NetDataWriter writer, ref T packet) where T : INetSerializable { - WriteHash(typeof(T), writer); + WriteHash(writer); packet.Serialize(writer); } - public byte[] Write(T packet) where T : class, new() - { - _netDataWriter.Reset(); - WriteHash(typeof(T), _netDataWriter); - _netSerializer.Serialize(_netDataWriter, packet); - return _netDataWriter.CopyData(); - } - - public byte[] WriteNetSerializable(T packet) where T : INetSerializable - { - _netDataWriter.Reset(); - WriteHash(typeof(T), _netDataWriter); - packet.Serialize(_netDataWriter); - return _netDataWriter.CopyData(); - } - /// /// Reads one packet from NetDataReader and calls OnReceive delegate /// @@ -196,12 +149,16 @@ public void ReadPacket(NetDataReader reader, object userData) /// Register and subscribe to packet receive event /// /// event that will be called when packet deserialized with ReadPacket method - /// Method that constructs packet intead of slow Activator.CreateInstance + /// Method that constructs packet instead of slow Activator.CreateInstance /// 's fields are not supported, or it has no fields - public void Subscribe(Action onReceive, Func packetConstructor) where T : class, new() + public void Subscribe< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>(Action onReceive, Func packetConstructor) where T : class, new() { _netSerializer.Register(); - _callbacks[GetHash(typeof(T))] = (reader, userData) => + _callbacks[GetHash()] = (reader, userData) => { var reference = packetConstructor(); _netSerializer.Deserialize(reader, reference); @@ -213,12 +170,16 @@ public void ReadPacket(NetDataReader reader, object userData) /// Register and subscribe to packet receive event (with userData) /// /// event that will be called when packet deserialized with ReadPacket method - /// Method that constructs packet intead of slow Activator.CreateInstance + /// Method that constructs packet instead of slow Activator.CreateInstance /// 's fields are not supported, or it has no fields - public void Subscribe(Action onReceive, Func packetConstructor) where T : class, new() + public void Subscribe< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T, TUserData>(Action onReceive, Func packetConstructor) where T : class, new() { _netSerializer.Register(); - _callbacks[GetHash(typeof(T))] = (reader, userData) => + _callbacks[GetHash()] = (reader, userData) => { var reference = packetConstructor(); _netSerializer.Deserialize(reader, reference); @@ -228,15 +189,19 @@ public void ReadPacket(NetDataReader reader, object userData) /// /// Register and subscribe to packet receive event - /// This metod will overwrite last received packet class on receive (less garbage) + /// This method will overwrite last received packet class on receive (less garbage) /// /// event that will be called when packet deserialized with ReadPacket method /// 's fields are not supported, or it has no fields - public void SubscribeReusable(Action onReceive) where T : class, new() + public void SubscribeReusable< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>(Action onReceive) where T : class, new() { _netSerializer.Register(); var reference = new T(); - _callbacks[GetHash(typeof(T))] = (reader, userData) => + _callbacks[GetHash()] = (reader, userData) => { _netSerializer.Deserialize(reader, reference); onReceive(reference); @@ -245,26 +210,37 @@ public void ReadPacket(NetDataReader reader, object userData) /// /// Register and subscribe to packet receive event - /// This metod will overwrite last received packet class on receive (less garbage) + /// This method will overwrite last received packet class on receive (less garbage) /// /// event that will be called when packet deserialized with ReadPacket method /// 's fields are not supported, or it has no fields - public void SubscribeReusable(Action onReceive) where T : class, new() + public void SubscribeReusable< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T, TUserData>(Action onReceive) where T : class, new() { _netSerializer.Register(); var reference = new T(); - _callbacks[GetHash(typeof(T))] = (reader, userData) => + _callbacks[GetHash()] = (reader, userData) => { _netSerializer.Deserialize(reader, reference); onReceive(reference, (TUserData)userData); }; } + /// + /// Registers a callback for a packet type that implements , using a custom constructor and supporting user data. + /// + /// The type of the packet. Must implement . + /// The type of the user data (typically ). + /// The delegate to be executed when the packet is received. + /// A function that returns a new instance of . public void SubscribeNetSerializable( - Action onReceive, + Action onReceive, Func packetConstructor) where T : INetSerializable { - _callbacks[GetHash(typeof(T))] = (reader, userData) => + _callbacks[GetHash()] = (reader, userData) => { var pkt = packetConstructor(); pkt.Deserialize(reader); @@ -272,11 +248,17 @@ public void SubscribeNetSerializable( }; } + /// + /// Registers a callback for a packet type that implements , using a custom constructor. + /// + /// The type of the packet. Must implement . + /// The delegate to be executed when the packet is received. + /// A function that returns a new instance of . public void SubscribeNetSerializable( Action onReceive, Func packetConstructor) where T : INetSerializable { - _callbacks[GetHash(typeof(T))] = (reader, userData) => + _callbacks[GetHash()] = (reader, userData) => { var pkt = packetConstructor(); pkt.Deserialize(reader); @@ -284,22 +266,39 @@ public void SubscribeNetSerializable( }; } + /// + /// Registers a callback for a packet type that implements and has a parameterless constructor, supporting user data. + /// + /// + /// To reduce allocations, this method uses a single internal reference to for deserialization. + /// + /// The type of the packet. Must implement and have a () constraint. + /// The type of the user data (typically ). + /// The delegate to be executed when the packet is received. public void SubscribeNetSerializable( Action onReceive) where T : INetSerializable, new() { var reference = new T(); - _callbacks[GetHash(typeof(T))] = (reader, userData) => + _callbacks[GetHash()] = (reader, userData) => { reference.Deserialize(reader); onReceive(reference, (TUserData)userData); }; } + /// + /// Registers a callback for a packet type that implements and has a parameterless constructor. + /// + /// + /// To reduce allocations, this method uses a single internal reference to for deserialization. + /// + /// The type of the packet. Must implement and have a () constraint. + /// The delegate to be executed when the packet is received. public void SubscribeNetSerializable( Action onReceive) where T : INetSerializable, new() { var reference = new T(); - _callbacks[GetHash(typeof(T))] = (reader, userData) => + _callbacks[GetHash()] = (reader, userData) => { reference.Deserialize(reader); onReceive(reference); @@ -313,7 +312,7 @@ public void SubscribeNetSerializable( /// true if remove is success public bool RemoveSubscription() { - return _callbacks.Remove(GetHash(typeof(T))); + return _callbacks.Remove(GetHash()); } } } diff --git a/LiteNetLib/Utils/NetSerializer.cs b/LiteNetLib/Utils/NetSerializer.cs index 57476a48..fae854a7 100644 --- a/LiteNetLib/Utils/NetSerializer.cs +++ b/LiteNetLib/Utils/NetSerializer.cs @@ -1,560 +1,685 @@ using System; using System.Reflection; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net; - -#if NETSTANDARD2_0 || NETCOREAPP2_0 -using System.Linq; -#endif +using System.Runtime.Serialization; namespace LiteNetLib.Utils { public class InvalidTypeException : ArgumentException { - public InvalidTypeException() + public InvalidTypeException(string message) : base(message) { } + } + + public class ParseException : Exception + { + public ParseException(string message) : base(message) { } + } + + public class NetSerializer + { + private enum CallType { + Basic, + Array, + List } - public InvalidTypeException(string message) : base(message) + private abstract class FastCall { + public CallType Type; + public virtual void Init(MethodInfo getMethod, MethodInfo setMethod, CallType type) { Type = type; } + public abstract void Read(T inf, NetDataReader r); + public abstract void Write(T inf, NetDataWriter w); + public abstract void ReadArray(T inf, NetDataReader r); + public abstract void WriteArray(T inf, NetDataWriter w); + public abstract void ReadList(T inf, NetDataReader r); + public abstract void WriteList(T inf, NetDataWriter w); } - public InvalidTypeException(string message, Exception innerException) : base(message, innerException) + private abstract class FastCallSpecific : FastCall { + protected Func Getter; + protected Action Setter; + protected Func GetterArr; + protected Action SetterArr; + protected Func> GetterList; + protected Action> SetterList; + + public override void ReadArray(TClass inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: " + typeof(TProperty) + "[]"); } + public override void WriteArray(TClass inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: " + typeof(TProperty) + "[]"); } + public override void ReadList(TClass inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: List<" + typeof(TProperty) + ">"); } + public override void WriteList(TClass inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: List<" + typeof(TProperty) + ">"); } + + protected TProperty[] ReadArrayHelper(TClass inf, NetDataReader r) + { + ushort count = r.GetUShort(); + var arr = GetterArr(inf); + arr = arr == null || arr.Length != count ? new TProperty[count] : arr; + SetterArr(inf, arr); + return arr; + } + + protected TProperty[] WriteArrayHelper(TClass inf, NetDataWriter w) + { + var arr = GetterArr(inf); + w.Put((ushort)arr.Length); + return arr; + } + + protected List ReadListHelper(TClass inf, NetDataReader r, out int len) + { + len = r.GetUShort(); + var list = GetterList(inf); + if (list == null) + { + list = new List(len); + SetterList(inf, list); + } + return list; + } + + protected List WriteListHelper(TClass inf, NetDataWriter w, out int len) + { + var list = GetterList(inf); + if (list == null) + { + len = 0; + w.Put(0); + return null; + } + len = list.Count; + w.Put((ushort)len); + return list; + } + + public override void Init(MethodInfo getMethod, MethodInfo setMethod, CallType type) + { + base.Init(getMethod, setMethod, type); + switch (type) + { + case CallType.Array: + GetterArr = (Func)Delegate.CreateDelegate(typeof(Func), getMethod); + SetterArr = (Action)Delegate.CreateDelegate(typeof(Action), setMethod); + break; + case CallType.List: + GetterList = (Func>)Delegate.CreateDelegate(typeof(Func>), getMethod); + SetterList = (Action>)Delegate.CreateDelegate(typeof(Action>), setMethod); + break; + default: + Getter = (Func)Delegate.CreateDelegate(typeof(Func), getMethod); + Setter = (Action)Delegate.CreateDelegate(typeof(Action), setMethod); + break; + } + } } - public InvalidTypeException(string message, string paramName) : base(message, paramName) + private abstract class FastCallSpecificAuto : FastCallSpecific { + protected abstract void ElementRead(NetDataReader r, out TProperty prop); + protected abstract void ElementWrite(NetDataWriter w, ref TProperty prop); + + public override void Read(TClass inf, NetDataReader r) + { + ElementRead(r, out var elem); + Setter(inf, elem); + } + + public override void Write(TClass inf, NetDataWriter w) + { + var elem = Getter(inf); + ElementWrite(w, ref elem); + } + + public override void ReadArray(TClass inf, NetDataReader r) + { + var arr = ReadArrayHelper(inf, r); + for (int i = 0; i < arr.Length; i++) + ElementRead(r, out arr[i]); + } + + public override void WriteArray(TClass inf, NetDataWriter w) + { + var arr = WriteArrayHelper(inf, w); + for (int i = 0; i < arr.Length; i++) + ElementWrite(w, ref arr[i]); + } } - public InvalidTypeException(string message, string paramName, Exception innerException) : base(message, paramName, innerException) + private sealed class FastCallStatic : FastCallSpecific { + private readonly Action _writer; + private readonly Func _reader; + + public FastCallStatic(Action write, Func read) + { + _writer = write; + _reader = read; + } + + public override void Read(TClass inf, NetDataReader r) { Setter(inf, _reader(r)); } + public override void Write(TClass inf, NetDataWriter w) { _writer(w, Getter(inf)); } + + public override void ReadList(TClass inf, NetDataReader r) + { + var list = ReadListHelper(inf, r, out int len); + int listCount = list.Count; + for (int i = 0; i < len; i++) + { + if (i < listCount) + list[i] = _reader(r); + else + list.Add(_reader(r)); + } + if (len < listCount) + list.RemoveRange(len, listCount - len); + } + + public override void WriteList(TClass inf, NetDataWriter w) + { + var list = WriteListHelper(inf, w, out int len); + for (int i = 0; i < len; i++) + _writer(w, list[i]); + } + + public override void ReadArray(TClass inf, NetDataReader r) + { + var arr = ReadArrayHelper(inf, r); + int len = arr.Length; + for (int i = 0; i < len; i++) + arr[i] = _reader(r); + } + + public override void WriteArray(TClass inf, NetDataWriter w) + { + var arr = WriteArrayHelper(inf, w); + int len = arr.Length; + for (int i = 0; i < len; i++) + _writer(w, arr[i]); + } } - } - public class ParseException : Exception - { - public ParseException() + private sealed class FastCallStruct : FastCallSpecific where TProperty : struct, INetSerializable { + private TProperty _p; + + public override void Read(TClass inf, NetDataReader r) + { + _p.Deserialize(r); + Setter(inf, _p); + } + + public override void Write(TClass inf, NetDataWriter w) + { + _p = Getter(inf); + _p.Serialize(w); + } + + public override void ReadList(TClass inf, NetDataReader r) + { + var list = ReadListHelper(inf, r, out int len); + int listCount = list.Count; + for (int i = 0; i < len; i++) + { + var itm = default(TProperty); + itm.Deserialize(r); + if(i < listCount) + list[i] = itm; + else + list.Add(itm); + } + if (len < listCount) + list.RemoveRange(len, listCount - len); + } + + public override void WriteList(TClass inf, NetDataWriter w) + { + var list = WriteListHelper(inf, w, out int len); + for (int i = 0; i < len; i++) + list[i].Serialize(w); + } + + public override void ReadArray(TClass inf, NetDataReader r) + { + var arr = ReadArrayHelper(inf, r); + int len = arr.Length; + for (int i = 0; i < len; i++) + arr[i].Deserialize(r); + } + + public override void WriteArray(TClass inf, NetDataWriter w) + { + var arr = WriteArrayHelper(inf, w); + int len = arr.Length; + for (int i = 0; i < len; i++) + arr[i].Serialize(w); + } } - public ParseException(string message) : base(message) + private sealed class FastCallClass : FastCallSpecific where TProperty : class, INetSerializable { + private readonly Func _constructor; + public FastCallClass(Func constructor) { _constructor = constructor; } + + public override void Read(TClass inf, NetDataReader r) + { + var p = _constructor(); + p.Deserialize(r); + Setter(inf, p); + } + + public override void Write(TClass inf, NetDataWriter w) + { + var p = Getter(inf); + p?.Serialize(w); + } + + public override void ReadList(TClass inf, NetDataReader r) + { + var list = ReadListHelper(inf, r, out int len); + int listCount = list.Count; + for (int i = 0; i < len; i++) + { + if (i < listCount) + { + list[i].Deserialize(r); + } + else + { + var itm = _constructor(); + itm.Deserialize(r); + list.Add(itm); + } + } + if (len < listCount) + list.RemoveRange(len, listCount - len); + } + + public override void WriteList(TClass inf, NetDataWriter w) + { + var list = WriteListHelper(inf, w, out int len); + for (int i = 0; i < len; i++) + list[i].Serialize(w); + } + + public override void ReadArray(TClass inf, NetDataReader r) + { + var arr = ReadArrayHelper(inf, r); + int len = arr.Length; + for (int i = 0; i < len; i++) + { + arr[i] = _constructor(); + arr[i].Deserialize(r); + } + } + + public override void WriteArray(TClass inf, NetDataWriter w) + { + var arr = WriteArrayHelper(inf, w); + int len = arr.Length; + for (int i = 0; i < len; i++) + arr[i].Serialize(w); + } } - public ParseException(string message, Exception innerException) : base(message, innerException) + private class IntSerializer : FastCallSpecific { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetInt()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetIntArray()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } } - } - - public sealed class NetSerializer - { - private sealed class NestedType + + private class UIntSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetUInt()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetUIntArray()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } + } + + private class ShortSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetShort()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetShortArray()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } + } + + private class UShortSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetUShort()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetUShortArray()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } + } + + private class LongSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetLong()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetLongArray()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } + } + + private class ULongSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetULong()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetULongArray()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } + } + + private class ByteSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetByte()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetBytesWithLength()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutBytesWithLength(GetterArr(inf)); } + } + + private class SByteSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetSByte()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetSBytesWithLength()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutSBytesWithLength(GetterArr(inf)); } + } + + private class FloatSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetFloat()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetFloatArray()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } + } + + private class DoubleSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetDouble()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetDoubleArray()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } + } + + private class BoolSerializer : FastCallSpecific + { + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetBool()); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetBoolArray()); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } + } + + private class CharSerializer : FastCallSpecificAuto + { + protected override void ElementWrite(NetDataWriter w, ref char prop) { w.Put(prop); } + protected override void ElementRead(NetDataReader r, out char prop) { prop = r.GetChar(); } + } + + private class IPEndPointSerializer : FastCallSpecificAuto + { + protected override void ElementWrite(NetDataWriter w, ref IPEndPoint prop) { w.Put(prop); } + protected override void ElementRead(NetDataReader r, out IPEndPoint prop) { prop = r.GetIPEndPoint(); } + } + + private class GuidSerializer : FastCallSpecificAuto { - public readonly NestedTypeWriter WriteDelegate; - public readonly NestedTypeReader ReadDelegate; - public readonly NestedTypeWriter ArrayWriter; - public readonly NestedTypeReader ArrayReader; + protected override void ElementWrite(NetDataWriter w, ref Guid guid) { w.Put(guid); } + protected override void ElementRead(NetDataReader r, out Guid guid) { guid = r.GetGuid(); } + } - public NestedType(NestedTypeWriter writeDelegate, NestedTypeReader readDelegate, NestedTypeWriter arrayWriter, NestedTypeReader arrayReader) + private class StringSerializer : FastCallSpecific + { + private readonly int _maxLength; + public StringSerializer(int maxLength) { _maxLength = maxLength > 0 ? maxLength : short.MaxValue; } + public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetString(_maxLength)); } + public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf), _maxLength); } + public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetStringArray(_maxLength)); } + public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf), _maxLength); } + } + + private class EnumByteSerializer : FastCall + { + protected readonly PropertyInfo Property; + protected readonly Type PropertyType; + public EnumByteSerializer(PropertyInfo property, Type propertyType) { - WriteDelegate = writeDelegate; - ReadDelegate = readDelegate; - ArrayWriter = arrayWriter; - ArrayReader = arrayReader; + Property = property; + PropertyType = propertyType; } + public override void Read(T inf, NetDataReader r) { Property.SetValue(inf, Enum.ToObject(PropertyType, r.GetByte()), null); } + public override void Write(T inf, NetDataWriter w) { w.Put((byte)Property.GetValue(inf, null)); } + public override void ReadArray(T inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: Enum[]"); } + public override void WriteArray(T inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: Enum[]"); } + public override void ReadList(T inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: List"); } + public override void WriteList(T inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: List"); } } - private delegate void NestedTypeWriter(NetDataWriter writer, object customObj); - private delegate object NestedTypeReader(NetDataReader reader); + private class EnumIntSerializer : EnumByteSerializer + { + public EnumIntSerializer(PropertyInfo property, Type propertyType) : base(property, propertyType) { } + public override void Read(T inf, NetDataReader r) { Property.SetValue(inf, Enum.ToObject(PropertyType, r.GetInt()), null); } + public override void Write(T inf, NetDataWriter w) { w.Put((int)Property.GetValue(inf, null)); } + } private sealed class ClassInfo { public static ClassInfo Instance; - public readonly Action[] WriteDelegate; - public readonly Action[] ReadDelegate; + private readonly FastCall[] _serializers; private readonly int _membersCount; - public ClassInfo(int membersCount) + public ClassInfo(List> serializers) { - _membersCount = membersCount; - WriteDelegate = new Action[membersCount]; - ReadDelegate = new Action[membersCount]; + _membersCount = serializers.Count; + _serializers = serializers.ToArray(); } public void Write(T obj, NetDataWriter writer) { for (int i = 0; i < _membersCount; i++) - WriteDelegate[i](obj, writer); + { + var s = _serializers[i]; + if (s.Type == CallType.Basic) + s.Write(obj, writer); + else if (s.Type == CallType.Array) + s.WriteArray(obj, writer); + else + s.WriteList(obj, writer); + } } public void Read(T obj, NetDataReader reader) { for (int i = 0; i < _membersCount; i++) - ReadDelegate[i](obj, reader); + { + var s = _serializers[i]; + if (s.Type == CallType.Basic) + s.Read(obj, reader); + else if(s.Type == CallType.Array) + s.ReadArray(obj, reader); + else + s.ReadList(obj, reader); + } } } - private static readonly HashSet BasicTypes = new HashSet + private abstract class CustomType { - typeof(int), - typeof(uint), - typeof(byte), - typeof(sbyte), - typeof(short), - typeof(ushort), - typeof(long), - typeof(ulong), - typeof(string), - typeof(float), - typeof(double), - typeof(bool), - typeof(char), - typeof(IPEndPoint) - }; - - private readonly NetDataWriter _writer; - private readonly int _maxStringLength; - private readonly Dictionary _registeredNestedTypes; + public abstract FastCall Get(); + } - public NetSerializer() : this(0) + private sealed class CustomTypeStruct : CustomType where TProperty : struct, INetSerializable { - + public override FastCall Get() { return new FastCallStruct(); } } - public NetSerializer(int maxStringLength) + private sealed class CustomTypeClass : CustomType where TProperty : class, INetSerializable { - _maxStringLength = maxStringLength; - _registeredNestedTypes = new Dictionary(); - _writer = new NetDataWriter(); + private readonly Func _constructor; + public CustomTypeClass(Func constructor) { _constructor = constructor; } + public override FastCall Get() { return new FastCallClass(_constructor); } } - private bool RegisterNestedTypeInternal(Func constructor) where T : INetSerializable + private sealed class CustomTypeStatic : CustomType { - var t = typeof(T); - if (_registeredNestedTypes.ContainsKey(t)) - return false; - NestedType nestedType; - NestedTypeWriter nestedTypeWriter = (writer, obj) => ((T) obj).Serialize(writer); - NestedTypeWriter nestedTypeArrayWriter = (writer, arr) => - { - var typedArr = (T[]) arr; - writer.Put((ushort) typedArr.Length); - for (int i = 0; i < typedArr.Length; i++) - typedArr[i].Serialize(writer); - }; - - //struct - if (constructor == null) - { - nestedType = new NestedType( - nestedTypeWriter, - reader => - { - var instance = default(T); - instance.Deserialize(reader); - return instance; - }, - nestedTypeArrayWriter, - reader => - { - var typedArr = new T[reader.GetUShort()]; - for (int i = 0; i < typedArr.Length; i++) - typedArr[i].Deserialize(reader); - return typedArr; - }); - } - else //class + private readonly Action _writer; + private readonly Func _reader; + public CustomTypeStatic(Action writer, Func reader) { - nestedType = new NestedType( - nestedTypeWriter, - reader => - { - var instance = constructor(); - instance.Deserialize(reader); - return instance; - }, - nestedTypeArrayWriter, - reader => - { - var typedArr = new T[reader.GetUShort()]; - for (int i = 0; i < typedArr.Length; i++) - { - typedArr[i] = constructor(); - typedArr[i].Deserialize(reader); - } - return typedArr; - }); - } - _registeredNestedTypes.Add(t, nestedType); - return true; + _writer = writer; + _reader = reader; + } + public override FastCall Get() { return new FastCallStatic(_writer, _reader); } } /// - /// Register nested property type + /// Register custom property type /// /// INetSerializable structure - /// True - if register successful, false - if type already registered - public bool RegisterNestedType() where T : struct, INetSerializable + public void RegisterNestedType() where T : struct, INetSerializable { - return RegisterNestedTypeInternal(null); + _registeredTypes.Add(typeof(T), new CustomTypeStruct()); } /// - /// Register nested property type + /// Register custom property type /// /// INetSerializable class - /// True - if register successful, false - if type already registered - public bool RegisterNestedType(Func constructor) where T : class, INetSerializable + public void RegisterNestedType(Func constructor) where T : class, INetSerializable { - return RegisterNestedTypeInternal(constructor); + _registeredTypes.Add(typeof(T), new CustomTypeClass(constructor)); } /// - /// Register nested property type + /// Register custom property type /// - /// - /// - /// True - if register successful, false - if type already registered - public bool RegisterNestedType(Action writeDelegate, Func readDelegate) + /// Any packet + /// custom type writer + /// custom type reader + public void RegisterNestedType(Action writer, Func reader) { - var t = typeof(T); - if (BasicTypes.Contains(t) || _registeredNestedTypes.ContainsKey(t)) - return false; - - var rwDelegates = new NestedType( - (writer, obj) => writeDelegate(writer, (T)obj), - reader => readDelegate(reader), - (writer, arr) => - { - var typedArr = (T[])arr; - writer.Put((ushort)typedArr.Length); - for (int i = 0; i < typedArr.Length; i++) - writeDelegate(writer, typedArr[i]); - }, - reader => - { - var typedArr = new T[reader.GetUShort()]; - for (int i = 0; i < typedArr.Length; i++) - typedArr[i] = readDelegate(reader); - return typedArr; - }); - - _registeredNestedTypes.Add(t, rwDelegates); - return true; + _registeredTypes.Add(typeof(T), new CustomTypeStatic(writer, reader)); } - private static Delegate CreateDelegate(Type type, MethodInfo info) - { -#if NETSTANDARD2_0 || NETCOREAPP2_0 - return info.CreateDelegate(type); -#else - return Delegate.CreateDelegate(type, info); -#endif - } + private NetDataWriter _writer; + private readonly int _maxStringLength; + private readonly Dictionary _registeredTypes = new Dictionary(); - private static Func ExtractGetDelegate(MethodInfo info) + public NetSerializer() : this(0) { - return (Func)CreateDelegate(typeof(Func), info); } - private static Action ExtractSetDelegate(MethodInfo info) + public NetSerializer(int maxStringLength) { - return (Action)CreateDelegate(typeof(Action), info); + _maxStringLength = maxStringLength; } - private ClassInfo RegisterInternal() + private ClassInfo RegisterInternal< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>() { if (ClassInfo.Instance != null) return ClassInfo.Instance; - Type t = typeof(T); -#if NETSTANDARD2_0 || NETCOREAPP2_0 - var props = t.GetRuntimeProperties().ToArray(); -#else - var props = t.GetProperties( + var props = typeof(T).GetProperties( BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.SetProperty); -#endif - int propsCount = props.Length; - if (props == null) - throw new InvalidTypeException("Type does not contain acceptable fields"); - - var info = new ClassInfo(propsCount); - for (int i = 0; i < propsCount; i++) + var serializers = new List>(); + for (int i = 0; i < props.Length; i++) { var property = props[i]; var propertyType = property.PropertyType; -#if NETSTANDARD2_0 || NETCOREAPP2_0 - bool isEnum = propertyType.GetTypeInfo().IsEnum; - var getMethod = property.GetMethod; - var setMethod = property.SetMethod; -#else - bool isEnum = propertyType.IsEnum; + var elementType = propertyType.IsArray ? propertyType.GetElementType() : propertyType; + var callType = propertyType.IsArray ? CallType.Array : CallType.Basic; + + if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) + { + elementType = propertyType.GetGenericArguments()[0]; + callType = CallType.List; + } + + if (Attribute.IsDefined(property, typeof(IgnoreDataMemberAttribute))) + continue; + var getMethod = property.GetGetMethod(); var setMethod = property.GetSetMethod(); -#endif - if (isEnum) + if (getMethod == null || setMethod == null) + continue; + + FastCall serialzer = null; + if (propertyType.IsEnum) { var underlyingType = Enum.GetUnderlyingType(propertyType); if (underlyingType == typeof(byte)) - { - info.ReadDelegate[i] = (inf, r) => - { - property.SetValue(inf, Enum.ToObject(propertyType, r.GetByte()), null); - }; - info.WriteDelegate[i] = (inf, w) => - { - w.Put((byte)property.GetValue(inf, null)); - }; - } + serialzer = new EnumByteSerializer(property, propertyType); else if (underlyingType == typeof(int)) - { - info.ReadDelegate[i] = (inf, r) => - { - property.SetValue(inf, Enum.ToObject(propertyType, r.GetInt()), null); - }; - info.WriteDelegate[i] = (inf, w) => - { - w.Put((int)property.GetValue(inf, null)); - }; - } + serialzer = new EnumIntSerializer(property, propertyType); else - { throw new InvalidTypeException("Not supported enum underlying type: " + underlyingType.Name); - } } - else if (propertyType == typeof(string)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - if (_maxStringLength <= 0) - { - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetString()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else - { - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetString(_maxStringLength)); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf), _maxStringLength); - } - } - else if (propertyType == typeof(bool)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetBool()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(byte)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetByte()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(sbyte)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetSByte()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(short)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetShort()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(ushort)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetUShort()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(int)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetInt()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(uint)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetUInt()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(long)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetLong()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(ulong)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetULong()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(float)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetFloat()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(double)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetDouble()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(char)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetChar()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - else if (propertyType == typeof(IPEndPoint)) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetNetEndPoint()); - info.WriteDelegate[i] = (inf, w) => w.Put(getDelegate(inf)); - } - // Array types - else if (propertyType == typeof(string[])) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - if (_maxStringLength <= 0) - { - info.ReadDelegate[i] = (inf, r) => setDelegate( inf, r.GetStringArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate( inf)); - } - else - { - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetStringArray(_maxStringLength)); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf), _maxStringLength); - } - } - else if (propertyType == typeof(bool[])) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetBoolArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf)); - } - else if (propertyType == typeof(byte[])) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetBytesWithLength()); - info.WriteDelegate[i] = (inf, w) => w.PutBytesWithLength(getDelegate(inf)); - } - else if (propertyType == typeof(short[])) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetShortArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf)); - } - else if (propertyType == typeof(ushort[])) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetUShortArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf)); - } - else if (propertyType == typeof(int[])) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetIntArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf)); - } - else if (propertyType == typeof(uint[])) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetUIntArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf)); - } - else if (propertyType == typeof(long[])) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetLongArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf)); - } - else if (propertyType == typeof(ulong[])) - { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetULongArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf)); - } - else if (propertyType == typeof(float[])) + else if (elementType == typeof(string)) + serialzer = new StringSerializer(_maxStringLength); + else if (elementType == typeof(bool)) + serialzer = new BoolSerializer(); + else if (elementType == typeof(byte)) + serialzer = new ByteSerializer(); + else if (elementType == typeof(sbyte)) + serialzer = new SByteSerializer(); + else if (elementType == typeof(short)) + serialzer = new ShortSerializer(); + else if (elementType == typeof(ushort)) + serialzer = new UShortSerializer(); + else if (elementType == typeof(int)) + serialzer = new IntSerializer(); + else if (elementType == typeof(uint)) + serialzer = new UIntSerializer(); + else if (elementType == typeof(long)) + serialzer = new LongSerializer(); + else if (elementType == typeof(ulong)) + serialzer = new ULongSerializer(); + else if (elementType == typeof(float)) + serialzer = new FloatSerializer(); + else if (elementType == typeof(double)) + serialzer = new DoubleSerializer(); + else if (elementType == typeof(char)) + serialzer = new CharSerializer(); + else if (elementType == typeof(IPEndPoint)) + serialzer = new IPEndPointSerializer(); + else if (elementType == typeof(Guid)) + serialzer = new GuidSerializer(); + else { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetFloatArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf)); + _registeredTypes.TryGetValue(elementType, out var customType); + if (customType != null) + serialzer = customType.Get(); } - else if (propertyType == typeof(double[])) + + if (serialzer != null) { - var setDelegate = ExtractSetDelegate(setMethod); - var getDelegate = ExtractGetDelegate(getMethod); - info.ReadDelegate[i] = (inf, r) => setDelegate(inf, r.GetDoubleArray()); - info.WriteDelegate[i] = (inf, w) => w.PutArray(getDelegate(inf)); + serialzer.Init(getMethod, setMethod, callType); + serializers.Add(serialzer); } else { - NestedType registeredNestedType; - bool array = false; - - if (propertyType.IsArray) - { - array = true; - propertyType = propertyType.GetElementType(); - } - - if (_registeredNestedTypes.TryGetValue(propertyType, out registeredNestedType)) - { - if (array) //Array type serialize/deserialize - { - info.ReadDelegate[i] = (inf, r) => property.SetValue(inf, registeredNestedType.ArrayReader(r), null); - info.WriteDelegate[i] = (inf, w) => registeredNestedType.ArrayWriter(w, property.GetValue(inf, null)); - } - else //Simple - { - info.ReadDelegate[i] = (inf, r) => property.SetValue(inf, registeredNestedType.ReadDelegate(r), null); - info.WriteDelegate[i] = (inf, w) => registeredNestedType.WriteDelegate(w, property.GetValue(inf, null)); - } - } - else - { - throw new InvalidTypeException("Unknown property type: " + propertyType.FullName); - } + throw new InvalidTypeException("Unknown property type: " + propertyType.FullName); } } - ClassInfo.Instance = info; - return info; + ClassInfo.Instance = new ClassInfo(serializers); + return ClassInfo.Instance; } /// 's fields are not supported, or it has no fields - public void Register() + public void Register< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>() { RegisterInternal(); } @@ -565,7 +690,11 @@ public void Register() /// NetDataReader with packet /// Returns packet if packet in reader is matched type /// 's fields are not supported, or it has no fields - public T Deserialize(NetDataReader reader) where T : class, new() + public T Deserialize< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>(NetDataReader reader) where T : class, new() { var info = RegisterInternal(); var result = new T(); @@ -587,7 +716,11 @@ public void Register() /// Deserialization target /// Returns true if packet in reader is matched type /// 's fields are not supported, or it has no fields - public bool Deserialize(NetDataReader reader, T target) where T : class, new() + public bool Deserialize< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>(NetDataReader reader, T target) where T : class, new() { var info = RegisterInternal(); try @@ -602,23 +735,33 @@ public void Register() } /// - /// Serialize struct to NetDataWriter (fast) + /// Serialize object to NetDataWriter (fast) /// /// Serialization target NetDataWriter /// Object to serialize /// 's fields are not supported, or it has no fields - public void Serialize(NetDataWriter writer, T obj) where T : class, new() + public void Serialize< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>(NetDataWriter writer, T obj) where T : class, new() { RegisterInternal().Write(obj, writer); } /// - /// Serialize struct to byte array + /// Serialize object to byte array /// /// Object to serialize /// byte array with serialized data - public byte[] Serialize(T obj) where T : class, new() + public byte[] Serialize< +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(Trimming.SerializerMemberTypes)] +#endif + T>(T obj) where T : class, new() { + if (_writer == null) + _writer = new NetDataWriter(); _writer.Reset(); Serialize(_writer, obj); return _writer.CopyData(); diff --git a/LiteNetLib/Utils/NtpPacket.cs b/LiteNetLib/Utils/NtpPacket.cs index 52afdd5c..1ba52107 100644 --- a/LiteNetLib/Utils/NtpPacket.cs +++ b/LiteNetLib/Utils/NtpPacket.cs @@ -38,7 +38,7 @@ public class NtpPacket /// This is the only real property. All other properties except /// read from or write to this byte array. /// - public byte[] Bytes { get; private set; } + public byte[] Bytes { get; } /// /// Gets the leap second indicator. @@ -51,10 +51,7 @@ public class NtpPacket /// /// Only servers fill in this property. Clients can consult this property for possible leap second warning. /// - public NtpLeapIndicator LeapIndicator - { - get { return (NtpLeapIndicator)((Bytes[0] & 0xC0) >> 6); } - } + public NtpLeapIndicator LeapIndicator => (NtpLeapIndicator)((Bytes[0] & 0xC0) >> 6); /// /// Gets or sets protocol version number. @@ -68,8 +65,8 @@ public NtpLeapIndicator LeapIndicator /// public int VersionNumber { - get { return (Bytes[0] & 0x38) >> 3; } - private set { Bytes[0] = (byte)((Bytes[0] & ~0x38) | value << 3); } + get => (Bytes[0] & 0x38) >> 3; + private set => Bytes[0] = (byte)((Bytes[0] & ~0x38) | value << 3); } /// @@ -81,8 +78,8 @@ public int VersionNumber /// public NtpMode Mode { - get { return (NtpMode)(Bytes[0] & 0x07); } - private set { Bytes[0] = (byte)((Bytes[0] & ~0x07) | (int)value); } + get => (NtpMode)(Bytes[0] & 0x07); + private set => Bytes[0] = (byte)((Bytes[0] & ~0x07) | (int)value); } /// @@ -99,7 +96,7 @@ public NtpMode Mode /// with kiss code stored in . /// /// - public int Stratum { get { return Bytes[1]; } } + public int Stratum => Bytes[1]; /// /// Gets server's preferred polling interval. @@ -107,7 +104,7 @@ public NtpMode Mode /// /// Polling interval in log2 seconds, e.g. 4 stands for 16s and 17 means 131,072s. /// - public int Poll { get { return Bytes[2]; } } + public int Poll => Bytes[2]; /// /// Gets the precision of server clock. @@ -115,7 +112,7 @@ public NtpMode Mode /// /// Clock precision in log2 seconds, e.g. -20 for microsecond precision. /// - public int Precision { get { return (sbyte)Bytes[3]; } } + public int Precision => (sbyte)Bytes[3]; /// /// Gets the total round-trip delay from the server to the reference clock. @@ -123,7 +120,7 @@ public NtpMode Mode /// /// Round-trip delay to the reference clock. Normally a positive value smaller than one second. /// - public TimeSpan RootDelay { get { return GetTimeSpan32(4); } } + public TimeSpan RootDelay => GetTimeSpan32(4); /// /// Gets the estimated error in time reported by the server. @@ -131,7 +128,7 @@ public NtpMode Mode /// /// Estimated error in time reported by the server. Normally a positive value smaller than one second. /// - public TimeSpan RootDispersion { get { return GetTimeSpan32(8); } } + public TimeSpan RootDispersion => GetTimeSpan32(8); /// /// Gets the ID of the time source used by the server or Kiss-o'-Death code sent by the server. @@ -153,7 +150,7 @@ public NtpMode Mode /// this property contains so called kiss code that instructs the client to stop querying the server. /// /// - public uint ReferenceId { get { return GetUInt32BE(12); } } + public uint ReferenceId => GetUInt32BE(12); /// /// Gets or sets the time when the server clock was last set or corrected. @@ -165,7 +162,7 @@ public NtpMode Mode /// This Property is usually set only by servers. It usually lags server's current time by several minutes, /// so don't use this property for time synchronization. /// - public DateTime? ReferenceTimestamp { get { return GetDateTime64(16); } } + public DateTime? ReferenceTimestamp => GetDateTime64(16); /// /// Gets or sets the time when the client sent its request. @@ -178,7 +175,7 @@ public NtpMode Mode /// /// /// - public DateTime? OriginTimestamp { get { return GetDateTime64(24); } } + public DateTime? OriginTimestamp => GetDateTime64(24); /// /// Gets or sets the time when the request was received by the server. @@ -189,7 +186,7 @@ public NtpMode Mode /// /// /// - public DateTime? ReceiveTimestamp { get { return GetDateTime64(32); } } + public DateTime? ReceiveTimestamp => GetDateTime64(32); /// /// Gets or sets the time when the packet was sent. @@ -222,7 +219,7 @@ public NtpMode Mode /// Gets the round-trip time to the server. /// /// - /// Time the request spent travelling to the server plus the time the reply spent travelling back. + /// Time the request spent traveling to the server plus the time the reply spent traveling back. /// This is calculated from timestamps in the packet as (t1 - t0) + (t3 - t2) /// where t0 is , /// t1 is , @@ -293,9 +290,7 @@ internal NtpPacket(byte[] bytes) /// public static NtpPacket FromServerResponse(byte[] bytes, DateTime destinationTimestamp) { - var packet = new NtpPacket(bytes); - packet.DestinationTimestamp = destinationTimestamp; - return packet; + return new NtpPacket(bytes) { DestinationTimestamp = destinationTimestamp }; } internal void ValidateRequest() @@ -425,4 +420,4 @@ public enum NtpMode /// Server = 4, } -} \ No newline at end of file +} diff --git a/LiteNetLib/Utils/NtpRequest.cs b/LiteNetLib/Utils/NtpRequest.cs index 3b3c9170..dc0c2aed 100644 --- a/LiteNetLib/Utils/NtpRequest.cs +++ b/LiteNetLib/Utils/NtpRequest.cs @@ -1,142 +1,66 @@ -using System; -using System.Net; +using System.Net; using System.Net.Sockets; namespace LiteNetLib.Utils { /// - /// Make NTP request. - /// - /// 1. Create the object by method. - /// - /// - /// 2. Use method to send requests. 3. Call to release the socket - /// AFTER you have received the response or some timeout. If you close the socket too early, you may miss the response. - /// - /// - /// 3. Call to release the socket AFTER you have received the response or some timeout. - /// If you close the socket too early, you may miss the response. - /// + /// Represents an active NTP (Network Time Protocol) query to a remote time server. + /// Handles retransmission and lifetime management of the request. /// - public sealed class NtpRequest : INetSocketListener + internal sealed class NtpRequest { + private const int ResendTimer = 1000; + private const int KillTimer = 10000; + /// + /// Standard UDP port used by NTP servers. + /// public const int DefaultPort = 123; - - private readonly NetSocket _socket; - private readonly Action _onRequestComplete; private readonly IPEndPoint _ntpEndPoint; + private float _resendTime = ResendTimer; + private float _killTime = 0; /// - /// Initialize object, open socket. + /// Initializes a new instance of the class for a specific server. /// - /// NTP Server endpoint - /// callback (called from other thread!) - private NtpRequest(IPEndPoint endPoint, Action onRequestComplete) + /// The IP endpoint of the NTP server. + public NtpRequest(IPEndPoint endPoint) { _ntpEndPoint = endPoint; - _onRequestComplete = onRequestComplete; - - // Create and start socket - _socket = new NetSocket(this); - _socket.Bind(IPAddress.Any, IPAddress.IPv6Any, 0, false, true); } /// - /// Create the requests for NTP server, open socket. + /// Gets a value indicating whether the request has exceeded its maximum lifetime and should be removed. /// - /// NTP Server address. - /// callback (called from other thread!) - public static NtpRequest Create(IPEndPoint endPoint, Action onRequestComplete) - { - return new NtpRequest(endPoint, onRequestComplete); - } + public bool NeedToKill => _killTime >= KillTimer; /// - /// Create the requests for NTP server (default port), open socket. + /// Attempts to send an NTP query packet to the remote endpoint. /// - /// NTP Server address. - /// callback (called from other thread!) - public static NtpRequest Create(IPAddress ipAddress, Action onRequestComplete) + /// + /// The packet is only sent if the internal retransmission timer has elapsed. + /// Updates internal timers for both retransmission and total lifetime. + /// + /// The underlying socket used to send the datagram. + /// The amount of time elapsed since the last update/call, in seconds. + /// if the packet was successfully transmitted; otherwise, . + public bool Send(Socket socket, float time) { - IPEndPoint endPoint = new IPEndPoint(ipAddress, DefaultPort); - return Create(endPoint, onRequestComplete); - } - - /// - /// Create the requests for NTP server, open socket. - /// - /// NTP Server address. - /// port - /// callback (called from other thread!) - public static NtpRequest Create(string ntpServerAddress, int port, Action onRequestComplete) - { - IPEndPoint endPoint = NetUtils.MakeEndPoint(ntpServerAddress, port); - return Create(endPoint, onRequestComplete); - } - - /// - /// Create the requests for NTP server (default port), open socket. - /// - /// NTP Server address. - /// callback (called from other thread!) - public static NtpRequest Create(string ntpServerAddress, Action onRequestComplete) - { - IPEndPoint endPoint = NetUtils.MakeEndPoint(ntpServerAddress, DefaultPort); - return Create(endPoint, onRequestComplete); - } - - /// - /// Send request to the NTP server calls callback (if success). - /// In case of error the callbacke is called with null param. - /// - public void Send() - { - SocketError errorCode = 0; - var packet = new NtpPacket(); - packet.ValidateRequest(); // not necessary - byte[] sendData = packet.Bytes; - var sendCount = _socket.SendTo(sendData, 0, sendData.Length, _ntpEndPoint, ref errorCode); - if (errorCode != 0 || sendCount != sendData.Length) - { - _onRequestComplete(null); - } - } - - /// - /// Close socket. - /// - public void Close() - { - _socket.Close(); - } - - /// - /// Handle received data: transform bytes to NtpPacket, close socket and call the callback. - /// - void INetSocketListener.OnMessageReceived(byte[] data, int length, SocketError errorCode, IPEndPoint remoteEndPoint) - { - DateTime destinationTimestamp = DateTime.UtcNow; - if (!remoteEndPoint.Equals(_ntpEndPoint)) - return; - - if (length < 48) + _resendTime += time; + _killTime += time; + if (_resendTime < ResendTimer) { - NetDebug.Write(NetLogLevel.Trace, "NTP response too short: {}", length); - _onRequestComplete(null); - return; + return false; } - - NtpPacket packet = NtpPacket.FromServerResponse(data, destinationTimestamp); + var packet = new NtpPacket(); try { - packet.ValidateReply(); + int sendCount = socket.SendTo(packet.Bytes, 0, packet.Bytes.Length, SocketFlags.None, _ntpEndPoint); + return sendCount == packet.Bytes.Length; } - catch (InvalidOperationException ex) + catch { - NetDebug.Write(NetLogLevel.Trace, "NTP response error: {}", ex.Message); - packet = null; + return false; } - _onRequestComplete(packet); } } -} \ No newline at end of file +} diff --git a/LiteNetLib/Utils/Preserve.cs b/LiteNetLib/Utils/Preserve.cs new file mode 100644 index 00000000..057798b9 --- /dev/null +++ b/LiteNetLib/Utils/Preserve.cs @@ -0,0 +1,12 @@ +using System; + +namespace LiteNetLib.Utils +{ + /// + /// PreserveAttribute prevents byte code stripping from removing a class, method, field, or property. + /// + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)] + public class PreserveAttribute : Attribute + { + } +} diff --git a/LiteNetLib/package.json b/LiteNetLib/package.json new file mode 100644 index 00000000..e5011459 --- /dev/null +++ b/LiteNetLib/package.json @@ -0,0 +1,11 @@ +{ + "name": "com.revenantx.litenetlib", + "version": "1.0.1-1", + "displayName": "LiteNetLib", + "description": "Lite reliable UDP library for .NET Standard 2.0 (Mono, .NET Core, .NET Framework)", + "unity": "2018.3", + "author": { + "name": "RevenantX", + "url": "https://github.com/RevenantX" + } +} \ No newline at end of file diff --git a/LiteNetLibSampleUnity/Assets/GameClient.cs b/LiteNetLibSampleUnity/Assets/GameClient.cs index b469edb4..f6a120e9 100644 --- a/LiteNetLibSampleUnity/Assets/GameClient.cs +++ b/LiteNetLibSampleUnity/Assets/GameClient.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Net; using System.Net.Sockets; using UnityEngine; using LiteNetLib; @@ -14,7 +14,7 @@ public class GameClient : MonoBehaviour, INetEventListener private float _oldBallPosX; private float _lerpTime; - void Start() + private void Start() { _netClient = new NetManager(this); _netClient.UnconnectedMessagesEnabled = true; @@ -22,7 +22,7 @@ void Start() _netClient.Start(); } - void Update() + private void Update() { _netClient.PollEvents(); @@ -43,23 +43,23 @@ void Update() } } - void OnDestroy() + private void OnDestroy() { if (_netClient != null) _netClient.Stop(); } - public void OnPeerConnected(NetPeer peer) + void INetEventListener.OnPeerConnected(NetPeer peer) { - Debug.Log("[CLIENT] We connected to " + peer.EndPoint); + Debug.Log("[CLIENT] We connected to " + peer); } - public void OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) + void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) { Debug.Log("[CLIENT] We received error " + socketErrorCode); } - public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) + void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { _newBallPosX = reader.GetFloat(); @@ -73,27 +73,27 @@ public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMetho _lerpTime = 0f; } - public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) + void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { - if (messageType == UnconnectedMessageType.BasicMessage && _netClient.PeersCount == 0 && reader.GetInt() == 1) + if (messageType == UnconnectedMessageType.BasicMessage && _netClient.ConnectedPeersCount == 0 && reader.GetInt() == 1) { Debug.Log("[CLIENT] Received discovery response. Connecting to: " + remoteEndPoint); _netClient.Connect(remoteEndPoint, "sample_app"); } } - public void OnNetworkLatencyUpdate(NetPeer peer, int latency) + void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) { } - public void OnConnectionRequest(ConnectionRequest request) + void INetEventListener.OnConnectionRequest(ConnectionRequest request) { - + } - public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { Debug.Log("[CLIENT] We disconnected because " + disconnectInfo.Reason); } -} \ No newline at end of file +} diff --git a/LiteNetLibSampleUnity/Assets/GameServer.cs b/LiteNetLibSampleUnity/Assets/GameServer.cs index 2a87f205..0f5641ee 100644 --- a/LiteNetLibSampleUnity/Assets/GameServer.cs +++ b/LiteNetLibSampleUnity/Assets/GameServer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using System.Net.Sockets; using UnityEngine; @@ -13,7 +13,7 @@ public class GameServer : MonoBehaviour, INetEventListener, INetLogger [SerializeField] private GameObject _serverBall; - void Start() + private void Start() { NetDebug.Logger = this; _dataWriter = new NetDataWriter(); @@ -23,12 +23,12 @@ void Start() _netServer.UpdateTime = 15; } - void Update() + private void Update() { _netServer.PollEvents(); } - void FixedUpdate() + private void FixedUpdate() { if (_ourPeer != null) { @@ -39,25 +39,25 @@ void FixedUpdate() } } - void OnDestroy() + private void OnDestroy() { NetDebug.Logger = null; if (_netServer != null) _netServer.Stop(); } - public void OnPeerConnected(NetPeer peer) + void INetEventListener.OnPeerConnected(NetPeer peer) { - Debug.Log("[SERVER] We have new peer " + peer.EndPoint); + Debug.Log("[SERVER] We have new peer " + peer); _ourPeer = peer; } - public void OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) + void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) { Debug.Log("[SERVER] error " + socketErrorCode); } - public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, + void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { if (messageType == UnconnectedMessageType.Broadcast) @@ -69,28 +69,28 @@ public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketRead } } - public void OnNetworkLatencyUpdate(NetPeer peer, int latency) + void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) { } - public void OnConnectionRequest(ConnectionRequest request) + void INetEventListener.OnConnectionRequest(ConnectionRequest request) { request.AcceptIfKey("sample_app"); } - public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) { - Debug.Log("[SERVER] peer disconnected " + peer.EndPoint + ", info: " + disconnectInfo.Reason); + Debug.Log("[SERVER] peer disconnected " + peer + ", info: " + disconnectInfo.Reason); if (peer == _ourPeer) _ourPeer = null; } - public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) + void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) { } - public void WriteNet(NetLogLevel level, string str, params object[] args) + void INetLogger.WriteNet(NetLogLevel level, string str, params object[] args) { Debug.LogFormat(str, args); } -} \ No newline at end of file +} diff --git a/LiteNetLibSampleUnity/Assets/LiteNetLib.dll b/LiteNetLibSampleUnity/Assets/LiteNetLib.dll deleted file mode 100644 index eee0a973..00000000 Binary files a/LiteNetLibSampleUnity/Assets/LiteNetLib.dll and /dev/null differ diff --git a/LiteNetLibSampleUnity/Assets/LiteNetLib.dll.meta b/LiteNetLibSampleUnity/Assets/LiteNetLib.dll.meta deleted file mode 100644 index 8da128bb..00000000 --- a/LiteNetLibSampleUnity/Assets/LiteNetLib.dll.meta +++ /dev/null @@ -1,32 +0,0 @@ -fileFormatVersion: 2 -guid: c132af3a96c6a4049b6b3b3b044dacb0 -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/LiteNetLibSampleUnity/Assets/LiteNetLib.xml b/LiteNetLibSampleUnity/Assets/LiteNetLib.xml deleted file mode 100644 index f4972288..00000000 --- a/LiteNetLibSampleUnity/Assets/LiteNetLib.xml +++ /dev/null @@ -1,1290 +0,0 @@ - - - - LiteNetLib - - - - - Accept connection and get new NetPeer as result - - Connected NetPeer - - - - Type of message that you receive in OnNetworkReceiveUnconnected event - - - - - Disconnect reason that you receive in OnPeerDisconnected event - - - - - Additional information about disconnection - - - - - Additional info why peer disconnected - - - - - Error code (if reason is SocketSendError or SocketReceiveError) - - - - - Additional data that can be accessed (only if reason is RemoteConnectionClose) - - - - - New remote peer connected to host, or client connected to remote host - - Connected peer object - - - - Peer disconnected - - disconnected peer - additional info about reason, errorCode or data received with disconnect message - - - - Network error (on send or receive) - - From endPoint (can be null) - Socket error - - - - Received some data - - From peer - DataReader containing all received data - Type of received packet - - - - Received unconnected message - - From address (IP and Port) - Message data - Message type (simple, discovery request or responce) - - - - Latency information updated - - Peer with updated latency - latency value in milliseconds - - - - On peer connection requested - - Request information (EndPoint, internal id, additional data) - - - - Module for UDP NAT Hole punching operations. Can be accessed from NetManager - - - - - Sending method type - - - - - Unreliable. Packets can be dropped, duplicated or arrive without order - - - - - Reliable. All packets will be sent and received, but without order - - - - - Unreliable. Packets can be dropped, but never duplicated and arrive in order - - - - - Reliable and ordered. All packets will be sent and received in order - - - - - Reliable only last packet - - - - - Network constants. Can be tuned from sources for your purposes. - - - - - Interface to implement for your own logger - - - - - Static class for defining your own LiteNetLib logger instead of Console.WriteLine - or Debug.Log if compiled with UNITY flag - - - - - Main class for all network operations. Can be used as client and/or server. - - - - - Enable messages receiving without connection. (with SendUnconnectedMessage method) - - - - - Enable nat punch messages - - - - - Library logic update and send period in milliseconds - - - - - Interval for latency detection and checking connection - - - - - If NetManager doesn't receive any packet from remote peer during this time then connection will be closed - (including library internal keepalive packets) - - - - - Simulate packet loss by dropping random amout of packets. (Works only in DEBUG mode) - - - - - Simulate latency by holding packets for random time. (Works only in DEBUG mode) - - - - - Chance of packet loss when simulation enabled. value in percents (1 - 100). - - - - - Minimum simulated latency - - - - - Maximum simulated latency - - - - - Experimental feature. Events automatically will be called without PollEvents method from another thread - - - - - Allows receive broadcast packets - - - - - Delay betwen initial connection attempts - - - - - Maximum connection attempts before client stops and call disconnect event. - - - - - Enables socket option "ReuseAddress" for specific purposes - - - - - Statistics of all connections - - - - - NatPunchModule for NAT hole punching operations - - - - - Returns true if socket listening and update thread is running - - - - - Local EndPoint (host and port) - - - - - Automatically recycle NetPacketReader after OnReceive event - - - - - IPv6 support - - - - - First peer. Useful for Client mode - - - - - QoS channel count per message type (value must be between 1 and 64 channels) - - - - - Returns connected peers list (with internal cached list) - - - - - Returns connected peers count - - - - - NetManager constructor - - Network events listener - - - - Send data to all connected peers (channel - 0) - - DataWriter with data - Send options (reliable, unreliable, etc.) - - - - Send data to all connected peers (channel - 0) - - Data - Send options (reliable, unreliable, etc.) - - - - Send data to all connected peers (channel - 0) - - Data - Start of data - Length of data - Send options (reliable, unreliable, etc.) - - - - Send data to all connected peers - - DataWriter with data - Number of channel (from 0 to channelsCount - 1) - Send options (reliable, unreliable, etc.) - - - - Send data to all connected peers - - Data - Number of channel (from 0 to channelsCount - 1) - Send options (reliable, unreliable, etc.) - - - - Send data to all connected peers - - Data - Start of data - Length of data - Number of channel (from 0 to channelsCount - 1) - Send options (reliable, unreliable, etc.) - - - - Send data to all connected peers (channel - 0) - - DataWriter with data - Send options (reliable, unreliable, etc.) - Excluded peer - - - - Send data to all connected peers (channel - 0) - - Data - Send options (reliable, unreliable, etc.) - Excluded peer - - - - Send data to all connected peers (channel - 0) - - Data - Start of data - Length of data - Send options (reliable, unreliable, etc.) - Excluded peer - - - - Send data to all connected peers - - DataWriter with data - Number of channel (from 0 to channelsCount - 1) - Send options (reliable, unreliable, etc.) - Excluded peer - - - - Send data to all connected peers - - Data - Number of channel (from 0 to channelsCount - 1) - Send options (reliable, unreliable, etc.) - Excluded peer - - - - Send data to all connected peers - - Data - Start of data - Length of data - Number of channel (from 0 to channelsCount - 1) - Send options (reliable, unreliable, etc.) - Excluded peer - - - - Start logic thread and listening on available port - - - - - Start logic thread and listening on selected port - - bind to specific ipv4 address - bind to specific ipv6 address - port to listen - - - - Start logic thread and listening on selected port - - bind to specific ipv4 address - bind to specific ipv6 address - port to listen - - - - Start logic thread and listening on selected port - - port to listen - - - - Send message without connection - - Raw data - Packet destination - Operation result - - - - Send message without connection - - Data serializer - Packet destination - Operation result - - - - Send message without connection - - Raw data - data start - data length - Packet destination - Operation result - - - - Flush all queued packets of all peers - - - - - Receive all pending events. Call this in game update code - - - - - Connect to remote host - - Server IP or hostname - Server Port - Connection key - New NetPeer if new connection, Old NetPeer if already connected - Manager is not running. Call - - - - Connect to remote host - - Server IP or hostname - Server Port - Additional data for remote peer - New NetPeer if new connection, Old NetPeer if already connected - Manager is not running. Call - - - - Connect to remote host - - Server end point (ip and port) - Connection key - New NetPeer if new connection, Old NetPeer if already connected - Manager is not running. Call - - - - Connect to remote host - - Server end point (ip and port) - Additional data for remote peer - New NetPeer if new connection, Old NetPeer if already connected - Manager is not running. Call - - - - Force closes connection and stop all threads. - - - - - Force closes connection and stop all threads. - - Send disconnect messages - - - - Return peers count with connection state - - peer connection state (you can use as bit flags) - peers count - - - - Get copy of current connected peers (slow! use GetPeersNonAlloc for best performance) - - Array with connected peers - - - - Get copy of current connected peers (slow! use GetPeersNonAlloc for best performance) - - Array with connected peers - - - - Get copy of peers (without allocations) - - List that will contain result - State of peers - - - - Disconnect all peers without any additional data - - - - - Disconnect all peers with shutdown message - - Data to send (must be less or equal MTU) - Data start - Data count - - - - Immediately disconnect peer from server without additional data - - peer to disconnect - - - - Disconnect peer from server - - peer to disconnect - - - - Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - - peer to disconnect - additional data - - - - Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - - peer to disconnect - additional data - - - - Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - - peer to disconnect - additional data - data start - data length - - - - Peer connection state - - - - - Network peer. Main purpose is sending messages to specific peer. - - - - - Current connection state - - - - - Connection time for internal purposes - - - - - Peer id can be used as key in your dictionary of peers - - - - - Peer ip address and port - - - - - Current ping in milliseconds - - - - - Current MTU - Maximum Transfer Unit ( maximum udp packet size without fragmentation ) - - - - - Delta with remote time in ticks (not accurate) - positive - remote time > our time - - - - - Remote UTC time (not accurate) - - - - - Time since last packet received (including internal library packets) - - - - - Peer parent NetManager - - - - - Application defined object containing data about the connection - - - - - Statistics of peer connection - - - - - Gets maximum size of packet that will be not fragmented. - - Type of packet that you want send - size in bytes - - - - Send data to peer (channel - 0) - - Data - Send options (reliable, unreliable, etc.) - - If size exceeds maximum limit: - MTU - headerSize bytes for Unreliable - Fragment count exceeded ushort.MaxValue - - - - - Send data to peer (channel - 0) - - DataWriter with data - Send options (reliable, unreliable, etc.) - - If size exceeds maximum limit: - MTU - headerSize bytes for Unreliable - Fragment count exceeded ushort.MaxValue - - - - - Send data to peer (channel - 0) - - Data - Start of data - Length of data - Send options (reliable, unreliable, etc.) - - If size exceeds maximum limit: - MTU - headerSize bytes for Unreliable - Fragment count exceeded ushort.MaxValue - - - - - Send data to peer - - Data - Number of channel (from 0 to channelsCount - 1) - Send options (reliable, unreliable, etc.) - - If size exceeds maximum limit: - MTU - headerSize bytes for Unreliable - Fragment count exceeded ushort.MaxValue - - - - - Send data to peer - - DataWriter with data - Number of channel (from 0 to channelsCount - 1) - Send options (reliable, unreliable, etc.) - - If size exceeds maximum limit: - MTU - headerSize bytes for Unreliable - Fragment count exceeded ushort.MaxValue - - - - - Send data to peer - - Data - Start of data - Length of data - Number of channel (from 0 to channelsCount - 1) - Send options (reliable, unreliable, etc.) - - If size exceeds maximum limit: - MTU - headerSize bytes for Unreliable - Fragment count exceeded ushort.MaxValue - - - - - Flush all queued packets - - - - - Address type that you want to receive from NetUtils.GetLocalIp method - - - - - Some specific network utilities - - - - - Get all local ip addresses - - type of address (IPv4, IPv6 or both) - List with all local ip adresses - - - - Get all local ip addresses (non alloc version) - - result list - type of address (IPv4, IPv6 or both) - - - - Get first detected local ip address - - type of address (IPv4, IPv6 or both) - IP address if available. Else - string.Empty - - - - Creates NetDataWriter from existing ByteArray - - Source byte array - Copy array to new location or use existing - - - - Creates NetDataWriter from existing ByteArray (always copied data) - - Source byte array - Offset of array - Length of array - - - - Register nested property type - - INetSerializable structure - True - if register successful, false - if type already registered - - - - Register nested property type - - - - True - if register successful, false - if type already registered - - - - Register nested property type - - INetSerializable class - True - if register successful, false - if type already registered - - - - Reads all available data from NetDataReader and calls OnReceive delegates - - NetDataReader with packets data - - - - Reads all available data from NetDataReader and calls OnReceive delegates - - NetDataReader with packets data - Argument that passed to OnReceivedEvent - Malformed packet - - - - Reads one packet from NetDataReader and calls OnReceive delegate - - NetDataReader with packet - Malformed packet - - - - Reads one packet from NetDataReader and calls OnReceive delegate - - NetDataReader with packet - Argument that passed to OnReceivedEvent - Malformed packet - - - - Register and subscribe to packet receive event - - event that will be called when packet deserialized with ReadPacket method - Method that constructs packet intead of slow Activator.CreateInstance - 's fields are not supported, or it has no fields - - - - Register and subscribe to packet receive event (with userData) - - event that will be called when packet deserialized with ReadPacket method - Method that constructs packet intead of slow Activator.CreateInstance - 's fields are not supported, or it has no fields - - - - Register and subscribe to packet receive event - This metod will overwrite last received packet class on receive (less garbage) - - event that will be called when packet deserialized with ReadPacket method - 's fields are not supported, or it has no fields - - - - Register and subscribe to packet receive event - This metod will overwrite last received packet class on receive (less garbage) - - event that will be called when packet deserialized with ReadPacket method - 's fields are not supported, or it has no fields - - - - Remove any subscriptions by type - - Packet type - true if remove is success - - - - Register nested property type - - INetSerializable structure - True - if register successful, false - if type already registered - - - - Register nested property type - - INetSerializable class - True - if register successful, false - if type already registered - - - - Register nested property type - - - - True - if register successful, false - if type already registered - - - 's fields are not supported, or it has no fields - - - - Reads packet with known type - - NetDataReader with packet - Returns packet if packet in reader is matched type - 's fields are not supported, or it has no fields - - - - Reads packet with known type (non alloc variant) - - NetDataReader with packet - Deserialization target - Returns true if packet in reader is matched type - 's fields are not supported, or it has no fields - - - - Serialize struct to NetDataWriter (fast) - - Serialization target NetDataWriter - Object to serialize - 's fields are not supported, or it has no fields - - - - Serialize struct to byte array - - Object to serialize - byte array with serialized data - - - - Represents RFC4330 SNTP packet used for communication to and from a network time server. - - - - Most applications should just use the property. - - - The same data structure represents both request and reply packets. - Request and reply differ in which properties are set and to what values. - - - The only real property is . - All other properties read from and write to the underlying byte array - with the exception of , - which is not part of the packet on network and it is instead set locally after receiving the packet. - - - Copied from GuerrillaNtp project - with permission from Robert Vazan (@robertvazan) under MIT license, see https://github.com/RevenantX/LiteNetLib/pull/236 - - - - - - Gets RFC4330-encoded SNTP packet. - - - Byte array containing RFC4330-encoded SNTP packet. It is at least 48 bytes long. - - - This is the only real property. All other properties except - read from or write to this byte array. - - - - - Gets the leap second indicator. - - - Leap second warning, if any. Special value - indicates unsynchronized server clock. - Default is . - - - Only servers fill in this property. Clients can consult this property for possible leap second warning. - - - - - Gets or sets protocol version number. - - - SNTP protocol version. Default is 4, which is the latest version at the time of this writing. - - - In request packets, clients should leave this property at default value 4. - Servers usually reply with the same protocol version. - - - - - Gets or sets SNTP packet mode, i.e. whether this is client or server packet. - - - SNTP packet mode. Default is in newly created packets. - Server reply should have this property set to . - - - - - Gets server's distance from the reference clock. - - - - Distance from the reference clock. This property is set only in server reply packets. - Servers connected directly to reference clock hardware set this property to 1. - Statum number is incremented by 1 on every hop down the NTP server hierarchy. - - - Special value 0 indicates that this packet is a Kiss-o'-Death message - with kiss code stored in . - - - - - - Gets server's preferred polling interval. - - - Polling interval in log2 seconds, e.g. 4 stands for 16s and 17 means 131,072s. - - - - - Gets the precision of server clock. - - - Clock precision in log2 seconds, e.g. -20 for microsecond precision. - - - - - Gets the total round-trip delay from the server to the reference clock. - - - Round-trip delay to the reference clock. Normally a positive value smaller than one second. - - - - - Gets the estimated error in time reported by the server. - - - Estimated error in time reported by the server. Normally a positive value smaller than one second. - - - - - Gets the ID of the time source used by the server or Kiss-o'-Death code sent by the server. - - - - ID of server's time source or Kiss-o'-Death code. - Purpose of this property depends on value of property. - - - Stratum 1 servers write here one of several special values that describe the kind of hardware clock they use. - - - Stratum 2 and lower servers set this property to IPv4 address of their upstream server. - If upstream server has IPv6 address, the address is hashed, because it doesn't fit in this property. - - - When server sets to special value 0, - this property contains so called kiss code that instructs the client to stop querying the server. - - - - - - Gets or sets the time when the server clock was last set or corrected. - - - Time when the server clock was last set or corrected or null when not specified. - - - This Property is usually set only by servers. It usually lags server's current time by several minutes, - so don't use this property for time synchronization. - - - - - Gets or sets the time when the client sent its request. - - - This property is null in request packets. - In reply packets, it is the time when the client sent its request. - Servers copy this value from - that they find in received request packet. - - - - - - - Gets or sets the time when the request was received by the server. - - - This property is null in request packets. - In reply packets, it is the time when the server received client request. - - - - - - - Gets or sets the time when the packet was sent. - - - Time when the packet was sent. It should never be null. - Default value is . - - - This property must be set by both clients and servers. - - - - - - - Gets or sets the time of reception of response SNTP packet on the client. - - - Time of reception of response SNTP packet on the client. It is null in request packets. - - - This property is not part of the protocol and has to be set when reply packet is received. - - - - - - - Gets the round-trip time to the server. - - - Time the request spent travelling to the server plus the time the reply spent travelling back. - This is calculated from timestamps in the packet as (t1 - t0) + (t3 - t2) - where t0 is , - t1 is , - t2 is , - and t3 is . - This property throws an exception in request packets. - - - - - Gets the offset that should be added to local time to synchronize it with server time. - - - Time difference between server and client. It should be added to local time to get server time. - It is calculated from timestamps in the packet as 0.5 * ((t1 - t0) - (t3 - t2)) - where t0 is , - t1 is , - t2 is , - and t3 is . - This property throws an exception in request packets. - - - - - Initializes default request packet. - - - Properties and - are set appropriately for request packet. Property - is set to . - - - - - Initializes packet from received data. - - - - - Initializes packet from data received from a server. - - Data received from the server. - Utc time of reception of response SNTP packet on the client. - - - - - Represents leap second warning from the server that instructs the client to add or remove leap second. - - - - - - No leap second warning. No action required. - - - - - Warns the client that the last minute of the current day has 61 seconds. - - - - - Warns the client that the last minute of the current day has 59 seconds. - - - - - Special value indicating that the server clock is unsynchronized and the returned time is unreliable. - - - - - Describes SNTP packet mode, i.e. client or server. - - - - - - Identifies client-to-server SNTP packet. - - - - - Identifies server-to-client SNTP packet. - - - - - Make NTP request. - - 1. Create the object by method. - - - 2. Use method to send requests. 3. Call to release the socket - AFTER you have received the response or some timeout. If you close the socket too early, you may miss the response. - - - 3. Call to release the socket AFTER you have received the response or some timeout. - If you close the socket too early, you may miss the response. - - - - - - Initialize object, open socket. - - NTP Server endpoint - callback (called from other thread!) - - - - Create the requests for NTP server, open socket. - - NTP Server address. - callback (called from other thread!) - - - - Create the requests for NTP server (default port), open socket. - - NTP Server address. - callback (called from other thread!) - - - - Create the requests for NTP server, open socket. - - NTP Server address. - port - callback (called from other thread!) - - - - Create the requests for NTP server (default port), open socket. - - NTP Server address. - callback (called from other thread!) - - - - Send request to the NTP server calls callback (if success). - In case of error the callbacke is called with null param. - - - - - Close socket. - - - - - Handle received data: transform bytes to NtpPacket, close socket and call the callback. - - - - diff --git a/LiteNetLibSampleUnity/Assets/LiteNetLib.pdb.meta b/LiteNetLibSampleUnity/Assets/Resources.meta similarity index 67% rename from LiteNetLibSampleUnity/Assets/LiteNetLib.pdb.meta rename to LiteNetLibSampleUnity/Assets/Resources.meta index c632ac23..0ec2b74a 100644 --- a/LiteNetLibSampleUnity/Assets/LiteNetLib.pdb.meta +++ b/LiteNetLibSampleUnity/Assets/Resources.meta @@ -1,5 +1,6 @@ fileFormatVersion: 2 -guid: 8d4c429e3f74b074299de2753ca8de73 +guid: 0680635b8862cad41957568e3143c8c7 +folderAsset: yes DefaultImporter: externalObjects: {} userData: diff --git a/LiteNetLibSampleUnity/Assets/Resources/BillingMode.json b/LiteNetLibSampleUnity/Assets/Resources/BillingMode.json new file mode 100644 index 00000000..6f4bfb71 --- /dev/null +++ b/LiteNetLibSampleUnity/Assets/Resources/BillingMode.json @@ -0,0 +1 @@ +{"androidStore":"GooglePlay"} \ No newline at end of file diff --git a/LiteNetLibSampleUnity/Assets/LiteNetLib.xml.meta b/LiteNetLibSampleUnity/Assets/Resources/BillingMode.json.meta similarity index 75% rename from LiteNetLibSampleUnity/Assets/LiteNetLib.xml.meta rename to LiteNetLibSampleUnity/Assets/Resources/BillingMode.json.meta index 30ffaeba..19a785dd 100644 --- a/LiteNetLibSampleUnity/Assets/LiteNetLib.xml.meta +++ b/LiteNetLibSampleUnity/Assets/Resources/BillingMode.json.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 46009b99ce8ce4b4b9de461f4fc5cec2 +guid: 935f8151a9a85ee40bfcd6cbe0333137 TextScriptImporter: externalObjects: {} userData: diff --git a/LiteNetLibSampleUnity/Packages/manifest.json b/LiteNetLibSampleUnity/Packages/manifest.json new file mode 100644 index 00000000..af623106 --- /dev/null +++ b/LiteNetLibSampleUnity/Packages/manifest.json @@ -0,0 +1,54 @@ +{ + "dependencies": { + "com.revenantx.litenetlib": "1.3.0", + "com.unity.2d.sprite": "1.0.0", + "com.unity.collab-proxy": "2.5.2", + "com.unity.ide.rider": "3.0.34", + "com.unity.ide.visualstudio": "2.0.22", + "com.unity.ide.vscode": "1.2.5", + "com.unity.textmeshpro": "3.0.9", + "com.unity.timeline": "1.7.6", + "com.unity.ugui": "1.0.0", + "com.unity.xr.legacyinputhelpers": "2.1.11", + "com.unity.modules.ai": "1.0.0", + "com.unity.modules.androidjni": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.cloth": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", + "com.unity.modules.screencapture": "1.0.0", + "com.unity.modules.terrain": "1.0.0", + "com.unity.modules.terrainphysics": "1.0.0", + "com.unity.modules.tilemap": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.umbra": "1.0.0", + "com.unity.modules.unityanalytics": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.unitywebrequesttexture": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.modules.vehicles": "1.0.0", + "com.unity.modules.video": "1.0.0", + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.wind": "1.0.0", + "com.unity.modules.xr": "1.0.0" + }, + "scopedRegistries": [ + { + "name": "package.openupm.com", + "url": "https://package.openupm.com", + "scopes": [ + "com.revenantx.litenetlib" + ] + } + ] +} diff --git a/LiteNetLibSampleUnity/Packages/packages-lock.json b/LiteNetLibSampleUnity/Packages/packages-lock.json new file mode 100644 index 00000000..f5692892 --- /dev/null +++ b/LiteNetLibSampleUnity/Packages/packages-lock.json @@ -0,0 +1,350 @@ +{ + "dependencies": { + "com.revenantx.litenetlib": { + "version": "1.3.0", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://package.openupm.com" + }, + "com.unity.2d.sprite": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.collab-proxy": { + "version": "2.5.2", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.ext.nunit": { + "version": "1.0.6", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.ide.rider": { + "version": "3.0.34", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.visualstudio": { + "version": "2.0.22", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.9" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.vscode": { + "version": "1.2.5", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.test-framework": { + "version": "1.1.33", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.textmeshpro": { + "version": "3.0.9", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.timeline": { + "version": "1.7.6", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.director": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ugui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, + "com.unity.xr.legacyinputhelpers": { + "version": "2.1.11", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.xr": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.modules.ai": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.androidjni": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.animation": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.assetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.audio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.cloth": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.director": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.animation": "1.0.0" + } + }, + "com.unity.modules.imageconversion": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.imgui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.jsonserialize": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics2d": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.screencapture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.subsystems": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.terrainphysics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.terrain": "1.0.0" + } + }, + "com.unity.modules.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics2d": "1.0.0" + } + }, + "com.unity.modules.ui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.uielements": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.umbra": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unityanalytics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unitywebrequestassetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestaudio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.audio": "1.0.0" + } + }, + "com.unity.modules.unitywebrequesttexture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestwww": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.vehicles": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.video": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.vr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } + }, + "com.unity.modules.wind": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.xr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.subsystems": "1.0.0" + } + } + } +} diff --git a/LiteNetLibSampleUnity/ProjectSettings/MemorySettings.asset b/LiteNetLibSampleUnity/ProjectSettings/MemorySettings.asset new file mode 100644 index 00000000..5b5facec --- /dev/null +++ b/LiteNetLibSampleUnity/ProjectSettings/MemorySettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!387306366 &1 +MemorySettings: + m_ObjectHideFlags: 0 + m_EditorMemorySettings: + m_MainAllocatorBlockSize: -1 + m_ThreadAllocatorBlockSize: -1 + m_MainGfxBlockSize: -1 + m_ThreadGfxBlockSize: -1 + m_CacheBlockSize: -1 + m_TypetreeBlockSize: -1 + m_ProfilerBlockSize: -1 + m_ProfilerEditorBlockSize: -1 + m_BucketAllocatorGranularity: -1 + m_BucketAllocatorBucketsCount: -1 + m_BucketAllocatorBlockSize: -1 + m_BucketAllocatorBlockCount: -1 + m_ProfilerBucketAllocatorGranularity: -1 + m_ProfilerBucketAllocatorBucketsCount: -1 + m_ProfilerBucketAllocatorBlockSize: -1 + m_ProfilerBucketAllocatorBlockCount: -1 + m_TempAllocatorSizeMain: -1 + m_JobTempAllocatorBlockSize: -1 + m_BackgroundJobTempAllocatorBlockSize: -1 + m_JobTempAllocatorReducedBlockSize: -1 + m_TempAllocatorSizeGIBakingWorker: -1 + m_TempAllocatorSizeNavMeshWorker: -1 + m_TempAllocatorSizeAudioWorker: -1 + m_TempAllocatorSizeCloudWorker: -1 + m_TempAllocatorSizeGfx: -1 + m_TempAllocatorSizeJobWorker: -1 + m_TempAllocatorSizeBackgroundWorker: -1 + m_TempAllocatorSizePreloadManager: -1 + m_PlatformMemorySettings: {} diff --git a/LiteNetLibSampleUnity/ProjectSettings/PackageManagerSettings.asset b/LiteNetLibSampleUnity/ProjectSettings/PackageManagerSettings.asset new file mode 100644 index 00000000..d5a8583c --- /dev/null +++ b/LiteNetLibSampleUnity/ProjectSettings/PackageManagerSettings.asset @@ -0,0 +1,44 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 53 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_EnablePreReleasePackages: 0 + m_AdvancedSettingsExpanded: 1 + m_ScopedRegistriesSettingsExpanded: 1 + m_SeeAllPackageVersions: 0 + m_DismissPreviewPackagesInUse: 0 + oneTimeWarningShown: 0 + m_Registries: + - m_Id: main + m_Name: + m_Url: https://packages.unity.com + m_Scopes: [] + m_IsDefault: 1 + m_Capabilities: 7 + m_ConfigSource: 0 + - m_Id: scoped:project:package.openupm.com + m_Name: package.openupm.com + m_Url: https://package.openupm.com + m_Scopes: + - com.revenantx.litenetlib + m_IsDefault: 0 + m_Capabilities: 0 + m_ConfigSource: 4 + m_UserSelectedRegistryName: package.openupm.com + m_UserAddingNewScopedRegistry: 0 + m_RegistryInfoDraft: + m_Modified: 0 + m_ErrorMessage: + m_UserModificationsInstanceId: -834 + m_OriginalInstanceId: -836 + m_LoadAssets: 0 diff --git a/LiteNetLibSampleUnity/ProjectSettings/PresetManager.asset b/LiteNetLibSampleUnity/ProjectSettings/PresetManager.asset index 3f07844c..67a94dae 100644 Binary files a/LiteNetLibSampleUnity/ProjectSettings/PresetManager.asset and b/LiteNetLibSampleUnity/ProjectSettings/PresetManager.asset differ diff --git a/LiteNetLibSampleUnity/ProjectSettings/ProjectSettings.asset b/LiteNetLibSampleUnity/ProjectSettings/ProjectSettings.asset index 85038959..ed32fde8 100644 --- a/LiteNetLibSampleUnity/ProjectSettings/ProjectSettings.asset +++ b/LiteNetLibSampleUnity/ProjectSettings/ProjectSettings.asset @@ -3,9 +3,11 @@ --- !u!129 &1 PlayerSettings: m_ObjectHideFlags: 0 - serializedVersion: 11 + serializedVersion: 26 productGUID: cf592f4b245d2b540a06dd6461f51365 AndroidProfiler: 0 + AndroidFilterTouchesWhenObscured: 0 + AndroidEnableSustainedPerformanceMode: 0 defaultScreenOrientation: 4 targetDevice: 2 useOnDemandResources: 0 @@ -14,7 +16,7 @@ PlayerSettings: productName: LiteNetLibSampleUnity defaultCursor: {fileID: 0} cursorHotspot: {x: 0, y: 0} - m_SplashScreenBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21176471, a: 1} + m_SplashScreenBackgroundColor: {r: 0.12156863, g: 0.12156863, b: 0.1254902, a: 1} m_ShowUnitySplashScreen: 1 m_ShowUnitySplashLogo: 1 m_SplashScreenOverlayOpacity: 1 @@ -38,8 +40,6 @@ PlayerSettings: width: 1 height: 1 m_SplashScreenLogos: [] - m_SplashScreenBackgroundLandscape: {fileID: 0} - m_SplashScreenBackgroundPortrait: {fileID: 0} m_VirtualRealitySplashScreen: {fileID: 0} m_HolographicTrackingLossScreen: {fileID: 0} defaultScreenWidth: 1024 @@ -48,37 +48,55 @@ PlayerSettings: defaultScreenHeightWeb: 600 m_StereoRenderingPath: 0 m_ActiveColorSpace: 0 + unsupportedMSAAFallback: 0 + m_SpriteBatchVertexThreshold: 300 m_MTRendering: 1 - m_MobileMTRendering: 0 + mipStripping: 0 + numberOfMipsStripped: 0 + numberOfMipsStrippedPerMipmapLimitGroup: {} m_StackTraceTypes: 010000000100000001000000010000000100000001000000 iosShowActivityIndicatorOnLoading: -1 androidShowActivityIndicatorOnLoading: -1 - tizenShowActivityIndicatorOnLoading: -1 - iosAppInBackgroundBehavior: 0 - displayResolutionDialog: 1 - iosAllowHTTPDownload: 1 + iosUseCustomAppBackgroundBehavior: 0 allowedAutorotateToPortrait: 1 allowedAutorotateToPortraitUpsideDown: 1 allowedAutorotateToLandscapeRight: 1 allowedAutorotateToLandscapeLeft: 1 useOSAutorotation: 1 use32BitDisplayBuffer: 1 + preserveFramebufferAlpha: 0 disableDepthAndStencilBuffers: 0 - defaultIsFullScreen: 1 + androidStartInFullscreen: 1 + androidRenderOutsideSafeArea: 1 + androidUseSwappy: 0 + androidBlitType: 0 + androidResizableWindow: 0 + androidDefaultWindowWidth: 1920 + androidDefaultWindowHeight: 1080 + androidMinimumWindowWidth: 400 + androidMinimumWindowHeight: 300 + androidFullscreenMode: 1 + androidAutoRotationBehavior: 1 defaultIsNativeResolution: 1 + macRetinaSupport: 1 runInBackground: 0 captureSingleScreen: 0 muteOtherAudioSources: 0 Prepare IOS For Recording: 0 + Force IOS Speakers When Recording: 0 + audioSpatialExperience: 0 + deferSystemGesturesMode: 0 + hideHomeButton: 0 submitAnalytics: 1 usePlayerLog: 1 + dedicatedServerOptimizations: 0 bakeCollisionMeshes: 0 forceSingleInstance: 0 + useFlipModelSwapchain: 1 resizableWindow: 0 useMacAppStoreValidation: 0 macAppStoreCategory: public.app-category.games gpuSkinning: 0 - graphicsJobs: 0 xboxPIXTextureCapture: 0 xboxEnableAvatar: 0 xboxEnableKinect: 0 @@ -86,66 +104,77 @@ PlayerSettings: xboxEnableFitness: 0 visibleInBackground: 0 allowFullscreenSwitch: 1 - graphicsJobMode: 0 - macFullscreenMode: 2 - d3d9FullscreenMode: 1 - d3d11FullscreenMode: 1 + fullscreenMode: 1 xboxSpeechDB: 0 xboxEnableHeadOrientation: 0 xboxEnableGuest: 0 xboxEnablePIXSampling: 0 - n3dsDisableStereoscopicView: 0 - n3dsEnableSharedListOpt: 1 - n3dsEnableVSync: 0 - ignoreAlphaClear: 0 + metalFramebufferOnly: 0 xboxOneResolution: 0 + xboxOneSResolution: 0 + xboxOneXResolution: 3 xboxOneMonoLoggingLevel: 0 xboxOneLoggingLevel: 1 - videoMemoryForVertexBuffers: 0 - psp2PowerMode: 0 - psp2AcquireBGM: 1 - wiiUTVResolution: 0 - wiiUGamePadMSAA: 1 - wiiUSupportsNunchuk: 0 - wiiUSupportsClassicController: 0 - wiiUSupportsBalanceBoard: 0 - wiiUSupportsMotionPlus: 0 - wiiUSupportsProController: 0 - wiiUAllowScreenCapture: 1 - wiiUControllerCount: 0 - m_SupportedAspectRatios: - 4:3: 1 - 5:4: 1 - 16:10: 1 - 16:9: 1 - Others: 1 + xboxOneDisableEsram: 0 + xboxOneEnableTypeOptimization: 0 + xboxOnePresentImmediateThreshold: 0 + switchQueueCommandMemory: 1048576 + switchQueueControlMemory: 16384 + switchQueueComputeMemory: 262144 + switchNVNShaderPoolsGranularity: 33554432 + switchNVNDefaultPoolsGranularity: 16777216 + switchNVNOtherPoolsGranularity: 16777216 + switchGpuScratchPoolGranularity: 2097152 + switchAllowGpuScratchShrinking: 0 + switchNVNMaxPublicTextureIDCount: 0 + switchNVNMaxPublicSamplerIDCount: 0 + switchNVNGraphicsFirmwareMemory: 32 + switchMaxWorkerMultiple: 8 + stadiaPresentMode: 0 + stadiaTargetFramerate: 0 + vulkanNumSwapchainBuffers: 3 + vulkanEnableSetSRGBWrite: 0 + vulkanEnablePreTransform: 0 + vulkanEnableLateAcquireNextImage: 0 + vulkanEnableCommandBufferRecycling: 1 + loadStoreDebugModeEnabled: 0 + visionOSBundleVersion: 1.0 + tvOSBundleVersion: 1.0 bundleVersion: 1.0 preloadedAssets: [] metroInputSource: 0 + wsaTransparentSwapchain: 0 m_HolographicPauseOnTrackingLoss: 1 xboxOneDisableKinectGpuReservation: 0 xboxOneEnable7thCore: 0 vrSettings: - cardboard: - depthFormat: 0 - enableTransitionView: 0 - daydream: - depthFormat: 0 - useSustainedPerformanceMode: 0 - hololens: - depthFormat: 1 - protectGraphicsMemory: 0 + enable360StereoCapture: 0 + isWsaHolographicRemotingEnabled: 0 + enableFrameTimingStats: 0 + enableOpenGLProfilerGPURecorders: 1 + allowHDRDisplaySupport: 0 useHDRDisplay: 0 + hdrBitDepth: 0 + m_ColorGamuts: 00000000 + targetPixelDensity: 30 + resolutionScalingMode: 0 + resetResolutionOnWindowResize: 0 + androidSupportedAspectRatio: 1 + androidMaxAspectRatio: 2.1 applicationIdentifier: Android: com.RevenantX.LiteNetLibTest Standalone: unity.RevenantX.LiteNetLibSampleUnity Tizen: com.RevenantX.LiteNetLibTest - iOS: com.RevenantX.LiteNetLibTest + iPhone: com.RevenantX.LiteNetLibTest tvOS: com.RevenantX.LiteNetLibTest buildNumber: - iOS: 0 + Standalone: 0 + VisionOS: 0 + iPhone: 0 + tvOS: 0 + overrideDefaultApplicationIdentifier: 1 AndroidBundleVersionCode: 1 - AndroidMinSdkVersion: 16 + AndroidMinSdkVersion: 22 AndroidTargetSdkVersion: 0 AndroidPreferredInstallLocation: 1 aotOptions: @@ -158,35 +187,31 @@ PlayerSettings: APKExpansionFiles: 0 keepLoadedShadersAlive: 0 StripUnusedMeshComponents: 0 - VertexChannelCompressionMask: - serializedVersion: 2 - m_Bits: 238 + strictShaderVariantMatching: 0 + VertexChannelCompressionMask: 214 iPhoneSdkVersion: 988 - iOSTargetOSVersionString: 6.0 + iOSTargetOSVersionString: 12.0 tvOSSdkVersion: 0 tvOSRequireExtendedGameController: 0 - tvOSTargetOSVersionString: 9.0 + tvOSTargetOSVersionString: 12.0 + VisionOSSdkVersion: 0 + VisionOSTargetOSVersionString: 1.0 uIPrerenderedIcon: 0 uIRequiresPersistentWiFi: 0 uIRequiresFullScreen: 1 uIStatusBarHidden: 1 uIExitOnSuspend: 0 uIStatusBarStyle: 0 - iPhoneSplashScreen: {fileID: 0} - iPhoneHighResSplashScreen: {fileID: 0} - iPhoneTallHighResSplashScreen: {fileID: 0} - iPhone47inSplashScreen: {fileID: 0} - iPhone55inPortraitSplashScreen: {fileID: 0} - iPhone55inLandscapeSplashScreen: {fileID: 0} - iPadPortraitSplashScreen: {fileID: 0} - iPadHighResPortraitSplashScreen: {fileID: 0} - iPadLandscapeSplashScreen: {fileID: 0} - iPadHighResLandscapeSplashScreen: {fileID: 0} appleTVSplashScreen: {fileID: 0} + appleTVSplashScreen2x: {fileID: 0} tvOSSmallIconLayers: [] + tvOSSmallIconLayers2x: [] tvOSLargeIconLayers: [] + tvOSLargeIconLayers2x: [] tvOSTopShelfImageLayers: [] + tvOSTopShelfImageLayers2x: [] tvOSTopShelfImageWideLayers: [] + tvOSTopShelfImageWideLayers2x: [] iOSLaunchScreenType: 0 iOSLaunchScreenPortrait: {fileID: 0} iOSLaunchScreenLandscape: {fileID: 0} @@ -204,31 +229,65 @@ PlayerSettings: iOSLaunchScreeniPadFillPct: 100 iOSLaunchScreeniPadSize: 100 iOSLaunchScreeniPadCustomXibPath: + iOSLaunchScreenCustomStoryboardPath: + iOSLaunchScreeniPadCustomStoryboardPath: iOSDeviceRequirements: [] iOSURLSchemes: [] + macOSURLSchemes: [] iOSBackgroundModes: 0 iOSMetalForceHardShadows: 0 metalEditorSupport: 1 metalAPIValidation: 1 + metalCompileShaderBinary: 0 iOSRenderExtraFrameOnPause: 1 + iosCopyPluginsCodeInsteadOfSymlink: 0 appleDeveloperTeamID: iOSManualSigningProvisioningProfileID: tvOSManualSigningProvisioningProfileID: + VisionOSManualSigningProvisioningProfileID: + iOSManualSigningProvisioningProfileType: 0 + tvOSManualSigningProvisioningProfileType: 0 + VisionOSManualSigningProvisioningProfileType: 0 appleEnableAutomaticSigning: 0 - AndroidTargetDevice: 0 + iOSRequireARKit: 0 + iOSAutomaticallyDetectAndAddCapabilities: 1 + appleEnableProMotion: 0 + shaderPrecisionModel: 0 + clonedFromGUID: 00000000000000000000000000000000 + templatePackageId: + templateDefaultScene: + useCustomMainManifest: 0 + useCustomLauncherManifest: 0 + useCustomMainGradleTemplate: 0 + useCustomLauncherGradleManifest: 0 + useCustomBaseGradleTemplate: 0 + useCustomGradlePropertiesTemplate: 0 + useCustomGradleSettingsTemplate: 0 + useCustomProguardFile: 0 + AndroidTargetArchitectures: 1 + AndroidTargetDevices: 0 AndroidSplashScreenScale: 0 androidSplashScreen: {fileID: 0} - AndroidKeystoreName: + AndroidKeystoreName: '{inproject}: ' AndroidKeyaliasName: + AndroidEnableArmv9SecurityFeatures: 0 + AndroidBuildApkPerCpuArchitecture: 0 AndroidTVCompatibility: 1 AndroidIsGame: 1 + AndroidEnableTango: 0 androidEnableBanner: 1 + androidUseLowAccuracyLocation: 0 + androidUseCustomKeystore: 0 m_AndroidBanners: - width: 320 height: 180 banner: {fileID: 0} androidGamepadSupportLevel: 0 - resolutionDialogBanner: {fileID: 0} + chromeosInputEmulation: 1 + AndroidMinifyRelease: 0 + AndroidMinifyDebug: 0 + AndroidValidateAppBundleSize: 1 + AndroidAppBundleSizeToValidate: 150 m_BuildTargetIcons: - m_BuildTarget: m_Icons: @@ -236,14 +295,256 @@ PlayerSettings: m_Icon: {fileID: 0} m_Width: 128 m_Height: 128 + m_Kind: 0 + m_BuildTargetPlatformIcons: + - m_BuildTarget: Android + m_Icons: + - m_Textures: [] + m_Width: 432 + m_Height: 432 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 324 + m_Height: 324 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 216 + m_Height: 216 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 162 + m_Height: 162 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 108 + m_Height: 108 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 81 + m_Height: 81 + m_Kind: 2 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 1 + m_SubKind: + - m_Textures: [] + m_Width: 192 + m_Height: 192 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 144 + m_Height: 144 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 96 + m_Height: 96 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 72 + m_Height: 72 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 48 + m_Height: 48 + m_Kind: 0 + m_SubKind: + - m_Textures: [] + m_Width: 36 + m_Height: 36 + m_Kind: 0 + m_SubKind: + - m_BuildTarget: iPhone + m_Icons: + - m_Textures: [] + m_Width: 180 + m_Height: 180 + m_Kind: 0 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 120 + m_Height: 120 + m_Kind: 0 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 167 + m_Height: 167 + m_Kind: 0 + m_SubKind: iPad + - m_Textures: [] + m_Width: 152 + m_Height: 152 + m_Kind: 0 + m_SubKind: iPad + - m_Textures: [] + m_Width: 76 + m_Height: 76 + m_Kind: 0 + m_SubKind: iPad + - m_Textures: [] + m_Width: 120 + m_Height: 120 + m_Kind: 3 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 80 + m_Height: 80 + m_Kind: 3 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 80 + m_Height: 80 + m_Kind: 3 + m_SubKind: iPad + - m_Textures: [] + m_Width: 40 + m_Height: 40 + m_Kind: 3 + m_SubKind: iPad + - m_Textures: [] + m_Width: 87 + m_Height: 87 + m_Kind: 1 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 58 + m_Height: 58 + m_Kind: 1 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 29 + m_Height: 29 + m_Kind: 1 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 58 + m_Height: 58 + m_Kind: 1 + m_SubKind: iPad + - m_Textures: [] + m_Width: 29 + m_Height: 29 + m_Kind: 1 + m_SubKind: iPad + - m_Textures: [] + m_Width: 60 + m_Height: 60 + m_Kind: 2 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 40 + m_Height: 40 + m_Kind: 2 + m_SubKind: iPhone + - m_Textures: [] + m_Width: 40 + m_Height: 40 + m_Kind: 2 + m_SubKind: iPad + - m_Textures: [] + m_Width: 20 + m_Height: 20 + m_Kind: 2 + m_SubKind: iPad + - m_Textures: [] + m_Width: 1024 + m_Height: 1024 + m_Kind: 4 + m_SubKind: App Store m_BuildTargetBatching: [] - m_BuildTargetGraphicsAPIs: [] + m_BuildTargetShaderSettings: [] + m_BuildTargetGraphicsJobs: + - m_BuildTarget: WindowsStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: MacStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: LinuxStandaloneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AndroidPlayer + m_GraphicsJobs: 0 + - m_BuildTarget: iOSSupport + m_GraphicsJobs: 0 + - m_BuildTarget: PS4Player + m_GraphicsJobs: 0 + - m_BuildTarget: PS5Player + m_GraphicsJobs: 0 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobs: 0 + - m_BuildTarget: GameCoreXboxOneSupport + m_GraphicsJobs: 0 + - m_BuildTarget: GameCoreScarlettSupport + m_GraphicsJobs: 0 + - m_BuildTarget: Switch + m_GraphicsJobs: 0 + - m_BuildTarget: WebGLSupport + m_GraphicsJobs: 0 + - m_BuildTarget: MetroSupport + m_GraphicsJobs: 0 + - m_BuildTarget: AppleTVSupport + m_GraphicsJobs: 0 + - m_BuildTarget: VisionOSPlayer + m_GraphicsJobs: 0 + - m_BuildTarget: BJMSupport + m_GraphicsJobs: 0 + - m_BuildTarget: CloudRendering + m_GraphicsJobs: 0 + - m_BuildTarget: EmbeddedLinux + m_GraphicsJobs: 0 + - m_BuildTarget: QNX + m_GraphicsJobs: 0 + m_BuildTargetGraphicsJobMode: + - m_BuildTarget: PS4Player + m_GraphicsJobMode: 0 + - m_BuildTarget: XboxOnePlayer + m_GraphicsJobMode: 0 + m_BuildTargetGraphicsAPIs: + - m_BuildTarget: iOSSupport + m_APIs: 10000000 + m_Automatic: 1 + - m_BuildTarget: AndroidPlayer + m_APIs: 0b00000008000000 + m_Automatic: 0 m_BuildTargetVRSettings: - m_BuildTarget: Android m_Enabled: 0 m_Devices: - Oculus - - m_BuildTarget: Metro + - m_BuildTarget: Windows Store Apps m_Enabled: 0 m_Devices: [] - m_BuildTarget: N3DS @@ -287,35 +588,50 @@ PlayerSettings: - m_BuildTarget: XboxOne m_Enabled: 0 m_Devices: [] - - m_BuildTarget: iOS + - m_BuildTarget: iPhone m_Enabled: 0 m_Devices: [] - m_BuildTarget: tvOS m_Enabled: 0 m_Devices: [] + m_DefaultShaderChunkSizeInMB: 16 + m_DefaultShaderChunkCount: 0 openGLRequireES31: 0 openGLRequireES31AEP: 0 - webPlayerTemplate: APPLICATION:Default + openGLRequireES32: 0 m_TemplateCustomTags: {} - wiiUTitleID: 0005000011000000 - wiiUGroupID: 00010000 - wiiUCommonSaveSize: 4096 - wiiUAccountSaveSize: 2048 - wiiUOlvAccessKey: 0 - wiiUTinCode: 0 - wiiUJoinGameId: 0 - wiiUJoinGameModeMask: 0000000000000000 - wiiUCommonBossSize: 0 - wiiUAccountBossSize: 0 - wiiUAddOnUniqueIDs: [] - wiiUMainThreadStackSize: 3072 - wiiULoaderThreadStackSize: 1024 - wiiUSystemHeapSize: 128 - wiiUTVStartupScreen: {fileID: 0} - wiiUGamePadStartupScreen: {fileID: 0} - wiiUDrcBufferDisabled: 0 - wiiUProfilerLibPath: + mobileMTRendering: + VisionOS: 1 + iPhone: 1 + tvOS: 1 + m_BuildTargetGroupLightmapEncodingQuality: + - m_BuildTarget: Standalone + m_EncodingQuality: 1 + - m_BuildTarget: XboxOne + m_EncodingQuality: 1 + - m_BuildTarget: PS4 + m_EncodingQuality: 1 + - m_BuildTarget: GameCoreScarlett + m_EncodingQuality: 1 + - m_BuildTarget: GameCoreXboxOne + m_EncodingQuality: 1 + m_BuildTargetGroupHDRCubemapEncodingQuality: + - m_BuildTarget: Standalone + m_EncodingQuality: 2 + - m_BuildTarget: XboxOne + m_EncodingQuality: 2 + - m_BuildTarget: PS4 + m_EncodingQuality: 2 + - m_BuildTarget: GameCoreScarlett + m_EncodingQuality: 2 + - m_BuildTarget: GameCoreXboxOne + m_EncodingQuality: 2 + m_BuildTargetGroupLightmapSettings: [] + m_BuildTargetGroupLoadStoreDebugModeSettings: [] + m_BuildTargetNormalMapEncoding: [] + m_BuildTargetDefaultTextureCompressionFormat: [] playModeTestRunnerEnabled: 0 + runPlayModeTestAsEditModeTest: 0 actionOnDotNetUnhandledException: 1 enableInternalProfiler: 0 logObjCUncaughtExceptions: 1 @@ -323,13 +639,20 @@ PlayerSettings: cameraUsageDescription: locationUsageDescription: microphoneUsageDescription: + bluetoothUsageDescription: + macOSTargetOSVersion: 10.13.0 + switchNMETAOverride: switchNetLibKey: switchSocketMemoryPoolSize: 6144 switchSocketAllocatorPoolSize: 128 switchSocketConcurrencyLimit: 14 + switchScreenResolutionBehavior: 2 switchUseCPUProfiler: 0 + switchEnableFileSystemTrace: 0 + switchLTOSetting: 0 switchApplicationID: 0x0005000C10000001 switchNSODependencies: + switchCompilerFlags: switchTitleNames_0: switchTitleNames_1: switchTitleNames_2: @@ -345,6 +668,7 @@ PlayerSettings: switchTitleNames_12: switchTitleNames_13: switchTitleNames_14: + switchTitleNames_15: switchPublisherNames_0: switchPublisherNames_1: switchPublisherNames_2: @@ -360,6 +684,7 @@ PlayerSettings: switchPublisherNames_12: switchPublisherNames_13: switchPublisherNames_14: + switchPublisherNames_15: switchIcons_0: {fileID: 0} switchIcons_1: {fileID: 0} switchIcons_2: {fileID: 0} @@ -375,6 +700,7 @@ PlayerSettings: switchIcons_12: {fileID: 0} switchIcons_13: {fileID: 0} switchIcons_14: {fileID: 0} + switchIcons_15: {fileID: 0} switchSmallIcons_0: {fileID: 0} switchSmallIcons_1: {fileID: 0} switchSmallIcons_2: {fileID: 0} @@ -390,6 +716,7 @@ PlayerSettings: switchSmallIcons_12: {fileID: 0} switchSmallIcons_13: {fileID: 0} switchSmallIcons_14: {fileID: 0} + switchSmallIcons_15: {fileID: 0} switchManualHTML: switchAccessibleURLs: switchLegalInformation: @@ -399,13 +726,12 @@ PlayerSettings: switchReleaseVersion: 0 switchDisplayVersion: 1.0.0 switchStartupUserAccount: 0 - switchTouchScreenUsage: 0 switchSupportedLanguagesMask: 0 switchLogoType: 0 switchApplicationErrorCodeCategory: switchUserAccountSaveDataSize: 0 switchUserAccountSaveDataJournalSize: 0 - switchAttribute: 0 + switchApplicationAttribute: 0 switchCardSpecSize: 4 switchCardSpecClock: 25 switchRatingsMask: 0 @@ -421,6 +747,7 @@ PlayerSettings: switchRatingsInt_9: 0 switchRatingsInt_10: 0 switchRatingsInt_11: 0 + switchRatingsInt_12: 0 switchLocalCommunicationIds_0: 0x0005000C10000001 switchLocalCommunicationIds_1: switchLocalCommunicationIds_2: @@ -431,7 +758,32 @@ PlayerSettings: switchLocalCommunicationIds_7: switchParentalControl: 0 switchAllowsScreenshot: 1 + switchAllowsVideoCapturing: 1 + switchAllowsRuntimeAddOnContentInstall: 0 switchDataLossConfirmation: 0 + switchUserAccountLockEnabled: 0 + switchSystemResourceMemory: 16777216 + switchSupportedNpadStyles: 22 + switchNativeFsCacheSize: 32 + switchIsHoldTypeHorizontal: 1 + switchSupportedNpadCount: 8 + switchEnableTouchScreen: 1 + switchSocketConfigEnabled: 0 + switchTcpInitialSendBufferSize: 32 + switchTcpInitialReceiveBufferSize: 64 + switchTcpAutoSendBufferSizeMax: 256 + switchTcpAutoReceiveBufferSizeMax: 256 + switchUdpSendBufferSize: 9 + switchUdpReceiveBufferSize: 42 + switchSocketBufferEfficiency: 4 + switchSocketInitializeEnabled: 1 + switchNetworkInterfaceManagerInitializeEnabled: 1 + switchUseNewStyleFilepaths: 1 + switchUseLegacyFmodPriorities: 0 + switchUseMicroSleepForYield: 1 + switchEnableRamDiskSupport: 0 + switchMicroSleepForYieldTime: 25 + switchRamDiskSpaceSize: 12 ps4NPAgeRating: 12 ps4NPTitleSecret: ps4NPTrophyPackPath: @@ -450,12 +802,15 @@ PlayerSettings: ps4PronunciationSIGPath: ps4BackgroundImagePath: ps4StartupImagePath: + ps4StartupImagesFolder: + ps4IconImagesFolder: ps4SaveDataImagePath: ps4SdkOverride: ps4BGMPath: ps4ShareFilePath: ps4ShareOverlayImagePath: ps4PrivacyGuardImagePath: + ps4ExtraSceSysFile: ps4NPtitleDatPath: ps4RemotePlayKeyAssignment: -1 ps4RemotePlayKeyMappingDir: @@ -468,17 +823,20 @@ PlayerSettings: ps4DownloadDataSize: 0 ps4GarlicHeapSize: 2048 ps4ProGarlicHeapSize: 2560 + playerPrefsMaxSize: 32768 ps4Passcode: frAQBc8Wsa1xVPfvJcrgRYwTiizs2trQ - ps4UseDebugIl2cppLibs: 0 ps4pnSessions: 1 ps4pnPresence: 1 ps4pnFriends: 1 ps4pnGameCustomData: 1 playerPrefsSupport: 0 + enableApplicationExit: 0 + resetTempFolder: 1 restrictedAudioUsageRights: 0 ps4UseResolutionFallback: 0 ps4ReprojectionSupport: 0 ps4UseAudio3dBackend: 0 + ps4UseLowGarlicFragmentationMode: 1 ps4SocialScreenEnabled: 0 ps4ScriptOptimizationLevel: 3 ps4Audio3dVirtualSpeakerCount: 14 @@ -495,63 +853,21 @@ PlayerSettings: ps4disableAutoHideSplash: 0 ps4videoRecordingFeaturesUsed: 0 ps4contentSearchFeaturesUsed: 0 + ps4CompatibilityPS5: 0 + ps4AllowPS5Detection: 0 + ps4GPU800MHz: 1 ps4attribEyeToEyeDistanceSettingVR: 0 ps4IncludedModules: [] + ps4attribVROutputEnabled: 0 monoEnv: - psp2Splashimage: {fileID: 0} - psp2NPTrophyPackPath: - psp2NPSupportGBMorGJP: 0 - psp2NPAgeRating: 12 - psp2NPTitleDatPath: - psp2NPCommsID: - psp2NPCommunicationsID: - psp2NPCommsPassphrase: - psp2NPCommsSig: - psp2ParamSfxPath: - psp2ManualPath: - psp2LiveAreaGatePath: - psp2LiveAreaBackroundPath: - psp2LiveAreaPath: - psp2LiveAreaTrialPath: - psp2PatchChangeInfoPath: - psp2PatchOriginalPackage: - psp2PackagePassword: F69AzBlax3CF3EDNhm3soLBPh71Yexui - psp2KeystoneFile: - psp2MemoryExpansionMode: 0 - psp2DRMType: 0 - psp2StorageType: 0 - psp2MediaCapacity: 0 - psp2DLCConfigPath: - psp2ThumbnailPath: - psp2BackgroundPath: - psp2SoundPath: - psp2TrophyCommId: - psp2TrophyPackagePath: - psp2PackagedResourcesPath: - psp2SaveDataQuota: 10240 - psp2ParentalLevel: 1 - psp2ShortTitle: Not Set - psp2ContentID: IV0000-ABCD12345_00-0123456789ABCDEF - psp2Category: 0 - psp2MasterVersion: 01.00 - psp2AppVersion: 01.00 - psp2TVBootMode: 0 - psp2EnterButtonAssignment: 2 - psp2TVDisableEmu: 0 - psp2AllowTwitterDialog: 1 - psp2Upgradable: 0 - psp2HealthWarning: 0 - psp2UseLibLocation: 0 - psp2InfoBarOnStartup: 0 - psp2InfoBarColor: 0 - psp2UseDebugIl2cppLibs: 0 - psmSplashimage: {fileID: 0} splashScreenBackgroundSourceLandscape: {fileID: 0} splashScreenBackgroundSourcePortrait: {fileID: 0} + blurSplashScreenBackground: 1 spritePackerPolicy: webGLMemorySize: 256 webGLExceptionSupport: 1 webGLNameFilesAsHashes: 0 + webGLShowDiagnostics: 0 webGLDataCaching: 0 webGLDebugSymbols: 0 webGLEmscriptenArgs: @@ -559,19 +875,53 @@ PlayerSettings: webGLTemplate: APPLICATION:Default webGLAnalyzeBuildSize: 0 webGLUseEmbeddedResources: 0 - webGLUseWasm: 0 webGLCompressionFormat: 1 + webGLWasmArithmeticExceptions: 0 + webGLLinkerTarget: 1 + webGLThreadsSupport: 0 + webGLDecompressionFallback: 0 + webGLInitialMemorySize: 32 + webGLMaximumMemorySize: 2048 + webGLMemoryGrowthMode: 2 + webGLMemoryLinearGrowthStep: 16 + webGLMemoryGeometricGrowthStep: 0.2 + webGLMemoryGeometricGrowthCap: 96 + webGLPowerPreference: 2 scriptingDefineSymbols: - 7: UNITY + Android: UNITY + additionalCompilerArguments: {} platformArchitecture: {} scriptingBackend: Android: 0 - Metro: 2 Standalone: 0 WebGL: 1 WebPlayer: 0 + Windows Store Apps: 2 + il2cppCompilerConfiguration: {} + il2cppCodeGeneration: {} + managedStrippingLevel: + EmbeddedLinux: 1 + GameCoreScarlett: 1 + GameCoreXboxOne: 1 + Nintendo Switch: 1 + PS4: 1 + PS5: 1 + QNX: 1 + Stadia: 1 + VisionOS: 1 + WebGL: 1 + Windows Store Apps: 1 + XboxOne: 1 + iPhone: 1 + tvOS: 1 incrementalIl2cppBuild: {} + suppressCommonWarnings: 1 + allowUnsafeCode: 0 + useDeterministicCompilation: 1 additionalIl2CppArgs: + scriptingRuntimeVersion: 1 + gcIncremental: 1 + gcWBarrierValidation: 0 apiCompatibilityLevelPerPlatform: {} m_RenderingPath: 1 m_MobileRenderingPath: 1 @@ -585,47 +935,25 @@ PlayerSettings: metroApplicationDescription: LiteNetLibSampleUnity wsaImages: {} metroTileShortName: - metroCommandLineArgsFile: metroTileShowName: 0 metroMediumTileShowName: 0 metroLargeTileShowName: 0 metroWideTileShowName: 0 + metroSupportStreamingInstall: 0 + metroLastRequiredScene: 0 metroDefaultTileSize: 1 metroTileForegroundText: 1 metroTileBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 0} metroSplashScreenBackgroundColor: {r: 0.13333334, g: 0.17254902, b: 0.21568628, a: 1} metroSplashScreenUseBackgroundColor: 1 + syncCapabilities: 0 platformCapabilities: {} + metroTargetDeviceFamilies: {} metroFTAName: metroFTAFileTypes: [] metroProtocolName: - metroCompilationOverrides: 1 - tizenProductDescription: - tizenProductURL: - tizenSigningProfileName: - tizenGPSPermissions: 0 - tizenMicrophonePermissions: 0 - tizenDeploymentTarget: - tizenDeploymentTargetType: -1 - tizenMinOSVersion: 1 - n3dsUseExtSaveData: 0 - n3dsCompressStaticMem: 1 - n3dsExtSaveDataNumber: 0x12345 - n3dsStackSize: 131072 - n3dsTargetPlatform: 2 - n3dsRegion: 7 - n3dsMediaSize: 0 - n3dsLogoStyle: 3 - n3dsTitle: GameName - n3dsProductCode: - n3dsApplicationId: 0xFF3FF - stvDeviceAddress: - stvProductDescription: - stvProductAuthor: - stvProductAuthorEmail: - stvProductLink: - stvProductCategory: 0 + vcxProjDefaultLanguage: XboxOneProductId: XboxOneUpdateKey: XboxOneSandboxId: @@ -635,6 +963,7 @@ PlayerSettings: XboxOneGameOsOverridePath: XboxOnePackagingOverridePath: XboxOneAppManifestOverridePath: + XboxOneVersion: 1.0.0.0 XboxOnePackageEncryption: 0 XboxOnePackageUpdateGranularity: 2 XboxOneDescription: @@ -643,16 +972,16 @@ PlayerSettings: XboxOneCapability: [] XboxOneGameRating: {} XboxOneIsContentPackage: 0 + XboxOneEnhancedXboxCompatibilityMode: 0 XboxOneEnableGPUVariability: 0 XboxOneSockets: {} XboxOneSplashScreen: {fileID: 0} XboxOneAllowedProductIds: [] XboxOnePersistentLocalStorageSize: 0 - xboxOneScriptCompiler: 0 - vrEditorSettings: - daydream: - daydreamIconForeground: {fileID: 0} - daydreamIconBackground: {fileID: 0} + XboxOneXTitleMemory: 8 + XboxOneOverrideIdentityName: + XboxOneOverrideIdentityPublisher: + vrEditorSettings: {} cloudServicesEnabled: Analytics: 0 Build: 0 @@ -663,10 +992,33 @@ PlayerSettings: Purchasing: 0 UNet: 0 Unity_Ads: 0 - facebookSdkVersion: 7.9.1 - apiCompatibilityLevel: 2 + luminIcon: + m_Name: + m_ModelFolderPath: + m_PortalFolderPath: + luminCert: + m_CertPath: + m_SignPackage: 1 + luminIsChannelApp: 0 + luminVersion: + m_VersionCode: 1 + m_VersionName: + hmiPlayerDataPath: + hmiForceSRGBBlit: 1 + embeddedLinuxEnableGamepadInput: 1 + hmiLogStartupTiming: 0 + hmiCpuConfiguration: + apiCompatibilityLevel: 6 + activeInputHandler: 0 + windowsGamepadBackendHint: 0 cloudProjectId: + framebufferDepthMemorylessMode: 0 + qualitySettingsNames: [] projectName: organizationId: cloudEnabled: 0 - enableNewInputSystem: 0 + legacyClampBlendShapeWeights: 1 + hmiLoadingImage: {fileID: 0} + platformRequiresReadableAssets: 0 + virtualTexturingSupportEnabled: 0 + insecureHttpOption: 0 diff --git a/LiteNetLibSampleUnity/ProjectSettings/ProjectVersion.txt b/LiteNetLibSampleUnity/ProjectSettings/ProjectVersion.txt index d848e35e..8fb87e60 100644 --- a/LiteNetLibSampleUnity/ProjectSettings/ProjectVersion.txt +++ b/LiteNetLibSampleUnity/ProjectSettings/ProjectVersion.txt @@ -1 +1,2 @@ -m_EditorVersion: 2018.3.8f1 +m_EditorVersion: 2022.3.50f1 +m_EditorVersionWithRevision: 2022.3.50f1 (c3db7f8bf9b1) diff --git a/LiteNetLibSampleUnity/ProjectSettings/VFXManager.asset b/LiteNetLibSampleUnity/ProjectSettings/VFXManager.asset index 85b0821b..852348bc 100644 Binary files a/LiteNetLibSampleUnity/ProjectSettings/VFXManager.asset and b/LiteNetLibSampleUnity/ProjectSettings/VFXManager.asset differ diff --git a/LiteNetLibSampleUnity/ProjectSettings/VersionControlSettings.asset b/LiteNetLibSampleUnity/ProjectSettings/VersionControlSettings.asset new file mode 100644 index 00000000..dca28814 --- /dev/null +++ b/LiteNetLibSampleUnity/ProjectSettings/VersionControlSettings.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!890905787 &1 +VersionControlSettings: + m_ObjectHideFlags: 0 + m_Mode: Visible Meta Files + m_CollabEditorSettings: + inProgressEnabled: 1 diff --git a/LiteNetLibSampleUnity/UserSettings/EditorUserSettings.asset b/LiteNetLibSampleUnity/UserSettings/EditorUserSettings.asset new file mode 100644 index 00000000..18e0c11a --- /dev/null +++ b/LiteNetLibSampleUnity/UserSettings/EditorUserSettings.asset @@ -0,0 +1,28 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!162 &1 +EditorUserSettings: + m_ObjectHideFlags: 0 + serializedVersion: 4 + m_ConfigSettings: + RecentlyUsedSceneGuid-0: + value: 0700075501055a5f5e5b5f2742275e4413164e792e2e25362f281b60bab5353b + flags: 0 + vcSharedLogLevel: + value: 0d5e400f0650 + flags: 0 + m_VCAutomaticAdd: 1 + m_VCDebugCom: 0 + m_VCDebugCmd: 0 + m_VCDebugOut: 0 + m_SemanticMergeMode: 2 + m_DesiredImportWorkerCount: 4 + m_StandbyImportWorkerCount: 2 + m_IdleImportWorkerShutdownDelay: 60000 + m_VCShowFailedCheckout: 1 + m_VCOverwriteFailedCheckoutAssets: 1 + m_VCProjectOverlayIcons: 1 + m_VCHierarchyOverlayIcons: 1 + m_VCOtherOverlayIcons: 1 + m_VCAllowAsyncUpdate: 1 + m_ArtifactGarbageCollection: 1 diff --git a/LiteNetLibSampleUnity/UserSettings/Layouts/CurrentMaximizeLayout.dwlt b/LiteNetLibSampleUnity/UserSettings/Layouts/CurrentMaximizeLayout.dwlt new file mode 100644 index 00000000..956ebeb9 --- /dev/null +++ b/LiteNetLibSampleUnity/UserSettings/Layouts/CurrentMaximizeLayout.dwlt @@ -0,0 +1,1680 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 3} + - {fileID: 8} + - {fileID: 13} + m_Position: + serializedVersion: 2 + x: 0 + y: 30 + width: 2560 + height: 1299 + m_MinSize: {x: 300, y: 100} + m_MaxSize: {x: 24288, y: 16192} + vertical: 0 + controlID: 372 +--- !u!114 &2 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12015, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Game + m_Image: {fileID: -6423792434712278376, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 0 + y: 19 + width: 1492 + height: 1278 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_SerializedViewNames: [] + m_SerializedViewValues: [] + m_PlayModeViewName: GameView + m_ShowGizmos: 0 + m_TargetDisplay: 0 + m_ClearColor: {r: 0, g: 0, b: 0, a: 0} + m_TargetSize: {x: 1492, y: 839} + m_TextureFilterMode: 0 + m_TextureHideFlags: 61 + m_RenderIMGUI: 1 + m_EnterPlayModeBehavior: 1 + m_UseMipMap: 0 + m_VSyncEnabled: 1 + m_Gizmos: 0 + m_Stats: 0 + m_SelectedSizes: 0100000000000000000000000e000000000000000000000000000000000000000000000000000000 + m_ZoomArea: + m_HRangeLocked: 0 + m_VRangeLocked: 0 + hZoomLockedByDefault: 0 + vZoomLockedByDefault: 0 + m_HBaseRangeMin: -746 + m_HBaseRangeMax: 746 + m_VBaseRangeMin: -419.5 + m_VBaseRangeMax: 419.5 + m_HAllowExceedBaseRangeMin: 1 + m_HAllowExceedBaseRangeMax: 1 + m_VAllowExceedBaseRangeMin: 1 + m_VAllowExceedBaseRangeMax: 1 + m_ScaleWithWindow: 0 + m_HSlider: 0 + m_VSlider: 0 + m_IgnoreScrollWheelUntilClicked: 0 + m_EnableMouseInput: 0 + m_EnableSliderZoomHorizontal: 0 + m_EnableSliderZoomVertical: 0 + m_UniformScale: 1 + m_UpDirection: 1 + m_DrawArea: + serializedVersion: 2 + x: 0 + y: 21 + width: 1492 + height: 1257 + m_Scale: {x: 1, y: 1} + m_Translation: {x: 746, y: 628.5} + m_MarginLeft: 0 + m_MarginRight: 0 + m_MarginTop: 0 + m_MarginBottom: 0 + m_LastShownAreaInsideMargins: + serializedVersion: 2 + x: -746 + y: -628.5 + width: 1492 + height: 1257 + m_MinimalGUI: 1 + m_defaultScale: 1 + m_LastWindowPixelSize: {x: 1492, y: 1278} + m_ClearInEditMode: 1 + m_NoCameraWarning: 1 + m_LowResolutionForAspectRatios: 01000001000000000000 + m_XRRenderMode: 0 + m_RenderTexture: {fileID: 0} +--- !u!114 &3 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 4} + - {fileID: 6} + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 1493 + height: 1299 + m_MinSize: {x: 100, y: 100} + m_MaxSize: {x: 8096, y: 16192} + vertical: 1 + controlID: 496 +--- !u!114 &4 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: GameView + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 1493 + height: 866 + m_MinSize: {x: 201, y: 221} + m_MaxSize: {x: 4001, y: 4021} + m_ActualView: {fileID: 2} + m_Panes: + - {fileID: 5} + - {fileID: 2} + m_Selected: 1 + m_LastSelected: 0 +--- !u!114 &5 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12013, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Scene + m_Image: {fileID: 2593428753322112591, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 0 + y: 73 + width: 1492 + height: 845 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: + - dockPosition: 0 + containerId: overlay-toolbar__top + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: -166, y: -26} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 3 + id: Tool Settings + index: 0 + layout: 1 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 0 + containerId: overlay-toolbar__top + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: -141, y: 149} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 1 + id: unity-grid-and-snap-toolbar + index: 1 + layout: 1 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-toolbar__top + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 0, y: 25} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: unity-scene-view-toolbar + index: 0 + layout: 1 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-toolbar__top + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 1 + id: unity-search-toolbar + index: 1 + layout: 1 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 0 + containerId: overlay-container--left + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 0, y: 25} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: unity-transform-toolbar + index: 0 + layout: 2 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 0 + containerId: overlay-container--left + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 0, y: 197} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: unity-component-tools + index: 1 + layout: 2 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 0 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 67.5, y: -123} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 2 + id: Orientation + index: 0 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Light Settings + index: 2 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Camera + index: 1 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Cloth Constraints + index: 3 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Cloth Collisions + index: 4 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Navmesh Display + index: 4 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Agent Display + index: 5 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Obstacle Display + index: 6 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Occlusion Culling + index: 5 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Physics Debugger + index: 6 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Scene Visibility + index: 7 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 25} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Particles + index: 8 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Tilemap + index: 11 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Tilemap Palette Helper + index: 12 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: -216, y: -156} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 3 + id: AINavigationOverlay + index: 9 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 48, y: 48} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: APV Overlay + index: 8 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 48, y: 48} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/TrailRenderer + index: 10 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 48, y: 10} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: UnityEditor.SceneViewCameraOverlay + index: 10 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 48, y: 48} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Open Tile Palette + index: 0 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 48, y: 48} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Tilemap Focus + index: 1 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + m_OverlaysVisible: 1 + m_WindowGUID: c9ee0d44a80756c48ba1fd3e952ef1b5 + m_Gizmos: 1 + m_OverrideSceneCullingMask: 6917529027641081856 + m_SceneIsLit: 1 + m_SceneLighting: 1 + m_2DMode: 0 + m_isRotationLocked: 0 + m_PlayAudio: 0 + m_AudioPlay: 0 + m_Position: + m_Target: {x: 0, y: 0, z: 0} + speed: 2 + m_Value: {x: 0, y: 0, z: 0} + m_RenderMode: 0 + m_CameraMode: + drawMode: 0 + name: Shaded + section: Shading Mode + m_ValidateTrueMetals: 0 + m_DoValidateTrueMetals: 0 + m_SceneViewState: + m_AlwaysRefresh: 0 + showFog: 1 + showSkybox: 1 + showFlares: 1 + showImageEffects: 1 + showParticleSystems: 1 + showVisualEffectGraphs: 1 + m_FxEnabled: 1 + m_Grid: + xGrid: + m_Fade: + m_Target: 0 + speed: 2 + m_Value: 0 + m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 0.4} + m_Pivot: {x: 0, y: 0, z: 0} + m_Size: {x: 0, y: 0} + yGrid: + m_Fade: + m_Target: 1 + speed: 2 + m_Value: 1 + m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 0.4} + m_Pivot: {x: 0, y: 0, z: 0} + m_Size: {x: 1, y: 1} + zGrid: + m_Fade: + m_Target: 0 + speed: 2 + m_Value: 0 + m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 0.4} + m_Pivot: {x: 0, y: 0, z: 0} + m_Size: {x: 1, y: 1} + m_ShowGrid: 1 + m_GridAxis: 1 + m_gridOpacity: 0.5 + m_Rotation: + m_Target: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} + speed: 2 + m_Value: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} + m_Size: + m_Target: 10 + speed: 2 + m_Value: 10 + m_Ortho: + m_Target: 0 + speed: 2 + m_Value: 0 + m_CameraSettings: + m_Speed: 0.8856 + m_SpeedNormalized: 0.44 + m_SpeedMin: 0.01 + m_SpeedMax: 2 + m_EasingEnabled: 1 + m_EasingDuration: 0.4 + m_AccelerationEnabled: 1 + m_FieldOfViewHorizontalOrVertical: 60 + m_NearClip: 0.03 + m_FarClip: 10000 + m_DynamicClip: 1 + m_OcclusionCulling: 0 + m_LastSceneViewRotation: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} + m_LastSceneViewOrtho: 0 + m_ReplacementShader: {fileID: 0} + m_ReplacementString: + m_SceneVisActive: 1 + m_LastLockedObject: {fileID: 0} + m_ViewIsLockedToObject: 0 +--- !u!114 &6 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: ProfilerWindow + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 866 + width: 1493 + height: 433 + m_MinSize: {x: 901, y: 237} + m_MaxSize: {x: 4001, y: 4021} + m_ActualView: {fileID: 7} + m_Panes: + - {fileID: 7} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &7 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 12070, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 900, y: 216} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Profiler + m_Image: {fileID: -1089619856830078684, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 0 + y: 939 + width: 1492 + height: 412 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_Recording: 1 + m_ActiveNativePlatformSupportModuleName: + m_AllModules: + - rid: 7267259948770525184 + - rid: 7267259948770525185 + - rid: 7267259948770525186 + - rid: 7267259948770525187 + - rid: 7267259948770525188 + - rid: 7267259948770525189 + - rid: 7267259948770525190 + - rid: 7267259948770525191 + - rid: 7267259948770525192 + - rid: 7267259948770525193 + - rid: 7267259948770525194 + - rid: 7267259948770525195 + - rid: 7267259948770525196 + - rid: 7267259948770525197 + m_CallstackRecordMode: 1 + m_ClearOnPlay: 0 + references: + version: 2 + RefIds: + - rid: 7267259948770525184 + type: {class: CPUProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.CPUProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_ViewType: 0 + updateViewLive: 0 + m_CurrentFrameIndex: -1 + m_HierarchyOverruledThreadFromSelection: 0 + m_ProfilerViewFilteringOptions: 1 + m_FrameDataHierarchyView: + m_Serialized: 1 + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_MultiColumnHeaderState: + m_Columns: + - width: 200 + sortedAscending: 1 + headerContent: + m_Text: Overview + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 200 + maxWidth: 1000000 + autoResize: 1 + allowToggleVisibility: 0 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Total + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Self + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Calls + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: GC Alloc + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Time ms + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Self ms + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 25 + sortedAscending: 0 + headerContent: + m_Text: + m_Image: {fileID: -5161429177145976760, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: Warnings + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 25 + maxWidth: 25 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + m_VisibleColumns: 0000000001000000020000000300000004000000050000000600000007000000 + m_SortedColumns: 05000000 + m_ThreadIndexInThreadNames: 0 + m_DetailedViewType: 0 + m_DetailedViewSpliterState: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: -1 + realSizes: + - 0 + - 0 + relativeSizes: + - 0.7 + - 0.3 + minSizes: + - 450 + - 50 + maxSizes: + - 0 + - 0 + lastTotalSize: 0 + splitSize: 6 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_DetailedObjectsView: + m_SelectedID: -1 + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_MultiColumnHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_VertSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_DetailedCallsView: + m_SelectedID: -1 + m_VertSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_CalleesTreeView: + m_ViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_ViewHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_CallersTreeView: + m_ViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_ViewHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_FullThreadName: Main Thread + m_ThreadName: Main Thread + k__BackingField: 0 + k__BackingField: -1 + m_GroupName: + - rid: 7267259948770525185 + type: {class: GPUProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.GPUProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_ViewType: 0 + updateViewLive: 0 + m_CurrentFrameIndex: -1 + m_HierarchyOverruledThreadFromSelection: 0 + m_ProfilerViewFilteringOptions: 1 + m_FrameDataHierarchyView: + m_Serialized: 0 + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_MultiColumnHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_ThreadIndexInThreadNames: 0 + m_DetailedViewType: 0 + m_DetailedViewSpliterState: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_DetailedObjectsView: + m_SelectedID: 0 + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_MultiColumnHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_VertSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_DetailedCallsView: + m_SelectedID: 0 + m_VertSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_CalleesTreeView: + m_ViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_ViewHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_CallersTreeView: + m_ViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_ViewHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_FullThreadName: Main Thread + m_ThreadName: Main Thread + k__BackingField: 0 + k__BackingField: -1 + m_GroupName: + - rid: 7267259948770525186 + type: {class: RenderingProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.RenderingProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525187 + type: {class: MemoryProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.MemoryProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_ViewSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: -1 + realSizes: + - 0 + - 0 + relativeSizes: + - 0.7 + - 0.3 + minSizes: + - 450 + - 50 + maxSizes: + - 0 + - 0 + lastTotalSize: 0 + splitSize: 6 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + - rid: 7267259948770525188 + type: {class: AudioProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.AudioProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_ShowInactiveDSPChains: 0 + m_HighlightAudibleDSPChains: 1 + m_DSPGraphZoomFactor: 1 + m_DSPGraphHorizontalLayout: 0 + - rid: 7267259948770525189 + type: {class: VideoProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.VideoProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525190 + type: {class: PhysicsProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.PhysicsProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525191 + type: {class: Physics2DProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.Physics2DProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525192 + type: {class: UIProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.UIProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525193 + type: {class: UIDetailsProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.UIDetailsProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525194 + type: {class: GlobalIlluminationProfilerModule, ns: UnityEditorInternal.Profiling, + asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.GlobalIlluminationProfilerModule, + UnityEditor.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525195 + type: {class: VirtualTexturingProfilerModule, ns: UnityEditorInternal.Profiling, + asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.VirtualTexturingProfilerModule, + UnityEditor.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_VTProfilerView: + rid: 7267259948770525198 + - rid: 7267259948770525196 + type: {class: FileIOProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.FileIOProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525197 + type: {class: AssetLoadingProfilerModule, ns: UnityEditorInternal.Profiling, + asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.AssetLoadingProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525198 + type: {class: VirtualTexturingProfilerView, ns: UnityEditor, asm: UnityEditor.CoreModule} + data: + m_SortAscending: 0 + m_SortedColumn: -1 +--- !u!114 &8 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 9} + - {fileID: 11} + m_Position: + serializedVersion: 2 + x: 1493 + y: 0 + width: 376 + height: 1299 + m_MinSize: {x: 100, y: 100} + m_MaxSize: {x: 8096, y: 16192} + vertical: 1 + controlID: 338 +--- !u!114 &9 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: SceneHierarchyWindow + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 376 + height: 624 + m_MinSize: {x: 202, y: 221} + m_MaxSize: {x: 4002, y: 4021} + m_ActualView: {fileID: 10} + m_Panes: + - {fileID: 10} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &10 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12061, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Hierarchy + m_Image: {fileID: 7966133145522015247, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 1493 + y: 73 + width: 374 + height: 603 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_SceneHierarchy: + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: 1638ffff1a3cffff + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 9} + m_SearchString: + m_ExpandedScenes: [] + m_CurrenRootInstanceID: 0 + m_LockTracker: + m_IsLocked: 0 + m_CurrentSortingName: TransformSorting + m_WindowGUID: 5744dfe58afd7b349b2bf692722dc4ff +--- !u!114 &11 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: ProjectBrowser + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 624 + width: 376 + height: 675 + m_MinSize: {x: 232, y: 271} + m_MaxSize: {x: 10002, y: 10021} + m_ActualView: {fileID: 12} + m_Panes: + - {fileID: 12} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &12 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12014, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 230, y: 250} + m_MaxSize: {x: 10000, y: 10000} + m_TitleContent: + m_Text: Project + m_Image: {fileID: -5467254957812901981, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 1493 + y: 697 + width: 374 + height: 654 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_SearchFilter: + m_NameFilter: + m_ClassNames: [] + m_AssetLabels: [] + m_AssetBundleNames: [] + m_ReferencingInstanceIDs: + m_SceneHandles: + m_ShowAllHits: 0 + m_SkipHidden: 0 + m_SearchArea: 1 + m_Folders: + - Assets + m_Globs: [] + m_OriginalText: + m_ImportLogFlags: 0 + m_FilterByTypeIntersection: 0 + m_ViewMode: 0 + m_StartGridSize: 16 + m_LastFolders: [] + m_LastFoldersGridSize: -1 + m_LastProjectPath: E:\Projects\LiteNetLib\LiteNetLibSampleUnity + m_LockTracker: + m_IsLocked: 0 + m_FolderTreeState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: 0e800000 + m_LastClickedID: 32782 + m_ExpandedIDs: ffffffffd0400000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 1 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_CreateAssetUtility: + m_EndAction: {fileID: 0} + m_InstanceID: 0 + m_Path: + m_Icon: {fileID: 0} + m_ResourceFile: + m_AssetTreeState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: 887a0000 + m_LastClickedID: 31368 + m_ExpandedIDs: ffffffffd0400000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 1 + m_ClientGUIView: {fileID: 11} + m_SearchString: + m_CreateAssetUtility: + m_EndAction: {fileID: 0} + m_InstanceID: 0 + m_Path: + m_Icon: {fileID: 0} + m_ResourceFile: + m_ListAreaState: + m_SelectedInstanceIDs: 887a0000 + m_LastClickedInstanceID: 31368 + m_HadKeyboardFocusLastEvent: 0 + m_ExpandedInstanceIDs: c6230000d0640000967900008e6d00007674000022740000a66f000000000000687100009c230000f4b70000288000007a5f00008e7d0000ecbc0000346d0000f09c0000c297000026850000f06c0000de7d0000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 1 + m_ClientGUIView: {fileID: 11} + m_CreateAssetUtility: + m_EndAction: {fileID: 0} + m_InstanceID: 0 + m_Path: + m_Icon: {fileID: 0} + m_ResourceFile: + m_NewAssetIndexInList: -1 + m_ScrollPosition: {x: 0, y: 0} + m_GridSize: 16 + m_SkipHiddenPackages: 0 + m_DirectoriesAreaWidth: 178 +--- !u!114 &13 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: InspectorWindow + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 1869 + y: 0 + width: 691 + height: 1299 + m_MinSize: {x: 275, y: 50} + m_MaxSize: {x: 4000, y: 4000} + m_ActualView: {fileID: 14} + m_Panes: + - {fileID: 14} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &14 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12019, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 275, y: 50} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Inspector + m_Image: {fileID: -2667387946076563598, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 1869 + y: 73 + width: 690 + height: 1278 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_ObjectsLockedBeforeSerialization: [] + m_InstanceIDsLockedBeforeSerialization: + m_PreviewResizer: + m_CachedPref: 526 + m_ControlHash: -371814159 + m_PrefName: Preview_InspectorPreview + m_LastInspectedObjectInstanceID: 31368 + m_LastVerticalScrollValue: 0 + m_GlobalObjectId: + m_InspectorMode: 0 + m_LockTracker: + m_IsLocked: 0 + m_PreviewWindow: {fileID: 0} diff --git a/LiteNetLibSampleUnity/UserSettings/Layouts/default-2022.dwlt b/LiteNetLibSampleUnity/UserSettings/Layouts/default-2022.dwlt new file mode 100644 index 00000000..bea9ed68 --- /dev/null +++ b/LiteNetLibSampleUnity/UserSettings/Layouts/default-2022.dwlt @@ -0,0 +1,2095 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12004, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_PixelRect: + serializedVersion: 2 + x: 0 + y: 43 + width: 2560 + height: 1349 + m_ShowMode: 4 + m_Title: Profiler + m_RootView: {fileID: 2} + m_MinSize: {x: 875, y: 421} + m_MaxSize: {x: 10000, y: 10000} + m_Maximized: 1 +--- !u!114 &2 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12008, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 3} + - {fileID: 5} + - {fileID: 4} + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 2560 + height: 1349 + m_MinSize: {x: 875, y: 300} + m_MaxSize: {x: 10000, y: 10000} + m_UseTopView: 1 + m_TopViewHeight: 30 + m_UseBottomView: 1 + m_BottomViewHeight: 20 +--- !u!114 &3 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12011, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 2560 + height: 30 + m_MinSize: {x: 0, y: 0} + m_MaxSize: {x: 0, y: 0} + m_LastLoadedLayoutName: +--- !u!114 &4 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12042, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 1329 + width: 2560 + height: 20 + m_MinSize: {x: 0, y: 0} + m_MaxSize: {x: 0, y: 0} +--- !u!114 &5 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 6} + - {fileID: 9} + - {fileID: 12} + m_Position: + serializedVersion: 2 + x: 0 + y: 30 + width: 2560 + height: 1299 + m_MinSize: {x: 300, y: 100} + m_MaxSize: {x: 24288, y: 16192} + vertical: 0 + controlID: 684 +--- !u!114 &6 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 7} + - {fileID: 8} + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 1493 + height: 1299 + m_MinSize: {x: 100, y: 100} + m_MaxSize: {x: 8096, y: 16192} + vertical: 1 + controlID: 690 +--- !u!114 &7 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: GameView + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 1493 + height: 733 + m_MinSize: {x: 201, y: 221} + m_MaxSize: {x: 4001, y: 4021} + m_ActualView: {fileID: 13} + m_Panes: + - {fileID: 14} + - {fileID: 13} + m_Selected: 1 + m_LastSelected: 0 +--- !u!114 &8 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: ProfilerWindow + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 733 + width: 1493 + height: 566 + m_MinSize: {x: 901, y: 237} + m_MaxSize: {x: 4001, y: 4021} + m_ActualView: {fileID: 15} + m_Panes: + - {fileID: 15} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &9 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_Children: + - {fileID: 10} + - {fileID: 11} + m_Position: + serializedVersion: 2 + x: 1493 + y: 0 + width: 376 + height: 1299 + m_MinSize: {x: 100, y: 100} + m_MaxSize: {x: 8096, y: 16192} + vertical: 1 + controlID: 635 +--- !u!114 &10 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: SceneHierarchyWindow + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 0 + width: 376 + height: 624 + m_MinSize: {x: 202, y: 221} + m_MaxSize: {x: 4002, y: 4021} + m_ActualView: {fileID: 16} + m_Panes: + - {fileID: 16} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &11 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: ProjectBrowser + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 0 + y: 624 + width: 376 + height: 675 + m_MinSize: {x: 232, y: 271} + m_MaxSize: {x: 10002, y: 10021} + m_ActualView: {fileID: 17} + m_Panes: + - {fileID: 17} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &12 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} + m_Name: InspectorWindow + m_EditorClassIdentifier: + m_Children: [] + m_Position: + serializedVersion: 2 + x: 1869 + y: 0 + width: 691 + height: 1299 + m_MinSize: {x: 276, y: 71} + m_MaxSize: {x: 4001, y: 4021} + m_ActualView: {fileID: 18} + m_Panes: + - {fileID: 18} + m_Selected: 0 + m_LastSelected: 0 +--- !u!114 &13 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12015, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Game + m_Image: {fileID: -6423792434712278376, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 0 + y: 73 + width: 1492 + height: 712 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_SerializedViewNames: [] + m_SerializedViewValues: [] + m_PlayModeViewName: GameView + m_ShowGizmos: 0 + m_TargetDisplay: 0 + m_ClearColor: {r: 0, g: 0, b: 0, a: 0} + m_TargetSize: {x: 1228, y: 691} + m_TextureFilterMode: 0 + m_TextureHideFlags: 61 + m_RenderIMGUI: 1 + m_EnterPlayModeBehavior: 0 + m_UseMipMap: 0 + m_VSyncEnabled: 1 + m_Gizmos: 0 + m_Stats: 0 + m_SelectedSizes: 0100000000000000000000000e000000000000000000000000000000000000000000000000000000 + m_ZoomArea: + m_HRangeLocked: 0 + m_VRangeLocked: 0 + hZoomLockedByDefault: 0 + vZoomLockedByDefault: 0 + m_HBaseRangeMin: -614 + m_HBaseRangeMax: 614 + m_VBaseRangeMin: -345.5 + m_VBaseRangeMax: 345.5 + m_HAllowExceedBaseRangeMin: 1 + m_HAllowExceedBaseRangeMax: 1 + m_VAllowExceedBaseRangeMin: 1 + m_VAllowExceedBaseRangeMax: 1 + m_ScaleWithWindow: 0 + m_HSlider: 0 + m_VSlider: 0 + m_IgnoreScrollWheelUntilClicked: 0 + m_EnableMouseInput: 1 + m_EnableSliderZoomHorizontal: 0 + m_EnableSliderZoomVertical: 0 + m_UniformScale: 1 + m_UpDirection: 1 + m_DrawArea: + serializedVersion: 2 + x: 0 + y: 21 + width: 1492 + height: 691 + m_Scale: {x: 1, y: 1} + m_Translation: {x: 746, y: 345.5} + m_MarginLeft: 0 + m_MarginRight: 0 + m_MarginTop: 0 + m_MarginBottom: 0 + m_LastShownAreaInsideMargins: + serializedVersion: 2 + x: -746 + y: -345.5 + width: 1492 + height: 691 + m_MinimalGUI: 1 + m_defaultScale: 1 + m_LastWindowPixelSize: {x: 1492, y: 712} + m_ClearInEditMode: 1 + m_NoCameraWarning: 1 + m_LowResolutionForAspectRatios: 01000001000000000000 + m_XRRenderMode: 0 + m_RenderTexture: {fileID: 0} +--- !u!114 &14 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12013, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Scene + m_Image: {fileID: 2593428753322112591, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 0 + y: 73 + width: 1492 + height: 845 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: + - dockPosition: 0 + containerId: overlay-toolbar__top + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: -166, y: -26} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 3 + id: Tool Settings + index: 0 + layout: 1 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 0 + containerId: overlay-toolbar__top + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: -141, y: 149} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 1 + id: unity-grid-and-snap-toolbar + index: 1 + layout: 1 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-toolbar__top + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 0, y: 25} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: unity-scene-view-toolbar + index: 0 + layout: 1 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-toolbar__top + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 1 + id: unity-search-toolbar + index: 1 + layout: 1 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 0 + containerId: overlay-container--left + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 0, y: 25} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: unity-transform-toolbar + index: 0 + layout: 2 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 0 + containerId: overlay-container--left + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 0, y: 197} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: unity-component-tools + index: 1 + layout: 2 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 0 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 67.5, y: -123} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 2 + id: Orientation + index: 0 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Light Settings + index: 2 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Camera + index: 1 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Cloth Constraints + index: 3 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Cloth Collisions + index: 4 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Navmesh Display + index: 4 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Agent Display + index: 5 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Obstacle Display + index: 6 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Occlusion Culling + index: 5 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Physics Debugger + index: 6 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Scene Visibility + index: 7 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 25} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Particles + index: 8 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Tilemap + index: 11 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 0, y: 0} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Tilemap Palette Helper + index: 12 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: -216, y: -156} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 3 + id: AINavigationOverlay + index: 9 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 48, y: 48} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: APV Overlay + index: 8 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 48, y: 48} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/TrailRenderer + index: 10 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 1 + snapOffset: {x: 48, y: 10} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: UnityEditor.SceneViewCameraOverlay + index: 10 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 48, y: 48} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Open Tile Palette + index: 0 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + - dockPosition: 1 + containerId: overlay-container--right + floating: 0 + collapsed: 0 + displayed: 0 + snapOffset: {x: 48, y: 48} + snapOffsetDelta: {x: 0, y: 0} + snapCorner: 0 + id: Scene View/Tilemap Focus + index: 1 + layout: 4 + size: {x: 0, y: 0} + sizeOverriden: 0 + m_OverlaysVisible: 1 + m_WindowGUID: c9ee0d44a80756c48ba1fd3e952ef1b5 + m_Gizmos: 1 + m_OverrideSceneCullingMask: 6917529027641081856 + m_SceneIsLit: 1 + m_SceneLighting: 1 + m_2DMode: 0 + m_isRotationLocked: 0 + m_PlayAudio: 0 + m_AudioPlay: 0 + m_Position: + m_Target: {x: 0, y: 0, z: 0} + speed: 2 + m_Value: {x: 0, y: 0, z: 0} + m_RenderMode: 0 + m_CameraMode: + drawMode: 0 + name: Shaded + section: Shading Mode + m_ValidateTrueMetals: 0 + m_DoValidateTrueMetals: 0 + m_SceneViewState: + m_AlwaysRefresh: 0 + showFog: 1 + showSkybox: 1 + showFlares: 1 + showImageEffects: 1 + showParticleSystems: 1 + showVisualEffectGraphs: 1 + m_FxEnabled: 1 + m_Grid: + xGrid: + m_Fade: + m_Target: 0 + speed: 2 + m_Value: 0 + m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 0.4} + m_Pivot: {x: 0, y: 0, z: 0} + m_Size: {x: 0, y: 0} + yGrid: + m_Fade: + m_Target: 1 + speed: 2 + m_Value: 1 + m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 0.4} + m_Pivot: {x: 0, y: 0, z: 0} + m_Size: {x: 1, y: 1} + zGrid: + m_Fade: + m_Target: 0 + speed: 2 + m_Value: 0 + m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 0.4} + m_Pivot: {x: 0, y: 0, z: 0} + m_Size: {x: 1, y: 1} + m_ShowGrid: 1 + m_GridAxis: 1 + m_gridOpacity: 0.5 + m_Rotation: + m_Target: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} + speed: 2 + m_Value: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} + m_Size: + m_Target: 10 + speed: 2 + m_Value: 10 + m_Ortho: + m_Target: 0 + speed: 2 + m_Value: 0 + m_CameraSettings: + m_Speed: 0.8856 + m_SpeedNormalized: 0.44 + m_SpeedMin: 0.01 + m_SpeedMax: 2 + m_EasingEnabled: 1 + m_EasingDuration: 0.4 + m_AccelerationEnabled: 1 + m_FieldOfViewHorizontalOrVertical: 60 + m_NearClip: 0.03 + m_FarClip: 10000 + m_DynamicClip: 1 + m_OcclusionCulling: 0 + m_LastSceneViewRotation: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} + m_LastSceneViewOrtho: 0 + m_ReplacementShader: {fileID: 0} + m_ReplacementString: + m_SceneVisActive: 1 + m_LastLockedObject: {fileID: 0} + m_ViewIsLockedToObject: 0 +--- !u!114 &15 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 12070, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 900, y: 216} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Profiler + m_Image: {fileID: -1089619856830078684, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 0 + y: 806 + width: 1492 + height: 545 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_Recording: 1 + m_ActiveNativePlatformSupportModuleName: + m_AllModules: + - rid: 7267259948770525184 + - rid: 7267259948770525185 + - rid: 7267259948770525186 + - rid: 7267259948770525187 + - rid: 7267259948770525188 + - rid: 7267259948770525189 + - rid: 7267259948770525190 + - rid: 7267259948770525191 + - rid: 7267259948770525192 + - rid: 7267259948770525193 + - rid: 7267259948770525194 + - rid: 7267259948770525195 + - rid: 7267259948770525196 + - rid: 7267259948770525197 + m_CallstackRecordMode: 1 + m_ClearOnPlay: 0 + references: + version: 2 + RefIds: + - rid: 7267259948770525184 + type: {class: CPUProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.CPUProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_ViewType: 0 + updateViewLive: 0 + m_CurrentFrameIndex: 1103 + m_HierarchyOverruledThreadFromSelection: 0 + m_ProfilerViewFilteringOptions: 1 + m_FrameDataHierarchyView: + m_Serialized: 1 + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: 8b000000 + m_LastClickedID: 139 + m_ExpandedIDs: 040000003a0000008b000000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_MultiColumnHeaderState: + m_Columns: + - width: 200 + sortedAscending: 1 + headerContent: + m_Text: Overview + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 200 + maxWidth: 1000000 + autoResize: 1 + allowToggleVisibility: 0 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Total + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Self + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Calls + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: GC Alloc + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Time ms + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 80 + sortedAscending: 0 + headerContent: + m_Text: Self ms + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 50 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 25 + sortedAscending: 0 + headerContent: + m_Text: + m_Image: {fileID: -5161429177145976760, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: Warnings + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 25 + maxWidth: 25 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + m_VisibleColumns: 0000000001000000020000000300000004000000050000000600000007000000 + m_SortedColumns: 05000000 + m_ThreadIndexInThreadNames: 0 + m_DetailedViewType: 0 + m_DetailedViewSpliterState: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: -1 + realSizes: + - 0 + - 0 + relativeSizes: + - 0.7 + - 0.3 + minSizes: + - 450 + - 50 + maxSizes: + - 0 + - 0 + lastTotalSize: 0 + splitSize: 6 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_DetailedObjectsView: + m_SelectedID: -1 + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_MultiColumnHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_VertSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_DetailedCallsView: + m_SelectedID: -1 + m_VertSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_CalleesTreeView: + m_ViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_ViewHeaderState: + m_Columns: + - width: 150 + sortedAscending: 1 + headerContent: + m_Text: Called From + m_Image: {fileID: 0} + m_Tooltip: 'Parents the selected function is called from + + + (Press + ''F'' for frame selection)' + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 150 + maxWidth: 1000000 + autoResize: 1 + allowToggleVisibility: 0 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Calls + m_Image: {fileID: 0} + m_Tooltip: Total number of calls in a selected frame + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: GC Alloc + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Time ms + m_Image: {fileID: 0} + m_Tooltip: Total time the selected function spends within a parent + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Time % + m_Image: {fileID: 0} + m_Tooltip: Shows how often the selected function was called from + the parent call + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + m_VisibleColumns: 0000000001000000020000000300000004000000 + m_SortedColumns: 03000000 + m_CallersTreeView: + m_ViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_ViewHeaderState: + m_Columns: + - width: 150 + sortedAscending: 1 + headerContent: + m_Text: Called From + m_Image: {fileID: 0} + m_Tooltip: 'Parents the selected function is called from + + + (Press + ''F'' for frame selection)' + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 150 + maxWidth: 1000000 + autoResize: 1 + allowToggleVisibility: 0 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Calls + m_Image: {fileID: 0} + m_Tooltip: Total number of calls in a selected frame + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: GC Alloc + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Time ms + m_Image: {fileID: 0} + m_Tooltip: Total time the selected function spends within a parent + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Time % + m_Image: {fileID: 0} + m_Tooltip: Shows how often the selected function was called from + the parent call + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + m_VisibleColumns: 0000000001000000020000000300000004000000 + m_SortedColumns: 03000000 + m_FullThreadName: Main Thread + m_ThreadName: Main Thread + k__BackingField: 1059876 + k__BackingField: 0 + m_GroupName: + - rid: 7267259948770525185 + type: {class: GPUProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.GPUProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_ViewType: 0 + updateViewLive: 0 + m_CurrentFrameIndex: -1 + m_HierarchyOverruledThreadFromSelection: 0 + m_ProfilerViewFilteringOptions: 1 + m_FrameDataHierarchyView: + m_Serialized: 0 + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_MultiColumnHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_ThreadIndexInThreadNames: 0 + m_DetailedViewType: 0 + m_DetailedViewSpliterState: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_DetailedObjectsView: + m_SelectedID: 0 + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_MultiColumnHeaderState: + m_Columns: [] + m_VisibleColumns: + m_SortedColumns: + m_VertSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_DetailedCallsView: + m_SelectedID: -1 + m_VertSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: 0 + realSizes: [] + relativeSizes: [] + minSizes: [] + maxSizes: [] + lastTotalSize: 0 + splitSize: 0 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + m_CalleesTreeView: + m_ViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_ViewHeaderState: + m_Columns: + - width: 150 + sortedAscending: 1 + headerContent: + m_Text: Called From + m_Image: {fileID: 0} + m_Tooltip: 'Parents the selected function is called from + + + (Press + ''F'' for frame selection)' + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 150 + maxWidth: 1000000 + autoResize: 1 + allowToggleVisibility: 0 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Calls + m_Image: {fileID: 0} + m_Tooltip: Total number of calls in a selected frame + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: GC Alloc + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Time ms + m_Image: {fileID: 0} + m_Tooltip: Total time the selected function spends within a parent + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Time % + m_Image: {fileID: 0} + m_Tooltip: Shows how often the selected function was called from + the parent call + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + m_VisibleColumns: 0000000001000000020000000300000004000000 + m_SortedColumns: 03000000 + m_CallersTreeView: + m_ViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_ViewHeaderState: + m_Columns: + - width: 150 + sortedAscending: 1 + headerContent: + m_Text: Called From + m_Image: {fileID: 0} + m_Tooltip: 'Parents the selected function is called from + + + (Press + ''F'' for frame selection)' + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 150 + maxWidth: 1000000 + autoResize: 1 + allowToggleVisibility: 0 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Calls + m_Image: {fileID: 0} + m_Tooltip: Total number of calls in a selected frame + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: GC Alloc + m_Image: {fileID: 0} + m_Tooltip: + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Time ms + m_Image: {fileID: 0} + m_Tooltip: Total time the selected function spends within a parent + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + - width: 60 + sortedAscending: 0 + headerContent: + m_Text: Time % + m_Image: {fileID: 0} + m_Tooltip: Shows how often the selected function was called from + the parent call + contextMenuText: + headerTextAlignment: 0 + sortingArrowAlignment: 2 + minWidth: 60 + maxWidth: 1000000 + autoResize: 0 + allowToggleVisibility: 1 + canSort: 1 + userData: 0 + m_VisibleColumns: 0000000001000000020000000300000004000000 + m_SortedColumns: 03000000 + m_FullThreadName: Main Thread + m_ThreadName: Main Thread + k__BackingField: 0 + k__BackingField: -1 + m_GroupName: + - rid: 7267259948770525186 + type: {class: RenderingProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.RenderingProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525187 + type: {class: MemoryProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.MemoryProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_ViewSplit: + ID: 0 + splitterInitialOffset: 0 + currentActiveSplitter: -1 + realSizes: + - 0 + - 0 + relativeSizes: + - 0.7 + - 0.3 + minSizes: + - 450 + - 50 + maxSizes: + - 0 + - 0 + lastTotalSize: 0 + splitSize: 6 + xOffset: 0 + m_Version: 1 + oldRealSizes: + oldMinSizes: + oldMaxSizes: + oldSplitSize: 0 + - rid: 7267259948770525188 + type: {class: AudioProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.AudioProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_ShowInactiveDSPChains: 0 + m_HighlightAudibleDSPChains: 1 + m_DSPGraphZoomFactor: 1 + m_DSPGraphHorizontalLayout: 0 + - rid: 7267259948770525189 + type: {class: VideoProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.VideoProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525190 + type: {class: PhysicsProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.PhysicsProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525191 + type: {class: Physics2DProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.Physics2DProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525192 + type: {class: UIProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.UIProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525193 + type: {class: UIDetailsProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.UIDetailsProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525194 + type: {class: GlobalIlluminationProfilerModule, ns: UnityEditorInternal.Profiling, + asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.GlobalIlluminationProfilerModule, + UnityEditor.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525195 + type: {class: VirtualTexturingProfilerModule, ns: UnityEditorInternal.Profiling, + asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.VirtualTexturingProfilerModule, + UnityEditor.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + m_VTProfilerView: + rid: 7267259948770525198 + - rid: 7267259948770525196 + type: {class: FileIOProfilerModule, ns: UnityEditorInternal.Profiling, asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.FileIOProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525197 + type: {class: AssetLoadingProfilerModule, ns: UnityEditorInternal.Profiling, + asm: UnityEditor.CoreModule} + data: + m_Identifier: UnityEditorInternal.Profiling.AssetLoadingProfilerModule, UnityEditor.CoreModule, + Version=0.0.0.0, Culture=neutral, PublicKeyToken=null + m_PaneScroll: {x: 0, y: 0} + - rid: 7267259948770525198 + type: {class: VirtualTexturingProfilerView, ns: UnityEditor, asm: UnityEditor.CoreModule} + data: + m_SortAscending: 0 + m_SortedColumn: -1 +--- !u!114 &16 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12061, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Hierarchy + m_Image: {fileID: 7966133145522015247, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 1493 + y: 73 + width: 374 + height: 603 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_SceneHierarchy: + m_TreeViewState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: 887a0000 + m_LastClickedID: 0 + m_ExpandedIDs: 1638ffff1a3cffff + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 0 + m_ClientGUIView: {fileID: 10} + m_SearchString: + m_ExpandedScenes: [] + m_CurrenRootInstanceID: 0 + m_LockTracker: + m_IsLocked: 0 + m_CurrentSortingName: TransformSorting + m_WindowGUID: 5744dfe58afd7b349b2bf692722dc4ff +--- !u!114 &17 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12014, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 230, y: 250} + m_MaxSize: {x: 10000, y: 10000} + m_TitleContent: + m_Text: Project + m_Image: {fileID: -5467254957812901981, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 1493 + y: 697 + width: 374 + height: 654 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_SearchFilter: + m_NameFilter: + m_ClassNames: [] + m_AssetLabels: [] + m_AssetBundleNames: [] + m_ReferencingInstanceIDs: + m_SceneHandles: + m_ShowAllHits: 0 + m_SkipHidden: 0 + m_SearchArea: 1 + m_Folders: + - Assets + m_Globs: [] + m_OriginalText: + m_ImportLogFlags: 0 + m_FilterByTypeIntersection: 0 + m_ViewMode: 0 + m_StartGridSize: 16 + m_LastFolders: [] + m_LastFoldersGridSize: -1 + m_LastProjectPath: E:\Projects\LiteNetLib\LiteNetLibSampleUnity + m_LockTracker: + m_IsLocked: 0 + m_FolderTreeState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: 0e800000 + m_LastClickedID: 32782 + m_ExpandedIDs: ffffffffd0400000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 1 + m_ClientGUIView: {fileID: 0} + m_SearchString: + m_CreateAssetUtility: + m_EndAction: {fileID: 0} + m_InstanceID: 0 + m_Path: + m_Icon: {fileID: 0} + m_ResourceFile: + m_AssetTreeState: + scrollPos: {x: 0, y: 0} + m_SelectedIDs: + m_LastClickedID: 0 + m_ExpandedIDs: ffffffffd0400000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 1 + m_ClientGUIView: {fileID: 11} + m_SearchString: + m_CreateAssetUtility: + m_EndAction: {fileID: 0} + m_InstanceID: 0 + m_Path: + m_Icon: {fileID: 0} + m_ResourceFile: + m_ListAreaState: + m_SelectedInstanceIDs: + m_LastClickedInstanceID: 0 + m_HadKeyboardFocusLastEvent: 0 + m_ExpandedInstanceIDs: c6230000d0640000967900008e6d00007674000022740000a66f000000000000687100009c230000f4b70000288000007a5f00008e7d0000ecbc0000346d0000f09c0000c297000026850000f06c0000de7d0000 + m_RenameOverlay: + m_UserAcceptedRename: 0 + m_Name: + m_OriginalName: + m_EditFieldRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 0 + height: 0 + m_UserData: 0 + m_IsWaitingForDelay: 0 + m_IsRenaming: 0 + m_OriginalEventType: 11 + m_IsRenamingFilename: 1 + m_ClientGUIView: {fileID: 11} + m_CreateAssetUtility: + m_EndAction: {fileID: 0} + m_InstanceID: 0 + m_Path: + m_Icon: {fileID: 0} + m_ResourceFile: + m_NewAssetIndexInList: -1 + m_ScrollPosition: {x: 0, y: 0} + m_GridSize: 16 + m_SkipHiddenPackages: 0 + m_DirectoriesAreaWidth: 178 +--- !u!114 &18 +MonoBehaviour: + m_ObjectHideFlags: 52 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 1 + m_Script: {fileID: 12019, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_MinSize: {x: 275, y: 50} + m_MaxSize: {x: 4000, y: 4000} + m_TitleContent: + m_Text: Inspector + m_Image: {fileID: -2667387946076563598, guid: 0000000000000000d000000000000000, + type: 0} + m_Tooltip: + m_Pos: + serializedVersion: 2 + x: 1869 + y: 73 + width: 690 + height: 1278 + m_SerializedDataModeController: + m_DataMode: 0 + m_PreferredDataMode: 0 + m_SupportedDataModes: + isAutomatic: 1 + m_ViewDataDictionary: {fileID: 0} + m_OverlayCanvas: + m_LastAppliedPresetName: Default + m_SaveData: [] + m_OverlaysVisible: 1 + m_ObjectsLockedBeforeSerialization: [] + m_InstanceIDsLockedBeforeSerialization: + m_PreviewResizer: + m_CachedPref: 526 + m_ControlHash: -371814159 + m_PrefName: Preview_InspectorPreview + m_LastInspectedObjectInstanceID: -1 + m_LastVerticalScrollValue: 0 + m_GlobalObjectId: + m_InspectorMode: 0 + m_LockTracker: + m_IsLocked: 0 + m_PreviewWindow: {fileID: 0} diff --git a/LiteNetLibSampleUnity/UserSettings/Search.settings b/LiteNetLibSampleUnity/UserSettings/Search.settings new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/LiteNetLibSampleUnity/UserSettings/Search.settings @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/README.md b/README.md index 4bed5ce0..85caa020 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,35 @@ -# LiteNetLib 0.9 indev +# LiteNetLib 2 -Lite reliable UDP library for .NET Framework 3.5, Mono, .NET Core 2.0, .NET Standard 2.0. +Lite reliable UDP library for .NET Standard 2.1 (Mono, .NET) -[STABLE BRANCH (and examples) for 0.8.x](https://github.com/RevenantX/LiteNetLib/tree/0.8) +[![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://stand-with-ukraine.pp.ua) -[![Discord](https://img.shields.io/discord/501682175930925058.svg)](https://discord.gg/FATFPdy) +**HighLevel API Part**: [LiteEntitySystem](https://github.com/RevenantX/LiteEntitySystem) + +**Matrix chat**: [Matrix](https://matrix.to/#/#litenetlib:matrix.org) + +**Discord chat**: [![Discord](https://img.shields.io/discord/501682175930925058.svg)](https://discord.gg/FATFPdy) + +[OLD BRANCH (and examples) for 1.x](https://github.com/RevenantX/LiteNetLib/tree/1.x) [Little Game Example on Unity](https://github.com/RevenantX/NetGameExample) +[Documentation](https://revenantx.github.io/LiteNetLib/index.html) + ## Build -### [NuGet](https://www.nuget.org/packages/LiteNetLib/) [![NuGet](https://img.shields.io/nuget/v/LiteNetLib.svg)](https://www.nuget.org/packages/LiteNetLib/) [![NuGet](https://img.shields.io/nuget/dt/LiteNetLib.svg)](https://www.nuget.org/packages/LiteNetLib/) +### [NuGet](https://www.nuget.org/packages/LiteNetLib/) [![NuGet](https://img.shields.io/nuget/v/LiteNetLib?color=blue)](https://www.nuget.org/packages/LiteNetLib/) [![NuGet](https://img.shields.io/nuget/vpre/LiteNetLib)](https://www.nuget.org/packages/LiteNetLib/#versions-body-tab) [![NuGet](https://img.shields.io/nuget/dt/LiteNetLib)](https://www.nuget.org/packages/LiteNetLib/) ### [Release builds](https://github.com/RevenantX/LiteNetLib/releases) [![GitHub (pre-)release](https://img.shields.io/github/release/RevenantX/LiteNetLib/all.svg)](https://github.com/RevenantX/LiteNetLib/releases) ### [DLL build from master](https://ci.appveyor.com/project/RevenantX/litenetlib/branch/master/artifacts) [![](https://ci.appveyor.com/api/projects/status/354501wnvxs8kuh3/branch/master?svg=true)](https://ci.appveyor.com/project/RevenantX/litenetlib/branch/master) ( Warning! Master branch can be unstable! ) -### Donations are welcome and will help further development of this project. -[![Bountysource](https://img.shields.io/badge/bountysource-donate-green.svg)](https://salt.bountysource.com/checkout/amount?team=litenetlib) - ## Features * Lightweight * Small CPU and RAM usage - * Small packet size overhead ( 1 byte for unreliable, 3 bytes for reliable packets ) + * Small packet size overhead ( 1 byte for unreliable, 4 bytes for reliable packets ) * Simple connection handling * Peer to peer connections * Helper classes for sending and reading messages @@ -32,39 +37,51 @@ Lite reliable UDP library for .NET Framework 3.5, Mono, .NET Core 2.0, .NET Stan * Different send mechanics * Reliable with order * Reliable without order - * Reliable sequenced (realiable only last packet) + * Reliable sequenced (reliable only last packet) * Ordered but unreliable with duplication prevention * Simple UDP packets without order and reliability -* Fast packet serializer [(Usage manual)](https://github.com/RevenantX/LiteNetLib/wiki/NetSerializer-usage) +* Fast packet serializer [(Usage manual)](https://revenantx.github.io/LiteNetLib/articles/netserializerusage.html) * Automatic small packets merging * Automatic fragmentation of reliable packets * Automatic MTU detection +* Optional CRC32C checksums * UDP NAT hole punching * NTP time requests * Packet loss and latency simulation -* IPv6 support (dual mode) -* Connection statisitcs (need DEBUG or STATS_ENABLED flag) +* IPv6 support (using separate socket for performance) +* Connection statistics * Multicasting (for discovering hosts in local network) * Unity support +* Support for .NET8 optimized socket calls (much less gc) * Supported platforms: - * Windows/Mac/Linux (.NET Framework, Mono, .NET Core) - * Android (Unity) - * iOS (Unity) - * UWP Windows 10 including phones + * Windows/Mac/Linux (.NET Framework, Mono, .NET Core, .NET Standard) * Lumin OS (Magic Leap) + * MonoGame + * Godot + * Unity 2021.2 (Desktop platforms, Android, iOS, Switch) + +## Support developer +* [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/revx) + +* USDT TRC20: `TE5eBgq8SyEeZFKtCgZG9GwL34sANmbc67` + +* USDT BEP20/ERC20: `0x4c0D6DC76c6A6B354f5ec6c9e51893fFC6510d1E` + +* Bitcoin: `bc1q269ecs8r5vnrum5qr5j98sdglhnxlulv0f6egd` ## Unity notes!!! -* Always use library sources instead of precompiled DLL files ( because there are platform specific #ifdefs and workarounds for unity bugs ) +* Minimal supported Unity is 2021.2. For older Unity versions use [1.x library](https://github.com/RevenantX/LiteNetLib/tree/1.x) versions +* Always use library sources or [OpenUPM package](https://openupm.com/packages/com.revenantx.litenetlib/) instead of precompiled DLL files ( because there are platform specific #ifdefs and workarounds for unity bugs ) ## Usage samples ### Client ```csharp -EventBasedNetListener listener = new EventBasedNetListener(); -NetManager client = new NetManager(listener); +var listener = new EventBasedNetListener(); +var client = new NetManager(listener); client.Start(); -client.Connect("localhost" /* host ip or name */, 9050 /* port */, "SomeConnectionKey" /* text key or NetDataWriter */); -listener.NetworkReceiveEvent += (fromPeer, dataReader, deliveryMethod) => +client.Connect("localhost" /* host IP or name */, 9050 /* port */, "SomeConnectionKey" /* text key or NetDataWriter */); +listener.NetworkReceiveEvent += (fromPeer, dataReader, channel, deliveryMethod) => { Console.WriteLine("We got: {0}", dataReader.GetString(100 /* max length of string */)); dataReader.Recycle(); @@ -80,13 +97,13 @@ client.Stop(); ``` ### Server ```csharp -EventBasedNetListener listener = new EventBasedNetListener(); -NetManager server = new NetManager(listener); +var listener = new EventBasedNetListener(); +var server = new NetManager(listener); server.Start(9050 /* port */); listener.ConnectionRequestEvent += request => { - if(server.PeersCount < 10 /* max connections */) + if(server.ConnectedPeersCount < 10 /* max connections */) request.AcceptIfKey("SomeConnectionKey"); else request.Reject(); @@ -94,10 +111,10 @@ listener.ConnectionRequestEvent += request => listener.PeerConnectedEvent += peer => { - Console.WriteLine("We got connection: {0}", peer.EndPoint); // Show peer ip - NetDataWriter writer = new NetDataWriter(); // Create writer class - writer.Put("Hello client!"); // Put some string - peer.Send(writer, DeliveryMethod.ReliableOrdered); // Send with reliability + Console.WriteLine("We got connection: {0}", peer); // Show peer IP + var writer = new NetDataWriter(); // Create writer class + writer.Put("Hello client!"); // Put some string + peer.Send(writer, DeliveryMethod.ReliableOrdered); // Send with reliability }; while (!Console.KeyAvailable) @@ -108,48 +125,6 @@ while (!Console.KeyAvailable) server.Stop(); ``` -## NetManager settings description - -* **UnconnectedMessagesEnabled** - * enable messages receiving without connection. (with SendUnconnectedMessage method) - * default value: **false** -* **NatPunchEnabled** - * enable NAT punch messages - * default value: **false** -* **UpdateTime** - * library logic update (and send) period in milliseconds - * default value: **15 msec**. -* **PingInterval** - * Interval for latency detection and checking connection - * default value: **1000 msec**. -* **DisconnectTimeout** - * if client or server doesn't receive any packet from remote peer during this time then connection will be closed - * (including library internal keepalive packets) - * default value: **5000 msec**. -* **SimulatePacketLoss** - * simulate packet loss by dropping random amout of packets. (Works only in DEBUG mode) - * default value: **false** -* **SimulateLatency** - * simulate latency by holding packets for random time. (Works only in DEBUG mode) - * default value: **false** -* **SimulationPacketLossChance** - * chance of packet loss when simulation enabled. value in percents. - * default value: **10 (%)** -* **SimulationMinLatency** - * minimum simulated latency - * default value: **30 msec** -* **SimulationMaxLatency** - * maximum simulated latency - * default value: **100 msec** -* **BroadcastEnabled** - * Allows receive Broadcast packets - * default value: **false** -* **ReconnectDelay** - * delay betwen connection attempts - * default value: **500 msec** -* **MaxConnectAttempts** - * maximum connection attempts before client stops and call disconnect event. - * default value: **10** -* **UnsyncedEvents** - * Experimental feature. Events automatically will be called without PollEvents method from another thread - * default value: **false** + + + diff --git a/appveyor.yml b/appveyor.yml index 93204a19..d7204858 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,27 +1,26 @@ -image: Visual Studio 2017 -version: 0.9.{build} +image: Visual Studio 2022 +version: 1.0.{build} configuration: - Release before_build: - nuget restore - dotnet restore assembly_info: - patch: true + patch: false file: AssemblyInfo.cs assembly_version: '{version}' assembly_file_version: '{version}' build: project: LiteNetLib.sln -test: - assemblies: - - '**\*.Tests.dll' +test_script: + - dotnet test -m:1 "LiteNetLib.Tests\LiteNetLib.Tests.csproj" --configuration Release --no-build artifacts: - - path: LiteNetLib/bin/Release/net35 + - path: LiteNetLib/bin/Release/net471 name: LiteNetLib-$(appveyor_build_version) type: Zip - path: LiteNetLib/bin/Release/netstandard2.0 name: LiteNetLibStandard-$(appveyor_build_version) type: Zip - - path: LiteNetLib/bin/Release/netcoreapp2.0 + - path: LiteNetLib/bin/Release/netcoreapp3.1 name: LiteNetLibNetCore-$(appveyor_build_version) type: Zip diff --git a/docfx_project/articles/intro.md b/docfx_project/articles/intro.md deleted file mode 100644 index c0478ced..00000000 --- a/docfx_project/articles/intro.md +++ /dev/null @@ -1 +0,0 @@ -# Add your introductions here! diff --git a/docfx_project/articles/netserializerusage.md b/docfx_project/articles/netserializerusage.md new file mode 100644 index 00000000..e18a079b --- /dev/null +++ b/docfx_project/articles/netserializerusage.md @@ -0,0 +1,176 @@ +# NetPacketProcessor +Fast specialized for network purposes serializer.
+It supports **classes** with **public properties with "get" and "set"** methods or **classes/structs which implements `INetSerializable`**.
+Serializer adds some overhead to packet size: 64 bit hash of class name and namespace (8 bytes). All other class fields will be as is in resulting packet. + +## Serialization speed comparsion +Serialization 100000 times of simple structure from [example](https://github.com/RevenantX/LiteNetLib/blob/master/LibSample/SerializerBenchmark.cs) (`NET 4.5`): +Serializer|Time|Size +---|---|---| +BinaryFormatter|3334 ms|1096 bytes +NetSerializer (first run)|45 ms|204 bytes +NetSerializer (second run)|37 ms|204 bytes +Raw|24 ms|204 bytes + +## Supported property types +```csharp +byte sbyte short ushort int uint long ulong float double bool string char IPEndPoint +``` +Arrays of all these types (and custom types) are also supported.
+Enums are supported but work a bit slower than other types. + +## Custom types +NetPacketProcessor doesn't support nested structs or classes, but you can register your own custom type processors.
+That useful for game engine types such as Vector3 and Quaternion (in Unity3d). + +```csharp +// Your packet that will be sent over network +class SamplePacket +{ + // Both property and array are supported + public MyType SomeMyType { get; set; } + public MyType[] SomeMyTypes { get; set; } +} + +// Some custom type variant 1: Basic struct +struct MyType +{ + public int Value1; + public string Value2; + + public static void Serialize(NetDataWriter writer, SomeMyType mytype) + { + writer.Put(mytype.Value1); + writer.Put(mytype.Value2); + } + + public static MyType Deserialize(NetDataReader reader) + { + MyType res = new MyType(); + res.Value1 = reader.GetInt(); + res.Value2 = reader.GetString(); + return res; + } +} +... +netPacketProcessor = new NetPacketProcessor(); +netPacketProcessor.RegisterNestedType( MyType.Serialize, MyType.Deserialize ); // Supply Serialization methods +``` + +You can also implement INetSerializable interface: +```csharp +// Some custom type variant 2: INetSerializable struct +struct MyType : INetSerializable +{ + public int Value1; + public string Value2; + + public void Serialize(NetDataWriter writer) + { + writer.Put(Value1); + writer.Put(Value2); + } + + public void Deserialize(NetDataReader reader) + { + Value1 = reader.GetInt(); + Value2 = reader.GetString(); + } +} +... +netPacketProcessor = new NetPacketProcessor(); +netPacketProcessor.RegisterNestedType(); // Serialization handled automatically thanks to INetSerializable +``` + +If you want to use a class instead of a struct you must implement the INetSerializable interface and provide a constructor: +```csharp +// Some custom type variant 3: Class, must implement INetSerializable +class MyType : INetSerializable +{ + public int Value1; + public string Value2; + + public void Serialize(NetDataWriter writer) + { + writer.Put(Value1); + writer.Put(Value2); + } + + public void Deserialize(NetDataReader reader) + { + Value1 = reader.GetInt(); + Value2 = reader.GetString(); + } +} +... +netPacketProcessor = new NetPacketProcessor(); +netPacketProcessor.RegisterNestedType(() => { return new SomeMyType(); }); // Must provide constructor +``` + +## Usage example +For full example look at source [SerializerBenchmark](https://github.com/RevenantX/LiteNetLib/blob/master/LibSample/SerializerBenchmark.cs) + +### Packet +```csharp +class SamplePacket +{ + // All of these will be automatically serialized and deserialized + public string SomeString { get; set; } + public float SomeFloat { get; set; } + public int[] SomeIntArray { get; set; } +} +``` + +### Sending / recieving +```csharp +// Client +class SomeClientListener : INetEventListener +{ + private readonly NetPacketProcessor _netPacketProcessor = new NetPacketProcessor(); +... + public void OnPeerConnected(NetPeer peer) + { + // After connection is established you will have the server as a NetPeer + SamplePacket packet = new SamplePacket + { + SomeFloat = 3.42f, + SomeIntArray = new[] {6, 5, 4}, + SomeString = "Test String", + } + // Serialize the packet with NetSerializer and send it to the peer (server) + peer.Send(_netPacketProcessor.Write(packet), DeliveryMethod.ReliableOrdered); + //You can also use _netPacketProcessor.Send(peer, packet, DeliveryMethod.ReliableOrdered); + } +} + +// Server +class SomeServerListener : INetEventListener +{ + private readonly NetPacketProcessor _netPacketProcessor = new NetPacketProcessor(); + + public SomeServerListener() + { + // Subscribe to recieving packets. + _netPacketProcessor.SubscribeReusable(OnSamplePacketReceived); + } + + // Handler for SamplePacket, registered in constructor above + private void OnSamplePacketReceived(SamplePacket samplePacket, NetPeer peer) + { + Console.WriteLine("[Server] ReceivedPacket:\n" + samplePacket.SomeString); + } + + // INetEventListener function. + public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) + { + Console.WriteLine("[Server] received data. Processing..."); + // Deserializes packet and calls the handler registered in constructor + _netPacketProcessor.ReadAllPackets(reader, peer); + } +} +``` + +### Mini FAQ +Q: `NetPacketProcessor` throws "`Undefined packet in NetDataReader`" but all packets are registered.
+A: This can happen when packet definitions resides in different namespaces. Check that registered packet classes/structs are in the same namespace on both ends. +To avoid this error altogether, use shared code/-assembly for packets. diff --git a/docfx_project/articles/toc.yml b/docfx_project/articles/toc.yml index ff89ef1f..8be02d89 100644 --- a/docfx_project/articles/toc.yml +++ b/docfx_project/articles/toc.yml @@ -1,2 +1,2 @@ -- name: Introduction - href: intro.md +- name: NetSerializer usage + href: netserializerusage.md diff --git a/docfx_project/docfx.json b/docfx_project/docfx.json index d9e47375..1f4d7ffd 100644 --- a/docfx_project/docfx.json +++ b/docfx_project/docfx.json @@ -12,7 +12,7 @@ "disableDefaultFilter": false, "filter": "filterConfig.yml", "properties": { - "TargetFramework": "net35" + "TargetFramework": "netstandard2.1" } } ], @@ -58,7 +58,7 @@ "default" ], "postProcessors": [], - "markdownEngineName": "markdig", + "markdownEngineName": "dfm", "noLangKeyword": false, "keepFileLink": false, "cleanupCacheHistory": false, diff --git a/docfx_project/index.md b/docfx_project/index.md index 3ae25063..110827a2 100644 --- a/docfx_project/index.md +++ b/docfx_project/index.md @@ -1,4 +1,141 @@ -# This is the **HOMEPAGE**. -Refer to [Markdown](http://daringfireball.net/projects/markdown/) for how to write markdown files. -## Quick Start Notes: -1. Add images to the *images* folder if the file is referencing an image. +# LiteNetLib + +Lite reliable UDP library for .NET Framework 3.5, Mono, .NET Core 2.0, .NET Standard 2.0. + +[![Discord](https://img.shields.io/discord/501682175930925058.svg)](https://discord.gg/FATFPdy) + +[Little Game Example on Unity](https://github.com/RevenantX/NetGameExample) + +## Features + +* Lightweight + * Small CPU and RAM usage + * Small packet size overhead ( 1 byte for unreliable, 3 bytes for reliable packets ) +* Simple connection handling +* Peer to peer connections +* Helper classes for sending and reading messages +* Multiple data channels +* Different send mechanics + * Reliable with order + * Reliable without order + * Reliable sequenced (realiable only last packet) + * Ordered but unreliable with duplication prevention + * Simple UDP packets without order and reliability +* Fast packet serializer [(Usage manual)](https://github.com/RevenantX/LiteNetLib/wiki/NetSerializer-usage) +* Automatic small packets merging +* Automatic fragmentation of reliable packets +* Automatic MTU detection +* UDP NAT hole punching +* NTP time requests +* Packet loss and latency simulation +* IPv6 support (dual mode) +* Connection statisitcs (need DEBUG or STATS_ENABLED flag) +* Multicasting (for discovering hosts in local network) +* Unity support +* Supported platforms: + * Windows/Mac/Linux (.NET Framework, Mono, .NET Core) + * Android (Unity) + * iOS (Unity) + * UWP Windows 10 including phones + * Lumin OS (Magic Leap) + +## Unity notes!!! +* Always use library sources instead of precompiled DLL files ( because there are platform specific #ifdefs and workarounds for unity bugs ) + +## Usage samples + +### Client +```csharp +EventBasedNetListener listener = new EventBasedNetListener(); +NetManager client = new NetManager(listener); +client.Start(); +client.Connect("localhost" /* host ip or name */, 9050 /* port */, "SomeConnectionKey" /* text key or NetDataWriter */); +listener.NetworkReceiveEvent += (fromPeer, dataReader, deliveryMethod) => +{ + Console.WriteLine("We got: {0}", dataReader.GetString(100 /* max length of string */)); + dataReader.Recycle(); +}; + +while (!Console.KeyAvailable) +{ + client.PollEvents(); + Thread.Sleep(15); +} + +client.Stop(); +``` +### Server +```csharp +EventBasedNetListener listener = new EventBasedNetListener(); +NetManager server = new NetManager(listener); +server.Start(9050 /* port */); + +listener.ConnectionRequestEvent += request => +{ + if(server.PeersCount < 10 /* max connections */) + request.AcceptIfKey("SomeConnectionKey"); + else + request.Reject(); +}; + +listener.PeerConnectedEvent += peer => +{ + Console.WriteLine("We got connection: {0}", peer.EndPoint); // Show peer ip + NetDataWriter writer = new NetDataWriter(); // Create writer class + writer.Put("Hello client!"); // Put some string + peer.Send(writer, DeliveryMethod.ReliableOrdered); // Send with reliability +}; + +while (!Console.KeyAvailable) +{ + server.PollEvents(); + Thread.Sleep(15); +} +server.Stop(); +``` + +## NetManager settings description + +* **UnconnectedMessagesEnabled** + * enable messages receiving without connection. (with SendUnconnectedMessage method) + * default value: **false** +* **NatPunchEnabled** + * enable NAT punch messages + * default value: **false** +* **UpdateTime** + * library logic update (and send) period in milliseconds + * default value: **15 msec**. +* **PingInterval** + * Interval for latency detection and checking connection + * default value: **1000 msec**. +* **DisconnectTimeout** + * if client or server doesn't receive any packet from remote peer during this time then connection will be closed + * (including library internal keepalive packets) + * default value: **5000 msec**. +* **SimulatePacketLoss** + * simulate packet loss by dropping random amout of packets. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) + * default value: **false** +* **SimulateLatency** + * simulate latency by holding packets for random time. (Works only in DEBUG builds or when SIMULATE_NETWORK is defined) + * default value: **false** +* **SimulationPacketLossChance** + * chance of packet loss when simulation enabled. value in percents. + * default value: **10 (%)** +* **SimulationMinLatency** + * minimum simulated latency + * default value: **30 msec** +* **SimulationMaxLatency** + * maximum simulated latency + * default value: **100 msec** +* **BroadcastEnabled** + * Allows receive Broadcast packets + * default value: **false** +* **ReconnectDelay** + * delay betwen connection attempts + * default value: **500 msec** +* **MaxConnectAttempts** + * maximum connection attempts before client stops and call disconnect event. + * default value: **10** +* **UnsyncedEvents** + * Experimental feature. Events automatically will be called without PollEvents method from another thread + * default value: **false** diff --git a/docs/api/LiteNetLib.ConnectionRequest.html b/docs/api/LiteNetLib.ConnectionRequest.html new file mode 100644 index 00000000..1c60018b --- /dev/null +++ b/docs/api/LiteNetLib.ConnectionRequest.html @@ -0,0 +1,239 @@ + + + + + + + + Class ConnectionRequest + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.ConnectionRequestType.html b/docs/api/LiteNetLib.ConnectionRequestType.html new file mode 100644 index 00000000..1d74755a --- /dev/null +++ b/docs/api/LiteNetLib.ConnectionRequestType.html @@ -0,0 +1,139 @@ + + + + + + + + Enum ConnectionRequestType + + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.ConnectionState.html b/docs/api/LiteNetLib.ConnectionState.html new file mode 100644 index 00000000..c6497d19 --- /dev/null +++ b/docs/api/LiteNetLib.ConnectionState.html @@ -0,0 +1,157 @@ + + + + + + + + Enum ConnectionState + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.DeliveryMethod.html b/docs/api/LiteNetLib.DeliveryMethod.html new file mode 100644 index 00000000..1d830771 --- /dev/null +++ b/docs/api/LiteNetLib.DeliveryMethod.html @@ -0,0 +1,158 @@ + + + + + + + + Enum DeliveryMethod + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.DisconnectInfo.html b/docs/api/LiteNetLib.DisconnectInfo.html new file mode 100644 index 00000000..74879117 --- /dev/null +++ b/docs/api/LiteNetLib.DisconnectInfo.html @@ -0,0 +1,197 @@ + + + + + + + + Struct DisconnectInfo + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.DisconnectReason.html b/docs/api/LiteNetLib.DisconnectReason.html new file mode 100644 index 00000000..0a39bf21 --- /dev/null +++ b/docs/api/LiteNetLib.DisconnectReason.html @@ -0,0 +1,185 @@ + + + + + + + + Enum DisconnectReason + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.EventBasedLiteNetListener.html b/docs/api/LiteNetLib.EventBasedLiteNetListener.html new file mode 100644 index 00000000..9dcfbc72 --- /dev/null +++ b/docs/api/LiteNetLib.EventBasedLiteNetListener.html @@ -0,0 +1,461 @@ + + + + + + + + Class EventBasedLiteNetListener + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.EventBasedNatPunchListener.html b/docs/api/LiteNetLib.EventBasedNatPunchListener.html new file mode 100644 index 00000000..64a64655 --- /dev/null +++ b/docs/api/LiteNetLib.EventBasedNatPunchListener.html @@ -0,0 +1,185 @@ + + + + + + + + Class EventBasedNatPunchListener + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.EventBasedNetListener.html b/docs/api/LiteNetLib.EventBasedNetListener.html new file mode 100644 index 00000000..d319a771 --- /dev/null +++ b/docs/api/LiteNetLib.EventBasedNetListener.html @@ -0,0 +1,497 @@ + + + + + + + + Class EventBasedNetListener + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.IDeliveryEventListener.html b/docs/api/LiteNetLib.IDeliveryEventListener.html new file mode 100644 index 00000000..b64791e0 --- /dev/null +++ b/docs/api/LiteNetLib.IDeliveryEventListener.html @@ -0,0 +1,154 @@ + + + + + + + + Interface IDeliveryEventListener + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.ILiteNetEventListener.html b/docs/api/LiteNetLib.ILiteNetEventListener.html new file mode 100644 index 00000000..09fbcb96 --- /dev/null +++ b/docs/api/LiteNetLib.ILiteNetEventListener.html @@ -0,0 +1,435 @@ + + + + + + + + Interface ILiteNetEventListener + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.INatPunchListener.html b/docs/api/LiteNetLib.INatPunchListener.html new file mode 100644 index 00000000..579fcb36 --- /dev/null +++ b/docs/api/LiteNetLib.INatPunchListener.html @@ -0,0 +1,204 @@ + + + + + + + + Interface INatPunchListener + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.INetEventListener.html b/docs/api/LiteNetLib.INetEventListener.html new file mode 100644 index 00000000..69aea427 --- /dev/null +++ b/docs/api/LiteNetLib.INetEventListener.html @@ -0,0 +1,469 @@ + + + + + + + + Interface INetEventListener + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.INetLogger.html b/docs/api/LiteNetLib.INetLogger.html new file mode 100644 index 00000000..51136839 --- /dev/null +++ b/docs/api/LiteNetLib.INetLogger.html @@ -0,0 +1,159 @@ + + + + + + + + Interface INetLogger + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.INtpEventListener.html b/docs/api/LiteNetLib.INtpEventListener.html new file mode 100644 index 00000000..8fcaeeec --- /dev/null +++ b/docs/api/LiteNetLib.INtpEventListener.html @@ -0,0 +1,149 @@ + + + + + + + + Interface INtpEventListener + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.IPeerAddressChangedListener.html b/docs/api/LiteNetLib.IPeerAddressChangedListener.html new file mode 100644 index 00000000..9deead5b --- /dev/null +++ b/docs/api/LiteNetLib.IPeerAddressChangedListener.html @@ -0,0 +1,156 @@ + + + + + + + + Interface IPeerAddressChangedListener + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.IPv6Mode.html b/docs/api/LiteNetLib.IPv6Mode.html new file mode 100644 index 00000000..20d8ea81 --- /dev/null +++ b/docs/api/LiteNetLib.IPv6Mode.html @@ -0,0 +1,143 @@ + + + + + + + + Enum IPv6Mode + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.InvalidPacketException.html b/docs/api/LiteNetLib.InvalidPacketException.html new file mode 100644 index 00000000..5a7d107c --- /dev/null +++ b/docs/api/LiteNetLib.InvalidPacketException.html @@ -0,0 +1,210 @@ + + + + + + + + Class InvalidPacketException + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Layers.Crc32cLayer.html b/docs/api/LiteNetLib.Layers.Crc32cLayer.html new file mode 100644 index 00000000..11ffd257 --- /dev/null +++ b/docs/api/LiteNetLib.Layers.Crc32cLayer.html @@ -0,0 +1,228 @@ + + + + + + + + Class Crc32cLayer + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Layers.PacketLayerBase.html b/docs/api/LiteNetLib.Layers.PacketLayerBase.html new file mode 100644 index 00000000..1a8c519a --- /dev/null +++ b/docs/api/LiteNetLib.Layers.PacketLayerBase.html @@ -0,0 +1,262 @@ + + + + + + + + Class PacketLayerBase + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Layers.XorEncryptLayer.html b/docs/api/LiteNetLib.Layers.XorEncryptLayer.html new file mode 100644 index 00000000..6ef948af --- /dev/null +++ b/docs/api/LiteNetLib.Layers.XorEncryptLayer.html @@ -0,0 +1,336 @@ + + + + + + + + Class XorEncryptLayer + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Layers.html b/docs/api/LiteNetLib.Layers.html new file mode 100644 index 00000000..573749d7 --- /dev/null +++ b/docs/api/LiteNetLib.Layers.html @@ -0,0 +1,122 @@ + + + + + + + + Namespace LiteNetLib.Layers + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.LiteConnectionRequest.html b/docs/api/LiteNetLib.LiteConnectionRequest.html new file mode 100644 index 00000000..68fb5ad5 --- /dev/null +++ b/docs/api/LiteNetLib.LiteConnectionRequest.html @@ -0,0 +1,527 @@ + + + + + + + + Class LiteConnectionRequest + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.LiteNetManager.NetPeerEnumerator-1.html b/docs/api/LiteNetLib.LiteNetManager.NetPeerEnumerator-1.html new file mode 100644 index 00000000..f927307d --- /dev/null +++ b/docs/api/LiteNetLib.LiteNetManager.NetPeerEnumerator-1.html @@ -0,0 +1,291 @@ + + + + + + + + Struct LiteNetManager.NetPeerEnumerator<T> + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.LiteNetManager.NetPeerEnumerator.html b/docs/api/LiteNetLib.LiteNetManager.NetPeerEnumerator.html new file mode 100644 index 00000000..2f0abbe7 --- /dev/null +++ b/docs/api/LiteNetLib.LiteNetManager.NetPeerEnumerator.html @@ -0,0 +1,276 @@ + + + + + + + + Struct LiteNetManager.NetPeerEnumerator + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.LiteNetManager.html b/docs/api/LiteNetLib.LiteNetManager.html new file mode 100644 index 00000000..96542635 --- /dev/null +++ b/docs/api/LiteNetLib.LiteNetManager.html @@ -0,0 +1,3715 @@ + + + + + + + + Class LiteNetManager + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.LiteNetPeer.html b/docs/api/LiteNetLib.LiteNetPeer.html new file mode 100644 index 00000000..d541b653 --- /dev/null +++ b/docs/api/LiteNetLib.LiteNetPeer.html @@ -0,0 +1,1390 @@ + + + + + + + + Class LiteNetPeer + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.LocalAddrType.html b/docs/api/LiteNetLib.LocalAddrType.html new file mode 100644 index 00000000..e6088603 --- /dev/null +++ b/docs/api/LiteNetLib.LocalAddrType.html @@ -0,0 +1,145 @@ + + + + + + + + Enum LocalAddrType + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NatAddressType.html b/docs/api/LiteNetLib.NatAddressType.html new file mode 100644 index 00000000..feae78d5 --- /dev/null +++ b/docs/api/LiteNetLib.NatAddressType.html @@ -0,0 +1,142 @@ + + + + + + + + Enum NatAddressType + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NatPunchModule.html b/docs/api/LiteNetLib.NatPunchModule.html new file mode 100644 index 00000000..a789e407 --- /dev/null +++ b/docs/api/LiteNetLib.NatPunchModule.html @@ -0,0 +1,354 @@ + + + + + + + + Class NatPunchModule + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetConnectRequestPacket.html b/docs/api/LiteNetLib.NetConnectRequestPacket.html new file mode 100644 index 00000000..c39a3bde --- /dev/null +++ b/docs/api/LiteNetLib.NetConnectRequestPacket.html @@ -0,0 +1,270 @@ + + + + + + + + Class NetConnectRequestPacket + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetConstants.html b/docs/api/LiteNetLib.NetConstants.html new file mode 100644 index 00000000..e89a2a94 --- /dev/null +++ b/docs/api/LiteNetLib.NetConstants.html @@ -0,0 +1,462 @@ + + + + + + + + Class NetConstants + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetDebug.html b/docs/api/LiteNetLib.NetDebug.html new file mode 100644 index 00000000..f6d47a40 --- /dev/null +++ b/docs/api/LiteNetLib.NetDebug.html @@ -0,0 +1,152 @@ + + + + + + + + Class NetDebug + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetEvent.EType.html b/docs/api/LiteNetLib.NetEvent.EType.html new file mode 100644 index 00000000..40a3bc78 --- /dev/null +++ b/docs/api/LiteNetLib.NetEvent.EType.html @@ -0,0 +1,182 @@ + + + + + + + + Enum NetEvent.EType + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetEvent.html b/docs/api/LiteNetLib.NetEvent.html new file mode 100644 index 00000000..2235de1f --- /dev/null +++ b/docs/api/LiteNetLib.NetEvent.html @@ -0,0 +1,458 @@ + + + + + + + + Class NetEvent + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetLogLevel.html b/docs/api/LiteNetLib.NetLogLevel.html new file mode 100644 index 00000000..b8decbe7 --- /dev/null +++ b/docs/api/LiteNetLib.NetLogLevel.html @@ -0,0 +1,147 @@ + + + + + + + + Enum NetLogLevel + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetManager.NetPeerEnumerator.html b/docs/api/LiteNetLib.NetManager.NetPeerEnumerator.html new file mode 100644 index 00000000..3050dfb0 --- /dev/null +++ b/docs/api/LiteNetLib.NetManager.NetPeerEnumerator.html @@ -0,0 +1,276 @@ + + + + + + + + Struct NetManager.NetPeerEnumerator + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetManager.html b/docs/api/LiteNetLib.NetManager.html new file mode 100644 index 00000000..4e240dbe --- /dev/null +++ b/docs/api/LiteNetLib.NetManager.html @@ -0,0 +1,1574 @@ + + + + + + + + Class NetManager + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetPacketReader.html b/docs/api/LiteNetLib.NetPacketReader.html new file mode 100644 index 00000000..df51c9cb --- /dev/null +++ b/docs/api/LiteNetLib.NetPacketReader.html @@ -0,0 +1,476 @@ + + + + + + + + Class NetPacketReader + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetPeer.html b/docs/api/LiteNetLib.NetPeer.html new file mode 100644 index 00000000..c8234ae8 --- /dev/null +++ b/docs/api/LiteNetLib.NetPeer.html @@ -0,0 +1,953 @@ + + + + + + + + Class NetPeer + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetStatistics.html b/docs/api/LiteNetLib.NetStatistics.html new file mode 100644 index 00000000..19c6e66b --- /dev/null +++ b/docs/api/LiteNetLib.NetStatistics.html @@ -0,0 +1,446 @@ + + + + + + + + Class NetStatistics + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.NetUtils.html b/docs/api/LiteNetLib.NetUtils.html new file mode 100644 index 00000000..18dba861 --- /dev/null +++ b/docs/api/LiteNetLib.NetUtils.html @@ -0,0 +1,419 @@ + + + + + + + + Class NetUtils + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.PooledPacket.html b/docs/api/LiteNetLib.PooledPacket.html new file mode 100644 index 00000000..21bc7f4b --- /dev/null +++ b/docs/api/LiteNetLib.PooledPacket.html @@ -0,0 +1,199 @@ + + + + + + + + Struct PooledPacket + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.TooBigPacketException.html b/docs/api/LiteNetLib.TooBigPacketException.html new file mode 100644 index 00000000..78a1a988 --- /dev/null +++ b/docs/api/LiteNetLib.TooBigPacketException.html @@ -0,0 +1,210 @@ + + + + + + + + Class TooBigPacketException + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.UnconnectedMessageType.html b/docs/api/LiteNetLib.UnconnectedMessageType.html new file mode 100644 index 00000000..75c7be41 --- /dev/null +++ b/docs/api/LiteNetLib.UnconnectedMessageType.html @@ -0,0 +1,140 @@ + + + + + + + + Enum UnconnectedMessageType + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.CRC32C.html b/docs/api/LiteNetLib.Utils.CRC32C.html new file mode 100644 index 00000000..918c04d1 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.CRC32C.html @@ -0,0 +1,209 @@ + + + + + + + + Class CRC32C + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.FastBitConverter.html b/docs/api/LiteNetLib.Utils.FastBitConverter.html new file mode 100644 index 00000000..ba3f4c92 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.FastBitConverter.html @@ -0,0 +1,199 @@ + + + + + + + + Class FastBitConverter + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.INetSerializable.html b/docs/api/LiteNetLib.Utils.INetSerializable.html new file mode 100644 index 00000000..f536537a --- /dev/null +++ b/docs/api/LiteNetLib.Utils.INetSerializable.html @@ -0,0 +1,183 @@ + + + + + + + + Interface INetSerializable + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.InvalidTypeException.html b/docs/api/LiteNetLib.Utils.InvalidTypeException.html new file mode 100644 index 00000000..b5c7a616 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.InvalidTypeException.html @@ -0,0 +1,209 @@ + + + + + + + + Class InvalidTypeException + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.NetDataReader.html b/docs/api/LiteNetLib.Utils.NetDataReader.html new file mode 100644 index 00000000..4b210e38 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.NetDataReader.html @@ -0,0 +1,4053 @@ + + + + + + + + Class NetDataReader + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.NetDataWriter.html b/docs/api/LiteNetLib.Utils.NetDataWriter.html new file mode 100644 index 00000000..39a22e04 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.NetDataWriter.html @@ -0,0 +1,2164 @@ + + + + + + + + Class NetDataWriter + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.NetPacketProcessor.html b/docs/api/LiteNetLib.Utils.NetPacketProcessor.html new file mode 100644 index 00000000..b80de24b --- /dev/null +++ b/docs/api/LiteNetLib.Utils.NetPacketProcessor.html @@ -0,0 +1,1191 @@ + + + + + + + + Class NetPacketProcessor + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.NetSerializer.html b/docs/api/LiteNetLib.Utils.NetSerializer.html new file mode 100644 index 00000000..3cc35124 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.NetSerializer.html @@ -0,0 +1,612 @@ + + + + + + + + Class NetSerializer + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.NtpLeapIndicator.html b/docs/api/LiteNetLib.Utils.NtpLeapIndicator.html new file mode 100644 index 00000000..6ab9dbab --- /dev/null +++ b/docs/api/LiteNetLib.Utils.NtpLeapIndicator.html @@ -0,0 +1,156 @@ + + + + + + + + Enum NtpLeapIndicator + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.NtpMode.html b/docs/api/LiteNetLib.Utils.NtpMode.html new file mode 100644 index 00000000..55f84b00 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.NtpMode.html @@ -0,0 +1,146 @@ + + + + + + + + Enum NtpMode + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.NtpPacket.html b/docs/api/LiteNetLib.Utils.NtpPacket.html new file mode 100644 index 00000000..d63393a4 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.NtpPacket.html @@ -0,0 +1,758 @@ + + + + + + + + Class NtpPacket + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.NtpRequest.html b/docs/api/LiteNetLib.Utils.NtpRequest.html new file mode 100644 index 00000000..fae22ed1 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.NtpRequest.html @@ -0,0 +1,395 @@ + + + + + + + + Class NtpRequest + + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.ParseException.html b/docs/api/LiteNetLib.Utils.ParseException.html new file mode 100644 index 00000000..34652750 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.ParseException.html @@ -0,0 +1,204 @@ + + + + + + + + Class ParseException + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.PreserveAttribute.html b/docs/api/LiteNetLib.Utils.PreserveAttribute.html new file mode 100644 index 00000000..d6063883 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.PreserveAttribute.html @@ -0,0 +1,241 @@ + + + + + + + + Class PreserveAttribute + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.Utils.html b/docs/api/LiteNetLib.Utils.html new file mode 100644 index 00000000..344fbc61 --- /dev/null +++ b/docs/api/LiteNetLib.Utils.html @@ -0,0 +1,153 @@ + + + + + + + + Namespace LiteNetLib.Utils + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/LiteNetLib.html b/docs/api/LiteNetLib.html new file mode 100644 index 00000000..323dc0da --- /dev/null +++ b/docs/api/LiteNetLib.html @@ -0,0 +1,219 @@ + + + + + + + + Namespace LiteNetLib + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/index.html b/docs/api/index.html new file mode 100644 index 00000000..d95b666e --- /dev/null +++ b/docs/api/index.html @@ -0,0 +1,109 @@ + + + + + + + + LiteNetLib + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/api/toc.html b/docs/api/toc.html new file mode 100644 index 00000000..290589f2 --- /dev/null +++ b/docs/api/toc.html @@ -0,0 +1,190 @@ + + diff --git a/docs/api/toc.json b/docs/api/toc.json new file mode 100644 index 00000000..17d2e194 --- /dev/null +++ b/docs/api/toc.json @@ -0,0 +1,2 @@ + +{"items":[{"name":"LiteNetLib","href":"LiteNetLib.html","topicHref":"LiteNetLib.html","topicUid":"LiteNetLib","type":"Namespace","items":[{"name":"ConnectionRequest","href":"LiteNetLib.ConnectionRequest.html","topicHref":"LiteNetLib.ConnectionRequest.html","topicUid":"LiteNetLib.ConnectionRequest","type":"Class"},{"name":"ConnectionState","href":"LiteNetLib.ConnectionState.html","topicHref":"LiteNetLib.ConnectionState.html","topicUid":"LiteNetLib.ConnectionState","type":"Enum"},{"name":"DeliveryMethod","href":"LiteNetLib.DeliveryMethod.html","topicHref":"LiteNetLib.DeliveryMethod.html","topicUid":"LiteNetLib.DeliveryMethod","type":"Enum"},{"name":"DisconnectInfo","href":"LiteNetLib.DisconnectInfo.html","topicHref":"LiteNetLib.DisconnectInfo.html","topicUid":"LiteNetLib.DisconnectInfo","type":"Struct"},{"name":"DisconnectReason","href":"LiteNetLib.DisconnectReason.html","topicHref":"LiteNetLib.DisconnectReason.html","topicUid":"LiteNetLib.DisconnectReason","type":"Enum"},{"name":"EventBasedLiteNetListener","href":"LiteNetLib.EventBasedLiteNetListener.html","topicHref":"LiteNetLib.EventBasedLiteNetListener.html","topicUid":"LiteNetLib.EventBasedLiteNetListener","type":"Class"},{"name":"EventBasedNatPunchListener","href":"LiteNetLib.EventBasedNatPunchListener.html","topicHref":"LiteNetLib.EventBasedNatPunchListener.html","topicUid":"LiteNetLib.EventBasedNatPunchListener","type":"Class"},{"name":"EventBasedNetListener","href":"LiteNetLib.EventBasedNetListener.html","topicHref":"LiteNetLib.EventBasedNetListener.html","topicUid":"LiteNetLib.EventBasedNetListener","type":"Class"},{"name":"ILiteNetEventListener","href":"LiteNetLib.ILiteNetEventListener.html","topicHref":"LiteNetLib.ILiteNetEventListener.html","topicUid":"LiteNetLib.ILiteNetEventListener","type":"Interface"},{"name":"INatPunchListener","href":"LiteNetLib.INatPunchListener.html","topicHref":"LiteNetLib.INatPunchListener.html","topicUid":"LiteNetLib.INatPunchListener","type":"Interface"},{"name":"INetEventListener","href":"LiteNetLib.INetEventListener.html","topicHref":"LiteNetLib.INetEventListener.html","topicUid":"LiteNetLib.INetEventListener","type":"Interface"},{"name":"INetLogger","href":"LiteNetLib.INetLogger.html","topicHref":"LiteNetLib.INetLogger.html","topicUid":"LiteNetLib.INetLogger","type":"Interface"},{"name":"InvalidPacketException","href":"LiteNetLib.InvalidPacketException.html","topicHref":"LiteNetLib.InvalidPacketException.html","topicUid":"LiteNetLib.InvalidPacketException","type":"Class"},{"name":"LiteConnectionRequest","href":"LiteNetLib.LiteConnectionRequest.html","topicHref":"LiteNetLib.LiteConnectionRequest.html","topicUid":"LiteNetLib.LiteConnectionRequest","type":"Class"},{"name":"LiteNetManager","href":"LiteNetLib.LiteNetManager.html","topicHref":"LiteNetLib.LiteNetManager.html","topicUid":"LiteNetLib.LiteNetManager","type":"Class"},{"name":"LiteNetManager.NetPeerEnumerator","href":"LiteNetLib.LiteNetManager.NetPeerEnumerator-1.html","topicHref":"LiteNetLib.LiteNetManager.NetPeerEnumerator-1.html","topicUid":"LiteNetLib.LiteNetManager.NetPeerEnumerator`1","type":"Struct"},{"name":"LiteNetPeer","href":"LiteNetLib.LiteNetPeer.html","topicHref":"LiteNetLib.LiteNetPeer.html","topicUid":"LiteNetLib.LiteNetPeer","type":"Class"},{"name":"LocalAddrType","href":"LiteNetLib.LocalAddrType.html","topicHref":"LiteNetLib.LocalAddrType.html","topicUid":"LiteNetLib.LocalAddrType","type":"Enum"},{"name":"NatAddressType","href":"LiteNetLib.NatAddressType.html","topicHref":"LiteNetLib.NatAddressType.html","topicUid":"LiteNetLib.NatAddressType","type":"Enum"},{"name":"NatPunchModule","href":"LiteNetLib.NatPunchModule.html","topicHref":"LiteNetLib.NatPunchModule.html","topicUid":"LiteNetLib.NatPunchModule","type":"Class"},{"name":"NetConnectRequestPacket","href":"LiteNetLib.NetConnectRequestPacket.html","topicHref":"LiteNetLib.NetConnectRequestPacket.html","topicUid":"LiteNetLib.NetConnectRequestPacket","type":"Class"},{"name":"NetConstants","href":"LiteNetLib.NetConstants.html","topicHref":"LiteNetLib.NetConstants.html","topicUid":"LiteNetLib.NetConstants","type":"Class"},{"name":"NetDebug","href":"LiteNetLib.NetDebug.html","topicHref":"LiteNetLib.NetDebug.html","topicUid":"LiteNetLib.NetDebug","type":"Class"},{"name":"NetEvent","href":"LiteNetLib.NetEvent.html","topicHref":"LiteNetLib.NetEvent.html","topicUid":"LiteNetLib.NetEvent","type":"Class"},{"name":"NetEvent.EType","href":"LiteNetLib.NetEvent.EType.html","topicHref":"LiteNetLib.NetEvent.EType.html","topicUid":"LiteNetLib.NetEvent.EType","type":"Enum"},{"name":"NetLogLevel","href":"LiteNetLib.NetLogLevel.html","topicHref":"LiteNetLib.NetLogLevel.html","topicUid":"LiteNetLib.NetLogLevel","type":"Enum"},{"name":"NetManager","href":"LiteNetLib.NetManager.html","topicHref":"LiteNetLib.NetManager.html","topicUid":"LiteNetLib.NetManager","type":"Class"},{"name":"NetPacketReader","href":"LiteNetLib.NetPacketReader.html","topicHref":"LiteNetLib.NetPacketReader.html","topicUid":"LiteNetLib.NetPacketReader","type":"Class"},{"name":"NetPeer","href":"LiteNetLib.NetPeer.html","topicHref":"LiteNetLib.NetPeer.html","topicUid":"LiteNetLib.NetPeer","type":"Class"},{"name":"NetStatistics","href":"LiteNetLib.NetStatistics.html","topicHref":"LiteNetLib.NetStatistics.html","topicUid":"LiteNetLib.NetStatistics","type":"Class"},{"name":"NetUtils","href":"LiteNetLib.NetUtils.html","topicHref":"LiteNetLib.NetUtils.html","topicUid":"LiteNetLib.NetUtils","type":"Class"},{"name":"PooledPacket","href":"LiteNetLib.PooledPacket.html","topicHref":"LiteNetLib.PooledPacket.html","topicUid":"LiteNetLib.PooledPacket","type":"Struct"},{"name":"TooBigPacketException","href":"LiteNetLib.TooBigPacketException.html","topicHref":"LiteNetLib.TooBigPacketException.html","topicUid":"LiteNetLib.TooBigPacketException","type":"Class"},{"name":"UnconnectedMessageType","href":"LiteNetLib.UnconnectedMessageType.html","topicHref":"LiteNetLib.UnconnectedMessageType.html","topicUid":"LiteNetLib.UnconnectedMessageType","type":"Enum"}]},{"name":"LiteNetLib.Layers","href":"LiteNetLib.Layers.html","topicHref":"LiteNetLib.Layers.html","topicUid":"LiteNetLib.Layers","type":"Namespace","items":[{"name":"Crc32cLayer","href":"LiteNetLib.Layers.Crc32cLayer.html","topicHref":"LiteNetLib.Layers.Crc32cLayer.html","topicUid":"LiteNetLib.Layers.Crc32cLayer","type":"Class"},{"name":"PacketLayerBase","href":"LiteNetLib.Layers.PacketLayerBase.html","topicHref":"LiteNetLib.Layers.PacketLayerBase.html","topicUid":"LiteNetLib.Layers.PacketLayerBase","type":"Class"},{"name":"XorEncryptLayer","href":"LiteNetLib.Layers.XorEncryptLayer.html","topicHref":"LiteNetLib.Layers.XorEncryptLayer.html","topicUid":"LiteNetLib.Layers.XorEncryptLayer","type":"Class"}]},{"name":"LiteNetLib.Utils","href":"LiteNetLib.Utils.html","topicHref":"LiteNetLib.Utils.html","topicUid":"LiteNetLib.Utils","type":"Namespace","items":[{"name":"CRC32C","href":"LiteNetLib.Utils.CRC32C.html","topicHref":"LiteNetLib.Utils.CRC32C.html","topicUid":"LiteNetLib.Utils.CRC32C","type":"Class"},{"name":"FastBitConverter","href":"LiteNetLib.Utils.FastBitConverter.html","topicHref":"LiteNetLib.Utils.FastBitConverter.html","topicUid":"LiteNetLib.Utils.FastBitConverter","type":"Class"},{"name":"INetSerializable","href":"LiteNetLib.Utils.INetSerializable.html","topicHref":"LiteNetLib.Utils.INetSerializable.html","topicUid":"LiteNetLib.Utils.INetSerializable","type":"Interface"},{"name":"InvalidTypeException","href":"LiteNetLib.Utils.InvalidTypeException.html","topicHref":"LiteNetLib.Utils.InvalidTypeException.html","topicUid":"LiteNetLib.Utils.InvalidTypeException","type":"Class"},{"name":"NetDataReader","href":"LiteNetLib.Utils.NetDataReader.html","topicHref":"LiteNetLib.Utils.NetDataReader.html","topicUid":"LiteNetLib.Utils.NetDataReader","type":"Class"},{"name":"NetDataWriter","href":"LiteNetLib.Utils.NetDataWriter.html","topicHref":"LiteNetLib.Utils.NetDataWriter.html","topicUid":"LiteNetLib.Utils.NetDataWriter","type":"Class"},{"name":"NetPacketProcessor","href":"LiteNetLib.Utils.NetPacketProcessor.html","topicHref":"LiteNetLib.Utils.NetPacketProcessor.html","topicUid":"LiteNetLib.Utils.NetPacketProcessor","type":"Class"},{"name":"NetSerializer","href":"LiteNetLib.Utils.NetSerializer.html","topicHref":"LiteNetLib.Utils.NetSerializer.html","topicUid":"LiteNetLib.Utils.NetSerializer","type":"Class"},{"name":"NtpLeapIndicator","href":"LiteNetLib.Utils.NtpLeapIndicator.html","topicHref":"LiteNetLib.Utils.NtpLeapIndicator.html","topicUid":"LiteNetLib.Utils.NtpLeapIndicator","type":"Enum"},{"name":"NtpMode","href":"LiteNetLib.Utils.NtpMode.html","topicHref":"LiteNetLib.Utils.NtpMode.html","topicUid":"LiteNetLib.Utils.NtpMode","type":"Enum"},{"name":"NtpPacket","href":"LiteNetLib.Utils.NtpPacket.html","topicHref":"LiteNetLib.Utils.NtpPacket.html","topicUid":"LiteNetLib.Utils.NtpPacket","type":"Class"},{"name":"ParseException","href":"LiteNetLib.Utils.ParseException.html","topicHref":"LiteNetLib.Utils.ParseException.html","topicUid":"LiteNetLib.Utils.ParseException","type":"Class"},{"name":"PreserveAttribute","href":"LiteNetLib.Utils.PreserveAttribute.html","topicHref":"LiteNetLib.Utils.PreserveAttribute.html","topicUid":"LiteNetLib.Utils.PreserveAttribute","type":"Class"}]}],"memberLayout":"SamePage"} diff --git a/docs/articles/netserializerusage.html b/docs/articles/netserializerusage.html new file mode 100644 index 00000000..16a66a14 --- /dev/null +++ b/docs/articles/netserializerusage.html @@ -0,0 +1,268 @@ + + + + + + + + NetPacketProcessor + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/articles/toc.html b/docs/articles/toc.html new file mode 100644 index 00000000..dd5b5ec1 --- /dev/null +++ b/docs/articles/toc.html @@ -0,0 +1,22 @@ + +
+
+
+
+ + + +
+
+
+
+ + +
+
+
+
diff --git a/docs/articles/toc.json b/docs/articles/toc.json new file mode 100644 index 00000000..a67e5ac5 --- /dev/null +++ b/docs/articles/toc.json @@ -0,0 +1,2 @@ + +{"items":[{"name":"NetSerializer usage","href":"netserializerusage.html","topicHref":"netserializerusage.html"}]} diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100755 index 00000000..71570f61 Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/fonts/glyphicons-halflings-regular.eot b/docs/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 00000000..b93a4953 Binary files /dev/null and b/docs/fonts/glyphicons-halflings-regular.eot differ diff --git a/docs/fonts/glyphicons-halflings-regular.svg b/docs/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 00000000..94fb5490 --- /dev/null +++ b/docs/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/glyphicons-halflings-regular.ttf b/docs/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 00000000..1413fc60 Binary files /dev/null and b/docs/fonts/glyphicons-halflings-regular.ttf differ diff --git a/docs/fonts/glyphicons-halflings-regular.woff b/docs/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 00000000..9e612858 Binary files /dev/null and b/docs/fonts/glyphicons-halflings-regular.woff differ diff --git a/docs/fonts/glyphicons-halflings-regular.woff2 b/docs/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 00000000..64539b54 Binary files /dev/null and b/docs/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..189405fb --- /dev/null +++ b/docs/index.html @@ -0,0 +1,287 @@ + + + + + + + + LiteNetLib + + + + + + + + + + + + + + + +
+
+ + + + +
+ + + +
+ + + + + + diff --git a/docs/logo.svg b/docs/logo.svg new file mode 100755 index 00000000..ccb2d7bc --- /dev/null +++ b/docs/logo.svg @@ -0,0 +1,25 @@ + + + + +Created by Docfx + + + + + + + diff --git a/docs/manifest.json b/docs/manifest.json new file mode 100644 index 00000000..3d87716c --- /dev/null +++ b/docs/manifest.json @@ -0,0 +1,836 @@ +{ + "source_base_path": "/home/revenantx/Projects/LiteNetLib/docfx_project", + "xrefmap": "xrefmap.yml", + "files": [ + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.ConnectionRequest.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.ConnectionRequest.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.ConnectionRequest", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.ConnectionState.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.ConnectionState.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.ConnectionState", + "Summary": "

Peer connection state

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.DeliveryMethod.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.DeliveryMethod.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.DeliveryMethod", + "Summary": "

Sending method type

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.DisconnectInfo.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.DisconnectInfo.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.DisconnectInfo", + "Summary": "

Additional information about disconnection

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.DisconnectReason.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.DisconnectReason.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.DisconnectReason", + "Summary": "

Disconnect reason that you receive in OnPeerDisconnected event

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.EventBasedLiteNetListener.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.EventBasedLiteNetListener.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.EventBasedLiteNetListener", + "Summary": "

Simple event based listener for simple setups and benchmarks

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.EventBasedNatPunchListener.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.EventBasedNatPunchListener.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.EventBasedNatPunchListener", + "Summary": "

An implementation of that maps callbacks to events.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.EventBasedNetListener.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.EventBasedNetListener.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.EventBasedNetListener", + "Summary": "

Simple event based listener for simple setups and benchmarks

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.ILiteNetEventListener.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.ILiteNetEventListener.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.ILiteNetEventListener", + "Summary": "

Interface for implementing own ILiteNetEventListener. This is a bit faster than use EventBasedListener

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.INatPunchListener.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.INatPunchListener.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.INatPunchListener", + "Summary": "

Interface for handling events related to NAT punchthrough and introduction.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.INetEventListener.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.INetEventListener.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.INetEventListener", + "Summary": "

Interface for implementing own INetEventListener. This is a bit faster than use EventBasedListener

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.INetLogger.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.INetLogger.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.INetLogger", + "Summary": "

Interface to implement for your own logger

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.InvalidPacketException.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.InvalidPacketException.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.InvalidPacketException", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Layers.Crc32cLayer.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Layers.Crc32cLayer.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Layers.Crc32cLayer", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Layers.PacketLayerBase.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Layers.PacketLayerBase.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Layers.PacketLayerBase", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Layers.XorEncryptLayer.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Layers.XorEncryptLayer.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Layers.XorEncryptLayer", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Layers.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Layers.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Layers", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.LiteConnectionRequest.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.LiteConnectionRequest.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.LiteConnectionRequest", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.LiteNetManager.NetPeerEnumerator-1.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.LiteNetManager.NetPeerEnumerator-1.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.LiteNetManager.NetPeerEnumerator", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.LiteNetManager.NetPeerEnumerator.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.LiteNetManager.NetPeerEnumerator.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.LiteNetManager.NetPeerEnumerator", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.LiteNetManager.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.LiteNetManager.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.LiteNetManager", + "Summary": "

Main class for all network operations. Can be used as client and/or server.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.LiteNetPeer.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.LiteNetPeer.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.LiteNetPeer", + "Summary": "

Network peer. Main purpose is sending messages to specific peer.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.LocalAddrType.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.LocalAddrType.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.LocalAddrType", + "Summary": "

Address type that you want to receive from NetUtils.GetLocalIp method

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NatAddressType.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NatAddressType.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NatAddressType", + "Summary": "

Specifies the type of network address discovered during NAT punchthrough.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NatPunchModule.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NatPunchModule.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NatPunchModule", + "Summary": "

Module for UDP NAT Hole punching operations. Can be accessed from NetManager

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetConnectRequestPacket.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetConnectRequestPacket.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetConnectRequestPacket", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetConstants.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetConstants.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetConstants", + "Summary": "

Network constants. Can be tuned from sources for your purposes.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetDebug.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetDebug.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetDebug", + "Summary": "

Static class for defining your own LiteNetLib logger instead of Console.WriteLine\nor Debug.Log if compiled with UNITY flag

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetEvent.EType.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetEvent.EType.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetEvent.EType", + "Summary": "

Specifies the category of the network event.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetEvent.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetEvent.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetEvent", + "Summary": "

Internally used event type

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetLogLevel.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetLogLevel.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetLogLevel", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetManager.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetManager.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetManager", + "Summary": "

More feature rich network manager with adjustable channels count

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetPacketReader.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetPacketReader.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetPacketReader", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetPeer.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetPeer.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetPeer", + "Summary": "

Improved LiteNetPeer with full multi-channel support

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetStatistics.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetStatistics.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetStatistics", + "Summary": "

Thread-safe counter for network statistics including sent/received packets, bytes, and packet loss.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.NetUtils.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.NetUtils.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.NetUtils", + "Summary": "

Some specific network utilities

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.PooledPacket.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.PooledPacket.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.PooledPacket", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.TooBigPacketException.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.TooBigPacketException.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.TooBigPacketException", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.UnconnectedMessageType.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.UnconnectedMessageType.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.UnconnectedMessageType", + "Summary": "

Type of message that you receive in OnNetworkReceiveUnconnected event

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.CRC32C.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.CRC32C.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.CRC32C", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.FastBitConverter.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.FastBitConverter.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.FastBitConverter", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.INetSerializable.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.INetSerializable.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.INetSerializable", + "Summary": "

Interface for implementing custom data serialization for network transmission.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.InvalidTypeException.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.InvalidTypeException.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.InvalidTypeException", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.NetDataReader.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.NetDataReader.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.NetDataReader", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.NetDataWriter.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.NetDataWriter.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.NetDataWriter", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.NetPacketProcessor.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.NetPacketProcessor.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.NetPacketProcessor", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.NetSerializer.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.NetSerializer.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.NetSerializer", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.NtpLeapIndicator.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.NtpLeapIndicator.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.NtpLeapIndicator", + "Summary": "

Represents leap second warning from the server that instructs the client to add or remove leap second.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.NtpMode.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.NtpMode.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.NtpMode", + "Summary": "

Describes SNTP packet mode, i.e. client or server.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.NtpPacket.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.NtpPacket.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.NtpPacket", + "Summary": "

Represents RFC4330 SNTP packet used for communication to and from a network time server.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.ParseException.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.ParseException.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.ParseException", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.PreserveAttribute.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.PreserveAttribute.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils.PreserveAttribute", + "Summary": "

PreserveAttribute prevents byte code stripping from removing a class, method, field, or property.

\n" + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.Utils.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.Utils.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib.Utils", + "Summary": null + }, + { + "type": "ManagedReference", + "source_relative_path": "api/LiteNetLib.yml", + "output": { + ".html": { + "relative_path": "api/LiteNetLib.html" + } + }, + "version": "", + "Uid": null, + "IsMRef": true, + "Title": "LiteNetLib", + "Summary": null + }, + { + "type": "Conceptual", + "source_relative_path": "api/index.md", + "output": { + ".html": { + "relative_path": "api/index.html" + } + }, + "version": "" + }, + { + "type": "Toc", + "source_relative_path": "api/toc.yml", + "output": { + ".html": { + "relative_path": "api/toc.html" + }, + ".json": { + "relative_path": "api/toc.json" + } + }, + "version": "" + }, + { + "type": "Conceptual", + "source_relative_path": "articles/netserializerusage.md", + "output": { + ".html": { + "relative_path": "articles/netserializerusage.html" + } + }, + "version": "" + }, + { + "type": "Toc", + "source_relative_path": "articles/toc.yml", + "output": { + ".html": { + "relative_path": "articles/toc.html" + }, + ".json": { + "relative_path": "articles/toc.json" + } + }, + "version": "" + }, + { + "type": "Conceptual", + "source_relative_path": "index.md", + "output": { + ".html": { + "relative_path": "index.html" + } + }, + "version": "" + }, + { + "type": "Toc", + "source_relative_path": "toc.yml", + "output": { + ".html": { + "relative_path": "toc.html" + }, + ".json": { + "relative_path": "toc.json" + } + }, + "version": "" + } + ], + "groups": [ + { + "xrefmap": "xrefmap.yml" + } + ] +} \ No newline at end of file diff --git a/docs/search-stopwords.json b/docs/search-stopwords.json new file mode 100755 index 00000000..0bdcc2c0 --- /dev/null +++ b/docs/search-stopwords.json @@ -0,0 +1,121 @@ +[ + "a", + "able", + "about", + "across", + "after", + "all", + "almost", + "also", + "am", + "among", + "an", + "and", + "any", + "are", + "as", + "at", + "be", + "because", + "been", + "but", + "by", + "can", + "cannot", + "could", + "dear", + "did", + "do", + "does", + "either", + "else", + "ever", + "every", + "for", + "from", + "get", + "got", + "had", + "has", + "have", + "he", + "her", + "hers", + "him", + "his", + "how", + "however", + "i", + "if", + "in", + "into", + "is", + "it", + "its", + "just", + "least", + "let", + "like", + "likely", + "may", + "me", + "might", + "most", + "must", + "my", + "neither", + "no", + "nor", + "not", + "of", + "off", + "often", + "on", + "only", + "or", + "other", + "our", + "own", + "rather", + "said", + "say", + "says", + "she", + "should", + "since", + "so", + "some", + "than", + "that", + "the", + "their", + "them", + "then", + "there", + "these", + "they", + "this", + "tis", + "to", + "too", + "twas", + "us", + "wants", + "was", + "we", + "were", + "what", + "when", + "where", + "which", + "while", + "who", + "whom", + "why", + "will", + "with", + "would", + "yet", + "you", + "your" +] diff --git a/docs/styles/docfx.css b/docs/styles/docfx.css new file mode 100755 index 00000000..7d80d139 --- /dev/null +++ b/docs/styles/docfx.css @@ -0,0 +1,1041 @@ +/** + * Licensed to the .NET Foundation under one or more agreements. + * The .NET Foundation licenses this file to you under the MIT license. + */ +html, +body { + font-family: 'Segoe UI', Tahoma, Helvetica, sans-serif; + height: 100%; +} +button, +a { + color: #337ab7; + cursor: pointer; +} +button:hover, +button:focus, +a:hover, +a:focus { + color: #23527c; + text-decoration: none; +} +a.disable, +a.disable:hover { + text-decoration: none; + cursor: default; + color: #000000; +} + +h1, h2, h3, h4, h5, h6, .text-break { + word-wrap: break-word; + word-break: break-word; +} + +h1 mark, +h2 mark, +h3 mark, +h4 mark, +h5 mark, +h6 mark { + padding: 0; +} + +.inheritance .level0:before, +.inheritance .level1:before, +.inheritance .level2:before, +.inheritance .level3:before, +.inheritance .level4:before, +.inheritance .level5:before, +.inheritance .level6:before, +.inheritance .level7:before, +.inheritance .level8:before, +.inheritance .level9:before { + content: '↳'; + margin-right: 5px; +} + +.inheritance .level0 { + margin-left: 0em; +} + +.inheritance .level1 { + margin-left: 1em; +} + +.inheritance .level2 { + margin-left: 2em; +} + +.inheritance .level3 { + margin-left: 3em; +} + +.inheritance .level4 { + margin-left: 4em; +} + +.inheritance .level5 { + margin-left: 5em; +} + +.inheritance .level6 { + margin-left: 6em; +} + +.inheritance .level7 { + margin-left: 7em; +} + +.inheritance .level8 { + margin-left: 8em; +} + +.inheritance .level9 { + margin-left: 9em; +} + +.level0.summary { + margin: 2em 0 2em 0; +} + +.level1.summary { + margin: 1em 0 1em 0; +} + +span.parametername, +span.paramref, +span.typeparamref { + font-style: italic; +} +span.languagekeyword{ + font-weight: bold; +} + +.hljs { + display: inline; + background-color: inherit; + padding: 0; +} +/* additional spacing fixes */ +.btn + .btn { + margin-left: 10px; +} +.btn.pull-right { + margin-left: 10px; + margin-top: 5px; +} +.table { + margin-bottom: 10px; +} +table p { + margin-bottom: 0; +} +table a { + display: inline-block; +} + +/* Make hidden attribute compatible with old browser.*/ +[hidden] { + display: none !important; +} + +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 15px; + margin-bottom: 10px; + font-weight: 400; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 5px; +} +.navbar { + margin-bottom: 0; +} +#wrapper { + min-height: 100%; + position: relative; +} +/* blends header footer and content together with gradient effect */ +.grad-top { + /* For Safari 5.1 to 6.0 */ + /* For Opera 11.1 to 12.0 */ + /* For Firefox 3.6 to 15 */ + background: linear-gradient(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0)); + /* Standard syntax */ + height: 5px; +} +.grad-bottom { + /* For Safari 5.1 to 6.0 */ + /* For Opera 11.1 to 12.0 */ + /* For Firefox 3.6 to 15 */ + background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.05)); + /* Standard syntax */ + height: 5px; +} +.divider { + margin: 0 5px; + color: #cccccc; +} +hr { + border-color: #cccccc; +} +header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 1000; +} +header .navbar { + border-width: 0 0 1px; + border-radius: 0; +} +.navbar-brand { + font-size: inherit; + padding: 0; +} +.navbar-collapse { + margin: 0 -15px; +} +.subnav { + min-height: 40px; +} + +.inheritance h5, .inheritedMembers h5{ + padding-bottom: 5px; + border-bottom: 1px solid #ccc; +} + +article h1, article h2, article h3, article h4{ + margin-top: 25px; +} + +article h4{ + border: 0; + font-weight: bold; + margin-top: 2em; +} + +article span.small.pull-right{ + margin-top: 20px; +} + +article section { + margin-left: 1em; +} + +/*.expand-all { + padding: 10px 0; +}*/ +.breadcrumb { + margin: 0; + padding: 10px 0; + background-color: inherit; + white-space: nowrap; +} +.breadcrumb > li + li:before { + content: "\00a0/"; +} +#autocollapse.collapsed .navbar-header { + float: none; +} +#autocollapse.collapsed .navbar-toggle { + display: block; +} +#autocollapse.collapsed .navbar-collapse { + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); +} +#autocollapse.collapsed .navbar-collapse.collapse { + display: none !important; +} +#autocollapse.collapsed .navbar-nav { + float: none !important; + margin: 7.5px -15px; +} +#autocollapse.collapsed .navbar-nav > li { + float: none; +} +#autocollapse.collapsed .navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; +} +#autocollapse.collapsed .collapse.in, +#autocollapse.collapsed .collapsing { + display: block !important; +} +#autocollapse.collapsed .collapse.in .navbar-right, +#autocollapse.collapsed .collapsing .navbar-right { + float: none !important; +} +#autocollapse .form-group { + width: 100%; +} +#autocollapse .form-control { + width: 100%; +} +#autocollapse .navbar-header { + margin-left: 0; + margin-right: 0; +} +#autocollapse .navbar-brand { + margin-left: 0; +} +.collapse.in, +.collapsing { + text-align: center; +} +.collapsing .navbar-form { + margin: 0 auto; + max-width: 400px; + padding: 10px 15px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); +} +.collapsed .collapse.in .navbar-form { + margin: 0 auto; + max-width: 400px; + padding: 10px 15px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); +} +.navbar .navbar-nav { + display: inline-block; +} +.docs-search { + background: white; + vertical-align: middle; +} +.docs-search > .search-query { + font-size: 14px; + border: 0; + width: 120%; + color: #555; +} +.docs-search > .search-query:focus { + outline: 0; +} +.search-results-frame { + clear: both; + display: table; + width: 100%; +} +.search-results.ng-hide { + display: none; +} +.search-results-container { + padding-bottom: 1em; + border-top: 1px solid #111; + background: rgba(25, 25, 25, 0.5); +} +.search-results-container .search-results-group { + padding-top: 50px !important; + padding: 10px; +} +.search-results-group-heading { + font-family: "Open Sans"; + padding-left: 10px; + color: white; +} +.search-close { + position: absolute; + left: 50%; + margin-left: -100px; + color: white; + text-align: center; + padding: 5px; + background: #333; + border-top-right-radius: 5px; + border-top-left-radius: 5px; + width: 200px; + box-shadow: 0 0 10px #111; +} +#search { + display: none; +} + +/* Search results display*/ +#search-results { + max-width: 960px !important; + margin-top: 120px; + margin-bottom: 115px; + margin-left: auto; + margin-right: auto; + line-height: 1.8; + display: none; +} + +#search-results>.search-list { + text-align: center; + font-size: 2.5rem; + margin-bottom: 50px; +} + +#search-results p { + text-align: center; +} + +#search-results p .index-loading { + animation: index-loading 1.5s infinite linear; + -webkit-animation: index-loading 1.5s infinite linear; + -o-animation: index-loading 1.5s infinite linear; + font-size: 2.5rem; +} + +@keyframes index-loading { + from { transform: scale(1) rotate(0deg);} + to { transform: scale(1) rotate(360deg);} +} + +@-webkit-keyframes index-loading { + from { -webkit-transform: rotate(0deg);} + to { -webkit-transform: rotate(360deg);} +} + +@-o-keyframes index-loading { + from { -o-transform: rotate(0deg);} + to { -o-transform: rotate(360deg);} +} + +#search-results .sr-items { + font-size: 24px; +} + +.sr-item { + margin-bottom: 25px; +} + +.sr-item>.item-href { + font-size: 14px; + color: #093; +} + +.sr-item>.item-brief { + font-size: 13px; +} + +.pagination>li>a { + color: #47A7A0 +} + +.pagination>.active>a { + background-color: #47A7A0; + border-color: #47A7A0; +} + +.fixed_header { + position: fixed; + width: 100%; + padding-bottom: 10px; + padding-top: 10px; + margin: 0px; + top: 0; + z-index: 9999; + left: 0; +} + +.fixed_header+.toc{ + margin-top: 50px; + margin-left: 0; +} + +.sidenav, .fixed_header, .toc { + background-color: #f1f1f1; +} + +.sidetoc { + position: fixed; + width: 260px; + top: 150px; + bottom: 0; + overflow-x: hidden; + overflow-y: auto; + background-color: #f1f1f1; + border-left: 1px solid #e7e7e7; + border-right: 1px solid #e7e7e7; + z-index: 1; +} + +.sidetoc.shiftup { + bottom: 70px; +} + +body .toc{ + background-color: #f1f1f1; + overflow-x: hidden; +} + +.sidetoggle.ng-hide { + display: block !important; +} +.sidetoc-expand > .caret { + margin-left: 0px; + margin-top: -2px; +} +.sidetoc-expand > .caret-side { + border-left: 4px solid; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + margin-left: 4px; + margin-top: -4px; +} +.sidetoc-heading { + font-weight: 500; +} + +.toc { + margin: 0px 0 0 10px; + padding: 0 10px; +} +.expand-stub { + position: absolute; + left: -10px; + font-weight: bold; +} +.expand-stub + a { + font-weight: bold; +} +.toc .nav > li > a.sidetoc-expand { + position: absolute; + top: 0; + left: 0; +} +.toc .nav > li > a { + color: #666666; + margin-left: 5px; + display: block; + padding: 0; +} +.toc .nav > li > a:hover, +.toc .nav > li > a:focus { + color: #000000; + background: none; + text-decoration: inherit; +} +.toc .nav > li.active > a { + color: #337ab7; +} +.toc .nav > li.active > a:hover, +.toc .nav > li.active > a:focus { + color: #23527c; +} + +.toc .nav > li> .expand-stub { + cursor: pointer; +} + +.toc .nav > li.active > .expand-stub::before, +.toc .nav > li.in > .expand-stub::before, +.toc .nav > li.in.active > .expand-stub::before, +.toc .nav > li.filtered > .expand-stub::before { + content: "-"; +} + +.toc .nav > li > .expand-stub::before, +.toc .nav > li.active > .expand-stub::before { + content: "+"; +} + +.toc .nav > li.filtered > ul, +.toc .nav > li.in > ul { + display: block; +} + +.toc .nav > li > ul { + display: none; +} + +.toc ul{ + font-size: 12px; + margin: 0 0 0 3px; +} + +.toc .level1 > li { + font-weight: bold; + margin-top: 10px; + position: relative; + font-size: 16px; +} +.toc .level2 { + font-weight: normal; + margin: 5px 0 0 15px; + font-size: 14px; +} +.toc-toggle { + display: none; + margin: 0 15px 0px 15px; +} +.sidefilter { + position: fixed; + top: 90px; + width: 260px; + background-color: #f1f1f1; + padding: 15px; + border-left: 1px solid #e7e7e7; + border-right: 1px solid #e7e7e7; + z-index: 1; +} +.toc-filter { + border-radius: 5px; + background: #fff; + color: #666666; + padding: 5px; + position: relative; + margin: 0 5px 0 5px; +} +.toc-filter > input { + border: 0; + color: #666666; + padding-left: 20px; + padding-right: 20px; + width: 100%; +} +.toc-filter > input:focus { + outline: 0; +} +.toc-filter > .filter-icon { + position: absolute; + top: 10px; + left: 5px; +} +.toc-filter > .clear-icon { + position: absolute; + top: 10px; + right: 5px; +} +.article { + margin-top: 120px; + margin-bottom: 115px; +} + +#_content>a{ + margin-top: 105px; +} + +.article.grid-right { + margin-left: 280px; +} + +.inheritance hr { + margin-top: 5px; + margin-bottom: 5px; +} +.article img { + max-width: 100%; +} +.sideaffix { + margin-top: 50px; + font-size: 12px; + max-height: 100%; + overflow: hidden; + top: 100px; + bottom: 10px; + position: fixed; +} +.sideaffix.shiftup { + bottom: 70px; +} +.affix { + position: relative; + height: 100%; +} +.sideaffix > div.contribution { + margin-bottom: 20px; +} +.sideaffix > div.contribution > ul > li > a.contribution-link { + padding: 6px 10px; + font-weight: bold; + font-size: 14px; +} +.sideaffix > div.contribution > ul > li > a.contribution-link:hover { + background-color: #ffffff; +} +.sideaffix ul.nav > li > a:focus { + background: none; +} +.affix h5 { + font-weight: bold; + text-transform: uppercase; + padding-left: 10px; + font-size: 12px; +} +.affix > ul.level1 { + overflow: hidden; + padding-bottom: 10px; + height: calc(100% - 100px); +} +.affix ul > li > a:before { + color: #cccccc; + position: absolute; +} +.affix ul > li > a:hover { + background: none; + color: #666666; +} +.affix ul > li.active > a, +.affix ul > li.active > a:before { + color: #337ab7; +} +.affix ul > li > a { + padding: 5px 12px; + color: #666666; +} +.affix > ul > li.active:last-child { + margin-bottom: 50px; +} +.affix > ul > li > a:before { + content: "|"; + font-size: 16px; + top: 1px; + left: 0; +} +.affix > ul > li.active > a, +.affix > ul > li.active > a:before { + color: #337ab7; + font-weight: bold; +} +.affix ul ul > li > a { + padding: 2px 15px; +} +.affix ul ul > li > a:before { + content: ">"; + font-size: 14px; + top: -1px; + left: 5px; +} +.affix ul > li > a:before, +.affix ul ul { + display: none; +} +.affix ul > li.active > ul, +.affix ul > li.active > a:before, +.affix ul > li > a:hover:before { + display: block; + white-space: nowrap; +} +.codewrapper { + position: relative; +} +.trydiv { + height: 0px; +} +.tryspan { + position: absolute; + top: 0px; + right: 0px; + border-style: solid; + border-radius: 0px 4px; + box-sizing: border-box; + border-width: 1px; + border-color: #cccccc; + text-align: center; + padding: 2px 8px; + background-color: white; + font-size: 12px; + cursor: pointer; + z-index: 100; + display: none; + color: #767676; +} +.tryspan:hover { + background-color: #3b8bd0; + color: white; + border-color: #3b8bd0; +} +.codewrapper:hover .tryspan { + display: block; +} +.sample-response .response-content{ + max-height: 200px; +} +footer { + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; +} +.footer { + border-top: 1px solid #e7e7e7; + background-color: #f8f8f8; + padding: 15px 0; +} +@media (min-width: 768px) { + #sidetoggle.collapse { + display: block; + } + .topnav .navbar-nav { + float: none; + white-space: nowrap; + } + .topnav .navbar-nav > li { + float: none; + display: inline-block; + } +} +@media only screen and (max-width: 768px) { + #mobile-indicator { + display: block; + } + /* TOC display for responsive */ + .article { + margin-top: 30px !important; + } + header { + position: static; + } + .topnav { + text-align: center; + } + .sidenav { + padding: 15px 0; + margin-left: -15px; + margin-right: -15px; + } + .sidefilter { + position: static; + width: auto; + float: none; + border: none; + } + .sidetoc { + position: static; + width: auto; + float: none; + padding-bottom: 0px; + border: none; + } + .toc .nav > li, .toc .nav > li >a { + display: inline-block; + } + .toc li:after { + margin-left: -3px; + margin-right: 5px; + content: ", "; + color: #666666; + } + .toc .level1 > li { + display: block; + } + + .toc .level1 > li:after { + display: none; + } + .article.grid-right { + margin-left: 0; + } + .grad-top, + .grad-bottom { + display: none; + } + .toc-toggle { + display: block; + } + .sidetoggle.ng-hide { + display: none !important; + } + /*.expand-all { + display: none; + }*/ + .sideaffix { + display: none; + } + .mobile-hide { + display: none; + } + .breadcrumb { + white-space: inherit; + } + + /* workaround for #hashtag url is no longer needed*/ + h1:before, + h2:before, + h3:before, + h4:before { + content: ''; + display: none; + } +} + +/* For toc iframe */ +@media (max-width: 260px) { + .toc .level2 > li { + display: block; + } + + .toc .level2 > li:after { + display: none; + } +} + +/* Code snippet */ +code { + color: #717374; + background-color: #f1f2f3; +} + +a code { + color: #337ab7; + background-color: #f1f2f3; +} + +a code:hover { + text-decoration: underline; +} + +.hljs-keyword { + color: rgb(86,156,214); +} + +.hljs-string { + color: rgb(214, 157, 133); +} + +pre { + border: 0; +} + +/* For code snippet line highlight */ +pre > code .line-highlight { + background-color: #ffffcc; +} + +/* Alerts */ +.alert h5 { + text-transform: uppercase; + font-weight: bold; + margin-top: 0; +} + +.alert h5:before { + position:relative; + top:1px; + display:inline-block; + font-family:'Glyphicons Halflings'; + line-height:1; + -webkit-font-smoothing:antialiased; + -moz-osx-font-smoothing:grayscale; + margin-right: 5px; + font-weight: normal; +} + +.alert-info h5:before { + content:"\e086" +} + +.alert-warning h5:before { + content:"\e127" +} + +.alert-danger h5:before { + content:"\e107" +} + +/* For Embedded Video */ +div.embeddedvideo { + padding-top: 56.25%; + position: relative; + width: 100%; +} + +div.embeddedvideo iframe { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; +} + +/* For printer */ +@media print{ + .article.grid-right { + margin-top: 0px; + margin-left: 0px; + } + .sideaffix { + display: none; + } + .mobile-hide { + display: none; + } + .footer { + display: none; + } +} + +/* For tabbed content */ + +.tabGroup { + margin-top: 1rem; } + .tabGroup ul[role="tablist"] { + margin: 0; + padding: 0; + list-style: none; } + .tabGroup ul[role="tablist"] > li { + list-style: none; + display: inline-block; } + .tabGroup a[role="tab"] { + color: #6e6e6e; + box-sizing: border-box; + display: inline-block; + padding: 5px 7.5px; + text-decoration: none; + border-bottom: 2px solid #fff; } + .tabGroup a[role="tab"]:hover, .tabGroup a[role="tab"]:focus, .tabGroup a[role="tab"][aria-selected="true"] { + border-bottom: 2px solid #0050C5; } + .tabGroup a[role="tab"][aria-selected="true"] { + color: #222; } + .tabGroup a[role="tab"]:hover, .tabGroup a[role="tab"]:focus { + color: #0050C5; } + .tabGroup a[role="tab"]:focus { + outline: 1px solid #0050C5; + outline-offset: -1px; } + @media (min-width: 768px) { + .tabGroup a[role="tab"] { + padding: 5px 15px; } } + .tabGroup section[role="tabpanel"] { + border: 1px solid #e0e0e0; + padding: 15px; + margin: 0; + overflow: hidden; } + .tabGroup section[role="tabpanel"] > .codeHeader, + .tabGroup section[role="tabpanel"] > pre { + margin-left: -16px; + margin-right: -16px; } + .tabGroup section[role="tabpanel"] > :first-child { + margin-top: 0; } + .tabGroup section[role="tabpanel"] > pre:last-child { + display: block; + margin-bottom: -16px; } + +.mainContainer[dir='rtl'] main ul[role="tablist"] { + margin: 0; } + +/* Color theme */ + +/* These are not important, tune down **/ +.declaration, .fieldValue, .parameters, .returns { + color: #a2a2a2; +} + +/* Major sections, increase visibility **/ +#fields, #properties, #methods, #events { + font-weight: bold; + margin-top: 2em; +} + +@media print { + @page { + margin: .4in; + } +} diff --git a/docs/styles/docfx.js b/docs/styles/docfx.js new file mode 100755 index 00000000..39943509 --- /dev/null +++ b/docs/styles/docfx.js @@ -0,0 +1,1188 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +$(function () { + var active = 'active'; + var expanded = 'in'; + var collapsed = 'collapsed'; + var filtered = 'filtered'; + var show = 'show'; + var hide = 'hide'; + var util = new utility(); + + workAroundFixedHeaderForAnchors(); + highlight(); + enableSearch(); + + renderTables(); + renderAlerts(); + renderLinks(); + renderNavbar(); + renderSidebar(); + renderAffix(); + renderFooter(); + renderLogo(); + + breakText(); + renderTabs(); + + window.refresh = function (article) { + // Update markup result + if (typeof article == 'undefined' || typeof article.content == 'undefined') + console.error("Null Argument"); + $("article.content").html(article.content); + + highlight(); + renderTables(); + renderAlerts(); + renderAffix(); + renderTabs(); + } + + // Add this event listener when needed + // window.addEventListener('content-update', contentUpdate); + + function breakText() { + $(".xref").addClass("text-break"); + var texts = $(".text-break"); + texts.each(function () { + $(this).breakWord(); + }); + } + + // Styling for tables in conceptual documents using Bootstrap. + // See http://getbootstrap.com/css/#tables + function renderTables() { + $('table').addClass('table table-bordered table-condensed').wrap('
'); + } + + // Styling for alerts. + function renderAlerts() { + $('.NOTE, .TIP').addClass('alert alert-info'); + $('.WARNING').addClass('alert alert-warning'); + $('.IMPORTANT, .CAUTION').addClass('alert alert-danger'); + } + + // Enable anchors for headings. + (function () { + anchors.options = { + placement: 'left', + visible: 'hover' + }; + anchors.add('article h2:not(.no-anchor), article h3:not(.no-anchor), article h4:not(.no-anchor)'); + })(); + + // Open links to different host in a new window. + function renderLinks() { + if ($("meta[property='docfx:newtab']").attr("content") === "true") { + $(document.links).filter(function () { + return this.hostname !== window.location.hostname; + }).attr('target', '_blank'); + } + } + + // Enable highlight.js + function highlight() { + $('pre code').each(function (i, block) { + hljs.highlightElement(block); + }); + $('pre code[highlight-lines]').each(function (i, block) { + if (block.innerHTML === "") return; + var lines = block.innerHTML.split('\n'); + + queryString = block.getAttribute('highlight-lines'); + if (!queryString) return; + + var ranges = queryString.split(','); + for (var j = 0, range; range = ranges[j++];) { + var found = range.match(/^(\d+)\-(\d+)?$/); + if (found) { + // consider region as `{startlinenumber}-{endlinenumber}`, in which {endlinenumber} is optional + var start = +found[1]; + var end = +found[2]; + if (isNaN(end) || end > lines.length) { + end = lines.length; + } + } else { + // consider region as a sigine line number + if (isNaN(range)) continue; + var start = +range; + var end = start; + } + if (start <= 0 || end <= 0 || start > end || start > lines.length) { + // skip current region if invalid + continue; + } + lines[start - 1] = '' + lines[start - 1]; + lines[end - 1] = lines[end - 1] + ''; + } + + block.innerHTML = lines.join('\n'); + }); + } + + // Support full-text-search + function enableSearch() { + var query; + var relHref = $("meta[property='docfx\\:rel']").attr("content"); + if (typeof relHref === 'undefined') { + return; + } + try { + if(!window.Worker){ + return; + } + webWorkerSearch(); + renderSearchBox(); + highlightKeywords(); + addSearchEvent(); + } catch (e) { + console.error(e); + } + + //Adjust the position of search box in navbar + function renderSearchBox() { + autoCollapse(); + $(window).on('resize', autoCollapse); + $(document).on('click', '.navbar-collapse.in', function (e) { + if ($(e.target).is('a')) { + $(this).collapse('hide'); + } + }); + + function autoCollapse() { + var navbar = $('#autocollapse'); + if (navbar.height() === null) { + setTimeout(autoCollapse, 300); + } + navbar.removeClass(collapsed); + if (navbar.height() > 60) { + navbar.addClass(collapsed); + } + } + } + + function webWorkerSearch() { + var indexReady = $.Deferred(); + + var worker = new Worker(relHref + 'styles/search-worker.min.js'); + worker.onerror = function (oEvent) { + console.error('Error occurred at search-worker. message: ' + oEvent.message); + } + + worker.onmessage = function (oEvent) { + switch (oEvent.data.e) { + case 'index-ready': + indexReady.resolve(); + break; + case 'query-ready': + var hits = oEvent.data.d; + handleSearchResults(hits); + break; + } + } + + indexReady.promise().done(function () { + $("body").bind("queryReady", function () { + worker.postMessage({ q: query }); + }); + if (query && (query.length >= 3)) { + worker.postMessage({ q: query }); + } + }); + } + + // Highlight the searching keywords + function highlightKeywords() { + var q = url('?q'); + if (q) { + var keywords = q.split("%20"); + keywords.forEach(function (keyword) { + if (keyword !== "") { + $('.data-searchable *').mark(keyword); + $('article *').mark(keyword); + } + }); + } + } + + function addSearchEvent() { + $('body').bind("searchEvent", function () { + $('#search-query').keypress(function (e) { + return e.which !== 13; + }); + + $('#search-query').keyup(function () { + query = $(this).val(); + if (query === '') { + flipContents("show"); + } else { + flipContents("hide"); + $("body").trigger("queryReady"); + $('#search-results>.search-list>span').text('"' + query + '"'); + } + }).off("keydown"); + }); + } + + function flipContents(action) { + if (action === "show") { + $('.hide-when-search').show(); + $('#search-results').hide(); + } else { + $('.hide-when-search').hide(); + $('#search-results').show(); + } + } + + function relativeUrlToAbsoluteUrl(currentUrl, relativeUrl) { + var currentItems = currentUrl.split(/\/+/); + var relativeItems = relativeUrl.split(/\/+/); + var depth = currentItems.length - 1; + var items = []; + for (var i = 0; i < relativeItems.length; i++) { + if (relativeItems[i] === '..') { + depth--; + } else if (relativeItems[i] !== '.') { + items.push(relativeItems[i]); + } + } + return currentItems.slice(0, depth).concat(items).join('/'); + } + + function extractContentBrief(content) { + if (!content) { + return + } + var briefOffset = 512; + var words = query.split(/\s+/g); + var queryIndex = content.indexOf(words[0]); + var briefContent; + if (queryIndex > briefOffset) { + return "..." + content.slice(queryIndex - briefOffset, queryIndex + briefOffset) + "..."; + } else if (queryIndex <= briefOffset) { + return content.slice(0, queryIndex + briefOffset) + "..."; + } + } + + function handleSearchResults(hits) { + var numPerPage = 10; + var pagination = $('#pagination'); + pagination.empty(); + pagination.removeData("twbs-pagination"); + if (hits.length === 0) { + $('#search-results>.sr-items').html('

No results found

'); + } else { + pagination.twbsPagination({ + first: pagination.data('first'), + prev: pagination.data('prev'), + next: pagination.data('next'), + last: pagination.data('last'), + totalPages: Math.ceil(hits.length / numPerPage), + visiblePages: 5, + onPageClick: function (event, page) { + var start = (page - 1) * numPerPage; + var curHits = hits.slice(start, start + numPerPage); + $('#search-results>.sr-items').empty().append( + curHits.map(function (hit) { + var currentUrl = window.location.href; + var itemRawHref = relativeUrlToAbsoluteUrl(currentUrl, relHref + hit.href); + var itemHref = relHref + hit.href + "?q=" + query; + var itemTitle = hit.title; + var itemBrief = extractContentBrief(hit.summary || ''); + + var itemNode = $('
').attr('class', 'sr-item'); + var itemTitleNode = $('
').attr('class', 'item-title').append($('').attr('href', itemHref).attr("target", "_blank").attr("rel", "noopener noreferrer").text(itemTitle)); + var itemHrefNode = $('
').attr('class', 'item-href').text(itemRawHref); + var itemBriefNode = $('
').attr('class', 'item-brief').text(itemBrief); + itemNode.append(itemTitleNode).append(itemHrefNode).append(itemBriefNode); + return itemNode; + }) + ); + query.split(/\s+/).forEach(function (word) { + if (word !== '') { + $('#search-results>.sr-items *').mark(word); + } + }); + } + }); + } + } + }; + + // Update href in navbar + function renderNavbar() { + var navbar = $('#navbar ul')[0]; + if (typeof (navbar) === 'undefined') { + loadNavbar(); + } else { + $('#navbar ul a.active').parents('li').addClass(active); + renderBreadcrumb(); + showSearch(); + } + + function showSearch() { + if ($('#search-results').length !== 0) { + $('#search').show(); + $('body').trigger("searchEvent"); + } + } + + function loadNavbar() { + var navbarPath = $("meta[property='docfx\\:navrel']").attr("content"); + if (!navbarPath) { + return; + } + navbarPath = navbarPath.replace(/\\/g, '/'); + var tocPath = $("meta[property='docfx\\:tocrel']").attr("content") || ''; + if (tocPath) tocPath = tocPath.replace(/\\/g, '/'); + $.get(navbarPath, function (data) { + $(data).find("#toc>ul").appendTo("#navbar"); + showSearch(); + var index = navbarPath.lastIndexOf('/'); + var navrel = ''; + if (index > -1) { + navrel = navbarPath.substr(0, index + 1); + } + $('#navbar>ul').addClass('navbar-nav'); + var currentAbsPath = util.getCurrentWindowAbsolutePath(); + // set active item + $('#navbar').find('a[href]').each(function (i, e) { + var href = $(e).attr("href"); + if (util.isRelativePath(href)) { + href = navrel + href; + $(e).attr("href", href); + + var isActive = false; + var originalHref = e.name; + if (originalHref) { + originalHref = navrel + originalHref; + if (util.getDirectory(util.getAbsolutePath(originalHref)) === util.getDirectory(util.getAbsolutePath(tocPath))) { + isActive = true; + } + } else { + if (util.getAbsolutePath(href) === currentAbsPath) { + var dropdown = $(e).attr('data-toggle') == "dropdown" + if (!dropdown) { + isActive = true; + } + } + } + if (isActive) { + $(e).addClass(active); + } + } + }); + renderNavbar(); + }); + } + } + + function renderSidebar() { + var sidetoc = $('#sidetoggle .sidetoc')[0]; + if (typeof (sidetoc) === 'undefined') { + loadToc(); + } else { + registerTocEvents(); + if ($('footer').is(':visible')) { + $('.sidetoc').addClass('shiftup'); + } + + // Scroll to active item + var top = 0; + $('#toc a.active').parents('li').each(function (i, e) { + $(e).addClass(active).addClass(expanded); + $(e).children('a').addClass(active); + }) + $('#toc a.active').parents('li').each(function (i, e) { + top += $(e).position().top; + }) + $('.sidetoc').scrollTop(top - 50); + + if ($('footer').is(':visible')) { + $('.sidetoc').addClass('shiftup'); + } + + renderBreadcrumb(); + } + + function registerTocEvents() { + var tocFilterInput = $('#toc_filter_input'); + var tocFilterClearButton = $('#toc_filter_clear'); + + $('.toc .nav > li > .expand-stub').click(function (e) { + $(e.target).parent().toggleClass(expanded); + }); + $('.toc .nav > li > .expand-stub + a:not([href])').click(function (e) { + $(e.target).parent().toggleClass(expanded); + }); + tocFilterInput.on('input', function (e) { + var val = this.value; + //Save filter string to local session storage + if (typeof(Storage) !== "undefined") { + try { + sessionStorage.filterString = val; + } + catch(e) + {} + } + if (val === '') { + // Clear 'filtered' class + $('#toc li').removeClass(filtered).removeClass(hide); + tocFilterClearButton.fadeOut(); + return; + } + tocFilterClearButton.fadeIn(); + + // set all parent nodes status + $('#toc li>a').filter(function (i, e) { + return $(e).siblings().length > 0 + }).each(function (i, anchor) { + var parent = $(anchor).parent(); + parent.addClass(hide); + parent.removeClass(show); + parent.removeClass(filtered); + }) + + // Get leaf nodes + $('#toc li>a').filter(function (i, e) { + return $(e).siblings().length === 0 + }).each(function (i, anchor) { + var text = $(anchor).attr('title'); + var parent = $(anchor).parent(); + var parentNodes = parent.parents('ul>li'); + for (var i = 0; i < parentNodes.length; i++) { + var parentText = $(parentNodes[i]).children('a').attr('title'); + if (parentText) text = parentText + '.' + text; + }; + if (filterNavItem(text, val)) { + parent.addClass(show); + parent.removeClass(hide); + } else { + parent.addClass(hide); + parent.removeClass(show); + } + }); + $('#toc li>a').filter(function (i, e) { + return $(e).siblings().length > 0 + }).each(function (i, anchor) { + var parent = $(anchor).parent(); + if (parent.find('li.show').length > 0) { + parent.addClass(show); + parent.addClass(filtered); + parent.removeClass(hide); + } else { + parent.addClass(hide); + parent.removeClass(show); + parent.removeClass(filtered); + } + }) + + function filterNavItem(name, text) { + if (!text) return true; + if (name && name.toLowerCase().indexOf(text.toLowerCase()) > -1) return true; + return false; + } + }); + + // toc filter clear button + tocFilterClearButton.hide(); + tocFilterClearButton.on("click", function(e){ + tocFilterInput.val(""); + tocFilterInput.trigger('input'); + if (typeof(Storage) !== "undefined") { + try { + sessionStorage.filterString = ""; + } + catch(e) + {} + } + }); + + //Set toc filter from local session storage on page load + if (typeof(Storage) !== "undefined") { + try { + tocFilterInput.val(sessionStorage.filterString); + tocFilterInput.trigger('input'); + } + catch(e) + {} + } + } + + function loadToc() { + var tocPath = $("meta[property='docfx\\:tocrel']").attr("content"); + if (!tocPath) { + return; + } + tocPath = tocPath.replace(/\\/g, '/'); + $('#sidetoc').load(tocPath + " #sidetoggle > div", function () { + var index = tocPath.lastIndexOf('/'); + var tocrel = ''; + if (index > -1) { + tocrel = tocPath.substr(0, index + 1); + } + var currentHref = util.getCurrentWindowAbsolutePath(); + if(!currentHref.endsWith('.html')) { + currentHref += '.html'; + } + $('#sidetoc').find('a[href]').each(function (i, e) { + var href = $(e).attr("href"); + if (util.isRelativePath(href)) { + href = tocrel + href; + $(e).attr("href", href); + } + + if (util.getAbsolutePath(e.href) === currentHref) { + $(e).addClass(active); + } + + $(e).breakWord(); + }); + + renderSidebar(); + }); + } + } + + function renderBreadcrumb() { + var breadcrumb = []; + $('#navbar a.active').each(function (i, e) { + breadcrumb.push({ + href: e.href, + name: e.innerHTML + }); + }) + $('#toc a.active').each(function (i, e) { + breadcrumb.push({ + href: e.href, + name: e.innerHTML + }); + }) + + var html = util.formList(breadcrumb, 'breadcrumb'); + $('#breadcrumb').html(html); + } + + //Setup Affix + function renderAffix() { + var hierarchy = getHierarchy(); + if (!hierarchy || hierarchy.length <= 0) { + $("#affix").hide(); + } + else { + var html = util.formList(hierarchy, ['nav', 'bs-docs-sidenav']); + $("#affix>div").empty().append(html); + if ($('footer').is(':visible')) { + $(".sideaffix").css("bottom", "70px"); + } + $('#affix a').click(function(e) { + var scrollspy = $('[data-spy="scroll"]').data()['bs.scrollspy']; + var target = e.target.hash; + if (scrollspy && target) { + scrollspy.activate(target); + } + }); + } + + function getHierarchy() { + // supported headers are h1, h2, h3, and h4 + var $headers = $($.map(['h1', 'h2', 'h3', 'h4'], function (h) { return ".article article " + h; }).join(", ")); + + // a stack of hierarchy items that are currently being built + var stack = []; + $headers.each(function (i, e) { + if (!e.id) { + return; + } + + var item = { + name: htmlEncode($(e).text()), + href: "#" + e.id, + items: [] + }; + + if (!stack.length) { + stack.push({ type: e.tagName, siblings: [item] }); + return; + } + + var frame = stack[stack.length - 1]; + if (e.tagName === frame.type) { + frame.siblings.push(item); + } else if (e.tagName[1] > frame.type[1]) { + // we are looking at a child of the last element of frame.siblings. + // push a frame onto the stack. After we've finished building this item's children, + // we'll attach it as a child of the last element + stack.push({ type: e.tagName, siblings: [item] }); + } else { // e.tagName[1] < frame.type[1] + // we are looking at a sibling of an ancestor of the current item. + // pop frames from the stack, building items as we go, until we reach the correct level at which to attach this item. + while (e.tagName[1] < stack[stack.length - 1].type[1]) { + buildParent(); + } + if (e.tagName === stack[stack.length - 1].type) { + stack[stack.length - 1].siblings.push(item); + } else { + stack.push({ type: e.tagName, siblings: [item] }); + } + } + }); + while (stack.length > 1) { + buildParent(); + } + + function buildParent() { + var childrenToAttach = stack.pop(); + var parentFrame = stack[stack.length - 1]; + var parent = parentFrame.siblings[parentFrame.siblings.length - 1]; + $.each(childrenToAttach.siblings, function (i, child) { + parent.items.push(child); + }); + } + if (stack.length > 0) { + + var topLevel = stack.pop().siblings; + if (topLevel.length === 1) { // if there's only one topmost header, dump it + return topLevel[0].items; + } + return topLevel; + } + return undefined; + } + + function htmlEncode(str) { + if (!str) return str; + return str + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + function htmlDecode(value) { + if (!str) return str; + return value + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&'); + } + + function cssEscape(str) { + // see: http://stackoverflow.com/questions/2786538/need-to-escape-a-special-character-in-a-jquery-selector-string#answer-2837646 + if (!str) return str; + return str + .replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, "\\$&"); + } + } + + // Show footer + function renderFooter() { + initFooter(); + $(window).on("scroll", showFooterCore); + + function initFooter() { + if (needFooter()) { + shiftUpBottomCss(); + $("footer").show(); + } else { + resetBottomCss(); + $("footer").hide(); + } + } + + function showFooterCore() { + if (needFooter()) { + shiftUpBottomCss(); + $("footer").fadeIn(); + } else { + resetBottomCss(); + $("footer").fadeOut(); + } + } + + function needFooter() { + var scrollHeight = $(document).height(); + var scrollPosition = $(window).height() + $(window).scrollTop(); + return (scrollHeight - scrollPosition) < 1; + } + + function resetBottomCss() { + $(".sidetoc").removeClass("shiftup"); + $(".sideaffix").removeClass("shiftup"); + } + + function shiftUpBottomCss() { + $(".sidetoc").addClass("shiftup"); + $(".sideaffix").addClass("shiftup"); + } + } + + function renderLogo() { + // For LOGO SVG + // Replace SVG with inline SVG + // http://stackoverflow.com/questions/11978995/how-to-change-color-of-svg-image-using-css-jquery-svg-image-replacement + jQuery('img.svg').each(function () { + var $img = jQuery(this); + var imgID = $img.attr('id'); + var imgClass = $img.attr('class'); + var imgURL = $img.attr('src'); + + jQuery.get(imgURL, function (data) { + // Get the SVG tag, ignore the rest + var $svg = jQuery(data).find('svg'); + + // Add replaced image's ID to the new SVG + if (typeof imgID !== 'undefined') { + $svg = $svg.attr('id', imgID); + } + // Add replaced image's classes to the new SVG + if (typeof imgClass !== 'undefined') { + $svg = $svg.attr('class', imgClass + ' replaced-svg'); + } + + // Remove any invalid XML tags as per http://validator.w3.org + $svg = $svg.removeAttr('xmlns:a'); + + // Replace image with new SVG + $img.replaceWith($svg); + + }, 'xml'); + }); + } + + function renderTabs() { + var contentAttrs = { + id: 'data-bi-id', + name: 'data-bi-name', + type: 'data-bi-type' + }; + + var Tab = (function () { + function Tab(li, a, section) { + this.li = li; + this.a = a; + this.section = section; + } + Object.defineProperty(Tab.prototype, "tabIds", { + get: function () { return this.a.getAttribute('data-tab').split(' '); }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Tab.prototype, "condition", { + get: function () { return this.a.getAttribute('data-condition'); }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Tab.prototype, "visible", { + get: function () { return !this.li.hasAttribute('hidden'); }, + set: function (value) { + if (value) { + this.li.removeAttribute('hidden'); + this.li.removeAttribute('aria-hidden'); + } + else { + this.li.setAttribute('hidden', 'hidden'); + this.li.setAttribute('aria-hidden', 'true'); + } + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Tab.prototype, "selected", { + get: function () { return !this.section.hasAttribute('hidden'); }, + set: function (value) { + if (value) { + this.a.setAttribute('aria-selected', 'true'); + this.a.tabIndex = 0; + this.section.removeAttribute('hidden'); + this.section.removeAttribute('aria-hidden'); + } + else { + this.a.setAttribute('aria-selected', 'false'); + this.a.tabIndex = -1; + this.section.setAttribute('hidden', 'hidden'); + this.section.setAttribute('aria-hidden', 'true'); + } + }, + enumerable: true, + configurable: true + }); + Tab.prototype.focus = function () { + this.a.focus(); + }; + return Tab; + }()); + + initTabs(document.body); + + function initTabs(container) { + var queryStringTabs = readTabsQueryStringParam(); + var elements = container.querySelectorAll('.tabGroup'); + var state = { groups: [], selectedTabs: [] }; + for (var i = 0; i < elements.length; i++) { + var group = initTabGroup(elements.item(i)); + if (!group.independent) { + updateVisibilityAndSelection(group, state); + state.groups.push(group); + } + } + container.addEventListener('click', function (event) { return handleClick(event, state); }); + if (state.groups.length === 0) { + return state; + } + selectTabs(queryStringTabs, container); + updateTabsQueryStringParam(state); + notifyContentUpdated(); + return state; + } + + function initTabGroup(element) { + var group = { + independent: element.hasAttribute('data-tab-group-independent'), + tabs: [] + }; + var li = element.firstElementChild.firstElementChild; + while (li) { + var a = li.firstElementChild; + a.setAttribute(contentAttrs.name, 'tab'); + var dataTab = a.getAttribute('data-tab').replace(/\+/g, ' '); + a.setAttribute('data-tab', dataTab); + var section = element.querySelector("[id=\"" + a.getAttribute('aria-controls') + "\"]"); + var tab = new Tab(li, a, section); + group.tabs.push(tab); + li = li.nextElementSibling; + } + element.setAttribute(contentAttrs.name, 'tab-group'); + element.tabGroup = group; + return group; + } + + function updateVisibilityAndSelection(group, state) { + var anySelected = false; + var firstVisibleTab; + for (var _i = 0, _a = group.tabs; _i < _a.length; _i++) { + var tab = _a[_i]; + tab.visible = tab.condition === null || state.selectedTabs.indexOf(tab.condition) !== -1; + if (tab.visible) { + if (!firstVisibleTab) { + firstVisibleTab = tab; + } + } + tab.selected = tab.visible && arraysIntersect(state.selectedTabs, tab.tabIds); + anySelected = anySelected || tab.selected; + } + if (!anySelected) { + for (var _b = 0, _c = group.tabs; _b < _c.length; _b++) { + var tabIds = _c[_b].tabIds; + for (var _d = 0, tabIds_1 = tabIds; _d < tabIds_1.length; _d++) { + var tabId = tabIds_1[_d]; + var index = state.selectedTabs.indexOf(tabId); + if (index === -1) { + continue; + } + state.selectedTabs.splice(index, 1); + } + } + var tab = firstVisibleTab; + tab.selected = true; + state.selectedTabs.push(tab.tabIds[0]); + } + } + + function getTabInfoFromEvent(event) { + if (!(event.target instanceof HTMLElement)) { + return null; + } + var anchor = event.target.closest('a[data-tab]'); + if (anchor === null) { + return null; + } + var tabIds = anchor.getAttribute('data-tab').split(' '); + var group = anchor.parentElement.parentElement.parentElement.tabGroup; + if (group === undefined) { + return null; + } + return { tabIds: tabIds, group: group, anchor: anchor }; + } + + function handleClick(event, state) { + var info = getTabInfoFromEvent(event); + if (info === null) { + return; + } + event.preventDefault(); + info.anchor.href = 'javascript:'; + setTimeout(function () { return info.anchor.href = '#' + info.anchor.getAttribute('aria-controls'); }); + var tabIds = info.tabIds, group = info.group; + var originalTop = info.anchor.getBoundingClientRect().top; + if (group.independent) { + for (var _i = 0, _a = group.tabs; _i < _a.length; _i++) { + var tab = _a[_i]; + tab.selected = arraysIntersect(tab.tabIds, tabIds); + } + } + else { + if (arraysIntersect(state.selectedTabs, tabIds)) { + return; + } + var previousTabId = group.tabs.filter(function (t) { return t.selected; })[0].tabIds[0]; + state.selectedTabs.splice(state.selectedTabs.indexOf(previousTabId), 1, tabIds[0]); + for (var _b = 0, _c = state.groups; _b < _c.length; _b++) { + var group_1 = _c[_b]; + updateVisibilityAndSelection(group_1, state); + } + updateTabsQueryStringParam(state); + } + notifyContentUpdated(); + var top = info.anchor.getBoundingClientRect().top; + if (top !== originalTop && event instanceof MouseEvent) { + window.scrollTo(0, window.pageYOffset + top - originalTop); + } + } + + function selectTabs(tabIds) { + for (var _i = 0, tabIds_1 = tabIds; _i < tabIds_1.length; _i++) { + var tabId = tabIds_1[_i]; + var a = document.querySelector(".tabGroup > ul > li > a[data-tab=\"" + tabId + "\"]:not([hidden])"); + if (a === null) { + return; + } + a.dispatchEvent(new CustomEvent('click', { bubbles: true })); + } + } + + function readTabsQueryStringParam() { + var qs = parseQueryString(window.location.search); + var t = qs.tabs; + if (t === undefined || t === '') { + return []; + } + return t.split(','); + } + + function updateTabsQueryStringParam(state) { + var qs = parseQueryString(window.location.search); + qs.tabs = state.selectedTabs.join(); + var url = location.protocol + "//" + location.host + location.pathname + "?" + toQueryString(qs) + location.hash; + if (location.href === url) { + return; + } + history.replaceState({}, document.title, url); + } + + function toQueryString(args) { + var parts = []; + for (var name_1 in args) { + if (args.hasOwnProperty(name_1) && args[name_1] !== '' && args[name_1] !== null && args[name_1] !== undefined) { + parts.push(encodeURIComponent(name_1) + '=' + encodeURIComponent(args[name_1])); + } + } + return parts.join('&'); + } + + function parseQueryString(queryString) { + var match; + var pl = /\+/g; + var search = /([^&=]+)=?([^&]*)/g; + var decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')); }; + if (queryString === undefined) { + queryString = ''; + } + queryString = queryString.substring(1); + var urlParams = {}; + while (match = search.exec(queryString)) { + urlParams[decode(match[1])] = decode(match[2]); + } + return urlParams; + } + + function arraysIntersect(a, b) { + for (var _i = 0, a_1 = a; _i < a_1.length; _i++) { + var itemA = a_1[_i]; + for (var _a = 0, b_1 = b; _a < b_1.length; _a++) { + var itemB = b_1[_a]; + if (itemA === itemB) { + return true; + } + } + } + return false; + } + + function notifyContentUpdated() { + // Dispatch this event when needed + // window.dispatchEvent(new CustomEvent('content-update')); + } + } + + function utility() { + this.getAbsolutePath = getAbsolutePath; + this.isRelativePath = isRelativePath; + this.isAbsolutePath = isAbsolutePath; + this.getCurrentWindowAbsolutePath = getCurrentWindowAbsolutePath; + this.getDirectory = getDirectory; + this.formList = formList; + + function getAbsolutePath(href) { + if (isAbsolutePath(href)) return href; + var currentAbsPath = getCurrentWindowAbsolutePath(); + var stack = currentAbsPath.split("/"); + stack.pop(); + var parts = href.split("/"); + for (var i=0; i< parts.length; i++) { + if (parts[i] == ".") continue; + if (parts[i] == ".." && stack.length > 0) + stack.pop(); + else + stack.push(parts[i]); + } + var p = stack.join("/"); + return p; + } + + function isRelativePath(href) { + if (href === undefined || href === '' || href[0] === '/') { + return false; + } + return !isAbsolutePath(href); + } + + function isAbsolutePath(href) { + return (/^(?:[a-z]+:)?\/\//i).test(href); + } + + function getCurrentWindowAbsolutePath() { + return window.location.origin + window.location.pathname; + } + function getDirectory(href) { + if (!href) return ''; + var index = href.lastIndexOf('/'); + if (index == -1) return ''; + if (index > -1) { + return href.substr(0, index); + } + } + + function formList(item, classes) { + var level = 1; + var model = { + items: item + }; + var cls = [].concat(classes).join(" "); + return getList(model, cls); + + function getList(model, cls) { + if (!model || !model.items) return null; + var l = model.items.length; + if (l === 0) return null; + var html = ''; + return html; + } + } + + /** + * Add into long word. + * @param {String} text - The word to break. It should be in plain text without HTML tags. + */ + function breakPlainText(text) { + if (!text) return text; + return text.replace(/([a-z])([A-Z])|(\.)(\w)/g, '$1$3$2$4') + } + + /** + * Add into long word. The jQuery element should contain no html tags. + * If the jQuery element contains tags, this function will not change the element. + */ + $.fn.breakWord = function () { + if (!this.html().match(/(<\w*)((\s\/>)|(.*<\/\w*>))/g)) { + this.html(function (index, text) { + return breakPlainText(text); + }) + } + return this; + } + } + + // adjusted from https://stackoverflow.com/a/13067009/1523776 + function workAroundFixedHeaderForAnchors() { + var HISTORY_SUPPORT = !!(history && history.pushState); + var ANCHOR_REGEX = /^#[^ ]+$/; + + function getFixedOffset() { + return $('header').first().height(); + } + + /** + * If the provided href is an anchor which resolves to an element on the + * page, scroll to it. + * @param {String} href + * @return {Boolean} - Was the href an anchor. + */ + function scrollIfAnchor(href, pushToHistory) { + var match, rect, anchorOffset; + + if (!ANCHOR_REGEX.test(href)) { + return false; + } + + match = document.getElementById(href.slice(1)); + + if (match) { + rect = match.getBoundingClientRect(); + anchorOffset = window.pageYOffset + rect.top - getFixedOffset(); + window.scrollTo(window.pageXOffset, anchorOffset); + + // Add the state to history as-per normal anchor links + if (HISTORY_SUPPORT && pushToHistory) { + history.pushState({}, document.title, location.pathname + href); + } + } + + return !!match; + } + + /** + * Attempt to scroll to the current location's hash. + */ + function scrollToCurrent() { + scrollIfAnchor(window.location.hash); + } + + /** + * If the click event's target was an anchor, fix the scroll position. + */ + function delegateAnchors(e) { + var elem = e.target; + + if (scrollIfAnchor(elem.getAttribute('href'), true)) { + e.preventDefault(); + } + } + + $(window).on('hashchange', scrollToCurrent); + + $(window).on('load', function () { + // scroll to the anchor if present, offset by the header + scrollToCurrent(); + }); + + $(document).ready(function () { + // Exclude tabbed content case + $('a:not([data-tab])').click(function (e) { delegateAnchors(e); }); + }); + } +}); diff --git a/docs/styles/docfx.vendor.css b/docs/styles/docfx.vendor.css new file mode 100644 index 00000000..a65413ad --- /dev/null +++ b/docs/styles/docfx.vendor.css @@ -0,0 +1,14 @@ +/*! + * Bootstrap v3.4.1 (https://getbootstrap.com/) + * Copyright 2011-2019 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:"Glyphicons Halflings";src:url("../fonts/glyphicons-halflings-regular.eot");src:url("../fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/glyphicons-halflings-regular.woff2") format("woff2"),url("../fonts/glyphicons-halflings-regular.woff") format("woff"),url("../fonts/glyphicons-halflings-regular.ttf") format("truetype"),url("../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*=col-]{padding-right:0;padding-left:0}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;line-height:normal}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin-top:8px;margin-bottom:8px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0%;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.in{opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;opacity:0}.tooltip.in{opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! + Theme: GitHub + Description: Light theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-light + Current colors taken from GitHub's CSS +*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0} \ No newline at end of file diff --git a/docs/styles/docfx.vendor.js b/docs/styles/docfx.vendor.js new file mode 100644 index 00000000..6ee8780a --- /dev/null +++ b/docs/styles/docfx.vendor.js @@ -0,0 +1,38 @@ +(()=>{var gp=Object.create;var rr=Object.defineProperty;var Sp=Object.getOwnPropertyDescriptor;var fp=Object.getOwnPropertyNames;var Tp=Object.getPrototypeOf,bp=Object.prototype.hasOwnProperty;var Jr=(e,n)=>()=>(e&&(n=e(e=0)),n);var I=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports),Rp=(e,n)=>{for(var i in n)rr(e,i,{get:n[i],enumerable:!0})},Di=(e,n,i,u)=>{if(n&&typeof n=="object"||typeof n=="function")for(let l of fp(n))!bp.call(e,l)&&l!==i&&rr(e,l,{get:()=>n[l],enumerable:!(u=Sp(n,l))||u.enumerable});return e};var Cp=(e,n,i)=>(i=e!=null?gp(Tp(e)):{},Di(n||!e||!e.__esModule?rr(i,"default",{value:e,enumerable:!0}):i,e)),hp=e=>Di(rr({},"__esModule",{value:!0}),e);var jr=I((Mi,ir)=>{(function(e,n){"use strict";typeof ir=="object"&&typeof ir.exports=="object"?ir.exports=e.document?n(e,!0):function(i){if(!i.document)throw new Error("jQuery requires a window with a document");return n(i)}:n(e)})(typeof window<"u"?window:Mi,function(e,n){"use strict";var i=[],u=Object.getPrototypeOf,l=i.slice,a=i.flat?function(t){return i.flat.call(t)}:function(t){return i.concat.apply([],t)},s=i.push,d=i.indexOf,p={},g=p.toString,f=p.hasOwnProperty,A=f.toString,b=A.call(Object),R={},C=function(r){return typeof r=="function"&&typeof r.nodeType!="number"},D=function(r){return r!=null&&r===r.window},x=e.document,F={type:!0,src:!0,nonce:!0,noModule:!0};function V(t,r,o){o=o||x;var _,m,E=o.createElement("script");if(E.text=t,r)for(_ in F)m=r[_]||r.getAttribute&&r.getAttribute(_),m&&E.setAttribute(_,m);o.head.appendChild(E).parentNode.removeChild(E)}function G(t){return t==null?t+"":typeof t=="object"||typeof t=="function"?p[g.call(t)]||"object":typeof t}var H="3.5.1",c=function(t,r){return new c.fn.init(t,r)};c.fn=c.prototype={jquery:H,constructor:c,length:0,toArray:function(){return l.call(this)},get:function(t){return t==null?l.call(this):t<0?this[t+this.length]:this[t]},pushStack:function(t){var r=c.merge(this.constructor(),t);return r.prevObject=this,r},each:function(t){return c.each(this,t)},map:function(t){return this.pushStack(c.map(this,function(r,o){return t.call(r,o,r)}))},slice:function(){return this.pushStack(l.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(c.grep(this,function(t,r){return(r+1)%2}))},odd:function(){return this.pushStack(c.grep(this,function(t,r){return r%2}))},eq:function(t){var r=this.length,o=+t+(t<0?r:0);return this.pushStack(o>=0&&o0&&r-1 in t}var J=function(t){var r,o,_,m,E,S,N,O,M,P,q,w,k,te,Ee,ne,Ge,Fe,st,Oe="sizzle"+1*new Date,me=t.document,et=0,Te=0,we=jn(),mn=jn(),Xn=jn(),lt=jn(),Bt=function(T,h){return T===h&&(q=!0),0},Gt={}.hasOwnProperty,tt=[],Lt=tt.pop,dt=tt.push,xt=tt.push,Ri=tt.slice,Yt=function(T,h){for(var y=0,U=T.length;y+~]|"+Ne+")"+Ne+"*"),ip=new RegExp(Ne+"|>"),ap=new RegExp(Wr),op=new RegExp("^"+Ht+"$"),Jn={ID:new RegExp("^#("+Ht+")"),CLASS:new RegExp("^\\.("+Ht+")"),TAG:new RegExp("^("+Ht+"|[*])"),ATTR:new RegExp("^"+Ci),PSEUDO:new RegExp("^"+Wr),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+Ne+"*(even|odd|(([+-]|)(\\d*)n|)"+Ne+"*(?:([+-]|)"+Ne+"*(\\d+)|))"+Ne+"*\\)|)","i"),bool:new RegExp("^(?:"+Vr+")$","i"),needsContext:new RegExp("^"+Ne+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+Ne+"*((?:-\\d)?\\d*)"+Ne+"*\\)|)(?=[^-]|$)","i")},sp=/HTML$/i,lp=/^(?:input|select|textarea|button)$/i,cp=/^h\d$/i,En=/^[^{]+\{\s*\[native \w/,_p=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,zr=/[+~]/,ht=new RegExp("\\\\[\\da-fA-F]{1,6}"+Ne+"?|\\\\([^\\r\\n\\f])","g"),Nt=function(T,h){var y="0x"+T.slice(1)-65536;return h||(y<0?String.fromCharCode(y+65536):String.fromCharCode(y>>10|55296,y&1023|56320))},Ni=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,Oi=function(T,h){return h?T==="\0"?"\uFFFD":T.slice(0,-1)+"\\"+T.charCodeAt(T.length-1).toString(16)+" ":"\\"+T},Ai=function(){w()},dp=tr(function(T){return T.disabled===!0&&T.nodeName.toLowerCase()==="fieldset"},{dir:"parentNode",next:"legend"});try{xt.apply(tt=Ri.call(me.childNodes),me.childNodes),tt[me.childNodes.length].nodeType}catch{xt={apply:tt.length?function(h,y){dt.apply(h,Ri.call(y))}:function(h,y){for(var U=h.length,L=0;h[U++]=y[L++];);h.length=U-1}}}function Ie(T,h,y,U){var L,B,Y,$,Q,se,oe,ce=h&&h.ownerDocument,Se=h?h.nodeType:9;if(y=y||[],typeof T!="string"||!T||Se!==1&&Se!==9&&Se!==11)return y;if(!U&&(w(h),h=h||k,Ee)){if(Se!==11&&(Q=_p.exec(T)))if(L=Q[1]){if(Se===9)if(Y=h.getElementById(L)){if(Y.id===L)return y.push(Y),y}else return y;else if(ce&&(Y=ce.getElementById(L))&&st(h,Y)&&Y.id===L)return y.push(Y),y}else{if(Q[2])return xt.apply(y,h.getElementsByTagName(T)),y;if((L=Q[3])&&o.getElementsByClassName&&h.getElementsByClassName)return xt.apply(y,h.getElementsByClassName(L)),y}if(o.qsa&&!lt[T+" "]&&(!ne||!ne.test(T))&&(Se!==1||h.nodeName.toLowerCase()!=="object")){if(oe=T,ce=h,Se===1&&(ip.test(T)||hi.test(T))){for(ce=zr.test(T)&&Kr(h.parentNode)||h,(ce!==h||!o.scope)&&(($=h.getAttribute("id"))?$=$.replace(Ni,Oi):h.setAttribute("id",$=Oe)),se=S(T),B=se.length;B--;)se[B]=($?"#"+$:":scope")+" "+er(se[B]);oe=se.join(",")}try{return xt.apply(y,ce.querySelectorAll(oe)),y}catch{lt(T,!0)}finally{$===Oe&&h.removeAttribute("id")}}}return O(T.replace(Zn,"$1"),h,y,U)}function jn(){var T=[];function h(y,U){return T.push(y+" ")>_.cacheLength&&delete h[T.shift()],h[y+" "]=U}return h}function ut(T){return T[Oe]=!0,T}function pt(T){var h=k.createElement("fieldset");try{return!!T(h)}catch{return!1}finally{h.parentNode&&h.parentNode.removeChild(h),h=null}}function $r(T,h){for(var y=T.split("|"),U=y.length;U--;)_.attrHandle[y[U]]=h}function Ii(T,h){var y=h&&T,U=y&&T.nodeType===1&&h.nodeType===1&&T.sourceIndex-h.sourceIndex;if(U)return U;if(y){for(;y=y.nextSibling;)if(y===h)return-1}return T?1:-1}function up(T){return function(h){var y=h.nodeName.toLowerCase();return y==="input"&&h.type===T}}function pp(T){return function(h){var y=h.nodeName.toLowerCase();return(y==="input"||y==="button")&&h.type===T}}function vi(T){return function(h){return"form"in h?h.parentNode&&h.disabled===!1?"label"in h?"label"in h.parentNode?h.parentNode.disabled===T:h.disabled===T:h.isDisabled===T||h.isDisabled!==!T&&dp(h)===T:h.disabled===T:"label"in h?h.disabled===T:!1}}function qt(T){return ut(function(h){return h=+h,ut(function(y,U){for(var L,B=T([],y.length,h),Y=B.length;Y--;)y[L=B[Y]]&&(y[L]=!(U[L]=y[L]))})})}function Kr(T){return T&&typeof T.getElementsByTagName<"u"&&T}o=Ie.support={},E=Ie.isXML=function(T){var h=T.namespaceURI,y=(T.ownerDocument||T).documentElement;return!sp.test(h||y&&y.nodeName||"HTML")},w=Ie.setDocument=function(T){var h,y,U=T?T.ownerDocument||T:me;return U==k||U.nodeType!==9||!U.documentElement||(k=U,te=k.documentElement,Ee=!E(k),me!=k&&(y=k.defaultView)&&y.top!==y&&(y.addEventListener?y.addEventListener("unload",Ai,!1):y.attachEvent&&y.attachEvent("onunload",Ai)),o.scope=pt(function(L){return te.appendChild(L).appendChild(k.createElement("div")),typeof L.querySelectorAll<"u"&&!L.querySelectorAll(":scope fieldset div").length}),o.attributes=pt(function(L){return L.className="i",!L.getAttribute("className")}),o.getElementsByTagName=pt(function(L){return L.appendChild(k.createComment("")),!L.getElementsByTagName("*").length}),o.getElementsByClassName=En.test(k.getElementsByClassName),o.getById=pt(function(L){return te.appendChild(L).id=Oe,!k.getElementsByName||!k.getElementsByName(Oe).length}),o.getById?(_.filter.ID=function(L){var B=L.replace(ht,Nt);return function(Y){return Y.getAttribute("id")===B}},_.find.ID=function(L,B){if(typeof B.getElementById<"u"&&Ee){var Y=B.getElementById(L);return Y?[Y]:[]}}):(_.filter.ID=function(L){var B=L.replace(ht,Nt);return function(Y){var $=typeof Y.getAttributeNode<"u"&&Y.getAttributeNode("id");return $&&$.value===B}},_.find.ID=function(L,B){if(typeof B.getElementById<"u"&&Ee){var Y,$,Q,se=B.getElementById(L);if(se){if(Y=se.getAttributeNode("id"),Y&&Y.value===L)return[se];for(Q=B.getElementsByName(L),$=0;se=Q[$++];)if(Y=se.getAttributeNode("id"),Y&&Y.value===L)return[se]}return[]}}),_.find.TAG=o.getElementsByTagName?function(L,B){if(typeof B.getElementsByTagName<"u")return B.getElementsByTagName(L);if(o.qsa)return B.querySelectorAll(L)}:function(L,B){var Y,$=[],Q=0,se=B.getElementsByTagName(L);if(L==="*"){for(;Y=se[Q++];)Y.nodeType===1&&$.push(Y);return $}return se},_.find.CLASS=o.getElementsByClassName&&function(L,B){if(typeof B.getElementsByClassName<"u"&&Ee)return B.getElementsByClassName(L)},Ge=[],ne=[],(o.qsa=En.test(k.querySelectorAll))&&(pt(function(L){var B;te.appendChild(L).innerHTML="",L.querySelectorAll("[msallowcapture^='']").length&&ne.push("[*^$]="+Ne+`*(?:''|"")`),L.querySelectorAll("[selected]").length||ne.push("\\["+Ne+"*(?:value|"+Vr+")"),L.querySelectorAll("[id~="+Oe+"-]").length||ne.push("~="),B=k.createElement("input"),B.setAttribute("name",""),L.appendChild(B),L.querySelectorAll("[name='']").length||ne.push("\\["+Ne+"*name"+Ne+"*="+Ne+`*(?:''|"")`),L.querySelectorAll(":checked").length||ne.push(":checked"),L.querySelectorAll("a#"+Oe+"+*").length||ne.push(".#.+[+~]"),L.querySelectorAll("\\\f"),ne.push("[\\r\\n\\f]")}),pt(function(L){L.innerHTML="";var B=k.createElement("input");B.setAttribute("type","hidden"),L.appendChild(B).setAttribute("name","D"),L.querySelectorAll("[name=d]").length&&ne.push("name"+Ne+"*[*^$|!~]?="),L.querySelectorAll(":enabled").length!==2&&ne.push(":enabled",":disabled"),te.appendChild(L).disabled=!0,L.querySelectorAll(":disabled").length!==2&&ne.push(":enabled",":disabled"),L.querySelectorAll("*,:x"),ne.push(",.*:")})),(o.matchesSelector=En.test(Fe=te.matches||te.webkitMatchesSelector||te.mozMatchesSelector||te.oMatchesSelector||te.msMatchesSelector))&&pt(function(L){o.disconnectedMatch=Fe.call(L,"*"),Fe.call(L,"[s!='']:x"),Ge.push("!=",Wr)}),ne=ne.length&&new RegExp(ne.join("|")),Ge=Ge.length&&new RegExp(Ge.join("|")),h=En.test(te.compareDocumentPosition),st=h||En.test(te.contains)?function(L,B){var Y=L.nodeType===9?L.documentElement:L,$=B&&B.parentNode;return L===$||!!($&&$.nodeType===1&&(Y.contains?Y.contains($):L.compareDocumentPosition&&L.compareDocumentPosition($)&16))}:function(L,B){if(B){for(;B=B.parentNode;)if(B===L)return!0}return!1},Bt=h?function(L,B){if(L===B)return q=!0,0;var Y=!L.compareDocumentPosition-!B.compareDocumentPosition;return Y||(Y=(L.ownerDocument||L)==(B.ownerDocument||B)?L.compareDocumentPosition(B):1,Y&1||!o.sortDetached&&B.compareDocumentPosition(L)===Y?L==k||L.ownerDocument==me&&st(me,L)?-1:B==k||B.ownerDocument==me&&st(me,B)?1:P?Yt(P,L)-Yt(P,B):0:Y&4?-1:1)}:function(L,B){if(L===B)return q=!0,0;var Y,$=0,Q=L.parentNode,se=B.parentNode,oe=[L],ce=[B];if(!Q||!se)return L==k?-1:B==k?1:Q?-1:se?1:P?Yt(P,L)-Yt(P,B):0;if(Q===se)return Ii(L,B);for(Y=L;Y=Y.parentNode;)oe.unshift(Y);for(Y=B;Y=Y.parentNode;)ce.unshift(Y);for(;oe[$]===ce[$];)$++;return $?Ii(oe[$],ce[$]):oe[$]==me?-1:ce[$]==me?1:0}),k},Ie.matches=function(T,h){return Ie(T,null,null,h)},Ie.matchesSelector=function(T,h){if(w(T),o.matchesSelector&&Ee&&!lt[h+" "]&&(!Ge||!Ge.test(h))&&(!ne||!ne.test(h)))try{var y=Fe.call(T,h);if(y||o.disconnectedMatch||T.document&&T.document.nodeType!==11)return y}catch{lt(h,!0)}return Ie(h,k,null,[T]).length>0},Ie.contains=function(T,h){return(T.ownerDocument||T)!=k&&w(T),st(T,h)},Ie.attr=function(T,h){(T.ownerDocument||T)!=k&&w(T);var y=_.attrHandle[h.toLowerCase()],U=y&&Gt.call(_.attrHandle,h.toLowerCase())?y(T,h,!Ee):void 0;return U!==void 0?U:o.attributes||!Ee?T.getAttribute(h):(U=T.getAttributeNode(h))&&U.specified?U.value:null},Ie.escape=function(T){return(T+"").replace(Ni,Oi)},Ie.error=function(T){throw new Error("Syntax error, unrecognized expression: "+T)},Ie.uniqueSort=function(T){var h,y=[],U=0,L=0;if(q=!o.detectDuplicates,P=!o.sortStable&&T.slice(0),T.sort(Bt),q){for(;h=T[L++];)h===T[L]&&(U=y.push(L));for(;U--;)T.splice(y[U],1)}return P=null,T},m=Ie.getText=function(T){var h,y="",U=0,L=T.nodeType;if(L){if(L===1||L===9||L===11){if(typeof T.textContent=="string")return T.textContent;for(T=T.firstChild;T;T=T.nextSibling)y+=m(T)}else if(L===3||L===4)return T.nodeValue}else for(;h=T[U++];)y+=m(h);return y},_=Ie.selectors={cacheLength:50,createPseudo:ut,match:Jn,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(T){return T[1]=T[1].replace(ht,Nt),T[3]=(T[3]||T[4]||T[5]||"").replace(ht,Nt),T[2]==="~="&&(T[3]=" "+T[3]+" "),T.slice(0,4)},CHILD:function(T){return T[1]=T[1].toLowerCase(),T[1].slice(0,3)==="nth"?(T[3]||Ie.error(T[0]),T[4]=+(T[4]?T[5]+(T[6]||1):2*(T[3]==="even"||T[3]==="odd")),T[5]=+(T[7]+T[8]||T[3]==="odd")):T[3]&&Ie.error(T[0]),T},PSEUDO:function(T){var h,y=!T[6]&&T[2];return Jn.CHILD.test(T[0])?null:(T[3]?T[2]=T[4]||T[5]||"":y&&ap.test(y)&&(h=S(y,!0))&&(h=y.indexOf(")",y.length-h)-y.length)&&(T[0]=T[0].slice(0,h),T[2]=y.slice(0,h)),T.slice(0,3))}},filter:{TAG:function(T){var h=T.replace(ht,Nt).toLowerCase();return T==="*"?function(){return!0}:function(y){return y.nodeName&&y.nodeName.toLowerCase()===h}},CLASS:function(T){var h=we[T+" "];return h||(h=new RegExp("(^|"+Ne+")"+T+"("+Ne+"|$)"))&&we(T,function(y){return h.test(typeof y.className=="string"&&y.className||typeof y.getAttribute<"u"&&y.getAttribute("class")||"")})},ATTR:function(T,h,y){return function(U){var L=Ie.attr(U,T);return L==null?h==="!=":h?(L+="",h==="="?L===y:h==="!="?L!==y:h==="^="?y&&L.indexOf(y)===0:h==="*="?y&&L.indexOf(y)>-1:h==="$="?y&&L.slice(-y.length)===y:h==="~="?(" "+L.replace(np," ")+" ").indexOf(y)>-1:h==="|="?L===y||L.slice(0,y.length+1)===y+"-":!1):!0}},CHILD:function(T,h,y,U,L){var B=T.slice(0,3)!=="nth",Y=T.slice(-4)!=="last",$=h==="of-type";return U===1&&L===0?function(Q){return!!Q.parentNode}:function(Q,se,oe){var ce,Se,ve,le,Ye,Qe,ct=B!==Y?"nextSibling":"previousSibling",Le=Q.parentNode,gn=$&&Q.nodeName.toLowerCase(),Sn=!oe&&!$,_t=!1;if(Le){if(B){for(;ct;){for(le=Q;le=le[ct];)if($?le.nodeName.toLowerCase()===gn:le.nodeType===1)return!1;Qe=ct=T==="only"&&!Qe&&"nextSibling"}return!0}if(Qe=[Y?Le.firstChild:Le.lastChild],Y&&Sn){for(le=Le,ve=le[Oe]||(le[Oe]={}),Se=ve[le.uniqueID]||(ve[le.uniqueID]={}),ce=Se[T]||[],Ye=ce[0]===et&&ce[1],_t=Ye&&ce[2],le=Ye&&Le.childNodes[Ye];le=++Ye&&le&&le[ct]||(_t=Ye=0)||Qe.pop();)if(le.nodeType===1&&++_t&&le===Q){Se[T]=[et,Ye,_t];break}}else if(Sn&&(le=Q,ve=le[Oe]||(le[Oe]={}),Se=ve[le.uniqueID]||(ve[le.uniqueID]={}),ce=Se[T]||[],Ye=ce[0]===et&&ce[1],_t=Ye),_t===!1)for(;(le=++Ye&&le&&le[ct]||(_t=Ye=0)||Qe.pop())&&!(($?le.nodeName.toLowerCase()===gn:le.nodeType===1)&&++_t&&(Sn&&(ve=le[Oe]||(le[Oe]={}),Se=ve[le.uniqueID]||(ve[le.uniqueID]={}),Se[T]=[et,_t]),le===Q)););return _t-=L,_t===U||_t%U===0&&_t/U>=0}}},PSEUDO:function(T,h){var y,U=_.pseudos[T]||_.setFilters[T.toLowerCase()]||Ie.error("unsupported pseudo: "+T);return U[Oe]?U(h):U.length>1?(y=[T,T,"",h],_.setFilters.hasOwnProperty(T.toLowerCase())?ut(function(L,B){for(var Y,$=U(L,h),Q=$.length;Q--;)Y=Yt(L,$[Q]),L[Y]=!(B[Y]=$[Q])}):function(L){return U(L,0,y)}):U}},pseudos:{not:ut(function(T){var h=[],y=[],U=N(T.replace(Zn,"$1"));return U[Oe]?ut(function(L,B,Y,$){for(var Q,se=U(L,null,$,[]),oe=L.length;oe--;)(Q=se[oe])&&(L[oe]=!(B[oe]=Q))}):function(L,B,Y){return h[0]=L,U(h,null,Y,y),h[0]=null,!y.pop()}}),has:ut(function(T){return function(h){return Ie(T,h).length>0}}),contains:ut(function(T){return T=T.replace(ht,Nt),function(h){return(h.textContent||m(h)).indexOf(T)>-1}}),lang:ut(function(T){return op.test(T||"")||Ie.error("unsupported lang: "+T),T=T.replace(ht,Nt).toLowerCase(),function(h){var y;do if(y=Ee?h.lang:h.getAttribute("xml:lang")||h.getAttribute("lang"))return y=y.toLowerCase(),y===T||y.indexOf(T+"-")===0;while((h=h.parentNode)&&h.nodeType===1);return!1}}),target:function(T){var h=t.location&&t.location.hash;return h&&h.slice(1)===T.id},root:function(T){return T===te},focus:function(T){return T===k.activeElement&&(!k.hasFocus||k.hasFocus())&&!!(T.type||T.href||~T.tabIndex)},enabled:vi(!1),disabled:vi(!0),checked:function(T){var h=T.nodeName.toLowerCase();return h==="input"&&!!T.checked||h==="option"&&!!T.selected},selected:function(T){return T.parentNode&&T.parentNode.selectedIndex,T.selected===!0},empty:function(T){for(T=T.firstChild;T;T=T.nextSibling)if(T.nodeType<6)return!1;return!0},parent:function(T){return!_.pseudos.empty(T)},header:function(T){return cp.test(T.nodeName)},input:function(T){return lp.test(T.nodeName)},button:function(T){var h=T.nodeName.toLowerCase();return h==="input"&&T.type==="button"||h==="button"},text:function(T){var h;return T.nodeName.toLowerCase()==="input"&&T.type==="text"&&((h=T.getAttribute("type"))==null||h.toLowerCase()==="text")},first:qt(function(){return[0]}),last:qt(function(T,h){return[h-1]}),eq:qt(function(T,h,y){return[y<0?y+h:y]}),even:qt(function(T,h){for(var y=0;yh?h:y;--U>=0;)T.push(U);return T}),gt:qt(function(T,h,y){for(var U=y<0?y+h:y;++U1?function(h,y,U){for(var L=T.length;L--;)if(!T[L](h,y,U))return!1;return!0}:T[0]}function mp(T,h,y){for(var U=0,L=h.length;U-1&&(Y[oe]=!($[oe]=Se))}}else Le=nr(Le===$?Le.splice(Ye,Le.length):Le),L?L(null,$,Le,se):xt.apply($,Le)})}function Zr(T){for(var h,y,U,L=T.length,B=_.relative[T[0].type],Y=B||_.relative[" "],$=B?1:0,Q=tr(function(ce){return ce===h},Y,!0),se=tr(function(ce){return Yt(h,ce)>-1},Y,!0),oe=[function(ce,Se,ve){var le=!B&&(ve||Se!==M)||((h=Se).nodeType?Q(ce,Se,ve):se(ce,Se,ve));return h=null,le}];$1&&Qr(oe),$>1&&er(T.slice(0,$-1).concat({value:T[$-2].type===" "?"*":""})).replace(Zn,"$1"),y,$0,U=T.length>0,L=function(B,Y,$,Q,se){var oe,ce,Se,ve=0,le="0",Ye=B&&[],Qe=[],ct=M,Le=B||U&&_.find.TAG("*",se),gn=et+=ct==null?1:Math.random()||.1,Sn=Le.length;for(se&&(M=Y==k||Y||se);le!==Sn&&(oe=Le[le])!=null;le++){if(U&&oe){for(ce=0,!Y&&oe.ownerDocument!=k&&(w(oe),$=!Ee);Se=T[ce++];)if(Se(oe,Y||k,$)){Q.push(oe);break}se&&(et=gn)}y&&((oe=!Se&&oe)&&ve--,B&&Ye.push(oe))}if(ve+=le,y&&le!==ve){for(ce=0;Se=h[ce++];)Se(Ye,Qe,Y,$);if(B){if(ve>0)for(;le--;)Ye[le]||Qe[le]||(Qe[le]=Lt.call(Q));Qe=nr(Qe)}xt.apply(Q,Qe),se&&!B&&Qe.length>0&&ve+h.length>1&&Ie.uniqueSort(Q)}return se&&(et=gn,M=ct),Ye};return y?ut(L):L}return N=Ie.compile=function(T,h){var y,U=[],L=[],B=Xn[T+" "];if(!B){for(h||(h=S(T)),y=h.length;y--;)B=Zr(h[y]),B[Oe]?U.push(B):L.push(B);B=Xn(T,Ep(L,U)),B.selector=T}return B},O=Ie.select=function(T,h,y,U){var L,B,Y,$,Q,se=typeof T=="function"&&T,oe=!U&&S(T=se.selector||T);if(y=y||[],oe.length===1){if(B=oe[0]=oe[0].slice(0),B.length>2&&(Y=B[0]).type==="ID"&&h.nodeType===9&&Ee&&_.relative[B[1].type]){if(h=(_.find.ID(Y.matches[0].replace(ht,Nt),h)||[])[0],h)se&&(h=h.parentNode);else return y;T=T.slice(B.shift().value.length)}for(L=Jn.needsContext.test(T)?0:B.length;L--&&(Y=B[L],!_.relative[$=Y.type]);)if((Q=_.find[$])&&(U=Q(Y.matches[0].replace(ht,Nt),zr.test(B[0].type)&&Kr(h.parentNode)||h))){if(B.splice(L,1),T=U.length&&er(B),!T)return xt.apply(y,U),y;break}}return(se||N(T,oe))(U,h,!Ee,y,!h||zr.test(T)&&Kr(h.parentNode)||h),y},o.sortStable=Oe.split("").sort(Bt).join("")===Oe,o.detectDuplicates=!!q,w(),o.sortDetached=pt(function(T){return T.compareDocumentPosition(k.createElement("fieldset"))&1}),pt(function(T){return T.innerHTML="",T.firstChild.getAttribute("href")==="#"})||$r("type|href|height|width",function(T,h,y){if(!y)return T.getAttribute(h,h.toLowerCase()==="type"?1:2)}),(!o.attributes||!pt(function(T){return T.innerHTML="",T.firstChild.setAttribute("value",""),T.firstChild.getAttribute("value")===""}))&&$r("value",function(T,h,y){if(!y&&T.nodeName.toLowerCase()==="input")return T.defaultValue}),pt(function(T){return T.getAttribute("disabled")==null})||$r(Vr,function(T,h,y){var U;if(!y)return T[h]===!0?h.toLowerCase():(U=T.getAttributeNode(h))&&U.specified?U.value:null}),Ie}(e);c.find=J,c.expr=J.selectors,c.expr[":"]=c.expr.pseudos,c.uniqueSort=c.unique=J.uniqueSort,c.text=J.getText,c.isXMLDoc=J.isXML,c.contains=J.contains,c.escapeSelector=J.escape;var re=function(t,r,o){for(var _=[],m=o!==void 0;(t=t[r])&&t.nodeType!==9;)if(t.nodeType===1){if(m&&c(t).is(o))break;_.push(t)}return _},ue=function(t,r){for(var o=[];t;t=t.nextSibling)t.nodeType===1&&t!==r&&o.push(t);return o},ee=c.expr.match.needsContext;function ge(t,r){return t.nodeName&&t.nodeName.toLowerCase()===r.toLowerCase()}var xe=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function Ae(t,r,o){return C(r)?c.grep(t,function(_,m){return!!r.call(_,m,_)!==o}):r.nodeType?c.grep(t,function(_){return _===r!==o}):typeof r!="string"?c.grep(t,function(_){return d.call(r,_)>-1!==o}):c.filter(r,t,o)}c.filter=function(t,r,o){var _=r[0];return o&&(t=":not("+t+")"),r.length===1&&_.nodeType===1?c.find.matchesSelector(_,t)?[_]:[]:c.find.matches(t,c.grep(r,function(m){return m.nodeType===1}))},c.fn.extend({find:function(t){var r,o,_=this.length,m=this;if(typeof t!="string")return this.pushStack(c(t).filter(function(){for(r=0;r<_;r++)if(c.contains(m[r],this))return!0}));for(o=this.pushStack([]),r=0;r<_;r++)c.find(t,m[r],o);return _>1?c.uniqueSort(o):o},filter:function(t){return this.pushStack(Ae(this,t||[],!1))},not:function(t){return this.pushStack(Ae(this,t||[],!0))},is:function(t){return!!Ae(this,typeof t=="string"&&ee.test(t)?c(t):t||[],!1).length}});var ye,qe=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,W=c.fn.init=function(t,r,o){var _,m;if(!t)return this;if(o=o||ye,typeof t=="string")if(t[0]==="<"&&t[t.length-1]===">"&&t.length>=3?_=[null,t,null]:_=qe.exec(t),_&&(_[1]||!r))if(_[1]){if(r=r instanceof c?r[0]:r,c.merge(this,c.parseHTML(_[1],r&&r.nodeType?r.ownerDocument||r:x,!0)),xe.test(_[1])&&c.isPlainObject(r))for(_ in r)C(this[_])?this[_](r[_]):this.attr(_,r[_]);return this}else return m=x.getElementById(_[2]),m&&(this[0]=m,this.length=1),this;else return!r||r.jquery?(r||o).find(t):this.constructor(r).find(t);else{if(t.nodeType)return this[0]=t,this.length=1,this;if(C(t))return o.ready!==void 0?o.ready(t):t(c)}return c.makeArray(t,this)};W.prototype=c.fn,ye=c(x);var z=/^(?:parents|prev(?:Until|All))/,Z={children:!0,contents:!0,next:!0,prev:!0};c.fn.extend({has:function(t){var r=c(t,this),o=r.length;return this.filter(function(){for(var _=0;_-1:o.nodeType===1&&c.find.matchesSelector(o,t))){E.push(o);break}}return this.pushStack(E.length>1?c.uniqueSort(E):E)},index:function(t){return t?typeof t=="string"?d.call(c(t),this[0]):d.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,r){return this.pushStack(c.uniqueSort(c.merge(this.get(),c(t,r))))},addBack:function(t){return this.add(t==null?this.prevObject:this.prevObject.filter(t))}});function ae(t,r){for(;(t=t[r])&&t.nodeType!==1;);return t}c.each({parent:function(t){var r=t.parentNode;return r&&r.nodeType!==11?r:null},parents:function(t){return re(t,"parentNode")},parentsUntil:function(t,r,o){return re(t,"parentNode",o)},next:function(t){return ae(t,"nextSibling")},prev:function(t){return ae(t,"previousSibling")},nextAll:function(t){return re(t,"nextSibling")},prevAll:function(t){return re(t,"previousSibling")},nextUntil:function(t,r,o){return re(t,"nextSibling",o)},prevUntil:function(t,r,o){return re(t,"previousSibling",o)},siblings:function(t){return ue((t.parentNode||{}).firstChild,t)},children:function(t){return ue(t.firstChild)},contents:function(t){return t.contentDocument!=null&&u(t.contentDocument)?t.contentDocument:(ge(t,"template")&&(t=t.content||t),c.merge([],t.childNodes))}},function(t,r){c.fn[t]=function(o,_){var m=c.map(this,r,o);return t.slice(-5)!=="Until"&&(_=o),_&&typeof _=="string"&&(m=c.filter(_,m)),this.length>1&&(Z[t]||c.uniqueSort(m),z.test(t)&&m.reverse()),this.pushStack(m)}});var _e=/[^\x20\t\r\n\f]+/g;function Ce(t){var r={};return c.each(t.match(_e)||[],function(o,_){r[_]=!0}),r}c.Callbacks=function(t){t=typeof t=="string"?Ce(t):c.extend({},t);var r,o,_,m,E=[],S=[],N=-1,O=function(){for(m=m||t.once,_=r=!0;S.length;N=-1)for(o=S.shift();++N-1;)E.splice(w,1),w<=N&&N--}),this},has:function(P){return P?c.inArray(P,E)>-1:E.length>0},empty:function(){return E&&(E=[]),this},disable:function(){return m=S=[],E=o="",this},disabled:function(){return!E},lock:function(){return m=S=[],!o&&!r&&(E=o=""),this},locked:function(){return!!m},fireWith:function(P,q){return m||(q=q||[],q=[P,q.slice?q.slice():q],S.push(q),r||O()),this},fire:function(){return M.fireWith(this,arguments),this},fired:function(){return!!_}};return M};function be(t){return t}function He(t){throw t}function De(t,r,o,_){var m;try{t&&C(m=t.promise)?m.call(t).done(r).fail(o):t&&C(m=t.then)?m.call(t,r,o):r.apply(void 0,[t].slice(_))}catch(E){o.apply(void 0,[E])}}c.extend({Deferred:function(t){var r=[["notify","progress",c.Callbacks("memory"),c.Callbacks("memory"),2],["resolve","done",c.Callbacks("once memory"),c.Callbacks("once memory"),0,"resolved"],["reject","fail",c.Callbacks("once memory"),c.Callbacks("once memory"),1,"rejected"]],o="pending",_={state:function(){return o},always:function(){return m.done(arguments).fail(arguments),this},catch:function(E){return _.then(null,E)},pipe:function(){var E=arguments;return c.Deferred(function(S){c.each(r,function(N,O){var M=C(E[O[4]])&&E[O[4]];m[O[1]](function(){var P=M&&M.apply(this,arguments);P&&C(P.promise)?P.promise().progress(S.notify).done(S.resolve).fail(S.reject):S[O[0]+"With"](this,M?[P]:arguments)})}),E=null}).promise()},then:function(E,S,N){var O=0;function M(P,q,w,k){return function(){var te=this,Ee=arguments,ne=function(){var Fe,st;if(!(P=O&&(w!==He&&(te=void 0,Ee=[Fe]),q.rejectWith(te,Ee))}};P?Ge():(c.Deferred.getStackHook&&(Ge.stackTrace=c.Deferred.getStackHook()),e.setTimeout(Ge))}}return c.Deferred(function(P){r[0][3].add(M(0,P,C(N)?N:be,P.notifyWith)),r[1][3].add(M(0,P,C(E)?E:be)),r[2][3].add(M(0,P,C(S)?S:He))}).promise()},promise:function(E){return E!=null?c.extend(E,_):_}},m={};return c.each(r,function(E,S){var N=S[2],O=S[5];_[S[1]]=N.add,O&&N.add(function(){o=O},r[3-E][2].disable,r[3-E][3].disable,r[0][2].lock,r[0][3].lock),N.add(S[3].fire),m[S[0]]=function(){return m[S[0]+"With"](this===m?void 0:this,arguments),this},m[S[0]+"With"]=N.fireWith}),_.promise(m),t&&t.call(m,m),m},when:function(t){var r=arguments.length,o=r,_=Array(o),m=l.call(arguments),E=c.Deferred(),S=function(N){return function(O){_[N]=this,m[N]=arguments.length>1?l.call(arguments):O,--r||E.resolveWith(_,m)}};if(r<=1&&(De(t,E.done(S(o)).resolve,E.reject,!r),E.state()==="pending"||C(m[o]&&m[o].then)))return E.then();for(;o--;)De(m[o],S(o),E.reject);return E.promise()}});var Ve=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;c.Deferred.exceptionHook=function(t,r){e.console&&e.console.warn&&t&&Ve.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,r)},c.readyException=function(t){e.setTimeout(function(){throw t})};var rt=c.Deferred();c.fn.ready=function(t){return rt.then(t).catch(function(r){c.readyException(r)}),this},c.extend({isReady:!1,readyWait:1,ready:function(t){(t===!0?--c.readyWait:c.isReady)||(c.isReady=!0,!(t!==!0&&--c.readyWait>0)&&rt.resolveWith(x,[c]))}}),c.ready.then=rt.then;function Xe(){x.removeEventListener("DOMContentLoaded",Xe),e.removeEventListener("load",Xe),c.ready()}x.readyState==="complete"||x.readyState!=="loading"&&!x.documentElement.doScroll?e.setTimeout(c.ready):(x.addEventListener("DOMContentLoaded",Xe),e.addEventListener("load",Xe));var We=function(t,r,o,_,m,E,S){var N=0,O=t.length,M=o==null;if(G(o)==="object"){m=!0;for(N in o)We(t,r,N,o[N],!0,E,S)}else if(_!==void 0&&(m=!0,C(_)||(S=!0),M&&(S?(r.call(t,_),r=null):(M=r,r=function(P,q,w){return M.call(c(P),w)})),r))for(;N1,null,!0)},removeData:function(t){return this.each(function(){Ue.remove(this,t)})}}),c.extend({queue:function(t,r,o){var _;if(t)return r=(r||"fx")+"queue",_=ie.get(t,r),o&&(!_||Array.isArray(o)?_=ie.access(t,r,c.makeArray(o)):_.push(o)),_||[]},dequeue:function(t,r){r=r||"fx";var o=c.queue(t,r),_=o.length,m=o.shift(),E=c._queueHooks(t,r),S=function(){c.dequeue(t,r)};m==="inprogress"&&(m=o.shift(),_--),m&&(r==="fx"&&o.unshift("inprogress"),delete E.stop,m.call(t,S,E)),!_&&E&&E.empty.fire()},_queueHooks:function(t,r){var o=r+"queueHooks";return ie.get(t,o)||ie.access(t,o,{empty:c.Callbacks("once memory").add(function(){ie.remove(t,[r+"queue",o])})})}}),c.fn.extend({queue:function(t,r){var o=2;return typeof t!="string"&&(r=t,t="fx",o--),arguments.length\x20\t\r\n\f]*)/i,Nn=/^$|^module$|\/(?:java|ecma)script/i;(function(){var t=x.createDocumentFragment(),r=t.appendChild(x.createElement("div")),o=x.createElement("input");o.setAttribute("type","radio"),o.setAttribute("checked","checked"),o.setAttribute("name","t"),r.appendChild(o),R.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,r.innerHTML="",R.noCloneChecked=!!r.cloneNode(!0).lastChild.defaultValue,r.innerHTML="",R.option=!!r.lastChild})();var je={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};je.tbody=je.tfoot=je.colgroup=je.caption=je.thead,je.th=je.td,R.option||(je.optgroup=je.option=[1,""]);function $e(t,r){var o;return typeof t.getElementsByTagName<"u"?o=t.getElementsByTagName(r||"*"):typeof t.querySelectorAll<"u"?o=t.querySelectorAll(r||"*"):o=[],r===void 0||r&&ge(t,r)?c.merge([t],o):o}function rn(t,r){for(var o=0,_=t.length;o<_;o++)ie.set(t[o],"globalEval",!r||ie.get(r[o],"globalEval"))}var Sr=/<|&#?\w+;/;function On(t,r,o,_,m){for(var E,S,N,O,M,P,q=r.createDocumentFragment(),w=[],k=0,te=t.length;k-1){m&&m.push(E);continue}if(M=K(E),S=$e(q.appendChild(E),"script"),M&&rn(S),o)for(P=0;E=S[P++];)Nn.test(E.type||"")&&o.push(E)}return q}var fr=/^key/,Tr=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,An=/^([^.]*)(?:\.(.+)|)/;function It(){return!0}function vt(){return!1}function br(t,r){return t===Rr()==(r==="focus")}function Rr(){try{return x.activeElement}catch{}}function an(t,r,o,_,m,E){var S,N;if(typeof r=="object"){typeof o!="string"&&(_=_||o,o=void 0);for(N in r)an(t,N,o,_,r[N],E);return t}if(_==null&&m==null?(m=o,_=o=void 0):m==null&&(typeof o=="string"?(m=_,_=void 0):(m=_,_=o,o=void 0)),m===!1)m=vt;else if(!m)return t;return E===1&&(S=m,m=function(O){return c().off(O),S.apply(this,arguments)},m.guid=S.guid||(S.guid=c.guid++)),t.each(function(){c.event.add(this,r,m,_,o)})}c.event={global:{},add:function(t,r,o,_,m){var E,S,N,O,M,P,q,w,k,te,Ee,ne=ie.get(t);if(gt(t))for(o.handler&&(E=o,o=E.handler,m=E.selector),m&&c.find.matchesSelector(it,m),o.guid||(o.guid=c.guid++),(O=ne.events)||(O=ne.events=Object.create(null)),(S=ne.handle)||(S=ne.handle=function(Ge){return typeof c<"u"&&c.event.triggered!==Ge.type?c.event.dispatch.apply(t,arguments):void 0}),r=(r||"").match(_e)||[""],M=r.length;M--;)N=An.exec(r[M])||[],k=Ee=N[1],te=(N[2]||"").split(".").sort(),k&&(q=c.event.special[k]||{},k=(m?q.delegateType:q.bindType)||k,q=c.event.special[k]||{},P=c.extend({type:k,origType:Ee,data:_,handler:o,guid:o.guid,selector:m,needsContext:m&&c.expr.match.needsContext.test(m),namespace:te.join(".")},E),(w=O[k])||(w=O[k]=[],w.delegateCount=0,(!q.setup||q.setup.call(t,_,te,S)===!1)&&t.addEventListener&&t.addEventListener(k,S)),q.add&&(q.add.call(t,P),P.handler.guid||(P.handler.guid=o.guid)),m?w.splice(w.delegateCount++,0,P):w.push(P),c.event.global[k]=!0)},remove:function(t,r,o,_,m){var E,S,N,O,M,P,q,w,k,te,Ee,ne=ie.hasData(t)&&ie.get(t);if(!(!ne||!(O=ne.events))){for(r=(r||"").match(_e)||[""],M=r.length;M--;){if(N=An.exec(r[M])||[],k=Ee=N[1],te=(N[2]||"").split(".").sort(),!k){for(k in O)c.event.remove(t,k+r[M],o,_,!0);continue}for(q=c.event.special[k]||{},k=(_?q.delegateType:q.bindType)||k,w=O[k]||[],N=N[2]&&new RegExp("(^|\\.)"+te.join("\\.(?:.*\\.|)")+"(\\.|$)"),S=E=w.length;E--;)P=w[E],(m||Ee===P.origType)&&(!o||o.guid===P.guid)&&(!N||N.test(P.namespace))&&(!_||_===P.selector||_==="**"&&P.selector)&&(w.splice(E,1),P.selector&&w.delegateCount--,q.remove&&q.remove.call(t,P));S&&!w.length&&((!q.teardown||q.teardown.call(t,te,ne.handle)===!1)&&c.removeEvent(t,k,ne.handle),delete O[k])}c.isEmptyObject(O)&&ie.remove(t,"handle events")}},dispatch:function(t){var r,o,_,m,E,S,N=new Array(arguments.length),O=c.event.fix(t),M=(ie.get(this,"events")||Object.create(null))[O.type]||[],P=c.event.special[O.type]||{};for(N[0]=O,r=1;r=1)){for(;M!==this;M=M.parentNode||this)if(M.nodeType===1&&!(t.type==="click"&&M.disabled===!0)){for(E=[],S={},o=0;o-1:c.find(m,this,null,[M]).length),S[m]&&E.push(_);E.length&&N.push({elem:M,handlers:E})}}return M=this,O\s*$/g;function In(t,r){return ge(t,"table")&&ge(r.nodeType!==11?r:r.firstChild,"tr")&&c(t).children("tbody")[0]||t}function Or(t){return t.type=(t.getAttribute("type")!==null)+"/"+t.type,t}function Ar(t){return(t.type||"").slice(0,5)==="true/"?t.type=t.type.slice(5):t.removeAttribute("type"),t}function vn(t,r){var o,_,m,E,S,N,O;if(r.nodeType===1){if(ie.hasData(t)&&(E=ie.get(t),O=E.events,O)){ie.remove(r,"handle events");for(m in O)for(o=0,_=O[m].length;o<_;o++)c.event.add(r,m,O[m][o])}Ue.hasData(t)&&(S=Ue.access(t),N=c.extend({},S),Ue.set(r,N))}}function Ir(t,r){var o=r.nodeName.toLowerCase();o==="input"&&bt.test(t.type)?r.checked=t.checked:(o==="input"||o==="textarea")&&(r.defaultValue=t.defaultValue)}function yt(t,r,o,_){r=a(r);var m,E,S,N,O,M,P=0,q=t.length,w=q-1,k=r[0],te=C(k);if(te||q>1&&typeof k=="string"&&!R.checkClone&&hr.test(k))return t.each(function(Ee){var ne=t.eq(Ee);te&&(r[0]=k.call(this,Ee,ne.html())),yt(ne,r,o,_)});if(q&&(m=On(r,t[0].ownerDocument,!1,t,_),E=m.firstChild,m.childNodes.length===1&&(m=E),E||_)){for(S=c.map($e(m,"script"),Or),N=S.length;P0&&rn(S,!O&&$e(t,"script")),N},cleanData:function(t){for(var r,o,_,m=c.event.special,E=0;(o=t[E])!==void 0;E++)if(gt(o)){if(r=o[ie.expando]){if(r.events)for(_ in r.events)m[_]?c.event.remove(o,_):c.removeEvent(o,_,r.handle);o[ie.expando]=void 0}o[Ue.expando]&&(o[Ue.expando]=void 0)}}}),c.fn.extend({detach:function(t){return yn(this,t,!0)},remove:function(t){return yn(this,t)},text:function(t){return We(this,function(r){return r===void 0?c.text(this):this.empty().each(function(){(this.nodeType===1||this.nodeType===11||this.nodeType===9)&&(this.textContent=r)})},null,t,arguments.length)},append:function(){return yt(this,arguments,function(t){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var r=In(this,t);r.appendChild(t)}})},prepend:function(){return yt(this,arguments,function(t){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var r=In(this,t);r.insertBefore(t,r.firstChild)}})},before:function(){return yt(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this)})},after:function(){return yt(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)})},empty:function(){for(var t,r=0;(t=this[r])!=null;r++)t.nodeType===1&&(c.cleanData($e(t,!1)),t.textContent="");return this},clone:function(t,r){return t=t??!1,r=r??t,this.map(function(){return c.clone(this,t,r)})},html:function(t){return We(this,function(r){var o=this[0]||{},_=0,m=this.length;if(r===void 0&&o.nodeType===1)return o.innerHTML;if(typeof r=="string"&&!Cr.test(r)&&!je[(hn.exec(r)||["",""])[1].toLowerCase()]){r=c.htmlPrefilter(r);try{for(;_3,it.removeChild(P)),S}}))})();function kt(t,r,o){var _,m,E,S,N=t.style;return o=o||Qt(t),o&&(S=o.getPropertyValue(r)||o[r],S===""&&!K(t)&&(S=c.style(t,r)),!R.pixelBoxStyles()&&on.test(S)&&vr.test(r)&&(_=N.width,m=N.minWidth,E=N.maxWidth,N.minWidth=N.maxWidth=N.width=S,S=o.width,N.width=_,N.minWidth=m,N.maxWidth=E)),S!==void 0?S+"":S}function Mn(t,r){return{get:function(){if(t()){delete this.get;return}return(this.get=r).apply(this,arguments)}}}var Ln=["Webkit","Moz","ms"],xn=x.createElement("div").style,wn={};function yr(t){for(var r=t[0].toUpperCase()+t.slice(1),o=Ln.length;o--;)if(t=Ln[o]+r,t in xn)return t}function sn(t){var r=c.cssProps[t]||wn[t];return r||(t in xn?t:wn[t]=yr(t)||t)}var Dr=/^(none|table(?!-c[ea]).+)/,Pn=/^--/,Mr={position:"absolute",visibility:"hidden",display:"block"},kn={letterSpacing:"0",fontWeight:"400"};function Un(t,r,o){var _=Ze.exec(r);return _?Math.max(0,_[2]-(o||0))+(_[3]||"px"):r}function ln(t,r,o,_,m,E){var S=r==="width"?1:0,N=0,O=0;if(o===(_?"border":"content"))return 0;for(;S<4;S+=2)o==="margin"&&(O+=c.css(t,o+Je[S],!0,m)),_?(o==="content"&&(O-=c.css(t,"padding"+Je[S],!0,m)),o!=="margin"&&(O-=c.css(t,"border"+Je[S]+"Width",!0,m))):(O+=c.css(t,"padding"+Je[S],!0,m),o!=="padding"?O+=c.css(t,"border"+Je[S]+"Width",!0,m):N+=c.css(t,"border"+Je[S]+"Width",!0,m));return!_&&E>=0&&(O+=Math.max(0,Math.ceil(t["offset"+r[0].toUpperCase()+r.slice(1)]-E-O-N-.5))||0),O}function Fn(t,r,o){var _=Qt(t),m=!R.boxSizingReliable()||o,E=m&&c.css(t,"boxSizing",!1,_)==="border-box",S=E,N=kt(t,r,_),O="offset"+r[0].toUpperCase()+r.slice(1);if(on.test(N)){if(!o)return N;N="auto"}return(!R.boxSizingReliable()&&E||!R.reliableTrDimensions()&&ge(t,"tr")||N==="auto"||!parseFloat(N)&&c.css(t,"display",!1,_)==="inline")&&t.getClientRects().length&&(E=c.css(t,"boxSizing",!1,_)==="border-box",S=O in t,S&&(N=t[O])),N=parseFloat(N)||0,N+ln(t,r,o||(E?"border":"content"),S,_,N)+"px"}c.extend({cssHooks:{opacity:{get:function(t,r){if(r){var o=kt(t,"opacity");return o===""?"1":o}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(t,r,o,_){if(!(!t||t.nodeType===3||t.nodeType===8||!t.style)){var m,E,S,N=Be(r),O=Pn.test(r),M=t.style;if(O||(r=sn(N)),S=c.cssHooks[r]||c.cssHooks[N],o!==void 0){if(E=typeof o,E==="string"&&(m=Ze.exec(o))&&m[1]&&(o=fe(t,r,m),E="number"),o==null||o!==o)return;E==="number"&&!O&&(o+=m&&m[3]||(c.cssNumber[N]?"":"px")),!R.clearCloneStyle&&o===""&&r.indexOf("background")===0&&(M[r]="inherit"),(!S||!("set"in S)||(o=S.set(t,o,_))!==void 0)&&(O?M.setProperty(r,o):M[r]=o)}else return S&&"get"in S&&(m=S.get(t,!1,_))!==void 0?m:M[r]}},css:function(t,r,o,_){var m,E,S,N=Be(r),O=Pn.test(r);return O||(r=sn(N)),S=c.cssHooks[r]||c.cssHooks[N],S&&"get"in S&&(m=S.get(t,!0,o)),m===void 0&&(m=kt(t,r,_)),m==="normal"&&r in kn&&(m=kn[r]),o===""||o?(E=parseFloat(m),o===!0||isFinite(E)?E||0:m):m}}),c.each(["height","width"],function(t,r){c.cssHooks[r]={get:function(o,_,m){if(_)return Dr.test(c.css(o,"display"))&&(!o.getClientRects().length||!o.getBoundingClientRect().width)?Dn(o,Mr,function(){return Fn(o,r,m)}):Fn(o,r,m)},set:function(o,_,m){var E,S=Qt(o),N=!R.scrollboxSize()&&S.position==="absolute",O=N||m,M=O&&c.css(o,"boxSizing",!1,S)==="border-box",P=m?ln(o,r,m,M,S):0;return M&&N&&(P-=Math.ceil(o["offset"+r[0].toUpperCase()+r.slice(1)]-parseFloat(S[r])-ln(o,r,"border",!1,S)-.5)),P&&(E=Ze.exec(_))&&(E[3]||"px")!=="px"&&(o.style[r]=_,_=c.css(o,r)),Un(o,_,P)}}}),c.cssHooks.marginLeft=Mn(R.reliableMarginLeft,function(t,r){if(r)return(parseFloat(kt(t,"marginLeft"))||t.getBoundingClientRect().left-Dn(t,{marginLeft:0},function(){return t.getBoundingClientRect().left}))+"px"}),c.each({margin:"",padding:"",border:"Width"},function(t,r){c.cssHooks[t+r]={expand:function(o){for(var _=0,m={},E=typeof o=="string"?o.split(" "):[o];_<4;_++)m[t+Je[_]+r]=E[_]||E[_-2]||E[0];return m}},t!=="margin"&&(c.cssHooks[t+r].set=Un)}),c.fn.extend({css:function(t,r){return We(this,function(o,_,m){var E,S,N={},O=0;if(Array.isArray(_)){for(E=Qt(o),S=_.length;O1)}});function Ke(t,r,o,_,m){return new Ke.prototype.init(t,r,o,_,m)}c.Tween=Ke,Ke.prototype={constructor:Ke,init:function(t,r,o,_,m,E){this.elem=t,this.prop=o,this.easing=m||c.easing._default,this.options=r,this.start=this.now=this.cur(),this.end=_,this.unit=E||(c.cssNumber[o]?"":"px")},cur:function(){var t=Ke.propHooks[this.prop];return t&&t.get?t.get(this):Ke.propHooks._default.get(this)},run:function(t){var r,o=Ke.propHooks[this.prop];return this.options.duration?this.pos=r=c.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=r=t,this.now=(this.end-this.start)*r+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),o&&o.set?o.set(this):Ke.propHooks._default.set(this),this}},Ke.prototype.init.prototype=Ke.prototype,Ke.propHooks={_default:{get:function(t){var r;return t.elem.nodeType!==1||t.elem[t.prop]!=null&&t.elem.style[t.prop]==null?t.elem[t.prop]:(r=c.css(t.elem,t.prop,""),!r||r==="auto"?0:r)},set:function(t){c.fx.step[t.prop]?c.fx.step[t.prop](t):t.elem.nodeType===1&&(c.cssHooks[t.prop]||t.elem.style[sn(t.prop)]!=null)?c.style(t.elem,t.prop,t.now+t.unit):t.elem[t.prop]=t.now}}},Ke.propHooks.scrollTop=Ke.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},c.easing={linear:function(t){return t},swing:function(t){return .5-Math.cos(t*Math.PI)/2},_default:"swing"},c.fx=Ke.prototype.init,c.fx.step={};var Dt,Xt,Lr=/^(?:toggle|show|hide)$/,xr=/queueHooks$/;function cn(){Xt&&(x.hidden===!1&&e.requestAnimationFrame?e.requestAnimationFrame(cn):e.setTimeout(cn,c.fx.interval),c.fx.tick())}function Bn(){return e.setTimeout(function(){Dt=void 0}),Dt=Date.now()}function Zt(t,r){var o,_=0,m={height:t};for(r=r?1:0;_<4;_+=2-r)o=Je[_],m["margin"+o]=m["padding"+o]=t;return r&&(m.opacity=m.width=t),m}function Gn(t,r,o){for(var _,m=(ot.tweeners[r]||[]).concat(ot.tweeners["*"]),E=0,S=m.length;E1)},removeAttr:function(t){return this.each(function(){c.removeAttr(this,t)})}}),c.extend({attr:function(t,r,o){var _,m,E=t.nodeType;if(!(E===3||E===8||E===2)){if(typeof t.getAttribute>"u")return c.prop(t,r,o);if((E!==1||!c.isXMLDoc(t))&&(m=c.attrHooks[r.toLowerCase()]||(c.expr.match.bool.test(r)?Yn:void 0)),o!==void 0){if(o===null){c.removeAttr(t,r);return}return m&&"set"in m&&(_=m.set(t,o,r))!==void 0?_:(t.setAttribute(r,o+""),o)}return m&&"get"in m&&(_=m.get(t,r))!==null?_:(_=c.find.attr(t,r),_??void 0)}},attrHooks:{type:{set:function(t,r){if(!R.radioValue&&r==="radio"&&ge(t,"input")){var o=t.value;return t.setAttribute("type",r),o&&(t.value=o),r}}}},removeAttr:function(t,r){var o,_=0,m=r&&r.match(_e);if(m&&t.nodeType===1)for(;o=m[_++];)t.removeAttribute(o)}}),Yn={set:function(t,r,o){return r===!1?c.removeAttr(t,o):t.setAttribute(o,o),o}},c.each(c.expr.match.bool.source.match(/\w+/g),function(t,r){var o=Ut[r]||c.find.attr;Ut[r]=function(_,m,E){var S,N,O=m.toLowerCase();return E||(N=Ut[O],Ut[O]=S,S=o(_,m,E)!=null?O:null,Ut[O]=N),S}});var kr=/^(?:input|select|textarea|button)$/i,Ur=/^(?:a|area)$/i;c.fn.extend({prop:function(t,r){return We(this,c.prop,t,r,arguments.length>1)},removeProp:function(t){return this.each(function(){delete this[c.propFix[t]||t]})}}),c.extend({prop:function(t,r,o){var _,m,E=t.nodeType;if(!(E===3||E===8||E===2))return(E!==1||!c.isXMLDoc(t))&&(r=c.propFix[r]||r,m=c.propHooks[r]),o!==void 0?m&&"set"in m&&(_=m.set(t,o,r))!==void 0?_:t[r]=o:m&&"get"in m&&(_=m.get(t,r))!==null?_:t[r]},propHooks:{tabIndex:{get:function(t){var r=c.find.attr(t,"tabindex");return r?parseInt(r,10):kr.test(t.nodeName)||Ur.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),R.optSelected||(c.propHooks.selected={get:function(t){var r=t.parentNode;return r&&r.parentNode&&r.parentNode.selectedIndex,null},set:function(t){var r=t.parentNode;r&&(r.selectedIndex,r.parentNode&&r.parentNode.selectedIndex)}}),c.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){c.propFix[this.toLowerCase()]=this});function Rt(t){var r=t.match(_e)||[];return r.join(" ")}function Ct(t){return t.getAttribute&&t.getAttribute("class")||""}function _n(t){return Array.isArray(t)?t:typeof t=="string"?t.match(_e)||[]:[]}c.fn.extend({addClass:function(t){var r,o,_,m,E,S,N,O=0;if(C(t))return this.each(function(M){c(this).addClass(t.call(this,M,Ct(this)))});if(r=_n(t),r.length){for(;o=this[O++];)if(m=Ct(o),_=o.nodeType===1&&" "+Rt(m)+" ",_){for(S=0;E=r[S++];)_.indexOf(" "+E+" ")<0&&(_+=E+" ");N=Rt(_),m!==N&&o.setAttribute("class",N)}}return this},removeClass:function(t){var r,o,_,m,E,S,N,O=0;if(C(t))return this.each(function(M){c(this).removeClass(t.call(this,M,Ct(this)))});if(!arguments.length)return this.attr("class","");if(r=_n(t),r.length){for(;o=this[O++];)if(m=Ct(o),_=o.nodeType===1&&" "+Rt(m)+" ",_){for(S=0;E=r[S++];)for(;_.indexOf(" "+E+" ")>-1;)_=_.replace(" "+E+" "," ");N=Rt(_),m!==N&&o.setAttribute("class",N)}}return this},toggleClass:function(t,r){var o=typeof t,_=o==="string"||Array.isArray(t);return typeof r=="boolean"&&_?r?this.addClass(t):this.removeClass(t):C(t)?this.each(function(m){c(this).toggleClass(t.call(this,m,Ct(this),r),r)}):this.each(function(){var m,E,S,N;if(_)for(E=0,S=c(this),N=_n(t);m=N[E++];)S.hasClass(m)?S.removeClass(m):S.addClass(m);else(t===void 0||o==="boolean")&&(m=Ct(this),m&&ie.set(this,"__className__",m),this.setAttribute&&this.setAttribute("class",m||t===!1?"":ie.get(this,"__className__")||""))})},hasClass:function(t){var r,o,_=0;for(r=" "+t+" ";o=this[_++];)if(o.nodeType===1&&(" "+Rt(Ct(o))+" ").indexOf(r)>-1)return!0;return!1}});var Fr=/\r/g;c.fn.extend({val:function(t){var r,o,_,m=this[0];return arguments.length?(_=C(t),this.each(function(E){var S;this.nodeType===1&&(_?S=t.call(this,E,c(this).val()):S=t,S==null?S="":typeof S=="number"?S+="":Array.isArray(S)&&(S=c.map(S,function(N){return N==null?"":N+""})),r=c.valHooks[this.type]||c.valHooks[this.nodeName.toLowerCase()],(!r||!("set"in r)||r.set(this,S,"value")===void 0)&&(this.value=S))})):m?(r=c.valHooks[m.type]||c.valHooks[m.nodeName.toLowerCase()],r&&"get"in r&&(o=r.get(m,"value"))!==void 0?o:(o=m.value,typeof o=="string"?o.replace(Fr,""):o??"")):void 0}}),c.extend({valHooks:{option:{get:function(t){var r=c.find.attr(t,"value");return r??Rt(c.text(t))}},select:{get:function(t){var r,o,_,m=t.options,E=t.selectedIndex,S=t.type==="select-one",N=S?null:[],O=S?E+1:m.length;for(E<0?_=O:_=S?E:0;_-1)&&(o=!0);return o||(t.selectedIndex=-1),E}}}}),c.each(["radio","checkbox"],function(){c.valHooks[this]={set:function(t,r){if(Array.isArray(r))return t.checked=c.inArray(c(t).val(),r)>-1}},R.checkOn||(c.valHooks[this].get=function(t){return t.getAttribute("value")===null?"on":t.value})}),R.focusin="onfocusin"in e;var Hn=/^(?:focusinfocus|focusoutblur)$/,qn=function(t){t.stopPropagation()};c.extend(c.event,{trigger:function(t,r,o,_){var m,E,S,N,O,M,P,q,w=[o||x],k=f.call(t,"type")?t.type:t,te=f.call(t,"namespace")?t.namespace.split("."):[];if(E=q=S=o=o||x,!(o.nodeType===3||o.nodeType===8)&&!Hn.test(k+c.event.triggered)&&(k.indexOf(".")>-1&&(te=k.split("."),k=te.shift(),te.sort()),O=k.indexOf(":")<0&&"on"+k,t=t[c.expando]?t:new c.Event(k,typeof t=="object"&&t),t.isTrigger=_?2:3,t.namespace=te.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+te.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=o),r=r==null?[t]:c.makeArray(r,[t]),P=c.event.special[k]||{},!(!_&&P.trigger&&P.trigger.apply(o,r)===!1))){if(!_&&!P.noBubble&&!D(o)){for(N=P.delegateType||k,Hn.test(N+k)||(E=E.parentNode);E;E=E.parentNode)w.push(E),S=E;S===(o.ownerDocument||x)&&w.push(S.defaultView||S.parentWindow||e)}for(m=0;(E=w[m++])&&!t.isPropagationStopped();)q=E,t.type=m>1?N:P.bindType||k,M=(ie.get(E,"events")||Object.create(null))[t.type]&&ie.get(E,"handle"),M&&M.apply(E,r),M=O&&E[O],M&&M.apply&>(E)&&(t.result=M.apply(E,r),t.result===!1&&t.preventDefault());return t.type=k,!_&&!t.isDefaultPrevented()&&(!P._default||P._default.apply(w.pop(),r)===!1)&>(o)&&O&&C(o[k])&&!D(o)&&(S=o[O],S&&(o[O]=null),c.event.triggered=k,t.isPropagationStopped()&&q.addEventListener(k,qn),o[k](),t.isPropagationStopped()&&q.removeEventListener(k,qn),c.event.triggered=void 0,S&&(o[O]=S)),t.result}},simulate:function(t,r,o){var _=c.extend(new c.Event,o,{type:t,isSimulated:!0});c.event.trigger(_,null,r)}}),c.fn.extend({trigger:function(t,r){return this.each(function(){c.event.trigger(t,r,this)})},triggerHandler:function(t,r){var o=this[0];if(o)return c.event.trigger(t,r,o,!0)}}),R.focusin||c.each({focus:"focusin",blur:"focusout"},function(t,r){var o=function(_){c.event.simulate(r,_.target,c.event.fix(_))};c.event.special[r]={setup:function(){var _=this.ownerDocument||this.document||this,m=ie.access(_,r);m||_.addEventListener(t,o,!0),ie.access(_,r,(m||0)+1)},teardown:function(){var _=this.ownerDocument||this.document||this,m=ie.access(_,r)-1;m?ie.access(_,r,m):(_.removeEventListener(t,o,!0),ie.remove(_,r))}}});var Ft=e.location,dn={guid:Date.now()},Jt=/\?/;c.parseXML=function(t){var r;if(!t||typeof t!="string")return null;try{r=new e.DOMParser().parseFromString(t,"text/xml")}catch{r=void 0}return(!r||r.getElementsByTagName("parsererror").length)&&c.error("Invalid XML: "+t),r};var Vn=/\[\]$/,Wn=/\r?\n/g,Br=/^(?:submit|button|image|reset|file)$/i,zn=/^(?:input|select|textarea|keygen)/i;function Mt(t,r,o,_){var m;if(Array.isArray(r))c.each(r,function(E,S){o||Vn.test(t)?_(t,S):Mt(t+"["+(typeof S=="object"&&S!=null?E:"")+"]",S,o,_)});else if(!o&&G(r)==="object")for(m in r)Mt(t+"["+m+"]",r[m],o,_);else _(t,r)}c.param=function(t,r){var o,_=[],m=function(E,S){var N=C(S)?S():S;_[_.length]=encodeURIComponent(E)+"="+encodeURIComponent(N??"")};if(t==null)return"";if(Array.isArray(t)||t.jquery&&!c.isPlainObject(t))c.each(t,function(){m(this.name,this.value)});else for(o in t)Mt(o,t[o],r,m);return _.join("&")},c.fn.extend({serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=c.prop(this,"elements");return t?c.makeArray(t):this}).filter(function(){var t=this.type;return this.name&&!c(this).is(":disabled")&&zn.test(this.nodeName)&&!Br.test(t)&&(this.checked||!bt.test(t))}).map(function(t,r){var o=c(this).val();return o==null?null:Array.isArray(o)?c.map(o,function(_){return{name:r.name,value:_.replace(Wn,`\r +`)}}):{name:r.name,value:o.replace(Wn,`\r +`)}}).get()}});var un=/%20/g,$n=/#.*$/,Kn=/([?&])_=[^&]*/,Qn=/^(.*?):[ \t]*([^\r\n]*)$/mg,mi=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Ei=/^(?:GET|HEAD)$/,Qu=/^\/\//,gi={},Gr={},Si="*/".concat("*"),Yr=x.createElement("a");Yr.href=Ft.href;function fi(t){return function(r,o){typeof r!="string"&&(o=r,r="*");var _,m=0,E=r.toLowerCase().match(_e)||[];if(C(o))for(;_=E[m++];)_[0]==="+"?(_=_.slice(1)||"*",(t[_]=t[_]||[]).unshift(o)):(t[_]=t[_]||[]).push(o)}}function Ti(t,r,o,_){var m={},E=t===Gr;function S(N){var O;return m[N]=!0,c.each(t[N]||[],function(M,P){var q=P(r,o,_);if(typeof q=="string"&&!E&&!m[q])return r.dataTypes.unshift(q),S(q),!1;if(E)return!(O=q)}),O}return S(r.dataTypes[0])||!m["*"]&&S("*")}function Hr(t,r){var o,_,m=c.ajaxSettings.flatOptions||{};for(o in r)r[o]!==void 0&&((m[o]?t:_||(_={}))[o]=r[o]);return _&&c.extend(!0,t,_),t}function Xu(t,r,o){for(var _,m,E,S,N=t.contents,O=t.dataTypes;O[0]==="*";)O.shift(),_===void 0&&(_=t.mimeType||r.getResponseHeader("Content-Type"));if(_){for(m in N)if(N[m]&&N[m].test(_)){O.unshift(m);break}}if(O[0]in o)E=O[0];else{for(m in o){if(!O[0]||t.converters[m+" "+O[0]]){E=m;break}S||(S=m)}E=E||S}if(E)return E!==O[0]&&O.unshift(E),o[E]}function Zu(t,r,o,_){var m,E,S,N,O,M={},P=t.dataTypes.slice();if(P[1])for(S in t.converters)M[S.toLowerCase()]=t.converters[S];for(E=P.shift();E;)if(t.responseFields[E]&&(o[t.responseFields[E]]=r),!O&&_&&t.dataFilter&&(r=t.dataFilter(r,t.dataType)),O=E,E=P.shift(),E){if(E==="*")E=O;else if(O!=="*"&&O!==E){if(S=M[O+" "+E]||M["* "+E],!S){for(m in M)if(N=m.split(" "),N[1]===E&&(S=M[O+" "+N[0]]||M["* "+N[0]],S)){S===!0?S=M[m]:M[m]!==!0&&(E=N[0],P.unshift(N[1]));break}}if(S!==!0)if(S&&t.throws)r=S(r);else try{r=S(r)}catch(q){return{state:"parsererror",error:S?q:"No conversion from "+O+" to "+E}}}}return{state:"success",data:r}}c.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ft.href,type:"GET",isLocal:mi.test(Ft.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Si,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":c.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,r){return r?Hr(Hr(t,c.ajaxSettings),r):Hr(c.ajaxSettings,t)},ajaxPrefilter:fi(gi),ajaxTransport:fi(Gr),ajax:function(t,r){typeof t=="object"&&(r=t,t=void 0),r=r||{};var o,_,m,E,S,N,O,M,P,q,w=c.ajaxSetup({},r),k=w.context||w,te=w.context&&(k.nodeType||k.jquery)?c(k):c.event,Ee=c.Deferred(),ne=c.Callbacks("once memory"),Ge=w.statusCode||{},Fe={},st={},Oe="canceled",me={readyState:0,getResponseHeader:function(Te){var we;if(O){if(!E)for(E={};we=Qn.exec(m);)E[we[1].toLowerCase()+" "]=(E[we[1].toLowerCase()+" "]||[]).concat(we[2]);we=E[Te.toLowerCase()+" "]}return we==null?null:we.join(", ")},getAllResponseHeaders:function(){return O?m:null},setRequestHeader:function(Te,we){return O==null&&(Te=st[Te.toLowerCase()]=st[Te.toLowerCase()]||Te,Fe[Te]=we),this},overrideMimeType:function(Te){return O==null&&(w.mimeType=Te),this},statusCode:function(Te){var we;if(Te)if(O)me.always(Te[me.status]);else for(we in Te)Ge[we]=[Ge[we],Te[we]];return this},abort:function(Te){var we=Te||Oe;return o&&o.abort(we),et(0,we),this}};if(Ee.promise(me),w.url=((t||w.url||Ft.href)+"").replace(Qu,Ft.protocol+"//"),w.type=r.method||r.type||w.method||w.type,w.dataTypes=(w.dataType||"*").toLowerCase().match(_e)||[""],w.crossDomain==null){N=x.createElement("a");try{N.href=w.url,N.href=N.href,w.crossDomain=Yr.protocol+"//"+Yr.host!=N.protocol+"//"+N.host}catch{w.crossDomain=!0}}if(w.data&&w.processData&&typeof w.data!="string"&&(w.data=c.param(w.data,w.traditional)),Ti(gi,w,r,me),O)return me;M=c.event&&w.global,M&&c.active++===0&&c.event.trigger("ajaxStart"),w.type=w.type.toUpperCase(),w.hasContent=!Ei.test(w.type),_=w.url.replace($n,""),w.hasContent?w.data&&w.processData&&(w.contentType||"").indexOf("application/x-www-form-urlencoded")===0&&(w.data=w.data.replace(un,"+")):(q=w.url.slice(_.length),w.data&&(w.processData||typeof w.data=="string")&&(_+=(Jt.test(_)?"&":"?")+w.data,delete w.data),w.cache===!1&&(_=_.replace(Kn,"$1"),q=(Jt.test(_)?"&":"?")+"_="+dn.guid+++q),w.url=_+q),w.ifModified&&(c.lastModified[_]&&me.setRequestHeader("If-Modified-Since",c.lastModified[_]),c.etag[_]&&me.setRequestHeader("If-None-Match",c.etag[_])),(w.data&&w.hasContent&&w.contentType!==!1||r.contentType)&&me.setRequestHeader("Content-Type",w.contentType),me.setRequestHeader("Accept",w.dataTypes[0]&&w.accepts[w.dataTypes[0]]?w.accepts[w.dataTypes[0]]+(w.dataTypes[0]!=="*"?", "+Si+"; q=0.01":""):w.accepts["*"]);for(P in w.headers)me.setRequestHeader(P,w.headers[P]);if(w.beforeSend&&(w.beforeSend.call(k,me,w)===!1||O))return me.abort();if(Oe="abort",ne.add(w.complete),me.done(w.success),me.fail(w.error),o=Ti(Gr,w,r,me),!o)et(-1,"No Transport");else{if(me.readyState=1,M&&te.trigger("ajaxSend",[me,w]),O)return me;w.async&&w.timeout>0&&(S=e.setTimeout(function(){me.abort("timeout")},w.timeout));try{O=!1,o.send(Fe,et)}catch(Te){if(O)throw Te;et(-1,Te)}}function et(Te,we,mn,Xn){var lt,Bt,Gt,tt,Lt,dt=we;O||(O=!0,S&&e.clearTimeout(S),o=void 0,m=Xn||"",me.readyState=Te>0?4:0,lt=Te>=200&&Te<300||Te===304,mn&&(tt=Xu(w,me,mn)),!lt&&c.inArray("script",w.dataTypes)>-1&&(w.converters["text script"]=function(){}),tt=Zu(w,tt,me,lt),lt?(w.ifModified&&(Lt=me.getResponseHeader("Last-Modified"),Lt&&(c.lastModified[_]=Lt),Lt=me.getResponseHeader("etag"),Lt&&(c.etag[_]=Lt)),Te===204||w.type==="HEAD"?dt="nocontent":Te===304?dt="notmodified":(dt=tt.state,Bt=tt.data,Gt=tt.error,lt=!Gt)):(Gt=dt,(Te||!dt)&&(dt="error",Te<0&&(Te=0))),me.status=Te,me.statusText=(we||dt)+"",lt?Ee.resolveWith(k,[Bt,dt,me]):Ee.rejectWith(k,[me,dt,Gt]),me.statusCode(Ge),Ge=void 0,M&&te.trigger(lt?"ajaxSuccess":"ajaxError",[me,w,lt?Bt:Gt]),ne.fireWith(k,[me,dt]),M&&(te.trigger("ajaxComplete",[me,w]),--c.active||c.event.trigger("ajaxStop")))}return me},getJSON:function(t,r,o){return c.get(t,r,o,"json")},getScript:function(t,r){return c.get(t,void 0,r,"script")}}),c.each(["get","post"],function(t,r){c[r]=function(o,_,m,E){return C(_)&&(E=E||m,m=_,_=void 0),c.ajax(c.extend({url:o,type:r,dataType:E,data:_,success:m},c.isPlainObject(o)&&o))}}),c.ajaxPrefilter(function(t){var r;for(r in t.headers)r.toLowerCase()==="content-type"&&(t.contentType=t.headers[r]||"")}),c._evalUrl=function(t,r,o){return c.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(_){c.globalEval(_,r,o)}})},c.fn.extend({wrapAll:function(t){var r;return this[0]&&(C(t)&&(t=t.call(this[0])),r=c(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&r.insertBefore(this[0]),r.map(function(){for(var o=this;o.firstElementChild;)o=o.firstElementChild;return o}).append(this)),this},wrapInner:function(t){return C(t)?this.each(function(r){c(this).wrapInner(t.call(this,r))}):this.each(function(){var r=c(this),o=r.contents();o.length?o.wrapAll(t):r.append(t)})},wrap:function(t){var r=C(t);return this.each(function(o){c(this).wrapAll(r?t.call(this,o):t)})},unwrap:function(t){return this.parent(t).not("body").each(function(){c(this).replaceWith(this.childNodes)}),this}}),c.expr.pseudos.hidden=function(t){return!c.expr.pseudos.visible(t)},c.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},c.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch{}};var Ju={0:200,1223:204},pn=c.ajaxSettings.xhr();R.cors=!!pn&&"withCredentials"in pn,R.ajax=pn=!!pn,c.ajaxTransport(function(t){var r,o;if(R.cors||pn&&!t.crossDomain)return{send:function(_,m){var E,S=t.xhr();if(S.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(E in t.xhrFields)S[E]=t.xhrFields[E];t.mimeType&&S.overrideMimeType&&S.overrideMimeType(t.mimeType),!t.crossDomain&&!_["X-Requested-With"]&&(_["X-Requested-With"]="XMLHttpRequest");for(E in _)S.setRequestHeader(E,_[E]);r=function(N){return function(){r&&(r=o=S.onload=S.onerror=S.onabort=S.ontimeout=S.onreadystatechange=null,N==="abort"?S.abort():N==="error"?typeof S.status!="number"?m(0,"error"):m(S.status,S.statusText):m(Ju[S.status]||S.status,S.statusText,(S.responseType||"text")!=="text"||typeof S.responseText!="string"?{binary:S.response}:{text:S.responseText},S.getAllResponseHeaders()))}},S.onload=r(),o=S.onerror=S.ontimeout=r("error"),S.onabort!==void 0?S.onabort=o:S.onreadystatechange=function(){S.readyState===4&&e.setTimeout(function(){r&&o()})},r=r("abort");try{S.send(t.hasContent&&t.data||null)}catch(N){if(r)throw N}},abort:function(){r&&r()}}}),c.ajaxPrefilter(function(t){t.crossDomain&&(t.contents.script=!1)}),c.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return c.globalEval(t),t}}}),c.ajaxPrefilter("script",function(t){t.cache===void 0&&(t.cache=!1),t.crossDomain&&(t.type="GET")}),c.ajaxTransport("script",function(t){if(t.crossDomain||t.scriptAttrs){var r,o;return{send:function(_,m){r=c("