diff --git a/paper-api/src/main/java/org/bukkit/generator/BiomeProvider.java b/paper-api/src/main/java/org/bukkit/generator/BiomeProvider.java index 65fd14b7b3fe..c761033a2675 100644 --- a/paper-api/src/main/java/org/bukkit/generator/BiomeProvider.java +++ b/paper-api/src/main/java/org/bukkit/generator/BiomeProvider.java @@ -1,6 +1,7 @@ package org.bukkit.generator; import java.util.List; +import java.util.Optional; import org.bukkit.block.Biome; import org.jetbrains.annotations.NotNull; @@ -61,6 +62,29 @@ public Biome getBiome(@NotNull WorldInfo worldInfo, int x, int y, int z, @NotNul return getBiome(worldInfo, x, y, z); } + /** + * Return a biome for structure placement searching (e.g. stronghold ring + * position computation), bypassing expensive biome pipeline evaluation. + *

+ * Implementations may return a coarse approximation sufficient to determine + * whether a position is eligible for a structure (e.g. land vs ocean). + * Return {@link Optional#empty()} to fall back to the full + * {@link #getBiome(WorldInfo, int, int, int)} path. + *

+ * This is called from {@code findBiomeHorizontal} which runs on background + * executor threads. Implementations must be thread-safe. + * + * @param worldInfo The world info of the world being searched + * @param x The X block coordinate + * @param z The Z block coordinate + * @return An optional biome for fast structure placement eligibility + * checking, or empty to use the full pipeline + */ + @NotNull + public Optional getStructurePlacementBiome(@NotNull WorldInfo worldInfo, int x, int z) { + return Optional.empty(); + } + /** * Returns a list with every biome the {@link BiomeProvider} will use for * the given world. diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomWorldChunkManager.java b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomWorldChunkManager.java index f1b3286a83c7..79a483f077e3 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomWorldChunkManager.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomWorldChunkManager.java @@ -1,18 +1,42 @@ package org.bukkit.craftbukkit.generator; import com.google.common.base.Preconditions; +import com.mojang.datafixers.util.Pair; import com.mojang.serialization.MapCodec; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.stream.Stream; +import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.QuartPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.Climate; import org.bukkit.craftbukkit.block.CraftBiome; import org.bukkit.generator.BiomeProvider; import org.bukkit.generator.WorldInfo; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class CustomWorldChunkManager extends BiomeSource { + private static final Logger LOGGER = LoggerFactory.getLogger(CustomWorldChunkManager.class); + // One-shot guards: prevent flooding debug/warn logs across 400k+ biome queries per search. + private static final AtomicBoolean LOGGED_SEARCH_INTERCEPT = new AtomicBoolean(false); + private static final AtomicBoolean LOGGED_FAST_PATH = new AtomicBoolean(false); + private static final AtomicBoolean LOGGED_FAST_PATH_EMPTY = new AtomicBoolean(false); + + // Set true on the calling thread while inside findBiomeHorizontal so that + // getNoiseBiome can use the fast path for structure placement searches. + // findBiomeHorizontal is the only caller of getNoiseBiome for stronghold + // ring position computation; normal chunk generation calls getNoiseBiome + // directly without going through findBiomeHorizontal. + private static final ThreadLocal IN_STRUCTURE_SEARCH = + ThreadLocal.withInitial(() -> false); + private final WorldInfo worldInfo; private final BiomeProvider biomeProvider; public final BiomeSource vanillaBiomeSource; @@ -28,8 +52,54 @@ protected MapCodec codec() { throw new UnsupportedOperationException("Cannot serialize CustomWorldChunkManager"); } + // Paper - structure search fast path: intercept the horizontal biome search used + // by ConcentricRingsStructurePlacement (strongholds). Sets a thread-local so + // getNoiseBiome can call BiomeProvider.getStructurePlacementBiome() instead of + // the full pipeline, avoiding 100-2000x amplification from pipeline chunk cache misses. + @Override + public @Nullable Pair> findBiomeHorizontal( + int x, int y, int z, int searchRadius, int skipSteps, + Predicate> allowed, RandomSource random, + boolean findClosest, Climate.Sampler sampler) { + if (LOGGED_SEARCH_INTERCEPT.compareAndSet(false, true)) { + LOGGER.debug("Structure search fast path active for world '{}'", this.worldInfo.getName()); + } + IN_STRUCTURE_SEARCH.set(true); + try { + return super.findBiomeHorizontal(x, y, z, searchRadius, skipSteps, + allowed, random, findClosest, sampler); + } finally { + IN_STRUCTURE_SEARCH.set(false); + } + } + @Override public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler noise) { + if (IN_STRUCTURE_SEARCH.get()) { + Optional fast = + this.biomeProvider.getStructurePlacementBiome(this.worldInfo, QuartPos.toBlock(x), QuartPos.toBlock(z)); + if (fast.isPresent()) { + Holder biome = CraftBiome.bukkitToMinecraftHolder(fast.get()); + if (biome != null) { + if (LOGGED_FAST_PATH.compareAndSet(false, true)) { + LOGGER.debug("Structure search fast path hit for world '{}' — provider: {}, biome: {}", + this.worldInfo.getName(), this.biomeProvider.getClass().getSimpleName(), fast.get()); + } + return biome; + } else { + if (LOGGED_FAST_PATH_EMPTY.compareAndSet(false, true)) { + LOGGER.warn("Structure search fast path for world '{}': biome '{}' returned by {} is not in the biome registry; falling back to full pipeline", + this.worldInfo.getName(), fast.get(), this.biomeProvider.getClass().getSimpleName()); + } + } + } else { + if (LOGGED_FAST_PATH_EMPTY.compareAndSet(false, true)) { + LOGGER.debug("Structure search fast path not available for world '{}' — {} returned empty; using full pipeline", + this.worldInfo.getName(), this.biomeProvider.getClass().getSimpleName()); + } + } + } + Holder biome = CraftBiome.bukkitToMinecraftHolder( this.biomeProvider.getBiome(this.worldInfo, QuartPos.toBlock(x), QuartPos.toBlock(y), QuartPos.toBlock(z), CraftBiomeParameterPoint.createBiomeParameterPoint(noise, noise.sample(x, y, z))) );