aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/1030-Optimise-chunk-tick-iteration.patch
diff options
context:
space:
mode:
authorNassim Jahnke <[email protected]>2024-02-11 22:28:00 +0100
committerGitHub <[email protected]>2024-02-11 22:28:00 +0100
commit31699ae9a8f3a57491e9c9276cffa5a51e9a5f60 (patch)
tree066237108974856e6a02b481451a17642afa530e /patches/server/1030-Optimise-chunk-tick-iteration.patch
parentcde5587e58f08ce2d8875bb9d13a4be56825e353 (diff)
downloadPaper-31699ae9a8f3a57491e9c9276cffa5a51e9a5f60.tar.gz
Paper-31699ae9a8f3a57491e9c9276cffa5a51e9a5f60.zip
Updated Upstream (Bukkit/CraftBukkit) (#10242)
* Updated Upstream (Bukkit/CraftBukkit) Upstream has released updates that appear to apply and compile correctly. This update has not been tested by PaperMC and as with ANY update, please do your own testing Bukkit Changes: a6a9d2a4 Remove some old ApiStatus.Experimental annotations be72314c SPIGOT-7300, PR-829: Add new DamageSource API providing enhanced information about entity damage b252cf05 SPIGOT-7576, PR-970: Add methods in MushroomCow to change stew effects b1c689bd PR-902: Add Server#isLoggingIPs to get log-ips configuration 08f86d1c PR-971: Add Player methods for client-side potion effects 2e3024a9 PR-963: Add API for in-world structures a23292a7 SPIGOT-7530, PR-948: Improve Resource Pack API with new 1.20.3 functionality 1851857b SPIGOT-3071, PR-969: Add entity spawn method with spawn reason cde4c52a SPIGOT-5553, PR-964: Add EntityKnockbackEvent CraftBukkit Changes: 38fd4bd50 Fix accidentally renamed internal damage method 80f0ce4be SPIGOT-7300, PR-1180: Add new DamageSource API providing enhanced information about entity damage 7e43f3b16 SPIGOT-7581: Fix typo in BlockMushroom ea14b7d90 SPIGOT-7576, PR-1347: Add methods in MushroomCow to change stew effects 4c687f243 PR-1259: Add Server#isLoggingIPs to get log-ips configuration 22a541a29 Improve support for per-world game rules cb7dccce2 PR-1348: Add Player methods for client-side potion effects b8d6109f0 PR-1335: Add API for in-world structures 4398a1b5b SPIGOT-7577: Make CraftWindCharge#explode discard the entity e74107678 Fix Crafter maximum stack size 0bb0f4f6a SPIGOT-7530, PR-1314: Improve Resource Pack API with new 1.20.3 functionality 4949f556d SPIGOT-3071, PR-1345: Add entity spawn method with spawn reason 20ac73ca2 PR-1353: Fix Structure#place not working as documented with 0 palette 3c1b77871 SPIGOT-6911, PR-1349: Change max book length in CraftMetaBook 333701839 SPIGOT-7572: Bee nests generated without bees f48f4174c SPIGOT-5553, PR-1336: Add EntityKnockbackEvent
Diffstat (limited to 'patches/server/1030-Optimise-chunk-tick-iteration.patch')
-rw-r--r--patches/server/1030-Optimise-chunk-tick-iteration.patch380
1 files changed, 380 insertions, 0 deletions
diff --git a/patches/server/1030-Optimise-chunk-tick-iteration.patch b/patches/server/1030-Optimise-chunk-tick-iteration.patch
new file mode 100644
index 0000000000..600f4520b4
--- /dev/null
+++ b/patches/server/1030-Optimise-chunk-tick-iteration.patch
@@ -0,0 +1,380 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf <[email protected]>
+Date: Sat, 23 Sep 2023 21:36:36 -0700
+Subject: [PATCH] Optimise chunk tick iteration
+
+When per-player mob spawning is enabled we do not need to randomly
+shuffle the chunk list. Additionally, we can use the NearbyPlayers
+class to quickly retrieve nearby players instead of possible
+searching all players on the server.
+
+diff --git a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
+index c3ce8a42dddd76b7189ad5685b23f9d9f8ccadb3..f164256d59b761264876ca0c85f812d101bfd5de 100644
+--- a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
++++ b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java
+@@ -17,7 +17,8 @@ public final class NearbyPlayers {
+ GENERAL_SMALL,
+ GENERAL_REALLY_SMALL,
+ TICK_VIEW_DISTANCE,
+- VIEW_DISTANCE;
++ VIEW_DISTANCE, // Paper - optimise chunk iteration
++ SPAWN_RANGE, // Paper - optimise chunk iteration
+ }
+
+ private static final NearbyMapType[] MOB_TYPES = NearbyMapType.values();
+@@ -26,10 +27,12 @@ public final class NearbyPlayers {
+ private static final int GENERAL_AREA_VIEW_DISTANCE = 33;
+ private static final int GENERAL_SMALL_VIEW_DISTANCE = 10;
+ private static final int GENERAL_REALLY_SMALL_VIEW_DISTANCE = 3;
++ private static final int SPAWN_RANGE_VIEW_DISTANCE = net.minecraft.server.level.DistanceManager.MOB_SPAWN_RANGE; // Paper - optimise chunk iteration
+
+ public static final int GENERAL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_AREA_VIEW_DISTANCE << 4);
+ public static final int GENERAL_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_SMALL_VIEW_DISTANCE << 4);
+ public static final int GENERAL_REALLY_SMALL_AREA_VIEW_DISTANCE_BLOCKS = (GENERAL_REALLY_SMALL_VIEW_DISTANCE << 4);
++ public static final int SPAWN_RANGE_VIEW_DISTANCE_BLOCKS = (SPAWN_RANGE_VIEW_DISTANCE << 4); // Paper - optimise chunk iteration
+
+ private final ServerLevel world;
+ private final Reference2ReferenceOpenHashMap<ServerPlayer, TrackedPlayer[]> players = new Reference2ReferenceOpenHashMap<>();
+@@ -80,6 +83,7 @@ public final class NearbyPlayers {
+ players[NearbyMapType.GENERAL_REALLY_SMALL.ordinal()].update(chunk.x, chunk.z, GENERAL_REALLY_SMALL_VIEW_DISTANCE);
+ players[NearbyMapType.TICK_VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getTickViewDistance(player));
+ players[NearbyMapType.VIEW_DISTANCE.ordinal()].update(chunk.x, chunk.z, ChunkSystem.getLoadViewDistance(player));
++ players[NearbyMapType.SPAWN_RANGE.ordinal()].update(chunk.x, chunk.z, SPAWN_RANGE_VIEW_DISTANCE); // Paper - optimise chunk iteration
+ }
+
+ public TrackedChunk getChunk(final ChunkPos pos) {
+diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+index 2b998bdbe49bf8211b755e0eb7c1bf13ac280eab..627a88ec8c3b215b19b55a6d461c8754b4fcd1e8 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
++++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
+@@ -79,11 +79,19 @@ public class ChunkHolder {
+
+ // Paper start
+ public void onChunkAdd() {
+-
++ // Paper start - optimise chunk tick iteration
++ if (this.needsBroadcastChanges()) {
++ this.chunkMap.needsChangeBroadcasting.add(this);
++ }
++ // Paper end - optimise chunk tick iteration
+ }
+
+ public void onChunkRemove() {
+-
++ // Paper start - optimise chunk tick iteration
++ if (this.needsBroadcastChanges()) {
++ this.chunkMap.needsChangeBroadcasting.remove(this);
++ }
++ // Paper end - optimise chunk tick iteration
+ }
+ // Paper end
+
+@@ -230,7 +238,7 @@ public class ChunkHolder {
+
+ if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296
+ if (this.changedBlocksPerSection[i] == null) {
+- this.hasChangedSections = true;
++ this.hasChangedSections = true; this.addToBroadcastMap(); // Paper - optimise chunk tick iteration
+ this.changedBlocksPerSection[i] = new ShortOpenHashSet();
+ }
+
+@@ -254,6 +262,7 @@ public class ChunkHolder {
+ int k = this.lightEngine.getMaxLightSection();
+
+ if (y >= j && y <= k) {
++ this.addToBroadcastMap(); // Paper - optimise chunk tick iteration
+ int l = y - j;
+
+ if (lightType == LightLayer.SKY) {
+@@ -268,8 +277,19 @@ public class ChunkHolder {
+ }
+ }
+
++ // Paper start - optimise chunk tick iteration
++ public final boolean needsBroadcastChanges() {
++ return this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty();
++ }
++
++ private void addToBroadcastMap() {
++ io.papermc.paper.util.TickThread.ensureTickThread(this.chunkMap.level, this.pos, "Asynchronous ChunkHolder update is not allowed");
++ this.chunkMap.needsChangeBroadcasting.add(this);
++ }
++ // Paper end - optimise chunk tick iteration
++
+ public void broadcastChanges(LevelChunk chunk) {
+- if (this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) {
++ if (this.needsBroadcastChanges()) { // Paper - optimise chunk tick iteration; moved into above, other logic needs to call
+ Level world = chunk.getLevel();
+ List list;
+
+diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
+index 7e5a8789e06a5ea1d2657ea8ee5c0460da92aaeb..5a7278b093e37b95fb005ad5cc3cac90ac36f8fb 100644
+--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
+@@ -191,6 +191,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ this.playerEntityTrackerTrackMaps[i].remove(player);
+ }
+ // Paper end - use distance map to optimise tracker
++ this.playerMobSpawnMap.remove(player); // Paper - optimise chunk tick iteration
+ }
+
+ void updateMaps(ServerPlayer player) {
+@@ -240,6 +241,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+ public final io.papermc.paper.util.player.NearbyPlayers nearbyPlayers;
+ // Paper end
++ // Paper start - optimise chunk tick iteration
++ public final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<ChunkHolder> needsChangeBroadcasting = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>();
++ public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets);
++ // Paper end - optimise chunk tick iteration
+
+ public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory, int viewDistance, boolean dsync) {
+ super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync);
+@@ -408,7 +413,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
+ }
+ // Paper end - Optional per player mob spawns
+
+- private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) {
++ public static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { // Paper - optimise chunk iteration; public
+ double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8);
+ double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8);
+ double d2 = d0 - entity.getX();
+diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
+index c80a625f7289e3bb33c6851d2072957e153ca1fb..7c425ac50c83757b66a2178bc19d4c920b82f12f 100644
+--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
++++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
+@@ -50,7 +50,7 @@ public abstract class DistanceManager {
+ private static final int INITIAL_TICKET_LIST_CAPACITY = 4;
+ final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap();
+ // Paper - rewrite chunk system
+- private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
++ public static final int MOB_SPAWN_RANGE = 8; //private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8); // Paper - optimise chunk tick iteration
+ // Paper - rewrite chunk system
+ private final ChunkMap chunkMap; // Paper
+
+@@ -135,7 +135,7 @@ public abstract class DistanceManager {
+ long i = chunkcoordintpair.toLong();
+
+ // Paper - no longer used
+- this.naturalSpawnChunkCounter.update(i, 0, true);
++ //this.naturalSpawnChunkCounter.update(i, 0, true); // Paper - optimise chunk tick iteration
+ //this.playerTicketManager.update(i, 0, true); // Paper - no longer used
+ //this.tickingTicketsTracker.addTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used
+ }
+@@ -149,7 +149,7 @@ public abstract class DistanceManager {
+ if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully
+ if (objectset == null || objectset.isEmpty()) { // Paper
+ this.playersPerChunk.remove(i);
+- this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
++ //this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false); // Paper - optimise chunk tick iteration
+ //this.playerTicketManager.update(i, Integer.MAX_VALUE, false); // Paper - no longer used
+ //this.tickingTicketsTracker.removeTicket(TicketType.PLAYER, chunkcoordintpair, this.getPlayerTicketLevel(), chunkcoordintpair); // Paper - no longer used
+ }
+@@ -191,13 +191,11 @@ public abstract class DistanceManager {
+ }
+
+ public int getNaturalSpawnChunkCount() {
+- this.naturalSpawnChunkCounter.runAllUpdates();
+- return this.naturalSpawnChunkCounter.chunks.size();
++ return this.chunkMap.playerMobSpawnMap.size(); // Paper - optimise chunk tick iteration
+ }
+
+ public boolean hasPlayersNearby(long chunkPos) {
+- this.naturalSpawnChunkCounter.runAllUpdates();
+- return this.naturalSpawnChunkCounter.chunks.containsKey(chunkPos);
++ return this.chunkMap.playerMobSpawnMap.getObjectsInRange(chunkPos) != null; // Paper - optimise chunk tick iteration
+ }
+
+ public String getDebugStatus() {
+diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+index 2b33a3d8fdb86024acb2a3ee9d0a4a7dd4989c98..366c0c9b45a819f7f94ebe3e49b8ab7f9edf9ce7 100644
+--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+@@ -508,18 +508,10 @@ public class ServerChunkCache extends ChunkSource {
+
+ gameprofilerfiller.push("pollingChunks");
+ gameprofilerfiller.push("filteringLoadedChunks");
+- List<ServerChunkCache.ChunkAndHolder> list = Lists.newArrayListWithCapacity(this.chunkMap.size());
+- Iterator iterator = this.chunkMap.getChunks().iterator();
++ // Paper - optimise chunk tick iteration
+ if (this.level.getServer().tickRateManager().runsNormally()) this.level.timings.chunkTicks.startTiming(); // Paper
+
+- while (iterator.hasNext()) {
+- ChunkHolder playerchunk = (ChunkHolder) iterator.next();
+- LevelChunk chunk = playerchunk.getTickingChunk();
+-
+- if (chunk != null) {
+- list.add(new ServerChunkCache.ChunkAndHolder(chunk, playerchunk));
+- }
+- }
++ // Paper - optimise chunk tick iteration
+
+ if (this.level.getServer().tickRateManager().runsNormally()) {
+ gameprofilerfiller.popPush("naturalSpawnCount");
+@@ -554,38 +546,109 @@ public class ServerChunkCache extends ChunkSource {
+ gameprofilerfiller.popPush("spawnAndTick");
+ boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit
+
+- Util.shuffle(list, this.level.random);
+- // Paper start - PlayerNaturallySpawnCreaturesEvent
+- int chunkRange = level.spigotConfig.mobSpawnRange;
+- chunkRange = (chunkRange > level.spigotConfig.viewDistance) ? (byte) level.spigotConfig.viewDistance : chunkRange;
+- chunkRange = Math.min(chunkRange, 8);
+- for (ServerPlayer entityPlayer : this.level.players()) {
+- entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange);
+- entityPlayer.playerNaturallySpawnedEvent.callEvent();
++ // Paper start - optimise chunk tick iteration
++ ChunkMap playerChunkMap = this.chunkMap;
++ for (ServerPlayer player : this.level.players) {
++ if (!player.affectsSpawning || player.isSpectator()) {
++ playerChunkMap.playerMobSpawnMap.remove(player);
++ player.playerNaturallySpawnedEvent = null;
++ player.lastEntitySpawnRadiusSquared = -1.0;
++ continue;
++ }
++
++ int viewDistance = io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player);
++
++ // copied and modified from isOutisdeRange
++ int chunkRange = (int)level.spigotConfig.mobSpawnRange;
++ chunkRange = (chunkRange > viewDistance) ? viewDistance : chunkRange;
++ chunkRange = (chunkRange > DistanceManager.MOB_SPAWN_RANGE) ? DistanceManager.MOB_SPAWN_RANGE : chunkRange;
++
++ com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(player.getBukkitEntity(), (byte)chunkRange);
++ event.callEvent();
++ if (event.isCancelled() || event.getSpawnRadius() < 0) {
++ playerChunkMap.playerMobSpawnMap.remove(player);
++ player.playerNaturallySpawnedEvent = null;
++ player.lastEntitySpawnRadiusSquared = -1.0;
++ continue;
++ }
++
++ int range = Math.min(event.getSpawnRadius(), DistanceManager.MOB_SPAWN_RANGE); // limit to max spawn range
++ int chunkX = io.papermc.paper.util.CoordinateUtils.getChunkCoordinate(player.getX());
++ int chunkZ = io.papermc.paper.util.CoordinateUtils.getChunkCoordinate(player.getZ());
++
++ playerChunkMap.playerMobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range);
++ player.lastEntitySpawnRadiusSquared = (double)((range << 4) * (range << 4)); // used in anyPlayerCloseEnoughForSpawning
++ player.playerNaturallySpawnedEvent = event;
+ }
+- // Paper end - PlayerNaturallySpawnCreaturesEvent
++ // Paper end - optimise chunk tick iteration
+ int l = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
+ boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit
+- Iterator iterator1 = list.iterator();
++ // Paper - optimise chunk tick iteration
+
+ int chunksTicked = 0; // Paper
+- while (iterator1.hasNext()) {
+- ServerChunkCache.ChunkAndHolder chunkproviderserver_a = (ServerChunkCache.ChunkAndHolder) iterator1.next();
+- LevelChunk chunk1 = chunkproviderserver_a.chunk;
++ // Paper start - optimise chunk tick iteration
++ io.papermc.paper.util.player.NearbyPlayers nearbyPlayers = this.chunkMap.getNearbyPlayers(); // Paper - optimise chunk tick iteration
++ Iterator<LevelChunk> chunkIterator;
++ if (this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
++ chunkIterator = this.tickingChunks.iterator();
++ } else {
++ chunkIterator = this.tickingChunks.unsafeIterator();
++ List<LevelChunk> shuffled = Lists.newArrayListWithCapacity(this.tickingChunks.size());
++ while (chunkIterator.hasNext()) {
++ shuffled.add(chunkIterator.next());
++ }
++ Util.shuffle(shuffled, this.level.random);
++ chunkIterator = shuffled.iterator();
++ }
++ try {
++ // Paper end - optimise chunk tick iteration
++ while (chunkIterator.hasNext()) {
++ LevelChunk chunk1 = chunkIterator.next();
++ // Paper end - optimise chunk tick iteration
+ ChunkPos chunkcoordintpair = chunk1.getPos();
+
+- if (this.level.isNaturalSpawningAllowed(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair)) {
++ // Paper start - optimise chunk tick iteration
++ com.destroystokyo.paper.util.maplist.ReferenceList<ServerPlayer> playersNearby
++ = nearbyPlayers.getPlayers(chunkcoordintpair, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.SPAWN_RANGE);
++ if (playersNearby == null) {
++ continue;
++ }
++ Object[] rawData = playersNearby.getRawData();
++ boolean spawn = false;
++ boolean tick = false;
++ for (int itr = 0, len = playersNearby.size(); itr < len; ++itr) {
++ ServerPlayer player = (ServerPlayer)rawData[itr];
++ if (player.isSpectator()) {
++ continue;
++ }
++
++ double distance = ChunkMap.euclideanDistanceSquared(chunkcoordintpair, player);
++ spawn |= player.lastEntitySpawnRadiusSquared >= distance;
++ tick |= ((double)io.papermc.paper.util.player.NearbyPlayers.SPAWN_RANGE_VIEW_DISTANCE_BLOCKS) * ((double)io.papermc.paper.util.player.NearbyPlayers.SPAWN_RANGE_VIEW_DISTANCE_BLOCKS) >= distance;
++ if (spawn & tick) {
++ break;
++ }
++ }
++ if (tick && chunk1.chunkStatus.isOrAfter(net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING)) {
++ // Paper end - optimise chunk tick iteration
+ chunk1.incrementInhabitedTime(j);
+- if (flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair) && this.chunkMap.anyPlayerCloseEnoughForSpawning(chunkcoordintpair, true)) { // Spigot
++ if (spawn && flag && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunkcoordintpair)) { // Spigot // Paper - optimise chunk tick iteration
+ NaturalSpawner.spawnForChunk(this.level, chunk1, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1);
+ }
+
+- if (this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) {
++ if (true || this.level.shouldTickBlocksAt(chunkcoordintpair.toLong())) { // Paper - optimise chunk tick iteration
+ this.level.tickChunk(chunk1, l);
+ if ((chunksTicked++ & 1) == 0) net.minecraft.server.MinecraftServer.getServer().executeMidTickTasks(); // Paper
+ }
+ }
+ }
++ // Paper start - optimise chunk tick iteration
++ } finally {
++ if (chunkIterator instanceof io.papermc.paper.util.maplist.IteratorSafeOrderedReferenceSet.Iterator safeIterator) {
++ safeIterator.finishedIterating();
++ }
++ }
++ // Paper end - optimise chunk tick iteration
+ this.level.timings.chunkTicks.stopTiming(); // Paper
+
+ gameprofilerfiller.popPush("customSpawners");
+@@ -597,11 +660,23 @@ public class ServerChunkCache extends ChunkSource {
+ }
+
+ gameprofilerfiller.popPush("broadcast");
+- list.forEach((chunkproviderserver_a1) -> {
++ // Paper - optimise chunk tick iteration
+ this.level.timings.broadcastChunkUpdates.startTiming(); // Paper - timing
+- chunkproviderserver_a1.holder.broadcastChanges(chunkproviderserver_a1.chunk);
++ // Paper start - optimise chunk tick iteration
++ if (!this.chunkMap.needsChangeBroadcasting.isEmpty()) {
++ it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<ChunkHolder> copy = this.chunkMap.needsChangeBroadcasting.clone();
++ this.chunkMap.needsChangeBroadcasting.clear();
++ for (ChunkHolder holder : copy) {
++ holder.broadcastChanges(holder.getFullChunkNowUnchecked()); // LevelChunks are NEVER unloaded
++ if (holder.needsBroadcastChanges()) {
++ // I DON'T want to KNOW what DUMB plugins might be doing.
++ this.chunkMap.needsChangeBroadcasting.add(holder);
++ }
++ }
++ }
++ // Paper end - optimise chunk tick iteration
+ this.level.timings.broadcastChunkUpdates.stopTiming(); // Paper - timing
+- });
++ // Paper - optimise chunk tick iteration
+ gameprofilerfiller.pop();
+ gameprofilerfiller.pop();
+ }
+diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+index c99d2f2d64b73179e4e27b63030e26a07953041b..58591bf2f63b9c5e97d9ce4188dff3366968a178 100644
+--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
+@@ -325,6 +325,9 @@ public class ServerPlayer extends Player {
+ });
+ }
+ // Paper end - replace player chunk loader
++ // Paper start - optimise chunk tick iteration
++ public double lastEntitySpawnRadiusSquared = -1.0;
++ // Paper end - optimise chunk tick iteration
+
+ public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) {
+ super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile);