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 fe79c0add4f7cb18d487c5bb9415c40c5b551ea2..8d9ddad1879e7616d980ca70de8aecacaa86db35 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; @@ -121,6 +122,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 be4a36df28d4f16727daad1270d5c3a84ae94613..16f013ffe992a934e9d0b32e764a14a8fd204449 100644 --- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java +++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java @@ -503,5 +503,10 @@ 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..72063ba7fb0d04594043cb07034590d597c3d77e --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java @@ -0,0 +1,252 @@ +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.getLastSectionPos(); + 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.getLastSectionPos(); + + 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..11de56afaf059b00fa5bec293516bcdce7c4b2b9 --- /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/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 273cd2e0fc38801a5ecb26579e4d0e9ee017bb3c..e1498a5a8e664b922c77a8524ed2ea38c91834ce 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -69,6 +69,7 @@ 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; @@ -140,6 +141,7 @@ 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(); @@ -263,6 +265,7 @@ 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, @@ -304,6 +307,25 @@ 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 c47d1772044913475a60292162ef4be594bed4c6..78143fe566fef13604c46029d4184ba39ed4aefc 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -754,7 +754,22 @@ 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 92a77fa90828cc4bc3d9217d2df37ffeec6ee58e..ec7804e80f9c6b0fc53eb67823c6325a685c850b 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -90,12 +90,7 @@ 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; @@ -223,6 +218,11 @@ 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; @@ -317,6 +317,7 @@ 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 19d0ed5ff8569b0280117750f9ce05095afd9f70..55b937f802ee7066cb13b9a497932038b2905ff0 100644 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java +++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java @@ -17,6 +17,7 @@ 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; @@ -65,7 +66,12 @@ 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(); @@ -103,6 +109,11 @@ public final class NaturalSpawner { } object2intopenhashmap.addTo(enumcreaturetype, 1); + // Paper start + if (countMobs) { + chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity); + } + // Paper end }); } } @@ -161,13 +172,31 @@ 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.mobCategoryCounts.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.mobCategoryCounts.mergeInt(enumcreaturetype, spawnCount, Integer::sum); + // Paper end - per player mob spawning } } @@ -175,12 +204,18 @@ 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 @@ -191,15 +226,21 @@ 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) { @@ -241,14 +282,14 @@ public final class NaturalSpawner { // Paper start Boolean doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2); if (doSpawning == null) { - return; + return j; // Paper } if (doSpawning && checker.test(biomesettingsmobs_c.type, blockposition_mutableblockposition, chunk)) { // Paper end 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); @@ -260,10 +301,15 @@ 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)) { @@ -285,6 +331,7 @@ public final class NaturalSpawner { } } + return j; // Paper } private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) {