package com.simibubi.create.content.contraptions.components.structureMovement; import static com.simibubi.create.content.contraptions.components.structureMovement.piston.MechanicalPistonBlock.isExtensionPole; import static com.simibubi.create.content.contraptions.components.structureMovement.piston.MechanicalPistonBlock.isPistonHead; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.Queue; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; import javax.annotation.Nullable; import org.apache.commons.lang3.tuple.MutablePair; import org.apache.commons.lang3.tuple.Pair; import com.simibubi.create.AllBlockEntityTypes; import com.simibubi.create.AllBlocks; import com.simibubi.create.AllInteractionBehaviours; import com.simibubi.create.AllMovementBehaviours; import com.simibubi.create.content.contraptions.base.IRotate; import com.simibubi.create.content.contraptions.base.KineticBlockEntity; import com.simibubi.create.content.contraptions.components.actors.BlockBreakingMovementBehaviour; import com.simibubi.create.content.contraptions.components.actors.HarvesterMovementBehaviour; import com.simibubi.create.content.contraptions.components.actors.SeatBlock; import com.simibubi.create.content.contraptions.components.actors.SeatEntity; import com.simibubi.create.content.contraptions.components.actors.controls.ContraptionControlsMovement; import com.simibubi.create.content.contraptions.components.steam.PoweredShaftBlockEntity; import com.simibubi.create.content.contraptions.components.structureMovement.bearing.MechanicalBearingBlock; import com.simibubi.create.content.contraptions.components.structureMovement.bearing.StabilizedContraption; import com.simibubi.create.content.contraptions.components.structureMovement.bearing.WindmillBearingBlock; import com.simibubi.create.content.contraptions.components.structureMovement.bearing.WindmillBearingBlockEntity; import com.simibubi.create.content.contraptions.components.structureMovement.chassis.AbstractChassisBlock; import com.simibubi.create.content.contraptions.components.structureMovement.chassis.ChassisBlockEntity; import com.simibubi.create.content.contraptions.components.structureMovement.chassis.StickerBlock; import com.simibubi.create.content.contraptions.components.structureMovement.gantry.GantryCarriageBlock; import com.simibubi.create.content.contraptions.components.structureMovement.glue.SuperGlueEntity; import com.simibubi.create.content.contraptions.components.structureMovement.interaction.controls.ControlsBlock; import com.simibubi.create.content.contraptions.components.structureMovement.piston.MechanicalPistonBlock; import com.simibubi.create.content.contraptions.components.structureMovement.piston.MechanicalPistonBlock.PistonState; import com.simibubi.create.content.contraptions.components.structureMovement.piston.MechanicalPistonHeadBlock; import com.simibubi.create.content.contraptions.components.structureMovement.piston.PistonExtensionPoleBlock; import com.simibubi.create.content.contraptions.components.structureMovement.pulley.PulleyBlock; import com.simibubi.create.content.contraptions.components.structureMovement.pulley.PulleyBlock.MagnetBlock; import com.simibubi.create.content.contraptions.components.structureMovement.pulley.PulleyBlock.RopeBlock; import com.simibubi.create.content.contraptions.components.structureMovement.pulley.PulleyBlockEntity; import com.simibubi.create.content.contraptions.components.structureMovement.render.ContraptionLighter; import com.simibubi.create.content.contraptions.components.structureMovement.render.EmptyLighter; import com.simibubi.create.content.contraptions.fluids.tank.FluidTankBlockEntity; import com.simibubi.create.content.contraptions.relays.advanced.GantryShaftBlock; import com.simibubi.create.content.contraptions.relays.belt.BeltBlock; import com.simibubi.create.content.contraptions.relays.elementary.ShaftBlock; import com.simibubi.create.content.curiosities.deco.SlidingDoorBlock; import com.simibubi.create.content.logistics.block.inventories.CreativeCrateBlockEntity; import com.simibubi.create.content.logistics.block.redstone.RedstoneContactBlock; import com.simibubi.create.content.logistics.block.vault.ItemVaultBlockEntity; import com.simibubi.create.content.logistics.trains.AbstractBogeyBlock; import com.simibubi.create.foundation.blockEntity.IMultiBlockEntityContainer; import com.simibubi.create.foundation.blockEntity.behaviour.filtering.FilteringBehaviour; import com.simibubi.create.foundation.config.AllConfigs; import com.simibubi.create.foundation.utility.BBHelper; import com.simibubi.create.foundation.utility.BlockFace; import com.simibubi.create.foundation.utility.BlockHelper; import com.simibubi.create.foundation.utility.ICoordinate; import com.simibubi.create.foundation.utility.Iterate; import com.simibubi.create.foundation.utility.NBTHelper; import com.simibubi.create.foundation.utility.NBTProcessors; import com.simibubi.create.foundation.utility.UniqueLinkedList; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Direction.Axis; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.NbtUtils; import net.minecraft.nbt.Tag; import net.minecraft.network.protocol.game.DebugPackets; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ai.village.poi.PoiType; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.ButtonBlock; import net.minecraft.world.level.block.ChestBlock; import net.minecraft.world.level.block.DoorBlock; import net.minecraft.world.level.block.PressurePlateBlock; import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.SimpleWaterloggedBlock; 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.minecraft.world.level.block.state.properties.ChestType; import net.minecraft.world.level.block.state.properties.PistonType; import net.minecraft.world.level.chunk.HashMapPalette; import net.minecraft.world.level.levelgen.structure.BoundingBox; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate.StructureBlockInfo; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.material.Fluids; import net.minecraft.world.level.material.PushReaction; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.BooleanOp; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.client.model.data.IModelData; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.items.IItemHandlerModifiable; import net.minecraftforge.items.wrapper.CombinedInvWrapper; import net.minecraftforge.registries.GameData; public abstract class Contraption { public Optional> simplifiedEntityColliders; public AbstractContraptionEntity entity; public AABB bounds; public BlockPos anchor; public boolean stalled; public boolean hasUniversalCreativeCrate; public boolean disassembled; protected Map blocks; protected List> actors; protected Map interactors; protected List disabledActors; protected List superglue; protected List seats; protected Map seatMapping; protected Map stabilizedSubContraptions; protected MountedStorageManager storage; private Set glueToRemove; private Map initialPassengers; private List pendingSubContraptions; private CompletableFuture simplifiedEntityColliderProvider; // Client public Map modelData; public Map presentBlockEntities; public List maybeInstancedBlockEntities; public List specialRenderedBlockEntities; protected ContraptionWorld world; public boolean deferInvalidate; public Contraption() { blocks = new HashMap<>(); seats = new ArrayList<>(); actors = new ArrayList<>(); disabledActors = new ArrayList<>(); modelData = new HashMap<>(); interactors = new HashMap<>(); superglue = new ArrayList<>(); seatMapping = new HashMap<>(); glueToRemove = new HashSet<>(); initialPassengers = new HashMap<>(); presentBlockEntities = new HashMap<>(); maybeInstancedBlockEntities = new ArrayList<>(); specialRenderedBlockEntities = new ArrayList<>(); pendingSubContraptions = new ArrayList<>(); stabilizedSubContraptions = new HashMap<>(); simplifiedEntityColliders = Optional.empty(); storage = new MountedStorageManager(); } public ContraptionWorld getContraptionWorld() { if (world == null) world = new ContraptionWorld(entity.level, this); return world; } public abstract boolean assemble(Level world, BlockPos pos) throws AssemblyException; public abstract boolean canBeStabilized(Direction facing, BlockPos localPos); protected abstract ContraptionType getType(); protected boolean customBlockPlacement(LevelAccessor world, BlockPos pos, BlockState state) { return false; } protected boolean customBlockRemoval(LevelAccessor world, BlockPos pos, BlockState state) { return false; } protected boolean addToInitialFrontier(Level world, BlockPos pos, Direction forcedDirection, Queue frontier) throws AssemblyException { return true; } public static Contraption fromNBT(Level world, CompoundTag nbt, boolean spawnData) { String type = nbt.getString("Type"); Contraption contraption = ContraptionType.fromType(type); contraption.readNBT(world, nbt, spawnData); contraption.world = new ContraptionWorld(world, contraption); contraption.gatherBBsOffThread(); return contraption; } public boolean searchMovedStructure(Level world, BlockPos pos, @Nullable Direction forcedDirection) throws AssemblyException { initialPassengers.clear(); Queue frontier = new UniqueLinkedList<>(); Set visited = new HashSet<>(); anchor = pos; if (bounds == null) bounds = new AABB(BlockPos.ZERO); if (!BlockMovementChecks.isBrittle(world.getBlockState(pos))) frontier.add(pos); if (!addToInitialFrontier(world, pos, forcedDirection, frontier)) return false; for (int limit = 100000; limit > 0; limit--) { if (frontier.isEmpty()) return true; if (!moveBlock(world, forcedDirection, frontier, visited)) return false; } throw AssemblyException.structureTooLarge(); } public void onEntityCreated(AbstractContraptionEntity entity) { this.entity = entity; // Create subcontraptions for (BlockFace blockFace : pendingSubContraptions) { Direction face = blockFace.getFace(); StabilizedContraption subContraption = new StabilizedContraption(face); Level world = entity.level; BlockPos pos = blockFace.getPos(); try { if (!subContraption.assemble(world, pos)) continue; } catch (AssemblyException e) { continue; } subContraption.removeBlocksFromWorld(world, BlockPos.ZERO); OrientedContraptionEntity movedContraption = OrientedContraptionEntity.create(world, subContraption, face); BlockPos anchor = blockFace.getConnectedPos(); movedContraption.setPos(anchor.getX() + .5f, anchor.getY(), anchor.getZ() + .5f); world.addFreshEntity(movedContraption); stabilizedSubContraptions.put(movedContraption.getUUID(), new BlockFace(toLocalPos(pos), face)); } storage.createHandlers(); gatherBBsOffThread(); } public void onEntityRemoved(AbstractContraptionEntity entity) { if (simplifiedEntityColliderProvider != null) { simplifiedEntityColliderProvider.cancel(false); simplifiedEntityColliderProvider = null; } } public void onEntityInitialize(Level world, AbstractContraptionEntity contraptionEntity) { if (world.isClientSide) return; for (OrientedContraptionEntity orientedCE : world.getEntitiesOfClass(OrientedContraptionEntity.class, contraptionEntity.getBoundingBox() .inflate(1))) if (stabilizedSubContraptions.containsKey(orientedCE.getUUID())) orientedCE.startRiding(contraptionEntity); for (BlockPos seatPos : getSeats()) { Entity passenger = initialPassengers.get(seatPos); if (passenger == null) continue; int seatIndex = getSeats().indexOf(seatPos); if (seatIndex == -1) continue; contraptionEntity.addSittingPassenger(passenger, seatIndex); } } /** move the first block in frontier queue */ protected boolean moveBlock(Level world, @Nullable Direction forcedDirection, Queue frontier, Set visited) throws AssemblyException { BlockPos pos = frontier.poll(); if (pos == null) return false; visited.add(pos); if (world.isOutsideBuildHeight(pos)) return true; if (!world.isLoaded(pos)) throw AssemblyException.unloadedChunk(pos); if (isAnchoringBlockAt(pos)) return true; BlockState state = world.getBlockState(pos); if (!BlockMovementChecks.isMovementNecessary(state, world, pos)) return true; if (!movementAllowed(state, world, pos)) throw AssemblyException.unmovableBlock(pos, state); if (state.getBlock() instanceof AbstractChassisBlock && !moveChassis(world, pos, forcedDirection, frontier, visited)) return false; if (AllBlocks.BELT.has(state)) moveBelt(pos, frontier, visited, state); if (AllBlocks.WINDMILL_BEARING.has(state) && world.getBlockEntity(pos)instanceof WindmillBearingBlockEntity wbte) wbte.disassembleForMovement(); if (AllBlocks.GANTRY_CARRIAGE.has(state)) moveGantryPinion(world, pos, frontier, visited, state); if (AllBlocks.GANTRY_SHAFT.has(state)) moveGantryShaft(world, pos, frontier, visited, state); if (AllBlocks.STICKER.has(state) && state.getValue(StickerBlock.EXTENDED)) { Direction offset = state.getValue(StickerBlock.FACING); BlockPos attached = pos.relative(offset); if (!visited.contains(attached) && !BlockMovementChecks.isNotSupportive(world.getBlockState(attached), offset.getOpposite())) frontier.add(attached); } // Double Chest halves stick together if (state.hasProperty(ChestBlock.TYPE) && state.hasProperty(ChestBlock.FACING) && state.getValue(ChestBlock.TYPE) != ChestType.SINGLE) { Direction offset = ChestBlock.getConnectedDirection(state); BlockPos attached = pos.relative(offset); if (!visited.contains(attached)) frontier.add(attached); } // Bogeys tend to have sticky sides if (state.getBlock()instanceof AbstractBogeyBlock bogey) for (Direction d : bogey.getStickySurfaces(world, pos, state)) if (!visited.contains(pos.relative(d))) frontier.add(pos.relative(d)); // Bearings potentially create stabilized sub-contraptions if (AllBlocks.MECHANICAL_BEARING.has(state)) moveBearing(pos, frontier, visited, state); // WM Bearings attach their structure when moved if (AllBlocks.WINDMILL_BEARING.has(state)) moveWindmillBearing(pos, frontier, visited, state); // Seats transfer their passenger to the contraption if (state.getBlock() instanceof SeatBlock) moveSeat(world, pos); // Pulleys drag their rope and their attached structure if (state.getBlock() instanceof PulleyBlock) movePulley(world, pos, frontier, visited); // Pistons drag their attaches poles and extension if (state.getBlock() instanceof MechanicalPistonBlock) if (!moveMechanicalPiston(world, pos, frontier, visited, state)) return false; if (isExtensionPole(state)) movePistonPole(world, pos, frontier, visited, state); if (isPistonHead(state)) movePistonHead(world, pos, frontier, visited, state); // Cart assemblers attach themselves BlockPos posDown = pos.below(); BlockState stateBelow = world.getBlockState(posDown); if (!visited.contains(posDown) && AllBlocks.CART_ASSEMBLER.has(stateBelow)) frontier.add(posDown); // Slime blocks and super glue drag adjacent blocks if possible for (Direction offset : Iterate.directions) { BlockPos offsetPos = pos.relative(offset); BlockState blockState = world.getBlockState(offsetPos); if (isAnchoringBlockAt(offsetPos)) continue; if (!movementAllowed(blockState, world, offsetPos)) { if (offset == forcedDirection) throw AssemblyException.unmovableBlock(pos, state); continue; } boolean wasVisited = visited.contains(offsetPos); boolean faceHasGlue = SuperGlueEntity.isGlued(world, pos, offset, glueToRemove); boolean blockAttachedTowardsFace = BlockMovementChecks.isBlockAttachedTowards(blockState, world, offsetPos, offset.getOpposite()); boolean brittle = BlockMovementChecks.isBrittle(blockState); boolean canStick = !brittle && state.canStickTo(blockState) && blockState.canStickTo(state); if (canStick) { if (state.getPistonPushReaction() == PushReaction.PUSH_ONLY || blockState.getPistonPushReaction() == PushReaction.PUSH_ONLY) { canStick = false; } if (BlockMovementChecks.isNotSupportive(state, offset)) { canStick = false; } if (BlockMovementChecks.isNotSupportive(blockState, offset.getOpposite())) { canStick = false; } } if (!wasVisited && (canStick || blockAttachedTowardsFace || faceHasGlue || (offset == forcedDirection && !BlockMovementChecks.isNotSupportive(state, forcedDirection)))) frontier.add(offsetPos); } addBlock(pos, capture(world, pos)); if (blocks.size() <= AllConfigs.server().kinetics.maxBlocksMoved.get()) return true; else throw AssemblyException.structureTooLarge(); } protected void movePistonHead(Level world, BlockPos pos, Queue frontier, Set visited, BlockState state) { Direction direction = state.getValue(MechanicalPistonHeadBlock.FACING); BlockPos offset = pos.relative(direction.getOpposite()); if (!visited.contains(offset)) { BlockState blockState = world.getBlockState(offset); if (isExtensionPole(blockState) && blockState.getValue(PistonExtensionPoleBlock.FACING) .getAxis() == direction.getAxis()) frontier.add(offset); if (blockState.getBlock() instanceof MechanicalPistonBlock) { Direction pistonFacing = blockState.getValue(MechanicalPistonBlock.FACING); if (pistonFacing == direction && blockState.getValue(MechanicalPistonBlock.STATE) == PistonState.EXTENDED) frontier.add(offset); } } if (state.getValue(MechanicalPistonHeadBlock.TYPE) == PistonType.STICKY) { BlockPos attached = pos.relative(direction); if (!visited.contains(attached)) frontier.add(attached); } } protected void movePistonPole(Level world, BlockPos pos, Queue frontier, Set visited, BlockState state) { for (Direction d : Iterate.directionsInAxis(state.getValue(PistonExtensionPoleBlock.FACING) .getAxis())) { BlockPos offset = pos.relative(d); if (!visited.contains(offset)) { BlockState blockState = world.getBlockState(offset); if (isExtensionPole(blockState) && blockState.getValue(PistonExtensionPoleBlock.FACING) .getAxis() == d.getAxis()) frontier.add(offset); if (isPistonHead(blockState) && blockState.getValue(MechanicalPistonHeadBlock.FACING) .getAxis() == d.getAxis()) frontier.add(offset); if (blockState.getBlock() instanceof MechanicalPistonBlock) { Direction pistonFacing = blockState.getValue(MechanicalPistonBlock.FACING); if (pistonFacing == d || pistonFacing == d.getOpposite() && blockState.getValue(MechanicalPistonBlock.STATE) == PistonState.EXTENDED) frontier.add(offset); } } } } protected void moveGantryPinion(Level world, BlockPos pos, Queue frontier, Set visited, BlockState state) { BlockPos offset = pos.relative(state.getValue(GantryCarriageBlock.FACING)); if (!visited.contains(offset)) frontier.add(offset); Axis rotationAxis = ((IRotate) state.getBlock()).getRotationAxis(state); for (Direction d : Iterate.directionsInAxis(rotationAxis)) { offset = pos.relative(d); BlockState offsetState = world.getBlockState(offset); if (AllBlocks.GANTRY_SHAFT.has(offsetState) && offsetState.getValue(GantryShaftBlock.FACING) .getAxis() == d.getAxis()) if (!visited.contains(offset)) frontier.add(offset); } } protected void moveGantryShaft(Level world, BlockPos pos, Queue frontier, Set visited, BlockState state) { for (Direction d : Iterate.directions) { BlockPos offset = pos.relative(d); if (!visited.contains(offset)) { BlockState offsetState = world.getBlockState(offset); Direction facing = state.getValue(GantryShaftBlock.FACING); if (d.getAxis() == facing.getAxis() && AllBlocks.GANTRY_SHAFT.has(offsetState) && offsetState.getValue(GantryShaftBlock.FACING) == facing) frontier.add(offset); else if (AllBlocks.GANTRY_CARRIAGE.has(offsetState) && offsetState.getValue(GantryCarriageBlock.FACING) == d) frontier.add(offset); } } } private void moveWindmillBearing(BlockPos pos, Queue frontier, Set visited, BlockState state) { Direction facing = state.getValue(WindmillBearingBlock.FACING); BlockPos offset = pos.relative(facing); if (!visited.contains(offset)) frontier.add(offset); } private void moveBearing(BlockPos pos, Queue frontier, Set visited, BlockState state) { Direction facing = state.getValue(MechanicalBearingBlock.FACING); if (!canBeStabilized(facing, pos.subtract(anchor))) { BlockPos offset = pos.relative(facing); if (!visited.contains(offset)) frontier.add(offset); return; } pendingSubContraptions.add(new BlockFace(pos, facing)); } private void moveBelt(BlockPos pos, Queue frontier, Set visited, BlockState state) { BlockPos nextPos = BeltBlock.nextSegmentPosition(state, pos, true); BlockPos prevPos = BeltBlock.nextSegmentPosition(state, pos, false); if (nextPos != null && !visited.contains(nextPos)) frontier.add(nextPos); if (prevPos != null && !visited.contains(prevPos)) frontier.add(prevPos); } private void moveSeat(Level world, BlockPos pos) { BlockPos local = toLocalPos(pos); getSeats().add(local); List seatsEntities = world.getEntitiesOfClass(SeatEntity.class, new AABB(pos)); if (!seatsEntities.isEmpty()) { SeatEntity seat = seatsEntities.get(0); List passengers = seat.getPassengers(); if (!passengers.isEmpty()) initialPassengers.put(local, passengers.get(0)); } } private void movePulley(Level world, BlockPos pos, Queue frontier, Set visited) { int limit = AllConfigs.server().kinetics.maxRopeLength.get(); BlockPos ropePos = pos; while (limit-- >= 0) { ropePos = ropePos.below(); if (!world.isLoaded(ropePos)) break; BlockState ropeState = world.getBlockState(ropePos); Block block = ropeState.getBlock(); if (!(block instanceof RopeBlock) && !(block instanceof MagnetBlock)) { if (!visited.contains(ropePos)) frontier.add(ropePos); break; } addBlock(ropePos, capture(world, ropePos)); } } private boolean moveMechanicalPiston(Level world, BlockPos pos, Queue frontier, Set visited, BlockState state) throws AssemblyException { Direction direction = state.getValue(MechanicalPistonBlock.FACING); PistonState pistonState = state.getValue(MechanicalPistonBlock.STATE); if (pistonState == PistonState.MOVING) return false; BlockPos offset = pos.relative(direction.getOpposite()); if (!visited.contains(offset)) { BlockState poleState = world.getBlockState(offset); if (AllBlocks.PISTON_EXTENSION_POLE.has(poleState) && poleState.getValue(PistonExtensionPoleBlock.FACING) .getAxis() == direction.getAxis()) frontier.add(offset); } if (pistonState == PistonState.EXTENDED || MechanicalPistonBlock.isStickyPiston(state)) { offset = pos.relative(direction); if (!visited.contains(offset)) frontier.add(offset); } return true; } private boolean moveChassis(Level world, BlockPos pos, Direction movementDirection, Queue frontier, Set visited) { BlockEntity be = world.getBlockEntity(pos); if (!(be instanceof ChassisBlockEntity)) return false; ChassisBlockEntity chassis = (ChassisBlockEntity) be; chassis.addAttachedChasses(frontier, visited); List includedBlockPositions = chassis.getIncludedBlockPositions(movementDirection, false); if (includedBlockPositions == null) return false; for (BlockPos blockPos : includedBlockPositions) if (!visited.contains(blockPos)) frontier.add(blockPos); return true; } protected Pair capture(Level world, BlockPos pos) { BlockState blockstate = world.getBlockState(pos); if (AllBlocks.REDSTONE_CONTACT.has(blockstate)) blockstate = blockstate.setValue(RedstoneContactBlock.POWERED, true); if (AllBlocks.POWERED_SHAFT.has(blockstate)) blockstate = BlockHelper.copyProperties(blockstate, AllBlocks.SHAFT.getDefaultState()); if (blockstate.getBlock() instanceof ControlsBlock && getType() == ContraptionType.CARRIAGE) blockstate = blockstate.setValue(ControlsBlock.OPEN, true); if (blockstate.hasProperty(SlidingDoorBlock.VISIBLE)) blockstate = blockstate.setValue(SlidingDoorBlock.VISIBLE, false); if (blockstate.getBlock() instanceof ButtonBlock) { blockstate = blockstate.setValue(ButtonBlock.POWERED, false); world.scheduleTick(pos, blockstate.getBlock(), -1); } if (blockstate.getBlock() instanceof PressurePlateBlock) { blockstate = blockstate.setValue(PressurePlateBlock.POWERED, false); world.scheduleTick(pos, blockstate.getBlock(), -1); } CompoundTag compoundnbt = getBlockEntityNBT(world, pos); BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity instanceof PoweredShaftBlockEntity) blockEntity = AllBlockEntityTypes.BRACKETED_KINETIC.create(pos, blockstate); return Pair.of(new StructureBlockInfo(pos, blockstate, compoundnbt), blockEntity); } protected void addBlock(BlockPos pos, Pair pair) { StructureBlockInfo captured = pair.getKey(); BlockPos localPos = pos.subtract(anchor); StructureBlockInfo structureBlockInfo = new StructureBlockInfo(localPos, captured.state, captured.nbt); if (blocks.put(localPos, structureBlockInfo) != null) return; bounds = bounds.minmax(new AABB(localPos)); BlockEntity be = pair.getValue(); storage.addBlock(localPos, be); if (AllMovementBehaviours.getBehaviour(captured.state) != null) actors.add(MutablePair.of(structureBlockInfo, null)); MovingInteractionBehaviour interactionBehaviour = AllInteractionBehaviours.getBehaviour(captured.state); if (interactionBehaviour != null) interactors.put(localPos, interactionBehaviour); if (be instanceof CreativeCrateBlockEntity && ((CreativeCrateBlockEntity) be).getBehaviour(FilteringBehaviour.TYPE) .getFilter() .isEmpty()) hasUniversalCreativeCrate = true; } @Nullable protected CompoundTag getBlockEntityNBT(Level world, BlockPos pos) { BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity == null) return null; CompoundTag nbt = blockEntity.saveWithFullMetadata(); nbt.remove("x"); nbt.remove("y"); nbt.remove("z"); if ((blockEntity instanceof FluidTankBlockEntity || blockEntity instanceof ItemVaultBlockEntity) && nbt.contains("Controller")) nbt.put("Controller", NbtUtils.writeBlockPos(toLocalPos(NbtUtils.readBlockPos(nbt.getCompound("Controller"))))); return nbt; } protected BlockPos toLocalPos(BlockPos globalPos) { return globalPos.subtract(anchor); } protected boolean movementAllowed(BlockState state, Level world, BlockPos pos) { return BlockMovementChecks.isMovementAllowed(state, world, pos); } protected boolean isAnchoringBlockAt(BlockPos pos) { return pos.equals(anchor); } public void readNBT(Level world, CompoundTag nbt, boolean spawnData) { blocks.clear(); presentBlockEntities.clear(); specialRenderedBlockEntities.clear(); Tag blocks = nbt.get("Blocks"); // used to differentiate between the 'old' and the paletted serialization boolean usePalettedDeserialization = blocks != null && blocks.getId() == 10 && ((CompoundTag) blocks).contains("Palette"); readBlocksCompound(blocks, world, usePalettedDeserialization); actors.clear(); nbt.getList("Actors", 10) .forEach(c -> { CompoundTag comp = (CompoundTag) c; StructureBlockInfo info = this.blocks.get(NbtUtils.readBlockPos(comp.getCompound("Pos"))); if (info == null) return; MovementContext context = MovementContext.readNBT(world, info, comp, this); getActors().add(MutablePair.of(info, context)); }); disabledActors = NBTHelper.readItemList(nbt.getList("DisabledActors", Tag.TAG_COMPOUND)); for (ItemStack stack : disabledActors) setActorsActive(stack, false); superglue.clear(); NBTHelper.iterateCompoundList(nbt.getList("Superglue", Tag.TAG_COMPOUND), c -> superglue.add(SuperGlueEntity.readBoundingBox(c))); seats.clear(); NBTHelper.iterateCompoundList(nbt.getList("Seats", Tag.TAG_COMPOUND), c -> seats.add(NbtUtils.readBlockPos(c))); seatMapping.clear(); NBTHelper.iterateCompoundList(nbt.getList("Passengers", Tag.TAG_COMPOUND), c -> seatMapping.put(NbtUtils.loadUUID(NBTHelper.getINBT(c, "Id")), c.getInt("Seat"))); stabilizedSubContraptions.clear(); NBTHelper.iterateCompoundList(nbt.getList("SubContraptions", Tag.TAG_COMPOUND), c -> stabilizedSubContraptions.put(c.getUUID("Id"), BlockFace.fromNBT(c.getCompound("Location")))); interactors.clear(); NBTHelper.iterateCompoundList(nbt.getList("Interactors", Tag.TAG_COMPOUND), c -> { BlockPos pos = NbtUtils.readBlockPos(c.getCompound("Pos")); StructureBlockInfo structureBlockInfo = getBlocks().get(pos); if (structureBlockInfo == null) return; MovingInteractionBehaviour behaviour = AllInteractionBehaviours.getBehaviour(structureBlockInfo.state); if (behaviour != null) interactors.put(pos, behaviour); }); storage.read(nbt, presentBlockEntities, spawnData); if (nbt.contains("BoundsFront")) bounds = NBTHelper.readAABB(nbt.getList("BoundsFront", 5)); stalled = nbt.getBoolean("Stalled"); hasUniversalCreativeCrate = nbt.getBoolean("BottomlessSupply"); anchor = NbtUtils.readBlockPos(nbt.getCompound("Anchor")); } public CompoundTag writeNBT(boolean spawnPacket) { CompoundTag nbt = new CompoundTag(); nbt.putString("Type", getType().id); CompoundTag blocksNBT = writeBlocksCompound(); ListTag actorsNBT = new ListTag(); for (MutablePair actor : getActors()) { MovementBehaviour behaviour = AllMovementBehaviours.getBehaviour(actor.left.state); if (behaviour == null) continue; CompoundTag compound = new CompoundTag(); compound.put("Pos", NbtUtils.writeBlockPos(actor.left.pos)); behaviour.writeExtraData(actor.right); actor.right.writeToNBT(compound); actorsNBT.add(compound); } ListTag disabledActorsNBT = NBTHelper.writeItemList(disabledActors); ListTag superglueNBT = new ListTag(); if (!spawnPacket) { for (AABB glueEntry : superglue) { CompoundTag c = new CompoundTag(); SuperGlueEntity.writeBoundingBox(c, glueEntry); superglueNBT.add(c); } } (spawnPacket ? getStorageForSpawnPacket() : storage).write(nbt, spawnPacket); ListTag interactorNBT = new ListTag(); for (BlockPos pos : interactors.keySet()) { CompoundTag c = new CompoundTag(); c.put("Pos", NbtUtils.writeBlockPos(pos)); interactorNBT.add(c); } nbt.put("Seats", NBTHelper.writeCompoundList(getSeats(), NbtUtils::writeBlockPos)); nbt.put("Passengers", NBTHelper.writeCompoundList(getSeatMapping().entrySet(), e -> { CompoundTag tag = new CompoundTag(); tag.put("Id", NbtUtils.createUUID(e.getKey())); tag.putInt("Seat", e.getValue()); return tag; })); nbt.put("SubContraptions", NBTHelper.writeCompoundList(stabilizedSubContraptions.entrySet(), e -> { CompoundTag tag = new CompoundTag(); tag.putUUID("Id", e.getKey()); tag.put("Location", e.getValue() .serializeNBT()); return tag; })); nbt.put("Blocks", blocksNBT); nbt.put("Actors", actorsNBT); nbt.put("DisabledActors", disabledActorsNBT); nbt.put("Interactors", interactorNBT); nbt.put("Superglue", superglueNBT); nbt.put("Anchor", NbtUtils.writeBlockPos(anchor)); nbt.putBoolean("Stalled", stalled); nbt.putBoolean("BottomlessSupply", hasUniversalCreativeCrate); if (bounds != null) { ListTag bb = NBTHelper.writeAABB(bounds); nbt.put("BoundsFront", bb); } return nbt; } protected MountedStorageManager getStorageForSpawnPacket() { return storage; } private CompoundTag writeBlocksCompound() { CompoundTag compound = new CompoundTag(); HashMapPalette palette = new HashMapPalette<>(GameData.getBlockStateIDMap(), 16, (i, s) -> { throw new IllegalStateException("Palette Map index exceeded maximum"); }); ListTag blockList = new ListTag(); for (StructureBlockInfo block : this.blocks.values()) { int id = palette.idFor(block.state); CompoundTag c = new CompoundTag(); c.putLong("Pos", block.pos.asLong()); c.putInt("State", id); if (block.nbt != null) c.put("Data", block.nbt); blockList.add(c); } ListTag paletteNBT = new ListTag(); for (int i = 0; i < palette.getSize(); ++i) paletteNBT.add(NbtUtils.writeBlockState(palette.values.byId(i))); compound.put("Palette", paletteNBT); compound.put("BlockList", blockList); return compound; } private void readBlocksCompound(Tag compound, Level world, boolean usePalettedDeserialization) { HashMapPalette palette = null; ListTag blockList; if (usePalettedDeserialization) { CompoundTag c = ((CompoundTag) compound); palette = new HashMapPalette<>(GameData.getBlockStateIDMap(), 16, (i, s) -> { throw new IllegalStateException("Palette Map index exceeded maximum"); }); ListTag list = c.getList("Palette", 10); palette.values.clear(); for (int i = 0; i < list.size(); ++i) palette.values.add(NbtUtils.readBlockState(list.getCompound(i))); blockList = c.getList("BlockList", 10); } else { blockList = (ListTag) compound; } HashMapPalette finalPalette = palette; blockList.forEach(e -> { CompoundTag c = (CompoundTag) e; StructureBlockInfo info = usePalettedDeserialization ? readStructureBlockInfo(c, finalPalette) : legacyReadStructureBlockInfo(c); this.blocks.put(info.pos, info); if (!world.isClientSide) return; CompoundTag tag = info.nbt; if (tag == null) return; tag.putInt("x", info.pos.getX()); tag.putInt("y", info.pos.getY()); tag.putInt("z", info.pos.getZ()); BlockEntity be = BlockEntity.loadStatic(info.pos, info.state, tag); if (be == null) return; be.setLevel(world); modelData.put(info.pos, be.getModelData()); if (be instanceof KineticBlockEntity kte) kte.setSpeed(0); be.getBlockState(); MovementBehaviour movementBehaviour = AllMovementBehaviours.getBehaviour(info.state); if (movementBehaviour == null || !movementBehaviour.hasSpecialInstancedRendering()) maybeInstancedBlockEntities.add(be); if (movementBehaviour != null && !movementBehaviour.renderAsNormalBlockEntity()) return; presentBlockEntities.put(info.pos, be); specialRenderedBlockEntities.add(be); }); } private static StructureBlockInfo readStructureBlockInfo(CompoundTag blockListEntry, HashMapPalette palette) { return new StructureBlockInfo(BlockPos.of(blockListEntry.getLong("Pos")), Objects.requireNonNull(palette.valueFor(blockListEntry.getInt("State"))), blockListEntry.contains("Data") ? blockListEntry.getCompound("Data") : null); } private static StructureBlockInfo legacyReadStructureBlockInfo(CompoundTag blockListEntry) { return new StructureBlockInfo(NbtUtils.readBlockPos(blockListEntry.getCompound("Pos")), NbtUtils.readBlockState(blockListEntry.getCompound("Block")), blockListEntry.contains("Data") ? blockListEntry.getCompound("Data") : null); } public void removeBlocksFromWorld(Level world, BlockPos offset) { storage.removeStorageFromWorld(); glueToRemove.forEach(glue -> { superglue.add(glue.getBoundingBox() .move(Vec3.atLowerCornerOf(offset.offset(anchor)) .scale(-1))); glue.discard(); }); List minimisedGlue = new ArrayList<>(); for (int i = 0; i < superglue.size(); i++) minimisedGlue.add(null); for (boolean brittles : Iterate.trueAndFalse) { for (Iterator iterator = blocks.values() .iterator(); iterator.hasNext();) { StructureBlockInfo block = iterator.next(); if (brittles != BlockMovementChecks.isBrittle(block.state)) continue; for (int i = 0; i < superglue.size(); i++) { AABB aabb = superglue.get(i); if (aabb == null || !aabb.contains(block.pos.getX() + .5, block.pos.getY() + .5, block.pos.getZ() + .5)) continue; if (minimisedGlue.get(i) == null) minimisedGlue.set(i, new BoundingBox(block.pos)); else minimisedGlue.set(i, BBHelper.encapsulate(minimisedGlue.get(i), block.pos)); } BlockPos add = block.pos.offset(anchor) .offset(offset); if (customBlockRemoval(world, add, block.state)) continue; BlockState oldState = world.getBlockState(add); Block blockIn = oldState.getBlock(); boolean blockMismatch = block.state.getBlock() != blockIn; blockMismatch &= !AllBlocks.POWERED_SHAFT.is(blockIn) || !AllBlocks.SHAFT.has(block.state); if (blockMismatch) iterator.remove(); world.removeBlockEntity(add); int flags = Block.UPDATE_MOVE_BY_PISTON | Block.UPDATE_SUPPRESS_DROPS | Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS | Block.UPDATE_IMMEDIATE; if (blockIn instanceof SimpleWaterloggedBlock && oldState.hasProperty(BlockStateProperties.WATERLOGGED) && oldState.getValue(BlockStateProperties.WATERLOGGED)) { world.setBlock(add, Blocks.WATER.defaultBlockState(), flags); continue; } world.setBlock(add, Blocks.AIR.defaultBlockState(), flags); } } superglue.clear(); for (BoundingBox box : minimisedGlue) { if (box == null) continue; AABB bb = new AABB(box.minX(), box.minY(), box.minZ(), box.maxX() + 1, box.maxY() + 1, box.maxZ() + 1); if (bb.getSize() > 1.01) superglue.add(bb); } for (StructureBlockInfo block : blocks.values()) { BlockPos add = block.pos.offset(anchor) .offset(offset); // if (!shouldUpdateAfterMovement(block)) // continue; int flags = Block.UPDATE_MOVE_BY_PISTON | Block.UPDATE_ALL; world.sendBlockUpdated(add, block.state, Blocks.AIR.defaultBlockState(), flags); // when the blockstate is set to air, the block's POI data is removed, but // markAndNotifyBlock tries to // remove it again, so to prevent an error from being logged by double-removal // we add the POI data back now // (code copied from ServerWorld.onBlockStateChange) ServerLevel serverWorld = (ServerLevel) world; PoiType.forState(block.state) .ifPresent(poiType -> { world.getServer() .execute(() -> { serverWorld.getPoiManager() .add(add, poiType); DebugPackets.sendPoiAddedPacket(serverWorld, add); }); }); world.markAndNotifyBlock(add, world.getChunkAt(add), block.state, Blocks.AIR.defaultBlockState(), flags, 512); block.state.updateIndirectNeighbourShapes(world, add, flags & -2); } } public void addBlocksToWorld(Level world, StructureTransform transform) { if (disassembled) return; disassembled = true; for (boolean nonBrittles : Iterate.trueAndFalse) { for (StructureBlockInfo block : blocks.values()) { if (nonBrittles == BlockMovementChecks.isBrittle(block.state)) continue; BlockPos targetPos = transform.apply(block.pos); BlockState state = transform.apply(block.state); if (customBlockPlacement(world, targetPos, state)) continue; if (nonBrittles) for (Direction face : Iterate.directions) state = state.updateShape(face, world.getBlockState(targetPos.relative(face)), world, targetPos, targetPos.relative(face)); BlockState blockState = world.getBlockState(targetPos); if (blockState.getDestroySpeed(world, targetPos) == -1 || (state.getCollisionShape(world, targetPos) .isEmpty() && !blockState.getCollisionShape(world, targetPos) .isEmpty())) { if (targetPos.getY() == world.getMinBuildHeight()) targetPos = targetPos.above(); world.levelEvent(2001, targetPos, Block.getId(state)); Block.dropResources(state, world, targetPos, null); continue; } if (state.getBlock() instanceof SimpleWaterloggedBlock && state.hasProperty(BlockStateProperties.WATERLOGGED)) { FluidState FluidState = world.getFluidState(targetPos); state = state.setValue(BlockStateProperties.WATERLOGGED, FluidState.getType() == Fluids.WATER); } world.destroyBlock(targetPos, true); if (AllBlocks.SHAFT.has(state)) state = ShaftBlock.pickCorrectShaftType(state, world, targetPos); if (state.hasProperty(SlidingDoorBlock.VISIBLE)) state = state.setValue(SlidingDoorBlock.VISIBLE, !state.getValue(SlidingDoorBlock.OPEN)) .setValue(SlidingDoorBlock.POWERED, false); world.setBlock(targetPos, state, Block.UPDATE_MOVE_BY_PISTON | Block.UPDATE_ALL); boolean verticalRotation = transform.rotationAxis == null || transform.rotationAxis.isHorizontal(); verticalRotation = verticalRotation && transform.rotation != Rotation.NONE; if (verticalRotation) { if (state.getBlock() instanceof RopeBlock || state.getBlock() instanceof MagnetBlock || state.getBlock() instanceof DoorBlock) world.destroyBlock(targetPos, true); } BlockEntity blockEntity = world.getBlockEntity(targetPos); CompoundTag tag = block.nbt; if (blockEntity != null) tag = NBTProcessors.process(blockEntity, tag, false); if (blockEntity != null && tag != null) { tag.putInt("x", targetPos.getX()); tag.putInt("y", targetPos.getY()); tag.putInt("z", targetPos.getZ()); if (verticalRotation && blockEntity instanceof PulleyBlockEntity) { tag.remove("Offset"); tag.remove("InitialOffset"); } if (blockEntity instanceof IMultiBlockEntityContainer && tag.contains("LastKnownPos")) tag.put("LastKnownPos", NbtUtils.writeBlockPos(BlockPos.ZERO.below(Integer.MAX_VALUE - 1))); blockEntity.load(tag); storage.addStorageToWorld(block, blockEntity); } transform.apply(blockEntity); } } for (StructureBlockInfo block : blocks.values()) { if (!shouldUpdateAfterMovement(block)) continue; BlockPos targetPos = transform.apply(block.pos); world.markAndNotifyBlock(targetPos, world.getChunkAt(targetPos), block.state, block.state, Block.UPDATE_MOVE_BY_PISTON | Block.UPDATE_ALL, 512); } for (AABB box : superglue) { box = new AABB(transform.apply(new Vec3(box.minX, box.minY, box.minZ)), transform.apply(new Vec3(box.maxX, box.maxY, box.maxZ))); if (!world.isClientSide) world.addFreshEntity(new SuperGlueEntity(world, box)); } storage.clear(); } public void addPassengersToWorld(Level world, StructureTransform transform, List seatedEntities) { for (Entity seatedEntity : seatedEntities) { if (getSeatMapping().isEmpty()) continue; Integer seatIndex = getSeatMapping().get(seatedEntity.getUUID()); if (seatIndex == null) continue; BlockPos seatPos = getSeats().get(seatIndex); seatPos = transform.apply(seatPos); if (!(world.getBlockState(seatPos) .getBlock() instanceof SeatBlock)) continue; if (SeatBlock.isSeatOccupied(world, seatPos)) continue; SeatBlock.sitDown(world, seatPos, seatedEntity); } } public void startMoving(Level world) { disabledActors.clear(); for (MutablePair pair : actors) { MovementContext context = new MovementContext(world, pair.left, this); MovementBehaviour behaviour = AllMovementBehaviours.getBehaviour(pair.left.state); if (behaviour != null) behaviour.startMoving(context); pair.setRight(context); if (behaviour instanceof ContraptionControlsMovement) disableActorOnStart(context); } for (ItemStack stack : disabledActors) setActorsActive(stack, false); } protected void disableActorOnStart(MovementContext context) { if (!ContraptionControlsMovement.isDisabledInitially(context)) return; ItemStack filter = ContraptionControlsMovement.getFilter(context); if (filter == null) return; if (isActorTypeDisabled(filter)) return; disabledActors.add(filter); } public boolean isActorTypeDisabled(ItemStack filter) { return disabledActors.stream() .anyMatch(i -> ContraptionControlsMovement.isSameFilter(i, filter)); } public void setActorsActive(ItemStack referenceStack, boolean enable) { for (MutablePair pair : actors) { MovementBehaviour behaviour = AllMovementBehaviours.getBehaviour(pair.left.state); if (behaviour == null) continue; ItemStack behaviourStack = behaviour.canBeDisabledVia(pair.right); if (behaviourStack == null) continue; if (!referenceStack.isEmpty() && !ContraptionControlsMovement.isSameFilter(referenceStack, behaviourStack)) continue; pair.right.disabled = !enable; if (!enable) behaviour.onDisabledByControls(pair.right); } } public List getDisabledActors() { return disabledActors; } public void stop(Level world) { forEachActor(world, (behaviour, ctx) -> { behaviour.stopMoving(ctx); ctx.position = null; ctx.motion = Vec3.ZERO; ctx.relativeMotion = Vec3.ZERO; ctx.rotation = v -> v; }); } public void forEachActor(Level world, BiConsumer callBack) { for (MutablePair pair : actors) { MovementBehaviour behaviour = AllMovementBehaviours.getBehaviour(pair.getLeft().state); if (behaviour == null) continue; callBack.accept(behaviour, pair.getRight()); } } protected boolean shouldUpdateAfterMovement(StructureBlockInfo info) { if (PoiType.forState(info.state) .isPresent()) return false; if (info.state.getBlock() instanceof SlidingDoorBlock) return false; return true; } public void expandBoundsAroundAxis(Axis axis) { Set blocks = getBlocks().keySet(); int radius = (int) (Math.ceil(Math.sqrt(getRadius(blocks, axis)))); int maxX = radius + 2; int maxY = radius + 2; int maxZ = radius + 2; int minX = -radius - 1; int minY = -radius - 1; int minZ = -radius - 1; if (axis == Direction.Axis.X) { maxX = (int) bounds.maxX; minX = (int) bounds.minX; } else if (axis == Direction.Axis.Y) { maxY = (int) bounds.maxY; minY = (int) bounds.minY; } else if (axis == Direction.Axis.Z) { maxZ = (int) bounds.maxZ; minZ = (int) bounds.minZ; } bounds = new AABB(minX, minY, minZ, maxX, maxY, maxZ); } public Map getSeatMapping() { return seatMapping; } public BlockPos getSeatOf(UUID entityId) { if (!getSeatMapping().containsKey(entityId)) return null; int seatIndex = getSeatMapping().get(entityId); if (seatIndex >= getSeats().size()) return null; return getSeats().get(seatIndex); } public BlockPos getBearingPosOf(UUID subContraptionEntityId) { if (stabilizedSubContraptions.containsKey(subContraptionEntityId)) return stabilizedSubContraptions.get(subContraptionEntityId) .getConnectedPos(); return null; } public void setSeatMapping(Map seatMapping) { this.seatMapping = seatMapping; } public List getSeats() { return seats; } public Map getBlocks() { return blocks; } public List> getActors() { return actors; } @Nullable public MutablePair getActorAt(BlockPos localPos) { for (MutablePair pair : actors) if (localPos.equals(pair.left.pos)) return pair; return null; } public Map getInteractors() { return interactors; } @OnlyIn(Dist.CLIENT) public ContraptionLighter makeLighter() { // TODO: move lighters to registry return new EmptyLighter(this); } public void invalidateColliders() { simplifiedEntityColliders = Optional.empty(); gatherBBsOffThread(); } private void gatherBBsOffThread() { getContraptionWorld(); simplifiedEntityColliderProvider = CompletableFuture.supplyAsync(() -> { VoxelShape combinedShape = Shapes.empty(); for (Entry entry : blocks.entrySet()) { StructureBlockInfo info = entry.getValue(); BlockPos localPos = entry.getKey(); VoxelShape collisionShape = info.state.getCollisionShape(world, localPos, CollisionContext.empty()); if (collisionShape.isEmpty()) continue; combinedShape = Shapes.joinUnoptimized(combinedShape, collisionShape.move(localPos.getX(), localPos.getY(), localPos.getZ()), BooleanOp.OR); } return combinedShape.optimize() .toAabbs(); }) .thenAccept(r -> { simplifiedEntityColliders = Optional.of(r); simplifiedEntityColliderProvider = null; }); } public static float getRadius(Set blocks, Direction.Axis axis) { switch (axis) { case X: return getMaxDistSqr(blocks, BlockPos::getY, BlockPos::getZ); case Y: return getMaxDistSqr(blocks, BlockPos::getX, BlockPos::getZ); case Z: return getMaxDistSqr(blocks, BlockPos::getX, BlockPos::getY); } throw new IllegalStateException("Impossible axis"); } public static float getMaxDistSqr(Set blocks, ICoordinate one, ICoordinate other) { float maxDistSq = -1; for (BlockPos pos : blocks) { float a = one.get(pos); float b = other.get(pos); float distSq = a * a + b * b; if (distSq > maxDistSq) maxDistSq = distSq; } return maxDistSq; } public IItemHandlerModifiable getSharedInventory() { return storage.getItems(); } public IItemHandlerModifiable getSharedFuelInventory() { return storage.getFuelItems(); } public IFluidHandler getSharedFluidTanks() { return storage.getFluids(); } public Collection getRenderedBlocks() { return blocks.values(); } public Collection getSpecialRenderedTEs() { return specialRenderedBlockEntities; } public boolean isHiddenInPortal(BlockPos localPos) { return false; } public Optional> getSimplifiedEntityColliders() { return simplifiedEntityColliders; } public void handleContraptionFluidPacket(BlockPos localPos, FluidStack containedFluid) { storage.updateContainedFluid(localPos, containedFluid); } public static class ContraptionInvWrapper extends CombinedInvWrapper { protected final boolean isExternal; public ContraptionInvWrapper(boolean isExternal, IItemHandlerModifiable... itemHandler) { super(itemHandler); this.isExternal = isExternal; } public ContraptionInvWrapper(IItemHandlerModifiable... itemHandler) { this(false, itemHandler); } public boolean isSlotExternal(int slot) { if (isExternal) return true; IItemHandlerModifiable handler = getHandlerFromIndex(getIndexForSlot(slot)); return handler instanceof ContraptionInvWrapper && ((ContraptionInvWrapper) handler).isSlotExternal(slot); } } public void tickStorage(AbstractContraptionEntity entity) { storage.entityTick(entity); } public boolean containsBlockBreakers() { for (MutablePair pair : actors) { MovementBehaviour behaviour = AllMovementBehaviours.getBehaviour(pair.getLeft().state); if (behaviour instanceof BlockBreakingMovementBehaviour || behaviour instanceof HarvesterMovementBehaviour) return true; } return false; } }