Extract fluid containers from the same inventories as items

This commit is contained in:
ABKQPO committed 2026-09-11 08:51:36 +08:00
1 parent 98101c00aa
commit 51a85dfa1b
3 files changed
+177 -37

No files matched your search

@@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 <R extends Iterable<ItemStack>> int takeFromProvider(EntityPlayerMP player,
InventoryProvider<R> 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 InventoryIterable<?>iterable)) 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.
* <p>
* 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.
* <p>
* 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;
}
/**
@@ -24,6 +24,16 @@ public class InventoryIterable<Inv extends IInventory> implements Iterable<ItemS
return inv;
}
/**
* The slot this iteration stops at, exclusive, or -1 when it runs to the end of the inventory.
* <p>
* 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<ItemStack> iterator() {
return new Iterator<ItemStack>() {
@@ -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.
* <p>
* 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<InventoryProvider<?>> getInventoryProviders(EntityPlayerMP player) {
return inventoryProviders.getPlayerOrdering(player);
}
public static <Inv extends IInventory> InventoryProvider<InventoryIterable<Inv>> newInventoryProvider(
Function<EntityPlayerMP, ? extends Inv> extractor) {
return new InventoryProvider<InventoryIterable<Inv>>() {
@@ -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<ItemStack> 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.
* <p>
* 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.