Rocket assembler block bounds calculation and debug renderer

This commit is contained in:
XevianLight
2026-02-07 16:38:46 -07:00
parent 9d186a0dd5
commit af1efb5c57
11 changed files with 374 additions and 13 deletions

View File

@@ -0,0 +1,3 @@
{
"parent": "aphelion:block/rocket_assembler_block"
}

View File

@@ -0,0 +1,21 @@
{
"type": "minecraft:block",
"pools": [
{
"bonus_rolls": 0.0,
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
],
"entries": [
{
"type": "minecraft:item",
"name": "aphelion:rocket_assembler_block"
}
],
"rolls": 1.0
}
],
"random_sequence": "aphelion:blocks/rocket_assembler_block"
}

View File

@@ -15,6 +15,7 @@ import net.neoforged.neoforge.event.tick.ServerTickEvent;
import net.neoforged.neoforge.network.PacketDistributor; import net.neoforged.neoforge.network.PacketDistributor;
import net.xevianlight.aphelion.block.dummy.renderer.MultiblockDummyRenderer; import net.xevianlight.aphelion.block.dummy.renderer.MultiblockDummyRenderer;
import net.xevianlight.aphelion.block.entity.custom.renderer.OxygenTestRenderer; import net.xevianlight.aphelion.block.entity.custom.renderer.OxygenTestRenderer;
import net.xevianlight.aphelion.block.entity.custom.renderer.RocketAssemblerBlockEntityRenderer;
import net.xevianlight.aphelion.client.AphelionConfig; import net.xevianlight.aphelion.client.AphelionConfig;
import net.xevianlight.aphelion.core.saveddata.EnvironmentSavedData; import net.xevianlight.aphelion.core.saveddata.EnvironmentSavedData;
import net.xevianlight.aphelion.network.packet.PartitionPayload; import net.xevianlight.aphelion.network.packet.PartitionPayload;
@@ -139,6 +140,7 @@ public class Aphelion {
@SubscribeEvent @SubscribeEvent
public static void registerBER(EntityRenderersEvent.RegisterRenderers event) { public static void registerBER(EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(ModBlockEntities.VAF_MULTIBLOCK_DUMMY_ENTITY.get(), MultiblockDummyRenderer::new); event.registerBlockEntityRenderer(ModBlockEntities.VAF_MULTIBLOCK_DUMMY_ENTITY.get(), MultiblockDummyRenderer::new);
event.registerBlockEntityRenderer(ModBlockEntities.ROCKET_ASSEMBLER_BLOCK_ENTITY.get(), RocketAssemblerBlockEntityRenderer::new);
// event.registerBlockEntityRenderer(ModBlockEntities.OXYGEN_TEST_BLOCK_ENTITY.get(), OxygenTestRenderer::new); // event.registerBlockEntityRenderer(ModBlockEntities.OXYGEN_TEST_BLOCK_ENTITY.get(), OxygenTestRenderer::new);
} }

View File

@@ -2,21 +2,22 @@ package net.xevianlight.aphelion.block.custom;
import com.mojang.serialization.MapCodec; import com.mojang.serialization.MapCodec;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.*; import net.minecraft.world.level.block.*;
import net.minecraft.world.level.block.entity.BlockEntity; 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.BooleanProperty;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.xevianlight.aphelion.block.custom.base.BasicHorizontalEntityBlock; import net.xevianlight.aphelion.block.custom.base.BasicHorizontalEntityBlock;
import net.xevianlight.aphelion.block.entity.custom.RocketAssemblerBlockEntity; import net.xevianlight.aphelion.block.entity.custom.RocketAssemblerBlockEntity;
import net.xevianlight.aphelion.util.AphelionBlockStateProperties;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public class RocketAssemblerBlock extends BasicHorizontalEntityBlock { public class RocketAssemblerBlock extends BasicHorizontalEntityBlock {
public static final BooleanProperty FORMED = AphelionBlockStateProperties.FORMED;
public RocketAssemblerBlock(Properties properties) { public RocketAssemblerBlock(Properties properties) {
super(properties, true); super(properties, true);
} }
@@ -24,7 +25,7 @@ public class RocketAssemblerBlock extends BasicHorizontalEntityBlock {
public static final MapCodec<RocketAssemblerBlock> CODEC = simpleCodec(RocketAssemblerBlock::new); public static final MapCodec<RocketAssemblerBlock> CODEC = simpleCodec(RocketAssemblerBlock::new);
@Override @Override
protected MapCodec<? extends BaseEntityBlock> codec() { protected @NotNull MapCodec<? extends BaseEntityBlock> codec() {
return CODEC; return CODEC;
} }
@@ -38,7 +39,18 @@ public class RocketAssemblerBlock extends BasicHorizontalEntityBlock {
} }
@Override @Override
public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { public @Nullable BlockEntity newBlockEntity(@NotNull BlockPos blockPos, @NotNull BlockState blockState) {
return new RocketAssemblerBlockEntity(blockPos, blockState); return new RocketAssemblerBlockEntity(blockPos, blockState);
} }
@Override
public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) {
return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()).setValue(FORMED, false);
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FORMED);
super.createBlockStateDefinition(builder);
}
} }

