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. *

* 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: *

* 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, and * together with the fact that an autoplace round places a limited number of elements it effectively builds the * structure first and fills it with fluid afterwards. */ STRICT } private FluidAutoplace() {} /** * Try to place a fluid block, draining the fluid from the autoplace environment's fluid source. *

* 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 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. *

* 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. *

* 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. *

* With {@link GateMode#STRICT} the answer is computed once per autoplace round and cached on the environment, as it * costs a walk over the whole structure. That also means a fluid is at worst one round late, which is what keeps * the walk cheap on big multiblocks. * * @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. *

* 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[] 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[] getElements(IStructureDefinition definition, String piece) { try { return (IStructureElement[]) 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(); if (requirement.placer() != null) { return requirement.placer().place(world, x, y, z, requirement.block(), requirement.meta(), fluid); } return world.setBlock(x, y, z, requirement.block(), requirement.meta(), 2); } private static void restore(World world, int x, int y, int z, Block block, int meta) { if (block == null || block.isAir(world, x, y, z)) world.setBlockToAir(x, y, z); else world.setBlock(x, y, z, block, meta, 2); } private static void reportMissingFluid(AutoPlaceEnvironment env, FluidBlockRequirement requirement, int missing) { Consumer 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))); } }