--- a/src/main/java/com/mr/ai/NestingSiteFinder.java +++ b/src/main/java/com/mr/ai/NestingSiteFinder.java @@ -1,21 +1,64 @@ package com.mr.ai; import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; import net.minecraft.tags.BlockTags; import net.minecraft.tags.FluidTags; import net.minecraft.world.entity.animal.turtle.Turtle; +import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelReader; import net.minecraft.world.level.block.TurtleEggBlock; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.Vec3; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -/** Finds dry nesting blocks beside nearby, exposed source water. */ +/** + * Finds dry nesting blocks beside nearby, exposed source water. + * + *

This class was rewritten for throughput. The scan is the hot path: one call + * covers 73 x 73 columns x 19 vertical steps, and a pregnant turtle re-runs it + * whenever it drifts more than four blocks from the previous search origin or + * exhausts its cached candidate list. + * + *

Every result-affecting rule is unchanged: + *

+ * + *

What changed, and why it is equivalent: + *

+ */ public final class NestingSiteFinder { public static final int SEARCH_RADIUS = 32; public static final int VERTICAL_SEARCH_RADIUS = 8; public static final int WATER_RADIUS = 4; @@ -36,38 +79,82 @@ * turtle. A four-block halo includes water beside nests at the boundary. * The caller must check the turtle's collision box and navigation path. */ public static List findCandidates(Turtle turtle) { - LevelReader level = turtle.level(); + Level level = turtle.level(); BlockPos origin = turtle.blockPosition(); + int originX = origin.getX(); + int originY = origin.getY(); + int originZ = origin.getZ(); int scanRadius = SEARCH_RADIUS + WATER_RADIUS; - int minY = Math.max(level.getMinY(), origin.getY() - VERTICAL_SEARCH_RADIUS - WATER_VERTICAL_RADIUS); - int maxY = Math.min(level.getMaxY() - 1, origin.getY() + VERTICAL_SEARCH_RADIUS + WATER_VERTICAL_RADIUS); + int minY = Math.max(level.getMinY(), originY - VERTICAL_SEARCH_RADIUS - WATER_VERTICAL_RADIUS); + int maxY = Math.min(level.getMaxY() - 1, originY + VERTICAL_SEARCH_RADIUS + WATER_VERTICAL_RADIUS); + int nestMinX = originX - SEARCH_RADIUS; + int nestMaxX = originX + SEARCH_RADIUS; + int nestMinZ = originZ - SEARCH_RADIUS; + int nestMaxZ = originZ + SEARCH_RADIUS; + int nestMinY = originY - VERTICAL_SEARCH_RADIUS; + int nestMaxY = originY + VERTICAL_SEARCH_RADIUS; + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); - List sandBlocks = new ArrayList<>(); + LongArrayList sandBlocks = new LongArrayList(); Long2DoubleOpenHashMap waterDistances = new Long2DoubleOpenHashMap(); waterDistances.defaultReturnValue(Double.POSITIVE_INFINITY); // Read each block once, instead of repeatedly querying the world around // every sand block. Unloaded chunks are never requested or generated. - for (int dx = -scanRadius; dx <= scanRadius; dx++) { - int x = origin.getX() + dx; - for (int dz = -scanRadius; dz <= scanRadius; dz++) { - int z = origin.getZ() + dz; - if (!level.hasChunk(x >> 4, z >> 4)) { - continue; - } - boolean inNestRange = Math.abs(dx) <= SEARCH_RADIUS && Math.abs(dz) <= SEARCH_RADIUS; - for (int y = minY; y <= maxY; y++) { - cursor.set(x, y, z); - BlockState state = level.getBlockState(cursor); - if (isExposedSourceWater(level, cursor, state)) { - waterDistances.put(cursor.asLong(), cursor.distToCenterSqr(turtle.position())); - } - if (inNestRange && Math.abs(y - origin.getY()) <= VERTICAL_SEARCH_RADIUS - && state.is(BlockTags.SAND) - && level.isEmptyBlock(cursor.above())) { - sandBlocks.add(cursor.immutable()); + if (minY <= maxY) { + Vec3 turtlePos = turtle.position(); + int minSection = level.getSectionIndex(minY); + int maxSection = level.getSectionIndex(maxY); + for (int chunkX = (originX - scanRadius) >> 4; chunkX <= (originX + scanRadius) >> 4; chunkX++) { + int fromX = Math.max(originX - scanRadius, chunkX << 4); + int toX = Math.min(originX + scanRadius, (chunkX << 4) + 15); + for (int chunkZ = (originZ - scanRadius) >> 4; chunkZ <= (originZ + scanRadius) >> 4; chunkZ++) { + // The same test Level#hasChunk performs, except the chunk is + // kept: loadOrGenerate = false never loads or generates. + ChunkAccess chunk = level.getChunk(chunkX, chunkZ, ChunkStatus.FULL, false); + if (chunk == null) { + continue; + } + int fromZ = Math.max(originZ - scanRadius, chunkZ << 4); + int toZ = Math.min(originZ + scanRadius, (chunkZ << 4) + 15); + LevelChunkSection[] sections = chunk.getSections(); + for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) { + if (sectionIndex < 0 || sectionIndex >= sections.length) { + continue; + } + LevelChunkSection section = sections[sectionIndex]; + // An all-air section holds neither water nor sand, so + // skipping it reads no block the loops could have used. + if (section == null || section.hasOnlyAir()) { + continue; + } + int sectionMinY = SectionPos.sectionToBlockCoord(level.getMinSectionY() + sectionIndex); + int fromY = Math.max(minY, sectionMinY); + int toY = Math.min(maxY, sectionMinY + 15); + boolean nestSection = fromY <= nestMaxY && toY >= nestMinY; + for (int x = fromX; x <= toX; x++) { + int localX = x & 15; + boolean nestX = x >= nestMinX && x <= nestMaxX; + for (int z = fromZ; z <= toZ; z++) { + boolean inNestRange = nestSection && nestX && z >= nestMinZ && z <= nestMaxZ; + int localZ = z & 15; + for (int y = fromY; y <= toY; y++) { + BlockState state = section.getBlockState(localX, y & 15, localZ); + if (isExposedSourceWater(level, cursor, x, y, z, state)) { + cursor.set(x, y, z); + waterDistances.put(BlockPos.asLong(x, y, z), + cursor.distToCenterSqr(turtlePos)); + } + if (inNestRange && y >= nestMinY && y <= nestMaxY + && state.is(BlockTags.SAND) + && level.isEmptyBlock(cursor.set(x, y + 1, z))) { + sandBlocks.add(BlockPos.asLong(x, y, z)); + } + } + } + } } } } } @@ -75,23 +162,30 @@ List candidates = new ArrayList<>(); if (waterDistances.isEmpty()) { return candidates; } - for (BlockPos sand : sandBlocks) { + Vec3 turtlePos = turtle.position(); + for (int index = 0; index < sandBlocks.size(); index++) { + long packedSand = sandBlocks.getLong(index); + int sandX = BlockPos.getX(packedSand); + int sandY = BlockPos.getY(packedSand); + int sandZ = BlockPos.getZ(packedSand); double nearestWaterDistance = Double.POSITIVE_INFINITY; for (int dx = -WATER_RADIUS; dx <= WATER_RADIUS; dx++) { for (int dz = -WATER_RADIUS; dz <= WATER_RADIUS; dz++) { if (dx * dx + dz * dz > WATER_RADIUS * WATER_RADIUS) { continue; } for (int dy = -WATER_VERTICAL_RADIUS; dy <= WATER_VERTICAL_RADIUS; dy++) { - long waterPosition = BlockPos.asLong(sand.getX() + dx, sand.getY() + dy, sand.getZ() + dz); + long waterPosition = BlockPos.asLong(sandX + dx, sandY + dy, sandZ + dz); nearestWaterDistance = Math.min(nearestWaterDistance, waterDistances.get(waterPosition)); } } } if (Double.isFinite(nearestWaterDistance)) { - candidates.add(new Candidate(sand, nearestWaterDistance, sand.distToCenterSqr(turtle.position()))); + cursor.set(sandX, sandY, sandZ); + candidates.add(new Candidate(new BlockPos(sandX, sandY, sandZ), nearestWaterDistance, + cursor.distToCenterSqr(turtlePos))); } } candidates.sort(CANDIDATE_ORDER); return candidates; @@ -104,39 +198,56 @@ || !TurtleEggBlock.isSand(level, sandPos) || !level.isEmptyBlock(sandPos.above())) { return false; } + int sandX = sandPos.getX(); + int sandY = sandPos.getY(); + int sandZ = sandPos.getZ(); BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + int lastChunkX = Integer.MIN_VALUE; + int lastChunkZ = Integer.MIN_VALUE; + boolean lastChunkLoaded = false; for (int dx = -WATER_RADIUS; dx <= WATER_RADIUS; dx++) { for (int dz = -WATER_RADIUS; dz <= WATER_RADIUS; dz++) { if (dx * dx + dz * dz > WATER_RADIUS * WATER_RADIUS) { continue; } - int x = sandPos.getX() + dx; - int z = sandPos.getZ() + dz; - if (!level.hasChunk(x >> 4, z >> 4)) { + int x = sandX + dx; + int z = sandZ + dz; + int chunkX = x >> 4; + int chunkZ = z >> 4; + // The disk above spans at most four chunks, so this only removes + // repeated queries for columns that share a chunk. + if (chunkX != lastChunkX || chunkZ != lastChunkZ) { + lastChunkX = chunkX; + lastChunkZ = chunkZ; + lastChunkLoaded = level.hasChunk(chunkX, chunkZ); + } + if (!lastChunkLoaded) { continue; } for (int dy = -WATER_VERTICAL_RADIUS; dy <= WATER_VERTICAL_RADIUS; dy++) { - cursor.set(x, sandPos.getY() + dy, z); - if (level.isInsideBuildHeight(cursor) && level.isInsideBuildHeight(cursor.above()) - && isExposedSourceWater(level, cursor, level.getBlockState(cursor))) { + int y = sandY + dy; + cursor.set(x, y, z); + if (level.isInsideBuildHeight(y) && level.isInsideBuildHeight(y + 1) + && isExposedSourceWater(level, cursor, x, y, z, level.getBlockState(cursor))) { return true; } } } } return false; } - private static boolean isExposedSourceWater(LevelReader level, BlockPos pos, BlockState state) { + private static boolean isExposedSourceWater(LevelReader level, BlockPos.MutableBlockPos cursor, + int x, int y, int z, BlockState state) { // Requiring a liquid block excludes waterlogged fences, slabs and other // solid blocks even though their fluid state may report source water. if (!state.liquid()) { return false; } FluidState fluid = state.getFluidState(); - return fluid.is(FluidTags.WATER) && fluid.isSource() && level.isEmptyBlock(pos.above()); + return fluid.is(FluidTags.WATER) && fluid.isSource() && level.isEmptyBlock(cursor.set(x, y + 1, z)); } public record Candidate(BlockPos sandPos, double waterDistanceSquared, double sandDistanceSquared) { }