diff --git a/gradle.properties b/gradle.properties index 144357e..6a7a9f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,17 +5,17 @@ org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/develop -minecraft_version=1.21.8 -yarn_mappings=1.21.8+build.1 -loader_version=0.17.2 +minecraft_version=1.21.10 +yarn_mappings=1.21.10+build.2 +loader_version=0.17.3 loom_version=1.11-SNAPSHOT # Mod Properties -mod_version=1.1.4+1.21.8 +mod_version=1.1.5+1.21.10 maven_group=borknbeans.lightweightinventorysorting archives_base_name=lightweight-inventory-sorting # Dependencies -fabric_version=0.132.0+1.21.8 +fabric_version=0.136.0+1.21.10 modmenu_version=15.0.0-beta.3 cloth_version=19.0.147 \ No newline at end of file diff --git a/src/client/java/borknbeans/lightweightinventorysorting/LightweightInventorySortingClient.java b/src/client/java/borknbeans/lightweightinventorysorting/LightweightInventorySortingClient.java index 36155ed..572d897 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/LightweightInventorySortingClient.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/LightweightInventorySortingClient.java @@ -10,6 +10,7 @@ import net.minecraft.item.ItemGroup; import net.minecraft.item.ItemGroups; import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; import org.lwjgl.glfw.GLFW; import java.util.*; @@ -27,11 +28,12 @@ public void onInitializeClient() { } private void registerKeyBindings() { + KeyBinding.Category titleCategory = KeyBinding.Category.create(Identifier.of("category.lightweight-inventory-sorting.title")); sortKeyBind = KeyBindingHelper.registerKeyBinding(new KeyBinding( "key.lightweight-inventory-sorting.sort", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_R, - "category.lightweight-inventory-sorting.title" + titleCategory )); } diff --git a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/GenericContainerScreenMixin.java b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/GenericContainerScreenMixin.java index 2949e48..cb00d34 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/GenericContainerScreenMixin.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/GenericContainerScreenMixin.java @@ -4,9 +4,12 @@ import borknbeans.lightweightinventorysorting.LightweightInventorySortingClient; import borknbeans.lightweightinventorysorting.config.Config; import borknbeans.lightweightinventorysorting.sorting.SortButton; +import net.minecraft.client.gui.Click; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.input.KeyInput; +import net.minecraft.client.input.MouseInput; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.screen.GenericContainerScreenHandler; import net.minecraft.text.Text; @@ -47,20 +50,22 @@ private void onRender(DrawContext context, int mouseX, int mouseY, float delta, } @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (LightweightInventorySortingClient.sortKeyBind.matchesKey(keyCode, scanCode)) { - sortButton.onClick(0f, 0f); // Simulate a click + public boolean keyPressed(KeyInput keyInput) { + if (LightweightInventorySortingClient.sortKeyBind.matchesKey(keyInput)) { + sortButton.onPress(new MouseInput(0,0)); + return true; } - return super.keyPressed(keyCode, scanCode, modifiers); + return super.keyPressed(keyInput); } @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (LightweightInventorySortingClient.sortKeyBind.matchesMouse(button)) { - sortButton.onClick(0f, 0f); // Simulate a click + public boolean mouseClicked(Click click, boolean doubleClick) { + if (LightweightInventorySortingClient.sortKeyBind.matchesMouse(click)) { + sortButton.simulateClick(click); // Simulate a click + return true; } - return super.mouseClicked(mouseX, mouseY, button); + return super.mouseClicked(click, doubleClick); } } diff --git a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/HorseScreenMixin.java b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/HorseScreenMixin.java new file mode 100644 index 0000000..8ba86ec --- /dev/null +++ b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/HorseScreenMixin.java @@ -0,0 +1,70 @@ +package borknbeans.lightweightinventorysorting.mixin.client; + +import borknbeans.lightweightinventorysorting.LightweightInventorySortingClient; +import borknbeans.lightweightinventorysorting.config.Config; +import borknbeans.lightweightinventorysorting.sorting.SortButton; +import net.minecraft.client.gui.Click; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.gui.screen.ingame.HorseScreen; +import net.minecraft.client.input.KeyInput; +import net.minecraft.client.input.MouseInput; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.HorseScreenHandler; +import net.minecraft.text.Text; +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(HorseScreen.class) +public abstract class HorseScreenMixin extends HandledScreen { + @Unique + private SortButton sortButton; + + public HorseScreenMixin(HorseScreenHandler handler, PlayerInventory inventory, Text title) { + super(handler, inventory, title); + } + + @Override + public void init() { + super.init(); + + // Initialize button + int x = this.x + this.backgroundWidth - 20 + Config.xOffsetContainer; + int y = this.y + 4 + Config.yOffsetContainer; + int size = Config.buttonSize.getButtonSize(); + sortButton = new SortButton(x, y, size, size, Text.literal("S"), 0, getScreenHandler().slots.size() - 37); + + // Add button to the screen + this.addDrawableChild(sortButton); + } + + @Inject(method = "render", at = @At("RETURN")) + private void onRender(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + if (sortButton != null) { + sortButton.render(context, mouseX, mouseY, delta); + } + } + + @Override + public boolean keyPressed(KeyInput keyInput) { + if (LightweightInventorySortingClient.sortKeyBind.matchesKey(keyInput)) { + sortButton.onPress(new MouseInput(0,0)); + return true; + } + + return super.keyPressed(keyInput); + } + + @Override + public boolean mouseClicked(Click click, boolean doubleClick) { + if (LightweightInventorySortingClient.sortKeyBind.matchesMouse(click)) { + sortButton.simulateClick(click); // Simulate a click + return true; + } + + return super.mouseClicked(click, doubleClick); + } +} diff --git a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/InventoryScreenMixin.java b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/InventoryScreenMixin.java index b526b5e..61ffec7 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/InventoryScreenMixin.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/InventoryScreenMixin.java @@ -9,9 +9,13 @@ import borknbeans.lightweightinventorysorting.LightweightInventorySortingClient; import borknbeans.lightweightinventorysorting.config.Config; import borknbeans.lightweightinventorysorting.sorting.SortButton; + import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Click; import net.minecraft.client.gui.screen.ingame.HandledScreen; import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.input.KeyInput; +import net.minecraft.client.input.MouseInput; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.screen.PlayerScreenHandler; import net.minecraft.text.Text; @@ -43,24 +47,28 @@ private void onRender(DrawContext context, int mouseX, int mouseY, float delta, } } - // This override is NOT an ideal solution as it could lead to conflicts with other mods + // Updated: uses KeyInput instead of (int, int, int) @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (LightweightInventorySortingClient.sortKeyBind.matchesKey(keyCode, scanCode)) { - sortButton.onClick(0f, 0f); // Simulate a click + public boolean keyPressed(KeyInput keyInput) { + if (LightweightInventorySortingClient.sortKeyBind.matchesKey(keyInput)) { + sortButton.onPress(new MouseInput(0,0)); // Simulate a click + return true; } - return super.keyPressed(keyCode, scanCode, modifiers); + return super.keyPressed(keyInput); } + // Updated: uses Click + boolean doubleClick @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (LightweightInventorySortingClient.sortKeyBind.matchesMouse(button)) { - sortButton.onClick(0f, 0f); // Simulate a click + public boolean mouseClicked(Click click, boolean doubleClick) { + if (LightweightInventorySortingClient.sortKeyBind.matchesMouse(click)) { + sortButton.simulateClick(click); // Simulate a click + return true; } - return super.mouseClicked(mouseX, mouseY, button); + return super.mouseClicked(click, doubleClick); } + @Unique private void setButtonCoordinates() { sortButton.setX(this.x + this.backgroundWidth - 20 + Config.xOffsetInventory); sortButton.setY(this.height / 2 - 15 + Config.yOffsetInventory); diff --git a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/ShulkerBoxScreenMixin.java b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/ShulkerBoxScreenMixin.java index 2473ecf..05d051c 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/ShulkerBoxScreenMixin.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/mixin/client/ShulkerBoxScreenMixin.java @@ -3,18 +3,24 @@ import borknbeans.lightweightinventorysorting.LightweightInventorySortingClient; import borknbeans.lightweightinventorysorting.config.Config; import borknbeans.lightweightinventorysorting.sorting.SortButton; + import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Click; import net.minecraft.client.gui.screen.ingame.HandledScreen; import net.minecraft.client.gui.screen.ingame.ShulkerBoxScreen; +import net.minecraft.client.input.KeyInput; +import net.minecraft.client.input.MouseInput; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.screen.ShulkerBoxScreenHandler; import net.minecraft.text.Text; + 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(ShulkerBoxScreen.class) public abstract class ShulkerBoxScreenMixin extends HandledScreen { @Unique @@ -45,21 +51,24 @@ private void onRender(DrawContext context, int mouseX, int mouseY, float delta, } } + // Updated to new signature @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (LightweightInventorySortingClient.sortKeyBind.matchesKey(keyCode, scanCode)) { - sortButton.onClick(0f, 0f); // Simulate a click + public boolean keyPressed(KeyInput keyInput) { + if (LightweightInventorySortingClient.sortKeyBind.matchesKey(keyInput)) { + sortButton.onPress(new MouseInput(0,0)); + return true; } - return super.keyPressed(keyCode, scanCode, modifiers); + return super.keyPressed(keyInput); } + // Updated to new signature @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (LightweightInventorySortingClient.sortKeyBind.matchesMouse(button)) { - sortButton.onClick(0f, 0f); // Simulate a click + public boolean mouseClicked(Click click, boolean doubleClick) { + if (LightweightInventorySortingClient.sortKeyBind.matchesMouse(click)) { + sortButton.simulateClick(click); // Simulate a click } - return super.mouseClicked(mouseX, mouseY, button); + return super.mouseClicked(click, doubleClick); } } diff --git a/src/client/java/borknbeans/lightweightinventorysorting/sorting/ClickOperation.java b/src/client/java/borknbeans/lightweightinventorysorting/sorting/ClickOperation.java index daee57f..6a0c056 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/sorting/ClickOperation.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/sorting/ClickOperation.java @@ -2,7 +2,6 @@ import java.util.List; -import borknbeans.lightweightinventorysorting.config.Config; import net.minecraft.client.MinecraftClient; import net.minecraft.item.ItemStack; import net.minecraft.screen.slot.SlotActionType; @@ -28,6 +27,15 @@ public ClickOperation(MinecraftClient client, int syncId, int targetSlot, ItemSt this.expectedEndingMouseStack = expectedEndingMouseStack; } + // Custom exception to signal that a click was intentionally skipped + 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 void execute() throws Exception { if (client.player == null) { throw new Exception("Player is null"); @@ -43,6 +51,12 @@ 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: + if (!canSafelyClick(startingMouseStack)) { + throw new SkippedUnsafeClickException(targetSlot); + } + + click(); Exception error = null; @@ -88,4 +102,27 @@ private void postClickVerification() throws Exception{ private String getItemStackString(ItemStack stack) { return String.format("%dx %s", stack.getCount(), stack.getItem().getName().getString()); } + + private boolean canSafelyClick(ItemStack mouseStack) { + if (client.player == null) return false; + + var handler = client.player.currentScreenHandler; + if (targetSlot < 0 || targetSlot >= handler.slots.size()) return false; + + var slot = handler.getSlot(targetSlot); + if (slot == null || !slot.isEnabled()) return false; + + // Equipment / non-insertable slots will often reject generic items + // Check both directions: whether we can place *into* this slot OR pick up from it. + if (!slot.canTakeItems(client.player) && !slot.hasStack()) return false; + + // If holding something, make sure this slot can accept it. + if (!mouseStack.isEmpty() && !slot.canInsert(mouseStack)) return false; + + // If this slot has special restrictions (e.g. saddle/armor), skip it. + if (slot.getMaxItemCount() <= 0) return false; + + return true; + } + } diff --git a/src/client/java/borknbeans/lightweightinventorysorting/sorting/SortButton.java b/src/client/java/borknbeans/lightweightinventorysorting/sorting/SortButton.java index e790851..2584e24 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/sorting/SortButton.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/sorting/SortButton.java @@ -1,24 +1,22 @@ package borknbeans.lightweightinventorysorting.sorting; -import com.mojang.blaze3d.pipeline.RenderPipeline; - import borknbeans.lightweightinventorysorting.LightweightInventorySorting; import borknbeans.lightweightinventorysorting.config.Config; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gl.RenderPipelines; +import net.minecraft.client.gui.Click; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; -import net.minecraft.client.gui.widget.ClickableWidget; -import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.gui.widget.PressableWidget; import net.minecraft.text.Text; import net.minecraft.util.Identifier; -public class SortButton extends ClickableWidget { +public class SortButton extends PressableWidget { - private Identifier buttonTexture; - private Identifier buttonHoverTexture; + private final Identifier buttonTexture; + private final Identifier buttonHoverTexture; - private int sortStartIndex, sortEndIndex; + private final int sortStartIndex, sortEndIndex; public SortButton(int x, int y, int width, int height, Text message, int startIndex, int endIndex) { super(x, y, width, height, message); @@ -45,7 +43,7 @@ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float d } @Override - public void onClick(double mouseX, double mouseY) { + public void onPress(net.minecraft.client.input.AbstractInput input) { MinecraftClient client = MinecraftClient.getInstance(); if (client.player != null) { @@ -55,5 +53,11 @@ public void onClick(double mouseX, double mouseY) { LightweightInventorySorting.LOGGER.error("Player is not available."); } } + + public void simulateClick(Click click){ + // For simplicity, ignore coordinates and button type, just trigger onPress + // Convert Click → AbstractInput for onPress + this.onPress(click.buttonInfo()); + } } diff --git a/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java b/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java index 24790f8..a94142d 100644 --- a/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java +++ b/src/client/java/borknbeans/lightweightinventorysorting/sorting/Sorter.java @@ -10,11 +10,65 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.item.BundleItem; import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.screen.slot.Slot; public class Sorter { private static boolean isSorting = false; + // Returns a list of actual handler slot indices that are considered "sortable" / safe. +// This skips saddle, armor, disabled, etc. + private static List getSafeSlots(MinecraftClient client, int sortStartIndex, int sortEndIndex) { + var handler = client.player.currentScreenHandler; + 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.isEnabled()) continue; + + // 1️⃣ 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.getMaxItemCount() == 1 && !slot.canInsert(probe)); + + if (restricted) continue; // saddle, armor, etc. + + // 2️⃣ Skip disabled or read-only slots + if (!slot.canTakeItems(client.player) && !slot.hasStack()) continue; + + // 3️⃣ If it's empty AND can't accept normal items, skip it + if (!slot.hasStack() && !slot.canInsert(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(MinecraftClient client, List safeSlots) { + var handler = client.player.currentScreenHandler; + var out = new ArrayList(safeSlots.size()); + for (int idx : safeSlots) { + out.add(handler.getSlot(idx).getStack().copy()); + } + return out; + } + + // Find the FIRST empty safe slot index (return 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(MinecraftClient client, int sortStartIndex, int sortEndIndex) { if (FabricLoader.getInstance().getEnvironmentType() != EnvType.CLIENT) { return; @@ -47,32 +101,67 @@ public static void sortContainerClientside(MinecraftClient client, int sortStart } private static void clearMouseStack(MinecraftClient client, int syncId, int sortStartIndex, int sortEndIndex) throws Exception { - var snapshot = getInventorySnapshot(client, sortStartIndex, sortEndIndex); - - // Clear any existing item that is on the mouse var mouseStack = getMouseStack(client).copy(); - if (!mouseStack.isEmpty()) { - var emptyIndex = getEmptySlotIndex(snapshot); - if (emptyIndex == -1) { - throw new Exception("[Sort] No empty slot found to clear mouse stack"); - } + if (mouseStack.isEmpty()) return; + + var handler = client.player.currentScreenHandler; + 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.isEnabled()) continue; + if (!slot.getStack().isEmpty()) continue; + if (!slot.canInsert(mouseStack)) continue; + if (slot.getMaxItemCount() <= 0) continue; + + chosenSafeListIndex = k; + break; + } + + if (chosenSafeListIndex == -1) { + throw new Exception("[Sort] No safe empty slot found to clear mouse stack"); + } - var 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); } } private static void combineLikeStacks(MinecraftClient client, int syncId, int sortStartIndex, int sortEndIndex) throws Exception { - var snapshot = getInventorySnapshot(client, sortStartIndex, sortEndIndex); 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++) { var stackOriginal = snapshot.get(i).copy(); mouseStack = getMouseStack(client); + if (stackOriginal.isEmpty() || stackOriginal.getCount() == stackOriginal.getMaxCount()) { continue; } @@ -89,139 +178,354 @@ private static void combineLikeStacks(MinecraftClient client, int syncId, int so var maxStackSize = stack.getMaxCount(); var combinedSize = stack.getCount() + otherStack.getCount(); - var 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 + ); - var expectedEndingMouseStack = combinedSize > maxStackSize ? stack.copyWithCount(combinedSize - maxStackSize) : ItemStack.EMPTY; - var combineStacks = new ClickOperation(client, syncId, j + sortStartIndex, otherStack, stack.copyWithCount(Math.min(combinedSize, maxStackSize)), stack, expectedEndingMouseStack); try { - if (mouseStack.isEmpty()) { // Dont pickup 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()) { - var 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); } } private static void sort(MinecraftClient client, int syncId, int sortStartIndex, int sortEndIndex) throws Exception { - var snapshot = getInventorySnapshot(client, sortStartIndex, sortEndIndex); + // 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 (int i = 0; i < snapshot.size(); i++) { - var stack = snapshot.get(i).copy(); - if (stack.isEmpty()) { - continue; + for (ItemStack st : snapshot) { + if (!st.isEmpty()) { + sortedStacks.add(st.copy()); } - - sortedStacks.add(stack); } + // Sort them using your comparator sortedStacks.sort(new SortComparator()); 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++) { var sortedStack = sortedStacks.get(i); - var 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.areItemsAndComponentsEqual(sortedStack, snapshot.get(j)) && sortedStack.getCount() == snapshot.get(j).getCount()) { - stackCurrIndex = j + sortStartIndex; + ItemStack snapStack = snapshot.get(j); + if (ItemStack.areItemsAndComponentsEqual(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; } - var 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); - var 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) { - var pickupBundleOperation = new ClickOperation(client, syncId, i + sortStartIndex, existingStack, ItemStack.EMPTY, ItemStack.EMPTY, existingStack); - var 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; - } + 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()); + } - var placeOperation = new ClickOperation(client, syncId, i + sortStartIndex, existingStack, sortedStack, sortedStack, existingStack); + // resync snapshot/safeSlots then continue outer loop + safeSlots = getSafeSlots(client, sortStartIndex, sortEndIndex); + snapshot = getSnapshotForSlots(client, safeSlots); + continue; + } - var emptyHandOperation = new ClickOperation(client, syncId, stackCurrIndex, ItemStack.EMPTY, existingStack, existingStack, ItemStack.EMPTY); - - // If the item we are sorting is a bundle, we need to handle it differently - if (sortedStack.getItem() instanceof BundleItem) { - if (!existingStack.isEmpty()) { - var pickupTargetSlotOperation = new ClickOperation(client, syncId, i + sortStartIndex, existingStack, ItemStack.EMPTY, ItemStack.EMPTY, existingStack); - var 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"); + } - var 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++) { var expectedStack = sortedStacks.get(i).copy(); var actualStack = snapshot.get(i).copy(); - if (!ItemStack.areItemsAndComponentsEqual(expectedStack, actualStack)) { + if (!ItemStack.areItemsAndComponentsEqual(expectedStack, actualStack) + || expectedStack.getCount() != actualStack.getCount()) { throw new Exception("[Sort] Stack not in correct position"); } } @@ -251,15 +555,6 @@ private static List getInventorySnapshot(MinecraftClient client, int 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(MinecraftClient client, int index) { if (client.player == null) { return ItemStack.EMPTY; diff --git a/src/client/resources/lightweight-inventory-sorting.client.mixins.json b/src/client/resources/lightweight-inventory-sorting.client.mixins.json index ea5459d..c644b82 100644 --- a/src/client/resources/lightweight-inventory-sorting.client.mixins.json +++ b/src/client/resources/lightweight-inventory-sorting.client.mixins.json @@ -1,13 +1,14 @@ { - "required": true, - "package": "borknbeans.lightweightinventorysorting.mixin.client", - "compatibilityLevel": "JAVA_21", - "client": [ - "InventoryScreenMixin", - "GenericContainerScreenMixin", - "ShulkerBoxScreenMixin" - ], - "injectors": { - "defaultRequire": 1 + "required": true, + "package": "borknbeans.lightweightinventorysorting.mixin.client", + "compatibilityLevel": "JAVA_21", + "client": [ + "GenericContainerScreenMixin", + "HorseScreenMixin", + "InventoryScreenMixin", + "ShulkerBoxScreenMixin" + ], + "injectors": { + "defaultRequire": 1 } } \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index eeb60b3..d629f74 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -39,7 +39,7 @@ ], "depends": { "fabricloader": ">=0.16.14", - "minecraft": "~1.21.8", + "minecraft": "~1.21.10", "java": ">=21", "fabric-api": "*" },