View File

@@ -8,14 +8,43 @@ import net.minecraft.world.level.block.state.BlockState;
public interface TickableBlockEntity { public interface TickableBlockEntity {
/**
* Runs on both the client AND server.
* @param level
* @param time
* @param state
* @param pos
*/
default void tick (Level level, long time, BlockState state, BlockPos pos) {};
/**
* Runs on the client only
* @param level
* @param time
* @param state
* @param pos
*/
void clientTick(ClientLevel level, long time, BlockState state, BlockPos pos); void clientTick(ClientLevel level, long time, BlockState state, BlockPos pos);
/**
* Runs on the server only
* @param level
* @param time
* @param state
* @param pos
*/
void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos); void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos);
default boolean isInitialized() { default boolean isInitialized() {
return true; return true;
}; };
/**
* Runs on client AND server, once only.
* @param level
* @param state
* @param pos
*/
void firstTick(Level level, BlockState state, BlockPos pos); void firstTick(Level level, BlockState state, BlockPos pos);
default void onRemoved() {} default void onRemoved() {}

View File

@@ -1,23 +1,43 @@
package net.xevianlight.aphelion.block.entity.custom; package net.xevianlight.aphelion.block.entity.custom;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.Connection;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.game.ClientGamePacketListener;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity; 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.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.xevianlight.aphelion.Aphelion;
import net.xevianlight.aphelion.block.custom.base.TickableBlockEntity; import net.xevianlight.aphelion.block.custom.base.TickableBlockEntity;
import net.xevianlight.aphelion.core.init.ModBlockEntities; import net.xevianlight.aphelion.core.init.ModBlockEntities;
import net.xevianlight.aphelion.core.init.ModBlocks;
import net.xevianlight.aphelion.util.AphelionBlockStateProperties;
import net.xevianlight.aphelion.util.ModTags; import net.xevianlight.aphelion.util.ModTags;
import net.xevianlight.aphelion.util.RocketStructure; import net.xevianlight.aphelion.util.RocketStructure;
import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.NotImplementedException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayDeque;
public class RocketAssemblerBlockEntity extends BlockEntity implements TickableBlockEntity { public class RocketAssemblerBlockEntity extends BlockEntity implements TickableBlockEntity {
Direction facing; Direction facing;
BlockPos padScanStart = BlockPos.ZERO; BlockPos padScanStart = BlockPos.ZERO;
private PadInfo padBounds;
public @Nullable PadInfo getPadBounds() {
return padBounds;
}
public boolean isInitialized; public boolean isInitialized;
@Override @Override
@@ -29,9 +49,20 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB
super(ModBlockEntities.ROCKET_ASSEMBLER_BLOCK_ENTITY.get(), pos, blockState); super(ModBlockEntities.ROCKET_ASSEMBLER_BLOCK_ENTITY.get(), pos, blockState);
} }
public record PadInfo(BlockPos min, BlockPos max, int size) {}; public record PadInfo(BlockPos min, BlockPos max) {
public int getVolume() {
int dx = max.getX() - min.getX() + 1;
int dy = max.getY() - min.getY() + 1;
int dz = max.getZ() - min.getZ() + 1;
public PadInfo getPlatform() { return dx * dy * dz;
}
}
private final Block TOWER_BLOCK = ModBlocks.BLOCK_STEEL.get();
public BlockPos towerBasePos;
public @Nullable PadInfo getPlatform() {
// TODO // TODO
int y = this.padScanStart.getY(); int y = this.padScanStart.getY();
BlockPos start = this.padScanStart; BlockPos start = this.padScanStart;
@@ -67,7 +98,107 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB
} }
} }
return new PadInfo(new BlockPos(minX, y, minZ), new BlockPos(maxX, y, maxZ), width); return new PadInfo(new BlockPos(minX, y, minZ), new BlockPos(maxX, y, maxZ));
}
private boolean connected(BlockState state, Direction dir) {
return switch (dir) {
case NORTH -> state.getValue(BlockStateProperties.NORTH);
case SOUTH -> state.getValue(BlockStateProperties.SOUTH);
case EAST -> state.getValue(BlockStateProperties.EAST);
case WEST -> state.getValue(BlockStateProperties.WEST);
default -> false;
};
}
public @Nullable PadInfo getPlatformFill() {
if (level == null) return null;
BlockPos start = this.padScanStart;
if (!isPad(level.getBlockState(start))) return null;
final int y = start.getY();
int towerHeight = 0;
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
LongOpenHashSet visited = new LongOpenHashSet();
queue.add(start);
visited.add(start.asLong());
final Direction[] CARDINALS = {Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST};
// When the $#$# are we going to have a rocket larger than 64x64... don't...
final int MAX_PAD_BLOCKS = 4096;
boolean towerFound = false;
while (!queue.isEmpty()) {
BlockPos p = queue.removeFirst();
BlockState s = level.getBlockState(p);
// We trust block states entirely. If a block state says it has a pad in a direction and is wrong, something else has failed. To minimize level checks and have this run as fast as possible, I won't compare the block
// if (!isPad(s)) continue;
for (Direction d : CARDINALS) {
// Only keep going if the block claims to have a neighbor in that direction
BlockPos n = p.relative(d);
if (!connected(s, d)) {
if (level.getBlockState(n).is(TOWER_BLOCK)) {
if (!towerFound) {
towerBasePos = n;
towerFound = true;
} else if (!n.equals(towerBasePos)) {
Aphelion.LOGGER.warn("Multiple towers found, rocket pad invalid");
return null;
}
}
continue;
}
if (visited.contains(n.asLong())) continue; // skip if we've already seen this block
visited.add(n.asLong());
queue.addLast(n);
if (visited.size() > MAX_PAD_BLOCKS) return null;
}
}
// Pads missing a tower should be invalid
if (!towerFound || towerBasePos == null) return null;
towerHeight = getTowerHeight(level, towerBasePos);
if (towerHeight <= 0) return null;
int minX = Integer.MAX_VALUE, maxX = Integer.MIN_VALUE;
int minZ = Integer.MAX_VALUE, maxZ = Integer.MIN_VALUE;
for (Long p : visited) {
minX = Math.min(minX, BlockPos.of(p).getX());
maxX = Math.max(maxX, BlockPos.of(p).getX());
minZ = Math.min(minZ, BlockPos.of(p).getZ());
maxZ = Math.max(maxZ, BlockPos.of(p).getZ());
}
int width = (maxX - minX) + 1;
int length = (maxZ - minZ) + 1;
// SQUARE AND SOLID??? The math works out here that this will only be true if the number of blocks found matches the side lengths squared. We don't need to check for holes manually!
if (visited.size() != length * width || length != width) return null;
return new PadInfo(new BlockPos(minX, y + 1, minZ), new BlockPos(maxX, y + towerHeight, maxZ));
}
private int getTowerHeight(@NotNull Level level, @NotNull BlockPos base) {
int h = 0;
BlockPos p = base.above();
while (level.getBlockState(p).is(TOWER_BLOCK)) {
h++;
p = p.above();
}
return h;
} }
public RocketStructure scan() { public RocketStructure scan() {
@@ -82,7 +213,13 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB
@Override @Override
public void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos) { public void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos) {
PadInfo newBounds = getPlatformFill();
setPadBoundsAndSync(newBounds);
boolean formed = newBounds != null;
if (state.getValue(AphelionBlockStateProperties.FORMED) != formed) {
level.setBlockAndUpdate(pos, state.setValue(AphelionBlockStateProperties.FORMED, formed));
}
} }
@Override @Override
@@ -96,4 +233,67 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB
private static boolean isPad(BlockState s) { private static boolean isPad(BlockState s) {
return s.is(ModTags.Blocks.LAUNCH_PAD); // or s.getBlock() == ModBlocks.PAD.get() return s.is(ModTags.Blocks.LAUNCH_PAD); // or s.getBlock() == ModBlocks.PAD.get()
} }
@Override
protected void saveAdditional(@NotNull CompoundTag tag, HolderLookup.@NotNull Provider registries) {
super.saveAdditional(tag, registries);
PadInfo pad = this.padBounds;
if (pad != null) {
tag.putLong("PadMin", padBounds.min().asLong());
tag.putLong("PadMax", padBounds.max().asLong());
}
}
@Override
protected void loadAdditional(@NotNull CompoundTag tag, HolderLookup.@NotNull Provider registries) {
super.loadAdditional(tag, registries);
if (tag.contains("PadMin") && tag.contains("PadMax")) {
BlockPos min = BlockPos.of(tag.getLong("PadMin"));
BlockPos max = BlockPos.of(tag.getLong("PadMax"));
this.padBounds = new PadInfo(min, max);
} else {
this.padBounds = null;
}
}
@Override
public @NotNull CompoundTag getUpdateTag(HolderLookup.@NotNull Provider registries) {
CompoundTag tag = super.getUpdateTag(registries);
saveAdditional(tag, registries);
return tag;
}
@Override
public void handleUpdateTag(@NotNull CompoundTag tag, HolderLookup.@NotNull Provider registries) {
loadAdditional(tag, registries);
}
private void setPadBoundsAndSync(@Nullable PadInfo newBounds) {
if (java.util.Objects.equals(this.padBounds, newBounds)) return;
this.padBounds = newBounds;
setChanged(); // marks BE dirty for saving
if (level instanceof ServerLevel server) {
BlockState state = getBlockState();
server.sendBlockUpdated(worldPosition, state, state, 3);
}
}
@Override
public Packet<ClientGamePacketListener> getUpdatePacket() {
// sends the tag from getUpdateTag()
return ClientboundBlockEntityDataPacket.create(this);
}
@Override
public void onDataPacket(@NotNull Connection net, ClientboundBlockEntityDataPacket pkt, HolderLookup.@NotNull Provider registries) {
// apply the received tag on client
CompoundTag tag = pkt.getTag();
if (tag != null) {
this.loadAdditional(tag, registries);
}
}
} }

