package net.minecraft.world.level.block; import com.mojang.serialization.MapCodec; import it.unimi.dsi.fastutil.floats.Float2FloatFunction; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BiPredicate; import java.util.function.Supplier; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.stats.Stat; import net.minecraft.stats.Stats; import net.minecraft.util.RandomSource; import net.minecraft.world.CompoundContainer; import net.minecraft.world.Container; import net.minecraft.world.Containers; import net.minecraft.world.InteractionResult; import net.minecraft.world.MenuProvider; import net.minecraft.world.entity.animal.Cat; import net.minecraft.world.entity.monster.piglin.PiglinAi; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; 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.LevelReader; import net.minecraft.world.level.ScheduledTickAccess; import net.minecraft.world.level.block.ChestBlock.2.1; import net.minecraft.world.level.block.DoubleBlockCombiner.BlockType; import net.minecraft.world.level.block.DoubleBlockCombiner.Combiner; import net.minecraft.world.level.block.DoubleBlockCombiner.NeighborCombineResult; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityTicker; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.entity.ChestBlockEntity; import net.minecraft.world.level.block.entity.LidBlockEntity; import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition.Builder; 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.ChestType; import net.minecraft.world.level.block.state.properties.EnumProperty; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.material.Fluids; import net.minecraft.world.level.pathfinder.PathComputationType; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import org.jetbrains.annotations.Nullable; public class ChestBlock extends AbstractChestBlock implements SimpleWaterloggedBlock { public static final MapCodec CODEC = simpleCodec(properties -> new ChestBlock(() -> BlockEntityType.CHEST, properties)); public static final EnumProperty FACING = HorizontalDirectionalBlock.FACING; public static final EnumProperty TYPE = BlockStateProperties.CHEST_TYPE; public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED; public static final int EVENT_SET_OPEN_COUNT = 1; private static final VoxelShape SHAPE = Block.column(14.0, 0.0, 14.0); private static final Map HALF_SHAPES = Shapes.rotateHorizontal(Block.boxZ(14.0, 0.0, 14.0, 0.0, 15.0)); private static final Combiner> CHEST_COMBINER = new Combiner>() { public Optional acceptDouble(ChestBlockEntity chestBlockEntity, ChestBlockEntity chestBlockEntity2) { return Optional.of(new CompoundContainer(chestBlockEntity, chestBlockEntity2)); } public Optional acceptSingle(ChestBlockEntity chestBlockEntity) { return Optional.of(chestBlockEntity); } public Optional acceptNone() { return Optional.empty(); } }; private static final Combiner> MENU_PROVIDER_COMBINER = new Combiner>() { public Optional acceptDouble(ChestBlockEntity chestBlockEntity, ChestBlockEntity chestBlockEntity2) { Container container = new CompoundContainer(chestBlockEntity, chestBlockEntity2); return Optional.of(new 1(this, chestBlockEntity, chestBlockEntity2, container)); } public Optional acceptSingle(ChestBlockEntity chestBlockEntity) { return Optional.of(chestBlockEntity); } public Optional acceptNone() { return Optional.empty(); } }; @Override public MapCodec codec() { return CODEC; } protected ChestBlock(Supplier> blockEntityType, BlockBehaviour.Properties properties) { super(properties, blockEntityType); this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(TYPE, ChestType.SINGLE).setValue(WATERLOGGED, false)); } public static BlockType getBlockType(BlockState state) { ChestType chestType = state.getValue(TYPE); if (chestType == ChestType.SINGLE) { return BlockType.SINGLE; } else { return chestType == ChestType.RIGHT ? BlockType.FIRST : BlockType.SECOND; } } @Override protected BlockState updateShape( BlockState state, LevelReader level, ScheduledTickAccess scheduledTickAccess, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, RandomSource random ) { if ((Boolean)state.getValue(WATERLOGGED)) { scheduledTickAccess.scheduleTick(pos, Fluids.WATER, Fluids.WATER.getTickDelay(level)); } if (neighborState.is(this) && direction.getAxis().isHorizontal()) { ChestType chestType = neighborState.getValue(TYPE); if (state.getValue(TYPE) == ChestType.SINGLE && chestType != ChestType.SINGLE && state.getValue(FACING) == neighborState.getValue(FACING) && getConnectedDirection(neighborState) == direction.getOpposite()) { return state.setValue(TYPE, chestType.getOpposite()); } } else if (getConnectedDirection(state) == direction) { return state.setValue(TYPE, ChestType.SINGLE); } return super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random); } @Override protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { return switch ((ChestType)state.getValue(TYPE)) { case SINGLE -> SHAPE; case LEFT, RIGHT -> (VoxelShape)HALF_SHAPES.get(getConnectedDirection(state)); }; } /** * @return the Direction pointing from the given state to its attached double chest */ public static Direction getConnectedDirection(BlockState state) { Direction direction = state.getValue(FACING); return state.getValue(TYPE) == ChestType.LEFT ? direction.getClockWise() : direction.getCounterClockWise(); } @Override public BlockState getStateForPlacement(BlockPlaceContext context) { ChestType chestType = ChestType.SINGLE; Direction direction = context.getHorizontalDirection().getOpposite(); FluidState fluidState = context.getLevel().getFluidState(context.getClickedPos()); boolean bl = context.isSecondaryUseActive(); Direction direction2 = context.getClickedFace(); if (direction2.getAxis().isHorizontal() && bl) { Direction direction3 = this.candidatePartnerFacing(context, direction2.getOpposite()); if (direction3 != null && direction3.getAxis() != direction2.getAxis()) { direction = direction3; chestType = direction3.getCounterClockWise() == direction2.getOpposite() ? ChestType.RIGHT : ChestType.LEFT; } } if (chestType == ChestType.SINGLE && !bl) { if (direction == this.candidatePartnerFacing(context, direction.getClockWise())) { chestType = ChestType.LEFT; } else if (direction == this.candidatePartnerFacing(context, direction.getCounterClockWise())) { chestType = ChestType.RIGHT; } } return this.defaultBlockState().setValue(FACING, direction).setValue(TYPE, chestType).setValue(WATERLOGGED, fluidState.getType() == Fluids.WATER); } @Override protected FluidState getFluidState(BlockState state) { return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state); } @Nullable private Direction candidatePartnerFacing(BlockPlaceContext context, Direction direction) { BlockState blockState = context.getLevel().getBlockState(context.getClickedPos().relative(direction)); return blockState.is(this) && blockState.getValue(TYPE) == ChestType.SINGLE ? blockState.getValue(FACING) : null; } @Override protected void affectNeighborsAfterRemoval(BlockState state, ServerLevel level, BlockPos pos, boolean movedByPiston) { Containers.updateNeighboursAfterDestroy(state, level, pos); } @Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) { if (level instanceof ServerLevel serverLevel) { MenuProvider menuProvider = this.getMenuProvider(state, level, pos); if (menuProvider != null) { player.openMenu(menuProvider); player.awardStat(this.getOpenChestStat()); PiglinAi.angerNearbyPiglins(serverLevel, player, true); } } return InteractionResult.SUCCESS; } protected Stat getOpenChestStat() { return Stats.CUSTOM.get(Stats.OPEN_CHEST); } public BlockEntityType blockEntityType() { return (BlockEntityType)this.blockEntityType.get(); } @Nullable public static Container getContainer(ChestBlock chest, BlockState state, Level level, BlockPos pos, boolean override) { return (Container)chest.combine(state, level, pos, override).apply(CHEST_COMBINER).orElse(null); } @Override public NeighborCombineResult combine(BlockState state, Level level, BlockPos pos, boolean override) { BiPredicate biPredicate; if (override) { biPredicate = (levelAccessor, blockPos) -> false; } else { biPredicate = ChestBlock::isChestBlockedAt; } return DoubleBlockCombiner.combineWithNeigbour( (BlockEntityType)this.blockEntityType.get(), ChestBlock::getBlockType, ChestBlock::getConnectedDirection, FACING, state, level, pos, biPredicate ); } @Nullable @Override protected MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) { return (MenuProvider)this.combine(state, level, pos, false).apply(MENU_PROVIDER_COMBINER).orElse(null); } public static Combiner opennessCombiner(LidBlockEntity lid) { return new Combiner() { public Float2FloatFunction acceptDouble(ChestBlockEntity chestBlockEntity, ChestBlockEntity chestBlockEntity2) { return f -> Math.max(chestBlockEntity.getOpenNess(f), chestBlockEntity2.getOpenNess(f)); } public Float2FloatFunction acceptSingle(ChestBlockEntity chestBlockEntity) { return chestBlockEntity::getOpenNess; } public Float2FloatFunction acceptNone() { return lid::getOpenNess; } }; } @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new ChestBlockEntity(pos, state); } @Nullable @Override public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType blockEntityType) { return level.isClientSide ? createTickerHelper(blockEntityType, this.blockEntityType(), ChestBlockEntity::lidAnimateTick) : null; } public static boolean isChestBlockedAt(LevelAccessor level, BlockPos pos) { return isBlockedChestByBlock(level, pos) || isCatSittingOnChest(level, pos); } private static boolean isBlockedChestByBlock(BlockGetter level, BlockPos pos) { BlockPos blockPos = pos.above(); return level.getBlockState(blockPos).isRedstoneConductor(level, blockPos); } private static boolean isCatSittingOnChest(LevelAccessor level, BlockPos pos) { List list = level.getEntitiesOfClass(Cat.class, new AABB(pos.getX(), pos.getY() + 1, pos.getZ(), pos.getX() + 1, pos.getY() + 2, pos.getZ() + 1)); if (!list.isEmpty()) { for (Cat cat : list) { if (cat.isInSittingPose()) { return true; } } } return false; } @Override protected boolean hasAnalogOutputSignal(BlockState state) { return true; } @Override protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos) { return AbstractContainerMenu.getRedstoneSignalFromContainer(getContainer(this, state, level, pos, false)); } @Override protected BlockState rotate(BlockState state, Rotation rotation) { return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); } @Override protected BlockState mirror(BlockState state, Mirror mirror) { return state.rotate(mirror.getRotation(state.getValue(FACING))); } @Override protected void createBlockStateDefinition(Builder builder) { builder.add(FACING, TYPE, WATERLOGGED); } @Override protected boolean isPathfindable(BlockState state, PathComputationType pathComputationType) { return false; } @Override protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { BlockEntity blockEntity = level.getBlockEntity(pos); if (blockEntity instanceof ChestBlockEntity) { ((ChestBlockEntity)blockEntity).recheckOpen(); } } }