Files
paper-mc/paper-server/patches/sources/io/papermc/paper/FeatureHooks.java.patch
Spottedleaf 38c1ddb52a Add and use FeatureHooks.getAllEntities
The ServerLevel#getAllEntities function only returns entities which
are accessible. FeatureHooks#getAllEntities will return all
entities, whether or not they are accessible.

Use the new hook in the EntityCommand, which allows server admins
to inspect entities in unloaded chunks.

Use the hook as well for ticking the EntityScheduler. This fixes
an issue whether unloaded entities did not have their scheduler ticked.
2025-06-24 04:55:58 -07:00

250 lines
12 KiB
Diff

--- /dev/null
+++ b/io/papermc/paper/FeatureHooks.java
@@ -1,0 +_,246 @@
+package io.papermc.paper;
+
+import io.papermc.paper.command.PaperSubcommand;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import it.unimi.dsi.fastutil.longs.LongSet;
+import it.unimi.dsi.fastutil.longs.LongSets;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ObjectSet;
+import it.unimi.dsi.fastutil.objects.ObjectSets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import net.minecraft.core.Registry;
+import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.monster.Spider;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.biome.Biome;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import net.minecraft.world.level.chunk.PalettedContainer;
+import org.bukkit.Chunk;
+import org.bukkit.World;
+
+public final class FeatureHooks {
+
+ // this includes non-accessible entities
+ public static Iterable<Entity> getAllEntities(final net.minecraft.server.level.ServerLevel world) {
+ return ((net.minecraft.world.level.entity.LevelEntityGetterAdapter<Entity>)world.getEntities()).sectionStorage.getAllEntities();
+ }
+
+ public static void setPlayerChunkUnloadDelay(final long ticks) {
+ }
+
+ public static void initChunkTaskScheduler(final boolean useParallelGen) {
+ }
+
+ public static void registerPaperCommands(final Map<Set<String>, PaperSubcommand> commands) {
+ }
+
+ public static LevelChunkSection createSection(final Registry<Biome> biomeRegistry, final Level level, final ChunkPos chunkPos, final int chunkSection) {
+ return new LevelChunkSection(biomeRegistry);
+ }
+
+ public static void sendChunkRefreshPackets(final List<ServerPlayer> playersInRange, final LevelChunk chunk) {
+ final ClientboundLevelChunkWithLightPacket refreshPacket = new ClientboundLevelChunkWithLightPacket(chunk, chunk.level.getLightEngine(), null, null);
+ for (final ServerPlayer player : playersInRange) {
+ if (player.connection == null) continue;
+
+ player.connection.send(refreshPacket);
+ }
+ }
+
+ public static PalettedContainer<BlockState> emptyPalettedBlockContainer() {
+ return new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, Blocks.AIR.defaultBlockState(), PalettedContainer.Strategy.SECTION_STATES);
+ }
+
+ public static Set<Long> getSentChunkKeys(final ServerPlayer player) {
+ final LongSet keys = new LongOpenHashSet();
+ player.getChunkTrackingView().forEach(pos -> keys.add(pos.longKey));
+ return LongSets.unmodifiable(keys);
+ }
+
+ public static Set<Chunk> getSentChunks(final ServerPlayer player) {
+ final ObjectSet<Chunk> chunks = new ObjectOpenHashSet<>();
+ final World world = player.level().getWorld();
+ player.getChunkTrackingView().forEach(pos -> {
+ final org.bukkit.Chunk chunk = world.getChunkAt(pos.longKey);
+ chunks.add(chunk);
+ });
+ return ObjectSets.unmodifiable(chunks);
+ }
+
+ public static boolean isChunkSent(final ServerPlayer player, final long chunkKey) {
+ return player.getChunkTrackingView().contains(new ChunkPos(chunkKey));
+ }
+
+ public static boolean isSpiderCollidingWithWorldBorder(final Spider spider) {
+ return true; // ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(spider.level().getWorldBorder(), spider.getBoundingBox().inflate(ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.COLLISION_EPSILON))
+ }
+
+ public static void dumpAllChunkLoadInfo(net.minecraft.server.MinecraftServer server, boolean isLongTimeout) {
+ }
+
+ private static void dumpEntity(final Entity entity) {
+ }
+
+ public static org.bukkit.entity.Entity[] getChunkEntities(net.minecraft.server.level.ServerLevel world, int chunkX, int chunkZ) {
+ world.getChunk(chunkX, chunkZ); // ensure full loaded
+
+ net.minecraft.world.level.entity.PersistentEntitySectionManager<net.minecraft.world.entity.Entity> entityManager = world.entityManager;
+ long pair = ChunkPos.asLong(chunkX, chunkZ);
+
+ if (entityManager.areEntitiesLoaded(pair)) {
+ return entityManager.getEntities(new ChunkPos(chunkX, chunkZ)).stream()
+ .map(net.minecraft.world.entity.Entity::getBukkitEntity)
+ .filter(java.util.Objects::nonNull).toArray(org.bukkit.entity.Entity[]::new);
+ }
+
+ entityManager.ensureChunkQueuedForLoad(pair); // Start entity loading
+
+ // SPIGOT-6772: Use entity mailbox and re-schedule entities if they get unloaded
+ net.minecraft.util.thread.ConsecutiveExecutor mailbox = ((net.minecraft.world.level.chunk.storage.EntityStorage) entityManager.permanentStorage).entityDeserializerQueue;
+ java.util.function.BooleanSupplier supplier = () -> {
+ // only execute inbox if our entities are not present
+ if (entityManager.areEntitiesLoaded(pair)) {
+ return true;
+ }
+
+ if (!entityManager.isPending(pair)) {
+ // Our entities got unloaded, this should normally not happen.
+ entityManager.ensureChunkQueuedForLoad(pair); // Re-start entity loading
+ }
+
+ // tick loading inbox, which loads the created entities to the world
+ // (if present)
+ entityManager.tick();
+ // check if our entities are loaded
+ return entityManager.areEntitiesLoaded(pair);
+ };
+
+ // now we wait until the entities are loaded,
+ // the converting from NBT to entity object is done on the main Thread which is why we wait
+ while (!supplier.getAsBoolean()) {
+ if (mailbox.size() != 0) {
+ mailbox.run();
+ } else {
+ Thread.yield();
+ java.util.concurrent.locks.LockSupport.parkNanos("waiting for entity loading", 100000L);
+ }
+ }
+
+ return entityManager.getEntities(new ChunkPos(chunkX, chunkZ)).stream()
+ .map(net.minecraft.world.entity.Entity::getBukkitEntity)
+ .filter(java.util.Objects::nonNull).toArray(org.bukkit.entity.Entity[]::new);
+ }
+
+ public static java.util.Collection<org.bukkit.plugin.Plugin> getPluginChunkTickets(net.minecraft.server.level.ServerLevel world,
+ int x, int z) {
+ net.minecraft.server.level.DistanceManager chunkDistanceManager = world.getChunkSource().chunkMap.distanceManager;
+ List<net.minecraft.server.level.Ticket> tickets = chunkDistanceManager.ticketStorage.tickets.get(ChunkPos.asLong(x, z));
+
+ if (tickets == null) {
+ return java.util.Collections.emptyList();
+ }
+
+ com.google.common.collect.ImmutableList.Builder<org.bukkit.plugin.Plugin> ret = com.google.common.collect.ImmutableList.builder();
+ for (net.minecraft.server.level.Ticket ticket : tickets) {
+ if (ticket.getType() == net.minecraft.server.level.TicketType.PLUGIN_TICKET) {
+ ret.add((org.bukkit.plugin.Plugin) ticket.getIdentifier());
+ }
+ }
+
+ return ret.build();
+ }
+
+ public static Map<org.bukkit.plugin.Plugin, java.util.Collection<org.bukkit.Chunk>> getPluginChunkTickets(net.minecraft.server.level.ServerLevel world) {
+ Map<org.bukkit.plugin.Plugin, com.google.common.collect.ImmutableList.Builder<Chunk>> ret = new HashMap<>();
+ net.minecraft.server.level.DistanceManager chunkDistanceManager = world.getChunkSource().chunkMap.distanceManager;
+
+ for (it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry<List<net.minecraft.server.level.Ticket>> chunkTickets : chunkDistanceManager.ticketStorage.tickets.long2ObjectEntrySet()) {
+ long chunkKey = chunkTickets.getLongKey();
+ List<net.minecraft.server.level.Ticket> tickets = chunkTickets.getValue();
+
+ org.bukkit.Chunk chunk = null;
+ for (net.minecraft.server.level.Ticket ticket : tickets) {
+ if (ticket.getType() != net.minecraft.server.level.TicketType.PLUGIN_TICKET) {
+ continue;
+ }
+
+ if (chunk == null) {
+ chunk = world.getWorld().getChunkAt(ChunkPos.getX(chunkKey), ChunkPos.getZ(chunkKey));
+ }
+
+ ret.computeIfAbsent((org.bukkit.plugin.Plugin) ticket.getIdentifier(), (key) -> com.google.common.collect.ImmutableList.builder()).add(chunk);
+ }
+ }
+
+ return ret.entrySet().stream().collect(com.google.common.collect.ImmutableMap.toImmutableMap(Map.Entry::getKey, (entry) -> entry.getValue().build()));
+ }
+
+ public static int getViewDistance(net.minecraft.server.level.ServerLevel world) {
+ return world.getChunkSource().chunkMap.serverViewDistance;
+ }
+
+ public static int getSimulationDistance(net.minecraft.server.level.ServerLevel world) {
+ return world.getChunkSource().chunkMap.getDistanceManager().simulationDistance;
+ }
+
+ public static int getSendViewDistance(net.minecraft.server.level.ServerLevel world) {
+ return getViewDistance(world);
+ }
+
+ public static void setViewDistance(net.minecraft.server.level.ServerLevel world, int distance) {
+ if (distance < 2 || distance > 32) {
+ throw new IllegalArgumentException("View distance " + distance + " is out of range of [2, 32]");
+ }
+ world.chunkSource.chunkMap.setServerViewDistance(distance);
+ }
+
+ public static void setSimulationDistance(net.minecraft.server.level.ServerLevel world, int distance) {
+ if (distance < 2 || distance > 32) {
+ throw new IllegalArgumentException("Simulation distance " + distance + " is out of range of [2, 32]");
+ }
+ world.chunkSource.chunkMap.distanceManager.updateSimulationDistance(distance);
+ }
+
+ public static void setSendViewDistance(net.minecraft.server.level.ServerLevel world, int distance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public static void tickEntityManager(net.minecraft.server.level.ServerLevel world) {
+ world.entityManager.tick();
+ }
+
+ public static void closeEntityManager(net.minecraft.server.level.ServerLevel world, boolean save) {
+ try {
+ world.entityManager.close(save);
+ } catch (final java.io.IOException exception) {
+ throw new RuntimeException("Failed to close entity manager", exception);
+ }
+ }
+
+ public static java.util.concurrent.Executor getWorldgenExecutor() {
+ return net.minecraft.Util.backgroundExecutor();
+ }
+
+ public static void setViewDistance(ServerPlayer player, int distance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public static void setSimulationDistance(ServerPlayer player, int distance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ public static void setSendViewDistance(ServerPlayer player, int distance) {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+}