View File

@@ -0,0 +1,44 @@
package net.xevianlight.aphelion.block.entity.custom.renderer;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.core.BlockPos;
import net.minecraft.world.phys.AABB;
import net.xevianlight.aphelion.block.entity.custom.RocketAssemblerBlockEntity;
import org.jetbrains.annotations.NotNull;
public class RocketAssemblerBlockEntityRenderer implements BlockEntityRenderer<RocketAssemblerBlockEntity> {
public RocketAssemblerBlockEntityRenderer (BlockEntityRendererProvider.Context context) {
}
@Override
public void render(@NotNull RocketAssemblerBlockEntity be, float v, @NotNull PoseStack poseStack, @NotNull MultiBufferSource multiBufferSource, int i, int i1) {
if (!Minecraft.getInstance().gui.getDebugOverlay().showDebugScreen()) return;
if (be.getPadBounds() == null) return;
BlockPos min = be.getPadBounds().min();
BlockPos max = be.getPadBounds().max();
AABB box = new AABB(
min.getX(), min.getY(), min.getZ(),
max.getX() + 1, max.getY() + 1, max.getZ() + 1
);
poseStack.pushPose();
poseStack.translate(-be.getBlockPos().getX(), -be.getBlockPos().getY(), -be.getBlockPos().getZ());
VertexConsumer vc = multiBufferSource.getBuffer(RenderType.lines());
LevelRenderer.renderLineBox(poseStack, vc, box, 0.0f, 1.0f, 0.0f, 1.0f);
poseStack.popPose();
}
}

