From 369a27b800cf43ade85a01dc971f84f7d2b88fab Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 20 Oct 2020 20:55:19 -0700 Subject: [PATCH] Replacing Networking (#271) - [X] Removing Kestrel & Libuv - [X] Cleaning up NetState - [X] Removing System.IO.Pipelines - [X] Cleaning up packet reading - [X] Adds a maximum of 5000 sockets (configurable) to prevent OOM - [X] Replaces the AsyncState with a thread-safe wrapped boolean called NetworkState - [X] Removes Parallel.ForEach (no perf gain) - [X] Removes custom houses compression on another thread - [X] Test high load scenarios Bumps release version --- Distribution/Data/assemblies.json | 9 - .../Server.Tests/Network/PacketReaderTests.cs | 101 ++++ .../Old/Outgoing/AccountPacketTests.cs | 19 +- .../Packets/Old/Outgoing/GumpPacketTests.cs | 26 +- .../Packets/Old/Outgoing/MobilePacketTests.cs | 16 +- Projects/Server.Tests/Network/PipeTests.cs | 214 ++++++++ Projects/Server/Buffers/MemoryPoolBlock.cs | 58 -- Projects/Server/Buffers/MemoryPoolFactory.cs | 11 - Projects/Server/Buffers/MemoryPoolSlab.cs | 82 --- .../Server/Buffers/MemoryPoolThrowHelper.cs | 53 -- Projects/Server/Buffers/SlabMemoryPool.cs | 210 -------- .../Server/Events/SocketConnectionEvent.cs | 8 +- .../InvalidThreadException.cs} | 21 +- .../Exceptions/MaxConnectionsException.cs | 26 + Projects/Server/Items/Item.cs | 11 +- Projects/Server/Main.cs | 55 +- Projects/Server/Mobiles/Mobile.cs | 12 +- Projects/Server/Network/EncodedReader.cs | 4 +- Projects/Server/Network/MessagePumpService.cs | 69 --- Projects/Server/Network/NetState/NetState.cs | 387 ++++++++++---- Projects/Server/Network/NetworkState.cs | 38 ++ Projects/Server/Network/Packet.cs | 36 +- Projects/Server/Network/PacketHandlers.cs | 181 +++---- Projects/Server/Network/PacketReader.cs | 495 +++++++++--------- .../Packets/Old Packets/MessagePackets.cs | 3 +- Projects/Server/Network/Pipe.cs | 356 +++++++++++++ .../Server/Network/ServerConnectionHandler.cs | 91 ---- Projects/Server/Network/TcpServer.cs | 232 ++++++-- Projects/Server/Pipelines/DuplexPipe.cs | 42 -- Projects/Server/Server.csproj | 18 - .../Utilities}/HexStringConverter.cs | 17 +- Projects/Server/Utilities/Utility.cs | 9 +- .../Accounting/AccessRestrictions.cs | 4 +- .../UOContent/Accounting/AccountHandler.cs | 2 +- Projects/UOContent/Engines/Chat/Chat.cs | 6 +- .../Items/Addons/BaseAddonContainer.cs | 7 +- Projects/UOContent/Items/Books/BaseBook.cs | 10 +- .../Games/Mahjong/MahjongPacketHandlers.cs | 2 +- .../UOContent/Items/Misc/BulletinBoards.cs | 4 +- Projects/UOContent/Misc/ClientVerification.cs | 2 +- Projects/UOContent/Misc/Fastwalk.cs | 3 +- Projects/UOContent/Misc/HardwareInfo.cs | 6 +- Projects/UOContent/Misc/ProtocolExtensions.cs | 19 +- Projects/UOContent/Mobiles/BaseCreature.cs | 137 +++-- Projects/UOContent/Multis/HouseFoundation.cs | 220 +++----- Projects/UOContent/UOContent.csproj | 15 - version.json | 2 +- 47 files changed, 1780 insertions(+), 1569 deletions(-) create mode 100644 Projects/Server.Tests/Network/PacketReaderTests.cs create mode 100644 Projects/Server.Tests/Network/PipeTests.cs delete mode 100644 Projects/Server/Buffers/MemoryPoolBlock.cs delete mode 100644 Projects/Server/Buffers/MemoryPoolFactory.cs delete mode 100644 Projects/Server/Buffers/MemoryPoolSlab.cs delete mode 100644 Projects/Server/Buffers/MemoryPoolThrowHelper.cs delete mode 100644 Projects/Server/Buffers/SlabMemoryPool.cs rename Projects/Server/{Network/ServerStartup.cs => Exceptions/InvalidThreadException.cs} (61%) create mode 100644 Projects/Server/Exceptions/MaxConnectionsException.cs delete mode 100644 Projects/Server/Network/MessagePumpService.cs create mode 100644 Projects/Server/Network/NetworkState.cs create mode 100644 Projects/Server/Network/Pipe.cs delete mode 100644 Projects/Server/Network/ServerConnectionHandler.cs delete mode 100644 Projects/Server/Pipelines/DuplexPipe.cs rename Projects/{UOContent/Misc => Server/Utilities}/HexStringConverter.cs (63%) diff --git a/Distribution/Data/assemblies.json b/Distribution/Data/assemblies.json index 27a2c2715..6e40c5338 100644 --- a/Distribution/Data/assemblies.json +++ b/Distribution/Data/assemblies.json @@ -2,16 +2,7 @@ "Argon2.Bindings.dll", "BouncyCastle.Crypto.dll", "MailKit.dll", - "Microsoft.AspNetCore.Connections.Abstractions.dll", - "Microsoft.AspNetCore.Http.Features.dll", - "Microsoft.Extensions.Configuration.Abstractions.dll", - "Microsoft.Extensions.DependencyInjection.Abstractions.dll", - "Microsoft.Extensions.FileProviders.Abstractions.dll", - "Microsoft.Extensions.Hosting.Abstractions.dll", - "Microsoft.Extensions.Logging.Abstractions.dll", - "Microsoft.Extensions.Primitives.dll", "MimeKit.dll", - "System.IO.Pipelines.dll", "Zlib.Bindings.dll", "UOContent.dll" ] diff --git a/Projects/Server.Tests/Network/PacketReaderTests.cs b/Projects/Server.Tests/Network/PacketReaderTests.cs new file mode 100644 index 000000000..b509e35cf --- /dev/null +++ b/Projects/Server.Tests/Network/PacketReaderTests.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Text; +using Server.Network; +using Xunit; +using Xunit.Abstractions; + +namespace Server.Tests.Network +{ + public class PacketReaderTests + { + private (Encoding, Type) GetEncoding(string value) => + value.ToUpper() switch + { + "UTF8" => (Utility.UTF8, typeof(byte)), + "UNICODELE" => (Utility.UnicodeLE, typeof(char)), + "UNICODE" => (Utility.Unicode, typeof(char)), + _ => (Encoding.ASCII, typeof(byte)) + }; + + private ITestOutputHelper _outputHelper; + + public PacketReaderTests(ITestOutputHelper outputHelper) => _outputHelper = outputHelper; + + [Theory] + // First only, beginning + [InlineData("Test String", "ASCII", false, -1, 1024, 1024, 0)] + [InlineData("Test String", "UTF8", false, -1, 1024, 1024, 0)] + [InlineData("Test String", "Unicode", false, -1, 1024, 1024, 0)] + [InlineData("Test String", "UnicodeLE", false, -1, 1024, 1024, 0)] + + // Second only + [InlineData("Test String", "ASCII", false, -1, 1024, 1024, 1030)] + [InlineData("Test String", "UTF8", false, -1, 1024, 1024, 1030)] + [InlineData("Test String", "Unicode", false, -1, 1024, 1024, 1030)] + [InlineData("Test String", "UnicodeLE", false, -1, 1024, 1024, 1030)] + public void TestReadString( + string value, + string encodingStr, + bool isSafe, + int fixedLength, + int firstSize, + int secondSize, + int offset + ) + { + var (encoding, byteType) = GetEncoding(encodingStr); + var bytes = encoding.GetBytes(value); + Span expectedBytes = bytes.AsSpan(0, fixedLength > -1 ? Math.Min(fixedLength, bytes.Length) : bytes.Length); + + if (offset + expectedBytes.Length > firstSize + secondSize) + { + throw new ArgumentException("Test failed due to bad assumptions. Out of memory exception would be thrown"); + } + + Span first = stackalloc byte[firstSize]; + first.Clear(); // Just in case + + int bytesWrittenFirst = 0; + + if (offset < first.Length) + { + bytesWrittenFirst = Math.Min(first.Length, expectedBytes.Length); + expectedBytes.Slice(offset, bytesWrittenFirst).CopyTo(first); + } + + Span second = stackalloc byte[secondSize]; + second.Clear(); // Just in case + + if (offset > first.Length || expectedBytes.Length > bytesWrittenFirst) + { + var secondOffset = Math.Max(offset - first.Length, 0); + var secondSlice = second.Slice(secondOffset, second.Length - secondOffset); + expectedBytes.Slice(bytesWrittenFirst, expectedBytes.Length - bytesWrittenFirst).CopyTo(secondSlice); + } + + // _outputHelper.WriteLine(HexStringConverter.GetString(first)); + // _outputHelper.WriteLine(HexStringConverter.GetString(second)); + + var reader = new PacketReader(first, second); + reader.Seek(offset, SeekOrigin.Begin); + + _outputHelper.WriteLine(reader.Position.ToString()); + + var actual = byteType switch + { + var b when b == typeof(char) => reader.ReadString(encoding, isSafe, fixedLength), + _ => reader.ReadString(encoding, isSafe, fixedLength) + }; + + _outputHelper.WriteLine(actual); + + if (fixedLength > 0 && fixedLength < value.Length) + { + value = value.Substring(0, fixedLength); + } + + Assert.Equal(value, actual); + } + } +} diff --git a/Projects/Server.Tests/Network/Packets/Old/Outgoing/AccountPacketTests.cs b/Projects/Server.Tests/Network/Packets/Old/Outgoing/AccountPacketTests.cs index 7e7b10bc6..9c1db367e 100644 --- a/Projects/Server.Tests/Network/Packets/Old/Outgoing/AccountPacketTests.cs +++ b/Projects/Server.Tests/Network/Packets/Old/Outgoing/AccountPacketTests.cs @@ -1,11 +1,7 @@ using System; using System.Buffers; -using System.Collections.Generic; -using System.IO.Pipelines; using System.Linq; using System.Net; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Http.Features; using Server.Accounting; using Server.Network; using Xunit; @@ -111,12 +107,7 @@ namespace Server.Tests.Network.Packets var account = new TestAccount(new[] { firstMobile, null, null, null, null }); - var ns = new NetState( - new TestConnectionContext - { - RemoteEndPoint = IPEndPoint.Parse("127.0.0.1") - } - ) + var ns = new NetState(null) { Account = account, ProtocolChanges = protocolChanges @@ -653,13 +644,5 @@ namespace Server.Tests.Network.Packets public int CompareTo(TestAccount other) => other == null ? 1 : Username.CompareTo(other.Username); } - - internal class TestConnectionContext : ConnectionContext - { - public override string ConnectionId { get; set; } - public override IFeatureCollection Features { get; } - public override IDictionary Items { get; set; } - public override IDuplexPipe Transport { get; set; } - } } } diff --git a/Projects/Server.Tests/Network/Packets/Old/Outgoing/GumpPacketTests.cs b/Projects/Server.Tests/Network/Packets/Old/Outgoing/GumpPacketTests.cs index c22c387b5..2b253933a 100644 --- a/Projects/Server.Tests/Network/Packets/Old/Outgoing/GumpPacketTests.cs +++ b/Projects/Server.Tests/Network/Packets/Old/Outgoing/GumpPacketTests.cs @@ -59,12 +59,7 @@ namespace Server.Tests.Network.Packets [Fact] public void TestFastGumpPacket() { - var ns = new NetState( - new AccountPacketTests.TestConnectionContext - { - RemoteEndPoint = IPEndPoint.Parse("127.0.0.1") - } - ); + var ns = new NetState(null); var gump = new ResurrectGump(2); @@ -134,12 +129,7 @@ namespace Server.Tests.Network.Packets [Fact] public void TestPackedGumpPacket() { - var ns = new NetState( - new AccountPacketTests.TestConnectionContext - { - RemoteEndPoint = IPEndPoint.Parse("127.0.0.1") - } - ) + var ns = new NetState(null) { ProtocolChanges = ProtocolChanges.Unpack }; @@ -194,9 +184,8 @@ namespace Server.Tests.Network.Packets layoutList.Add(str); } - var memOwner = SlabMemoryPool.Shared.Rent(bufferLength); - - var buffer = memOwner.Memory.Span; + var rawBuffer = ArrayPool.Shared.Rent(bufferLength); + Span buffer = rawBuffer; var bufferPos = 0; foreach (var layout in layoutList) @@ -211,12 +200,12 @@ namespace Server.Tests.Network.Packets #endif expectedData.WritePacked(ref pos, buffer.Slice(0, bufferPos)); - memOwner.Dispose(); + ArrayPool.Shared.Return(rawBuffer); expectedData.Write(ref pos, gump.Strings.Count); bufferLength = gump.Strings.Sum(str => 2 + str.Length * 2); - memOwner = SlabMemoryPool.Shared.Rent(bufferLength); - buffer = memOwner.Memory.Span; + rawBuffer = ArrayPool.Shared.Rent(bufferLength); + buffer = rawBuffer; bufferPos = 0; foreach (var str in gump.Strings) @@ -225,6 +214,7 @@ namespace Server.Tests.Network.Packets } expectedData.WritePacked(ref pos, buffer.Slice(0, bufferPos)); + ArrayPool.Shared.Return(rawBuffer); // Length expectedData.Slice(1, 2).Write((ushort)pos); diff --git a/Projects/Server.Tests/Network/Packets/Old/Outgoing/MobilePacketTests.cs b/Projects/Server.Tests/Network/Packets/Old/Outgoing/MobilePacketTests.cs index 769bb03c7..5f624a355 100644 --- a/Projects/Server.Tests/Network/Packets/Old/Outgoing/MobilePacketTests.cs +++ b/Projects/Server.Tests/Network/Packets/Old/Outgoing/MobilePacketTests.cs @@ -154,7 +154,7 @@ namespace Server.Tests.Network.Packets Span expectedData = stackalloc byte[14]; var pos = 0; - expectedData.Write(ref pos, (byte)0x6E); + expectedData.Write(ref pos, (byte)0x6E); // Packet ID expectedData.Write(ref pos, mobile); expectedData.Write(ref pos, (ushort)action); expectedData.Write(ref pos, (ushort)frameCount); @@ -240,12 +240,7 @@ namespace Server.Tests.Network.Packets }; beheld.DefaultMobileInit(); - var ns = new NetState( - new AccountPacketTests.TestConnectionContext - { - RemoteEndPoint = IPEndPoint.Parse("127.0.0.1") - } - ) + var ns = new NetState(null) { ProtocolChanges = changes }; @@ -642,12 +637,7 @@ namespace Server.Tests.Network.Packets beheld.FacialHairItemID = facialHairItemId; beheld.FacialHairHue = facialHairHue; - var ns = new NetState( - new AccountPacketTests.TestConnectionContext - { - RemoteEndPoint = IPEndPoint.Parse("127.0.0.1") - } - ) + var ns = new NetState(null) { ProtocolChanges = protocolChanges }; diff --git a/Projects/Server.Tests/Network/PipeTests.cs b/Projects/Server.Tests/Network/PipeTests.cs new file mode 100644 index 000000000..9d7e81823 --- /dev/null +++ b/Projects/Server.Tests/Network/PipeTests.cs @@ -0,0 +1,214 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Server.Network; +using Xunit; + +namespace Server.Tests.Network +{ + public class PipeTests + { + private async void DelayedExecute(Action action) + { + await Task.Delay(5); + + action(); + } + + [Fact] + public async void Await() + { + var pipe = new Pipe(new byte[100]); + + var reader = pipe.Reader; + var writer = pipe.Writer; + + DelayedExecute(() => + { + // Write some data into the pipe + var buffer = writer.GetAvailable(); + Assert.True(buffer.Length == 99); + + buffer.CopyFrom(new byte[] { 1 }); + buffer.CopyFrom(new byte[] { 2 }); + buffer.CopyFrom(new byte[] { 3 }); + + writer.Advance(3); + writer.Flush(); + }); + + var result = await reader; + + Assert.True(result.Buffer[0].Count == 3); + } + + private bool _signal; + + private void Consumer(object state) + { + var reader = ((Pipe)state).Reader; + + int count = 0; + byte expected_value = 0xFA; + + while (count < 0x8000000) + { + var result = reader.TryRead(); + + for (int i = 0; i < result.Buffer[0].Count; i++) + { + Assert.True(result.Buffer[0][i] == expected_value); + count++; + + if (count == 0x1000) + { + expected_value = 0xAC; + } + } + + for (int i = 0; i < result.Buffer[1].Count; i++) + { + Assert.True(result.Buffer[1][i] == expected_value); + count++; + + if (count == 0x1000) + { + expected_value = 0xAC; + } + } + + reader.Advance((uint)result.Length); + } + + _signal = true; + } + + [Fact] + public async void Threading() + { + var pipe = new Pipe(new byte[0x1001]); + + ThreadPool.UnsafeQueueUserWorkItem(Consumer, pipe); + + var writer = pipe.Writer; + + int count = 0; + byte expected_value = 0xFA; + + while (count < 0x8000000) + { + var result = writer.GetAvailable(); + + if (result.Length < 16) + { + continue; + } + + result.CopyFrom(new[] { expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, + expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, expected_value, expected_value }); + + writer.Advance(16); + count += 16; + + if (count == 0x1000) + { + expected_value = 0xAC; + } + } + + while (_signal == false) { } + + _signal = false; + } + + [Fact] + public void Wrap() + { + var pipe = new Pipe(new byte[10]); + + var reader = pipe.Reader; + var writer = pipe.Writer; + + var result = writer.GetAvailable(); + Assert.True(result.Length == 9); + Assert.True(reader.GetRemaining() == 0); + result = reader.TryRead(); + Assert.True(result.Length == 0); + + writer.Advance(7); + result = writer.GetAvailable(); + Assert.True(result.Length == 2); + Assert.True(reader.GetRemaining() == 7); + result = reader.TryRead(); + Assert.True(result.Length == 7); + + reader.Advance(4); + result = writer.GetAvailable(); + Assert.True(result.Length == 6); + Assert.True(reader.GetRemaining() == 3); + result = reader.TryRead(); + Assert.True(result.Length == 3); + + writer.Advance(3); + result = writer.GetAvailable(); + Assert.True(result.Length == 3); + Assert.True(reader.GetRemaining() == 6); + result = reader.TryRead(); + Assert.True(result.Length == 6); + + } + + [Fact] + public void Match() + { + var pipe = new Pipe(new byte[10]); + + var reader = pipe.Reader; + var writer = pipe.Writer; + + for (uint i = 0; i < 9; i++) + { + writer.Advance(i); + writer.Flush(); + + Assert.True(reader.GetRemaining() == i); + reader.Advance(i); + } + } + + [Fact] + public void Sequence() + { + var pipe = new Pipe(new byte[10]); + + var reader = pipe.Reader; + var writer = pipe.Writer; + + var buffer = writer.GetAvailable(); + Assert.True(buffer.Length == 9); + + buffer.CopyFrom(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }); + + writer.Advance(9); + writer.Flush(); + + Assert.True(reader.GetRemaining() == 9); + + buffer = reader.TryRead(); + + for (int i = 0; i < 9; i++) + { + Assert.True(buffer.Buffer[0][i] == i); + } + + reader.Advance(4); + buffer = reader.TryRead(); + Assert.True(buffer.Length == 5); + Assert.True(buffer.Buffer[0][0] == 4); + Assert.True(buffer.Buffer[0][1] == 5); + Assert.True(buffer.Buffer[0][2] == 6); + Assert.True(buffer.Buffer[0][3] == 7); + Assert.True(buffer.Buffer[0][4] == 8); + } + } +} diff --git a/Projects/Server/Buffers/MemoryPoolBlock.cs b/Projects/Server/Buffers/MemoryPoolBlock.cs deleted file mode 100644 index fa69341af..000000000 --- a/Projects/Server/Buffers/MemoryPoolBlock.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Runtime.InteropServices; - -namespace System.Buffers -{ - /// - /// Block tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller - /// blocks. The - /// individual blocks are then treated as independent array segments. - /// - public sealed class MemoryPoolBlock : IMemoryOwner - { - private readonly int _length; - private readonly int _offset; - - /// - /// This object cannot be instantiated outside of the static Create method - /// - internal MemoryPoolBlock(SlabMemoryPool pool, MemoryPoolSlab slab, int offset, int length) - { - _offset = offset; - _length = length; - - Pool = pool; - Slab = slab; - - Memory = MemoryMarshal.CreateFromPinnedArray(slab.Array, _offset, _length); - } - - /// - /// Back-reference to the memory pool which this block was allocated from. It may only be returned to this pool. - /// - public SlabMemoryPool Pool { get; } - - /// - /// Back-reference to the slab from which this block was taken, or null if it is one-time-use memory. - /// - public MemoryPoolSlab Slab { get; } - - public Memory Memory { get; } - - public void Dispose() - { - Pool.Return(this); - } - - ~MemoryPoolBlock() - { - Pool.RefreshBlock(Slab, _offset, _length); - } - - public void Lease() - { - } - } -} diff --git a/Projects/Server/Buffers/MemoryPoolFactory.cs b/Projects/Server/Buffers/MemoryPoolFactory.cs deleted file mode 100644 index 64b931880..000000000 --- a/Projects/Server/Buffers/MemoryPoolFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace System.Buffers -{ - public static class SlabMemoryPoolFactory - { - public static MemoryPool Create() => CreateSlabMemoryPool(); - public static MemoryPool CreateSlabMemoryPool() => new SlabMemoryPool(); - } -} diff --git a/Projects/Server/Buffers/MemoryPoolSlab.cs b/Projects/Server/Buffers/MemoryPoolSlab.cs deleted file mode 100644 index 520ac3e74..000000000 --- a/Projects/Server/Buffers/MemoryPoolSlab.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Runtime.InteropServices; - -namespace System.Buffers -{ - /// - /// Slab tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller - /// blocks. The - /// individual blocks are then treated as independent array segments. - /// - public class MemoryPoolSlab : IDisposable - { - /// - /// This handle pins the managed array in memory until the slab is disposed. This prevents it from being - /// relocated and enables any subsections of the array to be used as native memory pointers to P/Invoked API calls. - /// - private GCHandle _gcHandle; - - private bool _isDisposed; - - public MemoryPoolSlab(byte[] data) - { - Array = data; - _gcHandle = GCHandle.Alloc(data, GCHandleType.Pinned); - NativePointer = _gcHandle.AddrOfPinnedObject(); - } - - /// - /// True as long as the blocks from this slab are to be considered returnable to the pool. In order to shrink the - /// memory pool size an entire slab must be removed. That is done by (1) setting IsActive to false and removing the - /// slab from the pool's _slabs collection, (2) as each block currently in use is Return()ed to the pool it will - /// be allowed to be garbage collected rather than re-pooled, and (3) when all block tracking objects are garbage - /// collected and the slab is no longer references the slab will be garbage collected and the memory unpinned will - /// be unpinned by the slab's Dispose. - /// - public bool IsActive => !_isDisposed; - - public IntPtr NativePointer { get; private set; } - - public byte[] Array { get; private set; } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public static MemoryPoolSlab Create(int length) - { - // allocate and pin requested memory length - var array = new byte[length]; - - // allocate and return slab tracking object - return new MemoryPoolSlab(array); - } - - protected void Dispose(bool disposing) - { - if (_isDisposed) - { - return; - } - - _isDisposed = true; - - Array = null; - NativePointer = IntPtr.Zero; - - if (_gcHandle.IsAllocated) - { - _gcHandle.Free(); - } - } - - ~MemoryPoolSlab() - { - Dispose(false); - } - } -} diff --git a/Projects/Server/Buffers/MemoryPoolThrowHelper.cs b/Projects/Server/Buffers/MemoryPoolThrowHelper.cs deleted file mode 100644 index 2a62bab70..000000000 --- a/Projects/Server/Buffers/MemoryPoolThrowHelper.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Runtime.CompilerServices; - -namespace System.Buffers -{ - public static class MemoryPoolThrowHelper - { - public enum ExceptionArgument - { - size, - offset, - length, - MemoryPoolBlock, - MemoryPool - } - - public static void ThrowArgumentOutOfRangeException(int sourceLength, int offset) - { - throw GetArgumentOutOfRangeException(sourceLength, offset); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(int sourceLength, int offset) => - (uint)offset > (uint)sourceLength - ? new ArgumentOutOfRangeException(GetArgumentName(ExceptionArgument.offset)) - : new ArgumentOutOfRangeException(GetArgumentName(ExceptionArgument.length)); - - public static void ThrowArgumentOutOfRangeException_BufferRequestTooLarge(int maxSize) - { - throw GetArgumentOutOfRangeException_BufferRequestTooLarge(maxSize); - } - - public static void ThrowObjectDisposedException(ExceptionArgument argument) - { - throw GetObjectDisposedException(argument); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static ArgumentOutOfRangeException GetArgumentOutOfRangeException_BufferRequestTooLarge(int maxSize) => - new ArgumentOutOfRangeException( - GetArgumentName(ExceptionArgument.size), - $"Cannot allocate more than {maxSize} bytes in a single buffer" - ); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static ObjectDisposedException GetObjectDisposedException(ExceptionArgument argument) => - new ObjectDisposedException(GetArgumentName(argument)); - - private static string GetArgumentName(ExceptionArgument argument) => argument.ToString(); - } -} diff --git a/Projects/Server/Buffers/SlabMemoryPool.cs b/Projects/Server/Buffers/SlabMemoryPool.cs deleted file mode 100644 index d58f655f5..000000000 --- a/Projects/Server/Buffers/SlabMemoryPool.cs +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Concurrent; -using System.Threading; - -namespace System.Buffers -{ - /// - /// Used to allocate and distribute re-usable blocks of memory. - /// - public sealed class SlabMemoryPool : MemoryPool - { - /// - /// The size of a block. 4096 is chosen because most operating systems use 4k pages. - /// - private const int _blockSize = 4096; - - /// - /// Allocating 32 contiguous blocks per slab makes the slab size 128k. This is larger than the 85k size which will place the - /// memory - /// in the large object heap. This means the GC will not try to relocate this array, so the fact it remains pinned does not - /// negatively - /// affect memory management's compactification. - /// - private const int _blockCount = 32; - - /// - /// This default value passed in to Rent to use the default value for the pool. - /// - private const int AnySize = -1; - - /// - /// 4096 * 32 gives you a slabLength of 128k contiguous bytes allocated per slab - /// - private static readonly int _slabLength = _blockSize * _blockCount; - - /// - /// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking - /// objects - /// and add them to this collection. When memory is requested it is taken from here first, and when it is returned it is - /// re-added. - /// - private readonly ConcurrentQueue _blocks = new ConcurrentQueue(); - - private readonly object _disposeSync = new object(); - - /// - /// Thread-safe collection of slabs which have been allocated by this pool. As long as a slab is in this collection and - /// slab.IsActive, - /// the blocks will be added to _blocks when returned. - /// - private readonly ConcurrentStack _slabs = new ConcurrentStack(); - - /// - /// This is part of implementing the IDisposable pattern. - /// - private bool _isDisposed; // To detect redundant calls - - private int _totalAllocatedBlocks; - - /// - /// Max allocation block size for pooled blocks, - /// larger values can be leased but they will be disposed after use rather than returned to the pool. - /// - public override int MaxBufferSize { get; } = _blockSize; - - /// - /// The size of a block. 4096 is chosen because most operating systems use 4k pages. - /// - public static int BlockSize => _blockSize; - - public override IMemoryOwner Rent(int size = AnySize) - { - if (size > _blockSize) - { - MemoryPoolThrowHelper.ThrowArgumentOutOfRangeException_BufferRequestTooLarge(_blockSize); - } - - var block = Lease(); - return block; - } - - /// - /// Called to take a block from the pool. - /// - /// The block that is reserved for the called. It must be passed to Return when it is no longer being used. - private MemoryPoolBlock Lease() - { - if (_isDisposed) - { - MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPool); - } - - if (_blocks.TryDequeue(out var block)) - { - // block successfully taken from the stack - return it - - block.Lease(); - return block; - } - - // no blocks available - grow the pool - block = AllocateSlab(); - block.Lease(); - return block; - } - - /// - /// Internal method called when a block is requested and the pool is empty. It allocates one additional slab, creates all of - /// the - /// block tracking objects, and adds them all to the pool. - /// - private MemoryPoolBlock AllocateSlab() - { -#pragma warning disable CA2000 // Dispose objects before losing scope - var slab = MemoryPoolSlab.Create(_slabLength); -#pragma warning restore CA2000 // Dispose objects before losing scope - _slabs.Push(slab); - - var basePtr = slab.NativePointer; - // Page align the blocks - var offset = (int)((((ulong)basePtr + _blockSize - 1) & ~((uint)_blockSize - 1)) - (ulong)basePtr); - - var blockCount = (_slabLength - offset) / _blockSize; - Interlocked.Add(ref _totalAllocatedBlocks, blockCount); - - MemoryPoolBlock block = null; - - for (var i = 0; i < blockCount; i++) - { - block = new MemoryPoolBlock(this, slab, offset, _blockSize); - - if (i != blockCount - 1) // last block - { - Return(block); - } - - offset += _blockSize; - } - - return block; - } - - /// - /// Called to return a block to the pool. Once Return has been called the memory no longer belongs to the caller, and - /// Very Bad Things will happen if the memory is read of modified subsequently. If a caller fails to call Return and the - /// block tracking object is garbage collected, the block tracking object's finalizer will automatically re-create and - /// return - /// a new tracking object into the pool. This will only happen if there is a bug in the server, however it is necessary to - /// avoid - /// leaving "dead zones" in the slab due to lost block tracking objects. - /// - /// The block to return. It must have been acquired by calling Lease on the same memory pool instance. - internal void Return(MemoryPoolBlock block) - { - if (!_isDisposed) - { - _blocks.Enqueue(block); - } - else - { - GC.SuppressFinalize(block); - } - } - - // This method can ONLY be called from the finalizer of MemoryPoolBlock - internal void RefreshBlock(MemoryPoolSlab slab, int offset, int length) - { - lock (_disposeSync) - { - if (!_isDisposed && slab?.IsActive == true) - // Need to make a new object because this one is being finalized - // Note, this must be called within the _disposeSync lock because the block - // could be disposed at the same time as the finalizer. - { - Return(new MemoryPoolBlock(this, slab, offset, length)); - } - } - } - - protected override void Dispose(bool disposing) - { - if (_isDisposed) - { - return; - } - - lock (_disposeSync) - { - _isDisposed = true; - - if (disposing) - { - while (_slabs.TryPop(out var slab)) - // dispose managed state (managed objects). - { - slab.Dispose(); - } - } - - // Discard blocks in pool - while (_blocks.TryDequeue(out var block)) - { - GC.SuppressFinalize(block); - } - } - } - } -} diff --git a/Projects/Server/Events/SocketConnectionEvent.cs b/Projects/Server/Events/SocketConnectionEvent.cs index fa1f21a60..ce3ba08c5 100644 --- a/Projects/Server/Events/SocketConnectionEvent.cs +++ b/Projects/Server/Events/SocketConnectionEvent.cs @@ -14,19 +14,19 @@ *************************************************************************/ using System; -using Microsoft.AspNetCore.Connections; +using System.Net.Sockets; namespace Server { public class SocketConnectEventArgs : EventArgs { - public SocketConnectEventArgs(ConnectionContext c) + public SocketConnectEventArgs(Socket c) { - Context = c; + Connection = c; AllowConnection = true; } - public ConnectionContext Context { get; } + public Socket Connection { get; } public bool AllowConnection { get; set; } } diff --git a/Projects/Server/Network/ServerStartup.cs b/Projects/Server/Exceptions/InvalidThreadException.cs similarity index 61% rename from Projects/Server/Network/ServerStartup.cs rename to Projects/Server/Exceptions/InvalidThreadException.cs index 20d2678cc..de199ceb6 100644 --- a/Projects/Server/Network/ServerStartup.cs +++ b/Projects/Server/Exceptions/InvalidThreadException.cs @@ -2,7 +2,7 @@ * ModernUO * * Copyright 2019-2020 - ModernUO Development Team * * Email: hi@modernuo.com * - * File: ServerStartup.cs * + * File: InvalidThreadException.cs * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -13,25 +13,14 @@ * along with this program. If not, see . * *************************************************************************/ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; +using System; -namespace Server.Network +namespace Server.Exceptions { - public class ServerStartup + public class InvalidThreadException : Exception { - private readonly IMessagePumpService _messagePumpService; - public ServerStartup(IMessagePumpService messagePumpService) => _messagePumpService = messagePumpService; - - public void ConfigureServices(IServiceCollection services) + public InvalidThreadException(string methodName) : base($"{methodName} executed on an invalid thread.") { } - - public void Configure(IApplicationBuilder app) - { - // Run async? - Task.Run(() => Core.RunEventLoop(_messagePumpService)); - } } } diff --git a/Projects/Server/Exceptions/MaxConnectionsException.cs b/Projects/Server/Exceptions/MaxConnectionsException.cs new file mode 100644 index 000000000..f0b35954e --- /dev/null +++ b/Projects/Server/Exceptions/MaxConnectionsException.cs @@ -0,0 +1,26 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2020 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: MaxConnectionsException.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; + +namespace Server.Exceptions +{ + public class MaxConnectionsException : Exception + { + public MaxConnectionsException() : base("Maximum connections exceeded") + { + } + } +} diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index 3488d52eb..fb926306e 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -3404,16 +3404,9 @@ namespace Server { _processing = true; - if (m_DeltaQueue.Count >= 512) + for (var i = 0; i < m_DeltaQueue.Count; i++) { - Parallel.ForEach(m_DeltaQueue, i => i.ProcessDelta()); - } - else - { - for (var i = 0; i < m_DeltaQueue.Count; i++) - { - m_DeltaQueue[i].ProcessDelta(); - } + m_DeltaQueue[i].ProcessDelta(); } m_DeltaQueue.Clear(); diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index 1f4a14c23..24694cf2c 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -24,7 +24,6 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; using Server.Json; using Server.Network; @@ -41,23 +40,10 @@ namespace Server private static TimeSpan m_ProfileTime; private static bool? m_IsRunningFromXUnit; - /* - * DateTime.Now and DateTime.UtcNow are based on actual system clock time. - * The resolution is acceptable but large clock jumps are possible and cause issues. - * GetTickCount and GetTickCount64 have poor resolution. - * Stopwatch.GetTimestamp() (QueryPerformanceCounter) is high resolution, but - * somewhat expensive to call because of its difference to DateTime.Now, - * which is why Stopwatch has been used to verify HRT before calling GetTimestamp(), - * enabling the usage of DateTime.UtcNow instead. - */ - - private static readonly double m_HighFrequency = 1000.0 / Stopwatch.Frequency; - private static readonly double m_LowFrequency = 1000.0 / TimeSpan.TicksPerSecond; - private static int m_CycleIndex = 1; private static readonly float[] m_CyclesPerSecond = new float[100]; - private static readonly AutoResetEvent m_Signal = new AutoResetEvent(true); + // private static readonly AutoResetEvent m_Signal = new AutoResetEvent(true); private static int m_ItemCount; private static int m_MobileCount; @@ -115,12 +101,8 @@ namespace Server public static Thread Thread { get; private set; } - public static long TickCount => (long)Ticks; - - public static double Ticks => - Stopwatch.IsHighResolution - ? Stopwatch.GetTimestamp() * m_HighFrequency - : DateTime.UtcNow.Ticks * m_LowFrequency; + // Milliseconds + public static long TickCount => Stopwatch.GetTimestamp() * 1000L / Stopwatch.Frequency; public static bool MultiProcessor { get; private set; } @@ -345,7 +327,7 @@ namespace Server public static void Set() { - m_Signal.Set(); + // m_Signal.Set(); } public static void Main(string[] args) @@ -466,22 +448,16 @@ namespace Server m.Tiles.Force(); } + TcpServer.Start(); EventSink.InvokeServerStarted(); - - // Start net socket server - _tcpHost = TcpServer.CreateWebHostBuilder().Build(); - - // Run indefinitely and block - _tcpHost.RunAsync(ClosingTokenSource.Token).Wait(); + RunEventLoop(); } - private static IWebHost _tcpHost; - - public static void RunEventLoop(IMessagePumpService messagePumpService) + public static void RunEventLoop() { try { - var last = TickCount; + long last = TickCount; const int sampleInterval = 100; const float ticksPerSecond = 1000.0f * sampleInterval; @@ -490,18 +466,21 @@ namespace Server while (!Closing) { - m_Signal.WaitOne(); + // m_Signal.WaitOne(); - Task.WaitAll( - Task.Run(Mobile.ProcessDeltaQueue), - Task.Run(Item.ProcessDeltaQueue) - ); + Mobile.ProcessDeltaQueue(); + Item.ProcessDeltaQueue(); Timer.Slice(); - messagePumpService.DoWork(); + // Handle networking + TcpServer.Slice(); + NetState.HandleAllReceives(); + NetState.FlushAll(); NetState.ProcessDisposedQueue(); + // Execute other stuff + if (sample++ % sampleInterval != 0) { continue; diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index c858a9c2a..40afe92c1 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -8532,17 +8532,9 @@ namespace Server { _processing = true; - if (m_DeltaQueue.Count >= 512) + while (m_DeltaQueue.TryDequeue(out var m)) { - Parallel.ForEach(m_DeltaQueue, m => m.ProcessDelta()); - m_DeltaQueue.Clear(); - } - else - { - while (m_DeltaQueue.TryDequeue(out var m)) - { - m.ProcessDelta(); - } + m.ProcessDelta(); } _processing = false; diff --git a/Projects/Server/Network/EncodedReader.cs b/Projects/Server/Network/EncodedReader.cs index 33a46e267..bcf3a2d1e 100644 --- a/Projects/Server/Network/EncodedReader.cs +++ b/Projects/Server/Network/EncodedReader.cs @@ -18,9 +18,9 @@ namespace Server.Network : new Point3D(m_Reader.ReadInt16(), m_Reader.ReadInt16(), m_Reader.ReadByte()); public string ReadUnicodeStringSafe() => - m_Reader.ReadByte() != 2 ? string.Empty : m_Reader.ReadUnicodeStringSafe(m_Reader.ReadUInt16()); + m_Reader.ReadByte() != 2 ? string.Empty : m_Reader.ReadBigUniSafe(m_Reader.ReadUInt16()); public string ReadUnicodeString() => - m_Reader.ReadByte() != 2 ? string.Empty : m_Reader.ReadUnicodeString(m_Reader.ReadUInt16()); + m_Reader.ReadByte() != 2 ? string.Empty : m_Reader.ReadBigUni(m_Reader.ReadUInt16()); } } diff --git a/Projects/Server/Network/MessagePumpService.cs b/Projects/Server/Network/MessagePumpService.cs deleted file mode 100644 index 25c8eb9d0..000000000 --- a/Projects/Server/Network/MessagePumpService.cs +++ /dev/null @@ -1,69 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2020 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: MessagePumpService.cs * - * * - * This program is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, either version 3 of the License, or * - * (at your option) any later version. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program. If not, see . * - *************************************************************************/ - -using System.Buffers; -using System.Collections.Concurrent; - -namespace Server.Network -{ - public interface IMessagePumpService - { - void QueueWork(NetState ns, IMemoryOwner memOwner, int length, OnPacketReceive onReceive); - void DoWork(); - } - - public class MessagePumpService : IMessagePumpService - { - private readonly ConcurrentQueue m_WorkQueue = new ConcurrentQueue(); - - public void QueueWork(NetState ns, IMemoryOwner memOwner, int length, OnPacketReceive onReceive) - { - m_WorkQueue.Enqueue(new Work(ns, memOwner, length, onReceive)); - Core.Set(); - } - - public void DoWork() - { - var count = 0; - while (!m_WorkQueue.IsEmpty && count++ < 250) - { - if (!m_WorkQueue.TryDequeue(out var work)) - { - break; - } - - var seq = new ReadOnlySequence(work.MemoryOwner.Memory.Slice(0, work.Length)); - work.OnReceive(work.State, new PacketReader(seq)); - work.MemoryOwner.Dispose(); - } - } - - private class Work - { - public readonly int Length; - public readonly IMemoryOwner MemoryOwner; - public readonly OnPacketReceive OnReceive; - public readonly NetState State; - - public Work(NetState ns, IMemoryOwner memOwner, int length, OnPacketReceive onReceive) - { - State = ns; - MemoryOwner = memOwner; - OnReceive = onReceive; - Length = length; - } - } - } -} diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs index e87fa9e21..51291e4c1 100644 --- a/Projects/Server/Network/NetState/NetState.cs +++ b/Projects/Server/Network/NetState/NetState.cs @@ -1,13 +1,28 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2020 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: NetState.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; +using System.Runtime.CompilerServices; using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Connections; using Server.Accounting; +using Server.Exceptions; using Server.Gumps; using Server.HuePickers; using Server.Items; @@ -17,41 +32,66 @@ namespace Server.Network { public delegate void NetStateCreatedCallback(NetState ns); - public class AsyncState - { - public AsyncState(bool paused) => Paused = paused; - public bool Paused { get; set; } - } - public partial class NetState : IComparable { - private static readonly AsyncState m_PauseState = new AsyncState(true); - private static readonly AsyncState m_ResumeState = new AsyncState(false); - - private static AsyncState m_AsyncState = m_ResumeState; + private static int IncomingPipeSize = 1024 * 64; + private static int OutgoingPipeSize = 1024 * 256; + private static int GumpCap = 512; + private static int HuePickerCap = 512; + private static int MenuCap = 512; private static readonly ConcurrentQueue m_Disposed = new ConcurrentQueue(); + private static NetworkState m_NetworkState = NetworkState.ResumeState; + + public static NetStateCreatedCallback CreatedCallback { get; set; } + private readonly string m_ToString; - internal int m_AuthID; - private int m_Disposing; - - internal int m_Seed; private ClientVersion m_Version; + private byte[] m_IncomingBuffer; + private Pipe m_IncomingPipe; + private byte[] m_OutgoingBuffer; + private Pipe m_OutgoingPipe; + private long m_NextCheckActivity; + private volatile bool m_Running; - public NetState(ConnectionContext connection) + internal int m_AuthID; + internal int m_Seed; + + public static void Configure() { + IncomingPipeSize = ServerConfiguration.GetOrUpdateSetting("netstate.incomingPipeSize", IncomingPipeSize); + OutgoingPipeSize = ServerConfiguration.GetOrUpdateSetting("netstate.outgoingPipeSize", OutgoingPipeSize); + GumpCap = ServerConfiguration.GetOrUpdateSetting("netstate.gumpCap", GumpCap); + HuePickerCap = ServerConfiguration.GetOrUpdateSetting("netstate.huePickerCap", HuePickerCap); + MenuCap = ServerConfiguration.GetOrUpdateSetting("netstate.menuCap", MenuCap); + } + + public static void Initialize() + { + var checkAliveDuration = TimeSpan.FromMinutes(1.5); + Timer.DelayCall(checkAliveDuration, checkAliveDuration, CheckAllAlive); + } + + public NetState(Socket connection) + { + m_Running = false; Connection = connection; Seeded = false; Gumps = new List(); HuePickers = new List(); Menus = new List(); Trades = new List(); + m_IncomingBuffer = new byte[IncomingPipeSize]; + m_IncomingPipe = new Pipe(m_IncomingBuffer); + m_OutgoingBuffer = new byte[OutgoingPipeSize]; + m_OutgoingPipe = new Pipe(m_OutgoingBuffer); + m_NextCheckActivity = Core.TickCount + 30000; try { - Address = Utility.Intern(((IPEndPoint)Connection.RemoteEndPoint).Address); - m_ToString = Address.ToString(); + Address = Utility.Intern((Connection?.RemoteEndPoint as IPEndPoint)?.Address); + m_ToString = Address?.ToString() ?? "(error)"; } catch (Exception ex) { @@ -62,14 +102,6 @@ namespace Server.Network ConnectedOn = DateTime.UtcNow; - connection.ConnectionClosed.Register( - () => - { - TcpServer.Instances.Remove(this); - Dispose(); - } - ); - CreatedCallback?.Invoke(this); } @@ -80,21 +112,20 @@ namespace Server.Network public DateTime ThrottledUntil { get; set; } public IPAddress Address { get; } - public static AsyncState AsyncState => m_AsyncState; public IPacketEncoder PacketEncoder { get; set; } - public static NetStateCreatedCallback CreatedCallback { get; set; } - public bool SentFirstPacket { get; set; } public bool BlockAllPackets { get; set; } public List Trades { get; } + public bool Running => m_Running; + public bool Seeded { get; set; } - public ConnectionContext Connection { get; private set; } + public Socket Connection { get; private set; } public bool CompressionEnabled { get; set; } @@ -106,12 +137,6 @@ namespace Server.Network public List Menus { get; private set; } - public static int GumpCap { get; set; } = 512; - - public static int HuePickerCap { get; set; } = 512; - - public static int MenuCap { get; set; } = 512; - public CityInfo[] CityInfo { get; set; } public Mobile Mobile { get; set; } @@ -208,11 +233,13 @@ namespace Server.Network return newTrade.From.Container; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteConsole(string text) { Console.WriteLine("Client: {0}: {1}", this, text); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteConsole(string format, params object[] args) { WriteConsole(string.Format(format, args)); @@ -318,15 +345,15 @@ namespace Server.Network public static void Pause() { - m_AsyncState = Interlocked.Exchange(ref m_AsyncState, m_PauseState); + NetworkState.Pause(ref m_NetworkState); } public static void Resume() { - m_AsyncState = Interlocked.Exchange(ref m_AsyncState, m_ResumeState); + NetworkState.Resume(ref m_NetworkState); } - public virtual async void Send(Packet p) + public virtual void Send(Packet p) { if (Connection == null || BlockAllPackets) { @@ -334,83 +361,101 @@ namespace Server.Network return; } - var outPipe = Connection.Transport.Output; + var currentThread = Thread.CurrentThread; + + if (currentThread != Core.Thread) + { + Console.Error.WriteLine("Core: Attempted to send packet outside core thread! [{0}]", currentThread.ManagedThreadId); +#if DEBUG + throw new InvalidThreadException(nameof(Send)); +#endif + } + + var writer = m_OutgoingPipe.Writer; try { - // TODO: Rented memory - ReadOnlyMemory buffer = p.Compile(CompressionEnabled, out var length); + byte[] buffer = p.Compile(CompressionEnabled, out var length); if (buffer.Length > 0 && length > 0) { - var result = await outPipe.WriteAsync(buffer.Slice(0, length)); + var result = writer.GetAvailable(); - if (result.IsCanceled || result.IsCompleted) + if (result.Length >= length) { - Dispose(); - return; + result.CopyFrom(buffer.AsSpan(0, length)); + writer.Advance((uint)length); + + // Flush at the end of the game loop } + else + { + WriteConsole("Too much data pending, disconnecting..."); + Dispose(); + } + } + else + { + WriteConsole("Didn't write anything!"); } p.OnSend(); } - catch (SocketException ex) - { - Console.WriteLine(ex); - TraceException(ex); - Dispose(); - } catch (Exception ex) { +#if DEBUG Console.WriteLine(ex); + TraceException(ex); +#endif Dispose(); } } - public async Task ProcessIncoming(IMessagePumpService messagePumpService) + internal void Start() { - var inPipe = Connection.Transport.Input; + if (Connection == null || m_Running) + { + return; + } + + m_Running = true; + + ThreadPool.UnsafeQueueUserWorkItem(RecvTask, null); + ThreadPool.UnsafeQueueUserWorkItem(SendTask, null); + } + + private async void SendTask(object state) + { + var reader = m_OutgoingPipe.Reader; try { - while (true) + while (m_Running) { - if (AsyncState.Paused) + var result = await reader.Read(); + + if (result.Length <= 0) { continue; } - var result = await inPipe.ReadAsync(); - if (result.IsCanceled || result.IsCompleted) + var buffer = result.Buffer; + + var bytesWritten = await Connection.SendAsync(buffer, SocketFlags.None); + + if (bytesWritten > 0) { - return; + m_NextCheckActivity = Core.TickCount + 90000; + reader.Advance((uint)bytesWritten); } - - var seq = result.Buffer; - - if (seq.IsEmpty) - { - break; - } - - var pos = PacketHandlers.ProcessPacket(messagePumpService, this, seq); - - if (pos <= 0) - { - break; - } - - inPipe.AdvanceTo(seq.Slice(0, pos).End); } } - catch (SocketException ex) + catch (Exception ex) { +#if DEBUG Console.WriteLine(ex); TraceException(ex); - } - catch - { - // Console.WriteLine(ex); +#endif } finally { @@ -418,12 +463,165 @@ namespace Server.Network } } + private async void RecvTask(object state) + { + var socket = Connection; + var writer = m_IncomingPipe.Writer; + + try + { + while (m_Running) + { + if (m_NetworkState == NetworkState.PauseState) + { + continue; + } + + var result = writer.GetAvailable(); + + if (result.Length <= 0) + { + continue; + } + + var buffer = result.Buffer; + + var bytesWritten = await socket.ReceiveAsync(buffer, SocketFlags.None); + + if (bytesWritten <= 0) + { + break; + } + + writer.Advance((uint)bytesWritten); + m_NextCheckActivity = Core.TickCount + 90000; + + // No need to flush + } + } + catch (Exception ex) + { +#if DEBUG + Console.WriteLine(ex); + TraceException(ex); +#endif + } + finally + { + Dispose(); + } + } + + public static void HandleAllReceives() + { + var clients = TcpServer.Instances; + + for (int i = 0; i < clients.Count; ++i) + { + clients[i].HandleReceive(); + } + } + + public void HandleReceive() + { + if (Connection == null) + { + return; + } + + try + { + var reader = m_IncomingPipe.Reader; + + // Process as many packets as we can synchronously + while (true) + { + var result = reader.TryRead(); + + if (result.Length <= 0) + { + return; + } + + var bytesProcessed = PacketHandlers.ProcessPacket(this, result.Buffer); + + if (bytesProcessed <= 0) + { + // Error + // TODO: Throw exception instead? + if (bytesProcessed < 0) + { + Dispose(); + } + + return; + } + + reader.Advance((uint)bytesProcessed); + } + } + catch (Exception ex) + { +#if DEBUG + Console.WriteLine(ex); + TraceException(ex); +#endif + Dispose(); + } + } + + public void Flush() + { + if (Connection != null) + { + m_OutgoingPipe.Writer.Flush(); + } + } + + public static void FlushAll() + { + var clients = TcpServer.Instances; + + for (int i = 0; i < clients.Count; ++i) + { + clients[i].Flush(); + } + } + + public void CheckAlive(long curTicks) + { + if (Connection != null && m_NextCheckActivity - curTicks < 0) + { + WriteConsole("Disconnecting due to inactivity..."); + Dispose(); + } + } + + public static void CheckAllAlive() + { + try + { + long curTicks = Core.TickCount; + + var clients = TcpServer.Instances; + + for (int i = 0; i < clients.Count; ++i) + { + clients[i].CheckAlive(curTicks); + } + } + catch (Exception ex) + { + TraceException(ex); + } + } + public bool CheckEncrypted(int packetID) { if (!SentFirstPacket && packetID != 0xF0 && packetID != 0xF1 && packetID != 0xCF && packetID != 0x80 && packetID != 0x91 && packetID != 0xA4 && packetID != 0xEF) { - Console.WriteLine("Client: {0}: Encrypted client detected, disconnecting", this); + WriteConsole("Encrypted client detected, disconnecting"); Dispose(); return true; } @@ -456,26 +654,25 @@ namespace Server.Network public virtual void Dispose() { - var disposing = Interlocked.Exchange(ref m_Disposing, 1); - if (disposing == 1) + if (Connection == null || Interlocked.Exchange(ref m_Disposing, 1) != 0) { return; } + m_OutgoingPipe.Writer.Close(); + try { - Connection.Transport.Input.Complete(); - Connection.Transport.Output.Complete(); - Connection.Abort(); - Task.Run(Connection.DisposeAsync).Wait(); + Connection.Shutdown(SocketShutdown.Both); } catch (Exception ex) { TraceException(ex); } - - Connection = null; - m_Disposed.Enqueue(this); + finally + { + m_Disposed.Enqueue(this); + } } public static void ProcessDisposedQueue() @@ -498,6 +695,12 @@ namespace Server.Network ns.Mobile = null; } + ns.m_Running = false; + ns.Connection = null; + ns.m_IncomingBuffer = null; + ns.m_IncomingPipe = null; + ns.m_OutgoingBuffer = null; + ns.m_OutgoingPipe = null; ns.Gumps.Clear(); ns.Menus.Clear(); ns.HuePickers.Clear(); @@ -505,6 +708,8 @@ namespace Server.Network ns.ServerInfo = null; ns.CityInfo = null; + TcpServer.Instances.Remove(ns); + if (a != null) { ns.WriteConsole("Disconnected. [{0} Online] [{1}]", TcpServer.Instances.Count, a); diff --git a/Projects/Server/Network/NetworkState.cs b/Projects/Server/Network/NetworkState.cs new file mode 100644 index 000000000..359289f75 --- /dev/null +++ b/Projects/Server/Network/NetworkState.cs @@ -0,0 +1,38 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2020 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: NetworkState.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Server.Network +{ + public class NetworkState + { + public static readonly NetworkState PauseState = new NetworkState(true); + public static readonly NetworkState ResumeState = new NetworkState(false); + + public bool Paused { get; } + + internal NetworkState(bool paused) => Paused = paused; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Pause(ref NetworkState state) => + Interlocked.Exchange(ref state, PauseState)?.Paused != true; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Resume(ref NetworkState state) => + Interlocked.Exchange(ref state, ResumeState)?.Paused == true; + } +} diff --git a/Projects/Server/Network/Packet.cs b/Projects/Server/Network/Packet.cs index 43ac2c614..a2bbe7b55 100644 --- a/Projects/Server/Network/Packet.cs +++ b/Projects/Server/Network/Packet.cs @@ -193,9 +193,8 @@ namespace Server.Network if (compress) { - var buffer = ArrayPool.Shared.Rent(CompressorBufferSize); - - NetworkCompression.Compress(m_CompiledBuffer, 0, length, buffer, out length); + var compressorBuffer = ArrayPool.Shared.Rent(CompressorBufferSize); + NetworkCompression.Compress(m_CompiledBuffer, 0, length, compressorBuffer, out var compressedLength); if (length <= 0) { @@ -214,28 +213,23 @@ namespace Server.Network length ); op.WriteLine(new StackTrace()); + + ArrayPool.Shared.Return(compressorBuffer); } else { - m_CompiledLength = length; - - if ((m_State & State.Static) != 0) - { - m_CompiledBuffer = new byte[length]; - Buffer.BlockCopy(buffer, 0, m_CompiledBuffer, 0, length); - ArrayPool.Shared.Return(buffer); - } - else - { - m_CompiledBuffer = buffer; - m_State |= State.Buffered; - } + m_CompiledBuffer = compressorBuffer; + m_CompiledLength = compressedLength; } } - else if (length > 0) + else + { + m_CompiledLength = length; + } + + if (length > 0) { var old = m_CompiledBuffer; - m_CompiledLength = length; if ((m_State & State.Static) != 0) { @@ -243,11 +237,17 @@ namespace Server.Network } else { + // Release it later using Release() m_CompiledBuffer = ArrayPool.Shared.Rent(length); m_State |= State.Buffered; } Buffer.BlockCopy(old, 0, m_CompiledBuffer, 0, length); + + if (compress) + { + ArrayPool.Shared.Return(old); + } } PacketWriter.ReleaseInstance(Stream); diff --git a/Projects/Server/Network/PacketHandlers.cs b/Projects/Server/Network/PacketHandlers.cs index 1a28a8c43..b2564f5d1 100644 --- a/Projects/Server/Network/PacketHandlers.cs +++ b/Projects/Server/Network/PacketHandlers.cs @@ -1,5 +1,19 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2020 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: PacketHandlers.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + using System; -using System.Buffers; using System.Collections.Generic; using System.IO; using Server.ContextMenus; @@ -34,9 +48,6 @@ namespace Server.Network { public delegate void PlayCharCallback(NetState state, bool val); - private const int BadFood = unchecked((int)0xBAADF00D); - private const int BadUOTD = unchecked((int)0xFFCEFFCE); - private const int m_AuthIDWindowSize = 128; private static readonly PacketHandler[] m_6017Handlers = new PacketHandler[0x100]; @@ -55,8 +66,6 @@ namespace Server.Network private static readonly Dictionary m_AuthIDWindow = new Dictionary(m_AuthIDWindowSize); - private static readonly MemoryPool _memoryPool = SlabMemoryPoolFactory.Create(); - static PacketHandlers() { Register(0x00, 104, false, CreateCharacter); @@ -272,15 +281,11 @@ namespace Server.Network } } - public static int ProcessPacket(IMessagePumpService pump, NetState ns, in ReadOnlySequence seq) + public static int ProcessPacket(NetState ns, ArraySegment[] segments) { - var r = new PacketReader(seq); + var reader = new PacketReader(segments); - if (!r.TryReadByte(out var packetId)) - { - ns.Dispose(); - return -1; - } + var packetId = reader.ReadByte(); if (!ns.Seeded) { @@ -292,12 +297,11 @@ namespace Server.Network } else { - var seed = (packetId << 24) | (r.ReadByte() << 16) | (r.ReadByte() << 8) | r.ReadByte(); + var seed = (packetId << 24) | (reader.ReadByte() << 16) | (reader.ReadByte() << 8) | reader.ReadByte(); if (seed == 0) { - Console.WriteLine("Login: {0}: Invalid client detected, disconnecting", ns); - ns.Dispose(); + ns.WriteConsole("Invalid client detected, disconnecting"); return -1; } @@ -310,7 +314,6 @@ namespace Server.Network if (ns.CheckEncrypted(packetId)) { - ns.Dispose(); return -1; } @@ -319,34 +322,29 @@ namespace Server.Network if (handler == null) { - r.Trace(ns); + reader.Trace(ns); return -1; } var packetLength = handler.Length; - if (handler.Length <= 0 && r.Length >= 3) + if (handler.Length <= 0 && reader.Length >= 3) { - packetLength = r.ReadUInt16(); + packetLength = reader.ReadUInt16(); if (packetLength < 3) { - ns.Dispose(); return -1; } } - if (r.Length < packetLength) + // Not enough data, let's wait for more to come in + if (reader.Length < packetLength) { return 0; } if (handler.Ingame && ns.Mobile?.Deleted != false) { - Console.WriteLine( - "Client: {0}: Sent ingame packet (0x{1:X2}) without being attached to a valid mobile.", - ns, - packetId - ); - ns.Dispose(); + ns.WriteConsole("Sent ingame packet (0x{1:X2}) without being attached to a valid mobile.", ns, packetId); return -1; } @@ -357,14 +355,7 @@ namespace Server.Network ns.ThrottledUntil = DateTime.UtcNow + throttled; } - var packet = seq.Slice(r.Position); - var length = (int)packet.Length; - var memOwner = _memoryPool.Rent(length); - - // TODO: This is slow, find another way - packet.CopyTo(memOwner.Memory.Span); - - pump.QueueWork(ns, memOwner, length, handler.OnReceive); + handler.OnReceive(ns, reader); return packetLength; } @@ -403,9 +394,8 @@ namespace Server.Network { if (ph.Ingame && state.Mobile == null) { - Console.WriteLine( - "Client: {0}: Sent ingame packet (0xD7x{1:X2}) before having been attached to a mobile", - state, + state.WriteConsole( + "Sent ingame packet (0xD7x{0:X2}) before having been attached to a mobile", packetId ); state.Dispose(); @@ -432,7 +422,7 @@ namespace Server.Network if (targ != null) { - EventSink.InvokeRenameRequest(from, targ, reader.ReadStringSafe()); + EventSink.InvokeRenameRequest(from, targ, reader.ReadAsciiSafe()); } } @@ -526,7 +516,7 @@ namespace Server.Network if (flag == 0x02) { - var msgSize = (int)reader.Remaining; + var msgSize = reader.Remaining; if (msgSize / 7 > 100) { @@ -616,7 +606,7 @@ namespace Server.Network Serial serial = reader.ReadUInt32(); int unk = reader.ReadByte(); - var lang = reader.ReadString(3); + var lang = reader.ReadAscii(3); if (serial.IsItem) { @@ -692,10 +682,10 @@ namespace Server.Network int v1 = reader.ReadByte(); int v2 = reader.ReadUInt16(); int v3 = reader.ReadByte(); - var s1 = reader.ReadString(32); - var s2 = reader.ReadString(32); - var s3 = reader.ReadString(32); - var s4 = reader.ReadString(32); + var s1 = reader.ReadAscii(32); + var s2 = reader.ReadAscii(32); + var s3 = reader.ReadAscii(32); + var s4 = reader.ReadAscii(32); int v4 = reader.ReadUInt16(); int v5 = reader.ReadUInt16(); var v6 = reader.ReadInt32(); @@ -710,7 +700,7 @@ namespace Server.Network public static void TextCommand(NetState state, PacketReader reader) { int type = reader.ReadByte(); - var command = reader.ReadString(); + var command = reader.ReadAscii(); var m = state.Mobile; @@ -794,7 +784,7 @@ namespace Server.Network } default: { - Console.WriteLine("Client: {0}: Unknown text-command type 0x{1:X2}: {2}", state, type, command); + state.WriteConsole("Unknown text-command type 0x{0:X2}: {1}", state, type, command); break; } } @@ -805,7 +795,7 @@ namespace Server.Network var serial = reader.ReadUInt32(); var prompt = reader.ReadInt32(); var type = reader.ReadInt32(); - var text = reader.ReadStringSafe(); + var text = reader.ReadAsciiSafe(); if (text.Length > 128) { @@ -835,8 +825,8 @@ namespace Server.Network var serial = reader.ReadUInt32(); var prompt = reader.ReadInt32(); var type = reader.ReadInt32(); - var lang = reader.ReadString(4); - var text = reader.ReadUnicodeStringLESafe(); + var lang = reader.ReadAscii(4); + var text = reader.ReadLittleUniSafe(); if (text.Length > 128) { @@ -922,7 +912,7 @@ namespace Server.Network return; } - var text = reader.ReadUnicodeString(length); + var text = reader.ReadBigUni(length); EventSink.InvokeChangeProfileRequest(beholder, beheld, text); @@ -1259,7 +1249,7 @@ namespace Server.Network return; } - var text = reader.ReadUnicodeStringSafe(textLength); + var text = reader.ReadBigUniSafe(textLength); textEntries[j] = new TextRelay(entryID, text); } @@ -1336,7 +1326,7 @@ namespace Server.Network var type = (MessageType)reader.ReadByte(); int hue = reader.ReadInt16(); reader.ReadInt16(); // font - var text = reader.ReadStringSafe().Trim(); + var text = reader.ReadAsciiSafe().Trim(); if (text.Length <= 0 || text.Length > 128) { @@ -1358,7 +1348,7 @@ namespace Server.Network var type = (MessageType)reader.ReadByte(); int hue = reader.ReadInt16(); reader.ReadInt16(); // font - var lang = reader.ReadString(4); + var lang = reader.ReadAscii(4); string text; var isEncoded = (type & MessageType.Encoded) != 0; @@ -1401,13 +1391,13 @@ namespace Server.Network } } - text = reader.ReadUTF8StringSafe(); + text = reader.ReadUTF8Safe(); keywords = keyList.ToArray(); } else { - text = reader.ReadUnicodeStringSafe(); + text = reader.ReadBigUniSafe(); keywords = m_EmptyInts; } @@ -1603,9 +1593,8 @@ namespace Server.Network { if (state.Mobile == null) { - Console.WriteLine( - "Client: {0}: Sent in-game packet (0xBFx{1:X2}) before having been attached to a mobile", - state, + state.WriteConsole( + "Sent in-game packet (0xBFx{0:X2}) before having been attached to a mobile", packetID ); } @@ -1804,13 +1793,13 @@ namespace Server.Network PartyCommands.Handler?.OnPrivateMessage( state.Mobile, World.FindMobile(reader.ReadUInt32()), - reader.ReadUnicodeStringSafe() + reader.ReadBigUniSafe() ); } public static void PartyMessage_PublicMessage(NetState state, PacketReader reader) { - PartyCommands.Handler?.OnPublicMessage(state.Mobile, reader.ReadUnicodeStringSafe()); + PartyCommands.Handler?.OnPublicMessage(state.Mobile, reader.ReadBigUniSafe()); } public static void PartyMessage_SetCanLoot(NetState state, PacketReader reader) @@ -1980,7 +1969,7 @@ namespace Server.Network public static void Language(NetState state, PacketReader reader) { - var lang = reader.ReadString(4); + var lang = reader.ReadAscii(4); if (state.Mobile != null) { @@ -1991,12 +1980,12 @@ namespace Server.Network public static void AssistVersion(NetState state, PacketReader reader) { var unk = reader.ReadInt32(); - var av = reader.ReadString(); + var av = reader.ReadAscii(); } public static void ClientVersion(NetState state, PacketReader reader) { - var version = state.Version = new CV(reader.ReadString()); + var version = state.Version = new CV(reader.ReadAscii()); EventSink.InvokeClientVersionReceived(state, version); } @@ -2006,7 +1995,7 @@ namespace Server.Network reader.ReadUInt16(); int type = reader.ReadUInt16(); - var version = state.Version = new CV(reader.ReadString()); + var version = state.Version = new CV(reader.ReadAscii()); EventSink.InvokeClientVersionReceived(state, version); } @@ -2046,7 +2035,7 @@ namespace Server.Network { reader.ReadInt32(); // 0xEDEDEDED - var name = reader.ReadString(30); + var name = reader.ReadAscii(30); reader.Seek(2, SeekOrigin.Current); @@ -2074,7 +2063,7 @@ namespace Server.Network if (check != null && check.Map != Map.Internal && check != m) { - Console.WriteLine("Login: {0}: Account in use", state); + state.WriteConsole("Account in use"); state.Send(new PopupMessage(PMMessage.CharInWorld)); return; } @@ -2217,7 +2206,7 @@ namespace Server.Network var unk1 = reader.ReadInt32(); var unk2 = reader.ReadInt32(); int unk3 = reader.ReadByte(); - var name = reader.ReadString(30); + var name = reader.ReadAscii(30); reader.Seek(2, SeekOrigin.Current); var flags = reader.ReadInt32(); @@ -2292,7 +2281,7 @@ namespace Server.Network if (check != null && check.Map != Map.Internal) { - Console.WriteLine("Login: {0}: Account in use", state); + state.WriteConsole("Account in use"); state.Send(new PopupMessage(PMMessage.CharInWorld)); return; } @@ -2353,7 +2342,7 @@ namespace Server.Network var unk1 = reader.ReadInt32(); var unk2 = reader.ReadInt32(); int unk3 = reader.ReadByte(); - var name = reader.ReadString(30); + var name = reader.ReadAscii(30); reader.Seek(2, SeekOrigin.Current); var flags = reader.ReadInt32(); @@ -2417,7 +2406,7 @@ namespace Server.Network if (check != null && check.Map != Map.Internal) { - Console.WriteLine("Login: {0}: Account in use", state); + state.WriteConsole("Account in use"); state.Send(new PopupMessage(PMMessage.CharInWorld)); return; } @@ -2530,27 +2519,27 @@ namespace Server.Network } else if (ClientVerification) { - Console.WriteLine("Login: {0}: Invalid client detected, disconnecting", state); + state.WriteConsole("Invalid client detected, disconnecting"); state.Dispose(); return; } if (state.m_AuthID != 0 && authID != state.m_AuthID) { - Console.WriteLine("Login: {0}: Invalid client detected, disconnecting", state); + state.WriteConsole("Invalid client detected, disconnecting"); state.Dispose(); return; } if (state.m_AuthID == 0 && authID != state.m_Seed) { - Console.WriteLine("Login: {0}: Invalid client detected, disconnecting", state); + state.WriteConsole("Invalid client detected, disconnecting"); state.Dispose(); return; } - var username = reader.ReadString(30); - var password = reader.ReadString(30); + var username = reader.ReadAscii(30); + var password = reader.ReadAscii(30); var e = new GameLoginEventArgs(state, username, password); @@ -2559,7 +2548,7 @@ namespace Server.Network if (e.Accepted) { state.CityInfo = e.CityInfo; - state.CompressionEnabled = true; + // state.CompressionEnabled = true; state.Send(SupportedFeatures.Instantiate(state)); @@ -2606,7 +2595,7 @@ namespace Server.Network if (state.m_Seed == 0) { - Console.WriteLine("Login: {0}: Invalid client detected, disconnecting", state); + state.WriteConsole("Invalid client detected, disconnecting"); state.Dispose(); return; } @@ -2631,15 +2620,15 @@ namespace Server.Network var z = reader.ReadSByte(); var map = reader.ReadByte(); - var account = reader.ReadString(32); - var character = reader.ReadString(32); - var ip = reader.ReadString(15); + var account = reader.ReadAscii(32); + var character = reader.ReadAscii(32); + var ip = reader.ReadAscii(15); var unk1 = reader.ReadInt32(); var exception = reader.ReadInt32(); - var process = reader.ReadString(100); - var report = reader.ReadString(100); + var process = reader.ReadAscii(100); + var report = reader.ReadAscii(100); reader.ReadByte(); // 0x00 @@ -2663,8 +2652,8 @@ namespace Server.Network state.SentFirstPacket = true; - var username = reader.ReadString(30); - var password = reader.ReadString(30); + var username = reader.ReadAscii(30); + var password = reader.ReadAscii(30); var e = new AccountLoginEventArgs(state, username, password); @@ -2707,7 +2696,7 @@ namespace Server.Network state.Dispose(); } - public static void EquipMacro(NetState ns, PacketReader reader) + public static void EquipMacro(NetState state, PacketReader reader) { int count = reader.ReadByte(); var serialList = new List(count); @@ -2716,10 +2705,10 @@ namespace Server.Network serialList.Add(reader.ReadUInt32()); } - EventSink.InvokeEquipMacro(ns.Mobile, serialList); + EventSink.InvokeEquipMacro(state.Mobile, serialList); } - public static void UnequipMacro(NetState ns, PacketReader reader) + public static void UnequipMacro(NetState state, PacketReader reader) { int count = reader.ReadByte(); var layers = new List(count); @@ -2728,30 +2717,30 @@ namespace Server.Network layers.Add((Layer)reader.ReadUInt16()); } - EventSink.InvokeUnequipMacro(ns.Mobile, layers); + EventSink.InvokeUnequipMacro(state.Mobile, layers); } - public static void TargetedSpell(NetState ns, PacketReader reader) + public static void TargetedSpell(NetState state, PacketReader reader) { var spellId = (short)(reader.ReadInt16() - 1); // zero based; - EventSink.InvokeTargetedSpell(ns.Mobile, World.FindEntity(reader.ReadUInt32()), spellId); + EventSink.InvokeTargetedSpell(state.Mobile, World.FindEntity(reader.ReadUInt32()), spellId); } - public static void TargetedSkillUse(NetState ns, PacketReader reader) + public static void TargetedSkillUse(NetState state, PacketReader reader) { var skillId = reader.ReadInt16(); - EventSink.InvokeTargetedSkillUse(ns.Mobile, World.FindEntity(reader.ReadUInt32()), skillId); + EventSink.InvokeTargetedSkillUse(state.Mobile, World.FindEntity(reader.ReadUInt32()), skillId); } - public static void TargetByResourceMacro(NetState ns, PacketReader reader) + public static void TargetByResourceMacro(NetState state, PacketReader reader) { Serial serial = reader.ReadUInt32(); if (serial.IsItem) { - EventSink.InvokeTargetByResourceMacro(ns.Mobile, World.FindItem(serial), reader.ReadInt16()); + EventSink.InvokeTargetByResourceMacro(state.Mobile, World.FindItem(serial), reader.ReadInt16()); } } diff --git a/Projects/Server/Network/PacketReader.cs b/Projects/Server/Network/PacketReader.cs index 26623fe25..cf8b4b4f3 100644 --- a/Projects/Server/Network/PacketReader.cs +++ b/Projects/Server/Network/PacketReader.cs @@ -1,39 +1,68 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2020 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: PacketReader.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + using System; -using System.Buffers; +using System.Buffers.Binary; +using System.Data; using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; namespace Server.Network { public ref struct PacketReader { - private SequenceReader m_Reader; + public Span First; + public Span Second; - public SequencePosition Position => m_Reader.Position; - public long Length => m_Reader.Length; - public long Consumed => m_Reader.Consumed; - public long Remaining => m_Reader.Remaining; + public int Length { get; } + public int Position { get; private set; } + public int Remaining => Length - Position; - public PacketReader(ReadOnlySequence seq) => m_Reader = new SequenceReader(seq); + public PacketReader(ArraySegment[] buffers) + { + First = buffers[0]; + Second = buffers[1]; + Position = 0; + Length = First.Length + Second.Length; + } - public byte Peek() => m_Reader.TryPeek(out var value) ? value : (byte)0; + public PacketReader(Span first, Span second) + { + First = first; + Second = second; + Position = 0; + Length = first.Length + second.Length; + } public void Trace(NetState state) { + // We don't have data, so nothing to trace + if (First.Length == 0) + { + return; + } + try { using var sw = new StreamWriter("Packets.log", true); - var buffer = m_Reader.Sequence.ToArray(); - if (buffer.Length > 0) - { - sw.WriteLine("Client: {0}: Unhandled packet 0x{1:X2}", state, buffer[0]); - } + sw.WriteLine("Client: {0}: Unhandled packet 0x{1:X2}", state, First[0]); - using (var ms = new MemoryStream(buffer)) - { - Utility.FormatBuffer(sw, ms, buffer.Length); - } + Utility.FormatBuffer(sw, First.ToArray(), new Memory(Second.ToArray())); sw.WriteLine(); sw.WriteLine(); @@ -44,319 +73,279 @@ namespace Server.Network } } - public SequencePosition Seek(long offset, SeekOrigin origin) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() { - switch (origin) + if (Position < First.Length) { - case SeekOrigin.Begin: - if (offset < m_Reader.Consumed) - { - m_Reader.Rewind(m_Reader.Consumed - Math.Max(offset, 0L)); - } - else - { - m_Reader.Advance(offset - m_Reader.Consumed); - } - - break; - case SeekOrigin.Current: - if (offset < 0) - { - m_Reader.Rewind(Math.Min(m_Reader.Consumed, offset * -1)); - } - else - { - m_Reader.Advance(Math.Min(m_Reader.Remaining, offset)); - } - - break; - case SeekOrigin.End: - var count = m_Reader.Remaining - offset; - if (count < 0) - { - m_Reader.Rewind(count * -1); - } - else if (count > 0) - { - m_Reader.Advance(count); - } - - break; + return First[Position++]; } - return m_Reader.Position; + if (Position < Length) + { + return Second[Position++ - First.Length]; + } + + throw new OutOfMemoryException(); } - public bool TryReadByte(out byte value) => m_Reader.TryRead(out value); - - public int ReadInt32() => m_Reader.TryReadBigEndian(out int value) ? value : 0; - - public short ReadInt16() => m_Reader.TryReadBigEndian(out short value) ? value : (short)0; - - public byte ReadByte() => m_Reader.TryRead(out var value) ? value : (byte)0; - - public uint ReadUInt32() => (uint)ReadInt32(); - - public ushort ReadUInt16() => (ushort)ReadInt16(); - - public sbyte ReadSByte() => (sbyte)ReadByte(); - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ReadBoolean() => ReadByte() > 0; - public string ReadUnicodeStringLE() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public sbyte ReadSByte() => (sbyte)ReadByte(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public short ReadInt16() { - var sb = new StringBuilder(); + short value; - while (m_Reader.TryReadLittleEndian(out short c) && c != 0) + if (Position < First.Length) { - sb.Append((char)c); - } - - return sb.ToString(); - } - - public string ReadUnicodeStringLE(int fixedLength) - { - var sb = new StringBuilder(); - - while (fixedLength-- > 0 && m_Reader.TryReadLittleEndian(out short c) && c != 0) - { - sb.Append((char)c); - } - - if (fixedLength > 0) - { - m_Reader.Advance(fixedLength); - } - - return sb.ToString(); - } - - public string ReadUnicodeStringLESafe(int fixedLength) - { - var sb = new StringBuilder(); - - while (fixedLength-- > 0 && m_Reader.TryReadLittleEndian(out short c) && c != 0) - { - if (IsSafeChar(c)) + if (!BinaryPrimitives.TryReadInt16BigEndian(First.Slice(Position), out value)) { - sb.Append((char)c); + // Not enough space. Split the spans + return (short)((ReadByte() >> 8) | ReadByte()); } } - - if (fixedLength > 0) + else if (!BinaryPrimitives.TryReadInt16BigEndian(Second.Slice(Position - First.Length), out value)) { - m_Reader.Advance(fixedLength * 2); + throw new OutOfMemoryException(); } - return sb.ToString(); + Position += 2; + return value; } - public string ReadUnicodeStringLESafe() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort ReadUInt16() { - var sb = new StringBuilder(); + ushort value; - while (m_Reader.TryReadLittleEndian(out short c) && c != 0) + if (Position < First.Length) { - if (IsSafeChar(c)) + if (!BinaryPrimitives.TryReadUInt16BigEndian(First.Slice(Position), out value)) { - sb.Append((char)c); + // Not enough space. Split the spans + return (ushort)((ReadByte() >> 8) | ReadByte()); } } + else if (!BinaryPrimitives.TryReadUInt16BigEndian(Second.Slice(Position - First.Length), out value)) + { + throw new OutOfMemoryException(); + } - return sb.ToString(); + Position += 2; + return value; } - public string ReadUnicodeStringSafe() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadInt32() { - var sb = new StringBuilder(); + int value; - while (m_Reader.TryReadBigEndian(out short c) && c != 0) + if (Position < First.Length) { - if (IsSafeChar(c)) + if (!BinaryPrimitives.TryReadInt32BigEndian(First.Slice(Position), out value)) { - sb.Append((char)c); + // Not enough space. Split the spans + return (ReadByte() >> 24) | (ReadByte() >> 16) | (ReadByte() >> 8) | ReadByte(); } } - - return sb.ToString(); - } - - public string ReadUnicodeString() - { - var sb = new StringBuilder(); - - while (m_Reader.TryReadBigEndian(out short c) && c != 0) + else if (!BinaryPrimitives.TryReadInt32BigEndian(Second.Slice(Position - First.Length), out value)) { - sb.Append((char)c); + throw new OutOfMemoryException(); } - return sb.ToString(); + Position += 4; + return value; } - private static bool IsSafeChar(int c) => c >= 0x20 && c < 0xFFFE; - - public string ReadUTF8StringSafe(int fixedLength) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint ReadUInt32() { - string s; + uint value; - if (m_Reader.TryReadTo(out ReadOnlySpan span, (byte)'\0')) + if (Position < First.Length) { - s = Utility.UTF8.GetString(span.Length > fixedLength ? span.Slice(0, fixedLength) : span); + if (!BinaryPrimitives.TryReadUInt32BigEndian(First.Slice(Position), out value)) + { + // Not enough space. Split the spans + return (uint)((ReadByte() >> 24) | (ReadByte() >> 16) | (ReadByte() >> 8) | ReadByte()); + } + } + else if (!BinaryPrimitives.TryReadUInt32BigEndian(Second.Slice(Position - First.Length), out value)) + { + throw new OutOfMemoryException(); + } + + Position += 4; + return value; + } + + private static bool IsSafeChar(ushort c) => c >= 0x20 && c < 0xFFFE; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadString(Encoding encoding, bool safeString = false, int fixedLength = -1) where T : struct, IEquatable + { + int sizeT = Unsafe.SizeOf(); + + if (sizeT > 2) + { + throw new InvalidConstraintException("ReadString only accepts byte, sbyte, char, short, and ushort as a constraint"); + } + + bool isFixedLength = fixedLength > -1; + + var remaining = Remaining; + int size; + + // Not fixed length + if (isFixedLength) + { + size = fixedLength * sizeT; + if (fixedLength > Remaining) + { + throw new OutOfMemoryException(); + } } else { - var size = Math.Min(m_Reader.Remaining, fixedLength); - s = Utility.UTF8.GetString(m_Reader.Sequence.Slice(m_Reader.Position, size).ToArray()); - m_Reader.Advance(size); + size = remaining - (remaining & (sizeT - 1)); } - var sb = new StringBuilder(s.Length); + Span span; + int index; - for (var i = 0; i < s.Length; ++i) + if (Position < First.Length) { - if (IsSafeChar(s[i])) + // Find terminator + index = MemoryMarshal + .Cast(First.Slice(Position, Math.Min(size, First.Length))) + .IndexOf(default(T)) * sizeT; + + if (index < 0) { - sb.Append(s[i]); + remaining = size - First.Length; + // We don't have a terminator, but a fixed size to the end of the first span, so stop there + if (remaining <= 0) + { + index = First.Length; + } + else + { + index = MemoryMarshal + .Cast(Second.Slice(0, remaining)) + .IndexOf(default(T)) * sizeT; + + int secondLength = index < 0 ? remaining : index; + int length = First.Length + secondLength; + + // Assume no strings should be too long for the stack + Span bytes = stackalloc byte[length]; + First.Slice(Position).CopyTo(bytes); + Second.Slice(0, secondLength).CopyTo(bytes.Slice(First.Length)); + + Position += length; + return GetString(bytes, encoding, safeString); + } } - } - return sb.ToString(); - } - - public string ReadUTF8StringSafe() - { - string s; - - if (m_Reader.TryReadTo(out ReadOnlySpan span, (byte)'\0')) - { - s = Utility.UTF8.GetString(span); + span = First.Slice(Position, index); } else { - s = Utility.UTF8.GetString(m_Reader.Sequence.Slice(m_Reader.Position, m_Reader.Remaining).ToArray()); - m_Reader.Advance(m_Reader.Remaining); - } + span = Second.Slice( Position - First.Length, Math.Min(remaining, size)); + index = MemoryMarshal.Cast(span).IndexOf(default(T)) * sizeT; - var sb = new StringBuilder(s.Length); - - for (var i = 0; i < s.Length; ++i) - { - if (IsSafeChar(s[i])) + if (index >= 0) { - sb.Append(s[i]); + span = span.Slice(0, index); } } - return sb.ToString(); + Position += isFixedLength ? size : index; + return GetString(span, encoding, safeString); } - public string ReadUTF8String() => - Utility.UTF8.GetString( - m_Reader.TryReadTo(out ReadOnlySpan span, (byte)'\0') - ? span - : m_Reader.Sequence.Slice(m_Reader.Position, m_Reader.Remaining).ToArray() - ); - - public string ReadString() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetString(Span span, Encoding encoding, bool safeString = false) { - var sb = new StringBuilder(); + string s = encoding.GetString(span); - while (m_Reader.TryRead(out var c)) + if (!safeString) { - sb.Append((char)c); + return s; } - return sb.ToString(); - } + ReadOnlySpan chars = s.AsSpan(); - public string ReadStringSafe() - { - var sb = new StringBuilder(); + StringBuilder stringBuilder = null; - while (m_Reader.TryRead(out var c)) + for (int i = 0, last = 0; i < chars.Length; i++) { - if (IsSafeChar(c)) + if (!IsSafeChar(chars[i]) || stringBuilder != null && i == chars.Length - 1) { - sb.Append((char)c); + (stringBuilder ??= new StringBuilder()).Append(chars.Slice(last, i - last)); + last = i + 1; // Skip the unsafe char } } - return sb.ToString(); + return stringBuilder?.ToString() ?? s; } - public string ReadUnicodeStringSafe(int fixedLength) - { - var sb = new StringBuilder(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadLittleUniSafe(int fixedLength) => ReadString(Utility.UnicodeLE, true, fixedLength); - while (fixedLength-- > 0 && m_Reader.TryReadBigEndian(out short c) && c != 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadLittleUniSafe() => ReadString(Utility.UnicodeLE, true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadLittleUni(int fixedLength) => ReadString(Utility.UnicodeLE, false, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadLittleUni() => ReadString(Utility.UnicodeLE); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadBigUniSafe(int fixedLength) => ReadString(Utility.Unicode, true, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadBigUniSafe() => ReadString(Utility.Unicode, true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadBigUni(int fixedLength) => ReadString(Utility.Unicode, false, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadBigUni() => ReadString(Utility.Unicode); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadUTF8Safe(int fixedLength) => ReadString(Utility.UTF8, true, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadUTF8Safe() => ReadString(Utility.UTF8, true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadUTF8() => ReadString(Utility.UTF8); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadAsciiSafe(int fixedLength) => ReadString(Encoding.ASCII, true, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadAsciiSafe() => ReadString(Encoding.ASCII, true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadAscii(int fixedLength) => ReadString(Encoding.ASCII, false, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadAscii() => ReadString(Encoding.ASCII); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Seek(int offset, SeekOrigin origin) => + Position = origin switch { - if (IsSafeChar(c)) - { - sb.Append((char)c); - } - } - - if (fixedLength > 0) - { - m_Reader.Advance(fixedLength * 2); - } - - return sb.ToString(); - } - - public string ReadUnicodeString(int fixedLength) - { - var sb = new StringBuilder(); - - while (fixedLength-- > 0 && m_Reader.TryReadBigEndian(out short c) && c != 0) - { - sb.Append((char)c); - } - - if (fixedLength > 0) - { - m_Reader.Advance(fixedLength * 2); - } - - return sb.ToString(); - } - - public string ReadStringSafe(int fixedLength) - { - var sb = new StringBuilder(); - - while (fixedLength-- > 0 && m_Reader.TryRead(out var c) && c != 0) - { - if (IsSafeChar(c)) - { - sb.Append((char)c); - } - } - - if (fixedLength > 0) - { - m_Reader.Advance(fixedLength); - } - - return sb.ToString(); - } - - public string ReadString(int fixedLength) - { - var sb = new StringBuilder(); - - while (fixedLength-- > 0 && m_Reader.TryRead(out var c) && c != 0) - { - sb.Append((char)c); - } - - if (fixedLength > 0) - { - m_Reader.Advance(fixedLength); - } - - return sb.ToString(); - } + SeekOrigin.Begin => offset, + SeekOrigin.End => Length - offset, + _ => Position + offset // Current + }; } } diff --git a/Projects/Server/Network/Packets/Old Packets/MessagePackets.cs b/Projects/Server/Network/Packets/Old Packets/MessagePackets.cs index 9adae8fd4..ab896f8ae 100644 --- a/Projects/Server/Network/Packets/Old Packets/MessagePackets.cs +++ b/Projects/Server/Network/Packets/Old Packets/MessagePackets.cs @@ -32,8 +32,7 @@ namespace Server.Network private static readonly MessageLocalized[] m_Cache_CliLocCmp = new MessageLocalized[5000]; public MessageLocalized( - Serial serial, int graphic, MessageType type, int hue, int font, int number, string name, - string args + Serial serial, int graphic, MessageType type, int hue, int font, int number, string name, string args ) : base(0xC1) { name ??= ""; diff --git a/Projects/Server/Network/Pipe.cs b/Projects/Server/Network/Pipe.cs new file mode 100644 index 000000000..63c1c0643 --- /dev/null +++ b/Projects/Server/Network/Pipe.cs @@ -0,0 +1,356 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2020 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Pipe.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Server.Network +{ + public interface IPipeTask : INotifyCompletion + { + public IPipeTask GetAwaiter(); + + public bool IsCompleted { get; } + + public T GetResult(); + + public void OnCompleted(Action continuation); + } + + public class Pipe + { + public struct Result + { + public ArraySegment[] Buffer { get; } + public bool IsClosed { get; set; } + + public int Length + { + get + { + var length = 0; + for (int i = 0; i < Buffer.Length; i++) + { + length += Buffer[i].Count; + } + + return length; + } + } + + public void CopyFrom(ReadOnlySpan bytes) + { + var remaining = bytes.Length; + var offset = 0; + + if (remaining == 0) + { + return; + } + + for (int i = 0; i < Buffer.Length; i++) + { + var buffer = Buffer[i]; + var sz = Math.Min(remaining, buffer.Count); + bytes.Slice(offset, sz).CopyTo(buffer); + + remaining -= sz; + offset += sz; + + if (remaining == 0) + { + return; + } + } + + throw new OutOfMemoryException(); + } + + public Result(int segments) + { + IsClosed = false; + Buffer = new ArraySegment[segments]; + } + } + + public class PipeWriter + { + private readonly Pipe _pipe; + + public PipeWriter(Pipe pipe) => _pipe = pipe; + + public Result GetAvailable() + { + var read = _pipe._readIdx; + var write = _pipe._writeIdx; + + var result = new Result(2) { IsClosed = _pipe._closed }; + + if (read <= write) + { + var readZero = read == 0; + var sz = _pipe.Size - write - (readZero ? 1 : 0); + + result.Buffer[0] = sz == 0 ? ArraySegment.Empty : new ArraySegment(_pipe._buffer, (int)write, (int)sz); + result.Buffer[1] = readZero ? ArraySegment.Empty : new ArraySegment(_pipe._buffer, 0, (int)read - 1); + } + else + { + var sz = read - write - 1; + + result.Buffer[0] = sz == 0 ? ArraySegment.Empty : new ArraySegment(_pipe._buffer, (int)write, (int)sz); + result.Buffer[1] = ArraySegment.Empty; + } + + return result; + } + + public void Advance(uint bytes) + { + var read = _pipe._readIdx; + var write = _pipe._writeIdx; + + if (bytes == 0) + { + return; + } + + if (bytes > _pipe.Size - 1) + { + throw new InvalidOperationException(); + } + + if (read <= write) + { + if (bytes > read + _pipe.Size - write - 1) + { + throw new InvalidOperationException(); + } + + var sz = Math.Min(bytes, _pipe.Size - write); + + write += sz; + if (write > _pipe.Size - 1) + { + write = 0; + } + bytes -= sz; + + if (bytes > 0) + { + if (bytes >= read) + { + throw new InvalidOperationException(); + } + + write = bytes; + } + } + else + { + if (bytes > read - write - 1) + { + throw new InvalidOperationException(); + } + + write += bytes; + } + + // It's never valid to advance the write pointer to become equal to + // the read pointer. Check that here. + if (write == read) + { + throw new InvalidOperationException("Write index equals read index after advance"); + } + + _pipe._writeIdx = write; + } + + public void Close() + { + _pipe._closed = true; + + Flush(); + } + + public void Flush() + { + var waiting = _pipe._awaitBeginning; + + if (!waiting) + { + return; + } + + Action continuation; + + do + { + continuation = _pipe._readerContinuation; + } while (continuation == null); + + _pipe._readerContinuation = null; + _pipe._awaitBeginning = false; + + ThreadPool.UnsafeQueueUserWorkItem(state => continuation(), true); + } + } + + public class PipeReader : IPipeTask> + { + private readonly Pipe _pipe; + + internal PipeReader(Pipe pipe) => _pipe = pipe; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint GetRemaining() + { + var read = _pipe._readIdx; + var write = _pipe._writeIdx; + + if (read <= write) + { + return write - read; + } + + return write + _pipe.Size - read; + } + + public Result TryRead() + { + var read = _pipe._readIdx; + var write = _pipe._writeIdx; + + var result = new Result(2) { IsClosed = _pipe._closed }; + + if (read <= write) + { + result.Buffer[0] = write - read == 0 ? ArraySegment.Empty : new ArraySegment(_pipe._buffer, (int)read, (int)(write - read)); + result.Buffer[1] = ArraySegment.Empty; + } + else + { + result.Buffer[0] = _pipe.Size - read == 0 ? ArraySegment.Empty : new ArraySegment(_pipe._buffer, (int)read, (int)(_pipe.Size - read)); + result.Buffer[1] = write == 0 ? ArraySegment.Empty : new ArraySegment(_pipe._buffer, 0, (int)write); + } + + return result; + } + + public IPipeTask> Read() + { + if (_pipe._awaitBeginning) + { + throw new Exception("Double await on reader"); + } + + return this; + } + + public void Advance(uint bytes) + { + var read = _pipe._readIdx; + var write = _pipe._writeIdx; + + if (read <= write) + { + if (bytes > write - read) + { + throw new InvalidOperationException(); + } + + read += bytes; + } + else + { + var sz = Math.Min(bytes, _pipe.Size - read); + + read += sz; + if (read > _pipe.Size - 1) + { + read = 0; + } + bytes -= sz; + + if (bytes > 0) + { + if (bytes > write) + { + throw new InvalidOperationException(); + } + + read = bytes; + } + } + + _pipe._readIdx = read; + } + + #region Awaitable + + // The following makes it possible to await the reader. Do not use any of this directly. + + public IPipeTask> GetAwaiter() => this; + + public bool IsCompleted + { + get + { + if (GetRemaining() > 0) + { + return true; + } + + _pipe._awaitBeginning = true; + return false; + } + } + + public Result GetResult() => TryRead(); + + public void OnCompleted(Action continuation) => _pipe._readerContinuation = continuation; + + #endregion + } + + private readonly T[] _buffer; + private volatile uint _writeIdx; + private volatile uint _readIdx; + private bool _closed; + + public PipeWriter Writer { get; } + public PipeReader Reader { get; } + + public uint Size => (uint)_buffer.Length; + + public Pipe(T[] buf) + { + _buffer = buf; + _writeIdx = 0; + _readIdx = 0; + _closed = false; + + Writer = new PipeWriter(this); + Reader = new PipeReader(this); + } + + #region Awaitable + private volatile bool _awaitBeginning; + private volatile Action _readerContinuation; + + #endregion + } +} diff --git a/Projects/Server/Network/ServerConnectionHandler.cs b/Projects/Server/Network/ServerConnectionHandler.cs deleted file mode 100644 index 1610d4378..000000000 --- a/Projects/Server/Network/ServerConnectionHandler.cs +++ /dev/null @@ -1,91 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2020 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: ServerConnectionHandler.cs * - * * - * This program is free software: you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation, either version 3 of the License, or * - * (at your option) any later version. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program. If not, see . * - *************************************************************************/ - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Connections; -using Microsoft.Extensions.Logging; - -namespace Server.Network -{ - public class ServerConnectionHandler : ConnectionHandler - { - private readonly ILogger _logger; - private readonly IMessagePumpService _messagePumpService; - - public ServerConnectionHandler( - IMessagePumpService messagePumpService, - ILogger logger - ) - { - _messagePumpService = messagePumpService; - _logger = logger; - } - - public override async Task OnConnectedAsync(ConnectionContext connection) - { - if (!VerifySocket(connection)) - { - Release(connection); - return; - } - - var ns = new NetState(connection); - TcpServer.Instances.Add(ns); - Console.WriteLine($"Client: {ns}: Connected. [{TcpServer.Instances.Count} Online]"); - - await ns.ProcessIncoming(_messagePumpService).ConfigureAwait(false); - } - - private static bool VerifySocket(ConnectionContext connection) - { - try - { - var args = new SocketConnectEventArgs(connection); - - EventSink.InvokeSocketConnect(args); - - return args.AllowConnection; - } - catch (Exception ex) - { - NetState.TraceException(ex); - return false; - } - } - - private static void Release(ConnectionContext connection) - { - try - { - connection.Abort(new ConnectionAbortedException("Failed socket verification.")); - } - catch (Exception ex) - { - NetState.TraceException(ex); - } - - try - { - // TODO: Is this needed? - connection.DisposeAsync(); - } - catch (Exception ex) - { - NetState.TraceException(ex); - } - } - } -} diff --git a/Projects/Server/Network/TcpServer.cs b/Projects/Server/Network/TcpServer.cs index 86dbc0f77..dd1a705db 100644 --- a/Projects/Server/Network/TcpServer.cs +++ b/Projects/Server/Network/TcpServer.cs @@ -14,79 +14,211 @@ *************************************************************************/ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.NetworkInformation; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; +using System.Net.Sockets; +using Server.Exceptions; namespace Server.Network { public static class TcpServer { - private static IPAddress[] m_ListeningAddresses; + private static NetworkState m_NetworkState = NetworkState.ResumeState; - // Make this thread safe - public static List Instances { get; } = new List(); + // Sanity. 256 * 1024 * 5000 = ~1.3GB of ram + public static int MaxConnections { get; set; } = 5000; - public static IWebHostBuilder CreateWebHostBuilder(string[] args = null) => - WebHost.CreateDefaultBuilder(args) - .UseSetting(WebHostDefaults.SuppressStatusMessagesKey, "True") - .ConfigureServices(services => { services.AddSingleton(new MessagePumpService()); }) - .UseKestrel( - options => - { - foreach (var ipep in ServerConfiguration.Listeners) - { - options.Listen(ipep, builder => { builder.UseConnectionHandler(); }); - m_ListeningAddresses = GetListeningAddresses(ipep); - DisplayListener(ipep); - } + private const long _listenerErrorMessageDelay = 10000; // 10 seconds + private static long _nextMaximumSocketsReachedMessage; - // Webservices here - } - ) - .UseLibuv() - .UseStartup(); + // AccountLoginReject BadComm + private static readonly byte[] socketRejected = { 0x82, 0xFF }; - public static IPAddress[] GetListeningAddresses(IPEndPoint ipep) + public static IPEndPoint[] ListeningAddresses { get; private set; } + public static TcpListener[] Listeners { get; private set; } + public static List Instances { get; } = new List(128); + + public static ConcurrentQueue m_ConnectedQueue = new ConcurrentQueue(); + + public static void Configure() { - if (m_ListeningAddresses != null) - { - return m_ListeningAddresses; - } - - var list = new List(); - foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces()) - { - var properties = adapter.GetIPProperties(); - foreach (var unicast in properties.UnicastAddresses) - { - if (ipep.AddressFamily == unicast.Address.AddressFamily) - { - list.Add(unicast.Address); - } - } - } - - return list.ToArray(); + MaxConnections = ServerConfiguration.GetOrUpdateSetting("tcpServer.maxConnections", MaxConnections); } - private static void DisplayListener(IPEndPoint ipep) + public static void Start() { - if (ipep.Address.Equals(IPAddress.Any) || ipep.Address.Equals(IPAddress.IPv6Any)) + HashSet listeningAddresses = new HashSet(); + List listeners = new List(); + + foreach (var ipep in ServerConfiguration.Listeners) { - foreach (var ip in m_ListeningAddresses) + var listener = CreateListener(ipep); + if (listener == null) { - Console.WriteLine("Listening: {0}:{1}", ip, ipep.Port); + continue; } + + if (ipep.Address.Equals(IPAddress.Any) || ipep.Address.Equals(IPAddress.IPv6Any)) + { + listeningAddresses.UnionWith(GetListeningAddresses(ipep)); + } + else + { + listeningAddresses.Add(ipep); + } + + listeners.Add(listener); + listener.BeginAcceptingSockets(); } - else + + foreach (var ipep in listeningAddresses) { Console.WriteLine("Listening: {0}:{1}", ipep.Address, ipep.Port); } + + ListeningAddresses = listeningAddresses.ToArray(); + Listeners = listeners.ToArray(); + } + + public static IEnumerable GetListeningAddresses(IPEndPoint ipep) => + NetworkInterface.GetAllNetworkInterfaces().SelectMany(adapter => + adapter.GetIPProperties().UnicastAddresses + .Where(uip => ipep.AddressFamily == uip.Address.AddressFamily) + .Select(uip => new IPEndPoint(uip.Address, ipep.Port)) + ); + + public static TcpListener CreateListener(IPEndPoint ipep) + { + var listener = new TcpListener(ipep); + listener.Server.ExclusiveAddressUse = false; + + try + { + listener.Start(8); + return listener; + } + catch (SocketException se) + { + // WSAEADDRINUSE + if (se.ErrorCode == 10048) + { + Console.WriteLine("Listener: {0}:{1}: Failed (In Use)", ipep.Address, ipep.Port); + } + // WSAEADDRNOTAVAIL + else if (se.ErrorCode == 10049) + { + Console.WriteLine("Listener {0}:{1}: Failed (Unavailable)", ipep.Address, ipep.Port); + } + else + { + Console.WriteLine("Listener Exception:"); + Console.WriteLine(se); + } + } + + return null; + } + + /** + * Pauses the TcpServer and stops accepting new sockets. + * This is thread-safe without using locks. + */ + public static void Pause() + { + NetworkState.Pause(ref m_NetworkState); + } + + /** + * Resumes accepting sockets on the TcpServer. + * This is thread-safe using a lock on the listeners + */ + public static void Resume() + { + if (!NetworkState.Resume(ref m_NetworkState)) + { + return; + } + + lock (Listeners) + { + foreach (var listener in Listeners) + { + listener.BeginAcceptingSockets(); + } + } + } + + public static void Slice() + { + int count = 0; + + while (count++ < 250) + { + if (!m_ConnectedQueue.TryDequeue(out var ns)) + { + break; + } + + Instances.Add(ns); + ns.WriteConsole("Connected. [{0} Online]", Instances.Count); + } + } + + private static async void BeginAcceptingSockets(this TcpListener listener) + { + while (true) + { + if (m_NetworkState.Paused) + { + return; + } + + Socket socket = null; + + try + { + socket = await listener.AcceptSocketAsync(); + if (Instances.Count >= MaxConnections) + { + socket.Send(socketRejected, SocketFlags.None); + socket.Shutdown(SocketShutdown.Both); + socket.Close(); + throw new MaxConnectionsException(); + } + + var args = new SocketConnectEventArgs(socket); + EventSink.InvokeSocketConnect(args); + + if (!args.AllowConnection) + { + socket.Send(socketRejected, SocketFlags.None); + socket.Shutdown(SocketShutdown.Both); + socket.Close(); + } + } + catch (MaxConnectionsException) + { + var ticks = Core.TickCount; + + if (_nextMaximumSocketsReachedMessage <= ticks) + { + var ipep = (IPEndPoint)socket!.RemoteEndPoint; + Console.WriteLine("Listener {0}:{1}: Failed (Maximum connections reached)", ipep.Address, ipep.Port); + _nextMaximumSocketsReachedMessage = ticks + _listenerErrorMessageDelay; + } + } + catch + { + // ignored + } + + var ns = new NetState(socket); + m_ConnectedQueue.Enqueue(ns); + ns.Start(); + } } } } diff --git a/Projects/Server/Pipelines/DuplexPipe.cs b/Projects/Server/Pipelines/DuplexPipe.cs deleted file mode 100644 index f7d6142ed..000000000 --- a/Projects/Server/Pipelines/DuplexPipe.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace System.IO.Pipelines -{ - public class DuplexPipe : IDuplexPipe - { - public DuplexPipe(PipeReader reader, PipeWriter writer) - { - Input = reader; - Output = writer; - } - - public PipeReader Input { get; } - - public PipeWriter Output { get; } - - public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions) - { - var input = new Pipe(inputOptions); - var output = new Pipe(outputOptions); - - var transportToApplication = new DuplexPipe(output.Reader, input.Writer); - var applicationToTransport = new DuplexPipe(input.Reader, output.Writer); - - return new DuplexPipePair(applicationToTransport, transportToApplication); - } - - // This class exists to work around issues with value tuple on .NET Framework - public readonly struct DuplexPipePair - { - public IDuplexPipe Transport { get; } - public IDuplexPipe Application { get; } - - public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application) - { - Transport = transport; - Application = application; - } - } - } -} diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index 4dc2e800e..80bb024bc 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -12,23 +12,10 @@ - - - - - - - - - - - - - @@ -37,17 +24,12 @@ - - - - all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/Projects/UOContent/Misc/HexStringConverter.cs b/Projects/Server/Utilities/HexStringConverter.cs similarity index 63% rename from Projects/UOContent/Misc/HexStringConverter.cs rename to Projects/Server/Utilities/HexStringConverter.cs index 7715ea0f8..09891c71a 100644 --- a/Projects/UOContent/Misc/HexStringConverter.cs +++ b/Projects/Server/Utilities/HexStringConverter.cs @@ -1,6 +1,21 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2020 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: HexStringConverter.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + using System; -namespace Server.Misc +namespace Server { public class HexStringConverter { diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index aaa05ed6e..60c092fed 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -14,7 +14,7 @@ namespace Server { public static class Utility { - private static Encoding m_UTF8, m_UTF8WithEncoding; + private static Encoding m_UTF8, m_UTF8WithEncoding, m_Unicode, m_UnicodeLE; private static Dictionary _ipAddressTable; @@ -103,6 +103,8 @@ namespace Server public static Encoding UTF8 => m_UTF8 ??= new UTF8Encoding(false, false); public static Encoding UTF8WithEncoding => m_UTF8WithEncoding ??= new UTF8Encoding(true, false); + public static Encoding Unicode => m_Unicode ??= new UnicodeEncoding(true, false, false); + public static Encoding UnicodeLE => m_UnicodeLE ??= new UnicodeEncoding(false, false, false); public static void Separate(StringBuilder sb, string value, string separator) { @@ -126,6 +128,11 @@ namespace Server public static IPAddress Intern(IPAddress ipAddress) { + if (ipAddress == null) + { + return null; + } + _ipAddressTable ??= new Dictionary(); if (!_ipAddressTable.TryGetValue(ipAddress, out var interned)) diff --git a/Projects/UOContent/Accounting/AccessRestrictions.cs b/Projects/UOContent/Accounting/AccessRestrictions.cs index c35ef0efa..6b9bdc06e 100644 --- a/Projects/UOContent/Accounting/AccessRestrictions.cs +++ b/Projects/UOContent/Accounting/AccessRestrictions.cs @@ -5,7 +5,7 @@ using Server.Misc; namespace Server { - public class AccessRestrictions + public static class AccessRestrictions { public static void Initialize() { @@ -16,7 +16,7 @@ namespace Server { try { - var ip = ((IPEndPoint)e.Context.RemoteEndPoint).Address; + var ip = ((IPEndPoint)e.Connection.RemoteEndPoint).Address; if (Firewall.IsBlocked(ip)) { diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index 5ac1e407a..28ec4fcb9 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -252,7 +252,7 @@ namespace Server.Misc } else { - Console.WriteLine("Client: {0}: Deleting character {1} (0x{2:X})", state, index, m.Serial.Value); + state.WriteConsole("Deleting character {0} (0x{1:X})", index, m.Serial.Value); acct.Comments.Add(new AccountComment("System", $"Character #{index + 1} {m} deleted by {state}")); diff --git a/Projects/UOContent/Engines/Chat/Chat.cs b/Projects/UOContent/Engines/Chat/Chat.cs index b15bf25d2..46184e8e6 100644 --- a/Projects/UOContent/Engines/Chat/Chat.cs +++ b/Projects/UOContent/Engines/Chat/Chat.cs @@ -73,15 +73,15 @@ namespace Server.Engines.Chat return; } - var lang = reader.ReadStringSafe(4); + var lang = reader.ReadAsciiSafe(4); int actionID = reader.ReadInt16(); - var param = reader.ReadUnicodeString(); + var param = reader.ReadBigUniSafe(); var handler = ChatActionHandlers.GetHandler(actionID); if (handler == null) { - Console.WriteLine("Client: {0}: Unknown chat action 0x{1:X}: {2}", state, actionID, param); + state.WriteConsole("Unknown chat action 0x{0:X}: {1}", actionID, param); return; } diff --git a/Projects/UOContent/Items/Addons/BaseAddonContainer.cs b/Projects/UOContent/Items/Addons/BaseAddonContainer.cs index 60a30f56f..8a18cbc9e 100644 --- a/Projects/UOContent/Items/Addons/BaseAddonContainer.cs +++ b/Projects/UOContent/Items/Addons/BaseAddonContainer.cs @@ -295,7 +295,12 @@ namespace Server.Items { var door = doors[i]; - if (door?.Open == true) + if (door == null) + { + continue; + } + + if (door.Open) { return AddonFitResult.DoorsNotClosed; } diff --git a/Projects/UOContent/Items/Books/BaseBook.cs b/Projects/UOContent/Items/Books/BaseBook.cs index df555926e..cd5d7cbaf 100644 --- a/Projects/UOContent/Items/Books/BaseBook.cs +++ b/Projects/UOContent/Items/Books/BaseBook.cs @@ -378,8 +378,8 @@ namespace Server.Items reader.Seek(4, SeekOrigin.Current); // Skip flags and page count - var title = reader.ReadStringSafe(60); - var author = reader.ReadStringSafe(30); + var title = reader.ReadAsciiSafe(60); + var author = reader.ReadAsciiSafe(30); book.Title = Utility.FixHtml(title); book.Author = Utility.FixHtml(author); @@ -404,7 +404,7 @@ namespace Server.Items return; } - var title = reader.ReadUTF8StringSafe(titleLength); + var title = reader.ReadUTF8Safe(titleLength); int authorLength = reader.ReadUInt16(); @@ -413,7 +413,7 @@ namespace Server.Items return; } - var author = reader.ReadUTF8StringSafe(authorLength); + var author = reader.ReadUTF8Safe(authorLength); book.Title = Utility.FixHtml(title); book.Author = Utility.FixHtml(author); @@ -452,7 +452,7 @@ namespace Server.Items for (var j = 0; j < lineCount; ++j) { - if ((lines[j] = reader.ReadUTF8StringSafe()).Length >= 80) + if ((lines[j] = reader.ReadUTF8Safe()).Length >= 80) { return; } diff --git a/Projects/UOContent/Items/Games/Mahjong/MahjongPacketHandlers.cs b/Projects/UOContent/Items/Games/Mahjong/MahjongPacketHandlers.cs index c037e1221..afc2e5d78 100644 --- a/Projects/UOContent/Items/Games/Mahjong/MahjongPacketHandlers.cs +++ b/Projects/UOContent/Items/Games/Mahjong/MahjongPacketHandlers.cs @@ -41,7 +41,7 @@ namespace Server.Engines.Mahjong RegisterSubCommand(0x18, MoveDealerIndicator); } - public static void OnPacket(NetState state, PacketReader reader) + public static void OnPacket(NetState state,PacketReader reader) { var game = World.FindItem(reader.ReadUInt32()) as MahjongGame; diff --git a/Projects/UOContent/Items/Misc/BulletinBoards.cs b/Projects/UOContent/Items/Misc/BulletinBoards.cs index efe0a3836..7922cdbfb 100644 --- a/Projects/UOContent/Items/Misc/BulletinBoards.cs +++ b/Projects/UOContent/Items/Misc/BulletinBoards.cs @@ -315,7 +315,7 @@ namespace Server.Items } } - var subject = reader.ReadUTF8StringSafe(reader.ReadByte()); + var subject = reader.ReadUTF8Safe(reader.ReadByte()); if (subject.Length == 0) { @@ -331,7 +331,7 @@ namespace Server.Items for (var i = 0; i < lines.Length; ++i) { - lines[i] = reader.ReadUTF8StringSafe(reader.ReadByte()); + lines[i] = reader.ReadUTF8Safe(reader.ReadByte()); } board.PostMessage(from, thread, subject, lines); diff --git a/Projects/UOContent/Misc/ClientVerification.cs b/Projects/UOContent/Misc/ClientVerification.cs index fab83511b..cc460ed16 100644 --- a/Projects/UOContent/Misc/ClientVerification.cs +++ b/Projects/UOContent/Misc/ClientVerification.cs @@ -171,7 +171,7 @@ namespace Server.Misc { if (ns.Connection != null) { - Console.WriteLine("Client: {0}: Disconnecting, bad version", ns); + ns.WriteConsole("Disconnecting, bad version"); ns.Dispose(); } } diff --git a/Projects/UOContent/Misc/Fastwalk.cs b/Projects/UOContent/Misc/Fastwalk.cs index 38de130b3..b45ccf72c 100644 --- a/Projects/UOContent/Misc/Fastwalk.cs +++ b/Projects/UOContent/Misc/Fastwalk.cs @@ -29,7 +29,8 @@ namespace Server.Misc public static void OnFastWalk(FastWalkEventArgs e) { e.Blocked = true; // disallow this fastwalk - Console.WriteLine("Client: {0}: Fast movement detected (name={1})", e.NetState, e.NetState.Mobile.Name); + var state = e.NetState; + state.WriteConsole("Fast movement detected (name={0})", state.Mobile.Name); } } } diff --git a/Projects/UOContent/Misc/HardwareInfo.cs b/Projects/UOContent/Misc/HardwareInfo.cs index 5c5a9ec50..d02f32a1a 100644 --- a/Projects/UOContent/Misc/HardwareInfo.cs +++ b/Projects/UOContent/Misc/HardwareInfo.cs @@ -163,7 +163,7 @@ namespace Server info.ScreenDepth = reader.ReadInt32(); info.DXMajor = reader.ReadInt16(); info.DXMinor = reader.ReadInt16(); - info.VCDescription = reader.ReadUnicodeStringLESafe(64); + info.VCDescription = reader.ReadLittleUniSafe(64); info.VCVendorID = reader.ReadInt32(); info.VCDeviceID = reader.ReadInt32(); info.VCMemory = reader.ReadInt32(); @@ -171,8 +171,8 @@ namespace Server info.ClientsRunning = reader.ReadByte(); info.ClientsInstalled = reader.ReadByte(); info.PartialInstalled = reader.ReadByte(); - info.Language = reader.ReadUnicodeStringLESafe(4); - info.Unknown = reader.ReadStringSafe(64); + info.Language = reader.ReadLittleUniSafe(4); + info.Unknown = reader.ReadAsciiSafe(64); info.TimeReceived = DateTime.UtcNow; diff --git a/Projects/UOContent/Misc/ProtocolExtensions.cs b/Projects/UOContent/Misc/ProtocolExtensions.cs index 83f9000a1..ce374ab2e 100644 --- a/Projects/UOContent/Misc/ProtocolExtensions.cs +++ b/Projects/UOContent/Misc/ProtocolExtensions.cs @@ -1,9 +1,8 @@ -using System; using Server.Network; namespace Server.Misc { - public class ProtocolExtensions + public static class ProtocolExtensions { private static readonly PacketHandler[] m_Handlers = new PacketHandler[0x100]; @@ -17,15 +16,8 @@ namespace Server.Misc m_Handlers[packetID] = new PacketHandler(packetID, 0, ingame, onReceive); } - public static PacketHandler GetHandler(int packetID) - { - if (packetID >= 0 && packetID < m_Handlers.Length) - { - return m_Handlers[packetID]; - } - - return null; - } + public static PacketHandler GetHandler(int packetID) => + packetID >= 0 && packetID < m_Handlers.Length ? m_Handlers[packetID] : null; public static void DecodeBundledPacket(NetState state, PacketReader reader) { @@ -37,9 +29,8 @@ namespace Server.Misc { if (ph.Ingame && state.Mobile == null) { - Console.WriteLine( - "Client: {0}: Sent ingame packet (0xF0x{1:X2}) before having been attached to a mobile", - state, + state.WriteConsole( + "Sent ingame packet (0xF0x{0:X2}) before having been attached to a mobile", packetID ); state.Dispose(); diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 2636a1c37..b4ddb8c0e 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -5634,105 +5634,90 @@ namespace Server.Mobiles protected override void OnTick() { - if (DateTime.UtcNow >= m_NextHourlyCheck) - { - m_NextHourlyCheck = DateTime.UtcNow + TimeSpan.FromHours(1.0); - } - else + if (DateTime.UtcNow < m_NextHourlyCheck) { return; } + m_NextHourlyCheck = DateTime.UtcNow + TimeSpan.FromHours(1.0); + var toRelease = new List(); // added array for wild creatures in house regions to be removed var toRemove = new List(); - Parallel.ForEach( - World.Mobiles.Values, - m => + foreach (var m in World.Mobiles.Values) + { + if (!(m is BaseCreature c)) { - if (!(m is BaseCreature c)) - { - return; - } + continue; + } - if (c is BaseMount mount && mount.Rider != null) - { - mount.OwnerAbandonTime = DateTime.MinValue; - return; - } + if (c is BaseMount mount && mount.Rider != null) + { + mount.OwnerAbandonTime = DateTime.MinValue; + continue; + } - if (c.IsDeadPet) - { - var owner = c.ControlMaster; + if (c.IsDeadPet) + { + var owner = c.ControlMaster; - if (!c.IsStabled && (owner?.Deleted != false || owner.Map != c.Map || - !owner.InRange(c, 12) || !c.CanSee(owner) || !c.InLOS(owner))) + if (!c.IsStabled && (owner?.Deleted != false || owner.Map != c.Map || + !owner.InRange(c, 12) || !c.CanSee(owner) || !c.InLOS(owner))) + { + if (c.OwnerAbandonTime == DateTime.MinValue) { - if (c.OwnerAbandonTime == DateTime.MinValue) - { - c.OwnerAbandonTime = DateTime.UtcNow; - } - else if (c.OwnerAbandonTime + c.BondingAbandonDelay <= DateTime.UtcNow) - { - lock (toRemove) - { - toRemove.Add(c); - } - } + c.OwnerAbandonTime = DateTime.UtcNow; } - else + else if (c.OwnerAbandonTime + c.BondingAbandonDelay <= DateTime.UtcNow) { - c.OwnerAbandonTime = DateTime.MinValue; - } - } - else if (c.Controlled && c.Commandable) - { - c.OwnerAbandonTime = DateTime.MinValue; - - if (c.Map != Map.Internal) - { - c.Loyalty -= BaseCreature.MaxLoyalty / 10; - - if (c.Loyalty < BaseCreature.MaxLoyalty / 10) - { - c.Say(1043270, c.Name); // * ~1_NAME~ looks around desperately * - c.PlaySound(c.GetIdleSound()); - } - - if (c.Loyalty <= 0) - { - lock (toRelease) - { - toRelease.Add(c); - } - } - } - } - - // added lines to check if a wild creature in a house region has to be removed or not - if (!c.Controlled && !c.IsStabled && (c.Region.IsPartOf() && c.CanBeDamaged() || - c.RemoveIfUntamed && c.Spawner == null)) - { - c.RemoveStep++; - - if (c.RemoveStep >= 20) - { - lock (toRemove) - { - toRemove.Add(c); - } + toRemove.Add(c); } } else { - c.RemoveStep = 0; + c.OwnerAbandonTime = DateTime.MinValue; } } - ); + else if (c.Controlled && c.Commandable) + { + c.OwnerAbandonTime = DateTime.MinValue; + + if (c.Map != Map.Internal) + { + c.Loyalty -= BaseCreature.MaxLoyalty / 10; + + if (c.Loyalty < BaseCreature.MaxLoyalty / 10) + { + c.Say(1043270, c.Name); // * ~1_NAME~ looks around desperately * + c.PlaySound(c.GetIdleSound()); + } + + if (c.Loyalty <= 0) + { + toRelease.Add(c); + } + } + } + + // added lines to check if a wild creature in a house region has to be removed or not + if (!c.Controlled && !c.IsStabled && (c.Region.IsPartOf() && c.CanBeDamaged() || + c.RemoveIfUntamed && c.Spawner == null)) + { + c.RemoveStep++; + + if (c.RemoveStep >= 20) + { + toRemove.Add(c); + } + } + else + { + c.RemoveStep = 0; + } + } - // TODO: Parallelize this foreach (var c in toRelease) { c.Say(1043255, c.Name); // ~1_NAME~ appears to have decided that is better off without a master! diff --git a/Projects/UOContent/Multis/HouseFoundation.cs b/Projects/UOContent/Multis/HouseFoundation.cs index 0e7bf18cb..ad0c7867f 100644 --- a/Projects/UOContent/Multis/HouseFoundation.cs +++ b/Projects/UOContent/Multis/HouseFoundation.cs @@ -373,8 +373,7 @@ namespace Server.Multis 4 => new GenericHouseDoor(facing, 0x6B5, 0xEA, 0xF1), 5 => new GenericHouseDoor(facing, 0x6C5, 0xEC, 0xF3), 6 => new GenericHouseDoor(facing, 0x6D5, 0xEA, 0xF1), - 7 => new GenericHouseDoor(facing, 0x6E5, 0xEA, 0xF1), - _ => door + _ => new GenericHouseDoor(facing, 0x6E5, 0xEA, 0xF1), }; } else if (itemID >= 0x314 && itemID < 0x364) @@ -484,8 +483,7 @@ namespace Server.Multis door = type switch { 0 => new GenericHouseDoor(facing, 0x367B, 0xED, 0xF4), - 1 => new GenericHouseDoor(facing, 0x368B, 0xEC, 0x3E7), - _ => door + _ => new GenericHouseDoor(facing, 0x368B, 0xEC, 0x3E7), }; } else if (itemID >= 0x409B && itemID < 0x40A3) @@ -1978,14 +1976,9 @@ namespace Server.Multis public void OnRevised() { - lock (this) - { - Revision = ++Foundation.LastRevision; - - m_PacketCache?.Release(); - - m_PacketCache = null; - } + Revision = ++Foundation.LastRevision; + m_PacketCache?.Release(); + m_PacketCache = null; } public void SendGeneralInfoTo(NetState state) @@ -1995,19 +1988,18 @@ namespace Server.Multis public void SendDetailedInfoTo(NetState state) { - if (state != null) + if (state == null) { - lock (this) - { - if (m_PacketCache == null) - { - DesignStateDetailed.SendDetails(state, Foundation, this); - } - else - { - state.Send(m_PacketCache); - } - } + return; + } + + if (m_PacketCache == null) + { + DesignStateDetailed.SendDetails(state, Foundation, this); + } + else + { + state.Send(m_PacketCache); } } @@ -2444,22 +2436,8 @@ namespace Server.Multis { public const int MaxItemsPerStairBuffer = 750; - private static readonly ConcurrentQueue m_SendQueue; - private static readonly AutoResetEvent m_Sync; - - private readonly byte[][] m_PlaneBuffers; - private readonly bool[] m_PlaneUsed = new bool[9]; private readonly byte[] m_PrimBuffer = new byte[4]; - private readonly byte[][] m_StairBuffers; - - static DesignStateDetailed() - { - m_SendQueue = new ConcurrentQueue(); - m_Sync = new AutoResetEvent(false); - - Task.Run(ProcessCompression); - } public DesignStateDetailed(uint serial, int revision, int xMin, int yMin, int xMax, int yMax, MultiTileEntry[] tiles) : base(0xD8) @@ -2479,26 +2457,26 @@ namespace Server.Multis var width = xMax - xMin + 1; var height = yMax - yMin + 1; - m_PlaneBuffers = new byte[9][]; + var planeBuffers = new byte[9][]; - for (var i = 0; i < m_PlaneBuffers.Length; ++i) + for (var i = 0; i < planeBuffers.Length; ++i) { - m_PlaneBuffers[i] = ArrayPool.Shared.Rent(0x400); + planeBuffers[i] = ArrayPool.Shared.Rent(0x400); } - m_StairBuffers = new byte[6][]; + var stairBuffers = new byte[6][]; - for (var i = 0; i < m_StairBuffers.Length; ++i) + for (var i = 0; i < stairBuffers.Length; ++i) { - m_StairBuffers[i] = ArrayPool.Shared.Rent(MaxItemsPerStairBuffer * 5); + stairBuffers[i] = ArrayPool.Shared.Rent(MaxItemsPerStairBuffer * 5); } - Clear(m_PlaneBuffers[0], width * height * 2); + Clear(planeBuffers[0], width * height * 2); for (var i = 0; i < 4; ++i) { - Clear(m_PlaneBuffers[1 + i], (width - 1) * (height - 2) * 2); - Clear(m_PlaneBuffers[5 + i], width * (height - 1) * 2); + Clear(planeBuffers[1 + i], (width - 1) * (height - 2) * 2); + Clear(planeBuffers[5 + i], width * (height - 1) * 2); } var totalStairsUsed = 0; @@ -2532,7 +2510,7 @@ namespace Server.Multis default: { var stairBufferIndex = totalStairsUsed / MaxItemsPerStairBuffer; - var stairBuffer = m_StairBuffers[stairBufferIndex]; + var stairBuffer = stairBuffers[stairBufferIndex]; var byteIndex = totalStairsUsed % MaxItemsPerStairBuffer * 5; @@ -2541,7 +2519,7 @@ namespace Server.Multis stairBuffer[byteIndex++] = (byte)mte.OffsetX; stairBuffer[byteIndex++] = (byte)mte.OffsetY; - stairBuffer[byteIndex++] = (byte)mte.OffsetZ; + stairBuffer[byteIndex] = (byte)mte.OffsetZ; ++totalStairsUsed; @@ -2570,7 +2548,7 @@ namespace Server.Multis if (x < 0 || y < 0 || y >= size || index + 1 >= 0x400) { var stairBufferIndex = totalStairsUsed / MaxItemsPerStairBuffer; - var stairBuffer = m_StairBuffers[stairBufferIndex]; + var stairBuffer = stairBuffers[stairBufferIndex]; var byteIndex = totalStairsUsed % MaxItemsPerStairBuffer * 5; @@ -2579,27 +2557,27 @@ namespace Server.Multis stairBuffer[byteIndex++] = (byte)mte.OffsetX; stairBuffer[byteIndex++] = (byte)mte.OffsetY; - stairBuffer[byteIndex++] = (byte)mte.OffsetZ; + stairBuffer[byteIndex] = (byte)mte.OffsetZ; ++totalStairsUsed; } else { m_PlaneUsed[plane] = true; - m_PlaneBuffers[plane][index] = (byte)(mte.ItemId >> 8); - m_PlaneBuffers[plane][index + 1] = (byte)mte.ItemId; + planeBuffers[plane][index] = (byte)(mte.ItemId >> 8); + planeBuffers[plane][index + 1] = (byte)mte.ItemId; } } var planeCount = 0; - var m_DeflatedBuffer = ArrayPool.Shared.Rent(0x2000); + var deflatedBuffer = ArrayPool.Shared.Rent(0x2000); - for (var i = 0; i < m_PlaneBuffers.Length; ++i) + for (var i = 0; i < planeBuffers.Length; ++i) { if (!m_PlaneUsed[i]) { - ArrayPool.Shared.Return(m_PlaneBuffers[i]); + ArrayPool.Shared.Return(planeBuffers[i]); continue; } @@ -2620,11 +2598,11 @@ namespace Server.Multis size = width * (height - 1) * 2; } - var inflatedBuffer = m_PlaneBuffers[i]; + var inflatedBuffer = planeBuffers[i]; - var deflatedLength = m_DeflatedBuffer.Length; + var deflatedLength = deflatedBuffer.Length; var ce = Zlib.Pack( - m_DeflatedBuffer, + deflatedBuffer, ref deflatedLength, inflatedBuffer, size, @@ -2642,7 +2620,7 @@ namespace Server.Multis Write((byte)size); Write((byte)deflatedLength); Write((byte)(((size >> 4) & 0xF0) | ((deflatedLength >> 8) & 0xF))); - Write(m_DeflatedBuffer, 0, deflatedLength); + Write(deflatedBuffer, 0, deflatedLength); totalLength += 4 + deflatedLength; ArrayPool.Shared.Return(inflatedBuffer); @@ -2663,11 +2641,11 @@ namespace Server.Multis var size = count * 5; - var inflatedBuffer = m_StairBuffers[i]; + var inflatedBuffer = stairBuffers[i]; - var deflatedLength = m_DeflatedBuffer.Length; + var deflatedLength = deflatedBuffer.Length; var ce = Zlib.Pack( - m_DeflatedBuffer, + deflatedBuffer, ref deflatedLength, inflatedBuffer, size, @@ -2685,17 +2663,17 @@ namespace Server.Multis Write((byte)size); Write((byte)deflatedLength); Write((byte)(((size >> 4) & 0xF0) | ((deflatedLength >> 8) & 0xF))); - Write(m_DeflatedBuffer, 0, deflatedLength); + Write(deflatedBuffer, 0, deflatedLength); totalLength += 4 + deflatedLength; } - for (var i = 0; i < m_StairBuffers.Length; ++i) + for (var i = 0; i < stairBuffers.Length; ++i) { - ArrayPool.Shared.Return(m_StairBuffers[i]); + ArrayPool.Shared.Return(stairBuffers[i]); } - ArrayPool.Shared.Return(m_DeflatedBuffer); + ArrayPool.Shared.Return(deflatedBuffer); Stream.Seek(15, SeekOrigin.Begin); @@ -2749,106 +2727,28 @@ namespace Server.Multis } } - public static void ProcessCompression() - { - while (!Core.Closing) - { - m_Sync.WaitOne(); - - var count = m_SendQueue.Count; - - while (count > 0 && m_SendQueue.TryDequeue(out var sqe)) - { - try - { - Packet p; - - lock (sqe.m_Root) - { - p = sqe.m_Root.PacketCache; - } - - if (p == null) - { - p = new DesignStateDetailed( - sqe.m_Serial, - sqe.m_Revision, - sqe.m_xMin, - sqe.m_yMin, - sqe.m_xMax, - sqe.m_yMax, - sqe.m_Tiles - ); - p.SetStatic(); - - lock (sqe.m_Root) - { - if (sqe.m_Revision == sqe.m_Root.Revision) - { - sqe.m_Root.PacketCache = p; - } - } - } - - sqe.m_NetState.Send(p); - } - catch (Exception e) - { - Console.WriteLine(e); - - try - { - using var op = new StreamWriter("dsd_exceptions.txt", true); - op.WriteLine(e); - } - catch - { - // ignored - } - } - finally - { - count = m_SendQueue.Count; - } - } - } - } - public static void SendDetails(NetState ns, HouseFoundation house, DesignState state) { - m_SendQueue.Enqueue(new SendQueueEntry(ns, house, state)); + var p = state.PacketCache; - m_Sync.Set(); - } - - private class SendQueueEntry - { - public readonly NetState m_NetState; - public readonly int m_Revision; - public readonly DesignState m_Root; - public readonly uint m_Serial; - public readonly MultiTileEntry[] m_Tiles; - public readonly int m_xMax; - public readonly int m_xMin; - public readonly int m_yMax; - public readonly int m_yMin; - - public SendQueueEntry(NetState ns, HouseFoundation foundation, DesignState state) + if (p == null) { - m_NetState = ns; - m_Serial = foundation.Serial; - m_Revision = state.Revision; - m_Root = state; - var mcl = state.Components; + p = new DesignStateDetailed( + house.Serial, + state.Revision, + mcl.Min.X, + mcl.Min.Y, + mcl.Max.X, + mcl.Max.Y, + mcl.List + ); - m_xMin = mcl.Min.X; - m_yMin = mcl.Min.Y; - m_xMax = mcl.Max.X; - m_yMax = mcl.Max.Y; - - m_Tiles = mcl.List; + p.SetStatic(); + state.PacketCache = p; } + + ns.Send(p); } } } diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index a9fd964f6..6e8449f43 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -12,19 +12,10 @@ - - - - - - - - - @@ -37,12 +28,6 @@ false - - - - - - all diff --git a/version.json b/version.json index 3dda2c3cc..38d4958f3 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.6.3" + "version": "0.7.0" }