From 4edde4d479bb941a0491b72d5bb777b062cde767 Mon Sep 17 00:00:00 2001 From: Sollace Date: Fri, 16 Feb 2024 16:42:48 +0000 Subject: [PATCH] Added a new feature to hive blocks --- .../unicopia/block/BlockEntityUtil.java | 39 +++ .../unicopia/block/HiveBlock.java | 260 +++++++++++++++++- .../unicopia/block/UBlockEntities.java | 1 + 3 files changed, 296 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/minelittlepony/unicopia/block/BlockEntityUtil.java diff --git a/src/main/java/com/minelittlepony/unicopia/block/BlockEntityUtil.java b/src/main/java/com/minelittlepony/unicopia/block/BlockEntityUtil.java new file mode 100644 index 00000000..5337dbdc --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/block/BlockEntityUtil.java @@ -0,0 +1,39 @@ +package com.minelittlepony.unicopia.block; + +import java.util.Optional; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.block.BlockEntityProvider; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityTicker; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +public interface BlockEntityUtil { + @SuppressWarnings("unchecked") + @Nullable + static BlockEntityTicker checkType(BlockEntityType givenType, BlockEntityType expectedType, BlockEntityTicker ticker) { + return expectedType == givenType ? (@Nullable BlockEntityTicker) ticker : null; + } + + @SuppressWarnings({ "unchecked", "deprecation" }) + static Optional getOrCreateBlockEntity(World world, BlockPos pos, BlockEntityType type) { + return world.getBlockEntity(pos, type).or(() -> { + BlockState state = world.getBlockState(pos); + if (!(state.hasBlockEntity())) { + return Optional.empty(); + } + BlockEntity e = ((BlockEntityProvider)state.getBlock()).createBlockEntity(pos, state); + if (e == null || e.getType() != type) { + return Optional.empty(); + } + e.setCachedState(state); + world.addBlockEntity(e); + return Optional.of((T)e); + }); + } + +} diff --git a/src/main/java/com/minelittlepony/unicopia/block/HiveBlock.java b/src/main/java/com/minelittlepony/unicopia/block/HiveBlock.java index 35b2f1af..8fc18c1c 100644 --- a/src/main/java/com/minelittlepony/unicopia/block/HiveBlock.java +++ b/src/main/java/com/minelittlepony/unicopia/block/HiveBlock.java @@ -1,40 +1,69 @@ package com.minelittlepony.unicopia.block; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; import com.minelittlepony.unicopia.EquineContext; +import com.minelittlepony.unicopia.EquinePredicates; import com.minelittlepony.unicopia.Race; import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.entity.player.Pony; import com.minelittlepony.unicopia.particle.ParticleUtils; +import com.minelittlepony.unicopia.util.NbtSerialisable; +import com.minelittlepony.unicopia.util.NbtSerialisable.Serializer; +import com.minelittlepony.unicopia.util.PosHelper; import net.minecraft.block.Block; +import net.minecraft.block.BlockEntityProvider; import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; import net.minecraft.block.ConnectingBlock; import net.minecraft.block.ShapeContext; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityTicker; +import net.minecraft.block.entity.BlockEntityType; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; import net.minecraft.particle.ParticleTypes; import net.minecraft.server.world.ServerWorld; import net.minecraft.sound.SoundCategory; import net.minecraft.state.StateManager; import net.minecraft.state.property.BooleanProperty; import net.minecraft.state.property.Property; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.random.Random; import net.minecraft.util.shape.VoxelShape; import net.minecraft.util.shape.VoxelShapes; import net.minecraft.world.BlockView; import net.minecraft.world.World; import net.minecraft.world.WorldAccess; +import net.minecraft.world.event.BlockPositionSource; +import net.minecraft.world.event.GameEvent; +import net.minecraft.world.event.GameEvent.Emitter; +import net.minecraft.world.event.PositionSource; +import net.minecraft.world.event.listener.GameEventListener; -public class HiveBlock extends ConnectingBlock { +public class HiveBlock extends ConnectingBlock implements BlockEntityProvider { static final BooleanProperty AWAKE = BooleanProperty.of("awake"); + static final BooleanProperty CONSUMING = BooleanProperty.of("consuming"); static final Collection PROPERTIES = FACING_PROPERTIES.values(); public HiveBlock(Settings settings) { super(0.5F, settings); - setDefaultState(getDefaultState().with(AWAKE, false)); + setDefaultState(getDefaultState().with(AWAKE, false).with(CONSUMING, false)); PROPERTIES.forEach(property -> { setDefaultState(getDefaultState().with(property, true)); }); @@ -43,7 +72,7 @@ public class HiveBlock extends ConnectingBlock { @Override protected void appendProperties(StateManager.Builder builder) { builder.add(PROPERTIES.toArray(Property[]::new)); - builder.add(AWAKE); + builder.add(AWAKE, CONSUMING); } @Override @@ -66,6 +95,9 @@ public class HiveBlock extends ConnectingBlock { @Deprecated @Override public BlockState getStateForNeighborUpdate(BlockState state, Direction direction, BlockState neighborState, WorldAccess world, BlockPos pos, BlockPos neighborPos) { + if (state.get(CONSUMING)) { + return state; + } boolean connected = !neighborState.isAir(); state = state.with(FACING_PROPERTIES.get(direction), connected); @@ -78,7 +110,7 @@ public class HiveBlock extends ConnectingBlock { @Override public void scheduledTick(BlockState state, ServerWorld world, BlockPos pos, Random random) { - if (!state.get(AWAKE)) { + if (state.get(CONSUMING) || !state.get(AWAKE)) { return; } @@ -102,6 +134,29 @@ public class HiveBlock extends ConnectingBlock { } } + @Override + public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { + if (EquineContext.of(player).getCompositeRace().includes(Race.CHANGELING)) { + world.setBlockState(pos, state.with(CONSUMING, true)); + if (!world.isClient) { + BlockEntityUtil.getOrCreateBlockEntity(world, pos, UBlockEntities.HIVE_STORAGE).ifPresent(data -> { + + if (data.opening) { + data.opening = false; + data.closing = true; + } else { + data.opening = true; + data.closing = false; + } + data.tickNow = true; + data.markDirty(); + }); + } + return ActionResult.SUCCESS; + } + return ActionResult.PASS; + } + @Deprecated @Override public void neighborUpdate(BlockState state, World world, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { @@ -122,4 +177,201 @@ public class HiveBlock extends ConnectingBlock { delta *= Pony.of(player).getSpecies() == Race.CHANGELING ? 2 : 1; return delta; } + + @Nullable + @Override + public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { + return state.get(CONSUMING) ? new TileData(pos, state) : null; + } + + @Override + public BlockEntityTicker getTicker(World world, BlockState state, BlockEntityType type) { + if (!state.get(CONSUMING)) { + return null; + } + return BlockEntityUtil.checkType(type, UBlockEntities.HIVE_STORAGE, TileData::tick); + } + + static class TileData extends BlockEntity implements GameEventListener.Holder { + private final Map storedBlocks = new HashMap<>(); + private final List> lastConsumed = new ArrayList<>(); + private boolean opening; + private boolean closing; + private boolean tickNow; + private long lastTick; + private final Listener listener; + + public TileData(BlockPos pos, BlockState state) { + super(UBlockEntities.HIVE_STORAGE, pos, state); + listener = new Listener(pos); + } + + @Override + public void readNbt(NbtCompound nbt) { + opening = nbt.getBoolean("opening"); + closing = nbt.getBoolean("closing"); + storedBlocks.clear(); + if (nbt.contains("storedBlocks", NbtElement.LIST_TYPE)) { + Entry.SERIALIZER.readAll(nbt.getList("storedBlocks", NbtElement.COMPOUND_TYPE)).forEach(entry -> { + storedBlocks.put(entry.pos(), entry); + }); + } + } + + @Override + protected void writeNbt(NbtCompound nbt) { + nbt.putBoolean("opening", opening); + nbt.putBoolean("closing", closing); + nbt.put("storedBlocks", Entry.SERIALIZER.writeAll(storedBlocks.values())); + } + + static void tick(World world, BlockPos pos, BlockState state, TileData data) { + if (data.tickNow || (world.getTime() > data.lastTick + 2)) { + data.tickNow = false; + data.lastTick = world.getTime(); + if (data.closing) { + data.stepClosing(world, pos); + + if (data.lastConsumed.isEmpty() && state.get(CONSUMING)) { + world.setBlockState(pos, state.with(CONSUMING, false)); + } + } else if (data.opening) { + data.stepOpening(world, pos); + } + } + } + + private void stepOpening(World world, BlockPos pos) { + if (lastConsumed.size() >= 4) { + return; + } + + Set consumed = new HashSet<>(); + for (BlockPos neighbor : lastConsumed.isEmpty() ? Set.of(pos) : lastConsumed.get(lastConsumed.size() - 1)) { + if (!(neighbor.equals(pos) || (storedBlocks.containsKey(neighbor) && storedBlocks.get(neighbor).state().isOf(UBlocks.CHITIN)))) { + continue; + } + PosHelper.adjacentNeighbours(neighbor).forEach(adjacent -> { + BlockState s = world.getBlockState(adjacent); + + if (consumed.add(adjacent.toImmutable()) && !storedBlocks.containsKey(adjacent)) { + BlockEntity data = world.getBlockEntity(adjacent); + storedBlocks.put(adjacent.toImmutable(), new Entry(adjacent.toImmutable(), s, data instanceof TileData ? null : data)); + + if (s.isOf(UBlocks.CHITIN)) { + world.breakBlock(adjacent, false); + } else if (s.isOf(UBlocks.HIVE)) { + world.setBlockState(adjacent, s.with(CONSUMING, true)); + TileData next = BlockEntityUtil.getOrCreateBlockEntity(world, adjacent.toImmutable(), UBlockEntities.HIVE_STORAGE).orElse(null); + if (next != null) { + next.opening = opening; + next.closing = closing; + next.tickNow = true; + next.markDirty(); + } + } + } + }); + } + lastConsumed.add(consumed); + markDirty(); + } + + private void stepClosing(World world, BlockPos pos) { + if (lastConsumed.isEmpty()) { + closing = false; + markDirty(); + return; + } + + for (BlockPos neighbor : lastConsumed.remove(lastConsumed.size() - 1)) { + @Nullable + Entry entry = storedBlocks.remove(neighbor); + if (entry != null && !entry.state().isAir()) { + if (world.getBlockState(entry.pos()).isOf(UBlocks.HIVE)) { + BlockEntityUtil.getOrCreateBlockEntity(world, entry.pos(), UBlockEntities.HIVE_STORAGE).ifPresent(data -> { + data.closing = closing; + data.opening = opening; + data.tickNow = true; + data.markDirty(); + }); + } else { + entry.restore(world); + } + } + } + + markDirty(); + } + + record Entry (BlockPos pos, BlockState state, @Nullable BlockEntity data) { + public static final Serializer SERIALIZER = Serializer.of(compound -> new Entry( + NbtSerialisable.BLOCK_POS.read(compound.getCompound("pos")), + NbtSerialisable.decode(BlockState.CODEC, compound.get("state")).orElse(Blocks.AIR.getDefaultState()), + compound.getCompound("data") + ), entry -> { + NbtCompound compound = new NbtCompound(); + compound.put("pos", NbtSerialisable.BLOCK_POS.write(entry.pos())); + compound.put("state", NbtSerialisable.encode(BlockState.CODEC, entry.state())); + if (entry.data() != null) { + compound.put("data", entry.data().createNbtWithId()); + } + return compound; + }); + + Entry(BlockPos pos, BlockState state, NbtCompound nbt) { + this(pos, state, BlockEntity.createFromNbt(pos, state, nbt)); + } + + @SuppressWarnings("deprecation") + public void restore(World world) { + if (world.isAir(pos)) { + world.setBlockState(pos, state); + if (data != null) { + data.setCachedState(state); + world.addBlockEntity(data); + } + } + } + } + + @Override + public Listener getEventListener() { + return listener; + } + + class Listener implements GameEventListener { + private final PositionSource position; + + Listener(BlockPos pos) { + this.position = new BlockPositionSource(pos); + } + + @Override + public PositionSource getPositionSource() { + return position; + } + + @Override + public int getRange() { + return 15; + } + + @Override + public boolean listen(ServerWorld world, GameEvent event, Emitter emitter, Vec3d emitterPos) { + if (isImportant(event) || (EquinePredicates.IS_PLAYER.test(emitter.sourceEntity()) && !EquinePredicates.CHANGELING.test(emitter.sourceEntity()))) { + closing = true; + markDirty(); + return true; + } + return false; + } + + private boolean isImportant(GameEvent event) { + return event == GameEvent.EXPLODE + || event == GameEvent.ENTITY_ROAR + || event == GameEvent.SHRIEK; + } + } + } } diff --git a/src/main/java/com/minelittlepony/unicopia/block/UBlockEntities.java b/src/main/java/com/minelittlepony/unicopia/block/UBlockEntities.java index 570be63b..47a64397 100644 --- a/src/main/java/com/minelittlepony/unicopia/block/UBlockEntities.java +++ b/src/main/java/com/minelittlepony/unicopia/block/UBlockEntities.java @@ -14,6 +14,7 @@ public interface UBlockEntities { BlockEntityType WEATHER_VANE = create("weather_vane", BlockEntityType.Builder.create(WeatherVaneBlock.WeatherVane::new, UBlocks.WEATHER_VANE)); BlockEntityType FANCY_BED = create("fancy_bed", BlockEntityType.Builder.create(CloudBedBlock.Tile::new, UBlocks.CLOTH_BED, UBlocks.CLOUD_BED)); BlockEntityType CLOUD_CHEST = create("cloud_chest", BlockEntityType.Builder.create(CloudChestBlock.TileData::new, UBlocks.CLOUD_CHEST)); + BlockEntityType HIVE_STORAGE = create("hive_storage", BlockEntityType.Builder.create(HiveBlock.TileData::new, UBlocks.HIVE)); static BlockEntityType create(String id, Builder builder) { return Registry.register(Registries.BLOCK_ENTITY_TYPE, id, builder.build(null));