Tree cutter fixes and compat improvements (issue #54, issue #59). Experimental Fluid Collection Funnel implementation.

This commit is contained in:
stfwi 2019-11-03 13:01:03 +01:00
parent 8746491095
commit 7592c9d494
50 changed files with 1718 additions and 96 deletions

View file

@ -5,4 +5,4 @@ version_minecraft=1.14.4
version_forge_minecraft=1.14.4-28.1.68
version_fml_mappings=20190719-1.14.3
version_jei=1.14.4:6.0.0.10
version_engineersdecor=1.0.15-b3
version_engineersdecor=1.0.15-b4

View file

@ -11,6 +11,8 @@ Mod sources for Minecraft version 1.14.4.
## Version history
~ v1.0.15-b4 [A] Added Fluid Collection Funnel.
- v1.0.15-b3 [A] Added Small Block Breaker.
[M] Mineral Smelter fluid handler/transfer added.

View file

@ -434,6 +434,12 @@ public class ModContent
ModAuxiliaries.getPixeledAABB(0,0,0, 16,16,16)
)).setRegistryName(new ResourceLocation(ModEngineersDecor.MODID, "passive_fluid_accumulator"));
public static final BlockDecorFluidFunnel SMALL_FLUID_FUNNEL = (BlockDecorFluidFunnel)(new BlockDecorFluidFunnel(
BlockDecor.CFG_CUTOUT|BlockDecor.CFG_REDSTONE_CONTROLLED,
Block.Properties.create(Material.IRON, MaterialColor.IRON).hardnessAndResistance(2f, 15f).sound(SoundType.METAL),
ModAuxiliaries.getPixeledAABB(0,0,0, 16,16,16)
)).setRegistryName(new ResourceLocation(ModEngineersDecor.MODID, "small_fluid_funnel"));
// -------------------------------------------------------------------------------------------------------------------
public static final BlockDecorWall CONCRETE_WALL = (BlockDecorWall)(new BlockDecorWall(
@ -497,6 +503,11 @@ public class ModContent
SMALL_SOLAR_PANEL,
SMALL_WASTE_INCINERATOR,
SMALL_MINERAL_SMELTER,
STRAIGHT_CHECK_VALVE,
STRAIGHT_REDSTONE_VALVE,
STRAIGHT_REDSTONE_ANALOG_VALVE,
PASSIVE_FLUID_ACCUMULATOR,
SMALL_FLUID_FUNNEL,
CLINKER_BRICK_BLOCK,
CLINKER_BRICK_SLAB,
CLINKER_BRICK_STAIRS,
@ -558,10 +569,6 @@ public class ModContent
};
private static final Block devBlocks[] = {
STRAIGHT_CHECK_VALVE,
STRAIGHT_REDSTONE_VALVE,
STRAIGHT_REDSTONE_ANALOG_VALVE,
PASSIVE_FLUID_ACCUMULATOR,
};
//--------------------------------------------------------------------------------------------------------------------
@ -618,6 +625,11 @@ public class ModContent
.build(null)
.setRegistryName(ModEngineersDecor.MODID, "te_passive_fluid_accumulator");
public static final TileEntityType<?> TET_SMALL_FLUID_FUNNEL = TileEntityType.Builder
.create(BlockDecorFluidFunnel.BTileEntity::new, SMALL_FLUID_FUNNEL)
.build(null)
.setRegistryName(ModEngineersDecor.MODID, "te_small_fluid_funnel");
public static final TileEntityType<?> TET_MINERAL_SMELTER = TileEntityType.Builder
.create(BlockDecorMineralSmelter.BTileEntity::new, SMALL_MINERAL_SMELTER)
.build(null)
@ -648,6 +660,7 @@ public class ModContent
TET_SMALL_SOLAR_PANEL,
TET_STRAIGHT_PIPE_VALVE,
TET_PASSIVE_FLUID_ACCUMULATOR,
TET_SMALL_FLUID_FUNNEL,
};
//--------------------------------------------------------------------------------------------------------------------

View file

@ -40,6 +40,7 @@ public class ModEngineersDecor
public static final String MODID = "engineersdecor";
public static final int VERSION_DATAFIXER = 0;
private static final Logger LOGGER = LogManager.getLogger();
private static boolean config_loaded = false;
public ModEngineersDecor()
{
@ -63,6 +64,16 @@ public class ModEngineersDecor
LOGGER.info("Registering recipe condition processor ...");
CraftingHelper.register(Serializer.INSTANCE);
Networking.init();
if(config_loaded) {
try {
logger().info("Applying loaded config file.");
ModConfig.apply();
} catch(Throwable e) {
logger().error("Failed to apply config: " + e.getMessage());
}
} else {
logger().info("Cannot apply config, load event was not casted yet.");
}
}
private void onClientSetup(final FMLClientSetupEvent event)
@ -101,16 +112,9 @@ public class ModEngineersDecor
public static void onServerStarting(FMLServerStartingEvent event)
{}
// @SubscribeEvent
@SubscribeEvent
public static void onConfigLoad(net.minecraftforge.fml.config.ModConfig.Loading configEvent)
{
try {
ModEngineersDecor.logger().info("Loaded config file {}", configEvent.getConfig().getFileName());
ModConfig.apply();
} catch(Throwable e) {
ModEngineersDecor.logger().error("Failed to load config: " + e.getMessage());
}
}
{ config_loaded = true; }
@SubscribeEvent
public static void onConfigFileChange(net.minecraftforge.fml.config.ModConfig.ConfigReloading configEvent)
@ -128,7 +132,6 @@ public class ModEngineersDecor
{
event.getGenerator().addProvider(new ModLootTables(event.getGenerator()));
}
}
//

View file

