diff --git a/MinecraftClient/Mcp/MccMcpCapabilities.cs b/MinecraftClient/Mcp/MccMcpCapabilities.cs index 3f4fa060..01521c5c 100644 --- a/MinecraftClient/Mcp/MccMcpCapabilities.cs +++ b/MinecraftClient/Mcp/MccMcpCapabilities.cs @@ -2289,34 +2289,6 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities return client.GetInventories().Keys.Where(id => id > 0).DefaultIfEmpty(0).Max(); } - private static bool WaitForContainerOpen(McClient client, ISet beforeIds, int waitMs, out int inventoryId, out Container? inventory) - { - inventoryId = 0; - inventory = null; - DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); - while (true) - { - (int activeId, Container? activeInventory) state = client.InvokeOnMainThread(() => - { - int activeId = GetActiveContainerId(client); - Container? activeInventory = activeId > 0 ? client.GetInventory(activeId) : null; - return (activeId, activeInventory); - }); - - if (state.activeId > 0 && (!beforeIds.Contains(state.activeId) || beforeIds.Count == 0) && state.activeInventory is not null) - { - inventoryId = state.activeId; - inventory = state.activeInventory; - return true; - } - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - private static async Task WaitForContainerOpenAsync(McClient client, ISet beforeIds, int waitMs, Action onOpened) { DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); @@ -2342,22 +2314,6 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } } - private static bool WaitForContainerClose(McClient client, int inventoryId, int waitMs) - { - DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); - while (true) - { - bool stillOpen = client.InvokeOnMainThread(() => client.GetInventories().ContainsKey(inventoryId)); - if (!stillOpen) - return true; - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - private static async Task WaitForContainerCloseAsync(McClient client, int inventoryId, int waitMs) { DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); @@ -2453,14 +2409,6 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities : 0; } - private static bool TryDropInventorySlotItems(McClient client, int inventoryId, Container inventory, int slotId, ItemType itemType, int dropCount, out int droppedCount) - { - (bool success, int actualDroppedCount) = - TryDropInventorySlotItemsAsync(client, inventoryId, inventory, slotId, itemType, dropCount).GetAwaiter().GetResult(); - droppedCount = actualDroppedCount; - return success; - } - private static int CountItemInRange(Container inventory, ItemType itemType, int startSlot, int endSlot) { return inventory.Items @@ -2827,115 +2775,6 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } } - private static bool WaitForCursorItem(McClient client, ItemType itemType, int waitMs, out Item? cursorItem) - { - cursorItem = null; - DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); - while (true) - { - if (TryGetCursorItem(client, out cursorItem) && cursorItem is not null && cursorItem.Type == itemType) - return true; - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - - private static bool WaitForCursorClear(McClient client, int waitMs) - { - DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); - while (true) - { - if (!TryGetCursorItem(client, out _)) - return true; - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - - private static bool WaitForPlacement(McClient client, int inventoryId, int targetSlot, ItemType itemType, int beforeTargetCount, int beforeCursorCount, int placedCount) - { - DateTime deadline = DateTime.UtcNow.AddMilliseconds(DefaultInventoryActionWaitMs); - while (true) - { - bool targetUpdated = false; - bool cursorUpdated = false; - - Container? inventory = client.InvokeOnMainThread(() => client.GetInventory(inventoryId)); - if (inventory is not null) - { - int currentTargetCount = GetSlotItemCount(inventory, targetSlot, itemType); - targetUpdated = currentTargetCount >= beforeTargetCount + placedCount; - } - - if (placedCount >= beforeCursorCount) - { - cursorUpdated = !TryGetCursorItem(client, out _); - } - else if (TryGetCursorItem(client, out Item? cursorItem) && cursorItem is not null && cursorItem.Type == itemType) - { - cursorUpdated = cursorItem.Count <= beforeCursorCount - placedCount; - } - - if (targetUpdated && cursorUpdated) - return true; - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - - private static bool WaitForSlotItemCount(McClient client, int inventoryId, int slotId, ItemType itemType, Func predicate, int waitMs, out Container? inventory, out int itemCount) - { - inventory = null; - itemCount = 0; - DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); - while (true) - { - inventory = client.GetInventory(inventoryId); - if (inventory is not null) - { - itemCount = GetSlotItemCount(inventory, slotId, itemType); - if (predicate(itemCount)) - return true; - } - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - - private static bool WaitForRangeCount(McClient client, int inventoryId, ItemType itemType, int startSlot, int endSlot, Func predicate, int waitMs, out Container? inventory, out int itemCount) - { - inventory = null; - itemCount = 0; - DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); - while (true) - { - inventory = client.InvokeOnMainThread(() => client.GetInventory(inventoryId)); - if (inventory is not null) - { - itemCount = CountItemInRange(inventory, itemType, startSlot, endSlot); - if (predicate(itemCount)) - return true; - } - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - private static List BuildTrackedPlayerSnapshots(McClient client, bool includeSelf) { Location playerLocation = client.GetCurrentLocation(); @@ -3121,25 +2960,6 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities return new string(buffer); } - private static bool WaitForArrival(McClient client, Location goal, int waitMs, double tolerance, out Location? finalLocation) - { - finalLocation = null; - DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); - while (true) - { - Location location = client.InvokeOnMainThread(client.GetCurrentLocation); - finalLocation = location; - double distance = GetDistance(location, goal); - if (distance <= tolerance) - return true; - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - private static async Task<(bool Arrived, Location FinalLocation)> WaitForArrivalAsync(McClient client, Location goal, int waitMs, double tolerance) { DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); @@ -3186,24 +3006,6 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities return Math.Clamp(timeoutMs, MinPathQueryTimeoutMs, MaxPathQueryTimeoutMs); } - private static bool WaitForBlockChange(McClient client, Location target, Block beforeBlock, int waitMs, out Block afterBlock) - { - afterBlock = beforeBlock; - DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); - while (true) - { - Block current = client.InvokeOnMainThread(() => client.GetWorld().GetBlock(target)); - afterBlock = current; - if (!AreEquivalentBlocks(current, beforeBlock)) - return true; - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - private static async Task<(bool Changed, Block AfterBlock)> WaitForBlockChangeAsync(McClient client, Location target, Block beforeBlock, int waitMs) { Block afterBlock = beforeBlock; @@ -3337,22 +3139,6 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities .ToArray(); } - private static bool WaitForEntityRemoval(McClient client, int entityId, int waitMs) - { - DateTime deadline = DateTime.UtcNow.AddMilliseconds(waitMs); - while (true) - { - bool exists = client.InvokeOnMainThread(() => client.GetEntities().ContainsKey(entityId)); - if (!exists) - return true; - - if (DateTime.UtcNow >= deadline) - return false; - - Thread.Sleep(ArrivalPollIntervalMs); - } - } - private static int GetInventoryItemCount(McClient client, ItemType itemType) { Container? inventory = client.GetInventory(0); diff --git a/MinecraftClient/Protocol/Handlers/DataTypes.cs b/MinecraftClient/Protocol/Handlers/DataTypes.cs index 68a842e3..dab5b55e 100644 --- a/MinecraftClient/Protocol/Handlers/DataTypes.cs +++ b/MinecraftClient/Protocol/Handlers/DataTypes.cs @@ -8,6 +8,7 @@ using MinecraftClient.Inventory; using MinecraftClient.Inventory.ItemPalettes; using MinecraftClient.Mapping; using MinecraftClient.Mapping.EntityPalettes; +using MinecraftClient.Protocol.PacketPipeline; using MinecraftClient.Protocol.Handlers.StructuredComponents; using MinecraftClient.Protocol.Handlers.StructuredComponents.Core; using MinecraftClient.Protocol.Message; @@ -44,6 +45,12 @@ namespace MinecraftClient.Protocol.Handlers return result; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public byte[] ReadData(int offset, PacketReader reader) + { + return reader.ReadData(offset); + } + /// /// Read some data from a cache of bytes and remove it from the cache /// @@ -56,6 +63,12 @@ namespace MinecraftClient.Protocol.Handlers dest[i] = cache.Dequeue(); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void ReadDataReverse(PacketReader reader, Span dest) + { + reader.ReadDataReverse(dest); + } + /// /// Remove some data from the cache /// @@ -68,6 +81,12 @@ namespace MinecraftClient.Protocol.Handlers cache.Dequeue(); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void DropData(int offset, PacketReader reader) + { + reader.Skip(offset); + } + /// /// Read a string from a cache of bytes and remove it from the cache /// @@ -84,6 +103,13 @@ namespace MinecraftClient.Protocol.Handlers else return ""; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public string ReadNextString(PacketReader reader) + { + int length = ReadNextVarInt(reader); + return length > 0 ? Encoding.UTF8.GetString(ReadData(length, reader)) : ""; + } + /// /// Skip a string from a cache of bytes and remove it from the cache /// @@ -94,6 +120,12 @@ namespace MinecraftClient.Protocol.Handlers DropData(length, cache); } + public void SkipNextString(PacketReader reader) + { + int length = ReadNextVarInt(reader); + DropData(length, reader); + } + /// /// Read a boolean from a cache of bytes and remove it from the cache /// @@ -104,6 +136,12 @@ namespace MinecraftClient.Protocol.Handlers return ReadNextByte(cache) != 0x00; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public bool ReadNextBool(PacketReader reader) + { + return ReadNextByte(reader) != 0x00; + } + /// /// Read a short integer from a cache of bytes and remove it from the cache /// @@ -117,6 +155,12 @@ namespace MinecraftClient.Protocol.Handlers return BitConverter.ToInt16(rawValue); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public short ReadNextShort(PacketReader reader) + { + return reader.ReadInt16BigEndian(); + } + /// /// Read an integer from a cache of bytes and remove it from the cache /// @@ -130,6 +174,12 @@ namespace MinecraftClient.Protocol.Handlers return BitConverter.ToInt32(rawValue); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public int ReadNextInt(PacketReader reader) + { + return reader.ReadInt32BigEndian(); + } + /// /// Read a long integer from a cache of bytes and remove it from the cache /// @@ -143,6 +193,12 @@ namespace MinecraftClient.Protocol.Handlers return BitConverter.ToInt64(rawValue); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public long ReadNextLong(PacketReader reader) + { + return reader.ReadInt64BigEndian(); + } + /// /// Read an unsigned short integer from a cache of bytes and remove it from the cache /// @@ -156,6 +212,12 @@ namespace MinecraftClient.Protocol.Handlers return BitConverter.ToUInt16(rawValue); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ushort ReadNextUShort(PacketReader reader) + { + return reader.ReadUInt16BigEndian(); + } + /// /// Read an unsigned long integer from a cache of bytes and remove it from the cache /// @@ -169,6 +231,12 @@ namespace MinecraftClient.Protocol.Handlers return BitConverter.ToUInt64(rawValue); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public ulong ReadNextULong(PacketReader reader) + { + return unchecked((ulong)reader.ReadInt64BigEndian()); + } + /// /// Read a Location encoded as an ulong field and remove it from the cache /// @@ -199,6 +267,32 @@ namespace MinecraftClient.Protocol.Handlers return new Location(x, y, z); } + public Location ReadNextLocation(PacketReader reader) + { + ulong locEncoded = ReadNextULong(reader); + int x, y, z; + if (protocolversion >= Protocol18Handler.MC_1_14_Version) + { + x = (int)(locEncoded >> 38); + y = (int)(locEncoded & 0xFFF); + z = (int)(locEncoded << 26 >> 38); + } + else + { + x = (int)(locEncoded >> 38); + y = (int)((locEncoded >> 26) & 0xFFF); + z = (int)(locEncoded << 38 >> 38); + } + + if (x >= 0x02000000) + x -= 0x04000000; + if (y >= 0x00000800) + y -= 0x00001000; + if (z >= 0x02000000) + z -= 0x04000000; + return new Location(x, y, z); + } + /// /// Read several little endian unsigned short integers at once from a cache of bytes and remove them from the cache /// @@ -212,6 +306,15 @@ namespace MinecraftClient.Protocol.Handlers return result; } + public ushort[] ReadNextUShortsLittleEndian(int amount, PacketReader reader) + { + byte[] rawValues = ReadData(2 * amount, reader); + ushort[] result = new ushort[amount]; + for (int i = 0; i < amount; i++) + result[i] = BitConverter.ToUInt16(rawValues, i * 2); + return result; + } + /// /// Read a uuid from a cache of bytes and remove it from the cache /// @@ -228,6 +331,16 @@ namespace MinecraftClient.Protocol.Handlers return guid; } + public Guid ReadNextUUID(PacketReader reader) + { + Span javaUUID = stackalloc byte[16]; + reader.ReadData(javaUUID); + Guid guid = new(javaUUID); + if (BitConverter.IsLittleEndian) + guid = guid.ToLittleEndian(); + return guid; + } + /// /// Read a byte array from a cache of bytes and remove it from the cache /// @@ -242,6 +355,15 @@ namespace MinecraftClient.Protocol.Handlers return ReadData(len, cache); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public byte[] ReadNextByteArray(PacketReader reader) + { + int len = protocolversion >= Protocol18Handler.MC_1_8_Version + ? ReadNextVarInt(reader) + : ReadNextShort(reader); + return ReadData(len, reader); + } + /// /// Read a byte array with given length from a cache of bytes and remove it from the cache /// @@ -254,6 +376,12 @@ namespace MinecraftClient.Protocol.Handlers return ReadData(length, cache); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public byte[] ReadNextByteArray(PacketReader reader, int length) + { + return ReadData(length, reader); + } + /// /// Reads a length-prefixed array of unsigned long integers and removes it from the cache /// @@ -268,6 +396,15 @@ namespace MinecraftClient.Protocol.Handlers return result; } + public ulong[] ReadNextULongArray(PacketReader reader) + { + int len = ReadNextVarInt(reader); + ulong[] result = new ulong[len]; + for (int i = 0; i < len; i++) + result[i] = ReadNextULong(reader); + return result; + } + /// /// Read a double from a cache of bytes and remove it from the cache /// @@ -281,6 +418,12 @@ namespace MinecraftClient.Protocol.Handlers return BitConverter.ToDouble(rawValue); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public double ReadNextDouble(PacketReader reader) + { + return BitConverter.Int64BitsToDouble(ReadNextLong(reader)); + } + /// /// Read a float from a cache of bytes and remove it from the cache /// @@ -294,6 +437,12 @@ namespace MinecraftClient.Protocol.Handlers return BitConverter.ToSingle(rawValue); } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public float ReadNextFloat(PacketReader reader) + { + return BitConverter.Int32BitsToSingle(ReadNextInt(reader)); + } + /// /// Read an integer from the network /// @@ -357,6 +506,22 @@ namespace MinecraftClient.Protocol.Handlers return i; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public int ReadNextVarInt(PacketReader reader) + { + int i = 0; + int j = 0; + byte b; + do + { + b = reader.ReadByte(); + i |= (b & 0x7F) << j++ * 7; + if (j > 5) throw new OverflowException("VarInt too big"); + } while ((b & 0x80) == 128); + + return i; + } + /// /// Skip a VarInt from a cache of bytes with better performance /// @@ -369,6 +534,14 @@ namespace MinecraftClient.Protocol.Handlers break; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public void SkipNextVarInt(PacketReader reader) + { + while (true) + if ((ReadNextByte(reader) & 0x80) != 128) + break; + } + /// /// Read an "extended short", which is actually an int of some kind, from the cache of bytes. /// This is only done with forge. It looks like it's a normal short, except that if the high @@ -389,6 +562,19 @@ namespace MinecraftClient.Protocol.Handlers return ((high & 0xFF) << 15) | low; } + public int ReadNextVarShort(PacketReader reader) + { + ushort low = ReadNextUShort(reader); + byte high = 0; + if ((low & 0x8000) != 0) + { + low &= 0x7FFF; + high = ReadNextByte(reader); + } + + return ((high & 0xFF) << 15) | low; + } + /// /// Read a long from a cache of bytes and remove it from the cache /// @@ -415,6 +601,27 @@ namespace MinecraftClient.Protocol.Handlers return result; } + public long ReadNextVarLong(PacketReader reader) + { + int numRead = 0; + long result = 0; + byte read; + do + { + read = ReadNextByte(reader); + long value = (read & 0x7F); + result |= (value << (7 * numRead)); + + numRead++; + if (numRead > 10) + { + throw new OverflowException("VarLong is too big"); + } + } while ((read & 0x80) != 0); + + return result; + } + /// /// Read a single byte from a cache of bytes and remove it from the cache /// @@ -426,6 +633,12 @@ namespace MinecraftClient.Protocol.Handlers return result; } + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public byte ReadNextByte(PacketReader reader) + { + return reader.ReadByte(); + } + /// /// Read an uncompressed Named Binary Tag blob and remove it from the cache /// @@ -434,6 +647,11 @@ namespace MinecraftClient.Protocol.Handlers return ReadNextNbt(cache, true); } + public Dictionary ReadNextNbt(PacketReader reader) + { + return ReadWithQueueFallback(reader, cache => ReadNextNbt(cache, true)); + } + /// /// Read an ItemStackTemplate (26.1+) from a cache of bytes. /// Unlike ItemStack, this uses item-first encoding: item_id, count, DataComponentPatch. @@ -465,6 +683,11 @@ namespace MinecraftClient.Protocol.Handlers return item; } + public Item ReadNextItemStackTemplate(PacketReader reader, ItemPalette itemPalette) + { + return ReadWithQueueFallback(reader, cache => ReadNextItemStackTemplate(cache, itemPalette)); + } + /// /// Read a single item slot from a cache of bytes and remove it from the cache /// @@ -568,6 +791,11 @@ namespace MinecraftClient.Protocol.Handlers } } + public Item? ReadNextItemSlot(PacketReader reader, ItemPalette itemPalette) + { + return ReadWithQueueFallback(reader, cache => ReadNextItemSlot(cache, itemPalette)); + } + private void ReadNextDetail(Queue cache) { var potionEffectId = ReadNextVarInt(cache); @@ -724,6 +952,11 @@ namespace MinecraftClient.Protocol.Handlers return entity; } + public Entity ReadNextEntity(PacketReader reader, EntityPalette entityPalette, bool living) + { + return ReadWithQueueFallback(reader, cache => ReadNextEntity(cache, entityPalette, living)); + } + /// /// Read an uncompressed Named Binary Tag blob and remove it from the cache (internal) /// @@ -1077,6 +1310,12 @@ namespace MinecraftClient.Protocol.Handlers } } + public Dictionary ReadNextMetadata(PacketReader reader, ItemPalette itemPalette, + EntityMetadataPalette metadataPalette) + { + return ReadWithQueueFallback(reader, cache => ReadNextMetadata(cache, itemPalette, metadataPalette)); + } + private static bool HasLpVec3Continuation(int firstByte) => (firstByte & 4) == 4; private static double UnpackLpVec3(long packedAxis) @@ -1109,6 +1348,27 @@ namespace MinecraftClient.Protocol.Handlers ); } + public (double X, double Y, double Z) ReadNextLpVec3Values(PacketReader reader) + { + int first = ReadNextByte(reader); + if (first == 0) + return (0.0, 0.0, 0.0); + + int second = ReadNextByte(reader); + uint high = (uint)ReadNextInt(reader); + long packed = ((long)high << 16) | (long)(second << 8) | (uint)first; + + long scale = first & 3; + if (HasLpVec3Continuation(first)) + scale |= ((long)ReadNextVarInt(reader) & 0xFFFFFFFFL) << 2; + + return ( + UnpackLpVec3(packed >> 3) * scale, + UnpackLpVec3(packed >> 18) * scale, + UnpackLpVec3(packed >> 33) * scale + ); + } + /// /// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+) and discard it. /// @@ -1117,6 +1377,11 @@ namespace MinecraftClient.Protocol.Handlers ReadNextLpVec3Values(cache); } + public void ReadNextLpVec3(PacketReader reader) + { + ReadNextLpVec3Values(reader); + } + /// /// Consume bytes for a ResolvableProfile (1.21.9+). /// Wire: Either(GameProfile, Partial) + PlayerSkin.Patch @@ -1379,6 +1644,11 @@ namespace MinecraftClient.Protocol.Handlers } } + public void ReadParticleData(PacketReader reader, ItemPalette itemPalette) + { + ReadWithQueueFallback(reader, cache => ReadParticleData(cache, itemPalette)); + } + private void ReadDustParticle(Queue cache) { ReadNextFloat(cache); // Red @@ -1437,6 +1707,11 @@ namespace MinecraftClient.Protocol.Handlers maximumNumberOfTradeUses, xp, specialPrice, priceMultiplier, demand); } + public VillagerTrade ReadNextTrade(PacketReader reader, ItemPalette itemPalette) + { + return ReadWithQueueFallback(reader, cache => ReadNextTrade(cache, itemPalette)); + } + public string ReadNextChat(Queue cache) { if (protocolversion >= Protocol18Handler.MC_1_20_4_Version) @@ -1454,6 +1729,11 @@ namespace MinecraftClient.Protocol.Handlers } } + public string ReadNextChat(PacketReader reader) + { + return ReadWithQueueFallback(reader, ReadNextChat); + } + /// /// Build an uncompressed Named Binary Tag blob for sending over the network /// @@ -2035,5 +2315,22 @@ namespace MinecraftClient.Protocol.Handlers return fields.ToArray(); } + + private static T ReadWithQueueFallback(PacketReader reader, Func, T> read) + { + byte[] remaining = reader.CopyRemaining(); + Queue cache = new(remaining); + T result = read(cache); + reader.Skip(remaining.Length - cache.Count); + return result; + } + + private static void ReadWithQueueFallback(PacketReader reader, Action> read) + { + byte[] remaining = reader.CopyRemaining(); + Queue cache = new(remaining); + read(cache); + reader.Skip(remaining.Length - cache.Count); + } } } diff --git a/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs b/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs index 57f1bb92..467cb13b 100644 --- a/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs +++ b/MinecraftClient/Protocol/Handlers/Packet/s2c/DeclareCommands.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using MinecraftClient.Protocol.PacketPipeline; namespace MinecraftClient.Protocol.Handlers.packet.s2c { @@ -59,7 +60,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c public static bool IsCommandTreeAvailable => HasValidCommandTree(); - public static void Read(DataTypes dataTypes, Queue packetData, int protocolVersion) + public static void Read(DataTypes dataTypes, PacketReader packetData, int protocolVersion) { Reset(); ConsoleIO.OnDeclareMinecraftCommand(Array.Empty()); @@ -88,7 +89,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c : []; } - private static void ReadCommandTree(DataTypes dataTypes, Queue packetData, int protocolVersion) + private static void ReadCommandTree(DataTypes dataTypes, PacketReader packetData, int protocolVersion) { int count = dataTypes.ReadNextVarInt(packetData); Nodes = new CommandNode[count]; @@ -117,7 +118,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c private static CommandNode ReadArgumentNode( DataTypes dataTypes, - Queue packetData, + PacketReader packetData, int protocolVersion, byte flags, int[] children, @@ -135,7 +136,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c return new(flags, children, redirectNode, name, descriptor, suggestionsType, parserId); } - private static int[] ReadChildIndices(DataTypes dataTypes, Queue packetData) + private static int[] ReadChildIndices(DataTypes dataTypes, PacketReader packetData) { int childCount = dataTypes.ReadNextVarInt(packetData); int[] children = new int[childCount]; @@ -146,7 +147,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c return children; } - private static CommandArgumentDescriptor ReadArgumentDescriptor(DataTypes dataTypes, Queue packetData, ArgumentTypeLayout layout) + private static CommandArgumentDescriptor ReadArgumentDescriptor(DataTypes dataTypes, PacketReader packetData, ArgumentTypeLayout layout) { switch (layout.PayloadKind) { @@ -189,7 +190,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c } } - private static void ReadNumberBounds(DataTypes dataTypes, Queue packetData, Func, TValue> readValue) + private static void ReadNumberBounds(DataTypes dataTypes, PacketReader packetData, Func readValue) { byte flags = dataTypes.ReadNextByte(packetData); if ((flags & 0x01) != 0) diff --git a/MinecraftClient/Protocol/Handlers/Protocol16.cs b/MinecraftClient/Protocol/Handlers/Protocol16.cs index 23741df0..a2595149 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol16.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol16.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; using System.Net.Sockets; using System.Security.Cryptography; @@ -476,6 +477,15 @@ namespace MinecraftClient.Protocol.Handlers } } + private async Task ReceiveAsync(byte[] buffer, int start, int offset, CancellationToken cancellationToken = default) + { + if (offset <= 0) + return; + + Stream stream = encrypted ? s! : c.GetStream(); + await stream.ReadExactlyAsync(buffer.AsMemory(start, offset), cancellationToken); + } + private void Send(byte[] buffer) { if (encrypted) @@ -484,7 +494,61 @@ namespace MinecraftClient.Protocol.Handlers c.Client.Send(buffer); } - private bool Handshake(string uuid, string username, string sessionID, string host, int port, SessionToken session) + private async Task SendAsync(byte[] buffer, CancellationToken cancellationToken = default) + { + if (buffer.Length == 0) + return; + + Stream stream = encrypted ? s! : c.GetStream(); + await stream.WriteAsync(buffer.AsMemory(0, buffer.Length), cancellationToken); + await stream.FlushAsync(cancellationToken); + } + + private async Task ReadDataAsync(int offset, CancellationToken cancellationToken = default) + { + if (offset <= 0) + return []; + + byte[] cache = new byte[offset]; + await ReceiveAsync(cache, 0, offset, cancellationToken); + return cache; + } + + private async Task ReadNextStringAsync(CancellationToken cancellationToken = default) + { + ushort length = (ushort)await ReadNextShortAsync(cancellationToken); + if (length <= 0) + return ""; + + byte[] cache = new byte[length * 2]; + await ReceiveAsync(cache, 0, length * 2, cancellationToken); + return Encoding.BigEndianUnicode.GetString(cache); + } + + private async Task ReadNextByteArrayAsync(CancellationToken cancellationToken = default) + { + short len = await ReadNextShortAsync(cancellationToken); + byte[] data = new byte[len]; + await ReceiveAsync(data, 0, len, cancellationToken); + return data; + } + + private async Task ReadNextShortAsync(CancellationToken cancellationToken = default) + { + byte[] tmp = new byte[2]; + await ReceiveAsync(tmp, 0, 2, cancellationToken); + Array.Reverse(tmp); + return BitConverter.ToInt16(tmp, 0); + } + + private async Task ReadNextByteAsync(CancellationToken cancellationToken = default) + { + byte[] result = new byte[1]; + await ReceiveAsync(result, 0, 1, cancellationToken); + return result[0]; + } + + private async Task HandshakeAsync(string uuid, string username, string sessionID, string host, int port, SessionToken session, CancellationToken cancellationToken = default) { //array byte[] data = new byte[10 + (username.Length + host.Length) * 2]; @@ -518,28 +582,28 @@ namespace MinecraftClient.Protocol.Handlers Array.Reverse(sh); sh.CopyTo(data, 6 + (username.Length * 2) + (host.Length * 2)); - Send(data); + await SendAsync(data, cancellationToken); byte[] pid = new byte[1]; - Receive(pid, 0, 1, SocketFlags.None); + await ReceiveAsync(pid, 0, 1, cancellationToken); while (pid[0] == 0xFA) //Skip some early plugin messages { using (MainThreadExecutionScope.Enter(handler)) ProcessPacket(pid[0]); - Receive(pid, 0, 1, SocketFlags.None); + await ReceiveAsync(pid, 0, 1, cancellationToken); } if (pid[0] == 0xFD) { - string serverID = ReadNextString(); - byte[] PublicServerkey = ReadNextByteArray(); - byte[] token = ReadNextByteArray(); + string serverID = await ReadNextStringAsync(cancellationToken); + byte[] PublicServerkey = await ReadNextByteArrayAsync(cancellationToken); + byte[] token = await ReadNextByteArrayAsync(cancellationToken); if (serverID == "-") ConsoleIO.WriteLineFormatted("§8" + Translations.mcc_server_offline, acceptnewlines: true); else if (Settings.Config.Logging.DebugMessages) ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_handshake, serverID)); - return StartEncryption(uuid, username, sessionID, Config.Main.General.AccountType, token, serverID, PublicServerkey, session); + return await StartEncryptionAsync(uuid, username, sessionID, Config.Main.General.AccountType, token, serverID, PublicServerkey, session, cancellationToken); } else { @@ -548,7 +612,7 @@ namespace MinecraftClient.Protocol.Handlers } } - private bool StartEncryption(string uuid, string username, string sessionID, LoginType type, byte[] token, string serverIDhash, byte[] serverPublicKey, SessionToken session) + private async Task StartEncryptionAsync(string uuid, string username, string sessionID, LoginType type, byte[] token, string serverIDhash, byte[] serverPublicKey, SessionToken session, CancellationToken cancellationToken = default) { RSACryptoServiceProvider RSAService = CryptoHandler.DecodeRSAPublicKey(serverPublicKey)!; byte[] secretKey = CryptoHandler.ClientAESPrivateKey ?? CryptoHandler.GenerateAESPrivateKey(); @@ -571,7 +635,7 @@ namespace MinecraftClient.Protocol.Handlers if (needCheckSession) { - if (ProtocolHandler.SessionCheck(uuid, sessionID, serverHash, type)) + if (await ProtocolHandler.SessionCheckAsync(uuid, sessionID, serverHash, type)) { session.ServerIDhash = serverIDhash; session.ServerPublicKey = serverPublicKey; @@ -604,14 +668,14 @@ namespace MinecraftClient.Protocol.Handlers token_enc.CopyTo(data, 5 + (short)key_enc.Length); //Send it back - Send(data); + await SendAsync(data, cancellationToken); //Getting the next packet byte[] pid = new byte[1]; - Receive(pid, 0, 1, SocketFlags.None); + await ReceiveAsync(pid, 0, 1, cancellationToken); if (pid[0] == 0xFC) { - ReadData(4); + await ReadDataAsync(4, cancellationToken); s = new AesCfb8Stream(c.GetStream(), secretKey); encrypted = true; return true; @@ -625,9 +689,14 @@ namespace MinecraftClient.Protocol.Handlers public bool Login(PlayerKeyPair? playerKeyPair, SessionToken session, bool isTransfer = false) { - if (Handshake(handler.GetUserUuidStr(), handler.GetUsername(), handler.GetSessionID(), handler.GetServerHost(), handler.GetServerPort(), session)) + return LoginAsync(playerKeyPair, session, isTransfer).GetAwaiter().GetResult(); + } + + private async Task LoginAsync(PlayerKeyPair? playerKeyPair, SessionToken session, bool isTransfer = false) + { + if (await HandshakeAsync(handler.GetUserUuidStr(), handler.GetUsername(), handler.GetSessionID(), handler.GetServerHost(), handler.GetServerPort(), session)) { - Send(new byte[] { 0xCD, 0 }); + await SendAsync([0xCD, 0]); try { byte[] pid = new byte[1]; @@ -635,22 +704,24 @@ namespace MinecraftClient.Protocol.Handlers { if (c.Connected) { - Receive(pid, 0, 1, SocketFlags.None); + await ReceiveAsync(pid, 0, 1); while (pid[0] >= 0xC0 && pid[0] != 0xFF) //Skip some early packets or plugin messages { using (MainThreadExecutionScope.Enter(handler)) ProcessPacket(pid[0]); - Receive(pid, 0, 1, SocketFlags.None); + await ReceiveAsync(pid, 0, 1); } if (pid[0] == (byte)1) { - ReadData(4); ReadNextString(); ReadData(5); + await ReadDataAsync(4); + _ = await ReadNextStringAsync(); + await ReadDataAsync(5); StartUpdating(); return true; //The Server accepted the request } else if (pid[0] == (byte)0xFF) { - string reason = ReadNextString(); + string reason = await ReadNextStringAsync(); handler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, reason); return false; } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18.cs b/MinecraftClient/Protocol/Handlers/Protocol18.cs index 1765a3be..bdff67a0 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18.cs @@ -21,6 +21,7 @@ using MinecraftClient.Protocol.Handlers.Forge; using MinecraftClient.Protocol.Handlers.packet.s2c; using MinecraftClient.Protocol.Handlers.PacketPalettes; using MinecraftClient.Protocol.Message; +using MinecraftClient.Protocol.PacketPipeline; using MinecraftClient.Protocol.ProfileKey; using MinecraftClient.Protocol.Session; using MinecraftClient.Proxy; @@ -91,7 +92,7 @@ namespace MinecraftClient.Protocol.Handlers private readonly int rawProtocolVersion; private int currentDimension; private bool isOnlineMode = false; - private readonly BlockingCollection>> packetQueue = new(); + private readonly BlockingCollection packetQueue = new(); private readonly Dictionary legacyAchievementProgress = new(StringComparer.Ordinal); private float LastYaw, LastPitch; private double lastSentX, lastSentY, lastSentZ; @@ -309,8 +310,7 @@ namespace MinecraftClient.Protocol.Handlers if (packetQueue.TryTake(out var packetInfo, 1)) { - var (packetId, packetData) = packetInfo; - HandlePacket(packetId, packetData); + HandlePacket(packetInfo.PacketId, packetInfo.CreateReader()); continue; } @@ -393,27 +393,27 @@ namespace MinecraftClient.Protocol.Handlers /// /// will contain packet ID /// will contain raw packet Data - internal Tuple> ReadNextPacket() + internal IncomingPacket ReadNextPacket() { - var (packetId, packetData) = socketWrapper.GetNextPacket( + IncomingPacket packet = socketWrapper.GetNextPacket( protocolVersion >= MC_1_8_Version ? compression_treshold : -1, dataTypes); if (handler.GetNetworkPacketCaptureEnabled()) - handler.OnNetworkPacket(packetId, packetData.ToList(), currentState == CurrentState.Login, true); + handler.OnNetworkPacket(packet.PacketId, packet.Payload.ToList(), currentState == CurrentState.Login, true); - return new(packetId, packetData); + return packet; } - internal async Task>> ReadNextPacketAsync(CancellationToken cancellationToken) + internal async Task ReadNextPacketAsync(CancellationToken cancellationToken) { - var (packetId, packetData) = await socketWrapper.GetNextPacketAsync( + IncomingPacket packet = await socketWrapper.GetNextPacketAsync( protocolVersion >= MC_1_8_Version ? compression_treshold : -1, dataTypes, cancellationToken); if (handler.GetNetworkPacketCaptureEnabled()) - handler.OnNetworkPacket(packetId, packetData.ToList(), currentState == CurrentState.Login, true); + handler.OnNetworkPacket(packet.PacketId, packet.Payload.ToList(), currentState == CurrentState.Login, true); - return new(packetId, packetData); + return packet; } /// @@ -422,11 +422,9 @@ namespace MinecraftClient.Protocol.Handlers /// Packet ID /// Packet contents /// TRUE if the packet was processed, FALSE if ignored or unknown - internal bool HandlePacket(int packetId, Queue packetData) + internal bool HandlePacket(int packetId, PacketReader packetData) { - // This copy is necessary because by the time we get to the catch block, - // the packetData queue will have been processed and the data will be lost - var _copy = packetData.ToArray(); + byte[] _copy = packetData.GetRawData(); try { @@ -488,11 +486,11 @@ namespace MinecraftClient.Protocol.Handlers break; case ConfigurationPacketTypesIn.KeepAlive: - SendPacket(ConfigurationPacketTypesOut.KeepAlive, packetData); + SendPacket(ConfigurationPacketTypesOut.KeepAlive, packetData.CopyRemaining()); break; case ConfigurationPacketTypesIn.Ping: - SendPacket(ConfigurationPacketTypesOut.Pong, packetData); + SendPacket(ConfigurationPacketTypesOut.Pong, packetData.CopyRemaining()); break; case ConfigurationPacketTypesIn.RegistryData: @@ -674,7 +672,7 @@ namespace MinecraftClient.Protocol.Handlers return true; } - public void HandleResourcePackPacket(Queue packetData) + public void HandleResourcePackPacket(PacketReader packetData) { var uuid = Guid.Empty; @@ -722,17 +720,17 @@ namespace MinecraftClient.Protocol.Handlers } } - private bool HandlePlayPackets(int packetId, Queue packetData) + private bool HandlePlayPackets(int packetId, PacketReader packetData) { switch (packetPalette.GetIncomingTypeById(packetId)) { case PacketTypesIn.KeepAlive: // Keep Alive (Play) - SendPacket(PacketTypesOut.KeepAlive, packetData); + SendPacket(PacketTypesOut.KeepAlive, packetData.CopyRemaining()); handler.OnServerKeepAlive(); break; case PacketTypesIn.Ping: - SendPacket(PacketTypesOut.Pong, packetData); + SendPacket(PacketTypesOut.Pong, packetData.CopyRemaining()); break; case PacketTypesIn.JoinGame: @@ -1602,7 +1600,7 @@ namespace MinecraftClient.Protocol.Handlers pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, - new Queue(decompressed)); + new PacketReader(decompressed)); Interlocked.Decrement(ref handler.GetWorld().chunkLoadNotCompleted); } else @@ -1984,7 +1982,7 @@ namespace MinecraftClient.Protocol.Handlers hasSkyLight = dataTypes.ReadNextBool(packetData); var compressed = dataTypes.ReadData(compressedDataSize, packetData); var decompressed = ZlibUtils.Decompress(compressed); - chunkData = new Queue(decompressed); + chunkData = new PacketReader(decompressed); } else { @@ -2297,7 +2295,7 @@ namespace MinecraftClient.Protocol.Handlers // Length is unneeded as the whole remaining packetData is the entire payload of the packet. if (protocolVersion < MC_1_8_Version) pForge.ReadNextVarShort(packetData); - handler.OnPluginChannelMessage(channel, packetData.ToArray()); + handler.OnPluginChannelMessage(channel, packetData.CopyRemaining()); return pForge.HandlePluginMessage(channel, packetData, ref currentDimension); case PacketTypesIn.Disconnect: handler.OnConnectionLost(ChatBot.DisconnectReason.InGameKick, @@ -3323,7 +3321,7 @@ namespace MinecraftClient.Protocol.Handlers /// Read a Holder<SoundEvent> from packet data and return its key when inline. /// Returns null when the holder is a registry reference. /// - private string? ReadSoundEventHolderName(Queue packetData) + private string? ReadSoundEventHolderName(PacketReader packetData) { int soundHolderId = dataTypes.ReadNextVarInt(packetData); if (soundHolderId != 0) @@ -3339,7 +3337,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Handle the Statistics packet for pre-1.12 legacy achievements. /// - private void HandleLegacyStatistics(Queue packetData) + private void HandleLegacyStatistics(PacketReader packetData) { int statCount = dataTypes.ReadNextVarInt(packetData); @@ -3370,7 +3368,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Handle the Advancements packet (1.12+). /// - private void HandleAdvancements(Queue packetData) + private void HandleAdvancements(PacketReader packetData) { bool reset = dataTypes.ReadNextBool(packetData); @@ -3541,14 +3539,14 @@ namespace MinecraftClient.Protocol.Handlers /// /// Handle the SelectAdvancementTab packet. /// - private void HandleSelectAdvancementTab(Queue packetData) + private void HandleSelectAdvancementTab(PacketReader packetData) { bool hasTab = dataTypes.ReadNextBool(packetData); string? tabId = hasTab ? dataTypes.ReadNextString(packetData) : null; handler.OnSelectAdvancementTab(tabId); } - private void HandleUnlockRecipes(Queue packetData) + private void HandleUnlockRecipes(PacketReader packetData) { int action = dataTypes.ReadNextVarInt(packetData); if (!SkipRecipeBookSettings(packetData)) @@ -3576,7 +3574,7 @@ namespace MinecraftClient.Protocol.Handlers } } - private void HandleRecipeBookAdd(Queue packetData) + private void HandleRecipeBookAdd(PacketReader packetData) { int entryCount = dataTypes.ReadNextVarInt(packetData); RecipeBookRecipeEntry[] recipeEntries = new RecipeBookRecipeEntry[entryCount]; @@ -3593,7 +3591,7 @@ namespace MinecraftClient.Protocol.Handlers handler.OnRecipeBookAdd(recipeEntries, replace); } - private string[] ReadRecipeBookRecipeIds(Queue packetData) + private string[] ReadRecipeBookRecipeIds(PacketReader packetData) { int recipeCount = dataTypes.ReadNextVarInt(packetData); string[] recipeIds = new string[recipeCount]; @@ -3604,7 +3602,7 @@ namespace MinecraftClient.Protocol.Handlers return recipeIds; } - private string[] ReadRecipeBookDisplayIds(Queue packetData) + private string[] ReadRecipeBookDisplayIds(PacketReader packetData) { int recipeCount = dataTypes.ReadNextVarInt(packetData); string[] recipeIds = new string[recipeCount]; @@ -3615,7 +3613,7 @@ namespace MinecraftClient.Protocol.Handlers return recipeIds; } - private RecipeBookRecipeEntry ReadRecipeBookDisplayEntry(Queue packetData) + private RecipeBookRecipeEntry ReadRecipeBookDisplayEntry(PacketReader packetData) { int displayId = dataTypes.ReadNextVarInt(packetData); string resultLabel = ReadRecipeDisplayResultLabel(packetData); @@ -3629,7 +3627,7 @@ namespace MinecraftClient.Protocol.Handlers return new RecipeBookRecipeEntry(commandId, displayText); } - private string ReadRecipeDisplayResultLabel(Queue packetData) + private string ReadRecipeDisplayResultLabel(PacketReader packetData) { int displayType = dataTypes.ReadNextVarInt(packetData); return displayType switch @@ -3643,7 +3641,7 @@ namespace MinecraftClient.Protocol.Handlers }; } - private string ReadShapelessRecipeDisplayResultLabel(Queue packetData) + private string ReadShapelessRecipeDisplayResultLabel(PacketReader packetData) { int ingredientCount = dataTypes.ReadNextVarInt(packetData); for (int i = 0; i < ingredientCount; i++) @@ -3654,7 +3652,7 @@ namespace MinecraftClient.Protocol.Handlers return result; } - private string ReadShapedRecipeDisplayResultLabel(Queue packetData) + private string ReadShapedRecipeDisplayResultLabel(PacketReader packetData) { _ = dataTypes.ReadNextVarInt(packetData); // width _ = dataTypes.ReadNextVarInt(packetData); // height @@ -3667,7 +3665,7 @@ namespace MinecraftClient.Protocol.Handlers return result; } - private string ReadFurnaceRecipeDisplayResultLabel(Queue packetData) + private string ReadFurnaceRecipeDisplayResultLabel(PacketReader packetData) { _ = ReadSlotDisplayLabel(packetData); // ingredient _ = ReadSlotDisplayLabel(packetData); // fuel @@ -3678,7 +3676,7 @@ namespace MinecraftClient.Protocol.Handlers return result; } - private string ReadStonecutterRecipeDisplayResultLabel(Queue packetData) + private string ReadStonecutterRecipeDisplayResultLabel(PacketReader packetData) { _ = ReadSlotDisplayLabel(packetData); // input string result = ReadSlotDisplayLabel(packetData); @@ -3686,7 +3684,7 @@ namespace MinecraftClient.Protocol.Handlers return result; } - private string ReadSmithingRecipeDisplayResultLabel(Queue packetData) + private string ReadSmithingRecipeDisplayResultLabel(PacketReader packetData) { _ = ReadSlotDisplayLabel(packetData); // template _ = ReadSlotDisplayLabel(packetData); // base @@ -3696,7 +3694,7 @@ namespace MinecraftClient.Protocol.Handlers return result; } - private string ReadSlotDisplayLabel(Queue packetData) + private string ReadSlotDisplayLabel(PacketReader packetData) { int slotDisplayType = dataTypes.ReadNextVarInt(packetData); @@ -3739,7 +3737,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Reads a with_any_potion slot display (26.1+): contains a nested SlotDisplay. /// - private string ReadWithAnyPotionSlotDisplayLabel(Queue packetData) + private string ReadWithAnyPotionSlotDisplayLabel(PacketReader packetData) { return ReadSlotDisplayLabel(packetData); } @@ -3747,7 +3745,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Reads an only_with_component slot display (26.1+): contains a nested SlotDisplay and a DataComponentType VarInt ID. /// - private string ReadOnlyWithComponentSlotDisplayLabel(Queue packetData) + private string ReadOnlyWithComponentSlotDisplayLabel(PacketReader packetData) { string sourceLabel = ReadSlotDisplayLabel(packetData); _ = dataTypes.ReadNextVarInt(packetData); // DataComponentType registry id @@ -3757,14 +3755,14 @@ namespace MinecraftClient.Protocol.Handlers /// /// Reads a dyed slot display (26.1+): contains two nested SlotDisplays (dye + target). /// - private string ReadDyedSlotDisplayLabel(Queue packetData) + private string ReadDyedSlotDisplayLabel(PacketReader packetData) { _ = ReadSlotDisplayLabel(packetData); // dye string targetLabel = ReadSlotDisplayLabel(packetData); // target return targetLabel; } - private string ReadSmithingTrimSlotDisplayLabel(Queue packetData) + private string ReadSmithingTrimSlotDisplayLabel(PacketReader packetData) { string baseLabel = ReadSlotDisplayLabel(packetData); _ = ReadSlotDisplayLabel(packetData); // material @@ -3772,14 +3770,14 @@ namespace MinecraftClient.Protocol.Handlers return baseLabel; } - private string ReadWithRemainderSlotDisplayLabel(Queue packetData) + private string ReadWithRemainderSlotDisplayLabel(PacketReader packetData) { string inputLabel = ReadSlotDisplayLabel(packetData); _ = ReadSlotDisplayLabel(packetData); // remainder return inputLabel; } - private string ReadCompositeSlotDisplayLabel(Queue packetData) + private string ReadCompositeSlotDisplayLabel(PacketReader packetData) { int optionCount = dataTypes.ReadNextVarInt(packetData); string label = "Composite"; @@ -3798,12 +3796,12 @@ namespace MinecraftClient.Protocol.Handlers /// Read an ItemStackTemplate (26.1+) which encodes fields in a different order /// than ItemStack: item_id (VarInt), count (VarInt), DataComponentPatch. /// - private string ReadItemStackTemplateLabel(Queue packetData) + private string ReadItemStackTemplateLabel(PacketReader packetData) { return dataTypes.ReadNextItemStackTemplate(packetData, itemPalette).GetTypeString(); } - private void SkipOptionalCraftingRequirements(Queue packetData) + private void SkipOptionalCraftingRequirements(PacketReader packetData) { if (!dataTypes.ReadNextBool(packetData)) return; @@ -3813,7 +3811,7 @@ namespace MinecraftClient.Protocol.Handlers SkipItemHolderSet(packetData); } - private void SkipItemHolderSet(Queue packetData) + private void SkipItemHolderSet(PacketReader packetData) { int entryCount = dataTypes.ReadNextVarInt(packetData) - 1; if (entryCount == -1) @@ -3826,12 +3824,12 @@ namespace MinecraftClient.Protocol.Handlers _ = dataTypes.ReadNextVarInt(packetData); } - private bool SkipRecipeBookSettings(Queue packetData) + private bool SkipRecipeBookSettings(PacketReader packetData) { // MC 1.13 uses 4 booleans for the crafting/smelting recipe book states. // MC 1.14+ expands this to 8 booleans by adding blast furnace and smoker states. int boolCount = protocolVersion >= MC_1_14_Version ? 8 : 4; - if (packetData.Count < boolCount) + if (packetData.RemainingLength < boolCount) return false; for (int i = 0; i < boolCount; i++) @@ -3896,9 +3894,9 @@ namespace MinecraftClient.Protocol.Handlers SendPacket(packetPalette.GetOutgoingIdByType(packet), packetData); } - private void ProcessChunkBlockEntityData(int chunkX, int chunkZ, Queue packetData) + private void ProcessChunkBlockEntityData(int chunkX, int chunkZ, PacketReader packetData) { - if (protocolVersion < MC_1_17_Version || packetData.Count == 0) + if (protocolVersion < MC_1_17_Version || packetData.RemainingLength == 0) return; int blockEntityCount = dataTypes.ReadNextVarInt(packetData); @@ -4057,7 +4055,9 @@ namespace MinecraftClient.Protocol.Handlers // 3. Encryption Request - 9. Login Acknowledged while (true) { - var (packetId, packetData) = await ReadNextPacketAsync(CancellationToken.None); + IncomingPacket packet = await ReadNextPacketAsync(CancellationToken.None); + int packetId = packet.PacketId; + PacketReader packetData = packet.CreateReader(); switch (packetId) { @@ -4195,7 +4195,9 @@ namespace MinecraftClient.Protocol.Handlers int loopPrevention = ushort.MaxValue; while (true) { - var (packetId, packetData) = await ReadNextPacketAsync(CancellationToken.None); + IncomingPacket packet = await ReadNextPacketAsync(CancellationToken.None); + int packetId = packet.PacketId; + PacketReader packetData = packet.CreateReader(); if (packetId < 0 || loopPrevention-- < 0) // Failed to read packet or too many iterations (issue #1150) { handler.OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, @@ -4356,11 +4358,12 @@ namespace MinecraftClient.Protocol.Handlers var statusRequest = DataTypes.GetVarInt(0); socketWrapper.SendDataRAW(dataTypes.ConcatBytes(DataTypes.GetVarInt(statusRequest.Length), statusRequest)); - var (statusPacketId, packetData) = socketWrapper.GetNextPacket(-1, dataTypes); - if (statusPacketId != 0x00) + IncomingPacket statusPacket = socketWrapper.GetNextPacket(-1, dataTypes); + if (statusPacket.PacketId != 0x00) return false; // Get the Json data + var packetData = statusPacket.CreateReader(); var result = dataTypes.ReadNextString(packetData); if (Config.Logging.DebugMessages) @@ -4436,9 +4439,10 @@ namespace MinecraftClient.Protocol.Handlers var pingRequest = dataTypes.ConcatBytes(DataTypes.GetVarInt(0x01), DataTypes.GetLong(pingPayload)); socketWrapper.SendDataRAW(dataTypes.ConcatBytes(DataTypes.GetVarInt(pingRequest.Length), pingRequest)); - var (pongPacketId, pongPacketData) = socketWrapper.GetNextPacket(-1, dataTypes); - if (pongPacketId == 0x01) + IncomingPacket pongPacket = socketWrapper.GetNextPacket(-1, dataTypes); + if (pongPacket.PacketId == 0x01) { + var pongPacketData = pongPacket.CreateReader(); long pongPayload = dataTypes.ReadNextLong(pongPacketData); pingMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - pingPayload; } diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs b/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs index 40b748f3..3fef4122 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18Forge.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using MinecraftClient.Protocol.Handlers.Forge; using MinecraftClient.Protocol.Message; +using MinecraftClient.Protocol.PacketPipeline; using MinecraftClient.Scripting; namespace MinecraftClient.Protocol.Handlers @@ -52,7 +53,9 @@ namespace MinecraftClient.Protocol.Handlers { while (fmlHandshakeState != FMLHandshakeClientState.DONE) { - (int packetID, Queue packetData) = await protocol18.ReadNextPacketAsync(cancellationToken); + IncomingPacket packet = await protocol18.ReadNextPacketAsync(cancellationToken); + int packetID = packet.PacketId; + PacketReader packetData = packet.CreateReader(); if (packetID == 0x40) // Disconnect { @@ -93,7 +96,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Packet data to read from /// Length from packet data - public int ReadNextVarShort(Queue packetData) + public int ReadNextVarShort(PacketReader packetData) { if (ForgeEnabled()) { @@ -114,7 +117,7 @@ namespace MinecraftClient.Protocol.Handlers /// Plugin message data /// Current world dimension /// TRUE if the plugin message was recognized and handled - public bool HandlePluginMessage(string channel, Queue packetData, ref int currentDimension) + public bool HandlePluginMessage(string channel, PacketReader packetData, ref int currentDimension) { if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML && fmlHandshakeState != FMLHandshakeClientState.DONE) { @@ -243,7 +246,7 @@ namespace MinecraftClient.Protocol.Handlers /// Plugin message data /// Response data to return to server /// TRUE/FALSE depending on whether the packet was understood or not - public bool HandleLoginPluginRequest(string channel, Queue packetData, ref List responseData) + public bool HandleLoginPluginRequest(string channel, PacketReader packetData, ref List responseData) { if (ForgeEnabled() && (forgeInfo!.Version == FMLVersion.FML2 || forgeInfo!.Version == FMLVersion.FML3) && channel == "fml:loginwrapper") { @@ -332,7 +335,7 @@ namespace MinecraftClient.Protocol.Handlers // FML3 specific, List dataPackRegistries = new(); - if (forgeInfo!.Version == FMLVersion.FML3 && packetData.Count != 0) + if (forgeInfo!.Version == FMLVersion.FML3 && packetData.RemainingLength != 0) { int dataPackRegistryCount = dataTypes.ReadNextVarInt(packetData); for (int i = 0; i < dataPackRegistryCount; i++) diff --git a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs index 9bd29e87..92ccac03 100644 --- a/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs +++ b/MinecraftClient/Protocol/Handlers/Protocol18Terrain.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; //using System.Linq; //using System.Text; using MinecraftClient.Mapping; +using MinecraftClient.Protocol.PacketPipeline; namespace MinecraftClient.Protocol.Handlers { @@ -23,7 +24,7 @@ namespace MinecraftClient.Protocol.Handlers /// /// Cache for reading data [MethodImpl(MethodImplOptions.AggressiveOptimization)] - private Chunk? ReadBlockStatesField(Queue cache) + private Chunk? ReadBlockStatesField(PacketReader cache) { // read Block states (Type: Paletted Container) byte bitsPerEntry = dataTypes.ReadNextByte(cache); @@ -134,7 +135,7 @@ namespace MinecraftClient.Protocol.Handlers /// Cache for reading chunk data /// token to cancel the task [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, Queue cache) + public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, PacketReader cache) { World world = handler.GetWorld(); @@ -236,7 +237,7 @@ namespace MinecraftClient.Protocol.Handlers /// Cache for reading chunk data /// token to cancel the task [MethodImpl(MethodImplOptions.AggressiveOptimization)] - public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue cache) + public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, PacketReader cache) { World world = handler.GetWorld(); diff --git a/MinecraftClient/Protocol/Handlers/SocketWrapper.cs b/MinecraftClient/Protocol/Handlers/SocketWrapper.cs index c3ed8b15..3260adeb 100644 --- a/MinecraftClient/Protocol/Handlers/SocketWrapper.cs +++ b/MinecraftClient/Protocol/Handlers/SocketWrapper.cs @@ -107,24 +107,24 @@ namespace MinecraftClient.Protocol.Handlers return Array.Empty(); } - internal Tuple> GetNextPacket(int compressionThreshold, DataTypes dataTypes) + internal IncomingPacket GetNextPacket(int compressionThreshold, DataTypes dataTypes) { int packetLength = ReadNextVarIntRaw(); using PacketReadStream packetStream = new(readStream, packetLength); byte[] payload = ReadPacketPayload(packetStream, compressionThreshold); - Queue packetData = new(payload); + var packetData = new PacketReader(payload); int packetId = dataTypes.ReadNextVarInt(packetData); - return new(packetId, packetData); + return new(packetId, packetData.CopyRemaining()); } - internal async Task>> GetNextPacketAsync(int compressionThreshold, DataTypes dataTypes, CancellationToken cancellationToken) + internal async Task GetNextPacketAsync(int compressionThreshold, DataTypes dataTypes, CancellationToken cancellationToken) { int packetLength = await ReadNextVarIntRawAsync(cancellationToken); await using PacketReadStream packetStream = new(readStream, packetLength); byte[] payload = await ReadPacketPayloadAsync(packetStream, compressionThreshold, cancellationToken); - Queue packetData = new(payload); + var packetData = new PacketReader(payload); int packetId = dataTypes.ReadNextVarInt(packetData); - return new(packetId, packetData); + return new(packetId, packetData.CopyRemaining()); } /// diff --git a/MinecraftClient/Protocol/PacketPipeline/IncomingPacket.cs b/MinecraftClient/Protocol/PacketPipeline/IncomingPacket.cs new file mode 100644 index 00000000..8d5ae340 --- /dev/null +++ b/MinecraftClient/Protocol/PacketPipeline/IncomingPacket.cs @@ -0,0 +1,13 @@ +using System; + +namespace MinecraftClient.Protocol.PacketPipeline; + +internal readonly record struct IncomingPacket(int PacketId, byte[] Payload) +{ + public PacketReader CreateReader() + { + return new PacketReader(Payload); + } + + public ReadOnlySpan PayloadSpan => Payload; +} diff --git a/MinecraftClient/Protocol/PacketPipeline/PacketReader.cs b/MinecraftClient/Protocol/PacketPipeline/PacketReader.cs new file mode 100644 index 00000000..1f7fd6ea --- /dev/null +++ b/MinecraftClient/Protocol/PacketPipeline/PacketReader.cs @@ -0,0 +1,123 @@ +using System; +using System.Buffers.Binary; + +namespace MinecraftClient.Protocol.PacketPipeline; + +public sealed class PacketReader +{ + private readonly byte[] buffer; + private int position; + + public PacketReader(byte[] buffer) + { + ArgumentNullException.ThrowIfNull(buffer); + this.buffer = buffer; + } + + public int Position => position; + public int RemainingLength => buffer.Length - position; + public ReadOnlySpan RemainingSpan => buffer.AsSpan(position); + public ReadOnlySpan FullSpan => buffer; + + public byte[] GetRawData() => buffer; + + public byte ReadByte() + { + EnsureAvailable(1); + return buffer[position++]; + } + + public byte[] ReadData(int length) + { + if (length == 0) + return []; + + EnsureAvailable(length); + byte[] data = GC.AllocateUninitializedArray(length); + Buffer.BlockCopy(buffer, position, data, 0, length); + position += length; + return data; + } + + public void ReadData(Span destination) + { + if (destination.Length == 0) + return; + + EnsureAvailable(destination.Length); + buffer.AsSpan(position, destination.Length).CopyTo(destination); + position += destination.Length; + } + + public void ReadDataReverse(Span destination) + { + if (destination.Length == 0) + return; + + EnsureAvailable(destination.Length); + for (int i = destination.Length - 1; i >= 0; --i) + destination[i] = buffer[position++]; + } + + public void Skip(int length) + { + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length)); + + EnsureAvailable(length); + position += length; + } + + public ushort ReadUInt16BigEndian() + { + EnsureAvailable(sizeof(ushort)); + ushort value = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(position, sizeof(ushort))); + position += sizeof(ushort); + return value; + } + + public short ReadInt16BigEndian() + { + EnsureAvailable(sizeof(short)); + short value = BinaryPrimitives.ReadInt16BigEndian(buffer.AsSpan(position, sizeof(short))); + position += sizeof(short); + return value; + } + + public int ReadInt32BigEndian() + { + EnsureAvailable(sizeof(int)); + int value = BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(position, sizeof(int))); + position += sizeof(int); + return value; + } + + public long ReadInt64BigEndian() + { + EnsureAvailable(sizeof(long)); + long value = BinaryPrimitives.ReadInt64BigEndian(buffer.AsSpan(position, sizeof(long))); + position += sizeof(long); + return value; + } + + public byte[] CopyRemaining() + { + return ReadOnlySpanToArray(RemainingSpan); + } + + private void EnsureAvailable(int length) + { + if (RemainingLength < length) + throw new OverflowException("Reached the end of the packet."); + } + + private static byte[] ReadOnlySpanToArray(ReadOnlySpan span) + { + if (span.IsEmpty) + return []; + + byte[] copy = GC.AllocateUninitializedArray(span.Length); + span.CopyTo(copy); + return copy; + } +}