From a44b0cbdb1f82a9f0b9cda228ed70be91d281827 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 30bafb214b..c9a3ba4bfb 100644 --- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java +++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java @@ -60,6 +60,7 @@ public class WorldTimingsHandler { public final Timing miscMobSpawning; + public final Timing playerMobDistanceMapUpdate; public final Timing poiUnload; public final Timing chunkUnload; @@ -129,6 +130,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 9d9260ad07..fd3dbea628 100644 --- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java @@ -613,4 +613,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 0000000000..9ebd7ecb7a --- /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 0000000000..4f13d3ff83 --- /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 eb4bc66bce..a6529cefe8 100644 --- a/src/main/java/net/minecraft/server/ChunkProviderServer.java +++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java @@ -660,7 +660,22 @@ public class ChunkProviderServer extends IChunkProvider { this.world.timings.countNaturalMobs.startTiming(); // Paper - timings int l = this.chunkMapDistance.b(); EnumCreatureType[] aenumcreaturetype = EnumCreatureType.values(); - Object2IntMap object2intmap = this.world.l(); + // Paper start - per player mob spawning + int[] worldMobCount; + 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); + } + worldMobCount = this.world.countMobs(true); + } else { + worldMobCount = this.world.countMobs(false); + } + // Paper end this.world.timings.countNaturalMobs.stopTiming(); // Paper - timings this.world.getMethodProfiler().exit(); @@ -728,8 +743,23 @@ public class ChunkProviderServer extends IChunkProvider { if (enumcreaturetype != EnumCreatureType.MISC && (!enumcreaturetype.c() || this.allowAnimals) && (enumcreaturetype.c() || this.allowMonsters) && (!enumcreaturetype.d() || flag2)) { int k1 = limit * l / ChunkProviderServer.b; // CraftBukkit - use per-world limits - if (object2intmap.getInt(enumcreaturetype) <= k1) { - SpawnerCreature.a(enumcreaturetype, this.world, chunk, blockposition); + // Paper start - only allow spawns upto the limit per chunk and update count afterwards + int currEntityCount = worldMobCount[enumcreaturetype.ordinal()]; + int difference = k1 - currEntityCount; + + if (this.world.paperConfig.perPlayerMobSpawns) { + int minDiff = Integer.MAX_VALUE; + for (EntityPlayer entityplayer : this.playerChunkMap.playerMobDistanceMap.getPlayersInRange(chunk.getPos())) { + minDiff = Math.min(limit - this.playerChunkMap.getMobCountNear(entityplayer, enumcreaturetype), minDiff); + } + difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff; + } + + if (difference > 0) { + int spawnCount = SpawnerCreature.spawnMobs(enumcreaturetype, this.world, chunk, blockposition, difference, + this.world.paperConfig.perPlayerMobSpawns ? this.playerChunkMap::updatePlayerMobTypeMap : null); + worldMobCount[enumcreaturetype.ordinal()] += spawnCount; + // Paper end } } } diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java index d00b687d25..07a2310b18 100644 --- a/src/main/java/net/minecraft/server/EntityPlayer.java +++ b/src/main/java/net/minecraft/server/EntityPlayer.java @@ -81,6 +81,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; @@ -116,6 +121,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting { this.displayName = this.getName(); 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. @@ -1771,6 +1777,7 @@ public class EntityPlayer extends EntityHuman implements ICrafting { } + public SectionPosition getPlayerMapSection() { return this.K(); } // Paper - OBFHELPER public SectionPosition K() { return this.cs; } diff --git a/src/main/java/net/minecraft/server/EntityTypes.java b/src/main/java/net/minecraft/server/EntityTypes.java index 79f8549660..4100e367a4 100644 --- a/src/main/java/net/minecraft/server/EntityTypes.java +++ b/src/main/java/net/minecraft/server/EntityTypes.java @@ -254,6 +254,7 @@ public class EntityTypes { return this.bf; } + public EnumCreatureType getEnumCreatureType() { return this.e(); } // Paper - OBFHELPER public EnumCreatureType e() { return this.bb; } diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java index 893c542f57..09440fffb5 100644 --- a/src/main/java/net/minecraft/server/PlayerChunkMap.java +++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java @@ -78,7 +78,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { private final PlayerMap playerMap; public final Int2ObjectMap trackedEntities; private final Queue z; - 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(); @@ -165,6 +166,24 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d { this.l = supplier; this.m = new VillagePlace(new File(this.w, "poi"), datafixer, 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 fdac5bb3a2..58bbf2f9d2 100644 --- a/src/main/java/net/minecraft/server/SpawnerCreature.java +++ b/src/main/java/net/minecraft/server/SpawnerCreature.java @@ -3,6 +3,7 @@ package net.minecraft.server; import java.util.List; import java.util.Objects; import java.util.Random; +import java.util.function.Consumer; // Paper import javax.annotation.Nullable; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -16,9 +17,14 @@ public final class SpawnerCreature { private static final Logger LOGGER = LogManager.getLogger(); + // Paper start - add maxSpawns parameter and return spawned mobs public static void a(EnumCreatureType enumcreaturetype, WorldServer worldserver, Chunk chunk, BlockPosition blockposition) { + spawnMobs(enumcreaturetype, worldserver, chunk, blockposition, Integer.MAX_VALUE, null); + } + public static int spawnMobs(EnumCreatureType enumcreaturetype, WorldServer worldserver, Chunk chunk, BlockPosition blockposition, int maxSpawns, Consumer trackEntity) { + // Paper end ChunkGenerator chunkgenerator = worldserver.getChunkProvider().getChunkGenerator(); - int i = 0; + int i = 0; // Paper - force diff on name change BlockPosition blockposition1 = getRandomPosition(worldserver, chunk); int j = blockposition1.getX(); int k = blockposition1.getY(); @@ -88,7 +94,7 @@ public final class SpawnerCreature { ); if (!event.callEvent()) { if (event.shouldAbortSpawn()) { - return; + return i; // Paper } ++i2; continue; @@ -107,7 +113,7 @@ public final class SpawnerCreature { } catch (Exception exception) { SpawnerCreature.LOGGER.warn("Failed to create mob", exception); ServerInternalException.reportInternalException(exception); // Paper - return; + return i; // Paper } entityinsentient.setPositionRotation((double) f, (double) k, (double) f1, worldserver.random.nextFloat() * 360.0F, 0.0F); @@ -115,12 +121,16 @@ public final class SpawnerCreature { groupdataentity = entityinsentient.prepare(worldserver, worldserver.getDamageScaler(new BlockPosition(entityinsentient)), EnumMobSpawn.NATURAL, groupdataentity, (NBTTagCompound) null); // CraftBukkit start if (worldserver.addEntity(entityinsentient, SpawnReason.NATURAL)) { - ++i; + ++i; // Paper - force diff on name change ++i2; + if (trackEntity != null) { + trackEntity.accept(entityinsentient); // Paper + } } + if (i >= maxSpawns) { return i; } // Paper // CraftBukkit end if (i >= entityinsentient.getMaxSpawnGroup()) { - return; + return i; // Paper } if (entityinsentient.c(i2)) { @@ -146,6 +156,7 @@ public final class SpawnerCreature { } } + return i; // Paper } @Nullable diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java index 08e9444fad..1b823bcc08 100644 --- a/src/main/java/net/minecraft/server/WorldServer.java +++ b/src/main/java/net/minecraft/server/WorldServer.java @@ -1028,7 +1028,20 @@ public class WorldServer extends World { } public Object2IntMap l() { - Object2IntMap object2intmap = new Object2IntOpenHashMap(); + // Paper start + int[] values = this.countMobs(false); + EnumCreatureType[] byId = EnumCreatureType.values(); + Object2IntMap ret = new Object2IntOpenHashMap<>(); + + for (int i = 0, len = values.length; i < len; ++i) { + ret.put(byId[i], values[i]); + } + + return ret; + } + public int[] countMobs(boolean updatePlayerCounts) { + int[] ret = new int[EntityPlayer.ENUMCREATURETYPE_TOTAL_ENUMS]; + // Paper end ObjectIterator objectiterator = this.entitiesById.values().iterator(); while (objectiterator.hasNext()) { @@ -1053,11 +1066,16 @@ public class WorldServer extends World { continue; } // Paper end - object2intmap.mergeInt(enumcreaturetype, 1, Integer::sum); + // Paper start - rework mob spawning + if (updatePlayerCounts) { + this.getChunkProvider().playerChunkMap.updatePlayerMobTypeMap(entity); + } + ++ret[enumcreaturetype.ordinal()]; + // Paper end } } - return object2intmap; + return ret; } @Override -- 2.25.1