From c946bcff39fa2690a1fb5f4fb0b937761ba35e56 Mon Sep 17 00:00:00 2001 From: Omer Uddin Date: Sun, 13 Jun 2021 22:41:44 -0400 Subject: [PATCH] work work (#5821) --- ...lement-alternative-item-despawn-rate.patch | 118 +++ ...ement-optional-per-player-mob-spawns.patch | 816 ++++++++++++++++++ 2 files changed, 934 insertions(+) create mode 100644 patches/server/Implement-alternative-item-despawn-rate.patch create mode 100644 patches/server/implement-optional-per-player-mob-spawns.patch diff --git a/patches/server/Implement-alternative-item-despawn-rate.patch b/patches/server/Implement-alternative-item-despawn-rate.patch new file mode 100644 index 0000000000..d15f1c29e8 --- /dev/null +++ b/patches/server/Implement-alternative-item-despawn-rate.patch @@ -0,0 +1,118 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kickash32 +Date: Mon, 3 Jun 2019 02:02:39 -0400 +Subject: [PATCH] Implement alternative item-despawn-rate + + +diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java ++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +@@ -0,0 +0,0 @@ + package com.destroystokyo.paper; + ++import java.util.EnumMap; ++import java.util.HashMap; + import java.util.List; ++import java.util.Map; + + import org.bukkit.Bukkit; ++import org.bukkit.Material; ++import org.bukkit.configuration.ConfigurationSection; + import org.bukkit.configuration.file.YamlConfiguration; + import org.spigotmc.SpigotWorldConfig; + +@@ -0,0 +0,0 @@ public class PaperWorldConfig { + private void viewDistance() { + this.noTickViewDistance = this.getInt("viewdistances.no-tick-view-distance", -1); + } ++ ++ public boolean altItemDespawnRateEnabled; ++ public Map altItemDespawnRateMap; ++ private void altItemDespawnRate() { ++ String path = "alt-item-despawn-rate"; ++ ++ altItemDespawnRateEnabled = getBoolean(path + ".enabled", false); ++ ++ Map altItemDespawnRateMapDefault = new EnumMap<>(Material.class); ++ altItemDespawnRateMapDefault.put(Material.COBBLESTONE, 300); ++ for (Material key : altItemDespawnRateMapDefault.keySet()) { ++ config.addDefault("world-settings.default." + path + ".items." + key, altItemDespawnRateMapDefault.get(key)); ++ } ++ ++ Map rawMap = new HashMap<>(); ++ try { ++ ConfigurationSection mapSection = config.getConfigurationSection("world-settings." + worldName + "." + path + ".items"); ++ if (mapSection == null) { ++ mapSection = config.getConfigurationSection("world-settings.default." + path + ".items"); ++ } ++ for (String key : mapSection.getKeys(false)) { ++ int val = mapSection.getInt(key); ++ rawMap.put(key, val); ++ } ++ } ++ catch (Exception e) { ++ logError("alt-item-despawn-rate was malformatted"); ++ altItemDespawnRateEnabled = false; ++ } ++ ++ altItemDespawnRateMap = new EnumMap<>(Material.class); ++ if (!altItemDespawnRateEnabled) { ++ return; ++ } ++ ++ for(String key : rawMap.keySet()) { ++ try { ++ altItemDespawnRateMap.put(Material.valueOf(key), rawMap.get(key)); ++ } catch (Exception e) { ++ logError("Could not add item " + key + " to altItemDespawnRateMap: " + e.getMessage()); ++ } ++ } ++ if(altItemDespawnRateEnabled) { ++ for(Material key : altItemDespawnRateMap.keySet()) { ++ log("Alternative item despawn rate of " + key + ": " + altItemDespawnRateMap.get(key)); ++ } ++ } ++ } + } + +diff --git a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java ++++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java +@@ -0,0 +0,0 @@ public class ItemEntity extends Entity { + } + } + +- if (!this.level.isClientSide && this.age >= level.spigotConfig.itemDespawnRate) { // Spigot ++ if (!this.level.isClientSide && this.age >= this.getDespawnRate()) { // Spigot // Paper + // CraftBukkit start - fire ItemDespawnEvent + if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemDespawnEvent(this).isCancelled()) { + this.age = 0; +@@ -0,0 +0,0 @@ public class ItemEntity extends Entity { + this.lastTick = MinecraftServer.currentTick; + // CraftBukkit end + +- if (!this.level.isClientSide && this.age >= level.spigotConfig.itemDespawnRate) { // Spigot ++ if (!this.level.isClientSide && this.age >= this.getDespawnRate()) { // Spigot // Paper + // CraftBukkit start - fire ItemDespawnEvent + if (org.bukkit.craftbukkit.event.CraftEventFactory.callItemDespawnEvent(this).isCancelled()) { + this.age = 0; +@@ -0,0 +0,0 @@ public class ItemEntity extends Entity { + + public void makeFakeItem() { + this.setNeverPickUp(); +- this.age = level.spigotConfig.itemDespawnRate - 1; // Spigot ++ this.age = this.getDespawnRate() - 1; // Spigot + } + ++ // Paper start ++ public int getDespawnRate(){ ++ org.bukkit.Material material = this.getItem().getBukkitStack().getType(); ++ return level.paperConfig.altItemDespawnRateMap.getOrDefault(material, level.spigotConfig.itemDespawnRate); ++ } ++ // Paper end ++ + public float getSpin(float tickDelta) { + return ((float) this.getAge() + tickDelta) / 20.0F + this.bobOffs; + } diff --git a/patches/server/implement-optional-per-player-mob-spawns.patch b/patches/server/implement-optional-per-player-mob-spawns.patch new file mode 100644 index 0000000000..6feab88cbe --- /dev/null +++ b/patches/server/implement-optional-per-player-mob-spawns.patch @@ -0,0 +1,816 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: kickash32 +Date: Mon, 19 Aug 2019 01:27:58 +0500 +Subject: [PATCH] implement optional per player mob spawns + + +diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java ++++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java +@@ -0,0 +0,0 @@ public class WorldTimingsHandler { + + + public final Timing miscMobSpawning; ++ public final Timing playerMobDistanceMapUpdate; + + public final Timing poiUnload; + public final Timing chunkUnload; +@@ -0,0 +0,0 @@ public class WorldTimingsHandler { + + + miscMobSpawning = Timings.ofSafe(name + "Mob spawning - Misc"); ++ playerMobDistanceMapUpdate = Timings.ofSafe(name + "Per Player Mob Spawning - Distance Map Update"); + + poiUnload = Timings.ofSafe(name + "Chunk unload - POI"); + chunkUnload = Timings.ofSafe(name + "Chunk unload - Chunk"); +diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java ++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +@@ -0,0 +0,0 @@ public class PaperWorldConfig { + } + } + } ++ ++ public boolean perPlayerMobSpawns = false; ++ private void perPlayerMobSpawns() { ++ perPlayerMobSpawns = getBoolean("per-player-mob-spawns", false); ++ } + } + +diff --git a/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java b/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java +@@ -0,0 +0,0 @@ ++package com.destroystokyo.paper.util; ++ ++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; ++import java.util.List; ++import java.util.Map; ++import net.minecraft.core.SectionPos; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.level.ChunkPos; ++import org.spigotmc.AsyncCatcher; ++import java.util.HashMap; ++ ++/** @author Spottedleaf */ ++public final class PlayerMobDistanceMap { ++ ++ private static final PooledHashSets.PooledObjectLinkedOpenHashSet EMPTY_SET = new PooledHashSets.PooledObjectLinkedOpenHashSet<>(); ++ ++ private final Map players = new HashMap<>(); ++ // we use linked for better iteration. ++ private final Long2ObjectOpenHashMap> playerMap = new Long2ObjectOpenHashMap<>(32, 0.5f); ++ private int viewDistance; ++ ++ private final PooledHashSets pooledHashSets = new PooledHashSets<>(); ++ ++ public PooledHashSets.PooledObjectLinkedOpenHashSet getPlayersInRange(final ChunkPos chunkPos) { ++ return this.getPlayersInRange(chunkPos.x, chunkPos.z); ++ } ++ ++ public PooledHashSets.PooledObjectLinkedOpenHashSet getPlayersInRange(final int chunkX, final int chunkZ) { ++ return this.playerMap.getOrDefault(ChunkPos.asLong(chunkX, chunkZ), EMPTY_SET); ++ } ++ ++ public void update(final List currentPlayers, final int newViewDistance) { ++ AsyncCatcher.catchOp("Distance map update"); ++ final ObjectLinkedOpenHashSet gone = new ObjectLinkedOpenHashSet<>(this.players.keySet()); ++ ++ final int oldViewDistance = this.viewDistance; ++ this.viewDistance = newViewDistance; ++ ++ for (final ServerPlayer player : currentPlayers) { ++ if (player.isSpectator() || !player.affectsSpawning) { ++ continue; // will be left in 'gone' (or not added at all) ++ } ++ ++ gone.remove(player); ++ ++ final SectionPos newPosition = player.getPlayerMapSection(); ++ final SectionPos oldPosition = this.players.put(player, newPosition); ++ ++ if (oldPosition == null) { ++ this.addNewPlayer(player, newPosition, newViewDistance); ++ } else { ++ this.updatePlayer(player, oldPosition, newPosition, oldViewDistance, newViewDistance); ++ } ++ //this.validatePlayer(player, newViewDistance); // debug only ++ } ++ ++ for (final ServerPlayer player : gone) { ++ final SectionPos oldPosition = this.players.remove(player); ++ if (oldPosition != null) { ++ this.removePlayer(player, oldPosition, oldViewDistance); ++ } ++ } ++ } ++ ++ // expensive op, only for debug ++ private void validatePlayer(final ServerPlayer player, final int viewDistance) { ++ int entiesGot = 0; ++ int expectedEntries = (2 * viewDistance + 1); ++ expectedEntries *= expectedEntries; ++ ++ final SectionPos currPosition = player.getPlayerMapSection(); ++ ++ final int centerX = currPosition.getX(); ++ final int centerZ = currPosition.getZ(); ++ ++ for (final Long2ObjectLinkedOpenHashMap.Entry> entry : this.playerMap.long2ObjectEntrySet()) { ++ final long key = entry.getLongKey(); ++ final PooledHashSets.PooledObjectLinkedOpenHashSet map = entry.getValue(); ++ ++ if (map.referenceCount == 0) { ++ throw new IllegalStateException("Invalid map"); ++ } ++ ++ if (map.set.contains(player)) { ++ ++entiesGot; ++ ++ final int chunkX = ChunkPos.getX(key); ++ final int chunkZ = ChunkPos.getZ(key); ++ ++ final int dist = Math.max(Math.abs(chunkX - centerX), Math.abs(chunkZ - centerZ)); ++ ++ if (dist > viewDistance) { ++ throw new IllegalStateException("Expected view distance " + viewDistance + ", got " + dist); ++ } ++ } ++ } ++ ++ if (entiesGot != expectedEntries) { ++ throw new IllegalStateException("Expected " + expectedEntries + ", got " + entiesGot); ++ } ++ } ++ ++ private void addPlayerTo(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ this.playerMap.compute(ChunkPos.asLong(chunkX, chunkZ), (final Long key, final PooledHashSets.PooledObjectLinkedOpenHashSet players) -> { ++ if (players == null) { ++ return player.cachedSingleMobDistanceMap; ++ } else { ++ return PlayerMobDistanceMap.this.pooledHashSets.findMapWith(players, player); ++ } ++ }); ++ } ++ ++ private void removePlayerFrom(final ServerPlayer player, final int chunkX, final int chunkZ) { ++ this.playerMap.compute(ChunkPos.asLong(chunkX, chunkZ), (final Long keyInMap, final PooledHashSets.PooledObjectLinkedOpenHashSet players) -> { ++ return PlayerMobDistanceMap.this.pooledHashSets.findMapWithout(players, player); // rets null instead of an empty map ++ }); ++ } ++ ++ private void updatePlayer(final ServerPlayer player, final SectionPos oldPosition, final SectionPos newPosition, final int oldViewDistance, final int newViewDistance) { ++ final int toX = newPosition.getX(); ++ final int toZ = newPosition.getZ(); ++ final int fromX = oldPosition.getX(); ++ final int fromZ = oldPosition.getZ(); ++ ++ final int dx = toX - fromX; ++ final int dz = toZ - fromZ; ++ ++ final int totalX = Math.abs(fromX - toX); ++ final int totalZ = Math.abs(fromZ - toZ); ++ ++ if (Math.max(totalX, totalZ) > (2 * oldViewDistance)) { ++ // teleported? ++ this.removePlayer(player, oldPosition, oldViewDistance); ++ this.addNewPlayer(player, newPosition, newViewDistance); ++ return; ++ } ++ ++ // x axis is width ++ // z axis is height ++ // right refers to the x axis of where we moved ++ // top refers to the z axis of where we moved ++ ++ if (oldViewDistance == newViewDistance) { ++ // same view distance ++ ++ // used for relative positioning ++ final int up = 1 | (dz >> (Integer.SIZE - 1)); // 1 if dz >= 0, -1 otherwise ++ final int right = 1 | (dx >> (Integer.SIZE - 1)); // 1 if dx >= 0, -1 otherwise ++ ++ // The area excluded by overlapping the two view distance squares creates four rectangles: ++ // Two on the left, and two on the right. The ones on the left we consider the "removed" section ++ // and on the right the "added" section. ++ // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually ++ // exclusive to the regions they surround. ++ ++ // 4 points of the rectangle ++ int maxX; // exclusive ++ int minX; // inclusive ++ int maxZ; // exclusive ++ int minZ; // inclusive ++ ++ if (dx != 0) { ++ // handle right addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX + (oldViewDistance * right) + right; // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addPlayerTo(player, currX, currZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle up addition ++ ++ maxX = toX + (oldViewDistance * right) + right; // exclusive ++ minX = toX - (oldViewDistance * right); // inclusive ++ maxZ = toZ + (oldViewDistance * up) + up; // exclusive ++ minZ = fromZ + (oldViewDistance * up) + up; // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.addPlayerTo(player, currX, currZ); ++ } ++ } ++ } ++ ++ if (dx != 0) { ++ // handle left removal ++ ++ maxX = toX - (oldViewDistance * right); // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = fromZ + (oldViewDistance * up) + up; // exclusive ++ minZ = toZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removePlayerFrom(player, currX, currZ); ++ } ++ } ++ } ++ ++ if (dz != 0) { ++ // handle down removal ++ ++ maxX = fromX + (oldViewDistance * right) + right; // exclusive ++ minX = fromX - (oldViewDistance * right); // inclusive ++ maxZ = toZ - (oldViewDistance * up); // exclusive ++ minZ = fromZ - (oldViewDistance * up); // inclusive ++ ++ for (int currX = minX; currX != maxX; currX += right) { ++ for (int currZ = minZ; currZ != maxZ; currZ += up) { ++ this.removePlayerFrom(player, currX, currZ); ++ } ++ } ++ } ++ } else { ++ // different view distance ++ // for now :) ++ this.removePlayer(player, oldPosition, oldViewDistance); ++ this.addNewPlayer(player, newPosition, newViewDistance); ++ } ++ } ++ ++ private void removePlayer(final ServerPlayer player, final SectionPos position, final int viewDistance) { ++ final int x = position.getX(); ++ final int z = position.getZ(); ++ ++ for (int xoff = -viewDistance; xoff <= viewDistance; ++xoff) { ++ for (int zoff = -viewDistance; zoff <= viewDistance; ++zoff) { ++ this.removePlayerFrom(player, x + xoff, z + zoff); ++ } ++ } ++ } ++ ++ private void addNewPlayer(final ServerPlayer player, final SectionPos position, final int viewDistance) { ++ final int x = position.getX(); ++ final int z = position.getZ(); ++ ++ for (int xoff = -viewDistance; xoff <= viewDistance; ++xoff) { ++ for (int zoff = -viewDistance; zoff <= viewDistance; ++zoff) { ++ this.addPlayerTo(player, x + xoff, z + zoff); ++ } ++ } ++ } ++} + +diff --git a/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java b/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java +new file mode 100644 +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java +@@ -0,0 +0,0 @@ ++package com.destroystokyo.paper.util; ++ ++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; ++import java.lang.ref.WeakReference; ++import java.util.Iterator; ++ ++/** @author Spottedleaf */ ++public class PooledHashSets { ++ ++ // we really want to avoid that equals() check as much as possible... ++ protected final Object2ObjectOpenHashMap, PooledObjectLinkedOpenHashSet> mapPool = new Object2ObjectOpenHashMap<>(64, 0.25f); ++ ++ protected void decrementReferenceCount(final PooledObjectLinkedOpenHashSet current) { ++ if (current.referenceCount == 0) { ++ throw new IllegalStateException("Cannot decrement reference count for " + current); ++ } ++ if (current.referenceCount == -1 || --current.referenceCount > 0) { ++ return; ++ } ++ ++ this.mapPool.remove(current); ++ return; ++ } ++ ++ public PooledObjectLinkedOpenHashSet findMapWith(final PooledObjectLinkedOpenHashSet current, final E object) { ++ final PooledObjectLinkedOpenHashSet cached = current.getAddCache(object); ++ ++ if (cached != null) { ++ if (cached.referenceCount != -1) { ++ ++cached.referenceCount; ++ } ++ ++ decrementReferenceCount(current); ++ ++ return cached; ++ } ++ ++ if (!current.add(object)) { ++ return current; ++ } ++ ++ // we use get/put since we use a different key on put ++ PooledObjectLinkedOpenHashSet ret = this.mapPool.get(current); ++ ++ if (ret == null) { ++ ret = new PooledObjectLinkedOpenHashSet<>(current); ++ current.remove(object); ++ this.mapPool.put(ret, ret); ++ ret.referenceCount = 1; ++ } else { ++ if (ret.referenceCount != -1) { ++ ++ret.referenceCount; ++ } ++ current.remove(object); ++ } ++ ++ current.updateAddCache(object, ret); ++ ++ decrementReferenceCount(current); ++ return ret; ++ } ++ ++ // rets null if current.size() == 1 ++ public PooledObjectLinkedOpenHashSet findMapWithout(final PooledObjectLinkedOpenHashSet current, final E object) { ++ if (current.set.size() == 1) { ++ decrementReferenceCount(current); ++ return null; ++ } ++ ++ final PooledObjectLinkedOpenHashSet cached = current.getRemoveCache(object); ++ ++ if (cached != null) { ++ if (cached.referenceCount != -1) { ++ ++cached.referenceCount; ++ } ++ ++ decrementReferenceCount(current); ++ ++ return cached; ++ } ++ ++ if (!current.remove(object)) { ++ return current; ++ } ++ ++ // we use get/put since we use a different key on put ++ PooledObjectLinkedOpenHashSet ret = this.mapPool.get(current); ++ ++ if (ret == null) { ++ ret = new PooledObjectLinkedOpenHashSet<>(current); ++ current.add(object); ++ this.mapPool.put(ret, ret); ++ ret.referenceCount = 1; ++ } else { ++ if (ret.referenceCount != -1) { ++ ++ret.referenceCount; ++ } ++ current.add(object); ++ } ++ ++ current.updateRemoveCache(object, ret); ++ ++ decrementReferenceCount(current); ++ return ret; ++ } ++ ++ public static final class PooledObjectLinkedOpenHashSet implements Iterable { ++ ++ private static final WeakReference NULL_REFERENCE = new WeakReference(null); ++ ++ final ObjectLinkedOpenHashSet set; ++ int referenceCount; // -1 if special ++ int hash; // optimize hashcode ++ ++ // add cache ++ WeakReference lastAddObject = NULL_REFERENCE; ++ WeakReference> lastAddMap = NULL_REFERENCE; ++ ++ // remove cache ++ WeakReference lastRemoveObject = NULL_REFERENCE; ++ WeakReference> lastRemoveMap = NULL_REFERENCE; ++ ++ public PooledObjectLinkedOpenHashSet() { ++ this.set = new ObjectLinkedOpenHashSet<>(2, 0.6f); ++ } ++ ++ public PooledObjectLinkedOpenHashSet(final E single) { ++ this(); ++ this.referenceCount = -1; ++ this.add(single); ++ } ++ ++ public PooledObjectLinkedOpenHashSet(final PooledObjectLinkedOpenHashSet other) { ++ this.set = other.set.clone(); ++ this.hash = other.hash; ++ } ++ ++ // from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java ++ // generated by https://github.com/skeeto/hash-prospector ++ static int hash0(int x) { ++ x *= 0x36935555; ++ x ^= x >>> 16; ++ return x; ++ } ++ ++ public PooledObjectLinkedOpenHashSet getAddCache(final E element) { ++ final E currentAdd = this.lastAddObject.get(); ++ ++ if (currentAdd == null || !(currentAdd == element || currentAdd.equals(element))) { ++ return null; ++ } ++ ++ final PooledObjectLinkedOpenHashSet map = this.lastAddMap.get(); ++ if (map == null || map.referenceCount == 0) { ++ // we need to ret null if ref count is zero as calling code will assume the map is in use ++ return null; ++ } ++ ++ return map; ++ } ++ ++ public PooledObjectLinkedOpenHashSet getRemoveCache(final E element) { ++ final E currentRemove = this.lastRemoveObject.get(); ++ ++ if (currentRemove == null || !(currentRemove == element || currentRemove.equals(element))) { ++ return null; ++ } ++ ++ final PooledObjectLinkedOpenHashSet map = this.lastRemoveMap.get(); ++ if (map == null || map.referenceCount == 0) { ++ // we need to ret null if ref count is zero as calling code will assume the map is in use ++ return null; ++ } ++ ++ return map; ++ } ++ ++ public void updateAddCache(final E element, final PooledObjectLinkedOpenHashSet map) { ++ this.lastAddObject = new WeakReference<>(element); ++ this.lastAddMap = new WeakReference<>(map); ++ } ++ ++ public void updateRemoveCache(final E element, final PooledObjectLinkedOpenHashSet map) { ++ this.lastRemoveObject = new WeakReference<>(element); ++ this.lastRemoveMap = new WeakReference<>(map); ++ } ++ ++ boolean add(final E element) { ++ boolean added = this.set.add(element); ++ ++ if (added) { ++ this.hash += hash0(element.hashCode()); ++ } ++ ++ return added; ++ } ++ ++ boolean remove(Object element) { ++ boolean removed = this.set.remove(element); ++ ++ if (removed) { ++ this.hash -= hash0(element.hashCode()); ++ } ++ ++ return removed; ++ } ++ ++ @Override ++ public Iterator iterator() { ++ return this.set.iterator(); ++ } ++ ++ @Override ++ public int hashCode() { ++ return this.hash; ++ } ++ ++ @Override ++ public boolean equals(final Object other) { ++ if (!(other instanceof PooledObjectLinkedOpenHashSet)) { ++ return false; ++ } ++ if (this.referenceCount == 0) { ++ return other == this; ++ } else { ++ if (other == this) { ++ // Unfortunately we are never equal to our own instance while in use! ++ return false; ++ } ++ return this.hash == ((PooledObjectLinkedOpenHashSet)other).hash && this.set.equals(((PooledObjectLinkedOpenHashSet)other).set); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ return "PooledHashSet: size: " + this.set.size() + ", reference count: " + this.referenceCount + ", hash: " + ++ this.hashCode() + ", identity: " + System.identityHashCode(this) + " map: " + this.set.toString(); ++ } ++ } ++} +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -0,0 +0,0 @@ import net.minecraft.util.thread.ProcessorMailbox; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.entity.EntityType; + import net.minecraft.world.entity.Mob; ++import net.minecraft.world.entity.MobCategory; + import net.minecraft.world.entity.ai.village.poi.PoiManager; + import net.minecraft.world.entity.boss.EnderDragonPart; + import net.minecraft.world.level.ChunkPos; +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + private final Long2ByteMap chunkTypeCache; + private final Queue unloadQueue; + int viewDistance; ++ public final com.destroystokyo.paper.util.PlayerMobDistanceMap playerMobDistanceMap; // Paper + + // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback() + public final CallbackExecutor callbackExecutor = new CallbackExecutor(); +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + this.overworldDataStorage = persistentStateManagerFactory; + this.poiManager = new PoiManager(new File(this.storageFolder, "poi"), dataFixer, dsync, world); + this.setViewDistance(viewDistance); ++ this.playerMobDistanceMap = this.level.paperConfig.perPlayerMobSpawns ? new com.destroystokyo.paper.util.PlayerMobDistanceMap() : null; // Paper + // Paper start - no-tick view distance + this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance); + this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, +@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper end - no-tick view distance + } + ++ // Paper start ++ public void updatePlayerMobTypeMap(Entity entity) { ++ if (!this.level.paperConfig.perPlayerMobSpawns) { ++ return; ++ } ++ int chunkX = (int)Math.floor(entity.getX()) >> 4; ++ int chunkZ = (int)Math.floor(entity.getZ()) >> 4; ++ int index = entity.getType().getCategory().ordinal(); ++ ++ for (ServerPlayer player : this.playerMobDistanceMap.getPlayersInRange(chunkX, chunkZ)) { ++ ++player.mobCounts[index]; ++ } ++ } ++ ++ public int getMobCountNear(ServerPlayer entityPlayer, MobCategory enumCreatureType) { ++ return entityPlayer.mobCounts[enumCreatureType.ordinal()]; ++ } ++ // Paper end ++ + private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) { + double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8); + double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8); +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -0,0 +0,0 @@ public class ServerChunkCache extends ChunkSource { + this.level.getProfiler().push("naturalSpawnCount"); + this.level.timings.countNaturalMobs.startTiming(); // Paper - timings + int l = this.distanceManager.getNaturalSpawnChunkCount(); +- NaturalSpawner.SpawnState spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk); ++ // Paper start - per player mob spawning ++ NaturalSpawner.SpawnState spawnercreature_d; // moved down ++ if (this.chunkMap.playerMobDistanceMap != null) { ++ // update distance map ++ this.level.timings.playerMobDistanceMapUpdate.startTiming(); ++ this.chunkMap.playerMobDistanceMap.update(this.level.players, this.chunkMap.viewDistance); ++ this.level.timings.playerMobDistanceMapUpdate.stopTiming(); ++ // re-set mob counts ++ for (ServerPlayer player : this.level.players) { ++ Arrays.fill(player.mobCounts, 0); ++ } ++ spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, true); ++ } else { ++ spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, false); ++ } ++ // Paper end + this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings + + this.lastSpawnState = spawnercreature_d; +diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java ++++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java +@@ -0,0 +0,0 @@ import net.minecraft.world.damagesource.DamageSource; + import net.minecraft.world.damagesource.EntityDamageSource; + import net.minecraft.world.effect.MobEffectInstance; + import net.minecraft.world.effect.MobEffects; +-import net.minecraft.world.entity.Entity; +-import net.minecraft.world.entity.EntitySelector; +-import net.minecraft.world.entity.HumanoidArm; +-import net.minecraft.world.entity.LivingEntity; +-import net.minecraft.world.entity.Mob; +-import net.minecraft.world.entity.NeutralMob; ++import net.minecraft.world.entity.*; + import net.minecraft.world.entity.animal.horse.AbstractHorse; + import net.minecraft.world.entity.item.ItemEntity; + import net.minecraft.world.entity.monster.Monster; +@@ -0,0 +0,0 @@ public class ServerPlayer extends Player { + public boolean queueHealthUpdatePacket = false; + public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket; + // Paper end ++ // Paper start - mob spawning rework ++ public static final int ENUMCREATURETYPE_TOTAL_ENUMS = MobCategory.values().length; ++ public final int[] mobCounts = new int[ENUMCREATURETYPE_TOTAL_ENUMS]; // Paper ++ public final com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet cachedSingleMobDistanceMap; ++ // Paper end + + // CraftBukkit start + public String displayName; +@@ -0,0 +0,0 @@ public class ServerPlayer extends Player { + this.adventure$displayName = net.kyori.adventure.text.Component.text(this.getScoreboardName()); // Paper + this.bukkitPickUpLoot = true; + this.maxHealthCache = this.getMaxHealth(); ++ this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper + } + + // Yes, this doesn't match Vanilla, but it's the best we can do for now. +diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644 +--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java ++++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java +@@ -0,0 +0,0 @@ import net.minecraft.core.Registry; + import net.minecraft.core.SectionPos; + import net.minecraft.nbt.CompoundTag; + import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; + import net.minecraft.tags.BlockTags; + import net.minecraft.tags.FluidTags; + import net.minecraft.tags.Tag; +@@ -0,0 +0,0 @@ public final class NaturalSpawner { + + private NaturalSpawner() {} + ++ // Paper start - add countMobs parameter + public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource) { ++ return createState(spawningChunkCount, entities, chunkSource, false); ++ } ++ public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable entities, NaturalSpawner.ChunkGetter chunkSource, boolean countMobs) { ++ // Paper end - add countMobs parameter + PotentialCalculator spawnercreatureprobabilities = new PotentialCalculator(); + Object2IntOpenHashMap object2intopenhashmap = new Object2IntOpenHashMap(); + Iterator iterator = entities.iterator(); +@@ -0,0 +0,0 @@ public final class NaturalSpawner { + } + + object2intopenhashmap.addTo(enumcreaturetype, 1); ++ // Paper start ++ if (countMobs) { ++ chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); ++ } ++ // Paper end + }); + } + } +@@ -0,0 +0,0 @@ public final class NaturalSpawner { + continue; + } + +- if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && info.a(enumcreaturetype, limit)) { ++ // Paper start - only allow spawns upto the limit per chunk and update count afterwards ++ int currEntityCount = info.getMobCategoryCounts().getInt(enumcreaturetype); ++ int k1 = limit * info.getSpawnableChunkCount() / NaturalSpawner.MAGIC_NUMBER; ++ int difference = k1 - currEntityCount; ++ ++ if (world.paperConfig.perPlayerMobSpawns) { ++ int minDiff = Integer.MAX_VALUE; ++ for (ServerPlayer entityplayer : world.getChunkSource().chunkMap.playerMobDistanceMap.getPlayersInRange(chunk.getPos())) { ++ minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear(entityplayer, enumcreaturetype), minDiff); ++ } ++ difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; ++ } ++ // Paper end ++ ++ // Paper start - per player mob spawning ++ if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (spawnAnimals || !enumcreaturetype.isPersistent()) && info.a(enumcreaturetype, limit) && difference > 0) { + // CraftBukkit end + Objects.requireNonNull(info); + NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn; + + Objects.requireNonNull(info); +- NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn); ++ int spawnCount = NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn, ++ difference, world.paperConfig.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null); ++ info.getMobCategoryCounts().mergeInt(enumcreaturetype, spawnCount, Integer::sum); ++ // Paper end - per player mob spawning + } + } + +@@ -0,0 +0,0 @@ public final class NaturalSpawner { + world.getProfiler().pop(); + } + ++ // Paper start - add parameters and int ret type + public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { ++ spawnCategoryForChunk(group, world, chunk, checker, runner); ++ } ++ public static int spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { ++ // Paper end - add parameters and int ret type + BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk); + + if (blockposition.getY() >= world.getMinBuildHeight() + 1) { +- NaturalSpawner.spawnCategoryForPosition(group, world, (ChunkAccess) chunk, blockposition, checker, runner); ++ return NaturalSpawner.spawnCategoryForPosition(group, world, (ChunkAccess) chunk, blockposition, checker, runner, maxSpawns, trackEntity); // Paper + } ++ return 0; // Paper + } + + @VisibleForDebug +@@ -0,0 +0,0 @@ public final class NaturalSpawner { + }); + } + ++ // Paper start - add maxSpawns parameter and return spawned mobs + public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { ++ spawnCategoryForPosition(group, world,chunk, pos, checker, runner); ++ } ++ public static int spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer trackEntity) { ++ // Paper end - add maxSpawns parameter and return spawned mobs + StructureFeatureManager structuremanager = world.structureFeatureManager(); + ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator(); + int i = pos.getY(); + BlockState iblockdata = world.getTypeIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn ++ int j = 0; // Paper - moved up + + if (iblockdata != null && !iblockdata.isRedstoneConductor(chunk, pos)) { // Paper - don't load chunks for mob spawn + BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(); +- int j = 0; ++ // Paper - moved up + int k = 0; + + while (k < 3) { +@@ -0,0 +0,0 @@ public final class NaturalSpawner { + Mob entityinsentient = NaturalSpawner.getMobForSpawn(world, biomesettingsmobs_c.type); + + if (entityinsentient == null) { +- return; ++ return j; // Paper + } + + entityinsentient.moveTo(d0, (double) i, d1, world.random.nextFloat() * 360.0F, 0.0F); +@@ -0,0 +0,0 @@ public final class NaturalSpawner { + ++j; + ++k1; + runner.run(entityinsentient, chunk); ++ // Paper start ++ if (trackEntity != null) { ++ trackEntity.accept(entityinsentient); ++ } ++ // Paper end + } + // CraftBukkit end +- if (j >= entityinsentient.getMaxSpawnClusterSize()) { +- return; ++ if (j >= entityinsentient.getMaxSpawnClusterSize() || j >= maxSpawns) { // Paper ++ return j; // Paper + } + + if (entityinsentient.isMaxGroupSizeReached(k1)) { +@@ -0,0 +0,0 @@ public final class NaturalSpawner { + } + + } ++ return j; // Paper + } + + private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) {