added a "PipeGraph" so pipes can keep track of what they're connected to

This commit is contained in:
TechnoDraconic
2026-02-18 20:10:52 -08:00
parent 851645e93f
commit 97879e47ef
3 changed files with 174 additions and 13 deletions

View File

@@ -1,11 +1,16 @@
package net.xevianlight.aphelion.block.custom;
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.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
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;
@@ -14,9 +19,16 @@ 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;
import org.jetbrains.annotations.Nullable;
// a lot of this is ai slop so take it with a grainlet of salt
public class PipeTestBlock extends Block {
public class PipeTestBlock extends BasicEntityBlock {
// shortcuts for each directional property because they're pretty verbose
// t/f here means is/isn't connected outgoing in that direction
@@ -53,20 +65,25 @@ public class PipeTestBlock extends Block {
}
public PipeTestBlock(Properties properties) {
super(properties);
super(properties, true);
this.registerDefaultState(this.stateDefinition.any()
.setValue(NORTH, false).setValue(SOUTH, false)
.setValue(EAST, false).setValue(WEST, false)
.setValue(UP, false).setValue(DOWN, false));
}
// This method determines the state when first placed
@Override
protected MapCodec<? extends BaseEntityBlock> codec() {
return null;
}
// This method determines the state; called when first placed
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return makeConnections(context.getLevel(), context.getClickedPos());
}
// Updates the block when a neighbor changes
// Updates the block; called when a neighbor changes
@Override
public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos currentPos, BlockPos neighborPos) {
return makeConnections(level, currentPos);
@@ -78,17 +95,41 @@ public class PipeTestBlock extends Block {
private BlockState makeConnections(LevelAccessor level, BlockPos pos) {
return this.defaultBlockState()
.setValue(NORTH, canConnect(level, pos.north()))
.setValue(SOUTH, canConnect(level, pos.south()))
.setValue(EAST, canConnect(level, pos.east()))
.setValue(WEST, canConnect(level, pos.west()))
.setValue(UP, canConnect(level, pos.above()))
.setValue(DOWN, canConnect(level, pos.below()));
.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));
}
private boolean canConnect(LevelAccessor level, BlockPos neighborPos) {
// Simplest logic: connect if the neighbor is also a SimplePipeBlock
return level.getBlockState(neighborPos).getBlock() == this;
/// If a PipeTestBlock can connect to this position from the given direction.
/// If you're going to the NORTH of yourself, you should be accessing the SOUTH side.
public static boolean canConnect(LevelAccessor levelA, BlockPos neighborPos, Direction accessSide) {
// Methinks this is not the best way to test this.
boolean isPipe = levelA.getBlockState(neighborPos).is(ModBlocks.PIPE_TEST_BLOCK.get());
/// This code is AI, but I think it works? I think the reason it's a levelAccessor instead of a level
/// is that we're not sure if we're in, for example, an inventory slot or not.
/// Either way, this should only trigger when it makes sense (assuming this is correct in the first place)
boolean isInventory;
if (levelA instanceof Level level) {
isInventory = level.getCapability(Capabilities.ItemHandler.BLOCK, neighborPos, accessSide) != null;
} else {
isInventory = false;
}
return isPipe || isInventory;
}
@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
BlockEntity BE = level.getBlockEntity(pos);
if (BE instanceof PipeTestBlockEntity pipe) {
// force everything connected to this pipe to reevaluate
if (pipe.graph != null) pipe.graph.invalidate();
}
}
@Override
@@ -122,4 +163,9 @@ public class PipeTestBlock extends Block {
public float getShadeBrightness(BlockState state, BlockGetter world, BlockPos pos) {
return 1.0F; // Maintains full brightness
}
@Override
public @Nullable BlockEntity newBlockEntity(@NotNull BlockPos blockPos, @NotNull BlockState blockState) {
return new PipeTestBlockEntity(blockPos, blockState);
}
}

View File

@@ -0,0 +1,110 @@
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.item.ItemStack;
import net.minecraft.world.level.Level;
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.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;
public class PipeTestBlockEntity extends BlockEntity implements TickableBlockEntity {
public PipeTestBlockEntity(BlockPos pos, BlockState blockState) {
super(ModBlockEntities.PIPE_TEST_BLOCK_ENTITY.get(), pos, blockState);
}
@Override
public void clientTick(ClientLevel level, long time, BlockState state, BlockPos pos) {
}
@Override
public void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos) {
if (this.graph == null) initGraph(level, pos);
}
@Override
public void firstTick(Level level, BlockState state, BlockPos pos) {
}
//TODO: move all this somewhere else
public interface PipeInput {
void tick(Level level, BlockState state, BlockPos pos, Direction facingDirection);
}
public interface PipeOutput {
ItemStack insertItem(ItemStack stack, boolean simulate);
}
public static class PipeGraph {
boolean isInvalid = false;
public List<PipeTestBlockEntity> pipes = new ArrayList<>();
public Set<PipeOutput> outputs = new HashSet<>();
public ItemStack insertItem(ItemStack stack, boolean simulate) {
if (isInvalid) return stack;
for (PipeOutput output : outputs) {
stack = output.insertItem(stack, simulate);
if (stack.isEmpty()) break;
}
return stack;
}
public void addPipe(PipeTestBlockEntity pipe) {
//TODO: add outputs the pipe has already if applicable
pipes.add(pipe);
pipe.graph = this;
}
/// 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;
}
this.isInvalid = true;
}
}
public @Nullable PipeGraph graph = 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<BlockPos> 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();
for (BlockPos pipePos : pipes) {
BlockEntity BE = level.getBlockEntity(pipePos);
if (BE instanceof PipeTestBlockEntity pipe) {
// Invalidate any old graphs
if (pipe.graph != null) {
pipe.graph.invalidate();
}
graph.addPipe(pipe);
}
}
}
}

View File

@@ -51,4 +51,9 @@ public class ModBlockEntities {
BLOCK_ENTITIES.register("rocket_assembler_block_entity", () -> BlockEntityType.Builder.of(
RocketAssemblerBlockEntity::new, ModBlocks.ROCKET_ASSEMBLER_BLOCK.get()).build(null)
);
public static final Supplier<BlockEntityType<PipeTestBlockEntity>> PIPE_TEST_BLOCK_ENTITY =
BLOCK_ENTITIES.register("pipe_test_block_entity", () -> BlockEntityType.Builder.of(
PipeTestBlockEntity::new, ModBlocks.PIPE_TEST_BLOCK.get()).build(null)
);
}