Prepare for updating server patches
This commit is contained in:
		
					parent
					
						
							
								cb95469f99
							
						
					
				
			
			
				commit
				
					
						ed6d43ddb0
					
				
			
		
					 1192 changed files with 248 additions and 309 deletions
				
			
		|  | @ -1,799 +0,0 @@ | |||
| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 | ||||
| From: kickash32 <kickash32@gmail.com> | ||||
| 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 {
 | ||||
|              Bukkit.getLogger().warning("You have enabled permission-based Anti-Xray checking - depending on your permission plugin, this may cause performance issues"); | ||||
|          } | ||||
|      } | ||||
| +
 | ||||
| +    public boolean perPlayerMobSpawns = false;
 | ||||
| +    private void perPlayerMobSpawns() {
 | ||||
| +        if (PaperConfig.version < 22) {
 | ||||
| +            set("per-player-mob-spawns", Boolean.TRUE);
 | ||||
| +        }
 | ||||
| +        perPlayerMobSpawns = getBoolean("per-player-mob-spawns", true);
 | ||||
| +    }
 | ||||
|  } | ||||
|   | ||||
| 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<ServerPlayer> EMPTY_SET = new PooledHashSets.PooledObjectLinkedOpenHashSet<>();
 | ||||
| +
 | ||||
| +    private final Map<ServerPlayer, SectionPos> players = new HashMap<>();
 | ||||
| +    // we use linked for better iteration.
 | ||||
| +    private final Long2ObjectOpenHashMap<PooledHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer>> playerMap = new Long2ObjectOpenHashMap<>(32, 0.5f);
 | ||||
| +    private int viewDistance;
 | ||||
| +
 | ||||
| +    private final PooledHashSets<ServerPlayer> pooledHashSets = new PooledHashSets<>();
 | ||||
| +
 | ||||
| +    public PooledHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> getPlayersInRange(final ChunkPos chunkPos) {
 | ||||
| +        return this.getPlayersInRange(chunkPos.x, chunkPos.z);
 | ||||
| +    }
 | ||||
| +
 | ||||
| +    public PooledHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> getPlayersInRange(final int chunkX, final int chunkZ) {
 | ||||
| +        return this.playerMap.getOrDefault(ChunkPos.asLong(chunkX, chunkZ), EMPTY_SET);
 | ||||
| +    }
 | ||||
| +
 | ||||
| +    public void update(final List<ServerPlayer> currentPlayers, final int newViewDistance) {
 | ||||
| +        AsyncCatcher.catchOp("Distance map update");
 | ||||
| +        final ObjectLinkedOpenHashSet<ServerPlayer> 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<PooledHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer>> entry : this.playerMap.long2ObjectEntrySet()) {
 | ||||
| +            final long key = entry.getLongKey();
 | ||||
| +            final PooledHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<E> {
 | ||||
| +
 | ||||
| +    // we really want to avoid that equals() check as much as possible...
 | ||||
| +    protected final Object2ObjectOpenHashMap<PooledObjectLinkedOpenHashSet<E>, PooledObjectLinkedOpenHashSet<E>> mapPool = new Object2ObjectOpenHashMap<>(64, 0.25f);
 | ||||
| +
 | ||||
| +    protected void decrementReferenceCount(final PooledObjectLinkedOpenHashSet<E> 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<E> findMapWith(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
 | ||||
| +        final PooledObjectLinkedOpenHashSet<E> 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<E> 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<E> findMapWithout(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
 | ||||
| +        if (current.set.size() == 1) {
 | ||||
| +            decrementReferenceCount(current);
 | ||||
| +            return null;
 | ||||
| +        }
 | ||||
| +
 | ||||
| +        final PooledObjectLinkedOpenHashSet<E> 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<E> 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<E> implements Iterable<E> {
 | ||||
| +
 | ||||
| +        private static final WeakReference NULL_REFERENCE = new WeakReference(null);
 | ||||
| +
 | ||||
| +        final ObjectLinkedOpenHashSet<E> set;
 | ||||
| +        int referenceCount; // -1 if special
 | ||||
| +        int hash; // optimize hashcode
 | ||||
| +
 | ||||
| +        // add cache
 | ||||
| +        WeakReference<E> lastAddObject = NULL_REFERENCE;
 | ||||
| +        WeakReference<PooledObjectLinkedOpenHashSet<E>> lastAddMap = NULL_REFERENCE;
 | ||||
| +
 | ||||
| +        // remove cache
 | ||||
| +        WeakReference<E> lastRemoveObject = NULL_REFERENCE;
 | ||||
| +        WeakReference<PooledObjectLinkedOpenHashSet<E>> 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<E> 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<E> getAddCache(final E element) {
 | ||||
| +            final E currentAdd = this.lastAddObject.get();
 | ||||
| +
 | ||||
| +            if (currentAdd == null || !(currentAdd == element || currentAdd.equals(element))) {
 | ||||
| +                return null;
 | ||||
| +            }
 | ||||
| +
 | ||||
| +            final PooledObjectLinkedOpenHashSet<E> 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<E> getRemoveCache(final E element) {
 | ||||
| +            final E currentRemove = this.lastRemoveObject.get();
 | ||||
| +
 | ||||
| +            if (currentRemove == null || !(currentRemove == element || currentRemove.equals(element))) {
 | ||||
| +                return null;
 | ||||
| +            }
 | ||||
| +
 | ||||
| +            final PooledObjectLinkedOpenHashSet<E> 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<E> map) {
 | ||||
| +            this.lastAddObject = new WeakReference<>(element);
 | ||||
| +            this.lastAddMap = new WeakReference<>(map);
 | ||||
| +        }
 | ||||
| +
 | ||||
| +        public void updateRemoveCache(final E element, final PooledObjectLinkedOpenHashSet<E> 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<E> 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 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
 | ||||
|      private final Long2ByteMap chunkTypeCache; | ||||
|      private final Queue<Runnable> 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
 | ||||
|                  ChunkMap.this.updateChunkTracking(player, new ChunkPos(rangeX, rangeZ), null, true, false); // unloaded, loaded | ||||
|              }); | ||||
|          // Paper end - no-tick view distance | ||||
| +        this.playerMobDistanceMap = this.level.paperConfig.perPlayerMobSpawns ? new com.destroystokyo.paper.util.PlayerMobDistanceMap() : null; // Paper
 | ||||
|      } | ||||
|   | ||||
| +    // 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, net.minecraft.world.entity.MobCategory mobCategory) {
 | ||||
| +        return entityPlayer.mobCounts[mobCategory.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 @@ 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 MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length;
 | ||||
| +    public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper
 | ||||
| +    public final com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> 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<Entity> entities, NaturalSpawner.ChunkGetter chunkSource) { | ||||
| +        return createState(spawningChunkCount, entities, chunkSource, false);
 | ||||
| +    }
 | ||||
| +    public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkSource, boolean countMobs) {
 | ||||
| +    // Paper end - add countMobs parameter
 | ||||
|          PotentialCalculator spawnercreatureprobabilities = new PotentialCalculator(); | ||||
|          Object2IntOpenHashMap<MobCategory> 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.canSpawnForCategory(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()) && (rareSpawn || !enumcreaturetype.isPersistent()) && 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
 | ||||
|              } | ||||
|          } | ||||
|   | ||||
| @@ -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<Entity> 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<Entity> 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 {
 | ||||
|                                      // 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); | ||||
| @@ -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) { | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Jason Penilla
				Jason Penilla