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 a27dc38d1a29ed1d63d2f44b7984c2b65be487d9..96aaaab5b7685c874463505f9d25e8a0a01a6e7c 100644 --- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java +++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java @@ -57,6 +57,7 @@ public class WorldTimingsHandler { public final Timing miscMobSpawning; + public final Timing playerMobDistanceMapUpdate; public final Timing poiUnload; public final Timing chunkUnload; @@ -122,6 +123,7 @@ 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 b913cd2dd0cd1b369b3f7b5a9d8b1be73f6d7920..6aec502eb529d4090306e12e837117cde7e114eb 100644 --- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java @@ -565,4 +565,9 @@ 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..9ebd7ecb7a0ac73ccacf34c097056598bb5d26e8 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java @@ -0,0 +1,253 @@ +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 net.minecraft.server.ChunkCoordIntPair; +import net.minecraft.server.EntityPlayer; +import net.minecraft.server.SectionPosition; +import org.spigotmc.AsyncCatcher; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** @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 ChunkCoordIntPair chunkPos) { + return this.getPlayersInRange(chunkPos.x, chunkPos.z); + } + + public PooledHashSets.PooledObjectLinkedOpenHashSet getPlayersInRange(final int chunkX, final int chunkZ) { + return this.playerMap.getOrDefault(ChunkCoordIntPair.pair(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 EntityPlayer player : currentPlayers) { + if (player.isSpectator() || !player.affectsSpawning) { + continue; // will be left in 'gone' (or not added at all) + } + + gone.remove(player); + + final SectionPosition newPosition = player.getPlayerMapSection(); + final SectionPosition 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 EntityPlayer player : gone) { + final SectionPosition oldPosition = this.players.remove(player); + if (oldPosition != null) { + this.removePlayer(player, oldPosition, oldViewDistance); + } + } + } + + // expensive op, only for debug + private void validatePlayer(final EntityPlayer player, final int viewDistance) { + int entiesGot = 0; + int expectedEntries = (2 * viewDistance + 1); + expectedEntries *= expectedEntries; + + final SectionPosition 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 = ChunkCoordIntPair.getX(key); + final int chunkZ = ChunkCoordIntPair.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 EntityPlayer player, final int chunkX, final int chunkZ) { + this.playerMap.compute(ChunkCoordIntPair.pair(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 EntityPlayer player, final int chunkX, final int chunkZ) { + this.playerMap.compute(ChunkCoordIntPair.pair(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 EntityPlayer player, final SectionPosition oldPosition, final SectionPosition 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 EntityPlayer player, final SectionPosition 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 EntityPlayer player, final SectionPosition 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..4f13d3ff8391793a99f067189f854078334499c6 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java @@ -0,0 +1,241 @@ +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/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java index 2eb38bfcdc0c232da9dad6921df503e5a11de09f..2fad6a79ee977c2712c55433bde8eca524d09ba7 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java @@ -736,7 +736,22 @@ public class ChunkProviderServer extends IChunkProvider { this.world.getMethodProfiler().enter("naturalSpawnCount"); this.world.timings.countNaturalMobs.startTiming(); // Paper - timings int l = this.chunkMapDistance.b(); - SpawnerCreature.d spawnercreature_d = SpawnerCreature.a(l, this.world.A(), this::a); + // Paper start - per player mob spawning + SpawnerCreature.d spawnercreature_d; // moved down + if (this.playerChunkMap.playerMobDistanceMap != null) { + // update distance map + this.world.timings.playerMobDistanceMapUpdate.startTiming(); + this.playerChunkMap.playerMobDistanceMap.update(this.world.players, this.playerChunkMap.viewDistance); + this.world.timings.playerMobDistanceMapUpdate.stopTiming(); + // re-set mob counts + for (EntityPlayer player : this.world.players) { + Arrays.fill(player.mobCounts, 0); + } + spawnercreature_d = SpawnerCreature.countMobs(l, this.world.A(), this::a, true); + } else { + spawnercreature_d = SpawnerCreature.countMobs(l, this.world.A(), this::a, false); + } + // Paper end this.world.timings.countNaturalMobs.stopTiming(); // Paper - timings this.p = spawnercreature_d; diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java index 763edf6a18401f9f7a84ae99a7ee2eb114a5791c..bf68562b08b8d8fecb25a469b4fb4dbc868b0f28 100644 --- a/src/main/java/net/minecraft/server/EntityPlayer.java +++ b/src/main/java/net/minecraft/server/EntityPlayer.java @@ -91,6 +91,11 @@ public class EntityPlayer extends EntityHuman implements ICrafting { public boolean queueHealthUpdatePacket = false; public net.minecraft.server.PacketPlayOutUpdateHealth queuedHealthUpdatePacket; // Paper end + // Paper start - mob spawning rework + public static final int ENUMCREATURETYPE_TOTAL_ENUMS = EnumCreatureType.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; @@ -129,6 +134,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting { this.adventure$displayName = net.kyori.adventure.text.Component.text(this.getName()); // Paper this.canPickUpLoot = 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. @@ -1924,6 +1930,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting { } + public final SectionPosition getPlayerMapSection() { return this.O(); } // Paper - OBFHELPER public SectionPosition O() { return this.cj; } diff --git a/src/main/java/net/minecraft/server/EntityTypes.java b/src/main/java/net/minecraft/server/EntityTypes.java index 14f47d100c3f44a8263c9b907e9ca74944676687..c49e7ec418eedac45182de67cb673839dc7e27c2 100644 --- a/src/main/java/net/minecraft/server/EntityTypes.java +++ b/src/main/java/net/minecraft/server/EntityTypes.java @@ -295,6 +295,7 @@ public class EntityTypes { return this.bl; } + public final EnumCreatureType getEnumCreatureType() { return this.e(); } // Paper - OBFHELPER public EnumCreatureType e() { return this.bg; } diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java index 7f72d13bb39182666a741d43769215303d1f139e..a167696d2fc3cb69b9eaa720c5904762e073008b 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -81,7 +81,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { public final Int2ObjectMap trackedEntities; private final Long2ByteMap z; private final Queue A; private final Queue getUnloadQueueTasks() { return this.A; } // Paper - OBFHELPER - private int viewDistance; + int viewDistance; // Paper - private -> package private + 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(); @@ -160,6 +161,24 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.l = supplier; this.m = new VillagePlace(new File(this.w, "poi"), datafixer, flag, this.world); // Paper this.setViewDistance(i); + this.playerMobDistanceMap = this.world.paperConfig.perPlayerMobSpawns ? new com.destroystokyo.paper.util.PlayerMobDistanceMap() : null; // Paper + } + + public void updatePlayerMobTypeMap(Entity entity) { + if (!this.world.paperConfig.perPlayerMobSpawns) { + return; + } + int chunkX = (int)Math.floor(entity.locX()) >> 4; + int chunkZ = (int)Math.floor(entity.locZ()) >> 4; + int index = entity.getEntityType().getEnumCreatureType().ordinal(); + + for (EntityPlayer player : this.playerMobDistanceMap.getPlayersInRange(chunkX, chunkZ)) { + ++player.mobCounts[index]; + } + } + + public int getMobCountNear(EntityPlayer entityPlayer, EnumCreatureType enumCreatureType) { + return entityPlayer.mobCounts[enumCreatureType.ordinal()]; } private static double a(ChunkCoordIntPair chunkcoordintpair, Entity entity) { diff --git a/src/main/java/net/minecraft/server/SpawnerCreature.java b/src/main/java/net/minecraft/server/SpawnerCreature.java index 979988c3c39afd80ce0b99a65dff5618a69ab600..661ad8f8e67046211e001ea40d97660d7c88f8e5 100644 --- a/src/main/java/net/minecraft/server/SpawnerCreature.java +++ b/src/main/java/net/minecraft/server/SpawnerCreature.java @@ -29,6 +29,11 @@ public final class SpawnerCreature { }); public static SpawnerCreature.d a(int i, Iterable iterable, SpawnerCreature.b spawnercreature_b) { + // Paper start - add countMobs parameter + return countMobs(i, iterable, spawnercreature_b, false); + } + public static SpawnerCreature.d countMobs(int i, Iterable iterable, SpawnerCreature.b spawnercreature_b, boolean countMobs) { + // Paper end - add countMobs parameter SpawnerCreatureProbabilities spawnercreatureprobabilities = new SpawnerCreatureProbabilities(); Object2IntOpenHashMap object2intopenhashmap = new Object2IntOpenHashMap(); Iterator iterator = iterable.iterator(); @@ -66,6 +71,11 @@ public final class SpawnerCreature { } object2intopenhashmap.addTo(enumcreaturetype, 1); + // Paper start + if (countMobs) { + ((WorldServer)chunk.world).getChunkProvider().playerChunkMap.updatePlayerMobTypeMap(entity); + } + // Paper end }); } } @@ -124,13 +134,31 @@ public final class SpawnerCreature { continue; } - if ((flag || !enumcreaturetype.d()) && (flag1 || enumcreaturetype.d()) && (flag2 || !enumcreaturetype.e()) && spawnercreature_d.a(enumcreaturetype, limit)) { + // Paper start - only allow spawns upto the limit per chunk and update count afterwards + int currEntityCount = spawnercreature_d.getEntityCountsByType().getInt(enumcreaturetype); + int k1 = limit * spawnercreature_d.getSpawnerChunks() / SpawnerCreature.b; + int difference = k1 - currEntityCount; + + if (worldserver.paperConfig.perPlayerMobSpawns) { + int minDiff = Integer.MAX_VALUE; + for (EntityPlayer entityplayer : worldserver.getChunkProvider().playerChunkMap.playerMobDistanceMap.getPlayersInRange(chunk.getPos())) { + minDiff = Math.min(limit - worldserver.getChunkProvider().playerChunkMap.getMobCountNear(entityplayer, enumcreaturetype), minDiff); + } + difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; + } + // Paper end + + // Paper start - per player mob spawning + if ((flag || !enumcreaturetype.d()) && (flag1 || enumcreaturetype.d()) && (flag2 || !enumcreaturetype.e()) && difference > 0) { // CraftBukkit end - a(enumcreaturetype, worldserver, chunk, (entitytypes, blockposition, ichunkaccess) -> { + int spawnCount = spawnMobs(enumcreaturetype, worldserver, chunk, (entitytypes, blockposition, ichunkaccess) -> { return spawnercreature_d.a(entitytypes, blockposition, ichunkaccess); }, (entityinsentient, ichunkaccess) -> { spawnercreature_d.a(entityinsentient, ichunkaccess); - }); + }, + difference, worldserver.paperConfig.perPlayerMobSpawns ? worldserver.getChunkProvider().playerChunkMap::updatePlayerMobTypeMap : null); + spawnercreature_d.getEntityCountsByType().mergeInt(enumcreaturetype, spawnCount, Integer::sum); + // Paper end - per player mob spawning } } @@ -139,22 +167,34 @@ public final class SpawnerCreature { } public static void a(EnumCreatureType enumcreaturetype, WorldServer worldserver, Chunk chunk, SpawnerCreature.c spawnercreature_c, SpawnerCreature.a spawnercreature_a) { + // Paper start - add parameters and int ret type + spawnMobs(enumcreaturetype, worldserver, chunk, spawnercreature_c, spawnercreature_a, Integer.MAX_VALUE, null); + } + public static int spawnMobs(EnumCreatureType enumcreaturetype, WorldServer worldserver, Chunk chunk, SpawnerCreature.c spawnercreature_c, SpawnerCreature.a spawnercreature_a, int maxSpawns, Consumer trackEntity) { + // Paper end - add parameters and int ret type BlockPosition blockposition = getRandomPosition(worldserver, chunk); if (blockposition.getY() >= 1) { - a(enumcreaturetype, worldserver, (IChunkAccess) chunk, blockposition, spawnercreature_c, spawnercreature_a); + return spawnMobsInternal(enumcreaturetype, worldserver, (IChunkAccess) chunk, blockposition, spawnercreature_c, spawnercreature_a, maxSpawns, trackEntity); } + return 0; // Paper } public static void a(EnumCreatureType enumcreaturetype, WorldServer worldserver, IChunkAccess ichunkaccess, BlockPosition blockposition, SpawnerCreature.c spawnercreature_c, SpawnerCreature.a spawnercreature_a) { + // Paper start - add maxSpawns parameter and return spawned mobs + spawnMobsInternal(enumcreaturetype, worldserver, ichunkaccess, blockposition, spawnercreature_c, spawnercreature_a, Integer.MAX_VALUE, null); + } + public static int spawnMobsInternal(EnumCreatureType enumcreaturetype, WorldServer worldserver, IChunkAccess ichunkaccess, BlockPosition blockposition, SpawnerCreature.c spawnercreature_c, SpawnerCreature.a spawnercreature_a, int maxSpawns, Consumer trackEntity) { + // Paper end - add maxSpawns parameter and return spawned mobs StructureManager structuremanager = worldserver.getStructureManager(); ChunkGenerator chunkgenerator = worldserver.getChunkProvider().getChunkGenerator(); int i = blockposition.getY(); IBlockData iblockdata = worldserver.getTypeIfLoadedAndInBounds(blockposition); // Paper - don't load chunks for mob spawn + int j = 0; // Paper - moved up if (iblockdata != null && !iblockdata.isOccluding(ichunkaccess, blockposition)) { // Paper - don't load chunks for mob spawn BlockPosition.MutableBlockPosition blockposition_mutableblockposition = new BlockPosition.MutableBlockPosition(); - int j = 0; + // Paper - moved up int k = 0; while (k < 3) { @@ -194,7 +234,7 @@ public final class SpawnerCreature { // Paper start Boolean doSpawning = a(worldserver, enumcreaturetype, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2); if (doSpawning == null) { - return; + return j; // Paper } if (doSpawning && spawnercreature_c.test(biomesettingsmobs_c.c, blockposition_mutableblockposition, ichunkaccess)) { // Paper end @@ -202,7 +242,7 @@ public final class SpawnerCreature { if (entityinsentient == null) { - return; + return j; // Paper } entityinsentient.setPositionRotation(d0, (double) i, d1, worldserver.random.nextFloat() * 360.0F, 0.0F); @@ -211,13 +251,18 @@ public final class SpawnerCreature { // CraftBukkit start worldserver.addAllEntities(entityinsentient, SpawnReason.NATURAL); if (!entityinsentient.dead) { - ++j; + ++j; // Paper - force diff on name change - we expect this to be the total amount spawned ++k1; spawnercreature_a.run(entityinsentient, ichunkaccess); + // Paper start + if (trackEntity != null) { + trackEntity.accept(entityinsentient); + } + // Paper end } // CraftBukkit end - if (j >= entityinsentient.getMaxSpawnGroup()) { - return; + if (j >= entityinsentient.getMaxSpawnGroup() || j >= maxSpawns) { // Paper + return j; // Paper } if (entityinsentient.c(k1)) { @@ -239,6 +284,7 @@ public final class SpawnerCreature { } } + return j; // Paper } private static boolean a(WorldServer worldserver, IChunkAccess ichunkaccess, BlockPosition.MutableBlockPosition blockposition_mutableblockposition, double d0) { @@ -479,8 +525,8 @@ public final class SpawnerCreature { public static class d { - private final int a; - private final Object2IntOpenHashMap b; + private final int a; final int getSpawnerChunks() { return this.a; } // Paper - OBFHELPER + private final Object2IntOpenHashMap b; final Object2IntMap getEntityCountsByType() { return this.b; } // Paper - OBFHELPER private final SpawnerCreatureProbabilities c; private final Object2IntMap d; @Nullable @@ -541,7 +587,7 @@ public final class SpawnerCreature { // CraftBukkit start private boolean a(EnumCreatureType enumcreaturetype, int limit) { - int i = limit * this.a / SpawnerCreature.b; + int i = limit * this.a / SpawnerCreature.b; // Paper - diff on change, needed in the spawn method // CraftBukkit end return this.b.getInt(enumcreaturetype) < i;