diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/SLSCarpetSettings.java b/src/main/java/com/github/zly2006/carpetslsaddition/SLSCarpetSettings.java index 2f4de64..fb647b2 100644 --- a/src/main/java/com/github/zly2006/carpetslsaddition/SLSCarpetSettings.java +++ b/src/main/java/com/github/zly2006/carpetslsaddition/SLSCarpetSettings.java @@ -68,4 +68,7 @@ public class SLSCarpetSettings { @Rule(categories = {SLSA, RuleCategory.FEATURE}) public static int trialSpawnerCD = -1; + + @Rule(categories = {SLSA, RuleCategory.FEATURE}) + public static boolean turtleLocalNesting = false; } diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/PathNavigationAccessor.java b/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/PathNavigationAccessor.java new file mode 100644 index 0000000..8c238ea --- /dev/null +++ b/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/PathNavigationAccessor.java @@ -0,0 +1,11 @@ +package com.github.zly2006.carpetslsaddition.mixin.turtle; + +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(); +} diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/TurtleAccessor.java b/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/TurtleAccessor.java new file mode 100644 index 0000000..42f02ff --- /dev/null +++ b/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/TurtleAccessor.java @@ -0,0 +1,22 @@ +package com.github.zly2006.carpetslsaddition.mixin.turtle; + +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(); +} diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/TurtleGoHomeGoalMixin.java b/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/TurtleGoHomeGoalMixin.java new file mode 100644 index 0000000..d9a0019 --- /dev/null +++ b/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/TurtleGoHomeGoalMixin.java @@ -0,0 +1,33 @@ +package com.github.zly2006.carpetslsaddition.mixin.turtle; + +import com.github.zly2006.carpetslsaddition.SLSCarpetSettings; +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 cir) { + if (SLSCarpetSettings.turtleLocalNesting && this.turtle.hasEgg()) { + cir.setReturnValue(false); + } + } + + @Inject(method = "stop", at = @At("TAIL")) + private void turtleReproduce$discardOldHomeRoute(CallbackInfo ci) { + if (SLSCarpetSettings.turtleLocalNesting && this.turtle.hasEgg()) { + // The vanilla stop only clears goingHome, leaving its last route active. + this.turtle.getNavigation().stop(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/TurtleLayEggGoalMixin.java b/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/TurtleLayEggGoalMixin.java new file mode 100644 index 0000000..73825d1 --- /dev/null +++ b/src/main/java/com/github/zly2006/carpetslsaddition/mixin/turtle/TurtleLayEggGoalMixin.java @@ -0,0 +1,142 @@ +package com.github.zly2006.carpetslsaddition.mixin.turtle; + +import com.github.zly2006.carpetslsaddition.SLSCarpetSettings; +import com.github.zly2006.carpetslsaddition.turtle.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 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 cir) { + if (!SLSCarpetSettings.turtleLocalNesting) return; + cir.setReturnValue(this.turtle.hasEgg() && !this.turtle.isBaby() && super.canUse()); + } + + @Inject(method = "canContinueToUse", at = @At("HEAD"), cancellable = true) + private void turtleReproduce$continueAwayFromHome(CallbackInfoReturnable cir) { + if (!SLSCarpetSettings.turtleLocalNesting) return; + cir.setReturnValue(this.turtle.hasEgg() && super.canContinueToUse()); + } + + @Inject(method = "isValidTarget", at = @At("HEAD"), cancellable = true) + private void turtleReproduce$requireDrySandNearWater(LevelReader level, BlockPos pos, + CallbackInfoReturnable cir) { + if (!SLSCarpetSettings.turtleLocalNesting) return; + cir.setReturnValue(NestingSiteFinder.isValidSite(level, pos)); + } + + @Override + protected boolean findNearestBlock() { + if (!SLSCarpetSettings.turtleLocalNesting) { + return super.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() { + if (!SLSCarpetSettings.turtleLocalNesting) { + super.moveMobToBlock(); + return; + } + this.turtle.getNavigation().moveTo(this.turtleReproduce$nestingPath, this.speedModifier); + } + + @Inject(method = "tick", at = @At("TAIL")) + private void turtleReproduce$retryLongerPath(CallbackInfo ci) { + if (!SLSCarpetSettings.turtleLocalNesting) return; + 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(); + if (!SLSCarpetSettings.turtleLocalNesting) return; + this.turtle.getNavigation().stop(); + ((TurtleAccessor) this.turtle).turtleReproduce$setLayingEgg(false); + this.turtleReproduce$nestingPath = null; + this.turtleReproduce$candidates = List.of(); + } +} diff --git a/src/main/java/com/github/zly2006/carpetslsaddition/turtle/NestingSiteFinder.java b/src/main/java/com/github/zly2006/carpetslsaddition/turtle/NestingSiteFinder.java new file mode 100644 index 0000000..04d6008 --- /dev/null +++ b/src/main/java/com/github/zly2006/carpetslsaddition/turtle/NestingSiteFinder.java @@ -0,0 +1,254 @@ +package com.github.zly2006.carpetslsaddition.turtle; + +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. + * + *

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: + *

    + *
  • 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);
  • + *
  • the same conditions are tested, in the same order;
  • + *
  • unloaded chunks are still skipped, and are still never loaded or + * generated ({@code loadOrGenerate = false});
  • + *
  • 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.
  • + *
+ * + *

What changed, and why it is equivalent: + *

    + *
  • 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;
  • + *
  • 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;
  • + *
  • sand positions are collected as packed longs instead of thousands of + * {@code BlockPos} objects, so a scan allocates no per-block garbage;
  • + *
  • {@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.
  • + *
+ */ +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_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 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 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) { + } +} diff --git a/src/main/resources/assets/slsaddition/lang/en_us.json b/src/main/resources/assets/slsaddition/lang/en_us.json index c5ed7bd..05183b3 100644 --- a/src/main/resources/assets/slsaddition/lang/en_us.json +++ b/src/main/resources/assets/slsaddition/lang/en_us.json @@ -50,6 +50,8 @@ "carpet.rule.spectatorCannotUseLeash.desc": "If a player is a spectator, they cannot use the leash.", "carpet.rule.offlineFakePlayers.desc": "假人强制离线uuid", "carpet.rule.armadilloImmediateDespawns.desc": "Armadillos will despawn immediately if they are 128 blocks away from the player.", + "carpet.rule.turtleLocalNesting.name": "Turtle Local Nesting", + "carpet.rule.turtleLocalNesting.desc": "Turtles carrying eggs will lay them on reachable sand near the closest local water source instead of returning to their birthplace.", "carpet.rule.trialSpawnerCD.desc": "trialSpawnerCD.", "carpet.rule.skipTicksForJoblessVillager.extra.0": "Set to 0 to maintain the vanilla behavior", diff --git a/src/main/resources/assets/slsaddition/lang/zh_cn.json b/src/main/resources/assets/slsaddition/lang/zh_cn.json index e7f6b35..5d4337a 100644 --- a/src/main/resources/assets/slsaddition/lang/zh_cn.json +++ b/src/main/resources/assets/slsaddition/lang/zh_cn.json @@ -26,6 +26,8 @@ "carpet.rule.botMaxOnlineTime.name": "Bot最大存活时间", "carpet.rule.spectatorCannotUseLeash.name": "观察者模式禁用拴绳", "carpet.rule.armadilloImmediateDespawns.desc": "当犰狳远离玩家128格时,立即消失", + "carpet.rule.turtleLocalNesting.name": "海龟就地筑巢", + "carpet.rule.turtleLocalNesting.desc": "有蛋的海龟会在附近的水边沙滩下蛋,而不是返回出生地", "carpet.rule.obtainableReinforcedDeepSlate.desc": "当铁砧砸在9*64个深板岩的掉落物上时,可以获得强化深板岩", "carpet.rule.skipTicksForJoblessVillager.desc": "对于无业/傻子村民,执行设定数量的tick运算后,跳过1tick的运算", diff --git a/src/main/resources/slsaddition.mixins.json b/src/main/resources/slsaddition.mixins.json index 17082f9..a3833cd 100644 --- a/src/main/resources/slsaddition.mixins.json +++ b/src/main/resources/slsaddition.mixins.json @@ -32,7 +32,11 @@ "shulker.MixinServerPlayNetworkHandler", "shulker.MixinSlot", "world.MixinSleepManager", - "world.MixinSpawnHelper" + "world.MixinSpawnHelper", + "turtle.PathNavigationAccessor", + "turtle.TurtleAccessor", + "turtle.TurtleGoHomeGoalMixin", + "turtle.TurtleLayEggGoalMixin" ], "injectors": { "defaultRequire": 1