From 51a85dfa1b013c6d31ca2c36da0a8d1f3776c0b5 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Fri, 11 Sep 2026 08:51:36 +0800 Subject: [PATCH] Extract fluid containers from the same inventories as items --- .../fluid/FluidContainerExtractors.java | 168 ++++++++++++++---- .../structurelib/util/InventoryIterable.java | 10 ++ .../structurelib/util/InventoryUtility.java | 36 ++++ 3 files changed, 177 insertions(+), 37 deletions(-) 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
+ * 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 extends ItemStackExtractor> 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
+ * 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
+ * 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.