diff --git a/src/generated/resources/assets/aphelion/models/item/rocket_assembler_block.json b/src/generated/resources/assets/aphelion/models/item/rocket_assembler_block.json new file mode 100644 index 0000000..247366e --- /dev/null +++ b/src/generated/resources/assets/aphelion/models/item/rocket_assembler_block.json @@ -0,0 +1,3 @@ +{ + "parent": "aphelion:block/rocket_assembler_block" +} \ No newline at end of file diff --git a/src/generated/resources/data/aphelion/loot_table/blocks/rocket_assembler_block.json b/src/generated/resources/data/aphelion/loot_table/blocks/rocket_assembler_block.json new file mode 100644 index 0000000..c37829f --- /dev/null +++ b/src/generated/resources/data/aphelion/loot_table/blocks/rocket_assembler_block.json @@ -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" +} \ No newline at end of file diff --git a/src/main/java/net/xevianlight/aphelion/Aphelion.java b/src/main/java/net/xevianlight/aphelion/Aphelion.java index 4101621..7cf27cd 100644 --- a/src/main/java/net/xevianlight/aphelion/Aphelion.java +++ b/src/main/java/net/xevianlight/aphelion/Aphelion.java @@ -15,6 +15,7 @@ import net.neoforged.neoforge.event.tick.ServerTickEvent; import net.neoforged.neoforge.network.PacketDistributor; 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.RocketAssemblerBlockEntityRenderer; import net.xevianlight.aphelion.client.AphelionConfig; import net.xevianlight.aphelion.core.saveddata.EnvironmentSavedData; import net.xevianlight.aphelion.network.packet.PartitionPayload; @@ -139,6 +140,7 @@ public class Aphelion { @SubscribeEvent public static void registerBER(EntityRenderersEvent.RegisterRenderers event) { 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); } diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/RocketAssemblerBlock.java b/src/main/java/net/xevianlight/aphelion/block/custom/RocketAssemblerBlock.java index 3a68912..2fe72fc 100644 --- a/src/main/java/net/xevianlight/aphelion/block/custom/RocketAssemblerBlock.java +++ b/src/main/java/net/xevianlight/aphelion/block/custom/RocketAssemblerBlock.java @@ -2,21 +2,22 @@ package net.xevianlight.aphelion.block.custom; import com.mojang.serialization.MapCodec; import net.minecraft.core.BlockPos; -import net.minecraft.world.item.Item; 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.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; -import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.xevianlight.aphelion.block.custom.base.BasicHorizontalEntityBlock; import net.xevianlight.aphelion.block.entity.custom.RocketAssemblerBlockEntity; +import net.xevianlight.aphelion.util.AphelionBlockStateProperties; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class RocketAssemblerBlock extends BasicHorizontalEntityBlock { + public static final BooleanProperty FORMED = AphelionBlockStateProperties.FORMED; + public RocketAssemblerBlock(Properties properties) { super(properties, true); } @@ -24,7 +25,7 @@ public class RocketAssemblerBlock extends BasicHorizontalEntityBlock { public static final MapCodec CODEC = simpleCodec(RocketAssemblerBlock::new); @Override - protected MapCodec codec() { + protected @NotNull MapCodec codec() { return CODEC; } @@ -38,7 +39,18 @@ public class RocketAssemblerBlock extends BasicHorizontalEntityBlock { } @Override - public @Nullable BlockEntity newBlockEntity(BlockPos blockPos, BlockState blockState) { + public @Nullable BlockEntity newBlockEntity(@NotNull BlockPos blockPos, @NotNull BlockState 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 builder) { + builder.add(FORMED); + super.createBlockStateDefinition(builder); + } } diff --git a/src/main/java/net/xevianlight/aphelion/block/custom/base/TickableBlockEntity.java b/src/main/java/net/xevianlight/aphelion/block/custom/base/TickableBlockEntity.java index 74983ff..ae86af3 100644 --- a/src/main/java/net/xevianlight/aphelion/block/custom/base/TickableBlockEntity.java +++ b/src/main/java/net/xevianlight/aphelion/block/custom/base/TickableBlockEntity.java @@ -8,14 +8,43 @@ import net.minecraft.world.level.block.state.BlockState; 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); + /** + * Runs on the server only + * @param level + * @param time + * @param state + * @param pos + */ void serverTick(ServerLevel level, long time, BlockState state, BlockPos pos); default boolean isInitialized() { return true; }; + /** + * Runs on client AND server, once only. + * @param level + * @param state + * @param pos + */ void firstTick(Level level, BlockState state, BlockPos pos); default void onRemoved() {} diff --git a/src/main/java/net/xevianlight/aphelion/block/entity/custom/RocketAssemblerBlockEntity.java b/src/main/java/net/xevianlight/aphelion/block/entity/custom/RocketAssemblerBlockEntity.java index 67319a4..5693492 100644 --- a/src/main/java/net/xevianlight/aphelion/block/entity/custom/RocketAssemblerBlockEntity.java +++ b/src/main/java/net/xevianlight/aphelion/block/entity/custom/RocketAssemblerBlockEntity.java @@ -1,23 +1,43 @@ package net.xevianlight.aphelion.block.entity.custom; +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.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.world.level.Level; +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.properties.BlockStateProperties; +import net.xevianlight.aphelion.Aphelion; 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.AphelionBlockStateProperties; import net.xevianlight.aphelion.util.ModTags; import net.xevianlight.aphelion.util.RocketStructure; 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 { Direction facing; BlockPos padScanStart = BlockPos.ZERO; + private PadInfo padBounds; + + public @Nullable PadInfo getPadBounds() { + return padBounds; + } public boolean isInitialized; @Override @@ -29,9 +49,20 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB 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 int y = this.padScanStart.getY(); 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 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() { @@ -82,7 +213,13 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB @Override 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 @@ -96,4 +233,67 @@ public class RocketAssemblerBlockEntity extends BlockEntity implements TickableB private static boolean isPad(BlockState s) { 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 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); + } + } } diff --git a/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/RocketAssemblerBlockEntityRenderer.java b/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/RocketAssemblerBlockEntityRenderer.java new file mode 100644 index 0000000..1469525 --- /dev/null +++ b/src/main/java/net/xevianlight/aphelion/block/entity/custom/renderer/RocketAssemblerBlockEntityRenderer.java @@ -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 { + + 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(); + } +} diff --git a/src/main/java/net/xevianlight/aphelion/datagen/ModBlockStateProvider.java b/src/main/java/net/xevianlight/aphelion/datagen/ModBlockStateProvider.java index 25480d5..a062883 100644 --- a/src/main/java/net/xevianlight/aphelion/datagen/ModBlockStateProvider.java +++ b/src/main/java/net/xevianlight/aphelion/datagen/ModBlockStateProvider.java @@ -25,10 +25,10 @@ public class ModBlockStateProvider extends BlockStateProvider { blockItem(ModBlocks.ELECTRIC_ARC_FURNACE); blockItem(ModBlocks.VACUUM_ARC_FURNACE_CONTROLLER); - horizontalBlock(ModBlocks.ROCKET_ASSEMBLER_BLOCK.get(), models().orientable("aphelion:rocket_assembler_block", - modLoc("block/test_block"), - mcLoc("block/furnace_front"), - modLoc("block/test_block"))); +// horizontalBlock(ModBlocks.ROCKET_ASSEMBLER_BLOCK.get(), models().orientable("aphelion:rocket_assembler_block", +// modLoc("block/test_block"), +// mcLoc("block/furnace_front"), +// modLoc("block/test_block"))); blockItem(ModBlocks.ROCKET_ASSEMBLER_BLOCK); blockWithItem(ModBlocks.BLOCK_STEEL); diff --git a/src/main/resources/assets/aphelion/blockstates/rocket_assembler_block.json b/src/main/resources/assets/aphelion/blockstates/rocket_assembler_block.json new file mode 100644 index 0000000..3f6c25d --- /dev/null +++ b/src/main/resources/assets/aphelion/blockstates/rocket_assembler_block.json @@ -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 + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/aphelion/models/block/rocket_assembler_block.json b/src/main/resources/assets/aphelion/models/block/rocket_assembler_block.json new file mode 100644 index 0000000..919f908 --- /dev/null +++ b/src/main/resources/assets/aphelion/models/block/rocket_assembler_block.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/orientable", + "textures": { + "front": "minecraft:block/furnace_front", + "side": "aphelion:block/test_block", + "top": "aphelion:block/test_block" + } +} \ No newline at end of file diff --git a/src/main/resources/assets/aphelion/models/block/rocket_assembler_block_formed.json b/src/main/resources/assets/aphelion/models/block/rocket_assembler_block_formed.json new file mode 100644 index 0000000..b998b20 --- /dev/null +++ b/src/main/resources/assets/aphelion/models/block/rocket_assembler_block_formed.json @@ -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" + } +} \ No newline at end of file