Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import net.vercte.satchels.ModTags;
import net.vercte.satchels.content.satchel.SatchelData;
import net.vercte.satchels.content.satchel.SatchelEquipmentSlot;
import net.vercte.satchels.content.satchel.SatchelInventory;
import net.vercte.satchels.content.satchel.SatchelInventorySlot;
import org.spongepowered.asm.mixin.Debug;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Slice;

Expand Down Expand Up @@ -56,6 +58,98 @@ public class AbstractContainerMenuMixin {
return original || this.slots.get(from) instanceof SatchelInventorySlot;
}

// Shift-clicking (QUICK_MOVE) out of a satchel slot: the container's own quickMoveStack branches on
// hardcoded slot-index ranges that don't account for the satchel slots appended past the vanilla layout,
// so it misclassifies the source. Trick the container into treating the click as if it came from a real
// player-inventory slot: transiently present the satchel item in that slot, call the original with the
// borrowed menu-slot index, then copy the remainder back and restore the borrowed slot's item.
//
// We normally borrow the hotbar slot the satchel overlays, which routes the item into the main inventory
// (in the player's own InventoryMenu) or into the open container. But if the main inventory is full, the
// hotbar route has nowhere to go, so we instead borrow a main-inventory slot, which routes the item into
// the hotbar. All of this happens within one synchronous doClick, so no flicker or desync is ever observed.
@WrapOperation(
method = "doClick",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/world/inventory/AbstractContainerMenu;quickMoveStack(Lnet/minecraft/world/entity/player/Player;I)Lnet/minecraft/world/item/ItemStack;"
)
)
private ItemStack satchels$quickMoveFromSatchel(AbstractContainerMenu menu, Player player, int index, Operation<ItemStack> original) {
Slot slot = this.slots.get(index);
if(!(slot instanceof SatchelInventorySlot satchelSlot)) return original.call(menu, player, index);

SatchelData data = SatchelData.get(player);
if(!data.canAccess()) return ItemStack.EMPTY;

int satchelIndex = satchelSlot.getContainerSlot();
SatchelInventory satchelInv = data.getSatchelInventory();
ItemStack satchelItem = satchelInv.getItem(satchelIndex);
if(satchelItem.isEmpty()) return ItemStack.EMPTY;

Inventory inventory = player.getInventory();

// Pick the real inventory slot to borrow: the overlaid hotbar slot normally, or a main-inventory slot
// when the main inventory can't accept the item (so it routes into the hotbar instead).
int borrowMenuIdx = -1;
if(satchels$mainInventoryFull(inventory, satchelItem)) {
borrowMenuIdx = satchels$findMainInventoryMenuIndex(player);
}
if(borrowMenuIdx == -1) {
borrowMenuIdx = satchels$findInventoryMenuIndex(player, satchelIndex + data.getHotbarOffset());
}
if(borrowMenuIdx == -1) return original.call(menu, player, index);

int borrowInvIndex = this.slots.get(borrowMenuIdx).getContainerSlot();
ItemStack savedItem = inventory.getItem(borrowInvIndex);

// Present the satchel item in the borrowed slot (by reference; moveItemStackTo shrinks it in place).
inventory.setItem(borrowInvIndex, satchelItem);
satchelInv.setItem(satchelIndex, ItemStack.EMPTY);

ItemStack result = original.call(menu, player, borrowMenuIdx);

// Copy the remainder back into the satchel and restore the borrowed slot's item.
satchelInv.setItem(satchelIndex, inventory.getItem(borrowInvIndex));
inventory.setItem(borrowInvIndex, savedItem);

return result;
}

@Unique
private int satchels$findInventoryMenuIndex(Player player, int invIndex) {
for(int j = 0; j < this.slots.size(); j++) {
Slot s = this.slots.get(j);
if(s.container == player.getInventory() && s.getContainerSlot() == invIndex) return j;
}
return -1;
}

// Any occupied main-inventory slot (Inventory indices 9-35) present in this menu; -1 if none.
@Unique
private int satchels$findMainInventoryMenuIndex(Player player) {
for(int j = 0; j < this.slots.size(); j++) {
Slot s = this.slots.get(j);
if(s.container == player.getInventory()) {
int cs = s.getContainerSlot();
if(cs >= 9 && cs < 36) return j;
}
}
return -1;
}

// True when the main inventory (Inventory indices 9-35) has no room for the item — neither an empty slot
// nor a stackable partial slot of the same item.
@Unique
private boolean satchels$mainInventoryFull(Inventory inventory, ItemStack item) {
for(int i = 9; i < 36; i++) {
ItemStack s = inventory.getItem(i);
if(s.isEmpty()) return false;
if(ItemStack.isSameItemSameComponents(s, item) && s.getCount() < s.getMaxStackSize()) return false;
}
return true;
}

@Definition(id = "SWAP", field = "Lnet/minecraft/world/inventory/ClickType;SWAP:Lnet/minecraft/world/inventory/ClickType;")
@Expression("? == SWAP")
@WrapOperation(method = "doClick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Inventory;getItem(I)Lnet/minecraft/world/item/ItemStack;", ordinal = 0), slice = @Slice(from = @At("MIXINEXTRAS:EXPRESSION")))
Expand Down