package net.minecraft.world.level.material; import com.google.common.collect.Maps; import it.unimi.dsi.fastutil.objects.Object2ByteLinkedOpenHashMap; import it.unimi.dsi.fastutil.shorts.Short2BooleanFunction; import it.unimi.dsi.fastutil.shorts.Short2BooleanMap; import it.unimi.dsi.fastutil.shorts.Short2BooleanOpenHashMap; import it.unimi.dsi.fastutil.shorts.Short2ObjectFunction; import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; import java.util.Map; import java.util.Map.Entry; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; import net.minecraft.tags.BlockTags; 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.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.DoorBlock; import net.minecraft.world.level.block.IceBlock; import net.minecraft.world.level.block.LiquidBlockContainer; 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.IntegerProperty; import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; public abstract class FlowingFluid extends Fluid { public static final BooleanProperty FALLING = BlockStateProperties.FALLING; public static final IntegerProperty LEVEL = BlockStateProperties.LEVEL_FLOWING; private static final int CACHE_SIZE = 200; private static final ThreadLocal> OCCLUSION_CACHE = ThreadLocal.withInitial( () -> { Object2ByteLinkedOpenHashMap object2ByteLinkedOpenHashMap = new Object2ByteLinkedOpenHashMap( 200 ) { @Override protected void rehash(int i) { } }; object2ByteLinkedOpenHashMap.defaultReturnValue((byte)127); return object2ByteLinkedOpenHashMap; } ); private final Map shapes = Maps.newIdentityHashMap(); @Override protected void createFluidStateDefinition(Builder builder) { builder.add(FALLING); } @Override public Vec3 getFlow(BlockGetter blockReader, BlockPos pos, FluidState fluidState) { double d = 0.0; double e = 0.0; BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); for (Direction direction : Direction.Plane.HORIZONTAL) { mutableBlockPos.setWithOffset(pos, direction); FluidState fluidState2 = blockReader.getFluidState(mutableBlockPos); if (this.affectsFlow(fluidState2)) { float f = fluidState2.getOwnHeight(); float g = 0.0F; if (f == 0.0F) { if (!blockReader.getBlockState(mutableBlockPos).blocksMotion()) { BlockPos blockPos = mutableBlockPos.below(); FluidState fluidState3 = blockReader.getFluidState(blockPos); if (this.affectsFlow(fluidState3)) { f = fluidState3.getOwnHeight(); if (f > 0.0F) { g = fluidState.getOwnHeight() - (f - 0.8888889F); } } } } else if (f > 0.0F) { g = fluidState.getOwnHeight() - f; } if (g != 0.0F) { d += direction.getStepX() * g; e += direction.getStepZ() * g; } } } Vec3 vec3 = new Vec3(d, 0.0, e); if ((Boolean)fluidState.getValue(FALLING)) { for (Direction direction2 : Direction.Plane.HORIZONTAL) { mutableBlockPos.setWithOffset(pos, direction2); if (this.isSolidFace(blockReader, mutableBlockPos, direction2) || this.isSolidFace(blockReader, mutableBlockPos.above(), direction2)) { vec3 = vec3.normalize().add(0.0, -6.0, 0.0); break; } } } return vec3.normalize(); } private boolean affectsFlow(FluidState state) { return state.isEmpty() || state.getType().isSame(this); } protected boolean isSolidFace(BlockGetter level, BlockPos neighborPos, Direction side) { BlockState blockState = level.getBlockState(neighborPos); FluidState fluidState = level.getFluidState(neighborPos); if (fluidState.getType().isSame(this)) { return false; } else if (side == Direction.UP) { return true; } else { return blockState.getBlock() instanceof IceBlock ? false : blockState.isFaceSturdy(level, neighborPos, side); } } protected void spread(ServerLevel level, BlockPos pos, BlockState blockState, FluidState fluidState) { if (!fluidState.isEmpty()) { BlockPos blockPos = pos.below(); BlockState blockState2 = level.getBlockState(blockPos); FluidState fluidState2 = blockState2.getFluidState(); if (this.canMaybePassThrough(level, pos, blockState, Direction.DOWN, blockPos, blockState2, fluidState2)) { FluidState fluidState3 = this.getNewLiquid(level, blockPos, blockState2); Fluid fluid = fluidState3.getType(); if (fluidState2.canBeReplacedWith(level, blockPos, fluid, Direction.DOWN) && canHoldSpecificFluid(level, blockPos, blockState2, fluid)) { this.spreadTo(level, blockPos, blockState2, Direction.DOWN, fluidState3); if (this.sourceNeighborCount(level, pos) >= 3) { this.spreadToSides(level, pos, fluidState, blockState); } return; } } if (fluidState.isSource() || !this.isWaterHole(level, pos, blockState, blockPos, blockState2)) { this.spreadToSides(level, pos, fluidState, blockState); } } } private void spreadToSides(ServerLevel level, BlockPos pos, FluidState fluidState, BlockState blockState) { int i = fluidState.getAmount() - this.getDropOff(level); if ((Boolean)fluidState.getValue(FALLING)) { i = 7; } if (i > 0) { Map map = this.getSpread(level, pos, blockState); for (Entry entry : map.entrySet()) { Direction direction = (Direction)entry.getKey(); FluidState fluidState2 = (FluidState)entry.getValue(); BlockPos blockPos = pos.relative(direction); this.spreadTo(level, blockPos, level.getBlockState(blockPos), direction, fluidState2); } } } protected FluidState getNewLiquid(ServerLevel level, BlockPos pos, BlockState state) { int i = 0; int j = 0; BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); for (Direction direction : Direction.Plane.HORIZONTAL) { BlockPos blockPos = mutableBlockPos.setWithOffset(pos, direction); BlockState blockState = level.getBlockState(blockPos); FluidState fluidState = blockState.getFluidState(); if (fluidState.getType().isSame(this) && canPassThroughWall(direction, level, pos, state, blockPos, blockState)) { if (fluidState.isSource()) { j++; } i = Math.max(i, fluidState.getAmount()); } } if (j >= 2 && this.canConvertToSource(level)) { BlockState blockState2 = level.getBlockState(mutableBlockPos.setWithOffset(pos, Direction.DOWN)); FluidState fluidState2 = blockState2.getFluidState(); if (blockState2.isSolid() || this.isSourceBlockOfThisType(fluidState2)) { return this.getSource(false); } } BlockPos blockPos2 = mutableBlockPos.setWithOffset(pos, Direction.UP); BlockState blockState3 = level.getBlockState(blockPos2); FluidState fluidState3 = blockState3.getFluidState(); if (!fluidState3.isEmpty() && fluidState3.getType().isSame(this) && canPassThroughWall(Direction.UP, level, pos, state, blockPos2, blockState3)) { return this.getFlowing(8, true); } else { int k = i - this.getDropOff(level); return k <= 0 ? Fluids.EMPTY.defaultFluidState() : this.getFlowing(k, false); } } private static boolean canPassThroughWall(Direction direction, BlockGetter level, BlockPos pos, BlockState state, BlockPos spreadPos, BlockState spreadState) { VoxelShape voxelShape = spreadState.getCollisionShape(level, spreadPos); if (voxelShape == Shapes.block()) { return false; } else { VoxelShape voxelShape2 = state.getCollisionShape(level, pos); if (voxelShape2 == Shapes.block()) { return false; } else if (voxelShape2 == Shapes.empty() && voxelShape == Shapes.empty()) { return true; } else { Object2ByteLinkedOpenHashMap object2ByteLinkedOpenHashMap; if (!state.getBlock().hasDynamicShape() && !spreadState.getBlock().hasDynamicShape()) { object2ByteLinkedOpenHashMap = (Object2ByteLinkedOpenHashMap)OCCLUSION_CACHE.get(); } else { object2ByteLinkedOpenHashMap = null; } FlowingFluid.BlockStatePairKey blockStatePairKey; if (object2ByteLinkedOpenHashMap != null) { blockStatePairKey = new FlowingFluid.BlockStatePairKey(state, spreadState, direction); byte b = object2ByteLinkedOpenHashMap.getAndMoveToFirst(blockStatePairKey); if (b != 127) { return b != 0; } } else { blockStatePairKey = null; } boolean bl = !Shapes.mergedFaceOccludes(voxelShape2, voxelShape, direction); if (object2ByteLinkedOpenHashMap != null) { if (object2ByteLinkedOpenHashMap.size() == 200) { object2ByteLinkedOpenHashMap.removeLastByte(); } object2ByteLinkedOpenHashMap.putAndMoveToFirst(blockStatePairKey, (byte)(bl ? 1 : 0)); } return bl; } } } public abstract Fluid getFlowing(); public FluidState getFlowing(int level, boolean falling) { return this.getFlowing().defaultFluidState().setValue(LEVEL, level).setValue(FALLING, falling); } public abstract Fluid getSource(); public FluidState getSource(boolean falling) { return this.getSource().defaultFluidState().setValue(FALLING, falling); } protected abstract boolean canConvertToSource(ServerLevel level); protected void spreadTo(LevelAccessor level, BlockPos pos, BlockState blockState, Direction direction, FluidState fluidState) { if (blockState.getBlock() instanceof LiquidBlockContainer liquidBlockContainer) { liquidBlockContainer.placeLiquid(level, pos, blockState, fluidState); } else { if (!blockState.isAir()) { this.beforeDestroyingBlock(level, pos, blockState); } level.setBlock(pos, fluidState.createLegacyBlock(), 3); } } protected abstract void beforeDestroyingBlock(LevelAccessor level, BlockPos pos, BlockState state); protected int getSlopeDistance(LevelReader level, BlockPos pos, int depth, Direction direction, BlockState state, FlowingFluid.SpreadContext spreadContext) { int i = 1000; for (Direction direction2 : Direction.Plane.HORIZONTAL) { if (direction2 != direction) { BlockPos blockPos = pos.relative(direction2); BlockState blockState = spreadContext.getBlockState(blockPos); FluidState fluidState = blockState.getFluidState(); if (this.canPassThrough(level, this.getFlowing(), pos, state, direction2, blockPos, blockState, fluidState)) { if (spreadContext.isHole(blockPos)) { return depth; } if (depth < this.getSlopeFindDistance(level)) { int j = this.getSlopeDistance(level, blockPos, depth + 1, direction2.getOpposite(), blockState, spreadContext); if (j < i) { i = j; } } } } } return i; } boolean isWaterHole(BlockGetter level, BlockPos pos, BlockState state, BlockPos belowPos, BlockState belowState) { if (!canPassThroughWall(Direction.DOWN, level, pos, state, belowPos, belowState)) { return false; } else { return belowState.getFluidState().getType().isSame(this) ? true : canHoldFluid(level, belowPos, belowState, this.getFlowing()); } } private boolean canPassThrough( BlockGetter level, Fluid fluid, BlockPos pos, BlockState state, Direction direction, BlockPos spreadPos, BlockState spreadState, FluidState fluidState ) { return this.canMaybePassThrough(level, pos, state, direction, spreadPos, spreadState, fluidState) && canHoldSpecificFluid(level, spreadPos, spreadState, fluid); } private boolean canMaybePassThrough( BlockGetter level, BlockPos pos, BlockState state, Direction direction, BlockPos spreadPos, BlockState spreadState, FluidState fluidState ) { return !this.isSourceBlockOfThisType(fluidState) && canHoldAnyFluid(spreadState) && canPassThroughWall(direction, level, pos, state, spreadPos, spreadState); } private boolean isSourceBlockOfThisType(FluidState state) { return state.getType().isSame(this) && state.isSource(); } protected abstract int getSlopeFindDistance(LevelReader level); /** * Returns the number of immediately adjacent source blocks of the same fluid that lie on the horizontal plane. */ private int sourceNeighborCount(LevelReader level, BlockPos pos) { int i = 0; for (Direction direction : Direction.Plane.HORIZONTAL) { BlockPos blockPos = pos.relative(direction); FluidState fluidState = level.getFluidState(blockPos); if (this.isSourceBlockOfThisType(fluidState)) { i++; } } return i; } protected Map getSpread(ServerLevel level, BlockPos pos, BlockState state) { int i = 1000; Map map = Maps.newEnumMap(Direction.class); FlowingFluid.SpreadContext spreadContext = null; for (Direction direction : Direction.Plane.HORIZONTAL) { BlockPos blockPos = pos.relative(direction); BlockState blockState = level.getBlockState(blockPos); FluidState fluidState = blockState.getFluidState(); if (this.canMaybePassThrough(level, pos, state, direction, blockPos, blockState, fluidState)) { FluidState fluidState2 = this.getNewLiquid(level, blockPos, blockState); if (canHoldSpecificFluid(level, blockPos, blockState, fluidState2.getType())) { if (spreadContext == null) { spreadContext = new FlowingFluid.SpreadContext(level, pos); } int j; if (spreadContext.isHole(blockPos)) { j = 0; } else { j = this.getSlopeDistance(level, blockPos, 1, direction.getOpposite(), blockState, spreadContext); } if (j < i) { map.clear(); } if (j <= i) { if (fluidState.canBeReplacedWith(level, blockPos, fluidState2.getType(), direction)) { map.put(direction, fluidState2); } i = j; } } } } return map; } private static boolean canHoldAnyFluid(BlockState state) { Block block = state.getBlock(); if (block instanceof LiquidBlockContainer) { return true; } else { return state.blocksMotion() ? false : !(block instanceof DoorBlock) && !state.is(BlockTags.SIGNS) && !state.is(Blocks.LADDER) && !state.is(Blocks.SUGAR_CANE) && !state.is(Blocks.BUBBLE_COLUMN) && !state.is(Blocks.NETHER_PORTAL) && !state.is(Blocks.END_PORTAL) && !state.is(Blocks.END_GATEWAY) && !state.is(Blocks.STRUCTURE_VOID); } } private static boolean canHoldFluid(BlockGetter level, BlockPos pos, BlockState state, Fluid fluid) { return canHoldAnyFluid(state) && canHoldSpecificFluid(level, pos, state, fluid); } private static boolean canHoldSpecificFluid(BlockGetter level, BlockPos pos, BlockState state, Fluid fluid) { return state.getBlock() instanceof LiquidBlockContainer liquidBlockContainer ? liquidBlockContainer.canPlaceLiquid(null, level, pos, state, fluid) : true; } protected abstract int getDropOff(LevelReader level); protected int getSpreadDelay(Level level, BlockPos pos, FluidState currentState, FluidState newState) { return this.getTickDelay(level); } @Override public void tick(ServerLevel level, BlockPos pos, BlockState blockState, FluidState fluidState) { if (!fluidState.isSource()) { FluidState fluidState2 = this.getNewLiquid(level, pos, level.getBlockState(pos)); int i = this.getSpreadDelay(level, pos, fluidState, fluidState2); if (fluidState2.isEmpty()) { fluidState = fluidState2; blockState = Blocks.AIR.defaultBlockState(); level.setBlock(pos, blockState, 3); } else if (fluidState2 != fluidState) { fluidState = fluidState2; blockState = fluidState2.createLegacyBlock(); level.setBlock(pos, blockState, 3); level.scheduleTick(pos, fluidState2.getType(), i); } } this.spread(level, pos, blockState, fluidState); } protected static int getLegacyLevel(FluidState state) { return state.isSource() ? 0 : 8 - Math.min(state.getAmount(), 8) + (state.getValue(FALLING) ? 8 : 0); } private static boolean hasSameAbove(FluidState fluidState, BlockGetter level, BlockPos pos) { return fluidState.getType().isSame(level.getFluidState(pos.above()).getType()); } @Override public float getHeight(FluidState state, BlockGetter level, BlockPos pos) { return hasSameAbove(state, level, pos) ? 1.0F : state.getOwnHeight(); } @Override public float getOwnHeight(FluidState state) { return state.getAmount() / 9.0F; } @Override public abstract int getAmount(FluidState state); @Override public VoxelShape getShape(FluidState state, BlockGetter level, BlockPos pos) { return state.getAmount() == 9 && hasSameAbove(state, level, pos) ? Shapes.block() : (VoxelShape)this.shapes.computeIfAbsent(state, fluidState -> Shapes.box(0.0, 0.0, 0.0, 1.0, fluidState.getHeight(level, pos), 1.0)); } record BlockStatePairKey(BlockState first, BlockState second, Direction direction) { public boolean equals(Object object) { return object instanceof FlowingFluid.BlockStatePairKey blockStatePairKey && this.first == blockStatePairKey.first && this.second == blockStatePairKey.second && this.direction == blockStatePairKey.direction; } public int hashCode() { int i = System.identityHashCode(this.first); i = 31 * i + System.identityHashCode(this.second); return 31 * i + this.direction.hashCode(); } } protected class SpreadContext { private final BlockGetter level; private final BlockPos origin; private final Short2ObjectMap stateCache = new Short2ObjectOpenHashMap<>(); private final Short2BooleanMap holeCache = new Short2BooleanOpenHashMap(); SpreadContext(final BlockGetter level, final BlockPos origin) { this.level = level; this.origin = origin; } public BlockState getBlockState(BlockPos pos) { return this.getBlockState(pos, this.getCacheKey(pos)); } private BlockState getBlockState(BlockPos pos, short cacheKey) { return this.stateCache.computeIfAbsent(cacheKey, (Short2ObjectFunction)(s -> this.level.getBlockState(pos))); } public boolean isHole(BlockPos pos) { return this.holeCache.computeIfAbsent(this.getCacheKey(pos), (Short2BooleanFunction)(s -> { BlockState blockState = this.getBlockState(pos, s); BlockPos blockPos2 = pos.below(); BlockState blockState2 = this.level.getBlockState(blockPos2); return FlowingFluid.this.isWaterHole(this.level, pos, blockState, blockPos2, blockState2); })); } private short getCacheKey(BlockPos pos) { int i = pos.getX() - this.origin.getX(); int j = pos.getZ() - this.origin.getZ(); return (short)((i + 128 & 0xFF) << 8 | j + 128 & 0xFF); } } }