海龟就地下蛋mod
build / build (push) Has been cancelled

This commit is contained in:
AxTps committed 2026-10-03 08:40:13 +08:00
commit ab5eb31a28
28 files changed
+1936

No files matched your search

@@ -0,0 +1,9 @@
package com.mr.client
import net.fabricmc.api.ClientModInitializer
object TurtleReproduceClient : ClientModInitializer {
override fun onInitializeClient() {
// This entrypoint is suitable for setting up client-specific logic, such as rendering.
}
}
@@ -0,0 +1,9 @@
package com.mr.client
import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint
import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator
object TurtleReproduceDataGenerator : DataGeneratorEntrypoint {
override fun onInitializeDataGenerator(fabricDataGenerator: FabricDataGenerator) {
}
}
@@ -0,0 +1,202 @@
package com.mr.gametest;
import com.mr.mixin.TurtleAccessor;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.animal.turtle.Turtle;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.TurtleEggBlock;
import net.minecraft.world.phys.Vec3;
public class TurtleNestingGameTests {
private static final String SHORE = "turtle-reproduce-gametest:shore";
@GameTest(structure = SHORE, maxTicks = 1600)
public void breedingFarFromHomeLaysVanillaEggs(GameTestHelper helper) {
prepareFloor(helper);
makeShore(helper, 8, 9);
Turtle first = spawnTurtle(helper, 4.5, 10.5);
Turtle second = spawnTurtle(helper, 5.5, 10.5);
BlockPos firstHome = accessor(first).turtleReproduce$getHomePos();
BlockPos secondHome = accessor(second).turtleReproduce$getHomePos();
boolean[] becamePregnant = {false};
boolean[] dugSand = {false};
first.setInLove(null);
second.setInLove(null);
helper.onEachTick(() -> {
becamePregnant[0] |= first.hasEgg() || second.hasEgg();
dugSand[0] |= first.isLayingEgg() || second.isLayingEgg();
assertPregnantTurtleDoesNotGoHome(helper, first);
assertPregnantTurtleDoesNotGoHome(helper, second);
});
helper.succeedWhen(() -> {
BlockPos eggs = findEggsOnShore(helper, 8, 9);
helper.assertTrue(eggs != null, "The turtles should breed and lay eggs on the nearby shore");
helper.assertTrue(becamePregnant[0], "Breeding should produce a pregnant turtle");
helper.assertTrue(dugSand[0], "The turtle should perform the vanilla digging animation");
helper.assertFalse(first.hasEgg() || second.hasEgg(), "Laying should consume the carried eggs");
helper.assertValueEqual(accessor(first).turtleReproduce$getHomePos(), firstHome, "First birth position");
helper.assertValueEqual(accessor(second).turtleReproduce$getHomePos(), secondHome, "Second birth position");
assertVanillaEggCount(helper, eggs);
});
}
@GameTest(structure = SHORE, maxTicks = 1400)
public void choosesTheNearestReachableWaterShore(GameTestHelper helper) {
prepareFloor(helper);
makeShore(helper, 8, 9);
makeShore(helper, 19, 9);
Turtle turtle = spawnPregnantTurtle(helper, 4.5, 10.5);
expectEggs(helper, turtle, 8, 9, 19, 9);
}
@GameTest(structure = SHORE, maxTicks = 1800)
public void reachesAShoreBeyondTheVanillaSixteenBlockRange(GameTestHelper helper) {
prepareFloor(helper);
makeShore(helper, 22, 9);
Turtle turtle = spawnPregnantTurtle(helper, 3.5, 10.5);
expectEggs(helper, turtle, 22, 9, 8, 9);
}
// Vanilla turtles move slowly away from home and may pause to retry a long route.
@GameTest(structure = SHORE, maxTicks = 3000)
public void skipsAnEnclosedNearerShore(GameTestHelper helper) {
prepareFloor(helper);
makeShore(helper, 8, 7);
makeShore(helper, 19, 7);
// A three-block-high sealed enclosure makes the nearer sand unreachable.
for (int y = 2; y <= 4; y++) {
for (int x = 5; x <= 12; x++) {
helper.setBlock(x, y, 5, Blocks.STONE);
helper.setBlock(x, y, 12, Blocks.STONE);
}
for (int z = 5; z <= 12; z++) {
helper.setBlock(5, y, z, Blocks.STONE);
helper.setBlock(12, y, z, Blocks.STONE);
}
}
Turtle turtle = spawnPregnantTurtle(helper, 3.5, 8.5);
expectEggs(helper, turtle, 19, 7, 8, 7);
}
@GameTest(structure = SHORE, maxTicks = 650)
public void keepsEggsWithoutSandAndDoesNotReturnHome(GameTestHelper helper) {
prepareFloor(helper);
makeWater(helper, 8, 9);
Turtle turtle = spawnPregnantTurtle(helper, 4.5, 10.5);
BlockPos home = accessor(turtle).turtleReproduce$getHomePos();
helper.onEachTick(() -> {
helper.assertTrue(turtle.hasEgg(), "No suitable sand: the turtle must retain its eggs");
helper.assertFalse(turtle.isLayingEgg(), "The turtle must not dig on stone");
helper.assertFalse(accessor(turtle).turtleReproduce$isGoingHome(), "Pregnancy must not start a trip home");
helper.assertValueEqual(accessor(turtle).turtleReproduce$getHomePos(), home, "Birth position");
});
helper.runAtTickTime(600, helper::succeed);
}
@GameTest(structure = SHORE, maxTicks = 650)
public void drySandIsNotANestingShore(GameTestHelper helper) {
prepareFloor(helper);
for (int z = 9; z <= 11; z++) {
helper.setBlock(7, 1, z, Blocks.SAND);
}
Turtle turtle = spawnPregnantTurtle(helper, 4.5, 10.5);
helper.onEachTick(() -> {
helper.assertTrue(turtle.hasEgg(), "Sand without a nearby water source must not consume the eggs");
helper.assertFalse(turtle.isLayingEgg(), "Dry inland sand is not a valid shore");
helper.assertFalse(accessor(turtle).turtleReproduce$isGoingHome(), "Pregnancy must not start a trip home");
});
helper.runAtTickTime(600, helper::succeed);
}
private static void expectEggs(GameTestHelper helper, Turtle turtle,
int expectedWaterX, int expectedWaterZ,
int rejectedWaterX, int rejectedWaterZ) {
BlockPos home = accessor(turtle).turtleReproduce$getHomePos();
boolean[] dugSand = {false};
helper.onEachTick(() -> {
dugSand[0] |= turtle.isLayingEgg();
assertPregnantTurtleDoesNotGoHome(helper, turtle);
});
helper.succeedWhen(() -> {
BlockPos eggs = findEggsOnShore(helper, expectedWaterX, expectedWaterZ);
helper.assertTrue(eggs != null, "The turtle should lay eggs on the selected reachable shore");
helper.assertTrue(findEggsOnShore(helper, rejectedWaterX, rejectedWaterZ) == null,
"The other shore must not receive eggs");
helper.assertTrue(dugSand[0], "The turtle should dig before laying");
helper.assertFalse(turtle.hasEgg(), "The laid eggs should no longer be carried");
helper.assertValueEqual(accessor(turtle).turtleReproduce$getHomePos(), home, "Birth position");
assertVanillaEggCount(helper, eggs);
});
}
private static Turtle spawnTurtle(GameTestHelper helper, double x, double z) {
Turtle turtle = helper.spawn(EntityType.TURTLE, new Vec3(x, 2, z));
turtle.setAge(0);
turtle.setPersistenceRequired();
turtle.getRandom().setSeed(12345L);
turtle.setHomePos(helper.absolutePos(new BlockPos(2048, 2, 2048)));
return turtle;
}
private static Turtle spawnPregnantTurtle(GameTestHelper helper, double x, double z) {
Turtle turtle = spawnTurtle(helper, x, z);
accessor(turtle).turtleReproduce$setHasEgg(true);
return turtle;
}
private static void prepareFloor(GameTestHelper helper) {
for (int x = 0; x < 26; x++) {
for (int z = 0; z < 24; z++) {
helper.setBlock(x, 0, z, Blocks.STONE);
helper.setBlock(x, 1, z, Blocks.STONE);
for (int y = 2; y < 6; y++) {
helper.setBlock(x, y, z, Blocks.AIR);
}
}
}
}
private static void makeShore(GameTestHelper helper, int waterX, int waterZ) {
makeWater(helper, waterX, waterZ);
for (int z = waterZ; z < waterZ + 3; z++) {
helper.setBlock(waterX - 1, 1, z, Blocks.SAND);
}
}
private static void makeWater(GameTestHelper helper, int waterX, int waterZ) {
for (int x = waterX; x < waterX + 3; x++) {
for (int z = waterZ; z < waterZ + 3; z++) {
helper.setBlock(x, 1, z, Blocks.WATER);
}
}
}
private static BlockPos findEggsOnShore(GameTestHelper helper, int waterX, int waterZ) {
for (int z = waterZ; z < waterZ + 3; z++) {
BlockPos pos = new BlockPos(waterX - 1, 2, z);
if (helper.getBlockState(pos).is(Blocks.TURTLE_EGG)) {
return pos;
}
}
return null;
}
private static void assertVanillaEggCount(GameTestHelper helper, BlockPos pos) {
int count = helper.getBlockState(pos).getValue(TurtleEggBlock.EGGS);
helper.assertTrue(count >= 1 && count <= 4, "Vanilla laying should produce one to four hatchable eggs");
}
private static void assertPregnantTurtleDoesNotGoHome(GameTestHelper helper, Turtle turtle) {
if (turtle.hasEgg()) {
helper.assertFalse(accessor(turtle).turtleReproduce$isGoingHome(), "A pregnant turtle must ignore its distant home");
}
}
private static TurtleAccessor accessor(Turtle turtle) {
return (TurtleAccessor) turtle;
}
}
@@ -0,0 +1,113 @@
package com.mr.gametest;
import com.mr.ai.NestingSiteFinder;
import com.mr.mixin.TurtleAccessor;
import java.util.List;
import net.fabricmc.fabric.api.gametest.v1.GameTest;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.animal.turtle.Turtle;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.TurtleEggBlock;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.pathfinder.Path;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
public class WetNestingGameTests {
private static final BlockPos NEAR_SAND = new BlockPos(7, 1, 10);
private static final BlockPos FAR_SAND = new BlockPos(13, 1, 10);
@GameTest(structure = "turtle-reproduce-gametest:shore", maxTicks = 1800)
public void skipsDrySandWhoseStandingTurtleWouldTouchWater(GameTestHelper helper) {
prepareFloor(helper);
helper.setBlock(NEAR_SAND, Blocks.SAND);
helper.setBlock(FAR_SAND, Blocks.SAND);
helper.setBlock(8, 1, 10, Blocks.WATER);
helper.setBlock(15, 1, 10, Blocks.WATER);
// The pane's solid shape stays in the middle of its block. The turtle
// only extends 0.1 blocks into that block, touching fluid but no glass.
// Keep the pane north of the sand, outside the eastbound route to the
// fallback, so this tests destination validation rather than navigation
// through a waterlogged block's solid center.
// Place this last without neighbor updates so the fixture's contained
// water is not scheduled to flow across the otherwise dry sand.
helper.getLevel().setBlock(helper.absolutePos(new BlockPos(7, 2, 9)),
Blocks.GLASS_PANE.defaultBlockState().setValue(BlockStateProperties.WATERLOGGED, true),
Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
Turtle turtle = helper.spawn(EntityType.TURTLE, new Vec3(4.5, 2, 10.5));
turtle.setAge(0);
turtle.setPersistenceRequired();
turtle.getRandom().setSeed(12345L);
BlockPos home = helper.absolutePos(new BlockPos(2048, 2, 2048));
turtle.setHomePos(home);
BlockPos nearAbsolute = helper.absolutePos(NEAR_SAND);
BlockPos farAbsolute = helper.absolutePos(FAR_SAND);
Vec3 nearDestination = Vec3.atBottomCenterOf(nearAbsolute.above());
AABB nearBounds = turtle.getBoundingBox().move(nearDestination.subtract(turtle.position()));
assertWetButOtherwiseUsable(helper, turtle, nearAbsolute, nearBounds);
List<NestingSiteFinder.Candidate> candidates = NestingSiteFinder.findCandidates(turtle);
helper.assertTrue(!candidates.isEmpty() && candidates.getFirst().sandPos().equals(nearAbsolute),
"The wet standing position must be the first candidate before the body-fluid check");
helper.assertTrue(candidates.stream().anyMatch(candidate -> candidate.sandPos().equals(farAbsolute)),
"The dry fallback sand must also be a valid shore candidate");
Path nearPath = turtle.getNavigation().createPath(nearAbsolute.above(), 0, 48);
helper.assertTrue(nearPath != null && nearPath.canReach(),
"The near sand must have a navigation path, so only the body-fluid check rejects it");
((TurtleAccessor) turtle).turtleReproduce$setHasEgg(true);
boolean[] dugSand = {false};
helper.onEachTick(() -> {
// If the fixture ever floods, fail instead of mistaking ordinary
// invalid sand for the intended bounding-box regression coverage.
assertWetButOtherwiseUsable(helper, turtle, nearAbsolute, nearBounds);
dugSand[0] |= turtle.isLayingEgg();
if (turtle.hasEgg()) {
helper.assertFalse(((TurtleAccessor) turtle).turtleReproduce$isGoingHome(),
"A pregnant turtle must not return to its distant birth position");
}
});
helper.succeedWhen(() -> {
helper.assertTrue(helper.getBlockState(FAR_SAND.above()).is(Blocks.TURTLE_EGG),
"The turtle must lay real eggs on the farther, dry shore");
helper.assertFalse(helper.getBlockState(NEAR_SAND.above()).is(Blocks.TURTLE_EGG),
"Sand whose standing turtle touches water must not receive eggs");
helper.assertTrue(dugSand[0], "The turtle must perform the vanilla digging animation");
helper.assertFalse(turtle.hasEgg(), "Successful laying must consume the carried eggs");
int eggs = helper.getBlockState(FAR_SAND.above()).getValue(TurtleEggBlock.EGGS);
helper.assertTrue(eggs >= 1 && eggs <= 4, "The result must retain the vanilla egg count");
helper.assertValueEqual(((TurtleAccessor) turtle).turtleReproduce$getHomePos(), home,
"Birth position");
});
}
private static void assertWetButOtherwiseUsable(GameTestHelper helper, Turtle turtle,
BlockPos nearAbsolute, AABB nearBounds) {
helper.assertTrue(helper.getBlockState(NEAR_SAND.above()).isAir(),
"The near sand's own standing block must remain air");
helper.assertTrue(NestingSiteFinder.isValidSite(helper.getLevel(), nearAbsolute),
"The near sand must remain valid under the sand, air and source-water rules");
helper.assertTrue(helper.getLevel().noCollision(turtle, nearBounds),
"No solid collision may explain rejection of the near sand");
helper.assertTrue(helper.getLevel().containsAnyLiquid(nearBounds.deflate(0.001)),
"The standing turtle's width must overlap the neighboring waterlogged pane's fluid");
}
private static void prepareFloor(GameTestHelper helper) {
for (int x = 0; x < 26; x++) {
for (int z = 0; z < 24; z++) {
helper.setBlock(x, 0, z, Blocks.STONE);
helper.setBlock(x, 1, z, Blocks.STONE);
for (int y = 2; y < 6; y++) {
helper.setBlock(x, y, z, Blocks.AIR);
}
}
}
}
}
@@ -0,0 +1,7 @@
{
DataVersion: 2730,
size: [26, 6, 24],
data: [{pos: [0, 0, 0], state: "minecraft:air"}],
entities: [],
palette: ["minecraft:air"]
}
+17
View File
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"id": "turtle-reproduce-gametest",
"version": "1.0.0",
"name": "Turtle Reproduce Game Tests",
"environment": "*",
"entrypoints": {
"fabric-gametest": [
"com.mr.gametest.TurtleNestingGameTests",
"com.mr.gametest.WetNestingGameTests"
]
},
"depends": {
"turtle-reproduce": "*",
"fabric-gametest-api-v1": "*"
}
}
@@ -0,0 +1,254 @@
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.
*
* <p>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.
*
* <p>Every result-affecting rule is unchanged:
* <ul>
* <li>the same box is visited (32 in X/Z for nests, plus a 4 block halo for
* water, 8 up/down for nests, plus 1 for water);</li>
* <li>the same conditions are tested, in the same order;</li>
* <li>unloaded chunks are still skipped, and are still never loaded or
* generated ({@code loadOrGenerate = false});</li>
* <li>the candidate list is still ordered by the same comparator, which ends
* in the block coordinates and therefore is a total order. Visit order no
* longer matters, so the sort result is identical.</li>
* </ul>
*
* <p>What changed, and why it is equivalent:
* <ul>
* <li>blocks are read through the chunk section that holds them instead of
* {@code Level#getBlockState}, which re-resolves the chunk, the section
* index and the build-height check on every single block. Sections that
* {@code hasOnlyAir()} are skipped entirely: they cannot contribute water
* or sand;</li>
* <li>the same availability test as {@code Level#hasChunk} is used, but the
* resolved chunk is kept for the section reads instead of being thrown
* away and looked up again per block;</li>
* <li>sand positions are collected as packed longs instead of thousands of
* {@code BlockPos} objects, so a scan allocates no per-block garbage;</li>
* <li>{@code isValidSite} hoists its loop invariants and remembers the last
* chunk availability answer; the disk it walks spans at most four chunks,
* so this only removes repeated lookups.</li>
* </ul>
*/
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;
public static final int WATER_VERTICAL_RADIUS = 1;
private static final Comparator<Candidate> CANDIDATE_ORDER = Comparator
.comparingDouble(Candidate::waterDistanceSquared)
.thenComparingDouble(Candidate::sandDistanceSquared)
.thenComparingInt(candidate -> candidate.sandPos().getX())
.thenComparingInt(candidate -> candidate.sandPos().getY())
.thenComparingInt(candidate -> candidate.sandPos().getZ());
private NestingSiteFinder() {
}
/**
* Searches a square extending 32 blocks in X/Z and 8 blocks in Y from the
* 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<Candidate> findCandidates(Turtle turtle) {
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(), 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();
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.
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));
}
}
}
}
}
}
}
}
List<Candidate> candidates = new ArrayList<>();
if (waterDistances.isEmpty()) {
return candidates;
}
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(sandX + dx, sandY + dy, sandZ + dz);
nearestWaterDistance = Math.min(nearestWaterDistance, waterDistances.get(waterPosition));
}
}
}
if (Double.isFinite(nearestWaterDistance)) {
cursor.set(sandX, sandY, sandZ);
candidates.add(new Candidate(new BlockPos(sandX, sandY, sandZ), nearestWaterDistance,
cursor.distToCenterSqr(turtlePos)));
}
}
candidates.sort(CANDIDATE_ORDER);
return candidates;
}
/** Rechecks a chosen nest, including changes to its sand, air, or water. */
public static boolean isValidSite(LevelReader level, BlockPos sandPos) {
if (!level.isInsideBuildHeight(sandPos) || !level.isInsideBuildHeight(sandPos.above())
|| !level.hasChunkAt(sandPos)
|| !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 = 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++) {
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.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(cursor.set(x, y + 1, z));
}
public record Candidate(BlockPos sandPos, double waterDistanceSquared, double sandDistanceSquared) {
}
}
@@ -0,0 +1,11 @@
package com.mr.mixin;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(PathNavigation.class)
public interface PathNavigationAccessor {
@Accessor("maxVisitedNodesMultiplier")
float turtleReproduce$getMaxVisitedNodesMultiplier();
}
@@ -0,0 +1,22 @@
package com.mr.mixin;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.animal.turtle.Turtle;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(Turtle.class)
public interface TurtleAccessor {
@Invoker("setHasEgg")
void turtleReproduce$setHasEgg(boolean hasEgg);
@Invoker("setLayingEgg")
void turtleReproduce$setLayingEgg(boolean layingEgg);
@Accessor("homePos")
BlockPos turtleReproduce$getHomePos();
@Accessor("goingHome")
boolean turtleReproduce$isGoingHome();
}
@@ -0,0 +1,32 @@
package com.mr.mixin;
import net.minecraft.world.entity.animal.turtle.Turtle;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(targets = "net.minecraft.world.entity.animal.turtle.Turtle$TurtleGoHomeGoal")
public abstract class TurtleGoHomeGoalMixin {
@Shadow @Final
private Turtle turtle;
// This goal has no MOVE flag, so priority alone cannot stop it fighting nesting.
@Inject(method = {"canUse", "canContinueToUse"}, at = @At("HEAD"), cancellable = true)
private void turtleReproduce$stayLocalWhileCarryingEggs(CallbackInfoReturnable<Boolean> cir) {
if (this.turtle.hasEgg()) {
cir.setReturnValue(false);
}
}
@Inject(method = "stop", at = @At("TAIL"))
private void turtleReproduce$discardOldHomeRoute(CallbackInfo ci) {
if (this.turtle.hasEgg()) {
// The vanilla stop only clears goingHome, leaving its last route active.
this.turtle.getNavigation().stop();
}
}
}
@@ -0,0 +1,129 @@
package com.mr.mixin;
import com.mr.ai.NestingSiteFinder;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.MoveToBlockGoal;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.entity.animal.turtle.Turtle;
import net.minecraft.world.level.LevelReader;
import net.minecraft.world.level.pathfinder.Path;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(targets = "net.minecraft.world.entity.animal.turtle.Turtle$TurtleLayEggGoal")
public abstract class TurtleLayEggGoalMixin extends MoveToBlockGoal {
@Unique private static final int PATH_SEARCH_RANGE = 48;
@Unique private static final int PATH_ATTEMPTS_PER_CHECK = 16;
@Shadow @Final
private Turtle turtle;
@Unique private List<NestingSiteFinder.Candidate> turtleReproduce$candidates = List.of();
@Unique private int turtleReproduce$candidateIndex;
@Unique private BlockPos turtleReproduce$searchOrigin = BlockPos.ZERO;
@Unique private Path turtleReproduce$nestingPath;
protected TurtleLayEggGoalMixin(PathfinderMob mob, double speed, int range) {
super(mob, speed, range);
}
@Inject(method = "canUse", at = @At("HEAD"), cancellable = true)
private void turtleReproduce$findLocalShore(CallbackInfoReturnable<Boolean> cir) {
cir.setReturnValue(this.turtle.hasEgg() && !this.turtle.isBaby() && super.canUse());
}
@Inject(method = "canContinueToUse", at = @At("HEAD"), cancellable = true)
private void turtleReproduce$continueAwayFromHome(CallbackInfoReturnable<Boolean> cir) {
cir.setReturnValue(this.turtle.hasEgg() && super.canContinueToUse());
}
@Inject(method = "isValidTarget", at = @At("HEAD"), cancellable = true)
private void turtleReproduce$requireDrySandNearWater(LevelReader level, BlockPos pos,
CallbackInfoReturnable<Boolean> cir) {
cir.setReturnValue(NestingSiteFinder.isValidSite(level, pos));
}
@Override
protected boolean findNearestBlock() {
if (this.turtleReproduce$candidateIndex >= this.turtleReproduce$candidates.size()
|| this.turtle.blockPosition().distSqr(this.turtleReproduce$searchOrigin) > 16.0) {
this.turtleReproduce$candidates = NestingSiteFinder.findCandidates(this.turtle);
this.turtleReproduce$candidateIndex = 0;
this.turtleReproduce$searchOrigin = this.turtle.blockPosition();
}
// Spread expensive path checks over AI updates when a shore is inaccessible.
int attempts = 0;
while (this.turtleReproduce$candidateIndex < this.turtleReproduce$candidates.size()
&& attempts++ < PATH_ATTEMPTS_PER_CHECK) {
BlockPos sand = this.turtleReproduce$candidates.get(this.turtleReproduce$candidateIndex++).sandPos();
if (!this.turtle.isWithinHome(sand) || !NestingSiteFinder.isValidSite(this.turtle.level(), sand)) {
continue;
}
Vec3 destination = Vec3.atBottomCenterOf(sand.above());
AABB destinationBounds = this.turtle.getBoundingBox().move(destination.subtract(this.turtle.position()));
// Adult turtles are wider than one block. An air block alone does not
// guarantee that their body is clear of adjacent water or walls.
if (!this.turtle.level().noCollision(this.turtle, destinationBounds)
|| this.turtle.level().containsAnyLiquid(destinationBounds.deflate(0.001))) {
continue;
}
Path path = this.turtleReproduce$createNestingPath(sand.above());
if (path != null && path.canReach()) {
this.blockPos = sand;
this.turtleReproduce$nestingPath = path;
this.turtleReproduce$candidates = List.of();
return true;
}
}
if (this.turtleReproduce$candidateIndex < this.turtleReproduce$candidates.size()) {
this.nextStartTick = 1;
}
return false;
}
@Unique
private Path turtleReproduce$createNestingPath(BlockPos destination) {
PathNavigation navigation = this.turtle.getNavigation();
float previousMultiplier = ((PathNavigationAccessor) navigation).turtleReproduce$getMaxVisitedNodesMultiplier();
// A larger distance alone leaves vanilla's small node budget unchanged,
// incorrectly rejecting shores that require a detour around an obstacle.
navigation.setMaxVisitedNodesMultiplier(Math.max(previousMultiplier, 8.0F));
try {
return navigation.createPath(destination, 0, PATH_SEARCH_RANGE);
} finally {
navigation.setMaxVisitedNodesMultiplier(previousMultiplier);
}
}
@Override
protected void moveMobToBlock() {
this.turtle.getNavigation().moveTo(this.turtleReproduce$nestingPath, this.speedModifier);
}
@Inject(method = "tick", at = @At("TAIL"))
private void turtleReproduce$retryLongerPath(CallbackInfo ci) {
if (!this.isReachedTarget() && this.tryTicks % 40 == 0 && this.turtle.getNavigation().isDone()) {
this.turtleReproduce$nestingPath = this.turtleReproduce$createNestingPath(this.blockPos.above());
this.moveMobToBlock();
}
}
@Override
public void stop() {
super.stop();
this.turtle.getNavigation().stop();
((TurtleAccessor) this.turtle).turtleReproduce$setLayingEgg(false);
this.turtleReproduce$nestingPath = null;
this.turtleReproduce$candidates = List.of();
}
}
+18
View File
@@ -0,0 +1,18 @@
package com.mr
import net.fabricmc.api.ModInitializer
import net.minecraft.resources.Identifier
import org.slf4j.LoggerFactory
object TurtleReproduce : ModInitializer {
const val MOD_ID: String = "turtle-reproduce"
private val LOGGER = LoggerFactory.getLogger(MOD_ID)
override fun onInitialize() {
LOGGER.info("Turtle Reproduce: local waterside nesting enabled")
}
fun id(path: String): Identifier
= Identifier.fromNamespaceAndPath(MOD_ID, path)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

+43
View File
@@ -0,0 +1,43 @@
{
"schemaVersion": 1,
"id": "turtle-reproduce",
"version": "${version}",
"name": "Turtle-Reproduce",
"description": "Turtles lay eggs on reachable sand near the closest local water source instead of returning to their birthplace.",
"authors": [
"Me!"
],
"license": "CC0-1.0",
"icon": "assets/turtle-reproduce/icon.png",
"environment": "*",
"entrypoints": {
"main": [
{
"value": "com.mr.TurtleReproduce",
"adapter": "kotlin"
}
],
"client": [
{
"value": "com.mr.client.TurtleReproduceClient",
"adapter": "kotlin"
}
],
"fabric-datagen": [
{
"value": "com.mr.client.TurtleReproduceDataGenerator",
"adapter": "kotlin"
}
]
},
"mixins": [
"turtle-reproduce.mixins.json"
],
"depends": {
"fabricloader": ">=0.19.5",
"minecraft": "~26.1.2",
"java": ">=25",
"fabric-api": "*",
"fabric-language-kotlin": "*"
}
}
@@ -0,0 +1,17 @@
{
"required": true,
"package": "com.mr.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": [
"PathNavigationAccessor",
"TurtleAccessor",
"TurtleGoHomeGoalMixin",
"TurtleLayEggGoalMixin"
],
"injectors": {
"defaultRequire": 1
},
"overwrites": {
"requireAnnotations": true
}
}