From ab78582ccee37aa6b0421116aaf1d14ab473e92e Mon Sep 17 00:00:00 2001 From: TechnoDraconic Date: Mon, 9 Mar 2026 15:58:36 -0700 Subject: [PATCH] technically works, but needs visuals on attachments and a lot of organization --- .../aphelion/block/custom/PipeTestBlock.java | 48 +++- .../entity/custom/PipeTestBlockEntity.java | 212 ++++++++++++++++-- 2 files changed, 238 insertions(+), 22 deletions(-) diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/PipeTestBlock.java b/src/main/java/net/xevianlight/aphelion/block/custom/PipeTestBlock.java index 4516634..7e8a4ee 100644 --- a/src/main/java/net/xevianlight/aphelion/block/custom/PipeTestBlock.java +++ b/src/main/java/net/xevianlight/aphelion/block/custom/PipeTestBlock.java @@ -1,9 +1,15 @@ package net.xevianlight.aphelion.block.custom; +import com.mojang.datafixers.kinds.Const; +import com.mojang.math.Constants; import com.mojang.serialization.MapCodec; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; @@ -15,13 +21,14 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import net.neoforged.neoforge.capabilities.Capabilities; import net.xevianlight.aphelion.block.custom.base.BasicEntityBlock; -import net.xevianlight.aphelion.block.entity.custom.OxygenTestBlockEntity; import net.xevianlight.aphelion.block.entity.custom.PipeTestBlockEntity; import net.xevianlight.aphelion.core.init.ModBlocks; import org.jetbrains.annotations.NotNull; @@ -93,14 +100,24 @@ public class PipeTestBlock extends BasicEntityBlock { return Properties.of().noOcclusion(); } + private boolean hasAttachment(@Nullable PipeTestBlockEntity BE, Direction direction) { + return (BE != null && BE.hasAttachment(direction)); + } + private BlockState makeConnections(LevelAccessor level, BlockPos pos) { + BlockEntity BE = level.getBlockEntity(pos); + PipeTestBlockEntity PTBE = null; + if (BE instanceof PipeTestBlockEntity found) { + PTBE = found; + } + return this.defaultBlockState() - .setValue(NORTH, canConnect(level, pos.north(), Direction.SOUTH)) - .setValue(SOUTH, canConnect(level, pos.south(), Direction.NORTH)) - .setValue(EAST, canConnect(level, pos.east(), Direction.WEST)) - .setValue(WEST, canConnect(level, pos.west(), Direction.EAST)) - .setValue(UP, canConnect(level, pos.above(), Direction.DOWN)) - .setValue(DOWN, canConnect(level, pos.below(), Direction.UP)); + .setValue(NORTH, canConnect(level, pos.north(), Direction.SOUTH) || hasAttachment(PTBE, Direction.NORTH)) + .setValue(SOUTH, canConnect(level, pos.south(), Direction.NORTH) || hasAttachment(PTBE, Direction.SOUTH)) + .setValue(EAST, canConnect(level, pos.east(), Direction.WEST) || hasAttachment(PTBE, Direction.EAST)) + .setValue(WEST, canConnect(level, pos.west(), Direction.EAST) || hasAttachment(PTBE, Direction.WEST)) + .setValue(UP, canConnect(level, pos.above(), Direction.DOWN) || hasAttachment(PTBE, Direction.UP)) + .setValue(DOWN, canConnect(level, pos.below(), Direction.UP) || hasAttachment(PTBE, Direction.DOWN)); } /// If a PipeTestBlock can connect to this position from the given direction. @@ -130,6 +147,7 @@ public class PipeTestBlock extends BasicEntityBlock { // force everything connected to this pipe to reevaluate if (pipe.graph != null) pipe.graph.invalidate(); } + super.onRemove(state, level, pos, newState, isMoving); } @Override @@ -137,6 +155,22 @@ public class PipeTestBlock extends BasicEntityBlock { builder.add(NORTH, SOUTH, EAST, WEST, UP, DOWN); } + @Override + protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hitResult) { + + BlockEntity BE = level.getBlockEntity(pos); + if (BE instanceof PipeTestBlockEntity PTBE) { + ItemInteractionResult r = PTBE.useItemOn(stack, state, level, pos, player, hand, hitResult, this); + + BlockState newState = makeConnections(level, pos); + level.setBlock(pos, newState, 3); + + return r; + } else { + return super.useItemOn(stack, state, level, pos, player, hand, hitResult); + } + } + // Doesn't block sunlight // @Override // public boolean propagatesSkylightDown(BlockState state, BlockGetter reader, BlockPos pos) { diff --git a/src/main/java/net/xevianlight/aphelion/block/entity/custom/PipeTestBlockEntity.java b/src/main/java/net/xevianlight/aphelion/block/entity/custom/PipeTestBlockEntity.java index 0fd7886..056b3a9 100644 --- a/src/main/java/net/xevianlight/aphelion/block/entity/custom/PipeTestBlockEntity.java +++ b/src/main/java/net/xevianlight/aphelion/block/entity/custom/PipeTestBlockEntity.java @@ -1,33 +1,41 @@ package net.xevianlight.aphelion.block.entity.custom; -import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.entity.BlockEntity; -import net.minecraft.world.level.block.entity.BlockEntityType; -import net.minecraft.world.level.block.entity.HopperBlockEntity; import net.minecraft.world.level.block.state.BlockState; -import net.neoforged.fml.common.Mod; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.capabilities.BlockCapability; +import net.neoforged.neoforge.capabilities.BlockCapabilityCache; +import net.neoforged.neoforge.capabilities.Capabilities; +import net.neoforged.neoforge.items.IItemHandler; import net.xevianlight.aphelion.Aphelion; import net.xevianlight.aphelion.block.custom.PipeTestBlock; import net.xevianlight.aphelion.block.custom.base.TickableBlockEntity; import net.xevianlight.aphelion.core.init.ModBlockEntities; import net.xevianlight.aphelion.core.init.ModBlocks; import net.xevianlight.aphelion.util.FloodFill3D; -import org.apache.commons.lang3.NotImplementedException; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; + +// TODO: Rearrange this WHOOLE fucking thing into like, 10 different files for the actual pipes public class PipeTestBlockEntity extends BlockEntity implements TickableBlockEntity { + public @Nullable PipeGraph graph = null; + public final Map attachments = new HashMap<>(); + public final Map outputs = new HashMap<>(); + public PipeTestBlockEntity(BlockPos pos, BlockState blockState) { super(ModBlockEntities.PIPE_TEST_BLOCK_ENTITY.get(), pos, blockState); } @@ -40,6 +48,15 @@ public class PipeTestBlockEntity extends BlockEntity implements TickableBlockEnt @Override public void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos) { if (this.graph == null) initGraph(level, pos); + // TODO: Call this as little as necessary + makeOutputs(level, state, pos); + + for (Direction dir : Direction.values()) { + PipeAttachment attachment = attachments.get(dir); + if (attachment != null) { + attachment.tick(level, state, pos, dir, graph); + } + } } @Override @@ -47,12 +64,43 @@ public class PipeTestBlockEntity extends BlockEntity implements TickableBlockEnt } - //TODO: move all this somewhere else + private void addOutput(Direction dir, PipeOutput output) { + outputs.put(dir, output); + if (graph != null) graph.outputs.add(output); + } + + private void removeOutput(Direction dir) { + PipeOutput old = outputs.get(dir); + if (graph != null) graph.outputs.remove(old); + outputs.remove(dir); + } + + private boolean canOutputTo(Level level, BlockPos pos, Direction accessSide) { + return level.getCapability(Capabilities.ItemHandler.BLOCK, pos, accessSide) != null; + } + + protected void makeOutputs(ServerLevel level, BlockState state, BlockPos pos) { + BlockPos.MutableBlockPos neighbor = new BlockPos.MutableBlockPos(); + for (Direction dir : Direction.values()) { + neighbor.setWithOffset(pos, dir); + + if (canOutputTo(level, neighbor, dir.getOpposite()) && attachments.get(dir) == null && outputs.get(dir) == null) { + addOutput(dir, new BasicItemOutput(level, pos, dir)); + } + + if (!(canOutputTo(level, neighbor, dir.getOpposite()) && attachments.get(dir) == null) && outputs.get(dir) != null) { + removeOutput(dir); + } + } + } + + //TODO: move all these classes somewhere else public interface PipeInput { - void tick(Level level, BlockState state, BlockPos pos, Direction facingDirection); + void tick(ServerLevel level, BlockState state, BlockPos pos, Direction facingDirection, PipeGraph graph); } public interface PipeOutput { + /// @return Rejected items ItemStack insertItem(ItemStack stack, boolean simulate); } @@ -71,27 +119,46 @@ public class PipeTestBlockEntity extends BlockEntity implements TickableBlockEnt } public void addPipe(PipeTestBlockEntity pipe) { - //TODO: add outputs the pipe has already if applicable pipes.add(pipe); pipe.graph = this; + for (PipeOutput output : pipe.outputs.values()) { + if (output != null) this.outputs.add(output); + } } /// Called whenever a pipe is removed from a graph, or when a new graph comes across an old one. public void invalidate() { for (PipeTestBlockEntity pipe : pipes) { pipe.graph = null; + for (PipeOutput output : pipe.outputs.values()) { + if (output != null) this.outputs.remove(output); + } } this.isInvalid = true; } } - public @Nullable PipeGraph graph = null; + public static abstract class PipeAttachment { + PipeAttachment(ServerLevel level, BlockPos pos, Direction facingDirection) {} + + //TODO: put in the right interface for a render function + void render() {}; + + public void tick(ServerLevel level, BlockState state, BlockPos pos, Direction facingDirection, PipeGraph graph) {} + } + + public boolean hasAttachment(Direction direction) { + return attachments.get(direction) != null; + } // Simplest implementation I can think of public static void initGraph(Level level, BlockPos pos) { Aphelion.LOGGER.info("Init graph from {}", pos); if (!level.getBlockState(pos).is(ModBlocks.PIPE_TEST_BLOCK.get())) return; - Set pipes = FloodFill3D.run(level, pos, 1000, (var v1, var v2, var state, var v4, var v5, var v6) -> state.is(ModBlocks.PIPE_TEST_BLOCK.get()), false); + Set pipes = FloodFill3D.run( + level, pos, 1000, + (var v1, var v2, var state, var v4, var v5, var v6) -> state.is(ModBlocks.PIPE_TEST_BLOCK.get()), + false); Aphelion.LOGGER.info("Got {} pipes", pipes.size()); PipeGraph graph = new PipeGraph(); @@ -107,4 +174,119 @@ public class PipeTestBlockEntity extends BlockEntity implements TickableBlockEnt } } } + + public void setAttachment(Direction side, PipeAttachment attachment) { + attachments.put(side, attachment); + } + + public boolean trySetAttachment(Direction side, PipeAttachment attachment) { + if (hasAttachment(side)) return false; + setAttachment(side, attachment); + return true; + } + + public static boolean isAttachmentItem(ItemStack stack) { + //TODO: add actual attachment items instead of just stone + return stack.is(Item.byId(1)); + } + + public static class BasicItemExtractAttachment extends PipeAttachment implements PipeInput { + + BlockCapabilityCache capabilityCache; + + BasicItemExtractAttachment(ServerLevel level, BlockPos pos, Direction facingDirection) { + super(level, pos, facingDirection); + + capabilityCache = BlockCapabilityCache.create( + Capabilities.ItemHandler.BLOCK, + level, + pos.relative(facingDirection), + facingDirection.getOpposite() + ); + } + + @Override + public void render() { + + } + + @Override + public void tick(ServerLevel level, BlockState state, BlockPos pos, Direction facingDirection, PipeGraph graph) { + // do an extract and distribute + IItemHandler container = capabilityCache.getCapability(); + if (container == null) return; + + final int EXTRACT_PER_TICK = 4; + int to_extract = EXTRACT_PER_TICK; + + int extract_slot_id = container.getSlots() - 1; + while (extract_slot_id >= 0 && container.getStackInSlot(extract_slot_id).isEmpty()) extract_slot_id--; + if (extract_slot_id == -1) return; + + + // Do a simulated extract, then run through every output side on the graph and do real inserts. + // By the end, remove as many items as we successfully inserted with a real extract + ItemStack to_distribute = container.extractItem(extract_slot_id, to_extract, true); + int extracted_amount = to_distribute.getCount(); + + to_distribute = graph.insertItem(to_distribute, false); + + int distributed_amount = extracted_amount; + if (!to_distribute.isEmpty()) { + distributed_amount = extracted_amount - to_distribute.getCount(); + } + container.extractItem(extract_slot_id, distributed_amount, false); + } + } + + public static class BasicItemOutput implements PipeOutput { + + BlockCapabilityCache capabilityCache; + + BasicItemOutput(ServerLevel level, BlockPos pos, Direction facingDirection) { + capabilityCache = BlockCapabilityCache.create( + Capabilities.ItemHandler.BLOCK, + level, + pos.relative(facingDirection), + facingDirection.getOpposite() + ); + } + + @Override + public ItemStack insertItem(ItemStack stack, boolean simulate) { + ItemStack toInsert = stack.copy(); + + IItemHandler container = capabilityCache.getCapability(); + if (container == null) return toInsert; + + for (int insertIndex = 0; insertIndex < container.getSlots(); insertIndex++) { + if (toInsert.isEmpty()) break; + + toInsert = container.insertItem(insertIndex, toInsert, simulate); + } + + return toInsert; + } + } + + // Server only + public PipeAttachment getAttachmentForItem(ItemStack stack, Direction side) { + if (level.isClientSide()) throw new RuntimeException("Cannot get attachment item on client side!"); + return new BasicItemExtractAttachment((ServerLevel) level, getBlockPos(), side); + } + + /// Called from pipe test block's useItemOn + public ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hitResult, PipeTestBlock block) { + if (level.isClientSide) return ItemInteractionResult.SUCCESS; + Vec3 relativePos = hitResult.getLocation().subtract(pos.getCenter()); + Direction pipeSide = Direction.getNearest(relativePos); + + if (isAttachmentItem(stack)) { + boolean success = trySetAttachment(pipeSide, getAttachmentForItem(stack, pipeSide)); + + return success ? ItemInteractionResult.SUCCESS : ItemInteractionResult.FAIL; + } else { + return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; // goes through with whatever other interaction (placing a block, etc) + } + } }