@ -0,0 +1,392 @@
/*
* @file BlockDecorFluidFunnel.java
* @author Stefan Wilhelm (wile)
* @copyright (C) 2019 Stefan Wilhelm
* @license MIT (see https://opensource.org/licenses/MIT)
*
* A device that collects and stores fluid blocks above it.
* Tracks flowing fluid to their source blocks. Compatible
* with vanilla infinite water source.
*/
package wile.engineersdecor.blocks;
import wile.engineersdecor.ModContent;
import net.minecraft.block.*;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.BlockItemUseContext;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.state.IntegerProperty;
import net.minecraft.state.StateContainer;
import net.minecraft.fluid.Fluid;
import net.minecraft.fluid.Fluids;
import net.minecraft.fluid.IFluidState;
import net.minecraft.tileentity.ITickableTileEntity;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityType;
import net.minecraft.util.Direction;
import net.minecraft.util.Hand;
import net.minecraft.util.math.*;
import net.minecraft.world.IBlockReader;
import net.minecraft.world.World;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.fluids.*;
import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
import net.minecraftforge.fluids.capability.IFluidHandler;
import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
public class BlockDecorFluidFunnel extends BlockDecor
{
public static final int FILL_LEVEL_MAX = 3;
public static final IntegerProperty FILL_LEVEL = IntegerProperty.create("level", 0, FILL_LEVEL_MAX);
public BlockDecorFluidFunnel(long config, Block.Properties builder, final AxisAlignedBB unrotatedAABB)
{ super(config, builder, unrotatedAABB); }
@Override
protected void fillStateContainer(StateContainer.Builder<Block, BlockState> builder)
{ super.fillStateContainer(builder); builder.add(FILL_LEVEL); }
@Override
@Nullable
public BlockState getStateForPlacement(BlockItemUseContext context)
{ return super.getStateForPlacement(context).with(FILL_LEVEL, 0); }
@Override
public boolean hasTileEntity(BlockState state)
{ return true; }
@Override
@Nullable
public TileEntity createTileEntity(BlockState state, IBlockReader world)
{ return new BTileEntity(); }
@Override
@SuppressWarnings("deprecation")
public boolean hasComparatorInputOverride(BlockState state)
{ return true; }
@Override
@SuppressWarnings("deprecation")
public int getComparatorInputOverride(BlockState state, World world, BlockPos pos)
{ return MathHelper.clamp((state.get(FILL_LEVEL)*5), 0, 15); }
@Override
public void onBlockPlacedBy(World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack)
{
if(world.isRemote) return;
if((!stack.hasTag()) || (!stack.getTag().contains("tedata"))) return;
CompoundNBT te_nbt = stack.getTag().getCompound("tedata");
if(te_nbt.isEmpty()) return;
final TileEntity te = world.getTileEntity(pos);
if(!(te instanceof BTileEntity)) return;
((BTileEntity)te).readnbt(te_nbt);
((BTileEntity)te).markDirty();
world.setBlockState(pos, state.with(FILL_LEVEL, 0));
}
@Override
public boolean hasDynamicDropList()
{ return true; }
@Override
public List<ItemStack> dropList(BlockState state, World world, BlockPos pos, boolean explosion)
{
final List<ItemStack> stacks = new ArrayList<ItemStack>();
if(world.isRemote) return stacks;
final TileEntity te = world.getTileEntity(pos);
if(!(te instanceof BTileEntity)) return stacks;
if(!explosion) {
ItemStack stack = new ItemStack(this, 1);
CompoundNBT te_nbt = new CompoundNBT();
((BTileEntity)te).writenbt(te_nbt);
if(!te_nbt.isEmpty()) {
CompoundNBT nbt = new CompoundNBT();
nbt.put("tedata", te_nbt);
stack.setTag(nbt);
}
stacks.add(stack);
} else {
stacks.add(new ItemStack(this, 1));
}
return stacks;
}
@Override
@SuppressWarnings("deprecation")
public boolean onBlockActivated(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockRayTraceResult rayTraceResult)
{
if(world.isRemote) return true;
TileEntity te = world.getTileEntity(pos);
if(!(te instanceof BTileEntity)) return false;
return FluidUtil.interactWithFluidHandler(player, hand, world, pos, rayTraceResult.getFace());
}
@Override
@SuppressWarnings("deprecation")
public void neighborChanged(BlockState state, World world, BlockPos pos, Block block, BlockPos fromPos, boolean unused)
{ TileEntity te = world.getTileEntity(pos); if(te instanceof BTileEntity) ((BTileEntity)te).block_changed(); }
//--------------------------------------------------------------------------------------------------------------------
// Tile entity
//--------------------------------------------------------------------------------------------------------------------
public static class BTileEntity extends TileEntity implements ITickableTileEntity, ICapabilityProvider
{
public static final int TANK_CAPACITY = 3000;
public static final int TICK_INTERVAL = 10; // ca 500ms
public static final int COLLECTION_INTERVAL = 40; // ca 2000ms, simulates suction delay and saves CPU when not drained.
public static final int MAX_TRACK_RADIUS = 16;
public static final int MAX_TRACKING_STEPS_PER_CYCLE = 72;
public static final int MAX_TRACKING_STEPS_PER_CYCLE_INTENSIVE = 1024;
public static final int MAX_TRACK_RADIUS_SQ = MAX_TRACK_RADIUS*MAX_TRACK_RADIUS;
public static final int INTENSIVE_SEARCH_TRIGGER_THRESHOLD = 16;
private FluidStack tank_ = FluidStack.EMPTY.copy();
private int tick_timer_ = 0;
private int collection_timer_ = 0;
private int no_fluid_found_counter_ = 0;
private int intensive_search_counter_ = 0;
private int total_pick_counter_ = 0;
private BlockPos last_pick_pos_ = BlockPos.ZERO;
private ArrayList<Vec3i> search_offsets_ = null;
public void block_changed()
{ tick_timer_ = TICK_INTERVAL; } // collect after flowing fluid has a stable state, otherwise it looks odd.
public BTileEntity()
{ this(ModContent.TET_SMALL_FLUID_FUNNEL); }
public BTileEntity(TileEntityType<?> te_type)
{ super(te_type); }
public void readnbt(CompoundNBT nbt)
{
tank_ = (!nbt.contains("tank")) ? (FluidStack.EMPTY.copy()) : (FluidStack.loadFluidStackFromNBT(nbt.getCompound("tank")));
}
public void writenbt(CompoundNBT nbt)
{
if(!tank_.isEmpty()) nbt.put("tank", tank_.writeToNBT(new CompoundNBT()));
}
// TileEntity -----------------------------------------------------------------------------------------
@Override
public void read(CompoundNBT nbt)
{ super.read(nbt); readnbt(nbt); }
@Override
public CompoundNBT write(CompoundNBT nbt)
{ super.write(nbt); writenbt(nbt); return nbt; }
// ICapabilityProvider / Output flow handler ----------------------------------------------------------
private static class OutputFluidHandler implements IFluidHandler
{
private final BTileEntity te;
OutputFluidHandler(BTileEntity parent) { te = parent; }
@Override public int getTanks() { return 1; }
@Override public FluidStack getFluidInTank(int tank) { return te.tank_.copy(); }
@Override public int getTankCapacity(int tank) { return TANK_CAPACITY; }
@Override public boolean isFluidValid(int tank, @Nonnull FluidStack stack) { return true; }
@Override public int fill(FluidStack resource, FluidAction action) { return 0; }
@Override public FluidStack drain(FluidStack resource, FluidAction action)
{
if((resource==null) || (te.tank_.isEmpty())) return FluidStack.EMPTY.copy();
return (!(te.tank_.isFluidEqual(resource))) ? (FluidStack.EMPTY.copy()) : drain(resource.getAmount(), action);
}
@Override public FluidStack drain(int maxDrain, FluidAction action)
{
FluidStack res = te.tank_.copy();
if(res.isEmpty()) return res;
maxDrain = MathHelper.clamp(maxDrain ,0 , te.tank_.getAmount());
res.setAmount(maxDrain);
if(action != FluidAction.EXECUTE) return res;
te.tank_.setAmount(te.tank_.getAmount()-maxDrain);
if(te.tank_.getAmount() <= 0) te.tank_ = FluidStack.EMPTY.copy();
return res;
}
}
private final LazyOptional<IFluidHandler> fluid_handler_ = LazyOptional.of(() -> new OutputFluidHandler(this));
@Override
public <T> LazyOptional<T> getCapability(net.minecraftforge.common.capabilities.Capability<T> capability, @Nullable Direction facing)
{
if(capability == CapabilityFluidHandler.FLUID_HANDLER_CAPABILITY) return fluid_handler_.cast();
return super.getCapability(capability, facing);
}
// ITickableTileEntity --------------------------------------------------------------------------------
private IFluidState get_fluidstate(BlockPos pos)
{
// todo: check if getFluidState() is enough
final Block collection_block = world.getBlockState(pos).getBlock();
if((!(collection_block instanceof IFluidBlock)) && (!(collection_block instanceof FlowingFluidBlock)) && (!(collection_block instanceof IWaterLoggable))) {
return Fluids.EMPTY.getDefaultState();
}
return world.getFluidState(pos);
}
private boolean try_pick(BlockPos pos, IFluidState fluidstate)
{
if(!fluidstate.isSource()) return false;
IFluidHandler hnd = FluidUtil.getFluidHandler(world, pos, null).orElse(null);
FluidStack fs;
if(hnd != null) {
fs = hnd.drain(TANK_CAPACITY, FluidAction.EXECUTE); // IFluidBlock
} else {
fs = new FluidStack(fluidstate.getFluid(), 1000);
BlockState state = world.getBlockState(pos);
if(state instanceof IBucketPickupHandler) {
((IBucketPickupHandler)state).pickupFluid(world, pos, state);
} else {
world.setBlockState(pos, Blocks.AIR.getDefaultState(), 1|2); // ok we can't leave the block, that would be an infinite source of an unknown fluid.
}
}
if((fs==null) || (fs.isEmpty())) return false; // it's marked nonnull but I don't trust every modder - including meself ...
if(tank_.isEmpty()) {
tank_ = fs.copy();
} else if(tank_.isFluidEqual(fs)) {
tank_.setAmount(MathHelper.clamp(tank_.getAmount()+fs.getAmount(), 0, TANK_CAPACITY));
} else {
return false;
}
return true;
}
private boolean can_pick(BlockPos pos, IFluidState fluidstate)
{
if(fluidstate.isSource()) return true;
IFluidHandler hnd = FluidUtil.getFluidHandler(world, pos, null).orElse(null);
if(hnd == null) return false;
FluidStack fs = hnd.drain(TANK_CAPACITY, FluidAction.SIMULATE); // don't trust that everyone returns nonnull
return ((fs!=null) && (!fs.isEmpty())) && (fluidstate.getFluid().isEquivalentTo(fs.getFluid()));
}
private void rebuild_search_offsets(boolean intensive)
{
search_offsets_ = new ArrayList<>(9);
search_offsets_.add(new Vec3i(0, 1, 0)); // up first
{
ArrayList<Vec3i> ofs = new ArrayList<Vec3i>(Arrays.asList(new Vec3i(-1, 0, 0), new Vec3i( 1, 0, 0), new Vec3i( 0, 0,-1), new Vec3i( 0, 0, 1)));
if(intensive || (total_pick_counter_ > 50)) Collections.shuffle(ofs);
search_offsets_.addAll(ofs);
}
if(intensive) {
ArrayList<Vec3i> ofs = new ArrayList<Vec3i>(Arrays.asList(new Vec3i(-1, 1, 0), new Vec3i( 1, 1, 0), new Vec3i( 0, 1,-1), new Vec3i( 0, 1, 1)));
Collections.shuffle(ofs);
search_offsets_.addAll(ofs);
}
}
private boolean try_collect(final BlockPos collection_pos)
{
IFluidState collection_fluidstate = get_fluidstate(collection_pos);
if(collection_fluidstate.isEmpty()) return false;
Fluid fluid_to_collect = collection_fluidstate.getFluid();
if((!tank_.isEmpty()) && (!tank_.getFluid().isEquivalentTo(fluid_to_collect))) return false;
if(try_pick(collection_pos, collection_fluidstate)) { last_pick_pos_ = collection_pos; return true; } // Blocks directly always first. Allows water source blocks to recover/reflow to source blocks.
if((last_pick_pos_==null) || (last_pick_pos_.distanceSq(collection_pos) > MAX_TRACK_RADIUS_SQ)) { last_pick_pos_ = collection_pos; search_offsets_ = null; }
BlockPos pos = last_pick_pos_;
HashSet<BlockPos> checked = new HashSet<>();
Stack<BlockPos> trail = new Stack<BlockPos>();
trail.add(pos);
checked.add(pos);
int steps=0;
boolean intensive = (no_fluid_found_counter_ >= INTENSIVE_SEARCH_TRIGGER_THRESHOLD);
if(intensive) { no_fluid_found_counter_ = 0; ++intensive_search_counter_; }
if(search_offsets_ == null) rebuild_search_offsets(intensive);
int max = intensive ? MAX_TRACKING_STEPS_PER_CYCLE_INTENSIVE : MAX_TRACKING_STEPS_PER_CYCLE;
while(++steps <= max) {
int num_adjacent = 0;
for(int i=0; i<search_offsets_.size(); ++i) {
BlockPos p = pos.add(search_offsets_.get(i));
if(checked.contains(p)) continue;
checked.add(p);
++steps;
IFluidState fluidstate = get_fluidstate(p);
// @todo: nice thing in 1.14: the fluid level is easily readable,
// so lateral motion can be restricted to higher fill levels.
if(fluidstate.getFluid().isEquivalentTo(fluid_to_collect)) {
++num_adjacent;
pos = p;
trail.push(pos);
if(steps < MAX_TRACKING_STEPS_PER_CYCLE_INTENSIVE/2) {
// check for same fluid above (only source blocks)
final int max_surface_search = (MAX_TRACKING_STEPS_PER_CYCLE_INTENSIVE/2)-steps;
for(int k=0; k<max_surface_search; ++k) {
IFluidState fs = get_fluidstate(pos.up());
if(!can_pick(pos.up(), fs)) break;
fluidstate = fs;
pos = pos.up();
trail.push(pos);
}
}
if(try_pick(pos, fluidstate)) {
last_pick_pos_ = pos;
no_fluid_found_counter_ = 0;
search_offsets_ = null;
// probability reset, so it's not turteling too far away, mainly for large nether lava seas, not desert lakes.
if((++total_pick_counter_ > 50) && world.rand.nextInt(10)==0) last_pick_pos_ = collection_pos;
//println("PASS " + steps + " - " + (pos.subtract(collection_pos)));
return true;
}
}
}
if(trail.isEmpty()) break; // reset search
if(num_adjacent==0) pos = trail.pop();
}
//println("FAIL=" + steps + " - " + (pos.subtract(collection_pos)));
//String s = new String(); for(BlockPos p:checked) s += "\n" + p; println(s);
if(intensive_search_counter_ > 2) world.removeBlock(pos, false);
last_pick_pos_ = collection_pos;
search_offsets_ = null; // try other search order
++no_fluid_found_counter_;
return false;
}
public void tick()
{
if((world.isRemote) || (--tick_timer_ > 0)) return;
tick_timer_ = TICK_INTERVAL;
collection_timer_ += TICK_INTERVAL;
boolean dirty = false;
// Collection
if((collection_timer_ >= COLLECTION_INTERVAL) && ((tank_==null) || (tank_.getAmount() <= (TANK_CAPACITY-1000)))) {
collection_timer_ = 0;
if(!world.isBlockPowered(pos)) { // redstone disable feature
if(last_pick_pos_==null) last_pick_pos_ = pos.up();
if(try_collect(pos.up())) dirty = true;
}
}
// Gravity fluid transfer
if((tank_.getAmount() >= 1000)) {
IFluidHandler fh = FluidUtil.getFluidHandler(world, pos.down(), Direction.UP).orElse(null);
if(fh != null) {
FluidStack fs = new FluidStack(tank_.getFluid(), 1000);
int nfilled = MathHelper.clamp(fh.fill(fs, FluidAction.EXECUTE), 0, 1000);
tank_.shrink(nfilled);
dirty = true;
}
}
// Block state
int fill_level = (tank_==null) ? 0 : (MathHelper.clamp(tank_.getAmount()/1000,0,FILL_LEVEL_MAX));
final BlockState funnel_state = world.getBlockState(pos);
if(funnel_state.get(FILL_LEVEL) != fill_level) world.setBlockState(pos, funnel_state.with(FILL_LEVEL, fill_level), 2|16);
if(dirty) markDirty();
}
}
}

