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; package net.xevianlight.aphelion.block.custom;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelAccessor; 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.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition; 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.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.CollisionContext;
import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape; 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 // 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 // shortcuts for each directional property because they're pretty verbose
// t/f here means is/isn't connected outgoing in that direction // 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) { public PipeTestBlock(Properties properties) {
super(properties); super(properties, true);
this.registerDefaultState(this.stateDefinition.any() this.registerDefaultState(this.stateDefinition.any()
.setValue(NORTH, false).setValue(SOUTH, false) .setValue(NORTH, false).setValue(SOUTH, false)
.setValue(EAST, false).setValue(WEST, false) .setValue(EAST, false).setValue(WEST, false)
.setValue(UP, false).setValue(DOWN, 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 @Override
public BlockState getStateForPlacement(BlockPlaceContext context) { public BlockState getStateForPlacement(BlockPlaceContext context) {
return makeConnections(context.getLevel(), context.getClickedPos()); return makeConnections(context.getLevel(), context.getClickedPos());
} }
// Updates the block when a neighbor changes // Updates the block; called when a neighbor changes
@Override @Override
public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos currentPos, BlockPos neighborPos) { public BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor level, BlockPos currentPos, BlockPos neighborPos) {
return makeConnections(level, currentPos); return makeConnections(level, currentPos);
@@ -78,17 +95,41 @@ public class PipeTestBlock extends Block {
private BlockState makeConnections(LevelAccessor level, BlockPos pos) { private BlockState makeConnections(LevelAccessor level, BlockPos pos) {
return this.defaultBlockState() return this.defaultBlockState()
.setValue(NORTH, canConnect(level, pos.north())) .setValue(NORTH, canConnect(level, pos.north(), Direction.SOUTH))
.setValue(SOUTH, canConnect(level, pos.south())) .setValue(SOUTH, canConnect(level, pos.south(), Direction.NORTH))
.setValue(EAST, canConnect(level, pos.east())) .setValue(EAST, canConnect(level, pos.east(), Direction.WEST))
.setValue(WEST, canConnect(level, pos.west())) .setValue(WEST, canConnect(level, pos.west(), Direction.EAST))
.setValue(UP, canConnect(level, pos.above())) .setValue(UP, canConnect(level, pos.above(), Direction.DOWN))
.setValue(DOWN, canConnect(level, pos.below())); .setValue(DOWN, canConnect(level, pos.below(), Direction.UP));
} }
private boolean canConnect(LevelAccessor level, BlockPos neighborPos) { /// If a PipeTestBlock can connect to this position from the given direction.
// Simplest logic: connect if the neighbor is also a SimplePipeBlock /// If you're going to the NORTH of yourself, you should be accessing the SOUTH side.
return level.getBlockState(neighborPos).getBlock() == this; 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 @Override
@@ -122,4 +163,9 @@ public class PipeTestBlock extends Block {
public float getShadeBrightness(BlockState state, BlockGetter world, BlockPos pos) { public float getShadeBrightness(BlockState state, BlockGetter world, BlockPos pos) {
return 1.0F; // Maintains full brightness 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( BLOCK_ENTITIES.register("rocket_assembler_block_entity", () -> BlockEntityType.Builder.of(
RocketAssemblerBlockEntity::new, ModBlocks.ROCKET_ASSEMBLER_BLOCK.get()).build(null) 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)
);
} }