From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Thu, 27 Aug 2020 16:22:52 -0700 Subject: [PATCH] Optimise nearby player lookups Use a distance map to map out close players. Note that it's important that we cache the distance map value per chunk since the penalty of a map lookup could outweigh the benefits of searching less players (as it basically did in the outside range patch). diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java index 4588ae8037407b81c99135863eb0c4e97c564c24..2a33071c4b69cb7b5a7e296e8fd903e3a528b210 100644 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java @@ -242,6 +242,12 @@ public class ChunkHolder { long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos); this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key); this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key); + // Paper start - optimise checkDespawn + LevelChunk chunk = this.getFullChunkUnchecked(); + if (chunk != null) { + chunk.updateGeneralAreaCache(); + } + // Paper end - optimise checkDespawn } // Paper end - optimise isOutsideOfRange long lastAutoSaveTime; // Paper - incremental autosave diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 5cd766b25e63f05aa728da8dd109f0d5b91bc944..741609db2c8dfff24999b26f6fb98d1f27b9525e 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -217,6 +217,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap; // Paper end - optimise PlayerChunkMap#isOutsideRange + // Paper start - optimise checkDespawn + public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40; + public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1); + public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE; + public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap; + // Paper end - optimise checkDespawn void addPlayerToDistanceMaps(ServerPlayer player) { int chunkX = MCUtil.getChunkCoordinate(player.getX()); @@ -237,6 +243,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper end - optimise PlayerChunkMap#isOutsideRange this.playerChunkManager.addPlayer(player); // Paper - replace chunk loader + // Paper start - optimise checkDespawn + this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); + // Paper end - optimise checkDespawn } void removePlayerFromDistanceMaps(ServerPlayer player) { @@ -250,6 +259,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.playerChunkTickRangeMap.remove(player); // Paper end - optimise PlayerChunkMap#isOutsideRange this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader + // Paper start - optimise checkDespawn + this.playerGeneralAreaMap.remove(player); + // Paper end - optimise checkDespawn } void updateMaps(ServerPlayer player) { @@ -268,6 +280,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE); // Paper end - optimise PlayerChunkMap#isOutsideRange this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader + // Paper start - optimise checkDespawn + this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); + // Paper end - optimise checkDespawn } // Paper end // Paper start @@ -426,6 +441,23 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } }); // Paper end - optimise PlayerChunkMap#isOutsideRange + // Paper start - optimise checkDespawn + this.playerGeneralAreaMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); + if (chunk != null) { + chunk.updateGeneralAreaCache(newState); + } + }, + (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ, + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet newState) -> { + LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ); + if (chunk != null) { + chunk.updateGeneralAreaCache(newState); + } + }); + // Paper end - optimise checkDespawn } // Paper start - Chunk Prioritization @@ -739,7 +771,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } else { if (holder != null) { holder.setTicketLevel(level); - holder.updateRanges(); // Paper - optimise isOutsideOfRange + // Paper - move to correct place } if (holder != null) { @@ -754,6 +786,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider holder = (ChunkHolder) this.pendingUnloads.remove(pos); if (holder != null) { holder.setTicketLevel(level); + holder.updateRanges(); // Paper - optimise isOutsideOfRange // Paper - move to correct place } else { holder = new ChunkHolder(new ChunkPos(pos), level, this.level, this.lightEngine, this.queueSorter, this); // Paper start diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index f512d68cbeb243fbef8fad73472ea6587112f889..796ae447fcc305335fe943fc6d97e21422f406b1 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -392,6 +392,83 @@ public class ServerLevel extends Level implements WorldGenLevel { } } // Paper end - rewrite ticklistserver + // Paper start - optimise checkDespawn + public final List playersAffectingSpawning = new java.util.ArrayList<>(); + // Paper end - optimise checkDespawn + // Paper start - optimise get nearest players for entity AI + @Override + public final ServerPlayer getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, @Nullable LivingEntity source, + double centerX, double centerY, double centerZ) { + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; + nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); + + if (nearby == null) { + return null; + } + + Object[] backingSet = nearby.getBackingSet(); + + double closestDistanceSquared = Double.MAX_VALUE; + ServerPlayer closest = null; + + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof ServerPlayer)) { + continue; + } + ServerPlayer player = (ServerPlayer)_player; + + double distanceSquared = player.distanceToSqr(centerX, centerY, centerZ); + if (distanceSquared < closestDistanceSquared && condition.test(source, player)) { + closest = player; + closestDistanceSquared = distanceSquared; + } + } + + return closest; + } + + @Override + public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, LivingEntity entityliving) { + return this.getNearestPlayer(pathfindertargetcondition, entityliving, entityliving.getX(), entityliving.getY(), entityliving.getZ()); + } + + @Override + public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, + double d0, double d1, double d2) { + return this.getNearestPlayer(pathfindertargetcondition, null, d0, d1, d2); + } + + @Override + public List getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, LivingEntity source, AABB axisalignedbb) { + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby; + double centerX = (axisalignedbb.maxX + axisalignedbb.minX) * 0.5; + double centerZ = (axisalignedbb.maxZ + axisalignedbb.minZ) * 0.5; + nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4); + + List ret = new java.util.ArrayList<>(); + + if (nearby == null) { + return ret; + } + + Object[] backingSet = nearby.getBackingSet(); + + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof ServerPlayer)) { + continue; + } + ServerPlayer player = (ServerPlayer)_player; + + if (axisalignedbb.contains(player.getX(), player.getY(), player.getZ()) && condition.test(source, player)) { + ret.add(player); + } + } + + return ret; + } + // Paper end - optimise get nearest players for entity AI // Add env and gen to constructor, WorldData -> WorldDataServer public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, ServerLevelData iworlddataserver, ResourceKey resourcekey, DimensionType dimensionmanager, ChunkProgressListener worldloadlistener, ChunkGenerator chunkgenerator, boolean flag, long i, List list, boolean flag1, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { @@ -492,6 +569,14 @@ public class ServerLevel extends Level implements WorldGenLevel { } public void tick(BooleanSupplier shouldKeepTicking) { + // Paper start - optimise checkDespawn + this.playersAffectingSpawning.clear(); + for (ServerPlayer player : this.players) { + if (net.minecraft.world.entity.EntitySelector.affectsSpawning.test(player)) { + this.playersAffectingSpawning.add(player); + } + } + // Paper end - optimise checkDespawn ProfilerFiller gameprofilerfiller = this.getProfiler(); this.handlingTick = true; diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java index bada11542390b7575466f0e7062470665b8266c4..8a864238e154e2131834d013652746b7e7a78c97 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java @@ -789,7 +789,12 @@ public abstract class Mob extends LivingEntity { if (this.level.getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) { this.discard(); } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) { - Player entityhuman = this.level.findNearbyPlayer(this, -1.0D, EntitySelector.affectsSpawning); // Paper + // Paper start - optimise checkDespawn + Player entityhuman = this.level.findNearbyPlayer(this, level.paperConfig.hardDespawnDistance + 1, EntitySelector.affectsSpawning); // Paper + if (entityhuman == null) { + entityhuman = ((ServerLevel)this.level).playersAffectingSpawning.isEmpty() ? null : ((ServerLevel)this.level).playersAffectingSpawning.get(0); + } + // Paper end - optimise checkDespawn if (entityhuman != null) { double d0 = entityhuman.distanceToSqr((Entity) this); // CraftBukkit - decompile error diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index 5efafcadda2bfc5cab8ce5d3ede83a0467fe696f..b93056b91e7ebd49e6ddb53ccb6c05c056088df9 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -246,6 +246,69 @@ public abstract class Level implements LevelAccessor, AutoCloseable { return ret; } // Paper end + // Paper start - optimise checkDespawn + public final List getNearbyPlayers(@Nullable Entity source, double sourceX, double sourceY, + double sourceZ, double maxRange, @Nullable Predicate predicate) { + LevelChunk chunk; + if (maxRange < 0.0 || maxRange >= net.minecraft.server.level.ChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE || + (chunk = (LevelChunk)this.getChunkIfLoadedImmediately(Mth.floor(sourceX) >> 4, Mth.floor(sourceZ) >> 4)) == null) { + return this.getNearbyPlayersSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate); + } + + List ret = new java.util.ArrayList<>(); + chunk.getNearestPlayers(sourceX, sourceY, sourceZ, predicate, maxRange, ret); + return ret; + } + + private List getNearbyPlayersSlow(@Nullable Entity source, double sourceX, double sourceY, + double sourceZ, double maxRange, @Nullable Predicate predicate) { + List ret = new java.util.ArrayList<>(); + double maxRangeSquared = maxRange * maxRange; + + for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { + if ((maxRange < 0.0 || player.distanceToSqr(sourceX, sourceY, sourceZ) < maxRangeSquared)) { + if (predicate == null || predicate.test(player)) { + ret.add(player); + } + } + } + + return ret; + } + + private net.minecraft.server.level.ServerPlayer getNearestPlayerSlow(@Nullable Entity source, double sourceX, double sourceY, + double sourceZ, double maxRange, @Nullable Predicate predicate) { + net.minecraft.server.level.ServerPlayer closest = null; + double closestRangeSquared = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; + + for (net.minecraft.server.level.ServerPlayer player : (List)this.players()) { + double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); + if (distanceSquared < closestRangeSquared && (predicate == null || predicate.test(player))) { + closest = player; + closestRangeSquared = distanceSquared; + } + } + + return closest; + } + + + public final net.minecraft.server.level.ServerPlayer getNearestPlayer(@Nullable Entity source, double sourceX, double sourceY, + double sourceZ, double maxRange, @Nullable Predicate predicate) { + LevelChunk chunk; + if (maxRange < 0.0 || maxRange >= net.minecraft.server.level.ChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE || + (chunk = (LevelChunk)this.getChunkIfLoadedImmediately(Mth.floor(sourceX) >> 4, Mth.floor(sourceZ) >> 4)) == null) { + return this.getNearestPlayerSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate); + } + + return chunk.findNearestPlayer(sourceX, sourceY, sourceZ, maxRange, predicate); + } + + @Override + public @Nullable Player getNearestPlayer(double d0, double d1, double d2, double d3, @Nullable Predicate predicate) { + return this.getNearestPlayer(null, d0, d1, d2, d3, predicate); + } + // Paper end - optimise checkDespawn protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, final DimensionType dimensionmanager, Supplier supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.concurrent.Executor executor) { // Paper - Anti-Xray - Pass executor this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java index 75fc88e7def31a133e7d2a493507eacff2e7342d..bad8f0d2934218296954e9758810a68e1fedcacc 100644 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java +++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java @@ -262,7 +262,7 @@ public final class NaturalSpawner { blockposition_mutableblockposition.set(l, i, i1); double d0 = (double) l + 0.5D; double d1 = (double) i1 + 0.5D; - Player entityhuman = world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); + Player entityhuman = (chunk instanceof LevelChunk) ? ((LevelChunk)chunk).findNearestPlayer(d0, i, d1, 576.0D, net.minecraft.world.entity.EntitySelector.NO_SPECTATORS) : world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); // Paper - use chunk's player cache to optimize search in range if (entityhuman != null) { double d2 = entityhuman.distanceToSqr(d0, (double) i, d1); @@ -335,7 +335,7 @@ public final class NaturalSpawner { } private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) { - return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerThan((Position) (new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isPositionEntityTicking((BlockPos) pos)); + return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerThan((Position) (new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isPositionEntityTicking((BlockPos) pos)); // Paper - diff on change, copy into caller } private static Boolean isValidSpawnPostitionForType(ServerLevel world, MobCategory group, StructureFeatureManager structureAccessor, ChunkGenerator chunkGenerator, MobSpawnSettings.SpawnerData spawnEntry, BlockPos.MutableBlockPos pos, double squaredDistance) { // Paper diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java index 9e75ee7f43722f05dd5df4ef00f7d3e271f4c6e5..86686c24b0b7de4b4bfadbc77419a8872a8e86ee 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java @@ -369,6 +369,93 @@ public class LevelChunk implements ChunkAccess { } } // Paper end + // Paper start - optimise checkDespawn + private boolean playerGeneralAreaCacheSet; + private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet playerGeneralAreaCache; + + public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet getPlayerGeneralAreaCache() { + if (!this.playerGeneralAreaCacheSet) { + this.updateGeneralAreaCache(); + } + return this.playerGeneralAreaCache; + } + + public void updateGeneralAreaCache() { + this.updateGeneralAreaCache(((ServerLevel)this.level).getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey)); + } + + public void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet value) { + this.playerGeneralAreaCacheSet = true; + this.playerGeneralAreaCache = value; + } + + public net.minecraft.server.level.ServerPlayer findNearestPlayer(double sourceX, double sourceY, double sourceZ, + double maxRange, java.util.function.Predicate predicate) { + if (!this.playerGeneralAreaCacheSet) { + this.updateGeneralAreaCache(); + } + + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; + + if (nearby == null) { + return null; + } + + Object[] backingSet = nearby.getBackingSet(); + double closestDistance = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange; + net.minecraft.server.level.ServerPlayer closest = null; + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { + continue; + } + net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; + + double distance = player.distanceToSqr(sourceX, sourceY, sourceZ); + if (distance < closestDistance && predicate.test(player)) { + closest = player; + closestDistance = distance; + } + } + + return closest; + } + + public void getNearestPlayers(double sourceX, double sourceY, double sourceZ, java.util.function.Predicate predicate, + double range, java.util.List ret) { + if (!this.playerGeneralAreaCacheSet) { + this.updateGeneralAreaCache(); + } + + com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet nearby = this.playerGeneralAreaCache; + + if (nearby == null) { + return; + } + + double rangeSquared = range * range; + + Object[] backingSet = nearby.getBackingSet(); + for (int i = 0, len = backingSet.length; i < len; ++i) { + Object _player = backingSet[i]; + if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) { + continue; + } + net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player; + + if (range >= 0.0) { + double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ); + if (distanceSquared > rangeSquared) { + continue; + } + } + + if (predicate == null || predicate.test(player)) { + ret.add(player); + } + } + } + // Paper end - optimise checkDespawn public LevelChunk(ServerLevel worldserver, ProtoChunk protoChunk, @Nullable Consumer consumer) { this(worldserver, protoChunk.getPos(), protoChunk.getBiomes(), protoChunk.getUpgradeData(), protoChunk.getBlockTicks(), protoChunk.getLiquidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), consumer);