From fc05d2c98c7de63334da1b2b4d1b4c113e1463cd Mon Sep 17 00:00:00 2001 From: milutinke Date: Wed, 1 Apr 2026 13:41:07 +0200 Subject: [PATCH] Fixed item dropping issue. --- MinecraftClient/McClient.cs | 35 ++++ MinecraftClient/Mcp/MccMcpCapabilities.cs | 241 +++++++++++++++++++--- 2 files changed, 243 insertions(+), 33 deletions(-) diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index d64ef70a..6370a972 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -2613,6 +2613,41 @@ namespace MinecraftClient return handler.SendHeldItemChange(slot); } + /// + /// Drop the currently selected hotbar item like a real player pressing Q or Ctrl+Q. + /// + /// TRUE to drop the whole stack, FALSE to drop one item + /// TRUE if the packet was sent + public bool DropSelectedItem(bool dropEntireStack) + { + if (InvokeRequired) + return InvokeOnMainThread(() => DropSelectedItem(dropEntireStack)); + + Location actionLocation = GetCurrentLocation().ToFloor(); + int status = dropEntireStack ? 3 : 4; + bool sent = handler.SendPlayerDigging(status, actionLocation, Direction.Down, sequenceId++); + if (sent) + ApplySelectedItemDropPrediction(dropEntireStack); + + return sent; + } + + private void ApplySelectedItemDropPrediction(bool dropEntireStack) + { + if (!inventories.TryGetValue(0, out Container? playerInventory)) + return; + + int selectedSlotId = CurrentSlot + 36; + if (!playerInventory.Items.TryGetValue(selectedSlotId, out Item? heldItem) || heldItem.IsEmpty) + return; + + if (dropEntireStack || heldItem.Count <= 1) + playerInventory.Items.Remove(selectedSlotId); + else heldItem.Count--; + + DispatchBotEvent(bot => bot.OnInventoryUpdate(0)); + } + /// /// Update sign text /// diff --git a/MinecraftClient/Mcp/MccMcpCapabilities.cs b/MinecraftClient/Mcp/MccMcpCapabilities.cs index 9b9b7e15..cce37911 100644 --- a/MinecraftClient/Mcp/MccMcpCapabilities.cs +++ b/MinecraftClient/Mcp/MccMcpCapabilities.cs @@ -1872,12 +1872,17 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities if (!inventories.TryGetValue(inventoryId, out Container? inventory)) return MccMcpResult.Fail("invalid_state"); - var slots = inventory.Items.Select(item => new - { - slot = item.Key, - type = item.Value.Type.ToString(), - count = item.Value.Count - }).ToArray(); + var slots = inventory.Items + .Where(item => IsSnapshotInventorySlot(inventory, item.Key)) + .OrderBy(item => item.Key) + .Select(item => new + { + slot = item.Key, + type = item.Value.Type.ToString(), + count = item.Value.Count + }) + .ToArray(); + object? cursor = TryBuildCursorSnapshot(inventory); return MccMcpResult.Ok(new { @@ -1885,7 +1890,8 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities type = inventory.Type.ToString(), title = inventory.Title, slotCount = inventory.Type.SlotCount(), - slots + slots, + cursor }); }); } @@ -1975,7 +1981,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities type = entry.Value.Type.ToString(), title = entry.Value.Title, slotCount = entry.Value.Type.SlotCount(), - nonEmptySlots = entry.Value.Items.Count, + nonEmptySlots = entry.Value.Items.Count(item => IsSnapshotInventorySlot(entry.Value, item.Key)), active = entry.Key > 0 && entry.Key == GetActiveContainerId(client) }) .ToArray(); @@ -2118,12 +2124,19 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities if (!inventories.TryGetValue(inventoryId, out Container? inventory)) return MccMcpResult.Fail("invalid_state"); + int cursorCount = GetCursorItemCount(inventory, parsedItemType); var matchingSlotQuery = inventory.Items + .Where(pair => IsDroppableInventorySlot(inventory, pair.Key)) .Where(pair => pair.Value.Type == parsedItemType && pair.Value.Count > 0) - .Select(pair => new { slot = pair.Key, count = pair.Value.Count }); + .Select(pair => new + { + slot = pair.Key, + count = pair.Value.Count, + hotbarPriority = inventoryId == 0 && IsHotbarSlot(inventory, pair.Key) ? 0 : 1 + }); var matchingSlots = (preferStack - ? matchingSlotQuery.OrderByDescending(pair => pair.count).ThenBy(pair => pair.slot) - : matchingSlotQuery.OrderBy(pair => pair.count).ThenBy(pair => pair.slot)) + ? matchingSlotQuery.OrderBy(pair => pair.hotbarPriority).ThenByDescending(pair => pair.count).ThenBy(pair => pair.slot) + : matchingSlotQuery.OrderBy(pair => pair.hotbarPriority).ThenBy(pair => pair.count).ThenBy(pair => pair.slot)) .ToArray(); int beforeCount = matchingSlots.Sum(pair => pair.count); @@ -2134,6 +2147,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities itemType = parsedItemType.ToString(), requestedCount = count, availableCount = beforeCount, + cursorCount, inventoryId }); } @@ -2151,43 +2165,39 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities int dropFromSlot = Math.Min(remaining, currentItem.Count); touchedSlots.Add(entry.slot); - bool ok = true; - - if (dropFromSlot == currentItem.Count) - { - ok = client.DoWindowAction(inventoryId, entry.slot, WindowActionType.DropItemStack); - } - else - { - for (int i = 0; i < dropFromSlot; i++) - { - if (!client.DoWindowAction(inventoryId, entry.slot, WindowActionType.DropItem)) - { - ok = false; - break; - } - } - } + bool ok = TryDropInventorySlotItems(client, inventoryId, inventory, entry.slot, parsedItemType, dropFromSlot, out int droppedFromSlot); if (!ok) { + Container? failedInventory = client.GetInventory(inventoryId); + int currentCount = failedInventory is null + ? 0 + : failedInventory.Items + .Where(pair => IsDroppableInventorySlot(failedInventory, pair.Key)) + .Where(pair => pair.Value.Type == parsedItemType) + .Sum(pair => pair.Value.Count); return MccMcpResult.Fail("action_failed", data: new { itemType = parsedItemType.ToString(), requestedCount = count, - droppedCount = count - remaining, + droppedCount = count - remaining + droppedFromSlot, remainingCount = remaining, + currentCount, inventoryId, touchedSlots = touchedSlots.ToArray() }); } - remaining -= dropFromSlot; + remaining -= droppedFromSlot; } - int afterCount = inventory.Items - .Where(pair => pair.Value.Type == parsedItemType) - .Sum(pair => pair.Value.Count); + Container? finalInventory = client.GetInventory(inventoryId); + int afterCount = finalInventory is null + ? 0 + : finalInventory.Items + .Where(pair => IsDroppableInventorySlot(finalInventory, pair.Key)) + .Where(pair => pair.Value.Type == parsedItemType) + .Sum(pair => pair.Value.Count); int droppedCount = beforeCount - afterCount; if (remaining > 0) @@ -3118,6 +3128,56 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities return true; } + private static bool IsSnapshotInventorySlot(Container inventory, int slotId) + { + return slotId >= 0 && slotId < inventory.Type.SlotCount(); + } + + private static bool IsDroppableInventorySlot(Container inventory, int slotId) + { + if (!IsSnapshotInventorySlot(inventory, slotId)) + return false; + + return !(slotId == 0 && (inventory.Type == ContainerType.PlayerInventory || inventory.Type == ContainerType.Crafting)); + } + + private static bool IsHotbarSlot(Container inventory, int slotId) + { + return inventory.IsHotbar(slotId, out _); + } + + private static object? TryBuildCursorSnapshot(Container inventory) + { + return inventory.Items.TryGetValue(-1, out Item? cursorItem) && cursorItem.Count > 0 + ? new + { + type = cursorItem.Type.ToString(), + count = cursorItem.Count + } + : null; + } + + private static int GetCursorItemCount(Container inventory, ItemType itemType) + { + return inventory.Items.TryGetValue(-1, out Item? cursorItem) && cursorItem.Type == itemType + ? cursorItem.Count + : 0; + } + + private static bool TryDropInventorySlotItems(McClient client, int inventoryId, Container inventory, int slotId, ItemType itemType, int dropCount, out int droppedCount) + { + droppedCount = 0; + if (!inventory.Items.TryGetValue(slotId, out Item? currentItem) || currentItem.Type != itemType || currentItem.Count <= 0) + return false; + + if (inventoryId == 0 && inventory.IsHotbar(slotId, out int hotbarSlot)) + { + return TryDropHotbarSlotItems(client, slotId, hotbarSlot, itemType, dropCount, currentItem.Count, out droppedCount); + } + + return TryDropWindowSlotItems(client, inventoryId, slotId, itemType, dropCount, currentItem.Count, out droppedCount); + } + private static int CountItemInRange(Container inventory, ItemType itemType, int startSlot, int endSlot) { return inventory.Items @@ -3271,6 +3331,99 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities return inventory.Items.TryGetValue(slot, out Item? item) && item.Type == itemType ? item.Count : 0; } + private static bool TryDropHotbarSlotItems(McClient client, int slotId, int hotbarSlot, ItemType itemType, int dropCount, int availableInSlot, out int droppedCount) + { + droppedCount = 0; + byte previousSlot = client.GetCurrentSlot(); + bool restoreSlot = previousSlot != hotbarSlot; + + if (restoreSlot && !client.ChangeSlot((short)hotbarSlot)) + return false; + + try + { + if (dropCount >= availableInSlot) + { + if (!client.DropSelectedItem(dropEntireStack: true)) + return false; + + if (!WaitForSlotItemCount(client, 0, slotId, itemType, count => count == 0, DefaultInventoryActionWaitMs, out _, out _)) + return false; + + droppedCount = availableInSlot; + return true; + } + + int remaining = dropCount; + while (remaining > 0) + { + Container? currentInventory = client.GetInventory(0); + if (currentInventory is null) + return false; + + int beforeSlotCount = GetSlotItemCount(currentInventory, slotId, itemType); + if (beforeSlotCount <= 0) + break; + + if (!client.DropSelectedItem(dropEntireStack: false)) + return false; + + if (!WaitForSlotItemCount(client, 0, slotId, itemType, count => count <= beforeSlotCount - 1, DefaultInventoryActionWaitMs, out _, out _)) + return false; + + remaining--; + droppedCount++; + } + + return remaining == 0; + } + finally + { + if (restoreSlot) + client.ChangeSlot((short)previousSlot); + } + } + + private static bool TryDropWindowSlotItems(McClient client, int inventoryId, int slotId, ItemType itemType, int dropCount, int availableInSlot, out int droppedCount) + { + droppedCount = 0; + + if (dropCount >= availableInSlot) + { + if (!client.DoWindowAction(inventoryId, slotId, WindowActionType.DropItemStack)) + return false; + + if (!WaitForSlotItemCount(client, inventoryId, slotId, itemType, count => count == 0, DefaultInventoryActionWaitMs, out _, out _)) + return false; + + droppedCount = availableInSlot; + return true; + } + + int remaining = dropCount; + while (remaining > 0) + { + Container? currentInventory = client.GetInventory(inventoryId); + if (currentInventory is null) + return false; + + int beforeSlotCount = GetSlotItemCount(currentInventory, slotId, itemType); + if (beforeSlotCount <= 0) + break; + + if (!client.DoWindowAction(inventoryId, slotId, WindowActionType.DropItem)) + return false; + + if (!WaitForSlotItemCount(client, inventoryId, slotId, itemType, count => count <= beforeSlotCount - 1, DefaultInventoryActionWaitMs, out _, out _)) + return false; + + remaining--; + droppedCount++; + } + + return remaining == 0; + } + private static bool WaitForCursorItem(McClient client, ItemType itemType, int waitMs, out Item? cursorItem) { cursorItem = null; @@ -3336,6 +3489,28 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities } } + 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;