diff --git a/src/main/java/com/gtnewhorizon/structurelib/fluid/FluidContainerExtractors.java b/src/main/java/com/gtnewhorizon/structurelib/fluid/FluidContainerExtractors.java index ed9abe87..db89ad18 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/fluid/FluidContainerExtractors.java +++ b/src/main/java/com/gtnewhorizon/structurelib/fluid/FluidContainerExtractors.java @@ -1,5 +1,7 @@ package com.gtnewhorizon.structurelib.fluid; +import java.util.Iterator; + import javax.annotation.Nullable; import net.minecraft.entity.player.EntityPlayerMP; @@ -11,6 +13,10 @@ import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.IFluidContainerItem; import com.gtnewhorizon.structurelib.SortedRegistry; +import com.gtnewhorizon.structurelib.util.InventoryIterable; +import com.gtnewhorizon.structurelib.util.InventoryUtility; +import com.gtnewhorizon.structurelib.util.InventoryUtility.InventoryProvider; +import com.gtnewhorizon.structurelib.util.InventoryUtility.ItemStackExtractor; /** * The registry of every way StructureLib knows how to pull fluid out of an item, plus the inventory walk that uses @@ -49,11 +55,16 @@ public class FluidContainerExtractors { } /** - * Extract fluid from the main inventory of given player. + * Extract fluid from every inventory associated with the given player. *

- * Armor slots are excluded. Containers nested inside another item, e.g. inside a backpack, are not looked into. A - * mod that wants to support those can register its own extractor, or hand its own fluid source to autoplace through - * its autoplace environment. + * This walks exactly the same inventories as item autoplace does, see + * {@link com.gtnewhorizon.structurelib.util.InventoryUtility}: the main inventory first, then whatever else is + * registered, which currently means the ender chest when a mod asked for it and every inventory a mod registered + * for a backpack. Inside every item that holds an inventory the walk goes one level deep, so a bucket a player left + * in a backpack is found, and a mod that already lets autoplace take items out of its backpacks does not have to do + * anything for the fluid containers inside them. + *

+ * Armor slots are excluded, as there is no fluid container that goes into one. * * @param player where to take fluid from * @param resource what fluid to take, and how much of it at most @@ -61,22 +72,49 @@ public class FluidContainerExtractors { * @return what has actually been taken. Never null, and never holding more than requested. */ public static FluidStack takeFromPlayer(EntityPlayerMP player, FluidStack resource, boolean simulate) { - // Only the main inventory, as there is no fluid container that goes into an armor slot. - FluidStack taken = takeFromInventory( - player.inventory, - resource, - simulate, - player, - player.inventory.mainInventory.length); - // A fake player, e.g. one a structure preview tool uses, has no connection to sync the inventory over. - if (!simulate && taken.amount > 0 && player.playerNetServerHandler != null) { - player.inventoryContainer.detectAndSendChanges(); + if (player == null) throw new IllegalArgumentException(); + if (resource == null || resource.getFluid() == null) throw new IllegalArgumentException(); + int remaining = resource.amount; + for (InventoryProvider provider : InventoryUtility.getInventoryProviders(player)) { + if (remaining <= 0) break; + remaining -= takeFromProvider(player, provider, new FluidStack(resource, remaining), simulate); } + return new FluidStack(resource, resource.amount - remaining); + } + + /** + * Extract fluid from one registered inventory of a player, and let its provider know it changed. + *

+ * The wildcard of {@link InventoryProvider} is captured by this method's type parameter, which is what allows the + * inventory to be handed back to {@code markDirty}. + */ + private static > int takeFromProvider(EntityPlayerMP player, + InventoryProvider provider, FluidStack resource, boolean simulate) { + R inventory = provider.getInventory(player); + if (inventory == null) return 0; + // Only an inventory that is backed by an IInventory can have a slot written back, which a container such as a + // bucket needs when it is emptied into the world. A provider that hands out its stacks some other way is + // skipped rather than drained in place, as draining without replacing would leave the player a filled + // container that is already spent. + if (!(inventory instanceof InventoryIterableiterable)) return 0; + int taken = takeFromInventory(iterable.getInventory(), resource, simulate, player, iterable.getMaxSlot(), true); + if (taken > 0 && !simulate && hasConnection(player)) provider.markDirty(inventory); return taken; } + /** + * Whether the inventory of given player has a client to be kept in sync with. A fake player, e.g. the one a + * structure preview tool uses, has no connection, and an inventory it changed never has to be sent anywhere. + */ + private static boolean hasConnection(EntityPlayerMP player) { + return player.playerNetServerHandler != null; + } + /** * Extract fluid from given inventory. + *

+ * Containers held inside an item of this inventory, e.g. in a backpack, are not looked into here. Take from a + * player through {@link #takeFromPlayer(EntityPlayerMP, FluidStack, boolean)} to look into those as well. * * @param inv where to take fluid from * @param resource what fluid to take, and how much of it at most @@ -84,38 +122,94 @@ public class FluidContainerExtractors { * @return what has actually been taken. Never null, and never holding more than requested. */ public static FluidStack takeFromInventory(IInventory inv, FluidStack resource, boolean simulate) { - return takeFromInventory(inv, resource, simulate, null, -1); - } - - private static FluidStack takeFromInventory(IInventory inv, FluidStack resource, boolean simulate, - @Nullable EntityPlayerMP player, int maxSlot) { if (inv == null) throw new IllegalArgumentException(); if (resource == null || resource.getFluid() == null) throw new IllegalArgumentException(); + return new FluidStack(resource, takeFromInventory(inv, resource, simulate, null, -1, false)); + } + + /** + * Walk the slots of one inventory and take as much of the requested fluid as they hold. + * + * @param maxSlot slot to stop at, exclusive. -1 for every slot + * @param recursive whether to look inside the items of this inventory, e.g. into a backpack + * @return how much has been taken, never more than requested + */ + private static int takeFromInventory(IInventory inv, FluidStack resource, boolean simulate, + @Nullable EntityPlayerMP player, int maxSlot, boolean recursive) { int slots = maxSlot < 0 ? inv.getSizeInventory() : Math.min(maxSlot, inv.getSizeInventory()); int remaining = resource.amount; for (int slot = 0; slot < slots && remaining > 0; slot++) { ItemStack stack = inv.getStackInSlot(slot); if (stack == null || stack.getItem() == null || stack.stackSize <= 0) continue; - for (FluidContainerExtractor extractor : EXTRACTORS.getPlayerOrdering(player)) { - if (!extractor.isValidSource(stack)) continue; - FluidStack request = new FluidStack(resource, remaining); - boolean converts = extractor.convertsContainer(stack, request); - ItemStack replacement = converts ? extractor.getReplacement(stack) : null; - // A container that turns into another item can only be replaced when it is the only item in its slot. - if (replacement != null && stack.stackSize != 1) continue; - int drained = extractor.drain(stack, request, simulate); - if (drained <= 0) continue; - remaining -= Math.min(remaining, drained); - if (!simulate && converts) { - // A null replacement means the whole slot is spent, e.g. the drops of a fluid that was paid in - // full. - inv.setInventorySlotContents(slot, replacement); - inv.markDirty(); - } - break; + int taken = takeFromStack(stack, inv, slot, resource, remaining, simulate, player); + if (taken > 0) { + remaining -= taken; + continue; + } + if (recursive && player != null) { + remaining -= takeFromNested(player, stack, new FluidStack(resource, remaining), simulate); } } - return new FluidStack(resource, resource.amount - remaining); + return resource.amount - remaining; + } + + /** + * Take fluid out of a single slot, using the first extractor that recognises what is in it. + * + * @return how much has been taken out of this slot, never more than requested + */ + private static int takeFromStack(ItemStack stack, IInventory owner, int slot, FluidStack resource, int maxAmount, + boolean simulate, @Nullable EntityPlayerMP player) { + for (FluidContainerExtractor extractor : EXTRACTORS.getPlayerOrdering(player)) { + if (!extractor.isValidSource(stack)) continue; + FluidStack request = new FluidStack(resource, maxAmount); + boolean converts = extractor.convertsContainer(stack, request); + ItemStack replacement = converts ? extractor.getReplacement(stack) : null; + // A container that turns into another item can only be replaced when it is the only item in its slot. + if (replacement != null && stack.stackSize != 1) continue; + int drained = extractor.drain(stack, request, simulate); + if (drained <= 0) continue; + if (!simulate && converts) { + // A null replacement means the whole slot is spent, e.g. the drops of a fluid that was paid in full. + owner.setInventorySlotContents(slot, replacement); + owner.markDirty(); + } + return Math.min(drained, maxAmount); + } + return 0; + } + + /** + * Look inside the given stack for an inventory, e.g. a backpack, and take fluid out of the containers it holds. + *

+ * Which stacks hold an inventory is decided by {@link InventoryUtility}, which is the same registry item autoplace + * uses, so a mod that already lets autoplace take items out of its backpacks gets the fluid containers inside them + * for free and never has to register anything for fluid. Only one level is looked into, exactly like the item walk + * does. + * + * @return how much has been taken, never more than requested + */ + private static int takeFromNested(EntityPlayerMP player, ItemStack stack, FluidStack resource, boolean simulate) { + int taken = 0; + for (Iterator iter = InventoryUtility.getStackExtractors(player); iter + .hasNext();) { + ItemStackExtractor extractor = iter.next(); + if (!extractor.isValidSource(stack, player)) continue; + IInventory nested = extractor.getInventory(stack, player); + if (nested == null) continue; + int fromNested = takeFromInventory( + nested, + new FluidStack(resource, resource.amount - taken), + simulate, + player, + -1, + false); + if (fromNested <= 0) continue; + if (!simulate) nested.markDirty(); + taken += fromNested; + break; + } + return taken; } /** diff --git a/src/main/java/com/gtnewhorizon/structurelib/util/InventoryIterable.java b/src/main/java/com/gtnewhorizon/structurelib/util/InventoryIterable.java index 9b5a1041..7853c416 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/util/InventoryIterable.java +++ b/src/main/java/com/gtnewhorizon/structurelib/util/InventoryIterable.java @@ -24,6 +24,16 @@ public class InventoryIterable implements Iterable + * Callers that have to write into the inventory instead of iterating over it, e.g. because they replace a stack + * rather than removing it, need this to respect the same range the iterator uses. + */ + public int getMaxSlot() { + return maxSlot; + } + @Override public Iterator iterator() { return new Iterator() { diff --git a/src/main/java/com/gtnewhorizon/structurelib/util/InventoryUtility.java b/src/main/java/com/gtnewhorizon/structurelib/util/InventoryUtility.java index a067df89..04bc21e1 100644 --- a/src/main/java/com/gtnewhorizon/structurelib/util/InventoryUtility.java +++ b/src/main/java/com/gtnewhorizon/structurelib/util/InventoryUtility.java @@ -97,6 +97,19 @@ public class InventoryUtility { return stackExtractors.getPlayerOrdering(player).iterator(); } + /** + * Every inventory registered for the given player, in the order the player configured. + *

+ * This is the set of inventories {@link #takeFromInventory(EntityPlayerMP, ItemStack, boolean)} walks, exposed so + * that a walk that is not item based, e.g. StructureLib taking fluid out of a container a player carries, can cover + * the same inventories without having to register anything of its own. + * + * @param player the player whose inventories to walk + */ + public static Iterable> getInventoryProviders(EntityPlayerMP player) { + return inventoryProviders.getPlayerOrdering(player); + } + public static InventoryProvider> newInventoryProvider( Function extractor) { return new InventoryProvider>() { @@ -122,6 +135,12 @@ public class InventoryUtility { return type == APIType.MAIN; } + @Override + @Nullable + public IInventory getInventory(ItemStack source, @Nullable EntityPlayerMP player) { + return extractor.apply(source); + } + @Override public int takeFromStack(Predicate predicate, boolean simulate, int count, ItemStackCounter store, ItemStack stack, ItemStack filter, EntityPlayerMP player) { @@ -301,6 +320,23 @@ public class InventoryUtility { return true; } + /** + * The inventory held by the given item stack, if it holds one. + *

+ * An extractor for an item that contains an inventory is registered once for item extraction, and can hand that + * same inventory out here, so a walk that is not item based, e.g. StructureLib looking for a fluid container a + * player put in their backpack, reaches the backpack contents as well. Returning null is fine and means this + * extractor cannot hand out the inventory itself. + * + * @param source item stack that might hold an inventory + * @param player the player the stack belongs to, or null when it does not come from a player + * @return the inventory inside the given stack, or null + */ + @Nullable + default IInventory getInventory(ItemStack source, @Nullable EntityPlayerMP player) { + return null; + } + /** * Extract a particular type of item. The extractor can choose to not return all items contained within this * item as long as it makes sense, but the author should inform the player of this.