Files
StructureLib/src/main/java/com/gtnewhorizon/structurelib/structure/FluidAutoplace.java
T

336 lines
16 KiB
Java

package com.gtnewhorizon.structurelib.structure;
import java.util.function.Consumer;
import javax.annotation.Nullable;
import net.minecraft.block.Block;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.IChatComponent;
import net.minecraft.util.StatCollector;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fluids.IFluidBlock;
import com.gtnewhorizon.gtnhlib.chat.customcomponents.ChatComponentFluid;
import com.gtnewhorizon.gtnhlib.chat.customcomponents.ChatComponentFluidName;
import com.gtnewhorizon.gtnhlib.util.numberformatting.NumberFormatUtil;
import com.gtnewhorizon.structurelib.ConfigurationHandler;
import com.gtnewhorizon.structurelib.StructureLib;
import com.gtnewhorizon.structurelib.StructureLibAPI;
import com.gtnewhorizon.structurelib.alignment.enumerable.ExtendedFacing;
import com.gtnewhorizon.structurelib.fluid.FluidAmounts;
import com.gtnewhorizon.structurelib.fluid.FluidBlockRequirement;
import com.gtnewhorizon.structurelib.fluid.FluidFillPolicy;
import com.gtnewhorizon.structurelib.fluid.IFluidSource;
import com.gtnewhorizon.structurelib.structure.IStructureElement.PlaceResult;
/**
* Autoplace of fluid blocks.
* <p>
* Placing a fluid is not the same as placing a block. A block stays where it was put, while a fluid flows out of an
* unfinished structure, floods the surroundings, and turns a partially filled position into something that no longer
* counts as a source block. This class handles that:
* <ul>
* <li>It only places a fluid once the rest of the structure is in place, so that the fluid has something to hold it.
* See {@link GateMode} for how strict that gate is.</li>
* <li>It charges only for the fluid that is actually missing from a position, so topping a position up after a leak
* costs the difference instead of a whole bucket.</li>
* <li>It only consumes fluid after the block has been placed successfully, and puts the previous block back if the
* fluid turns out to be gone by then.</li>
* </ul>
* Structure elements reach this class through
* {@link StructureUtility#survivalPlaceBlock(Block, int, World, int, int, int, IItemSource, EntityPlayer, Consumer)} or
* {@link StructureUtility#ofFluidBlock(Block, int)}, and custom elements may call
* {@link #tryPlace(World, int, int, int, AutoPlaceEnvironment, FluidBlockRequirement)} directly.
*/
public class FluidAutoplace {
/**
* How careful autoplace is about placing a fluid into a structure that is not finished yet.
*/
public enum GateMode {
/**
* Place the fluid right away. The fluid can flow out of the unfinished structure.
*/
NONE,
/**
* Only place the fluid when none of its six neighbours is a position that the fluid can flow into and that
* doesn't belong to the structure. The fluid can still flow into the part of the structure that is not built
* yet, but it cannot leave the structure.
*/
LENIENT,
/**
* Only place the fluid once every non fluid element of the structure is satisfied. This is the default.
* <p>
* The fluid is placed as soon as the structure can hold it, which is normally the round that finishes the
* structure: the fluids that round could not place yet are tried once more when it is done building, so they do
* not have to wait for another round.
*/
STRICT
}
private FluidAutoplace() {}
/**
* Try to place a fluid block, draining the fluid from the autoplace environment's fluid source.
* <p>
* Nothing is consumed when this returns anything other than {@link PlaceResult#ACCEPT}, and the block that was
* there before is put back when the fluid cannot be taken after the block has been placed.
*
* @param world world to place in
* @param x x coord
* @param y y coord
* @param z z coord
* @param env autoplace environment, providing the fluid source, the actor and the chatter
* @param requirement which block the structure wants there, what it costs, and how strict the check is
* @return {@link PlaceResult#SKIP} when the position is already good enough, {@link PlaceResult#ACCEPT} when the
* fluid block has been placed, {@link PlaceResult#REJECT_CONTINUE} when the structure is not ready for its
* fluid yet or fluid autoplace is off, and {@link PlaceResult#REJECT} when the fluid is missing
*/
public static PlaceResult tryPlace(World world, int x, int y, int z, AutoPlaceEnvironment env,
FluidBlockRequirement requirement) {
if (world.isRemote || !ConfigurationHandler.INSTANCE.isFluidAutoplaceEnabled()) {
return PlaceResult.REJECT_CONTINUE;
}
if (isSatisfied(world, x, y, z, requirement)) return PlaceResult.SKIP;
if (!canReplace(world, x, y, z, env.getActor())) return PlaceResult.REJECT;
if (!isGateOpen(world, x, y, z, env)) {
env.markFluidPlacementDeferred();
return PlaceResult.REJECT_CONTINUE;
}
IFluidSource source = env.getEffectiveFluidSource();
if (source == null) {
reportMissingFluid(env, requirement, requirement.cost().amount);
return PlaceResult.REJECT;
}
Block existingBlock = world.getBlock(x, y, z);
int existingMeta = world.getBlockMetadata(x, y, z);
int target = requirement.cost().amount;
int existing = existingBlock == requirement.block() ? FluidAmounts.getStoredAmount(world, x, y, z, target) : 0;
int missing = Math.max(0, target - existing);
if (missing > 0 && !source.takeAll(new FluidStack(requirement.cost(), missing), true)) {
reportMissingFluid(
env,
requirement,
missing - source.getAvailable(new FluidStack(requirement.cost(), missing)));
return PlaceResult.REJECT;
}
if (!placeFluidBlock(world, x, y, z, requirement)) return PlaceResult.REJECT;
if (missing > 0 && !source.takeAll(new FluidStack(requirement.cost(), missing), false)) {
restore(world, x, y, z, existingBlock, existingMeta);
return PlaceResult.REJECT;
}
return PlaceResult.ACCEPT;
}
/**
* Try to place one of several fluid blocks, in order, and stop at the first one that is already there or that could
* be placed.
*
* @return the result of the first attempt that was more than a {@link PlaceResult#REJECT_CONTINUE}, or
* {@link PlaceResult#REJECT} when the fluid of an alternative was missing, or
* {@link PlaceResult#REJECT_CONTINUE} when every alternative had to wait
* @see #tryPlace(World, int, int, int, AutoPlaceEnvironment, FluidBlockRequirement)
*/
public static PlaceResult tryPlace(World world, int x, int y, int z, AutoPlaceEnvironment env,
Iterable<FluidBlockRequirement> requirements) {
PlaceResult result = PlaceResult.REJECT_CONTINUE;
for (FluidBlockRequirement requirement : requirements) {
PlaceResult attempt = tryPlace(world, x, y, z, env, requirement);
if (attempt == PlaceResult.REJECT_CONTINUE) continue;
if (attempt == PlaceResult.REJECT) {
result = PlaceResult.REJECT;
continue;
}
return attempt;
}
return result;
}
/**
* Whether the position already holds as much fluid as the structure asks for.
* <p>
* This is what a fluid structure element uses for its check, so that a check and an autoplace always agree.
*
* @param world world to look at
* @param x x coord
* @param y y coord
* @param z z coord
* @param requirement which block the structure wants there, and how strict the check is
*/
public static boolean isSatisfied(World world, int x, int y, int z, FluidBlockRequirement requirement) {
Block worldBlock = world.getBlock(x, y, z);
if (worldBlock != requirement.block()) return false;
if (world.getBlockMetadata(x, y, z) == requirement.meta()) return true;
if (requirement.policy() == FluidFillPolicy.EXACT_STATE) return false;
int target = requirement.cost().amount;
return requirement.policy().isSatisfied(FluidAmounts.getStoredAmount(world, x, y, z, target), target);
}
/**
* Put the fluid block into the world without paying for it, which is what a creative mode build does.
* <p>
* Unlike {@link #tryPlace(World, int, int, int, AutoPlaceEnvironment, FluidBlockRequirement)} this does not check
* whether the structure is ready to hold the fluid. A creative mode build places everything that isn't a fluid
* first for that reason.
*
* @return true if the block has been placed
*/
public static boolean place(World world, int x, int y, int z, FluidBlockRequirement requirement) {
return placeFluidBlock(world, x, y, z, requirement);
}
/**
* Returns a human-readable description of the structure element requirement.
*/
public static String describe(FluidBlockRequirement requirement) {
FluidStack stack = requirement.cost();
return StatCollector.translateToLocalFormatted(
"structurelib.autoplace.requirement_fluid",
stack.getLocalizedName(),
NumberFormatUtil.formatFluid(stack.amount));
}
/**
* Whether a fluid may be placed at this position right now.
* <p>
* With {@link GateMode#STRICT} the answer is computed by a walk over the whole structure and cached for the rest of
* the round, as that walk is expensive. The cache is dropped as soon as the round has built something, so that the
* fluids which waited for it are not held back by an answer that predates those placements.
*
* @param world world to place in
* @param x x coord
* @param y y coord
* @param z z coord
* @param env autoplace environment
*/
public static boolean isGateOpen(World world, int x, int y, int z, AutoPlaceEnvironment env) {
GateMode mode = ConfigurationHandler.INSTANCE.getFluidGateMode();
if (mode == GateMode.NONE) return true;
if (mode == GateMode.LENIENT) return isLocallyContained(world, x, y, z, env);
Boolean cached = env.getFluidGateResult();
if (cached != null) return cached;
boolean ready = isStructureReady(world, env);
env.setFluidGateResult(ready);
return ready;
}
/**
* Whether every non fluid element of the structure is satisfied, i.e. whether the structure is built far enough to
* hold the fluid.
* <p>
* An autoplace environment that doesn't know its structure, e.g. one handed to a legacy caller, always answers
* true, as there is nothing to be checked against.
*/
private static boolean isStructureReady(World world, AutoPlaceEnvironment env) {
IStructureDefinition<?> definition = env.getDefinition();
int[] base = env.getBasePositionXYZ();
if (definition == null || base == null) return true;
Object context = env.getContextObject();
int[] baseOffset = env.getBaseOffsetABC();
ExtendedFacing facing = env.getFacing();
IStructureElement<Object>[] elements = getElements(definition, env.getPiece());
if (elements == null) return true;
return StructureUtility.iterateV2(
elements,
world,
facing,
base[0],
base[1],
base[2],
baseOffset[0],
baseOffset[1],
baseOffset[2],
(element, w, ex, ey, ez, a, b, c) -> element.isFluidElement(context)
|| element.check(context, w, ex, ey, ez),
"fluid gate");
}
@SuppressWarnings("unchecked")
@Nullable
private static IStructureElement<Object>[] getElements(IStructureDefinition<?> definition, String piece) {
try {
return (IStructureElement<Object>[]) definition.getStructureFor(piece);
} catch (RuntimeException e) {
StructureLib.LOGGER
.warn("Could not look up structure piece {} to check whether it can hold fluid", piece, e);
return null;
}
}
/**
* Whether the fluid cannot flow out of the structure from this position, i.e. whether every neighbour is either
* blocked, or part of the structure and therefore eventually filled by it.
*/
private static boolean isLocallyContained(World world, int x, int y, int z, AutoPlaceEnvironment env) {
ExtendedFacing facing = env.getFacing();
// An environment that doesn't know the structure cannot tell which neighbours belong to it. Don't block then.
if (facing == null) return true;
int[] abcOffset = new int[3];
int[] xyzOffset = new int[3];
for (ForgeDirection direction : ForgeDirection.VALID_DIRECTIONS) {
int nx = x + direction.offsetX;
int ny = y + direction.offsetY;
int nz = z + direction.offsetZ;
if (isBlocking(world, nx, ny, nz)) continue;
xyzOffset[0] = direction.offsetX;
xyzOffset[1] = direction.offsetY;
xyzOffset[2] = direction.offsetZ;
facing.getOffsetABC(xyzOffset, abcOffset);
if (env.isContainedInPiece(abcOffset[0], abcOffset[1], abcOffset[2])) continue;
return false;
}
return true;
}
private static boolean isBlocking(World world, int x, int y, int z) {
Block block = world.getBlock(x, y, z);
if (block == null) return true;
if (block.isAir(world, x, y, z)) return false;
if (block.getMaterial() != null && block.getMaterial().isLiquid()) return false;
return !block.isReplaceable(world, x, y, z);
}
private static boolean canReplace(World world, int x, int y, int z, EntityPlayer actor) {
Block block = world.getBlock(x, y, z);
if (block == null || block.isAir(world, x, y, z)) return true;
if (block.getMaterial() != null && block.getMaterial().isLiquid()) return true;
if (block instanceof IFluidBlock) return true;
return StructureLibAPI.isBlockTriviallyReplaceable(world, x, y, z, actor);
}
private static boolean placeFluidBlock(World world, int x, int y, int z, FluidBlockRequirement requirement) {
FluidStack fluid = requirement.cost();
Block block = requirement.block();
if (requirement.placer() != null) {
if (!requirement.placer().place(world, x, y, z, block, requirement.meta(), fluid)) return false;
} else {
// The notification flags matter here. A fluid that is put into the world silently neither flows nor tells
// its neighbours about itself, so it would sit where it was placed until something else happens to touch
// it, which for water inside a multiblock can be never.
if (!world.setBlock(x, y, z, block, requirement.meta(), 3)) return false;
}
// A fluid that is never ticked does not spread, and a fluid placed as a flowing one would never settle.
world.scheduleBlockUpdate(x, y, z, block, block.tickRate(world));
return true;
}
private static void restore(World world, int x, int y, int z, Block block, int meta) {
StructureLibAPI.restoreBlock(world, x, y, z, block, meta);
}
private static void reportMissingFluid(AutoPlaceEnvironment env, FluidBlockRequirement requirement, int missing) {
Consumer<IChatComponent> chatter = env.getChatter();
if (chatter == null) return;
FluidStack stack = new FluidStack(requirement.cost(), Math.max(1, missing));
chatter.accept(
new ChatComponentTranslation(
"structurelib.autoplace.missing_fluid",
new ChatComponentFluidName(stack),
new ChatComponentFluid(stack)));
}
}