From d56cf37f2b315355d7341f192bb3913244c8b7c4 Mon Sep 17 00:00:00 2001 From: Electroblob77 <35599699+Electroblob77@users.noreply.github.com> Date: Thu, 9 Apr 2020 20:08:28 +0100 Subject: [PATCH] Make spell books return to the slots they came from (provided the container isn't closed) --- .../inventory/ContainerArcaneWorkbench.java | 81 +++++++++++++++++-- .../wizardry/inventory/VirtualSlot.java | 20 ++++- .../wizardry/util/WizardryUtilities.java | 17 ++++ 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java b/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java index 123fa1c0..4377e1eb 100644 --- a/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java +++ b/src/main/java/electroblob/wizardry/inventory/ContainerArcaneWorkbench.java @@ -11,6 +11,7 @@ import electroblob.wizardry.spell.Spell; import electroblob.wizardry.tileentity.TileEntityArcaneWorkbench; import electroblob.wizardry.util.ISpellSortable; import electroblob.wizardry.util.WandHelper; +import electroblob.wizardry.util.WizardryUtilities; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.inventory.ClickType; @@ -230,8 +231,7 @@ public class ContainerArcaneWorkbench extends Container implements ISpellSortabl // Workbench -> inventory/bookshelves if(clickedSlotId <= UPGRADE_SLOT){ // Try to move the stack into the bookshelves. If this fails... - if(getBookshelfSlots().isEmpty() || !this.mergeItemStack(stack, getBookshelfSlots().get(0).slotNumber, - getBookshelfSlots().get(getBookshelfSlots().size()-1).slotNumber + 1, false)){ + if(!mergeStackIntoBookshelves(stack)){ // ...try to move the stack into the player's inventory. If this fails... if(!this.mergeItemStack(stack, UPGRADE_SLOT + 1, UPGRADE_SLOT + 1 + PLAYER_INVENTORY_SIZE, true)){ return ItemStack.EMPTY; // ...nothing else happens. @@ -259,8 +259,7 @@ public class ContainerArcaneWorkbench extends Container implements ISpellSortabl // Try to move the stack into the workbench. If this fails... if(slotRange == null || !this.mergeItemStack(stack, slotRange[0], slotRange[1] + 1, false)){ // ...try to move the stack into the bookshelves. If this fails... - if(getBookshelfSlots().isEmpty() || !this.mergeItemStack(stack, getBookshelfSlots().get(0).slotNumber, - getBookshelfSlots().get(getBookshelfSlots().size()-1).slotNumber + 1, false)){ + if(!mergeStackIntoBookshelves(stack)){ return ItemStack.EMPTY; // ...nothing else happens. } } @@ -328,7 +327,7 @@ public class ContainerArcaneWorkbench extends Container implements ISpellSortabl ItemStack stack = player.inventory.getItemStack(); if(!stack.isEmpty() && !getBookshelfSlots().isEmpty()){ - mergeItemStack(stack, getBookshelfSlots().get(0).slotNumber, getBookshelfSlots().get(getBookshelfSlots().size() - 1).slotNumber + 1, false); + mergeStackIntoBookshelves(stack); return stack; } } @@ -336,6 +335,78 @@ public class ContainerArcaneWorkbench extends Container implements ISpellSortabl return super.slotClick(slotId, dragType, clickTypeIn, player); } + /** + * Tries to merge the given stack into the bookshelf slots, accounting for their previous contents. The stack will + * be merged into slots in the following order of priority: + *

+ * 1. Slots that currently contain a matching item
+ * 2. Empty slots that previously contained a matching item
+ * 3. Empty slots that did not previously contain any items
+ * 4. Empty slots that previously contained a non-matching item + * @param stack The {@link ItemStack} to be merged; will be reduced by the number of items that fitted into the + * bookshelf slots + * @return True if the entire stack was merged into the bookshelf slots, false if not (equivalent to calling + * {@link ItemStack#isEmpty()} on the input stack after this method returns) + */ + private boolean mergeStackIntoBookshelves(ItemStack stack){ + + // LinkedHashSet preserves iteration order whilst ignoring duplicates - neat! + Set slots = new LinkedHashSet<>(bookshelfSlots.size()); + + // Add all slots that currently contain a matching stack + slots.addAll(bookshelfSlots.stream().filter(s -> WizardryUtilities.canMerge(stack, s.getStack())).collect(Collectors.toSet())); + // Then add all empty slots that previously contained a matching stack, if they weren't already added + // No need to actually check if they're empty since inserting a new stack overwrites prevStack anyway + slots.addAll(bookshelfSlots.stream().filter(s -> WizardryUtilities.canMerge(stack, s.getPrevStack())).collect(Collectors.toSet())); + // Then add all slots that did not previously contain anything + slots.addAll(bookshelfSlots.stream().filter(s -> s.getPrevStack().isEmpty()).collect(Collectors.toSet())); + // Finally add all other empty slots (these will be the ones that used to contain something else) + slots.addAll(bookshelfSlots.stream().filter(s -> !s.getHasStack()).collect(Collectors.toSet())); + + slots.removeIf(s -> !s.isItemValid(stack)); // Should never be true, but just in case... + + // Now we have a set of slots in order of priority to try merging into + // We already know the stack will fit, so it's simply a question of distributing it + // This is waaay neater and more flexible than vanilla's (disgusting) mergeItemStack implementation - and that + // only has two priorities of slot to deal with! + + for(Slot slot : slots){ + + ItemStack contents = slot.getStack(); + + if(contents.isEmpty()){ + + // Not sure why mergeItemStack differentiates between full/partial merging, as far as I can tell the + // following line will work for both cases + slot.putStack(stack.splitStack(contents.getMaxStackSize())); + slot.onSlotChanged(); + + if(stack.isEmpty()) return true; // The whole stack has been merged, so we're done! + + }else{ + + int totalItemCount = contents.getCount() + stack.getCount(); + int maxSize = Math.min(slot.getSlotStackLimit(), stack.getMaxStackSize()); + + if(totalItemCount <= maxSize){ + + stack.setCount(0); + contents.setCount(totalItemCount); + slot.onSlotChanged(); + return true; // The whole stack has been merged, so we're done! + + }else if(contents.getCount() < maxSize){ + + stack.shrink(maxSize - contents.getCount()); + contents.setCount(maxSize); + slot.onSlotChanged(); + } + } + } + + return false; // If we get this far, it didn't all fit so return false + } + /** * Called (via {@link electroblob.wizardry.packet.PacketControlInput PacketControlInput}) when the apply button in * the arcane workbench GUI is pressed. diff --git a/src/main/java/electroblob/wizardry/inventory/VirtualSlot.java b/src/main/java/electroblob/wizardry/inventory/VirtualSlot.java index cf806246..619a77b4 100644 --- a/src/main/java/electroblob/wizardry/inventory/VirtualSlot.java +++ b/src/main/java/electroblob/wizardry/inventory/VirtualSlot.java @@ -20,11 +20,14 @@ import net.minecraft.tileentity.TileEntity; public class VirtualSlot extends Slot { private final TileEntity tileEntity; + /** Allows the virtual slot to remember what was last stored in it, so items can be put back in the same place. */ + private ItemStack prevStack; // For now this doesn't persist over GUI close public VirtualSlot(IInventory inventory, int index){ super(inventory, index, -999, -999); if(!(inventory instanceof TileEntity)) throw new IllegalArgumentException("Inventory must be a tile entity!"); this.tileEntity = (TileEntity)inventory; + this.prevStack = getStack().copy(); // We MUST copy the stack or it will get changed from elsewhere later! } @Override @@ -59,15 +62,28 @@ public class VirtualSlot extends Slot { return isValid() ? super.onTake(player, stack) : ItemStack.EMPTY; } + @Override + public void onSlotChanged(){ + super.onSlotChanged(); + if(this.getHasStack()) this.prevStack = this.getStack().copy(); // Ignore stack removal (insertion of empty stacks) + } + @Override public ItemStack getStack(){ return isValid() ? super.getStack() : ItemStack.EMPTY; } + /** Returns the stack that was last in this slot. */ + public ItemStack getPrevStack(){ + return prevStack; + } + @Override public void putStack(ItemStack stack){ - if(isValid() && inventory instanceof TileEntityBookshelf) ((TileEntityBookshelf)inventory).sync(); - if(isValid()) super.putStack(stack); + if(isValid()){ + if(inventory instanceof TileEntityBookshelf) ((TileEntityBookshelf)inventory).sync(); + super.putStack(stack); + } } @Override diff --git a/src/main/java/electroblob/wizardry/util/WizardryUtilities.java b/src/main/java/electroblob/wizardry/util/WizardryUtilities.java index 6e019a5e..05224957 100644 --- a/src/main/java/electroblob/wizardry/util/WizardryUtilities.java +++ b/src/main/java/electroblob/wizardry/util/WizardryUtilities.java @@ -39,6 +39,7 @@ import javax.annotation.Nullable; import java.util.*; import java.util.function.BiPredicate; import java.util.function.Predicate; +import java.util.stream.Collectors; /** * "Where do you put random but useful bits and pieces? {@code WizardryUtilities} of course - the 'stuff that doesn't @@ -810,6 +811,22 @@ public final class WizardryUtilities { return copy; } + /** + * Returns whether the two given item stacks can be merged, i.e. if they both contain the same (stackable) item, + * metadata and NBT. Importantly, the number of items in each stack need not be the same. No actual merging is + * performed by this method; the input stacks will not be modified. + * @param stack1 The first stack to be tested for mergeability + * @param stack2 The second stack to be tested for mergeability (order does not matter) + * @return True if the two stacks can be merged, false if not + */ + public static boolean canMerge(ItemStack stack1, ItemStack stack2){ + return !stack1.isEmpty() && !stack2.isEmpty() + && stack1.isStackable() && stack2.isStackable() + && stack1.getItem() == stack2.getItem() + && (!stack1.getHasSubtypes() || stack1.getMetadata() == stack2.getMetadata()) + && ItemStack.areItemStackTagsEqual(stack1, stack2); + } + /** * Checks if the given player is opped on the given server. If the server is a singleplayer or LAN server, this * means they have cheats enabled.