- 新增规则 turtleLocalNesting(默认 false)控制开关,关闭时走原版逻辑 & - 搬入 NestingSiteFinder(高性能就近沙滩搜索) & - 搬入 TurtleGoHomeGoalMixin / TurtleLayEggGoalMixin / TurtleAccessor / PathNavigationAccessor & - 原独立 mod turtle-reproduce 的功能并入本 Carpet 扩展 & - 补中英语言描述
This commit is contained in:
1 parent
6687583c96
commit
e1b1bed804
9 files changed
+474
-1
No files matched your search
@@ -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;
|
||||
}
|
||||
+11
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
+33
@@ -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<Boolean> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
@@ -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<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) {
|
||||
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<Boolean> 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<Boolean> 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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <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) {
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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的运算",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user