View file

@ -484,7 +484,7 @@ public class BlockDecorMineralSmelter extends BlockDecorDirectedHorizontal
@Override
public FluidStack drain(FluidStack resource, FluidAction action)
{
if(!resource.isFluidEqual(lava) || (te.fluid_level() <= 0)) return FluidStack.EMPTY;
if(!resource.isFluidEqual(lava) || (te.fluid_level() <= 0)) return FluidStack.EMPTY.copy();
FluidStack stack = new FluidStack(lava, te.fluid_level());
if(action == FluidAction.EXECUTE) te.fluid_level_drain(te.fluid_level());
return stack;
@ -493,7 +493,7 @@ public class BlockDecorMineralSmelter extends BlockDecorDirectedHorizontal
@Override
public FluidStack drain(int maxDrain, FluidAction action)
{
if(te.fluid_level() <= 0) return FluidStack.EMPTY;
if(te.fluid_level() <= 0) return FluidStack.EMPTY.copy();
maxDrain = (action==FluidAction.EXECUTE) ? (te.fluid_level_drain(maxDrain)) : (Math.min(maxDrain, te.fluid_level()));
return new FluidStack(lava, maxDrain);
}

View file

@ -15,21 +15,23 @@ package wile.engineersdecor.blocks;
import wile.engineersdecor.ModContent;
import wile.engineersdecor.detail.ModAuxiliaries;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.util.math.BlockRayTraceResult;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.tileentity.TileEntityType;
import net.minecraft.util.Hand;
import net.minecraft.world.World;
import net.minecraft.world.IBlockReader;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.tileentity.TileEntityType;
import net.minecraft.tileentity.ITickableTileEntity;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.math.BlockRayTraceResult;
import net.minecraft.util.Hand;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.World;
import net.minecraft.nbt.CompoundNBT;
import net.minecraft.util.Direction;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraftforge.common.util.LazyOptional;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fluids.capability.CapabilityFluidHandler;
import net.minecraftforge.fluids.capability.IFluidHandler;
@ -67,7 +69,6 @@ public class BlockDecorPassiveFluidAccumulator extends BlockDecorDirected
@SuppressWarnings("deprecation")
public void neighborChanged(BlockState state, World world, BlockPos pos, Block block, BlockPos fromPos, boolean unused)
{
// @todo double check if this is actually needed
TileEntity te = world.getTileEntity(pos);
if(te instanceof BlockDecorPipeValve.BTileEntity) ((BTileEntity)te).block_changed();
}
@ -76,7 +77,7 @@ public class BlockDecorPassiveFluidAccumulator extends BlockDecorDirected
// Tile entity
//--------------------------------------------------------------------------------------------------------------------
public static class BTileEntity extends TileEntity // implements ITickableTileEntity, IFluidHandler, IFluidTankProperties, ICapabilityProvider
public static class BTileEntity extends TileEntity implements ITickableTileEntity, ICapabilityProvider
{
protected static int tick_idle_interval = 20; // ca 1000ms, simulates suction delay and saves CPU when not drained.
protected static int max_flowrate = 1000;
@ -99,7 +100,30 @@ public class BlockDecorPassiveFluidAccumulator extends BlockDecorDirected
public void block_changed()
{ initialized_ = false; tick_timer_ = MathHelper.clamp(tick_timer_ , 0, tick_idle_interval); }
// Output flow handler ---------------------------------------------------------------------
// TileEntity ------------------------------------------------------------------------------
public BTileEntity()
{ this(ModContent.TET_PASSIVE_FLUID_ACCUMULATOR); }
public BTileEntity(TileEntityType<?> te_type)
{ super(te_type); }
@Override
public void read(CompoundNBT nbt)
{
super.read(nbt);
tank_ = (!nbt.contains("tank")) ? (FluidStack.EMPTY.copy()) : (FluidStack.loadFluidStackFromNBT(nbt.getCompound("tank")));
}
@Override
public CompoundNBT write(CompoundNBT nbt)
{
super.write(nbt);
if(!tank_.isEmpty()) nbt.put("tank", tank_.writeToNBT(new CompoundNBT()));
return nbt;
}
// Input flow handler ---------------------------------------------------------------------
private static class InputFillHandler implements IFluidHandler
{
@ -114,7 +138,7 @@ public class BlockDecorPassiveFluidAccumulator extends BlockDecorDirected
@Override public FluidStack drain(int maxDrain, FluidAction action) { return FluidStack.EMPTY.copy(); }
}
// Input flow handler ---------------------------------------------------------------------
// Output flow handler ---------------------------------------------------------------------
private static class OutputFlowHandler implements IFluidHandler
{
@ -148,29 +172,6 @@ public class BlockDecorPassiveFluidAccumulator extends BlockDecorDirected
}
}
// TileEntity ------------------------------------------------------------------------------
public BTileEntity()
{ this(ModContent.TET_PASSIVE_FLUID_ACCUMULATOR); }
public BTileEntity(TileEntityType<?> te_type)
{ super(te_type); }
@Override
public void read(CompoundNBT nbt)
{
super.read(nbt);
tank_ = (!nbt.contains("tank")) ? (FluidStack.EMPTY.copy()) : (FluidStack.loadFluidStackFromNBT(nbt.getCompound("tank")));
}
@Override
public CompoundNBT write(CompoundNBT nbt)
{
super.write(nbt);
if(!tank_.isEmpty()) nbt.put("tank", tank_.writeToNBT(new CompoundNBT()));
return nbt;
}
// ICapabilityProvider --------------------------------------------------------------------
private final LazyOptional<IFluidHandler> fluid_handler_ = LazyOptional.of(() -> new OutputFlowHandler(this));

View file

@ -0,0 +1,12 @@
{
"forge_marker": 1,
"defaults": { "model": "engineersdecor:block/device/small_fluid_funnel_model_s0" },
"variants": {
"level": {
"0":{},
"1":{"model": "engineersdecor:block/device/small_fluid_funnel_model_s1"},
"2":{"model": "engineersdecor:block/device/small_fluid_funnel_model_s2"},
"3":{"model": "engineersdecor:block/device/small_fluid_funnel_model_s3"}
}
}
}

View file

@ -155,6 +155,8 @@
"block.engineersdecor.straight_pipe_valve_redstone_analog.help": "§6Straight fluid pipe fragment.§r Conducts fluids only in one direction. Does not connect to the sides. Sneak to place in reverse direction. Blocks if not redstone powered, reduces the flow rate linear from power 1 to 14, opens to maximum possible valve flow rate for power 15.",
"block.engineersdecor.passive_fluid_accumulator": "Passive Fluid Accumulator",
"block.engineersdecor.passive_fluid_accumulator.help": "§6Vacuum suction based fluid collector.§r Has one output, all other sides are input. Drains fluids from adjacent tanks when being drained from the output port by a pump.",
"block.engineersdecor.small_fluid_funnel": "Small Fluid Collection Funnel",
"block.engineersdecor.small_fluid_funnel.help": "§6Collects fluids above it.§r Has an internal tank with three buckets capacity. Traces flowing fluids to nearby source blocks. The fluid can be obtained with fluid transfer systems or a bucket. Fills only tanks below (gravity transfer). Compatible with vanilla infinite-water-source creation.",
"block.engineersdecor.factory_dropper": "Factory Dropper",
"block.engineersdecor.factory_dropper.help": "§6Dropper suitable for advanced factory automation.§r Has twelve round-robin selected slots. Drop force, angle, stack size, and cool-down delay adjustable using sliders in the GUI. Three stack compare slots (below the inventory slots) with logical AND or OR can be used as internal trigger source. The internal trigger can be AND'ed or OR'ed with the external redstone signal trigger. Trigger simulation buttons for testing. Pre-opens shutter door when internal trigger conditions are met. Drops all matching stacks simultaneously. Simply click on all elements in the GUI to see how it works.",
"block.engineersdecor.factory_hopper": "Factory Hopper",

View file

@ -151,6 +151,7 @@
"block.engineersdecor.straight_pipe_valve_redstone_analog.help": "§6Фрагмент прямой трубы.§r Проводит жидкости только в одном направлении. Не соединяется по бокам. SHIFT для размещения в обратном направлении. Не пропускает при отсутствии сигнала красного камня, уменьшает расход линейно с мощности 1 до 14, открывается максимально-возможно при уровне сигнала красного камня 15.",
"block.engineersdecor.passive_fluid_accumulator": "Пассивный жидкостный накопитель",
"block.engineersdecor.passive_fluid_accumulator.help": "§6Вакуумный всасывающий жидкостный коллектор.§r Имеет один выход, все остальные стороны входные. Сливает жидкости из соседних резервуаров при выкачивании жидкости из выходного порта.",
"block.engineersdecor.small_fluid_funnel": "Small Fluid Collection Funnel",
"block.engineersdecor.factory_dropper": "Фабричный выбрасыватель",
"block.engineersdecor.factory_dropper.help": "§6Выбрасыватель подходит для продвинутой автоматизации производства.§r Имеет 12 выборочных слотов. Сила броска, угол, размер стопки и задержка настраиваются в GUI. 3 слота сравнения стека с логическим И или ИЛИ могут использоваться в качестве внутреннего источника запуска. Внутренний триггер может быть И или ИЛИ с внешним триггерным сигналом красного камня. Триггерные кнопки симуляции для тестирования. Предварительно открывает дверцу затвора, когда выполняются условия внутреннего запуска. Сбрасывает все соответствующие стеки одновременно. Нажмите на все элементы в GUI, чтобы увидеть, как это работает.",
"block.engineersdecor.factory_hopper": "Factory Hopper",

View file

@ -154,6 +154,7 @@
"block.engineersdecor.straight_pipe_valve_redstone_analog.help": "§6一段直输液管。§r单向传递流体。 侧面不会与管道连接。会减少流速。潜行能反方向放置。 没有红石信号时断流流速与红石信号强度从1到14线性增长 15时流速上限达到最大。",
"block.engineersdecor.passive_fluid_accumulator": "被动流体累积器。",
"block.engineersdecor.passive_fluid_accumulator.help": "§6基于真空吸力的流体收集器。§r有一个输出面其他面都是输入。 当从输出面被泵抽取时,从输入面邻接储罐抽取液体。",
"block.engineersdecor.small_fluid_funnel": "Small Fluid Collection Funnel",
"block.engineersdecor.factory_dropper": "工厂掉落器",
"block.engineersdecor.factory_dropper.help": "§6适用于高级工厂自动化的掉落器。§r有十二个轮询选择的储物格。 掉落的力度、角度、一叠数量和冷却延时可在GUI调节。三个 内部比较槽带有逻辑与或逻辑或功能,可用作内部触发源。内部触发 还能和外部红石信号触发再进行逻辑与或逻辑或。触发模拟按钮仅作测试用途。 当内部触发条件满足时,预先打开卷帘门。所有符合条件的物品 会同时掉落。点击GUI的各处来了解如何运作。",
"block.engineersdecor.factory_hopper": "Factory Hopper",

View file

@ -0,0 +1,295 @@
{
"parent": "block/cube",
"textures": {
"top": "engineersdecor:block/device/small_fluid_funnel_top",
"bottom": "engineersdecor:block/device/small_fluid_funnel_bottom",
"side": "engineersdecor:block/device/small_fluid_funnel_side_s0",
"particle": "engineersdecor:block/device/small_fluid_funnel_side_s0"
},
"elements": [
{
"from": [0, 0, 0],
"to": [16, 14, 16],
"faces": {
"north": {"uv": [0, 2, 16, 16], "texture": "#side"},
"east": {"uv": [0, 2, 16, 16], "texture": "#side"},
"south": {"uv": [0, 2, 16, 16], "texture": "#side"},
"west": {"uv": [0, 2, 16, 16], "texture": "#side"},
"up": {"uv": [0, 0, 16, 16], "texture": "#top"},
"down": {"uv": [0, 0, 16, 16], "texture": "#bottom"}
}
},
{
"from": [14, 15, 0],
"to": [16, 16, 16],
"rotation": {"angle": 0, "axis": "y", "origin": [8, 22, 8]},
"faces": {
"north": {"uv": [0, 0, 2, 1], "texture": "#side"},
"east": {"uv": [0, 0, 16, 1], "texture": "#side"},
"south": {"uv": [14, 0, 16, 1], "texture": "#side"},
"west": {"uv": [0, 0, 16, 1], "texture": "#side"},
"up": {"uv": [14, 0, 16, 16], "texture": "#top"},
"down": {"uv": [14, 0, 16, 16], "texture": "#bottom"}
}
},
{
"from": [13, 14, 2],
"to": [15, 15, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [7, 20, 8]},
"faces": {
"north": {"uv": [1, 1, 3, 2], "texture": "#side"},
"east": {"uv": [2, 1, 14, 2], "texture": "#side"},
"south": {"uv": [13, 1, 15, 2], "texture": "#side"},
"west": {"uv": [2, 1, 14, 2], "texture": "#side"},
"up": {"uv": [13, 2, 15, 14], "texture": "#top"},
"down": {"uv": [13, 2, 15, 14], "texture": "#bottom"}
}
},
{
"from": [2, 15, 0],
"to": [15, 16, 2],
"rotation": {"angle": 0, "axis": "y", "origin": [5, 22, 8]},
"faces": {
"north": {"uv": [1, 0, 14, 1], "texture": "#side"},
"east": {"uv": [14, 0, 16, 1], "texture": "#side"},
"south": {"uv": [2, 0, 15, 1], "texture": "#side"},
"west": {"uv": [0, 0, 2, 1], "texture": "#side"},
"up": {"uv": [2, 0, 15, 2], "texture": "#top"},
"down": {"uv": [2, 14, 15, 16], "texture": "#bottom"}
}
},
{
"from": [3, 14, 1],
"to": [13, 15, 3],
"rotation": {"angle": 0, "axis": "y", "origin": [5, 20, 9]},
"faces": {
"north": {"uv": [3, 1, 13, 2], "texture": "#side"},
"east": {"uv": [13, 1, 15, 2], "texture": "#side"},
"south": {"uv": [3, 1, 13, 2], "texture": "#side"},
"west": {"uv": [1, 1, 3, 2], "texture": "#side"},
"up": {"uv": [3, 1, 13, 3], "texture": "#top"},
"down": {"uv": [3, 13, 13, 15], "texture": "#bottom"}
}
},
{
"from": [2, 15, 14],
"to": [15, 16, 16],
"rotation": {"angle": 0, "axis": "y", "origin": [5, 22, 22]},
"faces": {
"north": {"uv": [1, 0, 14, 1], "texture": "#side"},
"east": {"uv": [0, 0, 2, 1], "texture": "#side"},
"south": {"uv": [2, 0, 15, 1], "texture": "#side"},
"west": {"uv": [14, 0, 16, 1], "texture": "#side"},
"up": {"uv": [2, 14, 15, 16], "texture": "#top"},
"down": {"uv": [2, 0, 15, 2], "texture": "#bottom"}
}
},
{
"from": [3, 14, 13],
"to": [13, 15, 15],
"rotation": {"angle": 0, "axis": "y", "origin": [5, 20, 21]},
"faces": {
"north": {"uv": [3, 1, 13, 2], "texture": "#side"},
"east": {"uv": [1, 1, 3, 2], "texture": "#side"},
"south": {"uv": [3, 1, 13, 2], "texture": "#side"},
"west": {"uv": [13, 1, 15, 2], "texture": "#side"},
"up": {"uv": [3, 13, 13, 15], "texture": "#top"},
"down": {"uv": [3, 1, 13, 3], "texture": "#bottom"}
}
},
{
"from": [2, 14, 2],
"to": [2.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [-4, 22, 8]},
"faces": {
"north": {"uv": [13.5, 0, 14, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [2, 0, 2.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [2, 2, 2.5, 14], "texture": "#top"},
"down": {"uv": [2, 2, 2.5, 14], "texture": "#bottom"}
}
},
{
"from": [3, 14, 2],
"to": [3.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [-3, 22, 8]},
"faces": {
"north": {"uv": [12.5, 0, 13, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [3, 0, 3.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [3, 2, 3.5, 14], "texture": "#top"},
"down": {"uv": [3, 2, 3.5, 14], "texture": "#bottom"}
}
},
{
"from": [4, 14, 2],
"to": [4.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [-1, 22, 8]},
"faces": {
"north": {"uv": [11.5, 0, 12, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [4, 0, 4.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [4, 2, 4.5, 14], "texture": "#top"},
"down": {"uv": [4, 2, 4.5, 14], "texture": "#bottom"}
}
},
{
"from": [5, 14, 2],
"to": [5.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [0, 22, 8]},
"faces": {
"north": {"uv": [10.5, 0, 11, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [5, 0, 5.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [5, 2, 5.5, 14], "texture": "#top"},
"down": {"uv": [5, 2, 5.5, 14], "texture": "#bottom"}
}
},
{
"from": [6, 14, 2],
"to": [6.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [1, 22, 8]},
"faces": {
"north": {"uv": [9.5, 0, 10, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [6, 0, 6.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [6, 2, 6.5, 14], "texture": "#top"},
"down": {"uv": [6, 2, 6.5, 14], "texture": "#bottom"}
}
},
{
"from": [7, 14, 2],
"to": [7.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [2, 22, 8]},
"faces": {
"north": {"uv": [8.5, 0, 9, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [7, 0, 7.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [7, 2, 7.5, 14], "texture": "#top"},
"down": {"uv": [7, 2, 7.5, 14], "texture": "#bottom"}
}
},
{
"from": [8, 14, 2],
"to": [8.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [3, 22, 8]},
"faces": {
"north": {"uv": [7.5, 0, 8, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [8, 0, 8.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [8, 2, 8.5, 14], "texture": "#top"},
"down": {"uv": [8, 2, 8.5, 14], "texture": "#bottom"}
}
},
{
"from": [9, 14, 2],
"to": [9.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [4, 22, 8]},
"faces": {
"north": {"uv": [6.5, 0, 7, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [9, 0, 9.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [9, 2, 9.5, 14], "texture": "#top"},
"down": {"uv": [9, 2, 9.5, 14], "texture": "#bottom"}
}
},
{
"from": [10, 14, 2],
"to": [10.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [5, 22, 8]},
"faces": {
"north": {"uv": [5.5, 0, 6, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [10, 0, 10.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [10, 2, 10.5, 14], "texture": "#top"},
"down": {"uv": [10, 2, 10.5, 14], "texture": "#bottom"}
}
},
{
"from": [11, 14, 2],
"to": [11.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [6, 22, 8]},
"faces": {
"north": {"uv": [4.5, 0, 5, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [11, 0, 11.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [11, 2, 11.5, 14], "texture": "#top"},
"down": {"uv": [11, 2, 11.5, 14], "texture": "#bottom"}
}
},
{
"from": [12, 14, 2],
"to": [12.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [7, 22, 8]},
"faces": {
"north": {"uv": [3.5, 0, 4, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [12, 0, 12.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [12, 2, 12.5, 14], "texture": "#top"},
"down": {"uv": [12, 2, 12.5, 14], "texture": "#bottom"}
}
},
{
"from": [13, 14, 2],
"to": [13.5, 16, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [8, 22, 8]},
"faces": {
"north": {"uv": [2.5, 0, 3, 2], "texture": "#side"},
"east": {"uv": [2, 0, 14, 2], "texture": "#side"},
"south": {"uv": [13, 0, 13.5, 2], "texture": "#side"},
"west": {"uv": [2, 0, 14, 2], "texture": "#side"},
"up": {"uv": [13, 2, 13.5, 14], "texture": "#top"},
"down": {"uv": [13, 2, 13.5, 14], "texture": "#bottom"}
}
},
{
"from": [0, 15, 0],
"to": [2, 16, 16],
"rotation": {"angle": 0, "axis": "y", "origin": [-6, 22, 8]},
"faces": {
"north": {"uv": [14, 0, 16, 1], "texture": "#side"},
"east": {"uv": [0, 0, 16, 1], "texture": "#side"},
"south": {"uv": [0, 0, 2, 1], "texture": "#side"},
"west": {"uv": [0, 0, 16, 1], "texture": "#side"},
"up": {"uv": [0, 0, 2, 16], "texture": "#top"},
"down": {"uv": [0, 0, 2, 16], "texture": "#bottom"}
}
},
{
"from": [1, 14, 2],
"to": [3, 15, 14],
"rotation": {"angle": 0, "axis": "y", "origin": [-5, 20, 8]},
"faces": {
"north": {"uv": [13, 1, 15, 2], "texture": "#side"},
"east": {"uv": [2, 1, 14, 2], "texture": "#side"},
"south": {"uv": [1, 1, 3, 2], "texture": "#side"},
"west": {"uv": [2, 1, 14, 2], "texture": "#side"},
"up": {"uv": [1, 2, 3, 14], "texture": "#top"},
"down": {"uv": [1, 2, 3, 14], "texture": "#bottom"}
}
}
],
"display": {
"ground": {
"scale": [0.2, 0.2, 0.2]
},
"gui": {
"rotation": [30, 225, 0],
"scale": [0.625, 0.625, 0.625]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}

View file

@ -0,0 +1,4 @@
{
"parent": "engineersdecor:block/device/small_fluid_funnel_model_s0",
"textures": { "side": "engineersdecor:block/device/small_fluid_funnel_side_s1" }
}

View file

@ -0,0 +1,4 @@
{
"parent": "engineersdecor:block/device/small_fluid_funnel_model_s0",
"textures": { "side": "engineersdecor:block/device/small_fluid_funnel_side_s2" }
}

View file

@ -0,0 +1,4 @@
{
"parent": "engineersdecor:block/device/small_fluid_funnel_model_s0",
"textures": { "side": "engineersdecor:block/device/small_fluid_funnel_side_s3" }
}

View file

@ -0,0 +1 @@
{ "parent": "engineersdecor:block/device/small_fluid_funnel_model_s0" }

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

View file

@ -0,0 +1,24 @@
{
"conditions": [
{
"type": "engineersdecor:optional",
"result": "engineersdecor:small_fluid_funnel",
"missing": ["immersiveengineering:metal_device1"]
}
],
"type": "minecraft:crafting_shaped",
"pattern": [
"HHH",
"IBI",
"III"
],
"key": {
"B": { "item": "minecraft:bucket" },
"I": { "item": "minecraft:iron_ingot" },
"H": { "item": "minecraft:hopper" }
},
"result": {
"item": "engineersdecor:small_fluid_funnel",
"count": 1
}
}