From cc26b4d3b37dd9990ac36bcc765279ac21a53781 Mon Sep 17 00:00:00 2001 From: Ethan Bork Date: Tue, 16 Jun 2026 12:08:12 -0700 Subject: [PATCH 1/4] gradle changes for 26.2 --- gradle.properties | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index c314d6c..edeadfa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,16 +5,16 @@ org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/develop -minecraft_version=26.1.2 -loader_version=0.18.6 -loom_version=1.16-SNAPSHOT +minecraft_version=26.2 +loader_version=0.19.3 +loom_version=1.17-SNAPSHOT # Mod Properties -mod_version=1.1.5+26.1 +mod_version=1.1.6+26.2 maven_group=borknbeans.lightweightinventorysorting archives_base_name=lightweight-inventory-sorting # Dependencies -fabric_version=0.145.4+26.1.1 -modmenu_version=18.0.0-alpha.8 +fabric_version=0.152.1+26.2 +modmenu_version=20.0.0-beta.2 cloth_version=26.1.154 From 81febce832ddcaa417977066ccd2477190e2b7ab Mon Sep 17 00:00:00 2001 From: Ethan Bork Date: Tue, 16 Jun 2026 12:11:10 -0700 Subject: [PATCH 2/4] rename files, add basic horse inventory menu handling. TODO: implement kuroneko safe spot checking --- ...enMixin.java => ContainerScreenMixin.java} | 4 +- .../client/HorseInventoryScreenMixin.java | 61 +++++++++++++++++++ ...eight-inventory-sorting.client.mixins.json | 5 +- 3 files changed, 66 insertions(+), 4 deletions(-) rename src/client/java/borknbeans/lightweightinventorysorting/mixin/client/{GenericContainerScreenMixin.java => ContainerScreenMixin.java} (92%) create mode 100644 src/client/java/borknbeans/lightweightinventorysorting/mixin/client/HorseInventoryScreenMixin.java diff --git a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/GenericContainerScreenMixin.java b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/ContainerScreenMixin.java similarity index 92% rename from src/client/java/borknbeans/lightweightinventorysorting/mixin/client/GenericContainerScreenMixin.java rename to src/client/java/borknbeans/lightweightinventorysorting/mixin/client/ContainerScreenMixin.java index 27ef974..415f9e7 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/GenericContainerScreenMixin.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/ContainerScreenMixin.java @@ -16,12 +16,12 @@ import org.spongepowered.asm.mixin.Unique; @Mixin(ContainerScreen.class) -public abstract class GenericContainerScreenMixin extends AbstractContainerScreen { +public abstract class ContainerScreenMixin extends AbstractContainerScreen { @Unique private SortButton sortButton; - public GenericContainerScreenMixin(ChestMenu menu, Inventory inventory, Component title) { + public ContainerScreenMixin(ChestMenu menu, Inventory inventory, Component title) { super(menu, inventory, title); } diff --git a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/HorseInventoryScreenMixin.java b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/HorseInventoryScreenMixin.java new file mode 100644 index 0000000..3dccee6 --- /dev/null +++ b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/HorseInventoryScreenMixin.java @@ -0,0 +1,61 @@ +package borknbeans.lightweightinventorysorting.mixin.client; + +import borknbeans.lightweightinventorysorting.LightweightInventorySortingClient; +import borknbeans.lightweightinventorysorting.config.Config; +import borknbeans.lightweightinventorysorting.sorting.SortButton; +import borknbeans.lightweightinventorysorting.sorting.Sorter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.HorseInventoryScreen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.HorseInventoryMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(HorseInventoryScreen.class) +public abstract class HorseInventoryScreenMixin extends AbstractContainerScreen { + + @Unique + private SortButton sortButton; + + public HorseInventoryScreenMixin(HorseInventoryMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + } + + @Override + protected void init() { + super.init(); + + int x = this.leftPos + this.imageWidth - 20 + Config.xOffsetContainer; + int y = this.topPos + 4 + Config.yOffsetContainer; + int size = Config.buttonSize.getButtonSize(); + sortButton = new SortButton(x, y, size, size, Component.literal("S"), 0, getMenu().slots.size() - 37); + this.addRenderableWidget(sortButton); + } + + + @Override + public boolean keyPressed(KeyEvent event) { + if (LightweightInventorySortingClient.sortKeyBind.matches(event)) { + if (sortButton != null) { + Sorter.sortContainerClientside(Minecraft.getInstance(), sortButton.getSortStartIndex(), sortButton.getSortEndIndex()); + } + return true; + } + return super.keyPressed(event); + } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { + if (LightweightInventorySortingClient.sortKeyBind.matchesMouse(event)) { + if (sortButton != null) { + Sorter.sortContainerClientside(Minecraft.getInstance(), sortButton.getSortStartIndex(), sortButton.getSortEndIndex()); + } + return true; + } + return super.mouseClicked(event, doubleClick); + } +} diff --git a/src/client/resources/lightweight-inventory-sorting.client.mixins.json b/src/client/resources/lightweight-inventory-sorting.client.mixins.json index ea5459d..17af704 100644 --- a/src/client/resources/lightweight-inventory-sorting.client.mixins.json +++ b/src/client/resources/lightweight-inventory-sorting.client.mixins.json @@ -4,8 +4,9 @@ "compatibilityLevel": "JAVA_21", "client": [ "InventoryScreenMixin", - "GenericContainerScreenMixin", - "ShulkerBoxScreenMixin" + "ContainerScreenMixin", + "ShulkerBoxScreenMixin", + "HorseInventoryScreenMixin" ], "injectors": { "defaultRequire": 1 From f71bd7a1dae75d67e73cbc22cd3e4f7be117bfb5 Mon Sep 17 00:00:00 2001 From: Ethan Bork Date: Tue, 16 Jun 2026 12:20:51 -0700 Subject: [PATCH 3/4] adapt kuroneko007's logic for safeslots and horse inventory Co-Authored-By: kuroneko007 --- .../sorting/ClickOperation.java | 39 ++ .../sorting/Sorter.java | 459 +++++++++++++++--- 2 files changed, 418 insertions(+), 80 deletions(-) diff --git a/src/client/java/borknbeans/lightweightinventorysorting/sorting/ClickOperation.java b/src/client/java/borknbeans/lightweightinventorysorting/sorting/ClickOperation.java index 220ebff..3db2643 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/sorting/ClickOperation.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/sorting/ClickOperation.java @@ -2,7 +2,9 @@ import net.minecraft.client.Minecraft; import net.minecraft.world.inventory.ContainerInput; +import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import java.util.List; @@ -17,6 +19,15 @@ public class ClickOperation { private final List delays = List.of(0, 5, 15); // in milliseconds + // Custom exception to signal that a click was intentionally skipped due to unsafe conditions + public static class SkippedUnsafeClickException extends Exception { + public final int slot; + public SkippedUnsafeClickException(int slot) { + super("Skipped unsafe click on slot " + slot); + this.slot = slot; + } + } + public ClickOperation(Minecraft client, int syncId, int targetSlot, ItemStack expectedStartingTargetStack, ItemStack expectedEndingTargetStack, ItemStack expectedStartingMouseStack, ItemStack expectedEndingMouseStack) { this.client = client; this.syncId = syncId; @@ -42,6 +53,11 @@ public void execute() throws Exception { throw new Exception("[Target: " + targetSlot + "] Starting target stack is not what we expected: (ACTUAL)" + getItemStackString(startingTargetStack) + " != (EXPECTED)" + getItemStackString(expectedStartingTargetStack)); } + // Safety guard: Check if this click is safe + if (!canSafelyClick(client, startingMouseStack)) { + throw new SkippedUnsafeClickException(targetSlot); + } + click(); Exception error = null; @@ -88,4 +104,27 @@ private void postClickVerification() throws Exception { private String getItemStackString(ItemStack stack) { return String.format("%dx %s", stack.getCount(), stack.getItem().getName(stack).getString()); } + + // Safety check: Verify slot is valid and can accept the item in hand + private boolean canSafelyClick(Minecraft client, ItemStack mouseStack) { + if (client.player == null) return false; + + var handler = client.player.containerMenu; + if (targetSlot < 0 || targetSlot >= handler.slots.size()) return false; + + var slot = handler.getSlot(targetSlot); + if (slot == null || !slot.isActive()) return false; + + // Equipment / non-insertable slots will often reject generic items + // Check both directions: whether we can take items FROM this slot OR insert INTO it + if (!slot.mayPickup(client.player) && !slot.hasItem()) return false; + + // If holding something, make sure this slot can accept it + if (!mouseStack.isEmpty() && !slot.mayPlace(mouseStack)) return false; + + // If this slot has special restrictions (e.g. saddle/armor), skip it + if (slot.getMaxStackSize() <= 0) return false; + + return true; + } } diff --git a/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java b/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java index 4a66788..1f7bcd8 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java @@ -10,6 +10,7 @@ import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.BundleItem; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import java.util.ArrayList; import java.util.List; @@ -18,6 +19,58 @@ public class Sorter { private static volatile boolean isSorting = false; + // Returns a list of actual handler slot indices that are considered "sortable" / safe. + // This skips saddle, armor, disabled, and other restricted slots. + private static List getSafeSlots(Minecraft client, int sortStartIndex, int sortEndIndex) { + var handler = client.player.containerMenu; + var slots = handler.slots; + var safe = new ArrayList(); + + for (int slotIndex = sortStartIndex; slotIndex <= sortEndIndex && slotIndex < slots.size(); slotIndex++) { + Slot slot = slots.get(slotIndex); + if (slot == null) continue; + if (!slot.isActive()) continue; + + // Skip slots that clearly aren't for normal inventory use + // - Max stack size of 1 (typical for armor/saddle) + // - Rejects a simple neutral item like dirt (means it's restricted) + ItemStack probe = new ItemStack(Items.DIRT); + boolean restricted = (slot.getMaxStackSize() == 1 && !slot.mayPlace(probe)); + + if (restricted) continue; // saddle, armor, etc. + + // Skip disabled or read-only slots + if (!slot.mayPickup(client.player) && !slot.hasItem()) continue; + + // If it's empty AND can't accept normal items, skip it + if (!slot.hasItem() && !slot.mayPlace(probe)) continue; + + // Otherwise it's safe to use + safe.add(slotIndex); + } + return safe; + } + + // Parallel snapshot of stacks in those safe slots, in the same order. + private static List getSnapshotForSlots(Minecraft client, List safeSlots) { + var handler = client.player.containerMenu; + var out = new ArrayList(safeSlots.size()); + for (int idx : safeSlots) { + out.add(handler.getSlot(idx).getItem().copy()); + } + return out; + } + + // Find the FIRST empty safe slot index (returns value is INDEX INTO safeSlots, not handler index) + private static int getFirstEmptySafeSlotIndex(List snapshotForSafeSlots) { + for (int i = 0; i < snapshotForSafeSlots.size(); i++) { + if (snapshotForSafeSlots.get(i).isEmpty()) { + return i; + } + } + return -1; + } + public static void sortContainerClientside(Minecraft client, int sortStartIndex, int sortEndIndex) { if (FabricLoader.getInstance().getEnvironmentType() != EnvType.CLIENT) { return; @@ -50,182 +103,435 @@ public static void sortContainerClientside(Minecraft client, int sortStartIndex, }).start(); } + // Clear any existing item that is on the mouse cursor private static void clearMouseStack(Minecraft client, int syncId, int sortStartIndex, int sortEndIndex) throws Exception { - List snapshot = getInventorySnapshot(client, sortStartIndex, sortEndIndex); + var mouseStack = getMouseStack(client).copy(); + if (mouseStack.isEmpty()) return; + + var handler = client.player.containerMenu; + var slots = handler.slots; + + // Build the list of valid destination slots + var safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + + // Try to find a safe empty slot that can accept what's on the mouse + int chosenSafeListIndex = -1; + for (int k = 0; k < safeSlots.size(); k++) { + int slotIdx = safeSlots.get(k); + var slot = slots.get(slotIdx); + if (!slot.isActive()) continue; + if (!slot.getItem().isEmpty()) continue; + if (!slot.mayPlace(mouseStack)) continue; + if (slot.getMaxStackSize() <= 0) continue; + + chosenSafeListIndex = k; + break; + } - // Clear any existing item that is on the mouse - ItemStack mouseStack = getMouseStack(client).copy(); - if (!mouseStack.isEmpty()) { - int emptyIndex = getEmptySlotIndex(snapshot); - if (emptyIndex == -1) { - throw new Exception("[Sort] No empty slot found to clear mouse stack"); - } + if (chosenSafeListIndex == -1) { + throw new Exception("[Sort] No safe empty slot found to clear mouse stack"); + } - ClickOperation emptySlotOperation = new ClickOperation(client, syncId, sortStartIndex + emptyIndex, ItemStack.EMPTY, mouseStack, mouseStack, ItemStack.EMPTY); - emptySlotOperation.execute(); + int handlerSlotIndex = safeSlots.get(chosenSafeListIndex); + + var op = new ClickOperation( + client, + syncId, + handlerSlotIndex, + ItemStack.EMPTY, + mouseStack, + mouseStack, + ItemStack.EMPTY + ); + + try { + op.execute(); + } catch (ClickOperation.SkippedUnsafeClickException e) { + // Extremely defensive fallback. At this point we're already in "we tried". + LightweightInventorySorting.LOGGER.warn("[clearMouseStack] Gave up clearing mouse; skipped slot {}", e.slot); } } + // Combine like stacks together (works with safe slots only) private static void combineLikeStacks(Minecraft client, int syncId, int sortStartIndex, int sortEndIndex) throws Exception { - List snapshot = getInventorySnapshot(client, sortStartIndex, sortEndIndex); - ItemStack mouseStack = getMouseStack(client); - + var mouseStack = getMouseStack(client); if (!mouseStack.isEmpty()) { throw new Exception("[CombineLikeStacks] Mouse stack is not empty"); } + // We'll loop until we walk all safe slots + var safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + var snapshot = getSnapshotForSlots(client, safeSlots); + for (int i = 0; i < snapshot.size(); i++) { - ItemStack stackOriginal = snapshot.get(i).copy(); + var stackOriginal = snapshot.get(i).copy(); mouseStack = getMouseStack(client); + if (stackOriginal.isEmpty() || stackOriginal.getCount() == stackOriginal.getMaxStackSize()) { continue; } for (int j = i + 1; j < snapshot.size(); j++) { - ItemStack otherStack = snapshot.get(j).copy(); + var otherStack = snapshot.get(j).copy(); if (otherStack.isEmpty() || otherStack.getCount() == otherStack.getMaxStackSize()) { continue; } - ItemStack stack = mouseStack.isEmpty() ? stackOriginal : mouseStack; + var stack = mouseStack.isEmpty() ? stackOriginal : mouseStack; if (ItemStack.isSameItemSameComponents(stack, otherStack)) { int maxStackSize = stack.getMaxStackSize(); int combinedSize = stack.getCount() + otherStack.getCount(); - ClickOperation pickupFirstStack = new ClickOperation(client, syncId, i + sortStartIndex, stack, ItemStack.EMPTY, mouseStack, stack); + int handlerSlotI = safeSlots.get(i); + int handlerSlotJ = safeSlots.get(j); + + var pickupFirstStack = new ClickOperation( + client, + syncId, + handlerSlotI, + stack, + ItemStack.EMPTY, + mouseStack, + stack + ); + + var expectedEndingMouseStack = + combinedSize > maxStackSize + ? stack.copyWithCount(combinedSize - maxStackSize) + : ItemStack.EMPTY; + + var combineStacks = new ClickOperation( + client, + syncId, + handlerSlotJ, + otherStack, + stack.copyWithCount(Math.min(combinedSize, maxStackSize)), + stack, + expectedEndingMouseStack + ); - ItemStack expectedEndingMouseStack = combinedSize > maxStackSize ? stack.copyWithCount(combinedSize - maxStackSize) : ItemStack.EMPTY; - ClickOperation combineStacks = new ClickOperation(client, syncId, j + sortStartIndex, otherStack, stack.copyWithCount(Math.min(combinedSize, maxStackSize)), stack, expectedEndingMouseStack); try { - if (mouseStack.isEmpty()) { // Don't pick up first stack if we have a stack in our hand from the previous iteration + if (mouseStack.isEmpty()) { pickupFirstStack.execute(); } Thread.sleep(Config.sortDelay); combineStacks.execute(); + } catch (ClickOperation.SkippedUnsafeClickException e) { + LightweightInventorySorting.LOGGER.warn("[CombineLikeStacks] Skipped unsafe slot {} — clearing mouse + resync", e.slot); + + try { + clearMouseStack(client, syncId, sortStartIndex, sortEndIndex); + } catch (Exception clearErr) { + LightweightInventorySorting.LOGGER.warn("[CombineLikeStacks] After skip, couldn't clear mouse: {}", clearErr.getMessage()); + } + + // refresh snapshot and continue main loop + safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + snapshot = getSnapshotForSlots(client, safeSlots); + continue; } catch (Exception e) { throw new Exception("Failed to combine like items: " + e.getMessage()); } mouseStack = expectedEndingMouseStack; - if (mouseStack.isEmpty()) { // If our hand is empty, move to the next stack - break; + if (mouseStack.isEmpty()) { + break; // done merging this base stack } } } + // try to put leftover mouse stack back into slot i + mouseStack = getMouseStack(client); if (!mouseStack.isEmpty()) { - ClickOperation putBackStack = new ClickOperation(client, syncId, i + sortStartIndex, ItemStack.EMPTY, mouseStack, mouseStack, ItemStack.EMPTY); + int handlerSlotI = safeSlots.get(i); + + var putBackStack = new ClickOperation( + client, + syncId, + handlerSlotI, + ItemStack.EMPTY, + mouseStack, + mouseStack, + ItemStack.EMPTY + ); try { putBackStack.execute(); + } catch (ClickOperation.SkippedUnsafeClickException e) { + LightweightInventorySorting.LOGGER.warn("[CombineLikeStacks] Put-back skipped on slot {} — clearing mouse + resync", e.slot); + + try { + clearMouseStack(client, syncId, sortStartIndex, sortEndIndex); + } catch (Exception clearErr) { + LightweightInventorySorting.LOGGER.warn("[CombineLikeStacks] After put-back skip, couldn't clear mouse: {}", clearErr.getMessage()); + } } catch (Exception e) { throw new Exception("Failed to put back item: " + e.getMessage()); } } - snapshot = getInventorySnapshot(client, sortStartIndex, sortEndIndex); // Update the inventory to reflect our changes + // refresh snapshot after each outer iteration + safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + snapshot = getSnapshotForSlots(client, safeSlots); } } + // Main sorting pass (works with safe slots only) private static void sort(Minecraft client, int syncId, int sortStartIndex, int sortEndIndex) throws Exception { - List snapshot = getInventorySnapshot(client, sortStartIndex, sortEndIndex); - - ArrayList sortedStacks = new ArrayList(); - for (int i = 0; i < snapshot.size(); i++) { - ItemStack stack = snapshot.get(i).copy(); - if (stack.isEmpty()) { - continue; + // Build current view + var safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + var snapshot = getSnapshotForSlots(client, safeSlots); + + // Build list of all non-empty stacks from those safe slots + var sortedStacks = new ArrayList(); + for (ItemStack st : snapshot) { + if (!st.isEmpty()) { + sortedStacks.add(st.copy()); } - - sortedStacks.add(stack); } + // Sort them using the comparator sortedStacks.sort(new SortComparator()); - ItemStack mouseStack = getMouseStack(client); - + var mouseStack = getMouseStack(client); if (!mouseStack.isEmpty()) { throw new Exception("[Sort] Mouse stack is not empty"); } + // For each desired sorted position i... for (int i = 0; i < sortedStacks.size(); i++) { - ItemStack sortedStack = sortedStacks.get(i); + var sortedStack = sortedStacks.get(i); - int stackCurrIndex = -1; + // Find where that exact stack currently lives (match item+components+count) + int foundJ = -1; for (int j = i; j < snapshot.size(); j++) { - if (ItemStack.isSameItemSameComponents(sortedStack, snapshot.get(j)) && sortedStack.getCount() == snapshot.get(j).getCount()) { - stackCurrIndex = j + sortStartIndex; + ItemStack snapStack = snapshot.get(j); + if (ItemStack.isSameItemSameComponents(sortedStack, snapStack) + && sortedStack.getCount() == snapStack.getCount()) { + foundJ = j; break; } } - if (stackCurrIndex == -1) { + if (foundJ == -1) { throw new Exception("[Sort] Stack not found in inventory, looking for: " + sortedStack.toString()); } - if (stackCurrIndex == i + sortStartIndex) { + // If it's already where we want it, skip + if (foundJ == i) { continue; } - ClickOperation pickupOperation = new ClickOperation(client, syncId, stackCurrIndex, sortedStack, ItemStack.EMPTY, ItemStack.EMPTY, sortedStack); + // Handler (real) slot indices for where it is and where it should go + int fromHandlerSlot = safeSlots.get(foundJ); + int toHandlerSlot = safeSlots.get(i); - ItemStack existingStack = snapshot.get(i).copy(); + var existingStack = snapshot.get(i).copy(); // what's currently sitting in target - // If the item that is in our desired slot is a bundle, we need to handle it differently + // Handle bundle blocking target slot if (existingStack.getItem() instanceof BundleItem) { - ClickOperation pickupBundleOperation = new ClickOperation(client, syncId, i + sortStartIndex, existingStack, ItemStack.EMPTY, ItemStack.EMPTY, existingStack); - ClickOperation placeBundleElsewhereOperation = new ClickOperation(client, syncId, getEmptySlotIndex(snapshot) + sortStartIndex, ItemStack.EMPTY, existingStack, existingStack, ItemStack.EMPTY); + // move that bundle out first + int emptyIndexInSafe = getFirstEmptySafeSlotIndex(snapshot); + if (emptyIndexInSafe == -1) { + throw new Exception("[Sort] No empty slot found to park bundle"); + } - pickupBundleOperation.execute(); - placeBundleElsewhereOperation.execute(); + int targetHandlerSlot = toHandlerSlot; + int emptyHandlerSlot = safeSlots.get(emptyIndexInSafe); + + var pickupBundleOperation = new ClickOperation( + client, + syncId, + targetHandlerSlot, + existingStack, + ItemStack.EMPTY, + ItemStack.EMPTY, + existingStack + ); + + var placeBundleElsewhereOperation = new ClickOperation( + client, + syncId, + emptyHandlerSlot, + ItemStack.EMPTY, + existingStack, + existingStack, + ItemStack.EMPTY + ); - existingStack = ItemStack.EMPTY; - } - - ClickOperation placeOperation = new ClickOperation(client, syncId, i + sortStartIndex, existingStack, sortedStack, sortedStack, existingStack); + try { + pickupBundleOperation.execute(); + placeBundleElsewhereOperation.execute(); + } catch (ClickOperation.SkippedUnsafeClickException e) { + LightweightInventorySorting.LOGGER.warn("[Sort] Bundle relocation skipped on slot {} — clearing mouse + resync", e.slot); + try { + clearMouseStack(client, syncId, sortStartIndex, sortEndIndex); + } catch (Exception clearErr) { + LightweightInventorySorting.LOGGER.warn("[Sort] After bundle skip, couldn't clear mouse: {}", clearErr.getMessage()); + } - ClickOperation emptyHandOperation = new ClickOperation(client, syncId, stackCurrIndex, ItemStack.EMPTY, existingStack, existingStack, ItemStack.EMPTY); + // resync snapshot/safeSlots then continue outer loop + safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + snapshot = getSnapshotForSlots(client, safeSlots); + continue; + } - // If the item we are sorting is a bundle, we need to handle it differently - if (sortedStack.getItem() instanceof BundleItem) { - if (!existingStack.isEmpty()) { - ClickOperation pickupTargetSlotOperation = new ClickOperation(client, syncId, i + sortStartIndex, existingStack, ItemStack.EMPTY, ItemStack.EMPTY, existingStack); - int emptySlotIndex = getEmptySlotIndex(snapshot); + existingStack = ItemStack.EMPTY; + } - if (emptySlotIndex == -1) { - throw new Exception("[Sort] No empty slot found"); - } + // Now build the normal operations + var pickupOperation = new ClickOperation( + client, + syncId, + fromHandlerSlot, + sortedStack, + ItemStack.EMPTY, + ItemStack.EMPTY, + sortedStack + ); + + var placeOperation = new ClickOperation( + client, + syncId, + toHandlerSlot, + existingStack, + sortedStack, + sortedStack, + existingStack + ); + + var emptyHandOperation = new ClickOperation( + client, + syncId, + fromHandlerSlot, + ItemStack.EMPTY, + existingStack, + existingStack, + ItemStack.EMPTY + ); + + // Special handling if the item we are moving is a bundle and target not empty + if (sortedStack.getItem() instanceof BundleItem && !existingStack.isEmpty()) { + int emptyIndexInSafe = getFirstEmptySafeSlotIndex(snapshot); + if (emptyIndexInSafe == -1) { + throw new Exception("[Sort] No empty slot found to park blocking stack before bundle"); + } - ClickOperation placeInEmptySlotOperation = new ClickOperation(client, syncId, sortStartIndex + emptySlotIndex, ItemStack.EMPTY, existingStack, existingStack, ItemStack.EMPTY); + int targetHandlerSlot = toHandlerSlot; + int emptyHandlerSlot = safeSlots.get(emptyIndexInSafe); + + var pickupTargetSlotOperation = new ClickOperation( + client, + syncId, + targetHandlerSlot, + existingStack, + ItemStack.EMPTY, + ItemStack.EMPTY, + existingStack + ); + + var placeInEmptySlotOperation = new ClickOperation( + client, + syncId, + emptyHandlerSlot, + ItemStack.EMPTY, + existingStack, + existingStack, + ItemStack.EMPTY + ); + try { pickupTargetSlotOperation.execute(); placeInEmptySlotOperation.execute(); Thread.sleep(Config.sortDelay); + } catch (ClickOperation.SkippedUnsafeClickException e) { + LightweightInventorySorting.LOGGER.warn("[Sort] Bundle target clearing skipped on slot {} — clearing mouse + resync", e.slot); + try { + clearMouseStack(client, syncId, sortStartIndex, sortEndIndex); + } catch (Exception clearErr) { + LightweightInventorySorting.LOGGER.warn("[Sort] After bundle target skip, couldn't clear mouse: {}", clearErr.getMessage()); + } + + safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + snapshot = getSnapshotForSlots(client, safeSlots); + continue; + } - existingStack = ItemStack.EMPTY; + // After moving that blocking stack out, `existingStack` at target is now empty + existingStack = ItemStack.EMPTY; + // Recompute placeOperation with now-empty target + placeOperation = new ClickOperation( + client, + syncId, + toHandlerSlot, + existingStack, + sortedStack, + sortedStack, + existingStack + ); + } - // Update place operation to expect the new empty stack - placeOperation = new ClickOperation(client, syncId, i + sortStartIndex, existingStack, sortedStack, sortedStack, existingStack); + // Execute pickup/place/return-hand with safety+resync + try { + pickupOperation.execute(); + } catch (ClickOperation.SkippedUnsafeClickException e) { + LightweightInventorySorting.LOGGER.warn("[Sort] Skipped unsafe slot {} on pickup — clearing mouse + resync", e.slot); + try { + clearMouseStack(client, syncId, sortStartIndex, sortEndIndex); + } catch (Exception clearErr) { + LightweightInventorySorting.LOGGER.warn("[Sort] After pickup skip, couldn't clear mouse: {}", clearErr.getMessage()); } + safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + snapshot = getSnapshotForSlots(client, safeSlots); + continue; + } + + try { + placeOperation.execute(); + } catch (ClickOperation.SkippedUnsafeClickException e) { + LightweightInventorySorting.LOGGER.warn("[Sort] Skipped unsafe slot {} on place — clearing mouse + resync", e.slot); + try { + clearMouseStack(client, syncId, sortStartIndex, sortEndIndex); + } catch (Exception clearErr) { + LightweightInventorySorting.LOGGER.warn("[Sort] After place skip, couldn't clear mouse: {}", clearErr.getMessage()); + } + safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + snapshot = getSnapshotForSlots(client, safeSlots); + continue; } - pickupOperation.execute(); - placeOperation.execute(); Thread.sleep(Config.sortDelay); + if (!existingStack.isEmpty()) { - emptyHandOperation.execute(); + try { + emptyHandOperation.execute(); + } catch (ClickOperation.SkippedUnsafeClickException e) { + LightweightInventorySorting.LOGGER.warn("[Sort] Skipped unsafe slot {} on empty hand — clearing mouse + resync", e.slot); + try { + clearMouseStack(client, syncId, sortStartIndex, sortEndIndex); + } catch (Exception clearErr) { + LightweightInventorySorting.LOGGER.warn("[Sort] After empty-hand skip, couldn't clear mouse: {}", clearErr.getMessage()); + } + } } - snapshot = getInventorySnapshot(client, sortStartIndex, sortEndIndex); + // resync after each outer loop iteration + safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + snapshot = getSnapshotForSlots(client, safeSlots); } + // final validation: do we match the sorted order at the front? + safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + snapshot = getSnapshotForSlots(client, safeSlots); + for (int i = 0; i < sortedStacks.size(); i++) { - ItemStack expectedStack = sortedStacks.get(i).copy(); - ItemStack actualStack = snapshot.get(i).copy(); + var expectedStack = sortedStacks.get(i).copy(); + var actualStack = snapshot.get(i).copy(); - if (!ItemStack.isSameItemSameComponents(expectedStack, actualStack)) { + if (!ItemStack.isSameItemSameComponents(expectedStack, actualStack) + || expectedStack.getCount() != actualStack.getCount()) { throw new Exception("[Sort] Stack not in correct position"); } } @@ -255,14 +561,7 @@ private static List getInventorySnapshot(Minecraft client, int sortSt return snapshot; } - private static int getEmptySlotIndex(List snapshot) { - for (int i = 0; i < snapshot.size(); i++) { - if (snapshot.get(i).isEmpty()) { - return i; - } - } - return -1; - } + public static ItemStack getInventoryStack(Minecraft client, int index) { if (client.player == null) { From a6833c8e916f62fe8b1dc284ea75c3965ae7ca52 Mon Sep 17 00:00:00 2001 From: Ethan Bork Date: Tue, 16 Jun 2026 12:55:23 -0700 Subject: [PATCH 4/4] add creative inventory sorting --- gradle.properties | 2 +- .../CreativeModeInventoryScreenMixin.java | 106 ++++++++++++++++++ .../sorting/Sorter.java | 10 ++ ...eight-inventory-sorting.client.mixins.json | 3 +- 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 src/client/java/borknbeans/lightweightinventorysorting/mixin/client/CreativeModeInventoryScreenMixin.java diff --git a/gradle.properties b/gradle.properties index edeadfa..1238481 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ loader_version=0.19.3 loom_version=1.17-SNAPSHOT # Mod Properties -mod_version=1.1.6+26.2 +mod_version=1.1.6+26.2-beta maven_group=borknbeans.lightweightinventorysorting archives_base_name=lightweight-inventory-sorting diff --git a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/CreativeModeInventoryScreenMixin.java b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/CreativeModeInventoryScreenMixin.java new file mode 100644 index 0000000..bad786a --- /dev/null +++ b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/CreativeModeInventoryScreenMixin.java @@ -0,0 +1,106 @@ +package borknbeans.lightweightinventorysorting.mixin.client; + +import borknbeans.lightweightinventorysorting.LightweightInventorySortingClient; +import borknbeans.lightweightinventorysorting.config.Config; +import borknbeans.lightweightinventorysorting.sorting.SortButton; +import borknbeans.lightweightinventorysorting.sorting.Sorter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen; +import net.minecraft.client.input.KeyEvent; +import net.minecraft.client.input.MouseButtonEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.CreativeModeTab; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(CreativeModeInventoryScreen.class) +public abstract class CreativeModeInventoryScreenMixin extends AbstractContainerScreen { + + @Unique + private SortButton sortButton; + + public CreativeModeInventoryScreenMixin(AbstractContainerMenu menu, Inventory inventory, Component title) { + super(menu, inventory, title); + } + + @Inject(method = "init", at = @At("TAIL")) + private void addSortButton(CallbackInfo ci) { + // Only add sort button for the inventory tab + updateSortButton(); + } + + @Inject(method = "selectTab", at = @At("TAIL")) + private void onTabSelected(CallbackInfo ci) { + // Update button visibility when tab changes + updateSortButton(); + } + + @Unique + private void updateSortButton() { + if (isInventoryTab()) { + if (sortButton == null) { + int x = this.leftPos + this.imageWidth - 20 + Config.xOffsetContainer; + int y = this.topPos + 4 + Config.yOffsetContainer; + int size = Config.buttonSize.getButtonSize(); + // Creative grid is 3x9 (27 slots) + // Slots 9-35: Creative grid (3 rows × 9 columns) + // Avoids armor slots (5-8) and hotbar items (36-44) + sortButton = new SortButton(x, y, size, size, Component.literal("S"), 9, 35); + this.addRenderableWidget(sortButton); + } + // Show button + if (sortButton != null) { + sortButton.visible = true; + } + } else { + // Hide button when not on inventory tab + if (sortButton != null) { + sortButton.visible = false; + } + } + } + + @Override + public boolean keyPressed(KeyEvent event) { + // Only allow sorting when on inventory tab + if (isInventoryTab() && LightweightInventorySortingClient.sortKeyBind.matches(event)) { + if (sortButton != null) { + Sorter.sortContainerClientside(Minecraft.getInstance(), sortButton.getSortStartIndex(), sortButton.getSortEndIndex()); + } + return true; + } + return super.keyPressed(event); + } + + @Override + public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { + // Only allow sorting when on inventory tab + if (isInventoryTab() && LightweightInventorySortingClient.sortKeyBind.matchesMouse(event)) { + if (sortButton != null) { + Sorter.sortContainerClientside(Minecraft.getInstance(), sortButton.getSortStartIndex(), sortButton.getSortEndIndex()); + } + return true; + } + return super.mouseClicked(event, doubleClick); + } + + @Unique + private boolean isInventoryTab() { + try { + // Access the static selectedTab field via reflection + var field = CreativeModeInventoryScreen.class.getDeclaredField("selectedTab"); + field.setAccessible(true); + CreativeModeTab selectedTab = (CreativeModeTab) field.get(null); + return selectedTab != null && selectedTab.getType() == CreativeModeTab.Type.INVENTORY; + } catch (Exception e) { + // Default to false if we can't access the field + return false; + } + } +} diff --git a/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java b/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java index 1f7bcd8..d1d5888 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java @@ -84,6 +84,16 @@ public static void sortContainerClientside(Minecraft client, int sortStartIndex, AbstractContainerMenu container = client.player.containerMenu; int syncId = container.containerId; + // Debug: Print all slots + LightweightInventorySorting.LOGGER.info("=== ALL SLOTS BEFORE SORTING ==="); + for (int i = 0; i < container.slots.size(); i++) { + Slot slot = container.slots.get(i); + ItemStack item = slot.getItem(); + LightweightInventorySorting.LOGGER.info("Slot [" + i + "]: " + (item.isEmpty() ? "EMPTY" : item.getCount() + "x " + item.getItem().getName(item).getString())); + } + LightweightInventorySorting.LOGGER.info("Sort range: " + sortStartIndex + " to " + sortEndIndex); + LightweightInventorySorting.LOGGER.info("=== END SLOT LIST ==="); + List snapshot = getInventorySnapshot(client, sortStartIndex, sortEndIndex); SortSnapshotClientside snapshotEncoder = new SortSnapshotClientside(snapshot); LightweightInventorySorting.LOGGER.info("Encoded snapshot: " + snapshotEncoder.encode()); diff --git a/src/client/resources/lightweight-inventory-sorting.client.mixins.json b/src/client/resources/lightweight-inventory-sorting.client.mixins.json index 17af704..4e735d9 100644 --- a/src/client/resources/lightweight-inventory-sorting.client.mixins.json +++ b/src/client/resources/lightweight-inventory-sorting.client.mixins.json @@ -6,7 +6,8 @@ "InventoryScreenMixin", "ContainerScreenMixin", "ShulkerBoxScreenMixin", - "HorseInventoryScreenMixin" + "HorseInventoryScreenMixin", + "CreativeModeInventoryScreenMixin" ], "injectors": { "defaultRequire": 1