View File

@@ -25,10 +25,10 @@ public class ModBlockStateProvider extends BlockStateProvider {
blockItem(ModBlocks.ELECTRIC_ARC_FURNACE); blockItem(ModBlocks.ELECTRIC_ARC_FURNACE);
blockItem(ModBlocks.VACUUM_ARC_FURNACE_CONTROLLER); blockItem(ModBlocks.VACUUM_ARC_FURNACE_CONTROLLER);
horizontalBlock(ModBlocks.ROCKET_ASSEMBLER_BLOCK.get(), models().orientable("aphelion:rocket_assembler_block", // horizontalBlock(ModBlocks.ROCKET_ASSEMBLER_BLOCK.get(), models().orientable("aphelion:rocket_assembler_block",
modLoc("block/test_block"), // modLoc("block/test_block"),
mcLoc("block/furnace_front"), // mcLoc("block/furnace_front"),
modLoc("block/test_block"))); // modLoc("block/test_block")));
blockItem(ModBlocks.ROCKET_ASSEMBLER_BLOCK); blockItem(ModBlocks.ROCKET_ASSEMBLER_BLOCK);
blockWithItem(ModBlocks.BLOCK_STEEL); blockWithItem(ModBlocks.BLOCK_STEEL);

View File

@@ -0,0 +1,34 @@
{
"variants": {
"facing=east,formed=false": {
"model": "aphelion:block/rocket_assembler_block",
"y": 90
},
"facing=north,formed=false": {
"model": "aphelion:block/rocket_assembler_block"
},
"facing=south,formed=false": {
"model": "aphelion:block/rocket_assembler_block",
"y": 180
},
"facing=west,formed=false": {
"model": "aphelion:block/rocket_assembler_block",
"y": 270
},
"facing=east,formed=true": {
"model": "aphelion:block/rocket_assembler_block_formed",
"y": 90
},
"facing=north,formed=true": {
"model": "aphelion:block/rocket_assembler_block_formed"
},
"facing=south,formed=true": {
"model": "aphelion:block/rocket_assembler_block_formed",
"y": 180
},
"facing=west,formed=true": {
"model": "aphelion:block/rocket_assembler_block_formed",
"y": 270
}
}
}

View File

@@ -0,0 +1,8 @@
{
"parent": "minecraft:block/orientable",
"textures": {
"front": "minecraft:block/furnace_front",
"side": "aphelion:block/test_block",
"top": "aphelion:block/test_block"
}
}

View File

@@ -0,0 +1,8 @@
{
"parent": "minecraft:block/orientable",
"textures": {
"front": "minecraft:block/furnace_front",
"side": "aphelion:block/arc_furnace_casing_formed",
"top": "aphelion:block/arc_furnace_casing_formed"
}
}