From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: froobynooby Date: Wed, 17 Jul 2024 18:46:11 +0930 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 For 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 ``` This patch adds the option to use longer ticks-per-spawn for a given mob type in chunks where spawn attempts are consecutively failing. This behaviour is particularly useful on servers where players build mob farms. Mob farm designs often require making surrounding chunks spawnproof, which causes the server to waste CPU cycles trying to spawn mobs in vain. Throttling spawn attempts in suspected spawnproof chunks improves performance without noticeably advantaging or disadvantaging the mob farm. diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java index 345d4b80bd4383e0fb66d744d87bc8ef4100fd32..a2da4fce50f31d56036d04041c4f80ed90c18b27 100644 --- a/net/minecraft/world/level/NaturalSpawner.java +++ b/net/minecraft/world/level/NaturalSpawner.java @@ -158,10 +158,21 @@ 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); } + if (!spawningThrottle.failedAttemptsThreshold.enabled() || spawnThisTick) { // Paper - throttle failed spawn attempts // Apply per-player limit int minDiff = Integer.MAX_VALUE; final ca.spottedleaf.moonrise.common.list.ReferenceList inRange = @@ -175,12 +186,20 @@ public final class NaturalSpawner { maxSpawns = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; canSpawn = maxSpawns > 0; + } else { canSpawn = false; } // Paper - 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 +223,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,15 +257,23 @@ 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; + //int i = 0; // Paper - throttle failed spawn attempts - move up for (int i1 = 0; i1 < 3; i1++) { int x = pos.getX(); @@ -278,14 +314,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 +344,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 +357,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 6683df8d0f5a61ab094393f761a3d3a22d6e0455..4fd9313ce2c87383685d80e2533b93d5b85a9f41 100644 --- a/net/minecraft/world/level/chunk/ChunkAccess.java +++ b/net/minecraft/world/level/chunk/ChunkAccess.java @@ -91,6 +91,7 @@ public abstract class ChunkAccess implements BiomeManager.NoiseBiomeSource, Ligh private boolean slimeChunk; private boolean hasComputedSlimeChunk; // Leaf end - Matter - Secure Seed + 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 749096358fccbd5d1d13801092255c51096eb001..ccec1b847ba1a3667206cbeac4ed541a9fb028ea 100644 --- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java @@ -92,6 +92,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 @@ -188,6 +189,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"); @@ -268,6 +282,7 @@ public record SerializableChunkData( list4, compoundOrEmpty , tag.get("ChunkBukkitValues") // CraftBukkit - ChunkBukkitValues + , failedSpawnAttemptsData // Paper - throttle failed spawn attempts ); } } @@ -424,6 +439,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 { @@ -554,6 +578,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, @@ -574,6 +599,7 @@ public record SerializableChunkData( list1, compoundTag , persistentDataContainer // CraftBukkit - persistentDataContainer + , failedSpawnAttemptsData // Paper - throttle failed spawn attempts ); } } @@ -658,6 +684,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 @@ -865,4 +906,50 @@ 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 }