/* * Decompiled with CFR 0.152. * * Could not load the following classes: * org.jspecify.annotations.Nullable */ package net.minecraft.server.level; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Supplier; import net.minecraft.CrashReport; import net.minecraft.CrashReportCategory; import net.minecraft.ReportedException; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.SectionPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.TicketType; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.world.entity.EntityDimensions; import net.minecraft.world.entity.EntityType; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.CollisionGetter; import net.minecraft.world.level.GameType; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.gamerules.GameRules; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.phys.Vec3; import org.jspecify.annotations.Nullable; public class PlayerSpawnFinder { private static final EntityDimensions PLAYER_DIMENSIONS = EntityType.PLAYER.getDimensions(); private static final int ABSOLUTE_MAX_ATTEMPTS = 1024; private final ServerLevel level; private final BlockPos spawnSuggestion; private final int radius; private final int candidateCount; private final int coprime; private final int offset; private int nextCandidateIndex; private final CompletableFuture finishedFuture = new CompletableFuture(); private PlayerSpawnFinder(ServerLevel level, BlockPos spawnSuggestion, int radius) { this.level = level; this.spawnSuggestion = spawnSuggestion; this.radius = radius; long squareSide = (long)radius * 2L + 1L; this.candidateCount = (int)Math.min(1024L, squareSide * squareSide); this.coprime = PlayerSpawnFinder.getCoprime(this.candidateCount); this.offset = RandomSource.create().nextInt(this.candidateCount); } public static CompletableFuture findSpawn(ServerLevel level, BlockPos spawnSuggestion) { if (!level.dimensionType().hasSkyLight() || level.getServer().getWorldData().getGameType() == GameType.ADVENTURE) { return CompletableFuture.completedFuture(PlayerSpawnFinder.fixupSpawnHeight(level, spawnSuggestion)); } int radius = Math.max(0, level.getGameRules().get(GameRules.RESPAWN_RADIUS)); int distToBorder = Mth.floor(level.getWorldBorder().getDistanceToBorder(spawnSuggestion.getX(), spawnSuggestion.getZ())); if (distToBorder < radius) { radius = distToBorder; } if (distToBorder <= 1) { radius = 1; } PlayerSpawnFinder finder = new PlayerSpawnFinder(level, spawnSuggestion, radius); finder.scheduleNext(); return finder.finishedFuture; } private void scheduleNext() { int candidateIndex; if ((candidateIndex = this.nextCandidateIndex++) < this.candidateCount) { int value = (this.offset + this.coprime * candidateIndex) % this.candidateCount; int deltaX = value % (this.radius * 2 + 1); int deltaZ = value / (this.radius * 2 + 1); int targetX = this.spawnSuggestion.getX() + deltaX - this.radius; int targetZ = this.spawnSuggestion.getZ() + deltaZ - this.radius; this.scheduleCandidate(targetX, targetZ, candidateIndex, () -> { BlockPos spawnPos = PlayerSpawnFinder.getOverworldRespawnPos(this.level, targetX, targetZ); if (spawnPos != null && PlayerSpawnFinder.noCollisionNoLiquid(this.level, spawnPos)) { return Optional.of(Vec3.atBottomCenterOf(spawnPos)); } return Optional.empty(); }); } else { this.scheduleCandidate(this.spawnSuggestion.getX(), this.spawnSuggestion.getZ(), candidateIndex, () -> Optional.of(PlayerSpawnFinder.fixupSpawnHeight(this.level, this.spawnSuggestion))); } } private static Vec3 fixupSpawnHeight(CollisionGetter level, BlockPos spawnPos) { BlockPos.MutableBlockPos mutablePos = spawnPos.mutable(); while (!PlayerSpawnFinder.noCollisionNoLiquid(level, mutablePos) && mutablePos.getY() < level.getMaxY()) { mutablePos.move(Direction.UP); } mutablePos.move(Direction.DOWN); while (PlayerSpawnFinder.noCollisionNoLiquid(level, mutablePos) && mutablePos.getY() > level.getMinY()) { mutablePos.move(Direction.DOWN); } mutablePos.move(Direction.UP); return Vec3.atBottomCenterOf(mutablePos); } private static boolean noCollisionNoLiquid(CollisionGetter level, BlockPos pos) { return level.noCollision(null, PLAYER_DIMENSIONS.makeBoundingBox(pos.getBottomCenter()), true); } private static int getCoprime(int possibleOrigins) { return possibleOrigins <= 16 ? possibleOrigins - 1 : 17; } private void scheduleCandidate(int candidateX, int candidateZ, int candidateIndex, Supplier> candidateChecker) { if (this.finishedFuture.isDone()) { return; } int chunkX = SectionPos.blockToSectionCoord(candidateX); int chunkZ = SectionPos.blockToSectionCoord(candidateZ); this.level.getChunkSource().addTicketAndLoadWithRadius(TicketType.SPAWN_SEARCH, new ChunkPos(chunkX, chunkZ), 0).whenCompleteAsync((ignored, throwable) -> { if (throwable == null) { try { Optional spawnPos = (Optional)candidateChecker.get(); if (spawnPos.isPresent()) { this.finishedFuture.complete((Vec3)spawnPos.get()); } else { this.scheduleNext(); } } catch (Throwable t) { throwable = t; } } if (throwable != null) { CrashReport report = CrashReport.forThrowable(throwable, "Searching for spawn"); CrashReportCategory details = report.addCategory("Spawn Lookup"); details.setDetail("Origin", this.spawnSuggestion::toString); details.setDetail("Radius", () -> Integer.toString(this.radius)); details.setDetail("Candidate", () -> "[" + candidateX + "," + candidateZ + "]"); details.setDetail("Progress", () -> candidateIndex + " out of " + this.candidateCount); this.finishedFuture.completeExceptionally(new ReportedException(report)); } }, (Executor)this.level.getServer()); } protected static @Nullable BlockPos getOverworldRespawnPos(ServerLevel level, int x, int z) { int topY; boolean caveWorld = level.dimensionType().hasCeiling(); LevelChunk chunk = level.getChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z)); int n = topY = caveWorld ? level.getChunkSource().getGenerator().getSpawnHeight(level) : chunk.getHeight(Heightmap.Types.MOTION_BLOCKING, x & 0xF, z & 0xF); if (topY < level.getMinY()) { return null; } int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, x & 0xF, z & 0xF); if (surface <= topY && surface > chunk.getHeight(Heightmap.Types.OCEAN_FLOOR, x & 0xF, z & 0xF)) { return null; } BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); for (int y = topY + 1; y >= level.getMinY(); --y) { pos.set(x, y, z); BlockState blockState = level.getBlockState(pos); if (!blockState.getFluidState().isEmpty()) break; if (!Block.isFaceFull(blockState.getCollisionShape(level, pos), Direction.UP)) continue; return ((BlockPos)pos.above()).immutable(); } return null; } public static @Nullable BlockPos getSpawnPosInChunk(ServerLevel level, ChunkPos chunkPos) { if (SharedConstants.debugVoidTerrain(chunkPos)) { return null; } for (int x = chunkPos.getMinBlockX(); x <= chunkPos.getMaxBlockX(); ++x) { for (int z = chunkPos.getMinBlockZ(); z <= chunkPos.getMaxBlockZ(); ++z) { BlockPos validSpawnPosition = PlayerSpawnFinder.getOverworldRespawnPos(level, x, z); if (validSpawnPosition == null) continue; return validSpawnPosition; } } return null; } }