rework BlockSpoutingBehaviour and add new behaviors

This commit is contained in:
TropheusJ 2025-02-15 21:46:30 -05:00
parent 6edf7ec68e
commit e966455143
10 changed files with 250 additions and 106 deletions

View file

@ -7,7 +7,7 @@ import org.slf4j.Logger;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.mojang.logging.LogUtils; import com.mojang.logging.LogUtils;
import com.simibubi.create.api.behaviour.BlockSpoutingBehaviour; import com.simibubi.create.api.behaviour.spouting.BlockSpoutingBehaviour;
import com.simibubi.create.compat.Mods; import com.simibubi.create.compat.Mods;
import com.simibubi.create.compat.computercraft.ComputerCraftProxy; import com.simibubi.create.compat.computercraft.ComputerCraftProxy;
import com.simibubi.create.compat.curios.Curios; import com.simibubi.create.compat.curios.Curios;
@ -135,7 +135,6 @@ public class Create {
AllArmInteractionPointTypes.register(modEventBus); AllArmInteractionPointTypes.register(modEventBus);
AllFanProcessingTypes.register(modEventBus); AllFanProcessingTypes.register(modEventBus);
AllItemAttributeTypes.register(modEventBus); AllItemAttributeTypes.register(modEventBus);
BlockSpoutingBehaviour.registerDefaults();
// FIXME: some of these registrations are not thread-safe // FIXME: some of these registrations are not thread-safe
AllMovementBehaviours.registerDefaults(); AllMovementBehaviours.registerDefaults();
@ -173,6 +172,7 @@ public class Create {
BoilerHeaters.registerDefaults(); BoilerHeaters.registerDefaults();
AllPortalTracks.registerDefaults(); AllPortalTracks.registerDefaults();
AllDisplayBehaviours.registerDefaults(); AllDisplayBehaviours.registerDefaults();
BlockSpoutingBehaviour.registerDefaults();
// -- // --
AllAdvancements.register(); AllAdvancements.register();

View file

@ -1,46 +0,0 @@
package com.simibubi.create.api.behaviour;
import com.simibubi.create.Create;
import com.simibubi.create.compat.tconstruct.SpoutCasting;
import com.simibubi.create.content.fluids.spout.SpoutBlockEntity;
import com.simibubi.create.impl.behaviour.BlockSpoutingBehaviourImpl;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import net.minecraftforge.fluids.FluidStack;
public abstract class BlockSpoutingBehaviour {
/**
* Register a new custom spout interaction
*
* @param resourceLocation The interaction id
* @param spoutingBehaviour An instance of your behaviour class
*/
public static void addCustomSpoutInteraction(ResourceLocation resourceLocation, BlockSpoutingBehaviour spoutingBehaviour) {
BlockSpoutingBehaviourImpl.addCustomSpoutInteraction(resourceLocation, spoutingBehaviour);
}
public static void registerDefaults() {
addCustomSpoutInteraction(Create.asResource("ticon_casting"), new SpoutCasting());
}
/**
* While idle, Spouts will call this every tick with simulate == true <br>
* When fillBlock returns &gt; 0, the Spout will start its animation cycle <br>
* <br>
* During this animation cycle, fillBlock is called once again with simulate == false but only on the relevant SpoutingBehaviour <br>
* When fillBlock returns &gt; 0 once again, the Spout will drain its content by the returned amount of units <br>
* Perform any other side effects in this method <br>
* This method is called server-side only (except in ponder) <br>
*
* @param level The current level
* @param pos The position of the affected block
* @param spout The spout block entity that is calling this
* @param availableFluid A copy of the fluidStack that is available, modifying this will do nothing, return the amount to be subtracted instead
* @param simulate Whether the spout is testing or actually performing this behaviour
* @return The amount filled into the block, 0 to idle/cancel
*/
public abstract int fillBlock(Level level, BlockPos pos, SpoutBlockEntity spout, FluidStack availableFluid, boolean simulate);
}

View file

@ -0,0 +1,103 @@
package com.simibubi.create.api.behaviour.spouting;
import java.util.List;
import java.util.function.Predicate;
import org.jetbrains.annotations.Nullable;
import com.simibubi.create.Create;
import com.simibubi.create.api.registry.SimpleRegistry;
import com.simibubi.create.compat.Mods;
import com.simibubi.create.compat.tconstruct.SpoutCasting;
import com.simibubi.create.content.fluids.spout.SpoutBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.FarmBlock;
import net.minecraft.world.level.block.LayeredCauldronBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.Fluids;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.registries.ForgeRegistries;
/**
* Interface for custom block-filling behavior for spouts.
* <p>
* Behaviors are queried by block first, through {@link #BY_BLOCK}. If no behavior was provided,
* they are then queried by block entity type, through {@link #BY_BLOCK_ENTITY}.
* @see StateChangingBehavior
* @see CauldronSpoutingBehavior
*/
@FunctionalInterface
public interface BlockSpoutingBehaviour {
SimpleRegistry<Block, BlockSpoutingBehaviour> BY_BLOCK = SimpleRegistry.create();
SimpleRegistry<BlockEntityType<?>, BlockSpoutingBehaviour> BY_BLOCK_ENTITY = SimpleRegistry.create();
/**
* Get the behavior that should be used for the block at the given location.
* Queries both the block and the block entity if needed.
*/
@Nullable
static BlockSpoutingBehaviour get(Level level, BlockPos pos) {
BlockState state = level.getBlockState(pos);
BlockSpoutingBehaviour byBlock = BY_BLOCK.get(state.getBlock());
if (byBlock != null)
return byBlock;
BlockEntity be = level.getBlockEntity(pos);
if (be == null)
return null;
return BY_BLOCK_ENTITY.get(be.getType());
}
static void registerDefaults() {
Predicate<Fluid> isWater = fluid -> fluid.isSame(Fluids.WATER);
BlockSpoutingBehaviour toMud = StateChangingBehavior.setTo(250, isWater, Blocks.MUD);
for (Block dirt : List.of(Blocks.DIRT, Blocks.COARSE_DIRT, Blocks.ROOTED_DIRT)) {
BY_BLOCK.register(dirt, toMud);
}
BY_BLOCK.register(Blocks.FARMLAND, StateChangingBehavior.incrementingState(100, isWater, FarmBlock.MOISTURE));
BY_BLOCK.register(Blocks.WATER_CAULDRON, StateChangingBehavior.incrementingState(250, isWater, LayeredCauldronBlock.LEVEL));
BY_BLOCK.register(Blocks.CAULDRON, CauldronSpoutingBehavior.INSTANCE);
if (!Mods.TCONSTRUCT.isLoaded())
return;
for (String name : List.of("table", "basin")) {
ResourceLocation id = Mods.TCONSTRUCT.rl(name);
if (ForgeRegistries.BLOCK_ENTITY_TYPES.containsKey(id)) {
BlockEntityType<?> table = ForgeRegistries.BLOCK_ENTITY_TYPES.getValue(id);
BY_BLOCK_ENTITY.register(table, SpoutCasting.INSTANCE);
} else {
Create.LOGGER.warn("Block entity {} wasn't found. Outdated compat?", id);
}
}
}
/**
* While idle, spouts will query the behavior provided by the block below it.
* If one is present, this method will be called every tick with simulate == true.
* <p>
* When a value greater than 0 is returned, the spout will begin processing. It will call this method again
* with simulate == false, which is when any filling behavior should actually occur.
* <p>
* This method is only called on the server side, except for in Ponder.
* @param level The current level
* @param pos The position of the affected block
* @param spout The spout block entity that is calling this
* @param availableFluid A copy of the fluidStack that is available, modifying this will do nothing, return the amount to be subtracted instead
* @param simulate Whether the spout is testing or actually performing this behaviour
* @return The amount filled into the block, 0 to idle/cancel
*/
int fillBlock(Level level, BlockPos pos, SpoutBlockEntity spout, FluidStack availableFluid, boolean simulate);
}

View file

@ -0,0 +1,56 @@
package com.simibubi.create.api.behaviour.spouting;
import com.simibubi.create.api.registry.SimpleRegistry;
import com.simibubi.create.content.fluids.spout.SpoutBlockEntity;
import net.minecraft.Util;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.Fluid;
import net.minecraft.world.level.material.Fluids;
import net.minecraftforge.fluids.FluidStack;
/**
* {@link BlockSpoutingBehaviour} for empty cauldrons. Mods can register their fluids
* to {@link #CAULDRON_INFO} to allow spouts to fill empty cauldrons with their fluids.
*/
public enum CauldronSpoutingBehavior implements BlockSpoutingBehaviour {
INSTANCE;
public static final SimpleRegistry<Fluid, CauldronInfo> CAULDRON_INFO = Util.make(() -> {
SimpleRegistry<Fluid, CauldronInfo> registry = SimpleRegistry.create();
registry.register(Fluids.WATER, new CauldronInfo(250, Blocks.WATER_CAULDRON));
registry.register(Fluids.LAVA, new CauldronInfo(1000, Blocks.LAVA_CAULDRON));
return registry;
});
@Override
public int fillBlock(Level level, BlockPos pos, SpoutBlockEntity spout, FluidStack availableFluid, boolean simulate) {
CauldronInfo info = CAULDRON_INFO.get(availableFluid.getFluid());
if (info == null)
return 0;
if (availableFluid.getAmount() < info.amount)
return 0;
if (!simulate) {
level.setBlockAndUpdate(pos, info.cauldron);
}
return info.amount;
}
/**
* @param amount the amount of fluid that must be inserted into an empty cauldron
* @param cauldron the BlockState to set after filling an empty cauldron with the given amount of fluid
*/
public record CauldronInfo(int amount, BlockState cauldron) {
public CauldronInfo(int amount, Block block) {
this(amount, block.defaultBlockState());
}
}
}

View file

@ -0,0 +1,71 @@
package com.simibubi.create.api.behaviour.spouting;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import com.simibubi.create.content.fluids.spout.SpoutBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.IntegerProperty;
import net.minecraft.world.level.material.Fluid;
import net.minecraftforge.fluids.FluidStack;
/**
* An implementation of {@link BlockSpoutingBehaviour} that allows for easily modifying a BlockState through spouting.
* @param amount the amount of fluid consumed when filling
* @param fluidTest a predicate for fluids that can be used to fill the target block
* @param canFill a predicate that must match the target BlockState to fill it
* @param fillFunction a function that converts the current state into the filled one
*/
public record StateChangingBehavior(int amount, Predicate<Fluid> fluidTest, Predicate<BlockState> canFill,
UnaryOperator<BlockState> fillFunction) implements BlockSpoutingBehaviour {
@Override
public int fillBlock(Level level, BlockPos pos, SpoutBlockEntity spout, FluidStack availableFluid, boolean simulate) {
if (availableFluid.getAmount() < this.amount || !this.fluidTest.test(availableFluid.getFluid()))
return 0;
BlockState state = level.getBlockState(pos);
if (!this.canFill.test(state))
return 0;
if (!simulate) {
BlockState newState = this.fillFunction.apply(state);
level.setBlockAndUpdate(pos, newState);
}
return this.amount;
}
/**
* Shortcut for {@link #setTo(int, Predicate, BlockState)} that uses the Block's default state.
*/
public static BlockSpoutingBehaviour setTo(int amount, Predicate<Fluid> fluidTest, Block block) {
return setTo(amount, fluidTest, block.defaultBlockState());
}
/**
* Create a {@link BlockSpoutingBehaviour} that will simply convert the target block to the given state.
* @param newState the state that will be set after filling
*/
public static BlockSpoutingBehaviour setTo(int amount, Predicate<Fluid> fluidTest, BlockState newState) {
return new StateChangingBehavior(amount, fluidTest, state -> true, state -> newState);
}
/**
* Create a {@link BlockSpoutingBehaviour} that will increment the given {@link IntegerProperty} until it reaches
* its maximum value, consuming {@code amount} each time fluid is filled.
* @param property the property that will be incremented by one on each fill
*/
public static BlockSpoutingBehaviour incrementingState(int amount, Predicate<Fluid> fluidTest, IntegerProperty property) {
int max = property.getPossibleValues().stream().max(Integer::compareTo).orElseThrow();
Predicate<BlockState> canFill = state -> state.getValue(property) < max;
UnaryOperator<BlockState> fillFunction = state -> state.setValue(property, state.getValue(property) + 1);
return new StateChangingBehavior(amount, fluidTest, canFill, fillFunction);
}
}

View file

@ -11,7 +11,7 @@ import net.minecraft.world.level.block.entity.BlockEntityType;
* <p> * <p>
* This is used to exclude specific tags that would result in exploits, ex. signs that execute commands when clicked. * This is used to exclude specific tags that would result in exploits, ex. signs that execute commands when clicked.
* <p> * <p>
* This is provided as an alternative to {@link IPartialSafeNBT}. * This is provided as an alternative to {@link PartialSafeNBT}.
*/ */
public class SafeNbtWriterRegistry { public class SafeNbtWriterRegistry {
public static final SimpleRegistry<BlockEntityType<?>, SafeNbtWriter> REGISTRY = SimpleRegistry.create(); public static final SimpleRegistry<BlockEntityType<?>, SafeNbtWriter> REGISTRY = SimpleRegistry.create();

View file

@ -18,9 +18,9 @@ import net.minecraft.world.level.block.state.BlockState;
* <p> * <p>
* This is provided as an alternative to the following interfaces: * This is provided as an alternative to the following interfaces:
* <ul> * <ul>
* <li>{@link ISpecialBlockItemRequirement}</li> * <li>{@link SpecialBlockItemRequirement}</li>
* <li>{@link ISpecialBlockEntityItemRequirement}</li> * <li>{@link SpecialBlockEntityItemRequirement}</li>
* <li>{@link ISpecialEntityItemRequirement}</li> * <li>{@link SpecialEntityItemRequirement}</li>
* </ul> * </ul>
*/ */
public class SchematicRequirementRegistries { public class SchematicRequirementRegistries {

View file

@ -1,31 +1,25 @@
package com.simibubi.create.compat.tconstruct; package com.simibubi.create.compat.tconstruct;
import com.simibubi.create.api.behaviour.BlockSpoutingBehaviour; import com.simibubi.create.api.behaviour.spouting.BlockSpoutingBehaviour;
import com.simibubi.create.compat.Mods;
import com.simibubi.create.content.fluids.spout.SpoutBlockEntity; import com.simibubi.create.content.fluids.spout.SpoutBlockEntity;
import com.simibubi.create.foundation.fluid.FluidHelper; import com.simibubi.create.foundation.fluid.FluidHelper;
import com.simibubi.create.infrastructure.config.AllConfigs; import com.simibubi.create.infrastructure.config.AllConfigs;
import net.createmod.catnip.platform.CatnipServices;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.common.capabilities.ForgeCapabilities; import net.minecraftforge.common.capabilities.ForgeCapabilities;
import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.fluids.capability.IFluidHandler;
import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction;
public class SpoutCasting extends BlockSpoutingBehaviour { public enum SpoutCasting implements BlockSpoutingBehaviour {
INSTANCE;
private static final boolean TICON_PRESENT = Mods.TCONSTRUCT.isLoaded();
ResourceLocation TABLE = new ResourceLocation("tconstruct", "table");
ResourceLocation BASIN = new ResourceLocation("tconstruct", "basin");
@Override @Override
public int fillBlock(Level level, BlockPos pos, SpoutBlockEntity spout, FluidStack availableFluid, public int fillBlock(Level level, BlockPos pos, SpoutBlockEntity spout, FluidStack availableFluid, boolean simulate) {
boolean simulate) {
if (!enabled()) if (!enabled())
return 0; return 0;
@ -40,9 +34,6 @@ public class SpoutCasting extends BlockSpoutingBehaviour {
if (handler.getTanks() != 1) if (handler.getTanks() != 1)
return 0; return 0;
ResourceLocation registryName = CatnipServices.REGISTRIES.getKeyOrThrow(blockEntity.getType());
if (!registryName.equals(TABLE) && !registryName.equals(BASIN))
return 0;
if (!handler.isFluidValid(0, availableFluid)) if (!handler.isFluidValid(0, availableFluid))
return 0; return 0;
@ -61,9 +52,6 @@ public class SpoutCasting extends BlockSpoutingBehaviour {
} }
private boolean enabled() { private boolean enabled() {
if (!TICON_PRESENT)
return false;
return AllConfigs.server().recipes.allowCastingBySpout.get(); return AllConfigs.server().recipes.allowCastingBySpout.get();
} }
} }

View file

@ -8,7 +8,7 @@ import java.util.List;
import com.simibubi.create.AllItems; import com.simibubi.create.AllItems;
import com.simibubi.create.AllSoundEvents; import com.simibubi.create.AllSoundEvents;
import com.simibubi.create.api.behaviour.BlockSpoutingBehaviour; import com.simibubi.create.api.behaviour.spouting.BlockSpoutingBehaviour;
import com.simibubi.create.api.equipment.goggles.IHaveGoggleInformation; import com.simibubi.create.api.equipment.goggles.IHaveGoggleInformation;
import com.simibubi.create.content.fluids.FluidFX; import com.simibubi.create.content.fluids.FluidFX;
import com.simibubi.create.content.kinetics.belt.behaviour.BeltProcessingBehaviour; import com.simibubi.create.content.kinetics.belt.behaviour.BeltProcessingBehaviour;
@ -22,7 +22,6 @@ import com.simibubi.create.foundation.blockEntity.SmartBlockEntity;
import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour; import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour;
import com.simibubi.create.foundation.blockEntity.behaviour.fluid.SmartFluidTankBehaviour; import com.simibubi.create.foundation.blockEntity.behaviour.fluid.SmartFluidTankBehaviour;
import com.simibubi.create.foundation.fluid.FluidHelper; import com.simibubi.create.foundation.fluid.FluidHelper;
import com.simibubi.create.impl.behaviour.BlockSpoutingBehaviourImpl;
import net.createmod.catnip.math.VecHelper; import net.createmod.catnip.math.VecHelper;
import net.createmod.catnip.nbt.NBTHelper; import net.createmod.catnip.nbt.NBTHelper;
@ -199,15 +198,13 @@ public class SpoutBlockEntity extends SmartBlockEntity implements IHaveGoggleInf
FluidStack currentFluidInTank = getCurrentFluidInTank(); FluidStack currentFluidInTank = getCurrentFluidInTank();
if (processingTicks == -1 && (isVirtual() || !level.isClientSide()) && !currentFluidInTank.isEmpty()) { if (processingTicks == -1 && (isVirtual() || !level.isClientSide()) && !currentFluidInTank.isEmpty()) {
BlockSpoutingBehaviourImpl.forEach(behaviour -> { BlockPos filling = this.worldPosition.below(2);
if (customProcess != null) BlockSpoutingBehaviour behavior = BlockSpoutingBehaviour.get(this.level, filling);
return; if (behavior != null && behavior.fillBlock(this.level, filling, this, currentFluidInTank.copy(), true) > 0) {
if (behaviour.fillBlock(level, worldPosition.below(2), this, currentFluidInTank.copy(), true) > 0) { processingTicks = FILLING_TIME;
processingTicks = FILLING_TIME; customProcess = behavior;
customProcess = behaviour; notifyUpdate();
notifyUpdate(); }
}
});
} }
if (processingTicks >= 0) { if (processingTicks >= 0) {

View file

@ -1,25 +0,0 @@
package com.simibubi.create.impl.behaviour;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import org.jetbrains.annotations.ApiStatus;
import com.simibubi.create.api.behaviour.BlockSpoutingBehaviour;
import net.minecraft.resources.ResourceLocation;
// TODO - Make this use AttachedRegistry later
@ApiStatus.Internal
public class BlockSpoutingBehaviourImpl {
private static final Map<ResourceLocation, BlockSpoutingBehaviour> BLOCK_SPOUTING_BEHAVIOURS = new ConcurrentHashMap<>();
public static void addCustomSpoutInteraction(ResourceLocation resourceLocation, BlockSpoutingBehaviour spoutingBehaviour) {
BLOCK_SPOUTING_BEHAVIOURS.put(resourceLocation, spoutingBehaviour);
}
public static void forEach(Consumer<? super BlockSpoutingBehaviour> accept) {
BLOCK_SPOUTING_BEHAVIOURS.values().forEach(accept);
}
}