From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: NONPLAYT <76615486+NONPLAYT@users.noreply.github.com> Date: Thu, 27 Mar 2025 00:04:19 +0300 Subject: [PATCH] Paper PR: Throttle failed spawn attempts Original license: GPLv3 Original project: https://github.com/PaperMC/Paper Paper pull request: https://github.com/PaperMC/Paper/pull/11099 Example config in paper-world-defaults.yml: ``` spawning-throttle: failed-attempts-threshold: 1200 throttled-ticks-per-spawn: ambient: 10 # default value in bukkit.yml tickers-per * 10 axolotls: 10 creature: 4000 monster: 10 underground_water_creature: 10 water_ambient: 10 water_creature: 10 ``` diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java index 345d4b80bd4383e0fb66d744d87bc8ef4100fd32..68a074a1eb11b158af773a2c44aa49d5d8462080 100644 --- a/net/minecraft/world/level/NaturalSpawner.java +++ b/net/minecraft/world/level/NaturalSpawner.java @@ -158,29 +158,52 @@ public final class NaturalSpawner { // Copied from getFilteredSpawningCategories int limit = mobCategory.getMaxInstancesPerChunk(); org.bukkit.entity.SpawnCategory spawnCategory = org.bukkit.craftbukkit.util.CraftSpawnCategory.toBukkit(mobCategory); + // Paper start - throttle failed spawn attempts + boolean spawnThisTick = true; + long ticksPerSpawn = level.ticksPerSpawnCategory.getLong(spawnCategory); + long ticksPerSpawnTmp = ticksPerSpawn; + io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.SpawningThrottle spawningThrottle = level.paperConfig().entities.spawning.spawningThrottle; + if (spawningThrottle.failedAttemptsThreshold.test(threshold -> chunk.failedSpawnAttempts[mobCategory.ordinal()] > threshold)) { + ticksPerSpawn = Math.max(ticksPerSpawn, spawningThrottle.throttledTicksPerSpawn.getOrDefault(mobCategory, -1)); + } + // Paper end - throttle failed spawn attempts if (org.bukkit.craftbukkit.util.CraftSpawnCategory.isValidForLimits(spawnCategory)) { + spawnThisTick = ticksPerSpawnTmp != 0 && level.getGameTime() % ticksPerSpawn == 0; // Paper - throttle failed spawn attempts limit = level.getWorld().getSpawnLimit(spawnCategory); } - // Apply per-player limit - int minDiff = Integer.MAX_VALUE; - final ca.spottedleaf.moonrise.common.list.ReferenceList inRange = - level.moonrise$getNearbyPlayers().getPlayers(chunk.getPos(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); - if (inRange != null) { - final net.minecraft.server.level.ServerPlayer[] backingSet = inRange.getRawDataUnchecked(); - for (int k = 0, len = inRange.size(); k < len; k++) { - minDiff = Math.min(limit - level.getChunkSource().chunkMap.getMobCountNear(backingSet[k], mobCategory), minDiff); + // Paper start - throttle failed spawn attempts + if (!spawningThrottle.failedAttemptsThreshold.enabled() || spawnThisTick) { + // Apply per-player limit + int minDiff = Integer.MAX_VALUE; + final ca.spottedleaf.moonrise.common.list.ReferenceList inRange = + level.moonrise$getNearbyPlayers().getPlayers(chunk.getPos(), ca.spottedleaf.moonrise.common.misc.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE); + if (inRange != null) { + final net.minecraft.server.level.ServerPlayer[] backingSet = inRange.getRawDataUnchecked(); + for (int k = 0, len = inRange.size(); k < len; k++) { + minDiff = Math.min(limit - level.getChunkSource().chunkMap.getMobCountNear(backingSet[k], mobCategory), minDiff); + } } - } - maxSpawns = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; - canSpawn = maxSpawns > 0; + maxSpawns = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; + canSpawn = maxSpawns > 0; + } else { + canSpawn = false; + } + // Paper end - throttle failed spawn attempts } else { canSpawn = spawnState.canSpawnForCategoryLocal(mobCategory, chunk.getPos()); } if (canSpawn) { - spawnCategoryForChunk(mobCategory, level, chunk, spawnState::canSpawn, spawnState::afterSpawn, - maxSpawns, level.paperConfig().entities.spawning.perPlayerMobSpawns ? level.getChunkSource().chunkMap::updatePlayerMobTypeMap : null); + // Paper start - throttle failed spawn attempts + int spawnCount = spawnCategoryForChunk(mobCategory, level, chunk, spawnState::canSpawn, spawnState::afterSpawn, + maxSpawns, level.paperConfig().entities.spawning.perPlayerMobSpawns ? level.getChunkSource().chunkMap::updatePlayerMobTypeMap : null, false); + if (spawnCount == 0) { + chunk.failedSpawnAttempts[mobCategory.ordinal()]++; + } else { + chunk.failedSpawnAttempts[mobCategory.ordinal()] = 0; + } + // Paper end - throttle failed spawn attempts // Paper end - Optional per player mob spawns } } @@ -204,12 +227,21 @@ public final class NaturalSpawner { } public static void spawnCategoryForChunk( MobCategory category, ServerLevel level, LevelChunk chunk, NaturalSpawner.SpawnPredicate filter, NaturalSpawner.AfterSpawnCallback callback, final int maxSpawns, final Consumer trackEntity + // Paper start - throttle failed spawn attempts + ) { + spawnCategoryForChunk(category, level, chunk, filter, callback, maxSpawns, trackEntity, false); + } + public static int spawnCategoryForChunk( + MobCategory category, ServerLevel level, LevelChunk chunk, NaturalSpawner.SpawnPredicate filter, NaturalSpawner.AfterSpawnCallback callback, final int maxSpawns, final Consumer trackEntity, final boolean nothing + // Paper end - throttle failed spawn attempts ) { // Paper end - Optional per player mob spawns BlockPos randomPosWithin = getRandomPosWithin(level, chunk); if (randomPosWithin.getY() >= level.getMinY() + 1) { - spawnCategoryForPosition(category, level, chunk, randomPosWithin, filter, callback, maxSpawns, trackEntity); // Paper - Optional per player mob spawns + return spawnCategoryForPosition(category, level, chunk, randomPosWithin, filter, callback, maxSpawns, trackEntity, false); // Paper - Optional per player mob spawns // Paper - throttle failed spawn attempts } + + return 0; // Paper - throttle failed spawn attempts } @VisibleForDebug @@ -229,16 +261,22 @@ public final class NaturalSpawner { } public static void spawnCategoryForPosition( MobCategory category, ServerLevel level, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate filter, NaturalSpawner.AfterSpawnCallback callback, final int maxSpawns, final @Nullable Consumer trackEntity + // Paper start - throttle failed spawn attempts + ) { + spawnCategoryForPosition(category, level, chunk, pos, filter, callback, maxSpawns, trackEntity, false); + } + public static int spawnCategoryForPosition( + MobCategory category, ServerLevel level, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate filter, NaturalSpawner.AfterSpawnCallback callback, final int maxSpawns, final @Nullable Consumer trackEntity, final boolean nothing + // Paper end - throttle failed spawn attempts ) { // Paper end - Optional per player mob spawns StructureManager structureManager = level.structureManager(); ChunkGenerator generator = level.getChunkSource().getGenerator(); int y = pos.getY(); + int i = 0; // Paper - throttle failed spawn attempts BlockState blockState = level.getBlockStateIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn if (blockState != null && !blockState.isRedstoneConductor(chunk, pos)) { // Paper - don't load chunks for mob spawn BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); - int i = 0; - for (int i1 = 0; i1 < 3; i1++) { int x = pos.getX(); int z = pos.getZ(); @@ -278,14 +316,14 @@ public final class NaturalSpawner { } // Paper end - per player mob count backoff if (doSpawning == PreSpawnStatus.ABORT) { - return; + return i; // Paper - throttle failed spawn attempts } if (doSpawning == PreSpawnStatus.SUCCESS // Paper end - PreCreatureSpawnEvent && filter.test(spawnerData.type(), mutableBlockPos, chunk)) { Mob mobForSpawn = getMobForSpawn(level, spawnerData.type()); if (mobForSpawn == null) { - return; + return i; // Paper - throttle failed spawn attempts } mobForSpawn.snapTo(d, y, d1, level.random.nextFloat() * 360.0F, 0.0F); @@ -308,7 +346,7 @@ public final class NaturalSpawner { } // CraftBukkit end if (i >= mobForSpawn.getMaxSpawnClusterSize() || i >= maxSpawns) { // Paper - Optional per player mob spawns - return; + return i; // Paper - throttle failed spawn attempts } if (mobForSpawn.isMaxGroupSizeReached(i3)) { @@ -321,6 +359,8 @@ public final class NaturalSpawner { } } } + + return i; // Paper - throttle failed spawn attempts } private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel level, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double distance) { diff --git a/net/minecraft/world/level/chunk/ChunkAccess.java b/net/minecraft/world/level/chunk/ChunkAccess.java index 182c14b660f8860bed627eed4e01fd4002153e9a..81511de113c292549fe5fe720a15bf3e0497ca84 100644 --- a/net/minecraft/world/level/chunk/ChunkAccess.java +++ b/net/minecraft/world/level/chunk/ChunkAccess.java @@ -88,6 +88,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY); // CraftBukkit end public final Registry biomeRegistry; // CraftBukkit + public final long[] failedSpawnAttempts = new long[net.minecraft.world.entity.MobCategory.values().length]; // Paper - throttle failed spawn attempts // Paper start - rewrite chunk system private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles; diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java index e04d3479383cd480cf35ed7ac3c82e7f6fb69e28..87abba1241552fe967229f1b442cd0a767cbf739 100644 --- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java @@ -94,6 +94,7 @@ public record SerializableChunkData( List blockEntities, CompoundTag structureData , @Nullable net.minecraft.nbt.Tag persistentDataContainer // CraftBukkit - persistentDataContainer + , @Nullable long[] failedSpawnAttempts // Paper - throttle failed spawn attempts ) { public static final Codec> BLOCK_STATE_CODEC = PalettedContainer.codecRW( Block.BLOCK_STATE_REGISTRY, BlockState.CODEC, PalettedContainer.Strategy.SECTION_STATES, Blocks.AIR.defaultBlockState(), null // Paper - Anti-Xray @@ -190,6 +191,19 @@ public record SerializableChunkData( lists[i] = list2; } + // Paper start - throttle failed spawn attempts + long[] failedSpawnAttemptsData = null; + if (tag.contains("Paper.FailedSpawnAttempts")) { + failedSpawnAttemptsData = new long[net.minecraft.world.entity.MobCategory.values().length]; + CompoundTag failedSpawnAttemptsTag = tag.getCompoundOrEmpty("Paper.FailedSpawnAttempts"); + for (net.minecraft.world.entity.MobCategory mobCategory : net.minecraft.world.level.NaturalSpawner.SPAWNING_CATEGORIES) { + if (failedSpawnAttemptsTag.contains(mobCategory.getSerializedName())) { + failedSpawnAttemptsData[mobCategory.ordinal()] = failedSpawnAttemptsTag.getLongOr(mobCategory.getSerializedName(), 0); + } + } + } + // Paper end - throttle failed spawn attempts + List list3 = tag.getList("entities").stream().flatMap(ListTag::compoundStream).toList(); List list4 = tag.getList("block_entities").stream().flatMap(ListTag::compoundStream).toList(); CompoundTag compoundOrEmpty = tag.getCompoundOrEmpty("structures"); @@ -270,6 +284,7 @@ public record SerializableChunkData( list4, compoundOrEmpty , tag.get("ChunkBukkitValues") // CraftBukkit - ChunkBukkitValues + , failedSpawnAttemptsData // Paper - throttle failed spawn attempts ); } } @@ -426,6 +441,15 @@ public record SerializableChunkData( chunkAccess.addPackedPostProcess(this.postProcessingSections[i], i); } + // Paper start - throttle failed spawn attempts + long[] failedSpawnAttemptsData = this.failedSpawnAttempts; + if (failedSpawnAttemptsData != null) { + for (net.minecraft.world.entity.MobCategory mobCategory : net.minecraft.world.entity.MobCategory.values()) { + System.arraycopy(failedSpawnAttemptsData, 0, chunkAccess.failedSpawnAttempts, 0, failedSpawnAttemptsData.length); + } + } + // Paper end - throttle failed spawn attempts + if (chunkType == ChunkType.LEVELCHUNK) { return this.loadStarlightLightData(level, new ImposterProtoChunk((LevelChunk)chunkAccess, false)); // Paper - starlight } else { @@ -556,6 +580,7 @@ public record SerializableChunkData( persistentDataContainer = chunk.persistentDataContainer.toTagCompound(); } // CraftBukkit end + final long[] failedSpawnAttemptsData = chunk.failedSpawnAttempts; // Paper - throttle failed spawn attempts return new SerializableChunkData( level.registryAccess().lookupOrThrow(Registries.BIOME), pos, @@ -576,6 +601,7 @@ public record SerializableChunkData( list1, compoundTag , persistentDataContainer // CraftBukkit - persistentDataContainer + , failedSpawnAttemptsData // Paper - throttle failed spawn attempts ); } } @@ -660,6 +686,21 @@ public record SerializableChunkData( compoundTag.put("ChunkBukkitValues", this.persistentDataContainer); } // CraftBukkit end + // Paper start - throttle failed spawn attempts + CompoundTag failedSpawnAttemptsTag = new CompoundTag(); + long[] failedSpawnAttemptsData = this.failedSpawnAttempts; + if (failedSpawnAttemptsData != null) { + for (net.minecraft.world.entity.MobCategory mobCategory : net.minecraft.world.entity.MobCategory.values()) { + long failedAttempts = failedSpawnAttemptsData[mobCategory.ordinal()]; + if (failedAttempts > 0) { + failedSpawnAttemptsTag.putLong(mobCategory.getSerializedName(), failedAttempts); + } + } + } + if (!failedSpawnAttemptsTag.isEmpty()) { + compoundTag.put("Paper.FailedSpawnAttempts", failedSpawnAttemptsTag); + } + // Paper end - throttle failed spawn attempts // Paper start - starlight if (this.lightCorrect && !this.chunkStatus.isBefore(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { // clobber vanilla value to force vanilla to relight @@ -875,4 +916,49 @@ public record SerializableChunkData( } // Paper end - starlight - convert from record } + + // Paper start - throttle failed spawn attempts - for plugin compatibility + public SerializableChunkData( + Registry biomeRegistry, + ChunkPos chunkPos, + int minSectionY, + long lastUpdateTime, + long inhabitedTime, + ChunkStatus chunkStatus, + @Nullable BlendingData.Packed blendingData, + @Nullable BelowZeroRetrogen belowZeroRetrogen, + UpgradeData upgradeData, + @Nullable long[] carvingMask, + Map heightmaps, + ChunkAccess.PackedTicks packedTicks, + ShortList[] postProcessingSections, + boolean lightCorrect, + List sectionData, + List entities, + List blockEntities, + CompoundTag structureData, + @Nullable net.minecraft.nbt.Tag persistentDataContainer // CraftBukkit - persistentDataContainer + ) { + this(biomeRegistry, + chunkPos, + minSectionY, + lastUpdateTime, + inhabitedTime, + chunkStatus, + blendingData, + belowZeroRetrogen, + upgradeData, + carvingMask, + heightmaps, + packedTicks, + postProcessingSections, + lightCorrect, + sectionData, + entities, + blockEntities, + structureData, + persistentDataContainer, + null); + } + // Paper end - throttle failed spawn attempts }