From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sun, 2 Oct 2022 21:28:53 -0700 Subject: [PATCH] Threaded Regions See https://docs.papermc.io/folia/reference/overview and https://docs.papermc.io/folia/reference/region-logic diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java index 0abba00741b39b69a7f167e5d2670f2565c9a752..83b052dbf6d21775664b286518f3cef1d86e87d1 100644 --- a/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/ChunkSystem.java @@ -72,11 +72,15 @@ public final class ChunkSystem { } public static void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { - + // Folia start - threaded regions + level.regioniser.addChunk(holder.getPos().x, holder.getPos().z); + // Folia end - threaded regions } public static void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { - + // Folia start - threaded regions + level.regioniser.removeChunk(holder.getPos().x, holder.getPos().z); + // Folia end - threaded regions } public static void onChunkPreBorder(final LevelChunk chunk, final ChunkHolder holder) { @@ -85,16 +89,12 @@ public final class ChunkSystem { } public static void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add( - ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().addChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading chunk.loadCallback(); } public static void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove( - ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().removeChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading chunk.unloadCallback(); } @@ -104,9 +104,7 @@ public final class ChunkSystem { } public static void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add( - ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().addTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { chunk.postProcessGeneration(); } @@ -115,21 +113,15 @@ public final class ChunkSystem { } public static void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove( - ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().removeTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading } public static void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add( - ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().addEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading } public static void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove( - ((ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().removeEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading } public static ChunkHolder getUnloadingChunkHolder(final ServerLevel level, final int chunkX, final int chunkZ) { diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java index 11b7f15755dde766140c29bedca456c80d53293f..7d626bec6f0a4497026de6c0311e27cf95cfd757 100644 --- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java @@ -1,5 +1,11 @@ package ca.spottedleaf.moonrise.common.util; +import io.papermc.paper.threadedregions.RegionShutdownThread; +import io.papermc.paper.threadedregions.RegionizedServer; +import io.papermc.paper.threadedregions.RegionizedWorldData; +import io.papermc.paper.threadedregions.ThreadedRegionizer; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import io.papermc.paper.threadedregions.TickRegions; import net.minecraft.core.BlockPos; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.ChunkPos; @@ -94,46 +100,149 @@ public class TickThread extends Thread { } public static boolean isShutdownThread() { - return false; + return Thread.currentThread().getClass() == RegionShutdownThread.class; } public static boolean isTickThreadFor(final Level world, final BlockPos pos) { - return isTickThread(); + return isTickThreadFor(world, pos.getX() >> 4, pos.getZ() >> 4); } public static boolean isTickThreadFor(final Level world, final ChunkPos pos) { - return isTickThread(); + return isTickThreadFor(world, pos.x, pos.z); } public static boolean isTickThreadFor(final Level world, final Vec3 pos) { - return isTickThread(); + return isTickThreadFor(world, net.minecraft.util.Mth.floor(pos.x) >> 4, net.minecraft.util.Mth.floor(pos.z) >> 4); } public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ) { - return isTickThread(); + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + return isShutdownThread(); + } + return ((net.minecraft.server.level.ServerLevel)world).regioniser.getRegionAtUnsynchronised(chunkX, chunkZ) == region; } public static boolean isTickThreadFor(final Level world, final AABB aabb) { - return isTickThread(); + return isTickThreadFor( + world, + CoordinateUtils.getChunkCoordinate(aabb.minX), CoordinateUtils.getChunkCoordinate(aabb.minZ), + CoordinateUtils.getChunkCoordinate(aabb.maxX), CoordinateUtils.getChunkCoordinate(aabb.maxZ) + ); } public static boolean isTickThreadFor(final Level world, final double blockX, final double blockZ) { - return isTickThread(); + return isTickThreadFor(world, CoordinateUtils.getChunkCoordinate(blockX), CoordinateUtils.getChunkCoordinate(blockZ)); } public static boolean isTickThreadFor(final Level world, final Vec3 position, final Vec3 deltaMovement, final int buffer) { - return isTickThread(); + final int fromChunkX = CoordinateUtils.getChunkX(position); + final int fromChunkZ = CoordinateUtils.getChunkZ(position); + + final int toChunkX = CoordinateUtils.getChunkCoordinate(position.x + deltaMovement.x); + final int toChunkZ = CoordinateUtils.getChunkCoordinate(position.z + deltaMovement.z); + + // expect from < to, but that may not be the case + return isTickThreadFor( + world, + Math.min(fromChunkX, toChunkX) - buffer, + Math.min(fromChunkZ, toChunkZ) - buffer, + Math.max(fromChunkX, toChunkX) + buffer, + Math.max(fromChunkZ, toChunkZ) + buffer + ); } public static boolean isTickThreadFor(final Level world, final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ) { - return isTickThread(); + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + return isShutdownThread(); + } + + final int shift = ((net.minecraft.server.level.ServerLevel)world).regioniser.sectionChunkShift; + + final int minSectionX = fromChunkX >> shift; + final int maxSectionX = toChunkX >> shift; + final int minSectionZ = fromChunkZ >> shift; + final int maxSectionZ = toChunkZ >> shift; + + for (int secZ = minSectionZ; secZ <= maxSectionZ; ++secZ) { + for (int secX = minSectionX; secX <= maxSectionX; ++secX) { + final int lowerLeftCX = secX << shift; + final int lowerLeftCZ = secZ << shift; + if (((net.minecraft.server.level.ServerLevel)world).regioniser.getRegionAtUnsynchronised(lowerLeftCX, lowerLeftCZ) != region) { + return false; + } + } + } + + return true; } public static boolean isTickThreadFor(final Level world, final int chunkX, final int chunkZ, final int radius) { - return isTickThread(); + return isTickThreadFor(world, chunkX - radius, chunkZ - radius, chunkX + radius, chunkZ + radius); } public static boolean isTickThreadFor(final Entity entity) { - return isTickThread(); + if (entity == null) { + return true; + } + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + if (RegionizedServer.isGlobalTickThread()) { + if (entity instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { + final net.minecraft.server.network.ServerGamePacketListenerImpl possibleBad = serverPlayer.connection; + if (possibleBad == null) { + return true; + } + + final net.minecraft.network.PacketListener packetListener = possibleBad.connection.getPacketListener(); + if (packetListener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) { + return gamePacketListener.waitingForSwitchToConfig; + } + if (packetListener instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { + return !configurationPacketListener.switchToMain; + } + return true; + } else { + return false; + } + } + if (isShutdownThread()) { + return true; + } + if (entity instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { + // off-main access to server player is never ok, server player is owned by one of global context or region context always + return false; + } + // only own entities that have not yet been added to the world + + // if the entity is removed, then it was in the world previously - which means that a region containing its location + // owns it + // if the entity has a callback, then it is contained in a world + return entity.hasNullCallback() && !entity.isRemoved(); + } + + final Level world = entity.level(); + if (world != region.regioniser.world) { + // world mismatch + return false; + } + + final RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + + // pass through the check if the entity is removed and we own its chunk + if (worldData.hasEntity(entity)) { + return true; + } + + if (entity instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { + net.minecraft.server.network.ServerGamePacketListenerImpl conn = serverPlayer.connection; + return conn != null && worldData.connections.contains(conn.connection); + } else { + return ((entity.hasNullCallback() || entity.isRemoved())) && isTickThreadFor((net.minecraft.server.level.ServerLevel)world, entity.chunkPosition()); + } } } diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java index efc0c1acc8239dd7b00211a1d3bfd3fc3b2c810c..d13902e58c9bf6b25469a432bb87230da98b9d12 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java @@ -460,6 +460,19 @@ public abstract class EntityLookup implements LevelEntityGetter { return slices == null || !slices.isPreventingStatusUpdates(); } + // Folia start - region threading + // only appropriate to use when in shutdown, as this performs no logic hooks to properly add to world + public boolean addEntityForShutdownTeleportComplete(final Entity entity) { + final BlockPos pos = entity.blockPosition(); + final int sectionX = pos.getX() >> 4; + final int sectionY = Mth.clamp(pos.getY() >> 4, this.minSection, this.maxSection); + final int sectionZ = pos.getZ() >> 4; + final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); + + return slices.addEntity(entity, sectionY); + } + // Folia end - region threading + protected void removeEntity(final Entity entity) { final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY(); @@ -1067,6 +1080,9 @@ public abstract class EntityLookup implements LevelEntityGetter { EntityLookup.this.removeEntityCallback(entity); this.entity.setLevelCallback(NoOpCallback.INSTANCE); + + // only AFTER full removal callbacks, so that thread checking will work. // Folia - region threading + EntityLookup.this.world.getCurrentWorldData().removeEntity(entity); // Folia - region threading } } @@ -1080,4 +1096,4 @@ public abstract class EntityLookup implements LevelEntityGetter { @Override public void onRemove(final Entity.RemovalReason reason) {} } -} \ No newline at end of file +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java index dacf2b2988ce603879fe525a3418ac77f8a663f7..e479cea245b0a345c46c9bb5fd9d86afba6fd77e 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java @@ -16,8 +16,7 @@ public final class ServerEntityLookup extends EntityLookup { private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0]; private final ServerLevel serverWorld; - public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker - public final ReferenceList trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker + // Folia - move to regionized world data public ServerEntityLookup(final ServerLevel world, final LevelCallback worldCallback) { super(world, worldCallback); @@ -70,6 +69,7 @@ public final class ServerEntityLookup extends EntityLookup { if (entity instanceof ServerPlayer player) { ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player); } + this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading } @Override @@ -77,22 +77,22 @@ public final class ServerEntityLookup extends EntityLookup { if (entity instanceof ServerPlayer player) { ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().removePlayer(player); } - this.trackerUnloadedEntities.remove(entity); // Moonrise - entity tracker + this.world.getCurrentWorldData().trackerUnloadedEntities.remove(entity); // Moonrise - entity tracker // Folia - region threading } @Override protected void entityStartLoaded(final Entity entity) { // Moonrise start - entity tracker - this.trackerEntities.add(entity); - this.trackerUnloadedEntities.remove(entity); + this.world.getCurrentWorldData().trackerEntities.add(entity); // Folia - region threading + this.world.getCurrentWorldData().trackerUnloadedEntities.remove(entity); // Folia - region threading // Moonrise end - entity tracker } @Override protected void entityEndLoaded(final Entity entity) { // Moonrise start - entity tracker - this.trackerEntities.remove(entity); - this.trackerUnloadedEntities.add(entity); + this.world.getCurrentWorldData().trackerEntities.remove(entity); // Folia - region threading + this.world.getCurrentWorldData().trackerUnloadedEntities.add(entity); // Folia - region threading // Moonrise end - entity tracker } diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java index a608f57ebca98eda88ad749d0aad021678be54f9..433abf4237f698872db0e07d92b680408109d470 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java @@ -213,7 +213,7 @@ public final class RegionizedPlayerChunkLoader { final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); if (loader == null) { - return; + throw new IllegalStateException("Player is already removed from player chunk loader"); // Folia - region threading } loader.remove(); @@ -301,7 +301,7 @@ public final class RegionizedPlayerChunkLoader { public void tick() { TickThread.ensureTickThread("Cannot tick player chunk loader async"); long currTime = System.nanoTime(); - for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) { + for (final ServerPlayer player : new java.util.ArrayList<>(this.world.getLocalPlayers())) { // Folia - region threding final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); if (loader == null || loader.removed || loader.world != this.world) { // not our problem anymore diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java index 7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309..4bfcae47ed76346e6200514ebce5b04f907c5026 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java @@ -29,6 +29,39 @@ public final class ChunkUnloadQueue { public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {} + // Folia start - threaded regions + public List retrieveForCurrentRegion() { + final io.papermc.paper.threadedregions.ThreadedRegionizer.ThreadedRegion region = + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion(); + final io.papermc.paper.threadedregions.ThreadedRegionizer regionizer = region.regioniser; + final int shift = this.coordinateShift; + + final List ret = new ArrayList<>(); + + for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) { + final ConcurrentLong2ReferenceChainedHashTable.TableEntry entry = iterator.next(); + final long key = entry.getKey(); + final UnloadSection section = entry.getValue(); + final int sectionX = CoordinateUtils.getChunkX(key); + final int sectionZ = CoordinateUtils.getChunkZ(key); + final int chunkX = sectionX << shift; + final int chunkZ = sectionZ << shift; + + if (regionizer.getRegionAtUnsynchronised(chunkX, chunkZ) != region) { + continue; + } + + ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size())); + } + + ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> { + return Long.compare(s1.order, s2.order); + }); + + return ret; + } + // Folia end - threaded regions + public List retrieveForAllRegions() { final List ret = new ArrayList<>(); @@ -141,4 +174,4 @@ public final class ChunkUnloadQueue { this.order = order; } } -} \ No newline at end of file +} diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java index 58d3d1a47e9f2423c467bb329c2d5f4b58a8b5ef..4b36209f016b025087da359ab49e44bd677cd937 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java @@ -56,6 +56,14 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; import java.util.function.Predicate; +// Folia start - region threading +import io.papermc.paper.threadedregions.RegionizedServer; +import io.papermc.paper.threadedregions.ThreadedRegionizer; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +// Folia end - region threading + public final class ChunkHolderManager { private static final Logger LOGGER = LogUtils.getClassLogger(); @@ -78,29 +86,83 @@ public final class ChunkHolderManager { private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); private final ServerLevel world; private final ChunkTaskScheduler taskScheduler; - private long currentTick; + // Folia start - region threading + public static final class HolderManagerRegionData { + private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); + private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { + if (c1 == c2) { + return 0; + } + + final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); + + if (saveTickCompare != 0) { + return saveTickCompare; + } + + final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); + final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); - private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); - private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { - if (c1 == c2) { - return 0; + if (coord1 == coord2) { + throw new IllegalStateException("Duplicate chunkholder in auto save queue"); + } + + return Long.compare(coord1, coord2); + }); + + public void merge(final HolderManagerRegionData into, final long tickOffset) { + // Order doesn't really matter for the pending full update... + into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate); + + // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating + // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant + // addition to every entry will not affect compareTo). + for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) { + holder.lastAutoSave += tickOffset; + into.autoSaveQueue.add(holder); + } } - final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); + public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, + final ReferenceOpenHashSet dataSet) { + for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) { + final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift; + final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift; + + final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); + if (data != null) { + data.pendingFullLoadUpdate.add(fullLoadUpdate); + } // else: fullLoadUpdate is an unloaded chunk holder + } + + for (final NewChunkHolder autoSave : this.autoSaveQueue) { + final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift; + final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift; - if (saveTickCompare != 0) { - return saveTickCompare; + final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); + if (data != null) { + data.autoSaveQueue.add(autoSave); + } // else: autoSave is an unloaded chunk holder + } } + } - final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); - final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); + private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); - if (coord1 == coord2) { - throw new IllegalStateException("Duplicate chunkholder in auto save queue"); + if (region == null) { + return null; } - return Long.compare(coord1, coord2); - }); + if (this.world != null && this.world != region.getData().world) { + throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); + } + + return region.getData().getHolderManagerRegionData(); + } + // Folia end - region threading + public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { this.world = world; @@ -185,8 +247,13 @@ public final class ChunkHolderManager { } public void close(final boolean save, final boolean halt) { + // Folia start - region threading + this.close(save, halt, true, true, true); + } + public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) { + // Folia end - region threading TickThread.ensureTickThread("Closing world off-main"); - if (halt) { + if (halt && halt) { // Folia - region threading LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { LOGGER.warn("Failed to halt world generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); @@ -196,9 +263,10 @@ public final class ChunkHolderManager { } if (save) { - this.saveAllChunks(true, true, true); + this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading } + if (last) { // Folia - region threading boolean hasTasks = false; for (final RegionFileIOThread.RegionFileType type : RegionFileIOThread.RegionFileType.values()) { if (RegionFileIOThread.getControllerFor(this.world, type).hasTasks()) { @@ -218,28 +286,35 @@ public final class ChunkHolderManager { LOGGER.error("Failed to close '" + type.name() + "' regionfile cache for world '" + WorldUtil.getWorldName(this.world) + "'", ex); } } + } // Folia - region threading } void ensureInAutosave(final NewChunkHolder holder) { - if (!this.autoSaveQueue.contains(holder)) { - holder.lastAutoSave = this.currentTick; - this.autoSaveQueue.add(holder); + // Folia start - region threading + final HolderManagerRegionData regionData = this.getCurrentRegionData(); + if (!regionData.autoSaveQueue.contains(holder)) { + holder.lastAutoSave = RegionizedServer.getCurrentTick(); + regionData.autoSaveQueue.add(holder); + // Folia end - region threading } } public void autoSave() { final List reschedule = new ArrayList<>(); - final long currentTick = this.currentTick; + final long currentTick = RegionizedServer.getCurrentTick(); // Folia - region threading final long maxSaveTime = currentTick - Math.max(1L, this.world.paperConfig().chunks.autoSaveInterval.value()); final int maxToSave = this.world.paperConfig().chunks.maxAutoSaveChunksPerTick; - for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) { - final NewChunkHolder holder = this.autoSaveQueue.first(); + // Folia start - region threading + final HolderManagerRegionData regionData = this.getCurrentRegionData(); + for (int autoSaved = 0; autoSaved < maxToSave && !regionData.autoSaveQueue.isEmpty();) { + final NewChunkHolder holder = regionData.autoSaveQueue.first(); + // Folia end - region threading if (holder.lastAutoSave > maxSaveTime) { break; } - this.autoSaveQueue.remove(holder); + regionData.autoSaveQueue.remove(holder); // Folia - region threading holder.lastAutoSave = currentTick; if (holder.save(false) != null) { @@ -253,15 +328,38 @@ public final class ChunkHolderManager { for (final NewChunkHolder holder : reschedule) { if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { - this.autoSaveQueue.add(holder); + regionData.autoSaveQueue.add(holder); // Folia start - region threading } } } public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { - final List holders = this.getChunkHolders(); + // Folia start - region threading + this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true); + } + public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) { + final List holders = new java.util.ArrayList<>(this.chunkHolders.size() / 10); + // we could iterate through all chunk holders with thread checks, however for many regions the iteration cost alone + // will multiply. to avoid this, we can simply iterate through all owned sections + final int regionShift = this.world.moonrise$getRegionChunkShift(); + for (final LongIterator iterator = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getOwnedSectionsUnsynchronised(); iterator.hasNext();) { + final long sectionKey = iterator.nextLong(); + final int width = 1 << regionShift; + final int offsetX = CoordinateUtils.getChunkX(sectionKey) << regionShift; + final int offsetZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift; + + for (int dz = 0; dz < width; ++dz) { + for (int dx = 0; dx < width; ++dx) { + final NewChunkHolder holder = this.getChunkHolder(offsetX | dx, offsetZ | dz); + if (holder != null) { + holders.add(holder); + } + } + } + } + // Folia end - region threading - if (logProgress) { + if (first && logProgress) { // Folia - region threading LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'"); } @@ -280,6 +378,12 @@ public final class ChunkHolderManager { for (int i = 0, len = holders.size(); i < len; ++i) { final NewChunkHolder holder = holders.get(i); + // Folia start - region threading + if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) { + // skip holders that would fail the thread check + continue; + } + // Folia end - region threading try { final NewChunkHolder.SaveStat saveStat = holder.save(shutdown); if (saveStat != null) { @@ -310,7 +414,7 @@ public final class ChunkHolderManager { } } } - if (flush) { + if (last && flush) { // Folia - region threading RegionFileIOThread.flush(); try { RegionFileIOThread.flushRegionStorages(this.world); @@ -711,7 +815,13 @@ public final class ChunkHolderManager { } public void tick() { - ++this.currentTick; + // Folia start - region threading + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + throw new IllegalStateException("Not running tick() while on a region"); + } + // Folia end - region threading final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); @@ -725,7 +835,7 @@ public final class ChunkHolderManager { return removeDelay <= 0L; }; - for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) { + for (final LongIterator iterator = region.getOwnedSectionsUnsynchronised(); iterator.hasNext();) { final long sectionKey = iterator.nextLong(); if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) { @@ -1010,26 +1120,58 @@ public final class ChunkHolderManager { if (changedFullStatus.isEmpty()) { return; } - if (!TickThread.isTickThread()) { - this.taskScheduler.scheduleChunkTask(() -> { - final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; - for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { - pendingFullLoadUpdate.add(changedFullStatus.get(i)); - } - ChunkHolderManager.this.processPendingFullUpdate(); - }, PrioritisedExecutor.Priority.HIGHEST); - } else { - final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; - for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { - pendingFullLoadUpdate.add(changedFullStatus.get(i)); + // Folia start - region threading + final Long2ObjectOpenHashMap> sectionToUpdates = new Long2ObjectOpenHashMap<>(); + final List thisRegionHolders = new ArrayList<>(); + + final int regionShift = this.world.moonrise$getRegionChunkShift(); + final ThreadedRegionizer.ThreadedRegion thisRegion + = TickRegionScheduler.getCurrentRegion(); + + for (final NewChunkHolder holder : changedFullStatus) { + final int regionX = holder.chunkX >> regionShift; + final int regionZ = holder.chunkZ >> regionShift; + final long holderSectionKey = CoordinateUtils.getChunkKey(regionX, regionZ); + + // region may be null + if (thisRegion != null && this.world.regioniser.getRegionAtUnsynchronised(holder.chunkX, holder.chunkZ) == thisRegion) { + thisRegionHolders.add(holder); + } else { + sectionToUpdates.computeIfAbsent(holderSectionKey, (final long keyInMap) -> { + return new ArrayList<>(); + }).add(holder); + } + } + // Folia end - region threading + + // Folia start - region threading + if (!thisRegionHolders.isEmpty()) { + thisRegion.getData().getHolderManagerRegionData().pendingFullLoadUpdate.addAll(thisRegionHolders); + } + + if (!sectionToUpdates.isEmpty()) { + for (final Iterator>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry> entry = iterator.next(); + final long sectionKey = entry.getLongKey(); + + final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift; + final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift; + + final List regionHolders = entry.getValue(); + this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> { // Folia - region threading + ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders); + ChunkHolderManager.this.processPendingFullUpdate(); + }, PrioritisedExecutor.Priority.HIGHEST); + // Folia end - region threading } } } private void removeChunkHolder(final NewChunkHolder holder) { holder.markUnloaded(); - this.autoSaveQueue.remove(holder); + this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading ChunkSystem.onChunkHolderDelete(this.world, holder.vanillaChunkHolder); this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); @@ -1043,7 +1185,7 @@ public final class ChunkHolderManager { throw new IllegalStateException("Cannot unload chunks recursively"); } final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift - final List unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions(); + final List unloadSectionsForRegion = this.unloadQueue.retrieveForCurrentRegion(); // Folia - threaded regions int unloadCountTentative = 0; for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { final ChunkUnloadQueue.UnloadSection section @@ -1353,7 +1495,13 @@ public final class ChunkHolderManager { // only call on tick thread private boolean processPendingFullUpdate() { - final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; + // Folia start - region threading + final HolderManagerRegionData data = this.getCurrentRegionData(); + if (data == null) { + return false; + } + final ArrayDeque pendingFullLoadUpdate = data.pendingFullLoadUpdate; + // Folia end - region threading boolean ret = false; @@ -1364,9 +1512,7 @@ public final class ChunkHolderManager { ret |= holder.handleFullStatusChange(changedFullStatus); if (!changedFullStatus.isEmpty()) { - for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { - pendingFullLoadUpdate.add(changedFullStatus.get(i)); - } + this.addChangedStatuses(changedFullStatus); // Folia - region threading changedFullStatus.clear(); } } diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java index 8671a90e969d16c7a57ddc38fedb7cf01815f64c..085d035b5d127d14af6b8487bee5446b814bc590 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java @@ -157,7 +157,7 @@ public final class ChunkTaskScheduler { private final PrioritisedThreadPool.PrioritisedPoolExecutor radiusAwareGenExecutor; public final PrioritisedThreadPool.PrioritisedPoolExecutor loadExecutor; - private final PrioritisedThreadedTaskQueue mainThreadExecutor = new PrioritisedThreadedTaskQueue(); + // Folia - regionised ticking public final ChunkHolderManager chunkHolderManager; @@ -360,14 +360,13 @@ public final class ChunkTaskScheduler { }; // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions - this.scheduleChunkTask(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); + this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, PrioritisedExecutor.Priority.BLOCKING); // Folia - region threading // so, make the main thread pick it up ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException)); } public boolean executeMainThreadTask() { - TickThread.ensureTickThread("Cannot execute main thread task off-main"); - return this.mainThreadExecutor.executeTask(); + throw new UnsupportedOperationException("Use regionised ticking hooks"); // Folia - regionised ticking } public void raisePriority(final int x, final int z, final PrioritisedExecutor.Priority priority) { @@ -607,7 +606,7 @@ public final class ChunkTaskScheduler { this.chunkHolderManager.processTicketUpdates(); } - final Consumer loadCallback = (final ChunkAccess chunk) -> { + final Consumer loadCallback = onComplete == null && !addTicket ? null : (final ChunkAccess chunk) -> { try { if (onComplete != null) { onComplete.accept(chunk); @@ -644,7 +643,9 @@ public final class ChunkTaskScheduler { if (!chunkHolder.upgradeGenTarget(toStatus)) { this.schedule(chunkX, chunkZ, toStatus, chunkHolder, tasks); } - chunkHolder.addStatusConsumer(toStatus, loadCallback); + if (loadCallback != null) { + chunkHolder.addStatusConsumer(toStatus, loadCallback); + } } } } finally { @@ -658,7 +659,7 @@ public final class ChunkTaskScheduler { tasks.get(i).schedule(); } - if (!scheduled) { + if (loadCallback != null && !scheduled) { // couldn't schedule try { loadCallback.accept(chunk); @@ -830,7 +831,7 @@ public final class ChunkTaskScheduler { */ @Deprecated public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run) { - return this.scheduleChunkTask(run, PrioritisedExecutor.Priority.NORMAL); + throw new UnsupportedOperationException(); // Folia - regionised ticking } /** @@ -838,7 +839,7 @@ public final class ChunkTaskScheduler { */ @Deprecated public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final PrioritisedExecutor.Priority priority) { - return this.mainThreadExecutor.queueRunnable(run, priority); + throw new UnsupportedOperationException(); // Folia - regionised ticking } public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { @@ -847,7 +848,7 @@ public final class ChunkTaskScheduler { public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { - return this.mainThreadExecutor.createTask(run, priority); + return MinecraftServer.getServer().regionizedServer.taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking } public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { @@ -856,8 +857,22 @@ public final class ChunkTaskScheduler { public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, final PrioritisedExecutor.Priority priority) { - return this.mainThreadExecutor.queueRunnable(run, priority); + return MinecraftServer.getServer().regionizedServer.taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking + } + + // Folia start - region threading + // this function is guaranteed to never touch the ticket lock or schedule lock + // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the + // ticket lock in the schedule logic + public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run, + final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority); + this.world.taskQueueRegionData.pushGlobalChunkTask(() -> { + MinecraftServer.getServer().regionizedServer.taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority); + }); + return ret; } + // Folia end - region threading public boolean halt(final boolean sync, final long maxWaitNS) { this.radiusAwareGenExecutor.halt(); diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java index 45eda96fd8a1acb87dbb69ce5495fec7e451416f..3210d4cb97301d6fa5ffe76fd7c263ab7aa0a8be 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java @@ -1378,7 +1378,7 @@ public final class NewChunkHolder { } // must be scheduled to main, we do not trust the callback to not do anything stupid - this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading for (final Consumer consumer : consumers) { try { consumer.accept(chunk); @@ -1406,7 +1406,7 @@ public final class NewChunkHolder { } // must be scheduled to main, we do not trust the callback to not do anything stupid - this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading for (final Consumer consumer : consumers) { try { consumer.accept(chunk); diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java index 748ab4d637ce463272bae4fdbab6842a27385126..8e2a3d85b7fb78d4e7198c09f356acb51f5be1e0 100644 --- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java @@ -1628,7 +1628,7 @@ public final class CollisionUtil { for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { - final net.minecraft.world.level.chunk.ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, loadChunks); + final net.minecraft.world.level.chunk.ChunkAccess chunk = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)world, currChunkX, currChunkZ) ? null : chunkSource.getChunk(currChunkX, currChunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, loadChunks); // Folia - region threading if (chunk == null) { if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { diff --git a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java index 9f17170179cc99d84ad25a1e838aff3d8cc66f93..780c31121ea62d986fe6918c095f61797083d1da 100644 --- a/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java +++ b/src/main/java/com/destroystokyo/paper/util/RedstoneWireTurbo.java @@ -833,14 +833,14 @@ public class RedstoneWireTurbo { j = getMaxCurrentStrength(upd, j); int l = 0; - wire.shouldSignal = false; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated, // and I'm not ready to try to replicate even more functionality from // elsewhere in Minecraft into this accelerator. So sadly, we must // suffer the performance hit of this very expensive call. If there // is consistency to what this call returns, we may be able to cache it. final int k = worldIn.getBestNeighborSignal(upd.self); - wire.shouldSignal = true; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading // The variable 'k' holds the maximum redstone power value of any adjacent blocks. // If 'k' has the highest level of all neighbors, then the power level of this diff --git a/src/main/java/io/papermc/paper/SparksFly.java b/src/main/java/io/papermc/paper/SparksFly.java index 19ee43e1ca053574a0151b4c43b01972183657e6..c7c396a7a94c35d973eb0796f8aba6e669ed5b7c 100644 --- a/src/main/java/io/papermc/paper/SparksFly.java +++ b/src/main/java/io/papermc/paper/SparksFly.java @@ -46,7 +46,7 @@ public final class SparksFly { @Override public void executeSync(final Runnable runnable) { - MCUtil.ensureMain(this.catching(runnable, "synchronous")); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(this.catching(runnable, "synchronous")); // Folia - region threading } private Runnable catching(final Runnable runnable, final String type) { diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java index 14e412ebf75b0e06ab53a1c8f9dd1be6ad1e2680..3f733319482fedcf7461f4b7466e84afeae1fc2b 100644 --- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java @@ -83,7 +83,7 @@ public final class ChatProcessor { final CraftPlayer player = this.player.getBukkitEntity(); final AsyncPlayerChatEvent ae = new AsyncPlayerChatEvent(this.async, player, this.craftbukkit$originalMessage, new LazyPlayerSet(this.server)); this.post(ae); - if (listenersOnSyncEvent) { + if (false && listenersOnSyncEvent) { // Folia - region threading final PlayerChatEvent se = new PlayerChatEvent(player, ae.getMessage(), ae.getFormat(), ae.getRecipients()); se.setCancelled(ae.isCancelled()); // propagate cancelled state this.queueIfAsyncOrRunImmediately(new Waitable() { @@ -150,7 +150,7 @@ public final class ChatProcessor { ae.setCancelled(cancelled); // propagate cancelled state this.post(ae); final boolean listenersOnSyncEvent = canYouHearMe(ChatEvent.getHandlerList()); - if (listenersOnSyncEvent) { + if (false && listenersOnSyncEvent) { // Folia - region threading this.queueIfAsyncOrRunImmediately(new Waitable() { @Override protected Void evaluate() { diff --git a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java index 23432eea862c6df716d7726a32da3a0612a3fb77..f59e8bb72c5233f26a8a0d506ac64bb37fef97a5 100644 --- a/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java +++ b/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java @@ -23,35 +23,42 @@ public class ClickCallbackProviderImpl implements ClickCallback.Provider { public static final class CallbackManager { - private final Map callbacks = new HashMap<>(); - private final Queue queue = new ConcurrentLinkedQueue<>(); + private final java.util.concurrent.ConcurrentHashMap callbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading + // Folia - region threading private CallbackManager() { } public UUID addCallback(final @NotNull ClickCallback callback, final ClickCallback.@NotNull Options options) { final UUID id = UUID.randomUUID(); - this.queue.add(new StoredCallback(callback, options, id)); + final StoredCallback scb = new StoredCallback(callback, options, id); // Folia - region threading + this.callbacks.put(scb.id(), scb); // Folia - region threading return id; } public void handleQueue(final int currentTick) { // Evict expired entries if (currentTick % 100 == 0) { - this.callbacks.values().removeIf(callback -> !callback.valid()); + this.callbacks.values().removeIf(StoredCallback::expired); // Folia - region threading - don't read uses field } - // Add entries from queue - StoredCallback callback; - while ((callback = this.queue.poll()) != null) { - this.callbacks.put(callback.id(), callback); - } + // Folia - region threading } public void runCallback(final @NotNull Audience audience, final UUID id) { - final StoredCallback callback = this.callbacks.get(id); - if (callback != null && callback.valid()) { //TODO Message if expired/invalid? - callback.takeUse(); + // Folia start - region threading + final StoredCallback[] use = new StoredCallback[1]; + this.callbacks.computeIfPresent(id, (final UUID keyInMap, final StoredCallback value) -> { + if (!value.valid()) { + return null; + } + use[0] = value; + value.takeUse(); + return value.valid() ? value : null; + }); + final StoredCallback callback = use[0]; + if (callback != null) { //TODO Message if expired/invalid? + // Folia end - region threading callback.callback.accept(audience); } } diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java index 7b58b2d6297800c2dcdbf7539e5ab8e7703f39f1..a587d83b78af4efc484f939529acf70834f60d7e 100644 --- a/src/main/java/io/papermc/paper/command/PaperCommands.java +++ b/src/main/java/io/papermc/paper/command/PaperCommands.java @@ -19,6 +19,7 @@ public final class PaperCommands { COMMANDS.put("paper", new PaperCommand("paper")); COMMANDS.put("callback", new CallbackCommand("callback")); COMMANDS.put("mspt", new MSPTCommand("mspt")); + COMMANDS.put("tps", new io.papermc.paper.threadedregions.commands.CommandServerHealth()); // Folia - region threading } public static void registerCommands(final MinecraftServer server) { diff --git a/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java index 777b789fdcdf297309cfb36fc7f77e3fdb6327ca..b5ded5d0ee80e4fc374d873facba5f20e7155e77 100644 --- a/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java +++ b/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java @@ -129,7 +129,7 @@ public final class EntityCommand implements PaperSubcommand { final int z = (e.getKey().z << 4) + 8; final Component message = text(" " + e.getValue() + ": " + e.getKey().x + ", " + e.getKey().z + (chunkProviderServer.isPositionTicking(e.getKey().toLong()) ? " (Ticking)" : " (Non-Ticking)")) .hoverEvent(HoverEvent.showText(text("Click to teleport to chunk", GREEN))) - .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey() + " run tp " + x + " " + (world.getWorld().getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING) + 1) + " " + z)); + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey() + " run tp " + x + " " + (128) + " " + z)); // Folia - region threading - avoid sync load here sender.sendMessage(message); }); } else { diff --git a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java index cd2e4d792e972b8bf1e07b8961594a670ae949cf..3ab8dbf2768a4ef8fb53af6f5431f7f6afe6d168 100644 --- a/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java +++ b/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java @@ -18,7 +18,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; public final class HeapDumpCommand implements PaperSubcommand { @Override public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading this.dumpHeap(sender); + }); // Folia - region threading return true; } diff --git a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java index bd68139ae635f2ad7ec8e7a21e0056a139c4c62e..48a43341b17247355a531164019d5cc9c5555f26 100644 --- a/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java +++ b/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java @@ -16,7 +16,9 @@ import static net.kyori.adventure.text.format.NamedTextColor.RED; public final class ReloadCommand implements PaperSubcommand { @Override public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading this.doReload(sender); + }); // Folia - region threading return true; } diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java index 214ea75502d4abf9ebbc99a3811e4d2f8465227e..eae1bb367483eb6343a8644f6bac56215298bb43 100644 --- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java @@ -356,4 +356,18 @@ public class GlobalConfiguration extends ConfigurationPart { public boolean disableChorusPlantUpdates = false; public boolean disableMushroomBlockUpdates = false; } + + // Folia start - threaded regions + public ThreadedRegions threadedRegions; + public class ThreadedRegions extends ConfigurationPart { + + public int threads = -1; + public int gridExponent = 2; + + @PostProcess + public void postProcess() { + io.papermc.paper.threadedregions.TickRegions.init(this); + } + } + // Folia end - threaded regions } diff --git a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java index 4bcf27f98765abf693e535cfc1756c27a10cb316..ce07b808067627b85e11ce9836ed91c175f11d93 100644 --- a/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java +++ b/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java @@ -475,6 +475,14 @@ public class WorldConfiguration extends ConfigurationPart { public Chunks chunks; public class Chunks extends ConfigurationPart { + + // Folia start - region threading - force prevent moving into unloaded chunks + @PostProcess + public void postProcess() { + this.preventMovingIntoUnloadedChunks = true; + } + // Folia end - region threading - force prevent moving into unloaded chunks + public AutosavePeriod autoSaveInterval = AutosavePeriod.def(); public int maxAutoSaveChunksPerTick = 24; public int fixedChunkInhabitedTime = -1; diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java index 3e82ea07ca4194844c5528446e2c4a46ff4acee5..adfd4c16809f6ddd9cc73e4bd845d7aed4925068 100644 --- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java @@ -256,12 +256,7 @@ class PaperPluginInstanceManager { + pluginName + " (Is it up to date?)", ex, plugin); // Paper } - try { - this.server.getScheduler().cancelTasks(plugin); - } catch (Throwable ex) { - this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for " - + pluginName + " (Is it up to date?)", ex, plugin); // Paper - } + // Folia - region threading // Paper start - Folia schedulers try { diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java new file mode 100644 index 0000000000000000000000000000000000000000..d1cd3c1428de206d1d0b58e507d5d5a72f846acc --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionShutdownThread.java @@ -0,0 +1,174 @@ +package io.papermc.paper.threadedregions; + +import com.mojang.logging.LogUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.util.TickThread { + + private static final Logger LOGGER = LogUtils.getClassLogger(); + + ThreadedRegionizer.ThreadedRegion shuttingDown; + + public RegionShutdownThread(final String name) { + super(name); + this.setUncaughtExceptionHandler((thread, thr) -> { + LOGGER.error("Error shutting down server", thr); + }); + } + + static ThreadedRegionizer.ThreadedRegion getRegion() { + final Thread currentThread = Thread.currentThread(); + if (currentThread instanceof RegionShutdownThread shutdownThread) { + return shutdownThread.shuttingDown; + } + return null; + } + + + static RegionizedWorldData getWorldData() { + final Thread currentThread = Thread.currentThread(); + if (currentThread instanceof RegionShutdownThread shutdownThread) { + // no fast path for shutting down + if (shutdownThread.shuttingDown != null) { + return shutdownThread.shuttingDown.getData().world.worldRegionData.get(); + } + } + return null; + } + + // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves + // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because + // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not. + // Thus, the only operation that the shutdown thread will perform + + private void saveLevelData(final ServerLevel world) { + try { + world.saveLevelData(false); + } catch (final Throwable thr) { + LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr); + } + } + + private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region, + final ServerLevel world) { + try { + this.shuttingDown = region; + final List pendingTeleports = world.removeAllRegionTeleports(); + if (pendingTeleports.isEmpty()) { + return; + } + final ChunkPos center = region.getCenterChunk(); + LOGGER.info("Completing " + pendingTeleports.size() + " pending teleports in region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); + for (final ServerLevel.PendingTeleport pendingTeleport : pendingTeleports) { + LOGGER.info("Completing teleportation to target position " + pendingTeleport.to()); + + // first, add entities to entity chunk so that they will be saved + for (final Entity.EntityTreeNode node : pendingTeleport.rootVehicle().getFullTree()) { + // assume that world and position are set to destination here + node.root.setLevel(world); // in case the pending teleport is from a portal before it finds the exact destination + world.moonrise$getEntityLookup().addEntityForShutdownTeleportComplete(node.root); + } + + // then, rebuild the passenger tree so that when saving only the root vehicle will be written - and if + // there are any player passengers, that the later player saving will save the tree + pendingTeleport.rootVehicle().restore(); + + // now we are finished + LOGGER.info("Completed teleportation to target position " + pendingTeleport.to()); + } + } catch (final Throwable thr) { + LOGGER.error("Failed to complete pending teleports", thr); + } finally { + this.shuttingDown = null; + } + } + + private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region, + final boolean last) { + ChunkPos center = null; + try { + this.shuttingDown = region; + center = region.getCenterChunk(); + LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); + region.regioniser.world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(true, true, false, last, false); + } catch (final Throwable thr) { + LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); + } finally { + this.shuttingDown = null; + } + } + + private void haltChunkSystem(final ServerLevel world) { + try { + world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(false, true, true, false, false); + } catch (final Throwable thr) { + LOGGER.error("Failed to halt chunk system for world '" + world.getWorld().getName() + "'", thr); + } + } + + private void haltWorldNoRegions(final ServerLevel world) { + try { + world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(true, true, true, true, false); + } catch (final Throwable thr) { + LOGGER.error("Failed to close world '" + world.getWorld().getName() + "' with no regions", thr); + } + } + + @Override + public final void run() { + // await scheduler termination + LOGGER.info("Awaiting scheduler termination for 60s"); + if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) { + LOGGER.info("Scheduler halted"); + } else { + LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways"); + TickRegions.getScheduler().dumpAliveThreadTraces("Did not shut down in time"); + } + + MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc + // halt all chunk systems first so that any in-progress chunk generation stops + LOGGER.info("Halting chunk systems"); + for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { + try { + world.moonrise$getChunkTaskScheduler().halt(false, 0L); + } catch (final Throwable throwable) { + LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable); + } + } + for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { + this.haltChunkSystem(world); + } + LOGGER.info("Halted chunk systems"); + for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { + final List> + regions = new ArrayList<>(); + world.regioniser.computeForAllRegionsUnsynchronised(regions::add); + + for (int i = 0, len = regions.size(); i < len; ++i) { + final ThreadedRegionizer.ThreadedRegion region = regions.get(i); + this.finishTeleportations(region, world); + } + + for (int i = 0, len = regions.size(); i < len; ++i) { + final ThreadedRegionizer.ThreadedRegion region = regions.get(i); + this.saveRegionChunks(region, (i + 1) == len); + } + + this.saveLevelData(world); + } + // moved from stop part 1 + // we need this to be after saving level data, as that will complete any teleportations the player is in + LOGGER.info("Saving players"); + MinecraftServer.getServer().getPlayerList().saveAll(); + + MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc) + // done, part 2 should call exit() + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java new file mode 100644 index 0000000000000000000000000000000000000000..1f48ada99d6d24880f9bda1cd05d41a4562e42f5 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedData.java @@ -0,0 +1,235 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.util.Validate; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.server.level.ServerLevel; +import javax.annotation.Nullable; +import java.util.function.Supplier; + +/** + * Use to manage data that needs to be regionised. + *

+ * Note: that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionizedData} object is GC'd. + * The data is held in reference to the world it resides in. + *

+ *

+ * Note: Keep in mind that when regionised ticking is disabled, the entire server is considered a single region. + * That is, the data may or may not cross worlds. As such, the {@code RegionizedData} object must be instanced + * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions. + * See below for more details on instancing per world. + *

+ *

+ * Regionised data may be world-checked. That is, {@link #get()} may throw an exception if the current + * region's world does not match the {@code RegionizedData}'s world. Consider the usages of {@code RegionizedData} below + * see why the behavior may or may not be desirable: + *

+ *         {@code
+ *         public class EntityTickList {
+ *             private final List entities = new ArrayList<>();
+ *
+ *             public void addEntity(Entity e) {
+ *                 this.entities.add(e);
+ *             }
+ *
+ *             public void removeEntity(Entity e) {
+ *                 this.entities.remove(e);
+ *             }
+ *         }
+ *
+ *         public class World {
+ *
+ *             // callback is left out of this example
+ *             // note: world != null here
+ *             public final RegionizedData entityTickLists =
+ *                 new RegionizedData<>(this, () -> new EntityTickList(), ...);
+ *
+ *             public void addTickingEntity(Entity e) {
+ *                 // What we expect here is that this world is the
+ *                 // current ticking region's world.
+ *                 // If that is true, then calling this.entityTickLists.get()
+ *                 // will retrieve the current region's EntityTickList
+ *                 // for this world, which is fine since the current
+ *                 // region is contained within this world.
+ *
+ *                 // But if the current region's world is not this world,
+ *                 // and if the world check is disabled, then we will actually
+ *                 // retrieve _this_ world's EntityTickList for the region,
+ *                 // and NOT the EntityTickList for the region's world.
+ *                 // This is because the RegionizedData object is instantiated
+ *                 // per world.
+ *                 this.entityTickLists.get().addEntity(e);
+ *             }
+ *         }
+ *
+ *         public class TickTimes {
+ *
+ *             private final List tickTimesNS = new ArrayList<>();
+ *
+ *             public void completeTick(long timeNS) {
+ *                 this.tickTimesNS.add(timeNS);
+ *             }
+ *
+ *             public double getAverageTickLengthMS() {
+ *                 double sum = 0.0;
+ *                 for (long time : tickTimesNS) {
+ *                     sum += (double)time;
+ *                 }
+ *                 return (sum / this.tickTimesNS.size()) / 1.0E6; // 1ms = 1 million ns
+ *             }
+ *         }
+ *
+ *         public class Server {
+ *             public final List worlds = ...;
+ *
+ *             // callback is left out of this example
+ *             // note: world == null here, because this RegionizedData object
+ *             // is not instantiated per world, but rather globally.
+ *             public final RegionizedData tickTimes =
+ *                  new RegionizedData<>(null, () -> new TickTimes(), ...);
+ *         }
+ *         }
+ *     
+ * In general, it is advised that if a RegionizedData object is instantiated per world, that world checking + * is enabled for it by passing the world to the constructor. + *

+ */ +public final class RegionizedData { + + private final ServerLevel world; + private final Supplier initialValueSupplier; + private final RegioniserCallback callback; + + /** + * Creates a regionised data holder. The provided initial value supplier may not be null, and it must + * never produce {@code null} values. + *

+ * Note that the supplier or regioniser callback may be used while the region lock is held, so any blocking + * operations may deadlock the entire server and as such the function should be completely non-blocking + * and must complete in a timely manner. + *

+ *

+ * If the provided world is {@code null}, then the world checks are disabled. The world should only ever + * be {@code null} if the data is specifically not specific to worlds. For example, using {@code null} + * for an entity tick list is invalid since the entities are tied to a world and region, + * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to + * region only. + *

+ * @param world The world in which the region data resides. + * @param supplier Initial value supplier used to lazy initialise region data. + * @param callback Region callback to manage this regionised data. + */ + public RegionizedData(final ServerLevel world, final Supplier supplier, final RegioniserCallback callback) { + this.world = world; + this.initialValueSupplier = Validate.notNull(supplier, "Supplier may not be null."); + this.callback = Validate.notNull(callback, "Regioniser callback may not be null."); + } + + T createNewValue() { + return Validate.notNull(this.initialValueSupplier.get(), "Initial value supplier may not return null"); + } + + RegioniserCallback getCallback() { + return this.callback; + } + + /** + * Returns the current data type for the current ticking region. If there is no region, returns {@code null}. + * @return the current data type for the current ticking region. If there is no region, returns {@code null}. + * @throws IllegalStateException If the following are true: The server is in region ticking mode, + * this {@code RegionizedData}'s world is not {@code null}, + * and the current ticking region's world does not match this {@code RegionizedData}'s world. + */ + public @Nullable T get() { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + + if (region == null) { + return null; + } + + if (this.world != null && this.world != region.getData().world) { + throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); + } + + return region.getData().getOrCreateRegionizedData(this); + } + + /** + * Class responsible for handling merge / split requests from the regioniser. + *

+ * It is critical to note that each function is called while holding the region lock. + *

+ */ + public static interface RegioniserCallback { + + /** + * Completely merges the data in {@code from} to {@code into}. + *

+ * Calculating Tick Offsets: + * Sometimes data stores absolute tick deadlines, and since regions tick independently, absolute deadlines + * are not comparable across regions. Consider absolute deadlines {@code deadlineFrom, deadlineTo} in + * regions {@code from} and {@code into} respectively. We can calculate the relative deadline for the from + * region with {@code relFrom = deadlineFrom - currentTickFrom}. Then, we can use the same equation for + * computing the absolute deadline in region {@code into} that has the same relative deadline as {@code from} + * as {@code deadlineTo = relFrom + currentTickTo}. By substituting {@code relFrom} as {@code deadlineFrom - currentTickFrom}, + * we finally have that {@code deadlineTo = deadlineFrom + (currentTickTo - currentTickFrom)} and + * that we can use an offset {@code fromTickOffset = currentTickTo - currentTickFrom} to calculate + * {@code deadlineTo} as {@code deadlineTo = deadlineFrom + fromTickOffset}. + *

+ *

+ * Critical Notes: + *

  • + *
      + * This function is called while the region lock is held, so any blocking operations may + * deadlock the entire server and as such the function should be completely non-blocking and must complete + * in a timely manner. + *
    + *
      + * This function may not throw any exceptions, or the server will be left in an unrecoverable state. + *
    + *
  • + *

    + * + * @param from The data to merge from. + * @param into The data to merge into. + * @param fromTickOffset The addend to absolute tick deadlines stored in the {@code from} region to adjust to the into region. + */ + public void merge(final T from, final T into, final long fromTickOffset); + + /** + * Splits the data in {@code from} into {@code dataSet}. + *

    + * The chunk coordinate to region section coordinate bit shift amount is provided in {@code chunkToRegionShift}. + * To convert from chunk coordinates to region coordinates and keys, see the code below: + *

    +         *         {@code
    +         *         int chunkX = ...;
    +         *         int chunkZ = ...;
    +         *
    +         *         int regionSectionX = chunkX >> chunkToRegionShift;
    +         *         int regionSectionZ = chunkZ >> chunkToRegionShift;
    +         *         long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
    +         *         }
    +         *     
    + *

    + *

    + * The {@code regionToData} hashtable provides a lookup from {@code regionSectionKey} (see above) to the + * data that is owned by the region which occupies the region section. + *

    + *

    + * Unlike {@link #merge(Object, Object, long)}, there is no absolute tick offset provided. This is because + * the new regions formed from the split will start at the same tick number, and so no adjustment is required. + *

    + * + * @param from The data to split from. + * @param chunkToRegionShift The signed right-shift value used to convert chunk coordinates into region section coordinates. + * @param regionToData Lookup hash table from region section key to . + * @param dataSet The data set to split into. + */ + public void split( + final T from, final int chunkToRegionShift, + final Long2ReferenceOpenHashMap regionToData, final ReferenceOpenHashSet dataSet + ); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java new file mode 100644 index 0000000000000000000000000000000000000000..1d41a6a204068122daa1b5b2cd7541bfeae52f4c --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedServer.java @@ -0,0 +1,455 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; +import ca.spottedleaf.moonrise.common.util.TickThread; +import com.mojang.logging.LogUtils; +import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketListener; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.level.GameRules; +import org.bukkit.Bukkit; +import org.slf4j.Logger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +public final class RegionizedServer { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final RegionizedServer INSTANCE = new RegionizedServer(); + + public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue(); + + private final CopyOnWriteArrayList worlds = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>(); + + private final MultiThreadedQueue globalTickQueue = new MultiThreadedQueue<>(); + + private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this); + + public static RegionizedServer getInstance() { + return INSTANCE; + } + + public void addConnection(final Connection conn) { + this.connections.add(conn); + } + + public boolean removeConnection(final Connection conn) { + return this.connections.remove(conn); + } + + public void addWorld(final ServerLevel world) { + this.worlds.add(world); + } + + public void init() { + // call init event _before_ scheduling anything + new RegionizedServerInitEvent().callEvent(); + + // now we can schedule + this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); + TickRegions.getScheduler().scheduleRegion(this.tickHandle); + TickRegions.getScheduler().init(); + } + + public void invalidateStatus() { + this.lastServerStatus = 0L; + } + + public void addTaskWithoutNotify(final Runnable run) { + this.globalTickQueue.add(run); + } + + public void addTask(final Runnable run) { + this.addTaskWithoutNotify(run); + TickRegions.getScheduler().setHasTasks(this.tickHandle); + } + + /** + * Returns the current tick of the region ticking. + * @throws IllegalStateException If there is no current region. + */ + public static long getCurrentTick() throws IllegalStateException { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + if (TickThread.isShutdownThread()) { + return 0L; + } + throw new IllegalStateException("No currently ticking region"); + } + return region.getData().getCurrentTick(); + } + + public static boolean isGlobalTickThread() { + return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask(); + } + + public static void ensureGlobalTickThread(final String reason) { + if (!isGlobalTickThread()) { + throw new IllegalStateException(reason); + } + } + + public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() { + return INSTANCE.tickHandle; + } + + private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle { + + private final RegionizedServer server; + + private final AtomicBoolean scheduled = new AtomicBoolean(); + private final AtomicBoolean ticking = new AtomicBoolean(); + + public GlobalTickTickHandle(final RegionizedServer server) { + super(null, SchedulerThreadPool.DEADLINE_NOT_SET); + this.server = server; + } + + /** + * Only valid to call BEFORE scheduled!!!! + */ + final void setInitialStart(final long start) { + if (this.scheduled.getAndSet(true)) { + throw new IllegalStateException("Double scheduling global tick"); + } + this.updateScheduledStart(start); + } + + @Override + protected boolean tryMarkTicking() { + return !this.ticking.getAndSet(true); + } + + @Override + protected boolean markNotTicking() { + return this.ticking.getAndSet(false); + } + + @Override + protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { + this.drainTasks(); + this.server.globalTick(tickCount); + } + + private void drainTasks() { + while (this.runOneTask()); + } + + private boolean runOneTask() { + final Runnable run = this.server.globalTickQueue.poll(); + if (run == null) { + return false; + } + + // TODO try catch? + run.run(); + + return true; + } + + @Override + protected boolean runRegionTasks(final BooleanSupplier canContinue) { + do { + if (!this.runOneTask()) { + return false; + } + } while (canContinue.getAsBoolean()); + + return true; + } + + @Override + protected boolean hasIntermediateTasks() { + return !this.server.globalTickQueue.isEmpty(); + } + } + + private long lastServerStatus; + private long tickCount; + + /* + private final java.util.Random random = new java.util.Random(4L); + private final List> walkers = + new java.util.ArrayList<>(); + static final int PLAYERS = 500; + static final int RAD_BLOCKS = 1000; + static final int RAD = RAD_BLOCKS >> 4; + static final int RAD_BIG_BLOCKS = 100_000; + static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; + static final int VD = 4 + 12; + static final int BIG_PLAYERS = 250; + static final double WALK_CHANCE = 0.3; + static final double TP_CHANCE = 0.2; + static final double TASK_CHANCE = 0.2; + + private ServerLevel getWorld() { + return this.worlds.get(0); + } + + private void init2() { + for (int i = 0; i < PLAYERS; ++i) { + int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; + int posX = this.random.nextInt(-rad, rad + 1); + int posZ = this.random.nextInt(-rad, rad + 1); + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { + @Override + protected void addCallback(Void parameter, int chunkX, int chunkZ) { + ServerLevel world = RegionizedServer.this.getWorld(); + if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) { + RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> { + RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {}); + }); + } + world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( + net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ) + ); + } + + @Override + protected void removeCallback(Void parameter, int chunkX, int chunkZ) { + ServerLevel world = RegionizedServer.this.getWorld(); + if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) { + RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> { + RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {}); + }); + } + world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( + net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ) + ); + } + }; + + map.add(posX, posZ, VD); + + walkers.add(map); + } + } + + private void randomWalk() { + if (this.walkers.isEmpty()) { + this.init2(); + return; + } + + for (int i = 0; i < PLAYERS; ++i) { + if (this.random.nextDouble() > WALK_CHANCE) { + continue; + } + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = this.walkers.get(i); + + int updateX = this.random.nextInt(-1, 2); + int updateZ = this.random.nextInt(-1, 2); + + map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); + } + + for (int i = 0; i < PLAYERS; ++i) { + if (random.nextDouble() >= TP_CHANCE) { + continue; + } + + int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; + int posX = random.nextInt(-rad, rad + 1); + int posZ = random.nextInt(-rad, rad + 1); + + io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); + + map.update(posX, posZ, VD); + } + } + */ + + private void globalTick(final int tickCount) { + /* + if (false) { + io.papermc.paper.threadedregions.ThreadedTicketLevelPropagator.main(null); + } + this.randomWalk(); + */ + ++this.tickCount; + // expire invalid click command callbacks + io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount); + + // scheduler + ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick(); + + // commands + ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs(); + + // needs + // player ping sample + // world global tick + // connection tick + + // tick player ping sample + this.tickPlayerSample(); + + // tick worlds + for (final ServerLevel world : this.worlds) { + this.globalTick(world, tickCount); + } + + // tick connections + this.tickConnections(); + + // player list + MinecraftServer.getServer().getPlayerList().tick(); + } + + private void tickPlayerSample() { + final MinecraftServer mcServer = MinecraftServer.getServer(); + + final long currtime = System.nanoTime(); + + // player ping sample + // copied from MinecraftServer#tickServer + // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing + // an uncomplete status + if (currtime - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) { + this.lastServerStatus = currtime; + mcServer.rebuildServerStatus(); + } + } + + public static boolean isNotOwnedByGlobalRegion(final Connection conn) { + final PacketListener packetListener = conn.getPacketListener(); + + if (packetListener instanceof ServerGamePacketListenerImpl gamePacketListener) { + return !gamePacketListener.waitingForSwitchToConfig; + } + + if (conn.getPacketListener() instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { + return configurationPacketListener.switchToMain; + } + + return false; + } + + private void tickConnections() { + final List connections = new ArrayList<>(this.connections); + Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging + for (final Connection conn : connections) { + if (!conn.becomeActive()) { + continue; + } + + if (isNotOwnedByGlobalRegion(conn)) { + // we actually require that the owning regions remove the connection for us, as it is possible + // that ownership is transferred back to us + continue; + } + + if (!conn.isConnected()) { + this.removeConnection(conn); + conn.handleDisconnection(); + continue; + } + + try { + conn.tick(); + } catch (final Exception exception) { + if (conn.isMemoryConnection()) { + throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); + } + + LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception); + MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); + + conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { + conn.disconnect(ichatmutablecomponent); + })); + conn.setReadOnly(); + continue; + } + } + } + + // A global tick only updates things like weather / worldborder, basically anything in the world that is + // NOT tied to a specific region, but rather shared amongst all of them. + private void globalTick(final ServerLevel world, final int tickCount) { + // needs + // worldborder tick + // advancing the weather cycle + // sleep status thing + // updating sky brightness + // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking + + // Typically, we expect there to be a running region to drain a world's global chunk tasks. However, + // this may not be the case - and thus, only the global tick thread can do anything. + world.taskQueueRegionData.drainGlobalChunkTasks(); + + // worldborder tick + this.tickWorldBorder(world); + + // weather cycle + this.advanceWeatherCycle(world); + + // sleep status + this.checkNightSkip(world); + + // update raids + this.updateRaids(world); + + // sky brightness + this.updateSkyBrightness(world); + + // time ticking (TODO API synchronisation?) + this.tickTime(world, tickCount); + + world.updateTickData(); + + world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates + } + + private void updateRaids(final ServerLevel world) { + world.getRaids().globalTick(); + } + + private void checkNightSkip(final ServerLevel world) { + world.tickSleep(); + } + + private void advanceWeatherCycle(final ServerLevel world) { + world.advanceWeatherCycle(); + } + + private void updateSkyBrightness(final ServerLevel world) { + world.updateSkyBrightness(); + } + + private void tickWorldBorder(final ServerLevel world) { + world.getWorldBorder().tick(); + } + + private void tickTime(final ServerLevel world, final int tickCount) { + if (world.tickTime) { + if (world.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { + world.setDayTime(world.levelData.getDayTime() + (long)tickCount); + } + world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount); + } + } + + public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) { + + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..a1e1782d87403ca8934d37361be7ba66ddba133f --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedTaskQueue.java @@ -0,0 +1,763 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.Unit; +import java.lang.invoke.VarHandle; +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicLong; + +public final class RegionizedTaskQueue { + + private static final TicketType TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0); + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run) { + return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final PrioritisedExecutor.Priority priority) { + return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority); + } + + public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run) { + return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final PrioritisedExecutor.Priority priority) { + return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority); + } + + public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run) { + return this.queueChunkTask(world, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority); + ret.queue(); + return ret; + } + + public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run) { + return this.queueTickTaskQueue(world, chunkX, chunkZ, run, PrioritisedExecutor.Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final PrioritisedExecutor.Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority); + ret.queue(); + return ret; + } + + public static final class WorldRegionTaskData { + private final ServerLevel world; + private final MultiThreadedQueue globalChunkTask = new MultiThreadedQueue<>(); + private final ConcurrentLong2ReferenceChainedHashTable referenceCounters = new ConcurrentLong2ReferenceChainedHashTable<>(); + + public WorldRegionTaskData(final ServerLevel world) { + this.world = world; + } + + private boolean executeGlobalChunkTask() { + final Runnable run = this.globalChunkTask.poll(); + if (run != null) { + run.run(); + return true; + } + return false; + } + + public void drainGlobalChunkTasks() { + while (this.executeGlobalChunkTask()); + } + + public void pushGlobalChunkTask(final Runnable run) { + this.globalChunkTask.add(run); + } + + private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) { + final ThreadedRegionizer regioniser = this.world.regioniser; + final ThreadedRegionizer.ThreadedRegion region + = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); + if (region == null) { + return null; + } + final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData(); + return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue); + } + + private void removeTicket(final long coord) { + this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( + TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE + ); + } + + private void addTicket(final long coord) { + this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( + TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE + ); + } + + private void processTicketUpdates(final long coord) { + this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(CoordinateUtils.getChunkX(coord), CoordinateUtils.getChunkZ(coord)); + } + + private void decrementReference(final AtomicLong reference, final long coord) { + final long val = reference.decrementAndGet(); + if (val == 0L) { + final int chunkX = CoordinateUtils.getChunkX(coord); + final int chunkZ = CoordinateUtils.getChunkZ(coord); + final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ); + try { + if (this.referenceCounters.remove(coord, reference) == reference) { + WorldRegionTaskData.this.removeTicket(coord); + } // else: race condition, something replaced our reference - not our issue anymore + } finally { + this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.ticketLockArea.unlock(ticketLock); + } + } else if (val < 0L) { + throw new IllegalStateException("Reference count < 0: " + val); + } + } + + private AtomicLong incrementReference(final long coord) { + final AtomicLong ret = this.referenceCounters.get(coord); + if (ret != null) { + // try to fast acquire counter + int failures = 0; + for (long curr = ret.get();;) { + if (curr == 0L) { + // failed to fast acquire as reference expired + break; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = ret.compareAndExchange(curr, curr + 1L))) { + return ret; + } + + ++failures; + } + } + + // slow acquire + final int chunkX = CoordinateUtils.getChunkX(coord); + final int chunkZ = CoordinateUtils.getChunkZ(coord); + final ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock.Node ticketLock = this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.ticketLockArea.lock(chunkX, chunkZ); + final AtomicLong ret2; + final boolean processTicketUpdates; + try { + final AtomicLong replace = new AtomicLong(1L); + final AtomicLong valueInMap = this.referenceCounters.putIfAbsent(coord, replace); + if (valueInMap == null) { + // replaced, we should usually be here + this.addTicket(coord); + ret2 = replace; + processTicketUpdates = true; + } else { + processTicketUpdates = false; + int failures = 0; + for (long curr = valueInMap.get();;) { + if (curr == 0L) { + // don't need to add ticket here, since ticket is only removed during the lock + // we just need to replace the value in the map so that the thread removing fails and doesn't + // remove the ticket (see decrementReference) + this.referenceCounters.put(coord, replace); + ret2 = replace; + break; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = valueInMap.compareAndExchange(curr, curr + 1L))) { + // acquired + ret2 = valueInMap; + break; + } + + ++failures; + } + } + } finally { + this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.ticketLockArea.unlock(ticketLock); + } + + if (processTicketUpdates) { + this.processTicketUpdates(coord); + } + + return ret2; + } + } + + public static final class RegionTaskQueueData { + private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue(); + private final PrioritisedQueue chunkQueue = new PrioritisedQueue(); + private final WorldRegionTaskData worldRegionTaskData; + + public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) { + this.worldRegionTaskData = worldRegionTaskData; + } + + void mergeInto(final RegionTaskQueueData into) { + this.tickTaskQueue.mergeInto(into.tickTaskQueue); + this.chunkQueue.mergeInto(into.chunkQueue); + } + + public boolean executeTickTask() { + return this.tickTaskQueue.executeTask(); + } + + public boolean executeChunkTask() { + return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask(); + } + + void split(final ThreadedRegionizer regioniser, + final Long2ReferenceOpenHashMap> into) { + this.tickTaskQueue.split( + false, regioniser, into + ); + this.chunkQueue.split( + true, regioniser, into + ); + } + + public void drainTasks() { + final PrioritisedQueue tickTaskQueue = this.tickTaskQueue; + final PrioritisedQueue chunkTaskQueue = this.chunkQueue; + + int allowedTickTasks = tickTaskQueue.getScheduledTasks(); + int allowedChunkTasks = chunkTaskQueue.getScheduledTasks(); + + boolean executeTickTasks = allowedTickTasks > 0; + boolean executeChunkTasks = allowedChunkTasks > 0; + boolean executeGlobalTasks = true; + + do { + executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask(); + executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask(); + executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask(); + } while (executeTickTasks | executeChunkTasks | executeGlobalTasks); + + if (allowedChunkTasks > 0) { + // if we executed chunk tasks, we should try to process ticket updates for full status changes + this.worldRegionTaskData.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); + } + } + + public boolean hasTasks() { + return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty(); + } + } + + static final class PrioritisedQueue { + private final ArrayDeque[] queues = new ArrayDeque[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { + for (int i = 0; i < PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { + this.queues[i] = new ArrayDeque<>(); + } + } + private boolean isDestroyed; + + public int getScheduledTasks() { + synchronized (this) { + int ret = 0; + + for (final ArrayDeque queue : this.queues) { + ret += queue.size(); + } + + return ret; + } + } + + public boolean isEmpty() { + final ArrayDeque[] queues = this.queues; + final int max = PrioritisedExecutor.Priority.IDLE.priority; + synchronized (this) { + for (int i = 0; i <= max; ++i) { + if (!queues[i].isEmpty()) { + return false; + } + } + return true; + } + } + + public void mergeInto(final PrioritisedQueue target) { + synchronized (this) { + this.isDestroyed = true; + mergeInto(target, this.queues); + } + } + + private static void mergeInto(final PrioritisedQueue target, final ArrayDeque[] thisQueues) { + synchronized (target) { + final ArrayDeque[] otherQueues = target.queues; + for (int i = 0; i < thisQueues.length; ++i) { + final ArrayDeque fromQ = thisQueues[i]; + final ArrayDeque intoQ = otherQueues[i]; + + // it is possible for another thread to queue tasks into the target queue before we do + // since only the ticking region can poll, we don't have to worry about it when they are being queued - + // but when we are merging, we need to ensure order is maintained (notwithstanding priority changes) + // we can ensure order is maintained by adding all of the tasks from the fromQ into the intoQ at the + // front of the queue, but we need to use descending iterator to ensure we do not reverse + // the order of elements from fromQ + for (final Iterator iterator = fromQ.descendingIterator(); iterator.hasNext();) { + intoQ.addFirst(iterator.next()); + } + } + } + } + + // into is a map of section coordinate to region + public void split(final boolean isChunkData, + final ThreadedRegionizer regioniser, + final Long2ReferenceOpenHashMap> into) { + final Reference2ReferenceOpenHashMap, ArrayDeque[]> + split = new Reference2ReferenceOpenHashMap<>(); + final int shift = regioniser.sectionChunkShift; + synchronized (this) { + this.isDestroyed = true; + // like mergeTarget, we need to be careful about insertion order so we can maintain order when splitting + + // first, build the targets + final ArrayDeque[] thisQueues = this.queues; + for (int i = 0; i < thisQueues.length; ++i) { + final ArrayDeque fromQ = thisQueues[i]; + + for (final ChunkBasedPriorityTask task : fromQ) { + final int sectionX = task.chunkX >> shift; + final int sectionZ = task.chunkZ >> shift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + final ThreadedRegionizer.ThreadedRegion + region = into.get(sectionKey); + if (region == null) { + throw new IllegalStateException(); + } + + split.computeIfAbsent(region, (keyInMap) -> { + final ArrayDeque[] ret = new ArrayDeque[PrioritisedExecutor.Priority.TOTAL_SCHEDULABLE_PRIORITIES]; + + for (int k = 0; k < ret.length; ++k) { + ret[k] = new ArrayDeque<>(); + } + + return ret; + })[i].add(task); + } + } + + // merge the targets into their queues + for (final Iterator, ArrayDeque[]>> + iterator = split.reference2ReferenceEntrySet().fastIterator(); + iterator.hasNext();) { + final Reference2ReferenceMap.Entry, ArrayDeque[]> + entry = iterator.next(); + final RegionTaskQueueData taskQueueData = entry.getKey().getData().getTaskQueueData(); + mergeInto(isChunkData ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue, entry.getValue()); + } + } + } + + /** + * returns null if the task cannot be scheduled, returns false if this task queue is dead, and returns true + * if the task was added + */ + private Boolean tryPush(final ChunkBasedPriorityTask task) { + final ArrayDeque[] queues = this.queues; + synchronized (this) { + final PrioritisedExecutor.Priority priority = task.getPriority(); + if (priority == PrioritisedExecutor.Priority.COMPLETING) { + return null; + } + if (this.isDestroyed) { + return Boolean.FALSE; + } + queues[priority.priority].addLast(task); + return Boolean.TRUE; + } + } + + private boolean executeTask() { + final ArrayDeque[] queues = this.queues; + final int max = PrioritisedExecutor.Priority.IDLE.priority; + ChunkBasedPriorityTask task = null; + AtomicLong referenceCounter = null; + synchronized (this) { + if (this.isDestroyed) { + throw new IllegalStateException("Attempting to poll from dead queue"); + } + + search_loop: + for (int i = 0; i <= max; ++i) { + final ArrayDeque queue = queues[i]; + while ((task = queue.pollFirst()) != null) { + if ((referenceCounter = task.trySetCompleting(i)) != null) { + break search_loop; + } + } + } + } + + if (task == null) { + return false; + } + + try { + task.executeInternal(); + } finally { + task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord); + } + + return true; + } + + private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask { + + private static final AtomicLong REFERENCE_COUNTER_NOT_SET = new AtomicLong(-1L); + + private final WorldRegionTaskData world; + private final int chunkX; + private final int chunkZ; + private final long sectionLowerLeftCoord; // chunk coordinate + private final boolean isChunkTask; + + private volatile AtomicLong referenceCounter; + private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", AtomicLong.class); + private Runnable run; + private volatile PrioritisedExecutor.Priority priority; + private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", PrioritisedExecutor.Priority.class); + + ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask, + final Runnable run, final PrioritisedExecutor.Priority priority) { + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.isChunkTask = isChunkTask; + this.run = run; + this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET); + this.setPriorityPlain(priority); + + final int regionShift = world.world.regioniser.sectionChunkShift; + final int regionMask = (1 << regionShift) - 1; + + this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask); + } + + private PrioritisedExecutor.Priority getPriorityVolatile() { + return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.getVolatile(this); + } + + private void setPriorityPlain(final PrioritisedExecutor.Priority priority) { + PRIORITY_HANDLE.set(this, priority); + } + + private void setPriorityVolatile(final PrioritisedExecutor.Priority priority) { + PRIORITY_HANDLE.setVolatile(this, priority); + } + + private PrioritisedExecutor.Priority compareAndExchangePriority(final PrioritisedExecutor.Priority expect, final PrioritisedExecutor.Priority update) { + return (PrioritisedExecutor.Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update); + } + + private void setReferenceCounterPlain(final AtomicLong value) { + REFERENCE_COUNTER_HANDLE.set(this, value); + } + + private AtomicLong getReferenceCounterVolatile() { + return (AtomicLong)REFERENCE_COUNTER_HANDLE.get(this); + } + + private AtomicLong compareAndExchangeReferenceCounter(final AtomicLong expect, final AtomicLong update) { + return (AtomicLong)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update); + } + + private void executeInternal() { + try { + this.run.run(); + } finally { + this.run = null; + } + } + + private void cancelInternal() { + this.run = null; + } + + private boolean tryComplete(final boolean cancel) { + int failures = 0; + for (AtomicLong curr = this.getReferenceCounterVolatile();;) { + if (curr == null) { + return false; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { + ++failures; + continue; + } + + // we have the reference count, we win no matter what. + this.setPriorityVolatile(PrioritisedExecutor.Priority.COMPLETING); + + try { + if (cancel) { + this.cancelInternal(); + } else { + this.executeInternal(); + } + } finally { + if (curr != REFERENCE_COUNTER_NOT_SET) { + this.world.decrementReference(curr, this.sectionLowerLeftCoord); + } + } + + return true; + } + } + + @Override + public boolean queue() { + if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) { + return false; + } + + final AtomicLong referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord); + if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) { + // we don't expect race conditions here, so it is OK if we have to needlessly reference count + this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord); + return false; + } + + boolean synchronise = false; + for (;;) { + // we need to synchronise for repeated operations so that we guarantee that we do not retrieve + // the same queue again, as the region lock will be given to us only when the merge/split operation + // is done + final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); + + if (queue == null) { + if (!synchronise) { + // may be incorrectly null when unsynchronised + synchronise = true; + continue; + } + // may have been cancelled before we got to the queue + if (this.getReferenceCounterVolatile() != null) { + throw new IllegalStateException("Expected null ref count when queue does not exist"); + } + // the task never could be polled from the queue, so we return false + // don't decrement reference count, as we were certainly cancelled by another thread, which + // will decrement the reference count + return false; + } + + synchronise = true; + + final Boolean res = queue.tryPush(this); + if (res == null) { + // we were cancelled + // don't decrement reference count, as we were certainly cancelled by another thread, which + // will decrement the reference count + return false; + } + + if (!res.booleanValue()) { + // failed, try again + continue; + } + + // successfully queued + return true; + } + } + + private AtomicLong trySetCompleting(final int minPriority) { + // first, try to set priority to EXECUTING + for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { + if (curr.isLowerPriority(minPriority)) { + return null; + } + + if (curr == (curr = this.compareAndExchangePriority(curr, PrioritisedExecutor.Priority.COMPLETING))) { + break; + } // else: continue + } + + for (AtomicLong curr = this.getReferenceCounterVolatile();;) { + if (curr == null) { + // something acquired before us + return null; + } + + if (curr == REFERENCE_COUNTER_NOT_SET) { + throw new IllegalStateException(); + } + + if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { + continue; + } + return curr; + } + } + + private void updatePriorityInQueue() { + boolean synchronise = false; + for (;;) { + final AtomicLong referenceCount = this.getReferenceCounterVolatile(); + if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) { + // cancelled or not queued + return; + } + + if (this.getPriorityVolatile() == PrioritisedExecutor.Priority.COMPLETING) { + // cancelled + return; + } + + // we need to synchronise for repeated operations so that we guarantee that we do not retrieve + // the same queue again, as the region lock will be given to us only when the merge/split operation + // is done + final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); + + if (queue == null) { + if (!synchronise) { + // may be incorrectly null when unsynchronised + continue; + } + // must have been removed + return; + } + + synchronise = true; + + final Boolean res = queue.tryPush(this); + if (res == null) { + // we were cancelled + return; + } + + if (!res.booleanValue()) { + // failed, try again + continue; + } + + // successfully queued + return; + } + } + + @Override + public PrioritisedExecutor.Priority getPriority() { + return this.getPriorityVolatile(); + } + + @Override + public boolean lowerPriority(final PrioritisedExecutor.Priority priority) { + int failures = 0; + for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr.isLowerOrEqualPriority(priority)) { + return false; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { + this.updatePriorityInQueue(); + return true; + } + ++failures; + } + } + + @Override + public boolean setPriority(final PrioritisedExecutor.Priority priority) { + int failures = 0; + for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr == priority) { + return false; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { + this.updatePriorityInQueue(); + return true; + } + ++failures; + } + } + + @Override + public boolean raisePriority(final PrioritisedExecutor.Priority priority) { + int failures = 0; + for (PrioritisedExecutor.Priority curr = this.getPriorityVolatile();;) { + if (curr == PrioritisedExecutor.Priority.COMPLETING) { + return false; + } + + if (curr.isHigherOrEqualPriority(priority)) { + return false; + } + + for (int i = 0; i < failures; ++i) { + ConcurrentUtil.backoff(); + } + + if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { + this.updatePriorityInQueue(); + return true; + } + ++failures; + } + } + + @Override + public boolean execute() { + return this.tryComplete(false); + } + + @Override + public boolean cancel() { + return this.tryComplete(true); + } + } + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java new file mode 100644 index 0000000000000000000000000000000000000000..5dc4c8a5b896f519be5414d4a53a71144c225324 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/RegionizedWorldData.java @@ -0,0 +1,772 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet; +import ca.spottedleaf.moonrise.common.list.ReferenceList; +import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.common.util.TickThread; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; +import net.minecraft.core.BlockPos; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.util.VisibleForDebug; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.village.VillageSiege; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.BlockEventData; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.NaturalSpawner; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.RedStoneWireBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.pathfinder.PathTypeCache; +import net.minecraft.world.level.redstone.CollectingNeighborUpdater; +import net.minecraft.world.level.redstone.NeighborUpdater; +import net.minecraft.world.ticks.LevelTicks; +import org.bukkit.craftbukkit.block.CraftBlockState; +import org.slf4j.Logger; +import javax.annotation.Nullable; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public final class RegionizedWorldData { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0]; + + public static final RegionizedData.RegioniserCallback REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() { + @Override + public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) { + // connections + for (final Connection conn : from.connections) { + into.connections.add(conn); + } + // time + final long fromRedstoneTimeOffset = into.redstoneTime - from.redstoneTime; + // entities + for (final ServerPlayer player : from.localPlayers) { + into.localPlayers.add(player); + into.nearbyPlayers.addPlayer(player); + } + for (final Entity entity : from.allEntities) { + into.allEntities.add(entity); + entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + } + for (final Entity entity : from.loadedEntities) { + into.loadedEntities.add(entity); + } + for (final Entity entity : from.toProcessTrackingUnloading) { + into.toProcessTrackingUnloading.add(entity); + } + for (final Iterator iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) { + into.entityTickList.add(iterator.next()); + } + for (final Iterator iterator = from.navigatingMobs.unsafeIterator(); iterator.hasNext();) { + into.navigatingMobs.add(iterator.next()); + } + for (final Iterator iterator = from.trackerEntities.iterator(); iterator.hasNext();) { + into.trackerEntities.add(iterator.next()); + } + for (final Iterator iterator = from.trackerUnloadedEntities.iterator(); iterator.hasNext();) { + into.trackerUnloadedEntities.add(iterator.next()); + } + // block ticking + into.blockEvents.addAll(from.blockEvents); + // ticklists use game time + from.blockLevelTicks.merge(into.blockLevelTicks, fromRedstoneTimeOffset); + from.fluidLevelTicks.merge(into.fluidLevelTicks, fromRedstoneTimeOffset); + + // tile entity ticking + for (final TickingBlockEntity tileEntityWrapped : from.pendingBlockEntityTickers) { + into.pendingBlockEntityTickers.add(tileEntityWrapped); + final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); + if (tileEntity != null) { + tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + } + } + for (final TickingBlockEntity tileEntityWrapped : from.blockEntityTickers) { + into.blockEntityTickers.add(tileEntityWrapped); + final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); + if (tileEntity != null) { + tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + } + } + + // ticking chunks + for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) { + into.entityTickingChunks.add(iterator.next()); + } + for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) { + into.tickingChunks.add(iterator.next()); + } + for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) { + into.chunks.add(iterator.next()); + } + // redstone torches + if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { + if (into.redstoneUpdateInfos == null) { + into.redstoneUpdateInfos = new ArrayDeque<>(); + } + for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { + info.offsetTime(fromRedstoneTimeOffset); + into.redstoneUpdateInfos.add(info); + } + } + // mob spawning + into.catSpawnerNextTick = Math.max(from.catSpawnerNextTick, into.catSpawnerNextTick); + into.patrolSpawnerNextTick = Math.max(from.patrolSpawnerNextTick, into.patrolSpawnerNextTick); + into.phantomSpawnerNextTick = Math.max(from.phantomSpawnerNextTick, into.phantomSpawnerNextTick); + if (from.wanderingTraderTickDelay != Integer.MIN_VALUE && into.wanderingTraderTickDelay != Integer.MIN_VALUE) { + into.wanderingTraderTickDelay = Math.max(from.wanderingTraderTickDelay, into.wanderingTraderTickDelay); + into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay); + into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance); + } + } + + @Override + public void split(final RegionizedWorldData from, final int chunkToRegionShift, + final Long2ReferenceOpenHashMap regionToData, + final ReferenceOpenHashSet dataSet) { + // connections + for (final Connection conn : from.connections) { + final ServerPlayer player = conn.getPlayer(); + final ChunkPos pos = player.chunkPosition(); + // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means + // the chunk holder must _exist_, and so the region section exists. + regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) + .connections.add(conn); + } + // entities + for (final ServerPlayer player : from.localPlayers) { + final ChunkPos pos = player.chunkPosition(); + // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means + // the chunk holder must _exist_, and so the region section exists. + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); + into.localPlayers.add(player); + into.nearbyPlayers.addPlayer(player); + } + for (final Entity entity : from.allEntities) { + final ChunkPos pos = entity.chunkPosition(); + // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means + // the chunk holder must _exist_, and so the region section exists. + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); + into.allEntities.add(entity); + // Note: entityTickList is a subset of allEntities + if (from.entityTickList.contains(entity)) { + into.entityTickList.add(entity); + } + // Note: loadedEntities is a subset of allEntities + if (from.loadedEntities.contains(entity)) { + into.loadedEntities.add(entity); + } + // Note: toProcessTrackingUnloading is not a subset of allEntities, but for the cases where it is not + // do not matter as the tracker removal is handled + if (from.toProcessTrackingUnloading.contains(entity)) { + into.toProcessTrackingUnloading.add(entity); + } + // Note: navigatingMobs is a subset of allEntities + if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) { + into.navigatingMobs.add(mob); + } + if (from.trackerEntities.contains(entity)) { + into.trackerEntities.add(entity); + } + if (from.trackerUnloadedEntities.contains(entity)) { + into.trackerUnloadedEntities.add(entity); + } + } + // block ticking + for (final BlockEventData blockEventData : from.blockEvents) { + final BlockPos pos = blockEventData.pos(); + final int chunkX = pos.getX() >> 4; + final int chunkZ = pos.getZ() >> 4; + + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); + // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events + // is just some list. So if it unloads, I guess it's just lost. + if (into != null) { + into.blockEvents.add(blockEventData); + } + } + + final Long2ReferenceOpenHashMap> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); + final Long2ReferenceOpenHashMap> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); + + for (final Iterator> iterator = regionToData.long2ReferenceEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ReferenceMap.Entry entry = iterator.next(); + final long key = entry.getLongKey(); + final RegionizedWorldData worldData = entry.getValue(); + + levelTicksBlockRegionData.put(key, worldData.blockLevelTicks); + levelTicksFluidRegionData.put(key, worldData.fluidLevelTicks); + } + + from.blockLevelTicks.split(chunkToRegionShift, levelTicksBlockRegionData); + from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData); + + // tile entity ticking + for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) { + final BlockPos pos = tileEntity.getPos(); + final int chunkX = pos.getX() >> 4; + final int chunkZ = pos.getZ() >> 4; + + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); + if (into != null) { + into.pendingBlockEntityTickers.add(tileEntity); + } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets + // marked as removed. So if there is no section, it's probably removed! + } + for (final TickingBlockEntity tileEntity : from.blockEntityTickers) { + final BlockPos pos = tileEntity.getPos(); + final int chunkX = pos.getX() >> 4; + final int chunkZ = pos.getZ() >> 4; + + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); + if (into != null) { + into.blockEntityTickers.add(tileEntity); + } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets + // marked as removed. So if there is no section, it's probably removed! + } + // time + for (final RegionizedWorldData regionizedWorldData : dataSet) { + regionizedWorldData.redstoneTime = from.redstoneTime; + } + // ticking chunks + for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) { + final ServerChunkCache.ChunkAndHolder holder = iterator.next(); + final ChunkPos pos = holder.chunk().getPos(); + + // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded + regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) + .entityTickingChunks.add(holder); + } + for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) { + final ServerChunkCache.ChunkAndHolder holder = iterator.next(); + final ChunkPos pos = holder.chunk().getPos(); + + // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded + regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) + .tickingChunks.add(holder); + } + for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) { + final ServerChunkCache.ChunkAndHolder holder = iterator.next(); + final ChunkPos pos = holder.chunk().getPos(); + + // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded + regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) + .chunks.add(holder); + } + + // redstone torches + if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { + for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { + final BlockPos pos = info.pos; + + final RegionizedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift)); + if (worldData != null) { + if (worldData.redstoneUpdateInfos == null) { + worldData.redstoneUpdateInfos = new ArrayDeque<>(); + } + worldData.redstoneUpdateInfos.add(info); + } // else: chunk unloaded + } + } + // mob spawning + for (final RegionizedWorldData regionizedWorldData : dataSet) { + regionizedWorldData.catSpawnerNextTick = from.catSpawnerNextTick; + regionizedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick; + regionizedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick; + regionizedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay; + regionizedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance; + regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay; + regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid + } + } + }; + + public final ServerLevel world; + + private RegionizedServer.WorldLevelData tickData; + + // connections + public final List connections = new ArrayList<>(); + + // misc. fields + private boolean isHandlingTick; + + public void setHandlingTick(final boolean to) { + this.isHandlingTick = to; + } + + public boolean isHandlingTick() { + return this.isHandlingTick; + } + + // entities + private final List localPlayers = new ArrayList<>(); + private final NearbyPlayers nearbyPlayers; + private final ReferenceList allEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); + private final ReferenceList loadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); + private final ReferenceList toProcessTrackingUnloading = new ReferenceList<>(EMPTY_ENTITY_ARRAY); + private final IteratorSafeOrderedReferenceSet entityTickList = new IteratorSafeOrderedReferenceSet<>(); + private final IteratorSafeOrderedReferenceSet navigatingMobs = new IteratorSafeOrderedReferenceSet<>(); + public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker + public final ReferenceList trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker + + // block ticking + private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); + private final LevelTicks blockLevelTicks; + private final LevelTicks fluidLevelTicks; + + // tile entity ticking + private final List pendingBlockEntityTickers = new ArrayList<>(); + private final List blockEntityTickers = new ArrayList<>(); + private boolean tickingBlockEntities; + + // time + private long redstoneTime = 1L; + + public long getRedstoneGameTime() { + return this.redstoneTime; + } + + public void setRedstoneGameTime(final long to) { + this.redstoneTime = to; + } + + // ticking chunks + private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDER_ARRAY = new ServerChunkCache.ChunkAndHolder[0]; + private final ReferenceList entityTickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY); + private final ReferenceList tickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY); + private final ReferenceList chunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY); + + // Paper/CB api hook misc + // don't bother to merge/split these, no point + // From ServerLevel + public boolean hasPhysicsEvent = true; // Paper + public boolean hasEntityMoveEvent = false; // Paper + // Paper start - Optimize Hoppers + public boolean skipPullModeEventFire = false; + public boolean skipPushModeEventFire = false; + public boolean skipHopperEvents = false; + // Paper end - Optimize Hoppers + public long lastMidTickExecute; + public long lastMidTickExecuteFailure; + // From Level + public boolean populating; + public final NeighborUpdater neighborUpdater; + public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 + public boolean captureBlockStates = false; + public boolean captureTreeGeneration = false; + public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent + public final Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper + public final Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper + public List captureDrops; + // Paper start + public int wakeupInactiveRemainingAnimals; + public int wakeupInactiveRemainingFlying; + public int wakeupInactiveRemainingMonsters; + public int wakeupInactiveRemainingVillagers; + // Paper end + public int currentPrimedTnt = 0; // Spigot + @Nullable + @VisibleForDebug + public NaturalSpawner.SpawnState lastSpawnState; + public boolean shouldSignal = true; + public final Map explosionDensityCache = new HashMap<>(64, 0.25f); + public final PathTypeCache pathTypesByPosCache = new PathTypeCache(); + public ServerChunkCache.ChunkAndHolder[] iterationCopy; // Paper - chunk tick iteration optimisations + + // not transient + public java.util.ArrayDeque redstoneUpdateInfos; + + // Mob spawning + public final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>(); + public int catSpawnerNextTick = 0; + public int patrolSpawnerNextTick = 0; + public int phantomSpawnerNextTick = 0; + public int wanderingTraderTickDelay = Integer.MIN_VALUE; + public int wanderingTraderSpawnDelay; + public int wanderingTraderSpawnChance; + public VillageSiegeState villageSiegeState = new VillageSiegeState(); + + public static final class VillageSiegeState { + public boolean hasSetupSiege; + public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE; + public int zombiesToSpawn; + public int nextSpawnTime; + public int spawnX; + public int spawnY; + public int spawnZ; + } + // Redstone + public final alternate.current.wire.WireHandler wireHandler; + public final com.destroystokyo.paper.util.RedstoneWireTurbo turbo; + + public RegionizedWorldData(final ServerLevel world) { + this.world = world; + this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, true); + this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world.getProfilerSupplier(), world, false); + this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax); + this.nearbyPlayers = new NearbyPlayers(world); + this.wireHandler = new alternate.current.wire.WireHandler(world); + this.turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo((RedStoneWireBlock)Blocks.REDSTONE_WIRE); + + // tasks may be drained before the region ticks, so we must set up the tick data early just in case + this.updateTickData(); + } + + public void checkWorld(final Level against) { + if (this.world != against) { + throw new IllegalStateException("World mismatch: expected " + this.world.getWorld().getName() + " but got " + (against == null ? "null" : against.getWorld().getName())); + } + } + + public RegionizedServer.WorldLevelData getTickData() { + return this.tickData; + } + + private long lagCompensationTick; + + public long getLagCompensationTick() { + return this.lagCompensationTick; + } + + public void updateTickData() { + this.tickData = this.world.tickData; + this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper + this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper + this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper + // always subtract from server init so that the tick starts at zero, allowing us to cast to int without much worry + this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / TickRegionScheduler.TIME_BETWEEN_TICKS; + } + + public NearbyPlayers getNearbyPlayers() { + return this.nearbyPlayers; + } + + private static void cleanUpConnection(final Connection conn) { + // note: ALL connections HERE have a player + final ServerPlayer player = conn.getPlayer(); + // now that the connection is removed, we can allow this region to die + player.serverLevel().chunkSource.removeTicketAtLevel( + ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos, + ChunkHolderManager.MAX_TICKET_LEVEL, + player.connection.disconnectTicketId + ); + } + + // connections + public void tickConnections() { + final List connections = new ArrayList<>(this.connections); + Collections.shuffle(connections); + for (final Connection conn : connections) { + if (!conn.isConnected()) { + conn.handleDisconnection(); + // global tick thread will not remove connections not owned by it, so we need to + RegionizedServer.getInstance().removeConnection(conn); + this.connections.remove(conn); + cleanUpConnection(conn); + continue; + } + if (!this.connections.contains(conn)) { + // removed by connection tick? + continue; + } + + try { + conn.tick(); + } catch (final Exception exception) { + if (conn.isMemoryConnection()) { + throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); + } + + LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception); + MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); + + conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { + conn.disconnect(ichatmutablecomponent); + })); + conn.setReadOnly(); + continue; + } + } + } + + // entities hooks + public int getEntityCount() { + return this.allEntities.size(); + } + + public int getPlayerCount() { + return this.localPlayers.size(); + } + + public Iterable getLocalEntities() { + return this.allEntities; + } + + public Entity[] getLocalEntitiesCopy() { + return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class); + } + + public List getLocalPlayers() { + return this.localPlayers; + } + + public void addLoadedEntity(final Entity entity) { + if (this.loadedEntities.add(entity)) { + this.toProcessTrackingUnloading.remove(entity); + } + } + + public boolean hasLoadedEntity(final Entity entity) { + return this.loadedEntities.contains(entity); + } + + public void removeLoadedEntity(final Entity entity) { + if (this.loadedEntities.remove(entity)) { + this.toProcessTrackingUnloading.add(entity); + } + } + + public Iterable getLoadedEntities() { + return this.loadedEntities; + } + + public Entity[] takeTrackingUnloads() { + final Entity[] ret = Arrays.copyOf(this.toProcessTrackingUnloading.getRawData(), this.toProcessTrackingUnloading.size(), Entity[].class); + + this.toProcessTrackingUnloading.clear(); + + return ret; + } + + public void addEntityTickingEntity(final Entity entity) { + if (!TickThread.isTickThreadFor(entity)) { + throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); + } + this.entityTickList.add(entity); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public boolean hasEntityTickingEntity(final Entity entity) { + return this.entityTickList.contains(entity); + } + + public void removeEntityTickingEntity(final Entity entity) { + if (!TickThread.isTickThreadFor(entity)) { + throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); + } + this.entityTickList.remove(entity); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void forEachTickingEntity(final Consumer action) { + final IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickList.iterator(); + try { + while (iterator.hasNext()) { + action.accept(iterator.next()); + } + } finally { + iterator.finishedIterating(); + } + } + + public void addEntity(final Entity entity) { + if (!TickThread.isTickThreadFor(this.world, entity.chunkPosition())) { + throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); + } + if (this.allEntities.add(entity)) { + if (entity instanceof ServerPlayer player) { + this.localPlayers.add(player); + } + TickRegions.RegionStats.updateCurrentRegion(); + } + } + + public boolean hasEntity(final Entity entity) { + return this.allEntities.contains(entity); + } + + public void removeEntity(final Entity entity) { + if (!TickThread.isTickThreadFor(entity)) { + throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); + } + if (this.allEntities.remove(entity)) { + if (entity instanceof ServerPlayer player) { + this.localPlayers.remove(player); + } + TickRegions.RegionStats.updateCurrentRegion(); + } + } + + public void addNavigatingMob(final Mob mob) { + if (!TickThread.isTickThreadFor(mob)) { + throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); + } + this.navigatingMobs.add(mob); + } + + public void removeNavigatingMob(final Mob mob) { + if (!TickThread.isTickThreadFor(mob)) { + throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); + } + this.navigatingMobs.remove(mob); + } + + public Iterator getNavigatingMobs() { + return this.navigatingMobs.unsafeIterator(); + } + + // block ticking hooks + // Since block event data does not require chunk holders to be created for the chunk they reside in, + // it's not actually guaranteed that when merging / splitting data that we actually own the data... + // Note that we can only ever not own the event data when the chunk unloads, and so I've decided to + // make the code easier by simply discarding it in such an event + public void pushBlockEvent(final BlockEventData blockEventData) { + TickThread.ensureTickThread(this.world, blockEventData.pos(), "Cannot queue block even data async"); + this.blockEvents.add(blockEventData); + } + + public void pushBlockEvents(final Collection blockEvents) { + for (final BlockEventData blockEventData : blockEvents) { + this.pushBlockEvent(blockEventData); + } + } + + public void removeIfBlockEvents(final Predicate predicate) { + for (final Iterator iterator = this.blockEvents.iterator(); iterator.hasNext();) { + final BlockEventData blockEventData = iterator.next(); + if (predicate.test(blockEventData)) { + iterator.remove(); + } + } + } + + public BlockEventData removeFirstBlockEvent() { + BlockEventData ret; + while (!this.blockEvents.isEmpty()) { + ret = this.blockEvents.removeFirst(); + if (TickThread.isTickThreadFor(this.world, ret.pos())) { + return ret; + } // else: chunk must have been unloaded + } + + return null; + } + + public LevelTicks getBlockLevelTicks() { + return this.blockLevelTicks; + } + + public LevelTicks getFluidLevelTicks() { + return this.fluidLevelTicks; + } + + // tile entity ticking + public void addBlockEntityTicker(final TickingBlockEntity ticker) { + TickThread.ensureTickThread(this.world, ticker.getPos(), "Tile entity must be owned by current region"); + + (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); + } + + public void seTtickingBlockEntities(final boolean to) { + this.tickingBlockEntities = true; + } + + public List getBlockEntityTickers() { + return this.blockEntityTickers; + } + + public void pushPendingTickingBlockEntities() { + if (!this.pendingBlockEntityTickers.isEmpty()) { + this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); + this.pendingBlockEntityTickers.clear(); + } + } + + // ticking chunks + public void addEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { + this.entityTickingChunks.add(holder); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void removeEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { + this.entityTickingChunks.remove(holder); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public ReferenceList getEntityTickingChunks() { + return this.entityTickingChunks; + } + + public void addTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { + this.tickingChunks.add(holder); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void removeTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { + this.tickingChunks.remove(holder); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public ReferenceList getTickingChunks() { + return this.tickingChunks; + } + + public void addChunk(final ServerChunkCache.ChunkAndHolder holder) { + this.chunks.add(holder); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void removeChunk(final ServerChunkCache.ChunkAndHolder holder) { + this.chunks.remove(holder); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public ReferenceList getChunks() { + return this.chunks; + } + + public int getEntityTickingChunkCount() { + return this.entityTickingChunks.size(); + } + + public int getChunkCount() { + return this.chunks.size(); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/Schedule.java b/src/main/java/io/papermc/paper/threadedregions/Schedule.java new file mode 100644 index 0000000000000000000000000000000000000000..112d24a93bddf3d81c9176c05340c94ecd1a40a3 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/Schedule.java @@ -0,0 +1,91 @@ +package io.papermc.paper.threadedregions; + +/** + * A Schedule is an object that can be used to maintain a periodic schedule for an event of interest. + */ +public final class Schedule { + + private long lastPeriod; + + /** + * Initialises a schedule with the provided period. + * @param firstPeriod The last time an event of interest occurred. + * @see #setLastPeriod(long) + */ + public Schedule(final long firstPeriod) { + this.lastPeriod = firstPeriod; + } + + /** + * Updates the last period to the specified value. This call sets the last "time" the event + * of interest took place at. Thus, the value returned by {@link #getDeadline(long)} is + * the provided time plus the period length provided to {@code getDeadline}. + * @param value The value to set the last period to. + */ + public void setLastPeriod(final long value) { + this.lastPeriod = value; + } + + /** + * Returns the last time the event of interest should have taken place. + */ + public long getLastPeriod() { + return this.lastPeriod; + } + + /** + * Returns the number of times the event of interest should have taken place between the last + * period and the provided time given the period between each event. + * @param periodLength The length of the period between events in ns. + * @param time The provided time. + */ + public int getPeriodsAhead(final long periodLength, final long time) { + final long difference = time - this.lastPeriod; + final int ret = (int)(Math.abs(difference) / periodLength); + return difference >= 0 ? ret : -ret; + } + + /** + * Returns the next starting deadline for the event of interest to take place, + * given the provided period length. + * @param periodLength The provided period length. + */ + public long getDeadline(final long periodLength) { + return this.lastPeriod + periodLength; + } + + /** + * Adjusts the last period so that the next starting deadline returned is the next period specified, + * given the provided period length. + * @param nextPeriod The specified next starting deadline. + * @param periodLength The specified period length. + */ + public void setNextPeriod(final long nextPeriod, final long periodLength) { + this.lastPeriod = nextPeriod - periodLength; + } + + /** + * Increases the last period by the specified number of periods and period length. + * The specified number of periods may be < 0, in which case the last period + * will decrease. + * @param periods The specified number of periods. + * @param periodLength The specified period length. + */ + public void advanceBy(final int periods, final long periodLength) { + this.lastPeriod += (long)periods * periodLength; + } + + /** + * Sets the last period so that it is the specified number of periods ahead + * given the specified time and period length. + * @param periodsToBeAhead Specified number of periods to be ahead by. + * @param periodLength The specified period length. + * @param time The specified time. + */ + public void setPeriodsAhead(final int periodsToBeAhead, final long periodLength, final long time) { + final int periodsAhead = this.getPeriodsAhead(periodLength, time); + final int periodsToAdd = periodsToBeAhead - periodsAhead; + + this.lastPeriod -= (long)periodsToAdd * periodLength; + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..7b31c4ea6d01f936271bdadc3626201dcf32a683 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/TeleportUtils.java @@ -0,0 +1,70 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.completable.Completable; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; +import org.bukkit.Location; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.event.player.PlayerTeleportEvent; +import java.util.function.Consumer; + +public final class TeleportUtils { + + public static void teleport(final Entity from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch, + final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) { + // retrieve coordinates + final Completable positionCompletable = new Completable<>(); + + positionCompletable.addWaiter( + (final Location loc, final Throwable thr) -> { + if (loc == null) { + if (onComplete != null) { + onComplete.accept(null); + } + return; + } + final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule( + (final Entity realFrom) -> { + final Vec3 pos = new Vec3( + loc.getX(), loc.getY(), loc.getZ() + ); + (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync( + ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null, + cause, teleportFlags, onComplete + ); + }, + (final Entity retired) -> { + if (onComplete != null) { + onComplete.accept(null); + } + }, + 1L + ); + if (!scheduled) { + if (onComplete != null) { + onComplete.accept(null); + } + } + } + ); + + final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule( + (final Entity target) -> { + positionCompletable.complete(target.getBukkitEntity().getLocation()); + }, + (final Entity retired) -> { + if (onComplete != null) { + onComplete.accept(null); + } + }, + 1L + ); + if (!scheduled) { + if (onComplete != null) { + onComplete.accept(null); + } + } + } + + private TeleportUtils() {} +} diff --git a/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java new file mode 100644 index 0000000000000000000000000000000000000000..ce388e0ef231d7d73f75f5778c58eb40f6402f0f --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/ThreadedRegionizer.java @@ -0,0 +1,1405 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import com.destroystokyo.paper.util.SneakyThrow; +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongComparator; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.StampedLock; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; + +public final class ThreadedRegionizer, S extends ThreadedRegionizer.ThreadedRegionSectionData> { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public final int regionSectionChunkSize; + public final int sectionChunkShift; + public final int minSectionRecalcCount; + public final int emptySectionCreateRadius; + public final int regionSectionMergeRadius; + public final double maxDeadRegionPercent; + public final ServerLevel world; + + private final SWMRLong2ObjectHashTable> sections = new SWMRLong2ObjectHashTable<>(); + private final SWMRLong2ObjectHashTable> regionsById = new SWMRLong2ObjectHashTable<>(); + private final RegionCallbacks callbacks; + private final StampedLock regionLock = new StampedLock(); + private Thread writeLockOwner; + + /* + static final record Operation(String type, int chunkX, int chunkZ) {} + private final MultiThreadedQueue ops = new MultiThreadedQueue<>(); + */ + + /* + * See REGION_LOGIC.md for complete details on what this class is doing + */ + + public ThreadedRegionizer(final int minSectionRecalcCount, final double maxDeadRegionPercent, + final int emptySectionCreateRadius, final int regionSectionMergeRadius, + final int regionSectionChunkShift, final ServerLevel world, + final RegionCallbacks callbacks) { + if (emptySectionCreateRadius <= 0) { + throw new IllegalStateException("Region section create radius must be > 0"); + } + if (regionSectionMergeRadius <= 0) { + throw new IllegalStateException("Region section merge radius must be > 0"); + } + this.regionSectionChunkSize = 1 << regionSectionChunkShift; + this.sectionChunkShift = regionSectionChunkShift; + this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); + this.maxDeadRegionPercent = maxDeadRegionPercent; + this.emptySectionCreateRadius = emptySectionCreateRadius; + this.regionSectionMergeRadius = regionSectionMergeRadius; + this.world = world; + this.callbacks = callbacks; + //this.loadTestData(); + } + + /* + private static String substr(String val, String prefix, int from) { + int idx = val.indexOf(prefix, from) + prefix.length(); + int idx2 = val.indexOf(',', idx); + if (idx2 == -1) { + idx2 = val.indexOf(']', idx); + } + return val.substring(idx, idx2); + } + + private void loadTestData() { + if (true) { + return; + } + try { + final JsonArray arr = JsonParser.parseReader(new FileReader("test.json")).getAsJsonArray(); + + List ops = new ArrayList<>(); + + for (JsonElement elem : arr) { + JsonObject obj = elem.getAsJsonObject(); + String val = obj.get("value").getAsString(); + + String type = substr(val, "type=", 0); + String x = substr(val, "chunkX=", 0); + String z = substr(val, "chunkZ=", 0); + + ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z))); + } + + for (Operation op : ops) { + switch (op.type) { + case "add": { + this.addChunk(op.chunkX, op.chunkZ); + break; + } + case "remove": { + this.removeChunk(op.chunkX, op.chunkZ); + break; + } + case "mark_ticking": { + this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking(); + break; + } + case "rel_region": { + if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) { + this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking(); + } + break; + } + } + } + + } catch (final Exception ex) { + throw new IllegalStateException(ex); + } + } + */ + + public void acquireReadLock() { + this.regionLock.readLock(); + } + + public void releaseReadLock() { + this.regionLock.tryUnlockRead(); + } + + private void acquireWriteLock() { + final Thread currentThread = Thread.currentThread(); + if (this.writeLockOwner == currentThread) { + throw new IllegalStateException("Cannot recursively operate in the regioniser"); + } + this.regionLock.writeLock(); + this.writeLockOwner = currentThread; + } + + private void releaseWriteLock() { + this.writeLockOwner = null; + this.regionLock.tryUnlockWrite(); + } + + private void onRegionCreate(final ThreadedRegion region) { + final ThreadedRegion conflict; + if ((conflict = this.regionsById.putIfAbsent(region.id, region)) != null) { + throw new IllegalStateException("Region " + region + " is already mapped to " + conflict); + } + } + + private void onRegionDestroy(final ThreadedRegion region) { + final ThreadedRegion removed = this.regionsById.remove(region.id); + if (removed != region) { + throw new IllegalStateException("Expected to remove " + region + ", but removed " + removed); + } + } + + public int getSectionCoordinate(final int chunkCoordinate) { + return chunkCoordinate >> this.sectionChunkShift; + } + + public long getSectionKey(final BlockPos pos) { + return CoordinateUtils.getChunkKey((pos.getX() >> 4) >> this.sectionChunkShift, (pos.getZ() >> 4) >> this.sectionChunkShift); + } + + public long getSectionKey(final ChunkPos pos) { + return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); + } + + public long getSectionKey(final Entity entity) { + final ChunkPos pos = entity.chunkPosition(); + return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); + } + + public void computeForAllRegions(final Consumer> consumer) { + this.regionLock.readLock(); + try { + this.regionsById.forEachValue(consumer); + } finally { + this.regionLock.tryUnlockRead(); + } + } + + public void computeForAllRegionsUnsynchronised(final Consumer> consumer) { + this.regionsById.forEachValue(consumer); + } + + public int computeForRegions(final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ, + final Consumer>> consumer) { + final int shift = this.sectionChunkShift; + final int fromSectionX = fromChunkX >> shift; + final int fromSectionZ = fromChunkZ >> shift; + final int toSectionX = toChunkX >> shift; + final int toSectionZ = toChunkZ >> shift; + this.acquireWriteLock(); + try { + final ReferenceOpenHashSet> set = new ReferenceOpenHashSet<>(); + + for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { + for (int currX = fromSectionX; currX <= toSectionX; ++currX) { + final ThreadedRegionSection section = this.sections.get(CoordinateUtils.getChunkKey(currX, currZ)); + if (section != null) { + set.add(section.getRegionPlain()); + } + } + } + + consumer.accept(set); + + return set.size(); + } finally { + this.releaseWriteLock(); + } + } + + public ThreadedRegion getRegionAtUnsynchronised(final int chunkX, final int chunkZ) { + final int sectionX = chunkX >> this.sectionChunkShift; + final int sectionZ = chunkZ >> this.sectionChunkShift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + + final ThreadedRegionSection section = this.sections.get(sectionKey); + + return section == null ? null : section.getRegion(); + } + + public ThreadedRegion getRegionAtSynchronised(final int chunkX, final int chunkZ) { + final int sectionX = chunkX >> this.sectionChunkShift; + final int sectionZ = chunkZ >> this.sectionChunkShift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + + // try an optimistic read + { + final long readAttempt = this.regionLock.tryOptimisticRead(); + final ThreadedRegionSection optimisticSection = this.sections.get(sectionKey); + final ThreadedRegion optimisticRet = + optimisticSection == null ? null : optimisticSection.getRegionPlain(); + if (this.regionLock.validate(readAttempt)) { + return optimisticRet; + } + } + + // failed, fall back to acquiring the lock + this.regionLock.readLock(); + try { + final ThreadedRegionSection section = this.sections.get(sectionKey); + + return section == null ? null : section.getRegionPlain(); + } finally { + this.regionLock.tryUnlockRead(); + } + } + + /** + * Adds a chunk to the regioniser. Note that it is illegal to add a chunk unless + * addChunk has not been called for it or removeChunk has been previously called. + * + *

    + * Note that it is illegal to additionally call addChunk or removeChunk for the same + * region section in parallel. + *

    + */ + public void addChunk(final int chunkX, final int chunkZ) { + final int sectionX = chunkX >> this.sectionChunkShift; + final int sectionZ = chunkZ >> this.sectionChunkShift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + + // Given that for each section, no addChunk/removeChunk can occur in parallel, + // we can avoid the lock IF the section exists AND it has a non-zero chunk count. + { + final ThreadedRegionSection existing = this.sections.get(sectionKey); + if (existing != null && !existing.isEmpty()) { + existing.addChunk(chunkX, chunkZ); + return; + } // else: just acquire the write lock + } + + this.acquireWriteLock(); + try { + ThreadedRegionSection section = this.sections.get(sectionKey); + + List> newSections = new ArrayList<>(); + + if (section == null) { + // no section at all + section = new ThreadedRegionSection<>(sectionX, sectionZ, this, chunkX, chunkZ); + this.sections.put(sectionKey, section); + newSections.add(section); + } else { + section.addChunk(chunkX, chunkZ); + } + // due to the fast check from above, we know the section is empty whether we needed to create it or not + + // enforce the adjacency invariant by creating / updating neighbour sections + final int createRadius = this.emptySectionCreateRadius; + final int searchRadius = createRadius + this.regionSectionMergeRadius; + ReferenceOpenHashSet> nearbyRegions = null; + for (int dx = -searchRadius; dx <= searchRadius; ++dx) { + for (int dz = -searchRadius; dz <= searchRadius; ++dz) { + if ((dx | dz) == 0) { + continue; + } + final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); + final boolean inCreateRange = squareDistance <= createRadius; + + final int neighbourX = dx + sectionX; + final int neighbourZ = dz + sectionZ; + final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); + + if (neighbourSection != null) { + if (nearbyRegions == null) { + nearbyRegions = new ReferenceOpenHashSet<>(((searchRadius * 2 + 1) * (searchRadius * 2 + 1)) >> 1); + } + nearbyRegions.add(neighbourSection.getRegionPlain()); + } + + if (!inCreateRange) { + continue; + } + + // we need to ensure the section exists + if (neighbourSection != null) { + // nothing else to do + neighbourSection.incrementNonEmptyNeighbours(); + continue; + } + neighbourSection = new ThreadedRegionSection<>(neighbourX, neighbourZ, this, 1); + if (null != this.sections.put(neighbourKey, neighbourSection)) { + throw new IllegalStateException("Failed to insert new section"); + } + newSections.add(neighbourSection); + } + } + + if (newSections.isEmpty()) { + // if we didn't add any sections, then we don't need to merge any regions or create a region + return; + } + + final ThreadedRegion regionOfInterest; + final boolean regionOfInterestAlive; + if (nearbyRegions == null) { + // we can simply create a new region, don't have neighbours to worry about merging into + regionOfInterest = new ThreadedRegion<>(this); + regionOfInterestAlive = true; + + for (int i = 0, len = newSections.size(); i < len; ++i) { + regionOfInterest.addSection(newSections.get(i)); + } + + // only call create callback after adding sections + regionOfInterest.onCreate(); + } else { + // need to merge the regions + ThreadedRegion firstUnlockedRegion = null; + + for (final ThreadedRegion region : nearbyRegions) { + if (region.isTicking()) { + continue; + } + firstUnlockedRegion = region; + if (firstUnlockedRegion.state == ThreadedRegion.STATE_READY && (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty())) { + throw new IllegalStateException("Illegal state for unlocked region " + firstUnlockedRegion); + } + break; + } + + if (firstUnlockedRegion != null) { + regionOfInterest = firstUnlockedRegion; + } else { + regionOfInterest = new ThreadedRegion<>(this); + } + + for (int i = 0, len = newSections.size(); i < len; ++i) { + regionOfInterest.addSection(newSections.get(i)); + } + + // only call create callback after adding sections + if (firstUnlockedRegion == null) { + regionOfInterest.onCreate(); + } + + if (firstUnlockedRegion != null && nearbyRegions.size() == 1) { + // nothing to do further, no need to merge anything + return; + } + + // we need to now tell all the other regions to merge into the region we just created, + // and to merge all the ones we can immediately + + for (final ThreadedRegion region : nearbyRegions) { + if (region == regionOfInterest) { + continue; + } + + if (!region.killAndMergeInto(regionOfInterest)) { + // note: the region may already be a merge target + regionOfInterest.mergeIntoLater(region); + } + } + + if (firstUnlockedRegion != null && firstUnlockedRegion.state == ThreadedRegion.STATE_READY) { + // we need to retire this region if the merges added other pending merges + if (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty()) { + firstUnlockedRegion.state = ThreadedRegion.STATE_TRANSIENT; + this.callbacks.onRegionInactive(firstUnlockedRegion); + } + } + + // need to set alive if we created it and there are no pending merges + regionOfInterestAlive = firstUnlockedRegion == null && regionOfInterest.mergeIntoLater.isEmpty() && regionOfInterest.expectingMergeFrom.isEmpty(); + } + + if (regionOfInterestAlive) { + regionOfInterest.state = ThreadedRegion.STATE_READY; + if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { + throw new IllegalStateException("Should not happen on region " + this); + } + this.callbacks.onRegionActive(regionOfInterest); + } + + if (regionOfInterest.state == ThreadedRegion.STATE_READY) { + if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { + throw new IllegalStateException("Should not happen on region " + this); + } + } + } catch (final Throwable throwable) { + LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); + SneakyThrow.sneaky(throwable); + return; // unreachable + } finally { + this.releaseWriteLock(); + } + } + + public void removeChunk(final int chunkX, final int chunkZ) { + final int sectionX = chunkX >> this.sectionChunkShift; + final int sectionZ = chunkZ >> this.sectionChunkShift; + final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + + // Given that for each section, no addChunk/removeChunk can occur in parallel, + // we can avoid the lock IF the section exists AND it has a chunk count > 1 + final ThreadedRegionSection section = this.sections.get(sectionKey); + if (section == null) { + throw new IllegalStateException("Chunk (" + chunkX + "," + chunkZ + ") has no section"); + } + if (!section.hasOnlyOneChunk()) { + // chunk will not go empty, so we don't need to acquire the lock + section.removeChunk(chunkX, chunkZ); + return; + } + + this.acquireWriteLock(); + try { + section.removeChunk(chunkX, chunkZ); + + final int searchRadius = this.emptySectionCreateRadius; + for (int dx = -searchRadius; dx <= searchRadius; ++dx) { + for (int dz = -searchRadius; dz <= searchRadius; ++dz) { + if ((dx | dz) == 0) { + continue; + } + + final int neighbourX = dx + sectionX; + final int neighbourZ = dz + sectionZ; + final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); + + final ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); + + // should be non-null here always + neighbourSection.decrementNonEmptyNeighbours(); + } + } + } catch (final Throwable throwable) { + LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); + SneakyThrow.sneaky(throwable); + return; // unreachable + } finally { + this.releaseWriteLock(); + } + } + + // must hold regionLock + private void onRegionRelease(final ThreadedRegion region) { + if (!region.mergeIntoLater.isEmpty()) { + throw new IllegalStateException("Region " + region + " should not have any regions to merge into!"); + } + + final boolean hasExpectingMerges = !region.expectingMergeFrom.isEmpty(); + + // is this region supposed to merge into any other region? + if (hasExpectingMerges) { + // merge the regions into this one + final ReferenceOpenHashSet> expectingMergeFrom = region.expectingMergeFrom.clone(); + for (final ThreadedRegion mergeFrom : expectingMergeFrom) { + if (!mergeFrom.killAndMergeInto(region)) { + throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + region); + } + } + + if (!region.expectingMergeFrom.isEmpty()) { + throw new IllegalStateException("Region " + region + " should no longer have merge requests after mering from " + expectingMergeFrom); + } + + if (!region.mergeIntoLater.isEmpty()) { + // There is another nearby ticking region that we need to merge into + region.state = ThreadedRegion.STATE_TRANSIENT; + this.callbacks.onRegionInactive(region); + // return to avoid removing dead sections or splitting, these actions will be performed + // by the region we merge into + return; + } + } + + // now check whether we need to recalculate regions + final boolean removeDeadSections = hasExpectingMerges || region.hasNoAliveSections() + || (region.sectionByKey.size() >= this.minSectionRecalcCount && region.getDeadSectionPercent() >= this.maxDeadRegionPercent); + final boolean removedDeadSections = removeDeadSections && !region.deadSections.isEmpty(); + if (removeDeadSections) { + // kill dead sections + for (final ThreadedRegionSection deadSection : region.deadSections) { + final long key = CoordinateUtils.getChunkKey(deadSection.sectionX, deadSection.sectionZ); + + if (!deadSection.isEmpty()) { + throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); + } + if (deadSection.hasNonEmptyNeighbours()) { + throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has non-empty neighbours!"); + } + if (!region.sectionByKey.remove(key, deadSection)) { + throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); + } + if (this.sections.remove(key) != deadSection) { + throw new IllegalStateException("Cannot remove dead section '" + + deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + this.sections.get(key)); + } + } + region.deadSections.clear(); + } + + // if we removed dead sections, we should check if the region can be split into smaller ones + // otherwise, the region remains alive + if (!removedDeadSections) { + // didn't remove dead sections, don't check for split + region.state = ThreadedRegion.STATE_READY; + if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { + throw new IllegalStateException("Illegal state " + region); + } + return; + } + + // first, we need to build copy of coordinate->section map of all sections in recalculate + final Long2ReferenceOpenHashMap> recalculateSections = region.sectionByKey.clone(); + + if (recalculateSections.isEmpty()) { + // looks like the region's sections were all dead, and now there is no region at all + region.state = ThreadedRegion.STATE_DEAD; + region.onRemove(true); + return; + } + + // merge radius is max, since recalculateSections includes the dead or empty sections + final int mergeRadius = Math.max(this.regionSectionMergeRadius, this.emptySectionCreateRadius); + + final List>> newRegions = new ArrayList<>(); + while (!recalculateSections.isEmpty()) { + // select any section, then BFS around it to find all of its neighbours to form a region + // once no more neighbours are found, the region is complete + final List> currRegion = new ArrayList<>(); + final Iterator> firstIterator = recalculateSections.values().iterator(); + + currRegion.add(firstIterator.next()); + firstIterator.remove(); + search_loop: + for (int idx = 0; idx < currRegion.size(); ++idx) { + final ThreadedRegionSection curr = currRegion.get(idx); + final int centerX = curr.sectionX; + final int centerZ = curr.sectionZ; + + // find neighbours in radius + for (int dz = -mergeRadius; dz <= mergeRadius; ++dz) { + for (int dx = -mergeRadius; dx <= mergeRadius; ++dx) { + if ((dx | dz) == 0) { + continue; + } + + final ThreadedRegionSection section = recalculateSections.remove(CoordinateUtils.getChunkKey(dx + centerX, dz + centerZ)); + if (section == null) { + continue; + } + + currRegion.add(section); + + if (recalculateSections.isEmpty()) { + // no point in searching further + break search_loop; + } + } + } + } + + newRegions.add(currRegion); + } + + // now we have split the regions into separate parts, we can split recalculate + + if (newRegions.size() == 1) { + // no need to split anything, we're done here + region.state = ThreadedRegion.STATE_READY; + if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { + throw new IllegalStateException("Illegal state " + region); + } + return; + } + + final List> newRegionObjects = new ArrayList<>(newRegions.size()); + for (int i = 0, len = newRegions.size(); i < len; ++i) { + newRegionObjects.add(new ThreadedRegion<>(this)); + } + + this.callbacks.preSplit(region, newRegionObjects); + + // need to split the region, so we need to kill the old one first + region.state = ThreadedRegion.STATE_DEAD; + region.onRemove(true); + + // create new regions + final Long2ReferenceOpenHashMap> newRegionsMap = new Long2ReferenceOpenHashMap<>(); + final ReferenceOpenHashSet> newRegionsSet = new ReferenceOpenHashSet<>(newRegionObjects); + + for (int i = 0, len = newRegions.size(); i < len; i++) { + final List> sections = newRegions.get(i); + final ThreadedRegion newRegion = newRegionObjects.get(i); + + for (final ThreadedRegionSection section : sections) { + section.setRegionRelease(null); + newRegion.addSection(section); + final ThreadedRegion curr = newRegionsMap.putIfAbsent(section.sectionKey, newRegion); + if (curr != null) { + throw new IllegalStateException("Expected no region at " + section + ", but got " + curr + ", should have put " + newRegion); + } + } + } + + region.split(newRegionsMap, newRegionsSet); + + // only after invoking data callbacks + + for (final ThreadedRegion newRegion : newRegionsSet) { + newRegion.state = ThreadedRegion.STATE_READY; + if (!newRegion.expectingMergeFrom.isEmpty() || !newRegion.mergeIntoLater.isEmpty()) { + throw new IllegalStateException("Illegal state " + newRegion); + } + newRegion.onCreate(); + this.callbacks.onRegionActive(newRegion); + } + } + + public static final class ThreadedRegion, S extends ThreadedRegionSectionData> { + + private static final AtomicLong REGION_ID_GENERATOR = new AtomicLong(); + + private static final int STATE_TRANSIENT = 0; + private static final int STATE_READY = 1; + private static final int STATE_TICKING = 2; + private static final int STATE_DEAD = 3; + + public final long id; + + private int state; + + private final Long2ReferenceOpenHashMap> sectionByKey = new Long2ReferenceOpenHashMap<>(); + private final ReferenceOpenHashSet> deadSections = new ReferenceOpenHashSet<>(); + + public final ThreadedRegionizer regioniser; + + private final R data; + + private final ReferenceOpenHashSet> mergeIntoLater = new ReferenceOpenHashSet<>(); + private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>(); + + public ThreadedRegion(final ThreadedRegionizer regioniser) { + this.regioniser = regioniser; + this.id = REGION_ID_GENERATOR.getAndIncrement(); + this.state = STATE_TRANSIENT; + this.data = regioniser.callbacks.createNewData(this); + } + + public LongArrayList getOwnedSections() { + final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); + if (lock) { + this.regioniser.regionLock.readLock(); + } + try { + final LongArrayList ret = new LongArrayList(this.sectionByKey.size()); + ret.addAll(this.sectionByKey.keySet()); + + return ret; + } finally { + if (lock) { + this.regioniser.regionLock.tryUnlockRead(); + } + } + } + + /** + * returns an iterator directly over the sections map. This is only to be used by a thread which is _ticking_ + * 'this' region. + */ + public LongIterator getOwnedSectionsUnsynchronised() { + return this.sectionByKey.keySet().iterator(); + } + + public LongArrayList getOwnedChunks() { + final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); + if (lock) { + this.regioniser.regionLock.readLock(); + } + try { + final LongArrayList ret = new LongArrayList(); + for (final ThreadedRegionSection section : this.sectionByKey.values()) { + ret.addAll(section.getChunks()); + } + + return ret; + } finally { + if (lock) { + this.regioniser.regionLock.tryUnlockRead(); + } + } + } + + public Long getCenterSection() { + final LongArrayList sections = this.getOwnedSections(); + + final LongComparator comparator = (final long k1, final long k2) -> { + final int x1 = CoordinateUtils.getChunkX(k1); + final int x2 = CoordinateUtils.getChunkX(k2); + + final int z1 = CoordinateUtils.getChunkZ(x1); + final int z2 = CoordinateUtils.getChunkZ(x2); + + final int zCompare = Integer.compare(z1, z2); + if (zCompare != 0) { + return zCompare; + } + + return Integer.compare(x1, x2); + }; + + // note: regions don't always have a chunk section at this point, because the region may have been killed + if (sections.isEmpty()) { + return null; + } + + sections.sort(comparator); + + return Long.valueOf(sections.getLong(sections.size() >> 1)); + } + + public ChunkPos getCenterChunk() { + final LongArrayList chunks = this.getOwnedChunks(); + + final LongComparator comparator = (final long k1, final long k2) -> { + final int x1 = CoordinateUtils.getChunkX(k1); + final int x2 = CoordinateUtils.getChunkX(k2); + + final int z1 = CoordinateUtils.getChunkZ(k1); + final int z2 = CoordinateUtils.getChunkZ(k2); + + final int zCompare = Integer.compare(z1, z2); + if (zCompare != 0) { + return zCompare; + } + + return Integer.compare(x1, x2); + }; + chunks.sort(comparator); + + // note: regions don't always have a chunk at this point, because the region may have been killed + if (chunks.isEmpty()) { + return null; + } + + final long middle = chunks.getLong(chunks.size() >> 1); + + return new ChunkPos(CoordinateUtils.getChunkX(middle), CoordinateUtils.getChunkZ(middle)); + } + + private void onCreate() { + this.regioniser.onRegionCreate(this); + this.regioniser.callbacks.onRegionCreate(this); + } + + private void onRemove(final boolean wasActive) { + if (wasActive) { + this.regioniser.callbacks.onRegionInactive(this); + } + this.regioniser.callbacks.onRegionDestroy(this); + this.regioniser.onRegionDestroy(this); + } + + private final boolean hasNoAliveSections() { + return this.deadSections.size() == this.sectionByKey.size(); + } + + private final double getDeadSectionPercent() { + return (double)this.deadSections.size() / (double)this.sectionByKey.size(); + } + + private void split(final Long2ReferenceOpenHashMap> into, final ReferenceOpenHashSet> regions) { + if (this.data != null) { + this.data.split(this.regioniser, into, regions); + } + } + + boolean killAndMergeInto(final ThreadedRegion mergeTarget) { + if (this.state == STATE_TICKING) { + return false; + } + + this.regioniser.callbacks.preMerge(this, mergeTarget); + + this.tryKill(); + + this.mergeInto(mergeTarget); + + return true; + } + + private void mergeInto(final ThreadedRegion mergeTarget) { + if (this == mergeTarget) { + throw new IllegalStateException("Cannot merge a region onto itself"); + } + if (!this.isDead()) { + throw new IllegalStateException("Source region is not dead! Source " + this + ", target " + mergeTarget); + } else if (mergeTarget.isDead()) { + throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); + } + + for (final ThreadedRegionSection section : this.sectionByKey.values()) { + section.setRegionRelease(null); + mergeTarget.addSection(section); + } + for (final ThreadedRegionSection deadSection : this.deadSections) { + if (this.sectionByKey.get(deadSection.sectionKey) != deadSection) { + throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); + } + if (!mergeTarget.deadSections.add(deadSection)) { + throw new IllegalStateException("Merge target contains dead section from source! Has " + deadSection + " from region " + this); + } + } + + // forward merge expectations + for (final ThreadedRegion region : this.expectingMergeFrom) { + if (!region.mergeIntoLater.remove(this)) { + throw new IllegalStateException("Region " + region + " was not supposed to merge into " + this + "?"); + } + if (region != mergeTarget) { + region.mergeIntoLater(mergeTarget); + } + } + + // forward merge into + for (final ThreadedRegion region : this.mergeIntoLater) { + if (!region.expectingMergeFrom.remove(this)) { + throw new IllegalStateException("Region " + this + " was not supposed to merge into " + region + "?"); + } + if (region != mergeTarget) { + mergeTarget.mergeIntoLater(region); + } + } + + // finally, merge data + if (this.data != null) { + this.data.mergeInto(mergeTarget); + } + } + + private void mergeIntoLater(final ThreadedRegion region) { + if (region.isDead()) { + throw new IllegalStateException("Trying to merge later into a dead region: " + region); + } + final boolean add1, add2; + if ((add1 = this.mergeIntoLater.add(region)) != (add2 = region.expectingMergeFrom.add(this))) { + throw new IllegalStateException("Inconsistent state between target merge " + region + " and this " + this + ": add1,add2:" + add1 + "," + add2); + } + } + + private boolean tryKill() { + switch (this.state) { + case STATE_TRANSIENT: { + this.state = STATE_DEAD; + this.onRemove(false); + return true; + } + case STATE_READY: { + this.state = STATE_DEAD; + this.onRemove(true); + return true; + } + case STATE_TICKING: { + return false; + } + case STATE_DEAD: { + throw new IllegalStateException("Already dead"); + } + default: { + throw new IllegalStateException("Unknown state: " + this.state); + } + } + } + + private boolean isDead() { + return this.state == STATE_DEAD; + } + + private boolean isTicking() { + return this.state == STATE_TICKING; + } + + private void removeDeadSection(final ThreadedRegionSection section) { + this.deadSections.remove(section); + } + + private void addDeadSection(final ThreadedRegionSection section) { + this.deadSections.add(section); + } + + private void addSection(final ThreadedRegionSection section) { + if (section.getRegionPlain() != null) { + throw new IllegalStateException("Section already has region"); + } + if (this.sectionByKey.putIfAbsent(section.sectionKey, section) != null) { + throw new IllegalStateException("Already have section " + section + ", mapped to " + this.sectionByKey.get(section.sectionKey)); + } + section.setRegionRelease(this); + } + + public R getData() { + return this.data; + } + + public boolean tryMarkTicking(final BooleanSupplier abort) { + this.regioniser.acquireWriteLock(); + try { + if (this.state != STATE_READY || abort.getAsBoolean()) { + return false; + } + + if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) { + throw new IllegalStateException("Region " + this + " should not be ready"); + } + + this.state = STATE_TICKING; + return true; + } finally { + this.regioniser.releaseWriteLock(); + } + } + + public boolean markNotTicking() { + this.regioniser.acquireWriteLock(); + try { + if (this.state != STATE_TICKING) { + throw new IllegalStateException("Attempting to release non-locked state"); + } + + this.regioniser.onRegionRelease(this); + + return this.state == STATE_READY; + } catch (final Throwable throwable) { + LOGGER.error("Failed to acquire region " + this, throwable); + SneakyThrow.sneaky(throwable); + return false; // unreachable + } finally { + this.regioniser.releaseWriteLock(); + } + } + + @Override + public String toString() { + final StringBuilder ret = new StringBuilder(128); + + ret.append("ThreadedRegion{"); + ret.append("state=").append(this.state).append(','); + // To avoid recursion in toString, maybe fix later? + //ret.append("mergeIntoLater=").append(this.mergeIntoLater).append(','); + //ret.append("expectingMergeFrom=").append(this.expectingMergeFrom).append(','); + + ret.append("sectionCount=").append(this.sectionByKey.size()).append(','); + ret.append("sections=["); + for (final Iterator> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) { + final ThreadedRegionSection section = iterator.next(); + + ret.append(section.toString()); + if (iterator.hasNext()) { + ret.append(','); + } + } + ret.append(']'); + + ret.append('}'); + return ret.toString(); + } + } + + public static final class ThreadedRegionSection, S extends ThreadedRegionSectionData> { + + public final int sectionX; + public final int sectionZ; + public final long sectionKey; + private final long[] chunksBitset; + private int chunkCount; + private int nonEmptyNeighbours; + + private ThreadedRegion region; + private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class); + + public final ThreadedRegionizer regioniser; + + private final int regionChunkShift; + private final int regionChunkMask; + + private final S data; + + private ThreadedRegion getRegionPlain() { + return (ThreadedRegion)REGION_HANDLE.get(this); + } + + private ThreadedRegion getRegionAcquire() { + return (ThreadedRegion)REGION_HANDLE.getAcquire(this); + } + + private void setRegionRelease(final ThreadedRegion value) { + REGION_HANDLE.setRelease(this, value); + } + + // creates an empty section with zero non-empty neighbours + private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser) { + this.sectionX = sectionX; + this.sectionZ = sectionZ; + this.sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); + this.chunksBitset = new long[Math.max(1, regioniser.regionSectionChunkSize * regioniser.regionSectionChunkSize / Long.SIZE)]; + this.regioniser = regioniser; + this.regionChunkShift = regioniser.sectionChunkShift; + this.regionChunkMask = regioniser.regionSectionChunkSize - 1; + this.data = regioniser.callbacks + .createNewSectionData(sectionX, sectionZ, this.regionChunkShift); + } + + // creates a section with an initial chunk with zero non-empty neighbours + private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, + final int chunkXInit, final int chunkZInit) { + this(sectionX, sectionZ, regioniser); + + final int initIndex = this.getChunkIndex(chunkXInit, chunkZInit); + this.chunkCount = 1; + this.chunksBitset[initIndex >>> 6] = 1L << (initIndex & (Long.SIZE - 1)); // index / Long.SIZE + } + + // creates an empty section with the specified number of non-empty neighbours + private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, + final int nonEmptyNeighbours) { + this(sectionX, sectionZ, regioniser); + + this.nonEmptyNeighbours = nonEmptyNeighbours; + } + + public LongArrayList getChunks() { + final LongArrayList ret = new LongArrayList(); + + if (this.chunkCount == 0) { + return ret; + } + + final int shift = this.regionChunkShift; + final int mask = this.regionChunkMask; + final int offsetX = this.sectionX << shift; + final int offsetZ = this.sectionZ << shift; + + final long[] bitset = this.chunksBitset; + for (int arrIdx = 0, arrLen = bitset.length; arrIdx < arrLen; ++arrIdx) { + long value = bitset[arrIdx]; + + for (int i = 0, bits = Long.bitCount(value); i < bits; ++i) { + final int valueIdx = Long.numberOfTrailingZeros(value); + value ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(value); + + final int idx = valueIdx | (arrIdx << 6); + + final int localX = idx & mask; + final int localZ = (idx >>> shift) & mask; + + ret.add(CoordinateUtils.getChunkKey(localX | offsetX, localZ | offsetZ)); + } + } + + return ret; + } + + private boolean isEmpty() { + return this.chunkCount == 0; + } + + private boolean hasOnlyOneChunk() { + return this.chunkCount == 1; + } + + public boolean hasNonEmptyNeighbours() { + return this.nonEmptyNeighbours != 0; + } + + /** + * Returns the section data associated with this region section. May be {@code null}. + */ + public S getData() { + return this.data; + } + + /** + * Returns the region that owns this section. Unsynchronised access may produce outdateed or transient results. + */ + public ThreadedRegion getRegion() { + return this.getRegionAcquire(); + } + + private int getChunkIndex(final int chunkX, final int chunkZ) { + return (chunkX & this.regionChunkMask) | ((chunkZ & this.regionChunkMask) << this.regionChunkShift); + } + + private void markAlive() { + this.getRegionPlain().removeDeadSection(this); + } + + private void markDead() { + this.getRegionPlain().addDeadSection(this); + } + + private void incrementNonEmptyNeighbours() { + if (++this.nonEmptyNeighbours == 1 && this.chunkCount == 0) { + this.markAlive(); + } + final int createRadius = this.regioniser.emptySectionCreateRadius; + if (this.nonEmptyNeighbours >= ((createRadius * 2 + 1) * (createRadius * 2 + 1))) { + throw new IllegalStateException("Non empty neighbours exceeded max value for radius " + createRadius); + } + } + + private void decrementNonEmptyNeighbours() { + if (--this.nonEmptyNeighbours == 0 && this.chunkCount == 0) { + this.markDead(); + } + if (this.nonEmptyNeighbours < 0) { + throw new IllegalStateException("Non empty neighbours reached zero"); + } + } + + /** + * Returns whether the chunk was zero. Effectively returns whether the caller needs to create + * dead sections / increase non-empty neighbour count for neighbouring sections. + */ + private boolean addChunk(final int chunkX, final int chunkZ) { + final int index = this.getChunkIndex(chunkX, chunkZ); + final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE + final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1))); + if (after == bitset) { + throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); + } + final boolean notEmpty = ++this.chunkCount == 1; + if (notEmpty && this.nonEmptyNeighbours == 0) { + this.markAlive(); + } + return notEmpty; + } + + /** + * Returns whether the chunk count is now zero. Effectively returns whether + * the caller needs to decrement the neighbour count for neighbouring sections. + */ + private boolean removeChunk(final int chunkX, final int chunkZ) { + final int index = this.getChunkIndex(chunkX, chunkZ); + final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE + final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1))); + if (before == bitset) { + throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); + } + final boolean empty = --this.chunkCount == 0; + if (empty && this.nonEmptyNeighbours == 0) { + this.markDead(); + } + return empty; + } + + @Override + public String toString() { + return "RegionSection{" + + "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + + "chunkCount=" + this.chunkCount + "," + + "chunksBitset=" + toString(this.chunksBitset) + "," + + "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + + "hash=" + this.hashCode() + + "}"; + } + + public String toStringWithRegion() { + return "RegionSection{" + + "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + + "chunkCount=" + this.chunkCount + "," + + "chunksBitset=" + toString(this.chunksBitset) + "," + + "hash=" + this.hashCode() + "," + + "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + + "region=" + this.getRegionAcquire() + + "}"; + } + + private static String toString(final long[] array) { + final StringBuilder ret = new StringBuilder(); + final char[] zeros = new char[Long.SIZE / 4]; + for (final long value : array) { + // zero pad the hex string + Arrays.fill(zeros, '0'); + final String string = Long.toHexString(value); + System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length()); + + ret.append(zeros); + } + + return ret.toString(); + } + } + + public static interface ThreadedRegionData, S extends ThreadedRegionSectionData> { + + /** + * Splits this region data into the specified regions set. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param regioniser Regioniser for which the regions reside in. + * @param into A map of region section coordinate key to the region that owns the section. + * @param regions The set of regions to split into. + */ + public void split(final ThreadedRegionizer regioniser, final Long2ReferenceOpenHashMap> into, + final ReferenceOpenHashSet> regions); + + /** + * Callback to merge {@code this} region data into the specified region. The state of the region is undefined + * except that its region data is already created. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param into Specified region. + */ + public void mergeInto(final ThreadedRegion into); + } + + public static interface ThreadedRegionSectionData {} + + public static interface RegionCallbacks, S extends ThreadedRegionSectionData> { + + /** + * Creates new section data for the specified section x and section z. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param sectionX x coordinate of the section. + * @param sectionZ z coordinate of the section. + * @param sectionShift The signed right shift value that can be applied to any chunk coordinate that + * produces a section coordinate. + * @return New section data, may be {@code null}. + */ + public S createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift); + + /** + * Creates new region data for the specified region. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param forRegion The region to create the data for. + * @return New region data, may be {@code null}. + */ + public R createNewData(final ThreadedRegion forRegion); + + /** + * Callback for when a region is created. This is invoked after the region is completely set up, + * so its data and owned sections are reliable to inspect. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param region The region that was created. + */ + public void onRegionCreate(final ThreadedRegion region); + + /** + * Callback for when a region is destroyed. This is invoked before the region is actually destroyed; so + * its data and owned sections are reliable to inspect. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param region The region that is about to be destroyed. + */ + public void onRegionDestroy(final ThreadedRegion region); + + /** + * Callback for when a region is considered "active." An active region x is a non-destroyed region which + * is not scheduled to merge into another region y and there are no non-destroyed regions z which are + * scheduled to merge into the region x. Equivalently, an active region is not directly adjacent to any + * other region considering the regioniser's empty section radius. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param region The region that is now active. + */ + public void onRegionActive(final ThreadedRegion region); + + /** + * Callback for when a region transistions becomes inactive. An inactive region is non-destroyed, but + * has neighbouring adjacent regions considering the regioniser's empty section radius. Effectively, + * an inactive region may not tick and needs to be merged into its neighbouring regions. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param region The region that is now inactive. + */ + public void onRegionInactive(final ThreadedRegion region); + + /** + * Callback for when a region (from) is about to be merged into a target region (into). Note that + * {@code from} is still alive and is a distinct region. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param from The region that will be merged into the target. + * @param into The target of the merge. + */ + public void preMerge(final ThreadedRegion from, final ThreadedRegion into); + + /** + * Callback for when a region (from) is about to be split into a list of target region (into). Note that + * {@code from} is still alive, while the list of target regions are not initialised. + *

    + * Note: + *

    + *

    + * This function is always called while holding critical locks and as such should not attempt to block on anything, and + * should NOT retrieve or modify ANY world state. + *

    + * @param from The region that will be merged into the target. + * @param into The list of regions to split into. + */ + public void preSplit(final ThreadedRegion from, final List> into); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/TickData.java b/src/main/java/io/papermc/paper/threadedregions/TickData.java new file mode 100644 index 0000000000000000000000000000000000000000..29f9fed5f02530b3256e6b993e607d4647daa7b6 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/TickData.java @@ -0,0 +1,333 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import io.papermc.paper.util.IntervalledCounter; +import it.unimi.dsi.fastutil.longs.LongArrayList; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class TickData { + + private final long interval; // ns + + private final ArrayDeque timeData = new ArrayDeque<>(); + + public TickData(final long intervalNS) { + this.interval = intervalNS; + } + + public void addDataFrom(final TickRegionScheduler.TickTime time) { + final long start = time.tickStart(); + + TickRegionScheduler.TickTime first; + while ((first = this.timeData.peekFirst()) != null) { + // only remove data completely out of window + if ((start - first.tickEnd()) <= this.interval) { + break; + } + this.timeData.pollFirst(); + } + + this.timeData.add(time); + } + + // fromIndex inclusive, toIndex exclusive + // will throw if arr.length == 0 + private static double median(final long[] arr, final int fromIndex, final int toIndex) { + final int len = toIndex - fromIndex; + final int middle = fromIndex + (len >>> 1); + if ((len & 1) == 0) { + // even, average the two middle points + return (double)(arr[middle - 1] + arr[middle]) / 2.0; + } else { + // odd, just grab the middle + return (double)arr[middle]; + } + } + + // will throw if arr.length == 0 + private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex, + final boolean inverse) { + final int len = toIndex - fromIndex; + long sum = 0L; + final double median = median(arr, fromIndex, toIndex); + long min = arr[0]; + long max = arr[0]; + + for (int i = fromIndex; i < toIndex; ++i) { + final long val = arr[i]; + sum += val; + if (val < min) { + min = val; + } + if (val > max) { + max = val; + } + } + + if (inverse) { + // for positive a,b we have that a >= b if and only if 1/a <= 1/b + return new SegmentData( + len, + (double)len / ((double)sum / 1.0E9), + 1.0E9 / median, + 1.0E9 / (double)max, + 1.0E9 / (double)min + ); + } else { + return new SegmentData( + len, + (double)sum / (double)len, + median, + (double)min, + (double)max + ); + } + } + + private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd, + final int percent99BestStart, final int percent99BestEnd, + final int percent95BestStart, final int percent95BestEnd, + final int percent1WorstStart, final int percent1WorstEnd, + final int percent5WorstStart, final int percent5WorstEnd, + final boolean inverse) { + return new SegmentedAverage( + computeSegmentData(data, allStart, allEnd, inverse), + computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse), + computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse), + computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse), + computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse) + ); + } + + private static record TickInformation( + long differenceFromLastTick, + long tickTime, + long tickTimeCPU + ) {} + + // rets null if there is no data + public TickReportData generateTickReport(final TickRegionScheduler.TickTime inProgress, final long endTime) { + if (this.timeData.isEmpty() && inProgress == null) { + return null; + } + + final List allData = new ArrayList<>(this.timeData); + if (inProgress != null) { + allData.add(inProgress); + } + + final long intervalStart = allData.get(0).tickStart(); + final long intervalEnd = allData.get(allData.size() - 1).tickEnd(); + + // to make utilisation accurate, we need to take the total time used over the last interval period - + // this means if a tick start before the measurement interval, but ends within the interval, then we + // only consider the time it spent ticking inside the interval + long totalTimeOverInterval = 0L; + long measureStart = endTime - this.interval; + + for (int i = 0, len = allData.size(); i < len; ++i) { + final TickRegionScheduler.TickTime time = allData.get(i); + if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) { + final long diff = time.tickEnd() - measureStart; + if (diff > 0L) { + totalTimeOverInterval += diff; + } // else: the time is entirely out of interval + } else { + totalTimeOverInterval += time.tickLength(); + } + } + + // we only care about ticks, but because of inbetween tick task execution + // there will be data in allData that isn't ticks. But, that data cannot + // be ignored since it contributes to utilisation. + // So, we will "compact" the data by merging any inbetween tick times + // the next tick. + // If there is no "next tick", then we will create one. + final List collapsedData = new ArrayList<>(); + for (int i = 0, len = allData.size(); i < len; ++i) { + final List toCollapse = new ArrayList<>(); + TickRegionScheduler.TickTime lastTick = null; + for (;i < len; ++i) { + final TickRegionScheduler.TickTime time = allData.get(i); + if (!time.isTickExecution()) { + toCollapse.add(time); + continue; + } + lastTick = time; + break; + } + + if (toCollapse.isEmpty()) { + // nothing to collapse + final TickRegionScheduler.TickTime last = allData.get(i); + collapsedData.add( + new TickInformation( + last.differenceFromLastTick(), + last.tickLength(), + last.supportCPUTime() ? last.tickCpuTime() : 0L + ) + ); + } else { + long totalTickTime = 0L; + long totalCpuTime = 0L; + for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) { + final TickRegionScheduler.TickTime time = toCollapse.get(k); + totalTickTime += time.tickLength(); + totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L; + } + if (i < len) { + // we know there is a tick to collapse into + final TickRegionScheduler.TickTime last = allData.get(i); + collapsedData.add( + new TickInformation( + last.differenceFromLastTick(), + last.tickLength() + totalTickTime, + (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime + ) + ); + } else { + // we do not have a tick to collapse into, so we must make one up + // we will assume that the tick is "starting now" and ongoing + + // compute difference between imaginary tick and last tick + final long differenceBetweenTicks; + if (lastTick != null) { + // we have a last tick, use it + differenceBetweenTicks = lastTick.tickStart(); + } else { + // we don't have a last tick, so we must make one up that makes sense + // if the current interval exceeds the max tick time, then use it + + // Otherwise use the interval length. + // This is how differenceFromLastTick() works on TickTime when there is no previous interval. + differenceBetweenTicks = Math.max( + TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime + ); + } + + collapsedData.add( + new TickInformation( + differenceBetweenTicks, + totalTickTime, + totalCpuTime + ) + ); + } + } + } + + + final int collectedTicks = collapsedData.size(); + final long[] tickStartToStartDifferences = new long[collectedTicks]; + final long[] timePerTickDataRaw = new long[collectedTicks]; + final long[] missingCPUTimeDataRaw = new long[collectedTicks]; + + long totalTimeTicking = 0L; + + int i = 0; + for (final TickInformation time : collapsedData) { + tickStartToStartDifferences[i] = time.differenceFromLastTick(); + final long timePerTick = timePerTickDataRaw[i] = time.tickTime(); + missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU()); + + ++i; + + totalTimeTicking += timePerTick; + } + + Arrays.sort(tickStartToStartDifferences); + Arrays.sort(timePerTickDataRaw); + Arrays.sort(missingCPUTimeDataRaw); + + // Note: computeSegmentData cannot take start == end + final int allStart = 0; + final int allEnd = collectedTicks; + final int percent95BestStart = 0; + final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks); + final int percent99BestStart = 0; + // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end + final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks); + final int percent1WorstStart = (int)(0.99 * collectedTicks); + final int percent1WorstEnd = collectedTicks; + final int percent5WorstStart = (int)(0.95 * collectedTicks); + final int percent5WorstEnd = collectedTicks; + + final SegmentedAverage tpsData = computeSegmentedAverage( + tickStartToStartDifferences, + allStart, allEnd, + percent99BestStart, percent99BestEnd, + percent95BestStart, percent95BestEnd, + percent1WorstStart, percent1WorstEnd, + percent5WorstStart, percent5WorstEnd, + true + ); + + final SegmentedAverage timePerTickData = computeSegmentedAverage( + timePerTickDataRaw, + allStart, allEnd, + percent99BestStart, percent99BestEnd, + percent95BestStart, percent95BestEnd, + percent1WorstStart, percent1WorstEnd, + percent5WorstStart, percent5WorstEnd, + false + ); + + final SegmentedAverage missingCPUTimeData = computeSegmentedAverage( + missingCPUTimeDataRaw, + allStart, allEnd, + percent99BestStart, percent99BestEnd, + percent95BestStart, percent95BestEnd, + percent1WorstStart, percent1WorstEnd, + percent5WorstStart, percent5WorstEnd, + false + ); + + final double utilisation = (double)totalTimeOverInterval / (double)this.interval; + + return new TickReportData( + collectedTicks, + intervalStart, + intervalEnd, + totalTimeTicking, + utilisation, + + tpsData, + timePerTickData, + missingCPUTimeData + ); + } + + public static final record TickReportData( + int collectedTicks, + long collectedTickIntervalStart, + long collectedTickIntervalEnd, + long totalTimeTicking, + double utilisation, + + SegmentedAverage tpsData, + // in ns + SegmentedAverage timePerTickData, + // in ns + SegmentedAverage missingCPUTimeData + ) {} + + public static final record SegmentedAverage( + SegmentData segmentAll, + SegmentData segment99PercentBest, + SegmentData segment95PercentBest, + SegmentData segment5PercentWorst, + SegmentData segment1PercentWorst + ) {} + + public static final record SegmentData( + int count, + double average, + double median, + double least, + double greatest + ) {} +} diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..4471285a4358e51da9912ed791a824527f1a2e8e --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/TickRegionScheduler.java @@ -0,0 +1,564 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import ca.spottedleaf.moonrise.common.util.TickThread; +import com.mojang.logging.LogUtils; +import io.papermc.paper.util.TraceUtil; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; + +public final class TickRegionScheduler { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); + private static final boolean MEASURE_CPU_TIME; + static { + MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported(); + if (MEASURE_CPU_TIME) { + THREAD_MX_BEAN.setThreadCpuTimeEnabled(true); + } else { + LOGGER.warn("TickRegionScheduler CPU time measurement is not available"); + } + } + + public static final int TICK_RATE = 20; + public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns + + private final SchedulerThreadPool scheduler; + + public TickRegionScheduler(final int threads) { + this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() { + private final AtomicInteger idGenerator = new AtomicInteger(); + + @Override + public Thread newThread(final Runnable run) { + final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement()); + ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException); + return ret; + } + }); + } + + public int getTotalThreadCount() { + return this.scheduler.getThreads().length; + } + + private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion region) { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + throw new IllegalStateException("Must be tick thread runner"); + } + if (region != null && tickThreadRunner.currentTickingRegion != null) { + throw new IllegalStateException("Trying to double set ticking region!"); + } + if (region == null && tickThreadRunner.currentTickingRegion == null) { + throw new IllegalStateException("Trying to double unset ticking region!"); + } + tickThreadRunner.currentTickingRegion = region; + if (region != null) { + tickThreadRunner.currentTickingWorldRegionizedData = region.regioniser.world.worldRegionData.get(); + } else { + tickThreadRunner.currentTickingWorldRegionizedData = null; + } + } + + private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + throw new IllegalStateException("Must be tick thread runner"); + } + if (task != null && tickThreadRunner.currentTickingTask != null) { + throw new IllegalStateException("Trying to double set ticking task!"); + } + if (task == null && tickThreadRunner.currentTickingTask == null) { + throw new IllegalStateException("Trying to double unset ticking task!"); + } + tickThreadRunner.currentTickingTask = task; + } + + /** + * Returns the current ticking region, or {@code null} if there is no ticking region. + * If this thread is not a TickThread, then returns {@code null}. + */ + public static ThreadedRegionizer.ThreadedRegion getCurrentRegion() { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + return RegionShutdownThread.getRegion(); + } + return tickThreadRunner.currentTickingRegion; + } + + /** + * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region. + * This is a faster alternative to calling the {@link RegionizedData#get()} method. + * If this thread is not a TickThread, then returns {@code null}. + */ + public static RegionizedWorldData getCurrentRegionizedWorldData() { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + return RegionShutdownThread.getWorldData(); + } + return tickThreadRunner.currentTickingWorldRegionizedData; + } + + /** + * Returns the current ticking task, or {@code null} if there is no ticking region. + * If this thread is not a TickThread, then returns {@code null}. + */ + public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() { + final Thread currThread = Thread.currentThread(); + if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { + return null; + } + return tickThreadRunner.currentTickingTask; + } + + /** + * Schedules the given region + * @throws IllegalStateException If the region is already scheduled or is ticking + */ + public void scheduleRegion(final RegionScheduleHandle region) { + region.scheduler = this; + this.scheduler.schedule(region); + } + + /** + * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task + * execution, then it will be cancelled after. + */ + public void descheduleRegion(final RegionScheduleHandle region) { + // To avoid acquiring any of the locks the scheduler may be using, we + // simply cancel the next action. + region.markNonSchedulable(); + } + + /** + * Updates the tick start to the farthest into the future of its current scheduled time and the + * provided time. + * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its + * current start time, {@code true} if the next tick start was adjusted. + */ + public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) { + return this.scheduler.updateTickStartToMax(region, newStart); + } + + public boolean halt(final boolean sync, final long maxWaitNS) { + return this.scheduler.halt(sync, maxWaitNS); + } + + void dumpAliveThreadTraces(final String reason) { + for (final Thread thread : this.scheduler.getThreads()) { + if (thread.isAlive()) { + TraceUtil.dumpTraceForThread(thread, reason); + } + } + } + + public void setHasTasks(final RegionScheduleHandle region) { + this.scheduler.notifyTasks(region); + } + + public void init() { + this.scheduler.start(); + } + + private void uncaughtException(final Thread thread, final Throwable thr) { + LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr); + + // prevent further ticks from occurring + // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD + this.scheduler.halt(false, 0L); + + MinecraftServer.getServer().stopServer(); + } + + private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) { + // when a region fails, we need to shut down the server gracefully + + // prevent further ticks from occurring + // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD + this.scheduler.halt(false, 0L); + + final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk(); + final ServerLevel world = handle.region == null ? null : handle.region.world; + + LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " in world '" + (world == null ? "null" : world.getWorld().getName()) + "' failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr); + + MinecraftServer.getServer().stopServer(); + } + + // By using our own thread object, we can use a field for the current region rather than a ThreadLocal. + // This is much faster than a thread local, since the thread local has to use a map lookup. + private static final class TickThreadRunner extends TickThread { + + private ThreadedRegionizer.ThreadedRegion currentTickingRegion; + private RegionizedWorldData currentTickingWorldRegionizedData; + private SchedulerThreadPool.SchedulableTick currentTickingTask; + + public TickThreadRunner(final Runnable run, final String name) { + super(run, name); + } + } + + public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick { + + protected long currentTick; + protected long lastTickStart; + + protected final TickData tickTimes5s; + protected final TickData tickTimes15s; + protected final TickData tickTimes1m; + protected final TickData tickTimes5m; + protected final TickData tickTimes15m; + protected TickTime currentTickData; + protected Thread currentTickingThread; + + public final TickRegions.TickRegionData region; + private final AtomicBoolean cancelled = new AtomicBoolean(); + + protected final Schedule tickSchedule; + + private TickRegionScheduler scheduler; + + public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) { + this.currentTick = 0L; + this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET; + this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L)); + this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L)); + this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L)); + this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L)); + this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L)); + this.region = region; + + this.setScheduledStart(firstStart); + this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS); + } + + /** + * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)} + * so that the tick schedule and scheduled start remain synchronised + */ + protected final void updateScheduledStart(final long to) { + this.setScheduledStart(to); + this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS); + } + + public final void markNonSchedulable() { + this.cancelled.set(true); + } + + public final boolean isMarkedAsNonSchedulable() { + return this.cancelled.get(); + } + + protected abstract boolean tryMarkTicking(); + + protected abstract boolean markNotTicking(); + + protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd); + + protected abstract boolean runRegionTasks(final BooleanSupplier canContinue); + + protected abstract boolean hasIntermediateTasks(); + + @Override + public final boolean hasTasks() { + return this.hasIntermediateTasks(); + } + + @Override + public final Boolean runTasks(final BooleanSupplier canContinue) { + if (this.cancelled.get()) { + return null; + } + + final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + final long tickStart = System.nanoTime(); + + if (!this.tryMarkTicking()) { + if (!this.cancelled.get()) { + throw new IllegalStateException("Scheduled region should be acquirable"); + } + // region was killed + return null; + } + + TickRegionScheduler.setTickTask(this); + if (this.region != null) { + TickRegionScheduler.setTickingRegion(this.region.region); + } + + synchronized (this) { + this.currentTickData = new TickTime( + SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart, + SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, + false + ); + this.currentTickingThread = Thread.currentThread(); + } + + final boolean ret; + try { + ret = this.runRegionTasks(() -> { + return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean(); + }); + } catch (final Throwable thr) { + this.scheduler.regionFailed(this, true, thr); + // don't release region for another tick + return null; + } finally { + final long tickEnd = System.nanoTime(); + final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + + final TickTime time = new TickTime( + SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, + tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false + ); + + this.addTickTime(time); + TickRegionScheduler.setTickTask(null); + if (this.region != null) { + TickRegionScheduler.setTickingRegion(null); + } + } + + return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret); + } + + @Override + public final boolean runTick() { + // Remember, we are supposed use setScheduledStart if we return true here, otherwise + // the scheduler will try to schedule for the same time. + if (this.cancelled.get()) { + return false; + } + + final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + final long tickStart = System.nanoTime(); + + // use max(), don't assume that tickStart >= scheduledStart + final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart)); + + if (!this.tryMarkTicking()) { + if (!this.cancelled.get()) { + throw new IllegalStateException("Scheduled region should be acquirable"); + } + // region was killed + return false; + } + if (this.cancelled.get()) { + this.markNotTicking(); + // region should be killed + return false; + } + + TickRegionScheduler.setTickTask(this); + if (this.region != null) { + TickRegionScheduler.setTickingRegion(this.region.region); + } + this.incrementTickCount(); + final long lastTickStart = this.lastTickStart; + this.lastTickStart = tickStart; + + final long scheduledStart = this.getScheduledStart(); + final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS; + + synchronized (this) { + this.currentTickData = new TickTime( + lastTickStart, scheduledStart, tickStart, cpuStart, + SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, + true + ); + this.currentTickingThread = Thread.currentThread(); + } + + try { + // next start isn't updated until the end of this tick + this.tickRegion(tickCount, tickStart, scheduledEnd); + } catch (final Throwable thr) { + this.scheduler.regionFailed(this, false, thr); + // regionFailed will schedule a shutdown, so we should avoid letting this region tick further + return false; + } finally { + final long tickEnd = System.nanoTime(); + final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; + + // in order to ensure all regions get their chance at scheduling, we have to ensure that regions + // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest + // of the current time and "ideal" next tick start. + this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS); + this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS))); + + final TickTime time = new TickTime( + lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true + ); + + this.addTickTime(time); + TickRegionScheduler.setTickTask(null); + if (this.region != null) { + TickRegionScheduler.setTickingRegion(null); + } + } + + // Only AFTER updating the tickStart + return this.markNotTicking() && !this.cancelled.get(); + } + + /** + * Only safe to call if this tick data matches the current ticking region. + */ + protected void addTickTime(final TickTime time) { + synchronized (this) { + this.currentTickData = null; + this.currentTickingThread = null; + this.tickTimes5s.addDataFrom(time); + this.tickTimes15s.addDataFrom(time); + this.tickTimes1m.addDataFrom(time); + this.tickTimes5m.addDataFrom(time); + this.tickTimes15m.addDataFrom(time); + } + } + + private TickTime adjustCurrentTickData(final long tickEnd) { + final TickTime currentTickData = this.currentTickData; + if (currentTickData == null) { + return null; + } + + final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L; + + return new TickTime( + currentTickData.previousTickStart(), currentTickData.scheduledTickStart(), + currentTickData.tickStart(), currentTickData.tickStartCPU(), + tickEnd, cpuEnd, + MEASURE_CPU_TIME, currentTickData.isTickExecution() + ); + } + + public final TickData.TickReportData getTickReport5s(final long currTime) { + synchronized (this) { + return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + public final TickData.TickReportData getTickReport15s(final long currTime) { + synchronized (this) { + return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + public final TickData.TickReportData getTickReport1m(final long currTime) { + synchronized (this) { + return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + public final TickData.TickReportData getTickReport5m(final long currTime) { + synchronized (this) { + return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + public final TickData.TickReportData getTickReport15m(final long currTime) { + synchronized (this) { + return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); + } + } + + /** + * Only safe to call if this tick data matches the current ticking region. + */ + private void incrementTickCount() { + ++this.currentTick; + } + + /** + * Only safe to call if this tick data matches the current ticking region. + */ + public final long getCurrentTick() { + return this.currentTick; + } + + protected final void setCurrentTick(final long value) { + this.currentTick = value; + } + } + + // All time units are in nanoseconds. + public static final record TickTime( + long previousTickStart, + long scheduledTickStart, + long tickStart, + long tickStartCPU, + long tickEnd, + long tickEndCPU, + boolean supportCPUTime, + boolean isTickExecution + ) { + /** + * The difference between the start tick time and the scheduled start tick time. This value is + * < 0 if the tick started before the scheduled tick time. + * Only valid when {@link #isTickExecution()} is {@code true}. + */ + public final long startOvershoot() { + return this.tickStart - this.scheduledTickStart; + } + + /** + * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong). + */ + public final long tickLength() { + return this.tickEnd - this.tickStart; + } + + /** + * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength, + * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE + * if CPU time measurement is not supported. + */ + public final long tickCpuTime() { + if (!this.supportCPUTime()) { + return Long.MIN_VALUE; + } + return this.tickEndCPU - this.tickStartCPU; + } + + /** + * The difference in time from the start of the last tick to the start of the current tick. If there is no + * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength). + * Only valid when {@link #isTickExecution()} is {@code true}. + */ + public final long differenceFromLastTick() { + if (this.hasLastTick()) { + return this.tickStart - this.previousTickStart; + } + return Math.max(TIME_BETWEEN_TICKS, this.tickLength()); + } + + /** + * Returns whether there was a tick that occurred before this one. + * Only valid when {@link #isTickExecution()} is {@code true}. + */ + public boolean hasLastTick() { + return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET; + } + + /* + * Remember, this is the expected behavior of the following: + * + * MSPT: Time per tick. This does not include overshoot time, just the tickLength(). + * + * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick). + */ + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java index 8424cf9d4617b4732d44cc460d25b04481068989..df15b1139e71dfe10b8f24ec6d235b99f6d5006a 100644 --- a/src/main/java/io/papermc/paper/threadedregions/TickRegions.java +++ b/src/main/java/io/papermc/paper/threadedregions/TickRegions.java @@ -1,10 +1,410 @@ package io.papermc.paper.threadedregions; -// placeholder class for Folia -public class TickRegions { +import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; +import ca.spottedleaf.concurrentutil.util.TimeUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import com.mojang.logging.LogUtils; +import io.papermc.paper.configuration.GlobalConfiguration; +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.slf4j.Logger; +import java.util.Iterator; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BooleanSupplier; + +public final class TickRegions implements ThreadedRegionizer.RegionCallbacks { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static int regionShift = 31; public static int getRegionChunkShift() { - return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT; + return regionShift; + } + + private static boolean initialised; + private static TickRegionScheduler scheduler; + + public static TickRegionScheduler getScheduler() { + return scheduler; + } + + public static void init(final GlobalConfiguration.ThreadedRegions config) { + if (initialised) { + return; + } + initialised = true; + int gridExponent = config.gridExponent; + gridExponent = Math.max(0, gridExponent); + gridExponent = Math.min(31, gridExponent); + regionShift = gridExponent; + + int tickThreads; + if (config.threads <= 0) { + tickThreads = Runtime.getRuntime().availableProcessors() / 2; + if (tickThreads <= 4) { + tickThreads = 1; + } else { + tickThreads = tickThreads / 4; + } + } else { + tickThreads = config.threads; + } + + scheduler = new TickRegionScheduler(tickThreads); + LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads"); + } + + @Override + public TickRegionData createNewData(final ThreadedRegionizer.ThreadedRegion region) { + return new TickRegionData(region); + } + + @Override + public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) { + return null; + } + + @Override + public void onRegionCreate(final ThreadedRegionizer.ThreadedRegion region) { + final TickRegionData data = region.getData(); + // post-region merge/split regioninfo update + data.getRegionStats().updateFrom(data.getOrCreateRegionizedData(data.world.worldRegionData)); + } + + @Override + public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion region) { + // nothing for now + } + + @Override + public void onRegionActive(final ThreadedRegionizer.ThreadedRegion region) { + final TickRegionData data = region.getData(); + + data.tickHandle.checkInitialSchedule(); + scheduler.scheduleRegion(data.tickHandle); + } + + @Override + public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion region) { + final TickRegionData data = region.getData(); + + scheduler.descheduleRegion(data.tickHandle); + // old handle cannot be scheduled anymore, copy to a new handle + data.tickHandle = data.tickHandle.copy(); + } + + @Override + public void preMerge(final ThreadedRegionizer.ThreadedRegion from, + final ThreadedRegionizer.ThreadedRegion into) { + + } + + @Override + public void preSplit(final ThreadedRegionizer.ThreadedRegion from, + final java.util.List> into) { + + } + + public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {} + + public static final class RegionStats { + + private final AtomicInteger entityCount = new AtomicInteger(); + private final AtomicInteger playerCount = new AtomicInteger(); + private final AtomicInteger chunkCount = new AtomicInteger(); + + public int getEntityCount() { + return this.entityCount.get(); + } + + public int getPlayerCount() { + return this.playerCount.get(); + } + + public int getChunkCount() { + return this.chunkCount.get(); + } + + void updateFrom(final RegionizedWorldData data) { + this.entityCount.setRelease(data == null ? 0 : data.getEntityCount()); + this.playerCount.setRelease(data == null ? 0 : data.getPlayerCount()); + this.chunkCount.setRelease(data == null ? 0 : data.getChunkCount()); + } + + static void updateCurrentRegion() { + TickRegionScheduler.getCurrentRegion().getData().getRegionStats().updateFrom(TickRegionScheduler.getCurrentRegionizedWorldData()); + } + } + + public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData { + + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + /** Never 0L, since 0L is reserved for global region. */ + public final long id = ID_GENERATOR.incrementAndGet(); + + public final ThreadedRegionizer.ThreadedRegion region; + public final ServerLevel world; + + // generic regionised data + private final Reference2ReferenceOpenHashMap, Object> regionizedData = new Reference2ReferenceOpenHashMap<>(); + + // tick data + private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET); + + // queue data + private final RegionizedTaskQueue.RegionTaskQueueData taskQueueData; + + // chunk holder manager data + private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData(); + + // async-safe read-only region data + private final RegionStats regionStats; + + private TickRegionData(final ThreadedRegionizer.ThreadedRegion region) { + this.region = region; + this.world = region.regioniser.world; + this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData); + this.regionStats = new RegionStats(); + } + + public RegionStats getRegionStats() { + return this.regionStats; + } + + public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() { + return this.taskQueueData; + } + + // the value returned can be invalidated at any time, except when the caller + // is ticking this region + public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() { + return this.tickHandle; + } + + public long getCurrentTick() { + return this.tickHandle.getCurrentTick(); + } + + public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() { + return this.holderManagerRegionData; + } + + T getRegionizedData(final RegionizedData regionizedData) { + return (T)this.regionizedData.get(regionizedData); + } + + T getOrCreateRegionizedData(final RegionizedData regionizedData) { + T ret = (T)this.regionizedData.get(regionizedData); + + if (ret != null) { + return ret; + } + + ret = regionizedData.createNewValue(); + this.regionizedData.put(regionizedData, ret); + + return ret; + } + + @Override + public void split(final ThreadedRegionizer regioniser, + final Long2ReferenceOpenHashMap> into, + final ReferenceOpenHashSet> regions) { + final int shift = regioniser.sectionChunkShift; + + // tick data + // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled), + // and the other regions to split into are not scheduled yet. + for (final ThreadedRegionizer.ThreadedRegion region : regions) { + final TickRegionData data = region.getData(); + data.tickHandle.copyDeadlineAndTickCount(this.tickHandle); + } + + // generic regionised data + for (final Iterator, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); + dataIterator.hasNext();) { + final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next(); + final RegionizedData data = regionDataEntry.getKey(); + final Object from = regionDataEntry.getValue(); + + final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); + + for (final ThreadedRegionizer.ThreadedRegion region : regions) { + dataSet.add(region.getData().getOrCreateRegionizedData(data)); + } + + final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); + + for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); + regionIterator.hasNext();) { + final Long2ReferenceMap.Entry> entry = regionIterator.next(); + final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); + final Object to = region.getData().getOrCreateRegionizedData(data); + + regionToData.put(entry.getLongKey(), to); + } + + ((RegionizedData)data).getCallback().split(from, shift, regionToData, dataSet); + } + + // chunk holder manager data + { + final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); + + for (final ThreadedRegionizer.ThreadedRegion region : regions) { + dataSet.add(region.getData().holderManagerRegionData); + } + + final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); + + for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); + regionIterator.hasNext();) { + final Long2ReferenceMap.Entry> entry = regionIterator.next(); + final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); + final ChunkHolderManager.HolderManagerRegionData to = region.getData().holderManagerRegionData; + + regionToData.put(entry.getLongKey(), to); + } + + this.holderManagerRegionData.split(shift, regionToData, dataSet); + } + + // task queue + this.taskQueueData.split(regioniser, into); + } + + @Override + public void mergeInto(final ThreadedRegionizer.ThreadedRegion into) { + // Note: merge target is always a region being released from ticking + final TickRegionData data = into.getData(); + final long currentTickTo = data.getCurrentTick(); + final long currentTickFrom = this.getCurrentTick(); + + // here we can access tickHandle because the target (into) is the region being released, so it is + // not actually scheduled + // there's not really a great solution to the tick problem, no matter what it'll be messed up + // we will pick the greatest time delay so that tps will not exceed TICK_RATE + data.tickHandle.updateSchedulingToMax(this.tickHandle); + + // generic regionised data + final long fromTickOffset = currentTickTo - currentTickFrom; // see merge jd + for (final Iterator, Object>> iterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); + iterator.hasNext();) { + final Reference2ReferenceMap.Entry, Object> entry = iterator.next(); + final RegionizedData regionizedData = entry.getKey(); + final Object from = entry.getValue(); + final Object to = into.getData().getOrCreateRegionizedData(regionizedData); + + ((RegionizedData)regionizedData).getCallback().merge(from, to, fromTickOffset); + } + + // chunk holder manager data + this.holderManagerRegionData.merge(into.getData().holderManagerRegionData, fromTickOffset); + + // task queue + this.taskQueueData.mergeInto(data.taskQueueData); + } + } + + private static final class ConcreteRegionTickHandle extends TickRegionScheduler.RegionScheduleHandle { + + private final TickRegionData region; + + private ConcreteRegionTickHandle(final TickRegionData region, final long start) { + super(region, start); + this.region = region; + } + + private ConcreteRegionTickHandle copy() { + final ConcreteRegionTickHandle ret = new ConcreteRegionTickHandle(this.region, this.getScheduledStart()); + + ret.currentTick = this.currentTick; + ret.lastTickStart = this.lastTickStart; + ret.tickSchedule.setLastPeriod(this.tickSchedule.getLastPeriod()); + + return ret; + } + + private void updateSchedulingToMax(final ConcreteRegionTickHandle from) { + if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { + return; + } + + if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { + this.updateScheduledStart(from.getScheduledStart()); + return; + } + + this.updateScheduledStart(TimeUtil.getGreatestTime(from.getScheduledStart(), this.getScheduledStart())); + } + + private void copyDeadlineAndTickCount(final ConcreteRegionTickHandle from) { + this.currentTick = from.currentTick; + + if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { + return; + } + + this.tickSchedule.setLastPeriod(from.tickSchedule.getLastPeriod()); + this.setScheduledStart(from.getScheduledStart()); + } + + private void checkInitialSchedule() { + if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { + this.updateScheduledStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); + } + } + + @Override + protected boolean tryMarkTicking() { + return this.region.region.tryMarkTicking(ConcreteRegionTickHandle.this::isMarkedAsNonSchedulable); + } + + @Override + protected boolean markNotTicking() { + return this.region.region.markNotTicking(); + } + + @Override + protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { + MinecraftServer.getServer().tickServer(startTime, scheduledEnd, TimeUnit.MILLISECONDS.toMillis(10L), this.region); + } + + @Override + protected boolean runRegionTasks(final BooleanSupplier canContinue) { + final RegionizedTaskQueue.RegionTaskQueueData queue = this.region.taskQueueData; + + boolean processedChunkTask = false; + + boolean executeChunkTask = true; + boolean executeTickTask = true; + do { + if (executeTickTask) { + executeTickTask = queue.executeTickTask(); + } + if (executeChunkTask) { + processedChunkTask |= (executeChunkTask = queue.executeChunkTask()); + } + } while ((executeChunkTask | executeTickTask) && canContinue.getAsBoolean()); + + if (processedChunkTask) { + // if we processed any chunk tasks, try to process ticket level updates for full status changes + this.region.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); + } + return true; + } + + @Override + protected boolean hasIntermediateTasks() { + return this.region.taskQueueData.hasTasks(); + } } } diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java new file mode 100644 index 0000000000000000000000000000000000000000..3bcb1dc98c61e025874cc9e008faa722581a530c --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandServerHealth.java @@ -0,0 +1,355 @@ +package io.papermc.paper.threadedregions.commands; + +import io.papermc.paper.threadedregions.RegionizedServer; +import io.papermc.paper.threadedregions.RegionizedWorldData; +import io.papermc.paper.threadedregions.ThreadedRegionizer; +import io.papermc.paper.threadedregions.TickData; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import io.papermc.paper.threadedregions.TickRegions; +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +public final class CommandServerHealth extends Command { + + private static final ThreadLocal TWO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0.00"); + }); + private static final ThreadLocal ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0.0"); + }); + private static final ThreadLocal NO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0"); + }); + + private static final TextColor HEADER = TextColor.color(79, 164, 240); + private static final TextColor PRIMARY = TextColor.color(48, 145, 237); + private static final TextColor SECONDARY = TextColor.color(104, 177, 240); + private static final TextColor INFORMATION = TextColor.color(145, 198, 243); + private static final TextColor LIST = TextColor.color(33, 97, 188); + + public CommandServerHealth() { + super("tps"); + this.setUsage("/ [server/region] [lowest regions to display]"); + this.setDescription("Reports information about server health."); + this.setPermission("bukkit.command.tps"); + } + + private static Component formatRegionInfo(final String prefix, final double util, final double mspt, final double tps, + final boolean newline) { + return Component.text() + .append(Component.text(prefix, PRIMARY, TextDecoration.BOLD)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) + .append(Component.text("% util at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) + .append(Component.text(" MSPT at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) + .append(Component.text(" TPS" + (newline ? "\n" : ""), PRIMARY)) + .build(); + } + + private static Component formatRegionStats(final TickRegions.RegionStats stats, final boolean newline) { + return Component.text() + .append(Component.text("Chunks: ", PRIMARY)) + .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getChunkCount()), INFORMATION)) + .append(Component.text(" Players: ", PRIMARY)) + .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getPlayerCount()), INFORMATION)) + .append(Component.text(" Entities: ", PRIMARY)) + .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getEntityCount()) + (newline ? "\n" : ""), INFORMATION)) + .build(); + } + + private static boolean executeRegion(final CommandSender sender, final String commandLabel, final String[] args) { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); + if (region == null) { + sender.sendMessage(Component.text("You are not in a region currently", NamedTextColor.RED)); + return true; + } + + final long currTime = System.nanoTime(); + + final TickData.TickReportData report15s = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); + final TickData.TickReportData report1m = region.getData().getRegionSchedulingHandle().getTickReport1m(currTime); + + final ServerLevel world = region.regioniser.world; + final ChunkPos chunkCenter = region.getCenterChunk(); + final int centerBlockX = ((chunkCenter.x << 4) | 7); + final int centerBlockZ = ((chunkCenter.z << 4) | 7); + + final double util15s = report15s.utilisation(); + final double tps15s = report15s.tpsData().segmentAll().average(); + final double mspt15s = report15s.timePerTickData().segmentAll().average() / 1.0E6; + + final double util1m = report1m.utilisation(); + final double tps1m = report1m.tpsData().segmentAll().average(); + final double mspt1m = report1m.timePerTickData().segmentAll().average() / 1.0E6; + + final int yLoc = 80; + final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; + + final Component line = Component.text() + .append(Component.text("Region around block ", PRIMARY)) + .append(Component.text(location, INFORMATION)) + .append(Component.text(":\n", PRIMARY)) + + .append( + formatRegionInfo("15s: ", util15s, mspt15s, tps15s, true) + ) + .append( + formatRegionInfo("1m: ", util1m, mspt1m, tps1m, true) + ) + .append( + formatRegionStats(region.getData().getRegionStats(), false) + ) + + .build(); + + sender.sendMessage(line); + + return true; + } + + private static boolean executeServer(final CommandSender sender, final String commandLabel, final String[] args) { + final int lowestRegionsCount; + if (args.length < 2) { + lowestRegionsCount = 3; + } else { + try { + lowestRegionsCount = Integer.parseInt(args[1]); + } catch (final NumberFormatException ex) { + sender.sendMessage(Component.text("Highest utilisation count '" + args[1] + "' must be an integer", NamedTextColor.RED)); + return true; + } + } + + final List> regions = + new ArrayList<>(); + + for (final World bukkitWorld : Bukkit.getWorlds()) { + final ServerLevel world = ((CraftWorld)bukkitWorld).getHandle(); + world.regioniser.computeForAllRegions(regions::add); + } + + final double minTps; + final double medianTps; + final double maxTps; + double totalUtil = 0.0; + + final DoubleArrayList tpsByRegion = new DoubleArrayList(); + final List reportsByRegion = new ArrayList<>(); + final int maxThreadCount = TickRegions.getScheduler().getTotalThreadCount(); + + final long currTime = System.nanoTime(); + final TickData.TickReportData globalTickReport = RegionizedServer.getGlobalTickData().getTickReport15s(currTime); + + for (final ThreadedRegionizer.ThreadedRegion region : regions) { + final TickData.TickReportData report = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); + tpsByRegion.add(report == null ? 20.0 : report.tpsData().segmentAll().average()); + reportsByRegion.add(report); + totalUtil += (report == null ? 0.0 : report.utilisation()); + } + + totalUtil += globalTickReport.utilisation(); + + tpsByRegion.sort(null); + if (!tpsByRegion.isEmpty()) { + minTps = tpsByRegion.getDouble(0); + maxTps = tpsByRegion.getDouble(tpsByRegion.size() - 1); + + final int middle = tpsByRegion.size() >> 1; + if ((tpsByRegion.size() & 1) == 0) { + // even, average the two middle points + medianTps = (tpsByRegion.getDouble(middle - 1) + tpsByRegion.getDouble(middle)) / 2.0; + } else { + // odd, can just grab middle + medianTps = tpsByRegion.getDouble(middle); + } + } else { + // no regions = green + minTps = medianTps = maxTps = 20.0; + } + + final List, TickData.TickReportData>> + regionsBelowThreshold = new ArrayList<>(); + + for (int i = 0, len = regions.size(); i < len; ++i) { + final TickData.TickReportData report = reportsByRegion.get(i); + + regionsBelowThreshold.add(new ObjectObjectImmutablePair<>(regions.get(i), report)); + } + + regionsBelowThreshold.sort((p1, p2) -> { + final TickData.TickReportData report1 = p1.right(); + final TickData.TickReportData report2 = p2.right(); + final double util1 = report1 == null ? 0.0 : report1.utilisation(); + final double util2 = report2 == null ? 0.0 : report2.utilisation(); + + // we want the largest first + return Double.compare(util2, util1); + }); + + final TextComponent.Builder lowestRegionsBuilder = Component.text(); + + if (sender instanceof Player) { + lowestRegionsBuilder.append(Component.text(" Click to teleport\n", SECONDARY)); + } + for (int i = 0, len = Math.min(lowestRegionsCount, regionsBelowThreshold.size()); i < len; ++i) { + final ObjectObjectImmutablePair, TickData.TickReportData> + pair = regionsBelowThreshold.get(i); + + final TickData.TickReportData report = pair.right(); + final ThreadedRegionizer.ThreadedRegion region = + pair.left(); + + if (report == null) { + // skip regions with no data + continue; + } + + final ServerLevel world = region.regioniser.world; + final ChunkPos chunkCenter = region.getCenterChunk(); + if (chunkCenter == null) { + // region does not exist anymore + continue; + } + final int centerBlockX = ((chunkCenter.x << 4) | 7); + final int centerBlockZ = ((chunkCenter.z << 4) | 7); + final double util = report.utilisation(); + final double tps = report.tpsData().segmentAll().average(); + final double mspt = report.timePerTickData().segmentAll().average() / 1.0E6; + + final int yLoc = 80; + final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; + final Component line = Component.text() + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Region around block ", PRIMARY)) + .append(Component.text(location, INFORMATION)) + .append(Component.text(":\n", PRIMARY)) + + .append(Component.text(" ", PRIMARY)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) + .append(Component.text("% util at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) + .append(Component.text(" MSPT at ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) + .append(Component.text(" TPS\n", PRIMARY)) + + .append(Component.text(" ", PRIMARY)) + .append(formatRegionStats(region.getData().getRegionStats(), (i + 1) != len)) + .build() + + .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey().toString() + " run tp " + centerBlockX + ".5 " + yLoc + " " + centerBlockZ + ".5")) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Click to teleport to " + location, SECONDARY))); + + lowestRegionsBuilder.append(line); + } + + sender.sendMessage( + Component.text() + .append(Component.text("Server Health Report\n", HEADER, TextDecoration.BOLD)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Online Players: ", PRIMARY)) + .append(Component.text(Bukkit.getOnlinePlayers().size() + "\n", INFORMATION)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Total regions: ", PRIMARY)) + .append(Component.text(regions.size() + "\n", INFORMATION)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Utilisation: ", PRIMARY)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(totalUtil * 100.0), CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount))) + .append(Component.text("% / ", PRIMARY)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(maxThreadCount * 100.0), INFORMATION)) + .append(Component.text("%\n", PRIMARY)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Lowest Region TPS: ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(minTps) + "\n", CommandUtil.getColourForTPS(minTps))) + + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Median Region TPS: ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(medianTps) + "\n", CommandUtil.getColourForTPS(medianTps))) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Highest Region TPS: ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(maxTps) + "\n", CommandUtil.getColourForTPS(maxTps))) + + .append(Component.text("Highest ", HEADER, TextDecoration.BOLD)) + .append(Component.text(Integer.toString(lowestRegionsCount), INFORMATION, TextDecoration.BOLD)) + .append(Component.text(" utilisation regions\n", HEADER, TextDecoration.BOLD)) + + .append(lowestRegionsBuilder.build()) + .build() + ); + + return true; + } + + @Override + public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { + final String type; + if (args.length < 1) { + type = "server"; + } else { + type = args[0]; + } + + switch (type.toLowerCase(Locale.ROOT)) { + case "server": { + return executeServer(sender, commandLabel, args); + } + case "region": { + if (!(sender instanceof Entity)) { + sender.sendMessage(Component.text("Cannot see current region information as console", NamedTextColor.RED)); + return true; + } + return executeRegion(sender, commandLabel, args); + } + default: { + sender.sendMessage(Component.text("Type '" + args[0] + "' must be one of: [server, region]", NamedTextColor.RED)); + return true; + } + } + } + + @Override + public List tabComplete(final CommandSender sender, final String alias, final String[] args) throws IllegalArgumentException { + if (args.length == 0) { + if (sender instanceof Entity) { + return CommandUtil.getSortedList(Arrays.asList("server", "region")); + } else { + return CommandUtil.getSortedList(Arrays.asList("server")); + } + } else if (args.length == 1) { + if (sender instanceof Entity) { + return CommandUtil.getSortedList(Arrays.asList("server", "region"), args[0]); + } else { + return CommandUtil.getSortedList(Arrays.asList("server"), args[0]); + } + } + return new ArrayList<>(); + } +} diff --git a/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java b/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..d016294fc7eafbddf6d2a758e5803498dfa207b8 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/commands/CommandUtil.java @@ -0,0 +1,121 @@ +package io.papermc.paper.threadedregions.commands; + +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.util.HSVLike; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public final class CommandUtil { + + public static List getSortedList(final Iterable iterable) { + final List ret = new ArrayList<>(); + for (final String val : iterable) { + ret.add(val); + } + + ret.sort(String.CASE_INSENSITIVE_ORDER); + + return ret; + } + + public static List getSortedList(final Iterable iterable, final String prefix) { + final List ret = new ArrayList<>(); + for (final String val : iterable) { + if (val.regionMatches(0, prefix, 0, prefix.length())) { + ret.add(val); + } + } + + ret.sort(String.CASE_INSENSITIVE_ORDER); + + return ret; + } + + public static List getSortedList(final Iterable iterable, final Function transform) { + final List ret = new ArrayList<>(); + for (final T val : iterable) { + final String transformed = transform.apply(val); + if (transformed != null) { + ret.add(transformed); + } + } + + ret.sort(String.CASE_INSENSITIVE_ORDER); + + return ret; + } + + public static List getSortedList(final Iterable iterable, final Function transform, final String prefix) { + final List ret = new ArrayList<>(); + for (final T val : iterable) { + final String string = transform.apply(val); + if (string != null && string.regionMatches(0, prefix, 0, prefix.length())) { + ret.add(string); + } + } + + ret.sort(String.CASE_INSENSITIVE_ORDER); + + return ret; + } + + public static TextColor getColourForTPS(final double tps) { + final double difference = Math.min(Math.abs(20.0 - tps), 20.0); + final double coordinate; + if (difference <= 2.0) { + // >= 18 tps + coordinate = 70.0 + ((140.0 - 70.0)/(0.0 - 2.0)) * (difference - 2.0); + } else if (difference <= 5.0) { + // >= 15 tps + coordinate = 30.0 + ((70.0 - 30.0)/(2.0 - 5.0)) * (difference - 5.0); + } else if (difference <= 10.0) { + // >= 10 tps + coordinate = 10.0 + ((30.0 - 10.0)/(5.0 - 10.0)) * (difference - 10.0); + } else { + // >= 0.0 tps + coordinate = 0.0 + ((10.0 - 0.0)/(10.0 - 20.0)) * (difference - 20.0); + } + + return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); + } + + public static TextColor getColourForMSPT(final double mspt) { + final double clamped = Math.min(Math.abs(mspt), 50.0); + final double coordinate; + if (clamped <= 15.0) { + coordinate = 130.0 + ((140.0 - 130.0)/(0.0 - 15.0)) * (clamped - 15.0); + } else if (clamped <= 25.0) { + coordinate = 90.0 + ((130.0 - 90.0)/(15.0 - 25.0)) * (clamped - 25.0); + } else if (clamped <= 35.0) { + coordinate = 30.0 + ((90.0 - 30.0)/(25.0 - 35.0)) * (clamped - 35.0); + } else if (clamped <= 40.0) { + coordinate = 15.0 + ((30.0 - 15.0)/(35.0 - 40.0)) * (clamped - 40.0); + } else { + coordinate = 0.0 + ((15.0 - 0.0)/(40.0 - 50.0)) * (clamped - 50.0); + } + + return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); + } + + public static TextColor getUtilisationColourRegion(final double util) { + // TODO anything better? + // assume 20TPS + return getColourForMSPT(util * 50.0); + } + + public static ServerPlayer getPlayer(final String name) { + for (final ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) { + if (player.getGameProfile().getName().equalsIgnoreCase(name)) { + return player; + } + } + + return null; + } + + private CommandUtil() {} +} diff --git a/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..85d3965a67cfb59790c664baa7840b50436a5e28 --- /dev/null +++ b/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java @@ -0,0 +1,424 @@ +package io.papermc.paper.threadedregions.scheduler; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; +import io.papermc.paper.threadedregions.RegionizedData; +import io.papermc.paper.threadedregions.RegionizedServer; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.Unit; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Level; + +public final class FoliaRegionScheduler implements RegionScheduler { + + private static Runnable wrap(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { + return () -> { + try { + run.run(); + } catch (final Throwable throwable) { + plugin.getLogger().log(Level.WARNING, "Location task for " + plugin.getDescription().getFullName() + + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); + } + }; + } + + private static final RegionizedData SCHEDULER_DATA = new RegionizedData<>(null, Scheduler::new, Scheduler.REGIONISER_CALLBACK); + + private static void scheduleInternalOnRegion(final LocationScheduledTask task, final long delay) { + SCHEDULER_DATA.get().queueTask(task, delay); + } + + private static void scheduleInternalOffRegion(final LocationScheduledTask task, final long delay) { + final World world = task.world; + if (world == null) { + // cancelled + return; + } + + RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + ((CraftWorld) world).getHandle(), task.chunkX, task.chunkZ, () -> { + scheduleInternalOnRegion(task, delay); + } + ); + } + + @Override + public void execute(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(world, "World may not be null"); + Validate.notNull(run, "Runnable may not be null"); + + RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + ((CraftWorld) world).getHandle(), chunkX, chunkZ, wrap(plugin, world, chunkX, chunkZ, run) + ); + } + + @Override + public ScheduledTask run(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Consumer task) { + return this.runDelayed(plugin, world, chunkX, chunkZ, task, 1); + } + + @Override + public ScheduledTask runDelayed(final Plugin plugin, final World world, final int chunkX, final int chunkZ, + final Consumer task, final long delayTicks) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(world, "World may not be null"); + Validate.notNull(task, "Task may not be null"); + if (delayTicks <= 0) { + throw new IllegalArgumentException("Delay ticks may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, -1, task); + + if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { + scheduleInternalOnRegion(ret, delayTicks); + } else { + scheduleInternalOffRegion(ret, delayTicks); + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public ScheduledTask runAtFixedRate(final Plugin plugin, final World world, final int chunkX, final int chunkZ, + final Consumer task, final long initialDelayTicks, final long periodTicks) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(world, "World may not be null"); + Validate.notNull(task, "Task may not be null"); + if (initialDelayTicks <= 0) { + throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); + } + if (periodTicks <= 0) { + throw new IllegalArgumentException("Period ticks may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, periodTicks, task); + + if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { + scheduleInternalOnRegion(ret, initialDelayTicks); + } else { + scheduleInternalOffRegion(ret, initialDelayTicks); + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + public void tick() { + SCHEDULER_DATA.get().tick(); + } + + private static final class Scheduler { + private static final RegionizedData.RegioniserCallback REGIONISER_CALLBACK = new RegionizedData.RegioniserCallback<>() { + @Override + public void merge(final Scheduler from, final Scheduler into, final long fromTickOffset) { + for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); + sectionIterator.hasNext();) { + final Long2ObjectMap.Entry>> entry = sectionIterator.next(); + final long sectionKey = entry.getLongKey(); + final Long2ObjectOpenHashMap> section = entry.getValue(); + + final Long2ObjectOpenHashMap> sectionAdjusted = new Long2ObjectOpenHashMap<>(section.size()); + + for (final Iterator>> iterator = section.long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry> e = iterator.next(); + final long newTick = e.getLongKey() + fromTickOffset; + final List tasks = e.getValue(); + + sectionAdjusted.put(newTick, tasks); + } + + into.tasksByDeadlineBySection.put(sectionKey, sectionAdjusted); + } + } + + @Override + public void split(final Scheduler from, final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, + final ReferenceOpenHashSet dataSet) { + for (final Scheduler into : dataSet) { + into.tickCount = from.tickCount; + } + + for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); + sectionIterator.hasNext();) { + final Long2ObjectMap.Entry>> entry = sectionIterator.next(); + final long sectionKey = entry.getLongKey(); + final Long2ObjectOpenHashMap> section = entry.getValue(); + + final Scheduler into = regionToData.get(sectionKey); + + into.tasksByDeadlineBySection.put(sectionKey, section); + } + } + }; + + private long tickCount = 0L; + // map of region section -> map of deadline -> list of tasks + private final Long2ObjectOpenHashMap>> tasksByDeadlineBySection = new Long2ObjectOpenHashMap<>(); + + private void addTicket(final int sectionX, final int sectionZ) { + final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; + final int shift = world.moonrise$getRegionChunkShift(); + final int chunkX = sectionX << shift; + final int chunkZ = sectionZ << shift; + + world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( + TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE + ); + } + + private void removeTicket(final long sectionKey) { + final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; + final int shift = world.moonrise$getRegionChunkShift(); + final int chunkX = CoordinateUtils.getChunkX(sectionKey) << shift; + final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << shift; + + world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( + TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE + ); + } + + private void queueTask(final LocationScheduledTask task, final long delay) { + // note: must be on the thread that owns this scheduler + // note: delay > 0 + + final World world = task.world; + if (world == null) { + // cancelled + return; + } + + final int shift = ((CraftWorld)world).getHandle().moonrise$getRegionChunkShift(); + final int sectionX = task.chunkX >> shift; + final int sectionZ = task.chunkZ >> shift; + + final Long2ObjectOpenHashMap> section = + this.tasksByDeadlineBySection.computeIfAbsent(CoordinateUtils.getChunkKey(sectionX, sectionZ), (final long keyInMap) -> { + return new Long2ObjectOpenHashMap<>(); + } + ); + + if (section.isEmpty()) { + // need to keep the scheduler loaded for this location in order for tick() to be called... + this.addTicket(sectionX, sectionZ); + } + + section.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> { + return new ArrayList<>(); + }).add(task); + } + + public void tick() { + ++this.tickCount; + + final List run = new ArrayList<>(); + + for (final Iterator>>> sectionIterator = this.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); + sectionIterator.hasNext();) { + final Long2ObjectMap.Entry>> entry = sectionIterator.next(); + final long sectionKey = entry.getLongKey(); + final Long2ObjectOpenHashMap> section = entry.getValue(); + + final List tasks = section.remove(this.tickCount); + + if (tasks == null) { + continue; + } + + run.addAll(tasks); + + if (section.isEmpty()) { + this.removeTicket(sectionKey); + sectionIterator.remove(); + } + } + + for (int i = 0, len = run.size(); i < len; ++i) { + run.get(i).run(); + } + } + } + + private static final class LocationScheduledTask implements ScheduledTask, Runnable { + + private static final int STATE_IDLE = 0; + private static final int STATE_EXECUTING = 1; + private static final int STATE_EXECUTING_CANCELLED = 2; + private static final int STATE_FINISHED = 3; + private static final int STATE_CANCELLED = 4; + + private final Plugin plugin; + private final int chunkX; + private final int chunkZ; + private final long repeatDelay; // in ticks + private World world; + private Consumer run; + + private volatile int state; + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(LocationScheduledTask.class, "state", int.class); + + private LocationScheduledTask(final Plugin plugin, final World world, final int chunkX, final int chunkZ, + final long repeatDelay, final Consumer run) { + this.plugin = plugin; + this.world = world; + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.repeatDelay = repeatDelay; + this.run = run; + } + + private final int getStateVolatile() { + return (int)STATE_HANDLE.get(this); + } + + private final int compareAndExchangeStateVolatile(final int expect, final int update) { + return (int)STATE_HANDLE.compareAndExchange(this, expect, update); + } + + private final void setStateVolatile(final int value) { + STATE_HANDLE.setVolatile(this, value); + } + + @Override + public void run() { + if (!this.plugin.isEnabled()) { + // don't execute if the plugin is disabled + return; + } + + final boolean repeating = this.isRepeatingTask(); + if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { + // cancelled + return; + } + + try { + this.run.accept(this); + } catch (final Throwable throwable) { + this.plugin.getLogger().log(Level.WARNING, "Location task for " + this.plugin.getDescription().getFullName() + + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); + } finally { + boolean reschedule = false; + if (!repeating) { + this.setStateVolatile(STATE_FINISHED); + } else if (!this.plugin.isEnabled()) { + this.setStateVolatile(STATE_CANCELLED); + } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { + reschedule = true; + } // else: cancelled repeating task + + if (!reschedule) { + this.run = null; + this.world = null; + } else { + FoliaRegionScheduler.scheduleInternalOnRegion(this, this.repeatDelay); + } + } + } + + @Override + public Plugin getOwningPlugin() { + return this.plugin; + } + + @Override + public boolean isRepeatingTask() { + return this.repeatDelay > 0; + } + + @Override + public CancelledState cancel() { + for (int curr = this.getStateVolatile();;) { + switch (curr) { + case STATE_IDLE: { + if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { + this.state = STATE_CANCELLED; + this.run = null; + this.world = null; + return CancelledState.CANCELLED_BY_CALLER; + } + // try again + continue; + } + case STATE_EXECUTING: { + if (!this.isRepeatingTask()) { + return CancelledState.RUNNING; + } + if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { + return CancelledState.NEXT_RUNS_CANCELLED; + } + // try again + continue; + } + case STATE_EXECUTING_CANCELLED: { + return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; + } + case STATE_FINISHED: { + return CancelledState.ALREADY_EXECUTED; + } + case STATE_CANCELLED: { + return CancelledState.CANCELLED_ALREADY; + } + default: { + throw new IllegalStateException("Unknown state: " + curr); + } + } + } + } + + @Override + public ExecutionState getExecutionState() { + final int state = this.getStateVolatile(); + switch (state) { + case STATE_IDLE: + return ExecutionState.IDLE; + case STATE_EXECUTING: + return ExecutionState.RUNNING; + case STATE_EXECUTING_CANCELLED: + return ExecutionState.CANCELLED_RUNNING; + case STATE_FINISHED: + return ExecutionState.FINISHED; + case STATE_CANCELLED: + return ExecutionState.CANCELLED; + default: { + throw new IllegalStateException("Unknown state: " + state); + } + } + } + } +} diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java index 0449d4619e3a0752dea0981fb149542e23076c52..5931afeccc17733103641c9d84390617fc9f62c0 100644 --- a/src/main/java/io/papermc/paper/util/MCUtil.java +++ b/src/main/java/io/papermc/paper/util/MCUtil.java @@ -90,6 +90,7 @@ public final class MCUtil { */ public static void ensureMain(String reason, Runnable run) { if (!isMainThread()) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading if (reason != null) { MinecraftServer.LOGGER.warn("Asynchronous " + reason + "!", new IllegalStateException()); } @@ -144,6 +145,30 @@ public final class MCUtil { return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ()); } + // Folia start - TODO MERGE INTO MCUTIL + /** + * Converts a NMS World/Vector to Bukkit Location + * @param world + * @param pos + * @return + */ + public static Location toLocation(Level world, Vec3 pos) { + return new Location(world.getWorld(), pos.x(), pos.y(), pos.z()); + } + + /** + * Converts a NMS World/Vector to Bukkit Location + * @param world + * @param pos + * @param yaw + * @param pitch + * @return + */ + public static Location toLocation(Level world, Vec3 pos, float yaw, float pitch) { + return new Location(world.getWorld(), pos.x(), pos.y(), pos.z(), yaw, pitch); + } + // Folia end - TODO MERGE INTO MCUTIL + public static BlockPos toBlockPosition(Location loc) { return new BlockPos(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()); } diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java index 2d344df35d47b4b1ecddf32ccaa4dae41e5f58cb..8263a1134582675e67afd5ee07c6d3d06172c460 100644 --- a/src/main/java/net/minecraft/commands/CommandSourceStack.java +++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java @@ -67,7 +67,7 @@ public class CommandSourceStack implements ExecutionCommandSource { io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);})); // Folia - region threading } protected CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity, boolean silent, CommandResultCallback resultStorer, EntityAnchorArgument.Anchor entityAnchor, CommandSigningContext signedArguments, TaskChainer messageChainTaskQueue) { diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java index 1d1e76de60e40224f5cb81893f9ee50fe987badb..460786301818da923664057c4cc0f5a76fcddd2c 100644 --- a/src/main/java/net/minecraft/commands/Commands.java +++ b/src/main/java/net/minecraft/commands/Commands.java @@ -159,13 +159,13 @@ public class Commands { AdvancementCommands.register(this.dispatcher); AttributeCommand.register(this.dispatcher, commandRegistryAccess); ExecuteCommand.register(this.dispatcher, commandRegistryAccess); - BossBarCommands.register(this.dispatcher, commandRegistryAccess); + //BossBarCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO ClearInventoryCommands.register(this.dispatcher, commandRegistryAccess); - CloneCommands.register(this.dispatcher, commandRegistryAccess); + //CloneCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO DamageCommand.register(this.dispatcher, commandRegistryAccess); - DataCommands.register(this.dispatcher); - DataPackCommand.register(this.dispatcher); - DebugCommand.register(this.dispatcher); + //DataCommands.register(this.dispatcher); // Folia - region threading - TODO + //DataPackCommand.register(this.dispatcher); // Folia - region threading - TODO + //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO DefaultGameModeCommands.register(this.dispatcher); DifficultyCommand.register(this.dispatcher); EffectCommands.register(this.dispatcher, commandRegistryAccess); @@ -175,46 +175,46 @@ public class Commands { FillCommand.register(this.dispatcher, commandRegistryAccess); FillBiomeCommand.register(this.dispatcher, commandRegistryAccess); ForceLoadCommand.register(this.dispatcher); - FunctionCommand.register(this.dispatcher); + //FunctionCommand.register(this.dispatcher); // Folia - region threading - TODO GameModeCommand.register(this.dispatcher); GameRuleCommand.register(this.dispatcher); GiveCommand.register(this.dispatcher, commandRegistryAccess); HelpCommand.register(this.dispatcher); - ItemCommands.register(this.dispatcher, commandRegistryAccess); + //ItemCommands.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later KickCommand.register(this.dispatcher); KillCommand.register(this.dispatcher); ListPlayersCommand.register(this.dispatcher); LocateCommand.register(this.dispatcher, commandRegistryAccess); - LootCommand.register(this.dispatcher, commandRegistryAccess); + //LootCommand.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later MsgCommand.register(this.dispatcher); ParticleCommand.register(this.dispatcher, commandRegistryAccess); PlaceCommand.register(this.dispatcher); PlaySoundCommand.register(this.dispatcher); RandomCommand.register(this.dispatcher); - ReloadCommand.register(this.dispatcher); + //ReloadCommand.register(this.dispatcher); // Folia - region threading RecipeCommand.register(this.dispatcher); - ReturnCommand.register(this.dispatcher); - RideCommand.register(this.dispatcher); + //ReturnCommand.register(this.dispatcher); // Folia - region threading - TODO later + //RideCommand.register(this.dispatcher); // Folia - region threading - TODO later SayCommand.register(this.dispatcher); - ScheduleCommand.register(this.dispatcher); - ScoreboardCommand.register(this.dispatcher, commandRegistryAccess); + //ScheduleCommand.register(this.dispatcher); // Folia - region threading + //ScoreboardCommand.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later SeedCommand.register(this.dispatcher, environment != Commands.CommandSelection.INTEGRATED); SetBlockCommand.register(this.dispatcher, commandRegistryAccess); SetSpawnCommand.register(this.dispatcher); SetWorldSpawnCommand.register(this.dispatcher); - SpectateCommand.register(this.dispatcher); - SpreadPlayersCommand.register(this.dispatcher); + //SpectateCommand.register(this.dispatcher); // Folia - region threading - TODO later + //SpreadPlayersCommand.register(this.dispatcher); // Folia - region threading - TODO later StopSoundCommand.register(this.dispatcher); SummonCommand.register(this.dispatcher, commandRegistryAccess); - TagCommand.register(this.dispatcher); - TeamCommand.register(this.dispatcher, commandRegistryAccess); - TeamMsgCommand.register(this.dispatcher); + //TagCommand.register(this.dispatcher); // Folia - region threading - TODO later + //TeamCommand.register(this.dispatcher, commandRegistryAccess); // Folia - region threading - TODO later + //TeamMsgCommand.register(this.dispatcher); // Folia - region threading - TODO later TeleportCommand.register(this.dispatcher); TellRawCommand.register(this.dispatcher, commandRegistryAccess); - TickCommand.register(this.dispatcher); + //TickCommand.register(this.dispatcher); // Folia - region threading - TODO later TimeCommand.register(this.dispatcher); TitleCommand.register(this.dispatcher, commandRegistryAccess); - TriggerCommand.register(this.dispatcher); + //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later WeatherCommand.register(this.dispatcher); WorldBorderCommand.register(this.dispatcher); if (JvmProfiler.INSTANCE.isAvailable()) { @@ -242,8 +242,8 @@ public class Commands { OpCommand.register(this.dispatcher); PardonCommand.register(this.dispatcher); PardonIpCommand.register(this.dispatcher); - PerfCommand.register(this.dispatcher); - SaveAllCommand.register(this.dispatcher); + //PerfCommand.register(this.dispatcher); // Folia - region threading - TODO later + //SaveAllCommand.register(this.dispatcher); // Folia - region threading - TODO later SaveOffCommand.register(this.dispatcher); SaveOnCommand.register(this.dispatcher); SetPlayerIdleTimeoutCommand.register(this.dispatcher); @@ -507,9 +507,12 @@ public class Commands { } // Paper start - Perf: Async command map building new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper - Brigadier API - net.minecraft.server.MinecraftServer.getServer().execute(() -> { - runSync(player, bukkit, rootcommandnode); - }); + // Folia start - region threading + // ignore if retired + player.getBukkitEntity().taskScheduler.schedule((updatedPlayer) -> { + runSync((ServerPlayer)updatedPlayer, bukkit, rootcommandnode); + }, null, 1L); + // Folia end - region threading } private void runSync(ServerPlayer player, Collection bukkit, RootCommandNode rootcommandnode) { diff --git a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java index 6df0db8b4cdab23494ea34236949ece4989110a3..2fd670d941bd575f99def28732ffee29f37a3182 100644 --- a/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java @@ -63,7 +63,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java index 39c96f5db6e90a470404c6387fa0c1d5531822e5..526a24d30e193a9f6760e9392364360f6f5fe020 100644 --- a/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java @@ -87,7 +87,7 @@ public class DefaultDispenseItemBehavior implements DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), CraftVector.toBukkit(entityitem.getDeltaMovement())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java index 60d3319016beb4f60cbc26dde165f64cf7577602..f2fe380c0cd07afabfaa257d5db04c4064f7a3cb 100644 --- a/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/DispenseItemBehavior.java @@ -115,7 +115,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -174,7 +174,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -229,7 +229,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } @@ -285,7 +285,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorseabstract.getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } @@ -359,7 +359,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityhorsechestedabstract.getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } @@ -431,7 +431,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -496,7 +496,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -534,7 +534,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -596,7 +596,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -614,7 +614,8 @@ public interface DispenseItemBehavior { } } - worldserver.captureTreeGeneration = true; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = worldserver.getCurrentWorldData(); // Folia - region threading + worldData.captureTreeGeneration = true; // Folia - region threading // CraftBukkit end if (!BoneMealItem.growCrop(stack, worldserver, blockposition) && !BoneMealItem.growWaterPlant(stack, worldserver, blockposition, (Direction) null)) { @@ -623,13 +624,13 @@ public interface DispenseItemBehavior { worldserver.levelEvent(1505, blockposition, 15); } // CraftBukkit start - worldserver.captureTreeGeneration = false; - if (worldserver.capturedBlockStates.size() > 0) { - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; + worldData.captureTreeGeneration = false; // Folia - region threading + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading + TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading + SaplingBlock.treeTypeRT.set(null); // Folia - region threading Location location = CraftLocation.toBukkit(blockposition, worldserver.getWorld()); - List blocks = new java.util.ArrayList<>(worldserver.capturedBlockStates.values()); - worldserver.capturedBlockStates.clear(); + List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading StructureGrowEvent structureEvent = null; if (treeType != null) { structureEvent = new StructureGrowEvent(location, treeType, false, null, blocks); @@ -665,7 +666,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockposition.getX() + 0.5D, (double) blockposition.getY(), (double) blockposition.getZ() + 0.5D)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -722,7 +723,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -771,7 +772,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -833,7 +834,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - only single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } @@ -915,7 +916,7 @@ public interface DispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) list.get(0).getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java index e37d2d29f3ba67cfe28abe4847a3dca07121f0be..067f888fdaeced3350c858e2b825a3841b539193 100644 --- a/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java @@ -45,7 +45,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) enumdirection.getStepX(), (double) enumdirection.getStepY(), (double) enumdirection.getStepZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java index 44b79a7c2f8b95a484d1999fa2167ce588f7985b..624df8e8dbf0922c9403d951967312128866515d 100644 --- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java @@ -41,7 +41,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack); // Paper - ignore stack size on damageable items BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java index cb308808906a8cdb127df8284e106e00553473ca..05bc1f010b50e673e9a185d96e338baef7ccad03 100644 --- a/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +++ b/src/main/java/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java @@ -37,7 +37,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { CraftItemStack craftItem = CraftItemStack.asCraftMirror(stack.copyWithCount(1)); // Paper - single item in event BlockDispenseEvent event = new BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockposition.getX(), blockposition.getY(), blockposition.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading pointer.level().getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java index 3e550f8e7cd4f4e16f499a8a2a4b95420270f07a..b0136591a37734493a310332b1f56f1259f913af 100644 --- a/src/main/java/net/minecraft/network/Connection.java +++ b/src/main/java/net/minecraft/network/Connection.java @@ -93,7 +93,7 @@ public class Connection extends SimpleChannelInboundHandler> { private static final ProtocolInfo INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND; private final PacketFlow receiving; private volatile boolean sendLoginDisconnect = true; - private final Queue pendingActions = Queues.newConcurrentLinkedQueue(); // Paper + private final Queue pendingActions = new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); // Paper // Folia - region threading - connection fixes public Channel channel; public SocketAddress address; // Spigot Start @@ -108,7 +108,7 @@ public class Connection extends SimpleChannelInboundHandler> { @Nullable private DisconnectionDetails disconnectionDetails; private boolean encrypted; - private boolean disconnectionHandled; + private final java.util.concurrent.atomic.AtomicBoolean disconnectionHandled = new java.util.concurrent.atomic.AtomicBoolean(false); // Folia - region threading - may be called concurrently during configuration stage private int receivedPackets; private int sentPackets; private float averageReceivedPackets; @@ -163,6 +163,32 @@ public class Connection extends SimpleChannelInboundHandler> { this.receiving = side; } + // Folia start - region threading + private volatile boolean becomeActive; + + public boolean becomeActive() { + return this.becomeActive; + } + + private static record DisconnectReq(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {} + + private final ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue disconnectReqs = + new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); + + /** + * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the + * same thread that could disconnect. + */ + public final void disconnectSafely(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { + this.disconnectReqs.add(new DisconnectReq(disconnectReason, cause)); + // We can't halt packet processing here because a plugin could cancel a kick request. + } + + public final boolean isPlayerConnected() { + return this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl; + } + // Folia end - region threading + public void channelActive(ChannelHandlerContext channelhandlercontext) throws Exception { super.channelActive(channelhandlercontext); this.channel = channelhandlercontext.channel(); @@ -173,7 +199,7 @@ public class Connection extends SimpleChannelInboundHandler> { if (this.delayedDisconnect != null) { this.disconnect(this.delayedDisconnect); } - + this.becomeActive = true; // Folia - region threading } public void channelInactive(ChannelHandlerContext channelhandlercontext) { @@ -451,7 +477,7 @@ public class Connection extends SimpleChannelInboundHandler> { } packet.onPacketDispatch(this.getPlayer()); - if (connected && (InnerUtil.canSendImmediate(this, packet) + if (false && connected && (InnerUtil.canSendImmediate(this, packet) // Folia - region threading - connection fixes || (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty() && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) { this.sendPacket(packet, callbacks, flush); @@ -480,11 +506,12 @@ public class Connection extends SimpleChannelInboundHandler> { } public void runOnceConnected(Consumer task) { - if (this.isConnected()) { + if (false && this.isConnected()) { // Folia - region threading - connection fixes this.flushQueue(); task.accept(this); } else { this.pendingActions.add(new WrappedConsumer(task)); // Paper - Optimize network + this.flushQueue(); // Folia - region threading - connection fixes } } @@ -543,10 +570,11 @@ public class Connection extends SimpleChannelInboundHandler> { } public void flushChannel() { - if (this.isConnected()) { + if (false && this.isConnected()) { // Folia - region threading - connection fixes this.flush(); } else { this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network + this.flushQueue(); // Folia - region threading - connection fixes } } @@ -564,53 +592,61 @@ public class Connection extends SimpleChannelInboundHandler> { // Paper start - Optimize network: Rewrite this to be safer if ran off main thread private boolean flushQueue() { - if (!this.isConnected()) { - return true; - } - if (io.papermc.paper.util.MCUtil.isMainThread()) { - return this.processQueue(); - } else if (this.isPending) { - // Should only happen during login/status stages - synchronized (this.pendingActions) { - return this.processQueue(); - } - } - return false; + return this.processQueue(); // Folia - region threading - connection fixes } + // Folia start - region threading - connection fixes + // allow only one thread to be flushing the queue at once to ensure packets are written in the order they are sent + // into the queue + private final java.util.concurrent.atomic.AtomicBoolean flushingQueue = new java.util.concurrent.atomic.AtomicBoolean(); + + private static boolean canWrite(WrappedConsumer queued) { + return queued != null && (!(queued instanceof PacketSendAction packet) || packet.packet.isReady()); + } + + private boolean canWritePackets() { + return canWrite(this.pendingActions.peek()); + } + // Folia end - region threading - connection fixes + private boolean processQueue() { - if (this.pendingActions.isEmpty()) { + // Folia start - region threading - connection fixes + if (!this.isConnected()) { return true; } - // If we are on main, we are safe here in that nothing else should be processing queue off main anymore - // But if we are not on main due to login/status, the parent is synchronized on packetQueue - final java.util.Iterator iterator = this.pendingActions.iterator(); - while (iterator.hasNext()) { - final WrappedConsumer queued = iterator.next(); // poll -> peek - - // Fix NPE (Spigot bug caused by handleDisconnection()) - if (queued == null) { - return true; - } + while (this.canWritePackets()) { + final boolean set = this.flushingQueue.getAndSet(true); + try { + if (set) { + // we didn't acquire the lock, break + return false; + } - if (queued.isConsumed()) { - continue; - } + ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue queue = + (ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue)this.pendingActions; + WrappedConsumer holder; + for (;;) { + // synchronise so that queue clears appear atomic + synchronized (queue) { + holder = queue.pollIf(Connection::canWrite); + } + if (holder == null) { + break; + } - if (queued instanceof PacketSendAction packetSendAction) { - final Packet packet = packetSendAction.packet; - if (!packet.isReady()) { - return false; + holder.accept(this); } - } - iterator.remove(); - if (queued.tryMarkConsumed()) { - queued.accept(this); + } finally { + if (!set) { + this.flushingQueue.set(false); + } } } + return true; + // Folia end - region threading - connection fixes } // Paper end - Optimize network @@ -619,19 +655,39 @@ public class Connection extends SimpleChannelInboundHandler> { private static int currTick; // Paper - Buffer joins to world public void tick() { this.flushQueue(); - // Paper start - Buffer joins to world - if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) { - Connection.currTick = net.minecraft.server.MinecraftServer.currentTick; - Connection.joinAttemptsThisTick = 0; + // Folia - this is broken + // Folia start - region threading + // handle disconnect requests, but only after flushQueue() + DisconnectReq disconnectReq; + while ((disconnectReq = this.disconnectReqs.poll()) != null) { + PacketListener packetlistener = this.packetListener; + + if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { + loginPacketListener.disconnect(disconnectReq.disconnectReason); + // this doesn't fail, so abort any further attempts + return; + } else if (packetlistener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { + commonPacketListener.disconnect(disconnectReq.disconnectReason, disconnectReq.cause); + // may be cancelled by a plugin, if not cancelled then any further calls do nothing + continue; + } else { + // no idea what packet to send + this.disconnect(disconnectReq.disconnectReason); + this.setReadOnly(); + return; + } } - // Paper end - Buffer joins to world + if (!this.isConnected()) { + // disconnected from above + this.handleDisconnection(); + return; + } + // Folia end - region threading PacketListener packetlistener = this.packetListener; if (packetlistener instanceof TickablePacketListener tickablepacketlistener) { // Paper start - Buffer joins to world - if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) - || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING - || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { + if (true) { // Folia - region threading // Paper start - detailed watchdog information net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); try { @@ -642,7 +698,7 @@ public class Connection extends SimpleChannelInboundHandler> { } // Paper end - Buffer joins to world } - if (!this.isConnected() && !this.disconnectionHandled) { + if (!this.isConnected()) {// Folia - region threading - it's fine to call if it is already handled, as it no longer logs this.handleDisconnection(); } @@ -692,6 +748,7 @@ public class Connection extends SimpleChannelInboundHandler> { this.channel.close(); // We can't wait as this may be called from an event loop. this.disconnectionDetails = disconnectionInfo; } + this.becomeActive = true; // Folia - region threading } @@ -887,10 +944,10 @@ public class Connection extends SimpleChannelInboundHandler> { public void handleDisconnection() { if (this.channel != null && !this.channel.isOpen()) { - if (this.disconnectionHandled) { + if (this.disconnectionHandled.getAndSet(true)) { // Folia - region threading - may be called concurrently during configuration stage // Connection.LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message } else { - this.disconnectionHandled = true; + // Folia - region threading - may be called concurrently during configuration stage - set above PacketListener packetlistener = this.getPacketListener(); PacketListener packetlistener1 = packetlistener != null ? packetlistener : this.disconnectListener; @@ -922,6 +979,22 @@ public class Connection extends SimpleChannelInboundHandler> { } } // Paper end - Add PlayerConnectionCloseEvent + // Folia start - region threading + if (packetlistener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { + net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( + commonPacketListener.getOwner().getName(), + commonPacketListener.getOwner().getId(), this + ); + } else if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { + if (loginPacketListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING.ordinal()) { + net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( + loginPacketListener.authenticatedProfile.getName(), + loginPacketListener.authenticatedProfile.getId(), this + ); + } + } + // Folia end - region threading + // Paper end } } @@ -942,15 +1015,25 @@ public class Connection extends SimpleChannelInboundHandler> { // Paper start - Optimize network public void clearPacketQueue() { final net.minecraft.server.level.ServerPlayer player = getPlayer(); - for (final Consumer queuedAction : this.pendingActions) { - if (queuedAction instanceof PacketSendAction packetSendAction) { - final Packet packet = packetSendAction.packet; - if (packet.hasFinishListener()) { - packet.onPacketDispatchFinish(player, null); + // Folia start - region threading - connection fixes + java.util.List queuedPackets = new java.util.ArrayList<>(); + // synchronise so that flushQueue does not poll values while the queue is being cleared + synchronized (this.pendingActions) { + Connection.WrappedConsumer consumer; + while ((consumer = this.pendingActions.poll()) != null) { + if (consumer instanceof Connection.PacketSendAction packetHolder) { + queuedPackets.add(packetHolder); } } } - this.pendingActions.clear(); + + for (Connection.PacketSendAction queuedPacket : queuedPackets) { + Packet packet = queuedPacket.packet; + if (packet.hasFinishListener()) { + packet.onPacketDispatchFinish(player, null); + } + } + // Folia end - region threading - connection fixes } private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up. diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java index e2c24813f59c2fd075c740ac1842a38f20ed8554..fbc619a132c6ca6b1abab51ac230be29367e9c6e 100644 --- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java +++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java @@ -46,7 +46,7 @@ public class PacketUtils { public static void ensureRunningOnSameThread(Packet packet, T listener, BlockableEventLoop engine) throws RunningOnDifferentThreadException { if (!engine.isSameThread()) { - engine.executeIfPossible(() -> { + Runnable run = () -> { // Folia - region threading packetProcessing.push(listener); // Paper - detailed watchdog information try { // Paper - detailed watchdog information if (listener instanceof ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // CraftBukkit - Don't handle sync packets for kicked players @@ -75,7 +75,23 @@ public class PacketUtils { } // Paper end - detailed watchdog information - }); + }; // Folia start - region threading + // ignore retired state, if removed then we don't want the packet to be handled + if (listener instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) { + gamePacketListener.player.getBukkitEntity().taskScheduler.schedule( + (net.minecraft.server.level.ServerPlayer player) -> { + run.run(); + }, + null, 1L + ); + } else if (listener instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); + } else if (listener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); + } else { + throw new UnsupportedOperationException("Unknown listener: " + listener); + } + // Folia end - region threading throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; } } diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 23ddd26af762c1cd7fb3920669abb96b3213ab37..eda5f0d099f9f8621de8ad7808098abf6f5cb544 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -210,7 +210,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; // Paper - don't store the vanilla dispatcher @@ -317,13 +316,41 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop S spin(Function serverFactory) { AtomicReference atomicreference = new AtomicReference(); Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system @@ -357,46 +384,30 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= MAX_CHUNK_EXEC_TIME) { if (!moreTasks) { - this.lastMidTickExecuteFailure = currTime; + worldData.lastMidTickExecuteFailure = currTime; // Folia - region threading } // note: negative values reduce the time @@ -409,7 +420,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> 4); + world.randomSpawnSelection = new ChunkPos(world.getChunkSource().randomState().sampler().findSpawnPosition()); + for (int currX = -loadRegionRadius; currX <= loadRegionRadius; ++currX) { + for (int currZ = -loadRegionRadius; currZ <= loadRegionRadius; ++currZ) { + ChunkPos pos = new ChunkPos(currX, currZ); + world.chunkSource.addTicketAtLevel( + net.minecraft.server.level.TicketType.UNKNOWN, pos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, pos + ); + } + } + // Folia end - region threading // Paper - Put world into worldlist before initing the world; move up this.getPlayerList().addWorldborderListener(world); @@ -734,6 +759,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 ? Mth.square(ChunkProgressListener.calculateDiameter(i)) : 0; - while (chunkproviderserver.getTickingGenerated() < j) { - // CraftBukkit start - // this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS; - this.executeModerately(); - } + // Folia - region threading // this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS; - this.executeModerately(); + //this.executeModerately(); // Folia - region threading // Iterator iterator = this.levels.values().iterator(); if (true) { @@ -922,7 +945,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { - return false; - } : this::haveTime); + if (true) throw new UnsupportedOperationException(); // Folia - region threading // Paper start - rewrite chunk system final Throwable crash = this.chunkSystemCrash; if (crash != null) { @@ -1489,21 +1566,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; - } - // Paper end - return new TickTask(this.tickCount, runnable); + throw new UnsupportedOperationException(); // Folia - region threading } protected boolean shouldRun(TickTask ticktask) { - return ticktask.getTick() + 3 < this.tickCount || this.haveTime(); + throw new UnsupportedOperationException(); // Folia - region threading } @Override public boolean pollTask() { + if (true) throw new UnsupportedOperationException(); // Folia - region threading boolean flag = this.pollTaskInternal(); this.mayHaveDelayedTasks = flag; @@ -1511,6 +1583,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + return scheduledEnd - System.nanoTime() > targetBuffer; + }; + this.server.spark.tickStart(); // Paper - spark + new com.destroystokyo.paper.event.server.ServerTickStartEvent((int)region.getCurrentTick()).callEvent(); // Paper + // Folia end - region threading co.aikar.timings.TimingsManager.FULL_SERVER_TICK.startTiming(); // Paper - long i = Util.getNanos(); + long i = startTime; // Folia - region threading // Paper start - move oversleep into full server tick + if (region == null) { // Folia - region threading isOversleep = true;MinecraftTimings.serverOversleep.startTiming(); this.managedBlock(() -> { return !this.canOversleep(); }); isOversleep = false;MinecraftTimings.serverOversleep.stopTiming(); + } // Folia - region threading // Paper end - this.server.spark.tickStart(); // Paper - spark - new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events + // Folia - region threading - move up + + // Folia start - region threading + if (region != null) { + region.getTaskQueueData().drainTasks(); + ((io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler)Bukkit.getRegionScheduler()).tick(); + // now run all the entity schedulers + // TODO there has got to be a more efficient variant of this crap + for (Entity entity : region.world.getCurrentWorldData().getLocalEntitiesCopy()) { + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) || entity.isRemoved()) { + continue; + } + org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); + if (bukkit != null) { + bukkit.taskScheduler.executeTick(); + } + } + } + // Folia end - region threading - ++this.tickCount; - this.tickRateManager.tick(); - this.tickChildren(shouldKeepTicking); - if (i - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) { + // Folia - region threading + if (region == null) this.tickRateManager.tick(); // Folia - region threading + this.tickChildren(shouldKeepTicking, region); // Folia - region threading + if (region == null && i - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) { // Folia - region threading this.lastServerStatus = i; this.status = this.buildServerStatus(); } - --this.ticksUntilAutosave; + // Folia - region threading // Paper start - Incremental chunk and player saving int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate; if (playerSaveInterval < 0) { playerSaveInterval = autosavePeriod; } this.profiler.push("save"); - final boolean fullSave = autosavePeriod > 0 && this.tickCount % autosavePeriod == 0; + final boolean fullSave = autosavePeriod > 0 && io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() % autosavePeriod == 0; // Folia - region threading try { this.isSaving = true; if (playerSaveInterval > 0) { this.playerList.saveAll(playerSaveInterval); } - for (ServerLevel level : this.getAllLevels()) { + for (ServerLevel level : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Folia - region threading if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { - level.saveIncrementally(fullSave); + level.saveIncrementally(region == null && fullSave); // Folia - region threading - don't save level.dat } } } finally { @@ -1625,30 +1739,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop list = this.playerList.getPlayers(); + List list = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading int i = this.getMaxPlayers(); if (this.hidesOnlinePlayers()) { @@ -1718,31 +1808,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + public void tickChildren(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - region threading + final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - regionised ticking + if (region == null) this.getPlayerList().getPlayers().forEach((entityplayer) -> { // Folia - region threading entityplayer.connection.suspendFlushing(); }); MinecraftTimings.bukkitSchedulerTimer.startTiming(); // Spigot // Paper - this.server.getScheduler().mainThreadHeartbeat(this.tickCount); // CraftBukkit + // Folia - region threading MinecraftTimings.bukkitSchedulerTimer.stopTiming(); // Spigot // Paper - // Paper start - Folia scheduler API - ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick(); - getAllLevels().forEach(level -> { - for (final Entity entity : level.moonrise$getEntityLookup().getAllCopy()) { // Paper - rewrite chunk system - if (entity.isRemoved()) { - continue; - } - final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); - if (bukkit != null) { - bukkit.taskScheduler.executeTick(); - } - } - }); - // Paper end - Folia scheduler API - io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper + // Folia - region threading - moved to global tick - and moved entity scheduler to tickRegion this.profiler.push("commandFunctions"); MinecraftTimings.commandFunctionsTimer.startTiming(); // Spigot // Paper - this.getFunctions().tick(); + if (region == null) this.getFunctions().tick(); // Folia - region threading - TODO Purge functions MinecraftTimings.commandFunctionsTimer.stopTiming(); // Spigot // Paper this.profiler.popPush("levels"); //Iterator iterator = this.getAllLevels().iterator(); // Paper - Throw exception on world create while being ticked; moved down @@ -1750,7 +1827,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent - worldserver.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent - net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = worldserver.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers - worldserver.updateLagCompensationTick(); // Paper - lag compensation + // Folia - region threading this.profiler.push(() -> { String s = String.valueOf(worldserver); @@ -1803,7 +1877,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().invalidateStatus(); + }); + return; + } + // Folia end - region threading this.lastServerStatus = 0L; } @@ -2255,6 +2339,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + operation.perform(player, selection); + }, null, 1L); + // Folia end - region threading } if (i == 0) { @@ -332,9 +337,12 @@ public class AdvancementCommands { throw ERROR_CRITERION_NOT_FOUND.create(Advancement.name(advancement), criterion); } else { for (ServerPlayer serverPlayer : targets) { - if (operation.performCriterion(serverPlayer, advancement, criterion)) { - i++; - } + // Folia start - region threading + ++i; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + operation.performCriterion(player, advancement, criterion); + }, null, 1L); + // Folia end - region threading } if (i == 0) { diff --git a/src/main/java/net/minecraft/server/commands/AttributeCommand.java b/src/main/java/net/minecraft/server/commands/AttributeCommand.java index 2122fcbbe84a8a797be56e3bdc799f1f33b1617d..c376f0365771f554943f607fd04fbde48c6b23ac 100644 --- a/src/main/java/net/minecraft/server/commands/AttributeCommand.java +++ b/src/main/java/net/minecraft/server/commands/AttributeCommand.java @@ -232,79 +232,151 @@ public class AttributeCommand { } } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int getAttributeValue(CommandSourceStack source, Entity target, Holder attribute, double multiplier) throws CommandSyntaxException { - LivingEntity livingEntity = getEntityWithAttribute(target, attribute); + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading double d = livingEntity.getAttributeValue(attribute); - source.sendSuccess(() -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), target.getName(), d), false); - return (int)(d * multiplier); + source.sendSuccess(() -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), d), false); // Folia - region threading + return; // Folia - region threading + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } private static int getAttributeBase(CommandSourceStack source, Entity target, Holder attribute, double multiplier) throws CommandSyntaxException { - LivingEntity livingEntity = getEntityWithAttribute(target, attribute); + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading double d = livingEntity.getAttributeBaseValue(attribute); source.sendSuccess( - () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), target.getName(), d), false + () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), d), false // Folia - region threading ); - return (int)(d * multiplier); + return; // Folia - region threading + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } private static int getAttributeModifier(CommandSourceStack source, Entity target, Holder attribute, ResourceLocation id, double multiplier) throws CommandSyntaxException { - LivingEntity livingEntity = getEntityWithAttribute(target, attribute); + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + LivingEntity livingEntity = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading AttributeMap attributeMap = livingEntity.getAttributes(); if (!attributeMap.hasModifier(attribute, id)) { - throw ERROR_NO_SUCH_MODIFIER.create(target.getName(), getAttributeDescription(attribute), id); + throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading } else { double d = attributeMap.getModifierValue(attribute, id); source.sendSuccess( () -> Component.translatable( - "commands.attribute.modifier.value.get.success", Component.translationArg(id), getAttributeDescription(attribute), target.getName(), d + "commands.attribute.modifier.value.get.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName(), d // Folia - region threading ), false ); - return (int)(d * multiplier); + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } private static int setAttributeBase(CommandSourceStack source, Entity target, Holder attribute, double value) throws CommandSyntaxException { - getAttributeInstance(target, attribute).setBaseValue(value); + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + getAttributeInstance(nmsEntity, attribute).setBaseValue(value); // Folia - region threading source.sendSuccess( - () -> Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), target.getName(), value), false + () -> Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), nmsEntity.getName(), value), false // Folia - region threading ); - return 1; + return; // Folia - region threading + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } private static int addModifier( CommandSourceStack source, Entity target, Holder attribute, ResourceLocation id, double value, AttributeModifier.Operation operation ) throws CommandSyntaxException { - AttributeInstance attributeInstance = getAttributeInstance(target, attribute); + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); // Folia - region threading AttributeModifier attributeModifier = new AttributeModifier(id, value, operation); if (attributeInstance.hasModifier(id)) { - throw ERROR_MODIFIER_ALREADY_PRESENT.create(target.getName(), getAttributeDescription(attribute), id); + throw ERROR_MODIFIER_ALREADY_PRESENT.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading } else { attributeInstance.addPermanentModifier(attributeModifier); source.sendSuccess( () -> Component.translatable( - "commands.attribute.modifier.add.success", Component.translationArg(id), getAttributeDescription(attribute), target.getName() + "commands.attribute.modifier.add.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName() // Folia - region threading ), false ); - return 1; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } private static int removeModifier(CommandSourceStack source, Entity target, Holder attribute, ResourceLocation id) throws CommandSyntaxException { - AttributeInstance attributeInstance = getAttributeInstance(target, attribute); + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); // Folia - region threading if (attributeInstance.removeModifier(id)) { source.sendSuccess( () -> Component.translatable( - "commands.attribute.modifier.remove.success", Component.translationArg(id), getAttributeDescription(attribute), target.getName() + "commands.attribute.modifier.remove.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName() // Folia - region threading ), false ); - return 1; + return; // Folia - region threading } else { - throw ERROR_NO_SUCH_MODIFIER.create(target.getName(), getAttributeDescription(attribute), id); + throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } private static Component getAttributeDescription(Holder attribute) { diff --git a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java index 4e6171ca870649114d4c7460baad2982173da09e..f8f0d33c663e6b9adac9f7e3eb21b5a6ff860c5d 100644 --- a/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java +++ b/src/main/java/net/minecraft/server/commands/ClearInventoryCommands.java @@ -65,9 +65,14 @@ public class ClearInventoryCommands { int i = 0; for (ServerPlayer serverPlayer : targets) { - i += serverPlayer.getInventory().clearOrCountMatchingItems(item, maxCount, serverPlayer.inventoryMenu.getCraftSlots()); - serverPlayer.containerMenu.broadcastChanges(); - serverPlayer.inventoryMenu.slotsChanged(serverPlayer.getInventory()); + // Folia start - region threading + ++i; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + player.getInventory().clearOrCountMatchingItems(item, maxCount, player.inventoryMenu.getCraftSlots()); + player.containerMenu.broadcastChanges(); + player.inventoryMenu.slotsChanged(player.getInventory()); + }, null, 1L); + // Folia end - region threading } if (i == 0) { diff --git a/src/main/java/net/minecraft/server/commands/DamageCommand.java b/src/main/java/net/minecraft/server/commands/DamageCommand.java index 43121f35a944fd9f8d4c5f1cc53749e79892478e..737c815cb0453b2bfafd59857c443e1cf333c23f 100644 --- a/src/main/java/net/minecraft/server/commands/DamageCommand.java +++ b/src/main/java/net/minecraft/server/commands/DamageCommand.java @@ -104,12 +104,29 @@ public class DamageCommand { ); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int damage(CommandSourceStack source, Entity target, float amount, DamageSource damageSource) throws CommandSyntaxException { - if (target.hurt(damageSource, amount)) { - source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, target.getDisplayName()), true); - return 1; + // Folia start - region threading + target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + if (nmsEntity.hurt(damageSource, amount)) { // Folia - region threading + source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, nmsEntity.getDisplayName()), true); // Folia - region threading + return; // Folia - region threading } else { throw ERROR_INVULNERABLE.create(); } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java index a046a0b1519806ff3d987e6402f152b60a3a6f4c..60649ced046fa126ef59b7a2dc3e6b5eeaf42bc4 100644 --- a/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java +++ b/src/main/java/net/minecraft/server/commands/DefaultGameModeCommands.java @@ -28,12 +28,14 @@ public class DefaultGameModeCommands { GameType gameType = minecraftServer.getForcedGameType(); if (gameType != null) { for (ServerPlayer serverPlayer : minecraftServer.getPlayerList().getPlayers()) { + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading // Paper start - Expand PlayerGameModeChangeEvent - org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); + org.bukkit.event.player.PlayerGameModeChangeEvent event = player.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); // Folia - region threading if (event != null && event.isCancelled()) { source.sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false); } // Paper end - Expand PlayerGameModeChangeEvent + }, null, 1L); // Folia - region threading i++; } } diff --git a/src/main/java/net/minecraft/server/commands/EffectCommands.java b/src/main/java/net/minecraft/server/commands/EffectCommands.java index ed6336065a0061af095d3395b927b8976443cb68..5a6f53cfaded2df14a82ee7639cdd0bb4728012e 100644 --- a/src/main/java/net/minecraft/server/commands/EffectCommands.java +++ b/src/main/java/net/minecraft/server/commands/EffectCommands.java @@ -84,7 +84,15 @@ public class EffectCommands { if (entity instanceof LivingEntity) { MobEffectInstance mobeffect = new MobEffectInstance(statusEffect, k, amplifier, false, showParticles); - if (((LivingEntity) entity).addEffect(mobeffect, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { + if (!(nmsEntity instanceof LivingEntity)) { + return; + } + ((LivingEntity) nmsEntity).addEffect(mobeffect, null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading + if (true) { // CraftBukkit // Folia - region threading ++j; } } @@ -114,8 +122,16 @@ public class EffectCommands { while (iterator.hasNext()) { Entity entity = (Entity) iterator.next(); - if (entity instanceof LivingEntity && ((LivingEntity) entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit + if (entity instanceof LivingEntity) { // CraftBukkit // Folia - region threading ++i; + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { + if (!(nmsEntity instanceof LivingEntity)) { + return; + } + ((LivingEntity) nmsEntity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading } } @@ -144,8 +160,16 @@ public class EffectCommands { while (iterator.hasNext()) { Entity entity = (Entity) iterator.next(); - if (entity instanceof LivingEntity && ((LivingEntity) entity).removeEffect(statusEffect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit + if (entity instanceof LivingEntity) { // CraftBukkit // Folia - region threading ++i; + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { + if (!(nmsEntity instanceof LivingEntity)) { + return; + } + ((LivingEntity) nmsEntity).removeEffect(statusEffect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/EnchantCommand.java b/src/main/java/net/minecraft/server/commands/EnchantCommand.java index 99695e38b6a10c3cffda6e453f9f0619c7406cc0..69a7f9f5dc69836a472af3b4d31c6fefd028505c 100644 --- a/src/main/java/net/minecraft/server/commands/EnchantCommand.java +++ b/src/main/java/net/minecraft/server/commands/EnchantCommand.java @@ -68,51 +68,78 @@ public class EnchantCommand { ); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int enchant(CommandSourceStack source, Collection targets, Holder enchantment, int level) throws CommandSyntaxException { Enchantment enchantment2 = enchantment.value(); if (level > enchantment2.getMaxLevel()) { throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment2.getMaxLevel()); } else { - int i = 0; + final java.util.concurrent.atomic.AtomicInteger changed = new java.util.concurrent.atomic.AtomicInteger(0); // Folia - region threading + final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(targets.size()); // Folia - region threading + final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName = new java.util.concurrent.atomic.AtomicReference<>(); // Folia - region threading for (Entity entity : targets) { if (entity instanceof LivingEntity) { - LivingEntity livingEntity = (LivingEntity)entity; - ItemStack itemStack = livingEntity.getMainHandItem(); - if (!itemStack.isEmpty()) { - if (enchantment2.canEnchant(itemStack) - && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(itemStack).keySet(), enchantment)) { - itemStack.enchant(enchantment, level); - i++; - } else if (targets.size() == 1) { - throw ERROR_INCOMPATIBLE.create(itemStack.getItem().getName(itemStack).getString()); + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + try { + LivingEntity livingEntity = (LivingEntity)nmsEntity; + ItemStack itemStack = livingEntity.getMainHandItem(); + if (!itemStack.isEmpty()) { + if (enchantment2.canEnchant(itemStack) + && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(itemStack).keySet(), enchantment)) { + itemStack.enchant(enchantment, level); + possibleSingleDisplayName.set(livingEntity.getDisplayName()); + changed.incrementAndGet(); + } else if (targets.size() == 1) { + throw ERROR_INCOMPATIBLE.create(itemStack.getItem().getName(itemStack).getString()); + } + } else if (targets.size() == 1) { + throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); + } + } catch (final CommandSyntaxException exception) { + sendMessage(source, exception); + return; // don't send feedback twice } - } else if (targets.size() == 1) { - throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); - } + sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed); + }, ignored -> sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed), 1L); } else if (targets.size() == 1) { throw ERROR_NOT_LIVING_ENTITY.create(entity.getName().getString()); + } else { + sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed); + // Folia end - region threading } } + return targets.size(); // Folia - region threading + } + } + // Folia start - region threading + private static void sendFeedback(final CommandSourceStack source, final Holder enchantment, final int level, final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName, final java.util.concurrent.atomic.AtomicInteger count, final java.util.concurrent.atomic.AtomicInteger changed) { + if (count.decrementAndGet() == 0) { + final int i = changed.get(); if (i == 0) { - throw ERROR_NOTHING_HAPPENED.create(); + sendMessage(source, ERROR_NOTHING_HAPPENED.create()); } else { - if (targets.size() == 1) { + if (i == 1) { source.sendSuccess( () -> Component.translatable( - "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), targets.iterator().next().getDisplayName() + "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), possibleSingleDisplayName.get() ), true ); } else { source.sendSuccess( - () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), targets.size()), true + () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), i), true ); } - - return i; } } } + // Folia end - region threading } diff --git a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java index 775eb9fd56c2d0eafcfb04ab97ddb270ba3faca9..a5a69f87fe9d35c489a57d4d93e9ad54ed458d30 100644 --- a/src/main/java/net/minecraft/server/commands/ExperienceCommand.java +++ b/src/main/java/net/minecraft/server/commands/ExperienceCommand.java @@ -131,14 +131,18 @@ public class ExperienceCommand { } private static int queryExperience(CommandSourceStack source, ServerPlayer player, ExperienceCommand.Type component) { - int i = component.query.applyAsInt(player); - source.sendSuccess(() -> Component.translatable("commands.experience.query." + component.name, player.getDisplayName(), i), false); - return i; + player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading + int i = component.query.applyAsInt(serverPlayer); // Folia - region threading + source.sendSuccess(() -> Component.translatable("commands.experience.query." + component.name, serverPlayer.getDisplayName(), i), false); // Folia - region threading + }, null, 1L); // Folia - region threading + return 0; // Folia - region threading } private static int addExperience(CommandSourceStack source, Collection targets, int amount, ExperienceCommand.Type component) { for (ServerPlayer serverPlayer : targets) { - component.add.accept(serverPlayer, amount); + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading + component.add.accept(player, amount); // Folia - region threading + }, null, 1L); // Folia - region threading } if (targets.size() == 1) { @@ -159,9 +163,11 @@ public class ExperienceCommand { int i = 0; for (ServerPlayer serverPlayer : targets) { - if (component.set.test(serverPlayer, amount)) { - i++; + ++i; serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading + if (component.set.test(player, amount)) { // Folia - region threading + // Folia - region threading } + }, null, 1L); // Folia - region threading } if (i == 0) { diff --git a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java index c5bba5ce879b336b43e742eaa21d661dcee379aa..0eda57c785bec699156b2b39919b35046dacb586 100644 --- a/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java +++ b/src/main/java/net/minecraft/server/commands/FillBiomeCommand.java @@ -106,6 +106,16 @@ public class FillBiomeCommand { }); } + // Folia start - region threading + private static void sendMessage(Consumer> src, Supplier> supplier) { + Either either = supplier.get(); + CommandSyntaxException ex = either == null ? null : either.right().orElse(null); + if (ex != null) { + src.accept(() -> (Component)ex.getRawMessage()); + } + } + // Folia end - region threading + public static Either fill( ServerLevel world, BlockPos from, BlockPos to, Holder biome, Predicate> filter, Consumer> feedbackConsumer ) { @@ -117,6 +127,17 @@ public class FillBiomeCommand { if (i > j) { return Either.right(ERROR_VOLUME_TOO_LARGE.create(j, i)); } else { + // Folia start - region threading + int buffer = 0; // no buffer, we do not touch neighbours + world.moonrise$loadChunksAsync( + (boundingBox.minX() - buffer) >> 4, + (boundingBox.maxX() + buffer) >> 4, + (boundingBox.minZ() - buffer) >> 4, + (boundingBox.maxZ() + buffer) >> 4, + net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunks) -> { + sendMessage(feedbackConsumer, () -> { List list = new ArrayList<>(); for (int k = SectionPos.blockToSectionCoord(boundingBox.minZ()); k <= SectionPos.blockToSectionCoord(boundingBox.maxZ()); k++) { @@ -153,6 +174,11 @@ public class FillBiomeCommand { ) ); return Either.left(mutableInt.getValue()); + // Folia start - region threading + }); // sendMessage + }); // loadChunksASync + return Either.left(Integer.valueOf(0)); + // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/FillCommand.java b/src/main/java/net/minecraft/server/commands/FillCommand.java index 0509e28f79d13615b5baefc34799b0ad2df071be..b4cc5328964a80c0f1f24085afd3831a4d7611e7 100644 --- a/src/main/java/net/minecraft/server/commands/FillCommand.java +++ b/src/main/java/net/minecraft/server/commands/FillCommand.java @@ -151,6 +151,12 @@ public class FillCommand { ); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int fillBlocks( CommandSourceStack source, BoundingBox range, BlockInput block, FillCommand.Mode mode, @Nullable Predicate filter ) throws CommandSyntaxException { @@ -161,6 +167,18 @@ public class FillCommand { } else { List list = Lists.newArrayList(); ServerLevel serverLevel = source.getLevel(); + // Folia start - region threading + int buffer = 32; + // physics may spill into neighbour chunks, so use a buffer + serverLevel.moonrise$loadChunksAsync( + (range.minX() - buffer) >> 4, + (range.maxX() + buffer) >> 4, + (range.minZ() - buffer) >> 4, + (range.maxZ() + buffer) >> 4, + net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunks) -> { + try { // Folia end - region threading int k = 0; for (BlockPos blockPos : BlockPos.betweenClosed(range.minX(), range.minY(), range.minZ(), range.maxX(), range.maxY(), range.maxZ())) { @@ -187,8 +205,13 @@ public class FillCommand { } else { int l = k; source.sendSuccess(() -> Component.translatable("commands.fill.success", l), true); - return k; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); return 0; // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java index 255211f47a1678f714b1e04b31298976ada8a781..87275b46c4411fd51d6572ec7b3f924e347d4ed7 100644 --- a/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java +++ b/src/main/java/net/minecraft/server/commands/ForceLoadCommand.java @@ -97,7 +97,17 @@ public class ForceLoadCommand { ); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int queryForceLoad(CommandSourceStack source, ColumnPos pos) throws CommandSyntaxException { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + // Folia end - region threading ChunkPos chunkPos = pos.toChunkPos(); ServerLevel serverLevel = source.getLevel(); ResourceKey resourceKey = serverLevel.dimension(); @@ -109,14 +119,22 @@ public class ForceLoadCommand { ), false ); - return 1; + return; // Folia - region threading } else { throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location()); } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } private static int listForceLoad(CommandSourceStack source) { ServerLevel serverLevel = source.getLevel(); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading ResourceKey resourceKey = serverLevel.dimension(); LongSet longSet = serverLevel.getForcedChunks(); int i = longSet.size(); @@ -134,20 +152,27 @@ public class ForceLoadCommand { } else { source.sendFailure(Component.translatable("commands.forceload.added.none", Component.translationArg(resourceKey.location()))); } + }); // Folia - region threading - return i; + return 1; // Folia - region threading } private static int removeAll(CommandSourceStack source) { ServerLevel serverLevel = source.getLevel(); ResourceKey resourceKey = serverLevel.dimension(); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading LongSet longSet = serverLevel.getForcedChunks(); longSet.forEach(chunkPos -> serverLevel.setChunkForced(ChunkPos.getX(chunkPos), ChunkPos.getZ(chunkPos), false)); source.sendSuccess(() -> Component.translatable("commands.forceload.removed.all", Component.translationArg(resourceKey.location())), true); + }); // Folia - region threading return 0; } private static int changeForceLoad(CommandSourceStack source, ColumnPos from, ColumnPos to, boolean forceLoaded) throws CommandSyntaxException { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + // Folia end - region threading int i = Math.min(from.x(), to.x()); int j = Math.min(from.z(), to.z()); int k = Math.max(from.x(), to.x()); @@ -207,11 +232,18 @@ public class ForceLoadCommand { ); } - return u; + return; // Folia - region threading } } } else { throw BlockPosArgument.ERROR_OUT_OF_WORLD.create(); } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/GameModeCommand.java b/src/main/java/net/minecraft/server/commands/GameModeCommand.java index d1da3600dc07107309b20ebe6e7c0c4da0e8de76..a2075133b0e8ea6299dec0fa5125e4296eb3276a 100644 --- a/src/main/java/net/minecraft/server/commands/GameModeCommand.java +++ b/src/main/java/net/minecraft/server/commands/GameModeCommand.java @@ -60,15 +60,18 @@ public class GameModeCommand { int i = 0; for (ServerPlayer serverPlayer : targets) { + serverPlayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading // Paper start - Expand PlayerGameModeChangeEvent org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameMode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); if (event != null && !event.isCancelled()) { logGamemodeChange(context.getSource(), serverPlayer, gameMode); - i++; + // Folia - region threading } else if (event != null && event.cancelMessage() != null) { context.getSource().sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true); // Paper end - Expand PlayerGameModeChangeEvent } + }, null, 1L); // Folia - region threading + ++i; // Folia - region threading } return i; diff --git a/src/main/java/net/minecraft/server/commands/GiveCommand.java b/src/main/java/net/minecraft/server/commands/GiveCommand.java index 0d9de4c61c7b26a6ff37c12fde629161fd0c3d5a..fca164cd59f1f0bf396e82a4c824c8d779b6c640 100644 --- a/src/main/java/net/minecraft/server/commands/GiveCommand.java +++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java @@ -57,6 +57,7 @@ public class GiveCommand { l -= i1; ItemStack itemstack1 = item.createItemStack(i1, false); + entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading boolean flag = entityplayer.getInventory().add(itemstack1); ItemEntity entityitem; @@ -75,6 +76,7 @@ public class GiveCommand { entityitem.setTarget(entityplayer.getUUID()); } } + }, null, 1L); // Folia - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/KillCommand.java b/src/main/java/net/minecraft/server/commands/KillCommand.java index c2974a6bd6851b54d1df2689195d896baf4906ee..57f5c571e63355b0e32480f31b9e5c8173d840c6 100644 --- a/src/main/java/net/minecraft/server/commands/KillCommand.java +++ b/src/main/java/net/minecraft/server/commands/KillCommand.java @@ -24,7 +24,9 @@ public class KillCommand { private static int kill(CommandSourceStack source, Collection targets) { for (Entity entity : targets) { - entity.kill(); + entity.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading + nmsEntity.kill(); // Folia - region threading + }, null, 1L); // Folia - region threading } if (targets.size() == 1) { diff --git a/src/main/java/net/minecraft/server/commands/PlaceCommand.java b/src/main/java/net/minecraft/server/commands/PlaceCommand.java index 00fe31a486121eabd6c1e6fc85b94ecede8a609e..351b3cb441a0700d961148239f4afed9ed5dcb57 100644 --- a/src/main/java/net/minecraft/server/commands/PlaceCommand.java +++ b/src/main/java/net/minecraft/server/commands/PlaceCommand.java @@ -88,12 +88,25 @@ public class PlaceCommand { }))))))))); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + public static int placeFeature(CommandSourceStack source, Holder.Reference> feature, BlockPos pos) throws CommandSyntaxException { ServerLevel worldserver = source.getLevel(); ConfiguredFeature worldgenfeatureconfigured = (ConfiguredFeature) feature.value(); ChunkPos chunkcoordintpair = new ChunkPos(pos); PlaceCommand.checkLoaded(worldserver, new ChunkPos(chunkcoordintpair.x - 1, chunkcoordintpair.z - 1), new ChunkPos(chunkcoordintpair.x + 1, chunkcoordintpair.z + 1)); + // Folia start - region threading + worldserver.moonrise$loadChunksAsync( + pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunks) -> { + try { + // Folia end - region threading if (!worldgenfeatureconfigured.place(worldserver, worldserver.getChunkSource().getGenerator(), worldserver.getRandom(), pos)) { throw PlaceCommand.ERROR_FEATURE_FAILED.create(); } else { @@ -102,8 +115,16 @@ public class PlaceCommand { source.sendSuccess(() -> { return Component.translatable("commands.place.feature.success", s, pos.getX(), pos.getY(), pos.getZ()); }, true); - return 1; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + } + ); + return 1; + // Folia end - region threading } public static int placeJigsaw(CommandSourceStack source, Holder structurePool, ResourceLocation id, int maxDepth, BlockPos pos) throws CommandSyntaxException { @@ -111,20 +132,42 @@ public class PlaceCommand { ChunkPos chunkcoordintpair = new ChunkPos(pos); PlaceCommand.checkLoaded(worldserver, chunkcoordintpair, chunkcoordintpair); + // Folia start - region threading + worldserver.moonrise$loadChunksAsync( + pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunks) -> { + try { + // Folia end - region threading if (!JigsawPlacement.generateJigsaw(worldserver, structurePool, id, maxDepth, pos, false)) { throw PlaceCommand.ERROR_JIGSAW_FAILED.create(); } else { source.sendSuccess(() -> { return Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()); }, true); - return 1; + return; // Folia start - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + } + ); + return 1; + // Folia end - region threading } public static int placeStructure(CommandSourceStack source, Holder.Reference structure, BlockPos pos) throws CommandSyntaxException { ServerLevel worldserver = source.getLevel(); Structure structure1 = (Structure) structure.value(); ChunkGenerator chunkgenerator = worldserver.getChunkSource().getGenerator(); + // Folia start - region threading + worldserver.moonrise$loadChunksAsync( + pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunks) -> { + try { + // Folia end - region threading StructureStart structurestart = structure1.generate(source.registryAccess(), chunkgenerator, chunkgenerator.getBiomeSource(), worldserver.getChunkSource().randomState(), worldserver.getStructureManager(), worldserver.getSeed(), new ChunkPos(pos), 0, worldserver, (holder) -> { return true; }); @@ -146,12 +189,27 @@ public class PlaceCommand { source.sendSuccess(() -> { return Component.translatable("commands.place.structure.success", s, pos.getX(), pos.getY(), pos.getZ()); }, true); - return 1; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + } + ); + return 1; + // Folia end - region threading } public static int placeTemplate(CommandSourceStack source, ResourceLocation id, BlockPos pos, Rotation rotation, Mirror mirror, float integrity, int seed) throws CommandSyntaxException { ServerLevel worldserver = source.getLevel(); + // Folia start - region threading + worldserver.moonrise$loadChunksAsync( + pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunks) -> { + try { + // Folia end - region threading StructureTemplateManager structuretemplatemanager = worldserver.getStructureManager(); Optional optional; @@ -182,9 +240,17 @@ public class PlaceCommand { source.sendSuccess(() -> { return Component.translatable("commands.place.template.success", Component.translationArg(id), pos.getX(), pos.getY(), pos.getZ()); }, true); - return 1; + return; // Folia - region threading } } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + } + ); + return 1; + // Folia end - region threading } private static void checkLoaded(ServerLevel world, ChunkPos pos1, ChunkPos pos2) throws CommandSyntaxException { diff --git a/src/main/java/net/minecraft/server/commands/RecipeCommand.java b/src/main/java/net/minecraft/server/commands/RecipeCommand.java index 869eceace975ae0a14e6be27f4c0d0e77afd715a..f8635a3beb5433269e3e7b7dc6c7f456783fd939 100644 --- a/src/main/java/net/minecraft/server/commands/RecipeCommand.java +++ b/src/main/java/net/minecraft/server/commands/RecipeCommand.java @@ -83,7 +83,12 @@ public class RecipeCommand { int i = 0; for (ServerPlayer serverPlayer : targets) { - i += serverPlayer.awardRecipes(recipes); + // Folia start - region threading + ++i; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + player.awardRecipes(recipes); + }, null, 1L); + // Folia end - region threading } if (i == 0) { @@ -105,7 +110,12 @@ public class RecipeCommand { int i = 0; for (ServerPlayer serverPlayer : targets) { - i += serverPlayer.resetRecipes(recipes); + // Folia start - region threading + ++i; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + player.resetRecipes(recipes); + }, null, 1L); + // Folia end - region threading } if (i == 0) { diff --git a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java index 977f3b1f0b6822d37254452c681717d31d3e1011..ea13a3b409b8a4a6a77071c700b4dd0a33b02716 100644 --- a/src/main/java/net/minecraft/server/commands/SetBlockCommand.java +++ b/src/main/java/net/minecraft/server/commands/SetBlockCommand.java @@ -80,10 +80,21 @@ public class SetBlockCommand { ); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int setBlock( CommandSourceStack source, BlockPos pos, BlockInput block, SetBlockCommand.Mode mode, @Nullable Predicate condition ) throws CommandSyntaxException { ServerLevel serverLevel = source.getLevel(); + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + serverLevel, pos.getX() >> 4, pos.getZ() >> 4, () -> { + try { + // Folia end - region threading if (condition != null && !condition.test(new BlockInWorld(serverLevel, pos, true))) { throw ERROR_FAILED.create(); } else { @@ -102,9 +113,16 @@ public class SetBlockCommand { } else { serverLevel.blockUpdated(pos, block.getState().getBlock()); source.sendSuccess(() -> Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true); - return 1; + return; // Folia - region threading } } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } public interface Filter { diff --git a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java index 15db9368227dbc29d07d74e85bd126b345b526b6..161ae9c5f9ff0e8cdf3bb3c6bb1d068607f9c2ad 100644 --- a/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java +++ b/src/main/java/net/minecraft/server/commands/SetSpawnCommand.java @@ -43,7 +43,11 @@ public class SetSpawnCommand { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); // Paper start - Add PlayerSetSpawnEvent - if (entityplayer.setRespawnPosition(resourcekey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) { + // Folia start - region threading + entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + player.setRespawnPosition(resourcekey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND); + }, null, 1L); + if (true) { // Folia end - region threading actualTargets.add(entityplayer); } // Paper end - Add PlayerSetSpawnEvent diff --git a/src/main/java/net/minecraft/server/commands/SummonCommand.java b/src/main/java/net/minecraft/server/commands/SummonCommand.java index 798999be50d26be357ef3c6d5b9383ce4d1048c1..af9b3ea74379d52f2ff08cb81e87a0a61062097a 100644 --- a/src/main/java/net/minecraft/server/commands/SummonCommand.java +++ b/src/main/java/net/minecraft/server/commands/SummonCommand.java @@ -64,11 +64,18 @@ public class SummonCommand { if (entity == null) { throw SummonCommand.ERROR_FAILED.create(); } else { - if (initialize && entity instanceof Mob) { - ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.COMMAND, (SpawnGroupData) null); - } + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + worldserver, entity.chunkPosition().x, entity.chunkPosition().z, () -> { + if (initialize && entity instanceof Mob) { + ((Mob) entity).finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), MobSpawnType.COMMAND, (SpawnGroupData) null); + } + worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND); + } + ); + // Folia end - region threading - if (!worldserver.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND" + if (false) { // CraftBukkit - pass a spawn reason of "COMMAND" // Folia - region threading throw SummonCommand.ERROR_DUPLICATE_UUID.create(); } else { return entity; diff --git a/src/main/java/net/minecraft/server/commands/TeleportCommand.java b/src/main/java/net/minecraft/server/commands/TeleportCommand.java index 54851f6cc0d5fddb32a9a1e84a4f5ae41af18758..4a13ac9f5342a1b8dc155431eb33f045f87c9196 100644 --- a/src/main/java/net/minecraft/server/commands/TeleportCommand.java +++ b/src/main/java/net/minecraft/server/commands/TeleportCommand.java @@ -77,7 +77,7 @@ public class TeleportCommand { while (iterator.hasNext()) { Entity entity1 = (Entity) iterator.next(); - TeleportCommand.performTeleport(source, entity1, (ServerLevel) destination.level(), destination.getX(), destination.getY(), destination.getZ(), EnumSet.noneOf(RelativeMovement.class), destination.getYRot(), destination.getXRot(), (TeleportCommand.LookAt) null); + io.papermc.paper.threadedregions.TeleportUtils.teleport(entity1, false, destination, Float.valueOf(destination.getYRot()), Float.valueOf(destination.getXRot()), Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, null); // Folia - region threading } if (targets.size() == 1) { @@ -161,6 +161,24 @@ public class TeleportCommand { float f2 = Mth.wrapDegrees(yaw); float f3 = Mth.wrapDegrees(pitch); + // Folia start - region threading + if (true) { + ServerLevel worldFinal = world; + Vec3 posFinal = new Vec3(x, y, z); + Float yawFinal = Float.valueOf(f2); + Float pitchFinal = Float.valueOf(f3); + target.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { + nmsEntity.unRide(); + nmsEntity.teleportAsync( + worldFinal, posFinal, yawFinal, pitchFinal, Vec3.ZERO, + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, + Entity.TELEPORT_FLAG_LOAD_CHUNK, + null + ); + }, null, 1L); + return; + } + // Folia end - region threading // CraftBukkit start - Teleport event boolean result; if (target instanceof ServerPlayer player) { diff --git a/src/main/java/net/minecraft/server/commands/TimeCommand.java b/src/main/java/net/minecraft/server/commands/TimeCommand.java index 44fcd43a466fb47d31ab05e44bafbef3c4cae63f..2989750a75b3dd244cf8f9ca78769308f982374b 100644 --- a/src/main/java/net/minecraft/server/commands/TimeCommand.java +++ b/src/main/java/net/minecraft/server/commands/TimeCommand.java @@ -58,6 +58,7 @@ public class TimeCommand { while (iterator.hasNext()) { ServerLevel worldserver = (ServerLevel) iterator.next(); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading // CraftBukkit start TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time - worldserver.getDayTime()); Bukkit.getPluginManager().callEvent(event); @@ -65,6 +66,7 @@ public class TimeCommand { worldserver.setDayTime((long) worldserver.getDayTime() + event.getSkipAmount()); } // CraftBukkit end + }); // Folia - region threading } source.sendSuccess(() -> { @@ -79,6 +81,7 @@ public class TimeCommand { while (iterator.hasNext()) { ServerLevel worldserver = (ServerLevel) iterator.next(); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading // CraftBukkit start TimeSkipEvent event = new TimeSkipEvent(worldserver.getWorld(), TimeSkipEvent.SkipReason.COMMAND, time); Bukkit.getPluginManager().callEvent(event); @@ -86,13 +89,16 @@ public class TimeCommand { worldserver.setDayTime(worldserver.getDayTime() + event.getSkipAmount()); } // CraftBukkit end + }); // Folia - region threading } + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading int j = TimeCommand.getDayTime(source.getLevel()); source.sendSuccess(() -> { return Component.translatable("commands.time.set", j); }, true); - return j; + }); // Folia - region threading + return 0; // Folia - region threading } } diff --git a/src/main/java/net/minecraft/server/commands/WeatherCommand.java b/src/main/java/net/minecraft/server/commands/WeatherCommand.java index 4f5463bc299a43c32fefad6c57aa6a74fe73245b..72ce93dbb8ed9c3063c20fcf1b3cf0b59de8c4cb 100644 --- a/src/main/java/net/minecraft/server/commands/WeatherCommand.java +++ b/src/main/java/net/minecraft/server/commands/WeatherCommand.java @@ -38,26 +38,32 @@ public class WeatherCommand { } private static int setClear(CommandSourceStack source, int duration) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(WeatherCommand.getDuration(source, duration, ServerLevel.RAIN_DELAY), 0, false, false); // CraftBukkit - SPIGOT-7680: per-world source.sendSuccess(() -> { return Component.translatable("commands.weather.set.clear"); }, true); + }); // Folia - region threading return duration; } private static int setRain(CommandSourceStack source, int duration) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(0, WeatherCommand.getDuration(source, duration, ServerLevel.RAIN_DURATION), true, false); // CraftBukkit - SPIGOT-7680: per-world source.sendSuccess(() -> { return Component.translatable("commands.weather.set.rain"); }, true); + }); // Folia - region threading return duration; } private static int setThunder(CommandSourceStack source, int duration) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(0, WeatherCommand.getDuration(source, duration, ServerLevel.THUNDER_DURATION), true, true); // CraftBukkit - SPIGOT-7680: per-world source.sendSuccess(() -> { return Component.translatable("commands.weather.set.thunder"); }, true); + }); // Folia - region threading return duration; } } diff --git a/src/main/java/net/minecraft/server/commands/WorldBorderCommand.java b/src/main/java/net/minecraft/server/commands/WorldBorderCommand.java index 812f2adc6fc20aa126e629284fe594a923b24540..0a5e6961fb37e9a53cd39b1bd233e0204986b244 100644 --- a/src/main/java/net/minecraft/server/commands/WorldBorderCommand.java +++ b/src/main/java/net/minecraft/server/commands/WorldBorderCommand.java @@ -56,7 +56,17 @@ public class WorldBorderCommand { }))))); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int setDamageBuffer(CommandSourceStack source, float distance) throws CommandSyntaxException { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + // Folia end - region threading WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit if (worldborder.getDamageSafeZone() == (double) distance) { @@ -66,11 +76,22 @@ public class WorldBorderCommand { source.sendSuccess(() -> { return Component.translatable("commands.worldborder.damage.buffer.success", String.format(Locale.ROOT, "%.2f", distance)); }, true); - return (int) distance; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } private static int setDamageAmount(CommandSourceStack source, float damagePerBlock) throws CommandSyntaxException { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + // Folia end - region threading WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit if (worldborder.getDamagePerBlock() == (double) damagePerBlock) { @@ -80,11 +101,22 @@ public class WorldBorderCommand { source.sendSuccess(() -> { return Component.translatable("commands.worldborder.damage.amount.success", String.format(Locale.ROOT, "%.2f", damagePerBlock)); }, true); - return (int) damagePerBlock; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } private static int setWarningTime(CommandSourceStack source, int time) throws CommandSyntaxException { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + // Folia end - region threading WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit if (worldborder.getWarningTime() == time) { @@ -94,11 +126,22 @@ public class WorldBorderCommand { source.sendSuccess(() -> { return Component.translatable("commands.worldborder.warning.time.success", time); }, true); - return time; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } private static int setWarningDistance(CommandSourceStack source, int distance) throws CommandSyntaxException { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + // Folia end - region threading WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit if (worldborder.getWarningBlocks() == distance) { @@ -108,20 +151,38 @@ public class WorldBorderCommand { source.sendSuccess(() -> { return Component.translatable("commands.worldborder.warning.distance.success", distance); }, true); - return distance; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } private static int getSize(CommandSourceStack source) { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + // Folia end - region threading double d0 = source.getLevel().getWorldBorder().getSize(); // CraftBukkit source.sendSuccess(() -> { return Component.translatable("commands.worldborder.get", String.format(Locale.ROOT, "%.0f", d0)); }, false); - return Mth.floor(d0 + 0.5D); + return; // Folia - region threading + // Folia start - region threading + }); + return 1; + // Folia end - region threading } private static int setCenter(CommandSourceStack source, Vec2 pos) throws CommandSyntaxException { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + // Folia end - region threading WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit if (worldborder.getCenterX() == (double) pos.x && worldborder.getCenterZ() == (double) pos.y) { @@ -131,13 +192,24 @@ public class WorldBorderCommand { source.sendSuccess(() -> { return Component.translatable("commands.worldborder.center.success", String.format(Locale.ROOT, "%.2f", pos.x), String.format(Locale.ROOT, "%.2f", pos.y)); }, true); - return 0; + return; // Folia - region threading } else { throw WorldBorderCommand.ERROR_TOO_FAR_OUT.create(); } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } private static int setSize(CommandSourceStack source, double distance, long time) throws CommandSyntaxException { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + // Folia end - region threading WorldBorder worldborder = source.getLevel().getWorldBorder(); // CraftBukkit double d1 = worldborder.getSize(); @@ -166,7 +238,14 @@ public class WorldBorderCommand { }, true); } - return (int) (distance - d1); + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } } diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index dd56c8e041116ef3602a9f89c998c8208ab89b51..301d7fa29fce2997a5881b3852896eff5af33672 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -469,9 +469,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface } @Override - public void tickChildren(BooleanSupplier shouldKeepTicking) { - super.tickChildren(shouldKeepTicking); - this.handleConsoleInputs(); + public void tickChildren(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - region threading + super.tickChildren(shouldKeepTicking, region); // Folia - region threading + if (region == null) this.handleConsoleInputs(); // Folia - region threading } @Override @@ -792,7 +792,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface Waitable[] waitableArray = new Waitable[1]; // Paper rconConsoleSource.prepareForCommand(); final java.util.concurrent.atomic.AtomicReference command = new java.util.concurrent.atomic.AtomicReference<>(s); // Paper - this.executeBlocking(() -> { + Runnable sync = () -> { // Folia - region threading CommandSourceStack wrapper = rconConsoleSource.createCommandSourceStack(); RemoteServerCommandEvent event = new RemoteServerCommandEvent(rconConsoleSource.getBukkitSender(wrapper), s); this.server.getPluginManager().callEvent(event); @@ -816,7 +816,16 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface ConsoleInput serverCommand = new ConsoleInput(event.getCommand(), wrapper); this.server.dispatchServerCommand(event.getSender(), serverCommand); } // Paper - }); + }; // Folia start - region threading + java.util.concurrent.CompletableFuture + .runAsync(sync, io.papermc.paper.threadedregions.RegionizedServer.getInstance()::addTask) + .whenComplete((Void r, Throwable t) -> { + if (t != null) { + LOGGER.error("Error handling command for rcon: " + s, t); + } + }) + .join(); + // Folia end - region threading // Paper start if (waitableArray[0] != null) { //noinspection unchecked diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 1e0a6e5a3c907ab55ee6f2780a7d43bd455f2b7b..7f161864ee6a43de8d37c0e5c9cba9918f2fed89 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -138,8 +138,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public final ChunkMap.ChunkDistanceManager distanceManager; public final AtomicInteger tickingGenerated; // Paper - public private final String storageName; - private final PlayerMap playerMap; - public final Int2ObjectMap entityMap; + //private final PlayerMap playerMap; // Folia - region threading + //public final Int2ObjectMap entityMap; // Folia - region threading private final Long2ByteMap chunkTypeCache; private final Long2LongMap chunkSaveCooldowns; // Paper - rewrite chunk system @@ -178,8 +178,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper - rewrite chunk system this.toDrop = new LongOpenHashSet(); this.tickingGenerated = new AtomicInteger(); - this.playerMap = new PlayerMap(); - this.entityMap = new Int2ObjectOpenHashMap(); + //this.playerMap = new PlayerMap(); // Folia - region threading + //this.entityMap = new Int2ObjectOpenHashMap(); // Folia - region threading this.chunkTypeCache = new Long2ByteOpenHashMap(); this.chunkSaveCooldowns = new Long2LongOpenHashMap(); // Paper - rewrite chunk system @@ -693,7 +693,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (!this.distanceManager.hasPlayersNearby(chunkcoordintpair.toLong())) { return false; } else { - Iterator iterator = this.playerMap.getAllPlayers().iterator(); + Iterator iterator = this.level.getLocalPlayers().iterator(); // Folia - region threading ServerPlayer entityplayer; @@ -725,7 +725,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider return List.of(); } else { Builder builder = ImmutableList.builder(); - Iterator iterator = this.playerMap.getAllPlayers().iterator(); + Iterator iterator = this.level.getLocalPlayers().iterator(); // Folia - region threading while (iterator.hasNext()) { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); @@ -755,13 +755,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider void updatePlayerStatus(ServerPlayer player, boolean added) { boolean flag1 = this.skipPlayer(player); - boolean flag2 = this.playerMap.ignoredOrUnknown(player); + // Folia - region threading if (added) { - this.playerMap.addPlayer(player, flag1); + // Folia - region threading this.updatePlayerPos(player); if (!flag1) { - this.distanceManager.addPlayer(SectionPos.of((EntityAccess) player), player); + // Folia - region threading ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation } @@ -770,9 +770,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } else { SectionPos sectionposition = player.getLastSectionPos(); - this.playerMap.removePlayer(player); - if (!flag2) { - this.distanceManager.removePlayer(sectionposition, player); + // Folia - region threading + if (true) { // Folia - region threading + // Folia - region threading ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation } @@ -792,28 +792,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider SectionPos sectionposition = player.getLastSectionPos(); SectionPos sectionposition1 = SectionPos.of((EntityAccess) player); - boolean flag = this.playerMap.ignored(player); + // Folia - region threading boolean flag1 = this.skipPlayer(player); - boolean flag2 = sectionposition.asLong() != sectionposition1.asLong(); + // Folia - region threading - if (flag2 || flag != flag1) { + if (true) { // Folia - region threading this.updatePlayerPos(player); - ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, sectionposition, sectionposition1, flag, flag1); // Paper - chunk tick iteration optimisation - if (!flag) { - this.distanceManager.removePlayer(sectionposition, player); - } - - if (!flag1) { - this.distanceManager.addPlayer(sectionposition1, player); - } - - if (!flag && flag1) { - this.playerMap.ignorePlayer(player); - } - - if (flag && !flag1) { - this.playerMap.unIgnorePlayer(player); - } + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, sectionposition, sectionposition1, false, flag1); // Paper - chunk tick iteration optimisation // Folia - region threading + // Folia - region threading // Paper - rewrite chunk system } @@ -844,9 +830,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public void addEntity(Entity entity) { org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot // Paper start - ignore and warn about illegal addEntity calls instead of crashing server - if (!entity.valid || entity.level() != this.level || this.entityMap.containsKey(entity.getId())) { + if (!entity.valid || entity.level() != this.level || entity.moonrise$getTrackedEntity() != null) { // Folia - region threading LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName() - + ": " + entity + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); + + ": " + entity + (entity.moonrise$getTrackedEntity() != null ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); // Folia - region threading return; } // Paper end - ignore and warn about illegal addEntity calls instead of crashing server @@ -859,32 +845,30 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (i != 0) { int j = entitytypes.updateInterval(); - if (this.entityMap.containsKey(entity.getId())) { + if (entity.moonrise$getTrackedEntity() != null) { // Folia - region threading throw (IllegalStateException) Util.pauseInIde(new IllegalStateException("Entity is already tracked!")); } else { ChunkMap.TrackedEntity playerchunkmap_entitytracker = new ChunkMap.TrackedEntity(entity, i, j, entitytypes.trackDeltas()); - this.entityMap.put(entity.getId(), playerchunkmap_entitytracker); + // Folia - region threading // Paper start - optimise entity tracker if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) { throw new IllegalStateException("Entity is already tracked"); } ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(playerchunkmap_entitytracker); // Paper end - optimise entity tracker - playerchunkmap_entitytracker.updatePlayers(this.level.players()); + playerchunkmap_entitytracker.updatePlayers(this.level.getLocalPlayers()); // Folia - region threading if (entity instanceof ServerPlayer) { ServerPlayer entityplayer = (ServerPlayer) entity; this.updatePlayerStatus(entityplayer, true); - ObjectIterator objectiterator = this.entityMap.values().iterator(); - - while (objectiterator.hasNext()) { - ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) objectiterator.next(); - - if (playerchunkmap_entitytracker1.entity != entityplayer) { - playerchunkmap_entitytracker1.updatePlayer(entityplayer); + // Folia start - region threading + for (Entity possible : this.level.getCurrentWorldData().getLoadedEntities()) { + if (possible.moonrise$getTrackedEntity() != null) { + possible.moonrise$getTrackedEntity().updatePlayer(entityplayer); } } + // Folia end - region threading } } @@ -896,16 +880,16 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider org.spigotmc.AsyncCatcher.catchOp("entity untrack"); // Spigot if (entity instanceof ServerPlayer entityplayer) { this.updatePlayerStatus(entityplayer, false); - ObjectIterator objectiterator = this.entityMap.values().iterator(); - - while (objectiterator.hasNext()) { - ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); - - playerchunkmap_entitytracker.removePlayer(entityplayer); + // Folia start - region threading + for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) { + if (possible.moonrise$getTrackedEntity() != null) { + possible.moonrise$getTrackedEntity().removePlayer(entityplayer); + } } + // Folia end - region threading } - ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = (ChunkMap.TrackedEntity) this.entityMap.remove(entity.getId()); + ChunkMap.TrackedEntity playerchunkmap_entitytracker1 = entity.moonrise$getTrackedEntity(); // Folia - region threading if (playerchunkmap_entitytracker1 != null) { playerchunkmap_entitytracker1.broadcastRemoved(); @@ -916,10 +900,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper start - optimise entity tracker private void newTrackerTick() { + final io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getNearbyPlayers(); final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();; - final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = entityLookup.trackerEntities; + final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = worldData.trackerEntities; // Folia - region threading final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked(); for (int i = 0, len = trackerEntities.size(); i < len; ++i) { final Entity entity = trackerEntitiesRaw[i]; @@ -932,7 +917,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } // process unloads - final ca.spottedleaf.moonrise.common.list.ReferenceList unloadedEntities = entityLookup.trackerUnloadedEntities; + final ca.spottedleaf.moonrise.common.list.ReferenceList unloadedEntities = worldData.trackerUnloadedEntities; // Folia - region threading final Entity[] unloadedEntitiesRaw = java.util.Arrays.copyOf(unloadedEntities.getRawDataUnchecked(), unloadedEntities.size()); unloadedEntities.clear(); @@ -955,51 +940,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper end - optimise entity tracker // Paper - rewrite chunk system - List list = Lists.newArrayList(); - List list1 = this.level.players(); - ObjectIterator objectiterator = this.entityMap.values().iterator(); - level.timings.tracker1.startTiming(); // Paper - - ChunkMap.TrackedEntity playerchunkmap_entitytracker; - - while (objectiterator.hasNext()) { - playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); - SectionPos sectionposition = playerchunkmap_entitytracker.lastSectionPos; - SectionPos sectionposition1 = SectionPos.of((EntityAccess) playerchunkmap_entitytracker.entity); - boolean flag = !Objects.equals(sectionposition, sectionposition1); - - if (flag) { - playerchunkmap_entitytracker.updatePlayers(list1); - Entity entity = playerchunkmap_entitytracker.entity; - - if (entity instanceof ServerPlayer) { - list.add((ServerPlayer) entity); - } - - playerchunkmap_entitytracker.lastSectionPos = sectionposition1; - } - - if (flag || this.distanceManager.inEntityTickingRange(sectionposition1.chunk().toLong())) { - playerchunkmap_entitytracker.serverEntity.sendChanges(); - } - } - level.timings.tracker1.stopTiming(); // Paper - - if (!list.isEmpty()) { - objectiterator = this.entityMap.values().iterator(); - - level.timings.tracker2.startTiming(); // Paper - while (objectiterator.hasNext()) { - playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) objectiterator.next(); - playerchunkmap_entitytracker.updatePlayers(list); - } - level.timings.tracker2.stopTiming(); // Paper - } + // Folia - region threading } public void broadcast(Entity entity, Packet packet) { - ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId()); + ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.moonrise$getTrackedEntity(); // Folia - region threading if (playerchunkmap_entitytracker != null) { playerchunkmap_entitytracker.broadcast(packet); @@ -1008,7 +954,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } protected void broadcastAndSend(Entity entity, Packet packet) { - ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) this.entityMap.get(entity.getId()); + ChunkMap.TrackedEntity playerchunkmap_entitytracker = (ChunkMap.TrackedEntity) entity.moonrise$getTrackedEntity(); // Folia - region threading if (playerchunkmap_entitytracker != null) { playerchunkmap_entitytracker.broadcastAndSend(packet); @@ -1255,9 +1201,14 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } } // Paper end - Configurable entity tracking range by Y + // Folia start - region threading + if (flag && (this.entity instanceof ServerPlayer thisEntity) && thisEntity.broadcastedDeath) { + flag = false; + } + // Folia end - region threading // CraftBukkit start - respect vanish API - if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits + if (flag && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !player.getBukkitEntity().canSee(this.entity.getBukkitEntity()))) { // Paper - only consider hits // Folia - region threading flag = false; } // CraftBukkit end diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java index 2d2596f04f5addac38037a14a02c6e0622d0c485..ddfbdd7c07be201bbb29fa980b6ba83216704bb0 100644 --- a/src/main/java/net/minecraft/server/level/DistanceManager.java +++ b/src/main/java/net/minecraft/server/level/DistanceManager.java @@ -62,16 +62,16 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches } // Paper end - rewrite chunk system // Paper start - chunk tick iteration optimisation - private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>(); + // Folia - move to regionized world data @Override public final void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos) { - this.spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); + this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading } @Override public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) { - this.spawnChunkTracker.remove(player); + this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading } @Override @@ -79,9 +79,9 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches final SectionPos oldPos, final SectionPos newPos, final boolean oldIgnore, final boolean newIgnore) { if (newIgnore) { - this.spawnChunkTracker.remove(player); + this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading } else { - this.spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); + this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading } } // Paper end - chunk tick iteration optimisation @@ -216,11 +216,11 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches } public int getNaturalSpawnChunkCount() { - return this.spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation + return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation // Folia - region threading } public boolean hasPlayersNearby(long chunkPos) { - return this.spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation + return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation // Folia - region threading } public String getDebugStatus() { diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index dcb5651d1d9b10b40430fb2f713beedf68336704..e13ccac27fa4f4c23185f12c776e80970ab844e6 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -57,16 +57,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; public final ChunkMap chunkMap; private final DimensionDataStorage dataStorage; - private long lastInhabitedUpdate; + //private long lastInhabitedUpdate; // Folia - region threading public boolean spawnEnemies = true; public boolean spawnFriendlies = true; private static final int CACHE_SIZE = 4; private final long[] lastChunkPos = new long[4]; private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4]; private final ChunkAccess[] lastChunk = new ChunkAccess[4]; - @Nullable - @VisibleForDebug - private NaturalSpawner.SpawnState lastSpawnState; + // Folia - moved to regionised world data // Paper start private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); long chunkFutureAwaitCounter; @@ -126,7 +124,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon return load ? this.syncLoad(chunkX, chunkZ, toStatus) : null; } // Paper end - rewrite chunk system - private ServerChunkCache.ChunkAndHolder[] iterationCopy; // Paper - chunk tick iteration optimisations + // Folia - moved to regionized data public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) { this.level = world; @@ -252,6 +250,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } public CompletableFuture> getChunkFuture(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading boolean flag1 = Thread.currentThread() == this.mainThread; CompletableFuture completablefuture; @@ -419,11 +418,12 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } private void tickChunks() { + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading long chunksTicked = 0; // Paper - rewrite chunk system - long i = this.level.getGameTime(); - long j = i - this.lastInhabitedUpdate; + //long i = this.level.getGameTime(); // Folia - region threading + long j = 1L; // Folia - region threading - this.lastInhabitedUpdate = i; + //this.lastInhabitedUpdate = i; // Folia - region threading if (!this.level.isDebug()) { ProfilerFiller gameprofilerfiller = this.level.getProfiler(); @@ -438,13 +438,13 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked(); final int size = tickingChunks.size(); - if (this.iterationCopy == null || this.iterationCopy.length < size) { - this.iterationCopy = new ServerChunkCache.ChunkAndHolder[raw.length]; + if (regionizedWorldData.iterationCopy == null || regionizedWorldData.iterationCopy.length < size) { // Folia - region threading + regionizedWorldData.iterationCopy = new ServerChunkCache.ChunkAndHolder[raw.length]; // Folia - region threading } - System.arraycopy(raw, 0, this.iterationCopy, 0, size); + System.arraycopy(raw, 0, regionizedWorldData.iterationCopy, 0, size); // Folia - region threading list = it.unimi.dsi.fastutil.objects.ObjectArrayList.wrap( - this.iterationCopy, size + regionizedWorldData.iterationCopy, size // Folia - region threading ); } // Paper end - chunk tick iteration optimisations @@ -462,7 +462,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon NaturalSpawner.SpawnState spawnercreature_d; // moved down if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled // re-set mob counts - for (ServerPlayer player : this.level.players) { + for (ServerPlayer player : this.level.getLocalPlayers()) { // Folia - region threading // Paper start - per player mob spawning backoff for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { player.mobCounts[ii] = 0; @@ -475,23 +475,23 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } // Paper end - per player mob spawning backoff } - spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); + spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all } else { - spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); + spawnercreature_d = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); // Folia - region threading - note: function only cares about loaded entities, doesn't need all } // Paper end - Optional per player mob spawns this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings - this.lastSpawnState = spawnercreature_d; + regionizedWorldData.lastSpawnState = spawnercreature_d; // Folia - region threading gameprofilerfiller.popPush("spawnAndTick"); - boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit + boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threadin if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) Util.shuffle(list, this.level.random); // Paper - per player mob spawns - do not need this when per-player is enabled // Paper start - PlayerNaturallySpawnCreaturesEvent int chunkRange = level.spigotConfig.mobSpawnRange; chunkRange = (chunkRange > level.spigotConfig.viewDistance) ? (byte) level.spigotConfig.viewDistance : chunkRange; chunkRange = Math.min(chunkRange, 8); - for (ServerPlayer entityPlayer : this.level.players()) { + for (ServerPlayer entityPlayer : this.level.getLocalPlayers()) { // Folia - region threading entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange); entityPlayer.playerNaturallySpawnedEvent.callEvent(); } @@ -604,14 +604,19 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon @Override public void onLightUpdate(LightLayer type, SectionPos pos) { - this.mainThreadProcessor.execute(() -> { + Runnable run = () -> { // Folia - region threading ChunkHolder playerchunk = this.getVisibleChunkIfPresent(pos.chunk().toLong()); if (playerchunk != null) { playerchunk.sectionLightChanged(type, pos.y()); } - }); + }; // Folia - region threading + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( + this.level, pos.getX(), pos.getZ(), run + ); + // Folia end - region threading } public void addRegionTicket(TicketType ticketType, ChunkPos pos, int radius, T argument) { @@ -689,7 +694,8 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon @Nullable @VisibleForDebug public NaturalSpawner.SpawnState getLastSpawnState() { - return this.lastSpawnState; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading + return worldData == null ? null : worldData.lastSpawnState; // Folia - region threading } public void removeTicketsOnClosing() { @@ -729,8 +735,43 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon return ServerChunkCache.this.mainThread; } + // Folia start - region threading + @Override + public void tell(Runnable runnable) { + if (true) { + throw new UnsupportedOperationException(); + } + super.tell(runnable); + } + + @Override + public void executeBlocking(Runnable runnable) { + if (true) { + throw new UnsupportedOperationException(); + } + super.executeBlocking(runnable); + } + + @Override + public void execute(Runnable runnable) { + if (true) { + throw new UnsupportedOperationException(); + } + super.execute(runnable); + } + + @Override + public void executeIfPossible(Runnable runnable) { + if (true) { + throw new UnsupportedOperationException(); + } + super.executeIfPossible(runnable); + } + // Folia end - region threading + @Override protected void doRunTask(Runnable task) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading ServerChunkCache.this.level.getProfiler().incrementCounter("runTask"); super.doRunTask(task); } @@ -738,12 +779,17 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon @Override // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task public boolean pollTask() { + // Folia start - region threading + if (ServerChunkCache.this.level != io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().world) { + throw new IllegalStateException("Polling tasks from non-owned region"); + } + // Folia end - region threading // Paper start - rewrite chunk system final ServerChunkCache serverChunkCache = ServerChunkCache.this; if (serverChunkCache.runDistanceManagerUpdates()) { return true; } else { - return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask(); + return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getData().getTaskQueueData().executeChunkTask(); // Folia - region threading } // Paper end - rewrite chunk system // CraftBukkit end diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 2fe9d9b38c01d04416843fdd48d3e33899b7de63..489b0019a859462756634a144952eb7e1fe973e3 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -198,37 +198,35 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. private final MinecraftServer server; public final PrimaryLevelData serverLevelData; // CraftBukkit - type private int lastSpawnChunkRadius; - final EntityTickList entityTickList; + //final EntityTickList entityTickList; // Folia - region threading // Paper - rewrite chunk system private final GameEventDispatcher gameEventDispatcher; public boolean noSave; private final SleepStatus sleepStatus; private int emptyTime; private final PortalForcer portalForcer; - private final LevelTicks blockTicks; - private final LevelTicks fluidTicks; - private final PathTypeCache pathTypesByPosCache; + //private final LevelTicks blockTicks; // Folia - region threading + //private final LevelTicks fluidTicks; // Folia - region threading + //private final PathTypeCache pathTypesByPosCache; // Folia - region threading final Set navigatingMobs; volatile boolean isUpdatingNavigations; protected final Raids raids; - private final ObjectLinkedOpenHashSet blockEvents; - private final List blockEventsToReschedule; - private boolean handlingTick; + //private final ObjectLinkedOpenHashSet blockEvents; // Folia - region threading + //private final List blockEventsToReschedule; // Folia - region threading + //private boolean handlingTick; // Folia - region threading private final List customSpawners; @Nullable private EndDragonFight dragonFight; final Int2ObjectMap dragonParts; private final StructureManager structureManager; private final StructureCheck structureCheck; - private final boolean tickTime; + public final boolean tickTime; // Folia - region threading private final RandomSequences randomSequences; // CraftBukkit start public final LevelStorageSource.LevelStorageAccess convertable; public final UUID uuid; - public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent - public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent - private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) + // Folia - region threading - move to regionised world data public LevelChunk getChunkIfLoaded(int x, int z) { return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately @@ -258,6 +256,36 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. ServerChunkCache chunkProvider = this.getChunkSource(); + // Folia start - region threading + // don't let players move into regions not owned + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, minChunkX, minChunkZ, maxChunkX, maxChunkZ)) { + return false; + } + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { + for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { + if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) { + return false; + } + } + } + + return true; + } + + public final boolean isAreaLoaded(final BlockPos center, final int radius) { + int minX = (center.getX() - radius) >> 4; + int minZ = (center.getZ() - radius) >> 4; + int maxX = (center.getX() + radius) >> 4; + int maxZ = (center.getZ() + radius) >> 4; + + return this.isAreaLoaded(minX, minZ, maxX, maxZ); + } + + public final boolean isAreaLoaded(final int minChunkX, final int minChunkZ, final int maxChunkX, final int maxChunkZ) { + // Folia end - region threading + ServerChunkCache chunkProvider = this.getChunkSource(); + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { for (int cz = minChunkZ; cz <= maxChunkZ; ++cz) { if (chunkProvider.getChunkAtIfLoadedImmediately(cx, cz) == null) { @@ -306,11 +334,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler; private long lastMidTickFailure; private long tickedBlocksOrFluids; - private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this); - private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; - private final ca.spottedleaf.moonrise.common.list.ReferenceList loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); - private final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); - private final ca.spottedleaf.moonrise.common.list.ReferenceList entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); + // Folia - region threading - move to regionized data @Override public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { @@ -368,7 +392,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. @Override public final int moonrise$getRegionChunkShift() { - return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); + return this.regioniser.sectionChunkShift; // Folia - region threading } @Override @@ -467,35 +491,87 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. @Override public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() { - return this.nearbyPlayers; + return this.getCurrentWorldData().getNearbyPlayers(); // Folia - region threading } @Override public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getLoadedChunks() { - return this.loadedChunks; + return this.getCurrentWorldData().getChunks(); // Folia - region threading } @Override public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getTickingChunks() { - return this.tickingChunks; + return this.getCurrentWorldData().getTickingChunks(); // Folia - region threading } @Override public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getEntityTickingChunks() { - return this.entityTickingChunks; + return this.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading } // Paper end - rewrite chunk system // Paper start - lag compensation private long lagCompensationTick = net.minecraft.server.MinecraftServer.SERVER_INIT; public long getLagCompensationTick() { - return this.lagCompensationTick; + return this.getCurrentWorldData().getLagCompensationTick(); // Folia - region threading } public void updateLagCompensationTick() { - this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); + throw new UnsupportedOperationException(); // Folia - region threading } // Paper end - lag compensation + // Folia start - region threading + public final io.papermc.paper.threadedregions.TickRegions tickRegions = new io.papermc.paper.threadedregions.TickRegions(); + public final io.papermc.paper.threadedregions.ThreadedRegionizer regioniser; + { + this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegionizer<>( + (int)Math.max(1L, (8L * 16L * 16L) / (1L << (2 * (io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())))), + (1.0 / 6.0), + Math.max(1, 8 / (1 << io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())), + 1, + io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(), + this, + this.tickRegions + ); + } + public final io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData(this); + public static final int WORLD_INIT_NOT_CHECKED = 0; + public static final int WORLD_INIT_CHECKING = 1; + public static final int WORLD_INIT_CHECKED = 2; + public final java.util.concurrent.atomic.AtomicInteger checkInitialised = new java.util.concurrent.atomic.AtomicInteger(WORLD_INIT_NOT_CHECKED); + public ChunkPos randomSpawnSelection; + + public static final record PendingTeleport(Entity.EntityTreeNode rootVehicle, Vec3 to) {} + private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet pendingTeleports = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); + + public void pushPendingTeleport(final PendingTeleport teleport) { + synchronized (this.pendingTeleports) { + this.pendingTeleports.add(teleport); + } + } + + public boolean removePendingTeleport(final PendingTeleport teleport) { + synchronized (this.pendingTeleports) { + return this.pendingTeleports.remove(teleport); + } + } + + public List removeAllRegionTeleports() { + final List ret = new ArrayList<>(); + + synchronized (this.pendingTeleports) { + for (final Iterator iterator = this.pendingTeleports.iterator(); iterator.hasNext(); ) { + final PendingTeleport pendingTeleport = iterator.next(); + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, pendingTeleport.to())) { + ret.add(pendingTeleport); + iterator.remove(); + } + } + } + + return ret; + } + // Folia end - region threading // Add env and gen to constructor, IWorldDataServer -> WorldDataServer public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { @@ -508,14 +584,14 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. this.convertable = convertable_conversionsession; this.uuid = WorldUUID.getUUID(convertable_conversionsession.levelDirectory.path().toFile()); // CraftBukkit end - this.players = Lists.newArrayList(); - this.entityTickList = new EntityTickList(); - this.blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); - this.fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); - this.pathTypesByPosCache = new PathTypeCache(); + this.players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - region threading + //this.entityTickList = new EntityTickList(); // Folia - region threading + //this.blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); // Folia - moved to RegioniedWorldData + //this.fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded, this.getProfilerSupplier()); // Folia - moved to RegioniedWorldData + //this.pathTypesByPosCache = new PathTypeCache(); // Folia - moved to RegioniedWorldData this.navigatingMobs = new ObjectOpenHashSet(); - this.blockEvents = new ObjectLinkedOpenHashSet(); - this.blockEventsToReschedule = new ArrayList(64); + //this.blockEvents = new ObjectLinkedOpenHashSet(); // Folia - moved to RegioniedWorldData + //this.blockEventsToReschedule = new ArrayList(64); // Folia - moved to RegioniedWorldData this.dragonParts = new Int2ObjectOpenHashMap(); this.tickTime = flag1; this.server = minecraftserver; @@ -553,7 +629,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. }); this.chunkSource.getGeneratorState().ensureStructuresGenerated(); this.portalForcer = new PortalForcer(this); - this.updateSkyBrightness(); + //this.updateSkyBrightness(); // Folia - region threading - delay until first tick this.prepareWeather(); this.getWorldBorder().setAbsoluteMaxSize(minecraftserver.getAbsoluteMaxWorldSize()); this.raids = (Raids) this.getDataStorage().computeIfAbsent(Raids.factory(this), Raids.getFileId(this.dimensionTypeRegistration())); @@ -590,7 +666,14 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. this.chunkTaskScheduler = new ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler((ServerLevel)(Object)this, ca.spottedleaf.moonrise.common.util.MoonriseCommon.WORKER_POOL); // Paper end - rewrite chunk system this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit + this.updateTickData(); // Folia - region threading - make sure it is initialised before ticked + } + + // Folia start - region threading + public void updateTickData() { + this.tickData = new io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData(this, this.serverLevelData.getGameTime(), this.serverLevelData.getDayTime()); } + // Folia end - region threading // Paper start @Override @@ -619,48 +702,32 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. return this.getChunkSource().getGenerator().getBiomeSource().getNoiseBiome(biomeX, biomeY, biomeZ, this.getChunkSource().randomState().sampler()); } + @Override // Folia - region threading public StructureManager structureManager() { return this.structureManager; } - public void tick(BooleanSupplier shouldKeepTicking) { + public void tick(BooleanSupplier shouldKeepTicking, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking + final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking ProfilerFiller gameprofilerfiller = this.getProfiler(); - this.handlingTick = true; + regionizedWorldData.setHandlingTick(true); // Folia - regionised ticking TickRateManager tickratemanager = this.tickRateManager(); boolean flag = tickratemanager.runsNormally(); if (flag) { gameprofilerfiller.push("world border"); - this.getWorldBorder().tick(); + if (region == null) this.getWorldBorder().tick(); // Folia - regionised ticking gameprofilerfiller.popPush("weather"); - this.advanceWeatherCycle(); + if (region == null) this.advanceWeatherCycle(); // Folia - regionised ticking } - int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); + //int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); // Folia - region threading - move into tickSleep long j; - if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { - // CraftBukkit start - j = this.levelData.getDayTime() + 24000L; - TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); - if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { - this.getCraftServer().getPluginManager().callEvent(event); - if (!event.isCancelled()) { - this.setDayTime(this.getDayTime() + event.getSkipAmount()); - } - } - - if (!event.isCancelled()) { - this.wakeUpAllPlayers(); - } - // CraftBukkit end - if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { - this.resetWeatherCycle(); - } - } + if (region == null) this.tickSleep(); // Folia - region threading - this.updateSkyBrightness(); + if (region == null) this.updateSkyBrightness(); // Folia - region threading if (flag) { this.tickTime(); } @@ -668,11 +735,11 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. gameprofilerfiller.popPush("tickPending"); this.timings.scheduledBlocks.startTiming(); // Paper if (!this.isDebug() && flag) { - j = this.getGameTime(); + j = regionizedWorldData.getRedstoneGameTime(); // Folia - region threading gameprofilerfiller.push("blockTicks"); - this.blockTicks.tick(j, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks + regionizedWorldData.getBlockLevelTicks().tick(j, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks // Folia - region ticking gameprofilerfiller.popPush("fluidTicks"); - this.fluidTicks.tick(j, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks + regionizedWorldData.getFluidLevelTicks().tick(j, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks // Folia - region ticking gameprofilerfiller.pop(); } this.timings.scheduledBlocks.stopTiming(); // Paper @@ -695,9 +762,9 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. this.timings.doSounds.stopTiming(); // Spigot } - this.handlingTick = false; + regionizedWorldData.setHandlingTick(false); // Folia - regionised ticking gameprofilerfiller.pop(); - boolean flag1 = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this + boolean flag1 = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this // Folia - unrestore this, we always need to tick empty worlds if (flag1) { this.resetEmptyTime(); @@ -707,20 +774,30 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. gameprofilerfiller.push("entities"); this.timings.tickEntities.startTiming(); // Spigot if (this.dragonFight != null && flag) { + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, this.dragonFight.origin)) { // Folia - region threading gameprofilerfiller.push("dragonFight"); this.dragonFight.tick(); gameprofilerfiller.pop(); + } else { // Folia start - region threading + // try to load dragon fight + ChunkPos fightCenter = new ChunkPos(this.dragonFight.origin); + this.chunkSource.addTicketAtLevel( + TicketType.UNKNOWN, fightCenter, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + fightCenter + ); + } // Folia end - region threading } org.spigotmc.ActivationRange.activateEntities(this); // Spigot this.timings.entityTick.startTiming(); // Spigot - this.entityTickList.forEach((entity) -> { + regionizedWorldData.forEachTickingEntity((entity) -> { // Folia - regionised ticking if (!entity.isRemoved()) { if (false && this.shouldDiscardEntity(entity)) { // CraftBukkit - We prevent spawning in general, so this butchering is not needed entity.discard(); } else if (!tickratemanager.isEntityFrozen(entity)) { gameprofilerfiller.push("checkDespawn"); entity.checkDespawn(); + if (entity.isRemoved()) return; // Folia - region threading - if we despawned, DON'T TICK IT! gameprofilerfiller.pop(); if (true || this.chunkSource.chunkMap.getDistanceManager().inEntityTickingRange(entity.chunkPosition().toLong())) { // Paper - rewrite chunk system Entity entity1 = entity.getVehicle(); @@ -751,6 +828,31 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. gameprofilerfiller.pop(); } + // Folia start - region threading + public void tickSleep() { + int i = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); long j; // Folia moved from tick loop + if (this.sleepStatus.areEnoughSleeping(i) && this.sleepStatus.areEnoughDeepSleeping(i, this.players)) { + // CraftBukkit start + j = this.levelData.getDayTime() + 24000L; + TimeSkipEvent event = new TimeSkipEvent(this.getWorld(), TimeSkipEvent.SkipReason.NIGHT_SKIP, (j - j % 24000L) - this.getDayTime()); + if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { + this.getCraftServer().getPluginManager().callEvent(event); + if (!event.isCancelled()) { + this.setDayTime(this.getDayTime() + event.getSkipAmount()); + } + } + + if (!event.isCancelled()) { + this.wakeUpAllPlayers(); + } + // CraftBukkit end + if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { + this.resetWeatherCycle(); + } + } + } + // Folia end - region threading + @Override public boolean shouldTickBlocksAt(long chunkPos) { // Paper start - rewrite chunk system @@ -761,11 +863,12 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. protected void tickTime() { if (this.tickTime) { - long i = this.levelData.getGameTime() + 1L; + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading + long i = regionizedWorldData.getRedstoneGameTime() + 1L; // Folia - region threading - this.serverLevelData.setGameTime(i); - this.serverLevelData.getScheduledEvents().tick(this.server, i); - if (this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { + regionizedWorldData.setRedstoneGameTime(i); // Folia - region threading + if (false) this.serverLevelData.getScheduledEvents().tick(this.server, i); // Folia - region threading - TODO any way to bring this in? + if (false && this.levelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { // Folia - region threading this.setDayTime(this.levelData.getDayTime() + 1L); } @@ -794,7 +897,14 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. private void wakeUpAllPlayers() { this.sleepStatus.removeAllSleepers(); (this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList())).forEach((entityplayer) -> { // CraftBukkit - decompile error - entityplayer.stopSleepInBed(false, false); + // Folia start - region threading + entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + if (player.level() != ServerLevel.this || !player.isSleeping()) { + return; + } + player.stopSleepInBed(false, false); + }, null, 1L); + // Folia end - region threading }); } @@ -995,7 +1105,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } public boolean isHandlingTick() { - return this.handlingTick; + return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking } public boolean canSleepThroughNights() { @@ -1027,6 +1137,14 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } public void updateSleepingPlayerList() { + // Folia start - region threading + if (!io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + ServerLevel.this.updateSleepingPlayerList(); + }); + return; + } + // Folia end - region threading if (!this.players.isEmpty() && this.sleepStatus.update(this.players)) { this.announceSleepStatus(); } @@ -1038,7 +1156,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. return this.server.getScoreboard(); } - private void advanceWeatherCycle() { + public void advanceWeatherCycle() { // Folia - region threading - public boolean flag = this.isRaining(); if (this.dimensionType().hasSkyLight()) { @@ -1124,23 +1242,24 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. this.server.getPlayerList().broadcastAll(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.THUNDER_LEVEL_CHANGE, this.thunderLevel)); } // */ - for (int idx = 0; idx < this.players.size(); ++idx) { - if (((ServerPlayer) this.players.get(idx)).level() == this) { - ((ServerPlayer) this.players.get(idx)).tickWeather(); + ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading + for (ServerPlayer player : players) { // Folia - region threading + if (player.level() == this) { // Folia - region threading + player.tickWeather(); // Folia - region threading } } if (flag != this.isRaining()) { // Only send weather packets to those affected - for (int idx = 0; idx < this.players.size(); ++idx) { - if (((ServerPlayer) this.players.get(idx)).level() == this) { - ((ServerPlayer) this.players.get(idx)).setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false); + for (ServerPlayer player : players) { // Folia - region threading + if (player.level() == this) { // Folia - region threading + player.setPlayerWeather((!flag ? WeatherType.DOWNFALL : WeatherType.CLEAR), false); // Folia - region threading } } } - for (int idx = 0; idx < this.players.size(); ++idx) { - if (((ServerPlayer) this.players.get(idx)).level() == this) { - ((ServerPlayer) this.players.get(idx)).updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); + for (ServerPlayer player : players) { // Folia - region threading + if (player.level() == this) { // Folia - region threading + player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); // Folia - region threading } } // CraftBukkit end @@ -1246,7 +1365,16 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. if (isActive) { // Paper - EAR 2 TimingHistory.activatedEntityTicks++; entity.tick(); - entity.postTick(); // CraftBukkit + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { + // removed from region while ticking + return; + } + if (entity.handlePortal()) { + // portalled + return; + } + // Folia end - region threading } else { entity.inactiveTick(); } // Paper - EAR 2 this.getProfiler().pop(); } finally { timer.stopTiming(); } // Paper - timings @@ -1269,7 +1397,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. private void tickPassenger(Entity vehicle, Entity passenger) { if (!passenger.isRemoved() && passenger.getVehicle() == vehicle) { - if (passenger instanceof Player || this.entityTickList.contains(passenger)) { + if (passenger instanceof Player || this.getCurrentWorldData().hasEntityTickingEntity(passenger)) { // Folia - region threading // Paper - EAR 2 final boolean isActive = org.spigotmc.ActivationRange.checkIfActive(passenger); co.aikar.timings.Timing timer = isActive ? passenger.getType().passengerTickTimer.startTiming() : passenger.getType().passengerInactiveTickTimer.startTiming(); // Paper @@ -1286,7 +1414,16 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. // Paper start - EAR 2 if (isActive) { passenger.rideTick(); - passenger.postTick(); // CraftBukkit + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(passenger)) { + // removed from region while ticking + return; + } + if (passenger.handlePortal()) { + // portalled + return; + } + // Folia end - region threading } else { passenger.setDeltaMovement(Vec3.ZERO); passenger.inactiveTick(); @@ -1381,20 +1518,22 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } // Paper end - add close param - // CraftBukkit start - moved from MinecraftServer.saveChunks - ServerLevel worldserver1 = this; - - this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); - this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); - this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); - // CraftBukkit end + // Folia - move into saveLevelData } - private void saveLevelData(boolean async) { // Paper - Write SavedData IO async + public void saveLevelData(boolean async) { // Paper - Write SavedData IO async // Folia - public if (this.dragonFight != null) { this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit } + // Folia start - moved into saveLevelData + ServerLevel worldserver1 = this; + + this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); + this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); + this.convertable.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); + // Folia end - moved into saveLevelData + this.getChunkSource().getDataStorage().save(async); // Paper - Write SavedData IO async } @@ -1448,6 +1587,19 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. return list; } + // Folia start - region threading + @Nullable + public ServerPlayer getRandomLocalPlayer() { + List list = this.getLocalPlayers(); + list = new java.util.ArrayList<>(list); + list.removeIf((ServerPlayer player) -> { + return !player.isAlive(); + }); + + return list.isEmpty() ? null : (ServerPlayer) list.get(this.random.nextInt(list.size())); + } + // Folia end - region threading + @Nullable public ServerPlayer getRandomPlayer() { List list = this.getPlayers(LivingEntity::isAlive); @@ -1532,8 +1684,8 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } else { if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added // Paper start - capture all item additions to the world - if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { - captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); + if (this.getCurrentWorldData().captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { // Folia - region threading + this.getCurrentWorldData().captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); // Folia - region threading return true; } // Paper end - capture all item additions to the world @@ -1684,21 +1836,22 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. @Override public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) { - if (this.isUpdatingNavigations) { + if (false && this.isUpdatingNavigations) { // Folia - region threading String s = "recursive call to sendBlockUpdated"; Util.logAndPauseIfInIde("recursive call to sendBlockUpdated", new IllegalStateException("recursive call to sendBlockUpdated")); } this.getChunkSource().blockChanged(pos); - this.pathTypesByPosCache.invalidate(pos); + final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading + regionizedWorldData.pathTypesByPosCache.invalidate(pos); // Folia - region threading if (this.paperConfig().misc.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates VoxelShape voxelshape = oldState.getCollisionShape(this, pos); VoxelShape voxelshape1 = newState.getCollisionShape(this, pos); if (Shapes.joinIsNotEmpty(voxelshape, voxelshape1, BooleanOp.NOT_SAME)) { List list = new ObjectArrayList(); - Iterator iterator = this.navigatingMobs.iterator(); + Iterator iterator = regionizedWorldData.getNavigatingMobs(); // Folia - region threading while (iterator.hasNext()) { // CraftBukkit start - fix SPIGOT-6362 @@ -1721,7 +1874,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } try { - this.isUpdatingNavigations = true; + //this.isUpdatingNavigations = true; // Folia - region threading iterator = list.iterator(); while (iterator.hasNext()) { @@ -1730,7 +1883,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. navigationabstract1.recomputePath(); } } finally { - this.isUpdatingNavigations = false; + //this.isUpdatingNavigations = false; // Folia - region threading } } @@ -1739,23 +1892,23 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. @Override public void updateNeighborsAt(BlockPos pos, Block sourceBlock) { - if (captureBlockStates) { return; } // Paper - Cancel all physics during placement - this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null); + if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading + this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, (Direction) null); // Folia - region threading } @Override public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block sourceBlock, Direction direction) { - this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction); + this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, sourceBlock, direction); // Folia - region threading } @Override public void neighborChanged(BlockPos pos, Block sourceBlock, BlockPos sourcePos) { - this.neighborUpdater.neighborChanged(pos, sourceBlock, sourcePos); + this.getCurrentWorldData().neighborUpdater.neighborChanged(pos, sourceBlock, sourcePos); // Folia - region threading } @Override public void neighborChanged(BlockState state, BlockPos pos, Block sourceBlock, BlockPos sourcePos, boolean notify) { - this.neighborUpdater.neighborChanged(state, pos, sourceBlock, sourcePos, notify); + this.getCurrentWorldData().neighborUpdater.neighborChanged(state, pos, sourceBlock, sourcePos, notify); // Folia - region threading } @Override @@ -1786,7 +1939,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. explosion.clearToBlow(); } - Iterator iterator = this.players.iterator(); + Iterator iterator = this.getLocalPlayers().iterator(); // Folia - region thraeding while (iterator.hasNext()) { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); @@ -1801,25 +1954,28 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. @Override public void blockEvent(BlockPos pos, Block block, int type, int data) { - this.blockEvents.add(new BlockEventData(pos, block, type, data)); + this.getCurrentWorldData().pushBlockEvent(new BlockEventData(pos, block, type, data)); // Folia - regionised ticking } private void runBlockEvents() { - this.blockEventsToReschedule.clear(); + List blockEventsToReschedule = new ArrayList<>(64); // Folia - regionised ticking - while (!this.blockEvents.isEmpty()) { - BlockEventData blockactiondata = (BlockEventData) this.blockEvents.removeFirst(); + // Folia start - regionised ticking + io.papermc.paper.threadedregions.RegionizedWorldData worldRegionData = this.getCurrentWorldData(); + BlockEventData blockactiondata; + while ((blockactiondata = worldRegionData.removeFirstBlockEvent()) != null) { + // Folia end - regionised ticking if (this.shouldTickBlocksAt(blockactiondata.pos())) { if (this.doBlockEvent(blockactiondata)) { this.server.getPlayerList().broadcast((Player) null, (double) blockactiondata.pos().getX(), (double) blockactiondata.pos().getY(), (double) blockactiondata.pos().getZ(), 64.0D, this.dimension(), new ClientboundBlockEventPacket(blockactiondata.pos(), blockactiondata.block(), blockactiondata.paramA(), blockactiondata.paramB())); } } else { - this.blockEventsToReschedule.add(blockactiondata); + blockEventsToReschedule.add(blockactiondata); // Folia - regionised ticking } } - this.blockEvents.addAll(this.blockEventsToReschedule); + worldRegionData.pushBlockEvents(blockEventsToReschedule); // Folia - regionised ticking } private boolean doBlockEvent(BlockEventData event) { @@ -1830,12 +1986,12 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. @Override public LevelTicks getBlockTicks() { - return this.blockTicks; + return this.getCurrentWorldData().getBlockLevelTicks(); // Folia - region ticking } @Override public LevelTicks getFluidTicks() { - return this.fluidTicks; + return this.getCurrentWorldData().getFluidLevelTicks(); // Folia - region ticking } @Nonnull @@ -1859,7 +2015,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. public int sendParticles(ServerPlayer sender, T t0, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6, boolean force) { // Paper start - Particle API - return sendParticles(players, sender, t0, d0, d1, d2, i, d3, d4, d5, d6, force); + return sendParticles(this.getLocalPlayers(), sender, t0, d0, d1, d2, i, d3, d4, d5, d6, force); // Folia - region threading } public int sendParticles(List receivers, @Nullable ServerPlayer sender, T t0, double d0, double d1, double d2, int i, double d3, double d4, double d5, double d6, boolean force) { // Paper end - Particle API @@ -1912,7 +2068,14 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. public Entity getEntityOrPart(int id) { Entity entity = (Entity) this.getEntities().get(id); - return entity != null ? entity : (Entity) this.dragonParts.get(id); + // Folia start - region threading + if (entity != null) { + return entity; + } + synchronized (this.dragonParts) { + return this.dragonParts.get(id); + } + // Folia end - region threading } @Nullable @@ -1967,6 +2130,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. // Paper start - Call missing map initialize event and set id final DimensionDataStorage storage = this.getServer().overworld().getDataStorage(); + synchronized (storage.cache) { // Folia - region threading final net.minecraft.world.level.saveddata.SavedData existing = storage.cache.get(id.key()); if (existing == null && !storage.cache.containsKey(id.key())) { final MapItemSavedData worldmap = (MapItemSavedData) this.getServer().overworld().getDataStorage().get(MapItemSavedData.factory(), id.key()); @@ -1981,6 +2145,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } return existing instanceof MapItemSavedData data ? data : null; + } // Folia - region threading // Paper end - Call missing map initialize event and set id } @@ -2030,6 +2195,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } public boolean setChunkForced(int x, int z, boolean forced) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify force loaded chunks off of the global region"); // Folia - region threading ForcedChunksSavedData forcedchunk = (ForcedChunksSavedData) this.getDataStorage().computeIfAbsent(ForcedChunksSavedData.factory(), "chunks"); ChunkPos chunkcoordintpair = new ChunkPos(x, z); long k = chunkcoordintpair.toLong(); @@ -2038,7 +2204,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. if (forced) { flag1 = forcedchunk.getChunks().add(k); if (flag1) { - this.getChunk(x, z); + //this.getChunk(x, z); // Folia - region threading - we must let the chunk load asynchronously } } else { flag1 = forcedchunk.getChunks().remove(k); @@ -2066,13 +2232,18 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. BlockPos blockposition1 = pos.immutable(); optional.ifPresent((holder) -> { - this.getServer().execute(() -> { + Runnable run = () -> { // Folia - region threading this.getPoiManager().remove(blockposition1); DebugPackets.sendPoiRemovedPacket(this, blockposition1); - }); + }; // Folia - region threading + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( + this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run + ); + // Folia end - region threading }); optional1.ifPresent((holder) -> { - this.getServer().execute(() -> { + Runnable run = () -> { // Folia - region threading // Paper start - Remove stale POIs if (optional.isEmpty() && this.getPoiManager().exists(blockposition1, poiType -> true)) { this.getPoiManager().remove(blockposition1); @@ -2080,7 +2251,12 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. // Paper end - Remove stale POIs this.getPoiManager().add(blockposition1, holder); DebugPackets.sendPoiAddedPacket(this, blockposition1); - }); + }; // Folia - region threading + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( + this, blockposition1.getX() >> 4, blockposition1.getZ() >> 4, run + ); + // Folia end - region threading }); } } @@ -2127,7 +2303,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. BufferedWriter bufferedwriter = Files.newBufferedWriter(path.resolve("stats.txt")); try { - bufferedwriter.write(String.format(Locale.ROOT, "spawning_chunks: %d\n", playerchunkmap.getDistanceManager().getNaturalSpawnChunkCount())); + //bufferedwriter.write(String.format(Locale.ROOT, "spawning_chunks: %d\n", playerchunkmap.getDistanceManager().getNaturalSpawnChunkCount())); // Folia - region threading NaturalSpawner.SpawnState spawnercreature_d = this.getChunkSource().getLastSpawnState(); if (spawnercreature_d != null) { @@ -2141,7 +2317,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } bufferedwriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system - bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); + //bufferedwriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); // Folia - region threading bufferedwriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count())); bufferedwriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count())); bufferedwriter.write("distance_manager: " + playerchunkmap.getDistanceManager().getDebugStatus() + "\n"); @@ -2287,7 +2463,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. private void dumpBlockEntityTickers(Writer writer) throws IOException { CsvOutput csvwriter = CsvOutput.builder().addColumn("x").addColumn("y").addColumn("z").addColumn("type").build(writer); - Iterator iterator = this.blockEntityTickers.iterator(); + Iterator iterator = null; // Folia - region threading while (iterator.hasNext()) { TickingBlockEntity tickingblockentity = (TickingBlockEntity) iterator.next(); @@ -2300,7 +2476,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. @VisibleForTesting public void clearBlockEvents(BoundingBox box) { - this.blockEvents.removeIf((blockactiondata) -> { + this.getCurrentWorldData().removeIfBlockEvents((blockactiondata) -> { // Folia - regionised ticking return box.isInside(blockactiondata.pos()); }); } @@ -2309,7 +2485,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. public void blockUpdated(BlockPos pos, Block block) { if (!this.isDebug()) { // CraftBukkit start - if (this.populating) { + if (this.getCurrentWorldData().populating) { // Folia - region threading return; } // CraftBukkit end @@ -2352,9 +2528,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. @VisibleForTesting public String getWatchdogStats() { - return String.format(Locale.ROOT, "players: %s, entities: %s [%s], block_entities: %d [%s], block_ticks: %d, fluid_ticks: %d, chunk_source: %s", this.players.size(), this.moonrise$getEntityLookup().getDebugInfo(), ServerLevel.getTypeCount(this.moonrise$getEntityLookup().getAll(), (entity) -> { // Paper - rewrite chunk system - return BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); - }), this.blockEntityTickers.size(), ServerLevel.getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats()); + return "region threading"; // Folia - region threading } private static String getTypeCount(Iterable items, Function classifier) { @@ -2404,17 +2578,18 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. } public void startTickingChunk(LevelChunk chunk) { - chunk.unpackTicks(this.getLevelData().getGameTime()); + chunk.unpackTicks(this.getRedstoneGameTime()); // Folia - region threading } public void onStructureStartsAvailable(ChunkAccess chunk) { - this.server.execute(() -> { - this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); - }); + // Folia start - region threading + // no longer needs to be on main + this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); + // Folia end - region threading } public PathTypeCache getPathTypeCache() { - return this.pathTypesByPosCache; + return this.getCurrentWorldData().pathTypesByPosCache; // Folia - region threading } @Override @@ -2434,7 +2609,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system } - private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { + public boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { // Folia - region threaded - make public // Paper start - rewrite chunk system final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos); // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded @@ -2494,7 +2669,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. // Paper start - optimize redstone (Alternate Current) @Override public alternate.current.wire.WireHandler getWireHandler() { - return wireHandler; + return this.getCurrentWorldData().wireHandler; // Folia - region threading - move to regionised data } // Paper end - optimize redstone (Alternate Current) @@ -2505,16 +2680,16 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. public void onCreated(Entity entity) {} public void onDestroyed(Entity entity) { - ServerLevel.this.getScoreboard().entityRemoved(entity); + // ServerLevel.this.getScoreboard().entityRemoved(entity); // Folia - region threading } public void onTickingStart(Entity entity) { if (entity instanceof net.minecraft.world.entity.Marker && !paperConfig().entities.markers.tick) return; // Paper - Configurable marker ticking - ServerLevel.this.entityTickList.add(entity); + ServerLevel.this.getCurrentWorldData().addEntityTickingEntity(entity); // Folia - region threading } public void onTickingEnd(Entity entity) { - ServerLevel.this.entityTickList.remove(entity); + ServerLevel.this.getCurrentWorldData().removeEntityTickingEntity(entity); // Folia - region threading // Paper start - Reset pearls when they stop being ticked if (paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { pearl.cachedOwner = null; @@ -2525,6 +2700,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. public void onTrackingStart(Entity entity) { org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot + ServerLevel.this.getCurrentWorldData().addLoadedEntity(entity); // Folia - region threading // ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server; moved down below valid=true if (entity instanceof ServerPlayer entityplayer) { ServerLevel.this.players.add(entityplayer); @@ -2538,7 +2714,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")); } - ServerLevel.this.navigatingMobs.add(entityinsentient); + ServerLevel.this.getCurrentWorldData().addNavigatingMob(entityinsentient); // Folia - region threading } if (entity instanceof EnderDragon entityenderdragon) { @@ -2548,7 +2724,9 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. for (int j = 0; j < i; ++j) { EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; + synchronized (ServerLevel.this.dragonParts) { // Folia - region threading ServerLevel.this.dragonParts.put(entitycomplexpart.getId(), entitycomplexpart); + } // Folia - region threading } } @@ -2570,16 +2748,24 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. public void onTrackingEnd(Entity entity) { org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot + ServerLevel.this.getCurrentWorldData().removeLoadedEntity(entity); // Spigot start if ( entity instanceof Player ) { com.google.common.collect.Streams.stream( ServerLevel.this.getServer().getAllLevels() ).map( ServerLevel::getDataStorage ).forEach( (worldData) -> { - for (Object o : worldData.cache.values() ) + // Folia start - make map data thread-safe + List worldDataCache; + synchronized (worldData.cache) { + worldDataCache = new java.util.ArrayList<>(worldData.cache.values()); + } + for (Object o : worldDataCache ) + // Folia end - make map data thread-safe { if ( o instanceof MapItemSavedData ) { MapItemSavedData map = (MapItemSavedData) o; + synchronized (map) { // Folia - make map data thread-safe map.carriedByPlayers.remove( (Player) entity ); for ( Iterator iter = (Iterator) map.carriedBy.iterator(); iter.hasNext(); ) { @@ -2589,6 +2775,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. iter.remove(); } } + } // Folia - make map data thread-safe } } } ); @@ -2619,7 +2806,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. Util.logAndPauseIfInIde("onTrackingStart called during navigation iteration", new IllegalStateException("onTrackingStart called during navigation iteration")); } - ServerLevel.this.navigatingMobs.remove(entityinsentient); + ServerLevel.this.getCurrentWorldData().removeNavigatingMob(entityinsentient); // Folia - region threading } if (entity instanceof EnderDragon entityenderdragon) { @@ -2629,13 +2816,16 @@ public class ServerLevel extends Level implements WorldGenLevel, ca.spottedleaf. for (int j = 0; j < i; ++j) { EnderDragonPart entitycomplexpart = aentitycomplexpart[j]; + synchronized (ServerLevel.this.dragonParts) { // Folia - region threading ServerLevel.this.dragonParts.remove(entitycomplexpart.getId()); + } // Folia - region threading } } entity.updateDynamicGameEventListener(DynamicGameEventListener::remove); // CraftBukkit start entity.valid = false; + // Folia - region threading - TODO THIS SHIT if (!(entity instanceof ServerPlayer)) { for (ServerPlayer player : ServerLevel.this.players) { player.getBukkitEntity().onEntityRemove(entity); diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java index 763cffdc2e1e2e7cc9af88cc46bbaa240a20fd0d..9de40f3c74d6fcf790d27388327896f97abb4c42 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -15,6 +15,7 @@ import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; import java.util.Set; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.annotation.Nullable; import net.minecraft.ChatFormatting; @@ -203,7 +204,7 @@ import org.bukkit.inventory.MainHand; public class ServerPlayer extends net.minecraft.world.entity.player.Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system private static final Logger LOGGER = LogUtils.getLogger(); - public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving + public long lastSave = Long.MIN_VALUE; // Paper // Folia - threaded regions - changed to nanoTime private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; private static final int FLY_STAT_RECORDING_SPEED = 25; @@ -483,8 +484,153 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple } // CraftBukkit end + // Folia start - region threading + private static final int SPAWN_RADIUS_SELECTION_SEARCH = 5; + + private static BlockPos getRandomSpawn(ServerLevel world, RandomSource random) { + BlockPos spawn = world.getSharedSpawnPos(); + double radius = (double)Math.max(0, world.getGameRules().getInt(GameRules.RULE_SPAWN_RADIUS)); + + double spawnX = (double)spawn.getX() + 0.5; + double spawnZ = (double)spawn.getZ() + 0.5; + + net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder(); + + double selectMinX = Math.max(worldBorder.getMinX() + 1.0, spawnX - radius); + double selectMinZ = Math.max(worldBorder.getMinZ() + 1.0, spawnZ - radius); + double selectMaxX = Math.min(worldBorder.getMaxX() - 1.0, spawnX + radius); + double selectMaxZ = Math.min(worldBorder.getMaxZ() - 1.0, spawnZ + radius); + + double amountX = selectMaxX - selectMinX; + double amountZ = selectMaxZ - selectMinZ; + + int selectX = amountX < 1.0 ? Mth.floor(worldBorder.getCenterX()) : (int)Mth.floor((amountX + 1.0) * random.nextDouble() + selectMinX); + int selectZ = amountZ < 1.0 ? Mth.floor(worldBorder.getCenterZ()) : (int)Mth.floor((amountZ + 1.0) * random.nextDouble() + selectMinZ); + + return new BlockPos(selectX, 0, selectZ); + } + + private static void completeSpawn(ServerLevel world, BlockPos selected, + ca.spottedleaf.concurrentutil.completable.Completable toComplete) { + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(selected), world.levelData.getSpawnAngle(), 0.0f)); + } + + private static BlockPos findSpawnAround(ServerLevel world, ServerPlayer player, BlockPos selected) { + // try hard to find, so that we don't attempt another chunk load + for (int dz = -SPAWN_RADIUS_SELECTION_SEARCH; dz <= SPAWN_RADIUS_SELECTION_SEARCH; ++dz) { + for (int dx = -SPAWN_RADIUS_SELECTION_SEARCH; dx <= SPAWN_RADIUS_SELECTION_SEARCH; ++dx) { + BlockPos inChunk = PlayerRespawnLogic.getOverworldRespawnPos(world, selected.getX(), selected.getZ()); + if (inChunk == null) { + continue; + } + + AABB checkVolume = player.getBoundingBoxAt((double)inChunk.getX() + 0.5, (double)inChunk.getY(), (double)inChunk.getZ() + 0.5); + + if (!world.noCollision(player, checkVolume)) { + continue; + } + + return inChunk; + } + } + + return null; + } + + // rets false when another attempt is required + private static boolean trySpawnOrSchedule(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts, + ca.spottedleaf.concurrentutil.completable.Completable toComplete) { + ++attemptCount[0]; + + BlockPos rough = getRandomSpawn(world, random); + + // add 2 to ensure that the chunks are loaded for collision checks + int minX = (rough.getX() - (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; + int minZ = (rough.getZ() - (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; + int maxX = (rough.getX() + (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; + int maxZ = (rough.getZ() + (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; + + // we could short circuit this check, but it would possibly recurse. Then, it could end up causing a stack overflow + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, minX, minZ, maxX, maxZ) || !world.isAreaLoaded(minX, minZ, maxX, maxZ)) { + world.moonrise$loadChunksAsync(minX, maxX, minZ, maxZ, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (unused) -> { + BlockPos selected = findSpawnAround(world, player, rough); + if (selected == null) { + // run more spawn attempts + selectSpawn(world, player, random, attemptCount, maxAttempts, toComplete); + return; + } + + completeSpawn(world, selected, toComplete); + return; + } + ); + return true; + } + + BlockPos selected = findSpawnAround(world, player, rough); + if (selected == null) { + return false; + } + + completeSpawn(world, selected, toComplete); + return true; + } + + private static void selectSpawn(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts, + ca.spottedleaf.concurrentutil.completable.Completable toComplete) { + do { + if (attemptCount[0] >= maxAttempts) { + BlockPos sharedSpawn = world.getSharedSpawnPos(); + BlockPos selected = world.getWorldBorder().clampToBounds((double)sharedSpawn.getX(), (double)sharedSpawn.getY(), (double)sharedSpawn.getZ()); + + LOGGER.warn("Found no spawn in radius for player '" + player.getName() + "', ignoring radius"); + + // this call requires to return a location with loaded chunks, so we need to schedule a load here + ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad( + world, selected.getX() >> 4, selected.getZ() >> 4, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + true, ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (unused) -> { + completeSpawn(world, selected, toComplete); + } + ); + return; + } + } while (!trySpawnOrSchedule(world, player, random, attemptCount, maxAttempts, toComplete)); + } + + public static void fudgeSpawnLocation(ServerLevel world, ServerPlayer player, ca.spottedleaf.concurrentutil.completable.Completable toComplete) { // Folia - region threading + BlockPos blockposition = world.getSharedSpawnPos(); + + if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit + selectSpawn(world, player, player.random, new int[1], 500, toComplete); + } else { + world.loadChunksForMoveAsync(player.getBoundingBoxAt(blockposition.getX() + 0.5, blockposition.getY(), blockposition.getZ() + 0.5), + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (c) -> { + BlockPos ret = blockposition; + while (!world.noCollision(player, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY(), ret.getZ() + 0.5)) && ret.getY() < (double) (world.getMaxBuildHeight() - 1)) { // Paper - make sure this loads chunks, we default to NOT loading now + ret = ret.above(); + } + while (world.noCollision(player, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY() - 1, ret.getZ() + 0.5)) && ret.getY() > (double) (world.getMaxBuildHeight() + 1)) { // Paper - make sure this loads chunks, we default to NOT loading now + ret = ret.below(); + } + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(ret), world.levelData.getSpawnAngle(), 0.0f)); + } + ); + } + + } + // Folia end - region threading + @Override public BlockPos adjustSpawnLocation(ServerLevel world, BlockPos basePos) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException(); + } + // Folia end - region threading AABB axisalignedbb = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO); BlockPos blockposition1 = basePos; @@ -1321,6 +1467,332 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple } + // Folia start - region threading + /** + * Teleport flag indicating that the player is to be respawned, expected to only be used + * internally for {@link #respawn(Consumer, PlayerRespawnEvent.RespawnReason)} + */ + public static final long TELEPORT_FLAGS_PLAYER_RESPAWN = Long.MIN_VALUE >>> 0; + /** + * Teleport flag indicating the player should be placed at the highest y-value that + * provides no collisions for the player's bounding box. Note that this setting + * does not imply {@link Entity#TELEPORT_FLAG_LOAD_CHUNK}, so it may + * sync load chunks unless the load chunk flag is provided. + */ + public static final long TELEPORT_FLAGS_AVOID_SUFFOCATION = Long.MIN_VALUE >>> 1; + + private void avoidSuffocation() { + while (!this.level().noCollision(this, this.getBoundingBox()) && this.getY() < (double)this.level().getMaxBuildHeight()) { + this.setPos(this.getX(), this.getY() + 1.0D, this.getZ()); + } + } + + public void exitEndCredits() { + if (!this.wonGame) { + // not in the end credits anymore + return; + } + this.wonGame = false; + + this.respawn((player) -> { + CriteriaTriggers.CHANGED_DIMENSION.trigger(player, Level.END, Level.OVERWORLD); + }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.END_PORTAL, true); + } + + public void respawn(java.util.function.Consumer respawnComplete, PlayerRespawnEvent.RespawnReason reason) { + this.respawn(respawnComplete, reason, false); + } + + private void respawn(java.util.function.Consumer respawnComplete, PlayerRespawnEvent.RespawnReason reason, boolean alive) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot respawn entity async"); + + this.getBukkitEntity(); // force bukkit entity to be created before TPing + + if (alive != this.isAlive()) { + throw new IllegalStateException("isAlive expected = " + alive); + } + + if (!this.hasNullCallback()) { + this.unRide(); + } + + if (this.isVehicle() || this.isPassenger()) { + throw new IllegalStateException("Dead player should not be a vehicle or passenger"); + } + + ServerLevel origin = this.serverLevel(); + ServerLevel respawnWorld = this.server.getLevel(this.getRespawnDimension()); + + // modified based off PlayerList#respawn + + EntityTreeNode passengerTree = this.makePassengerTree(); + + this.isChangingDimension = true; + origin.removePlayerImmediately(this, RemovalReason.CHANGED_DIMENSION); + // reset player if needed, only after removal from world + if (!alive) { + ServerPlayer.this.reset(); + } + // must be manually removed from connections, delay until after reset() so that we do not trip any thread checks + this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection); + + BlockPos respawnPos = this.getRespawnPosition(); + float respawnAngle = this.getRespawnAngle(); + boolean isRespawnForced = this.isRespawnForced(); + + ca.spottedleaf.concurrentutil.completable.Completable spawnPosComplete = + new ca.spottedleaf.concurrentutil.completable.Completable<>(); + boolean[] usedRespawnAnchor = new boolean[1]; + + // set up post spawn location logic + spawnPosComplete.addWaiter((spawnLoc, throwable) -> { + // update pos and velocity + ServerPlayer.this.setPosRaw(spawnLoc.getX(), spawnLoc.getY(), spawnLoc.getZ()); + ServerPlayer.this.setYRot(spawnLoc.getYaw()); + ServerPlayer.this.setYHeadRot(spawnLoc.getYaw()); + ServerPlayer.this.setXRot(spawnLoc.getPitch()); + ServerPlayer.this.setDeltaMovement(Vec3.ZERO); + // placeInAsync will update the world + + this.placeInAsync( + origin, + // use the load chunk flag just in case the spawn loc isn't loaded, and to ensure the chunks + // stay loaded for a bit with the teleport ticket + ((CraftWorld)spawnLoc.getWorld()).getHandle(), + TELEPORT_FLAG_LOAD_CHUNK | TELEPORT_FLAGS_PLAYER_RESPAWN | TELEPORT_FLAGS_AVOID_SUFFOCATION, + passengerTree, // note: we expect this to just be the player, no passengers + (entity) -> { + // now the player is in the world, and can receive sound + if (usedRespawnAnchor[0]) { + ServerPlayer.this.connection.send( + new ClientboundSoundPacket( + net.minecraft.sounds.SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS, + ServerPlayer.this.getX(), ServerPlayer.this.getY(), ServerPlayer.this.getZ(), + 1.0F, 1.0F, ServerPlayer.this.serverLevel().getRandom().nextLong() + ) + ); + } + // now the respawn logic is complete + + // last, call the function callback + if (respawnComplete != null) { + respawnComplete.accept(ServerPlayer.this); + } + } + ); + }); + + // find and modify respawn block state + if (respawnWorld == null || respawnPos == null) { + // default to regular spawn + fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); + } else { + // load chunk for block + // give at least 1 radius of loaded chunks so that we do not sync load anything + int radiusBlocks = 16; + respawnWorld.moonrise$loadChunksAsync(respawnPos, radiusBlocks, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (chunks) -> { + ServerPlayer.RespawnPosAngle spawnPos = ServerPlayer.findRespawnAndUseSpawnBlock( + respawnWorld, respawnPos, respawnAngle, isRespawnForced, alive + ).orElse(null); + if (spawnPos == null) { + // no spawn + ServerPlayer.this.connection.send( + new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F) + ); + ServerPlayer.this.setRespawnPosition( + null, null, 0f, false, false, + com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN + ); + // default to regular spawn + fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); + return; + } + + boolean isRespawnAnchor = respawnWorld.getBlockState(respawnPos).is(net.minecraft.world.level.block.Blocks.RESPAWN_ANCHOR); + boolean isBed = respawnWorld.getBlockState(respawnPos).is(net.minecraft.tags.BlockTags.BEDS); + usedRespawnAnchor[0] = !alive && isRespawnAnchor; + + ServerPlayer.this.setRespawnPosition( + respawnWorld.dimension(), respawnPos, respawnAngle, isRespawnForced, false, + com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN + ); + + // finished now, pass the location on + spawnPosComplete.complete( + io.papermc.paper.util.MCUtil.toLocation(respawnWorld, spawnPos.position(), spawnPos.yaw(), 0.0f) + ); + return; + } + ); + } + } + + @Override + protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { + if (yaw != null) { + this.setYRot(yaw.floatValue()); + this.setYHeadRot(yaw.floatValue()); + } + if (pitch != null) { + this.setXRot(pitch.floatValue()); + } + if (velocity != null) { + this.setDeltaMovement(velocity); + } + this.connection.internalTeleport(pos.x, pos.y, pos.z, this.getYRot(), this.getXRot(), java.util.Collections.emptySet()); + this.connection.resetPosition(); + this.resetStoredPositions(); + } + + @Override + protected ServerPlayer transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { + // must be manually removed from connections + this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection); + this.serverLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); + + this.spawnIn(destination); + this.transform(pos, yaw, pitch, velocity); + + return this; + } + + @Override + public void preChangeDimension() { + super.preChangeDimension(); + this.stopUsingItem(); + } + + @Override + protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { + if (destination == originWorld && (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L) { + this.unsetRemoved(); + destination.addDuringTeleport(this); + + // must be manually added to connections + this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection); + + if ((teleportFlags & TELEPORT_FLAGS_AVOID_SUFFOCATION) != 0L && treeNode.passengers == null && treeNode.parent == null) { + this.avoidSuffocation(); + } + + // required to set up the pending teleport stuff to the client, and to actually update + // the player's position clientside + this.connection.internalTeleport( + this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(), + java.util.Collections.emptySet() + ); + this.connection.resetPosition(); + + this.postChangeDimension(); + } else { + // Modelled after PlayerList#respawn + + // We avoid checking for disconnection here, which means we do not have to add/remove from + // the player list here. We can let this be properly handled by the connection handler + + // pre-add logic + PlayerList playerlist = this.server.getPlayerList(); + net.minecraft.world.level.storage.LevelData worlddata = destination.getLevelData(); + this.connection.send( + new ClientboundRespawnPacket( + this.createCommonSpawnInfo(destination), + (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L ? (byte)1 : (byte)0 + ) + ); + // don't bother with the chunk cache radius and simulation distance packets, they are handled + // by the chunk loader + this.spawnIn(destination); // important that destination != null + // we can delay teleport until later, the player position is already set up at the target + this.setShiftKeyDown(false); + + this.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket( + destination.getSharedSpawnPos(), destination.getSharedSpawnAngle() + )); + this.connection.send(new ClientboundChangeDifficultyPacket( + worlddata.getDifficulty(), worlddata.isDifficultyLocked() + )); + this.connection.send(new ClientboundSetExperiencePacket( + this.experienceProgress, this.totalExperience, this.experienceLevel + )); + + playerlist.sendLevelInfo(this, destination); + playerlist.sendPlayerPermissionLevel(this); + + // regular world add logic + this.unsetRemoved(); + destination.addDuringTeleport(this); + + // must be manually added to connections + this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection); + + if ((teleportFlags & TELEPORT_FLAGS_AVOID_SUFFOCATION) != 0L && treeNode.passengers == null && treeNode.parent == null) { + this.avoidSuffocation(); + } + + // required to set up the pending teleport stuff to the client, and to actually update + // the player's position clientside + this.connection.internalTeleport( + this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot(), + java.util.Collections.emptySet() + ); + this.connection.resetPosition(); + + // delay callback until after post add logic + + // post add logic + + // "Added from changeDimension" + this.setHealth(this.getHealth()); + playerlist.sendAllPlayerInfo(this); + this.onUpdateAbilities(); + for (MobEffectInstance mobEffect : this.getActiveEffects()) { + this.connection.send(new ClientboundUpdateMobEffectPacket(this.getId(), mobEffect, false)); + } + + // Paper start - Reset shield blocking on dimension change + if (this.isBlocking()) { + this.stopUsingItem(); + } + // Paper end - Reset shield blocking on dimension change + + this.triggerDimensionChangeTriggers(originWorld); + + // finished + + this.postChangeDimension(); + } + } + + @Override + public boolean endPortalLogicAsync(BlockPos portalPos) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + + if (this.level().getTypeKey() == LevelStem.END) { + if (!this.canPortalAsync(null, false)) { + return false; + } + this.wonGame = true; + // TODO is there a better solution to this that DOESN'T skip the credits? + this.seenCredits = true; + this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, this.seenCredits ? 0.0F : 1.0F)); + this.exitEndCredits(); + return true; + } else { + return super.endPortalLogicAsync(portalPos); + } + } + + @Override + protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { + super.prePortalLogic(origin, destination, type); + if (origin.getTypeKey() == LevelStem.OVERWORLD && destination.getTypeKey() == LevelStem.NETHER) { + this.enteredNetherPosition = this.position(); + } + } + // Folia end - region threading + @Nullable @Override public Entity changeDimension(DimensionTransition teleportTarget) { @@ -2366,6 +2838,12 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple public void setCamera(@Nullable Entity entity) { Entity entity1 = this.getCamera(); + // Folia start - region threading + if (entity != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { + return; + } + // Folia end - region threading + this.camera = (Entity) (entity == null ? this : entity); if (entity1 != this.camera) { // Paper start - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity @@ -2919,7 +3397,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple this.experienceLevel = this.newLevel; this.totalExperience = this.newTotalExp; this.experienceProgress = 0; - this.deathTime = 0; + this.deathTime = 0; this.broadcastedDeath = false; // Folia - region threading this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH); this.effectsDirty = true; diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java index cc01ead133cc6859ca5d7a1d0ac3c12955e590da..41fe0aeb3b74499ebce944fd43c3b4e9aec8b821 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java @@ -133,7 +133,7 @@ public class ServerPlayerGameMode { BlockState iblockdata; if (this.hasDelayedDestroy) { - iblockdata = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks + iblockdata = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.delayedDestroyPos) ? null : this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks // Folia - region threading - don't destroy blocks not owned if (iblockdata == null || iblockdata.isAir()) { // Paper - Don't allow digging into unloaded chunks this.hasDelayedDestroy = false; } else { @@ -146,7 +146,7 @@ public class ServerPlayerGameMode { } } else if (this.isDestroyingBlock) { // Paper start - Don't allow digging into unloaded chunks; don't want to do same logic as above, return instead - iblockdata = this.level.getBlockStateIfLoaded(this.destroyPos); + iblockdata = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.destroyPos) ? null : this.level.getBlockStateIfLoaded(this.destroyPos); if (iblockdata == null) { this.isDestroyingBlock = false; return; @@ -424,7 +424,7 @@ public class ServerPlayerGameMode { } else { // CraftBukkit start org.bukkit.block.BlockState state = bblock.getState(); - this.level.captureDrops = new ArrayList<>(); + this.level.getCurrentWorldData().captureDrops = new ArrayList<>(); // Folia - region threading // CraftBukkit end BlockState iblockdata1 = block.playerWillDestroy(this.level, pos, iblockdata, this.player); boolean flag = this.level.removeBlock(pos, false); @@ -452,8 +452,8 @@ public class ServerPlayerGameMode { // return true; // CraftBukkit } // CraftBukkit start - java.util.List itemsToDrop = this.level.captureDrops; // Paper - capture all item additions to the world - this.level.captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff + java.util.List itemsToDrop = this.level.getCurrentWorldData().captureDrops; // Paper - capture all item additions to the world // Folia - region threading + this.level.getCurrentWorldData().captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff // Folia - region threading if (event.isDropItems()) { org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - capture all item additions to the world } diff --git a/src/main/java/net/minecraft/server/level/TicketType.java b/src/main/java/net/minecraft/server/level/TicketType.java index f56e5c0f53f9b52a9247b9be9265b949494fc924..765cd1eedda2e9aa928db4ac3b0776d154126fe3 100644 --- a/src/main/java/net/minecraft/server/level/TicketType.java +++ b/src/main/java/net/minecraft/server/level/TicketType.java @@ -25,6 +25,14 @@ public class TicketType { public static final TicketType UNKNOWN = TicketType.create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1); public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit + // Folia start - region threading + public static final TicketType LOGIN = create("folia:login", (u1, u2) -> 0, 20); + public static final TicketType DELAYED = create("folia:delay", (u1, u2) -> 0, 5); + public static final TicketType END_GATEWAY_EXIT_SEARCH = create("folia:end_gateway_exit_search", Long::compareTo); + public static final TicketType NETHER_PORTAL_DOUBLE_CHECK = create("folia:nether_portal_double_check", Long::compareTo); + public static final TicketType TELEPORT_HOLD_TICKET = create("folia:teleport_hold_ticket", Long::compareTo); + public static final TicketType REGION_SCHEDULER_API_HOLD = create("folia:region_scheduler_api_hold", (a, b) -> 0); + // Folia end - region threading public static TicketType create(String name, Comparator argumentComparator) { return new TicketType<>(name, argumentComparator, 0L); diff --git a/src/main/java/net/minecraft/server/level/WorldGenRegion.java b/src/main/java/net/minecraft/server/level/WorldGenRegion.java index 5a8a33638ceb1d980ffc3e6dd86e7eb11dfd9375..67b45f2916fa061e7430c6f3987bdb25ede58c04 100644 --- a/src/main/java/net/minecraft/server/level/WorldGenRegion.java +++ b/src/main/java/net/minecraft/server/level/WorldGenRegion.java @@ -115,6 +115,14 @@ public class WorldGenRegion implements WorldGenLevel { } // Paper end - rewrite chunk system + // Folia start - region threading + private final net.minecraft.world.level.StructureManager structureManager; + @Override + public net.minecraft.world.level.StructureManager structureManager() { + return this.structureManager; + } + // Folia end - region threading + public WorldGenRegion(ServerLevel world, StaticCache2D chunks, ChunkStep generationStep, ChunkAccess centerPos) { this.generatingStep = generationStep; this.cache = chunks; @@ -125,6 +133,7 @@ public class WorldGenRegion implements WorldGenLevel { this.random = world.getChunkSource().randomState().getOrCreateRandomFactory(WorldGenRegion.WORLDGEN_REGION_RANDOM).at(this.center.getPos().getWorldPosition()); this.dimensionType = world.dimensionType(); this.biomeManager = new BiomeManager(this, BiomeManager.obfuscateSeed(this.seed)); + this.structureManager = world.structureManager().forWorldGenRegion(this); // Folia - region threading } public boolean isOldChunkAround(ChunkPos chunkPos, int checkRadius) { diff --git a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java index b43f87ff4b9853b5d4bbea5ff9686d64d9d0d26b..d766bb360eb98bab1e067cad032b54fa6455f386 100644 --- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java @@ -108,6 +108,10 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack } + // Folia start - region threading + private boolean handledDisconnect = false; + // Folia end - region threading + @Override public void onDisconnect(DisconnectionDetails info) { // Paper start - Fix kick event leave message not being sent @@ -115,10 +119,18 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack } public void onDisconnect(DisconnectionDetails info, @Nullable net.kyori.adventure.text.Component quitMessage) { // Paper end - Fix kick event leave message not being sent + // Folia start - region threading + if (this.handledDisconnect) { + // avoid retiring scheduler twice + return; + } + this.handledDisconnect = true; + // Folia end - region threading if (this.isSingleplayerOwner()) { ServerCommonPacketListenerImpl.LOGGER.info("Stopping singleplayer server as player logged out"); this.server.halt(false); } + this.player.getBukkitEntity().taskScheduler.retire(); // Folia - region threading } @@ -132,9 +144,9 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack this.keepAlivePending = false; } else if (!this.isSingleplayerOwner()) { // Paper start - This needs to be handled on the main thread for plugins - server.submit(() -> { + // Folia - region threading - do not schedule to main anymore, there is no main this.disconnect(ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE, PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause - }); + // Folia - region threading - do not schedule to main anymore, there is no main // Paper end - This needs to be handled on the main thread for plugins } @@ -346,24 +358,8 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack if (this.processedDisconnect) { return; } - if (!this.cserver.isPrimaryThread()) { - Waitable waitable = new Waitable() { - @Override - protected Object evaluate() { - ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause); // Paper - kick event causes - return null; - } - }; - - this.server.processQueue.add(waitable); - - try { - waitable.get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - throw new RuntimeException(e); - } + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player)) { // Folia - region threading + this.connection.disconnectSafely(disconnectionInfo.reason(), cause); // Folia - region threading - it HAS to be delayed/async to avoid deadlock if we try to wait for another region return; } @@ -396,7 +392,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack Objects.requireNonNull(this.connection); // CraftBukkit - Don't wait - minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper + // Folia - region threading } protected boolean isSingleplayerOwner() { diff --git a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java index 880e5c52746e9e3a9a1f42ec6461be54e3ee136c..34c7fbf154c42d07b3b317b13632243a81904e3c 100644 --- a/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java @@ -54,6 +54,7 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis private ClientInformation clientInformation; @Nullable private SynchronizeRegistriesTask synchronizeRegistriesTask; + public boolean switchToMain = false; // Folia - region threading - rewrite login process // CraftBukkit start public ServerConfigurationPacketListenerImpl(MinecraftServer minecraftserver, Connection networkmanager, CommonListenerCookie commonlistenercookie, ServerPlayer player) { @@ -170,7 +171,58 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis ServerPlayer entityplayer = playerlist.getPlayerForLogin(this.gameProfile, this.clientInformation, this.player); // CraftBukkit - playerlist.placeNewPlayer(this.connection, entityplayer, this.createCookie(this.clientInformation)); + // Folia start - region threading - rewrite login process + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot handle player login off global tick thread"); + CommonListenerCookie clientData = this.createCookie(this.clientInformation); + org.apache.commons.lang3.mutable.MutableObject data = new org.apache.commons.lang3.mutable.MutableObject<>(); + org.apache.commons.lang3.mutable.MutableObject lastKnownName = new org.apache.commons.lang3.mutable.MutableObject<>(); + ca.spottedleaf.concurrentutil.completable.Completable toComplete = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + // note: need to call addWaiter before completion to ensure the callback is invoked synchronously + // the loadSpawnForNewPlayer function always completes the completable once the chunks were loaded, + // on the load callback for those chunks (so on the same region) + // this guarantees the chunk cannot unload under our feet + toComplete.addWaiter((org.bukkit.Location loc, Throwable t) -> { + int chunkX = net.minecraft.util.Mth.floor(loc.getX()) >> 4; + int chunkZ = net.minecraft.util.Mth.floor(loc.getZ()) >> 4; + + net.minecraft.server.level.ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)loc.getWorld()).getHandle(); + // we just need to hold the chunks at loaded until the next tick + // so we do not need to care about unique IDs for the ticket + world.getChunkSource().addTicketAtLevel( + net.minecraft.server.level.TicketType.LOGIN, + new net.minecraft.world.level.ChunkPos(chunkX, chunkZ), + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, + net.minecraft.util.Unit.INSTANCE + ); + + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + world, chunkX, chunkZ, + () -> { + // once switchToMain is set, the current ticking region now owns the connection and is responsible + // for cleaning it up + playerlist.placeNewPlayer( + ServerConfigurationPacketListenerImpl.this.connection, + entityplayer, + clientData, + java.util.Optional.ofNullable(data.getValue()), + lastKnownName.getValue(), + loc + ); + }, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER + ); + }); + this.switchToMain = true; + try { + // now the connection responsibility is transferred on the region + playerlist.loadSpawnForNewPlayer(this.connection, entityplayer, clientData, data, lastKnownName, toComplete); + } catch (final Throwable throwable) { + // assume toComplete will not be invoked + // ensure global tick thread owns the connection again, to properly disconnect it + this.switchToMain = false; + throw new RuntimeException(throwable); + } + // Folia end - region threading - rewrite login process } catch (Exception exception) { ServerConfigurationPacketListenerImpl.LOGGER.error("Couldn't place player in world", exception); // Paper start - Debugging diff --git a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java index 8aff5129f85ab5729b3da2e465871be62d15bdf2..8044271ff01dfc6808f5a3b60be74f6ddeee1e8f 100644 --- a/src/main/java/net/minecraft/server/network/ServerConnectionListener.java +++ b/src/main/java/net/minecraft/server/network/ServerConnectionListener.java @@ -161,10 +161,13 @@ public class ServerConnectionListener { }); } // Paper end - Add support for proxy protocol - pending.add(object); // Paper - prevent blocking on adding a new connection while the server is ticking + // Folia - connection fixes - move down ((Connection) object).configurePacketHandler(channelpipeline); ((Connection) object).setListenerForServerboundHandshake(new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, (Connection) object)); io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper - Add Channel initialization listeners + // Folia start - regionised threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addConnection(object); + // Folia end - regionised threading } }).group(eventloopgroup).localAddress(address)).option(ChannelOption.AUTO_READ, false).bind().syncUninterruptibly()); // CraftBukkit // Paper - Unix domain socket support } @@ -226,7 +229,7 @@ public class ServerConnectionListener { // Spigot Start this.addPending(); // Paper - prevent blocking on adding a new connection while the server is ticking // This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order - if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0 ) + if ( org.spigotmc.SpigotConfig.playerShuffle > 0 && 0 % org.spigotmc.SpigotConfig.playerShuffle == 0 ) // Folia - region threading { Collections.shuffle( this.connections ); } diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index 60ff21c8df4168f14da04a12073bde47cd4693c4..76ab54485b5354c10316ac71ce7073732b397865 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -300,7 +300,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl private final LastSeenMessagesValidator lastSeenMessages = new LastSeenMessagesValidator(20); private final MessageSignatureCache messageSignatureCache = MessageSignatureCache.createDefault(); private final FutureChain chatMessageChain; - private boolean waitingForSwitchToConfig; + public volatile boolean waitingForSwitchToConfig; // Folia - rewrite login process - fix bad ordering of this field write + public private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie clientData) { @@ -317,10 +317,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl } // CraftBukkit start - add fields and methods - private int lastTick = MinecraftServer.currentTick; + private long lastTick = Util.getMillis() / 50L; // Folia - region threading private int allowedPlayerTicks = 1; - private int lastDropTick = MinecraftServer.currentTick; - private int lastBookTick = MinecraftServer.currentTick; + private long lastDropTick = Util.getMillis() / 50L; // Folia - region threading + private long lastBookTick = Util.getMillis() / 50L; // Folia - region threading private int dropCount = 0; private boolean hasMoved = false; @@ -332,8 +332,21 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl private boolean justTeleported = false; // CraftBukkit end + // Folia start - region threading + public net.minecraft.world.level.ChunkPos disconnectPos; + private static final java.util.concurrent.atomic.AtomicLong DISCONNECT_TICKET_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); + public static final net.minecraft.server.level.TicketType DISCONNECT_TICKET = net.minecraft.server.level.TicketType.create("disconnect_ticket", Long::compareTo); + public final Long disconnectTicketId = Long.valueOf(DISCONNECT_TICKET_ID_GENERATOR.getAndIncrement()); + // Folia end - region threading + @Override public void tick() { + // Folia start - region threading + this.keepConnectionAlive(); + if (this.processedDisconnect || this.player.wonGame) { + return; + } + // Folia end - region threading if (this.ackBlockChangesUpTo > -1) { this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo)); this.ackBlockChangesUpTo = -1; @@ -382,7 +395,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.aboveGroundVehicleTickCount = 0; } - this.keepConnectionAlive(); + // Folia - region threading - moved to beginning of method // CraftBukkit start for (int spam; (spam = this.chatSpamTickCount.get()) > 0 && !this.chatSpamTickCount.compareAndSet(spam, spam - 1); ) ; if (tabSpamLimiter.get() > 0) tabSpamLimiter.getAndDecrement(); // Paper - configurable tab spam limits @@ -431,6 +444,19 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.lastGoodX = this.player.getX(); this.lastGoodY = this.player.getY(); this.lastGoodZ = this.player.getZ(); + // Folia start - support vehicle teleportations + this.lastVehicle = this.player.getRootVehicle(); + if (this.lastVehicle != this.player && this.lastVehicle.getControllingPassenger() == this.player) { + this.vehicleFirstGoodX = this.lastVehicle.getX(); + this.vehicleFirstGoodY = this.lastVehicle.getY(); + this.vehicleFirstGoodZ = this.lastVehicle.getZ(); + this.vehicleLastGoodX = this.lastVehicle.getX(); + this.vehicleLastGoodY = this.lastVehicle.getY(); + this.vehicleLastGoodZ = this.lastVehicle.getZ(); + } else { + this.lastVehicle = null; + } + // Folia end - support vehicle teleportations } @Override @@ -531,9 +557,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl // Paper end - fix large move vectors killing the server // CraftBukkit start - handle custom speeds and skipped ticks - this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; + int currTick = (int)(Util.getMillis() / 50); // Folia - region threading + this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); - this.lastTick = (int) (System.currentTimeMillis() / 50); + this.lastTick = (int) currTick; // Folia - region threading ++this.receivedMovePacketCount; int i = this.receivedMovePacketCount - this.knownMovePacketCount; @@ -606,16 +633,24 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl } entity.absMoveTo(d3, d4, d5, f, f1); - this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit + //this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - move to repositionAllPassengers boolean flag3 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D)); if (flag && (flag2 || !flag3)) { entity.absMoveTo(d0, d1, d2, f, f1); - this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit + //this.player.absMoveTo(d0, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - not needed, the player is no longer updated this.send(new ClientboundMoveVehiclePacket(entity)); return; } + // Folia start - move to positionRider + // this correction is required on folia since we move the connection tick to the beginning of the server + // tick, which would make any desync here visible + // this will correctly update the passenger positions for all mounted entities + // this prevents desync and ensures that all passengers have the correct rider-adjusted position + entity.repositionAllPassengers(false); + // Folia end - move to positionRider + // CraftBukkit start - fire PlayerMoveEvent Player player = this.getCraftPlayer(); if (!this.hasMoved) { @@ -655,7 +690,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl // If the event is cancelled we move the player back to their old location. if (event.isCancelled()) { - this.teleport(from); + this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading return; } @@ -663,7 +698,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. // We only do this if the Event was not cancelled. if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { - this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); + this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading return; } @@ -757,7 +792,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl // PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); // Paper - AsyncTabCompleteEvent; run this async // CraftBukkit start if (this.chatSpamTickCount.addAndGet(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement) > io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit && !this.server.getPlayerList().isOp(this.player.getGameProfile())) { // Paper - configurable tab spam limits - this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - Kick event cause + this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - Kick event cause // Folia - region threading return; } // CraftBukkit end @@ -769,7 +804,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl // Paper start final int index; if (packet.getCommand().length() > 64 && ((index = packet.getCommand().indexOf(' ')) == -1 || index >= 64)) { - this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); + this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Folia - region threading return; } // Paper end @@ -794,7 +829,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl } // This needs to be on main - this.server.scheduleOnMain(() -> this.sendServerSuggestions(packet, stringreader)); + this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> this.sendServerSuggestions(packet, stringreader), null, 1L); // Folia - region threading } else if (!completions.isEmpty()) { final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packet.getCommand(), stringreader.getTotalLength()); final com.mojang.brigadier.suggestion.SuggestionsBuilder builder = builder0.createOffset(builder0.getInput().lastIndexOf(' ') + 1); @@ -1158,11 +1193,11 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl } // Paper end - Book size limits // CraftBukkit start - if (this.lastBookTick + 20 > MinecraftServer.currentTick) { + if (this.lastBookTick + 20 > this.lastTick) { // Folia - region threading this.disconnect(Component.literal("Book edited too quickly!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause return; } - this.lastBookTick = MinecraftServer.currentTick; + this.lastBookTick = this.lastTick; // Folia - region threading // CraftBukkit end int i = packet.slot(); @@ -1182,7 +1217,19 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.updateBookContents(list1, i); }; - this.filterTextPacket((List) list).thenAcceptAsync(consumer, this.server); + this.filterTextPacket(list).thenAcceptAsync(consumer, // Folia start - region threading + (Runnable run) -> { + this.player.getBukkitEntity().taskScheduler.schedule( + (player) -> { + run.run(); + }, + null, 1L); + }).whenComplete((Object res, Throwable thr) -> { + if (thr != null) { + LOGGER.error("Failed to handle book update packet", thr); + } + }); + // Folia end - region threading } } @@ -1330,9 +1377,10 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl int i = this.receivedMovePacketCount - this.knownMovePacketCount; // CraftBukkit start - handle custom speeds and skipped ticks - this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; + int currTick = (int)(Util.getMillis() / 50); // Folia - region threading + this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); - this.lastTick = (int) (System.currentTimeMillis() / 50); + this.lastTick = (int) currTick; // Folia - region threading if (i > Math.max(this.allowedPlayerTicks, 5)) { ServerGamePacketListenerImpl.LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i); @@ -1513,7 +1561,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl // If the event is cancelled we move the player back to their old location. if (event.isCancelled()) { - this.teleport(from); + this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading return; } @@ -1521,7 +1569,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. // We only do this if the Event was not cancelled. if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { - this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); + this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading return; } @@ -1738,9 +1786,9 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl if (!this.player.isSpectator()) { // limit how quickly items can be dropped // If the ticks aren't the same then the count starts from 0 and we update the lastDropTick. - if (this.lastDropTick != MinecraftServer.currentTick) { + if (this.lastDropTick != io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { this.dropCount = 0; - this.lastDropTick = MinecraftServer.currentTick; + this.lastDropTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); } else { // Else we increment the drop count and check the amount. this.dropCount++; @@ -1768,7 +1816,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl case ABORT_DESTROY_BLOCK: case STOP_DESTROY_BLOCK: // Paper start - Don't allow digging into unloaded chunks - if (this.player.level().getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) { + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), blockposition.getX() >> 4, blockposition.getZ() >> 4, 8) || this.player.level().getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) == null) { // Folia - region threading - don't destroy blocks not owned this.player.connection.ackBlockChangesUpTo(packet.getSequence()); return; } @@ -1851,7 +1899,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl // Paper end - improve distance check BlockPos blockposition = movingobjectpositionblock.getBlockPos(); - if (this.player.canInteractWithBlock(blockposition, 1.0D)) { + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), blockposition.getX() >> 4, blockposition.getZ() >> 4, 8) && this.player.canInteractWithBlock(blockposition, 1.0D)) { // Folia - do not allow players to interact with blocks outside the current region Vec3 vec3d1 = vec3d.subtract(Vec3.atCenterOf(blockposition)); double d0 = 1.0000001D; @@ -1973,7 +2021,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl Entity entity = packet.getEntity(worldserver); if (entity != null) { - this.player.teleportTo(worldserver, entity.getX(), entity.getY(), entity.getZ(), entity.getYRot(), entity.getXRot(), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit + io.papermc.paper.threadedregions.TeleportUtils.teleport(this.player, false, entity, null, null, Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null); // Folia - region threading return; } } @@ -2008,7 +2056,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl } // CraftBukkit end ServerGamePacketListenerImpl.LOGGER.info("{} lost connection: {}", this.player.getName().getString(), info.reason().getString()); - this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent + if (!this.waitingForSwitchToConfig) this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent // Folia - region threading super.onDisconnect(info, quitMessage); // Paper - Fix kick event leave message not being sent } @@ -2017,6 +2065,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.removePlayerFromWorld(null); } + public boolean hackSwitchingConfig; // Folia - rewrite login process + private void removePlayerFromWorld(@Nullable net.kyori.adventure.text.Component quitMessage) { // Paper end - Fix kick event leave message not being sent this.chatMessageChain.close(); @@ -2029,6 +2079,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.player.disconnect(); // Paper start - Adventure quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used + if (!this.hackSwitchingConfig) this.disconnectPos = this.player.chunkPosition(); // Folia - region threading - note: only set after removing, since it can tick the player + if (!this.hackSwitchingConfig) this.player.serverLevel().chunkSource.addTicketAtLevel(DISCONNECT_TICKET, this.disconnectPos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, this.disconnectTicketId); // Folia - region threading - force chunk to be loaded so that the region is not lost if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) { this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false); // Paper end @@ -2277,7 +2329,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.player.resetLastActionTime(); // CraftBukkit start if (sync) { - this.server.execute(runnable); + this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> runnable.run(), null, 1L); // Folia - region threading } else { runnable.run(); } @@ -2335,7 +2387,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl String originalFormat = event.getFormat(), originalMessage = event.getMessage(); this.cserver.getPluginManager().callEvent(event); - if (PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { + if (false && PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading // Evil plugins still listening to deprecated event final PlayerChatEvent queueEvent = new PlayerChatEvent(player, event.getMessage(), event.getFormat(), event.getRecipients()); queueEvent.setCancelled(event.isCancelled()); @@ -2433,6 +2485,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl if (s.isEmpty()) { ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " tried to send an empty message"); } else if (this.getCraftPlayer().isConversing()) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading final String conversationInput = s; this.server.processQueue.add(new Runnable() { @Override @@ -2664,8 +2717,25 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl // Spigot End public void switchToConfig() { - this.waitingForSwitchToConfig = true; + // Folia start - rewrite login process + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.player, "Cannot switch config off-main"); + if (io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { + throw new IllegalStateException("Cannot switch config while on global tick thread"); + } + // Folia end - rewrite login process + // Folia start - rewrite login process - fix bad ordering of this field write - move after removed from world + // the field write ordering is bad as it allows the client to send the response packet before the player is + // removed from the world + // Folia end - rewrite login process - fix bad ordering of this field write - move after removed from world + try { // Folia - rewrite login process - move connection ownership to global region + this.hackSwitchingConfig = true; // Folia - rewrite login process - avoid adding logout ticket here and retiring scheduler this.removePlayerFromWorld(); + } finally { // Folia start - rewrite login process - move connection ownership to global region + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.player.serverLevel().getCurrentWorldData(); + worldData.connections.remove(this.connection); + // once waitingForSwitchToConfig is set, the global tick thread will own the connection + } // Folia end - rewrite login process - move connection ownership to global region + this.waitingForSwitchToConfig = true; // Folia - rewrite login process - fix bad ordering of this field write - moved down this.send(ClientboundStartConfigurationPacket.INSTANCE); this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); } @@ -2691,7 +2761,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.player.resetLastActionTime(); this.player.setShiftKeyDown(packet.isUsingSecondaryAction()); - if (entity != null) { + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) && entity != null) { // Folia - region threading - do not allow interaction of entities outside the current region if (!worldserver.getWorldBorder().isWithinBounds(entity.blockPosition())) { return; } @@ -2835,6 +2905,12 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl switch (packetplayinclientcommand_enumclientcommand) { case PERFORM_RESPAWN: if (this.player.wonGame) { + // Folia start - region threading + if (true) { + this.player.exitEndCredits(); + return; + } + // Folia end - region threading this.player.wonGame = false; this.player = this.server.getPlayerList().respawn(this.player, true, Entity.RemovalReason.CHANGED_DIMENSION, RespawnReason.END_PORTAL); // CraftBukkit CriteriaTriggers.CHANGED_DIMENSION.trigger(this.player, Level.END, Level.OVERWORLD); @@ -2843,6 +2919,17 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl return; } + // Folia start - region threading + if (true) { + this.player.respawn((ServerPlayer player) -> { + if (ServerGamePacketListenerImpl.this.server.isHardcore()) { + player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent + ((GameRules.BooleanValue) player.level().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, player.serverLevel()); // CraftBukkit - per-world + } + }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.DEATH); + return; + } + // Folia end - region threading this.player = this.server.getPlayerList().respawn(this.player, false, Entity.RemovalReason.KILLED, RespawnReason.DEATH); // CraftBukkit if (this.server.isHardcore()) { this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent @@ -3376,7 +3463,18 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl this.filterTextPacket(list).thenAcceptAsync((list1) -> { this.updateSignText(packet, list1); - }, this.server); + }, (Runnable run) -> { // Folia start - region threading + this.player.getBukkitEntity().taskScheduler.schedule( + (player) -> { + run.run(); + }, + null, 1L); + }).whenComplete((Object res, Throwable thr) -> { + if (thr != null) { + LOGGER.error("Failed to handle sign update packet", thr); + } + }); + // Folia end - region threading } private void updateSignText(ServerboundSignUpdatePacket packet, List signText) { diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java index b0f7a378f19b9837c060c891002cd5db756cdae1..bc9956d36ce3adabd50dc4b6467a0fd24386e4e0 100644 --- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java @@ -106,7 +106,11 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, } // Paper end - Do not allow logins while the server is shutting down if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) { - if (this.connection.isConnected()) { // Paper - prevent logins to be processed even though disconnect was called + // Folia start - region threading - rewrite login process + String name = this.authenticatedProfile.getName(); + java.util.UUID uniqueId = this.authenticatedProfile.getId(); + if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) { + // Folia end - region threading - rewrite login process this.verifyLoginAndFinishConnectionSetup((GameProfile) Objects.requireNonNull(this.authenticatedProfile)); } // Paper - prevent logins to be processed even though disconnect was called } @@ -252,7 +256,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, })); } - boolean flag = playerlist.disconnectAllPlayersWithProfile(gameprofile, this.player); // CraftBukkit - add player reference + boolean flag = false && playerlist.disconnectAllPlayersWithProfile(gameprofile, this.player); // CraftBukkit - add player reference // Folia - rewrite login process - always false here if (flag) { this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT; @@ -372,7 +376,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, uniqueId = gameprofile.getId(); // Paper end - Add more fields to AsyncPlayerPreLoginEvent - if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { + if (false && PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId); if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) { event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure diff --git a/src/main/java/net/minecraft/server/players/BanListEntry.java b/src/main/java/net/minecraft/server/players/BanListEntry.java index 8b1da1fb5ca27432a39aff6dbc452b793268dab5..e83f3676d5a194fa8d3d1567edcb4b6f7847a4c1 100644 --- a/src/main/java/net/minecraft/server/players/BanListEntry.java +++ b/src/main/java/net/minecraft/server/players/BanListEntry.java @@ -10,7 +10,7 @@ import net.minecraft.network.chat.Component; public abstract class BanListEntry extends StoredUserEntry { - public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT); + public static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)); // Folia - region threading - SDF is not thread-safe public static final String EXPIRES_NEVER = "forever"; protected final Date created; protected final String source; @@ -32,7 +32,7 @@ public abstract class BanListEntry extends StoredUserEntry { Date date; try { - date = json.has("created") ? BanListEntry.DATE_FORMAT.parse(json.get("created").getAsString()) : new Date(); + date = json.has("created") ? BanListEntry.DATE_FORMAT.get().parse(json.get("created").getAsString()) : new Date(); // Folia - region threading - SDF is not thread-safe } catch (ParseException parseexception) { date = new Date(); } @@ -43,7 +43,7 @@ public abstract class BanListEntry extends StoredUserEntry { Date date1; try { - date1 = json.has("expires") ? BanListEntry.DATE_FORMAT.parse(json.get("expires").getAsString()) : null; + date1 = json.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(json.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe } catch (ParseException parseexception1) { date1 = null; } @@ -78,9 +78,9 @@ public abstract class BanListEntry extends StoredUserEntry { @Override protected void serialize(JsonObject json) { - json.addProperty("created", BanListEntry.DATE_FORMAT.format(this.created)); + json.addProperty("created", BanListEntry.DATE_FORMAT.get().format(this.created)); // Folia - region threading - SDF is not thread-safe json.addProperty("source", this.source); - json.addProperty("expires", this.expires == null ? "forever" : BanListEntry.DATE_FORMAT.format(this.expires)); + json.addProperty("expires", this.expires == null ? "forever" : BanListEntry.DATE_FORMAT.get().format(this.expires)); // Folia - region threading - SDF is not thread-safe json.addProperty("reason", this.reason); } @@ -89,7 +89,7 @@ public abstract class BanListEntry extends StoredUserEntry { Date expires = null; try { - expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.parse(jsonobject.get("expires").getAsString()) : null; + expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(jsonobject.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe } catch (ParseException ex) { // Guess we don't have a date } diff --git a/src/main/java/net/minecraft/server/players/OldUsersConverter.java b/src/main/java/net/minecraft/server/players/OldUsersConverter.java index 653856d0b8dcf2baf4cc77a276f17c8cc1fa717e..3f5639f26f249ca10e03826231d087ab715b7ce7 100644 --- a/src/main/java/net/minecraft/server/players/OldUsersConverter.java +++ b/src/main/java/net/minecraft/server/players/OldUsersConverter.java @@ -516,7 +516,7 @@ public class OldUsersConverter { Date date1; try { - date1 = BanListEntry.DATE_FORMAT.parse(dateString); + date1 = BanListEntry.DATE_FORMAT.get().parse(dateString); // Folia - region threading - SDF is not thread-safe } catch (ParseException parseexception) { date1 = fallback; } diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index 43eeb8ce4bc350c2b524ade11ca25d8d4d21bea5..cb8233a3c1d49197da7378adda5fa17ac44fd673 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -134,10 +134,10 @@ public abstract class PlayerList { public static final Component DUPLICATE_LOGIN_DISCONNECT_MESSAGE = Component.translatable("multiplayer.disconnect.duplicate_login"); private static final Logger LOGGER = LogUtils.getLogger(); private static final int SEND_PLAYER_INFO_INTERVAL = 600; - private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); + private static final ThreadLocal BAN_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z")); // Folia - region threading - SDF is not thread-safe private final MinecraftServer server; public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety - private final Map playersByUUID = Maps.newHashMap(); + private final Map playersByUUID = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! private final UserBanList bans; private final IpBanList ipBans; private final ServerOpList ops; @@ -158,9 +158,63 @@ public abstract class PlayerList { // CraftBukkit start private CraftServer cserver; - private final Map playersByName = new java.util.HashMap<>(); + private final Map playersByName = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! public @Nullable String collideRuleTeamName; // Paper - Configurable player collision + // Folia start - region threading + private final Object connectionsStateLock = new Object(); + private final Map connectionByName = new java.util.HashMap<>(); + private final Map connectionById = new java.util.HashMap<>(); + private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet usersCountedAgainstLimit = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); + + public boolean pushPendingJoin(String userName, UUID byId, Connection conn) { + userName = userName.toLowerCase(java.util.Locale.ROOT); + Connection conflictingName, conflictingId; + synchronized (this.connectionsStateLock) { + conflictingName = this.connectionByName.get(userName); + conflictingId = this.connectionById.get(byId); + + if (conflictingName == null && conflictingId == null) { + this.connectionByName.put(userName, conn); + this.connectionById.put(byId, conn); + } + } + + Component message = Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]); + + if (conflictingId != null || conflictingName != null) { + if (conflictingName != null && conflictingName.isPlayerConnected()) { + conflictingName.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); + } + if (conflictingName != conflictingId && conflictingId != null && conflictingId.isPlayerConnected()) { + conflictingId.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); + } + } + + return conflictingName == null && conflictingId == null; + } + + public void removeConnection(String userName, UUID byId, Connection conn) { + userName = userName.toLowerCase(java.util.Locale.ROOT); + synchronized (this.connectionsStateLock) { + this.connectionByName.remove(userName, conn); + this.connectionById.remove(byId, conn); + this.usersCountedAgainstLimit.remove(conn); + } + } + + private boolean countConnection(Connection conn, int limit) { + synchronized (this.connectionsStateLock) { + int count = this.usersCountedAgainstLimit.size(); + if (count >= limit) { + return false; + } + this.usersCountedAgainstLimit.add(conn); + return true; + } + } + // Folia end - region threading + public PlayerList(MinecraftServer server, LayeredRegistryAccess registryManager, PlayerDataStorage saveHandler, int maxPlayers) { this.cserver = server.server = new CraftServer((DedicatedServer) server, this); server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper @@ -181,7 +235,7 @@ public abstract class PlayerList { } abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor - public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) { + public void loadSpawnForNewPlayer(final Connection connection, final ServerPlayer player, final CommonListenerCookie clientData, org.apache.commons.lang3.mutable.MutableObject data, org.apache.commons.lang3.mutable.MutableObject lastKnownName, ca.spottedleaf.concurrentutil.completable.Completable toComplete) { // Folia - region threading - rewrite login process player.isRealPlayer = true; // Paper player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed GameProfile gameprofile = player.getGameProfile(); @@ -258,18 +312,42 @@ public abstract class PlayerList { player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login // Paper start - reset to main world spawn if first spawn or invalid world } + // Folia start - region threading - rewrite login process + // must write to these before toComplete is invoked + data.setValue(optional.orElse(null)); + lastKnownName.setValue(s); + // Folia end - region threading - rewrite login process if (optional.isEmpty() || invalidPlayerWorld[0]) { // Paper end - reset to main world spawn if first spawn or invalid world - player.moveTo(player.adjustSpawnLocation(worldserver1, worldserver1.getSharedSpawnPos()).getBottomCenter(), worldserver1.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored + ServerPlayer.fudgeSpawnLocation(worldserver1, player, toComplete); // Paper - MC-200092 - fix first spawn pos yaw being ignored // Folia - region threading + } else { + worldserver1.loadChunksForMoveAsync( + player.getBoundingBox(), + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (c) -> { + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(worldserver1, player.position())); + } + ); } + // Folia end - region threading - rewrite login process // Paper end - Entity#getEntitySpawnReason + // Folia start - region threading - rewrite login process + return; + } + // optional -> player data + // s -> last known name + public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData, Optional optional, String s, Location selectedSpawn) { + ServerLevel worldserver1 = ((CraftWorld)selectedSpawn.getWorld()).getHandle(); + player.setPosRaw(selectedSpawn.getX(), selectedSpawn.getY(), selectedSpawn.getZ()); + player.lastSave = System.nanoTime(); // changed to nanoTime + // Folia end - region threading - rewrite login process player.setServerLevel(worldserver1); String s1 = connection.getLoggableAddress(this.server.logIPs()); // Spigot start - spawn location event Player spawnPlayer = player.getBukkitEntity(); org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation()); - this.cserver.getPluginManager().callEvent(ev); + //this.cserver.getPluginManager().callEvent(ev); // Folia - region threading - TODO WTF TO DO WITH THIS EVENT? Location loc = ev.getSpawnLocation(); worldserver1 = ((CraftWorld) loc.getWorld()).getHandle(); @@ -288,6 +366,10 @@ public abstract class PlayerList { player.loadGameTypes((CompoundTag) optional.orElse(null)); // CraftBukkit - decompile error ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, clientData); + // Folia start - rewrite login process + // only after setting the connection listener to game type, add the connection to this regions list + worldserver1.getCurrentWorldData().connections.add(connection); + // Folia end - rewrite login process connection.setupInboundProtocol(GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), playerconnection); GameRules gamerules = worldserver1.getGameRules(); @@ -305,7 +387,7 @@ public abstract class PlayerList { this.sendPlayerPermissionLevel(player); player.getStats().markAllDirty(); player.getRecipeBook().sendInitialRecipeBook(player); - this.updateEntireScoreboard(worldserver1.getScoreboard(), player); + if (false) this.updateEntireScoreboard(worldserver1.getScoreboard(), player); // Folia - region threading this.server.invalidateStatus(); MutableComponent ichatmutablecomponent; @@ -347,7 +429,7 @@ public abstract class PlayerList { this.cserver.getPluginManager().callEvent(playerJoinEvent); if (!player.connection.isAcceptingMessages()) { - return; + //return; // Folia - region threading - must still allow the player to connect, as we must add to chunk map before handling disconnect } final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); @@ -362,8 +444,7 @@ public abstract class PlayerList { ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper - Add Listing API for Player final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - Use single player info update packet on join - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i); + for (ServerPlayer entityplayer1 : this.players) { // Folia - region threading if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) { // Paper start - Add Listing API for Player @@ -476,7 +557,7 @@ public abstract class PlayerList { // Paper start - Configurable player collision; Add to collideRule team if needed final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName); - if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { + if (false && this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { // Folia - region threading scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); } // Paper end - Configurable player collision @@ -569,7 +650,7 @@ public abstract class PlayerList { protected void save(ServerPlayer player) { if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit - player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving + player.lastSave = System.nanoTime(); // Folia - region threading - changed to nanoTime tracking this.playerIo.save(player); ServerStatsCounter serverstatisticmanager = (ServerStatsCounter) player.getStats(); // CraftBukkit @@ -609,7 +690,7 @@ public abstract class PlayerList { // CraftBukkit end // Paper start - Configurable player collision; Remove from collideRule team if needed - if (this.collideRuleTeamName != null) { + if (false && this.collideRuleTeamName != null) { // Folia - region threading final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); if (entityplayer.getTeam() == team && team != null) { @@ -649,7 +730,7 @@ public abstract class PlayerList { entityplayer.unRide(); worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); - entityplayer.retireScheduler(); // Paper - Folia schedulers + // Folia - region threading - move to onDisconnect of common packet listener entityplayer.getAdvancements().stopListening(); this.players.remove(entityplayer); this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot @@ -668,8 +749,7 @@ public abstract class PlayerList { // CraftBukkit start // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID()))); ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(entityplayer.getUUID())); - for (int i = 0; i < this.players.size(); i++) { - ServerPlayer entityplayer2 = (ServerPlayer) this.players.get(i); + for (ServerPlayer entityplayer2 : this.players) { // Folia - region threading if (entityplayer2.getBukkitEntity().canSee(entityplayer.getBukkitEntity())) { entityplayer2.connection.send(packet); @@ -694,19 +774,12 @@ public abstract class PlayerList { ServerPlayer entityplayer; - for (int i = 0; i < this.players.size(); ++i) { - entityplayer = (ServerPlayer) this.players.get(i); - if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameprofile.getName()))) { // Paper - validate usernames - list.add(entityplayer); - } - } + // Folia - region threading - rewrite login process - moved to pushPendingJoin Iterator iterator = list.iterator(); while (iterator.hasNext()) { - entityplayer = (ServerPlayer) iterator.next(); - this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved - entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause + // Folia - moved to pushPendingJoin } // Instead of kicking then returning, we need to store the kick reason @@ -726,7 +799,7 @@ public abstract class PlayerList { ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned.reason", gameprofilebanentry.getReason()); if (gameprofilebanentry.getExpires() != null) { - ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.format(gameprofilebanentry.getExpires()))); + ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned.expiration", PlayerList.BAN_DATE_FORMAT.get().format(gameprofilebanentry.getExpires()))); // Folia - region threading - SDF is not thread-safe } // return chatmessage; @@ -739,14 +812,14 @@ public abstract class PlayerList { ichatmutablecomponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipbanentry.getReason()); if (ipbanentry.getExpires() != null) { - ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.format(ipbanentry.getExpires()))); + ichatmutablecomponent.append((Component) Component.translatable("multiplayer.disconnect.banned_ip.expiration", PlayerList.BAN_DATE_FORMAT.get().format(ipbanentry.getExpires()))); // Folia - region threading - SDF is not thread-safe } // return chatmessage; event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure } else { // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null; - if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { + if (!this.countConnection(loginlistener.connection, this.maxPlayers) && !this.canBypassPlayerLimit(gameprofile)) { // Folia - region threading - we control connection state here now async, not player list size event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure } } @@ -806,6 +879,11 @@ public abstract class PlayerList { } public ServerPlayer respawn(ServerPlayer entityplayer, boolean flag, Entity.RemovalReason entity_removalreason, RespawnReason reason, Location location) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading entityplayer.stopRiding(); // CraftBukkit this.players.remove(entityplayer); this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot @@ -971,10 +1049,10 @@ public abstract class PlayerList { public void tick() { if (++this.sendAllPlayerInfoIn > 600) { // CraftBukkit start - for (int i = 0; i < this.players.size(); ++i) { - final ServerPlayer target = (ServerPlayer) this.players.get(i); + ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading + for (final ServerPlayer target : players) { // Folia - region threading - target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), this.players.stream().filter(new Predicate() { + target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), java.util.Arrays.stream(players).filter(new Predicate() { // Folia - region threading @Override public boolean test(ServerPlayer input) { return target.getBukkitEntity().canSee(input.getBukkitEntity()); @@ -1000,18 +1078,17 @@ public abstract class PlayerList { // CraftBukkit start - add a world/entity limited version public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) { - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer = this.players.get(i); + for (ServerPlayer entityplayer : this.players) { // Folia - region threading if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) { continue; } - ((ServerPlayer) this.players.get(i)).connection.send(packet); + entityplayer.connection.send(packet); // Folia - region threading } } public void broadcastAll(Packet packet, Level world) { - for (int i = 0; i < world.players().size(); ++i) { - ((ServerPlayer) world.players().get(i)).connection.send(packet); + for (net.minecraft.world.entity.player.Player player : world.players()) { // Folia - region threading + ((ServerPlayer) player).connection.send(packet); // Folia - region threading } } @@ -1055,8 +1132,7 @@ public abstract class PlayerList { if (scoreboardteam == null) { this.broadcastSystemMessage(message, false); } else { - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer = (ServerPlayer) this.players.get(i); + for (ServerPlayer entityplayer : this.players) { // Folia - region threading if (entityplayer.getTeam() != scoreboardteam) { entityplayer.sendSystemMessage(message); @@ -1067,10 +1143,12 @@ public abstract class PlayerList { } public String[] getPlayerNamesArray() { - String[] astring = new String[this.players.size()]; + List players = new java.util.ArrayList<>(this.players); // Folia start - region threading + String[] astring = new String[players.size()]; - for (int i = 0; i < this.players.size(); ++i) { - astring[i] = ((ServerPlayer) this.players.get(i)).getGameProfile().getName(); + for (int i = 0; i < players.size(); ++i) { + astring[i] = ((ServerPlayer) players.get(i)).getGameProfile().getName(); + // Folia end - region threading } return astring; @@ -1089,7 +1167,9 @@ public abstract class PlayerList { ServerPlayer entityplayer = this.getPlayer(profile.getId()); if (entityplayer != null) { + entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading this.sendPlayerPermissionLevel(entityplayer); + }, null, 1L); // Folia - region threading } } @@ -1099,7 +1179,10 @@ public abstract class PlayerList { ServerPlayer entityplayer = this.getPlayer(profile.getId()); if (entityplayer != null) { + entityplayer.getBukkitEntity().taskScheduler.schedule((nmsEntity) -> { // Folia - region threading this.sendPlayerPermissionLevel(entityplayer); + }, null, 1L); // Folia - region threading + } } @@ -1161,8 +1244,7 @@ public abstract class PlayerList { } public void broadcast(@Nullable net.minecraft.world.entity.player.Player player, double x, double y, double z, double distance, ResourceKey worldKey, Packet packet) { - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer = (ServerPlayer) this.players.get(i); + for (ServerPlayer entityplayer : this.players) { // Folia - region threading // CraftBukkit start - Test if player receiving packet can see the source of the packet if (player != null && !entityplayer.getBukkitEntity().canSee(player.getBukkitEntity())) { @@ -1192,12 +1274,21 @@ public abstract class PlayerList { io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main MinecraftTimings.savePlayers.startTiming(); // Paper int numSaved = 0; - long now = MinecraftServer.currentTick; - for (int i = 0; i < this.players.size(); ++i) { - ServerPlayer entityplayer = this.players.get(i); - if (interval == -1 || now - entityplayer.lastSave >= interval) { + long now = System.nanoTime(); // Folia - region threading + int max = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick(); // Folia - region threading + long timeInterval = (long)interval * io.papermc.paper.threadedregions.TickRegionScheduler.TIME_BETWEEN_TICKS; // Folia - region threading + for (ServerPlayer entityplayer : this.players) { // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entityplayer)) { + continue; + } + // Folia end - region threading + if (interval == -1 || now - entityplayer.lastSave >= timeInterval) { // Folia - region threading this.save(entityplayer); - if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; } + // Folia start - region threading + if (interval != -1 && max != -1 && ++numSaved >= max) { + break; + } + // Folia end - region threading } // Paper end - Incremental chunk and player saving } @@ -1316,6 +1407,20 @@ public abstract class PlayerList { } public void removeAll(boolean isRestarting) { + // Folia start - region threading + // just send disconnect packet, don't modify state + for (ServerPlayer player : this.players) { + final Component shutdownMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure + // CraftBukkit end + + player.connection.send(new net.minecraft.network.protocol.common.ClientboundDisconnectPacket(shutdownMessage), net.minecraft.network.PacketSendListener.thenRun(() -> { + player.connection.connection.disconnect(shutdownMessage); + })); + } + if (true) { + return; + } + // Folia end - region threading // Paper end // CraftBukkit start - disconnect safely for (ServerPlayer player : this.players) { @@ -1325,7 +1430,7 @@ public abstract class PlayerList { // CraftBukkit end // Paper start - Configurable player collision; Remove collideRule team if it exists - if (this.collideRuleTeamName != null) { + if (false && this.collideRuleTeamName != null) { // Folia - region threading final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName); if (team != null) scoreboard.removePlayerTeam(team); diff --git a/src/main/java/net/minecraft/server/players/StoredUserList.java b/src/main/java/net/minecraft/server/players/StoredUserList.java index c038da20b76c0b7b1c18471b20be01e849d29f3a..87114cc9ce7489ff8e29e2d88ebb0d47eb68232e 100644 --- a/src/main/java/net/minecraft/server/players/StoredUserList.java +++ b/src/main/java/net/minecraft/server/players/StoredUserList.java @@ -103,6 +103,7 @@ public abstract class StoredUserList> { } public void save() throws IOException { + synchronized (this) { // Folia - region threading this.removeExpired(); // Paper - remove expired values before saving JsonArray jsonarray = new JsonArray(); Stream stream = this.map.values().stream().map((jsonlistentry) -> { // CraftBukkit - decompile error @@ -133,10 +134,12 @@ public abstract class StoredUserList> { if (bufferedwriter != null) { bufferedwriter.close(); } + } // Folia - region threading } public void load() throws IOException { + synchronized (this) { // Folia - region threading if (this.file.exists()) { BufferedReader bufferedreader = Files.newReader(this.file, StandardCharsets.UTF_8); @@ -193,5 +196,6 @@ public abstract class StoredUserList> { } } + } // Folia - region threading } } diff --git a/src/main/java/net/minecraft/util/SpawnUtil.java b/src/main/java/net/minecraft/util/SpawnUtil.java index 5c8e36ea8287029b1789719c687bac1a2c4c3a69..db0fe7fa257842eacd250f4cae6f549b9a0a91c9 100644 --- a/src/main/java/net/minecraft/util/SpawnUtil.java +++ b/src/main/java/net/minecraft/util/SpawnUtil.java @@ -58,7 +58,7 @@ public class SpawnUtil { return Optional.of(t0); } - t0.discard(null); // CraftBukkit - add Bukkit remove cause + //t0.discard(null); // CraftBukkit - add Bukkit remove cause // Folia - region threading } } } diff --git a/src/main/java/net/minecraft/world/RandomSequences.java b/src/main/java/net/minecraft/world/RandomSequences.java index c83598bfaa6fc9a6041fd828eb3c8b46334461a4..f8bc3450c054b201b7105e51bbba254efe2c2341 100644 --- a/src/main/java/net/minecraft/world/RandomSequences.java +++ b/src/main/java/net/minecraft/world/RandomSequences.java @@ -21,7 +21,7 @@ public class RandomSequences extends SavedData { private int salt; private boolean includeWorldSeed = true; private boolean includeSequenceId = true; - private final Map sequences = new Object2ObjectOpenHashMap<>(); + private final Map sequences = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading public static SavedData.Factory factory(long seed) { return new SavedData.Factory<>(() -> new RandomSequences(seed), (nbt, registryLookup) -> load(seed, nbt), DataFixTypes.SAVED_DATA_RANDOM_SEQUENCES); @@ -114,61 +114,61 @@ public class RandomSequences extends SavedData { @Override public RandomSource fork() { RandomSequences.this.setDirty(); - return this.random.fork(); + synchronized (this.random) { return this.random.fork(); } // Folia - region threading } @Override public PositionalRandomFactory forkPositional() { RandomSequences.this.setDirty(); - return this.random.forkPositional(); + synchronized (this.random) { return this.random.forkPositional(); } // Folia - region threading } @Override public void setSeed(long seed) { RandomSequences.this.setDirty(); - this.random.setSeed(seed); + synchronized (this.random) { this.random.setSeed(seed); } // Folia - region threading } @Override public int nextInt() { RandomSequences.this.setDirty(); - return this.random.nextInt(); + synchronized (this.random) { return this.random.nextInt(); } // Folia - region threading } @Override public int nextInt(int bound) { RandomSequences.this.setDirty(); - return this.random.nextInt(bound); + synchronized (this.random) { return this.random.nextInt(bound); } // Folia - region threading } @Override public long nextLong() { RandomSequences.this.setDirty(); - return this.random.nextLong(); + synchronized (this.random) { return this.random.nextLong(); } // Folia - region threading } @Override public boolean nextBoolean() { RandomSequences.this.setDirty(); - return this.random.nextBoolean(); + synchronized (this.random) { return this.random.nextBoolean(); } // Folia - region threading } @Override public float nextFloat() { RandomSequences.this.setDirty(); - return this.random.nextFloat(); + synchronized (this.random) { return this.random.nextFloat(); } // Folia - region threading } @Override public double nextDouble() { RandomSequences.this.setDirty(); - return this.random.nextDouble(); + synchronized (this.random) { return this.random.nextDouble(); } // Folia - region threading } @Override public double nextGaussian() { RandomSequences.this.setDirty(); - return this.random.nextGaussian(); + synchronized (this.random) { return this.random.nextGaussian(); } // Folia - region threading } @Override diff --git a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java index 99a7e9eb75231c15bd8bb24fbb4e296bc9fdedff..b4a081392b68ccb869392f93ee1f259f0d4f6adc 100644 --- a/src/main/java/net/minecraft/world/damagesource/CombatTracker.java +++ b/src/main/java/net/minecraft/world/damagesource/CombatTracker.java @@ -53,7 +53,7 @@ public class CombatTracker { } private Component getMessageForAssistedFall(Entity attacker, Component attackerDisplayName, String itemDeathTranslationKey, String deathTranslationKey) { - ItemStack itemStack = attacker instanceof LivingEntity livingEntity ? livingEntity.getMainHandItem() : ItemStack.EMPTY; + ItemStack itemStack = attacker instanceof LivingEntity livingEntity && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity) ? livingEntity.getMainHandItem() : ItemStack.EMPTY; // Folia - region threading return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME) ? Component.translatable(itemDeathTranslationKey, this.mob.getDisplayName(), attackerDisplayName, itemStack.getDisplayName()) : Component.translatable(deathTranslationKey, this.mob.getDisplayName(), attackerDisplayName); @@ -80,7 +80,7 @@ public class CombatTracker { @Nullable private static Component getDisplayName(@Nullable Entity entity) { - return entity == null ? null : entity.getDisplayName(); + return entity == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) ? null : entity.getDisplayName(); // Folia - region threading } public Component getDeathMessage() { diff --git a/src/main/java/net/minecraft/world/damagesource/DamageSource.java b/src/main/java/net/minecraft/world/damagesource/DamageSource.java index bb1a60180e58c1333e7bb33e8acf1b0225eda8a8..bc7568c26e6f2b64365712b31d5fce708a0a272d 100644 --- a/src/main/java/net/minecraft/world/damagesource/DamageSource.java +++ b/src/main/java/net/minecraft/world/damagesource/DamageSource.java @@ -178,13 +178,13 @@ public class DamageSource { LivingEntity entityliving1 = killed.getKillCredit(); String s1 = s + ".player"; - return entityliving1 != null ? Component.translatable(s1, killed.getDisplayName(), entityliving1.getDisplayName()) : Component.translatable(s, killed.getDisplayName()); + return entityliving1 != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entityliving1) ? Component.translatable(s1, killed.getDisplayName(), entityliving1.getDisplayName()) : Component.translatable(s, killed.getDisplayName()); // Folia - region threading } else { Component ichatbasecomponent = this.causingEntity == null ? this.directEntity.getDisplayName() : this.causingEntity.getDisplayName(); Entity entity = this.causingEntity; ItemStack itemstack; - if (entity instanceof LivingEntity) { + if (entity instanceof LivingEntity livingEntity && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity)) { // Folia - region threading LivingEntity entityliving2 = (LivingEntity) entity; itemstack = entityliving2.getMainHandItem(); diff --git a/src/main/java/net/minecraft/world/damagesource/FallLocation.java b/src/main/java/net/minecraft/world/damagesource/FallLocation.java index e9df8f8541b8a1b85c7d2925ff3cba813007a1ef..d3f2775a68121ca80ef55ea4c280a0c9fcae2db3 100644 --- a/src/main/java/net/minecraft/world/damagesource/FallLocation.java +++ b/src/main/java/net/minecraft/world/damagesource/FallLocation.java @@ -35,7 +35,7 @@ public record FallLocation(String id) { @Nullable public static FallLocation getCurrentFallLocation(LivingEntity entity) { Optional optional = entity.getLastClimbablePos(); - if (optional.isPresent()) { + if (optional.isPresent() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)entity.level(), optional.get())) { // Folia - region threading BlockState blockState = entity.level().getBlockState(optional.get()); return blockToFallLocation(blockState); } else { diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 490ee48346395fcbaf2eb0151e9248f18974fea6..f736ad7e211c89e4c7edc689b089b1b3b486fbab 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -178,7 +178,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess // Paper start - Share random for entities to make them more random public static RandomSource SHARED_RANDOM = new RandomRandomSource(); - private static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource { + public static final class RandomRandomSource extends java.util.Random implements net.minecraft.world.level.levelgen.BitRandomSource { // Folia - region threading private boolean locked = false; @Override @@ -251,7 +251,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason public boolean collisionLoadChunks = false; // Paper - private CraftEntity bukkitEntity; + private volatile CraftEntity bukkitEntity; // Folia - region threading public CraftEntity getBukkitEntity() { if (this.bukkitEntity == null) { @@ -382,7 +382,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess private boolean hasGlowingTag; private final Set tags; private final double[] pistonDeltas; - private long pistonDeltasGameTime; + private long pistonDeltasGameTime = Long.MIN_VALUE; // Folia - region threading private EntityDimensions dimensions; private float eyeHeight; public boolean isInPowderSnow; @@ -549,6 +549,19 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } } // Paper end - optimise entity tracker + // Folia start - region ticking + public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { + if (this.activatedTick != Integer.MIN_VALUE) { + this.activatedTick += fromTickOffset; + } + if (this.activatedImmunityTick != Integer.MIN_VALUE) { + this.activatedImmunityTick += fromTickOffset; + } + if (this.pistonDeltasGameTime != Long.MIN_VALUE) { + this.pistonDeltasGameTime += fromRedstoneTimeOffset; + } + } + // Folia end - region ticking public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); @@ -696,8 +709,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess // due to interactions on the client. public void resendPossiblyDesyncedEntityData(net.minecraft.server.level.ServerPlayer player) { if (player.getBukkitEntity().canSee(this.getBukkitEntity())) { - ServerLevel world = (net.minecraft.server.level.ServerLevel)this.level(); - net.minecraft.server.level.ChunkMap.TrackedEntity tracker = world == null ? null : world.getChunkSource().chunkMap.entityMap.get(this.getId()); + net.minecraft.server.level.ChunkMap.TrackedEntity tracker = this.moonrise$getTrackedEntity(); // Folia - region threading if (tracker == null) { return; } @@ -852,7 +864,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess public void postTick() { // No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities - this.handlePortal(); + //this.handlePortal(); // Folia - region threading } } // CraftBukkit end @@ -872,7 +884,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.walkDistO = this.walkDist; this.xRotO = this.getXRot(); this.yRotO = this.getYRot(); - if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick + //if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick // Folia - region threading - ONLY allow in postTick() if (this.canSpawnSprintParticle()) { this.spawnSprintParticle(); } @@ -1128,8 +1140,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } else { this.wasOnFire = this.isOnFire(); if (movementType == MoverType.PISTON) { - this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper - this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper + this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper + this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper movement = this.limitPistonMovement(movement); if (movement.equals(Vec3.ZERO)) { return; @@ -1421,7 +1433,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess if (movement.lengthSqr() <= 1.0E-7D) { return movement; } else { - long i = this.level().getGameTime(); + long i = this.level().getRedstoneGameTime(); // Folia - region threading if (i != this.pistonDeltasGameTime) { Arrays.fill(this.pistonDeltas, 0.0D); @@ -3124,7 +3136,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.passengers = ImmutableList.copyOf(list); } - this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); + if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); // Folia - region threading - do not fire game events for entities not added } } @@ -3172,7 +3184,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } entity.boardingCooldown = 60; - this.gameEvent(GameEvent.ENTITY_DISMOUNT, entity); + if (!entity.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_DISMOUNT, entity); // Folia - region threading - do not fire game events for entities not added } return true; // CraftBukkit } @@ -3255,7 +3267,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } } - protected void handlePortal() { + public boolean handlePortal() { // Folia - region threading - public, ret type -> boolean Level world = this.level(); if (world instanceof ServerLevel worldserver) { @@ -3264,23 +3276,21 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess if (this.portalProcess.processPortalTeleportation(worldserver, this, this.canUsePortal(false))) { worldserver.getProfiler().push("portal"); this.setPortalCooldown(); - DimensionTransition dimensiontransition = this.portalProcess.getPortalDestination(worldserver, this); - - if (dimensiontransition != null) { - ServerLevel worldserver1 = dimensiontransition.newLevel(); - - if (this instanceof ServerPlayer || (worldserver1 != null && (worldserver1.dimension() == worldserver.dimension() || this.canChangeDimensions(worldserver, worldserver1)))) { // CraftBukkit - always call event for players - this.changeDimension(dimensiontransition); - } + // Folia start - region threading + try { + return this.portalProcess.portalAsync(worldserver, this); + } finally { + worldserver.getProfiler().pop(); } - - worldserver.getProfiler().pop(); + // Folia end - region threading } else if (this.portalProcess.hasExpired()) { this.portalProcess = null; } } } + + return false; // Folia - region threading } public int getDimensionChangingDelay() { @@ -3421,6 +3431,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess @Nullable public PlayerTeam getTeam() { + // Folia start - region threading + if (true) { + return null; + } + // Folia end - region threading if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper - Perf: Disable Scoreboards for non players by default return this.level().getScoreboard().getPlayersTeam(this.getScoreboardName()); } @@ -3710,8 +3725,751 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.portalProcess = original.portalProcess; } + // Folia start - region threading + public static class EntityTreeNode { + @Nullable + public EntityTreeNode parent; + public Entity root; + @Nullable + public EntityTreeNode[] passengers; + + public EntityTreeNode(EntityTreeNode parent, Entity root) { + this.parent = parent; + this.root = root; + } + + public EntityTreeNode(EntityTreeNode parent, Entity root, EntityTreeNode[] passengers) { + this.parent = parent; + this.root = root; + this.passengers = passengers; + } + + public List getFullTree() { + List ret = new java.util.ArrayList<>(); + ret.add(this); + + // this is just a BFS except we don't remove from head, we just advance down the list + for (int i = 0; i < ret.size(); ++i) { + EntityTreeNode node = ret.get(i); + + EntityTreeNode[] passengers = node.passengers; + if (passengers == null) { + continue; + } + for (EntityTreeNode passenger : passengers) { + ret.add(passenger); + } + } + + return ret; + } + + public void restore() { + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(this); + + EntityTreeNode curr; + while ((curr = queue.pollFirst()) != null) { + EntityTreeNode[] passengers = curr.passengers; + if (passengers == null) { + continue; + } + + List newPassengers = new java.util.ArrayList<>(); + for (EntityTreeNode passenger : passengers) { + newPassengers.add(passenger.root); + passenger.root.vehicle = curr.root; + } + + curr.root.passengers = ImmutableList.copyOf(newPassengers); + } + } + + public void addTracker() { + for (final EntityTreeNode node : this.getFullTree()) { + if (node.root.moonrise$getTrackedEntity() != null) { + for (final ServerPlayer player : node.root.level.getLocalPlayers()) { + node.root.moonrise$getTrackedEntity().updatePlayer(player); + } + } + } + } + + public void clearTracker() { + for (final EntityTreeNode node : this.getFullTree()) { + if (node.root.moonrise$getTrackedEntity() != null) { + node.root.moonrise$getTrackedEntity().moonrise$removeNonTickThreadPlayers(); + for (final ServerPlayer player : node.root.level.getLocalPlayers()) { + node.root.moonrise$getTrackedEntity().removePlayer(player); + } + } + } + } + + public void adjustRiders(boolean teleport) { + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(this); + + EntityTreeNode curr; + while ((curr = queue.pollFirst()) != null) { + EntityTreeNode[] passengers = curr.passengers; + if (passengers == null) { + continue; + } + + for (EntityTreeNode passenger : passengers) { + curr.root.positionRider(passenger.root, teleport ? Entity::moveTo : Entity::setPos); + } + } + } + } + + public void repositionAllPassengers(boolean teleport) { + this.makePassengerTree().adjustRiders(teleport); + } + + protected EntityTreeNode makePassengerTree() { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot read passengers off of the main thread"); + + EntityTreeNode root = new EntityTreeNode(null, this); + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(root); + EntityTreeNode curr; + while ((curr = queue.pollFirst()) != null) { + Entity vehicle = curr.root; + List passengers = vehicle.passengers; + if (passengers.isEmpty()) { + continue; + } + + EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; + curr.passengers = treePassengers; + + for (int i = 0; i < passengers.size(); ++i) { + Entity passenger = passengers.get(i); + queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); + } + } + + return root; + } + + protected EntityTreeNode detachPassengers() { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot adjust passengers/vehicle off of the main thread"); + + EntityTreeNode root = new EntityTreeNode(null, this); + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(root); + EntityTreeNode curr; + while ((curr = queue.pollFirst()) != null) { + Entity vehicle = curr.root; + List passengers = vehicle.passengers; + if (passengers.isEmpty()) { + continue; + } + + vehicle.passengers = ImmutableList.of(); + + EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; + curr.passengers = treePassengers; + + for (int i = 0; i < passengers.size(); ++i) { + Entity passenger = passengers.get(i); + passenger.vehicle = null; + queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); + } + } + + return root; + } + + /** + * This flag will perform an async load on the chunks determined by + * the entity's bounding box before teleporting the entity. + */ + public static final long TELEPORT_FLAG_LOAD_CHUNK = 1L << 0; + /** + * This flag requires the entity being teleported to be a root vehicle. + * Thus, if you want to teleport a non-root vehicle, you must dismount + * the target entity before calling teleport, otherwise the + * teleport will be refused. + */ + public static final long TELEPORT_FLAG_TELEPORT_PASSENGERS = 1L << 1; + /** + * The flag will dismount any passengers and dismout from the current vehicle + * to teleport if and only if dismounting would result in the teleport being allowed. + */ + public static final long TELEPORT_FLAG_UNMOUNT = 1L << 2; + + protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { + destination.addDuringTeleport(this); + } + + protected final void placeInAsync(ServerLevel originWorld, ServerLevel destination, long teleportFlags, + EntityTreeNode passengerTree, java.util.function.Consumer teleportComplete) { + Vec3 pos = this.position(); + ChunkPos posChunk = new ChunkPos( + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos) + ); + + // ensure the region is always ticking in case of a shutdown + // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region + Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); + originWorld.chunkSource.addTicketAtLevel( + TicketType.TELEPORT_HOLD_TICKET, posChunk, + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + teleportHoldId + ); + final ServerLevel.PendingTeleport pendingTeleport = new ServerLevel.PendingTeleport(passengerTree, pos); + destination.pushPendingTeleport(pendingTeleport); + + Runnable scheduleEntityJoin = () -> { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + destination, + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos), + () -> { + if (!destination.removePendingTeleport(pendingTeleport)) { + // shutdown logic placed the entity already, and we are shutting down - do nothing to ensure + // we do not produce any errors here + return; + } + originWorld.chunkSource.removeTicketAtLevel( + TicketType.TELEPORT_HOLD_TICKET, posChunk, + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + teleportHoldId + ); + List fullTree = passengerTree.getFullTree(); + for (EntityTreeNode node : fullTree) { + node.root.placeSingleSync(originWorld, destination, node, teleportFlags); + } + + // restore passenger tree + passengerTree.restore(); + passengerTree.adjustRiders(true); + + // invoke post dimension change now + for (EntityTreeNode node : fullTree) { + node.root.postChangeDimension(); + } + + if (teleportComplete != null) { + teleportComplete.accept(Entity.this); + } + } + ); + }; + + if ((teleportFlags & TELEPORT_FLAG_LOAD_CHUNK) != 0L) { + destination.loadChunksForMoveAsync( + this.getBoundingBox(), ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER, + (chunkList) -> { + for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunkList) { + destination.chunkSource.addTicketAtLevel( + TicketType.POST_TELEPORT, chunk.getPos(), + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, + Integer.valueOf(Entity.this.getId()) + ); + } + scheduleEntityJoin.run(); + } + ); + } else { + scheduleEntityJoin.run(); + } + } + + protected boolean canTeleportAsync() { + return !this.hasNullCallback() && !this.isRemoved() && this.isAlive() && (!(this instanceof net.minecraft.world.entity.LivingEntity livingEntity) || !livingEntity.isSleeping()); + } + + // Mojang for whatever reason has started storing positions to cache certain physics properties that entities collide with + // As usual though, they don't properly do anything to prevent serious desync with respect to the current entity position + // We add additional logic to reset these before teleporting to prevent issues with them possibly tripping thread checks. + protected void resetStoredPositions() { + this.mainSupportingBlockPos = Optional.empty(); + } + + protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { + if (yaw != null) { + this.setYRot(yaw.floatValue()); + this.setYHeadRot(yaw.floatValue()); + } + if (pitch != null) { + this.setXRot(pitch.floatValue()); + } + if (velocity != null) { + this.setDeltaMovement(velocity); + } + this.moveTo(pos.x, pos.y, pos.z); + this.resetStoredPositions(); + } + + protected void transform(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { + if (yaw != null) { + this.setYRot(yaw.floatValue()); + this.setYHeadRot(yaw.floatValue()); + } + if (pitch != null) { + this.setXRot(pitch.floatValue()); + } + if (velocity != null) { + this.setDeltaMovement(velocity); + } + if (pos != null) { + this.setPosRaw(pos.x, pos.y, pos.z); + } + } + + protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { + this.removeAfterChangingDimensions(); // remove before so that any CBEntity#getHandle call affects this entity before copying + + Entity copy = this.getType().create(destination); + copy.restoreFrom(this); + copy.transform(pos, yaw, pitch, velocity); + // vanilla code used to call remove _after_ copying, and some stuff is required to be after copy - so add hook here + // for example, clearing of inventory after switching dimensions + this.postRemoveAfterChangingDimensions(); + + return copy; + } + + public final boolean teleportAsync(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity, + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, long teleportFlags, + java.util.function.Consumer teleportComplete) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot teleport entity async"); + + if (!ServerLevel.isInSpawnableBounds(new BlockPos(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockY(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockZ(pos)))) { + return false; + } + + if (!this.canTeleportAsync()) { + return false; + } + this.getBukkitEntity(); // force bukkit entity to be created before TPing + if ((teleportFlags & TELEPORT_FLAG_UNMOUNT) == 0L) { + for (Entity entity : this.getIndirectPassengers()) { + if (!entity.canTeleportAsync()) { + return false; + } + entity.getBukkitEntity(); // force bukkit entity to be created before TPing + } + } else { + this.unRide(); + } + + if ((teleportFlags & TELEPORT_FLAG_TELEPORT_PASSENGERS) != 0L) { + if (this.isPassenger()) { + return false; + } + } else { + if (this.isVehicle() || this.isPassenger()) { + return false; + } + } + + // TODO any events that can modify go HERE + + // check for same region + if (destination == this.level()) { + Vec3 currPos = this.position(); + if ( + destination.regioniser.getRegionAtUnsynchronised( + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(currPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(currPos) + ) == destination.regioniser.getRegionAtUnsynchronised( + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos) + ) + ) { + EntityTreeNode passengerTree = this.detachPassengers(); + // Note: The client does not accept position updates for controlled entities. So, we must + // perform a lot of tracker updates here to make it all work out. + + // first, clear the tracker + passengerTree.clearTracker(); + for (EntityTreeNode entity : passengerTree.getFullTree()) { + entity.root.teleportSyncSameRegion(pos, yaw, pitch, velocity); + } + + passengerTree.restore(); + // re-add to the tracker once the tree is restored + passengerTree.addTracker(); + + // adjust entities to final position + passengerTree.adjustRiders(true); + + // the tracker clear/add logic is only used in the same region, as the other logic + // performs add/remove from world logic which will also perform add/remove tracker logic + + if (teleportComplete != null) { + teleportComplete.accept(this); + } + return true; + } + } + + EntityTreeNode passengerTree = this.detachPassengers(); + List fullPassengerTree = passengerTree.getFullTree(); + ServerLevel originWorld = (ServerLevel)this.level; + + for (EntityTreeNode node : fullPassengerTree) { + node.root.preChangeDimension(); + } + + for (EntityTreeNode node : fullPassengerTree) { + node.root = node.root.transformForAsyncTeleport(destination, pos, yaw, pitch, velocity); + } + + passengerTree.root.placeInAsync(originWorld, destination, teleportFlags, passengerTree, teleportComplete); + + return true; + } + + public void preChangeDimension() { + + } + + public void postChangeDimension() { + this.resetStoredPositions(); + } + + protected static enum PortalType { + NETHER, END; + } + + public boolean endPortalLogicAsync(BlockPos portalPos) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + + ServerLevel destination = this.getServer().getLevel(this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END ? Level.OVERWORLD : Level.END); + if (destination == null) { + // wat + return false; + } + + return this.portalToAsync(destination, portalPos, true, PortalType.END, null); + } + + public boolean netherPortalLogicAsync(BlockPos portalPos) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + + ServerLevel destination = this.getServer().getLevel(this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER ? Level.OVERWORLD : Level.NETHER); + if (destination == null) { + // wat + return false; + } + + return this.portalToAsync(destination, portalPos, true, PortalType.NETHER, null); + } + + private static final java.util.concurrent.atomic.AtomicLong CREATE_PORTAL_DOUBLE_CHECK = new java.util.concurrent.atomic.AtomicLong(); + private static final java.util.concurrent.atomic.AtomicLong TELEPORT_HOLD_TICKET_GEN = new java.util.concurrent.atomic.AtomicLong(); + + // To simplify portal logic, in region threading both players + // and non-player entities will create portals. By guaranteeing + // that the teleportation can take place, we can simply + // remove the entity, find/create the portal, and place async. + // If we have to worry about whether the entity may not teleport, + // we need to first search, then report back, ... + protected void findOrCreatePortalAsync(ServerLevel origin, BlockPos originPortal, ServerLevel destination, PortalType type, + ca.spottedleaf.concurrentutil.completable.Completable portalInfoCompletable) { + switch (type) { + // end portal logic is quite simple, the spawn in the end is fixed and when returning to the overworld + // we just select the spawn position + case END: { + if (destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { + BlockPos targetPos = ServerLevel.END_SPAWN_POINT; + // need to load chunks so we can create the platform + destination.moonrise$loadChunksAsync( + targetPos, 16, // load 16 blocks to be safe from block physics + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, + (chunks) -> { + net.minecraft.world.level.levelgen.feature.EndPlatformFeature.createEndPlatform(destination, targetPos.below(), true, null); + + // the portal obsidian is placed at targetPos.y - 2, so if we want to place the entity + // on the obsidian, we need to spawn at targetPos.y - 1 + portalInfoCompletable.complete( + new net.minecraft.world.level.portal.DimensionTransition( + destination, Vec3.atBottomCenterOf(targetPos.below()), Vec3.ZERO, 90.0f, 0.0f, + DimensionTransition.PLAY_PORTAL_SOUND.then(DimensionTransition.PLACE_PORTAL_TICKET), + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL + ) + ); + } + ); + } else { + BlockPos spawnPos = destination.getSharedSpawnPos(); + // need to load chunk for heightmap + destination.moonrise$loadChunksAsync( + spawnPos, 0, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, + (chunks) -> { + BlockPos adjustedSpawn = destination.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, spawnPos); + + // done + portalInfoCompletable.complete( + new net.minecraft.world.level.portal.DimensionTransition( + destination, Vec3.atBottomCenterOf(adjustedSpawn), Vec3.ZERO, 90.0f, 0.0f, + DimensionTransition.PLAY_PORTAL_SOUND.then(DimensionTransition.PLACE_PORTAL_TICKET), + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL + ) + ); + } + ); + } + + break; + } + // for the nether logic, we need to first load the chunks in radius to empty (so that POI is created) + // then we can search for an existing portal using the POI routines + // if we don't find a portal, then we bring the chunks in the create radius to full and + // create it + case NETHER: { + // hoisted from the create fallback, so that we can avoid the sync load later if we need it + BlockState originalPortalBlock = origin.getBlockStateIfLoaded(originPortal); + Direction.Axis originalPortalDirection = originalPortalBlock == null ? Direction.Axis.X : + originalPortalBlock.getOptionalValue(net.minecraft.world.level.block.NetherPortalBlock.AXIS).orElse(Direction.Axis.X); + BlockUtil.FoundRectangle originalPortalRectangle = + originalPortalBlock == null || !originalPortalBlock.hasProperty(net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_AXIS) + ? null + : BlockUtil.getLargestRectangleAround( + originPortal, originalPortalDirection, 21, Direction.Axis.Y, 21, + (blockpos) -> { + return origin.getBlockStateFromEmptyChunkIfLoaded(blockpos) == originalPortalBlock; + } + ); + + boolean destinationIsNether = destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER; + + int portalSearchRadius = origin.paperConfig().environment.portalSearchVanillaDimensionScaling && destinationIsNether ? + (int)(destination.paperConfig().environment.portalSearchRadius / destination.dimensionType().coordinateScale()) : + destination.paperConfig().environment.portalSearchRadius; + int portalCreateRadius = destination.paperConfig().environment.portalCreateRadius; + + WorldBorder destinationBorder = destination.getWorldBorder(); + double dimensionScale = net.minecraft.world.level.dimension.DimensionType.getTeleportationScale(origin.dimensionType(), destination.dimensionType()); + BlockPos targetPos = destination.getWorldBorder().clampToBounds(this.getX() * dimensionScale, this.getY(), this.getZ() * dimensionScale); + + ca.spottedleaf.concurrentutil.completable.Completable portalFound + = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + + // post portal find/create logic + portalFound.addWaiter( + (BlockUtil.FoundRectangle portal, Throwable thr) -> { + // no portal could be created + if (portal == null) { + portalInfoCompletable.complete( + new DimensionTransition(destination, Vec3.atCenterOf(targetPos), Vec3.ZERO, + 90.0f, 0.0f, + DimensionTransition.PLAY_PORTAL_SOUND.then(DimensionTransition.PLACE_PORTAL_TICKET)) + ); + return; + } + + Vec3 relativePos = originalPortalRectangle == null ? + new Vec3(0.5, 0.0, 0.0) : + Entity.this.getRelativePortalPosition(originalPortalDirection, originalPortalRectangle); + + portalInfoCompletable.complete( + net.minecraft.world.level.block.NetherPortalBlock.createDimensionTransition( + destination, portal, originalPortalDirection, relativePos, + Entity.this, Entity.this.getDeltaMovement(), Entity.this.getYRot(), Entity.this.getXRot(), + DimensionTransition.PLAY_PORTAL_SOUND.then(DimensionTransition.PLACE_PORTAL_TICKET) + ) + ); + } + ); + + // kick off search for existing portal or creation + destination.moonrise$loadChunksAsync( + // add 32 so that the final search for a portal frame doesn't load any chunks + targetPos, portalSearchRadius + 32, + net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, + (chunks) -> { + BlockUtil.FoundRectangle portal = + net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius); + if (portal != null) { + portalFound.complete(portal); + return; + } + + // add tickets so that we can re-search for a portal once the chunks are loaded + Long ticketId = Long.valueOf(CREATE_PORTAL_DOUBLE_CHECK.getAndIncrement()); + for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { + destination.chunkSource.addTicketAtLevel( + TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + ticketId + ); + } + + // no portal found - create one + destination.moonrise$loadChunksAsync( + targetPos, portalCreateRadius + 32, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGH, + (chunks2) -> { + // don't need the tickets anymore + // note: we expect removeTicketsAtLevel to add an unknown ticket for us automatically + // if the ticket level were to decrease + for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { + destination.chunkSource.removeTicketAtLevel( + TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + ticketId + ); + } + + // when two entities portal at the same time, it is possible that both entities reach this + // part of the code - and create a double portal + // to fix this, we just issue another search to try and see if another entity created + // a portal nearby + BlockUtil.FoundRectangle existingTryAgain = + net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius); + if (existingTryAgain != null) { + portalFound.complete(existingTryAgain); + return; + } + + // we do not have the correct entity reference here + BlockUtil.FoundRectangle createdPortal = + destination.getPortalForcer().createPortal(targetPos, originalPortalDirection, null, portalCreateRadius).orElse(null); + // if it wasn't created, passing null is expected here + portalFound.complete(createdPortal); + } + ); + } + ); + break; + } + default: { + throw new IllegalStateException("Unknown portal type " + type); + } + } + } + + public boolean canPortalAsync(ServerLevel to, boolean considerPassengers) { + return this.canPortalAsync(to, considerPassengers, false); + } + + protected boolean canPortalAsync(ServerLevel to, boolean considerPassengers, boolean skipPassengerCheck) { + if (considerPassengers) { + if (!skipPassengerCheck && this.isPassenger()) { + return false; + } + } else { + if (this.isVehicle() || (!skipPassengerCheck && this.isPassenger())) { + return false; + } + } + this.getBukkitEntity(); // force bukkit entity to be created before TPing + if (!this.canTeleportAsync() || !this.canChangeDimensions(this.level(), to)) { + return false; + } + if (considerPassengers) { + for (Entity entity : this.passengers) { + if (!entity.canPortalAsync(to, true, true)) { + return false; + } + } + } + + return true; + } + + protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { + + } + + protected boolean portalToAsync(ServerLevel destination, BlockPos portalPos, boolean takePassengers, + PortalType type, java.util.function.Consumer teleportComplete) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); + if (!this.canPortalAsync(destination, takePassengers)) { + return false; + } + + Vec3 initialPosition = this.position(); + ChunkPos initialPositionChunk = new ChunkPos( + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(initialPosition), + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(initialPosition) + ); + + // first, remove entity/passengers from world + EntityTreeNode passengerTree = this.detachPassengers(); + List fullPassengerTree = passengerTree.getFullTree(); + ServerLevel originWorld = (ServerLevel)this.level; + + for (EntityTreeNode node : fullPassengerTree) { + node.root.preChangeDimension(); + node.root.prePortalLogic(originWorld, destination, type); + } + + for (EntityTreeNode node : fullPassengerTree) { + // we will update pos/rot/speed later + node.root = node.root.transformForAsyncTeleport(destination, null, null, null, null); + // set portal cooldown + node.root.setPortalCooldown(); + } + + // ensure the region is always ticking in case of a shutdown + // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region + Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); + originWorld.chunkSource.addTicketAtLevel( + TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + teleportHoldId + ); + + ServerLevel.PendingTeleport beforeFindDestination = new ServerLevel.PendingTeleport(passengerTree, initialPosition); + originWorld.pushPendingTeleport(beforeFindDestination); + + ca.spottedleaf.concurrentutil.completable.Completable portalInfoCompletable + = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + + portalInfoCompletable.addWaiter((DimensionTransition info, Throwable throwable) -> { + if (!originWorld.removePendingTeleport(beforeFindDestination)) { + // the shutdown thread has placed us back into the origin world at the original position + // we just have to abandon this teleport to prevent duplication + return; + } + originWorld.chunkSource.removeTicketAtLevel( + TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + teleportHoldId + ); + // adjust passenger tree to final pos + for (EntityTreeNode node : fullPassengerTree) { + node.root.transform(info.pos(), Float.valueOf(info.yRot()), Float.valueOf(info.xRot()), info.speed()); + } + + // place + passengerTree.root.placeInAsync( + originWorld, destination, Entity.TELEPORT_FLAG_LOAD_CHUNK | (takePassengers ? Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS : 0L), + passengerTree, + (Entity teleported) -> { + if (info.postDimensionTransition() != null) { + info.postDimensionTransition().onTransition(teleported); + } + + if (teleportComplete != null) { + teleportComplete.accept(teleported); + } + } + ); + }); + + + passengerTree.root.findOrCreatePortalAsync(originWorld, portalPos, destination, type, portalInfoCompletable); + + return true; + } + // Folia end - region threading + @Nullable public Entity changeDimension(DimensionTransition teleportTarget) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading Level world = this.level(); // Paper start - Fix item duplication and teleport issues @@ -3828,6 +4586,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } + // Folia start - region threading - move inventory clearing until after the dimension change + protected void postRemoveAfterChangingDimensions() { + + } + // Folia end - region threading - move inventory clearing until after the dimension change + protected void removeAfterChangingDimensions() { this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION, null); // CraftBukkit - add Bukkit remove cause if (this instanceof Leashable leashable) { @@ -4699,7 +5463,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } } // Paper end - Fix MC-4 - if (this.position.x != x || this.position.y != y || this.position.z != z) { + boolean posChanged = this.position.x != x || this.position.y != y || this.position.z != z; // Folia - region threading + if (posChanged) { // Folia - region threading synchronized (this.posLock) { // Paper this.position = new Vec3(x, y, z); } // Paper @@ -4720,7 +5485,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess // Paper start - Block invalid positions and bounding box; don't allow desync of pos and AABB // hanging has its own special logic - if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || this.position.x != x || this.position.y != y || this.position.z != z)) { + if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || posChanged)) { this.setBoundingBox(this.makeBoundingBox()); } // Paper end - Block invalid positions and bounding box @@ -4805,6 +5570,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return this.removalReason != null; } + // Folia start - region threading + public final boolean hasNullCallback() { + return this.levelCallback == EntityInLevelCallback.NULL; + } + // Folia end - region threading + @Nullable public Entity.RemovalReason getRemovalReason() { return this.removalReason; @@ -4827,6 +5598,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit end final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers + // Folia start - region threading + this.preRemove(entity_removalreason); + // Folia end - region threading if (this.removalReason == null) { this.removalReason = entity_removalreason; } @@ -4849,6 +5623,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.removalReason = null; } + // Folia start - region threading + protected void preRemove(Entity.RemovalReason reason) {} + // Folia end - region threading + // Paper start - Folia schedulers /** * Invoked only when the entity is truly removed from the server, never to be added to any world. diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java index ccd9dff20a60f019e0c320acfb526b8bf3e5f806..6e2ce383bb4d52702dfcb8823ce0c5dce7c81b5f 100644 --- a/src/main/java/net/minecraft/world/entity/LivingEntity.java +++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java @@ -265,7 +265,7 @@ public abstract class LivingEntity extends Entity implements Attackable { private Optional lastClimbablePos; @Nullable private DamageSource lastDamageSource; - private long lastDamageStamp; + private long lastDamageStamp = Long.MIN_VALUE; // Folia - region threading protected int autoSpinAttackTicks; protected float autoSpinAttackDmg; @Nullable @@ -299,6 +299,21 @@ public abstract class LivingEntity extends Entity implements Attackable { ++this.noActionTime; // Above all the floats } // Spigot end + // Folia start - region threading + @Override + public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { + super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + if (this.lastDamageStamp != Long.MIN_VALUE) { + this.lastDamageStamp += fromRedstoneTimeOffset; + } + } + + @Override + protected void resetStoredPositions() { + super.resetStoredPositions(); + this.lastClimbablePos = Optional.empty(); + } + // Folia end - region threading protected LivingEntity(EntityType type, Level world) { super(type, world); @@ -519,7 +534,7 @@ public abstract class LivingEntity extends Entity implements Attackable { if (this.isDeadOrDying() && this.level().shouldTickDeath(this)) { this.tickDeath(); - } + } else { this.broadcastedDeath = false; } // Folia - region threading if (this.lastHurtByPlayerTime > 0) { --this.lastHurtByPlayerTime; @@ -609,11 +624,14 @@ public abstract class LivingEntity extends Entity implements Attackable { return true; } + public boolean broadcastedDeath = false; // Folia - region threading protected void tickDeath() { ++this.deathTime; - if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved()) { + if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved() && !this.broadcastedDeath) { // Folia - region threading this.level().broadcastEntityEvent(this, (byte) 60); - this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause + this.broadcastedDeath = true; // Folia - region threading - death has been broadcasted + if (!(this instanceof ServerPlayer)) this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause // Folia - region threading - don't remove, we want the tick scheduler to be running + if ((this instanceof ServerPlayer)) this.unRide(); // Folia - region threading - unmount player when dead } } @@ -866,9 +884,9 @@ public abstract class LivingEntity extends Entity implements Attackable { } this.hurtTime = nbt.getShort("HurtTime"); - this.deathTime = nbt.getShort("DeathTime"); + this.deathTime = nbt.getShort("DeathTime"); this.broadcastedDeath = false; // Folia - region threading this.lastHurtByMobTimestamp = nbt.getInt("HurtByTimestamp"); - if (nbt.contains("Team", 8)) { + if (false && nbt.contains("Team", 8)) { // Folia start - region threading String s = nbt.getString("Team"); Scoreboard scoreboard = this.level().getScoreboard(); PlayerTeam scoreboardteam = scoreboard.getPlayerTeam(s); @@ -1146,6 +1164,7 @@ public abstract class LivingEntity extends Entity implements Attackable { public boolean addEffect(MobEffectInstance mobeffect, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) { // Paper end - Don't fire sync event during generation // org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot // Paper - move to API + if (!this.hasNullCallback()) ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot add effects to entities asynchronously"); // Folia - region threading if (this.isTickingEffects) { this.effectsToProcess.add(new ProcessableEffect(mobeffect, cause)); return true; @@ -1594,7 +1613,7 @@ public abstract class LivingEntity extends Entity implements Attackable { if (flag2) { this.lastDamageSource = source; - this.lastDamageStamp = this.level().getGameTime(); + this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading Iterator iterator = this.getActiveEffects().iterator(); while (iterator.hasNext()) { @@ -1696,7 +1715,7 @@ public abstract class LivingEntity extends Entity implements Attackable { @Nullable public DamageSource getLastDamageSource() { - if (this.level().getGameTime() - this.lastDamageStamp > 40L) { + if (this.level().getRedstoneGameTime() - this.lastDamageStamp > 40L || this.lastDamageStamp == Long.MIN_VALUE) { // Folia - region threading this.lastDamageSource = null; } @@ -2451,7 +2470,7 @@ public abstract class LivingEntity extends Entity implements Attackable { @Nullable public LivingEntity getKillCredit() { - return (LivingEntity) (this.lastHurtByPlayer != null ? this.lastHurtByPlayer : (this.lastHurtByMob != null ? this.lastHurtByMob : null)); + return (LivingEntity) (this.lastHurtByPlayer != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByPlayer) ? this.lastHurtByPlayer : (this.lastHurtByMob != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByMob) ? this.lastHurtByMob : null)); // Folia - region threading } public final float getMaxHealth() { @@ -2529,7 +2548,7 @@ public abstract class LivingEntity extends Entity implements Attackable { this.hurt(this.damageSources().generic(), 0.0F); this.lastDamageSource = damageSource; - this.lastDamageStamp = this.level().getGameTime(); + this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading } @Override @@ -3553,7 +3572,7 @@ public abstract class LivingEntity extends Entity implements Attackable { this.pushEntities(); this.level().getProfiler().pop(); // Paper start - Add EntityMoveEvent - if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { + if (((ServerLevel) this.level()).getCurrentWorldData().hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.Player)) { // Folia - region threading if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { Location from = new Location(this.level().getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO); Location to = new Location(this.level().getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); @@ -4249,7 +4268,7 @@ public abstract class LivingEntity extends Entity implements Attackable { BlockPos blockposition = BlockPos.containing(d0, d1, d2); Level world = this.level(); - if (world.hasChunkAt(blockposition)) { + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)world, blockposition) && world.hasChunkAt(blockposition)) { // Folia - region threading boolean flag2 = false; while (!flag2 && blockposition.getY() > world.getMinBuildHeight()) { diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java index bd7c6ce15698aed70376c109ba36f52d6794a2f8..741ba5f0834ab5f0b0ffdeb91a9ff8cfaf922792 100644 --- a/src/main/java/net/minecraft/world/entity/Mob.java +++ b/src/main/java/net/minecraft/world/entity/Mob.java @@ -148,6 +148,14 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab public boolean aware = true; // CraftBukkit + // Folia start - region threading + @Override + public void preChangeDimension() { + super.preChangeDimension(); + this.dropLeash(true, true); + } + // Folia end - region threading + protected Mob(EntityType type, Level world) { super(type, world); this.handItems = NonNullList.withSize(2, ItemStack.EMPTY); @@ -293,8 +301,20 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab @Nullable @Override public LivingEntity getTarget() { + // Folia start - region threading + if (this.target != null && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.target) || this.target.isRemoved())) { + this.target = null; + return null; + } + // Folia end - region threading + return this.target; + } + + // Folia start - region threading + public LivingEntity getTargetRaw() { return this.target; } + // Folia end - region threading @Nullable protected final LivingEntity getTargetFromBrain() { @@ -307,7 +327,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab } public boolean setTarget(LivingEntity entityliving, EntityTargetEvent.TargetReason reason, boolean fireEvent) { - if (this.getTarget() == entityliving) return false; + if (this.getTargetRaw() == entityliving) return false; // Folia - region threading if (fireEvent) { if (reason == EntityTargetEvent.TargetReason.UNKNOWN && this.getTarget() != null && entityliving == null) { reason = this.getTarget().isAlive() ? EntityTargetEvent.TargetReason.FORGOT_TARGET : EntityTargetEvent.TargetReason.TARGET_DIED; @@ -1784,16 +1804,22 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab this.goalSelector.removeAllGoals(predicate); } + // Folia start - region threading - move inventory clearing until after the dimension change @Override - protected void removeAfterChangingDimensions() { - super.removeAfterChangingDimensions(); + protected void postRemoveAfterChangingDimensions() { this.getAllSlots().forEach((itemstack) -> { if (!itemstack.isEmpty()) { itemstack.setCount(0); } - }); } + // Folia end - region threading - move inventory clearing until after the dimension change + + @Override + protected void removeAfterChangingDimensions() { + super.removeAfterChangingDimensions(); + // Folia - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions + } @Nullable @Override diff --git a/src/main/java/net/minecraft/world/entity/PortalProcessor.java b/src/main/java/net/minecraft/world/entity/PortalProcessor.java index 45761c113116ae7417e6ae99069bff35dbccdf30..c55d795209222a34c349d6f59eea36186324825d 100644 --- a/src/main/java/net/minecraft/world/entity/PortalProcessor.java +++ b/src/main/java/net/minecraft/world/entity/PortalProcessor.java @@ -33,6 +33,12 @@ public class PortalProcessor { return this.portal.getPortalDestination(world, entity, this.entryPosition); } + // Folia start - region threading + public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget) { + return this.portal.portalAsync(sourceWorld, portalTarget, this.entryPosition); + } + // Folia end - region threading + public Portal.Transition getPortalLocalTransition() { return this.portal.getLocalTransition(); } diff --git a/src/main/java/net/minecraft/world/entity/TamableAnimal.java b/src/main/java/net/minecraft/world/entity/TamableAnimal.java index 45224dc3867892b298b006c17f7f85741fcc96d6..a72cc0eff0168e826c923607f693e8eebbb1f42f 100644 --- a/src/main/java/net/minecraft/world/entity/TamableAnimal.java +++ b/src/main/java/net/minecraft/world/entity/TamableAnimal.java @@ -266,6 +266,11 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { LivingEntity entityliving = this.getOwner(); if (entityliving != null) { + // Folia start - region threading + if (entityliving.isRemoved() || entityliving.level() != this.level()) { + return; + } + // Folia end - region threading this.teleportToAroundBlockPos(entityliving.blockPosition()); } @@ -303,7 +308,22 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { return false; } Location to = event.getTo(); - this.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); + // Folia start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick + // also, use teleportAsync so that crossing region boundaries will not blow up + Location finalTo = to; + Level sourceWorld = this.level(); + this.getBukkitEntity().taskScheduler.schedule((TamableAnimal nmsEntity) -> { + if (nmsEntity.level() == sourceWorld) { + nmsEntity.teleportAsync( + (net.minecraft.server.level.ServerLevel)nmsEntity.level(), + new net.minecraft.world.phys.Vec3(finalTo.getX(), finalTo.getY(), finalTo.getZ()), + Float.valueOf(finalTo.getYaw()), Float.valueOf(finalTo.getPitch()), + net.minecraft.world.phys.Vec3.ZERO, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN, Entity.TELEPORT_FLAG_LOAD_CHUNK, + null + ); + } + }, null, 1L); + // Folia end - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick // CraftBukkit end this.navigation.stop(); return true; diff --git a/src/main/java/net/minecraft/world/entity/ai/Brain.java b/src/main/java/net/minecraft/world/entity/ai/Brain.java index af6f91c66e9cc7e0d491e6efed992a140947155e..d4e6198fdfbefe54e374479a1f1d835ab98ce93a 100644 --- a/src/main/java/net/minecraft/world/entity/ai/Brain.java +++ b/src/main/java/net/minecraft/world/entity/ai/Brain.java @@ -424,9 +424,17 @@ public class Brain { } public void stopAll(ServerLevel world, E entity) { + // Folia start - region threading + List> behaviors = this.getRunningBehaviors(); + if (behaviors.isEmpty()) { + // avoid calling getGameTime, as this may be called while portalling an entity - which will cause + // the world data retrieval to fail + return; + } + // Folia end - region threading long l = entity.level().getGameTime(); - for (BehaviorControl behaviorControl : this.getRunningBehaviors()) { + for (BehaviorControl behaviorControl : behaviors) { // Folia - region threading behaviorControl.doStop(world, entity, l); } } diff --git a/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java b/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java index 7302d397d39d8400527ab2da4adaf8d792256749..ee3b8de9b700202da776c68579532bf11319a001 100644 --- a/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java +++ b/src/main/java/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java @@ -19,6 +19,11 @@ public class PoiCompetitorScan { context, (jobSite, mobs) -> (world, entity, time) -> { GlobalPos globalPos = context.get(jobSite); + // Folia start - region threading + if (globalPos.dimension() != world.dimension() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, globalPos.pos())) { + return true; + } + // Folia end - region threading world.getPoiManager() .getType(globalPos.pos()) .ifPresent( diff --git a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java index 15d7be9ed4a973044dd4399db46aaa244730b836..df4cce1d3baef0ad386f5bc201663ae569e7b36d 100644 --- a/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +++ b/src/main/java/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java @@ -51,7 +51,7 @@ public class FollowOwnerGoal extends Goal { public boolean canContinueToUse() { return !this.navigation.isDone() && !this.tamable.unableToMoveToOwner() - && !(this.tamable.distanceToSqr(this.owner) <= (double)(this.stopDistance * this.stopDistance)); + && !(this.owner.level() == this.tamable.level() && this.tamable.distanceToSqr(this.owner) <= (double)(this.stopDistance * this.stopDistance)); // Folia - region threading - check level } @Override diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java index 62634bedd97c5be9ecce24ab0cff205715a68da8..e03e07752e335d694c52c8dd780e71528021f15c 100644 --- a/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java +++ b/src/main/java/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java @@ -42,6 +42,11 @@ public class GroundPathNavigation extends PathNavigation { @Override public Path createPath(BlockPos target, @javax.annotation.Nullable Entity entity, int distance) { // Paper - EntityPathfindEvent + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level, target)) { + return null; + } + // Folia end - region threading LevelChunk levelChunk = this.level .getChunkSource() .getChunkNow(SectionPos.blockToSectionCoord(target.getX()), SectionPos.blockToSectionCoord(target.getZ())); diff --git a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java index 2e9991e6b3c05584002744a2ee2579b1dba218b2..7873462c6146b7431011a2b73eb7414f61afbccf 100644 --- a/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java +++ b/src/main/java/net/minecraft/world/entity/ai/navigation/PathNavigation.java @@ -80,11 +80,11 @@ public abstract class PathNavigation { } public void recomputePath() { - if (this.level.getGameTime() - this.timeLastRecompute > 20L) { + if (this.tick - this.timeLastRecompute > 20L) { // Folia - region threading if (this.targetPos != null) { this.path = null; this.path = this.createPath(this.targetPos, this.reachRange); - this.timeLastRecompute = this.level.getGameTime(); + this.timeLastRecompute = this.tick; // Folia - region threading this.hasDelayedRecomputation = false; } } else { @@ -203,7 +203,7 @@ public abstract class PathNavigation { public boolean moveTo(Entity entity, double speed) { // Paper start - Perf: Optimise pathfinding - if (this.pathfindFailures > 10 && this.path == null && net.minecraft.server.MinecraftServer.currentTick < this.lastFailure + 40) { + if (this.pathfindFailures > 10 && this.path == null && this.tick < this.lastFailure + 40) { // Folia - region threading return false; } // Paper end - Perf: Optimise pathfinding @@ -215,7 +215,7 @@ public abstract class PathNavigation { return true; } else { this.pathfindFailures++; - this.lastFailure = net.minecraft.server.MinecraftServer.currentTick; + this.lastFailure = this.tick; // Folia - region threading return false; } // Paper end - Perf: Optimise pathfinding diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java index 4d5372b80b2b1906ecf5bf7e75df08b5d3792bfd..9ee62edd8f71244ea406f19cdd62fa5a94b85a3d 100644 --- a/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java +++ b/src/main/java/net/minecraft/world/entity/ai/sensing/TemptingSensor.java @@ -38,7 +38,7 @@ public class TemptingSensor extends Sensor { protected void doTick(ServerLevel world, PathfinderMob entity) { Brain behaviorcontroller = entity.getBrain(); - Stream stream = world.players().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error + Stream stream = world.getLocalPlayers().stream().filter(EntitySelector.NO_SPECTATORS).filter((entityplayer) -> { // CraftBukkit - decompile error // Folia - region threading return TemptingSensor.TEMPT_TARGETING.test(entity, entityplayer); }).filter((entityplayer) -> { return entity.closerThan(entityplayer, 10.0D); diff --git a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java index 36dec6cd78a0990ba3c09a4a748c259ef5c0a2ff..a2bc957bd643b90595711270ab88382c957e24b6 100644 --- a/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java +++ b/src/main/java/net/minecraft/world/entity/ai/village/VillageSiege.java @@ -21,62 +21,66 @@ import org.slf4j.Logger; public class VillageSiege implements CustomSpawner { private static final Logger LOGGER = LogUtils.getLogger(); - private boolean hasSetupSiege; - private VillageSiege.State siegeState; - private int zombiesToSpawn; - private int nextSpawnTime; - private int spawnX; - private int spawnY; - private int spawnZ; + // Folia - region threading public VillageSiege() { - this.siegeState = VillageSiege.State.SIEGE_DONE; + // Folia - region threading } @Override public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + // Folia start - region threading + // check if the spawn pos is no longer owned by this region + if (worldData.villageSiegeState.siegeState != State.SIEGE_DONE + && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, worldData.villageSiegeState.spawnX >> 4, worldData.villageSiegeState.spawnZ >> 4, 8)) { + // can't spawn here, just re-set + worldData.villageSiegeState = new io.papermc.paper.threadedregions.RegionizedWorldData.VillageSiegeState(); + } + // Folia end - region threading if (!world.isDay() && spawnMonsters) { float f = world.getTimeOfDay(0.0F); if ((double) f == 0.5D) { - this.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; + worldData.villageSiegeState.siegeState = world.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; // Folia - region threading } - if (this.siegeState == VillageSiege.State.SIEGE_DONE) { + if (worldData.villageSiegeState.siegeState == VillageSiege.State.SIEGE_DONE) { // Folia - region threading return 0; } else { - if (!this.hasSetupSiege) { + if (!worldData.villageSiegeState.hasSetupSiege) { // Folia - region threading if (!this.tryToSetupSiege(world)) { return 0; } - this.hasSetupSiege = true; + worldData.villageSiegeState.hasSetupSiege = true; // Folia - region threading } - if (this.nextSpawnTime > 0) { - --this.nextSpawnTime; + if (worldData.villageSiegeState.nextSpawnTime > 0) { // Folia - region threading + --worldData.villageSiegeState.nextSpawnTime; // Folia - region threading return 0; } else { - this.nextSpawnTime = 2; - if (this.zombiesToSpawn > 0) { + worldData.villageSiegeState.nextSpawnTime = 2; // Folia - region threading + if (worldData.villageSiegeState.zombiesToSpawn > 0) { // Folia - region threading this.trySpawn(world); - --this.zombiesToSpawn; + --worldData.villageSiegeState.zombiesToSpawn; // Folia - region threading } else { - this.siegeState = VillageSiege.State.SIEGE_DONE; + worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading } return 1; } } } else { - this.siegeState = VillageSiege.State.SIEGE_DONE; - this.hasSetupSiege = false; + worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading + worldData.villageSiegeState.hasSetupSiege = false; // Folia - region threading return 0; } } private boolean tryToSetupSiege(ServerLevel world) { - Iterator iterator = world.players().iterator(); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + Iterator iterator = world.getLocalPlayers().iterator(); // Folia - region threading while (iterator.hasNext()) { Player entityhuman = (Player) iterator.next(); @@ -88,12 +92,12 @@ public class VillageSiege implements CustomSpawner { for (int i = 0; i < 10; ++i) { float f = world.random.nextFloat() * 6.2831855F; - this.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F); - this.spawnY = blockposition.getY(); - this.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F); - if (this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)) != null) { - this.nextSpawnTime = 0; - this.zombiesToSpawn = 20; + worldData.villageSiegeState.spawnX = blockposition.getX() + Mth.floor(Mth.cos(f) * 32.0F); // Folia - region threading + worldData.villageSiegeState.spawnY = blockposition.getY(); // Folia - region threading + worldData.villageSiegeState.spawnZ = blockposition.getZ() + Mth.floor(Mth.sin(f) * 32.0F); // Folia - region threading + if (this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)) != null) { // Folia - region threading + worldData.villageSiegeState.nextSpawnTime = 0; // Folia - region threading + worldData.villageSiegeState.zombiesToSpawn = 20; // Folia - region threading break; } } @@ -107,13 +111,15 @@ public class VillageSiege implements CustomSpawner { } private void trySpawn(ServerLevel world) { - Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + Vec3 vec3d = this.findRandomSpawnPos(world, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)); // Folia - region threading if (vec3d != null) { Zombie entityzombie; try { entityzombie = new Zombie(world); + entityzombie.moveTo(vec3d.x, vec3d.y, vec3d.z, world.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up entityzombie.finalizeSpawn(world, world.getCurrentDifficultyAt(entityzombie.blockPosition()), MobSpawnType.EVENT, (SpawnGroupData) null); } catch (Exception exception) { VillageSiege.LOGGER.warn("Failed to create zombie for village siege at {}", vec3d, exception); @@ -121,7 +127,7 @@ public class VillageSiege implements CustomSpawner { return; } - entityzombie.moveTo(vec3d.x, vec3d.y, vec3d.z, world.random.nextFloat() * 360.0F, 0.0F); + // Folia - region threading - move up world.addFreshEntityWithPassengers(entityzombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_INVASION); // CraftBukkit } } @@ -142,7 +148,7 @@ public class VillageSiege implements CustomSpawner { return null; } - private static enum State { + public static enum State { // Folia - region threading SIEGE_CAN_ACTIVATE, SIEGE_TONIGHT, SIEGE_DONE; diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java index a908bf1dc5e821dcf6981a8c21076fb0bdc6516d..1e7f11e5dda502fde6ca122f3edb2d2a161f3469 100644 --- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java +++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java @@ -58,11 +58,13 @@ public class PoiManager extends SectionStorage implements ca.spotted } private void updateDistanceTracking(long section) { + synchronized (this.villageDistanceTracker) { // Folia - region threading if (this.isVillageCenter(section)) { this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); } else { this.villageDistanceTracker.removeSource(section); } + } // Folia - region threading } @Override @@ -359,8 +361,10 @@ public class PoiManager extends SectionStorage implements ca.spotted } public int sectionsToVillage(SectionPos pos) { + synchronized (this.villageDistanceTracker) { // Folia - region threading this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(pos))); // Paper - rewrite chunk system + } // Folia - region threading } boolean isVillageCenter(long pos) { @@ -374,7 +378,9 @@ public class PoiManager extends SectionStorage implements ca.spotted @Override public void tick(BooleanSupplier shouldKeepTicking) { + synchronized (this.villageDistanceTracker) { // Folia - region threading this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system + } // Folia - region threading } @Override diff --git a/src/main/java/net/minecraft/world/entity/animal/Bee.java b/src/main/java/net/minecraft/world/entity/animal/Bee.java index 1b3978f4ea7e8491e0c0cb6de23c141f44fab414..9fa53a54f2d39962aec8abe95d8311da0f7b40a6 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Bee.java +++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java @@ -1036,6 +1036,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { @Override public boolean canBeeUse() { + // Folia start - region threading + if (Bee.this.hivePos != null && Bee.this.isTooFarAway(Bee.this.hivePos)) { + Bee.this.hivePos = null; + } + // Folia end - region threading return Bee.this.hivePos != null && !Bee.this.hasRestriction() && Bee.this.wantsToEnterHive() && !this.hasReachedTarget(Bee.this.hivePos) && Bee.this.level().getBlockState(Bee.this.hivePos).is(BlockTags.BEEHIVES); } @@ -1152,6 +1157,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { @Override public boolean canBeeUse() { + // Folia start - region threading + if (Bee.this.savedFlowerPos != null && Bee.this.isTooFarAway(Bee.this.savedFlowerPos)) { + Bee.this.savedFlowerPos = null; + } + // Folia end - region threading return Bee.this.savedFlowerPos != null && !Bee.this.hasRestriction() && this.wantsToGoToKnownFlower() && Bee.this.isFlowerValid(Bee.this.savedFlowerPos) && !Bee.this.closerThan(Bee.this.savedFlowerPos, 2); } diff --git a/src/main/java/net/minecraft/world/entity/animal/Cat.java b/src/main/java/net/minecraft/world/entity/animal/Cat.java index 23d4dcc82115fd1a0a77565a0472304042d5f12d..c7cd84527887cf7280b802cc110a9352cf63b4b3 100644 --- a/src/main/java/net/minecraft/world/entity/animal/Cat.java +++ b/src/main/java/net/minecraft/world/entity/animal/Cat.java @@ -357,7 +357,7 @@ public class Cat extends TamableAnimal implements VariantHolder 0 && this.pickupDelay != 32767) { + --this.pickupDelay; + } + // Folia end - region threading - restore original timers this.xo = this.getX(); this.yo = this.getY(); @@ -212,11 +210,11 @@ public class ItemEntity extends Entity implements TraceableEntity { this.mergeWithNeighbours(); } - /* CraftBukkit start - moved up + // Folia - region threading - restore original timers if (this.age != -32768) { ++this.age; } - // CraftBukkit end */ + // Folia - region threading - restore original timers this.hasImpulse |= this.updateInWaterStateAndDoFluidPushing(); if (!this.level().isClientSide) { @@ -243,13 +241,14 @@ public class ItemEntity extends Entity implements TraceableEntity { // Spigot start - copied from above @Override public void inactiveTick() { - // CraftBukkit start - Use wall time for pickup and despawn timers - int elapsedTicks = MinecraftServer.currentTick - this.lastTick; - if (this.pickupDelay != 32767) this.pickupDelay -= elapsedTicks; - this.pickupDelay = Math.max(0, this.pickupDelay); // Paper - don't go below 0 - if (this.age != -32768) this.age += elapsedTicks; - this.lastTick = MinecraftServer.currentTick; - // CraftBukkit end + // Folia start - region threading - restore original timers + if (this.pickupDelay > 0 && this.pickupDelay != 32767) { + --this.pickupDelay; + } + if (this.age != -32768) { + ++this.age; + } + // Folia end - region threading - restore original timers if (!this.level().isClientSide && this.age >= this.despawnRate) { // Spigot // Paper - Alternative item-despawn-rate // CraftBukkit start - fire ItemDespawnEvent @@ -561,14 +560,22 @@ public class ItemEntity extends Entity implements TraceableEntity { return false; } + // Folia start - region threading + @Override + public void postChangeDimension() { + super.postChangeDimension(); + if (!this.level().isClientSide) { + this.mergeWithNeighbours(); + } + } + // Folia end - region threading + @Nullable @Override public Entity changeDimension(DimensionTransition teleportTarget) { Entity entity = super.changeDimension(teleportTarget); - if (!this.level().isClientSide && entity instanceof ItemEntity entityitem) { - entityitem.mergeWithNeighbours(); - } + if (entity != null) entity.postChangeDimension(); // Folia - region threading - move to post change return entity; } diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java index 42bd2d9a1528b6210e4dfb56233062fd97c9743b..ee818605b7341e90da5e28206ffca04e1213393d 100644 --- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java @@ -95,8 +95,8 @@ public class PrimedTnt extends Entity implements TraceableEntity { @Override public void tick() { - if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot - this.handlePortal(); + if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().getCurrentWorldData().currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading + //this.handlePortal(); // Folia - region threading this.applyGravity(); this.move(MoverType.SELF, this.getDeltaMovement()); // Paper start - Configurable TNT height nerf @@ -135,7 +135,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { */ // Send position and velocity updates to nearby players on every tick while the TNT is in water. // This does pretty well at keeping their clients in sync with the server. - net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel)this.level()).getChunkSource().chunkMap.entityMap.get(this.getId()); + net.minecraft.server.level.ChunkMap.TrackedEntity ete = this.moonrise$getTrackedEntity(); // Folia - region threading if (ete != null) { net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this); net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = new net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket(this); diff --git a/src/main/java/net/minecraft/world/entity/monster/Vex.java b/src/main/java/net/minecraft/world/entity/monster/Vex.java index 2985296a9a034e535157f55e322fc8c107827752..d9ae587c5d3ef67d5ee8c9238c39a8232b479398 100644 --- a/src/main/java/net/minecraft/world/entity/monster/Vex.java +++ b/src/main/java/net/minecraft/world/entity/monster/Vex.java @@ -349,7 +349,7 @@ public class Vex extends Monster implements TraceableEntity { public void tick() { BlockPos blockposition = Vex.this.getBoundOrigin(); - if (blockposition == null) { + if (blockposition == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)Vex.this.level(), blockposition)) { // Folia - region threading blockposition = Vex.this.blockPosition(); } diff --git a/src/main/java/net/minecraft/world/entity/monster/Zombie.java b/src/main/java/net/minecraft/world/entity/monster/Zombie.java index 2280004638fd19ed018cb3e77d53a018b34ec516..9460ffdede40a2e3601d3c97b1d1ca4e62dcbf29 100644 --- a/src/main/java/net/minecraft/world/entity/monster/Zombie.java +++ b/src/main/java/net/minecraft/world/entity/monster/Zombie.java @@ -96,7 +96,7 @@ public class Zombie extends Monster { private boolean canBreakDoors; private int inWaterTime; public int conversionTime; - private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field + //private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers private boolean shouldBurnInDay = true; // Paper - Add more Zombie API public Zombie(EntityType type, Level world) { @@ -219,10 +219,7 @@ public class Zombie extends Monster { public void tick() { if (!this.level().isClientSide && this.isAlive() && !this.isNoAi()) { if (this.isUnderWaterConverting()) { - // CraftBukkit start - Use wall time instead of ticks for conversion - int elapsedTicks = MinecraftServer.currentTick - this.lastTick; - this.conversionTime -= elapsedTicks; - // CraftBukkit end + --this.conversionTime; // Folia - region threading - restore original timers if (this.conversionTime < 0) { this.doUnderWaterConversion(); } @@ -239,7 +236,7 @@ public class Zombie extends Monster { } super.tick(); - this.lastTick = MinecraftServer.currentTick; // CraftBukkit + //this.lastTick = MinecraftServer.currentTick; // CraftBukkit // Folia - region threading - restore original timers } @Override @@ -280,7 +277,7 @@ public class Zombie extends Monster { } // Paper end - Add more Zombie API public void startUnderWaterConversion(int ticksUntilWaterConversion) { - this.lastTick = MinecraftServer.currentTick; // CraftBukkit + // Folia - region threading - restore original timers this.conversionTime = ticksUntilWaterConversion; this.getEntityData().set(Zombie.DATA_DROWNED_CONVERSION_ID, true); } diff --git a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java index e0dabbf6d7a87b8722769c78ef0d2ba4353ed2cb..90627c6a0c0e0f03a20cd32a7f15a0cfab7e592e 100644 --- a/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java +++ b/src/main/java/net/minecraft/world/entity/monster/ZombieVillager.java @@ -74,7 +74,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { @Nullable private MerchantOffers tradeOffers; private int villagerXp; - private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field + // private int lastTick = MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers public ZombieVillager(EntityType type, Level world) { super(type, world); @@ -155,10 +155,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { public void tick() { if (!this.level().isClientSide && this.isAlive() && this.isConverting()) { int i = this.getConversionProgress(); - // CraftBukkit start - Use wall time instead of ticks for villager conversion - int elapsedTicks = MinecraftServer.currentTick - this.lastTick; - i *= elapsedTicks; - // CraftBukkit end + // Folia - region threading - restore original timers this.villagerConversionTime -= i; if (this.villagerConversionTime <= 0) { @@ -167,7 +164,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { } super.tick(); - this.lastTick = MinecraftServer.currentTick; // CraftBukkit + // Folia - region threading - restore original timers } @Override diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java index 49b35fab8ee98a384ee12d36bbe2ac813342f1d6..5f3104b2a46d4b47cf505012438f848e3b744315 100644 --- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java +++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java @@ -226,10 +226,18 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa this.readInventoryFromTag(nbt, this.registryAccess()); } + // Folia start - region threading + @Override + public void preChangeDimension() { + super.preChangeDimension(); + this.stopTrading(); + } + // Folia end - region threading + @Nullable @Override public Entity changeDimension(DimensionTransition teleportTarget) { - this.stopTrading(); + this.preChangeDimension(); // Folia - region threading - move into preChangeDimension return super.changeDimension(teleportTarget); } diff --git a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java index e0e5046c84941a8d17e18c177f3daea9cb631940..61adcf62cb738d7dd5c785780824f64c56b82082 100644 --- a/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java +++ b/src/main/java/net/minecraft/world/entity/npc/CatSpawner.java @@ -18,17 +18,18 @@ import net.minecraft.world.phys.AABB; public class CatSpawner implements CustomSpawner { private static final int TICK_DELAY = 1200; - private int nextTick; + //private int nextTick; // Folia - region threading @Override public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { if (spawnAnimals && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { - this.nextTick--; - if (this.nextTick > 0) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + --worldData.catSpawnerNextTick; // Folia - region threading + if (worldData.catSpawnerNextTick > 0) { // Folia - region threading return 0; } else { - this.nextTick = 1200; - Player player = world.getRandomPlayer(); + worldData.catSpawnerNextTick = 1200; // Folia - region threading + Player player = world.getRandomLocalPlayer(); // Folia - region threading if (player == null) { return 0; } else { diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java index 7e1871401ec5e3e9a85232053490259f132aec0a..91728a992a29bc22e46a260750d5dd88e629bfd1 100644 --- a/src/main/java/net/minecraft/world/entity/npc/Villager.java +++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java @@ -204,7 +204,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler brain.setCoreActivities(ImmutableSet.of(Activity.CORE)); brain.setDefaultActivity(Activity.IDLE); brain.setActiveActivityIfPossible(Activity.IDLE); - brain.updateActivityFromSchedule(this.level().getDayTime(), this.level().getGameTime()); + brain.updateActivityFromSchedule(this.level().getLevelData().getDayTime(), this.level().getLevelData().getGameTime()); // Folia - region threading } @Override @@ -724,6 +724,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler ServerLevel worldserver = minecraftserver.getLevel(globalpos.dimension()); if (worldserver != null) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( // Folia - region threading + worldserver, globalpos.pos().getX() >> 4, globalpos.pos().getZ() >> 4, () -> { // Folia - region threading PoiManager villageplace = worldserver.getPoiManager(); Optional> optional = villageplace.getType(globalpos.pos()); BiPredicate> bipredicate = (BiPredicate) Villager.POI_MEMORIES.get(pos); @@ -732,6 +734,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler villageplace.release(globalpos.pos()); DebugPackets.sendPoiTicketCountPacket(worldserver, globalpos.pos()); } + }); // Folia - region threading } }); diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java index c72b6ea5530e54fc373c701028e1c147cea34b59..6677b4cc23253a1b7dfbc2e12e666699bbadbdb4 100644 --- a/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTraderSpawner.java @@ -32,16 +32,14 @@ public class WanderingTraderSpawner implements CustomSpawner { private static final int SPAWN_CHANCE_INCREASE = 25; private static final int SPAWN_ONE_IN_X_CHANCE = 10; private static final int NUMBER_OF_SPAWN_ATTEMPTS = 10; - private final RandomSource random = RandomSource.create(); + private final RandomSource random = new net.minecraft.world.entity.Entity.RandomRandomSource(); // Folia - region threading private final ServerLevelData serverLevelData; - private int tickDelay; - private int spawnDelay; - private int spawnChance; + // Folia - region threading public WanderingTraderSpawner(ServerLevelData properties) { this.serverLevelData = properties; // Paper start - Add Wandering Trader spawn rate config options - this.tickDelay = Integer.MIN_VALUE; + //this.tickDelay = Integer.MIN_VALUE; // Folia - region threading - moved to regionisedworlddata //this.spawnDelay = properties.getWanderingTraderSpawnDelay(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value //this.spawnChance = properties.getWanderingTraderSpawnChance(); // Paper - This value is read from the world file only for the first spawn, after which vanilla uses a hardcoded value //if (this.spawnDelay == 0 && this.spawnChance == 0) { @@ -56,36 +54,37 @@ public class WanderingTraderSpawner implements CustomSpawner { @Override public int tick(ServerLevel world, boolean spawnMonsters, boolean spawnAnimals) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading // Paper start - Add Wandering Trader spawn rate config options - if (this.tickDelay == Integer.MIN_VALUE) { - this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; - this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; - this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; + if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Folia - region threading + worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading + worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading + worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading } if (!world.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) { return 0; - } else if (this.tickDelay - 1 > 0) { - this.tickDelay = this.tickDelay - 1; + } else if (worldData.wanderingTraderTickDelay - 1 > 0) { // Folia - region threading + worldData.wanderingTraderTickDelay = worldData.wanderingTraderTickDelay - 1; // Folia - region threading return 0; } else { - this.tickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; - this.spawnDelay = this.spawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; + worldData.wanderingTraderTickDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading + worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - world.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading //this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways - if (this.spawnDelay > 0) { + if (worldData.wanderingTraderSpawnDelay > 0) { // Folia - region threading return 0; } else { - this.spawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; + worldData.wanderingTraderSpawnDelay = world.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading if (!world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { return 0; } else { - int i = this.spawnChance; + int i = worldData.wanderingTraderSpawnChance; // Folia - region threading // this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways - this.spawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); + worldData.wanderingTraderSpawnChance = Mth.clamp(i + world.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); // Folia - region threading if (this.random.nextInt(100) > i) { return 0; } else if (this.spawn(world)) { - this.spawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; + worldData.wanderingTraderSpawnChance = world.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading // Paper end - Add Wandering Trader spawn rate config options return 1; } else { @@ -97,7 +96,7 @@ public class WanderingTraderSpawner implements CustomSpawner { } private boolean spawn(ServerLevel world) { - ServerPlayer entityplayer = world.getRandomPlayer(); + ServerPlayer entityplayer = world.getRandomLocalPlayer(); // Folia - region threading if (entityplayer == null) { return true; @@ -127,7 +126,7 @@ public class WanderingTraderSpawner implements CustomSpawner { this.tryToSpawnLlamaFor(world, entityvillagertrader, 4); } - this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); + //this.serverLevelData.setWanderingTraderId(entityvillagertrader.getUUID()); // Folia - region threading - doesn't appear to be used anywhere, so avoid the race condition here... // entityvillagertrader.setDespawnDelay(48000); // CraftBukkit - moved to EntityVillagerTrader constructor. This lets the value be modified by plugins on CreatureSpawnEvent entityvillagertrader.setWanderTarget(blockposition1); entityvillagertrader.restrictTo(blockposition1, 16); diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java index 09bcbc0ae36e4e69fee87a7e0c49acf496117a39..b3377ed06d703f54e01ba174e5a06dc928cdff96 100644 --- a/src/main/java/net/minecraft/world/entity/player/Player.java +++ b/src/main/java/net/minecraft/world/entity/player/Player.java @@ -1558,6 +1558,14 @@ public abstract class Player extends LivingEntity { } + // Folia start - region threading + @Override + protected void preRemove(RemovalReason reason) { + super.preRemove(reason); + this.fishing = null; + } + // Folia end - region threading + public boolean isLocalPlayer() { return false; } diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java index 9ca29b3d4bf8bca5f51f3644e12fcbec2cb5d35e..b5edbd36a1114449fe5f9bf1018bff7988fa7262 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java +++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractArrow.java @@ -187,6 +187,11 @@ public abstract class AbstractArrow extends Projectile { @Override public void tick() { super.tick(); + // Folia start - region threading - make sure entities do not move into regions they do not own + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { + return; + } + // Folia end - region threading - make sure entities do not move into regions they do not own boolean flag = this.isNoPhysics(); Vec3 vec3d = this.getDeltaMovement(); diff --git a/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java b/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java index 3107ad04dda9f43976a385976d6952e2f2af3939..dc075e777055508c17ac4fa4d46e51e1721cb2e8 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java @@ -80,6 +80,11 @@ public abstract class AbstractHurtingProjectile extends Projectile { this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause } else { super.tick(); + // Folia start - region threading - make sure entities do not move into regions they do not own + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { + return; + } + // Folia end - region threading - make sure entities do not move into regions they do not own if (this.shouldBurn()) { this.igniteForSeconds(1.0F); } diff --git a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java index 09d465947a5720e05c350d455c86002682104079..16bb8fedbfc1618a77c0967eaa0b234d0d82255b 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +++ b/src/main/java/net/minecraft/world/entity/projectile/FireworkRocketEntity.java @@ -136,6 +136,11 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { }); } + // Folia start - region threading + if (this.attachedToEntity != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.attachedToEntity)) { + this.attachedToEntity = null; + } + // Folia end - region threading if (this.attachedToEntity != null) { if (this.attachedToEntity.isFallFlying()) { diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java index 1223c5d23d0ea6aed068bdf0f5725e2ad49fc82c..cd058156c79223991c725882bc04426ad066af28 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java +++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java @@ -106,7 +106,7 @@ public class FishingHook extends Projectile { public FishingHook(net.minecraft.world.entity.player.Player thrower, Level world, int luckBonus, int waitTimeReductionTicks) { this(EntityType.FISHING_BOBBER, world, luckBonus, waitTimeReductionTicks); - this.setOwner(thrower); + //this.setOwner(thrower); // Folia - region threading - move this down after position so that thread-checks do not fail float f = thrower.getXRot(); float f1 = thrower.getYRot(); float f2 = Mth.cos(-f1 * 0.017453292F - 3.1415927F); @@ -118,6 +118,7 @@ public class FishingHook extends Projectile { double d2 = thrower.getZ() - (double) f2 * 0.3D; this.moveTo(d0, d1, d2, f1, f); + this.setOwner(thrower); // Folia - region threading - move this down after position so that thread-checks do not fail Vec3 vec3d = new Vec3((double) (-f3), (double) Mth.clamp(-(f5 / f4), -5.0F, 5.0F), (double) (-f2)); double d3 = vec3d.length(); @@ -268,6 +269,11 @@ public class FishingHook extends Projectile { } private boolean shouldStopFishing(net.minecraft.world.entity.player.Player player) { + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { + return true; + } + // Folia end - region threading ItemStack itemstack = player.getMainHandItem(); ItemStack itemstack1 = player.getOffhandItem(); boolean flag = itemstack.is(Items.FISHING_ROD); @@ -631,10 +637,18 @@ public class FishingHook extends Projectile { @Override public void remove(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) { // CraftBukkit end - this.updateOwnerInfo((FishingHook) null); + // Folia - region threading - move into preRemove super.remove(entity_removalreason, cause); // CraftBukkit - add Bukkit remove cause } + // Folia start - region threading + @Override + protected void preRemove(RemovalReason reason) { + super.preRemove(reason); + this.updateOwnerInfo((FishingHook) null); + } + // Folia end - region threading + @Override public void onClientRemoval() { this.updateOwnerInfo((FishingHook) null); diff --git a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java index 8575941fd238750c5d56843989a48bcbde2d8a88..185501a2daea0351281c578bff79dc506994e146 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java +++ b/src/main/java/net/minecraft/world/entity/projectile/LlamaSpit.java @@ -41,6 +41,11 @@ public class LlamaSpit extends Projectile { @Override public void tick() { super.tick(); + // Folia start - region threading - make sure entities do not move into regions they do not own + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { + return; + } + // Folia end - region threading - make sure entities do not move into regions they do not own Vec3 vec3d = this.getDeltaMovement(); HitResult movingobjectposition = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity); diff --git a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java index de64de5d1328d3e0826c9990eb7c7eca5088cb9c..fa3ec592bdb6325eebd5a7d59810add67c4a9968 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/Projectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/Projectile.java @@ -77,9 +77,20 @@ public abstract class Projectile extends Entity implements TraceableEntity { } // Paper end - Refresh ProjectileSource for projectiles + // Folia start - region threading + // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner. + // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior. @Nullable @Override public Entity getOwner() { + Entity ret = this.getOwnerRaw(); + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(ret) ? ret : null; + } + // Folia end - region threading + + @Nullable + public Entity getOwnerRaw() { // Folia - region threading + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot update owner state asynchronously"); // Folia - region threading if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) { this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles return this.cachedOwner; @@ -367,7 +378,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { public boolean mayInteract(Level world, BlockPos pos) { Entity entity = this.getOwner(); - return entity instanceof Player ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); + return entity instanceof Player && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) ? entity.mayInteract(world, pos) : entity == null || world.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Folia - region threading } public boolean mayBreak(Level world) { diff --git a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java index 1711ad457e7d1233fd32edc3e9e3b9f1e3be9980..62c55a5ef3c6c46d3c7638fd2e726d15690d8a0b 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java +++ b/src/main/java/net/minecraft/world/entity/projectile/SmallFireball.java @@ -29,7 +29,7 @@ public class SmallFireball extends Fireball { public SmallFireball(Level world, LivingEntity owner, Vec3 velocity) { super(EntityType.SMALL_FIREBALL, owner, velocity, world); // CraftBukkit start - if (this.getOwner() != null && this.getOwner() instanceof Mob) { + if (owner != null && owner instanceof Mob) { // Folia - region threading this.isIncendiary = this.level().getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); } // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java b/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java index bf4c1883a1257af89428d6580a177f3af3759ee7..38ff937ef3908d344dd43b8934854ea66eb7b733 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java +++ b/src/main/java/net/minecraft/world/entity/projectile/ThrowableProjectile.java @@ -43,6 +43,11 @@ public abstract class ThrowableProjectile extends Projectile { @Override public void tick() { super.tick(); + // Folia start - region threading - make sure entities do not move into regions they do not own + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { + return; + } + // Folia end - region threading - make sure entities do not move into regions they do not own HitResult movingobjectposition = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity); if (movingobjectposition.getType() != HitResult.Type.MISS) { diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java index 1aa5e57a4e6a4be60514d8808a2e6c973d93e798..dfee42d73853a87fb9bb0150e428582fe430536e 100644 --- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEnderpearl.java @@ -47,6 +47,76 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { entityHitResult.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F); } + // Folia start - region threading + private static void attemptTeleport(Entity source, ServerLevel checkWorld, net.minecraft.world.phys.Vec3 to) { + // ignore retired callback, in those cases we do not want to teleport + source.getBukkitEntity().taskScheduler.schedule( + (Entity entity) -> { + if (!isAllowedToTeleportOwner(entity, checkWorld)) { + return; + } + // source is now an invalid reference, do not use it, use the entity parameter + net.minecraft.world.phys.Vec3 endermitePos = entity.position(); + + // dismount from any vehicles, so we can teleport and to prevent desync + if (entity.isPassenger()) { + entity.stopRiding(); + } + + entity.teleportAsync( + checkWorld, to, null, null, null, + PlayerTeleportEvent.TeleportCause.ENDER_PEARL, + // chunk could have been unloaded + Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS | Entity.TELEPORT_FLAG_LOAD_CHUNK, + (Entity teleported) -> { + // entity is now an invalid reference, do not use it, instead use teleported + if (teleported instanceof ServerPlayer player) { + // connection teleport is already done + ServerLevel world = player.serverLevel(); + + // endermite spawn chance + if (world.random.nextFloat() < 0.05F && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { + Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(world); + + if (entityendermite != null) { + float yRot = teleported.getYRot(); + float xRot = teleported.getXRot(); + Runnable spawn = () -> { + entityendermite.moveTo(endermitePos.x, endermitePos.y, endermitePos.z, yRot, xRot); + world.addFreshEntity(entityendermite, CreatureSpawnEvent.SpawnReason.ENDER_PEARL); + }; + + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, endermitePos, net.minecraft.world.phys.Vec3.ZERO, 1)) { + spawn.run(); + } else { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + world, + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.x), + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.z), + spawn + ); + } + } + } + + // damage player + teleported.resetFallDistance(); + player.resetCurrentImpulseContext(); + player.hurt(teleported.damageSources().fall().customEventDamager(teleported), 5.0F); // CraftBukkit // Paper - fix DamageSource API + playSound(teleported.level(), to); + } else { + // reset fall damage so that if the entity was falling they do not instantly die + teleported.resetFallDistance(); + playSound(teleported.level(), to); + } + } + ); + }, + null, 1L + ); + } + // Folia end - region threading + @Override protected void onHit(HitResult hitResult) { super.onHit(hitResult); @@ -59,6 +129,20 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { if (world instanceof ServerLevel worldserver) { if (!this.isRemoved()) { + // Folia start - region threading + if (true) { + // we can't fire events, because we do not actually know where the other entity is located + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this)) { + throw new IllegalStateException("Must be on tick thread for ticking entity: " + this); + } + Entity entity = this.getOwnerRaw(); + if (entity != null) { + attemptTeleport(entity, (ServerLevel)this.level(), this.position()); + } + this.discard(EntityRemoveEvent.Cause.HIT); + return; + } + // Folia end - region threading Entity entity = this.getOwner(); if (entity != null && ThrownEnderpearl.isAllowedToTeleportOwner(entity, worldserver)) { @@ -135,7 +219,15 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { } - private void playSound(Level world, Vec3 pos) { + // Folia start - region threading + @Override + public void preChangeDimension() { + super.preChangeDimension(); + // Don't change the owner here, since the tick logic will consider it anyways. + } + // Folia end - region threading + + private static void playSound(Level world, Vec3 pos) { // Folia - region threading - static world.playSound((Player) null, pos.x, pos.y, pos.z, SoundEvents.PLAYER_TELEPORT, SoundSource.PLAYERS); } diff --git a/src/main/java/net/minecraft/world/entity/raid/Raid.java b/src/main/java/net/minecraft/world/entity/raid/Raid.java index dcbef04bbaab988096bf416163264833e84d1967..e1987d1d6e5fd05e155c05dd1fdaf569e303d74a 100644 --- a/src/main/java/net/minecraft/world/entity/raid/Raid.java +++ b/src/main/java/net/minecraft/world/entity/raid/Raid.java @@ -113,6 +113,13 @@ public class Raid { public final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(PDC_TYPE_REGISTRY); // Paper end + // Folia start - make raids thread-safe + public boolean ownsRaid() { + BlockPos center = this.getCenter(); + return center != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, center.getX() >> 4, center.getZ() >> 4, 8); + } + // Folia end - make raids thread-safe + public Raid(int id, ServerLevel world, BlockPos pos) { this.raidEvent = new ServerBossEvent(Raid.RAID_NAME_COMPONENT, BossEvent.BossBarColor.RED, BossEvent.BossBarOverlay.NOTCHED_10); this.random = RandomSource.create(); @@ -226,7 +233,7 @@ public class Raid { return (entityplayer) -> { BlockPos blockposition = entityplayer.blockPosition(); - return entityplayer.isAlive() && this.level.getRaidAt(blockposition) == this; + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entityplayer) && entityplayer.isAlive() && this.level.getRaidAt(blockposition) == this; // Folia - make raids thread-safe }; } @@ -550,7 +557,7 @@ public class Raid { boolean flag = true; Collection collection = this.raidEvent.getPlayers(); long i = this.random.nextLong(); - Iterator iterator = this.level.players().iterator(); + Iterator iterator = this.level.getLocalPlayers().iterator(); // Folia - region threading while (iterator.hasNext()) { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java index bbf21ea433f9e3963aac0ede597ed8d3c8e50ed8..bcf7a7d8137b62cd65ac3c6ff517025faa4f84b8 100644 --- a/src/main/java/net/minecraft/world/entity/raid/Raider.java +++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java @@ -89,7 +89,7 @@ public abstract class Raider extends PatrollingMonster { if (this.canJoinRaid()) { if (raid == null) { - if (this.level().getGameTime() % 20L == 0L) { + if (this.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading Raid raid1 = ((ServerLevel) this.level()).getRaidAt(this.blockPosition()); if (raid1 != null && Raids.canJoinRaid(this, raid1)) { diff --git a/src/main/java/net/minecraft/world/entity/raid/Raids.java b/src/main/java/net/minecraft/world/entity/raid/Raids.java index 8c60f71270d909c10e6617eb64b8fdb42deb73e9..f4f9670e7aa81e3f60b34f0bc7313784573c74b8 100644 --- a/src/main/java/net/minecraft/world/entity/raid/Raids.java +++ b/src/main/java/net/minecraft/world/entity/raid/Raids.java @@ -26,9 +26,9 @@ import net.minecraft.world.phys.Vec3; public class Raids extends SavedData { private static final String RAID_FILE_ID = "raids"; - public final Map raidMap = Maps.newHashMap(); + public final Map raidMap = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - make raids thread-safe private final ServerLevel level; - private int nextAvailableID; + private final java.util.concurrent.atomic.AtomicInteger nextAvailableID = new java.util.concurrent.atomic.AtomicInteger(); // Folia - make raids thread-safe private int tick; public static SavedData.Factory factory(ServerLevel world) { @@ -41,7 +41,7 @@ public class Raids extends SavedData { public Raids(ServerLevel world) { this.level = world; - this.nextAvailableID = 1; + this.nextAvailableID.set(1); // Folia - make raids thread-safe this.setDirty(); } @@ -49,12 +49,26 @@ public class Raids extends SavedData { return (Raid) this.raidMap.get(id); } - public void tick() { + // Folia start - make raids thread-safe + public void globalTick() { ++this.tick; + if (this.tick % 200 == 0) { + this.setDirty(); + } + // Folia end - make raids thread-safe + } + + public void tick() { + // Folia - make raids thread-safe - move to globalTick() Iterator iterator = this.raidMap.values().iterator(); while (iterator.hasNext()) { Raid raid = (Raid) iterator.next(); + // Folia start - make raids thread-safe + if (!raid.ownsRaid()) { + continue; + } + // Folia end - make raids thread-safe if (this.level.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS)) { raid.stop(); @@ -68,14 +82,17 @@ public class Raids extends SavedData { } } - if (this.tick % 200 == 0) { - this.setDirty(); - } + // Folia - make raids thread-safe - move to globalTick() DebugPackets.sendRaids(this.level, this.raidMap.values()); } public static boolean canJoinRaid(Raider raider, Raid raid) { + // Folia start - make raids thread-safe + if (!raid.ownsRaid()) { + return false; + } + // Folia end - make raids thread-safe return raider != null && raid != null && raid.getLevel() != null ? raider.isAlive() && raider.canJoinRaid() && raider.getNoActionTime() <= 2400 && raider.level().dimensionType() == raid.getLevel().dimensionType() : false; } @@ -88,7 +105,7 @@ public class Raids extends SavedData { } else { DimensionType dimensionmanager = player.level().dimensionType(); - if (!dimensionmanager.hasRaids()) { + if (!dimensionmanager.hasRaids() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, player.chunkPosition().x, player.chunkPosition().z, 8)) { // Folia - region threading return null; } else { List list = this.level.getPoiManager().getInRange((holder) -> { @@ -150,7 +167,7 @@ public class Raids extends SavedData { public static Raids load(ServerLevel world, CompoundTag nbt) { Raids persistentraid = new Raids(world); - persistentraid.nextAvailableID = nbt.getInt("NextAvailableID"); + persistentraid.nextAvailableID.set(nbt.getInt("NextAvailableID")); // Folia - make raids thread-safe persistentraid.tick = nbt.getInt("Tick"); ListTag nbttaglist = nbt.getList("Raids", 10); @@ -166,7 +183,7 @@ public class Raids extends SavedData { @Override public CompoundTag save(CompoundTag nbt, HolderLookup.Provider registryLookup) { - nbt.putInt("NextAvailableID", this.nextAvailableID); + nbt.putInt("NextAvailableID", this.nextAvailableID.get()); // Folia - make raids thread-safe nbt.putInt("Tick", this.tick); ListTag nbttaglist = new ListTag(); Iterator iterator = this.raidMap.values().iterator(); @@ -188,7 +205,7 @@ public class Raids extends SavedData { } private int getUniqueId() { - return ++this.nextAvailableID; + return this.nextAvailableID.incrementAndGet(); // Folia - make raids thread-safe } @Nullable @@ -199,6 +216,11 @@ public class Raids extends SavedData { while (iterator.hasNext()) { Raid raid1 = (Raid) iterator.next(); + // Folia start - make raids thread-safe + if (!raid1.ownsRaid()) { + continue; + } + // Folia end - make raids thread-safe double d1 = raid1.getCenter().distSqr(pos); if (raid1.isActive() && d1 < d0) { diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java index 83ef2c3e7b06152b9e68f90002c35e77f148347d..83d9b685aaf6934acfd0ca9a869723146ec7cfcb 100644 --- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java +++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java @@ -150,5 +150,11 @@ public class MinecartCommandBlock extends AbstractMinecart { return (org.bukkit.craftbukkit.entity.CraftMinecartCommand) MinecartCommandBlock.this.getBukkitEntity(); } // CraftBukkit end + // Folia start + @Override + public void threadCheck() { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(MinecartCommandBlock.this, "Asynchronous sendSystemMessage to a command block"); + } + // Folia end } } diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java index d7f8464bf3eed0e42a5fc7f14a5b243d171f8b5e..97bd1d7b4613c977376e43e180d8ec2a15acbcca 100644 --- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java +++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java @@ -132,7 +132,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper // Paper start public void immunize() { - this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20); + this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); } // Paper end diff --git a/src/main/java/net/minecraft/world/item/ArmorItem.java b/src/main/java/net/minecraft/world/item/ArmorItem.java index 647a4601deace52f8d855f512a73671f82b4762a..255f6e720ebe0f76954fdba03cd2aae006852a78 100644 --- a/src/main/java/net/minecraft/world/item/ArmorItem.java +++ b/src/main/java/net/minecraft/world/item/ArmorItem.java @@ -68,7 +68,7 @@ public class ArmorItem extends Item implements Equipable { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseArmorEvent event = new BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) entityliving.getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java index b6a2f3e4f22f36e75a1630bd456c2f471edbb398..0b802cd958d7a7087933f5d21dc16f9ee51426b6 100644 --- a/src/main/java/net/minecraft/world/item/ItemStack.java +++ b/src/main/java/net/minecraft/world/item/ItemStack.java @@ -421,31 +421,32 @@ public final class ItemStack implements DataComponentHolder { DataComponentPatch oldData = this.components.asPatch(); int oldCount = this.getCount(); ServerLevel world = (ServerLevel) context.getLevel(); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement - world.captureBlockStates = true; + worldData.captureBlockStates = true; // Folia - region threading // special case bonemeal if (item == Items.BONE_MEAL) { - world.captureTreeGeneration = true; + worldData.captureTreeGeneration = true; // Folia - region threading } } InteractionResult enuminteractionresult; try { enuminteractionresult = item.useOn(context); } finally { - world.captureBlockStates = false; + worldData.captureBlockStates = false; // Folia - region threading } DataComponentPatch newData = this.components.asPatch(); int newCount = this.getCount(); this.setCount(oldCount); this.restorePatch(oldData); - if (enuminteractionresult.consumesAction() && world.captureTreeGeneration && world.capturedBlockStates.size() > 0) { - world.captureTreeGeneration = false; + if (enuminteractionresult.consumesAction() && worldData.captureTreeGeneration && worldData.capturedBlockStates.size() > 0) { // Folia - region threading + worldData.captureTreeGeneration = false; Location location = CraftLocation.toBukkit(blockposition, world.getWorld()); - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; - List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading + SaplingBlock.treeTypeRT.set(null); // Folia - region threading + List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading StructureGrowEvent structureEvent = null; if (treeType != null) { boolean isBonemeal = this.getItem() == Items.BONE_MEAL; @@ -471,16 +472,16 @@ public final class ItemStack implements DataComponentHolder { entityhuman.awardStat(Stats.ITEM_USED.get(item)); // SPIGOT-7236 - award stat } - SignItem.openSign = null; // SPIGOT-6758 - Reset on early return + SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading return enuminteractionresult; } - world.captureTreeGeneration = false; + worldData.captureTreeGeneration = false; // Folia - region threading if (entityhuman != null && enuminteractionresult.indicateItemUse()) { InteractionHand enumhand = context.getHand(); org.bukkit.event.block.BlockPlaceEvent placeEvent = null; - List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading if (blocks.size() > 1) { placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(world, entityhuman, enumhand, blocks, blockposition.getX(), blockposition.getY(), blockposition.getZ()); } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement @@ -491,15 +492,15 @@ public final class ItemStack implements DataComponentHolder { enuminteractionresult = InteractionResult.FAIL; // cancel placement // PAIL: Remove this when MC-99075 fixed placeEvent.getPlayer().updateInventory(); - world.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot + worldData.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot // Folia - region threading // revert back all captured blocks - world.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 - world.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent + worldData.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 // Folia - region threading + worldData.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading for (BlockState blockstate : blocks) { blockstate.update(true, false); } - world.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent - world.preventPoiUpdated = false; + worldData.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading + worldData.preventPoiUpdated = false; // Folia - region threading // Brute force all possible updates // Paper start - Don't resync blocks @@ -508,7 +509,7 @@ public final class ItemStack implements DataComponentHolder { // ((ServerPlayer) entityhuman).connection.send(new ClientboundBlockUpdatePacket(world, placedPos.relative(dir))); // } // Paper end - Don't resync blocks - SignItem.openSign = null; // SPIGOT-6758 - Reset on early return + SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading } else { // Change the stack to its new contents if it hasn't been tampered with. if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), oldData)) { @@ -516,7 +517,7 @@ public final class ItemStack implements DataComponentHolder { this.setCount(newCount); } - for (Map.Entry e : world.capturedTileEntities.entrySet()) { + for (Map.Entry e : worldData.capturedTileEntities.entrySet()) { // Folia - region threading world.setBlockEntity(e.getValue()); } @@ -551,15 +552,15 @@ public final class ItemStack implements DataComponentHolder { } // SPIGOT-4678 - if (this.item instanceof SignItem && SignItem.openSign != null) { + if (this.item instanceof SignItem && SignItem.openSign.get() != null) { // Folia - region threading try { - if (world.getBlockEntity(SignItem.openSign) instanceof SignBlockEntity tileentitysign) { - if (world.getBlockState(SignItem.openSign).getBlock() instanceof SignBlock blocksign) { + if (world.getBlockEntity(SignItem.openSign.get()) instanceof SignBlockEntity tileentitysign) { // Folia - region threading + if (world.getBlockState(SignItem.openSign.get()).getBlock() instanceof SignBlock blocksign) { // Folia - region threading blocksign.openTextEdit(entityhuman, tileentitysign, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // Paper - Add PlayerOpenSignEvent } } } finally { - SignItem.openSign = null; + SignItem.openSign.set(null); // Folia - region threading } } @@ -587,8 +588,8 @@ public final class ItemStack implements DataComponentHolder { entityhuman.awardStat(Stats.ITEM_USED.get(item)); } } - world.capturedTileEntities.clear(); - world.capturedBlockStates.clear(); + worldData.capturedTileEntities.clear(); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading // CraftBukkit end return enuminteractionresult; diff --git a/src/main/java/net/minecraft/world/item/MapItem.java b/src/main/java/net/minecraft/world/item/MapItem.java index ce461b1a8d7fab87ae28e30205f6fab67f1808b6..e96f15814986109e5e947c7b3b210be6e56ba0ed 100644 --- a/src/main/java/net/minecraft/world/item/MapItem.java +++ b/src/main/java/net/minecraft/world/item/MapItem.java @@ -69,6 +69,7 @@ public class MapItem extends ComplexItem { } public void update(Level world, Entity entity, MapItemSavedData state) { + synchronized (state) { // Folia - make map data thread-safe if (world.dimension() == state.dimension && entity instanceof Player) { int i = 1 << state.scale; int j = state.centerX; @@ -98,8 +99,8 @@ public class MapItem extends ComplexItem { int r = (j / i + o - 64) * i; int s = (k / i + p - 64) * i; Multiset multiset = LinkedHashMultiset.create(); - LevelChunk levelChunk = world.getChunkIfLoaded(SectionPos.blockToSectionCoord(r), SectionPos.blockToSectionCoord(s)); // Paper - Maps shouldn't load chunks - if (levelChunk != null && !levelChunk.isEmpty()) { // Paper - Maps shouldn't load chunks + LevelChunk levelChunk = world.getChunkIfLoaded(SectionPos.blockToSectionCoord(r), SectionPos.blockToSectionCoord(s)); // Paper - Maps shouldn't load chunks // Folia - super important that it uses getChunkIfLoaded + if (levelChunk != null && !levelChunk.isEmpty() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)world, levelChunk.getPos())) { // Paper - Maps shouldn't load chunks // Folia - make sure chunk is owned int t = 0; double e = 0.0; if (world.dimensionType().hasCeiling()) { @@ -181,6 +182,7 @@ public class MapItem extends ComplexItem { } } } + } // Folia - make map data thread-safe } private BlockState getCorrectStateForFluidBlock(Level world, BlockState state, BlockPos pos) { @@ -195,6 +197,7 @@ public class MapItem extends ComplexItem { public static void renderBiomePreviewMap(ServerLevel world, ItemStack map) { MapItemSavedData mapItemSavedData = getSavedData(map, world); if (mapItemSavedData != null) { + synchronized (mapItemSavedData) { // Folia - make map data thread-safe if (world.dimension() == mapItemSavedData.dimension) { int i = 1 << mapItemSavedData.scale; int j = mapItemSavedData.centerX; @@ -264,6 +267,7 @@ public class MapItem extends ComplexItem { } } } + } // Folia - make map data thread-safe } } @@ -272,6 +276,7 @@ public class MapItem extends ComplexItem { if (!world.isClientSide) { MapItemSavedData mapItemSavedData = getSavedData(stack, world); if (mapItemSavedData != null) { + synchronized (mapItemSavedData) { // Folia - region threading if (entity instanceof Player player) { mapItemSavedData.tickCarriedBy(player, stack); } @@ -279,6 +284,7 @@ public class MapItem extends ComplexItem { if (!mapItemSavedData.locked && (selected || entity instanceof Player && ((Player)entity).getOffhandItem() == stack)) { this.update(world, entity, mapItemSavedData); } + } // Folia - region threading } } } diff --git a/src/main/java/net/minecraft/world/item/MinecartItem.java b/src/main/java/net/minecraft/world/item/MinecartItem.java index d524fcc191cb95d6ec7f12ae7fceeb8077bb08fc..dd5a4463f4b0273f5c7491a3c78567e3ac6a3351 100644 --- a/src/main/java/net/minecraft/world/item/MinecartItem.java +++ b/src/main/java/net/minecraft/world/item/MinecartItem.java @@ -71,7 +71,7 @@ public class MinecartItem extends Item { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemstack1); BlockDispenseEvent event = new BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(d0, d1 + d3, d2)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get()) { // Folia - region threading worldserver.getCraftServer().getPluginManager().callEvent(event); } diff --git a/src/main/java/net/minecraft/world/item/SignItem.java b/src/main/java/net/minecraft/world/item/SignItem.java index 21c25026da4117b2cb2c85576d2def945a97dbe2..e0b51cb4ef9e847dc9a2e66f2c87c94d6d93c6a3 100644 --- a/src/main/java/net/minecraft/world/item/SignItem.java +++ b/src/main/java/net/minecraft/world/item/SignItem.java @@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState; public class SignItem extends StandingAndWallBlockItem { - public static BlockPos openSign; // CraftBukkit + public static final ThreadLocal openSign = new ThreadLocal<>(); // CraftBukkit // Folia - region threading public SignItem(Item.Properties settings, Block standingBlock, Block wallBlock) { super(standingBlock, wallBlock, settings, Direction.DOWN); @@ -39,7 +39,7 @@ public class SignItem extends StandingAndWallBlockItem { // CraftBukkit start - SPIGOT-4678 // blocksign.openTextEdit(entityhuman, tileentitysign, true); - SignItem.openSign = pos; + SignItem.openSign.set(pos); // Folia - region threading // CraftBukkit end } } diff --git a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java index e6bfcc50cdf728216084bc00a5bb8b6b3b8f72e4..b8a571fcc44f67ac8f6089e039a8620bf6c40603 100644 --- a/src/main/java/net/minecraft/world/level/BaseCommandBlock.java +++ b/src/main/java/net/minecraft/world/level/BaseCommandBlock.java @@ -22,7 +22,7 @@ import net.minecraft.world.phys.Vec3; public abstract class BaseCommandBlock implements CommandSource { - private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); + private static final ThreadLocal TIME_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss")); // Folia - region threading - SDF is not thread-safe private static final Component DEFAULT_NAME = Component.literal("@"); private long lastExecution = -1L; private boolean updateLastExecution = true; @@ -117,6 +117,7 @@ public abstract class BaseCommandBlock implements CommandSource { } public boolean performCommand(Level world) { + if (true) return false; // Folia - region threading if (!world.isClientSide && world.getGameTime() != this.lastExecution) { if ("Searge".equalsIgnoreCase(this.command)) { this.lastOutput = Component.literal("#itzlipofutzli"); @@ -175,11 +176,14 @@ public abstract class BaseCommandBlock implements CommandSource { this.customName = customName; } + public void threadCheck() {} // Folia + @Override public void sendSystemMessage(Component message) { if (this.trackOutput) { org.spigotmc.AsyncCatcher.catchOp("sendSystemMessage to a command block"); // Paper - Don't broadcast messages to command blocks - SimpleDateFormat simpledateformat = BaseCommandBlock.TIME_FORMAT; + this.threadCheck(); // Folia + SimpleDateFormat simpledateformat = BaseCommandBlock.TIME_FORMAT.get(); // Folia - region threading - SDF is not thread-safe Date date = new Date(); this.lastOutput = Component.literal("[" + simpledateformat.format(date) + "] ").append(message); diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java index 141b748abe80402731cdaf14a3d36aa7cef4f4bd..95938efe2282a024d9d428dedd16cbe127d3ffed 100644 --- a/src/main/java/net/minecraft/world/level/EntityGetter.java +++ b/src/main/java/net/minecraft/world/level/EntityGetter.java @@ -27,6 +27,12 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst return this.getEntities(EntityTypeTest.forClass(entityClass), box, predicate); } + // Folia start - region threading + default List getLocalPlayers() { + return java.util.Collections.emptyList(); + } + // Folia end - region threading + List players(); default List getEntities(@Nullable Entity except, AABB box) { @@ -125,7 +131,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst double d = -1.0; Player player = null; - for (Player player2 : this.players()) { + for (Player player2 : this.getLocalPlayers()) { // Folia - region threading if (targetPredicate == null || targetPredicate.test(player2)) { double e = player2.distanceToSqr(x, y, z); if ((maxDistance < 0.0 || e < maxDistance * maxDistance) && (d == -1.0 || e < d)) { @@ -146,7 +152,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst default List findNearbyBukkitPlayers(double x, double y, double z, double radius, @Nullable Predicate predicate) { com.google.common.collect.ImmutableList.Builder builder = com.google.common.collect.ImmutableList.builder(); - for (Player human : this.players()) { + for (Player human : this.getLocalPlayers()) { // Folia - region threading if (predicate == null || predicate.test(human)) { double distanceSquared = human.distanceToSqr(x, y, z); @@ -173,7 +179,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst // Paper start - Affects Spawning API default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) { - for (Player player : this.players()) { + for (Player player : this.getLocalPlayers()) { // Folia - region threading if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check double distanceSqr = player.distanceToSqr(x, y, z); if (range < 0.0D || distanceSqr < range * range) { @@ -186,7 +192,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst // Paper end - Affects Spawning API default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { - for (Player player : this.players()) { + for (Player player : this.getLocalPlayers()) { // Folia - region threading if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { double d = player.distanceToSqr(x, y, z); if (range < 0.0 || d < range * range) { @@ -200,17 +206,17 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst @Nullable default Player getNearestPlayer(TargetingConditions targetPredicate, LivingEntity entity) { - return this.getNearestEntity(this.players(), targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); + return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, entity, entity.getX(), entity.getY(), entity.getZ()); // Folia - region threading } @Nullable default Player getNearestPlayer(TargetingConditions targetPredicate, LivingEntity entity, double x, double y, double z) { - return this.getNearestEntity(this.players(), targetPredicate, entity, x, y, z); + return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, entity, x, y, z); // Folia - region threading } @Nullable default Player getNearestPlayer(TargetingConditions targetPredicate, double x, double y, double z) { - return this.getNearestEntity(this.players(), targetPredicate, null, x, y, z); + return this.getNearestEntity(this.getLocalPlayers(), targetPredicate, null, x, y, z); // Folia - region threading } @Nullable @@ -243,7 +249,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst default List getNearbyPlayers(TargetingConditions targetPredicate, LivingEntity entity, AABB box) { List list = Lists.newArrayList(); - for (Player player : this.players()) { + for (Player player : this.getLocalPlayers()) { // Folia - region threading if (box.contains(player.getX(), player.getY(), player.getZ()) && targetPredicate.test(entity, player)) { list.add(player); } @@ -269,8 +275,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst @Nullable default Player getPlayerByUUID(UUID uuid) { - for (int i = 0; i < this.players().size(); i++) { - Player player = this.players().get(i); + for (Player player : this.getLocalPlayers()) { // Folia - region threading if (uuid.equals(player.getUUID())) { return player; } diff --git a/src/main/java/net/minecraft/world/level/Explosion.java b/src/main/java/net/minecraft/world/level/Explosion.java index ad57bf49476192dea6a7367cbd0ad3f11e142e1b..d95d9b2ee7de56627d146cc898bc9d37171c5030 100644 --- a/src/main/java/net/minecraft/world/level/Explosion.java +++ b/src/main/java/net/minecraft/world/level/Explosion.java @@ -844,17 +844,18 @@ public class Explosion { if (!this.level.paperConfig().environment.optimizeExplosions) { return this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise collisions } + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading CacheKey key = new CacheKey(this, entity.getBoundingBox()); - Float blockDensity = this.level.explosionDensityCache.get(key); + Float blockDensity = worldData.explosionDensityCache.get(key); // Folia - region threading if (blockDensity == null) { blockDensity = this.getSeenFraction(vec3d, entity, blockCache, blockPos); // Paper - optimise collisions - this.level.explosionDensityCache.put(key, blockDensity); + worldData.explosionDensityCache.put(key, blockDensity); // Folia - region threading } return blockDensity; } - static class CacheKey { + public static class CacheKey { // Folia - region threading - public private final Level world; private final double posX, posY, posZ; private final double minX, minY, minZ; diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java index e2a0487089eb5a7bdc1433e4c75f69d8e9f9d5f9..b0ad25daf2d3c1727c61686e35eb4c03e1c60122 100644 --- a/src/main/java/net/minecraft/world/level/Level.java +++ b/src/main/java/net/minecraft/world/level/Level.java @@ -116,10 +116,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl public static final int TICKS_PER_DAY = 24000; public static final int MAX_ENTITY_SPAWN_Y = 20000000; public static final int MIN_ENTITY_SPAWN_Y = -20000000; - public final List blockEntityTickers = Lists.newArrayList(); // Paper - public - protected final NeighborUpdater neighborUpdater; - private final List pendingBlockEntityTickers = Lists.newArrayList(); - private boolean tickingBlockEntities; + //public final List blockEntityTickers = Lists.newArrayList(); // Paper - public // Folia - region threading + public final int neighbourUpdateMax; //protected final NeighborUpdater neighborUpdater; // Folia - region threading + //private final List pendingBlockEntityTickers = Lists.newArrayList(); // Folia - region threading + //private boolean tickingBlockEntities; // Folia - region threading public final Thread thread; private final boolean isDebug; private int skyDarken; @@ -129,7 +129,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl public float rainLevel; protected float oThunderLevel; public float thunderLevel; - public final RandomSource random = RandomSource.create(); + public final RandomSource random = new Entity.RandomRandomSource(); // Folia - region threading /** @deprecated */ @Deprecated private final RandomSource threadSafeRandom = RandomSource.createThreadSafe(); @@ -142,28 +142,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl private final ResourceKey dimension; private final RegistryAccess registryAccess; private final DamageSources damageSources; - private long subTickCount; + private final java.util.concurrent.atomic.AtomicLong subTickCount = new java.util.concurrent.atomic.AtomicLong(); //private long subTickCount; // Folia - region threading // CraftBukkit start Added the following private final CraftWorld world; public boolean pvpMode; public org.bukkit.generator.ChunkGenerator generator; - public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 - public boolean captureBlockStates = false; - public boolean captureTreeGeneration = false; - public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent - public Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper - public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates - public List captureDrops; + // Folia - region threading - moved to regionised data public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>(); - // Paper start - public int wakeupInactiveRemainingAnimals; - public int wakeupInactiveRemainingFlying; - public int wakeupInactiveRemainingMonsters; - public int wakeupInactiveRemainingVillagers; - // Paper end - public boolean populating; + // Folia - region threading - moved to regionised data + // Folia - region threading public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot // Paper start - add paper world config private final io.papermc.paper.configuration.WorldConfiguration paperConfig; @@ -177,9 +166,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl public static BlockPos lastPhysicsProblem; // Spigot private org.spigotmc.TickLimiter entityLimiter; private org.spigotmc.TickLimiter tileLimiter; - private int tileTickPosition; - public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions - public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here + //private int tileTickPosition; // Folia - region threading + //public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions // Folia - region threading + //public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here // Folia - region threading public CraftWorld getWorld() { return this.world; @@ -683,6 +672,32 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z); } // Paper end - optimise random ticking + // Folia start - region ticking + public final io.papermc.paper.threadedregions.RegionizedData worldRegionData + = new io.papermc.paper.threadedregions.RegionizedData<>( + (ServerLevel)this, () -> new io.papermc.paper.threadedregions.RegionizedWorldData((ServerLevel)Level.this), + io.papermc.paper.threadedregions.RegionizedWorldData.REGION_CALLBACK + ); + public volatile io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData tickData; + public final java.util.concurrent.ConcurrentHashMap.KeySetView needsChangeBroadcasting = java.util.concurrent.ConcurrentHashMap.newKeySet(); + + public io.papermc.paper.threadedregions.RegionizedWorldData getCurrentWorldData() { + final io.papermc.paper.threadedregions.RegionizedWorldData ret = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + if (ret == null) { + return ret; + } + Level world = ret.world; + if (world != this) { + throw new IllegalStateException("World mismatch: expected " + this.getWorld().getName() + " but got " + world.getWorld().getName()); + } + return ret; + } + + @Override + public List getLocalPlayers() { + return this.getCurrentWorldData().getLocalPlayers(); + } + // Folia end - region ticking protected Level(WritableLevelData worlddatamutable, ResourceKey resourcekey, RegistryAccess iregistrycustom, Holder holder, Supplier supplier, boolean flag, boolean flag1, long i, int j, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider, org.bukkit.World.Environment env, java.util.function.Function paperWorldConfigCreator, java.util.concurrent.Executor executor) { // Paper - create paper world config & Anti-Xray this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((net.minecraft.world.level.storage.PrimaryLevelData) worlddatamutable).getLevelName()); // Spigot @@ -724,7 +739,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl this.thread = Thread.currentThread(); this.biomeManager = new BiomeManager(this, i); this.isDebug = flag1; - this.neighborUpdater = new CollectingNeighborUpdater(this, j); + this.neighbourUpdateMax = j; // Folia - region threading this.registryAccess = iregistrycustom; this.damageSources = new DamageSources(iregistrycustom); // CraftBukkit start @@ -951,8 +966,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Nullable public final BlockState getBlockStateIfLoaded(BlockPos pos) { // CraftBukkit start - tree generation - if (this.captureTreeGeneration) { - CraftBlockState previous = this.capturedBlockStates.get(pos); + if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading + CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Folia - region threading if (previous != null) { return previous.getHandle(); } @@ -1014,16 +1029,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Override public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, pos, "Updating block asynchronously"); // Folia - region threading + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); // Folia - region threading // CraftBukkit start - tree generation - if (this.captureTreeGeneration) { + if (worldData.captureTreeGeneration) { // Folia - region threading // Paper start - Protect Bedrock and End Portal/Frames from being destroyed BlockState type = getBlockState(pos); if (!type.isDestroyable()) return false; // Paper end - Protect Bedrock and End Portal/Frames from being destroyed - CraftBlockState blockstate = this.capturedBlockStates.get(pos); + CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Folia - region threading if (blockstate == null) { blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags); - this.capturedBlockStates.put(pos.immutable(), blockstate); + worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading } blockstate.setData(state); blockstate.setFlag(flags); @@ -1040,10 +1057,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl // CraftBukkit start - capture blockstates boolean captured = false; - if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) { + if (worldData.captureBlockStates && !worldData.capturedBlockStates.containsKey(pos)) { // Folia - region threading CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot blockstate.setFlag(flags); // Paper - set flag - this.capturedBlockStates.put(pos.immutable(), blockstate); + worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading captured = true; } // CraftBukkit end @@ -1053,8 +1070,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl if (iblockdata1 == null) { // CraftBukkit start - remove blockstate if failed (or the same) - if (this.captureBlockStates && captured) { - this.capturedBlockStates.remove(pos); + if (worldData.captureBlockStates && captured) { // Folia - region threading + worldData.capturedBlockStates.remove(pos); // Folia - region threading } // CraftBukkit end return false; @@ -1091,7 +1108,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl */ // CraftBukkit start - if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates + if (!worldData.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates // Folia - region threading // Modularize client and physic updates // Spigot start try { @@ -1136,7 +1153,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam CraftWorld world = ((ServerLevel) this).getWorld(); boolean cancelledUpdates = false; // Paper - Fix block place logic - if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent + if (world != null && ((ServerLevel)this).getCurrentWorldData().hasPhysicsEvent) { // Paper - BlockPhysicsEvent // Folia - region threading BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata)); this.getCraftServer().getPluginManager().callEvent(event); @@ -1150,7 +1167,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl } // CraftBukkit start - SPIGOT-5710 - if (!this.preventPoiUpdated) { + if (!this.getCurrentWorldData().preventPoiUpdated) { // Folia - region threading this.onBlockStateChange(blockposition, iblockdata1, iblockdata2); } // CraftBukkit end @@ -1234,7 +1251,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Override public void neighborShapeChanged(Direction direction, BlockState neighborState, BlockPos pos, BlockPos neighborPos, int flags, int maxUpdateDepth) { - this.neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth); + this.getCurrentWorldData().neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, maxUpdateDepth); // Folia - region threading } @Override @@ -1259,11 +1276,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl return this.getChunkSource().getLightEngine(); } + // Folia start - region threading + @Nullable + public BlockState getBlockStateFromEmptyChunkIfLoaded(BlockPos pos) { + net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); + ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); + if (chunk != null) { + return chunk.getBlockState(pos); + } + return null; + } + + @Nullable + public BlockState getBlockStateFromEmptyChunk(BlockPos pos) { + net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); + ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); + if (chunk != null) { + return chunk.getBlockState(pos); + } + chunk = chunkProvider.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY, true); + return chunk.getBlockState(pos); + } + // Folia end - region threading + @Override public BlockState getBlockState(BlockPos pos) { // CraftBukkit start - tree generation - if (this.captureTreeGeneration) { - CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper + if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading + CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Paper // Folia - region threading if (previous != null) { return previous.getHandle(); } @@ -1362,7 +1402,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl } public void addBlockEntityTicker(TickingBlockEntity ticker) { - (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); + ((ServerLevel)this).getCurrentWorldData().addBlockEntityTicker(ticker); // Folia - regionised ticking } protected void tickBlockEntities() { @@ -1370,11 +1410,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl gameprofilerfiller.push("blockEntities"); this.timings.tileEntityPending.startTiming(); // Spigot - this.tickingBlockEntities = true; - if (!this.pendingBlockEntityTickers.isEmpty()) { - this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); - this.pendingBlockEntityTickers.clear(); - } + final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking + regionizedWorldData.seTtickingBlockEntities(true); // Folia - regionised ticking + regionizedWorldData.pushPendingTickingBlockEntities(); // Folia - regionised ticking + List blockEntityTickers = regionizedWorldData.getBlockEntityTickers(); // Folia - regionised ticking this.timings.tileEntityPending.stopTiming(); // Spigot this.timings.tileEntityTick.startTiming(); // Spigot @@ -1387,9 +1426,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl int tilesThisCycle = 0; var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet(); // Paper - Fix MC-117075; use removeAll toRemove.add(null); // Paper - Fix MC-117075 - for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters - this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0; - TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(this.tileTickPosition); + for (int i = 0; i < blockEntityTickers.size(); i++) { // Paper - Disable tick limiters // Folia - regionised ticking + TickingBlockEntity tickingblockentity = (TickingBlockEntity) blockEntityTickers.get(i); // Folia - regionised ticking // Spigot end if (tickingblockentity.isRemoved()) { @@ -1406,13 +1444,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl // Paper end - rewrite chunk system } } - this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 + blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 // Folia - regionised ticking this.timings.tileEntityTick.stopTiming(); // Spigot - this.tickingBlockEntities = false; - co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper + regionizedWorldData.seTtickingBlockEntities(false); // Folia - regionised ticking + //co.aikar.timings.TimingHistory.tileEntityTicks += this.blockEntityTickers.size(); // Paper // Folia - region threading gameprofilerfiller.pop(); - this.spigotConfig.currentPrimedTnt = 0; // Spigot + regionizedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading } public void guardEntityTick(Consumer tickConsumer, T entity) { @@ -1424,7 +1462,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); MinecraftServer.LOGGER.error(msg, throwable); getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent - entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); + if (!(entity instanceof net.minecraft.server.level.ServerPlayer)) entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); // Folia - properly disconnect players + if (entity instanceof net.minecraft.server.level.ServerPlayer player) player.connection.disconnect(net.minecraft.network.chat.Component.translatable("multiplayer.disconnect.generic"), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Folia - properly disconnect players // Paper end - Prevent block entity and entity crashes } this.moonrise$midTickTasks(); // Paper - rewrite chunk system @@ -1527,9 +1566,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Nullable public BlockEntity getBlockEntity(BlockPos blockposition, boolean validate) { + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { + return null; + } + // Folia end - region threading // Paper start - Perf: Optimize capturedTileEntities lookup net.minecraft.world.level.block.entity.BlockEntity blockEntity; - if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(blockposition)) != null) { + if (!this.getCurrentWorldData().capturedTileEntities.isEmpty() && (blockEntity = this.getCurrentWorldData().capturedTileEntities.get(blockposition)) != null) { // Folia - region threading return blockEntity; } // Paper end - Perf: Optimize capturedTileEntities lookup @@ -1542,8 +1586,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl if (!this.isOutsideBuildHeight(blockposition)) { // CraftBukkit start - if (this.captureBlockStates) { - this.capturedTileEntities.put(blockposition.immutable(), blockEntity); + if (this.getCurrentWorldData().captureBlockStates) { // Folia - region threading + this.getCurrentWorldData().capturedTileEntities.put(blockposition.immutable(), blockEntity); // Folia - region threading return; } // CraftBukkit end @@ -1623,6 +1667,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Override public List getEntities(@Nullable Entity except, AABB box, Predicate predicate) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, box, "Cannot getEntities asynchronously"); // Folia - region threading this.getProfiler().incrementCounter("getEntities"); // Paper start - rewrite chunk system final List ret = new java.util.ArrayList<>(); @@ -1649,6 +1694,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl public void getEntities(final EntityTypeTest entityTypeTest, final AABB boundingBox, final Predicate predicate, final List into, final int maxCount) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, boundingBox, "Cannot getEntities asynchronously"); // Folia - region threading this.getProfiler().incrementCounter("getEntities"); if (entityTypeTest instanceof net.minecraft.world.entity.EntityType byType) { @@ -1734,13 +1780,30 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl public void disconnect() {} + @Override // Folia - region threading public long getGameTime() { - return this.levelData.getGameTime(); + // Dumb world gen thread calls this for some reason. So, check for null. + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); + return worldData == null ? this.getLevelData().getGameTime() : worldData.getTickData().nonRedstoneGameTime(); } public long getDayTime() { - return this.levelData.getDayTime(); + // Dumb world gen thread calls this for some reason. So, check for null. + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); + return worldData == null ? this.getLevelData().getDayTime() : worldData.getTickData().dayTime(); + } + + // Folia start - region threading + @Override + public long dayTime() { + return this.getDayTime(); + } + + @Override + public long getRedstoneGameTime() { + return this.getCurrentWorldData().getRedstoneGameTime(); } + // Folia end - region threading public boolean mayInteract(Player player, BlockPos pos) { return true; @@ -1933,8 +1996,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl public abstract RecipeManager getRecipeManager(); public BlockPos getBlockRandomPos(int x, int y, int z, int l) { - this.randValue = this.randValue * 3 + 1013904223; - int i1 = this.randValue >> 2; + int i1 = this.random.nextInt() >> 2; // Folia - region threading return new BlockPos(x + (i1 & 15), y + (i1 >> 16 & l), z + (i1 >> 8 & 15)); } @@ -1964,7 +2026,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Override public long nextSubTickCount() { - return (long) (this.subTickCount++); + return this.subTickCount.getAndIncrement(); // Folia - region threading } @Override diff --git a/src/main/java/net/minecraft/world/level/LevelAccessor.java b/src/main/java/net/minecraft/world/level/LevelAccessor.java index 54d13eebc9b01e9d77f51011b7de95b80bc21669..47c3e7f15e1ba9ea8a1c1bb1a0af61dd78dc1b85 100644 --- a/src/main/java/net/minecraft/world/level/LevelAccessor.java +++ b/src/main/java/net/minecraft/world/level/LevelAccessor.java @@ -38,12 +38,22 @@ public interface LevelAccessor extends CommonLevelAccessor, LevelTimeAccess { LevelTickAccess getBlockTicks(); + // Folia start - region threading + default long getGameTime() { + return this.getLevelData().getGameTime(); + } + + default long getRedstoneGameTime() { + return this.getLevelData().getGameTime(); + } + // Folia end - region threading + private ScheduledTick createTick(BlockPos pos, T type, int delay, TickPriority priority) { - return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + (long) delay, priority, this.nextSubTickCount()); + return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + (long) delay, priority, this.nextSubTickCount()); // Folia - region threading } private ScheduledTick createTick(BlockPos pos, T type, int delay) { - return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + (long) delay, this.nextSubTickCount()); + return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + (long) delay, this.nextSubTickCount()); // Folia - region threading } default void scheduleTick(BlockPos pos, Block block, int delay, TickPriority priority) { diff --git a/src/main/java/net/minecraft/world/level/LevelReader.java b/src/main/java/net/minecraft/world/level/LevelReader.java index 1a4dc4b2561dbaf01246b4fb46266b1ac84008b8..345090afeab4f6396244c50bdbe8320887e15d7e 100644 --- a/src/main/java/net/minecraft/world/level/LevelReader.java +++ b/src/main/java/net/minecraft/world/level/LevelReader.java @@ -207,6 +207,25 @@ public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_syste return maxY >= this.getMinBuildHeight() && minY < this.getMaxBuildHeight() && this.hasChunksAt(minX, minZ, maxX, maxZ); } + // Folia start - region threading + default boolean hasAndOwnsChunksAt(int minX, int minZ, int maxX, int maxZ) { + int i = SectionPos.blockToSectionCoord(minX); + int j = SectionPos.blockToSectionCoord(maxX); + int k = SectionPos.blockToSectionCoord(minZ); + int l = SectionPos.blockToSectionCoord(maxZ); + + for(int m = i; m <= j; ++m) { + for(int n = k; n <= l; ++n) { + if (!this.hasChunk(m, n) || (this instanceof net.minecraft.server.level.ServerLevel world && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, m, n))) { + return false; + } + } + } + + return true; + } + // Folia end - region threading + @Deprecated default boolean hasChunksAt(int minX, int minZ, int maxX, int maxZ) { int i = SectionPos.blockToSectionCoord(minX); diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java index e524b27d185da3e88668f8ef107517272860bd66..dd02f20e965e6e4aaaf5992ea3c4bb0b7f3cc270 100644 --- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java +++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java @@ -141,7 +141,7 @@ public final class NaturalSpawner { int limit = enumcreaturetype.getMaxInstancesPerChunk(); SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype); if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { - spawnThisTick = world.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % world.ticksPerSpawnCategory.getLong(spawnCategory) == 0; + spawnThisTick = world.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && world.getRedstoneGameTime() % world.ticksPerSpawnCategory.getLong(spawnCategory) == 0; // Folia - region threading limit = world.getWorld().getSpawnLimit(spawnCategory); } diff --git a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java index 3d377b9e461040405e0a7dcbd72d1506b48eb44e..782890e227ff9dab44dd92327979c201985f116e 100644 --- a/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java +++ b/src/main/java/net/minecraft/world/level/ServerLevelAccessor.java @@ -7,6 +7,12 @@ public interface ServerLevelAccessor extends LevelAccessor { ServerLevel getLevel(); + // Folia start - region threading + default public StructureManager structureManager() { + throw new UnsupportedOperationException(); + } + // Folia end - region threading + default void addFreshEntityWithPassengers(Entity entity) { // CraftBukkit start this.addFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT); diff --git a/src/main/java/net/minecraft/world/level/StructureManager.java b/src/main/java/net/minecraft/world/level/StructureManager.java index 54972cce2314eff774250101df43a9b7074e9604..0c4942895a83ce1392dd1e71a00ab6d0cb15dab7 100644 --- a/src/main/java/net/minecraft/world/level/StructureManager.java +++ b/src/main/java/net/minecraft/world/level/StructureManager.java @@ -48,11 +48,7 @@ public class StructureManager { } public List startsForStructure(ChunkPos pos, Predicate predicate) { - // Paper start - Fix swamp hut cat generation deadlock - return this.startsForStructure(pos, predicate, null); - } - public List startsForStructure(ChunkPos pos, Predicate predicate, @Nullable ServerLevelAccessor levelAccessor) { - Map map = (levelAccessor == null ? this.level : levelAccessor).getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); + Map map = this.level.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); // Folia - region threading // Paper end - Fix swamp hut cat generation deadlock Builder builder = ImmutableList.builder(); @@ -121,20 +117,11 @@ public class StructureManager { } public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate> predicate) { - // Paper start - Fix swamp hut cat generation deadlock - return this.getStructureWithPieceAt(pos, predicate, null); - } - - public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey tag, @Nullable ServerLevelAccessor levelAccessor) { - return this.getStructureWithPieceAt(pos, structure -> structure.is(tag), levelAccessor); - } - - public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate> predicate, @Nullable ServerLevelAccessor levelAccessor) { - // Paper end - Fix swamp hut cat generation deadlock + // Folia - region threading Registry registry = this.registryAccess().registryOrThrow(Registries.STRUCTURE); for (StructureStart structureStart : this.startsForStructure( - new ChunkPos(pos), structure -> registry.getHolder(registry.getId(structure)).map(predicate::test).orElse(false), levelAccessor // Paper - Fix swamp hut cat generation deadlock + new ChunkPos(pos), structure -> registry.getHolder(registry.getId(structure)).map(predicate::test).orElse(false) // Paper - Fix swamp hut cat generation deadlock // Folia - region threading )) { if (this.structureHasPieceAt(pos, structureStart)) { return structureStart; @@ -179,7 +166,7 @@ public class StructureManager { } public void addReference(StructureStart structureStart) { - structureStart.addReference(); + //structureStart.addReference(); // Folia - region threading - move to caller this.structureCheck.incrementReference(structureStart.getChunkPos(), structureStart.getStructure()); } diff --git a/src/main/java/net/minecraft/world/level/block/BedBlock.java b/src/main/java/net/minecraft/world/level/block/BedBlock.java index 85d598c3354ee62f0fd1b26e485e0084967c0380..8492c8fd3ff448d559b33f27b549180918aedeea 100644 --- a/src/main/java/net/minecraft/world/level/block/BedBlock.java +++ b/src/main/java/net/minecraft/world/level/block/BedBlock.java @@ -365,7 +365,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock world.setBlock(blockposition1, (BlockState) state.setValue(BedBlock.PART, BedPart.HEAD), 3); // CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states - if (world.captureBlockStates) { + if (world.getCurrentWorldData().captureBlockStates) { // Folia - region threading return; } // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java index a7108b2be0746aa1f0e574d8c6f5ffad6d369835..bf9d00463627c702c639c7cb625c3eb35c2e44aa 100644 --- a/src/main/java/net/minecraft/world/level/block/Block.java +++ b/src/main/java/net/minecraft/world/level/block/Block.java @@ -390,8 +390,8 @@ public class Block extends BlockBehaviour implements ItemLike { entityitem.setDefaultPickUpDelay(); // CraftBukkit start - if (world.captureDrops != null) { - world.captureDrops.add(entityitem); + if (world.getCurrentWorldData().captureDrops != null) { // Folia - region threading + world.getCurrentWorldData().captureDrops.add(entityitem); // Folia - region threading } else { world.addFreshEntity(entityitem); } diff --git a/src/main/java/net/minecraft/world/level/block/BushBlock.java b/src/main/java/net/minecraft/world/level/block/BushBlock.java index a7b4b5600e3c889c69ac22294899713d50b5fe5c..08847aff604a254e82b4a7d31281953ef04462dd 100644 --- a/src/main/java/net/minecraft/world/level/block/BushBlock.java +++ b/src/main/java/net/minecraft/world/level/block/BushBlock.java @@ -28,7 +28,7 @@ public abstract class BushBlock extends Block { protected BlockState updateShape(BlockState state, Direction direction, BlockState neighborState, LevelAccessor world, BlockPos pos, BlockPos neighborPos) { // CraftBukkit start if (!state.canSurvive(world, pos)) { - if (!(world instanceof net.minecraft.server.level.ServerLevel && ((net.minecraft.server.level.ServerLevel) world).hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper + if (!(world instanceof net.minecraft.server.level.ServerLevel && ((net.minecraft.server.level.ServerLevel) world).getCurrentWorldData().hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper // Folia - region threading return Blocks.AIR.defaultBlockState(); } } diff --git a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java index 26ec4838c476b8091f80aeac61db8041d5c89e88..82d333b2644c7af7137f155bfd10e03e8d7e9adc 100644 --- a/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java +++ b/src/main/java/net/minecraft/world/level/block/DaylightDetectorBlock.java @@ -120,7 +120,7 @@ public class DaylightDetectorBlock extends BaseEntityBlock { } private static void tickEntity(Level world, BlockPos pos, BlockState state, DaylightDetectorBlockEntity blockEntity) { - if (world.getGameTime() % 20L == 0L) { + if (world.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading DaylightDetectorBlock.updateSignalStrength(state, world, pos); } diff --git a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java index f6edfea463b3725d3a79aca38825e86dbf82175c..92cab51f6b3eeb07b574bd3ba82d736e8880acae 100644 --- a/src/main/java/net/minecraft/world/level/block/DispenserBlock.java +++ b/src/main/java/net/minecraft/world/level/block/DispenserBlock.java @@ -51,7 +51,7 @@ public class DispenserBlock extends BaseEntityBlock { object2objectopenhashmap.defaultReturnValue(DispenserBlock.DEFAULT_BEHAVIOR); }); private static final int TRIGGER_DURATION = 4; - public static boolean eventFired = false; // CraftBukkit + public static ThreadLocal eventFired = ThreadLocal.withInitial(() -> Boolean.FALSE); // CraftBukkit // Folia - region threading @Override public MapCodec codec() { @@ -111,7 +111,7 @@ public class DispenserBlock extends BaseEntityBlock { if (idispensebehavior != DispenseItemBehavior.NOOP) { if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(world, pos, itemstack, i)) return; // Paper - Add BlockPreDispenseEvent - DispenserBlock.eventFired = false; // CraftBukkit - reset event status + DispenserBlock.eventFired.set(Boolean.FALSE); // CraftBukkit - reset event status // Folia - region threading tileentitydispenser.setItem(i, idispensebehavior.dispense(sourceblock, itemstack)); } diff --git a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java index f446c40c4d90307c568faa2866800f5326634df6..455be3cd52cf21bc37d54b8d408e458da7ae9b72 100644 --- a/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java +++ b/src/main/java/net/minecraft/world/level/block/DoublePlantBlock.java @@ -102,7 +102,7 @@ public class DoublePlantBlock extends BushBlock { protected static void preventDropFromBottomPart(Level world, BlockPos pos, BlockState state, Player player) { // CraftBukkit start - if (((net.minecraft.server.level.ServerLevel)world).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper + if (((net.minecraft.server.level.ServerLevel)world).getCurrentWorldData().hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(world, pos).isCancelled()) { // Paper // Folia - region threading return; } // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java b/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java index 3f5bb5c9ceb5b31fcc9ef0a7a6157e1e1cb2a09f..deb555743b15afcc22e694dcf1d8ceca21c0c191 100644 --- a/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java +++ b/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java @@ -124,6 +124,29 @@ public class EndGatewayBlock extends BaseEntityBlock implements Portal { } } + // Folia start - region threading + @Override + public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { + return false; + } + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { + return false; + } + + BlockEntity tile = sourceWorld.getBlockEntity(portalPos); + + if (!(tile instanceof TheEndGatewayBlockEntity endGateway)) { + return false; + } + + return TheEndGatewayBlockEntity.teleportRegionThreading( + sourceWorld, portalPos, portalTarget, endGateway, EndGatewayBlock.calculateExitMovement(portalTarget), + DimensionTransition.PLACE_PORTAL_TICKET + ); + } + // Folia end - region threading + private static Vec3 calculateExitMovement(Entity entity) { return entity instanceof ThrownEnderpearl ? new Vec3(0.0D, -1.0D, 0.0D) : entity.getDeltaMovement(); } diff --git a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java index 6937d334b70791c094f6b752373225cfb64ee7ac..e92a463cf2a59d6d83826de1b53cc993bb05d64e 100644 --- a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java +++ b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java @@ -125,6 +125,20 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal { } } + // Folia start - region threading + @Override + public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { + return false; + } + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { + return false; + } + + return portalTarget.endPortalLogicAsync(portalPos); + } + // Folia end - region threading + @Override public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) { double d0 = (double) pos.getX() + random.nextDouble(); diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java index d59e33e7326489c6d55d316d0130f22235f4c63c..edb22983a17cc22ba01ee28a9ebc685aaae117eb 100644 --- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java +++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java @@ -93,8 +93,8 @@ public class FarmBlock extends Block { @Override protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { int i = (Integer) state.getValue(FarmBlock.MOISTURE); - if (i > 0 && world.paperConfig().tickRates.wetFarmland != 1 && (world.paperConfig().tickRates.wetFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks - if (i == 0 && world.paperConfig().tickRates.dryFarmland != 1 && (world.paperConfig().tickRates.dryFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks + if (i > 0 && world.paperConfig().tickRates.wetFarmland != 1 && (world.paperConfig().tickRates.wetFarmland < 1 || (world.getRedstoneGameTime() + pos.hashCode()) % world.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading + if (i == 0 && world.paperConfig().tickRates.dryFarmland != 1 && (world.paperConfig().tickRates.dryFarmland < 1 || (world.getRedstoneGameTime() + pos.hashCode()) % world.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading if (!FarmBlock.isNearWater(world, pos) && !world.isRainingAt(pos.above())) { if (i > 0) { diff --git a/src/main/java/net/minecraft/world/level/block/FungusBlock.java b/src/main/java/net/minecraft/world/level/block/FungusBlock.java index 454f95ba814b375e97189430b498c0e7486fbd94..59874de6c66f48984d9ba35c6886595027259765 100644 --- a/src/main/java/net/minecraft/world/level/block/FungusBlock.java +++ b/src/main/java/net/minecraft/world/level/block/FungusBlock.java @@ -76,9 +76,9 @@ public class FungusBlock extends BushBlock implements BonemealableBlock { this.getFeature(world).ifPresent((holder) -> { // CraftBukkit start if (this == Blocks.WARPED_FUNGUS) { - SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS; + SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.WARPED_FUNGUS); // Folia - region threading } else if (this == Blocks.CRIMSON_FUNGUS) { - SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS; + SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.CRIMSON_FUNGUS); // Folia - region threading } // CraftBukkit end ((ConfiguredFeature) holder.value()).place(world, world.getChunkSource().getGenerator(), random, pos); diff --git a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java index c6f7815b5fad3aad4635208aa2e5c6739e13cb45..be52d9e56559704c7f258502b4c2919a245d2c2f 100644 --- a/src/main/java/net/minecraft/world/level/block/HoneyBlock.java +++ b/src/main/java/net/minecraft/world/level/block/HoneyBlock.java @@ -86,7 +86,7 @@ public class HoneyBlock extends HalfTransparentBlock { } private void maybeDoSlideAchievement(Entity entity, BlockPos pos) { - if (entity instanceof ServerPlayer && entity.level().getGameTime() % 20L == 0L) { + if (entity instanceof ServerPlayer && entity.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading CriteriaTriggers.HONEY_BLOCK_SLIDE.trigger((ServerPlayer)entity, entity.level().getBlockState(pos)); } } diff --git a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java index a3ccd7ca5b280d4c14100bcc22c644e69262e7a3..97cef8c0d1313f0f4441cdfa96d35e9e891b2529 100644 --- a/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java +++ b/src/main/java/net/minecraft/world/level/block/LightningRodBlock.java @@ -108,7 +108,7 @@ public class LightningRodBlock extends RodBlock implements SimpleWaterloggedBloc @Override public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) { - if (world.isThundering() && (long) world.random.nextInt(200) <= world.getGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { + if (world.isThundering() && (long) world.random.nextInt(200) <= world.getRedstoneGameTime() % 200L && pos.getY() == world.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { // Folia - region threading ParticleUtils.spawnParticlesAlongAxis(((Direction) state.getValue(LightningRodBlock.FACING)).getAxis(), world, pos, 0.125D, ParticleTypes.ELECTRIC_SPARK, UniformInt.of(1, 2)); } } diff --git a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java index 1172d85c5c26ab2142343d91149766e5993cb36a..99d42fdf822c13e8d491b665220d61579143c8e6 100644 --- a/src/main/java/net/minecraft/world/level/block/MushroomBlock.java +++ b/src/main/java/net/minecraft/world/level/block/MushroomBlock.java @@ -105,7 +105,7 @@ public class MushroomBlock extends BushBlock implements BonemealableBlock { return false; } else { world.removeBlock(pos, false); - SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM; // CraftBukkit + SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? TreeType.BROWN_MUSHROOM : TreeType.RED_MUSHROOM); // CraftBukkit // Folia - region threading if (((ConfiguredFeature) ((Holder) optional.get()).value()).place(world, world.getChunkSource().getGenerator(), random, pos)) { return true; } else { diff --git a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java index 1722188fbfccd233625db540ddcaf646762fd023..881386c8126f23e1b3b905836ac063a4565615a9 100644 --- a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java +++ b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java @@ -170,6 +170,33 @@ public class NetherPortalBlock extends Block implements Portal { } } + // Folia start - region threading + @Override + public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { + return false; + } + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { + return false; + } + + return portalTarget.netherPortalLogicAsync(portalPos); + } + + public static BlockUtil.FoundRectangle findPortalAround(ServerLevel world, BlockPos rough, WorldBorder worldBorder, int searchRadius) { + BlockPos found = world.getPortalForcer().findClosestPortalPosition(rough, worldBorder, searchRadius).orElse(null); + if (found == null) { + return null; + } + + BlockState portalState = world.getBlockStateFromEmptyChunk(found); + + return BlockUtil.getLargestRectangleAround(found, portalState.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (pos) -> { + return world.getBlockStateFromEmptyChunk(pos) == portalState; + }); + } + // Folia end - region threading + @Nullable private DimensionTransition getExitPortal(ServerLevel worldserver, Entity entity, BlockPos blockposition, BlockPos blockposition1, boolean flag, WorldBorder worldborder, int searchRadius, boolean canCreatePortal, int createRadius) { Optional optional = worldserver.getPortalForcer().findClosestPortalPosition(blockposition1, worldborder, searchRadius); @@ -178,10 +205,10 @@ public class NetherPortalBlock extends Block implements Portal { if (optional.isPresent()) { BlockPos blockposition2 = (BlockPos) optional.get(); - BlockState iblockdata = worldserver.getBlockState(blockposition2); + BlockState iblockdata = worldserver.getBlockStateFromEmptyChunk(blockposition2); // Folia - region threading blockutil_rectangle = BlockUtil.getLargestRectangleAround(blockposition2, (Direction.Axis) iblockdata.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (blockposition3) -> { - return worldserver.getBlockState(blockposition3) == iblockdata; + return worldserver.getBlockStateFromEmptyChunk(blockposition3) == iblockdata; // Folia - region threading }); dimensiontransition_a = DimensionTransition.PLAY_PORTAL_SOUND.then((entity1) -> { entity1.placePortalTicket(blockposition2); @@ -227,7 +254,7 @@ public class NetherPortalBlock extends Block implements Portal { return NetherPortalBlock.createDimensionTransition(world, exitPortalRectangle, enumdirection_enumaxis, vec3d, entity, entity.getDeltaMovement(), entity.getYRot(), entity.getXRot(), postDimensionTransition); } - private static DimensionTransition createDimensionTransition(ServerLevel world, BlockUtil.FoundRectangle exitPortalRectangle, Direction.Axis axis, Vec3 positionInPortal, Entity entity, Vec3 velocity, float yaw, float pitch, DimensionTransition.PostDimensionTransition postDimensionTransition) { + public static DimensionTransition createDimensionTransition(ServerLevel world, BlockUtil.FoundRectangle exitPortalRectangle, Direction.Axis axis, Vec3 positionInPortal, Entity entity, Vec3 velocity, float yaw, float pitch, DimensionTransition.PostDimensionTransition postDimensionTransition) { // Folia - region threading - public BlockPos blockposition = exitPortalRectangle.minCorner; BlockState iblockdata = world.getBlockState(blockposition); Direction.Axis enumdirection_enumaxis1 = (Direction.Axis) iblockdata.getOptionalValue(BlockStateProperties.HORIZONTAL_AXIS).orElse(Direction.Axis.X); diff --git a/src/main/java/net/minecraft/world/level/block/Portal.java b/src/main/java/net/minecraft/world/level/block/Portal.java index 8ab8b76992bd4a222a9c217aba57392bf4d65d71..e2cb99c1cf5fd0468a80a3cebcb884617d0f3281 100644 --- a/src/main/java/net/minecraft/world/level/block/Portal.java +++ b/src/main/java/net/minecraft/world/level/block/Portal.java @@ -14,6 +14,10 @@ public interface Portal { @Nullable DimensionTransition getPortalDestination(ServerLevel world, Entity entity, BlockPos pos); + // Folia start - region threading + public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos); + // Folia end - region threading + default Portal.Transition getLocalTransition() { return Portal.Transition.NONE; } diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java index c131734cad123a35456d18f8a161f77a4ac9ac99..c4471342eea4f9e2b0916fc1c5f1b24bc07757fd 100644 --- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java +++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java @@ -68,7 +68,7 @@ public class RedStoneWireBlock extends Block { }); private static final float PARTICLE_DENSITY = 0.2F; private final BlockState crossState; - public boolean shouldSignal = true; + //public boolean shouldSignal = true; // Folia - region threading - move to regionised world data @Override public MapCodec codec() { @@ -260,7 +260,11 @@ public class RedStoneWireBlock extends Block { // Paper start - Optimize redstone (Eigencraft) // The bulk of the new functionality is found in RedstoneWireTurbo.java - com.destroystokyo.paper.util.RedstoneWireTurbo turbo = new com.destroystokyo.paper.util.RedstoneWireTurbo(this); + // Folia start - region threading + private com.destroystokyo.paper.util.RedstoneWireTurbo getTurbo(Level world) { + return world.getCurrentWorldData().turbo; + } + // Folia end - region threading /* * Modified version of pre-existing updateSurroundingRedstone, which is called from @@ -269,7 +273,7 @@ public class RedStoneWireBlock extends Block { */ private void updateSurroundingRedstone(Level worldIn, BlockPos pos, BlockState state, BlockPos source) { if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.EIGENCRAFT) { - turbo.updateSurroundingRedstone(worldIn, pos, state, source); + this.getTurbo(worldIn).updateSurroundingRedstone(worldIn, pos, state, source); // Folia - region threading return; } updatePowerStrength(worldIn, pos, state); @@ -288,9 +292,9 @@ public class RedStoneWireBlock extends Block { int i = state.getValue(POWER); int j = 0; j = this.getPower(j, worldIn.getBlockState(pos2)); - this.shouldSignal = false; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading int k = worldIn.getBestNeighborSignal(pos1); - this.shouldSignal = true; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading if (worldIn.paperConfig().misc.redstoneImplementation == io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation.VANILLA) { // This code is totally redundant to if statements just below the loop. @@ -360,7 +364,7 @@ public class RedStoneWireBlock extends Block { // [Space Walker] suppress shape updates and emit those manually to // bypass the new neighbor update stack. if (worldIn.setBlock(pos1, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) - turbo.updateNeighborShapes(worldIn, pos1, state); + this.getTurbo(worldIn).updateNeighborShapes(worldIn, pos1, state); // Folia - region threading } } @@ -409,10 +413,10 @@ public class RedStoneWireBlock extends Block { } private int calculateTargetStrength(Level world, BlockPos pos) { - this.shouldSignal = false; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading int i = world.getBestNeighborSignal(pos); - this.shouldSignal = true; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading int j = 0; if (i < 15) { @@ -551,12 +555,12 @@ public class RedStoneWireBlock extends Block { @Override protected int getDirectSignal(BlockState state, BlockGetter world, BlockPos pos, Direction direction) { - return !this.shouldSignal ? 0 : state.getSignal(world, pos, direction); + return !io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal ? 0 : state.getSignal(world, pos, direction); // Folia - region threading } @Override protected int getSignal(BlockState state, BlockGetter world, BlockPos pos, Direction direction) { - if (this.shouldSignal && direction != Direction.DOWN) { + if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal && direction != Direction.DOWN) { // Folia - region threading int i = (Integer) state.getValue(RedStoneWireBlock.POWER); return i == 0 ? 0 : (direction != Direction.UP && !((RedstoneSide) this.getConnectionState(world, state, pos).getValue((Property) RedStoneWireBlock.PROPERTY_BY_DIRECTION.get(direction.getOpposite()))).isConnected() ? 0 : i); @@ -583,7 +587,10 @@ public class RedStoneWireBlock extends Block { @Override protected boolean isSignalSource(BlockState state) { - return this.shouldSignal; + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); + return worldData == null || worldData.shouldSignal; + // Folia end - region threading } public static int getColorForPower(int powerLevel) { diff --git a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java index ceba9617748a8b4f3a9bd459475952c9c6c9ed7c..525d3b4c0b91b87399748fceda61c4046fa6bef2 100644 --- a/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java +++ b/src/main/java/net/minecraft/world/level/block/RedstoneTorchBlock.java @@ -81,10 +81,10 @@ public class RedstoneTorchBlock extends BaseTorchBlock { protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { boolean flag = this.hasNeighborSignal(world, pos, state); // Paper start - Faster redstone torch rapid clock removal - java.util.ArrayDeque redstoneUpdateInfos = world.redstoneUpdateInfos; + java.util.ArrayDeque redstoneUpdateInfos = world.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading if (redstoneUpdateInfos != null) { RedstoneTorchBlock.Toggle curr; - while ((curr = redstoneUpdateInfos.peek()) != null && world.getGameTime() - curr.when > 60L) { + while ((curr = redstoneUpdateInfos.peek()) != null && world.getRedstoneGameTime() - curr.when > 60L) { // Folia - region threading redstoneUpdateInfos.poll(); } } @@ -165,14 +165,14 @@ public class RedstoneTorchBlock extends BaseTorchBlock { private static boolean isToggledTooFrequently(Level world, BlockPos pos, boolean addNew) { // Paper start - Faster redstone torch rapid clock removal - java.util.ArrayDeque list = world.redstoneUpdateInfos; + java.util.ArrayDeque list = world.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading if (list == null) { - list = world.redstoneUpdateInfos = new java.util.ArrayDeque<>(); + list = world.getCurrentWorldData().redstoneUpdateInfos = new java.util.ArrayDeque<>(); // Folia - region threading } // Paper end - Faster redstone torch rapid clock removal if (addNew) { - list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getGameTime())); + list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getRedstoneGameTime())); // Folia - region threading } int i = 0; @@ -194,12 +194,18 @@ public class RedstoneTorchBlock extends BaseTorchBlock { public static class Toggle { - final BlockPos pos; - final long when; + public final BlockPos pos; // Folia - region threading + long when; // Folia - region ticking public Toggle(BlockPos pos, long time) { this.pos = pos; this.when = time; } + + // Folia start - region ticking + public void offsetTime(long offset) { + this.when += offset; + } + // Folia end - region ticking } } diff --git a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java index d262a5a6da57ef9ba9a6fe0dfbc88f577105e74f..e842c05cfe5991ba33582b9399610affbd02913f 100644 --- a/src/main/java/net/minecraft/world/level/block/SaplingBlock.java +++ b/src/main/java/net/minecraft/world/level/block/SaplingBlock.java @@ -35,7 +35,7 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { protected static final float AABB_OFFSET = 6.0F; protected static final VoxelShape SHAPE = Block.box(2.0D, 0.0D, 2.0D, 14.0D, 12.0D, 14.0D); protected final TreeGrower treeGrower; - public static TreeType treeType; // CraftBukkit + public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // CraftBukkit // Folia - region threading @Override public MapCodec codec() { @@ -66,18 +66,19 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { world.setBlock(pos, (net.minecraft.world.level.block.state.BlockState) state.cycle(SaplingBlock.STAGE), 4); } else { // CraftBukkit start - if (world.captureTreeGeneration) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + if (worldData.captureTreeGeneration) { // Folia - region threading this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); } else { - world.captureTreeGeneration = true; + worldData.captureTreeGeneration = true; // Folia - region threading this.treeGrower.growTree(world, world.getChunkSource().getGenerator(), pos, state, random); - world.captureTreeGeneration = false; - if (world.capturedBlockStates.size() > 0) { - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; + worldData.captureTreeGeneration = false; // Folia - region threading + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading + TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading + SaplingBlock.treeTypeRT.set(null); // Folia - region threading Location location = CraftLocation.toBukkit(pos, world.getWorld()); - java.util.List blocks = new java.util.ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + java.util.List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading StructureGrowEvent event = null; if (treeType != null) { event = new StructureGrowEvent(location, treeType, false, null, blocks); diff --git a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java index b7165ec19bef1a07f6618fc0429d86cda1b08da4..ad44708d19d216ed57c664940ccd1c8ecb78877c 100644 --- a/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +++ b/src/main/java/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java @@ -54,7 +54,7 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { @Override protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { - if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks + if (this instanceof GrassBlock && world.paperConfig().tickRates.grassSpread != 1 && (world.paperConfig().tickRates.grassSpread < 1 || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + pos.hashCode()) % world.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - regionised ticking // Paper start - Perf: optimize dirt and snow spreading final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = world.getChunkIfLoaded(pos); if (cachedBlockChunk == null) { // Is this needed? diff --git a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java index bbf59b2577812e74ffd45f694b83a42e043273c0..9d4ecf409098adbf4d47b08d7ef10c6d9c3de113 100644 --- a/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java +++ b/src/main/java/net/minecraft/world/level/block/WitherSkullBlock.java @@ -61,7 +61,7 @@ public class WitherSkullBlock extends SkullBlock { } public static void checkSpawn(Level world, BlockPos pos, SkullBlockEntity blockEntity) { - if (world.captureBlockStates) return; // CraftBukkit + if (world.getCurrentWorldData().captureBlockStates) return; // CraftBukkit // Folia - region threading if (!world.isClientSide) { BlockState iblockdata = blockEntity.getBlockState(); boolean flag = iblockdata.is(Blocks.WITHER_SKELETON_SKULL) || iblockdata.is(Blocks.WITHER_SKELETON_WALL_SKULL); diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java index 814e70f558d7a6186233da0ff86c94c95d390e09..412150d8f2c2604bc754ecbb04a1001932f29254 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/BeaconBlockEntity.java @@ -215,7 +215,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name } i1 = blockEntity.levels; - if (world.getGameTime() % 80L == 0L) { + if (world.getRedstoneGameTime() % 80L == 0L) { // Folia - region threading if (!blockEntity.beamSections.isEmpty()) { blockEntity.levels = BeaconBlockEntity.updateBase(world, i, j, k); } @@ -339,7 +339,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name list = world.getEntitiesOfClass(Player.class, axisalignedbb); } else { list = new java.util.ArrayList<>(); - for (Player player : world.players()) { + for (Player player : world.getLocalPlayers()) { // Folia - region threading if (player.isSpectator()) { continue; } diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java index 7dfabb11d3c8112f6daef35d204a2e324f4ddb5e..b7c1c8e3701fc2d67df34aaf5cebdf78e340f155 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java @@ -38,7 +38,7 @@ import co.aikar.timings.MinecraftTimings; // Paper import co.aikar.timings.Timing; // Paper public abstract class BlockEntity { - static boolean ignoreTileUpdates; // Paper - Perf: Optimize Hoppers + static final ThreadLocal IGNORE_TILE_UPDATES = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading public Timing tickTimer = MinecraftTimings.getTileEntityTimings(this); // Paper // CraftBukkit start - data containers @@ -54,6 +54,12 @@ public abstract class BlockEntity { private BlockState blockState; private DataComponentMap components; + // Folia start - region ticking + public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { + + } + // Folia end - region ticking + public BlockEntity(BlockEntityType type, BlockPos pos, BlockState state) { this.components = DataComponentMap.EMPTY; this.type = type; @@ -230,7 +236,7 @@ public abstract class BlockEntity { public void setChanged() { if (this.level != null) { - if (ignoreTileUpdates) return; // Paper - Perf: Optimize Hoppers + if (IGNORE_TILE_UPDATES.get().booleanValue()) return; // Paper - Perf: Optimize Hoppers // Folia - region threading BlockEntity.setChanged(this.level, this.worldPosition, this.blockState); } diff --git a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java index a2fafef89d5354e2cb02f5672810909950a57777..46011ababdadaafd72a8717911e49f6581ab5688 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java @@ -54,7 +54,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements public int fuel; protected final ContainerData dataAccess; // CraftBukkit start - add fields and methods - private int lastTick = MinecraftServer.currentTick; + //private int lastTick = MinecraftServer.currentTick; // Folia - region ticking - restore original timers public List transaction = new java.util.ArrayList(); private int maxStack = MAX_STACK; @@ -170,11 +170,10 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements ItemStack itemstack1 = (ItemStack) blockEntity.items.get(3); // CraftBukkit start - Use wall time instead of ticks for brewing - int elapsedTicks = MinecraftServer.currentTick - blockEntity.lastTick; - blockEntity.lastTick = MinecraftServer.currentTick; + // Folia - region ticking - restore original timers if (flag1) { - blockEntity.brewTime -= elapsedTicks; + --blockEntity.brewTime; // Folia - region ticking - restore original timers boolean flag2 = blockEntity.brewTime <= 0; // == -> <= // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java index 6c484f3f8f37a19992ebbfe30aba399cac21acfe..47c32e2639fdb6bac0df935822fff1a54c805aa4 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/CommandBlockEntity.java @@ -61,6 +61,13 @@ public class CommandBlockEntity extends BlockEntity { return new CommandSourceStack(this, Vec3.atCenterOf(CommandBlockEntity.this.worldPosition), new Vec2(0.0F, enumdirection.toYRot()), this.getLevel(), this.getLevel().paperConfig().commandBlocks.permissionsLevel, this.getName().getString(), this.getName(), this.getLevel().getServer(), (Entity) null); // Paper - configurable command block perm level } + // Folia start + @Override + public void threadCheck() { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel) CommandBlockEntity.this.level, CommandBlockEntity.this.worldPosition, "Asynchronous sendSystemMessage to a command block"); + } + // Folia end + @Override public boolean isValid() { return !CommandBlockEntity.this.isRemoved(); diff --git a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java index 73e532dc998e5701c1a73da846da3d3a79871b81..5e4cb998554791cdfc2e32ae7115f87e9e23feff 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/ConduitBlockEntity.java @@ -84,7 +84,7 @@ public class ConduitBlockEntity extends BlockEntity { public static void clientTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { ++blockEntity.tickCount; - long i = world.getGameTime(); + long i = world.getRedstoneGameTime(); // Folia - region threading List list = blockEntity.effectBlocks; if (i % 40L == 0L) { @@ -102,7 +102,7 @@ public class ConduitBlockEntity extends BlockEntity { public static void serverTick(Level world, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { ++blockEntity.tickCount; - long i = world.getGameTime(); + long i = world.getRedstoneGameTime(); // Folia - region threading List list = blockEntity.effectBlocks; if (i % 40L == 0L) { diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java index cab403efd471bb61835224eea4e99570d34dcaaa..1035a68aaeb9c50752a03cd72fe65c5eb7e8746c 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java @@ -49,7 +49,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen private static final int[][] CACHED_SLOTS = new int[54][]; private NonNullList items; public int cooldownTime; - private long tickedGameTime; + private long tickedGameTime = Long.MIN_VALUE; // Folia - region threading private Direction facing; // CraftBukkit start - add fields and methods @@ -82,6 +82,16 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } // CraftBukkit end + // Folia start - region threading + @Override + public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { + super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + if (this.tickedGameTime != Long.MIN_VALUE) { + this.tickedGameTime += fromRedstoneTimeOffset; + } + } + // Folia end - region threading + public HopperBlockEntity(BlockPos pos, BlockState state) { super(BlockEntityType.HOPPER, pos, state); this.items = NonNullList.withSize(5, ItemStack.EMPTY); @@ -141,7 +151,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen public static void pushItemsTick(Level world, BlockPos pos, BlockState state, HopperBlockEntity blockEntity) { --blockEntity.cooldownTime; - blockEntity.tickedGameTime = world.getGameTime(); + blockEntity.tickedGameTime = world.getRedstoneGameTime(); // Folia - region threading if (!blockEntity.isOnCooldown()) { blockEntity.setCooldown(0); // Spigot start @@ -237,12 +247,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } // Paper start - Perf: Optimize Hoppers - private static boolean skipPullModeEventFire; - private static boolean skipPushModeEventFire; - public static boolean skipHopperEvents; + // Folia - region threading - moved to RegionizedWorldData private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) { - skipPushModeEventFire = skipHopperEvents; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + worldData.skipPushModeEventFire = worldData.skipHopperEvents; // Folia - region threading boolean foundItem = false; for (int i = 0; i < hopper.getContainerSize(); ++i) { final ItemStack item = hopper.getItem(i); @@ -257,7 +266,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen // We only need to fire the event once to give protection plugins a chance to cancel this event // Because nothing uses getItem, every event call should end up the same result. - if (!skipPushModeEventFire) { + if (!worldData.skipPushModeEventFire) { // Folia - region threading movedItem = callPushMoveEvent(destination, movedItem, hopper); if (movedItem == null) { // cancelled origItemStack.setCount(originalItemCount); @@ -287,13 +296,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ItemStack movedItem = origItemStack; final int originalItemCount = origItemStack.getCount(); final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); container.setChanged(); // original logic always marks source inv as changed even if no move happens. movedItem.setCount(movedItemCount); - if (!skipPullModeEventFire) { + if (!worldData.skipPullModeEventFire) { // Folia - region threading movedItem = callPullMoveEvent(hopper, container, movedItem); if (movedItem == null) { // cancelled origItemStack.setCount(originalItemCount); @@ -313,9 +323,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); } - ignoreTileUpdates = true; + IGNORE_TILE_UPDATES.set(true); // Folia - region threading container.setItem(i, origItemStack); - ignoreTileUpdates = false; + IGNORE_TILE_UPDATES.set(false); // Folia - region threading container.setChanged(); return true; } @@ -330,12 +340,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen @Nullable private static ItemStack callPushMoveEvent(Container iinventory, ItemStack itemstack, HopperBlockEntity hopper) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading final Inventory destinationInventory = getInventory(iinventory); final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(hopper.getOwner(false).getInventory(), CraftItemStack.asCraftMirror(itemstack), destinationInventory, true); final boolean result = event.callEvent(); if (!event.calledGetItem && !event.calledSetItem) { - skipPushModeEventFire = true; + worldData.skipPushModeEventFire = true; // Folia - region threading } if (!result) { cooldownHopper(hopper); @@ -351,6 +362,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen @Nullable private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading final Inventory sourceInventory = getInventory(container); final Inventory destination = getInventory(hopper); @@ -358,7 +370,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, CraftItemStack.asCraftMirror(itemstack), destination, false); final boolean result = event.callEvent(); if (!event.calledGetItem && !event.calledSetItem) { - skipPullModeEventFire = true; + worldData.skipPullModeEventFire = true; // Folia - region threading } if (!result) { cooldownHopper(hopper); @@ -546,13 +558,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } public static boolean suckInItems(Level world, Hopper hopper) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading BlockPos blockposition = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0D, hopper.getLevelZ()); BlockState iblockdata = world.getBlockState(blockposition); Container iinventory = HopperBlockEntity.getSourceContainer(world, hopper, blockposition, iblockdata); if (iinventory != null) { Direction enumdirection = Direction.DOWN; - skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers + worldData.skipPullModeEventFire = worldData.skipHopperEvents; // Paper - Perf: Optimize Hoppers // Folia - region threading int[] aint = HopperBlockEntity.getSlots(iinventory, enumdirection); int i = aint.length; @@ -743,9 +756,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen stack = stack.split(to.getMaxStackSize()); } // Spigot end - ignoreTileUpdates = true; // Paper - Perf: Optimize Hoppers + IGNORE_TILE_UPDATES.set(Boolean.TRUE); // Paper - Perf: Optimize Hoppers // Folia - region threading to.setItem(slot, stack); - ignoreTileUpdates = false; // Paper - Perf: Optimize Hoppers + IGNORE_TILE_UPDATES.set(Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading stack = leftover; // Paper - Make hoppers respect inventory max stack size flag = true; } else if (HopperBlockEntity.canMergeItems(itemstack1, stack)) { diff --git a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java index a74732902c0494c67e6acf2fc04581ff9c46b832..f543be7f11511b57d510520a531cf39cd05ddafc 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java @@ -46,9 +46,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi // Paper end - Fix NPE in SculkBloomEvent world access public static void serverTick(Level world, BlockPos pos, BlockState state, SculkCatalystBlockEntity blockEntity) { - org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = blockEntity.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. + org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(blockEntity.getBlockPos()); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading blockEntity.catalystListener.getSculkSpreader().updateCursors(world, pos, world.getRandom(), true); - org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit + org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(null); // CraftBukkit // Folia - region threading } @Override diff --git a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java index 89df488afeffd0c060d2d0e7fae16daf978bd192..61a40071ea6d619bee6d876a193bb79e6d85df26 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java @@ -39,9 +39,12 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { public long age; private int teleportCooldown; @Nullable - public BlockPos exitPortal; + public volatile BlockPos exitPortal; // Folia - region threading - volatile public boolean exactTeleport; + private static final java.util.concurrent.atomic.AtomicLong SEARCHING_FOR_EXIT_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); // Folia - region threading + private Long searchingForExitId; // Folia - region threading + public TheEndGatewayBlockEntity(BlockPos pos, BlockState state) { super(BlockEntityType.END_GATEWAY, pos, state); } @@ -140,6 +143,103 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { } } + // Folia start - region threading + private void trySearchForExit(ServerLevel world, BlockPos fromPos) { + if (this.searchingForExitId != null) { + return; + } + this.searchingForExitId = Long.valueOf(SEARCHING_FOR_EXIT_ID_GENERATOR.getAndIncrement()); + int chunkX = fromPos.getX() >> 4; + int chunkZ = fromPos.getZ() >> 4; + world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( + net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, + chunkX, chunkZ, + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, + this.searchingForExitId + ); + + ca.spottedleaf.concurrentutil.completable.Completable complete = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + + complete.addWaiter((tpLoc, throwable) -> { + // create the exit portal + TheEndGatewayBlockEntity.LOGGER.debug("Creating portal at {}", tpLoc); + TheEndGatewayBlockEntity.spawnGatewayPortal(world, tpLoc, EndGatewayConfiguration.knownExit(fromPos, false)); + + // need to go onto the tick thread to avoid saving issues + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + world, chunkX, chunkZ, + () -> { + // update the exit portal location + TheEndGatewayBlockEntity.this.exitPortal = tpLoc; + + // remove ticket keeping the gateway loaded + world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( + net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, + chunkX, chunkZ, + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, + this.searchingForExitId + ); + TheEndGatewayBlockEntity.this.searchingForExitId = null; + } + ); + }); + + findOrCreateValidTeleportPosRegionThreading(world, fromPos, complete); + } + + public static boolean teleportRegionThreading(ServerLevel portalWorld, BlockPos portalPos, + net.minecraft.world.entity.Entity toTeleport, + TheEndGatewayBlockEntity portalTile, + Vec3 targetSpeed, + net.minecraft.world.level.portal.DimensionTransition.PostDimensionTransition post) { + // can we even teleport in this dimension? + if (portalTile.exitPortal == null && portalWorld.getTypeKey() != LevelStem.END) { + return false; + } + + // First, find the position we are trying to teleport to + BlockPos teleportPos = portalTile.exitPortal; + boolean isExactTeleport = portalTile.exactTeleport; + + if (teleportPos == null) { + portalTile.trySearchForExit(portalWorld, portalPos); + return false; + } + + if (isExactTeleport) { + // blind teleport + return toTeleport.teleportAsync( + portalWorld, Vec3.atCenterOf(teleportPos), null, null, targetSpeed, + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY, net.minecraft.world.entity.Entity.TELEPORT_FLAG_LOAD_CHUNK | net.minecraft.world.entity.Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, + (net.minecraft.world.entity.Entity teleportedEntity) -> { + if (post != null) { + post.onTransition(teleportedEntity); + } + } + ); + } else { + // we could hack around by first loading the chunks, then calling back to here and checking if the entity + // should be teleported, something something else... + // however, we know the target location cannot differ by one region section: so we can + // just teleport and adjust the position after + return toTeleport.teleportAsync( + portalWorld, Vec3.atCenterOf(teleportPos), null, null, targetSpeed, + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY, net.minecraft.world.entity.Entity.TELEPORT_FLAG_LOAD_CHUNK | net.minecraft.world.entity.Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, + (net.minecraft.world.entity.Entity teleportedEntity) -> { + // adjust to the final exit position + Vec3 adjusted = Vec3.atCenterOf(TheEndGatewayBlockEntity.findExitPosition(portalWorld, teleportPos)); + // teleportTo will adjust rider positions + teleportedEntity.teleportTo(adjusted.x, adjusted.y, adjusted.z); + + if (post != null) { + post.onTransition(teleportedEntity); + } + } + ); + } + } + // Folia end - region threading + @Nullable public Vec3 getPortalPosition(ServerLevel world, BlockPos pos) { BlockPos blockposition1; @@ -189,6 +289,124 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { return TheEndGatewayBlockEntity.findTallestBlock(world, blockposition1, 16, true); } + // Folia start - region threading + private static void findOrCreateValidTeleportPosRegionThreading(ServerLevel world, BlockPos pos, + ca.spottedleaf.concurrentutil.completable.Completable complete) { + ca.spottedleaf.concurrentutil.completable.Completable tentativeSelection = new ca.spottedleaf.concurrentutil.completable.Completable<>(); + + tentativeSelection.addWaiter((vec3d, throwable) -> { + LevelChunk chunk = TheEndGatewayBlockEntity.getChunk(world, vec3d); + BlockPos blockposition1 = TheEndGatewayBlockEntity.findValidSpawnInChunk(chunk); + if (blockposition1 == null) { + BlockPos blockposition2 = BlockPos.containing(vec3d.x + 0.5D, 75.0D, vec3d.z + 0.5D); + + TheEndGatewayBlockEntity.LOGGER.debug("Failed to find a suitable block to teleport to, spawning an island on {}", blockposition2); + world.registryAccess().registry(Registries.CONFIGURED_FEATURE).flatMap((iregistry) -> { + return iregistry.getHolder(EndFeatures.END_ISLAND); + }).ifPresent((holder_c) -> { + ((ConfiguredFeature) holder_c.value()).place(world, world.getChunkSource().getGenerator(), RandomSource.create(blockposition2.asLong()), blockposition2); + }); + blockposition1 = blockposition2; + } else { + TheEndGatewayBlockEntity.LOGGER.debug("Found suitable block to teleport to: {}", blockposition1); + } + + // Here, there is no guarantee the chunks in 1 radius are in this region due to the fact that we just chained + // possibly 16x chunk loads along an axis (findExitPortalXZPosTentativeRegionThreading) using the chunk queue + // (regioniser only guarantees at least 8 chunks along a single axis) + // so, we need to schedule for the next tick + int posX = blockposition1.getX(); + int posZ = blockposition1.getZ(); + int radius = 16; + + BlockPos finalBlockPosition1 = blockposition1; + world.moonrise$loadChunksAsync(blockposition1, radius, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (java.util.List chunks) -> { + // make sure chunks are kept loaded + for (net.minecraft.world.level.chunk.ChunkAccess access : chunks) { + world.chunkSource.addTicketAtLevel( + net.minecraft.server.level.TicketType.DELAYED, access.getPos(), + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, + net.minecraft.util.Unit.INSTANCE + ); + } + // now after the chunks are loaded, we can delay by one tick + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + world, posX >> 4, posZ >> 4, () -> { + // find final location + BlockPos tpLoc = TheEndGatewayBlockEntity.findTallestBlock(world, finalBlockPosition1, radius, true); + + // done + complete.complete(tpLoc); + } + ); + } + ); + }); + + // fire off chain + findExitPortalXZPosTentativeRegionThreading(world, pos, tentativeSelection); + } + + private static void findExitPortalXZPosTentativeRegionThreading(ServerLevel world, BlockPos pos, + ca.spottedleaf.concurrentutil.completable.Completable complete) { + Vec3 posDirFromOrigin = new Vec3(pos.getX(), 0.0D, pos.getZ()).normalize(); + Vec3 posDirExtruded = posDirFromOrigin.scale(1024.0D); + + class Vars { + int i = 16; + boolean mode = false; + Vec3 currPos = posDirExtruded; + } + Vars vars = new Vars(); + + Runnable handle = new Runnable() { + @Override + public void run() { + if (vars.mode != TheEndGatewayBlockEntity.isChunkEmpty(world, vars.currPos)) { + vars.i = 0; // fall back to completing + } + + // try to load next chunk + if (vars.i-- <= 0) { + if (vars.mode) { + complete.complete(vars.currPos); + return; + } + vars.mode = true; + vars.i = 16; + } + + vars.currPos = vars.currPos.add(posDirFromOrigin.scale(vars.mode ? 16.0 : -16.0)); + // schedule next iteration + world.moonrise$getChunkTaskScheduler().scheduleChunkLoad( + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(vars.currPos), + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(vars.currPos), + net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunk) -> { + this.run(); + } + ); + } + }; + + // kick off first chunk load + world.moonrise$getChunkTaskScheduler().scheduleChunkLoad( + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(posDirExtruded), + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(posDirExtruded), + net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + true, + ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, + (chunk) -> { + handle.run(); + } + ); + } + // Folia end - region threading + private static Vec3 findExitPortalXZPosTentative(ServerLevel world, BlockPos pos) { Vec3 vec3d = (new Vec3((double) pos.getX(), 0.0D, (double) pos.getZ())).normalize(); boolean flag = true; diff --git a/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java index 28e3b73507b988f7234cbf29c4024c88180d0aef..c8facee29ee08e0975528083f89b64f0b593957f 100644 --- a/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/entity/TickingBlockEntity.java @@ -10,4 +10,6 @@ public interface TickingBlockEntity { BlockPos getPos(); String getType(); + + BlockEntity getTileEntity(); // Folia - region threading } diff --git a/src/main/java/net/minecraft/world/level/block/grower/TreeGrower.java b/src/main/java/net/minecraft/world/level/block/grower/TreeGrower.java index 597599138f69c9ee05dc7657c51c25336337875e..686986d1b1cb12b6f957c050aae3de549f49993d 100644 --- a/src/main/java/net/minecraft/world/level/block/grower/TreeGrower.java +++ b/src/main/java/net/minecraft/world/level/block/grower/TreeGrower.java @@ -174,51 +174,53 @@ public final class TreeGrower { // CraftBukkit start private void setTreeType(Holder> holder) { ResourceKey> worldgentreeabstract = holder.unwrapKey().get(); + TreeType treeType; // Folia - region threading if (worldgentreeabstract == TreeFeatures.OAK || worldgentreeabstract == TreeFeatures.OAK_BEES_005) { - SaplingBlock.treeType = TreeType.TREE; + treeType = TreeType.TREE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.HUGE_RED_MUSHROOM) { - SaplingBlock.treeType = TreeType.RED_MUSHROOM; + treeType = TreeType.RED_MUSHROOM; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.HUGE_BROWN_MUSHROOM) { - SaplingBlock.treeType = TreeType.BROWN_MUSHROOM; + treeType = TreeType.BROWN_MUSHROOM; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE) { - SaplingBlock.treeType = TreeType.COCOA_TREE; + treeType = TreeType.COCOA_TREE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.JUNGLE_TREE_NO_VINE) { - SaplingBlock.treeType = TreeType.SMALL_JUNGLE; + treeType = TreeType.SMALL_JUNGLE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.PINE) { - SaplingBlock.treeType = TreeType.TALL_REDWOOD; + treeType = TreeType.TALL_REDWOOD; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.SPRUCE) { - SaplingBlock.treeType = TreeType.REDWOOD; + treeType = TreeType.REDWOOD; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.ACACIA) { - SaplingBlock.treeType = TreeType.ACACIA; + treeType = TreeType.ACACIA; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.BIRCH || worldgentreeabstract == TreeFeatures.BIRCH_BEES_005) { - SaplingBlock.treeType = TreeType.BIRCH; + treeType = TreeType.BIRCH; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.SUPER_BIRCH_BEES_0002) { - SaplingBlock.treeType = TreeType.TALL_BIRCH; + treeType = TreeType.TALL_BIRCH; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.SWAMP_OAK) { - SaplingBlock.treeType = TreeType.SWAMP; + treeType = TreeType.SWAMP; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.FANCY_OAK || worldgentreeabstract == TreeFeatures.FANCY_OAK_BEES_005) { - SaplingBlock.treeType = TreeType.BIG_TREE; + treeType = TreeType.BIG_TREE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.JUNGLE_BUSH) { - SaplingBlock.treeType = TreeType.JUNGLE_BUSH; + treeType = TreeType.JUNGLE_BUSH; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.DARK_OAK) { - SaplingBlock.treeType = TreeType.DARK_OAK; + treeType = TreeType.DARK_OAK; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.MEGA_SPRUCE) { - SaplingBlock.treeType = TreeType.MEGA_REDWOOD; + treeType = TreeType.MEGA_REDWOOD; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.MEGA_PINE) { - SaplingBlock.treeType = TreeType.MEGA_PINE; + treeType = TreeType.MEGA_PINE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.MEGA_JUNGLE_TREE) { - SaplingBlock.treeType = TreeType.JUNGLE; + treeType = TreeType.JUNGLE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.AZALEA_TREE) { - SaplingBlock.treeType = TreeType.AZALEA; + treeType = TreeType.AZALEA; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.MANGROVE) { - SaplingBlock.treeType = TreeType.MANGROVE; + treeType = TreeType.MANGROVE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.TALL_MANGROVE) { - SaplingBlock.treeType = TreeType.TALL_MANGROVE; + treeType = TreeType.TALL_MANGROVE; // Folia - region threading } else if (worldgentreeabstract == TreeFeatures.CHERRY || worldgentreeabstract == TreeFeatures.CHERRY_BEES_005) { - SaplingBlock.treeType = TreeType.CHERRY; + treeType = TreeType.CHERRY; // Folia - region threading } else { throw new IllegalArgumentException("Unknown tree generator " + worldgentreeabstract); } + SaplingBlock.treeTypeRT.set(treeType); // Folia - region threading } // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java index e0c62227b279a5fe0f3868fbf9ce8c78c515a09c..bf9a6f40998144ba7dbdfafc8dbdbb84168d1edb 100644 --- a/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java +++ b/src/main/java/net/minecraft/world/level/block/piston/PistonBaseBlock.java @@ -153,7 +153,7 @@ public class PistonBaseBlock extends DirectionalBlock { if (tileentity instanceof PistonMovingBlockEntity) { PistonMovingBlockEntity tileentitypiston = (PistonMovingBlockEntity) tileentity; - if (tileentitypiston.isExtending() && (tileentitypiston.getProgress(0.0F) < 0.5F || world.getGameTime() == tileentitypiston.getLastTicked() || ((ServerLevel) world).isHandlingTick())) { + if (tileentitypiston.isExtending() && (tileentitypiston.getProgress(0.0F) < 0.5F || world.getRedstoneGameTime() == tileentitypiston.getLastTicked() || ((ServerLevel) world).isHandlingTick())) { // Folia - region threading b0 = 2; } } diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java index b35f476e26a020cf75e53a5eb488717d996a6935..58b570cfdd52b414752b5a117946d59031eafe94 100644 --- a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +++ b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java @@ -40,9 +40,19 @@ public class PistonMovingBlockEntity extends BlockEntity { private static final ThreadLocal NOCLIP = ThreadLocal.withInitial(() -> null); private float progress; private float progressO; - private long lastTicked; + private long lastTicked = Long.MIN_VALUE; // Folia - region threading private int deathTicks; + // Folia start - region threading + @Override + public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { + super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + if (this.lastTicked != Long.MIN_VALUE) { + this.lastTicked += fromRedstoneTimeOffset; + } + } + // Folia end - region threading + public PistonMovingBlockEntity(BlockPos pos, BlockState state) { super(BlockEntityType.PISTON, pos, state); } @@ -149,8 +159,8 @@ public class PistonMovingBlockEntity extends BlockEntity { entity.setDeltaMovement(e, g, h); // Paper - EAR items stuck in in slime pushed by a piston - entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10); - entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10); + entity.activatedTick = Math.max(entity.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading + entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading // Paper end break; } @@ -290,7 +300,7 @@ public class PistonMovingBlockEntity extends BlockEntity { } public static void tick(Level world, BlockPos pos, BlockState state, PistonMovingBlockEntity blockEntity) { - blockEntity.lastTicked = world.getGameTime(); + blockEntity.lastTicked = world.getRedstoneGameTime(); // Folia - region threading blockEntity.progressO = blockEntity.progress; if (blockEntity.progressO >= 1.0F) { if (world.isClientSide && blockEntity.deathTicks < 5) { diff --git a/src/main/java/net/minecraft/world/level/border/WorldBorder.java b/src/main/java/net/minecraft/world/level/border/WorldBorder.java index 04e62c54f224f7949fde9ceded208e700db55aa1..298b00641580dfab39ac9139d1ea25cec178a0d4 100644 --- a/src/main/java/net/minecraft/world/level/border/WorldBorder.java +++ b/src/main/java/net/minecraft/world/level/border/WorldBorder.java @@ -34,6 +34,8 @@ public class WorldBorder { public WorldBorder() {} + // Folia - region threading - TODO make this shit thread-safe + public boolean isWithinBounds(BlockPos pos) { return this.isWithinBounds((double) pos.getX(), (double) pos.getZ()); } @@ -47,14 +49,12 @@ public class WorldBorder { } // Paper start - Bound treasure maps to world border - private final BlockPos.MutableBlockPos mutPos = new BlockPos.MutableBlockPos(); + private static final ThreadLocal mutPos = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Folia - region threading public boolean isBlockInBounds(int chunkX, int chunkZ) { - this.mutPos.set(chunkX, 64, chunkZ); - return this.isWithinBounds(this.mutPos); + return this.isWithinBounds(mutPos.get().set(chunkX, 64, chunkZ)); // Folia - region threading } public boolean isChunkInBounds(int chunkX, int chunkZ) { - this.mutPos.set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15); - return this.isWithinBounds(this.mutPos); + return this.isWithinBounds(mutPos.get().set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15)); // Folia - region threading } // Paper end - Bound treasure maps to world border diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java index 488938c32a48437721a71d294c77468f00c035b9..cead18e4f8ea278b999fa0e1aff58580a6604645 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java +++ b/src/main/java/net/minecraft/world/level/chunk/ChunkGenerator.java @@ -325,7 +325,7 @@ public abstract class ChunkGenerator { } private static boolean tryAddReference(StructureManager structureAccessor, StructureStart start) { - if (start.canBeReferenced()) { + if (start.tryReference()) { // Folia - region threading structureAccessor.addReference(start); return true; } else { 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 7c11853c5090fbc4fa5b3e73a69acf166158fdec..27e68ed3a508f16f53422c5aa1631b804c8eff9e 100644 --- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java +++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java @@ -60,6 +60,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p @Override public void tick() {} + // Folia start - region threading + @Override + public BlockEntity getTileEntity() { + return null; + } + // Folia end - region threading + @Override public boolean isRemoved() { return true; @@ -352,6 +359,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p @Nullable public BlockState setBlockState(BlockPos blockposition, BlockState iblockdata, boolean flag, boolean doPlace) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, blockposition, "Updating block asynchronously"); // Folia - region threading // CraftBukkit end int i = blockposition.getY(); LevelChunkSection chunksection = this.getSection(this.getSectionIndex(i)); @@ -392,7 +400,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p boolean flag3 = iblockdata1.hasBlockEntity(); - if (!this.level.isClientSide && !this.level.isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent + if (!this.level.isClientSide && !this.level.getCurrentWorldData().isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading iblockdata1.onRemove(this.level, blockposition, iblockdata, flag); } else if (!iblockdata1.is(block) && flag3) { this.removeBlockEntity(blockposition); @@ -402,7 +410,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p return null; } else { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. - if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { + if (!this.level.isClientSide && doPlace && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // Folia - region threading iblockdata.onPlace(this.level, blockposition, iblockdata1, flag); } @@ -453,7 +461,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p @Nullable public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) { // CraftBukkit start - BlockEntity tileentity = this.level.capturedTileEntities.get(pos); + BlockEntity tileentity = this.level.getCurrentWorldData().capturedTileEntities.get(pos); // Folia - region threading if (tileentity == null) { tileentity = (BlockEntity) this.blockEntities.get(pos); } @@ -676,13 +684,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p org.bukkit.World world = this.level.getWorld(); if (world != null) { - this.level.populating = true; + this.level.getCurrentWorldData().populating = true; // Folia - region threading try { for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) { populator.populate(world, random, bukkitChunk); } } finally { - this.level.populating = false; + this.level.getCurrentWorldData().populating = false; // Folia - region threading } } server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk)); @@ -709,7 +717,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p @Override public boolean isUnsaved() { // Paper start - rewrite chunk system - final long gameTime = this.level.getGameTime(); + final long gameTime = this.level.getRedstoneGameTime(); // Folia - region threading if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime) || ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) { return true; @@ -980,6 +988,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p this.ticker = wrapped; } + // Folia start - region threading + @Override + public BlockEntity getTileEntity() { + return this.ticker == null ? null : this.ticker.getTileEntity(); + } + // Folia end - region threading + @Override public void tick() { this.ticker.tick(); @@ -1016,6 +1031,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p this.ticker = blockentityticker; } + // Folia start - region threading + @Override + public BlockEntity getTileEntity() { + return this.blockEntity; + } + // Folia end - region threading + @Override public void tick() { if (!this.blockEntity.isRemoved() && this.blockEntity.hasLevel()) { diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java index 4bd048387651250135f963303c78c17f8473cfee..6ec9226b686e7cd9da966edc6b51d4f48dfe4d65 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java @@ -550,7 +550,7 @@ public class ChunkSerializer { } private static void saveTicks(ServerLevel world, CompoundTag nbt, ChunkAccess.TicksToSave tickSchedulers) { - long i = world.getLevelData().getGameTime(); + long i = world.getRedstoneGameTime(); // Folia - region threading nbt.put("block_ticks", tickSchedulers.blocks().save(i, (block) -> { return BuiltInRegistries.BLOCK.getKey(block).toString(); diff --git a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java index 18a1b4325cac81b040596071dab99ef9bf6f3142..f68907a175329a4a928db8085939a6c5836bd94e 100644 --- a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java +++ b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java @@ -76,7 +76,7 @@ public class EndDragonFight { private static final Component DEFAULT_BOSS_EVENT_NAME = Component.translatable("entity.minecraft.ender_dragon"); // Paper - ensure reset EnderDragon boss event name public final ServerBossEvent dragonEvent; public final ServerLevel level; - private final BlockPos origin; + public final BlockPos origin; // Folia - region threading public final ObjectArrayList gateways; private final BlockPattern exitPortalPattern; private int ticksSinceDragonSeen; @@ -152,7 +152,7 @@ public class EndDragonFight { if (!this.dragonEvent.getPlayers().isEmpty()) { this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE); - boolean flag = this.isArenaLoaded(); + boolean flag = this.isArenaLoaded(); if (!flag) { return; } // Folia - region threading - don't tick if we don't own the entire region if (this.needsStateScanning && flag) { this.scanState(); @@ -201,6 +201,12 @@ public class EndDragonFight { } List list = this.level.getDragons(); + // Folia start - region threading + // we do not want to deal with any dragons NOT nearby + list.removeIf((dragon) -> { + return !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(dragon); + }); + // Folia end - region threading if (list.isEmpty()) { this.dragonKilled = true; @@ -347,9 +353,8 @@ public class EndDragonFight { for (int i = -8 + chunkcoordintpair.x; i <= 8 + chunkcoordintpair.x; ++i) { for (int j = 8 + chunkcoordintpair.z; j <= 8 + chunkcoordintpair.z; ++j) { - ChunkAccess ichunkaccess = this.level.getChunk(i, j, ChunkStatus.FULL, false); - - if (!(ichunkaccess instanceof LevelChunk)) { + ChunkAccess ichunkaccess = this.level.getChunkIfLoaded(i, j); // Folia - region threading + if (!(ichunkaccess instanceof LevelChunk) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, i, j, this.level.regioniser.regionSectionChunkSize)) { // Folia - region threading return false; } @@ -536,6 +541,11 @@ public class EndDragonFight { } public void onCrystalDestroyed(EndCrystal enderCrystal, DamageSource source) { + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) { + return; + } + // Folia end - region threading if (this.respawnStage != null && this.respawnCrystals.contains(enderCrystal)) { EndDragonFight.LOGGER.debug("Aborting respawn sequence"); this.respawnStage = null; @@ -564,7 +574,7 @@ public class EndDragonFight { public boolean tryRespawn(@Nullable BlockPos placedEndCrystalPos) { // placedEndCrystalPos is null if the tryRespawn() call was not caused by a placed end crystal // Paper end - Perf: Do crystal-portal proximity check before entity lookup - if (this.dragonKilled && this.respawnStage == null) { + if (this.dragonKilled && this.respawnStage == null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) { // Folia - region threading BlockPos blockposition = this.portalLocation; if (blockposition == null) { diff --git a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java index 1741360aa3f2409b1a8ddf1d4602ffe57651a586..0788c67a09e67b9775c0d073efc72547f90df94a 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java +++ b/src/main/java/net/minecraft/world/level/levelgen/PatrolSpawner.java @@ -18,7 +18,7 @@ import net.minecraft.world.level.block.state.BlockState; public class PatrolSpawner implements CustomSpawner { - private int nextTick; + //private int nextTick; // Folia - region threading public PatrolSpawner() {} @@ -31,15 +31,16 @@ public class PatrolSpawner implements CustomSpawner { return 0; } else { RandomSource randomsource = world.random; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading // Paper start - Pillager patrol spawn settings and per player options // Random player selection moved up for per player spawning and configuration - int j = world.players().size(); + int j = world.getLocalPlayers().size(); // Folia - region threading if (j < 1) { return 0; } - net.minecraft.server.level.ServerPlayer entityhuman = world.players().get(randomsource.nextInt(j)); + net.minecraft.server.level.ServerPlayer entityhuman = world.getLocalPlayers().get(randomsource.nextInt(j)); // Folia - region threading if (entityhuman.isSpectator()) { return 0; } @@ -49,8 +50,8 @@ public class PatrolSpawner implements CustomSpawner { --entityhuman.patrolSpawnDelay; patrolSpawnDelay = entityhuman.patrolSpawnDelay; } else { - this.nextTick--; - patrolSpawnDelay = this.nextTick; + worldData.patrolSpawnerNextTick--; // Folia - region threading + patrolSpawnDelay = worldData.patrolSpawnerNextTick; // Folia - region threading } if (patrolSpawnDelay > 0) { @@ -65,7 +66,7 @@ public class PatrolSpawner implements CustomSpawner { if (world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) { entityhuman.patrolSpawnDelay += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); } else { - this.nextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); + worldData.patrolSpawnerNextTick += world.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomsource.nextInt(1200); // Folia - region threading } if (days >= world.paperConfig().entities.behavior.pillagerPatrols.start.day && world.isDay()) { diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java index 1b1b475ca27e799e251d6f8a8c9fe1a4fd8bae83..3085658976345d095e7c38a0edab42cb92fe5d03 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java +++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java @@ -21,7 +21,7 @@ import net.minecraft.world.level.material.FluidState; public class PhantomSpawner implements CustomSpawner { - private int nextTick; + //private int nextTick; // Folia - region threading public PhantomSpawner() {} @@ -39,20 +39,22 @@ public class PhantomSpawner implements CustomSpawner { // Paper end - Ability to control player's insomnia and phantoms RandomSource randomsource = world.random; - --this.nextTick; - if (this.nextTick > 0) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + + --worldData.phantomSpawnerNextTick; // Folia - region threading + if (worldData.phantomSpawnerNextTick > 0) { // Folia - region threading return 0; } else { // Paper start - Ability to control player's insomnia and phantoms int spawnAttemptMinSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds; int spawnAttemptMaxSeconds = world.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds; - this.nextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; + worldData.phantomSpawnerNextTick += (spawnAttemptMinSeconds + randomsource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; // Folia - region threading // Paper end - Ability to control player's insomnia and phantoms if (world.getSkyDarken() < 5 && world.dimensionType().hasSkyLight()) { return 0; } else { int i = 0; - Iterator iterator = world.players().iterator(); + Iterator iterator = world.getLocalPlayers().iterator(); // Folia - region threading while (iterator.hasNext()) { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); diff --git a/src/main/java/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java b/src/main/java/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java index 0bc659a8427b89b5e3211220c55b52eec6a20494..bcd7b23656bf77e9eca1bc194f9af360742b65be 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java +++ b/src/main/java/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java @@ -60,11 +60,7 @@ public class EndPlatformFeature extends Feature { return; } - org.bukkit.World bworld = worldaccess.getLevel().getWorld(); - PortalCreateEvent portalEvent = new PortalCreateEvent((List) (List) blockList.getList(), bworld, entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM); - - worldaccess.getLevel().getCraftServer().getPluginManager().callEvent(portalEvent); - if (!portalEvent.isCancelled()) { + if (true) { // Folia - region threading blockList.updateList(); } // CraftBukkit end diff --git a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java index c92a2c5bba1525eff39d9a3cad70fdacd426e8cb..04274f48627c40d3ac722e3ec595182d6d44b167 100644 --- a/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java +++ b/src/main/java/net/minecraft/world/level/levelgen/structure/StructureStart.java @@ -28,7 +28,7 @@ public final class StructureStart { private final Structure structure; private final PiecesContainer pieceContainer; private final ChunkPos chunkPos; - private int references; + private final java.util.concurrent.atomic.AtomicInteger references; // Folia - region threading @Nullable private volatile BoundingBox cachedBoundingBox; @@ -41,7 +41,7 @@ public final class StructureStart { public StructureStart(Structure structure, ChunkPos pos, int references, PiecesContainer children) { this.structure = structure; this.chunkPos = pos; - this.references = references; + this.references = new java.util.concurrent.atomic.AtomicInteger(references); // Folia - region threading this.pieceContainer = children; } @@ -137,7 +137,7 @@ public final class StructureStart { nbttagcompound.putString("id", context.registryAccess().registryOrThrow(Registries.STRUCTURE).getKey(this.structure).toString()); nbttagcompound.putInt("ChunkX", chunkPos.x); nbttagcompound.putInt("ChunkZ", chunkPos.z); - nbttagcompound.putInt("references", this.references); + nbttagcompound.putInt("references", this.references.get()); // Folia - region threading nbttagcompound.put("Children", this.pieceContainer.save(context)); return nbttagcompound; } else { @@ -155,15 +155,29 @@ public final class StructureStart { } public boolean canBeReferenced() { - return this.references < this.getMaxReferences(); + throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading } + // Folia start - region threading + public boolean tryReference() { + for (int curr = this.references.get();;) { + if (curr >= this.getMaxReferences()) { + return false; + } + + if (curr == (curr = this.references.compareAndExchange(curr, curr + 1))) { + return true; + } // else: try again + } + } + // Folia end - region threading + public void addReference() { - ++this.references; + throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading } public int getReferences() { - return this.references; + return this.references.get(); // Folia - region threading } protected int getMaxReferences() { diff --git a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java index 106af2b2c7ff72c7549975aef75cdcff8d9a7d97..2c9640001dba727981ad336eefade6d438eeb0ac 100644 --- a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +++ b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java @@ -48,6 +48,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { } private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates entry) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((net.minecraft.server.level.ServerLevel)this.level, pos, "Adding block without owning region"); // Folia - region threading boolean bl = this.count > 0; boolean bl2 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; this.count++; diff --git a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java index 4cbb943b702baaeb8bfd2b558cc848e719cf095d..6869144a2c1bb37d15f3fec622497b8d20ab2c68 100644 --- a/src/main/java/net/minecraft/world/level/saveddata/SavedData.java +++ b/src/main/java/net/minecraft/world/level/saveddata/SavedData.java @@ -14,7 +14,7 @@ import org.slf4j.Logger; public abstract class SavedData { private static final Logger LOGGER = LogUtils.getLogger(); - private boolean dirty; + private volatile boolean dirty; // Folia - make map data thread-safe public abstract CompoundTag save(CompoundTag nbt, HolderLookup.Provider registryLookup); @@ -39,6 +39,7 @@ public abstract class SavedData { public java.util.concurrent.CompletableFuture save(File file, HolderLookup.Provider registryLookup, @org.jetbrains.annotations.Nullable java.util.concurrent.ExecutorService ioExecutor) { // Paper end - Write SavedData IO async if (this.isDirty()) { + this.setDirty(false); // Folia - make map data thread-safe - move before save, so that any changes after are not lost CompoundTag compoundTag = new CompoundTag(); compoundTag.put("data", this.save(new CompoundTag(), registryLookup)); NbtUtils.addCurrentDataVersion(compoundTag); @@ -51,7 +52,7 @@ public abstract class SavedData { } }; // Paper - Write SavedData IO async - this.setDirty(false); + // Folia - make map data thread-safe - move before save, so that any changes after are not lost // Paper start - Write SavedData IO async if (ioExecutor == null) { return java.util.concurrent.CompletableFuture.runAsync(writeRunnable); // No executor, just use common pool diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java index 763b315b1d761bc3bd82d9b847ed3f64fd5ce991..7a044d337df8a14150a695d539a8e5c8799c4032 100644 --- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java +++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapIndex.java @@ -34,17 +34,21 @@ public class MapIndex extends SavedData { @Override public CompoundTag save(CompoundTag nbt, HolderLookup.Provider registryLookup) { + synchronized (this.usedAuxIds) { // Folia - make map data thread-safe for (Entry entry : this.usedAuxIds.object2IntEntrySet()) { nbt.putInt(entry.getKey(), entry.getIntValue()); } + } // Folia - make map data thread-safe return nbt; } public MapId getFreeAuxValueForMap() { + synchronized (this.usedAuxIds) { // Folia - make map data thread-safe int i = this.usedAuxIds.getInt("map") + 1; this.usedAuxIds.put("map", i); this.setDirty(); return new MapId(i); + } // Folia - make map data thread-safe } } diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java index 17f33c83c6033564d6bf4fbd388b0b847c68adb3..684a0a4c7583da882687e010e2f2e051804b6602 100644 --- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java @@ -212,7 +212,7 @@ public class MapItemSavedData extends SavedData { } @Override - public CompoundTag save(CompoundTag nbt, HolderLookup.Provider registryLookup) { + public synchronized CompoundTag save(CompoundTag nbt, HolderLookup.Provider registryLookup) { // Folia - make map data thread-safe DataResult dataresult = ResourceLocation.CODEC.encodeStart(NbtOps.INSTANCE, this.dimension.location()); // CraftBukkit - decompile error Logger logger = MapItemSavedData.LOGGER; @@ -262,7 +262,7 @@ public class MapItemSavedData extends SavedData { return nbt; } - public MapItemSavedData locked() { + public synchronized MapItemSavedData locked() { // Folia - make map data thread-safe MapItemSavedData worldmap = new MapItemSavedData(this.centerX, this.centerZ, this.scale, this.trackingPosition, this.unlimitedTracking, true, this.dimension); worldmap.bannerMarkers.putAll(this.bannerMarkers); @@ -273,7 +273,7 @@ public class MapItemSavedData extends SavedData { return worldmap; } - public MapItemSavedData scaled() { + public synchronized MapItemSavedData scaled() { // Folia - make map data thread-safe return MapItemSavedData.createFresh((double) this.centerX, (double) this.centerZ, (byte) Mth.clamp(this.scale + 1, 0, 4), this.trackingPosition, this.unlimitedTracking, this.dimension); } @@ -285,7 +285,8 @@ public class MapItemSavedData extends SavedData { }; } - public void tickCarriedBy(Player player, ItemStack stack) { + public synchronized void tickCarriedBy(Player player, ItemStack stack) { // Folia - make map data thread-safe + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(player, "Ticking map player in incorrect region"); // Folia - region threading if (!this.carriedByPlayers.containsKey(player)) { MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = new MapItemSavedData.HoldingPlayer(player); @@ -379,7 +380,7 @@ public class MapItemSavedData extends SavedData { rotation += rotation < 0.0D ? -8.0D : 8.0D; b2 = (byte) ((int) (rotation * 16.0D / 360.0D)); if (this.dimension == Level.NETHER && world != null) { - int j = (int) (world.getLevelData().getDayTime() / 10L); + int j = (int) (world.getLevelData().getDayTime() / 10L); // Folia - region threading - TODO b2 = (byte) (j * j * 34187121 + j * 121 >> 15 & 15); } @@ -438,14 +439,14 @@ public class MapItemSavedData extends SavedData { } @Nullable - public Packet getUpdatePacket(MapId mapId, Player player) { + public synchronized Packet getUpdatePacket(MapId mapId, Player player) { // Folia - make map data thread-safe MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = (MapItemSavedData.HoldingPlayer) this.carriedByPlayers.get(player); return worldmap_worldmaphumantracker == null ? null : worldmap_worldmaphumantracker.nextUpdatePacket(mapId); } - public void setColorsDirty(int x, int z) { - this.setDirty(); + public synchronized void setColorsDirty(int x, int z) { // Folia - make map data thread-safe + // Folia - make dirty only after updating data - moved down Iterator iterator = this.carriedBy.iterator(); while (iterator.hasNext()) { @@ -453,15 +454,16 @@ public class MapItemSavedData extends SavedData { worldmap_worldmaphumantracker.markColorsDirty(x, z); } - + this.setDirty(); // Folia - make dirty only after updating data - moved from above } - public void setDecorationsDirty() { - this.setDirty(); + public synchronized void setDecorationsDirty() { // Folia - make map data thread-safe + // Folia - make dirty only after updating data - moved down this.carriedBy.forEach(MapItemSavedData.HoldingPlayer::markDecorationsDirty); + this.setDirty(); // Folia - make dirty only after updating data - moved from above } - public MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { + public synchronized MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { // Folia - make map data thread-safe MapItemSavedData.HoldingPlayer worldmap_worldmaphumantracker = (MapItemSavedData.HoldingPlayer) this.carriedByPlayers.get(player); if (worldmap_worldmaphumantracker == null) { @@ -473,7 +475,7 @@ public class MapItemSavedData extends SavedData { return worldmap_worldmaphumantracker; } - public boolean toggleBanner(LevelAccessor world, BlockPos pos) { + public synchronized boolean toggleBanner(LevelAccessor world, BlockPos pos) { // Folia - make map data thread-safe double d0 = (double) pos.getX() + 0.5D; double d1 = (double) pos.getZ() + 0.5D; int i = 1 << this.scale; @@ -482,7 +484,7 @@ public class MapItemSavedData extends SavedData { boolean flag = true; if (d2 >= -63.0D && d3 >= -63.0D && d2 <= 63.0D && d3 <= 63.0D) { - MapBanner mapiconbanner = MapBanner.fromWorld(world, pos); + MapBanner mapiconbanner = world.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world.getMinecraftWorld(), pos) ? null : MapBanner.fromWorld(world, pos); // Folia - make map data thread-safe - don't sync load or read data we do not own if (mapiconbanner == null) { return false; @@ -503,7 +505,7 @@ public class MapItemSavedData extends SavedData { return false; } - public void checkBanners(BlockGetter world, int x, int z) { + public synchronized void checkBanners(BlockGetter world, int x, int z) { // Folia - make map data thread-safe Iterator iterator = this.bannerMarkers.values().iterator(); while (iterator.hasNext()) { @@ -525,12 +527,12 @@ public class MapItemSavedData extends SavedData { return this.bannerMarkers.values(); } - public void removedFromFrame(BlockPos pos, int id) { + public synchronized void removedFromFrame(BlockPos pos, int id) { // Folia - make map data thread-safe this.removeDecoration(MapItemSavedData.getFrameKey(id)); this.frameMarkers.remove(MapFrame.frameId(pos)); } - public boolean updateColor(int x, int z, byte color) { + public synchronized boolean updateColor(int x, int z, byte color) { // Folia - make map data thread-safe byte b1 = this.colors[x + z * 128]; if (b1 != color) { @@ -541,12 +543,12 @@ public class MapItemSavedData extends SavedData { } } - public void setColor(int x, int z, byte color) { + public synchronized void setColor(int x, int z, byte color) { // Folia - make map data thread-safe this.colors[x + z * 128] = color; this.setColorsDirty(x, z); } - public boolean isExplorationMap() { + public synchronized boolean isExplorationMap() { // Folia - make map data thread-safe Iterator iterator = this.decorations.values().iterator(); MapDecoration mapicon; @@ -581,7 +583,7 @@ public class MapItemSavedData extends SavedData { return this.decorations.values(); } - public boolean isTrackedCountOverLimit(int decorationCount) { + public synchronized boolean isTrackedCountOverLimit(int decorationCount) { // Folia - make map data thread-safe return this.trackedDecorationCount >= decorationCount; } @@ -729,11 +731,13 @@ public class MapItemSavedData extends SavedData { } public void applyToMap(MapItemSavedData mapState) { + synchronized (mapState) { // Folia - make map data thread-safe for (int i = 0; i < this.width; ++i) { for (int j = 0; j < this.height; ++j) { mapState.setColor(this.startX + i, this.startY + j, this.mapColors[i + j * this.width]); } } + } // Folia - make map data thread-safe } } diff --git a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java index 6e23e69abd56eeda3b52a22019e1b74ae10682e7..2c086f1d4cbfc29a759a77e5c5f9f39d177e81ba 100644 --- a/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java +++ b/src/main/java/net/minecraft/world/level/storage/DimensionDataStorage.java @@ -43,6 +43,7 @@ public class DimensionDataStorage implements java.io.Closeable { // Paper - Writ } public T computeIfAbsent(SavedData.Factory type, String id) { + synchronized (this.cache) { // Folia - make map data thread-safe T savedData = this.get(type, id); if (savedData != null) { return savedData; @@ -51,10 +52,12 @@ public class DimensionDataStorage implements java.io.Closeable { // Paper - Writ this.set(id, savedData2); return savedData2; } + } // Folia - make map data thread-safe } @Nullable public T get(SavedData.Factory type, String id) { + synchronized (this.cache) { // Folia - make map data thread-safe SavedData savedData = this.cache.get(id); if (savedData == null && !this.cache.containsKey(id)) { savedData = this.readSavedData(type.deserializer(), type.type(), id); @@ -62,6 +65,7 @@ public class DimensionDataStorage implements java.io.Closeable { // Paper - Writ } return (T)savedData; + } // Folia - make map data thread-safe } @Nullable @@ -80,7 +84,9 @@ public class DimensionDataStorage implements java.io.Closeable { // Paper - Writ } public void set(String id, SavedData state) { + synchronized (this.cache) { // Folia - make map data thread-safe this.cache.put(id, state); + } // Folia - make map data thread-safe } public CompoundTag readTagFromDisk(String id, DataFixTypes dataFixTypes, int currentSaveVersion) throws IOException { diff --git a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java index c42c0d1e4da30aa15f32d4ca524aeabd26fc50cf..e328d3d22d19b5dd1507c40119801fcfc3289e67 100644 --- a/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java +++ b/src/main/java/net/minecraft/world/ticks/LevelChunkTicks.java @@ -49,6 +49,21 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon this.dirty = false; } // Paper end - rewrite chunk system + // Folia start - region threading + public void offsetTicks(final long offset) { + if (offset == 0 || this.tickQueue.isEmpty()) { + return; + } + final ScheduledTick[] queue = this.tickQueue.toArray(new ScheduledTick[0]); + this.tickQueue.clear(); + for (final ScheduledTick entry : queue) { + final ScheduledTick newEntry = new ScheduledTick<>( + entry.type(), entry.pos(), entry.triggerTick() + offset, entry.subTickOrder() + ); + this.tickQueue.add(newEntry); + } + } + // Folia end - region threading public LevelChunkTicks() { } diff --git a/src/main/java/net/minecraft/world/ticks/LevelTicks.java b/src/main/java/net/minecraft/world/ticks/LevelTicks.java index 7a69564572357a7acc043e35b9c113beeb738951..ea161048b68de3c8cdcba5f4cb66eb1531364b2e 100644 --- a/src/main/java/net/minecraft/world/ticks/LevelTicks.java +++ b/src/main/java/net/minecraft/world/ticks/LevelTicks.java @@ -39,12 +39,69 @@ public class LevelTicks implements LevelTickAccess { private final List> alreadyRunThisTick = new ArrayList<>(); private final Set> toRunThisTickSet = new ObjectOpenCustomHashSet<>(ScheduledTick.UNIQUE_TICK_HASH); private final BiConsumer, ScheduledTick> chunkScheduleUpdater = (chunkTickScheduler, tick) -> { - if (tick.equals(chunkTickScheduler.peek())) { - this.updateContainerScheduling(tick); + if (tick.equals(chunkTickScheduler.peek())) { // Folia - diff on change + this.updateContainerScheduling(tick); // Folia - diff on change } }; - public LevelTicks(LongPredicate tickingFutureReadyPredicate, Supplier profilerGetter) { + // Folia start - region threading + public final net.minecraft.server.level.ServerLevel world; + public final boolean isBlock; + + public void merge(final LevelTicks into, final long tickOffset) { + // note: containersToTick, toRunThisTick, alreadyRunThisTick, toRunThisTickSet + // are all transient state, only ever non-empty during tick. But merging regions occurs while there + // is no tick happening, so we assume they are empty. + for (final java.util.Iterator>> iterator = + ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry> entry = iterator.next(); + final LevelChunkTicks tickContainer = entry.getValue(); + tickContainer.offsetTicks(tickOffset); + into.allContainers.put(entry.getLongKey(), tickContainer); + } + for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2LongMap.Entry entry = iterator.next(); + into.nextTickForContainer.put(entry.getLongKey(), entry.getLongValue() + tickOffset); + } + } + + public void split(final int chunkToRegionShift, + final it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap> regionToData) { + for (final java.util.Iterator>> iterator = + ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2ObjectMap.Entry> entry = iterator.next(); + + final long chunkKey = entry.getLongKey(); + final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey); + final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey); + + final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey( + chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift + ); + // Should always be non-null, since containers are removed on unload. + regionToData.get(regionSectionKey).allContainers.put(chunkKey, entry.getValue()); + } + for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); + iterator.hasNext();) { + final Long2LongMap.Entry entry = iterator.next(); + final long chunkKey = entry.getLongKey(); + final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey); + final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey); + + final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey( + chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift + ); + + // Should always be non-null, since containers are removed on unload. + regionToData.get(regionSectionKey).nextTickForContainer.put(chunkKey, entry.getLongValue()); + } + } + // Folia end - region threading + + public LevelTicks(LongPredicate tickingFutureReadyPredicate, Supplier profilerGetter, net.minecraft.server.level.ServerLevel world, boolean isBlock) { this.world = world; this.isBlock = isBlock; // Folia - add world and isBlock this.tickCheck = tickingFutureReadyPredicate; this.profiler = profilerGetter; } @@ -57,7 +114,17 @@ public class LevelTicks implements LevelTickAccess { this.nextTickForContainer.put(l, scheduledTick.triggerTick()); } - scheduler.setOnTickAdded(this.chunkScheduleUpdater); + // Folia start - region threading + final boolean isBlock = this.isBlock; + final net.minecraft.server.level.ServerLevel world = this.world; + // make sure the lambda contains no reference to this LevelTicks + scheduler.setOnTickAdded((chunkTickScheduler, tick) -> { + if (tick.equals(chunkTickScheduler.peek())) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); + (isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks()).updateContainerScheduling((ScheduledTick)tick); + } + }); + // Folia end - region threading } public void removeContainer(ChunkPos pos) { @@ -71,6 +138,7 @@ public class LevelTicks implements LevelTickAccess { @Override public void schedule(ScheduledTick orderedTick) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, orderedTick.pos(), "Cannot schedule tick for another region!"); // Folia - region threading long l = ChunkPos.asLong(orderedTick.pos()); LevelChunkTicks levelChunkTicks = this.allContainers.get(l); if (levelChunkTicks == null) { diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index f6de1c6e8fd9086b7bd725f75ee2606583591d6a..1f3d3b59236f45d0e7040968c9c1017eebf362c2 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -312,7 +312,7 @@ public final class CraftServer implements Server { public final io.papermc.paper.SparksFly spark; // Paper - spark // Paper start - Folia region threading API - private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler(); + private final io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler(); // Folia - region threading private final io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler asyncScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler(); private final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler globalRegionScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler(); @@ -380,6 +380,12 @@ public final class CraftServer implements Server { return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(((org.bukkit.craftbukkit.entity.CraftEntity) entity).getHandleRaw()); } // Paper end - Folia reagion threading API + // Folia start - region threading API + @Override + public boolean isGlobalTickThread() { + return io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread(); + } + // Folia end - region threading API static { ConfigurationSerialization.registerClass(CraftOfflinePlayer.class); @@ -972,6 +978,9 @@ public final class CraftServer implements Server { // NOTE: Should only be called from DedicatedServer.ah() public boolean dispatchServerCommand(CommandSender sender, ConsoleInput serverCommand) { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("May not dispatch server commands async"); + // Folia end - region threading if (sender instanceof Conversable) { Conversable conversable = (Conversable) sender; @@ -991,12 +1000,46 @@ public final class CraftServer implements Server { } } + // Folia start - region threading + public void dispatchCmdAsync(CommandSender sender, String commandLine) { + if ((sender instanceof Entity entity)) { + ((org.bukkit.craftbukkit.entity.CraftEntity)entity).taskScheduler.schedule( + (nmsEntity) -> { + CraftServer.this.dispatchCommand(nmsEntity.getBukkitEntity(), commandLine); + }, + null, + 1L + ); + } else if (sender instanceof ConsoleCommandSender || sender instanceof io.papermc.paper.commands.FeedbackForwardingSender) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + CraftServer.this.dispatchCommand(sender, commandLine); + }); + } else { + // huh? + throw new UnsupportedOperationException("Dispatching command for " + sender); + } + } + // Folia end - region threading + @Override public boolean dispatchCommand(CommandSender sender, String commandLine) { Preconditions.checkArgument(sender != null, "sender cannot be null"); Preconditions.checkArgument(commandLine != null, "commandLine cannot be null"); org.spigotmc.AsyncCatcher.catchOp("Command Dispatched Async: " + commandLine); // Spigot // Paper - Include command in error message + // Folia start - region threading + if ((sender instanceof Entity entity)) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(((org.bukkit.craftbukkit.entity.CraftEntity)entity).getHandle(), "Dispatching command async"); + } else if (sender instanceof ConsoleCommandSender || sender instanceof net.minecraft.server.rcon.RconConsoleSource + || sender instanceof org.bukkit.craftbukkit.command.CraftRemoteConsoleCommandSender + || sender instanceof io.papermc.paper.commands.FeedbackForwardingSender) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Dispatching command async"); + } else { + // huh? + throw new UnsupportedOperationException("Dispatching command for " + sender); + } + // Folia end - region threading + if (this.commandMap.dispatch(sender, commandLine)) { return true; } @@ -3194,7 +3237,7 @@ public final class CraftServer implements Server { @Override public int getCurrentTick() { - return net.minecraft.server.MinecraftServer.currentTick; + return (int)io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading } @Override diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 362ca138a5cd5ad19f1300015c2571794adc3649..a33a874a845967b78877b7bb4d072c36912165cf 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -192,7 +192,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public int getTickableTileEntityCount() { - return world.blockEntityTickers.size(); + throw new UnsupportedOperationException(); // Folia - region threading - TODO fix this? } @Override @@ -262,7 +262,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { // Paper start - per world spawn limits for (SpawnCategory spawnCategory : SpawnCategory.values()) { if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { - setSpawnLimit(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory))); + this.spawnCategoryLimit.put(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory))); // Folia - region threading } } // Paper end - per world spawn limits @@ -326,6 +326,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public Chunk getChunkAt(int x, int z) { + ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.getHandle(), x, z); // Folia - region threading warnUnsafeChunk("getting a faraway chunk", x, z); // Paper // Paper start - add ticket to hold chunk for a little while longer if plugin accesses it net.minecraft.world.level.chunk.LevelChunk chunk = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); @@ -349,7 +350,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { // Paper start private void addTicket(int x, int z) { - io.papermc.paper.util.MCUtil.MAIN_EXECUTOR.execute(() -> this.world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE)); // Paper + this.world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 0, Unit.INSTANCE); // Paper // Folia - region threading - does not need to be on the main thread anymore } // Paper end @@ -368,10 +369,10 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean isChunkGenerated(int x, int z) { // Paper start - Fix this method - if (!Bukkit.isPrimaryThread()) { + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.getHandle(), x, z)) { // Folia - region threading return java.util.concurrent.CompletableFuture.supplyAsync(() -> { return CraftWorld.this.isChunkGenerated(x, z); - }, world.getChunkSource().mainThreadProcessor).join(); + }, (run) -> { io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.getHandle(), x, z, run);}).join(); // Folia - region threading } ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); if (chunk != null) { @@ -428,7 +429,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { } private boolean unloadChunk0(int x, int z, boolean save) { - org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot unload chunk asynchronously"); // Folia - region threading if (!this.isChunkLoaded(x, z)) { return true; } @@ -443,7 +444,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean regenerateChunk(int x, int z) { - org.spigotmc.AsyncCatcher.catchOp("chunk regenerate"); // Spigot + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot regenerate chunk asynchronously"); // Folia - region threading throw new UnsupportedOperationException("Not supported in this Minecraft version! Unless you can fix it, this is not a bug :)"); /* if (!unloadChunk0(x, z, false)) { @@ -470,6 +471,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean refreshChunk(int x, int z) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot refresh chunk asynchronously"); // Folia - region threading ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); if (playerChunk == null) return false; @@ -530,7 +532,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean loadChunk(int x, int z, boolean generate) { - org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.getHandle(), x, z, "May not sync load chunks asynchronously"); // Folia - region threading warnUnsafeChunk("loading a faraway chunk", x, z); // Paper ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper @@ -571,7 +573,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { DistanceManager chunkDistanceManager = this.world.getChunkSource().chunkMap.distanceManager; if (chunkDistanceManager.addRegionTicketAtDistance(TicketType.PLUGIN_TICKET, new ChunkPos(x, z), 2, plugin)) { // keep in-line with force loading, add at level 31 - this.getChunkAt(x, z); // ensure loaded + //this.getChunkAt(x, z); // ensure loaded // Folia - region threading - do not load chunks for tickets anymore to make this mt-safe return true; } @@ -782,13 +784,15 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) { - this.world.captureTreeGeneration = true; - this.world.captureBlockStates = true; + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot generate tree asynchronously"); // Folia - region threading + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading + worldData.captureTreeGeneration = true; // Folia - region threading + worldData.captureBlockStates = true; // Folia - region threading boolean grownTree = this.generateTree(loc, type); - this.world.captureBlockStates = false; - this.world.captureTreeGeneration = false; + worldData.captureBlockStates = false; // Folia - region threading + worldData.captureTreeGeneration = false; // Folia - region threading if (grownTree) { // Copy block data to delegate - for (BlockState blockstate : this.world.capturedBlockStates.values()) { + for (BlockState blockstate : worldData.capturedBlockStates.values()) { // Folia - region threading BlockPos position = ((CraftBlockState) blockstate).getPosition(); net.minecraft.world.level.block.state.BlockState oldBlock = this.world.getBlockState(position); int flag = ((CraftBlockState) blockstate).getFlag(); @@ -796,10 +800,10 @@ public class CraftWorld extends CraftRegionAccessor implements World { net.minecraft.world.level.block.state.BlockState newBlock = this.world.getBlockState(position); this.world.notifyAndUpdatePhysics(position, null, oldBlock, newBlock, newBlock, flag, 512); } - this.world.capturedBlockStates.clear(); + worldData.capturedBlockStates.clear(); // Folia - region threading return true; } else { - this.world.capturedBlockStates.clear(); + worldData.capturedBlockStates.clear(); // Folia - region threading return false; } } @@ -833,6 +837,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setTime(long time) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading long margin = (time - this.getFullTime()) % 24000; if (margin < 0) margin += 24000; this.setFullTime(this.getFullTime() + margin); @@ -845,6 +850,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setFullTime(long time) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading // Notify anyone who's listening TimeSkipEvent event = new TimeSkipEvent(this, TimeSkipEvent.SkipReason.CUSTOM, time - this.world.getDayTime()); this.server.getPluginManager().callEvent(event); @@ -872,7 +878,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public long getGameTime() { - return this.world.levelData.getGameTime(); + return this.getHandle().getGameTime(); // Folia - region threading } @Override @@ -901,11 +907,13 @@ public class CraftWorld extends CraftRegionAccessor implements World { explosionType = net.minecraft.world.level.Level.ExplosionInteraction.MOB; // Respect mobGriefing gamerule } + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously"); // Folia - region threading return !this.world.explode(source == null ? null : ((CraftEntity) source).getHandle(), x, y, z, power, setFire, explosionType).wasCanceled; } // Paper start @Override public boolean createExplosion(Entity source, Location loc, float power, boolean setFire, boolean breakBlocks) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot create explosion asynchronously"); // Folia - region threading return !world.explode(source != null ? ((org.bukkit.craftbukkit.entity.CraftEntity) source).getHandle() : null, loc.getX(), loc.getY(), loc.getZ(), power, setFire, breakBlocks ? net.minecraft.world.level.Level.ExplosionInteraction.MOB : net.minecraft.world.level.Level.ExplosionInteraction.NONE).wasCanceled; } // Paper end @@ -982,6 +990,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public int getHighestBlockYAt(int x, int z, org.bukkit.HeightMap heightMap) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x >> 4, z >> 4, "Cannot retrieve chunk asynchronously"); // Folia - region threading warnUnsafeChunk("getting a faraway chunk", x >> 4, z >> 4); // Paper // Transient load for this tick return this.world.getChunk(x >> 4, z >> 4).getHeight(CraftHeightMap.toNMS(heightMap), x, z); @@ -1012,6 +1021,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setBiome(int x, int y, int z, Holder bb) { BlockPos pos = new BlockPos(x, 0, z); + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, pos, "Cannot retrieve chunk asynchronously"); // Folia - region threading if (this.world.hasChunkAt(pos)) { net.minecraft.world.level.chunk.LevelChunk chunk = this.world.getChunkAt(pos); @@ -1322,6 +1332,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setStorm(boolean hasStorm) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading this.world.serverLevelData.setRaining(hasStorm, org.bukkit.event.weather.WeatherChangeEvent.Cause.PLUGIN); // Paper - Add cause to Weather/ThunderChangeEvents this.setWeatherDuration(0); // Reset weather duration (legacy behaviour) this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands) @@ -1334,6 +1345,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setWeatherDuration(int duration) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading this.world.serverLevelData.setRainTime(duration); } @@ -1344,6 +1356,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setThundering(boolean thundering) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading this.world.serverLevelData.setThundering(thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.PLUGIN); // Paper - Add cause to Weather/ThunderChangeEvents this.setThunderDuration(0); // Reset weather duration (legacy behaviour) this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands) @@ -1356,6 +1369,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setThunderDuration(int duration) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading this.world.serverLevelData.setThunderTime(duration); } @@ -1366,6 +1380,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setClearWeatherDuration(int duration) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading this.world.serverLevelData.setClearWeatherTime(duration); } @@ -1560,6 +1575,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setKeepSpawnInMemory(boolean keepLoaded) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify keep spawn in memory off of the global region"); // Folia - region threading if (keepLoaded) { this.setGameRule(GameRule.SPAWN_CHUNK_RADIUS, this.getGameRuleDefault(GameRule.SPAWN_CHUNK_RADIUS)); } else { @@ -1628,6 +1644,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setHardcore(boolean hardcore) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.world.serverLevelData.settings.hardcore = hardcore; } @@ -1640,6 +1657,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerAnimalSpawns(int ticksPerAnimalSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.ANIMAL, ticksPerAnimalSpawns); } @@ -1652,6 +1670,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerMonsterSpawns(int ticksPerMonsterSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.MONSTER, ticksPerMonsterSpawns); } @@ -1664,6 +1683,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerWaterSpawns(int ticksPerWaterSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.WATER_ANIMAL, ticksPerWaterSpawns); } @@ -1676,6 +1696,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerWaterAmbientSpawns(int ticksPerWaterAmbientSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.WATER_AMBIENT, ticksPerWaterAmbientSpawns); } @@ -1688,6 +1709,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerWaterUndergroundCreatureSpawns(int ticksPerWaterUndergroundCreatureSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.WATER_UNDERGROUND_CREATURE, ticksPerWaterUndergroundCreatureSpawns); } @@ -1700,11 +1722,13 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setTicksPerAmbientSpawns(int ticksPerAmbientSpawns) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setTicksPerSpawns(SpawnCategory.AMBIENT, ticksPerAmbientSpawns); } @Override public void setTicksPerSpawns(SpawnCategory spawnCategory, int ticksPerCategorySpawn) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null"); Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory); @@ -1721,21 +1745,25 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setMetadata(String metadataKey, MetadataValue newMetadataValue) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading this.server.getWorldMetadata().setMetadata(this, metadataKey, newMetadataValue); } @Override public List getMetadata(String metadataKey) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading return this.server.getWorldMetadata().getMetadata(this, metadataKey); } @Override public boolean hasMetadata(String metadataKey) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading return this.server.getWorldMetadata().hasMetadata(this, metadataKey); } @Override public void removeMetadata(String metadataKey, Plugin owningPlugin) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading this.server.getWorldMetadata().removeMetadata(this, metadataKey, owningPlugin); } @@ -1748,6 +1776,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setMonsterSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.MONSTER, limit); } @@ -1760,6 +1789,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setAnimalSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.ANIMAL, limit); } @@ -1772,6 +1802,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setWaterAnimalSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.WATER_ANIMAL, limit); } @@ -1784,6 +1815,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setWaterAmbientSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.WATER_AMBIENT, limit); } @@ -1796,6 +1828,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setWaterUndergroundCreatureSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.WATER_UNDERGROUND_CREATURE, limit); } @@ -1808,6 +1841,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override @Deprecated public void setAmbientSpawnLimit(int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading this.setSpawnLimit(SpawnCategory.AMBIENT, limit); } @@ -1830,6 +1864,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void setSpawnLimit(SpawnCategory spawnCategory, int limit) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null"); Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory); @@ -1912,7 +1947,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return; ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(CraftSound.bukkitToMinecraftHolder(sound), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, seed); - ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId()); + ChunkMap.TrackedEntity entityTracker = ((CraftEntity) entity).getHandle().moonrise$getTrackedEntity(); // Folia - region threading if (entityTracker != null) { entityTracker.broadcastAndSend(packet); } @@ -1924,7 +1959,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return; ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(Holder.direct(SoundEvent.createVariableRangeEvent(ResourceLocation.parse(sound))), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, seed); - ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId()); + ChunkMap.TrackedEntity entityTracker = ((CraftEntity)entity).getHandle().moonrise$getTrackedEntity(); // Folia - region threading if (entityTracker != null) { entityTracker.broadcastAndSend(packet); } @@ -2013,6 +2048,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean setGameRuleValue(String rule, String value) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading // No null values allowed if (rule == null || value == null) return false; @@ -2055,6 +2091,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public boolean setGameRule(GameRule rule, T newValue) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading Preconditions.checkArgument(rule != null, "GameRule cannot be null"); Preconditions.checkArgument(newValue != null, "GameRule value cannot be null"); @@ -2281,6 +2318,12 @@ public class CraftWorld extends CraftRegionAccessor implements World { @Override public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) { + // Folia start - region threading + if (sourceEntity != null && !Bukkit.isOwnedByCurrentRegion(sourceEntity)) { + throw new IllegalStateException("Cannot send game event asynchronously"); + } + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, position.getX(), position.getZ(), "Cannot send game event asynchronously"); + // Folia end - region threading getHandle().gameEvent(sourceEntity != null ? ((CraftEntity) sourceEntity).getHandle(): null, net.minecraft.core.registries.BuiltInRegistries.GAME_EVENT.getHolder(org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(gameEvent.getKey())).orElseThrow(), org.bukkit.craftbukkit.util.CraftVector.toBlockPos(position)); } // Paper end @@ -2409,7 +2452,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { // Paper start public java.util.concurrent.CompletableFuture getChunkAtAsync(int x, int z, boolean gen, boolean urgent) { warnUnsafeChunk("getting a faraway chunk async", x, z); // Paper - if (Bukkit.isPrimaryThread()) { + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.getHandle(), x, z)) { // Folia - region threading net.minecraft.world.level.chunk.LevelChunk immediate = this.world.getChunkSource().getChunkAtIfLoadedImmediately(x, z); if (immediate != null) { return java.util.concurrent.CompletableFuture.completedFuture(new CraftChunk(immediate)); @@ -2426,7 +2469,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); ca.spottedleaf.moonrise.common.util.ChunkSystem.scheduleChunkLoad(this.getHandle(), x, z, gen, ChunkStatus.FULL, true, priority, (c) -> { - net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(this.getHandle(), x, z, () -> { // Folia - region threading net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk)c; if (chunk != null) this.addTicket(x, z); // Paper ret.complete(chunk == null ? null : new CraftChunk(chunk)); diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java index ac11f18690434922179b61ffcc3036dea025b0cb..5bc2c3cd495b752e523510cb6c410ea4ea800732 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java @@ -75,6 +75,11 @@ public class CraftBlock implements Block { } public net.minecraft.world.level.block.state.BlockState getNMS() { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading return this.world.getBlockState(this.position); } @@ -157,6 +162,11 @@ public class CraftBlock implements Block { } private void setData(final byte data, int flag) { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading this.world.setBlock(this.position, CraftMagicNumbers.getBlock(this.getType(), data), flag); } @@ -198,6 +208,11 @@ public class CraftBlock implements Block { } public static boolean setTypeAndData(LevelAccessor world, BlockPos position, net.minecraft.world.level.block.state.BlockState old, net.minecraft.world.level.block.state.BlockState blockData, boolean applyPhysics) { + // Folia start - region threading + if (world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading // SPIGOT-611: need to do this to prevent glitchiness. Easier to handle this here (like /setblock) than to fix weirdness in tile entity cleanup if (old.hasBlockEntity() && blockData.getBlock() != old.getBlock()) { // SPIGOT-3725 remove old tile entity if block changes // SPIGOT-4612: faster - just clear tile @@ -343,18 +358,33 @@ public class CraftBlock implements Block { @Override public Biome getBiome() { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading return this.getWorld().getBiome(this.getX(), this.getY(), this.getZ()); } // Paper start @Override public Biome getComputedBiome() { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading return this.getWorld().getComputedBiome(this.getX(), this.getY(), this.getZ()); } // Paper end @Override public void setBiome(Biome bio) { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading this.getWorld().setBiome(this.getX(), this.getY(), this.getZ(), bio); } @@ -402,6 +432,11 @@ public class CraftBlock implements Block { @Override public boolean isBlockFaceIndirectlyPowered(BlockFace face) { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading int power = this.world.getMinecraftWorld().getSignal(this.position, CraftBlock.blockFaceToNotch(face)); Block relative = this.getRelative(face); @@ -414,6 +449,11 @@ public class CraftBlock implements Block { @Override public int getBlockPower(BlockFace face) { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading int power = 0; net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); int x = this.getX(); @@ -500,6 +540,11 @@ public class CraftBlock implements Block { @Override public boolean breakNaturally(ItemStack item, boolean triggerEffect, boolean dropExperience) { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading // Paper end // Order matters here, need to drop before setting to air so skulls can get their data net.minecraft.world.level.block.state.BlockState iblockdata = this.getNMS(); @@ -543,21 +588,27 @@ public class CraftBlock implements Block { @Override public boolean applyBoneMeal(BlockFace face) { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading Direction direction = CraftBlock.blockFaceToNotch(face); BlockFertilizeEvent event = null; ServerLevel world = this.getCraftWorld().getHandle(); UseOnContext context = new UseOnContext(world, null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false)); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading // SPIGOT-6895: Call StructureGrowEvent and BlockFertilizeEvent - world.captureTreeGeneration = true; + worldData.captureTreeGeneration = true; // Folia - region threading InteractionResult result = BoneMealItem.applyBonemeal(context); - world.captureTreeGeneration = false; + worldData.captureTreeGeneration = false; // Folia - region threading - if (world.capturedBlockStates.size() > 0) { - TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; - List blocks = new ArrayList<>(world.capturedBlockStates.values()); - world.capturedBlockStates.clear(); + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading + TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading + SaplingBlock.treeTypeRT.set(null); // Folia - region threading + List blocks = new ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading StructureGrowEvent structureEvent = null; if (treeType != null) { @@ -644,6 +695,11 @@ public class CraftBlock implements Block { @Override public RayTraceResult rayTrace(Location start, Vector direction, double maxDistance, FluidCollisionMode fluidCollisionMode) { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading Preconditions.checkArgument(start != null, "Location start cannot be null"); Preconditions.checkArgument(this.getWorld().equals(start.getWorld()), "Location start cannot be a different world"); start.checkFinite(); @@ -685,6 +741,11 @@ public class CraftBlock implements Block { @Override public boolean canPlace(BlockData data) { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading Preconditions.checkArgument(data != null, "BlockData cannot be null"); net.minecraft.world.level.block.state.BlockState iblockdata = ((CraftBlockData) data).getState(); net.minecraft.world.level.Level world = this.world.getMinecraftWorld(); @@ -719,18 +780,32 @@ public class CraftBlock implements Block { @Override public void tick() { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading final ServerLevel level = this.world.getMinecraftWorld(); this.getNMS().tick(level, this.position, level.random); } - @Override public void fluidTick() { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading this.getNMSFluid().tick(this.world.getMinecraftWorld(), this.position); } @Override public void randomTick() { + // Folia start - region threading + if (this.world instanceof ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, this.position, "Cannot read world asynchronously"); + } + // Folia end - region threading final ServerLevel level = this.world.getMinecraftWorld(); this.getNMS().randomTick(level, this.position, level.random); } diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java index fa63a6cfcfcc4eee4503a82d85333c139c8c8b2b..def7749e6dc4ae8351b72deefc75936629c33d7f 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java @@ -215,6 +215,12 @@ public class CraftBlockState implements BlockState { LevelAccessor access = this.getWorldHandle(); CraftBlock block = this.getBlock(); + // Folia start - region threading + if (access instanceof net.minecraft.server.level.ServerLevel serverWorld) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(serverWorld, position, "Cannot modify world asynchronously"); + } + // Folia end - region threading + if (block.getType() != this.getType()) { if (!force) { return false; @@ -350,6 +356,9 @@ public class CraftBlockState implements BlockState { @Override public java.util.Collection getDrops(org.bukkit.inventory.ItemStack item, org.bukkit.entity.Entity entity) { + // Folia start - region threading + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(world.getHandle(), position, "Cannot modify world asynchronously"); + // Folia end - region threading this.requirePlaced(); net.minecraft.world.item.ItemStack nms = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(item); diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java index 3e93a6c489972ff2b4ecff3d83cc72b2d5c970f8..66dc7e20544c7000f4824b02cc3a31bc5c87f74c 100644 --- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java @@ -50,7 +50,7 @@ public class ConsoleCommandCompleter implements Completer { return syncEvent.callEvent() ? syncEvent.getCompletions() : com.google.common.collect.ImmutableList.of(); } }; - server.getServer().processQueue.add(syncCompletions); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(syncCompletions); // Folia - region threading try { final List legacyCompletions = syncCompletions.get(); completions.removeIf(it -> !legacyCompletions.contains(it.suggestion())); // remove any suggestions that were removed @@ -98,7 +98,7 @@ public class ConsoleCommandCompleter implements Completer { return tabEvent.isCancelled() ? Collections.EMPTY_LIST : tabEvent.getCompletions(); } }; - server.getServer().processQueue.add(waitable); // Paper - Remove "this." + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(waitable); // Folia - region threading try { List offers = waitable.get(); if (offers == null) { diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index cd789c235acf740ec29c30b180e7fbe1a140caa9..fe6f31d7db873de64c0cfc5c8248d3e5e96f1500 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -236,6 +236,11 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { @Override public boolean teleport(Location location, TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading // Paper end Preconditions.checkArgument(location != null, "location cannot be null"); location.checkFinite(); @@ -702,7 +707,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { ImmutableSet.Builder players = ImmutableSet.builder(); ServerLevel world = ((CraftWorld) this.getWorld()).getHandle(); - ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); + ChunkMap.TrackedEntity entityTracker = this.getHandle().moonrise$getTrackedEntity(); // Folia - region threading if (entityTracker != null) { for (ServerPlayerConnection connection : entityTracker.seenBy) { @@ -1006,7 +1011,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { } ServerLevel world = ((CraftWorld) this.getWorld()).getHandle(); - ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); + ChunkMap.TrackedEntity entityTracker = this.getHandle().moonrise$getTrackedEntity(); // Folia - region threading if (entityTracker == null) { return; @@ -1025,7 +1030,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { } ServerLevel world = ((CraftWorld) this.getWorld()).getHandle(); - ChunkMap.TrackedEntity entityTracker = world.getChunkSource().chunkMap.entityMap.get(this.getEntityId()); + ChunkMap.TrackedEntity entityTracker = this.entity.moonrise$getTrackedEntity(); // Folia - region threading if (entityTracker == null) { return; @@ -1059,29 +1064,43 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { location.checkFinite(); Location locationClone = location.clone(); // clone so we don't need to worry about mutations after this call. - net.minecraft.server.level.ServerLevel world = ((CraftWorld)locationClone.getWorld()).getHandle(); + // Folia start - region threading java.util.concurrent.CompletableFuture ret = new java.util.concurrent.CompletableFuture<>(); - - world.loadChunksForMoveAsync(getHandle().getBoundingBoxAt(locationClone.getX(), locationClone.getY(), locationClone.getZ()), - this instanceof CraftPlayer ? ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.HIGHER : ca.spottedleaf.concurrentutil.executor.standard.PrioritisedExecutor.Priority.NORMAL, (list) -> { - net.minecraft.server.level.ServerChunkCache chunkProviderServer = world.getChunkSource(); - for (net.minecraft.world.level.chunk.ChunkAccess chunk : list) { - chunkProviderServer.addTicketAtLevel(net.minecraft.server.level.TicketType.POST_TELEPORT, chunk.getPos(), 33, CraftEntity.this.getEntityId()); + java.util.function.Consumer run = (Entity nmsEntity) -> { + boolean success = nmsEntity.teleportAsync( + ((CraftWorld)locationClone.getWorld()).getHandle(), + new net.minecraft.world.phys.Vec3(locationClone.getX(), locationClone.getY(), locationClone.getZ()), + locationClone.getYaw(), locationClone.getPitch(), net.minecraft.world.phys.Vec3.ZERO, + cause == null ? TeleportCause.UNKNOWN : cause, + Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, // preserve behavior with old API: dismount the entity so it can teleport + (Entity entityTp) -> { + ret.complete(Boolean.TRUE); } - net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> { - try { - ret.complete(CraftEntity.this.teleport(locationClone, cause, teleportFlags) ? Boolean.TRUE : Boolean.FALSE); - } catch (Throwable throwable) { - if (throwable instanceof ThreadDeath) { - throw (ThreadDeath)throwable; - } - net.minecraft.server.MinecraftServer.LOGGER.error("Failed to teleport entity " + CraftEntity.this, throwable); - ret.completeExceptionally(throwable); - } - }); - }); + ); + if (!success) { + ret.complete(Boolean.FALSE); + } + }; + if (org.bukkit.Bukkit.isOwnedByCurrentRegion(this)) { + run.accept(this.getHandle()); + return ret; + } + boolean scheduled = this.taskScheduler.schedule( + // success + run, + // retired + (Entity nmsEntity) -> { + ret.complete(Boolean.FALSE); + }, + 1L + ); + + if (!scheduled) { + ret.complete(Boolean.FALSE); + } return ret; + // Folia end - region threading } // Paper end - more teleport API / async chunk API @@ -1194,8 +1213,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { // Paper start - tracked players API @Override public Set getTrackedPlayers() { - ServerLevel world = (net.minecraft.server.level.ServerLevel)this.entity.level(); - ChunkMap.TrackedEntity tracker = world == null ? null : world.getChunkSource().chunkMap.entityMap.get(this.entity.getId()); + ChunkMap.TrackedEntity tracker = this.entity.moonrise$getTrackedEntity(); // Folia - region threading if (tracker == null) { return java.util.Collections.emptySet(); } diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index a0d5082590ee03060f0dbb4770d196efc316c328..0e3eed03f307a414effdd30414b8d01797349908 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -649,7 +649,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { @Override public void kickPlayer(String message) { - org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot + //org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot // Folia - thread-safe now, as it will simply delay the kick if (this.getHandle().connection == null) return; this.getHandle().connection.disconnect(CraftChatMessage.fromStringOrEmpty(message, true), org.bukkit.event.player.PlayerKickEvent.Cause.PLUGIN); // Paper - kick event cause @@ -1408,6 +1408,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player { @Override public boolean teleport(Location location, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading Set relativeArguments; Set allFlags; if (flags.length == 0) { @@ -2060,7 +2065,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { private void unregisterEntity(Entity other) { // Paper end ChunkMap tracker = ((ServerLevel) this.getHandle().level()).getChunkSource().chunkMap; - ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); + ChunkMap.TrackedEntity entry = other.moonrise$getTrackedEntity(); // Folia - region threading if (entry != null) { entry.removePlayer(this.getHandle()); } @@ -2157,7 +2162,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { if (original != null) otherPlayer.setUUID(original); // Paper - uuid override } - ChunkMap.TrackedEntity entry = tracker.entityMap.get(other.getId()); + ChunkMap.TrackedEntity entry = other.moonrise$getTrackedEntity(); // Folia - region threading if (entry != null && !entry.seenBy.contains(this.getHandle().connection)) { entry.updatePlayer(this.getHandle()); } @@ -3353,7 +3358,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { { if ( CraftPlayer.this.getHealth() <= 0 && CraftPlayer.this.isOnline() ) { - CraftPlayer.this.server.getServer().getPlayerList().respawn( CraftPlayer.this.getHandle(), false, Entity.RemovalReason.KILLED, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.PLUGIN ); + CraftPlayer.this.getHandle().respawn(null, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.PLUGIN); // Folia - region threading } } diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java index 8dd85b9ca3b3e3429de4d0ec0654982589c6e93e..fced5379b855d100c46fa303cfab3a19f862f525 100644 --- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java @@ -951,7 +951,7 @@ public class CraftEventFactory { return CraftEventFactory.handleBlockSpreadEvent(world, source, target, block, 2); } - public static BlockPos sourceBlockOverride = null; // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. + public static final ThreadLocal sourceBlockOverrideRT = new ThreadLocal<>(); // SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading public static boolean handleBlockSpreadEvent(LevelAccessor world, BlockPos source, BlockPos target, net.minecraft.world.level.block.state.BlockState block, int flag) { // Suppress during worldgen @@ -963,7 +963,7 @@ public class CraftEventFactory { CraftBlockState state = CraftBlockStates.getBlockState(world, target, flag); state.setData(block); - BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverride != null ? CraftEventFactory.sourceBlockOverride : source), state); + BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, CraftEventFactory.sourceBlockOverrideRT.get() != null ? CraftEventFactory.sourceBlockOverrideRT.get() : source), state); // Folia - region threading Bukkit.getPluginManager().callEvent(event); if (!event.isCancelled()) { @@ -2209,7 +2209,7 @@ public class CraftEventFactory { CraftItemStack craftItem = CraftItemStack.asCraftMirror(itemStack.copyWithCount(1)); org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), CraftVector.toBukkit(to)); - if (!net.minecraft.world.level.block.DispenserBlock.eventFired) { + if (!net.minecraft.world.level.block.DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading if (!event.callEvent()) { return itemStack; } diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java index 2f4d6b56301195f8d39ed50dffe842464065bfe1..5a24902b735cbd64dd5cd5ad46b096c4bdfa799f 100644 --- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java +++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java @@ -521,6 +521,7 @@ public class CraftScheduler implements BukkitScheduler { } protected CraftTask handle(final CraftTask task, final long delay) { // Paper + if (true) throw new UnsupportedOperationException(); // Folia - region threading // Paper start if (!this.isAsyncScheduler && !task.isSync()) { this.asyncScheduler.handle(task, delay); diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java index 28dbe30a98a6730839949bc9a6a90b78619ff84d..c9e31dcdde9d96edfc12e694065f1f4643bb9db3 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java @@ -369,6 +369,12 @@ public final class CraftMagicNumbers implements UnsafeValues { throw new InvalidPluginException("Unsupported API version " + pdf.getAPIVersion()); } + // Folia start - block plugins not marked as supported + if (!pdf.isFoliaSupported()) { + throw new InvalidPluginException("Plugin " + pdf.getFullName() + " is not marked as supporting regionised multithreading"); + } + // Folia end - block plugins not marked as supported + if (toCheck.isOlderThan(minimumVersion)) { // Older than supported throw new InvalidPluginException("Plugin API version " + pdf.getAPIVersion() + " is lower than the minimum allowed version. Please update or replace it."); diff --git a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java index bf18f9ad7dec2b09ebfcb5ec6566f2556e842f22..2dddb661b29c1c3350bfbf71a0a1b961b07ad80f 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java +++ b/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java @@ -68,6 +68,13 @@ public abstract class DelegatedGeneratorAccess implements WorldGenLevel { this.handle = worldAccess; } + // Folia start - region threading + @Override + public net.minecraft.world.level.StructureManager structureManager() { + return this.handle.structureManager(); + } + // Folia end - region threading + public WorldGenLevel getHandle() { return this.handle; } diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java index bf2d18f74b0f0da7c3c30310c74224a1c0853564..91c63c3dceddf09018d3651f4c11bf521eb53ecf 100644 --- a/src/main/java/org/spigotmc/ActivationRange.java +++ b/src/main/java/org/spigotmc/ActivationRange.java @@ -52,7 +52,7 @@ public class ActivationRange RAIDER, MISC; - AABB boundingBox = new AABB( 0, 0, 0, 0, 0, 0 ); + // Folia - threaded regions - replaced by local variable } // Paper start @@ -65,26 +65,27 @@ public class ActivationRange private static int checkInactiveWakeup(Entity entity) { Level world = entity.level(); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions SpigotWorldConfig config = world.spigotConfig; - long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; + long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions if (entity.activationType == ActivationType.VILLAGER) { - if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) { - world.wakeupInactiveRemainingVillagers--; + if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions return config.wakeUpInactiveVillagersFor; } } else if (entity.activationType == ActivationType.ANIMAL) { - if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) { - world.wakeupInactiveRemainingAnimals--; + if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions return config.wakeUpInactiveAnimalsFor; } } else if (entity.activationType == ActivationType.FLYING_MONSTER) { - if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) { - world.wakeupInactiveRemainingFlying--; + if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions return config.wakeUpInactiveFlyingFor; } } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) { - if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) { - world.wakeupInactiveRemainingMonsters--; + if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions return config.wakeUpInactiveMonstersFor; } } @@ -92,7 +93,7 @@ public class ActivationRange } // Paper end - static AABB maxBB = new AABB( 0, 0, 0, 0, 0, 0 ); + // Folia - threaded regions - replaced by local variable /** * Initializes an entities type on construction to specify what group this @@ -176,10 +177,11 @@ public class ActivationRange final int waterActivationRange = world.spigotConfig.waterActivationRange; final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange; final int villagerActivationRange = world.spigotConfig.villagerActivationRange; - world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); - world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); - world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); - world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions + worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Folia - threaded regions + worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Folia - threaded regions + worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Folia - threaded regions + worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Folia - threaded regions final ServerChunkCache chunkProvider = (ServerChunkCache) world.getChunkSource(); // Paper end @@ -193,9 +195,9 @@ public class ActivationRange // Paper end maxRange = Math.min( ( world.spigotConfig.simulationDistance << 4 ) - 8, maxRange ); - for ( Player player : world.players() ) + for ( Player player : world.getLocalPlayers() ) // Folia - region threading { - player.activatedTick = MinecraftServer.currentTick; + player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading if ( world.spigotConfig.ignoreSpectatorActivation && player.isSpectator() ) { continue; @@ -203,26 +205,33 @@ public class ActivationRange // Paper start int worldHeight = world.getHeight(); - ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange ); - ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, worldHeight, miscActivationRange ); - ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, worldHeight, raiderActivationRange ); - ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate( animalActivationRange, worldHeight, animalActivationRange ); - ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate( monsterActivationRange, worldHeight, monsterActivationRange ); - ActivationType.WATER.boundingBox = player.getBoundingBox().inflate( waterActivationRange, worldHeight, waterActivationRange ); - ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate( flyingActivationRange, worldHeight, flyingActivationRange ); - ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate( villagerActivationRange, worldHeight, villagerActivationRange ); + AABB maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange ); // Folia - threaded regions + AABB[] bbByType = new AABB[ActivationType.values().length]; + bbByType[ActivationType.MISC.ordinal()] = player.getBoundingBox().inflate( miscActivationRange, worldHeight, miscActivationRange ); // Folia - threaded regions + bbByType[ActivationType.RAIDER.ordinal()] = player.getBoundingBox().inflate( raiderActivationRange, worldHeight, raiderActivationRange ); // Folia - threaded regions + bbByType[ActivationType.ANIMAL.ordinal()] = player.getBoundingBox().inflate( animalActivationRange, worldHeight, animalActivationRange ); // Folia - threaded regions + bbByType[ActivationType.MONSTER.ordinal()] = player.getBoundingBox().inflate( monsterActivationRange, worldHeight, monsterActivationRange ); // Folia - threaded regions + bbByType[ActivationType.WATER.ordinal()] = player.getBoundingBox().inflate( waterActivationRange, worldHeight, waterActivationRange ); // Folia - threaded regions + bbByType[ActivationType.FLYING_MONSTER.ordinal()] = player.getBoundingBox().inflate( flyingActivationRange, worldHeight, flyingActivationRange ); // Folia - threaded regions + bbByType[ActivationType.VILLAGER.ordinal()] = player.getBoundingBox().inflate( villagerActivationRange, worldHeight, villagerActivationRange ); // Folia - threaded regions // Paper end // Paper start - java.util.List entities = world.getEntities((Entity)null, ActivationRange.maxBB, null); + java.util.List entities = new java.util.ArrayList<>(); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later + ((net.minecraft.server.level.ServerLevel)world).moonrise$getEntityLookup().getEntities((Entity)null, maxBB, entities, null); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later boolean tickMarkers = world.paperConfig().entities.markers.tick; // Paper - Configurable marker ticking for (Entity entity : entities) { // Paper start - Configurable marker ticking + // Folia start - region ticking + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { + continue; + } + // Folia end - region ticking if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) { continue; } // Paper end - Configurable marker ticking - ActivationRange.activateEntity(entity); + ActivationRange.activateEntity(entity, bbByType); // Folia - threaded regions } // Paper end } @@ -234,18 +243,18 @@ public class ActivationRange * * @param chunk */ - private static void activateEntity(Entity entity) + private static void activateEntity(Entity entity, AABB[] bbByType) // Folia - threaded regions { - if ( MinecraftServer.currentTick > entity.activatedTick ) + if ( io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() > entity.activatedTick ) // Folia - threaded regions { if ( entity.defaultActivationState ) { - entity.activatedTick = MinecraftServer.currentTick; + entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions return; } - if ( entity.activationType.boundingBox.intersects( entity.getBoundingBox() ) ) + if ( bbByType[entity.activationType.ordinal()].intersects( entity.getBoundingBox() ) ) // Folia - threaded regions { - entity.activatedTick = MinecraftServer.currentTick; + entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions } } } @@ -268,10 +277,10 @@ public class ActivationRange if (entity.getRemainingFireTicks() > 0) { return 2; } - if (entity.activatedImmunityTick >= MinecraftServer.currentTick) { + if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - threaded regions return 1; } - long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; + long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions // Paper end // quick checks. if ( (entity.activationType != ActivationType.WATER && entity.wasTouchingWater && entity.isPushedByFluid()) ) // Paper @@ -394,19 +403,19 @@ public class ActivationRange } // Paper end - boolean isActive = entity.activatedTick >= MinecraftServer.currentTick; + boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions entity.isTemporarilyActive = false; // Paper // Should this entity tick? if ( !isActive ) { - if ( ( MinecraftServer.currentTick - entity.activatedTick - 1 ) % 20 == 0 ) + if ( ( io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick - 1 ) % 20 == 0 ) // Folia - threaded regions { // Check immunities every 20 ticks. // Paper start int immunity = checkEntityImmunities(entity); if (immunity >= 0) { - entity.activatedTick = MinecraftServer.currentTick + immunity; + entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + immunity; // Folia - threaded regions } else { entity.isTemporarilyActive = true; } diff --git a/src/main/java/org/spigotmc/SpigotCommand.java b/src/main/java/org/spigotmc/SpigotCommand.java index ac0fd418fcb437896dfdff53ab3eff19833d25fb..130220977477a5d8d51e17dcb989ae0c858643cb 100644 --- a/src/main/java/org/spigotmc/SpigotCommand.java +++ b/src/main/java/org/spigotmc/SpigotCommand.java @@ -29,6 +29,7 @@ public class SpigotCommand extends Command { Command.broadcastCommandMessage(sender, ChatColor.RED + "Please note that this command is not supported and may cause issues."); Command.broadcastCommandMessage(sender, ChatColor.RED + "If you encounter any issues please use the /stop command to restart your server."); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading MinecraftServer console = MinecraftServer.getServer(); org.spigotmc.SpigotConfig.init((File) console.options.valueOf("spigot-settings")); for (ServerLevel world : console.getAllLevels()) { @@ -37,6 +38,7 @@ public class SpigotCommand extends Command { console.server.reloadCount++; Command.broadcastCommandMessage(sender, ChatColor.GREEN + "Reload complete."); + }); // Folia - region threading } return true; diff --git a/src/main/java/org/spigotmc/SpigotConfig.java b/src/main/java/org/spigotmc/SpigotConfig.java index 4dbb109d0526afee99b9190fc256585121aac9b5..5c80e89034c1b3149729684ba4dd4ae26a4261c1 100644 --- a/src/main/java/org/spigotmc/SpigotConfig.java +++ b/src/main/java/org/spigotmc/SpigotConfig.java @@ -224,7 +224,7 @@ public class SpigotConfig SpigotConfig.restartOnCrash = SpigotConfig.getBoolean( "settings.restart-on-crash", SpigotConfig.restartOnCrash ); SpigotConfig.restartScript = SpigotConfig.getString( "settings.restart-script", SpigotConfig.restartScript ); SpigotConfig.restartMessage = SpigotConfig.transform( SpigotConfig.getString( "messages.restart", "Server is restarting" ) ); - SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) ); + // SpigotConfig.commands.put( "restart", new RestartCommand( "restart" ) ); // Folia - region threading // WatchdogThread.doStart( SpigotConfig.timeoutTime, SpigotConfig.restartOnCrash ); // Paper - moved to after paper config initialization } @@ -279,7 +279,7 @@ public class SpigotConfig private static void tpsCommand() { - SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) ); + //SpigotConfig.commands.put( "tps", new TicksPerSecondCommand( "tps" ) ); // Folia - region threading } public static int playerSample; diff --git a/src/main/java/org/spigotmc/SpigotWorldConfig.java b/src/main/java/org/spigotmc/SpigotWorldConfig.java index 2c408fa4abcbe1171c58aee8799c8cf7867d0f0a..9f38a0763597d9d70cb8d1b636c7d4b14d32c535 100644 --- a/src/main/java/org/spigotmc/SpigotWorldConfig.java +++ b/src/main/java/org/spigotmc/SpigotWorldConfig.java @@ -435,7 +435,7 @@ public class SpigotWorldConfig this.otherMultiplier = (float) this.getDouble( "hunger.other-multiplier", 0.0 ); } - public int currentPrimedTnt = 0; + //public int currentPrimedTnt = 0; // Folia - region threading - moved to regionised world data public int maxTntTicksPerTick; private void maxTntPerTick() { if ( SpigotConfig.version < 7 )