From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sun, 20 Apr 1997 05:37:42 -0800 Subject: [PATCH] Region Threading Base diff --git a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java index 1b8193587814225c2ef2c5d9e667436eb50ff6c5..0027a3896c0cfce2f46eca8a0a77a90223723dc7 100644 --- a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java +++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java @@ -244,7 +244,7 @@ public final class NearbyPlayers { created.addPlayer(parameter, type); type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ); - ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created; + //((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created; // Folia - region threading } } @@ -263,10 +263,7 @@ public final class NearbyPlayers { if (chunk.isEmpty()) { NearbyPlayers.this.byChunk.remove(chunkKey); - final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey); - if (chunkData != null) { - chunkData.nearbyPlayers = null; - } + // Folia - region threading } } } diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java index 4d344559a20a0c35c181e297e81788c747363ec9..779e6b7d025da185b33a963e42e91a56908e46dc 100644 --- a/ca/spottedleaf/moonrise/paper/PaperHooks.java +++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java @@ -105,7 +105,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo } for (final EnderDragonPart part : parts) { - if (part != entity && part.getBoundingBox().intersects(boundingBox) && (predicate == null || predicate.test(part))) { + if (part != entity && part.getBoundingBox().intersects(boundingBox) && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(part) && (predicate == null || predicate.test(part))) { // Folia - region threading into.add(part); } } @@ -127,7 +127,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo continue; } final T casted = (T)entityTypeTest.tryCast(part); - if (casted != null && (predicate == null || predicate.test(casted))) { + if (casted != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(part) && (predicate == null || predicate.test(casted))) { // Folia - region threading into.add(casted); if (into.size() >= maxCount) { break; @@ -275,4 +275,4 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo public int modifyEntityTrackingRange(final Entity entity, final int currentRange) { return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange); } -} \ No newline at end of file +} diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java index ece1261b67033e946dfc20a96872708755bffe0a..8d67b4629c69d3039b199aaad45533d1acde114e 100644 --- a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java +++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java @@ -80,18 +80,23 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co @Override public 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 } @Override public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { // Update progress listener for LevelLoadingScreen - final net.minecraft.server.level.progress.ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener; + final net.minecraft.server.level.progress.ChunkProgressListener progressListener = null; // Folia - threaded regions - cannot schedule chunk task here; as it would create a chunkholder if (progressListener != null) { this.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> { progressListener.onStatusChange(holder.getPos(), null); }); } + // Folia start - threaded regions + level.regioniser.removeChunk(holder.getPos().x, holder.getPos().z); + // Folia end - threaded regions } @Override @@ -102,17 +107,13 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co @Override public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add( - ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().addChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading chunk.loadCallback(); } @Override public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove( - ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().removeChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading chunk.unloadCallback(); } @@ -124,9 +125,7 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co @Override public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add( - ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().addTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { chunk.postProcessGeneration((ServerLevel)chunk.getLevel()); } @@ -137,24 +136,18 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co @Override public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove( - ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().removeTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration } @Override public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add( - ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().addEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading } @Override public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove( - ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() - ); + chunk.getLevel().getCurrentWorldData().removeEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading } @Override @@ -191,4 +184,4 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co public void updateMaps(final ServerLevel world, final ServerPlayer player) { ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player); } -} \ No newline at end of file +} diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java index 8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa..306216138e21c41937e4728e8004220a02d6ea4b 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java @@ -5,7 +5,7 @@ import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; public final class ChunkData { private int referenceCount = 0; - public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players + //public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players // Folia - region threading public ChunkData() { diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java index 7554c109c35397bc1a43dd80e87764fd78645bbf..db16fe8d664f9b04710200d63439564cb97c0066 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +++ b/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, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); + 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(); @@ -986,6 +999,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 } } diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java index 26207443b1223119c03db478d7e816d9cdf8e618..a89ee24c6aed46af23c5de7ae2234c7902f1c0f4 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java @@ -17,7 +17,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 + // Folia - move to regionized world data public ServerEntityLookup(final ServerLevel world, final LevelCallback worldCallback) { super(world, worldCallback); @@ -75,6 +75,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 @@ -87,14 +88,14 @@ public final class ServerEntityLookup extends EntityLookup { @Override protected void entityStartLoaded(final Entity entity) { // Moonrise start - entity tracker - this.trackerEntities.add(entity); + this.world.getCurrentWorldData().trackerEntities.add(entity); // Folia - region threading // Moonrise end - entity tracker } @Override protected void entityEndLoaded(final Entity entity) { // Moonrise start - entity tracker - this.trackerEntities.remove(entity); + this.world.getCurrentWorldData().trackerEntities.remove(entity); // Folia - region threading // Moonrise end - entity tracker } diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java index dd2509996bfd08e8c3f9f2be042229eac6d7692d..f77dcf5a42ff34a1624ddf16bcce2abee81194bb 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java @@ -216,7 +216,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(); @@ -304,7 +304,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/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java index 7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309..4bfcae47ed76346e6200514ebce5b04f907c5026 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java +++ b/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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java index b5817aa8f537593f6d9fc6b612c82ccccb250ac7..aae97116a22a87cffd4756d566da3acd96ce2ae0 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +++ b/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; + } - 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); + + 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 + } - if (saveTickCompare != 0) { - return saveTickCompare; + for (final NewChunkHolder autoSave : this.autoSaveQueue) { + final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift; + final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift; + + 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 (first && 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 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 MoonriseRegionFileIO.flush(this.world); if (halt) { @@ -220,28 +288,35 @@ public final class ChunkHolderManager { } this.taskScheduler.setShutdown(true); + } // 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, PlatformHooks.get().configAutoSaveInterval(this.world)); final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world); - 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) { @@ -255,15 +330,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(); + final int width = 1 << regionShift; + for (final LongIterator iterator = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getOwnedSectionsUnsynchronised(); iterator.hasNext();) { + final long sectionKey = iterator.nextLong(); + 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) + "'"); } @@ -292,6 +390,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) { @@ -327,7 +431,7 @@ public final class ChunkHolderManager { } } } - if (flush) { + if (last && flush) { // Folia - region threading MoonriseRegionFileIO.flush(this.world); try { MoonriseRegionFileIO.flushRegionStorages(this.world); @@ -732,7 +836,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(); @@ -746,7 +856,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)) { @@ -1031,26 +1141,56 @@ 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(); - }, 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); + } + } + 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, () -> { + ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders); + ChunkHolderManager.this.processPendingFullUpdate(); + }, Priority.HIGHEST); + } } + // Folia end - region threading } private void removeChunkHolder(final NewChunkHolder holder) { holder.onUnload(); - this.autoSaveQueue.remove(holder); + this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading PlatformHooks.get().onChunkHolderDelete(this.world, holder.vanillaChunkHolder); this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); } @@ -1063,7 +1203,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 @@ -1381,7 +1521,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; @@ -1392,9 +1538,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/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java index 67532b85073b7978254a0b04caadfe822679e61f..cba2d16c0cb5adc92952990ef95b1c979eafd40f 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java @@ -122,7 +122,7 @@ public final class ChunkTaskScheduler { public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor; public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor; - private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue(); + // Folia - regionised ticking public final ChunkHolderManager chunkHolderManager; @@ -337,14 +337,13 @@ public final class ChunkTaskScheduler { }; // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions - this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING); + this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, 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 Priority priority) { @@ -829,7 +828,7 @@ public final class ChunkTaskScheduler { */ @Deprecated public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) { - return this.mainThreadExecutor.queueTask(run, priority); + throw new UnsupportedOperationException(); // Folia - regionised ticking } public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { @@ -838,7 +837,7 @@ public final class ChunkTaskScheduler { public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, final Priority priority) { - return this.mainThreadExecutor.createTask(run, priority); + return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking } public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { @@ -847,9 +846,27 @@ public final class ChunkTaskScheduler { public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, final Priority priority) { - return this.mainThreadExecutor.queueTask(run, priority); + return io.papermc.paper.threadedregions.RegionizedServer.getInstance().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) { + return this.scheduleChunkTaskEventually(chunkX, chunkZ, run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run, + final Priority priority) { + final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority); + this.world.taskQueueRegionData.pushGlobalChunkTask(() -> { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().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(); this.parallelGenExecutor.halt(); diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java index e4a5fa25ed368fc4662c30934da2963ef446d782..601ed36413bbbf9c17e530b42906986e441237fd 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java @@ -1359,10 +1359,10 @@ public final class NewChunkHolder { private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { // Update progress listener for LevelLoadingScreen if (chunk != null) { - final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener; + final ChunkProgressListener progressListener = null; // Folia - threaded regions if (progressListener != null) { final ChunkStatus finalStatus = status; - this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { + this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - threaded regions progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus); }); } @@ -1383,7 +1383,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); @@ -1411,7 +1411,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/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java index e04bd54744335fb5398c6e4f7ce8b981f35bfb7d..471b6d49d77e03665ffc269d17ab46f225e3ce1c 100644 --- a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java @@ -1940,6 +1940,17 @@ public final class CollisionUtil { for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, currChunkX, currChunkZ, 4)) { + if (checkOnly) { + return true; + } else { + intoAABB.add(getBoxForChunk(currChunkX, currChunkZ)); + ret = true; + continue; + } + } + // Folia end - region threading final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); if (chunk == null) { diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java index ade6110cc6adb1263c0359ff7e96e96b959e61f3..c260741a87513b89a5cc62c543fb9f990f86491e 100644 --- a/io/papermc/paper/entity/activation/ActivationRange.java +++ b/io/papermc/paper/entity/activation/ActivationRange.java @@ -48,33 +48,34 @@ public final class ActivationRange { private static int checkInactiveWakeup(final Entity entity) { final Level world = entity.level(); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions final SpigotWorldConfig config = world.spigotConfig; - final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; + final 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; } } return -1; } - static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0); + //static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0); // Folia - threaded regions - replaced by local variable /** * These entities are excluded from Activation range checks. @@ -122,10 +123,11 @@ public final 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 int maxRange = Math.max(monsterActivationRange, animalActivationRange); maxRange = Math.max(maxRange, raiderActivationRange); @@ -135,30 +137,37 @@ public final class ActivationRange { maxRange = Math.max(maxRange, villagerActivationRange); maxRange = Math.min((world.spigotConfig.simulationDistance << 4) - 8, maxRange); - for (final Player player : world.players()) { - player.activatedTick = MinecraftServer.currentTick; + for (final Player player : world.getLocalPlayers()) { // Folia - region threading + player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading if (world.spigotConfig.ignoreSpectatorActivation && player.isSpectator()) { continue; } final 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); + final AABB maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange); // Folia - threaded regions + final AABB[] bbByType = new AABB[ActivationType.values().length]; // Folia - threaded regions + 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 - final java.util.List entities = world.getEntities((Entity) null, ActivationRange.maxBB, e -> true); + final 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 final boolean tickMarkers = world.paperConfig().entities.markers.tick; for (final Entity entity : entities) { + // 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; } - ActivationRange.activateEntity(entity); + ActivationRange.activateEntity(entity, bbByType); // Folia - threaded regions } } } @@ -168,14 +177,14 @@ public final class ActivationRange { * * @param entity */ - private static void activateEntity(final Entity entity) { - if (MinecraftServer.currentTick > entity.activatedTick) { + private static void activateEntity(final Entity entity, final AABB[] bbByType) { // Folia - threaded regions + 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())) { - entity.activatedTick = MinecraftServer.currentTick; + if (bbByType[entity.activationType.ordinal()].intersects(entity.getBoundingBox())) { // Folia - threaded regions + entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions } } } @@ -189,6 +198,7 @@ public final class ActivationRange { */ public static int checkEntityImmunities(final Entity entity) { // return # of ticks to get immunity final SpigotWorldConfig config = entity.level().spigotConfig; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = entity.level().getCurrentWorldData(); // Folia - threaded regions final int inactiveWakeUpImmunity = checkInactiveWakeup(entity); if (inactiveWakeUpImmunity > -1) { return inactiveWakeUpImmunity; @@ -196,10 +206,10 @@ public final 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; } - final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; + final long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions if ((entity.activationType != ActivationType.WATER && entity.isInWater() && entity.isPushedByFluid())) { return 100; } @@ -296,16 +306,16 @@ public final class ActivationRange { return true; } - boolean isActive = entity.activatedTick >= MinecraftServer.currentTick; + boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions entity.isTemporarilyActive = false; // 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. final 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/io/papermc/paper/redstone/RedstoneWireTurbo.java b/io/papermc/paper/redstone/RedstoneWireTurbo.java index ff747a1ecdf3c888bca0d69de4f85dcd810b6139..5a76c93eada8db35b1ddbb562ccfbd2f0d35f0ca 100644 --- a/io/papermc/paper/redstone/RedstoneWireTurbo.java +++ b/io/papermc/paper/redstone/RedstoneWireTurbo.java @@ -829,14 +829,14 @@ public final 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/io/papermc/paper/threadedregions/RegionShutdownThread.java b/io/papermc/paper/threadedregions/RegionShutdownThread.java new file mode 100644 index 0000000000000000000000000000000000000000..bbd14cf34438a9366f5ff29f1acba4282d77d983 --- /dev/null +++ b/io/papermc/paper/threadedregions/RegionShutdownThread.java @@ -0,0 +1,226 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import com.mojang.logging.LogUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ChunkPos; +import org.bukkit.event.inventory.InventoryCloseEvent; +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(true); + } 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 closePlayerInventories(final ThreadedRegionizer.ThreadedRegion region) { + ChunkPos center = null; + try { + this.shuttingDown = region; + center = region.getCenterChunk(); + + final RegionizedWorldData worldData = region.regioniser.world.worldRegionData.get(); + + for (final ServerPlayer player : worldData.getLocalPlayers()) { + try { + // close inventory + if (player.containerMenu != player.inventoryMenu) { + player.closeContainer(InventoryCloseEvent.Reason.DISCONNECT); + } + + // drop carried item + if (!player.containerMenu.getCarried().isEmpty()) { + ItemStack carried = player.containerMenu.getCarried(); + player.containerMenu.setCarried(ItemStack.EMPTY); + player.drop(carried, false); + } + } catch (final Throwable thr) { + LOGGER.error("Failed to close player inventory for player: " + player, thr); + } + } + } catch (final Throwable thr) { + LOGGER.error("Failed to close player inventories for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); + } finally { + this.shuttingDown = null; + } + } + + @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"); + + LOGGER.info("Finishing pending teleports..."); + 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) { + this.finishTeleportations(regions.get(i), world); + } + } + LOGGER.info("Finished pending teleports"); + + LOGGER.info("Saving all worlds"); + for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { + LOGGER.info("Saving world data for world '" + WorldUtil.getWorldName(world) + "'"); + + final List> + regions = new ArrayList<>(); + world.regioniser.computeForAllRegionsUnsynchronised(regions::add); + + LOGGER.info("Closing player inventories..."); + for (int i = 0, len = regions.size(); i < len; ++i) { + this.closePlayerInventories(regions.get(i)); + } + LOGGER.info("Closed player inventories"); + + LOGGER.info("Saving chunks..."); + for (int i = 0, len = regions.size(); i < len; ++i) { + this.saveRegionChunks(regions.get(i), (i + 1) == len); + } + LOGGER.info("Saved chunks"); + + LOGGER.info("Saving level data..."); + this.saveLevelData(world); + LOGGER.info("Saved level data"); + + LOGGER.info("Saved world data for world '" + WorldUtil.getWorldName(world) + "'"); + } + LOGGER.info("Saved all worlds"); + + // Note: only save after world data and pending teleportations + LOGGER.info("Saving all player data..."); + MinecraftServer.getServer().getPlayerList().saveAll(); + LOGGER.info("Saved all player data"); + + MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc) + // done, part 2 should call exit() + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/RegionizedData.java b/io/papermc/paper/threadedregions/RegionizedData.java new file mode 100644 index 0000000000000000000000000000000000000000..a1043c426d031755b57b77a9b2eec685e9861b13 --- /dev/null +++ b/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 + ); + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java new file mode 100644 index 0000000000000000000000000000000000000000..1382c695c4991488b113401e231875ddc74f6b01 --- /dev/null +++ b/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.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) { + + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..745ab870310733b569681f5280895bb9798620a4 --- /dev/null +++ b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java @@ -0,0 +1,807 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; +import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Priority; +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 this.createChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final 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 this.createTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final 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, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final 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, Priority.NORMAL); + } + + public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, + final Runnable run, final 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)); + } + + // note: only call on acquired referenceCountData + private void ensureTicketAdded(final long coord, final ReferenceCountData referenceCountData) { + if (!referenceCountData.addedTicket) { + // fine if multiple threads do this, no removeTicket may be called for this coord due to reference count inc + this.addTicket(coord); + this.processTicketUpdates(coord); + referenceCountData.addedTicket = true; + } + } + + private void decrementReference(final ReferenceCountData referenceCountData, final long coord) { + if (!referenceCountData.decreaseReferenceCount()) { + return; + } // else: need to remove ticket + + // note: it is possible that another thread increments and then removes the reference before we can, so + // use ifPresent + this.referenceCounters.computeIfPresent(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> { + if (valueInMap.referenceCount.get() != 0L) { + return valueInMap; + } + + // note: valueInMap may not be referenceCountData + + // possible to invoke this outside of the compute call, but not required and requires additional logic + WorldRegionTaskData.this.removeTicket(keyInMap); + + return null; + }); + } + + private ReferenceCountData incrementReference(final long coord) { + ReferenceCountData referenceCountData = this.referenceCounters.get(coord); + + if (referenceCountData != null && referenceCountData.addCount()) { + this.ensureTicketAdded(coord, referenceCountData); + return referenceCountData; + } + + referenceCountData = this.referenceCounters.compute(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> { + if (valueInMap == null) { + // sets reference count to 1 + return new ReferenceCountData(); + } + // OK if we add from 0, the remove call will use compute() and catch this race condition + valueInMap.referenceCount.getAndIncrement(); + + return valueInMap; + }); + + this.ensureTicketAdded(coord, referenceCountData); + + return referenceCountData; + } + } + + private static final class ReferenceCountData { + + public final AtomicLong referenceCount = new AtomicLong(1L); + public volatile boolean addedTicket; + + // returns false if reference count is 0, otherwise increments ref count + public boolean addCount() { + int failures = 0; + for (long curr = this.referenceCount.get();;) { + for (int i = 0; i < failures; ++i) { + Thread.onSpinWait(); + } + + if (curr == 0L) { + return false; + } + + if (curr == (curr = this.referenceCount.compareAndExchange(curr, curr + 1L))) { + return true; + } + + ++failures; + } + } + + // returns true if new reference count is 0 + public boolean decreaseReferenceCount() { + final long res = this.referenceCount.decrementAndGet(); + if (res >= 0L) { + return res == 0L; + } else { + throw new IllegalStateException("Negative reference count"); + } + } + } + + 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[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { + for (int i = 0; i < 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 = 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[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 Priority priority = task.getPriority(); + if (priority == 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 = Priority.IDLE.priority; + ChunkBasedPriorityTask task = null; + ReferenceCountData 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 ReferenceCountData REFERENCE_COUNTER_NOT_SET = new ReferenceCountData(); + static { + REFERENCE_COUNTER_NOT_SET.referenceCount.set((long)Integer.MIN_VALUE); + } + + private final WorldRegionTaskData world; + private final int chunkX; + private final int chunkZ; + private final long sectionLowerLeftCoord; // chunk coordinate + private final boolean isChunkTask; + + private volatile ReferenceCountData referenceCounter; + private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", ReferenceCountData.class); + private Runnable run; + private volatile Priority priority; + private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", Priority.class); + + ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask, + final Runnable run, final 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 Priority getPriorityVolatile() { + return (Priority)PRIORITY_HANDLE.getVolatile(this); + } + + private void setPriorityPlain(final Priority priority) { + PRIORITY_HANDLE.set(this, priority); + } + + private void setPriorityVolatile(final Priority priority) { + PRIORITY_HANDLE.setVolatile(this, priority); + } + + private Priority compareAndExchangePriority(final Priority expect, final Priority update) { + return (Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update); + } + + private void setReferenceCounterPlain(final ReferenceCountData value) { + REFERENCE_COUNTER_HANDLE.set(this, value); + } + + private ReferenceCountData getReferenceCounterVolatile() { + return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.get(this); + } + + private ReferenceCountData compareAndExchangeReferenceCounter(final ReferenceCountData expect, final ReferenceCountData update) { + return (ReferenceCountData)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 (ReferenceCountData 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(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 PrioritisedExecutor getExecutor() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isQueued() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean queue() { + if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) { + return false; + } + + final ReferenceCountData 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 ReferenceCountData trySetCompleting(final int minPriority) { + // first, try to set priority to EXECUTING + for (Priority curr = this.getPriorityVolatile();;) { + if (curr.isLowerPriority(minPriority)) { + return null; + } + + if (curr == (curr = this.compareAndExchangePriority(curr, Priority.COMPLETING))) { + break; + } // else: continue + } + + for (ReferenceCountData 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 ReferenceCountData referenceCount = this.getReferenceCounterVolatile(); + if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) { + // cancelled or not queued + return; + } + + if (this.getPriorityVolatile() == 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 + synchronise = true; + 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 Priority getPriority() { + return this.getPriorityVolatile(); + } + + @Override + public boolean lowerPriority(final Priority priority) { + int failures = 0; + for (Priority curr = this.getPriorityVolatile();;) { + if (curr == 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 long getSubOrder() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setSubOrder(final long subOrder) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean raiseSubOrder(final long subOrder) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean lowerSubOrder(final long subOrder) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { + return this.setPriority(priority); + } + + @Override + public boolean setPriority(final Priority priority) { + int failures = 0; + for (Priority curr = this.getPriorityVolatile();;) { + if (curr == 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 Priority priority) { + int failures = 0; + for (Priority curr = this.getPriorityVolatile();;) { + if (curr == 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); + } + } + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java new file mode 100644 index 0000000000000000000000000000000000000000..c6e487a4c14e6b82533881d01f32349b9ae28728 --- /dev/null +++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java @@ -0,0 +1,770 @@ +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.ReferenceLinkedOpenHashSet; +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.ChunkHolder; +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.ServerExplosion; +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.Set; +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 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); + } + // chunkHoldersToBroadcast + for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) { + into.chunkHoldersToBroadcast.add(chunkHolder); + } + } + + @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: 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 + } + // chunkHoldersToBroadcast + for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) { + final ChunkPos pos = chunkHolder.getPos(); + + // Possible for get() to return null, as the chunk holder is not removed during unload + final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); + if (into != null) { + into.chunkHoldersToBroadcast.add(chunkHolder); + } + } + } + }; + + 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 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 final List temporaryChunkTickList = new java.util.ArrayList<>(); + public final Set chunkHoldersToBroadcast = new ReferenceLinkedOpenHashSet<>(); + + // 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 io.papermc.paper.redstone.RedstoneWireTurbo turbo; + + public RegionizedWorldData(final ServerLevel world) { + this.world = world; + this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, true); + this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, 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 io.papermc.paper.redstone.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 - BlockPhysicsEvent + this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent + this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers + // 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) { + this.loadedEntities.add(entity); + } + + public boolean hasLoadedEntity(final Entity entity) { + return this.loadedEntities.contains(entity); + } + + public void removeLoadedEntity(final Entity entity) { + this.loadedEntities.remove(entity); + } + + public Iterable getLoadedEntities() { + return this.loadedEntities; + } + + 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(); + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/Schedule.java b/io/papermc/paper/threadedregions/Schedule.java new file mode 100644 index 0000000000000000000000000000000000000000..820b1c4dc1b19ee8602333295f2034362f885a37 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/TeleportUtils.java b/io/papermc/paper/threadedregions/TeleportUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..2a64a5b2cf049661fe3f5a22ddfa39979624f5ec --- /dev/null +++ b/io/papermc/paper/threadedregions/TeleportUtils.java @@ -0,0 +1,82 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; +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 T from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch, + final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) { + teleport(from, useFromRootVehicle, to, yaw, pitch, teleportFlags, cause, onComplete, null); + } + + public static void teleport(final T from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch, + final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete, + final java.util.function.Predicate preTeleport) { + // retrieve coordinates + final CallbackCompletable positionCompletable = new CallbackCompletable<>(); + + 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 T realFrom) -> { + final Vec3 pos = new Vec3( + loc.getX(), loc.getY(), loc.getZ() + ); + if (preTeleport != null && !preTeleport.test(realFrom)) { + if (onComplete != null) { + onComplete.accept(null); + } + return; + } + (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() {} +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/io/papermc/paper/threadedregions/ThreadedRegionizer.java new file mode 100644 index 0000000000000000000000000000000000000000..604385af903845d966382ad0a4168798e4ed4a0e --- /dev/null +++ b/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 release 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); + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/TickData.java b/io/papermc/paper/threadedregions/TickData.java new file mode 100644 index 0000000000000000000000000000000000000000..d4d80a69488f57704f1b3dc74cb379de36e80ec0 --- /dev/null +++ b/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 + ) {} +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..18f57216522a8e22ea6c217c05588f8be13f5c88 --- /dev/null +++ b/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). + */ + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java index 8424cf9d4617b4732d44cc460d25b04481068989..df15b1139e71dfe10b8f24ec6d235b99f6d5006a 100644 --- a/io/papermc/paper/threadedregions/TickRegions.java +++ b/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/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/io/papermc/paper/threadedregions/commands/CommandServerHealth.java new file mode 100644 index 0000000000000000000000000000000000000000..ebe2592a78e996fa2d415663bd6436effec1ca29 --- /dev/null +++ b/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<>(); + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/commands/CommandUtil.java b/io/papermc/paper/threadedregions/commands/CommandUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..1054e28e54f55e0a70eb25aee89cbb4898446fa5 --- /dev/null +++ b/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() {} +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java b/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java new file mode 100644 index 0000000000000000000000000000000000000000..bc8aa525b3488dc71e7ca0529c6a8c57eaa99e1e --- /dev/null +++ b/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); + } + } + } + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java b/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java new file mode 100644 index 0000000000000000000000000000000000000000..14d20c996e9b25077f7e51c5d7d432c4a2b01671 --- /dev/null +++ b/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java @@ -0,0 +1,79 @@ +package io.papermc.paper.threadedregions.util; + +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.levelgen.BitRandomSource; +import net.minecraft.world.level.levelgen.PositionalRandomFactory; +import java.util.concurrent.ThreadLocalRandom; + +public final class SimpleThreadLocalRandomSource implements BitRandomSource { + + public static final SimpleThreadLocalRandomSource INSTANCE = new SimpleThreadLocalRandomSource(); + + private final PositionalRandomFactory positionalRandomFactory = new SimpleThreadLocalRandomSource.SimpleThreadLocalRandomPositionalRandomFactory(); + + private SimpleThreadLocalRandomSource() {} + + @Override + public int next(final int bits) { + return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits); + } + + @Override + public int nextInt() { + return ThreadLocalRandom.current().nextInt(); + } + + @Override + public int nextInt(final int bound) { + if (bound <= 0) { + throw new IllegalArgumentException(); + } + + // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ + final long value = (long)this.nextInt() & 0xFFFFFFFFL; + return (int)((value * (long)bound) >>> Integer.SIZE); + } + + @Override + public void setSeed(final long seed) { + // no-op + } + + @Override + public double nextGaussian() { + return ThreadLocalRandom.current().nextGaussian(); + } + + @Override + public RandomSource fork() { + return this; + } + + @Override + public PositionalRandomFactory forkPositional() { + return this.positionalRandomFactory; + } + + private static final class SimpleThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory { + + @Override + public RandomSource fromHashOf(final String seed) { + return SimpleThreadLocalRandomSource.INSTANCE; + } + + @Override + public RandomSource fromSeed(final long seed) { + return SimpleThreadLocalRandomSource.INSTANCE; + } + + @Override + public RandomSource at(final int x, final int y, final int z) { + return SimpleThreadLocalRandomSource.INSTANCE; + } + + @Override + public void parityConfigString(final StringBuilder info) { + info.append("SimpleThreadLocalRandomPositionalRandomFactory{}"); + } + } +} \ No newline at end of file diff --git a/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java b/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java new file mode 100644 index 0000000000000000000000000000000000000000..c167179775ec09877808d91eb04b3cdb688c00a4 --- /dev/null +++ b/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java @@ -0,0 +1,73 @@ +package io.papermc.paper.threadedregions.util; + +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.levelgen.BitRandomSource; +import net.minecraft.world.level.levelgen.PositionalRandomFactory; +import java.util.concurrent.ThreadLocalRandom; + +public final class ThreadLocalRandomSource implements BitRandomSource { + + public static final ThreadLocalRandomSource INSTANCE = new ThreadLocalRandomSource(); + + private final PositionalRandomFactory positionalRandomFactory = new ThreadLocalRandomPositionalRandomFactory(); + + private ThreadLocalRandomSource() {} + + @Override + public int next(final int bits) { + return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits); + } + + @Override + public int nextInt() { + return ThreadLocalRandom.current().nextInt(); + } + + @Override + public int nextInt(final int bound) { + return ThreadLocalRandom.current().nextInt(bound); + } + + @Override + public void setSeed(final long seed) { + // no-op + } + + @Override + public double nextGaussian() { + return ThreadLocalRandom.current().nextGaussian(); + } + + @Override + public RandomSource fork() { + return this; + } + + @Override + public PositionalRandomFactory forkPositional() { + return this.positionalRandomFactory; + } + + private static final class ThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory { + + @Override + public RandomSource fromHashOf(final String seed) { + return ThreadLocalRandomSource.INSTANCE; + } + + @Override + public RandomSource fromSeed(final long seed) { + return ThreadLocalRandomSource.INSTANCE; + } + + @Override + public RandomSource at(final int x, final int y, final int z) { + return ThreadLocalRandomSource.INSTANCE; + } + + @Override + public void parityConfigString(final StringBuilder info) { + info.append("ThreadLocalRandomPositionalRandomFactory{}"); + } + } +} \ No newline at end of file diff --git a/net/minecraft/commands/CommandSourceStack.java b/net/minecraft/commands/CommandSourceStack.java index c2b7164a1395842ab95428540782eeda4c7960b0..d5eefed0912c728ded360ddac4d9bcd1813730b2 100644 --- a/net/minecraft/commands/CommandSourceStack.java +++ b/net/minecraft/commands/CommandSourceStack.java @@ -91,7 +91,7 @@ public class CommandSourceStack implements ExecutionCommandSource { io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);}) // Folia - region threading ); } diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java index f8969a68cd352ce4fe5109205e78f5e19ab6e020..009e6405a11a391adca41a7c4ecafbf3254d799d 100644 --- a/net/minecraft/commands/Commands.java +++ b/net/minecraft/commands/Commands.java @@ -158,13 +158,13 @@ public class Commands { AdvancementCommands.register(this.dispatcher); AttributeCommand.register(this.dispatcher, context); ExecuteCommand.register(this.dispatcher, context); - BossBarCommands.register(this.dispatcher, context); + //BossBarCommands.register(this.dispatcher, context); // Folia - region threading - TODO ClearInventoryCommands.register(this.dispatcher, context); - CloneCommands.register(this.dispatcher, context); + //CloneCommands.register(this.dispatcher, context); // Folia - region threading - TODO DamageCommand.register(this.dispatcher, context); - 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, context); @@ -174,47 +174,47 @@ public class Commands { FillCommand.register(this.dispatcher, context); FillBiomeCommand.register(this.dispatcher, context); ForceLoadCommand.register(this.dispatcher); - FunctionCommand.register(this.dispatcher); + //FunctionCommand.register(this.dispatcher); // Folia - region threading - TODO GameModeCommand.register(this.dispatcher); GameRuleCommand.register(this.dispatcher, context); GiveCommand.register(this.dispatcher, context); HelpCommand.register(this.dispatcher); - ItemCommands.register(this.dispatcher, context); + //ItemCommands.register(this.dispatcher, context); // Folia - region threading - TODO later KickCommand.register(this.dispatcher); KillCommand.register(this.dispatcher); ListPlayersCommand.register(this.dispatcher); LocateCommand.register(this.dispatcher, context); - LootCommand.register(this.dispatcher, context); + //LootCommand.register(this.dispatcher, context); // Folia - region threading - TODO later MsgCommand.register(this.dispatcher); ParticleCommand.register(this.dispatcher, context); 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); - RotateCommand.register(this.dispatcher); + //ReturnCommand.register(this.dispatcher); // Folia - region threading - TODO later + //RideCommand.register(this.dispatcher); // Folia - region threading - TODO later + //RotateCommand.register(this.dispatcher); // Folia - region threading - TODO later SayCommand.register(this.dispatcher); - ScheduleCommand.register(this.dispatcher); - ScoreboardCommand.register(this.dispatcher, context); + //ScheduleCommand.register(this.dispatcher); // Folia - region threading + //ScoreboardCommand.register(this.dispatcher, context); // Folia - region threading SeedCommand.register(this.dispatcher, selection != Commands.CommandSelection.INTEGRATED); SetBlockCommand.register(this.dispatcher, context); 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, context); - TagCommand.register(this.dispatcher); - TeamCommand.register(this.dispatcher, context); - TeamMsgCommand.register(this.dispatcher); + //TagCommand.register(this.dispatcher); // Folia - region threading - TODO later + //TeamCommand.register(this.dispatcher, context); // Folia - region threading - TODO later + //TeamMsgCommand.register(this.dispatcher); // Folia - region threading - TODO later TeleportCommand.register(this.dispatcher); TellRawCommand.register(this.dispatcher, context); - TickCommand.register(this.dispatcher); + //TickCommand.register(this.dispatcher); // Folia - region threading - TODO later TimeCommand.register(this.dispatcher); TitleCommand.register(this.dispatcher, context); - 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); @@ -495,9 +495,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((ServerPlayer updatedPlayer) -> { + runSync((ServerPlayer)updatedPlayer, bukkit, rootCommandNode); + }, null, 1L); + // Folia end - region threading } private void runSync(ServerPlayer player, java.util.Collection bukkit, RootCommandNode rootCommandNode) { diff --git a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java index 4a881636ba21fae9e50950bbba2b4321b71d35ab..af35d667f7dc752df34c49fe675cd0a6cf8ffe4b 100644 --- a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +++ b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java @@ -46,7 +46,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading serverLevel.getCraftServer().getPluginManager().callEvent(event); } diff --git a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java index bd5bbc7e55c6bea77991fe5a3c0c2580313d16c5..907d3a5385b8b9098051f4ec0887d778fb85cf8d 100644 --- a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +++ b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java @@ -78,7 +78,7 @@ public class DefaultDispenseItemBehavior implements DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack); org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(itemEntity.getDeltaMovement())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading level.getCraftServer().getPluginManager().callEvent(event); } diff --git a/net/minecraft/core/dispenser/DispenseItemBehavior.java b/net/minecraft/core/dispenser/DispenseItemBehavior.java index 717c84165d5e25cd384f56b7cb976abf6669b6f0..ebcd1949266f29ca0c99ee26252c366c3f887546 100644 --- a/net/minecraft/core/dispenser/DispenseItemBehavior.java +++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java @@ -89,7 +89,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading serverLevel.getCraftServer().getPluginManager().callEvent(event); } @@ -147,7 +147,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading serverLevel.getCraftServer().getPluginManager().callEvent(event); } @@ -201,7 +201,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } @@ -251,7 +251,7 @@ public interface DispenseItemBehavior { org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockSource.pos()); org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleCopy); org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), abstractChestedHorse.getBukkitLivingEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } @@ -329,7 +329,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading level.getCraftServer().getPluginManager().callEvent(event); } @@ -389,7 +389,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading levelAccessor.getMinecraftWorld().getCraftServer().getPluginManager().callEvent(event); } @@ -425,7 +425,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading serverLevel.getCraftServer().getPluginManager().callEvent(event); } @@ -482,7 +482,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading level.getCraftServer().getPluginManager().callEvent(event); } @@ -500,7 +500,8 @@ public interface DispenseItemBehavior { } } - level.captureTreeGeneration = true; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + worldData.captureTreeGeneration = true; // Folia - region threading // CraftBukkit end if (!BoneMealItem.growCrop(item, level, blockPos) && !BoneMealItem.growWaterPlant(item, level, blockPos, null)) { this.setSuccess(false); @@ -508,13 +509,13 @@ public interface DispenseItemBehavior { level.levelEvent(1505, blockPos, 15); } // CraftBukkit start - level.captureTreeGeneration = false; - if (level.capturedBlockStates.size() > 0) { - org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; - net.minecraft.world.level.block.SaplingBlock.treeType = null; + worldData.captureTreeGeneration = false; // Folia - region threading + if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading + org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // Folia - region threading + net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); // Folia - region threading org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(blockPos, level.getWorld()); - List blocks = new java.util.ArrayList<>(level.capturedBlockStates.values()); - level.capturedBlockStates.clear(); + List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading org.bukkit.event.world.StructureGrowEvent structureEvent = null; if (treeType != null) { structureEvent = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks); @@ -548,7 +549,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockPos.getX() + 0.5D, (double) blockPos.getY(), (double) blockPos.getZ() + 0.5D)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading level.getCraftServer().getPluginManager().callEvent(event); } @@ -591,7 +592,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading level.getCraftServer().getPluginManager().callEvent(event); } @@ -644,7 +645,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading level.getCraftServer().getPluginManager().callEvent(event); } @@ -702,7 +703,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - only single item in event org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading serverLevel.getCraftServer().getPluginManager().callEvent(event); } @@ -783,7 +784,7 @@ public interface DispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading serverLevel.getCraftServer().getPluginManager().callEvent(event); } diff --git a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java index 3595bbd05fb3e8fe57e38d4e2df5c6237046b726..9bcb803b761aef0bf29a76bd4bea22f22cbeda5d 100644 --- a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java +++ b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java @@ -39,7 +39,7 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack); org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) livingEntity.getBukkitEntity()); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading world.getCraftServer().getPluginManager().callEvent(event); } diff --git a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java index 116395b6c00a0814922516707544a9ff26d68835..26c326080ee6fc80f0cc6af3e9fcbc1a508ba01a 100644 --- a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java +++ b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java @@ -62,7 +62,7 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1); org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(vec31.x, vec31.y, vec31.z)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading serverLevel.getCraftServer().getPluginManager().callEvent(event); } diff --git a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java index 449d9b72ff4650961daa9d1bd25940f3914a6b12..b4f2dbe3dcdeac2a297b7909cedd54a8079938d8 100644 --- a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java +++ b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java @@ -32,7 +32,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1); org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) direction.getStepX(), (double) direction.getStepY(), (double) direction.getStepZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading serverLevel.getCraftServer().getPluginManager().callEvent(event); } diff --git a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java index 626e9feb6a6e7a2cbc7c63e30ba4fb6b923e85c7..eb63e114b666128df924dca46235ea8a7edbae54 100644 --- a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +++ b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java @@ -25,7 +25,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos()); org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading serverLevel.getCraftServer().getPluginManager().callEvent(event); } diff --git a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java index 5ab2c8333178335515e619b87ae420f948c83bd1..172f41f15e3f165b8faca85e7bc581082d330041 100644 --- a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +++ b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java @@ -27,7 +27,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockPos.getX(), blockPos.getY(), blockPos.getZ())); - if (!DispenserBlock.eventFired) { + if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading blockSource.level().getCraftServer().getPluginManager().callEvent(event); } diff --git a/net/minecraft/gametest/framework/GameTestHelper.java b/net/minecraft/gametest/framework/GameTestHelper.java index fe4ae6bcdcbb55c47e9f9a4d63ead4c39e6d63cf..36a5ca39214233f37ef7bfeb47331a7deb566e5c 100644 --- a/net/minecraft/gametest/framework/GameTestHelper.java +++ b/net/minecraft/gametest/framework/GameTestHelper.java @@ -306,7 +306,7 @@ public class GameTestHelper { }; Connection connection = new Connection(PacketFlow.SERVERBOUND); new EmbeddedChannel(connection); - this.getLevel().getServer().getPlayerList().placeNewPlayer(connection, serverPlayer, commonListenerCookie); + if (true) throw new UnsupportedOperationException(); // Folia - region threading return serverPlayer; } diff --git a/net/minecraft/gametest/framework/GameTestServer.java b/net/minecraft/gametest/framework/GameTestServer.java index 54ca624a8194e7d1c0f3b1c0ddba81165523382c..a8cc20bfad1790f254c4793f09fc4dd3ddd4f25b 100644 --- a/net/minecraft/gametest/framework/GameTestServer.java +++ b/net/minecraft/gametest/framework/GameTestServer.java @@ -175,8 +175,12 @@ public class GameTestServer extends MinecraftServer { } @Override - public void tickServer(BooleanSupplier hasTimeLeft) { - super.tickServer(hasTimeLeft); + // Folia start - region threading + public void tickServer(long startTime, long scheduledEnd, long targetBuffer, + io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { + if (true) throw new UnsupportedOperationException(); + super.tickServer(startTime, scheduledEnd, targetBuffer, region); + // Folia end - region threading ServerLevel serverLevel = this.overworld(); if (!this.haveTestsStarted()) { this.startTests(serverLevel); diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java index 4ed9611994c5c8da01fede690197527c5b3a5731..6caa695417945dc5a534755d6590555e0e5c50d2 100644 --- a/net/minecraft/network/Connection.java +++ b/net/minecraft/network/Connection.java @@ -85,7 +85,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 - Optimize network + private final Queue pendingActions = new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); // Paper - Optimize network // Folia - region threading - connection fixes public Channel channel; public SocketAddress address; // Spigot start @@ -100,7 +100,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; @@ -154,6 +154,41 @@ public class Connection extends SimpleChannelInboundHandler> { this.receiving = receiving; } + // Folia start - region threading + private volatile boolean becomeActive; + + public boolean becomeActive() { + return this.becomeActive; + } + + private static record DisconnectReq(DisconnectionDetails 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(DisconnectionDetails 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. + } + + /** + * 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(new DisconnectionDetails(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 + @Override public void channelActive(ChannelHandlerContext context) throws Exception { super.channelActive(context); @@ -163,6 +198,7 @@ public class Connection extends SimpleChannelInboundHandler> { if (this.delayedDisconnect != null) { this.disconnect(this.delayedDisconnect); } + this.becomeActive = true; // Folia - region threading } @Override @@ -434,7 +470,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, listener, flush); @@ -463,11 +499,12 @@ public class Connection extends SimpleChannelInboundHandler> { } public void runOnceConnected(Consumer action) { - if (this.isConnected()) { + if (false && this.isConnected()) { // Folia - region threading - connection fixes this.flushQueue(); action.accept(this); } else { this.pendingActions.add(new WrappedConsumer(action)); // Paper - Optimize network + this.flushQueue(); // Folia - region threading - connection fixes } } @@ -518,10 +555,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 } } @@ -535,53 +573,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 @@ -590,17 +636,37 @@ 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.reason()); + // 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 if (this.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 { @@ -611,7 +677,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(); } @@ -662,6 +728,7 @@ public class Connection extends SimpleChannelInboundHandler> { this.channel.close(); // We can't wait as this may be called from an event loop. this.disconnectionDetails = disconnectionDetails; } + this.becomeActive = true; // Folia - region threading } public boolean isMemoryConnection() { @@ -853,10 +920,10 @@ public class Connection extends SimpleChannelInboundHandler> { public void handleDisconnection() { if (this.channel != null && !this.channel.isOpen()) { - if (this.disconnectionHandled) { + if (!this.disconnectionHandled.compareAndSet(false, true)) { // Folia - region threading - may be called concurrently during configuration stage // LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message } else { - this.disconnectionHandled = true; + //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; if (packetListener1 != null) { @@ -885,6 +952,21 @@ 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 } } } @@ -904,15 +986,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/net/minecraft/network/protocol/PacketUtils.java b/net/minecraft/network/protocol/PacketUtils.java index 4535858701b2bb232b9d2feb2af6551526232ddc..b28ff2f18ab7e0e3a61e37ee46048ab5cb7ab45d 100644 --- a/net/minecraft/network/protocol/PacketUtils.java +++ b/net/minecraft/network/protocol/PacketUtils.java @@ -20,7 +20,7 @@ public class PacketUtils { public static void ensureRunningOnSameThread(Packet packet, T processor, BlockableEventLoop executor) throws RunningOnDifferentThreadException { if (!executor.isSameThread()) { - executor.executeIfPossible(() -> { + Runnable run = () -> { // Folia - region threading packetProcessing.push(processor); // Paper - detailed watchdog information try { // Paper - detailed watchdog information if (processor instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // Paper - Don't handle sync packets for kicked players @@ -43,7 +43,24 @@ public class PacketUtils { packetProcessing.pop(); } // 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 (processor instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) { + gamePacketListener.player.getBukkitEntity().taskScheduler.schedule( + (net.minecraft.server.level.ServerPlayer player) -> { + run.run(); + }, + null, 1L + ); + } else if (processor instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); + } else if (processor instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); + } else { + throw new UnsupportedOperationException("Unknown listener: " + processor); + } + // Folia end - region threading throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; } } diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java index ae220a732c78ab076261f20b5a54c71d7fceb407..9c9de462eb7187d6cc3562c796e3bcf69fb20783 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -184,7 +184,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); public int autosavePeriod; // Paper - don't store the vanilla dispatcher @@ -304,6 +303,50 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation + // Folia start - regionised ticking + public final io.papermc.paper.threadedregions.RegionizedServer regionizedServer = new io.papermc.paper.threadedregions.RegionizedServer(); + + @Override + public CompletableFuture submit(java.util.function.Supplier task) { + if (true) { + throw new UnsupportedOperationException(); + } + return super.submit(task); + } + + @Override + public CompletableFuture submit(Runnable task) { + if (true) { + throw new UnsupportedOperationException(); + } + return super.submit(task); + } + + @Override + public void schedule(TickTask task) { + if (true) { + throw new UnsupportedOperationException(); + } + super.schedule(task); + } + + @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); + } + // Folia end - regionised ticking + public static S spin(Function threadFunction) { ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system AtomicReference atomicReference = new AtomicReference<>(); @@ -332,46 +375,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 @@ -384,7 +411,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> 4; + serverLevel.randomSpawnSelection = new ChunkPos(serverLevel.getChunkSource().randomState().sampler().findSpawnPosition()); + for (int currX = -loadRegionRadius; currX <= loadRegionRadius; ++currX) { + for (int currZ = -loadRegionRadius; currZ <= loadRegionRadius; ++currZ) { + ChunkPos pos = new ChunkPos(currX, currZ); + serverLevel.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(serverLevel); @@ -723,6 +764,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 ? Mth.square(ChunkProgressListener.calculateDiameter(_int)) : 0; - while (chunkSource.getTickingGenerated() < i) { - // CraftBukkit start - // this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS; - this.executeModerately(); - } + // Folia - region threading // this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS; - this.executeModerately(); + //this.executeModerately(); // Folia - region threading if (true) { ServerLevel serverLevel1 = serverLevel; @@ -895,7 +934,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop false : this::haveTime); + if (true) throw new UnsupportedOperationException(); // Folia - region threading // Paper start - rewrite chunk system final Throwable crash = this.chunkSystemCrash; if (crash != null) { @@ -1403,28 +1497,24 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; - } - // Paper end - return new TickTask(this.tickCount, runnable); + throw new UnsupportedOperationException(); // Folia - region threading } @Override protected boolean shouldRun(TickTask runnable) { - return runnable.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; return flag; } private boolean pollTaskInternal() { + if (true) throw new UnsupportedOperationException(); // Folia - region threading if (super.pollTask()) { this.moonrise$executeMidTickTasks(); // Paper - rewrite chunk system return true; @@ -1444,6 +1534,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { + if (false && i > 0) { // Folia - region threading - this is complicated to implement, and even if done correctly is messy if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting() && this.pluginsBlockingSleep.isEmpty()) { // Paper - API to allow/disallow tick sleeping this.emptyTicks++; } else { @@ -1515,24 +1609,58 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop true, false); } // Paper end - avoid issues with certain tasks not processing during sleep - this.server.spark.executeMainThreadTasks(); // Paper - spark + //this.server.spark.executeMainThreadTasks(); // Paper - spark // Folia - region threading this.tickConnection(); this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark return; } } + // Folia start - region threading + region.world.getCurrentWorldData().updateTickData(); + if (region.world.checkInitialised.get() != ServerLevel.WORLD_INIT_CHECKED) { + synchronized (region.world.checkInitialised) { + if (region.world.checkInitialised.compareAndSet(ServerLevel.WORLD_INIT_NOT_CHECKED, ServerLevel.WORLD_INIT_CHECKING)) { + LOGGER.info("Initialising world '" + region.world.getWorld().getName() + "' before it can be ticked..."); + this.initWorld(region.world, region.world.serverLevelData, worldData, region.world.serverLevelData.worldGenOptions()); // Folia - delayed until first tick of world + region.world.checkInitialised.set(ServerLevel.WORLD_INIT_CHECKED); + LOGGER.info("Initialised world '" + region.world.getWorld().getName() + "'"); + } // else: must be checked + } + } + BooleanSupplier hasTimeLeft = () -> { + return scheduledEnd - System.nanoTime() > targetBuffer; + }; + // Folia end - region threading + this.server.spark.tickStart(); // Paper - spark - new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events - this.tickCount++; - this.tickRateManager.tick(); - this.tickChildren(hasTimeLeft); - if (nanos - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) { + new com.destroystokyo.paper.event.server.ServerTickStartEvent((int)region.getCurrentTick()).callEvent(); // Paper - Server Tick Events // Folia - region threading + // Folia start - region threading + if (region != null) { + region.getTaskQueueData().drainTasks(); + ((io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler)org.bukkit.Bukkit.getRegionScheduler()).tick(); + // now run all the entity schedulers + // TODO there has got to be a more efficient variant of this crap + for (net.minecraft.world.entity.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++; // Folia - region threading + //this.tickRateManager.tick(); // Folia - region threading + this.tickChildren(hasTimeLeft, region); // Folia - region threading + if (false && nanos - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) { // Folia - region threading this.lastServerStatus = nanos; this.status = this.buildServerStatus(); } - this.ticksUntilAutosave--; + //this.ticksUntilAutosave--; // Folia - region threading // Paper start - Incremental chunk and player saving final ProfilerFiller profiler = Profiler.get(); int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate; @@ -1540,15 +1668,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 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 (final ServerLevel level : this.getAllLevels()) { + for (final 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 { @@ -1558,32 +1686,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop players = this.playerList.getPlayers(); + List players = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading int maxPlayers = this.getMaxPlayers(); if (this.hidesOnlinePlayers()) { return new ServerStatus.Players(maxPlayers, players.size(), List.of()); @@ -1653,44 +1760,34 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop serverPlayer1.connection.suspendFlushing()); - this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit + //this.getPlayerList().getPlayers().forEach(serverPlayer1 -> serverPlayer1.connection.suspendFlushing()); // Folia - region threading + //this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit // Folia - region threading // Paper start - Folia scheduler API - ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) org.bukkit.Bukkit.getGlobalRegionScheduler()).tick(); - getAllLevels().forEach(level -> { - for (final net.minecraft.world.entity.Entity entity : level.getEntities().getAll()) { - if (entity.isRemoved()) { - continue; - } - final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); - if (bukkit != null) { - bukkit.taskScheduler.executeTick(); - } - } - }); + // Folia - region threading - moved to global tick - and moved entity scheduler to tickRegion // Paper end - Folia scheduler API - io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper + //io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper // Folia - region threading - moved to global tick profilerFiller.push("commandFunctions"); - this.getFunctions().tick(); + //this.getFunctions().tick(); // Folia - region threading - TODO Purge functions profilerFiller.popPush("levels"); // CraftBukkit start // Run tasks that are waiting on processing - while (!this.processQueue.isEmpty()) { + if (false) while (!this.processQueue.isEmpty()) { // Folia - region threading this.processQueue.remove().run(); } // Send time updates to everyone, it will get the right time from the world the player is in. // Paper start - Perf: Optimize time updates - for (final ServerLevel level : this.getAllLevels()) { + for (final ServerLevel level : Arrays.asList(region.world)) { // Folia - region threading final boolean doDaylight = level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT); final long dayTime = level.getDayTime(); long worldTime = level.getGameTime(); final ClientboundSetTimePacket worldPacket = new ClientboundSetTimePacket(worldTime, dayTime, doDaylight); - for (Player entityhuman : level.players()) { - if (!(entityhuman instanceof ServerPlayer) || (tickCount + entityhuman.getId()) % 20 != 0) { + for (Player entityhuman : level.getLocalPlayers()) { // Folia - region threading + if (!(entityhuman instanceof ServerPlayer) || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + entityhuman.getId()) % 20 != 0) { // Folia - region threading continue; } ServerPlayer entityplayer = (ServerPlayer) entityhuman; @@ -1703,12 +1800,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent - serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent - serverLevel.updateLagCompensationTick(); // Paper - lag compensation - net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = serverLevel.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers + //this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked // Folia - region threading + for (ServerLevel serverLevel : Arrays.asList(region.world)) { // Folia - region threading + // Folia - region threading profilerFiller.push(() -> serverLevel + " " + serverLevel.dimension().location()); /* Drop global time updates if (this.tickCount % 20 == 0) { @@ -1721,7 +1815,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().invalidateStatus(); + }); + return; + } + // Folia end - region threading this.lastServerStatus = 0L; } @@ -2142,6 +2245,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20; + return false; // Folia - region threading } public void addPluginAllowingSleep(final String pluginName, final boolean value) { - if (!value) { - this.pluginsBlockingSleep.add(pluginName); - } else { - this.pluginsBlockingSleep.remove(pluginName); - } + // Folia - region threading } private void removeDisabledPluginsBlockingSleep() { - if (this.pluginsBlockingSleep.isEmpty()) { - return; - } - this.pluginsBlockingSleep.removeIf(plugin -> ( - !io.papermc.paper.plugin.manager.PaperPluginManagerImpl.getInstance().isPluginEnabled(plugin) - )); + // Folia - region threading } // Paper end - API to check if the server is sleeping } diff --git a/net/minecraft/server/commands/AdvancementCommands.java b/net/minecraft/server/commands/AdvancementCommands.java index 9157c1efef669795c8408d2e344a2bfeeabeb842..7873f11d7462ef88b5ba27d99988ac9e45689d3a 100644 --- a/net/minecraft/server/commands/AdvancementCommands.java +++ b/net/minecraft/server/commands/AdvancementCommands.java @@ -246,7 +246,12 @@ public class AdvancementCommands { int i = 0; for (ServerPlayer serverPlayer : targets) { - i += action.perform(serverPlayer, advancements); + // Folia start - region threading + i += 1; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + action.perform(player, advancements); + }, null, 1L); + // Folia end - region threading } if (i == 0) { @@ -310,9 +315,12 @@ public class AdvancementCommands { throw ERROR_CRITERION_NOT_FOUND.create(Advancement.name(advancement), criterionName); } else { for (ServerPlayer serverPlayer : targets) { - if (action.performCriterion(serverPlayer, advancement, criterionName)) { - i++; - } + // Folia start - region threading + ++i; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + action.performCriterion(player, advancement, criterionName); + }, null, 1L); + // Folia end - region threading } if (i == 0) { diff --git a/net/minecraft/server/commands/AttributeCommand.java b/net/minecraft/server/commands/AttributeCommand.java index 2f0e8b2b1dda17cf861f80f8c1e655a345b76d10..505f0ce1f7b453d7e30e07c13a6b7678e12b0fda 100644 --- a/net/minecraft/server/commands/AttributeCommand.java +++ b/net/minecraft/server/commands/AttributeCommand.java @@ -266,30 +266,62 @@ 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 entity, Holder attribute, double scale) throws CommandSyntaxException { - LivingEntity entityWithAttribute = getEntityWithAttribute(entity, attribute); + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + LivingEntity entityWithAttribute = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading double attributeValue = entityWithAttribute.getAttributeValue(attribute); source.sendSuccess( - () -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), entity.getName(), attributeValue), false + () -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), attributeValue), false // Folia - region threading ); - return (int)(attributeValue * scale); + 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 entity, Holder attribute, double scale) throws CommandSyntaxException { - LivingEntity entityWithAttribute = getEntityWithAttribute(entity, attribute); + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + LivingEntity entityWithAttribute = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading double attributeBaseValue = entityWithAttribute.getAttributeBaseValue(attribute); source.sendSuccess( - () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), entity.getName(), attributeBaseValue), + () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), attributeBaseValue), // Folia - region threading false ); - return (int)(attributeBaseValue * scale); + 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 entity, Holder attribute, ResourceLocation id, double scale) throws CommandSyntaxException { - LivingEntity entityWithAttribute = getEntityWithAttribute(entity, attribute); + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + LivingEntity entityWithAttribute = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading AttributeMap attributes = entityWithAttribute.getAttributes(); if (!attributes.hasModifier(attribute, id)) { - throw ERROR_NO_SUCH_MODIFIER.create(entity.getName(), getAttributeDescription(attribute), id); + throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading } else { double modifierValue = attributes.getModifierValue(attribute, id); source.sendSuccess( @@ -297,13 +329,20 @@ public class AttributeCommand { "commands.attribute.modifier.value.get.success", Component.translationArg(id), getAttributeDescription(attribute), - entity.getName(), + nmsEntity.getName(), // Folia - region threading modifierValue ), false ); - return (int)(modifierValue * scale); + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }, null, 1L); + return 0; + // Folia end - region threading } private static Stream getAttributeModifiers(Entity entity, Holder attribute) throws CommandSyntaxException { @@ -312,11 +351,22 @@ public class AttributeCommand { } private static int setAttributeBase(CommandSourceStack source, Entity entity, Holder attribute, double value) throws CommandSyntaxException { - getAttributeInstance(entity, attribute).setBaseValue(value); + // Folia start - region threading + entity.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), entity.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 resetAttributeBase(CommandSourceStack source, Entity entity, Holder attribute) throws CommandSyntaxException { @@ -338,35 +388,57 @@ public class AttributeCommand { private static int addModifier( CommandSourceStack source, Entity entity, Holder attribute, ResourceLocation id, double amount, AttributeModifier.Operation operation ) throws CommandSyntaxException { - AttributeInstance attributeInstance = getAttributeInstance(entity, attribute); + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + try { + // Folia end - region threading + AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); // Folia - region threading AttributeModifier attributeModifier = new AttributeModifier(id, amount, operation); if (attributeInstance.hasModifier(id)) { - throw ERROR_MODIFIER_ALREADY_PRESENT.create(entity.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), entity.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 entity, Holder attribute, ResourceLocation id) throws CommandSyntaxException { - AttributeInstance attributeInstance = getAttributeInstance(entity, attribute); + // Folia start - region threading + entity.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), entity.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(entity.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/net/minecraft/server/commands/ClearInventoryCommands.java b/net/minecraft/server/commands/ClearInventoryCommands.java index 73650c835ae3a8709d21462bc91a466167cd115f..3cbeaf2046bb0a41085a00134e69162df46d2081 100644 --- a/net/minecraft/server/commands/ClearInventoryCommands.java +++ b/net/minecraft/server/commands/ClearInventoryCommands.java @@ -65,9 +65,14 @@ public class ClearInventoryCommands { int i = 0; for (ServerPlayer serverPlayer : targetPlayers) { - i += serverPlayer.getInventory().clearOrCountMatchingItems(itemPredicate, 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(itemPredicate, maxCount, player.inventoryMenu.getCraftSlots()); + player.containerMenu.broadcastChanges(); + player.inventoryMenu.slotsChanged(player.getInventory()); + }, null, 1L); + // Folia end - region threading } if (i == 0) { diff --git a/net/minecraft/server/commands/DamageCommand.java b/net/minecraft/server/commands/DamageCommand.java index d99602f2c7e5463243dfaf83ada12c1d8e7d1192..5f3c886e2bc8a23e902cf8037ac8c871a601883f 100644 --- a/net/minecraft/server/commands/DamageCommand.java +++ b/net/minecraft/server/commands/DamageCommand.java @@ -102,12 +102,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 damageType) throws CommandSyntaxException { - if (target.hurtServer(source.getLevel(), damageType, 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.hurtServer(source.getLevel(), damageType, 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/net/minecraft/server/commands/DefaultGameModeCommands.java b/net/minecraft/server/commands/DefaultGameModeCommands.java index fd42373ccfedf28ffc0fcf9b3153e5a308c561c5..8a8e51c6a63858df2eae4176df75a66636d3f458 100644 --- a/net/minecraft/server/commands/DefaultGameModeCommands.java +++ b/net/minecraft/server/commands/DefaultGameModeCommands.java @@ -28,12 +28,14 @@ public class DefaultGameModeCommands { GameType forcedGameType = server.getForcedGameType(); if (forcedGameType != null) { for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) { + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading // Paper start - Expand PlayerGameModeChangeEvent - org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); + org.bukkit.event.player.PlayerGameModeChangeEvent event = player.setGameMode(gamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); // Folia - region threading if (event != null && event.isCancelled()) { commandSource.sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false); } // Paper end - Expand PlayerGameModeChangeEvent + }, null, 1L); // Folia - region threading i++; } } diff --git a/net/minecraft/server/commands/EffectCommands.java b/net/minecraft/server/commands/EffectCommands.java index 0089ff5ca207278b829ec7530f50ec14681ab574..8d6e1dab63a6ef79d038fc6c3e9f7bf184b1d8c7 100644 --- a/net/minecraft/server/commands/EffectCommands.java +++ b/net/minecraft/server/commands/EffectCommands.java @@ -180,7 +180,12 @@ public class EffectCommands { for (Entity entity : targets) { if (entity instanceof LivingEntity) { MobEffectInstance mobEffectInstance = new MobEffectInstance(effect, i1, amplifier, false, showParticles); - if (((LivingEntity)entity).addEffect(mobEffectInstance, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + ((LivingEntity)nmsEntity).addEffect(mobEffectInstance, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading + if (true) { // CraftBukkit // Folia - region threading i++; } } @@ -210,7 +215,12 @@ public class EffectCommands { int i = 0; for (Entity entity : targets) { - if (entity instanceof LivingEntity && ((LivingEntity)entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit + if (entity instanceof LivingEntity && true) { // CraftBukkit // Folia - region threading + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + ((LivingEntity)nmsEntity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading i++; } } @@ -235,7 +245,12 @@ public class EffectCommands { int i = 0; for (Entity entity : targets) { - if (entity instanceof LivingEntity && ((LivingEntity)entity).removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit + if (entity instanceof LivingEntity && true) { // CraftBukkit // Folia - region threading + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + ((LivingEntity)nmsEntity).removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); + }, null, 1L); + // Folia end - region threading i++; } } diff --git a/net/minecraft/server/commands/EnchantCommand.java b/net/minecraft/server/commands/EnchantCommand.java index fe86823f1a02d66df143756f00ee56fb9f634475..b62ed9d5456ae2c050c4d502b10c5e50c7265b96 100644 --- a/net/minecraft/server/commands/EnchantCommand.java +++ b/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 enchantment1 = enchantment.value(); if (level > enchantment1.getMaxLevel()) { throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment1.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 mainHandItem = livingEntity.getMainHandItem(); - if (!mainHandItem.isEmpty()) { - if (enchantment1.canEnchant(mainHandItem) - && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(mainHandItem).keySet(), enchantment)) { - mainHandItem.enchant(enchantment, level); - i++; - } else if (targets.size() == 1) { - throw ERROR_INCOMPATIBLE.create(mainHandItem.getHoverName().getString()); + // Folia start - region threading + entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + try { + LivingEntity livingEntity = (LivingEntity)nmsEntity; + ItemStack mainHandItem = livingEntity.getMainHandItem(); + if (!mainHandItem.isEmpty()) { + if (enchantment1.canEnchant(mainHandItem) + && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(mainHandItem).keySet(), enchantment)) { + mainHandItem.enchant(enchantment, level); + possibleSingleDisplayName.set(livingEntity.getDisplayName()); + changed.incrementAndGet(); + } else if (targets.size() == 1) { + throw ERROR_INCOMPATIBLE.create(mainHandItem.getHoverName().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/net/minecraft/server/commands/ExperienceCommand.java b/net/minecraft/server/commands/ExperienceCommand.java index cb59af8018d3009876a47fae249885c00b6c7b57..e0d95f61e8a2841979bc9b5381dfaf7d3239beb7 100644 --- a/net/minecraft/server/commands/ExperienceCommand.java +++ b/net/minecraft/server/commands/ExperienceCommand.java @@ -131,14 +131,18 @@ public class ExperienceCommand { } private static int queryExperience(CommandSourceStack source, ServerPlayer player, ExperienceCommand.Type type) { - int i = type.query.applyAsInt(player); - source.sendSuccess(() -> Component.translatable("commands.experience.query." + type.name, player.getDisplayName(), i), false); - return i; + player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading + int i = type.query.applyAsInt(serverPlayer); // Folia - region threading + source.sendSuccess(() -> Component.translatable("commands.experience.query." + type.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 type) { for (ServerPlayer serverPlayer : targets) { - type.add.accept(serverPlayer, amount); + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading + type.add.accept(player, amount); + }, null, 1L); // Folia - region threading } if (targets.size() == 1) { @@ -157,9 +161,11 @@ public class ExperienceCommand { int i = 0; for (ServerPlayer serverPlayer : targets) { - if (type.set.test(serverPlayer, amount)) { - i++; + i++; serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading + if (type.set.test(player, amount)) { // Folia - region threading + //i++; // Folia - region threading } + }, null, 1L); // Folia - region threading } if (i == 0) { diff --git a/net/minecraft/server/commands/FillBiomeCommand.java b/net/minecraft/server/commands/FillBiomeCommand.java index bb2c8612b27bb04758c467ec6245de1236fc4de1..d5ae0eeb504b9306015de37abc59bf1a76a23837 100644 --- a/net/minecraft/server/commands/FillBiomeCommand.java +++ b/net/minecraft/server/commands/FillBiomeCommand.java @@ -107,6 +107,16 @@ public class FillBiomeCommand { return fill(level, from, to, biome, biome1 -> true, message -> {}); } + // 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 level, BlockPos from, BlockPos to, Holder biome, Predicate> filter, Consumer> messageOutput ) { @@ -118,6 +128,17 @@ public class FillBiomeCommand { if (i > _int) { return Either.right(ERROR_VOLUME_TOO_LARGE.create(_int, i)); } else { + // Folia start - region threading + int buffer = 0; // no buffer, we do not touch neighbours + level.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.util.Priority.NORMAL, + (chunks) -> { + sendMessage(messageOutput, () -> { List list = new ArrayList<>(); for (int sectionPosMinZ = SectionPos.blockToSectionCoord(boundingBox.minZ()); @@ -158,6 +179,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/net/minecraft/server/commands/FillCommand.java b/net/minecraft/server/commands/FillCommand.java index a224f8cc122fc6d79b4abd08815f58f0e6aa340b..89154adfc659afa188cd771e70087e3b1a9c98b9 100644 --- a/net/minecraft/server/commands/FillCommand.java +++ b/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 area, BlockInput newBlock, FillCommand.Mode mode, @Nullable Predicate replacingPredicate ) throws CommandSyntaxException { @@ -161,6 +167,18 @@ public class FillCommand { } else { List list = Lists.newArrayList(); ServerLevel level = source.getLevel(); + // Folia start - region threading + int buffer = 32; + // physics may spill into neighbour chunks, so use a buffer + level.moonrise$loadChunksAsync( + (area.minX() - buffer) >> 4, + (area.maxX() + buffer) >> 4, + (area.minZ() - buffer) >> 4, + (area.maxZ() + buffer) >> 4, + net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.util.Priority.NORMAL, + (chunks) -> { + try { // Folia end - region threading int i1 = 0; for (BlockPos blockPos : BlockPos.betweenClosed(area.minX(), area.minY(), area.minZ(), area.maxX(), area.maxY(), area.maxZ())) { @@ -187,8 +205,13 @@ public class FillCommand { } else { int i2 = i1; source.sendSuccess(() -> Component.translatable("commands.fill.success", i2), true); - return i1; + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); return 0; // Folia end - region threading } } diff --git a/net/minecraft/server/commands/ForceLoadCommand.java b/net/minecraft/server/commands/ForceLoadCommand.java index 619ffb7846047d3e033378c750dc4ceaf9ac6239..6e174d54a3bf6a7a23a0aa6e7802b407e3969a47 100644 --- a/net/minecraft/server/commands/ForceLoadCommand.java +++ b/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 level = source.getLevel(); ResourceKey resourceKey = level.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 level = source.getLevel(); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading ResourceKey resourceKey = level.dimension(); LongSet forcedChunks = level.getForcedChunks(); int size = forcedChunks.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 size; + return 1; // Folia - region threading } private static int removeAll(CommandSourceStack source) { ServerLevel level = source.getLevel(); ResourceKey resourceKey = level.dimension(); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading LongSet forcedChunks = level.getForcedChunks(); forcedChunks.forEach(packedChunkPos -> level.setChunkForced(ChunkPos.getX(packedChunkPos), ChunkPos.getZ(packedChunkPos), 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 add) throws CommandSyntaxException { + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { + try { + // Folia end - region threading int min = Math.min(from.x(), to.x()); int min1 = Math.min(from.z(), to.z()); int max = Math.max(from.x(), to.x()); @@ -207,11 +232,18 @@ public class ForceLoadCommand { ); } - return i2x; + 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/net/minecraft/server/commands/GameModeCommand.java b/net/minecraft/server/commands/GameModeCommand.java index c44cdbbdc06b25bd20a208386545a10af9b96df8..f6204e765afda2668ab394c570444fbb7f152b8b 100644 --- a/net/minecraft/server/commands/GameModeCommand.java +++ b/net/minecraft/server/commands/GameModeCommand.java @@ -54,15 +54,18 @@ public class GameModeCommand { int i = 0; for (ServerPlayer serverPlayer : players) { + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer nmsEntity) -> { // Folia - region threading // Paper start - Expand PlayerGameModeChangeEvent - org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); + org.bukkit.event.player.PlayerGameModeChangeEvent event = nmsEntity.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); // Folia - region threading if (event != null && !event.isCancelled()) { - logGamemodeChange(source.getSource(), serverPlayer, gameType); - i++; + logGamemodeChange(source.getSource(), nmsEntity, gameType); // Folia - region threading + //i++; // Folia - region threading } else if (event != null && event.cancelMessage() != null) { source.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/net/minecraft/server/commands/GiveCommand.java b/net/minecraft/server/commands/GiveCommand.java index 8b7af734ca4ed3cafa810460b2cea6c1e6342a69..6f5d88d83ad724fa2b7549075b687aebd4b24eed 100644 --- a/net/minecraft/server/commands/GiveCommand.java +++ b/net/minecraft/server/commands/GiveCommand.java @@ -65,32 +65,34 @@ public class GiveCommand { int min = Math.min(maxStackSize, i1); i1 -= min; ItemStack itemStack1 = item.createItemStack(min, false); - boolean flag = serverPlayer.getInventory().add(itemStack1); + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer nmsEntity) -> { // Folia - region threading + boolean flag = nmsEntity.getInventory().add(itemStack1); // Folia - region threading if (flag && itemStack1.isEmpty()) { - ItemEntity itemEntity = serverPlayer.drop(itemStack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event + ItemEntity itemEntity = nmsEntity.drop(itemStack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event // Folia - region threading if (itemEntity != null) { itemEntity.makeFakeItem(); } - serverPlayer.level() + nmsEntity.level() // Folia - region threading .playSound( null, - serverPlayer.getX(), - serverPlayer.getY(), - serverPlayer.getZ(), + nmsEntity.getX(), // Folia - region threading + nmsEntity.getY(), // Folia - region threading + nmsEntity.getZ(), // Folia - region threading SoundEvents.ITEM_PICKUP, SoundSource.PLAYERS, 0.2F, - ((serverPlayer.getRandom().nextFloat() - serverPlayer.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F + ((nmsEntity.getRandom().nextFloat() - nmsEntity.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F // Folia - region threading ); - serverPlayer.containerMenu.broadcastChanges(); + nmsEntity.containerMenu.broadcastChanges(); // Folia - region threading } else { - ItemEntity itemEntity = serverPlayer.drop(itemStack1, false); + ItemEntity itemEntity = nmsEntity.drop(itemStack1, false); // Folia - region threading if (itemEntity != null) { itemEntity.setNoPickUpDelay(); - itemEntity.setTarget(serverPlayer.getUUID()); + itemEntity.setTarget(nmsEntity.getUUID()); // Folia - region threading } } + }, null, 1L); // Folia - region threading } } diff --git a/net/minecraft/server/commands/KillCommand.java b/net/minecraft/server/commands/KillCommand.java index e8ab673921c8089a35a2e678d7a6efed1f728cd7..287681a351f49eabd4f480396314a882bee73645 100644 --- a/net/minecraft/server/commands/KillCommand.java +++ b/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(source.getLevel()); + entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { // Folia - region threading + nmsEntity.kill((net.minecraft.server.level.ServerLevel)nmsEntity.level()); // Folia - region threading + }, null, 1L); // Folia - region threading } if (targets.size() == 1) { diff --git a/net/minecraft/server/commands/PlaceCommand.java b/net/minecraft/server/commands/PlaceCommand.java index f019285714cf6e7ac08d6b3b96fe705b8a564c28..4decfa02f0fa11a14abd48944e9cb2dd86bb96a2 100644 --- a/net/minecraft/server/commands/PlaceCommand.java +++ b/net/minecraft/server/commands/PlaceCommand.java @@ -233,36 +233,79 @@ 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 level = source.getLevel(); ConfiguredFeature configuredFeature = feature.value(); ChunkPos chunkPos = new ChunkPos(pos); checkLoaded(level, new ChunkPos(chunkPos.x - 1, chunkPos.z - 1), new ChunkPos(chunkPos.x + 1, chunkPos.z + 1)); + // Folia start - region threading + level.moonrise$loadChunksAsync( + pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.util.Priority.NORMAL, + (chunks) -> { + try { + // Folia end - region threading if (!configuredFeature.place(level, level.getChunkSource().getGenerator(), level.getRandom(), pos)) { throw ERROR_FEATURE_FAILED.create(); } else { String string = feature.key().location().toString(); source.sendSuccess(() -> Component.translatable("commands.place.feature.success", string, 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 templatePool, ResourceLocation target, int maxDepth, BlockPos pos) throws CommandSyntaxException { ServerLevel level = source.getLevel(); ChunkPos chunkPos = new ChunkPos(pos); checkLoaded(level, chunkPos, chunkPos); + // Folia start - region threading + level.moonrise$loadChunksAsync( + pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.util.Priority.NORMAL, + (chunks) -> { + try { + // Folia end - region threading if (!JigsawPlacement.generateJigsaw(level, templatePool, target, maxDepth, pos, false)) { throw ERROR_JIGSAW_FAILED.create(); } else { source.sendSuccess(() -> Component.translatable("commands.place.jigsaw.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 static int placeStructure(CommandSourceStack source, Holder.Reference structure, BlockPos pos) throws CommandSyntaxException { ServerLevel level = source.getLevel(); Structure structure1 = structure.value(); ChunkGenerator generator = level.getChunkSource().getGenerator(); + // Folia start - region threading + level.moonrise$loadChunksAsync( + pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.util.Priority.NORMAL, + (chunks) -> { + try { + // Folia end - region threading StructureStart structureStart = structure1.generate( structure, level.dimension(), @@ -305,14 +348,29 @@ public class PlaceCommand { ); String string = structure.key().location().toString(); source.sendSuccess(() -> Component.translatable("commands.place.structure.success", string, 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 template, BlockPos pos, Rotation rotation, Mirror mirror, float integrity, int seed ) throws CommandSyntaxException { ServerLevel level = source.getLevel(); + // Folia start - region threading + level.moonrise$loadChunksAsync( + pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, + ca.spottedleaf.concurrentutil.util.Priority.NORMAL, + (chunks) -> { + try { + // Folia end - region threading StructureTemplateManager structureManager = level.getStructureManager(); Optional optional; @@ -340,9 +398,17 @@ public class PlaceCommand { () -> Component.translatable("commands.place.template.success", Component.translationArg(template), 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 level, ChunkPos start, ChunkPos end) throws CommandSyntaxException { diff --git a/net/minecraft/server/commands/RecipeCommand.java b/net/minecraft/server/commands/RecipeCommand.java index d171a5b8c1969f6a482f029afa5fb0228aefb04d..c8ab7f56c5e5af99b5410784a3ae33dedd7bf2f3 100644 --- a/net/minecraft/server/commands/RecipeCommand.java +++ b/net/minecraft/server/commands/RecipeCommand.java @@ -81,7 +81,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) { @@ -103,7 +108,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/net/minecraft/server/commands/SetBlockCommand.java b/net/minecraft/server/commands/SetBlockCommand.java index 8b72116b80da0497e255ce5a3f3c7bccb6321aec..05b824409546ba8bacf7efdaeac106af89ff0715 100644 --- a/net/minecraft/server/commands/SetBlockCommand.java +++ b/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 state, SetBlockCommand.Mode mode, @Nullable Predicate predicate ) throws CommandSyntaxException { ServerLevel level = source.getLevel(); + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + level, pos.getX() >> 4, pos.getZ() >> 4, () -> { + try { + // Folia end - region threading if (predicate != null && !predicate.test(new BlockInWorld(level, pos, true))) { throw ERROR_FAILED.create(); } else { @@ -102,9 +113,16 @@ public class SetBlockCommand { } else { level.blockUpdated(pos, state.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/net/minecraft/server/commands/SetSpawnCommand.java b/net/minecraft/server/commands/SetSpawnCommand.java index e38c7f012098e46337561b2225b31a7097495647..6fc6a748a8096524440d32d692088a8176875786 100644 --- a/net/minecraft/server/commands/SetSpawnCommand.java +++ b/net/minecraft/server/commands/SetSpawnCommand.java @@ -69,7 +69,11 @@ public class SetSpawnCommand { final Collection actualTargets = new java.util.ArrayList<>(); // Paper - Add PlayerSetSpawnEvent for (ServerPlayer serverPlayer : targets) { // Paper start - Add PlayerSetSpawnEvent - if (serverPlayer.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) { + // Folia start - region threading + serverPlayer.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(serverPlayer); } // Paper end - Add PlayerSetSpawnEvent diff --git a/net/minecraft/server/commands/SummonCommand.java b/net/minecraft/server/commands/SummonCommand.java index b68c0e617d3593cc9ba999ed25ea2c1b7c762597..2d4bf39f3f35811a7f48f361c91ee3d5722ba839 100644 --- a/net/minecraft/server/commands/SummonCommand.java +++ b/net/minecraft/server/commands/SummonCommand.java @@ -88,12 +88,18 @@ public class SummonCommand { if (entity == null) { throw ERROR_FAILED.create(); } else { - if (randomizeProperties && entity instanceof Mob) { - ((Mob)entity) - .finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, null); - } + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + level, entity.chunkPosition().x, entity.chunkPosition().z, () -> { + if (randomizeProperties && entity instanceof Mob) { + ((Mob)entity) + .finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, null); + } + level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND); + }); + // Folia end - region threading - if (!level.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 ERROR_DUPLICATE_UUID.create(); } else { return entity; diff --git a/net/minecraft/server/commands/TeleportCommand.java b/net/minecraft/server/commands/TeleportCommand.java index 01f8e2fec232210c9311565197860cf0257081fd..174122905addbc88e818cd4946e831aec051b91a 100644 --- a/net/minecraft/server/commands/TeleportCommand.java +++ b/net/minecraft/server/commands/TeleportCommand.java @@ -154,18 +154,7 @@ public class TeleportCommand { private static int teleportToEntity(CommandSourceStack source, Collection targets, Entity destination) throws CommandSyntaxException { for (Entity entity : targets) { - performTeleport( - source, - entity, - (ServerLevel)destination.level(), - destination.getX(), - destination.getY(), - destination.getZ(), - EnumSet.noneOf(Relative.class), - destination.getYRot(), - destination.getXRot(), - null - ); + io.papermc.paper.threadedregions.TeleportUtils.teleport(entity, 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) { @@ -290,6 +279,24 @@ public class TeleportCommand { float f1 = relatives.contains(Relative.X_ROT) ? xRot - target.getXRot() : xRot; float f2 = Mth.wrapDegrees(f); float f3 = Mth.wrapDegrees(f1); + // Folia start - region threading + if (true) { + ServerLevel worldFinal = level; + Vec3 posFinal = new Vec3(x, y, z); + Float yawFinal = Float.valueOf(f); + Float pitchFinal = Float.valueOf(f1); + target.getBukkitEntity().taskScheduler.schedule((Entity 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 final net.minecraft.server.level.ServerPlayer player) { diff --git a/net/minecraft/server/commands/TimeCommand.java b/net/minecraft/server/commands/TimeCommand.java index e952ca088a2f36fc7f1eef4d9b217351569becc1..5d1fc3bb00abd177325a292f55d2cf1cddd3158b 100644 --- a/net/minecraft/server/commands/TimeCommand.java +++ b/net/minecraft/server/commands/TimeCommand.java @@ -56,6 +56,7 @@ public class TimeCommand { } public static int setTime(CommandSourceStack source, int time) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change // serverLevel.setDayTime(time); // CraftBukkit start @@ -69,10 +70,12 @@ public class TimeCommand { source.getServer().forceTimeSynchronization(); source.sendSuccess(() -> Component.translatable("commands.time.set", time), true); - return getDayTime(source.getLevel()); + }); // Folia - region threading + return 0; // Folia - region threading } public static int addTime(CommandSourceStack source, int amount) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change // CraftBukkit start org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(serverLevel.getWorld(), org.bukkit.event.world.TimeSkipEvent.SkipReason.COMMAND, amount); @@ -86,6 +89,7 @@ public class TimeCommand { source.getServer().forceTimeSynchronization(); int dayTime = getDayTime(source.getLevel()); source.sendSuccess(() -> Component.translatable("commands.time.set", dayTime), true); - return dayTime; + }); // Folia - region threading + return 0; // Folia - region threading } } diff --git a/net/minecraft/server/commands/WeatherCommand.java b/net/minecraft/server/commands/WeatherCommand.java index 9b14b6218b2673e9b13b749b566e3b8a6a8d9c7d..dade5adec00c081cb4def7464f0f04d2f5a6ae26 100644 --- a/net/minecraft/server/commands/WeatherCommand.java +++ b/net/minecraft/server/commands/WeatherCommand.java @@ -48,20 +48,26 @@ public class WeatherCommand { } private static int setClear(CommandSourceStack source, int time) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(getDuration(source, time, ServerLevel.RAIN_DELAY), 0, false, false); // CraftBukkit - SPIGOT-7680: per-world source.sendSuccess(() -> Component.translatable("commands.weather.set.clear"), true); + }); // Folia - region threading return time; } private static int setRain(CommandSourceStack source, int time) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.RAIN_DURATION), true, false); // CraftBukkit - SPIGOT-7680: per-world source.sendSuccess(() -> Component.translatable("commands.weather.set.rain"), true); + }); // Folia - region threading return time; } private static int setThunder(CommandSourceStack source, int time) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.THUNDER_DURATION), true, true); // CraftBukkit - SPIGOT-7680: per-world source.sendSuccess(() -> Component.translatable("commands.weather.set.thunder"), true); + }); // Folia - region threading return time; } } diff --git a/net/minecraft/server/commands/WorldBorderCommand.java b/net/minecraft/server/commands/WorldBorderCommand.java index e2697b03a0d204eea537e3aaec2dd8fb9f426722..f6af541a7076c3fefb237b865038d08919de35ed 100644 --- a/net/minecraft/server/commands/WorldBorderCommand.java +++ b/net/minecraft/server/commands/WorldBorderCommand.java @@ -134,18 +134,39 @@ 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() == distance) { throw ERROR_SAME_DAMAGE_BUFFER.create(); } else { worldBorder.setDamageSafeZone(distance); source.sendSuccess(() -> 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() == damagePerBlock) { throw ERROR_SAME_DAMAGE_AMOUNT.create(); @@ -154,39 +175,79 @@ public class WorldBorderCommand { source.sendSuccess( () -> 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) { throw ERROR_SAME_WARNING_TIME.create(); } else { worldBorder.setWarningTime(time); source.sendSuccess(() -> 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) { throw ERROR_SAME_WARNING_DISTANCE.create(); } else { worldBorder.setWarningBlocks(distance); source.sendSuccess(() -> 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 size = source.getLevel().getWorldBorder().getSize(); // CraftBukkit source.sendSuccess(() -> Component.translatable("commands.worldborder.get", String.format(Locale.ROOT, "%.0f", size)), false); - return Mth.floor(size + 0.5); + 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() == pos.x && worldBorder.getCenterZ() == pos.y) { throw ERROR_SAME_CENTER.create(); @@ -198,13 +259,24 @@ public class WorldBorderCommand { ), true ); - return 0; + return; // Folia - region threading } else { throw 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 newSize, 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 size = worldBorder.getSize(); if (size == newSize) { @@ -234,7 +306,14 @@ public class WorldBorderCommand { source.sendSuccess(() -> Component.translatable("commands.worldborder.set.immediate", String.format(Locale.ROOT, "%.1f", newSize)), true); } - return (int)(newSize - size); + return; // Folia - region threading } + // Folia start - region threading + } catch (CommandSyntaxException ex) { + sendMessage(source, ex); + } + }); + return 1; + // Folia end - region threading } } diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java index d2db6e3a4af13984b0a790fb38e83c253914a973..e3b7184e20bda4983ee1fc5463d91ca0cdd8ec1f 100644 --- a/net/minecraft/server/dedicated/DedicatedServer.java +++ b/net/minecraft/server/dedicated/DedicatedServer.java @@ -425,7 +425,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface @Override public void tickConnection() { super.tickConnection(); - this.handleConsoleInputs(); + // Folia - region threading } @Override @@ -734,7 +734,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface if (s.isBlank()) return ""; // Paper - Do not process empty rcon commands rconConsoleSource.prepareForCommand(); - this.executeBlocking(() -> { + final java.util.concurrent.atomic.AtomicReference command = new java.util.concurrent.atomic.AtomicReference<>(s); // Folia start - region threading + Runnable sync = () -> { // Folia - region threading CommandSourceStack wrapper = rconConsoleSource.createCommandSourceStack(); org.bukkit.event.server.RemoteServerCommandEvent event = new org.bukkit.event.server.RemoteServerCommandEvent(rconConsoleSource.getBukkitSender(wrapper), s); this.server.getPluginManager().callEvent(event); @@ -743,7 +744,16 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface } ConsoleInput serverCommand = new ConsoleInput(event.getCommand(), wrapper); this.server.dispatchServerCommand(event.getSender(), serverCommand); - }); + }; // 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 return rconConsoleSource.getCommandResponse(); // CraftBukkit end } diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java index b3f498558614243cf633dcd71e3c49c2c55e6e0f..329e57af5cbd38425e80dba96eb972fdfb0ce5ce 100644 --- a/net/minecraft/server/level/ChunkMap.java +++ b/net/minecraft/server/level/ChunkMap.java @@ -128,8 +128,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider public final ChunkMap.DistanceManager distanceManager; public final AtomicInteger tickingGenerated = new AtomicInteger(); // Paper - public private final String storageName; - private final PlayerMap playerMap = new PlayerMap(); - public final Int2ObjectMap entityMap = new Int2ObjectOpenHashMap<>(); + //private final PlayerMap playerMap = new PlayerMap(); // Folia - region threading + //public final Int2ObjectMap entityMap = new Int2ObjectOpenHashMap<>(); // Folia - region threading private final Long2ByteMap chunkTypeCache = new Long2ByteOpenHashMap(); // Paper - rewrite chunk system public int serverViewDistance; @@ -797,12 +797,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider void updatePlayerStatus(ServerPlayer player, boolean track) { boolean flag = this.skipPlayer(player); - boolean flag1 = this.playerMap.ignoredOrUnknown(player); + //boolean flag1 = this.playerMap.ignoredOrUnknown(player); // Folia - region threading if (track) { - this.playerMap.addPlayer(player, flag); + //this.playerMap.addPlayer(player, flag); // Folia - region threading this.updatePlayerPos(player); if (!flag) { - this.distanceManager.addPlayer(SectionPos.of(player), player); + //this.distanceManager.addPlayer(SectionPos.of(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 } @@ -810,9 +810,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider ca.spottedleaf.moonrise.common.PlatformHooks.get().addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system } else { SectionPos lastSectionPos = player.getLastSectionPos(); - this.playerMap.removePlayer(player); - if (!flag1) { - this.distanceManager.removePlayer(lastSectionPos, player); + //this.playerMap.removePlayer(player); // Folia - region threading + if (true) { // Folia - region threading + //this.distanceManager.removePlayer(lastSectionPos, player); // Folia - region threading ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation } @@ -830,27 +830,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider SectionPos lastSectionPos = player.getLastSectionPos(); SectionPos sectionPos = SectionPos.of(player); - boolean flag = this.playerMap.ignored(player); + //boolean flag = this.playerMap.ignored(player); // Folia - region threading boolean flag1 = this.skipPlayer(player); - boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong(); - if (flag2 || flag != flag1) { + //boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong(); // Folia - region threading + if (true) { // Folia - region threading this.updatePlayerPos(player); - ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, lastSectionPos, sectionPos, flag, flag1); // Paper - chunk tick iteration optimisation - if (!flag) { - this.distanceManager.removePlayer(lastSectionPos, player); - } - - if (!flag1) { - this.distanceManager.addPlayer(sectionPos, 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, lastSectionPos, sectionPos, false, flag1); // Paper - chunk tick iteration optimisation // Folia - region threading + // Folia - region threading // Paper - rewrite chunk system } @@ -880,9 +866,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 @@ -893,22 +879,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider i = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i); // Spigot if (i != 0) { int updateInterval = type.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 trackedEntity = new ChunkMap.TrackedEntity(entity, i, updateInterval, type.trackDeltas()); - this.entityMap.put(entity.getId(), trackedEntity); + //this.entityMap.put(entity.getId(), trackedEntity); // 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(trackedEntity); // Paper end - optimise entity tracker - trackedEntity.updatePlayers(this.level.players()); + trackedEntity.updatePlayers(this.level.getLocalPlayers()); // Folia - region threading if (entity instanceof ServerPlayer serverPlayer) { this.updatePlayerStatus(serverPlayer, true); - for (ChunkMap.TrackedEntity trackedEntity1 : this.entityMap.values()) { + // Folia start - region threading + for (Entity possible : this.level.getCurrentWorldData().trackerEntities) { + ChunkMap.TrackedEntity trackedEntity1 = possible.moonrise$getTrackedEntity(); + if (trackedEntity1 == null) { + continue; + } + // Folia end - region threading if (trackedEntity1.entity != serverPlayer) { trackedEntity1.updatePlayer(serverPlayer); } @@ -924,12 +916,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (entity instanceof ServerPlayer serverPlayer) { this.updatePlayerStatus(serverPlayer, false); - for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { + // Folia start - region threading + for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) { + ChunkMap.TrackedEntity trackedEntity = possible.moonrise$getTrackedEntity(); + if (trackedEntity == null) { + continue; + } + // Folia end - region threading trackedEntity.removePlayer(serverPlayer); } + // Folia end - region threading } - ChunkMap.TrackedEntity trackedEntity1 = this.entityMap.remove(entity.getId()); + ChunkMap.TrackedEntity trackedEntity1 = entity.moonrise$getTrackedEntity(); // Folia - region threading if (trackedEntity1 != null) { trackedEntity1.broadcastRemoved(); } @@ -938,9 +937,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.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.misc.NearbyPlayers nearbyPlayers = this.level.moonrise$getNearbyPlayers(); // Folia - region threading - 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]; @@ -948,7 +949,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider if (tracker == null) { continue; } - ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkData().nearbyPlayers); + ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition())); // Folia - region threading if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$hasPlayers() || ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING)) { tracker.serverEntity.sendChanges(); @@ -966,44 +967,18 @@ 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(); - - for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { - SectionPos sectionPos = trackedEntity.lastSectionPos; - SectionPos sectionPos1 = SectionPos.of(trackedEntity.entity); - boolean flag = !Objects.equals(sectionPos, sectionPos1); - if (flag) { - trackedEntity.updatePlayers(list1); - Entity entity = trackedEntity.entity; - if (entity instanceof ServerPlayer) { - list.add((ServerPlayer)entity); - } - - trackedEntity.lastSectionPos = sectionPos1; - } - - if (flag || this.distanceManager.inEntityTickingRange(sectionPos1.chunk().toLong())) { - trackedEntity.serverEntity.sendChanges(); - } - } - - if (!list.isEmpty()) { - for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { - trackedEntity.updatePlayers(list); - } - } + // Folia - region threading } public void broadcast(Entity entity, Packet packet) { - ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId()); + ChunkMap.TrackedEntity trackedEntity = entity.moonrise$getTrackedEntity(); // Folia - region threading if (trackedEntity != null) { trackedEntity.broadcast(packet); } } protected void broadcastAndSend(Entity entity, Packet packet) { - ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId()); + ChunkMap.TrackedEntity trackedEntity = entity.moonrise$getTrackedEntity(); // Folia - region threading if (trackedEntity != null) { trackedEntity.broadcastAndSend(packet); } @@ -1231,8 +1206,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z); // 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/net/minecraft/server/level/DistanceManager.java b/net/minecraft/server/level/DistanceManager.java index 5eab6179ce3913cb4e4d424f910ba423faf21c85..338f9d047101619605cedab172358b4fd737af97 100644 --- a/net/minecraft/server/level/DistanceManager.java +++ b/net/minecraft/server/level/DistanceManager.java @@ -57,16 +57,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 @@ -74,9 +74,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 @@ -208,15 +208,15 @@ 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 LongIterator getSpawnCandidateChunks() { - return this.spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation + return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation // Folia - region threading } public String getDebugStatus() { diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java index 6540b2d6a1062d883811ce240c49d30d1925b291..548f5f0382c81ca86d238bfd7f94008bbd6e41bc 100644 --- a/net/minecraft/server/level/ServerChunkCache.java +++ b/net/minecraft/server/level/ServerChunkCache.java @@ -61,18 +61,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]; - private final List tickingChunks = new ArrayList<>(); - private final Set chunkHoldersToBroadcast = new ReferenceOpenHashSet<>(); - @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<>(); public int getFullChunksCount() { @@ -98,6 +94,11 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) { + // Folia start - region threading + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Cannot asynchronously load chunks"); + } + // Folia end - region threading final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); final CompletableFuture completable = new CompletableFuture<>(); chunkTaskScheduler.scheduleChunkLoad( @@ -154,9 +155,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon // Paper start - chunk tick iteration optimisations private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom shuffleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(0L); private boolean isChunkNearPlayer(final ChunkMap chunkMap, final ChunkPos chunkPos, final LevelChunk levelChunk) { - final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)levelChunk).moonrise$getChunkAndHolder().holder()) - .moonrise$getRealChunkHolder().holderData; - final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk nearbyPlayers = chunkData.nearbyPlayers; + final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk nearbyPlayers = this.level.moonrise$getNearbyPlayers().getChunk(chunkPos); // Folia - region threading if (nearbyPlayers == null) { return false; } @@ -355,6 +354,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } public CompletableFuture> getChunkFuture(int x, int z, ChunkStatus chunkStatus, boolean requireChunk) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading boolean flag = Thread.currentThread() == this.mainThread; CompletableFuture> chunkFutureMainThread; if (flag) { @@ -502,14 +502,15 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } private void tickChunks() { - long gameTime = this.level.getGameTime(); - long l = gameTime - this.lastInhabitedUpdate; - this.lastInhabitedUpdate = gameTime; + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading + //long gameTime = this.level.getGameTime(); // Folia - region threading + long l = 1L; // Folia - region threading + //this.lastInhabitedUpdate = gameTime; // Folia - region threading if (!this.level.isDebug()) { ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("pollingChunks"); if (this.level.tickRateManager().runsNormally()) { - List list = this.tickingChunks; + List list = regionizedWorldData.temporaryChunkTickList; // Folia - region threading try { profilerFiller.push("filteringTickingChunks"); @@ -532,23 +533,24 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } private void broadcastChangedChunks(ProfilerFiller profiler) { + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading profiler.push("broadcast"); - for (ChunkHolder chunkHolder : this.chunkHoldersToBroadcast) { + for (ChunkHolder chunkHolder : regionizedWorldData.chunkHoldersToBroadcast) { // Folia - region threading - note: do not need to thread check, as getChunkToSend is only non-null when the chunkholder is loaded LevelChunk tickingChunk = chunkHolder.getChunkToSend(); // Paper - rewrite chunk system if (tickingChunk != null) { chunkHolder.broadcastChanges(tickingChunk); } } - this.chunkHoldersToBroadcast.clear(); + regionizedWorldData.chunkHoldersToBroadcast.clear(); // Folia - region threading profiler.pop(); } private void collectTickingChunks(List output) { // Paper start - chunk tick iteration optimisation final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = - ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); + this.level.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked(); final int size = tickingChunks.size(); @@ -569,13 +571,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } private void tickChunks(ProfilerFiller profiler, long timeInhabited, List chunks) { + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading profiler.popPush("naturalSpawnCount"); int naturalSpawnChunkCount = this.distanceManager.getNaturalSpawnChunkCount(); // Paper start - Optional per player mob spawns NaturalSpawner.SpawnState spawnState; 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; @@ -588,26 +591,26 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon } // Paper end - per player mob spawning backoff } - spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); + spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all } else { - spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); + spawnState = 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.lastSpawnState = spawnState; + regionizedWorldData.lastSpawnState = spawnState; // Folia - region threading profiler.popPush("spawnAndTick"); - boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit + boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threading int _int = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); List filteredSpawningCategories; if (_boolean && (this.spawnEnemies || this.spawnFriendlies)) { // Paper start - PlayerNaturallySpawnCreaturesEvent - for (ServerPlayer entityPlayer : this.level.players()) { + for (ServerPlayer entityPlayer : this.level.getLocalPlayers()) { // Folia - region threading int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, entityPlayer.getBukkitEntity().getViewDistance()); chunkRange = Math.min(chunkRange, 8); entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange); entityPlayer.playerNaturallySpawnedEvent.callEvent(); } // Paper end - PlayerNaturallySpawnCreaturesEvent - boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit + boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getRedstoneGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit // Folia - region threading filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag, this.level); // CraftBukkit } else { filteredSpawningCategories = List.of(); @@ -673,18 +676,23 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon int sectionPosZ = SectionPos.blockToSectionCoord(pos.getZ()); ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(ChunkPos.asLong(sectionPosX, sectionPosZ)); if (visibleChunkIfPresent != null && visibleChunkIfPresent.blockChanged(pos)) { - this.chunkHoldersToBroadcast.add(visibleChunkIfPresent); + this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(visibleChunkIfPresent); // Folia - region threading } } @Override public void onLightUpdate(LightLayer type, SectionPos pos) { - this.mainThreadProcessor.execute(() -> { + Runnable run = () -> { // Folia - region threading ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(pos.chunk().toLong()); if (visibleChunkIfPresent != null && visibleChunkIfPresent.sectionLightChanged(type, pos.y())) { - this.chunkHoldersToBroadcast.add(visibleChunkIfPresent); + this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(visibleChunkIfPresent); // Folia - region threading } - }); + }; // 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 type, ChunkPos pos, int distance, T value) { @@ -766,7 +774,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() { @@ -775,7 +784,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon public void onChunkReadyToSend(ChunkHolder chunkHolder) { if (chunkHolder.hasChangesToBroadcast()) { - this.chunkHoldersToBroadcast.add(chunkHolder); + throw new UnsupportedOperationException(); // Folia - region threading } } @@ -812,20 +821,76 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon return ServerChunkCache.this.mainThread; } + // Folia start - region threading + @Override + public CompletableFuture submit(Supplier task) { + if (true) { + throw new UnsupportedOperationException(); + } + return super.submit(task); + } + + @Override + public CompletableFuture submit(Runnable task) { + if (true) { + throw new UnsupportedOperationException(); + } + return super.submit(task); + } + + @Override + public void schedule(Runnable runnable) { + if (true) { + throw new UnsupportedOperationException(); + } + super.schedule(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 Profiler.get().incrementCounter("runTask"); super.doRunTask(task); } @Override 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 } diff --git a/net/minecraft/server/level/ServerEntityGetter.java b/net/minecraft/server/level/ServerEntityGetter.java index 794770985c261fd56806188237921b5ec5e548e6..b715d1fbde9db81a2515249bb9a0fc7a5fee40f0 100644 --- a/net/minecraft/server/level/ServerEntityGetter.java +++ b/net/minecraft/server/level/ServerEntityGetter.java @@ -14,17 +14,17 @@ public interface ServerEntityGetter extends EntityGetter { @Nullable default Player getNearestPlayer(TargetingConditions targetingConditions, LivingEntity source) { - return this.getNearestEntity(this.players(), targetingConditions, source, source.getX(), source.getY(), source.getZ()); + return this.getNearestEntity(this.getLocalPlayers(), targetingConditions, source, source.getX(), source.getY(), source.getZ()); // Folia - region threading } @Nullable default Player getNearestPlayer(TargetingConditions targetingConditions, LivingEntity source, double x, double y, double z) { - return this.getNearestEntity(this.players(), targetingConditions, source, x, y, z); + return this.getNearestEntity(this.getLocalPlayers(), targetingConditions, source, x, y, z); // Folia - region threading } @Nullable default Player getNearestPlayer(TargetingConditions targetingConditions, double x, double y, double z) { - return this.getNearestEntity(this.players(), targetingConditions, null, x, y, z); + return this.getNearestEntity(this.getLocalPlayers(), targetingConditions, null, x, y, z); // Folia - region threading } @Nullable @@ -57,7 +57,7 @@ public interface ServerEntityGetter extends EntityGetter { default List getNearbyPlayers(TargetingConditions targetingConditions, LivingEntity source, AABB area) { List list = new ArrayList<>(); - for (Player player : this.players()) { + for (Player player : this.getLocalPlayers()) { // Folia - region threading if (area.contains(player.getX(), player.getY(), player.getZ()) && targetingConditions.test(this.getLevel(), source, player)) { list.add(player); } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java index d3c50acc5ca30b608825d4baff4b7e09a9e0f586..464bd9e968610ef9144f7dff1eead3db81be3caf 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -179,42 +179,40 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private static final Logger LOGGER = LogUtils.getLogger(); private static final int EMPTY_TIME_NO_TICK = 300; private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536; - final List players = Lists.newArrayList(); + final List players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - region threading public final ServerChunkCache chunkSource; private final MinecraftServer server; public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type private int lastSpawnChunkRadius; - final EntityTickList entityTickList = new EntityTickList(); + //final EntityTickList entityTickList = new 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 = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); - private final LevelTicks fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); - private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); - final Set navigatingMobs = new ObjectOpenHashSet<>(); + //private final LevelTicks blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading + //private final LevelTicks fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading + //private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); // Folia - region threading + //final Set navigatingMobs = new ObjectOpenHashSet<>(); // Folia - region threading volatile boolean isUpdatingNavigations; protected final Raids raids; - private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); - private final List blockEventsToReschedule = new ArrayList<>(64); - private boolean handlingTick; + //private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); // Folia - region threading + //private final List blockEventsToReschedule = new ArrayList<>(64); // Folia - region threading + //private boolean handlingTick; // Folia - region threading private final List customSpawners; @Nullable private EndDragonFight dragonFight; - final Int2ObjectMap dragonParts = new Int2ObjectOpenHashMap<>(); + final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable dragonParts = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); // Folia - region threading 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 levelStorageAccess; 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 @@ -242,6 +240,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe int minChunkZ = minBlockZ >> 4; int maxChunkZ = maxBlockZ >> 4; + // 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; + } + // Folia end - region threading + ServerChunkCache chunkProvider = this.getChunkSource(); for (int cx = minChunkX; cx <= maxChunkX; ++cx) { @@ -297,11 +302,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe 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) { @@ -359,7 +360,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public final int moonrise$getRegionChunkShift() { - return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); + return this.regioniser.sectionChunkShift; // Folia - region threading } @Override @@ -460,22 +461,22 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @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 } @Override @@ -495,80 +496,85 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper end - rewrite chunk system // Paper start - chunk tick iteration private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; - private final ca.spottedleaf.moonrise.common.list.ReferenceList playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS); - private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); + // Folia - region threading @Override public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getPlayerTickingChunks() { - return this.playerTickingChunks; + throw new UnsupportedOperationException(); // Folia - region threading } @Override public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) { - final ChunkPos pos = chunk.getPos(); - if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) { - return; - } - - this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); + // Folia - region threading } @Override public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) { - this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); + // Folia - region threading } @Override public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) { - ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async"); - - final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); - - if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) { - // already added - return; - } - - final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() - .chunkHolderManager.getChunkHolder(chunkKey); - - if (chunkHolder == null || !chunkHolder.isTickingReady()) { - return; - } - - this.playerTickingChunks.add( - ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() - ); + // Folia - region threading } @Override public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) { - ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async"); + // Folia - region threading + } + // Paper end - chunk tick iteration + // 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; - final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); - final int val = this.playerTickingRequests.addTo(chunkKey, -1); + 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<>(); - if (val <= 0) { - throw new IllegalStateException("Negative counter"); + public void pushPendingTeleport(final PendingTeleport teleport) { + synchronized (this.pendingTeleports) { + this.pendingTeleports.add(teleport); } + } - if (val != 1) { - // still has at least one request - return; + public boolean removePendingTeleport(final PendingTeleport teleport) { + synchronized (this.pendingTeleports) { + return this.pendingTeleports.remove(teleport); } + } - final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() - .chunkHolderManager.getChunkHolder(chunkKey); + public List removeAllRegionTeleports() { + final List ret = new ArrayList<>(); - if (chunkHolder == null || !chunkHolder.isTickingReady()) { - return; + synchronized (this.pendingTeleports) { + for (final java.util.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(); + } + } } - this.playerTickingChunks.remove( - ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() - ); + return ret; } - // Paper end - chunk tick iteration + // Folia end - region threading public ServerLevel( MinecraftServer server, @@ -633,7 +639,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe ); 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(server.getAbsoluteMaxWorldSize()); this.raids = this.getDataStorage().computeIfAbsent(Raids.factory(this), Raids.getFileId(this.dimensionTypeRegistration())); @@ -681,7 +687,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); // 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 @@ -709,61 +722,39 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe return this.getChunkSource().getGenerator().getBiomeSource().getNoiseBiome(x, y, z, this.getChunkSource().randomState().sampler()); } + @Override // Folia - region threading public StructureManager structureManager() { return this.structureManager; } - public void tick(BooleanSupplier hasTimeLeft) { + public void tick(BooleanSupplier hasTimeLeft, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking + final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking ProfilerFiller profilerFiller = Profiler.get(); - this.handlingTick = true; + regionizedWorldData.setHandlingTick(true); // Folia - regionised ticking TickRateManager tickRateManager = this.tickRateManager(); boolean runsNormally = tickRateManager.runsNormally(); if (runsNormally) { profilerFiller.push("world border"); - this.getWorldBorder().tick(); + //this.getWorldBorder().tick(); // Folia - regionised ticking profilerFiller.popPush("weather"); - this.advanceWeatherCycle(); + //this.advanceWeatherCycle(); // Folia - regionised ticking profilerFiller.pop(); } - int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); - if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) { - // Paper start - create time skip event - move up calculations - final long newDayTime = this.levelData.getDayTime() + 24000L; - org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent( - this.getWorld(), - org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP, - (newDayTime - newDayTime % 24000L) - this.getDayTime() - ); - // Paper end - create time skip event - move up calculations - if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { - // Paper start - call time skip event if gamerule is enabled - // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime - // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param - if (event.callEvent()) { - this.setDayTime(this.getDayTime() + event.getSkipAmount()); - } - // Paper end - call time skip event if gamerule is enabled - } - - if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled - if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { - this.resetWeatherCycle(); - } - } + this.tickSleep(); // Folia - region threading - move into tickSleep - this.updateSkyBrightness(); + //this.updateSkyBrightness(); // Folia - region threading if (runsNormally) { this.tickTime(); } profilerFiller.push("tickPending"); if (!this.isDebug() && runsNormally) { - long l = this.getGameTime(); + long l = regionizedWorldData.getRedstoneGameTime(); // Folia - region threading profilerFiller.push("blockTicks"); - this.blockTicks.tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks + regionizedWorldData.getBlockLevelTicks().tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks // Folia - region ticking profilerFiller.popPush("fluidTicks"); - this.fluidTicks.tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks + regionizedWorldData.getFluidLevelTicks().tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks // Folia - region ticking profilerFiller.pop(); } @@ -779,9 +770,9 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.runBlockEvents(); } - this.handlingTick = false; + regionizedWorldData.setHandlingTick(false); // Folia - regionised ticking profilerFiller.pop(); - boolean flag = !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 flag = 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 (flag) { this.resetEmptyTime(); } @@ -789,19 +780,29 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (flag || this.emptyTime++ < 300) { profilerFiller.push("entities"); if (this.dragonFight != null && runsNormally) { + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, this.dragonFight.origin)) { // Folia - region threading profilerFiller.push("dragonFight"); this.dragonFight.tick(); profilerFiller.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 } io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR - this.entityTickList - .forEach( + regionizedWorldData // Folia - regionised ticking + .forEachTickingEntity( // Folia - regionised ticking entity -> { if (!entity.isRemoved()) { if (!tickRateManager.isEntityFrozen(entity)) { profilerFiller.push("checkDespawn"); entity.checkDespawn(); + if (entity.isRemoved()) return; // Folia - region threading - if we despawned, DON'T TICK IT! profilerFiller.pop(); if (true) { // Paper - rewrite chunk system Entity vehicle = entity.getVehicle(); @@ -830,6 +831,36 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe profilerFiller.pop(); } + // Folia start - region threading + public void tickSleep() { + int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); + if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) { + // Paper start - create time skip event - move up calculations + final long newDayTime = this.levelData.getDayTime() + 24000L; + org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent( + this.getWorld(), + org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP, + (newDayTime - newDayTime % 24000L) - this.getDayTime() + ); + // Paper end - create time skip event - move up calculations + if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { + // Paper start - call time skip event if gamerule is enabled + // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime + // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param + if (event.callEvent()) { + this.setDayTime(this.getDayTime() + event.getSkipAmount()); + } + // Paper end - call time skip event if gamerule is enabled + } + + if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled + 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 @@ -840,12 +871,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe protected void tickTime() { if (this.tickTime) { - long l = this.levelData.getGameTime() + 1L; - this.serverLevelData.setGameTime(l); + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading + long l = regionizedWorldData.getRedstoneGameTime() + 1L; // Folia - region threading + regionizedWorldData.setRedstoneGameTime(l); // Folia - region threading Profiler.get().push("scheduledFunctions"); - this.serverLevelData.getScheduledEvents().tick(this.server, l); + //this.serverLevelData.getScheduledEvents().tick(this.server, l); // Folia - region threading - TODO any way to bring this in? Profiler.get().pop(); - if (this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { + if (false && this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { // Folia - region threading this.setDayTime(this.levelData.getDayTime() + 1L); } } @@ -863,16 +895,27 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private void wakeUpAllPlayers() { this.sleepStatus.removeAllSleepers(); - this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach(player -> player.stopSleepInBed(false, false)); + // Folia start - region threading + this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach((ServerPlayer entityplayer) -> { + // 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 } // Paper start - optimise random ticking - private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); + private final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource.INSTANCE; // Folia - region threading private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) { final LevelChunkSection[] sections = chunk.getSections(); final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this); - final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; + final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Folia - region threading final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294(); final ChunkPos cpos = chunk.getPos(); @@ -919,7 +962,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper end - optimise random ticking public void tickChunk(LevelChunk chunk, int randomTickSpeed) { - final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking + final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Paper - optimise random ticking // Folia - region threading ChunkPos pos = chunk.getPos(); boolean isRaining = this.isRaining(); int minBlockX = pos.getMinBlockX(); @@ -1044,7 +1087,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } public boolean isHandlingTick() { - return this.handlingTick; + return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking } public boolean canSleepThroughNights() { @@ -1070,6 +1113,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } 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(); } @@ -1080,7 +1131,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe return this.server.getScoreboard(); } - private void advanceWeatherCycle() { + public void advanceWeatherCycle() { // Folia - region threading - public boolean isRaining = this.isRaining(); if (this.dimensionType().hasSkyLight()) { if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE)) { @@ -1166,7 +1217,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, this.thunderLevel)); } */ - for (ServerPlayer player : this.players) { + ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading + for (ServerPlayer player : players) { // Folia - region threading if (player.level() == this) { player.tickWeather(); } @@ -1174,13 +1226,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (isRaining != this.isRaining()) { // Only send weather packets to those affected - for (ServerPlayer player : this.players) { + for (ServerPlayer player : players) { // Folia - region threading if (player.level() == this) { player.setPlayerWeather((!isRaining ? org.bukkit.WeatherType.DOWNFALL : org.bukkit.WeatherType.CLEAR), false); } } } - for (ServerPlayer player : this.players) { + for (ServerPlayer player : players) { // Folia - region threading if (player.level() == this) { player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); } @@ -1241,13 +1293,10 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper start - log detailed entity tick information // TODO replace with varhandle - static final java.util.concurrent.atomic.AtomicReference currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>(); + // Folia - region threading public static List getCurrentlyTickingEntities() { - Entity ticking = currentlyTickingEntity.get(); - List ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking }); - - return ret; + throw new UnsupportedOperationException(); // Folia - region threading } // Paper end - log detailed entity tick information @@ -1255,9 +1304,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper start - log detailed entity tick information ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); try { - if (currentlyTickingEntity.get() == null) { - currentlyTickingEntity.lazySet(entity); - } + // Folia - region threading // Paper end - log detailed entity tick information entity.setOldPosAndRot(); ProfilerFiller profilerFiller = Profiler.get(); @@ -1268,7 +1315,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe final boolean isActive = io.papermc.paper.entity.activation.ActivationRange.checkIfActive(entity); // Paper - EAR 2 if (isActive) { // Paper - EAR 2 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 profilerFiller.pop(); @@ -1277,9 +1333,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } // Paper start - log detailed entity tick information } finally { - if (currentlyTickingEntity.get() == entity) { - currentlyTickingEntity.lazySet(null); - } + // Folia - region threading } // Paper end - log detailed entity tick information } @@ -1287,7 +1341,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private void tickPassenger(Entity ridingEntity, Entity passengerEntity, final boolean isActive) { // Paper - EAR 2 if (passengerEntity.isRemoved() || passengerEntity.getVehicle() != ridingEntity) { passengerEntity.stopRiding(); - } else if (passengerEntity instanceof Player || this.entityTickList.contains(passengerEntity)) { + } else if (passengerEntity instanceof Player || this.getCurrentWorldData().hasEntityTickingEntity(passengerEntity)) { // Folia - region threading passengerEntity.setOldPosAndRot(); passengerEntity.tickCount++; passengerEntity.totalEntityAge++; // Paper - age-like counter for all entities @@ -1297,7 +1351,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper start - EAR 2 if (isActive) { passengerEntity.rideTick(); - passengerEntity.postTick(); // CraftBukkit + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(passengerEntity)) { + // removed from region while ticking + return; + } + if (passengerEntity.handlePortal()) { + // portalled + return; + } + // Folia end - region threading } else { passengerEntity.setDeltaMovement(Vec3.ZERO); passengerEntity.inactiveTick(); @@ -1371,19 +1434,20 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } // 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.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); - // CraftBukkit end + // Folia - move into saveLevelData } - private void saveLevelData(boolean join) { + public void saveLevelData(boolean join) { // 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.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); + // Folia end - moved into saveLevelData DimensionDataStorage dataStorage = this.getChunkSource().getDataStorage(); if (join) { @@ -1439,6 +1503,19 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe 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 players = this.getPlayers(LivingEntity::isAlive); @@ -1520,8 +1597,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } 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 @@ -1696,13 +1773,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) { - if (this.isUpdatingNavigations) { + final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading + if (false && this.isUpdatingNavigations) { // Folia - region threading String string = "recursive call to sendBlockUpdated"; Util.logAndPauseIfInIde("recursive call to sendBlockUpdated", new IllegalStateException("recursive call to sendBlockUpdated")); } this.getChunkSource().blockChanged(pos); - this.pathTypesByPosCache.invalidate(pos); + regionizedWorldData.pathTypesByPosCache.invalidate(pos); // Folia - region threading if (this.paperConfig().misc.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates VoxelShape collisionShape = oldState.getCollisionShape(this, pos); VoxelShape collisionShape1 = newState.getCollisionShape(this, pos); @@ -1710,7 +1788,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe List list = new ObjectArrayList<>(); try { // Paper - catch CME see below why - for (Mob mob : this.navigatingMobs) { + for (java.util.Iterator iterator = regionizedWorldData.getNavigatingMobs(); iterator.hasNext();) { // Folia - region threading + Mob mob = iterator.next(); // Folia - region threading PathNavigation navigation = mob.getNavigation(); if (navigation.shouldRecomputePath(pos)) { list.add(navigation); @@ -1727,13 +1806,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper end - catch CME see below why try { - this.isUpdatingNavigations = true; + //this.isUpdatingNavigations = true; // Folia - region threading for (PathNavigation pathNavigation : list) { pathNavigation.recomputePath(); } } finally { - this.isUpdatingNavigations = false; + //this.isUpdatingNavigations = false; // Folia - region threading } } } // Paper - option to disable pathfinding updates @@ -1741,29 +1820,29 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public void updateNeighborsAt(BlockPos pos, Block block) { - if (captureBlockStates) { return; } // Paper - Cancel all physics during placement + if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, null, null)); } @Override public void updateNeighborsAt(BlockPos pos, Block block, @Nullable Orientation orientation) { - if (captureBlockStates) { return; } // Paper - Cancel all physics during placement - this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation); + if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading + this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation); // Folia - region threading } @Override public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block block, Direction facing, @Nullable Orientation orientation) { - this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, facing, orientation); + this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, facing, orientation); // Folia - region threading } @Override public void neighborChanged(BlockPos pos, Block block, @Nullable Orientation orientation) { - this.neighborUpdater.neighborChanged(pos, block, orientation); + this.getCurrentWorldData().neighborUpdater.neighborChanged(pos, block, orientation); // Folia - region threading } @Override public void neighborChanged(BlockState state, BlockPos pos, Block block, @Nullable Orientation orientation, boolean movedByPiston) { - this.neighborUpdater.neighborChanged(state, pos, block, orientation, movedByPiston); + this.getCurrentWorldData().neighborUpdater.neighborChanged(state, pos, block, orientation, movedByPiston); // Folia - region threading } @Override @@ -1853,7 +1932,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // CraftBukkit end ParticleOptions particleOptions = serverExplosion.isSmall() ? smallExplosionParticles : largeExplosionParticles; - for (ServerPlayer serverPlayer : this.players) { + for (ServerPlayer serverPlayer : this.getLocalPlayers()) { // Folia - region thraeding if (serverPlayer.distanceToSqr(vec3) < 4096.0) { Optional optional = Optional.ofNullable(serverExplosion.getHitPlayers().get(serverPlayer)); serverPlayer.connection.send(new ClientboundExplodePacket(vec3, optional, particleOptions, explosionSound)); @@ -1869,14 +1948,17 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public void blockEvent(BlockPos pos, Block block, int eventID, int eventParam) { - this.blockEvents.add(new BlockEventData(pos, block, eventID, eventParam)); + this.getCurrentWorldData().pushBlockEvent(new BlockEventData(pos, block, eventID, eventParam)); // Folia - regionised ticking } private void runBlockEvents() { - this.blockEventsToReschedule.clear(); + List blockEventsToReschedule = new ArrayList<>(64); // Folia - regionised ticking - while (!this.blockEvents.isEmpty()) { - BlockEventData blockEventData = this.blockEvents.removeFirst(); + // Folia start - regionised ticking + io.papermc.paper.threadedregions.RegionizedWorldData worldRegionData = this.getCurrentWorldData(); + BlockEventData blockEventData; + while ((blockEventData = worldRegionData.removeFirstBlockEvent()) != null) { + // Folia end - regionised ticking if (this.shouldTickBlocksAt(blockEventData.pos())) { if (this.doBlockEvent(blockEventData)) { this.server @@ -1892,11 +1974,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe ); } } else { - this.blockEventsToReschedule.add(blockEventData); + blockEventsToReschedule.add(blockEventData); // Folia - regionised ticking } } - this.blockEvents.addAll(this.blockEventsToReschedule); + worldRegionData.pushBlockEvents(blockEventsToReschedule); // Folia - regionised ticking } private boolean doBlockEvent(BlockEventData event) { @@ -1906,12 +1988,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @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 @@ -1964,7 +2046,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe double zOffset, double speed ) { - return sendParticlesSource(this.players, sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed); + return sendParticlesSource(this.getLocalPlayers(), sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed); // Folia - region threading } public int sendParticlesSource( List receivers, @@ -2047,12 +2129,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Nullable public Entity getEntityOrPart(int id) { Entity entity = this.getEntities().get(id); - return entity != null ? entity : this.dragonParts.get(id); + return entity != null ? entity : this.dragonParts.get((long)id); // Folia - diff on change } @Override public Collection dragonParts() { - return this.dragonParts.values(); + return this.dragonParts.values(); // Folia - diff on change } @Nullable @@ -2107,6 +2189,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper start - Call missing map initialize event and set id final DimensionDataStorage storage = this.getServer().overworld().getDataStorage(); + synchronized (storage.cache) { // Folia - region threading final Optional cacheEntry = storage.cache.get(mapId.key()); if (cacheEntry == null) { // Cache did not contain, try to load and may init final MapItemSavedData mapData = storage.get(MapItemSavedData.factory(), mapId.key()); // get populates the cache @@ -2126,6 +2209,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } return null; + } // Folia - region threading // Paper end - Call missing map initialize event and set id } @@ -2180,6 +2264,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } public boolean setChunkForced(int chunkX, int chunkZ, boolean add) { + io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify force loaded chunks off of the global region"); // Folia - region threading ForcedChunksSavedData forcedChunksSavedData = this.getDataStorage().computeIfAbsent(ForcedChunksSavedData.factory(), "chunks"); ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); long packedChunkPos = chunkPos.toLong(); @@ -2187,7 +2272,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (add) { flag = forcedChunksSavedData.getChunks().add(packedChunkPos); if (flag) { - this.getChunk(chunkX, chunkZ); + //this.getChunk(chunkX, chunkZ); // Folia - region threading - we must let the chunk load asynchronously } } else { flag = forcedChunksSavedData.getChunks().remove(packedChunkPos); @@ -2212,11 +2297,24 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe Optional> optional1 = PoiTypes.forState(newState); if (!Objects.equals(optional, optional1)) { BlockPos blockPos = pos.immutable(); - optional.ifPresent(poiType -> this.getServer().execute(() -> { + // Folia start - region threading + optional.ifPresent(poiType -> { + Runnable run = () -> { + // Folia end - region threading this.getPoiManager().remove(blockPos); DebugPackets.sendPoiRemovedPacket(this, blockPos); - })); - optional1.ifPresent(poiType -> this.getServer().execute(() -> { + // Folia start - region threading + }; + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( + this, blockPos.getX() >> 4, blockPos.getZ() >> 4, run + ); + }); + // Folia end - region threading + // Folia start - region threading + optional1.ifPresent(poiType -> { + Runnable run = () -> { + // Folia end - region threading // Paper start - Remove stale POIs if (optional.isEmpty() && this.getPoiManager().exists(blockPos, ignored -> true)) { this.getPoiManager().remove(blockPos); @@ -2224,7 +2322,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper end - Remove stale POIs this.getPoiManager().add(blockPos, (Holder)poiType); DebugPackets.sendPoiAddedPacket(this, blockPos); - })); + // Folia start - region threading + }; + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( + this, blockPos.getX() >> 4, blockPos.getZ() >> 4, run + ); + // Folia end - region threading + }); + // Folia end - region threading } } @@ -2278,7 +2384,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } 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: " + chunkMap.getDistanceManager().getDebugStatus() + "\n"); @@ -2348,7 +2454,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private void dumpBlockEntityTickers(Writer output) throws IOException { CsvOutput csvOutput = CsvOutput.builder().addColumn("x").addColumn("y").addColumn("z").addColumn("type").build(output); - for (TickingBlockEntity tickingBlockEntity : this.blockEntityTickers) { + for (TickingBlockEntity tickingBlockEntity : (Iterable)null) { // Folia - region threading BlockPos pos = tickingBlockEntity.getPos(); csvOutput.writeRow(pos.getX(), pos.getY(), pos.getZ(), tickingBlockEntity.getType()); } @@ -2356,14 +2462,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @VisibleForTesting public void clearBlockEvents(BoundingBox boundingBox) { - this.blockEvents.removeIf(blockEventData -> boundingBox.isInside(blockEventData.pos())); + this.getCurrentWorldData().removeIfBlockEvents(blockEventData -> boundingBox.isInside(blockEventData.pos())); // Folia - regionised ticking } @Override 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 @@ -2412,8 +2518,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.players.size(), this.moonrise$getEntityLookup().getDebugInfo(), // Paper - rewrite chunk system getTypeCount(this.moonrise$getEntityLookup().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()), // Paper - rewrite chunk system - this.blockEntityTickers.size(), - getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), + 0, // Folia - region threading + "null", // Folia - region threading this.getBlockTicks().count(), this.getFluidTicks().count(), this.gatherChunkSourceStats() @@ -2465,15 +2571,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } 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())); + this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); // Folia - region threading } public PathTypeCache getPathTypeCache() { - return this.pathTypesByPosCache; + return this.getCurrentWorldData().pathTypesByPosCache; // Folia - region threading } @Override @@ -2491,7 +2597,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe 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 @@ -2583,7 +2689,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Paper start - optimize redstone (Alternate Current) @Override public alternate.current.wire.WireHandler getWireHandler() { - return wireHandler; + return this.getCurrentWorldData().wireHandler; // Folia - region threading } // Paper end - optimize redstone (Alternate Current) @@ -2595,18 +2701,18 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public void onDestroyed(Entity entity) { - ServerLevel.this.getScoreboard().entityRemoved(entity); + // ServerLevel.this.getScoreboard().entityRemoved(entity); // Folia - region threading } @Override 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 } @Override 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 (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { pearl.cachedOwner = null; @@ -2618,6 +2724,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override 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 serverPlayer) { ServerLevel.this.players.add(serverPlayer); @@ -2632,12 +2739,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe ); } - ServerLevel.this.navigatingMobs.add(mob); + ServerLevel.this.getCurrentWorldData().addNavigatingMob(mob); // Folia - region threading } if (entity instanceof EnderDragon enderDragon) { for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) { - ServerLevel.this.dragonParts.put(enderDragonPart.getId(), enderDragonPart); + ServerLevel.this.dragonParts.put((long)enderDragonPart.getId(), enderDragonPart); // Folia - diff on change } } @@ -2660,18 +2767,27 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public void onTrackingEnd(Entity entity) { org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot + ServerLevel.this.getCurrentWorldData().removeLoadedEntity(entity); // Folia - region threading // Spigot start // TODO I don't think this is needed anymore if (entity instanceof Player player) { for (final ServerLevel level : ServerLevel.this.getServer().getAllLevels()) { - for (final Optional savedData : level.getDataStorage().cache.values()) { + // Folia start - make map data thread-safe + List> worldDataCache; + synchronized (level.getDataStorage().cache) { + worldDataCache = new java.util.ArrayList<>(level.getDataStorage().cache.values()); + } + for (final Optional savedData : worldDataCache) { + // Folia end - make map data thread-safe if (savedData.isEmpty() || !(savedData.get() instanceof MapItemSavedData map)) { continue; } + synchronized (map) { // Folia - make map data thread-safe map.carriedByPlayers.remove(player); if (map.carriedBy.removeIf(holdingPlayer -> holdingPlayer.player == player)) { map.decorations.remove(player.getName().getString()); } + } // Folia - make map data thread-safe } } } @@ -2702,18 +2818,19 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe ); } - ServerLevel.this.navigatingMobs.remove(mob); + ServerLevel.this.getCurrentWorldData().removeNavigatingMob(mob); // Folia - region threading } if (entity instanceof EnderDragon enderDragon) { for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) { - ServerLevel.this.dragonParts.remove(enderDragonPart.getId()); + ServerLevel.this.dragonParts.remove((long)enderDragonPart.getId()); // Folia - diff on change } } entity.updateDynamicGameEventListener(DynamicGameEventListener::remove); // CraftBukkit start entity.valid = false; + // Folia - region threading - TODO THIS SHIT if (!(entity instanceof ServerPlayer)) { for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players player.getBukkitEntity().onEntityRemove(entity); @@ -2741,11 +2858,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private long lagCompensationTick = MinecraftServer.SERVER_INIT; public long getLagCompensationTick() { - return this.lagCompensationTick; + return this.getCurrentWorldData().getLagCompensationTick(); // Folia - region threading } public void updateLagCompensationTick() { - this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); + throw new UnsupportedOperationException(); // Folia - region threading } // Paper end - lag compensation } diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java index 57d432dc9e8d8e9a3e088e7c40b35178c30fe786..f5615c7f7127edda460db9158d6bd4ddad9193f7 100644 --- a/net/minecraft/server/level/ServerPlayer.java +++ b/net/minecraft/server/level/ServerPlayer.java @@ -180,7 +180,7 @@ import org.slf4j.Logger; public class ServerPlayer extends 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 static final long LAST_SAVE_ABSENT = Long.MIN_VALUE; public long lastSave = LAST_SAVE_ABSENT; // 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; @@ -443,8 +443,149 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc this.maxHealthCache = this.getMaxHealth(); } + // 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.CallbackCompletable 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() + dx, selected.getZ() + dz); + if (inChunk == null) { + continue; + } + + AABB checkVolume = player.getBoundingBoxAt((double)inChunk.getX() + 0.5, (double)inChunk.getY(), (double)inChunk.getZ() + 0.5); + + if (!player.noCollisionNoLiquid(world, 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.CallbackCompletable 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.moonrise$areChunksLoaded(minX, minZ, maxX, maxZ)) { + world.moonrise$loadChunksAsync(minX, maxX, minZ, maxZ, ca.spottedleaf.concurrentutil.util.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.CallbackCompletable toComplete) { + do { + if (attemptCount[0] >= maxAttempts) { + BlockPos sharedSpawn = world.getSharedSpawnPos(); + + LOGGER.warn("Found no spawn in radius for player '" + player.getName() + "', ignoring radius"); + + selectSpawnWithoutRadius(world, player, sharedSpawn, toComplete); + return; + } + } while (!trySpawnOrSchedule(world, player, random, attemptCount, maxAttempts, toComplete)); + } + + + private static void selectSpawnWithoutRadius(ServerLevel world, ServerPlayer player, BlockPos spawn, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { + world.loadChunksForMoveAsync(player.getBoundingBoxAt(spawn.getX() + 0.5, spawn.getY(), spawn.getZ() + 0.5), + ca.spottedleaf.concurrentutil.util.Priority.HIGHER, + (c) -> { + BlockPos ret = spawn; + while (!player.noCollisionNoLiquid(world, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY(), ret.getZ() + 0.5)) && ret.getY() < (double)world.getMaxY()) { + ret = ret.above(); + } + while (player.noCollisionNoLiquid(world, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY() - 1, ret.getZ() + 0.5)) && ret.getY() > (double)(world.getMinY() + 1)) { + ret = ret.below(); + } + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(ret), world.levelData.getSpawnAngle(), 0.0f)); + } + ); + } + + public static void fudgeSpawnLocation(ServerLevel world, ServerPlayer player, ca.spottedleaf.concurrentutil.completable.CallbackCompletable 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 { + selectSpawnWithoutRadius(world, player, blockposition, toComplete); + } + + } + // Folia end - region threading + @Override public BlockPos adjustSpawnLocation(ServerLevel level, BlockPos pos) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException(); + } + // Folia end - region threading AABB aabb = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO); BlockPos blockPos = pos; if (level.dimensionType().hasSkyLight() && level.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit @@ -533,7 +674,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc this.getBukkitEntity().readExtraData(compound); // CraftBukkit if (this.isSleeping()) { - this.stopSleeping(); + this.stopSleepingRaw(); // Folia - do not modify or read worldstate during data deserialization } // CraftBukkit start @@ -709,10 +850,17 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc ServerLevel level = this.level().getServer().getLevel(optional.get()); if (level != null) { Entity entity = EntityType.loadEntityRecursive( - compoundTag, level, EntitySpawnReason.LOAD, entity1 -> !level.addWithUUID(entity1) ? null : entity1 + compoundTag, level, EntitySpawnReason.LOAD, entity1 -> entity1 // Folia - region threading - delay world add ); if (entity != null) { - placeEnderPearlTicket(level, entity.chunkPosition()); + // Folia start - region threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + level, entity.chunkPosition().x, entity.chunkPosition().z, () -> { + level.addFreshEntityWithPassengers(entity); + ServerPlayer.placeEnderPearlTicket(level, entity.chunkPosition()); + } + ); + // Folia end - region threading } else { LOGGER.warn("Failed to spawn player ender pearl in level ({}), skipping", optional.get()); } @@ -817,12 +965,23 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc Entity camera = this.getCamera(); if (camera != this) { - if (camera.isAlive()) { + if (camera.canBeSpectated()) { // Folia - region threading - replace removed check + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(camera) && !camera.isRemoved()) { // Folia - region threading this.absMoveTo(camera.getX(), camera.getY(), camera.getZ(), camera.getYRot(), camera.getXRot()); this.serverLevel().getChunkSource().move(this); if (this.wantsToStopRiding()) { this.setCamera(this); } + } else { // Folia start - region threading + Entity realCamera = camera.getBukkitEntity().getHandleRaw(); + if (realCamera != camera) { + this.setCamera(this); + this.setCamera(realCamera); + } else { + this.teleportToCameraOffRegion(); + } + } + // Folia end - region threading } else { this.setCamera(this); } @@ -1357,9 +1516,332 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc } } + // Folia start - region threading + /** + * Teleport flag indicating that the player is to be respawned, expected to only be used + * internally for {@link #respawn(java.util.function.Consumer, PlayerRespawnEvent.RespawnReason)} + */ + public static final long TELEPORT_FLAGS_PLAYER_RESPAWN = Long.MIN_VALUE >>> 0; + + 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, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason reason) { + this.respawn(respawnComplete, reason, false); + } + + private void respawn(java.util.function.Consumer respawnComplete, org.bukkit.event.player.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.CallbackCompletable spawnPosComplete = + new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); + 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 + ((org.bukkit.craftbukkit.CraftWorld)spawnLoc.getWorld()).getHandle(), + TELEPORT_FLAG_LOAD_CHUNK | TELEPORT_FLAGS_PLAYER_RESPAWN, + 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.util.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( + new net.minecraft.world.entity.PositionMoveRotation( + pos, this.getDeltaMovement(), this.getYRot(), this.getXRot() + ), + java.util.Collections.emptySet() + ); + this.connection.resetPosition(); + this.setOldPosAndRot(); + 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); + + // required to set up the pending teleport stuff to the client, and to actually update + // the player's position clientside + this.connection.internalTeleport( + new net.minecraft.world.entity.PositionMoveRotation( + this.position(), this.getDeltaMovement(), 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.sendActivePlayerEffects(this); + 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); + + // required to set up the pending teleport stuff to the client, and to actually update + // the player's position clientside + this.connection.internalTeleport( + new net.minecraft.world.entity.PositionMoveRotation( + this.position(), this.getDeltaMovement(), 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)); + }*/ // handled by sendActivePlayerEffects + + // 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() == net.minecraft.world.level.dimension.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; + if (!this.seenCredits) { + this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 0.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() == net.minecraft.world.level.dimension.LevelStem.OVERWORLD && destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER) { + this.enteredNetherPosition = this.position(); + } + } + // Folia end - region threading + @Nullable @Override public ServerPlayer teleport(TeleportTransition teleportTransition) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading if (this.isSleeping()) return null; // CraftBukkit - SPIGOT-3154 if (this.isRemoved()) { return null; @@ -2397,7 +2879,30 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc return (Entity)(this.camera == null ? this : this.camera); } + // Folia start - region threading + private void teleportToCameraOffRegion() { + Entity cameraFinal = this.camera; + // use the task scheduler, as we don't know where the caller is invoking from + if (this != cameraFinal) { + this.getBukkitEntity().taskScheduler.schedule((final ServerPlayer newPlayer) -> { + io.papermc.paper.threadedregions.TeleportUtils.teleport( + newPlayer, false, cameraFinal, null, null, 0L, + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null, + (final ServerPlayer newerPlayer) -> { + return newerPlayer.camera == cameraFinal; + } + ); + }, null, 1L); + } // else: do not bother teleporting to self + } + // Folia end - region threading + public void setCamera(@Nullable Entity entityToSpectate) { + // Folia start - region threading + if (entityToSpectate != null && (entityToSpectate != this && !entityToSpectate.canBeSpectated())) { + return; + } + // Folia end - region threading Entity camera = this.getCamera(); this.camera = (Entity)(entityToSpectate == null ? this : entityToSpectate); if (camera != this.camera) { @@ -2416,16 +2921,19 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc } } // Paper end - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity - if (this.camera.level() instanceof ServerLevel serverLevel) { - this.teleportTo(serverLevel, this.camera.getX(), this.camera.getY(), this.camera.getZ(), Set.of(), this.getYRot(), this.getXRot(), false, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit - } + // Folia - region threading - move down - if (entityToSpectate != null) { - this.serverLevel().getChunkSource().move(this); - } + // Folia - region threading - not needed + // Folia start - region threading - handle camera setting better + if (this.camera == this + || (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.camera) && this.camera.moonrise$getTrackedEntity() != null + && this.camera.moonrise$getTrackedEntity().seenBy.contains(this.connection))) { + // Folia end - region threading - handle camera setting better this.connection.send(new ClientboundSetCameraPacket(this.camera)); - this.connection.resetPosition(); + } // Folia - region threading - handle camera setting better + //this.connection.resetPosition(); // Folia - region threading - not needed + this.teleportToCameraOffRegion(); // Folia - region threading - moved down } } @@ -2896,11 +3404,11 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc } public void registerEnderPearl(ThrownEnderpearl enderPearl) { - this.enderPearls.add(enderPearl); + //this.enderPearls.add(enderPearl); // Folia - region threading - do not track ender pearls } public void deregisterEnderPearl(ThrownEnderpearl enderPearl) { - this.enderPearls.remove(enderPearl); + //this.enderPearls.remove(enderPearl); // Folia - region threading - do not track ender pearls } public Set getEnderPearls() { @@ -3054,7 +3562,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc 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/net/minecraft/server/level/ServerPlayerGameMode.java b/net/minecraft/server/level/ServerPlayerGameMode.java index 623c069f1fe079e020c6391a3db1a3d95cd3dbf5..61804cdb6be06b1b3316e563df57f0b38268958a 100644 --- a/net/minecraft/server/level/ServerPlayerGameMode.java +++ b/net/minecraft/server/level/ServerPlayerGameMode.java @@ -114,7 +114,7 @@ public class ServerPlayerGameMode { // this.gameTicks = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit this.gameTicks = (int) this.level.getLagCompensationTick(); // Paper - lag compensate eating if (this.hasDelayedDestroy) { - BlockState blockState = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks + BlockState blockState = !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 (blockState == null || blockState.isAir()) { // Paper - Don't allow digging into unloaded chunks this.hasDelayedDestroy = false; } else { @@ -126,7 +126,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 - BlockState blockState = this.level.getBlockStateIfLoaded(this.destroyPos); + BlockState blockState = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.destroyPos) ? null : this.level.getBlockStateIfLoaded(this.destroyPos); // Folia - region threading - don't destroy blocks not owned if (blockState == null) { this.isDestroyingBlock = false; return; @@ -369,7 +369,7 @@ public class ServerPlayerGameMode { } else { // CraftBukkit start org.bukkit.block.BlockState state = bblock.getState(); - this.level.captureDrops = new java.util.ArrayList<>(); + this.level.getCurrentWorldData().captureDrops = new java.util.ArrayList<>(); // Folia - region threading // CraftBukkit end BlockState blockState1 = block.playerWillDestroy(this.level, pos, blockState, this.player); boolean flag = this.level.removeBlock(pos, false); @@ -395,8 +395,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/net/minecraft/server/level/TicketType.java b/net/minecraft/server/level/TicketType.java index 8f12a4df5d63ecd11e6e615d910b6e3f6dde5f3c..f8b74eaf534c6264ce018a6826c3d035089e7d30 100644 --- a/net/minecraft/server/level/TicketType.java +++ b/net/minecraft/server/level/TicketType.java @@ -17,10 +17,18 @@ public class TicketType { public static final TicketType FORCED = create("forced", Comparator.comparingLong(ChunkPos::toLong)); public static final TicketType PORTAL = create("portal", Vec3i::compareTo, 300); public static final TicketType ENDER_PEARL = create("ender_pearl", Comparator.comparingLong(ChunkPos::toLong), 40); - public static final TicketType UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1); + public static final TicketType UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 5); // Folia - region threading 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 public static final TicketType POST_TELEPORT = TicketType.create("post_teleport", Integer::compare, 5); // Paper - post teleport ticket type + // 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 comparator) { return new TicketType<>(name, comparator, 0L); diff --git a/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java index 7fa41dea184b01891f45d8e404bc1cba19cf1bcf..43de96cc3c2b1259b1edb5feae3f202dea65dcdf 100644 --- a/net/minecraft/server/level/WorldGenRegion.java +++ b/net/minecraft/server/level/WorldGenRegion.java @@ -107,6 +107,13 @@ public class WorldGenRegion implements WorldGenLevel { return this.getLightEngine().getRawBrightness(blockPos, subtract); } // 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 level, StaticCache2D cache, ChunkStep generatingStep, ChunkAccess center) { this.generatingStep = generatingStep; @@ -118,6 +125,7 @@ public class WorldGenRegion implements WorldGenLevel { this.random = level.getChunkSource().randomState().getOrCreateRandomFactory(WORLDGEN_REGION_RANDOM).at(this.center.getPos().getWorldPosition()); this.dimensionType = level.dimensionType(); this.biomeManager = new BiomeManager(this, BiomeManager.obfuscateSeed(this.seed)); + this.structureManager = level.structureManager().forWorldGenRegion(this); // Folia - region threading } public boolean isOldChunkAround(ChunkPos pos, int radius) { diff --git a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java index e71c1a564e5d4ac43460f89879ff709ee685706f..6eca15223b92aedac74233db886e2c1248750e2c 100644 --- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java @@ -96,6 +96,10 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack } } + // Folia start - region threading + private boolean handledDisconnect = false; + // Folia end - region threading + @Override public void onDisconnect(DisconnectionDetails details) { // Paper start - Fix kick event leave message not being sent @@ -104,10 +108,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()) { LOGGER.info("Stopping singleplayer server as player logged out"); this.server.halt(false); } + this.player.getBukkitEntity().taskScheduler.retire(); // Folia - region threading } @Override @@ -330,24 +342,8 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack if (this.processedDisconnect) { return; } - if (!this.cserver.isPrimaryThread()) { - org.bukkit.craftbukkit.util.Waitable waitable = new org.bukkit.craftbukkit.util.Waitable() { - @Override - protected Object evaluate() { - ServerCommonPacketListenerImpl.this.disconnect(disconnectionDetails, cause); // Paper - kick event causes - return null; - } - }; - - this.server.processQueue.add(waitable); - - try { - waitable.get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (java.util.concurrent.ExecutionException e) { - throw new RuntimeException(e); - } + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player)) { // Folia - region threading + this.connection.disconnectSafely(disconnectionDetails, cause); // Folia - region threading - it HAS to be delayed/async to avoid deadlock if we try to wait for another region return; } @@ -378,7 +374,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack this.onDisconnect(disconnectionDetails, leaveMessage); // CraftBukkit - fire quit instantly // Paper - use kick event leave message this.connection.setReadOnly(); // CraftBukkit - Don't wait - this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper + //this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper // Folia - region threading } // Paper start - add proper async disconnect @@ -391,19 +387,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack } public void disconnectAsync(DisconnectionDetails disconnectionInfo, org.bukkit.event.player.PlayerKickEvent.Cause cause) { - if (this.cserver.isPrimaryThread()) { - this.disconnect(disconnectionInfo, cause); - return; - } - - this.connection.setReadOnly(); - this.server.scheduleOnMain(() -> { - ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause); - if (ServerCommonPacketListenerImpl.this.player.quitReason == null) { - // cancelled - ServerCommonPacketListenerImpl.this.connection.enableAutoRead(); - } - }); + this.disconnect(disconnectionInfo, cause); // Folia - threaded regions } // Paper end - add proper async disconnect diff --git a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java index 2e9eb04c7c4342393c05339906c267bca9ff29b1..00fb8a5dda1f305a0e0f947bbb75a3f40b5318cc 100644 --- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java @@ -47,6 +47,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 server, Connection connection, CommonListenerCookie cookie, ServerPlayer player) { @@ -160,7 +161,58 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis } ServerPlayer playerForLogin = playerList.getPlayerForLogin(this.gameProfile, this.clientInformation, this.player); // CraftBukkit - playerList.placeNewPlayer(this.connection, playerForLogin, 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.CallbackCompletable toComplete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); + // 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, + playerForLogin, + clientData, + java.util.Optional.ofNullable(data.getValue()), + lastKnownName.getValue(), + loc + ); + }, + ca.spottedleaf.concurrentutil.util.Priority.HIGHER + ); + }); + this.switchToMain = true; + try { + // now the connection responsibility is transferred on the region + playerList.loadSpawnForNewPlayer(this.connection, playerForLogin, 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 var5) { LOGGER.error("Couldn't place player in world", (Throwable)var5); // Paper start - Debugging diff --git a/net/minecraft/server/network/ServerConnectionListener.java b/net/minecraft/server/network/ServerConnectionListener.java index bd07e6a5aa1883786d789ea71711a0c0c0a95c26..09469ad131622158fe5579216fc4164251ff2220 100644 --- a/net/minecraft/server/network/ServerConnectionListener.java +++ b/net/minecraft/server/network/ServerConnectionListener.java @@ -167,12 +167,15 @@ public class ServerConnectionListener { } // Paper end - Add support for proxy protocol // ServerConnectionListener.this.connections.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking - ServerConnectionListener.this.pending.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking + //ServerConnectionListener.this.pending.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking // Folia - connection fixes - move down connection.configurePacketHandler(channelPipeline); connection.setListenerForServerboundHandshake( new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, connection) ); io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper - Add Channel initialization listeners + // Folia start - regionised threading + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addConnection(connection); + // Folia end - regionised threading } } ) @@ -242,7 +245,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); } // Spigot end diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java index 1205307b7d2336fa6c5395a65be6643228c49d72..a2fe9286d432909ba0cb3731a166514af768dc37 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -292,10 +292,10 @@ public class ServerGamePacketListenerImpl private int knownMovePacketCount; private boolean receivedMovementThisTick; // CraftBukkit start - add fields - 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; @@ -313,10 +313,17 @@ public class ServerGamePacketListenerImpl 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 private final io.papermc.paper.event.packet.ClientTickEndEvent tickEndEvent; // Paper - add client tick end event + // 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 + public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie cookie) { super(server, connection, cookie, player); // CraftBukkit this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection()); @@ -330,6 +337,12 @@ public class ServerGamePacketListenerImpl @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; @@ -378,7 +391,7 @@ public class ServerGamePacketListenerImpl this.aboveGroundVehicleTickCount = 0; } - this.keepConnectionAlive(); + // Folia - region threading - moved to beginning of method this.chatSpamThrottler.tick(); this.dropSpamThrottler.tick(); this.tabSpamThrottler.tick(); // Paper - configurable tab spam limits @@ -414,6 +427,19 @@ public class ServerGamePacketListenerImpl 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 @@ -521,9 +547,10 @@ public class ServerGamePacketListenerImpl // 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; @@ -590,7 +617,7 @@ public class ServerGamePacketListenerImpl } rootVehicle.absMoveTo(d, d1, d2, f, f1); - this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit + //this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - move to repositionAllPassengers // Paper start - optimise out extra getCubes boolean teleportBack = flag2; // violating this is always a fail if (!teleportBack) { @@ -602,11 +629,19 @@ public class ServerGamePacketListenerImpl } if (teleportBack) { // Paper end - optimise out extra getCubes rootVehicle.absMoveTo(x, y, z, f, f1); - this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit + //this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - not needed, the player is no longer updated this.send(ClientboundMoveVehiclePacket.fromEntity(rootVehicle)); 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 + rootVehicle.repositionAllPassengers(false); + // Folia end - move to positionRider + // CraftBukkit start - fire PlayerMoveEvent org.bukkit.entity.Player player = this.getCraftPlayer(); if (!this.hasMoved) { @@ -637,7 +672,7 @@ public class ServerGamePacketListenerImpl // 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; } @@ -645,7 +680,7 @@ public class ServerGamePacketListenerImpl // 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; } @@ -826,7 +861,7 @@ public class ServerGamePacketListenerImpl } // 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); @@ -1210,11 +1245,11 @@ public class ServerGamePacketListenerImpl } // Paper end - Book size limits // CraftBukkit start - if (this.lastBookTick + 20 > MinecraftServer.currentTick) { + if (this.lastBookTick + 20 > this.lastTick) { // Folia - region threading this.disconnectAsync(Component.literal("Book edited too quickly!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - add proper async disconnect return; } - this.lastBookTick = MinecraftServer.currentTick; + this.lastBookTick = this.lastTick; // Folia - region threading // CraftBukkit end int slot = packet.slot(); if (Inventory.isHotbarSlot(slot) || slot == 40) { @@ -1225,7 +1260,22 @@ public class ServerGamePacketListenerImpl Consumer> consumer = optional.isPresent() ? texts -> this.signBook(texts.get(0), texts.subList(1, texts.size()), slot) : texts -> this.updateBookContents(texts, slot); - this.filterTextPacket(list).thenAcceptAsync(consumer, this.server); + // Folia start - region threading + this.filterTextPacket(list).thenAcceptAsync( + consumer, + (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 } } @@ -1351,9 +1401,10 @@ public class ServerGamePacketListenerImpl 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)) { LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i); @@ -1542,7 +1593,7 @@ public class ServerGamePacketListenerImpl // 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; } @@ -1550,7 +1601,7 @@ public class ServerGamePacketListenerImpl // 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; } @@ -1810,9 +1861,9 @@ public class ServerGamePacketListenerImpl 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()) { // Folia - region threading this.dropCount = 0; - this.lastDropTick = MinecraftServer.currentTick; + this.lastDropTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading } else { // Else we increment the drop count and check the amount. this.dropCount++; @@ -1843,7 +1894,7 @@ public class ServerGamePacketListenerImpl case ABORT_DESTROY_BLOCK: case STOP_DESTROY_BLOCK: // Paper start - Don't allow digging into unloaded chunks - if (this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !this.player.canInteractWithBlock(pos, 1.0)) { + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), pos.getX() >> 4, pos.getZ() >> 4, 8) || this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !this.player.canInteractWithBlock(pos, 1.0)) { // Folia - region threading - don't destroy blocks not owned this.player.connection.ackBlockChangesUpTo(packet.getSequence()); return; } @@ -1926,7 +1977,7 @@ public class ServerGamePacketListenerImpl } // Paper end - improve distance check BlockPos blockPos = hitResult.getBlockPos(); - if (this.player.canInteractWithBlock(blockPos, 1.0)) { + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), blockPos.getX() >> 4, blockPos.getZ() >> 4, 8) && this.player.canInteractWithBlock(blockPos, 1.0)) { // Folia - do not allow players to interact with blocks outside the current region Vec3 vec3 = location.subtract(Vec3.atCenterOf(blockPos)); double d = 1.0000001; if (Math.abs(vec3.x()) < 1.0000001 && Math.abs(vec3.y()) < 1.0000001 && Math.abs(vec3.z()) < 1.0000001) { @@ -2048,7 +2099,7 @@ public class ServerGamePacketListenerImpl for (ServerLevel serverLevel : this.server.getAllLevels()) { Entity entity = packet.getEntity(serverLevel); if (entity != null) { - this.player.teleportTo(serverLevel, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true, 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; } } @@ -2080,7 +2131,7 @@ public class ServerGamePacketListenerImpl } // CraftBukkit end LOGGER.info("{} lost connection: {}", this.player.getName().getString(), details.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(details, quitMessage); // Paper - Fix kick event leave message not being sent } @@ -2089,6 +2140,8 @@ public class ServerGamePacketListenerImpl 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(); @@ -2102,6 +2155,8 @@ public class ServerGamePacketListenerImpl 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 - Adventure @@ -2341,7 +2396,7 @@ public class ServerGamePacketListenerImpl this.player.resetLastActionTime(); // CraftBukkit start if (sync) { - this.server.execute(handler); + this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> handler.run(), null, 1L); // Folia - region threading } else { handler.run(); } @@ -2396,7 +2451,7 @@ public class ServerGamePacketListenerImpl 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()); @@ -2493,6 +2548,7 @@ public class ServerGamePacketListenerImpl if (rawMessage.isEmpty()) { LOGGER.warn("{} tried to send an empty message", this.player.getScoreboardName()); } else if (this.getCraftPlayer().isConversing()) { + if (true) throw new UnsupportedOperationException(); // Folia - region threading final String conversationInput = rawMessage; this.server.processQueue.add(() -> ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput)); } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { // Re-add "Command Only" flag check @@ -2718,8 +2774,25 @@ public class ServerGamePacketListenerImpl // 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); } @@ -2744,7 +2817,7 @@ public class ServerGamePacketListenerImpl // Spigot end this.player.resetLastActionTime(); this.player.setShiftKeyDown(packet.isUsingSecondaryAction()); - if (target != null) { + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(target) && target != null) { // Folia - region threading - do not allow interaction of entities outside the current region if (!serverLevel.getWorldBorder().isWithinBounds(target.blockPosition())) { return; } @@ -2877,6 +2950,12 @@ public class ServerGamePacketListenerImpl switch (action) { 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 this.resetPosition(); @@ -2886,6 +2965,17 @@ public class ServerGamePacketListenerImpl return; } + // Folia start - region threading + if (true) { + this.player.respawn((ServerPlayer player) -> { + if (ServerGamePacketListenerImpl.this.server.isHardcore()) { + ServerGamePacketListenerImpl.this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent + ((GameRules.BooleanValue) ServerGamePacketListenerImpl.this.player.serverLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, ServerGamePacketListenerImpl.this.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 this.resetPosition(); if (this.server.isHardcore()) { @@ -3462,7 +3552,21 @@ public class ServerGamePacketListenerImpl } List list = Stream.of(lines).map(ChatFormatting::stripFormatting).collect(Collectors.toList()); // Paper end - Limit client sign length - this.filterTextPacket(list).thenAcceptAsync(list1 -> this.updateSignText(packet, (List)list1), this.server); + // Folia start - region threading + this.filterTextPacket(list).thenAcceptAsync((list1) -> { + this.updateSignText(packet, (List)list1); + }, (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 sign update packet", thr); + } + }); + // Folia end - region threading } private void updateSignText(ServerboundSignUpdatePacket packet, List filteredText) { diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java index 6689aeacf50d1498e8d23cce696fb4fecbd1cf39..6173f704b0d093813ec67eb231c75be49a462e7d 100644 --- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java @@ -111,7 +111,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(Objects.requireNonNull(this.authenticatedProfile)); } // Paper - prevent logins to be processed even though disconnect was called } @@ -250,7 +254,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, ); } - boolean flag = playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference + boolean flag = false && playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference // Folia - rewrite login process - always false here if (flag) { this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT; } else { @@ -362,7 +366,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/net/minecraft/server/players/BanListEntry.java b/net/minecraft/server/players/BanListEntry.java index e111adec2116f922fe67ee434635e50c60dad15c..851d3ae5d37541e6455b83b3300d630e8f6d5c83 100644 --- a/net/minecraft/server/players/BanListEntry.java +++ b/net/minecraft/server/players/BanListEntry.java @@ -9,7 +9,7 @@ import javax.annotation.Nullable; 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; @@ -30,7 +30,7 @@ public abstract class BanListEntry extends StoredUserEntry { Date date; try { - date = entryData.has("created") ? DATE_FORMAT.parse(entryData.get("created").getAsString()) : new Date(); + date = entryData.has("created") ? DATE_FORMAT.get().parse(entryData.get("created").getAsString()) : new Date(); // Folia - region threading - SDF is not thread-safe } catch (ParseException var7) { date = new Date(); } @@ -40,7 +40,7 @@ public abstract class BanListEntry extends StoredUserEntry { Date date1; try { - date1 = entryData.has("expires") ? DATE_FORMAT.parse(entryData.get("expires").getAsString()) : null; + date1 = entryData.has("expires") ? DATE_FORMAT.get().parse(entryData.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe } catch (ParseException var6) { date1 = null; } @@ -75,9 +75,9 @@ public abstract class BanListEntry extends StoredUserEntry { @Override protected void serialize(JsonObject data) { - data.addProperty("created", DATE_FORMAT.format(this.created)); + data.addProperty("created", DATE_FORMAT.get().format(this.created)); // Folia - region threading - SDF is not thread-safe data.addProperty("source", this.source); - data.addProperty("expires", this.expires == null ? "forever" : DATE_FORMAT.format(this.expires)); + data.addProperty("expires", this.expires == null ? "forever" : DATE_FORMAT.get().format(this.expires)); // Folia - region threading - SDF is not thread-safe data.addProperty("reason", this.reason); } @@ -86,7 +86,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/net/minecraft/server/players/OldUsersConverter.java b/net/minecraft/server/players/OldUsersConverter.java index 7dbcd9d96f052bb10127ad2b061154c23cc9ffd4..20d895ed04cd2263560f91ef38dda6aa866bc603 100644 --- a/net/minecraft/server/players/OldUsersConverter.java +++ b/net/minecraft/server/players/OldUsersConverter.java @@ -469,7 +469,7 @@ public class OldUsersConverter { static Date parseDate(String input, Date defaultValue) { Date date; try { - date = BanListEntry.DATE_FORMAT.parse(input); + date = BanListEntry.DATE_FORMAT.get().parse(input); // Folia - region threading - SDF is not thread-safe } catch (ParseException var4) { date = defaultValue; } diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java index 5a4960fdbd97d830ac79845697eea9372c48a13b..7b13a9e7d38efe7786023747f55ebf5a2ba80688 100644 --- a/net/minecraft/server/players/PlayerList.java +++ b/net/minecraft/server/players/PlayerList.java @@ -110,10 +110,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 = new UserBanList(USERBANLIST_FILE); private final IpBanList ipBans = new IpBanList(IPBANLIST_FILE); private final ServerOpList ops = new ServerOpList(OPLIST_FILE); @@ -137,6 +137,60 @@ public abstract class PlayerList { private final Map playersByName = new java.util.HashMap<>(); 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 registries, PlayerDataStorage playerIo, int maxPlayers) { this.cserver = server.server = new org.bukkit.craftbukkit.CraftServer((net.minecraft.server.dedicated.DedicatedServer) server, this); server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper @@ -149,7 +203,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 cookie) { + public void loadSpawnForNewPlayer(final Connection connection, final ServerPlayer player, final CommonListenerCookie cookie, org.apache.commons.lang3.mutable.MutableObject data, org.apache.commons.lang3.mutable.MutableObject lastKnownName, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { // Folia - region threading - rewrite login process player.isRealPlayer = true; // Paper player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed GameProfile gameProfile = player.getGameProfile(); @@ -221,17 +275,41 @@ 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(string); + // 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(serverLevel, serverLevel.getSharedSpawnPos()).getBottomCenter(), serverLevel.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored + ServerPlayer.fudgeSpawnLocation(serverLevel, player, toComplete); // Paper - MC-200092 - fix first spawn pos yaw being ignored // Folia - region threading + } else { + serverLevel.loadChunksForMoveAsync( + player.getBoundingBox(), + ca.spottedleaf.concurrentutil.util.Priority.HIGHER, + (c) -> { + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(serverLevel, 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 cookie, Optional optional, String string, org.bukkit.Location selectedSpawn) { + ServerLevel serverLevel = ((org.bukkit.craftbukkit.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(serverLevel); String loggableAddress = connection.getLoggableAddress(this.server.logIPs()); // Spigot start - spawn location event org.bukkit.entity.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? org.bukkit.Location loc = ev.getSpawnLocation(); serverLevel = ((org.bukkit.craftbukkit.CraftWorld) loc.getWorld()).getHandle(); @@ -254,6 +332,11 @@ public abstract class PlayerList { LevelData levelData = serverLevel.getLevelData(); player.loadGameTypes(optional.orElse(null)); ServerGamePacketListenerImpl serverGamePacketListenerImpl = new ServerGamePacketListenerImpl(this.server, connection, player, cookie); + // Folia start - rewrite login process + // only after setting the connection listener to game type, add the connection to this regions list + serverLevel.getCurrentWorldData().connections.add(connection); + // Folia end - rewrite login process + connection.setupInboundProtocol( GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), serverGamePacketListenerImpl ); @@ -287,7 +370,7 @@ public abstract class PlayerList { this.sendPlayerPermissionLevel(player); player.getStats().markAllDirty(); player.getRecipeBook().sendInitialRecipeBook(player); - this.updateEntireScoreboard(serverLevel.getScoreboard(), player); + //this.updateEntireScoreboard(serverLevel.getScoreboard(), player); // Folia - region threading this.server.invalidateStatus(); MutableComponent mutableComponent; if (player.getGameProfile().getName().equalsIgnoreCase(string)) { @@ -327,7 +410,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(); @@ -342,8 +425,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 @@ -392,7 +474,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 @@ -482,7 +564,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 serverStatsCounter = player.getStats(); // CraftBukkit if (serverStatsCounter != null) { @@ -517,7 +599,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 (player.getTeam() == team && team != null) { @@ -566,7 +648,7 @@ public abstract class PlayerList { } serverLevel.removePlayerImmediately(player, Entity.RemovalReason.UNLOADED_WITH_PLAYER); - player.retireScheduler(); // Paper - Folia schedulers + //player.retireScheduler(); // Paper - Folia schedulers // Folia - region threading - move to onDisconnect of common packet listener player.getAdvancements().stopListening(); this.players.remove(player); this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot @@ -584,8 +666,7 @@ public abstract class PlayerList { // CraftBukkit start // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID()))); ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())); - for (int i = 0; i < this.players.size(); i++) { - ServerPlayer otherPlayer = (ServerPlayer) this.players.get(i); + for (ServerPlayer otherPlayer : this.players) { // Folia - region threading if (otherPlayer.getBukkitEntity().canSee(player.getBukkitEntity())) { otherPlayer.connection.send(packet); @@ -609,19 +690,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 java.util.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 @@ -641,7 +715,7 @@ public abstract class PlayerList { MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned.reason", userBanListEntry.getReason()); if (userBanListEntry.getExpires() != null) { mutableComponent.append( - Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.format(userBanListEntry.getExpires())) + Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.get().format(userBanListEntry.getExpires())) // Folia - region threading - SDF is not thread-safe ); } @@ -655,7 +729,7 @@ public abstract class PlayerList { MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipBanListEntry.getReason()); if (ipBanListEntry.getExpires() != null) { mutableComponent.append( - Component.translatable("multiplayer.disconnect.banned_ip.expiration", BAN_DATE_FORMAT.format(ipBanListEntry.getExpires())) + Component.translatable("multiplayer.disconnect.banned_ip.expiration", BAN_DATE_FORMAT.get().format(ipBanListEntry.getExpires())) // Folia - region threading - SDF is not thread-safe ); } @@ -665,7 +739,7 @@ public abstract class PlayerList { // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile) // ? Component.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(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure } } @@ -714,6 +788,11 @@ public abstract class PlayerList { return this.respawn(player, keepInventory, reason, eventReason, null); } public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, org.bukkit.Location location) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading player.stopRiding(); // CraftBukkit this.players.remove(player); this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot @@ -884,10 +963,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 = 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), com.google.common.collect.Collections2.filter(this.players, t -> target.getBukkitEntity().canSee(t.getBukkitEntity())))); + target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), com.google.common.collect.Collections2.filter(java.util.Arrays.asList(players),t -> target.getBukkitEntity().canSee(t.getBukkitEntity())))); // Folia - region threading } // CraftBukkit end this.sendAllPlayerInfoIn = 0; @@ -896,18 +975,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 } } @@ -944,8 +1022,7 @@ public abstract class PlayerList { if (team == null) { this.broadcastSystemMessage(message, false); } else { - for (int i = 0; i < this.players.size(); i++) { - ServerPlayer serverPlayer = this.players.get(i); + for (ServerPlayer serverPlayer : this.players) { // Folia - region threading if (serverPlayer.getTeam() != team) { serverPlayer.sendSystemMessage(message); } @@ -954,10 +1031,11 @@ public abstract class PlayerList { } public String[] getPlayerNamesArray() { + List players = new java.util.ArrayList<>(this.players); // Folia - region threading String[] strings = new String[this.players.size()]; - for (int i = 0; i < this.players.size(); i++) { - strings[i] = this.players.get(i).getGameProfile().getName(); + for (int i = 0; i < players.size(); i++) { // Folia - region threading + strings[i] = players.get(i).getGameProfile().getName(); // Folia - region threading } return strings; @@ -975,7 +1053,9 @@ public abstract class PlayerList { this.ops.add(new ServerOpListEntry(profile, this.server.getOperatorUserPermissionLevel(), this.ops.canBypassPlayerLimit(profile))); ServerPlayer player = this.getPlayer(profile.getId()); if (player != null) { - this.sendPlayerPermissionLevel(player); + player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading + this.sendPlayerPermissionLevel(serverPlayer); // Folia - region threading + }, null, 1L); // Folia - region threading } } @@ -983,7 +1063,9 @@ public abstract class PlayerList { this.ops.remove(profile); ServerPlayer player = this.getPlayer(profile.getId()); if (player != null) { - this.sendPlayerPermissionLevel(player); + player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading + this.sendPlayerPermissionLevel(serverPlayer); // Folia - region threading + }, null, 1L); // Folia - region threading } } @@ -1046,8 +1128,7 @@ public abstract class PlayerList { } public void broadcast(@Nullable Player except, double x, double y, double z, double radius, ResourceKey dimension, Packet packet) { - for (int i = 0; i < this.players.size(); i++) { - ServerPlayer serverPlayer = this.players.get(i); + for (ServerPlayer serverPlayer : this.players) { // Folia - region threading // CraftBukkit start - Test if player receiving packet can see the source of the packet if (except != null && !serverPlayer.getBukkitEntity().canSee(except.getBukkitEntity())) { continue; @@ -1072,10 +1153,15 @@ public abstract class PlayerList { public void saveAll(final int interval) { io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main int numSaved = 0; - final long now = MinecraftServer.currentTick; - for (int i = 0; i < this.players.size(); i++) { - final ServerPlayer player = this.players.get(i); - if (interval == -1 || now - player.lastSave >= interval) { + final long now = System.nanoTime(); // Folia - region threading + long timeInterval = (long)interval * io.papermc.paper.threadedregions.TickRegionScheduler.TIME_BETWEEN_TICKS; // Folia - region threading + for (final ServerPlayer player : this.players) { // Folia - region threading + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { + continue; + } + // Folia end - region threading + if (interval == -1 || now - player.lastSave >= timeInterval) { // Folia - region threading this.save(player); if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { break; @@ -1194,6 +1280,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) { @@ -1203,7 +1303,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/net/minecraft/server/players/StoredUserList.java b/net/minecraft/server/players/StoredUserList.java index d445e8f126f077d8419c52fa5436ea963a1a42a4..cabbc68f7cd5fd326c7ffd3b02b3ec4a4390f5b0 100644 --- a/net/minecraft/server/players/StoredUserList.java +++ b/net/minecraft/server/players/StoredUserList.java @@ -97,6 +97,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(); this.map.values().stream().map(storedEntry -> Util.make(new JsonObject(), storedEntry::serialize)).forEach(jsonArray::add); @@ -104,9 +105,11 @@ public abstract class StoredUserList> { try (BufferedWriter writer = Files.newWriter(this.file, StandardCharsets.UTF_8)) { GSON.toJson(jsonArray, GSON.newJsonWriter(writer)); } + } // Folia - region threading } public void load() throws IOException { + synchronized (this) { // Folia - region threading if (this.file.exists()) { try (BufferedReader reader = Files.newReader(this.file, StandardCharsets.UTF_8)) { this.map.clear(); @@ -131,5 +134,6 @@ public abstract class StoredUserList> { } // Spigot end } + } // Folia - region threading } } diff --git a/net/minecraft/util/SpawnUtil.java b/net/minecraft/util/SpawnUtil.java index f6fad24af884c8a37723c57718cee0096443efe6..ef75a71aee2576254e2c0752cc759c9637af9d69 100644 --- a/net/minecraft/util/SpawnUtil.java +++ b/net/minecraft/util/SpawnUtil.java @@ -83,7 +83,7 @@ public class SpawnUtil { return Optional.of(mob); } - mob.discard(null); // CraftBukkit - add Bukkit remove cause + //mob.discard(null); // CraftBukkit - add Bukkit remove cause // Folia - region threading } } } diff --git a/net/minecraft/world/RandomSequences.java b/net/minecraft/world/RandomSequences.java index f8e93fe461794058a26c90510cbd7698fa43b8f7..c1a62556546b05f79dad37875993c19bf9bb7c67 100644 --- a/net/minecraft/world/RandomSequences.java +++ b/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<>( @@ -120,61 +120,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/net/minecraft/world/damagesource/CombatTracker.java b/net/minecraft/world/damagesource/CombatTracker.java index d3de87eaf0eb84af77165391c7b94085d425f21d..019d1435cfe7769ed85b40c1c19d2d271d96f913 100644 --- a/net/minecraft/world/damagesource/CombatTracker.java +++ b/net/minecraft/world/damagesource/CombatTracker.java @@ -53,7 +53,7 @@ public class CombatTracker { } private Component getMessageForAssistedFall(Entity entity, Component entityDisplayName, String hasWeaponTranslationKey, String noWeaponTranslationKey) { - ItemStack itemStack = entity instanceof LivingEntity livingEntity ? livingEntity.getMainHandItem() : ItemStack.EMPTY; + ItemStack itemStack = entity 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(hasWeaponTranslationKey, this.mob.getDisplayName(), entityDisplayName, itemStack.getDisplayName()) : Component.translatable(noWeaponTranslationKey, this.mob.getDisplayName(), entityDisplayName); @@ -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/net/minecraft/world/damagesource/DamageSource.java b/net/minecraft/world/damagesource/DamageSource.java index daeb344f9e21e8155326b47aeccd4cfc07da42cd..9a204bbd91f48a3097471d1273d65c185b353fcc 100644 --- a/net/minecraft/world/damagesource/DamageSource.java +++ b/net/minecraft/world/damagesource/DamageSource.java @@ -163,12 +163,12 @@ public class DamageSource { if (this.causingEntity == null && this.directEntity == null) { LivingEntity killCredit = livingEntity.getKillCredit(); String string1 = string + ".player"; - return killCredit != null + return killCredit != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(killCredit) ? Component.translatable(string1, livingEntity.getDisplayName(), killCredit.getDisplayName()) : Component.translatable(string, livingEntity.getDisplayName()); } else { Component component = this.causingEntity == null ? this.directEntity.getDisplayName() : this.causingEntity.getDisplayName(); - ItemStack itemStack = this.causingEntity instanceof LivingEntity livingEntity1 ? livingEntity1.getMainHandItem() : ItemStack.EMPTY; + ItemStack itemStack = this.causingEntity instanceof LivingEntity livingEntity1 && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity1) ? livingEntity1.getMainHandItem() : ItemStack.EMPTY; // Folia - region threading return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME) ? Component.translatable(string + ".item", livingEntity.getDisplayName(), component, itemStack.getDisplayName()) : Component.translatable(string, livingEntity.getDisplayName(), component); diff --git a/net/minecraft/world/damagesource/FallLocation.java b/net/minecraft/world/damagesource/FallLocation.java index a3c7d68469075bf8d33f2016149a181b0fb87e0e..73c581d3ee21d8fa96eae3e47afd6ce204e03160 100644 --- a/net/minecraft/world/damagesource/FallLocation.java +++ b/net/minecraft/world/damagesource/FallLocation.java @@ -35,7 +35,7 @@ public record FallLocation(String id) { @Nullable public static FallLocation getCurrentFallLocation(LivingEntity entity) { Optional lastClimbablePos = entity.getLastClimbablePos(); - if (lastClimbablePos.isPresent()) { + if (lastClimbablePos.isPresent() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)entity.level(), lastClimbablePos.get())) { // Folia - region threading BlockState blockState = entity.level().getBlockState(lastClimbablePos.get()); return blockToFallLocation(blockState); } else { diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java index b2b61203438bb1fad1ee807729781718d2467155..2cd2ce1060f567be6c72b7bc9d02651ec7166203 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -145,7 +145,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(); + public static RandomSource SHARED_RANDOM = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading // Paper start - replace random private static final class RandomRandomSource extends ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom { public RandomRandomSource() { @@ -175,7 +175,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 @Nullable org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity; + private volatile @Nullable org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity; // Folia - region threading public org.bukkit.craftbukkit.entity.CraftEntity getBukkitEntity() { if (this.bukkitEntity == null) { @@ -294,7 +294,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess private boolean hasGlowingTag; private final Set tags = new io.papermc.paper.util.SizeLimitedSet<>(new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(), MAX_ENTITY_TAG_COUNT); // Paper - fully limit tag size - replace set impl private final double[] pistonDeltas = new double[]{0.0, 0.0, 0.0}; - private long pistonDeltasGameTime; + private long pistonDeltasGameTime = Long.MIN_VALUE; // Folia - region threading private EntityDimensions dimensions; private float eyeHeight; public boolean isInPowderSnow; @@ -522,6 +522,23 @@ 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; + } + } + + public boolean canBeSpectated() { + return !this.getBukkitEntity().taskScheduler.isRetired(); + } + // Folia end - region ticking public Entity(EntityType entityType, Level level) { this.type = entityType; @@ -652,8 +669,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; } @@ -820,7 +836,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 @@ -838,7 +854,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.boardingCooldown--; } - 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(); } @@ -1101,8 +1117,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } else { this.wasOnFire = this.isOnFire(); if (type == MoverType.PISTON) { - this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper - EAR 2 - this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper - EAR 2 + this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading + this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading movement = this.limitPistonMovement(movement); if (movement.equals(Vec3.ZERO)) { return; @@ -1401,7 +1417,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess if (pos.lengthSqr() <= 1.0E-7) { return pos; } else { - long gameTime = this.level().getGameTime(); + long gameTime = this.level().getRedstoneGameTime(); // Folia - region threading if (gameTime != this.pistonDeltasGameTime) { Arrays.fill(this.pistonDeltas, 0.0); this.pistonDeltasGameTime = gameTime; @@ -3035,6 +3051,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } if (force || this.canRide(vehicle) && vehicle.canAddPassenger(this)) { + if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen // CraftBukkit start if (vehicle.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && this.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) { org.bukkit.event.vehicle.VehicleEnterEvent event = new org.bukkit.event.vehicle.VehicleEnterEvent((org.bukkit.entity.Vehicle) vehicle.getBukkitEntity(), this.getBukkitEntity()); @@ -3056,6 +3073,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return false; } // CraftBukkit end + } // Folia - region threading - suppress entire event logic during worldgen if (this.isPassenger()) { this.stopRiding(); } @@ -3123,7 +3141,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 } } @@ -3137,6 +3155,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess throw new IllegalStateException("Use x.stopRiding(y), not y.removePassenger(x)"); } else { // CraftBukkit start + if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen org.bukkit.craftbukkit.entity.CraftEntity craft = (org.bukkit.craftbukkit.entity.CraftEntity) passenger.getBukkitEntity().getVehicle(); Entity orig = craft == null ? null : craft.getHandle(); if (this.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && passenger.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) { @@ -3164,6 +3183,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return false; } // CraftBukkit end + } // Folia - region threading - suppress entire event logic during worldgen if (this.passengers.size() == 1 && this.passengers.get(0) == passenger) { this.passengers = ImmutableList.of(); } else { @@ -3171,7 +3191,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } passenger.boardingCooldown = 60; - this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger); + if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger); // Folia - region threading - do not fire game events for entities not added } return true; // CraftBukkit } @@ -3255,7 +3275,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } } - protected void handlePortal() { + public boolean handlePortal() { // Folia - region threading - public, ret type -> boolean if (this.level() instanceof ServerLevel serverLevel) { this.processPortalCooldown(); if (this.portalProcess != null) { @@ -3263,21 +3283,20 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("portal"); this.setPortalCooldown(); - TeleportTransition portalDestination = this.portalProcess.getPortalDestination(serverLevel, this); - if (portalDestination != null) { - ServerLevel level = portalDestination.newLevel(); - if (this instanceof ServerPlayer // CraftBukkit - always call event for players - || (level != null && (level.dimension() == serverLevel.dimension() || this.canTeleport(serverLevel, level)))) { // CraftBukkit - this.teleport(portalDestination); - } + // Folia start - region threading + try { + return this.portalProcess.portalAsync(serverLevel, this); + } finally { + profilerFiller.pop(); } - - profilerFiller.pop(); + // Folia end - region threading } else if (this.portalProcess.hasExpired()) { this.portalProcess = null; } } } + + return false; // Folia - region threading } public int getDimensionChangingDelay() { @@ -3417,6 +3436,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()); } @@ -3723,8 +3747,793 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess this.portalProcess = entity.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.util.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(); + // this is copied from teleportSetPosition + this.movementThisTick.clear(); + } + + 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.setOldPosAndRot(); + this.resetStoredPositions(); + } + + protected final void transform(TeleportTransition telpeort) { + PositionMoveRotation move = PositionMoveRotation.calculateAbsolute( + PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives() + ); + this.transform( + move.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement() + ); + } + + 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); + } + this.setOldPosAndRot(); + } + + protected final Entity transformForAsyncTeleport(TeleportTransition telpeort) { + PositionMoveRotation move = PositionMoveRotation.calculateAbsolute( + PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives() + ); + return this.transformForAsyncTeleport( + telpeort.newLevel(), telpeort.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement() + ); + } + + 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, EntitySpawnReason.DIMENSION_TRAVEL); + 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(TeleportTransition teleportTarget, long teleportFlags, + java.util.function.Consumer teleportComplete) { + PositionMoveRotation move = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this), PositionMoveRotation.of(teleportTarget), teleportTarget.relatives()); + + return this.teleportAsync( + teleportTarget.newLevel(), move.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement(), + teleportTarget.cause(), teleportFlags, teleportComplete + ); + } + + 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) + ) + ) { + boolean hasPassengers = !this.passengers.isEmpty(); + EntityTreeNode passengerTree = this.detachPassengers(); + + if (hasPassengers) { + // 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); + } + + if (hasPassengers) { + 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() { + if (this instanceof Leashable leashable) { + leashable.dropLeash(); + } + } + + 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.CallbackCompletable 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.util.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.TeleportTransition( + destination, Vec3.atBottomCenterOf(targetPos.below()), Vec3.ZERO, Direction.WEST.toYRot(), 0.0f, + Relative.union(Relative.DELTA, Set.of(Relative.X_ROT)), + TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.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.util.Priority.HIGH, + (chunks) -> { + BlockPos adjustedSpawn = destination.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, spawnPos); + + // done + portalInfoCompletable.complete( + new net.minecraft.world.level.portal.TeleportTransition( + destination, Vec3.atBottomCenterOf(adjustedSpawn), Vec3.ZERO, 0.0f, 0.0f, + Relative.union(Relative.DELTA, Relative.ROTATION), + TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.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.CallbackCompletable portalFound + = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); + + // post portal find/create logic + portalFound.addWaiter( + (BlockUtil.FoundRectangle portal, Throwable thr) -> { + // no portal could be created + if (portal == null) { + portalInfoCompletable.complete( + new TeleportTransition(destination, Vec3.atCenterOf(targetPos), Vec3.ZERO, + 90.0f, 0.0f, + TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.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, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.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.util.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.util.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()) { + 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.CallbackCompletable portalInfoCompletable + = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); + + portalInfoCompletable.addWaiter((TeleportTransition 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/rot/speed + for (EntityTreeNode node : fullPassengerTree) { + node.root.transform(info); + } + + // place + passengerTree.root.placeInAsync( + originWorld, destination, Entity.TELEPORT_FLAG_LOAD_CHUNK | (takePassengers ? Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS : 0L), + passengerTree, + (Entity teleported) -> { + if (info.postTeleportTransition() != null) { + info.postTeleportTransition().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 teleport(TeleportTransition teleportTransition) { + // Folia start - region threading + if (true) { + throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); + } + // Folia end - region threading // Paper start - Fix item duplication and teleport issues if ((!this.isAlive() || !this.valid) && (teleportTransition.newLevel() != this.level)) { LOGGER.warn("Illegal Entity Teleport " + this + " to " + teleportTransition.newLevel() + ":" + teleportTransition.position(), new Throwable()); @@ -3908,6 +4717,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 && leashable.isLeashed()) { // Paper - only call if it is leashed @@ -4243,6 +5058,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } public void startSeenByPlayer(ServerPlayer serverPlayer) { + // Folia start - region threading + if (serverPlayer.getCamera() == this) { + // set camera again + serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCameraPacket(this)); + } + // Folia end - region threading } public void stopSeenByPlayer(ServerPlayer serverPlayer) { @@ -4252,6 +5073,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess new io.papermc.paper.event.player.PlayerUntrackEntityEvent(serverPlayer.getBukkitEntity(), this.getBukkitEntity()).callEvent(); } // Paper end - entity tracking events + // Folia start - region threading + if (serverPlayer.getCamera() == this) { + // unset camera, the player tick method should TP us close enough again to invoke startSeenByPlayer + serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCameraPacket(serverPlayer)); + } + // Folia end - region threading } public float rotate(Rotation transformRotation) { @@ -4787,7 +5614,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; + if (posChanged) { // Folia - region threading synchronized (this.posLock) { // Paper - detailed watchdog information this.position = new Vec3(x, y, z); } // Paper - detailed watchdog information @@ -4806,7 +5634,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 @@ -4890,6 +5718,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; @@ -4912,6 +5746,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit end final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers + // Folia start - region threading + this.preRemove(removalReason); + // Folia end - region threading if (this.removalReason == null) { this.removalReason = removalReason; } @@ -4935,6 +5772,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/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java index 4546aca8e2e144ec207653c713fc49f849908827..6258b008ad3e00d41e9f3014572d6f7a06b1847c 100644 --- a/net/minecraft/world/entity/LivingEntity.java +++ b/net/minecraft/world/entity/LivingEntity.java @@ -278,7 +278,7 @@ public abstract class LivingEntity extends Entity implements Attackable { private Optional lastClimbablePos = Optional.empty(); @Nullable private DamageSource lastDamageSource; - private long lastDamageStamp; + private long lastDamageStamp = Long.MIN_VALUE; // Folia - region threading protected int autoSpinAttackTicks; protected float autoSpinAttackDmg; @Nullable @@ -307,6 +307,26 @@ public abstract class LivingEntity extends Entity implements Attackable { return this.getYHeadRot(); } // CraftBukkit 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 + public boolean canBeSpectated() { + return super.canBeSpectated() && this.getHealth() > 0.0F; + } + + @Override + protected void resetStoredPositions() { + super.resetStoredPositions(); + this.lastClimbablePos = Optional.empty(); + } + // Folia end - region threading protected LivingEntity(EntityType entityType, Level level) { super(entityType, level); @@ -528,7 +548,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--; @@ -611,11 +631,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()) { 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 } } @@ -851,9 +874,9 @@ public abstract class LivingEntity extends Entity implements Attackable { } this.hurtTime = compound.getShort("HurtTime"); - this.deathTime = compound.getShort("DeathTime"); + this.deathTime = compound.getShort("DeathTime"); this.broadcastedDeath = false; // Folia - region threading this.lastHurtByMobTimestamp = compound.getInt("HurtByTimestamp"); - if (compound.contains("Team", 8)) { + if (false && compound.contains("Team", 8)) { // Folia start - region threading String string = compound.getString("Team"); Scoreboard scoreboard = this.level().getScoreboard(); PlayerTeam playerTeam = scoreboard.getPlayerTeam(string); @@ -1115,6 +1138,7 @@ public abstract class LivingEntity extends Entity implements Attackable { public boolean addEffect(MobEffectInstance effectInstance, @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(effectInstance, cause)); return true; @@ -1502,7 +1526,7 @@ public abstract class LivingEntity extends Entity implements Attackable { boolean flag2 = !flag; // CraftBukkit - Ensure to return false if damage is blocked if (flag2) { this.lastDamageSource = damageSource; - this.lastDamageStamp = this.level().getGameTime(); + this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading for (MobEffectInstance mobEffectInstance : this.getActiveEffects()) { mobEffectInstance.onMobHurt(level, this, damageSource, amount); @@ -1629,7 +1653,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; } @@ -2427,10 +2451,10 @@ public abstract class LivingEntity extends Entity implements Attackable { @Nullable public LivingEntity getKillCredit() { - if (this.lastHurtByPlayer != null) { + if (this.lastHurtByPlayer != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByPlayer)) { // Folia - region threading return this.lastHurtByPlayer; } else { - return this.lastHurtByMob != null ? this.lastHurtByMob : null; + return this.lastHurtByMob != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByMob) ? this.lastHurtByMob : null; // Folia - region threading } } @@ -2509,7 +2533,7 @@ public abstract class LivingEntity extends Entity implements Attackable { } this.lastDamageSource = damageSource; - this.lastDamageStamp = this.level().getGameTime(); + this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading } @Override @@ -3504,7 +3528,7 @@ public abstract class LivingEntity extends Entity implements Attackable { this.pushEntities(); profilerFiller.pop(); // Paper start - Add EntityMoveEvent - if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof Player)) { + if (((ServerLevel) this.level()).getCurrentWorldData().hasEntityMoveEvent && !(this instanceof 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()); @@ -4177,7 +4201,7 @@ public abstract class LivingEntity extends Entity implements Attackable { boolean flag = false; BlockPos blockPos = BlockPos.containing(x, y, z); Level level = this.level(); - if (level.hasChunkAt(blockPos)) { + if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)level, blockPos) && level.hasChunkAt(blockPos)) { // Folia - region threading boolean flag1 = false; while (!flag1 && blockPos.getY() > level.getMinY()) { @@ -4339,6 +4363,11 @@ public abstract class LivingEntity extends Entity implements Attackable { this.setXRot(0.0F); } }); + // Folia start - separate out + this.stopSleepingRaw(); + } + public void stopSleepingRaw() { + // Folia end - separate out Vec3 vec3 = this.position(); this.setPose(Pose.STANDING); this.setPos(vec3.x, vec3.y, vec3.z); diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java index e330bf990e4874baed1b21cd8c9b44d66ec5b823..da922d4c0ffa0f40d5e8dd69487bf30dbbbeed87 100644 --- a/net/minecraft/world/entity/Mob.java +++ b/net/minecraft/world/entity/Mob.java @@ -254,8 +254,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() { @@ -268,7 +280,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab } public boolean setTarget(LivingEntity target, EntityTargetEvent.TargetReason reason, boolean fireEvent) { - if (this.getTarget() == target) { + if (this.getTargetRaw() == target) { // Folia - region threading return false; } if (fireEvent) { @@ -1665,12 +1677,26 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab @Override protected void removeAfterChangingDimensions() { super.removeAfterChangingDimensions(); + // Folia start - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions +// this.getAllSlots().forEach(itemStack -> { +// if (!itemStack.isEmpty()) { +// itemStack.setCount(0); +// } +// }); + // Folia end - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions + } + + // Folia start - region threading + @Override + protected void postRemoveAfterChangingDimensions() { + super.postRemoveAfterChangingDimensions(); this.getAllSlots().forEach(itemStack -> { if (!itemStack.isEmpty()) { itemStack.setCount(0); } }); } + // Folia end - region threading @Nullable @Override diff --git a/net/minecraft/world/entity/PortalProcessor.java b/net/minecraft/world/entity/PortalProcessor.java index 88b07fbb96b20124777889830afa480673629d43..46d989aef0eceebd98bfd93999153319de77a8a0 100644 --- a/net/minecraft/world/entity/PortalProcessor.java +++ b/net/minecraft/world/entity/PortalProcessor.java @@ -33,6 +33,12 @@ public class PortalProcessor { return this.portal.getPortalDestination(level, 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/net/minecraft/world/entity/TamableAnimal.java b/net/minecraft/world/entity/TamableAnimal.java index fc3ba135ae502aaa5c3a9fa3297bf7b12c1ab063..b0b1e38f2b70ed548790fd0445db4541c34b0f34 100644 --- a/net/minecraft/world/entity/TamableAnimal.java +++ b/net/minecraft/world/entity/TamableAnimal.java @@ -263,6 +263,11 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { public void tryToTeleportToOwner() { LivingEntity owner = this.getOwner(); if (owner != null) { + // Folia start - region threading + if (owner.isRemoved() || owner.level() != this.level()) { + return; + } + // Folia end - region threading this.teleportToAroundBlockPos(owner.blockPosition()); } } @@ -295,7 +300,22 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { return false; } org.bukkit.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 + org.bukkit.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/net/minecraft/world/entity/ai/Brain.java b/net/minecraft/world/entity/ai/Brain.java index 450396468b23fd90cb8036dbbdd0927051f907af..65b2b3ece213d901cdd585093e2fafcd2ef4a7cd 100644 --- a/net/minecraft/world/entity/ai/Brain.java +++ b/net/minecraft/world/entity/ai/Brain.java @@ -425,9 +425,17 @@ public class Brain { } public void stopAll(ServerLevel level, E owner) { + // 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 gameTime = owner.level().getGameTime(); - for (BehaviorControl behaviorControl : this.getRunningBehaviors()) { + for (BehaviorControl behaviorControl : behaviors) { // Folia - region threading behaviorControl.doStop(level, owner, gameTime); } } diff --git a/net/minecraft/world/entity/ai/behavior/GoToPotentialJobSite.java b/net/minecraft/world/entity/ai/behavior/GoToPotentialJobSite.java index 3614551856c594f3c0cfee984fcf03fad672b007..f37aee679451dfcaf945aa7a3f668229bff03c3c 100644 --- a/net/minecraft/world/entity/ai/behavior/GoToPotentialJobSite.java +++ b/net/minecraft/world/entity/ai/behavior/GoToPotentialJobSite.java @@ -46,12 +46,14 @@ public class GoToPotentialJobSite extends Behavior { BlockPos blockPos = globalPos.pos(); ServerLevel level1 = level.getServer().getLevel(globalPos.dimension()); if (level1 != null) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(level1, blockPos.getX() >> 4, blockPos.getZ() >> 4, () -> { // Folia - region threading PoiManager poiManager = level1.getPoiManager(); if (poiManager.exists(blockPos, holder -> true)) { poiManager.release(blockPos); } DebugPackets.sendPoiTicketCountPacket(level, blockPos); + }); // Folia - region threading } }); entity.getBrain().eraseMemory(MemoryModuleType.POTENTIAL_JOB_SITE); diff --git a/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java b/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java index b11a16db0ea22ebd68db9c96e0ba0939b6596caf..9f5b5ad2fe08f25f4c922ae641d1a2e8bce18ccb 100644 --- a/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java +++ b/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java @@ -19,6 +19,11 @@ public class PoiCompetitorScan { instance, (jobSite, nearestLivingEntities) -> (level, villager, gameTime) -> { GlobalPos globalPos = instance.get(jobSite); + // Folia start - region threading + if (globalPos.dimension() != level.dimension() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, globalPos.pos())) { + return true; + } + // Folia end - region threading level.getPoiManager() .getType(globalPos.pos()) .ifPresent( diff --git a/net/minecraft/world/entity/ai/behavior/YieldJobSite.java b/net/minecraft/world/entity/ai/behavior/YieldJobSite.java index 37ad79e201e36a1a9520219e3faa4dcffa7b4dfd..4cf9bb7ef73fbeb400991c3d8ff59141a0ea2575 100644 --- a/net/minecraft/world/entity/ai/behavior/YieldJobSite.java +++ b/net/minecraft/world/entity/ai/behavior/YieldJobSite.java @@ -33,7 +33,13 @@ public class YieldJobSite { } else if (villager.getVillagerData().getProfession() != VillagerProfession.NONE) { return false; } else { - BlockPos blockPos = instance.get(potentialJobSite).pos(); + // Folia start - region threading + GlobalPos globalPos = instance.get(potentialJobSite); + BlockPos blockPos = globalPos.pos(); + if (globalPos.dimension() != level.dimension() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, blockPos)) { + return true; + } + // Folia end - region threading Optional> type = level.getPoiManager().getType(blockPos); if (type.isEmpty()) { return true; diff --git a/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java index dde287e823f906681e3addf03fa821c8786c9900..e5e3ce6eeb01ac4387eaee20d09ef469d8b3bc5e 100644 --- a/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +++ b/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) <= this.stopDistance * this.stopDistance); + && !(this.owner.level() == this.tamable.level() && this.tamable.distanceToSqr(this.owner) <= this.stopDistance * this.stopDistance); // Folia - region threading - check level } @Override diff --git a/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java b/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java index 045cfafb3afe8271d60852ae3c7cdcb039b44d4f..a24e964aff5623e3d7f2b79c87b6067f565458c2 100644 --- a/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java +++ b/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java @@ -42,6 +42,11 @@ public class GroundPathNavigation extends PathNavigation { @Override public Path createPath(BlockPos pos, @javax.annotation.Nullable Entity entity, int accuracy) { // Paper - EntityPathfindEvent + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level, pos)) { + return null; + } + // Folia end - region threading LevelChunk chunkNow = this.level.getChunkSource().getChunkNow(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())); if (chunkNow == null) { return null; diff --git a/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/net/minecraft/world/entity/ai/navigation/PathNavigation.java index b44f2c49509d847817a78e9c4fb1499fb378054b..386580035e6789d6e668b924513ddfc81947a9b3 100644 --- a/net/minecraft/world/entity/ai/navigation/PathNavigation.java +++ b/net/minecraft/world/entity/ai/navigation/PathNavigation.java @@ -96,11 +96,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 { @@ -221,7 +221,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 @@ -233,7 +233,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/net/minecraft/world/entity/ai/sensing/PlayerSensor.java b/net/minecraft/world/entity/ai/sensing/PlayerSensor.java index 6233e6b48aaa69ba9f577d0b480b1cdf2f55d16e..a4810c8a3b18082543d06787722d4ed5821a1943 100644 --- a/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +++ b/net/minecraft/world/entity/ai/sensing/PlayerSensor.java @@ -22,7 +22,7 @@ public class PlayerSensor extends Sensor { @Override protected void doTick(ServerLevel level, LivingEntity entity) { - List list = level.players() + List list = level.getLocalPlayers() // Folia - region threading .stream() .filter(EntitySelector.NO_SPECTATORS) .filter(serverPlayer -> entity.closerThan(serverPlayer, this.getFollowDistance(entity))) diff --git a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java index 4b3ba795bc18417f983600f1edbc1895ccb7deab..d06c2c72e6166bc8b7822966092b17440125b814 100644 --- a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java +++ b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java @@ -36,7 +36,7 @@ public class TemptingSensor extends Sensor { protected void doTick(ServerLevel level, PathfinderMob entity) { Brain brain = entity.getBrain(); TargetingConditions targetingConditions = TEMPT_TARGETING.copy().range((float)entity.getAttributeValue(Attributes.TEMPT_RANGE)); - List list = level.players() + List list = level.getLocalPlayers() // Folia - region threading .stream() .filter(EntitySelector.NO_SPECTATORS) .filter(serverPlayer -> targetingConditions.test(level, entity, serverPlayer)) diff --git a/net/minecraft/world/entity/ai/village/VillageSiege.java b/net/minecraft/world/entity/ai/village/VillageSiege.java index a1cea4a4f76a7bb771b8ab643bd9d473e16418bf..fa012c5b23f6fdd714d15282cc485492ae18672a 100644 --- a/net/minecraft/world/entity/ai/village/VillageSiege.java +++ b/net/minecraft/world/entity/ai/village/VillageSiege.java @@ -18,68 +18,72 @@ import org.slf4j.Logger; public class VillageSiege implements CustomSpawner { private static final Logger LOGGER = LogUtils.getLogger(); - private boolean hasSetupSiege; - private VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE; - private int zombiesToSpawn; - private int nextSpawnTime; - private int spawnX; - private int spawnY; - private int spawnZ; + // Folia - region threading @Override public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.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(level, 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 (!level.isDay() && spawnHostiles) { float timeOfDay = level.getTimeOfDay(0.0F); if (timeOfDay == 0.5) { - this.siegeState = level.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; + worldData.villageSiegeState.siegeState = level.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(level)) { 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(level); - 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 level) { - for (Player player : level.players()) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + for (Player player : level.getLocalPlayers()) { // Folia - region threading if (!player.isSpectator()) { BlockPos blockPos = player.blockPosition(); if (level.isVillage(blockPos) && !level.getBiome(blockPos).is(BiomeTags.WITHOUT_ZOMBIE_SIEGES)) { for (int i = 0; i < 10; i++) { float f = level.random.nextFloat() * (float) (Math.PI * 2); - this.spawnX = blockPos.getX() + Mth.floor(Mth.cos(f) * 32.0F); - this.spawnY = blockPos.getY(); - this.spawnZ = blockPos.getZ() + Mth.floor(Mth.sin(f) * 32.0F); - if (this.findRandomSpawnPos(level, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)) != null) { - this.nextSpawnTime = 0; - this.zombiesToSpawn = 20; + worldData.villageSiegeState.spawnX = blockPos.getX() + Mth.floor(Mth.cos(f) * 32.0F); // Folia - region threading + worldData.villageSiegeState.spawnY = blockPos.getY(); // Folia - region threading + worldData.villageSiegeState.spawnZ = blockPos.getZ() + Mth.floor(Mth.sin(f) * 32.0F); // Folia - region threading + if (this.findRandomSpawnPos(level, 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; } } @@ -93,11 +97,13 @@ public class VillageSiege implements CustomSpawner { } private void trySpawn(ServerLevel level) { - Vec3 vec3 = this.findRandomSpawnPos(level, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + Vec3 vec3 = this.findRandomSpawnPos(level, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)); // Folia - region threading if (vec3 != null) { Zombie zombie; try { zombie = new Zombie(level); + zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up zombie.finalizeSpawn(level, level.getCurrentDifficultyAt(zombie.blockPosition()), EntitySpawnReason.EVENT, null); } catch (Exception var5) { LOGGER.warn("Failed to create zombie for village siege at {}", vec3, var5); @@ -105,7 +111,7 @@ public class VillageSiege implements CustomSpawner { return; } - zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); + //zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up level.addFreshEntityWithPassengers(zombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_INVASION); // CraftBukkit } } @@ -125,7 +131,7 @@ public class VillageSiege implements CustomSpawner { return null; } - static enum State { + public static enum State { // Folia - region threading SIEGE_CAN_ACTIVATE, SIEGE_TONIGHT, SIEGE_DONE; diff --git a/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/net/minecraft/world/entity/ai/village/poi/PoiManager.java index 618fc0eb4fe70e46e55f3aa28e8eac1d2d01b6d9..c10810bf00d75f459c3c6a9415c1e09f0519d50e 100644 --- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java +++ b/net/minecraft/world/entity/ai/village/poi/PoiManager.java @@ -58,11 +58,13 @@ public class PoiManager extends SectionStorage im } 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 @@ -347,10 +349,12 @@ public class PoiManager extends SectionStorage im } public int sectionsToVillage(SectionPos sectionPos) { + synchronized (this.villageDistanceTracker) { // Folia - region threading // Paper start - rewrite chunk system this.villageDistanceTracker.propagateUpdates(); return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(sectionPos))); // Paper end - rewrite chunk system + } // Folia - region threading } boolean isVillageCenter(long chunkPos) { @@ -364,7 +368,9 @@ public class PoiManager extends SectionStorage im @Override public void tick(BooleanSupplier aheadOfTime) { + synchronized (this.villageDistanceTracker) { // Folia - region threading this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system + } // Folia - region threading } @Override diff --git a/net/minecraft/world/entity/animal/Bee.java b/net/minecraft/world/entity/animal/Bee.java index 94244b148533ef026bf5c56abbc2bb5cfa83c938..15360f560d9b6a762ebd4284b7d0ca0a3e13794e 100644 --- a/net/minecraft/world/entity/animal/Bee.java +++ b/net/minecraft/world/entity/animal/Bee.java @@ -815,6 +815,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.isTooFarAway(Bee.this.hivePos) && !Bee.this.hasRestriction() @@ -925,6 +930,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() diff --git a/net/minecraft/world/entity/animal/Cat.java b/net/minecraft/world/entity/animal/Cat.java index 1a7a5c81a260cc740994d1a63c4775c41c238dea..740ab58c733d9e3f05157fef6e6725fd72f90653 100644 --- a/net/minecraft/world/entity/animal/Cat.java +++ b/net/minecraft/world/entity/animal/Cat.java @@ -342,7 +342,7 @@ public class Cat extends TamableAnimal implements VariantHolder tagKey = flag ? CatVariantTags.FULL_MOON_SPAWNS : CatVariantTags.DEFAULT_SPAWNS; BuiltInRegistries.CAT_VARIANT.getRandomElementOf(tagKey, level.getRandom()).ifPresent(this::setVariant); ServerLevel level1 = level.getLevel(); - if (level1.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK, level).isValid()) { // Paper - Fix swamp hut cat generation deadlock + if (level.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK).isValid()) { // Paper - Fix swamp hut cat generation deadlock // Folia - region threading - properly fix this this.setVariant(BuiltInRegistries.CAT_VARIANT.getOrThrow(CatVariant.ALL_BLACK)); this.setPersistenceRequired(); } diff --git a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java index ff1c84d37db48e1bd0283a900e199647c0e8eba1..fc64c36a01eb8efdcfa487059078787900e34d86 100644 --- a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java +++ b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java @@ -53,7 +53,7 @@ public class EndCrystal extends Entity { public void tick() { this.time++; this.applyEffectsFromBlocks(); - this.handlePortal(); + //this.handlePortal(); // Folia - region threading if (this.level() instanceof ServerLevel) { BlockPos blockPos = this.blockPosition(); if (((ServerLevel)this.level()).getDragonFight() != null && this.level().getBlockState(blockPos).isAir()) { diff --git a/net/minecraft/world/entity/decoration/ItemFrame.java b/net/minecraft/world/entity/decoration/ItemFrame.java index 65e1d7c5ac94b1cfb921fa009be59d3e5872f0b5..5aefec7ecc2085659bebca25992dd3a76fff2b5e 100644 --- a/net/minecraft/world/entity/decoration/ItemFrame.java +++ b/net/minecraft/world/entity/decoration/ItemFrame.java @@ -242,7 +242,9 @@ public class ItemFrame extends HangingEntity { if (framedMapId != null) { MapItemSavedData savedData = MapItem.getSavedData(framedMapId, this.level()); if (savedData != null) { + synchronized (savedData) { // Folia - make map data thread-safe savedData.removedFromFrame(this.pos, this.getId()); + } // Folia - make map data thread-safe } } diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java index 5746587666c7cb788764aab2f6ccf0f3ac5c282f..1fa5e6a12b943e889bde566038a632a6adcf319e 100644 --- a/net/minecraft/world/entity/item/FallingBlockEntity.java +++ b/net/minecraft/world/entity/item/FallingBlockEntity.java @@ -162,7 +162,7 @@ public class FallingBlockEntity extends Entity { return; } // Paper end - Configurable falling blocks height nerf - this.handlePortal(); + //this.handlePortal(); // Folia - region threading if (this.level() instanceof ServerLevel serverLevel && (this.isAlive() || this.forceTickAfterTeleportToDuplicate)) { BlockPos blockPos = this.blockPosition(); boolean flag = this.blockState.getBlock() instanceof ConcretePowderBlock; diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java index 52a7ed0d991758bad0dcedcb7f97fb15ac6c6d04..7587130e021d494ae5013f7992b8f3c96590cbd7 100644 --- a/net/minecraft/world/entity/item/ItemEntity.java +++ b/net/minecraft/world/entity/item/ItemEntity.java @@ -521,13 +521,21 @@ 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 teleport(TeleportTransition teleportTransition) { Entity entity = super.teleport(teleportTransition); - if (!this.level().isClientSide && entity instanceof ItemEntity itemEntity) { - itemEntity.mergeWithNeighbours(); - } + if (entity != null) entity.postChangeDimension(); // Folia - region threading - move to post change return entity; } diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java index 40da052e7fea1306a007b3cb5c9daa33e0ef523e..88570bb4aa02896545805d7721c45cf9599befea 100644 --- a/net/minecraft/world/entity/item/PrimedTnt.java +++ b/net/minecraft/world/entity/item/PrimedTnt.java @@ -98,8 +98,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()); this.applyEffectsFromBlocks(); @@ -137,7 +137,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 = net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket.teleport(this.getId(), net.minecraft.world.entity.PositionMoveRotation.of(this), java.util.Set.of(), this.onGround); diff --git a/net/minecraft/world/entity/monster/Vex.java b/net/minecraft/world/entity/monster/Vex.java index af3fef70998cff4e4832adfa2071832324ebd91c..8751f80d48d11c33ddb6c553894c31e8b7630623 100644 --- a/net/minecraft/world/entity/monster/Vex.java +++ b/net/minecraft/world/entity/monster/Vex.java @@ -349,7 +349,7 @@ public class Vex extends Monster implements TraceableEntity { @Override public void tick() { BlockPos boundOrigin = Vex.this.getBoundOrigin(); - if (boundOrigin == null) { + if (boundOrigin == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)Vex.this.level(), boundOrigin)) { // Folia - region threading boundOrigin = Vex.this.blockPosition(); } diff --git a/net/minecraft/world/entity/monster/ZombieVillager.java b/net/minecraft/world/entity/monster/ZombieVillager.java index 8403257d81367c7371fa94d458a59a4589dc0bd7..d0e7eb3394b4e5b244cbd28424ff47ddf1b8f2bb 100644 --- a/net/minecraft/world/entity/monster/ZombieVillager.java +++ b/net/minecraft/world/entity/monster/ZombieVillager.java @@ -69,7 +69,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { @Nullable private MerchantOffers tradeOffers; private int villagerXp; - private int lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - add field + //private int lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers public ZombieVillager(EntityType entityType, Level level) { super(entityType, level); @@ -149,7 +149,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { } super.tick(); - this.lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit + //this.lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit // Folia - region threading - restore original timers } @Override diff --git a/net/minecraft/world/entity/npc/AbstractVillager.java b/net/minecraft/world/entity/npc/AbstractVillager.java index a71d16d968bb90fd7aca6f01a3dd56df4f9a7ce6..27ce04ecee778b73711ee55c7c75c541e1f86c38 100644 --- a/net/minecraft/world/entity/npc/AbstractVillager.java +++ b/net/minecraft/world/entity/npc/AbstractVillager.java @@ -218,10 +218,18 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa this.readInventoryFromTag(compound, this.registryAccess()); } + // Folia start - region threading + @Override + public void preChangeDimension() { + super.preChangeDimension(); + this.stopTrading(); + } + // Folia end - region threading + @Nullable @Override public Entity teleport(TeleportTransition teleportTransition) { - this.stopTrading(); + this.preChangeDimension(); // Folia - region threading - move into preChangeDimension return super.teleport(teleportTransition); } diff --git a/net/minecraft/world/entity/npc/CatSpawner.java b/net/minecraft/world/entity/npc/CatSpawner.java index e6d368bc601357cfca694ce328c8e6e47491f3b5..010bee26dfdf5cad186fa57c030540693ff71f23 100644 --- a/net/minecraft/world/entity/npc/CatSpawner.java +++ b/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 level, boolean spawnHostiles, boolean spawnPassives) { if (spawnPassives && level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { - this.nextTick--; - if (this.nextTick > 0) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + worldData.catSpawnerNextTick--; // Folia - region threading + if (worldData.catSpawnerNextTick > 0) { // Folia - region threading return 0; } else { - this.nextTick = 1200; - Player randomPlayer = level.getRandomPlayer(); + worldData.catSpawnerNextTick = 1200; // Folia - region threading + Player randomPlayer = level.getRandomLocalPlayer(); // Folia - region threading if (randomPlayer == null) { return 0; } else { diff --git a/net/minecraft/world/entity/npc/Villager.java b/net/minecraft/world/entity/npc/Villager.java index 2b83262e4a13eae86df82913ce4f3121e3631a43..7ea74aeb905b95e5919d74df5fbc5e8f7a9985e3 100644 --- a/net/minecraft/world/entity/npc/Villager.java +++ b/net/minecraft/world/entity/npc/Villager.java @@ -246,7 +246,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler villagerBrain.setCoreActivities(ImmutableSet.of(Activity.CORE)); villagerBrain.setDefaultActivity(Activity.IDLE); villagerBrain.setActiveActivityIfPossible(Activity.IDLE); - villagerBrain.updateActivityFromSchedule(this.level().getDayTime(), this.level().getGameTime()); + villagerBrain.updateActivityFromSchedule(this.level().getLevelData().getDayTime(), this.level().getLevelData().getGameTime()); // Folia - region threading - not in the world yet } @Override @@ -693,6 +693,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler this.brain.getMemory(moduleType).ifPresent(globalPos -> { ServerLevel level = server.getLevel(globalPos.dimension()); if (level != null) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( // Folia - region threading + level, globalPos.pos().getX() >> 4, globalPos.pos().getZ() >> 4, () -> { // Folia - region threading PoiManager poiManager = level.getPoiManager(); Optional> type = poiManager.getType(globalPos.pos()); BiPredicate> biPredicate = POI_MEMORIES.get(moduleType); @@ -700,6 +702,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler poiManager.release(globalPos.pos()); DebugPackets.sendPoiTicketCountPacket(level, globalPos.pos()); } + }); // Folia - region threading } }); } diff --git a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java index ef2afb17a22a703470e13d12c989a685e72f0ab8..984ac8efa2ed45be614e04eab8247481e3a08525 100644 --- a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +++ b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java @@ -30,16 +30,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 = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading private final ServerLevelData serverLevelData; - private int tickDelay; - private int spawnDelay; - private int spawnChance; + // Folia - region threading public WanderingTraderSpawner(ServerLevelData serverLevelData) { this.serverLevelData = serverLevelData; // 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 = serverLevelData.getWanderingTraderSpawnDelay(); // this.spawnChance = serverLevelData.getWanderingTraderSpawnChance(); // if (this.spawnDelay == 0 && this.spawnChance == 0) { @@ -53,35 +51,36 @@ public class WanderingTraderSpawner implements CustomSpawner { @Override public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading // Paper start - Add Wandering Trader spawn rate config options - if (this.tickDelay == Integer.MIN_VALUE) { - this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; - this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; - this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; + if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Folia - region threading + worldData.wanderingTraderTickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading + worldData.wanderingTraderSpawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading + worldData.wanderingTraderSpawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading } if (!level.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 = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; - this.spawnDelay = this.spawnDelay - level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; + worldData.wanderingTraderTickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading + worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - level.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 = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; + worldData.wanderingTraderSpawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading if (!level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { return 0; } else { - int i = this.spawnChance; - this.spawnChance = Mth.clamp(this.spawnChance + level.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); + int i = worldData.wanderingTraderSpawnChance; // Folia - region threading + worldData.wanderingTraderSpawnChance = Mth.clamp(worldData.wanderingTraderSpawnChance + level.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); // 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 if (this.random.nextInt(100) > i) { return 0; } else if (this.spawn(level)) { - this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; + worldData.wanderingTraderSpawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading // Paper end - Add Wandering Trader spawn rate config options return 1; } else { @@ -93,7 +92,7 @@ public class WanderingTraderSpawner implements CustomSpawner { } private boolean spawn(ServerLevel serverLevel) { - Player randomPlayer = serverLevel.getRandomPlayer(); + Player randomPlayer = serverLevel.getRandomLocalPlayer(); // Folia - region threading if (randomPlayer == null) { return true; } else if (this.random.nextInt(10) != 0) { @@ -116,7 +115,7 @@ public class WanderingTraderSpawner implements CustomSpawner { this.tryToSpawnLlamaFor(serverLevel, wanderingTrader, 4); } - this.serverLevelData.setWanderingTraderId(wanderingTrader.getUUID()); + //this.serverLevelData.setWanderingTraderId(wanderingTrader.getUUID()); // Folia - region threading - doesn't appear to be used anywhere, so avoid the race condition here... // wanderingTrader.setDespawnDelay(48000); // Paper - moved above, modifiable by plugins on CreatureSpawnEvent wanderingTrader.setWanderTarget(blockPos1); wanderingTrader.restrictTo(blockPos1, 16); diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java index a0813aa9ebf5b32375b1bc9f294d8fc34cc867fe..e70919757dee4b02384ded3551c8f580d289584a 100644 --- a/net/minecraft/world/entity/player/Player.java +++ b/net/minecraft/world/entity/player/Player.java @@ -1506,6 +1506,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/net/minecraft/world/entity/projectile/AbstractArrow.java b/net/minecraft/world/entity/projectile/AbstractArrow.java index 23a795eb4c7ad968448dd1405272056bac29c8f8..1a771e8510b2511945d253a1a5ad23054c464b0c 100644 --- a/net/minecraft/world/entity/projectile/AbstractArrow.java +++ b/net/minecraft/world/entity/projectile/AbstractArrow.java @@ -176,6 +176,11 @@ public abstract class AbstractArrow extends Projectile { @Override public void 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 deltaMovement = this.getDeltaMovement(); BlockPos blockPos = this.blockPosition(); diff --git a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java index 9a99b813de8b606fab26c87086a21372e5172ba3..4eeb1017576d23d206a7a47b9e9e74b19465b2ae 100644 --- a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java +++ b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java @@ -80,6 +80,11 @@ public abstract class AbstractHurtingProjectile extends Projectile { this.setPos(location); this.applyEffectsFromBlocks(); 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/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java index 774ca9e0b56fd175ae246051de762d0c4256ca58..0cfd2c937f93f1acb4afc01251f882710baf2591 100644 --- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java @@ -130,6 +130,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) { Vec3 handHoldingItemAngle; diff --git a/net/minecraft/world/entity/projectile/FishingHook.java b/net/minecraft/world/entity/projectile/FishingHook.java index 1e012c7ef699a64ff3f1b00f897bb893ab25ecbd..f9d7514764850fd02ed5853ba2fdf8ada40ce756 100644 --- a/net/minecraft/world/entity/projectile/FishingHook.java +++ b/net/minecraft/world/entity/projectile/FishingHook.java @@ -94,7 +94,7 @@ public class FishingHook extends Projectile { public FishingHook(Player player, Level level, int luck, int lureSpeed) { this(EntityType.FISHING_BOBBER, level, luck, lureSpeed); - this.setOwner(player); + //this.setOwner(player); // Folia - region threading - move this down after position so that thread-checks do not fail float xRot = player.getXRot(); float yRot = player.getYRot(); float cos = Mth.cos(-yRot * (float) (Math.PI / 180.0) - (float) Math.PI); @@ -105,6 +105,7 @@ public class FishingHook extends Projectile { double eyeY = player.getEyeY(); double d1 = player.getZ() - cos * 0.3; this.moveTo(d, eyeY, d1, yRot, xRot); + this.setOwner(player); // Folia - region threading - move this down after position so that thread-checks do not fail Vec3 vec3 = new Vec3(-sin, Mth.clamp(-(sin1 / f), -5.0F, 5.0F), -cos); double len = vec3.length(); vec3 = vec3.multiply( @@ -260,6 +261,11 @@ public class FishingHook extends Projectile { } private boolean shouldStopFishing(Player player) { + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { + return true; + } + // Folia end - region threading ItemStack mainHandItem = player.getMainHandItem(); ItemStack offhandItem = player.getOffhandItem(); boolean isFishingRod = mainHandItem.is(Items.FISHING_ROD); @@ -623,10 +629,18 @@ public class FishingHook extends Projectile { @Override public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) { // CraftBukkit end - this.updateOwnerInfo(null); + //this.updateOwnerInfo(null); // Folia - region threading - move into preRemove super.remove(reason, cause); // CraftBukkit - add Bukkit remove cause } + // Folia start - region threading + @Override + protected void preRemove(RemovalReason reason) { + super.preRemove(reason); + this.updateOwnerInfo(null); + } + // Folia end - region threading + @Override public void onClientRemoval() { this.updateOwnerInfo(null); diff --git a/net/minecraft/world/entity/projectile/LlamaSpit.java b/net/minecraft/world/entity/projectile/LlamaSpit.java index 4880db97135d54fa72f64c108b2bd4ded096438b..dc6ec52a513e2754a81733de5f389d6ada5215cc 100644 --- a/net/minecraft/world/entity/projectile/LlamaSpit.java +++ b/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 deltaMovement = this.getDeltaMovement(); HitResult hitResultOnMoveVector = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity); this.preHitTargetOrDeflectSelf(hitResultOnMoveVector); // CraftBukkit - projectile hit event diff --git a/net/minecraft/world/entity/projectile/Projectile.java b/net/minecraft/world/entity/projectile/Projectile.java index ad0bb896d6ea669ce88bfe6490319e8ba7a29001..abfe6765faec49d4b8897608582d738c7b09522d 100644 --- a/net/minecraft/world/entity/projectile/Projectile.java +++ b/net/minecraft/world/entity/projectile/Projectile.java @@ -38,7 +38,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { @Nullable public UUID ownerUUID; @Nullable - public Entity cachedOwner; + public org.bukkit.craftbukkit.entity.CraftEntity cachedOwner; // Folia - region threading - replace with CraftEntity public boolean leftOwner; public boolean hasBeenShot; @Nullable @@ -52,7 +52,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { public void setOwner(@Nullable Entity owner) { if (owner != null) { this.ownerUUID = owner.getUUID(); - this.cachedOwner = owner; + this.cachedOwner = owner.getBukkitEntity(); // Folia - region threading } // Paper start - Refresh ProjectileSource for projectiles else { @@ -69,22 +69,38 @@ public abstract class Projectile extends Entity implements TraceableEntity { if (fillCache) { this.getOwner(); } - if (this.cachedOwner != null && !this.cachedOwner.isRemoved() && this.projectileSource == null && this.cachedOwner.getBukkitEntity() instanceof org.bukkit.projectiles.ProjectileSource projSource) { + if (this.cachedOwner != null && !this.cachedOwner.getHandleRaw().isRemoved() && this.projectileSource == null && this.cachedOwner instanceof org.bukkit.projectiles.ProjectileSource projSource) { // Folia - region threading this.projectileSource = projSource; } } // 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() { - if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) { + Entity ret = this.getOwnerRaw(); + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(ret) && (ret == null || !ret.isRemoved()) ? 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.isPurged()) { // Folia - region threading this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles - return this.cachedOwner; + return this.cachedOwner.getHandleRaw(); // Folia - region threading } else if (this.ownerUUID != null) { - this.cachedOwner = this.findOwner(this.ownerUUID); + // Folia start - region threading + Entity ret = this.findOwner(this.ownerUUID); + if (ret != null) { + this.cachedOwner = ret.getBukkitEntity(); + } + // Folia end - region threading this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles - return this.cachedOwner; + return ret; // Folia - region threading } else { return null; } @@ -130,7 +146,12 @@ public abstract class Projectile extends Entity implements TraceableEntity { protected void setOwnerThroughUUID(UUID uuid) { if (this.ownerUUID != uuid) { this.ownerUUID = uuid; - this.cachedOwner = this.findOwner(uuid); + // Folia start - region threading + Entity cachedOwner = this.findOwner(this.ownerUUID); + if (cachedOwner != null) { + this.cachedOwner = cachedOwner.getBukkitEntity(); + } + // Folia end - region threading } } @@ -454,7 +475,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { @Override public boolean mayInteract(ServerLevel level, BlockPos pos) { Entity owner = this.getOwner(); - return owner instanceof Player ? owner.mayInteract(level, pos) : owner == null || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); + return owner instanceof Player && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(owner) ? owner.mayInteract(level, pos) : owner == null || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Folia - region threading } public boolean mayBreak(ServerLevel level) { diff --git a/net/minecraft/world/entity/projectile/SmallFireball.java b/net/minecraft/world/entity/projectile/SmallFireball.java index 8c84cea43fc0e42a576004663670977eac99f1a6..ba70ce3df630532b646eab0a5fabca15d67c379b 100644 --- a/net/minecraft/world/entity/projectile/SmallFireball.java +++ b/net/minecraft/world/entity/projectile/SmallFireball.java @@ -24,7 +24,7 @@ public class SmallFireball extends Fireball { public SmallFireball(Level level, LivingEntity owner, Vec3 movement) { super(EntityType.SMALL_FIREBALL, owner, movement, level); // CraftBukkit start - if (this.getOwner() != null && this.getOwner() instanceof Mob) { + if (owner != null && this.getOwner() != null && this.getOwner() instanceof Mob) { // Folia - region threading this.isIncendiary = (level instanceof ServerLevel serverLevel) && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); } // CraftBukkit end diff --git a/net/minecraft/world/entity/projectile/ThrowableProjectile.java b/net/minecraft/world/entity/projectile/ThrowableProjectile.java index f9fa2866cb28622785b4fcd54c0e2989569a401a..74590ac276965543c2d78fe85090097c8d3a7aed 100644 --- a/net/minecraft/world/entity/projectile/ThrowableProjectile.java +++ b/net/minecraft/world/entity/projectile/ThrowableProjectile.java @@ -43,6 +43,11 @@ public abstract class ThrowableProjectile extends Projectile { @Override public void 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 this.handleFirstTickBubbleColumn(); this.applyGravity(); this.applyInertia(); diff --git a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java index 1345097a2a417f95c44143fd7e0d4cec38990121..0a2b4d6da836d7907759b6cdc94afd031450018d 100644 --- a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +++ b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java @@ -58,15 +58,11 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { } private void deregisterFromCurrentOwner() { - if (this.getOwner() instanceof ServerPlayer serverPlayer) { - serverPlayer.deregisterEnderPearl(this); - } + // Folia - region threading - we remove the registration logic, we do not need to fetch the owner } private void registerToCurrentOwner() { - if (this.getOwner() instanceof ServerPlayer serverPlayer) { - serverPlayer.registerEnderPearl(this); - } + // Folia - region threading - we remove the registration logic, we do not need to fetch the owner } @Nullable @@ -99,6 +95,81 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { result.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) { + final boolean onPortalCooldown = source.isOnPortalCooldown(); + // 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.unRide(); + } + + if (onPortalCooldown) { + entity.setPortalCooldown(); + } + + entity.teleportAsync( + checkWorld, to, null, null, null, + org.bukkit.event.player.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, EntitySpawnReason.TRIGGERED); + + 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, org.bukkit.event.entity.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.hurtServer(player.serverLevel(), player.damageSources().enderPearl().eventEntityDamager(player), 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 result) { super.onHit(result); @@ -117,6 +188,20 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { } if (this.level() instanceof ServerLevel serverLevel && !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(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); + return; + } + // Folia end - region threading Entity owner = this.getOwner(); if (owner != null && isAllowedToTeleportOwner(owner, serverLevel)) { if (owner.isPassenger()) { @@ -212,7 +297,15 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { } } - private void playSound(Level level, 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 level, Vec3 pos) { // Folia - region threading - static level.playSound(null, pos.x, pos.y, pos.z, SoundEvents.PLAYER_TELEPORT, SoundSource.PLAYERS); } diff --git a/net/minecraft/world/entity/raid/Raid.java b/net/minecraft/world/entity/raid/Raid.java index 41b0db439b425b052bd1469daa6620a435ca852b..2f45befbb50645f1bfb5961ad725f3670ff0d592 100644 --- a/net/minecraft/world/entity/raid/Raid.java +++ b/net/minecraft/world/entity/raid/Raid.java @@ -110,6 +110,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 level, BlockPos center) { this.id = id; this.level = level; @@ -207,7 +214,7 @@ public class Raid { private Predicate validPlayer() { return player -> { BlockPos blockPos = player.blockPosition(); - return player.isAlive() && this.level.getRaidAt(blockPos) == this; + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) && player.isAlive() && this.level.getRaidAt(blockPos) == this; // Folia - make raids thread-safe }; } @@ -384,14 +391,21 @@ public class Raid { if (entity instanceof LivingEntity) { LivingEntity livingEntity = (LivingEntity)entity; if (!entity.isSpectator()) { - livingEntity.addEffect( - new MobEffectInstance(MobEffects.HERO_OF_THE_VILLAGE, 48000, this.raidOmenLevel - 1, false, false, true) - ); + //livingEntity.addEffect(new MobEffectInstance(MobEffects.HERO_OF_THE_VILLAGE, 48000, this.raidOmenLevel - 1, false, false, true)); // Folia start - Fix off region raid heroes - moved down if (livingEntity instanceof ServerPlayer serverPlayer) { - serverPlayer.awardStat(Stats.RAID_WIN); - CriteriaTriggers.RAID_WIN.trigger(serverPlayer); + //serverPlayer.awardStat(Stats.RAID_WIN); // Folia start - Fix off region raid heroes - moved down + //CriteriaTriggers.RAID_WIN.trigger(serverPlayer); // Folia start - Fix off region raid heroes - moved down winners.add(serverPlayer.getBukkitEntity()); // CraftBukkit } + // Folia start - Fix off region raid heroes + livingEntity.getBukkitEntity().taskScheduler.schedule((LivingEntity lv) -> { + lv.addEffect(new MobEffectInstance(MobEffects.HERO_OF_THE_VILLAGE, 48000, this.raidOmenLevel - 1, false, false, true)); + if (lv instanceof ServerPlayer serverPlayer) { + serverPlayer.awardStat(Stats.RAID_WIN); + CriteriaTriggers.RAID_WIN.trigger(serverPlayer); + } + }, null, 1L); + // Folia end - Fix off region raid heroes } } } @@ -496,7 +510,7 @@ public class Raid { Collection players = this.raidEvent.getPlayers(); long randomLong = this.random.nextLong(); - for (ServerPlayer serverPlayer : this.level.players()) { + for (ServerPlayer serverPlayer : this.level.getLocalPlayers()) { // Folia - region threading Vec3 vec3 = serverPlayer.position(); Vec3 vec31 = Vec3.atCenterOf(pos); double squareRoot = Math.sqrt((vec31.x - vec3.x) * (vec31.x - vec3.x) + (vec31.z - vec3.z) * (vec31.z - vec3.z)); diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java index 8270d76a753bfd26a4c8ef6610bee5c24ee59cfe..7c385baae81b9a987c0e1e4deb017884600331bc 100644 --- a/net/minecraft/world/entity/raid/Raider.java +++ b/net/minecraft/world/entity/raid/Raider.java @@ -86,7 +86,7 @@ public abstract class Raider extends PatrollingMonster { Raid currentRaid = this.getCurrentRaid(); if (this.canJoinRaid()) { if (currentRaid == null) { - if (this.level().getGameTime() % 20L == 0L) { + if (this.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading Raid raidAt = ((ServerLevel)this.level()).getRaidAt(this.blockPosition()); if (raidAt != null && Raids.canJoinRaid(this, raidAt)) { raidAt.joinRaid(raidAt.getGroupsSpawned(), this, null, true); diff --git a/net/minecraft/world/entity/raid/Raids.java b/net/minecraft/world/entity/raid/Raids.java index 34eb038725d1577f1a2d7c35c897b1270eac5749..0ffc1956d9e808871c5b36f6eb5ed750abaa880c 100644 --- a/net/minecraft/world/entity/raid/Raids.java +++ b/net/minecraft/world/entity/raid/Raids.java @@ -25,9 +25,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 level) { @@ -36,7 +36,7 @@ public class Raids extends SavedData { public Raids(ServerLevel level) { this.level = level; - this.nextAvailableID = 1; + this.nextAvailableID.set(1); // Folia - make raids thread-safe this.setDirty(); } @@ -44,12 +44,25 @@ public class Raids extends SavedData { return this.raidMap.get(id); } + // Folia start - make raids thread-safe + public void globalTick() { + ++this.tick; + if (this.tick % 200 == 0) { + this.setDirty(); + } + } + public void tick() { - this.tick++; + // Folia end - make raids thread-safe Iterator iterator = this.raidMap.values().iterator(); while (iterator.hasNext()) { 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(); } @@ -62,14 +75,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 @@ -87,7 +103,7 @@ public class Raids extends SavedData { return null; } else { DimensionType dimensionType = player.level().dimensionType(); - if (!dimensionType.hasRaids()) { + if (!dimensionType.hasRaids() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, pos.getX() >> 4, pos.getZ() >> 4, 8) || !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 @@ -145,7 +161,7 @@ public class Raids extends SavedData { public static Raids load(ServerLevel level, CompoundTag tag) { Raids raids = new Raids(level); - raids.nextAvailableID = tag.getInt("NextAvailableID"); + raids.nextAvailableID.set(tag.getInt("NextAvailableID")); // Folia - make raids thread-safe raids.tick = tag.getInt("Tick"); ListTag list = tag.getList("Raids", 10); @@ -160,7 +176,7 @@ public class Raids extends SavedData { @Override public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { - tag.putInt("NextAvailableID", this.nextAvailableID); + tag.putInt("NextAvailableID", this.nextAvailableID.get()); // Folia - make raids thread-safe tag.putInt("Tick", this.tick); ListTag listTag = new ListTag(); @@ -179,7 +195,7 @@ public class Raids extends SavedData { } private int getUniqueId() { - return ++this.nextAvailableID; + return this.nextAvailableID.incrementAndGet(); // Folia - make raids thread-safe } @Nullable @@ -188,6 +204,11 @@ public class Raids extends SavedData { double d = distance; for (Raid raid1 : this.raidMap.values()) { + // 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 < d) { raid = raid1; diff --git a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java index 82421d3b4116ca406cdfffec5a3d65a99cbe294b..3a575ff4860c3b000a23e7754181f48d942441e9 100644 --- a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java +++ b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java @@ -145,5 +145,11 @@ public class MinecartCommandBlock extends AbstractMinecart { return net.minecraft.world.entity.vehicle.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/net/minecraft/world/entity/vehicle/MinecartHopper.java b/net/minecraft/world/entity/vehicle/MinecartHopper.java index 8341e7f01606fca90e69384c16fc19bb9e20d1b7..c07f6fefdba5242c09c0081a0f074948f9df9ae6 100644 --- a/net/minecraft/world/entity/vehicle/MinecartHopper.java +++ b/net/minecraft/world/entity/vehicle/MinecartHopper.java @@ -145,7 +145,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/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java index 76f50437396f8f856381d0fbef52953ef7c263f6..d3b98f1d36b643989708ea22753a0c0d0d4243bc 100644 --- a/net/minecraft/world/item/ItemStack.java +++ b/net/minecraft/world/item/ItemStack.java @@ -386,31 +386,32 @@ public final class ItemStack implements DataComponentHolder { DataComponentPatch previousPatch = this.components.asPatch(); int oldCount = this.getCount(); ServerLevel serverLevel = (ServerLevel) context.getLevel(); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = serverLevel.getCurrentWorldData(); // Folia - region threading if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement - serverLevel.captureBlockStates = true; + worldData.captureBlockStates = true; // Folia - region threading // special case bonemeal if (item == Items.BONE_MEAL) { - serverLevel.captureTreeGeneration = true; + worldData.captureTreeGeneration = true; // Folia - region threading } } InteractionResult interactionResult; try { interactionResult = item.useOn(context); } finally { - serverLevel.captureBlockStates = false; + worldData.captureBlockStates = false; // Folia - region threading } DataComponentPatch newPatch = this.components.asPatch(); int newCount = this.getCount(); this.setCount(oldCount); this.restorePatch(previousPatch); - if (interactionResult.consumesAction() && serverLevel.captureTreeGeneration && !serverLevel.capturedBlockStates.isEmpty()) { - serverLevel.captureTreeGeneration = false; + if (interactionResult.consumesAction() && worldData.captureTreeGeneration && !worldData.capturedBlockStates.isEmpty()) { // Folia - region threading + worldData.captureTreeGeneration = false; // Folia - region threading org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(clickedPos, serverLevel.getWorld()); - org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; - net.minecraft.world.level.block.SaplingBlock.treeType = null; - List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); - serverLevel.capturedBlockStates.clear(); + org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // Folia - region threading + net.minecraft.world.level.block.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 org.bukkit.event.world.StructureGrowEvent structureEvent = null; if (treeType != null) { boolean isBonemeal = this.getItem() == Items.BONE_MEAL; @@ -436,15 +437,15 @@ public final class ItemStack implements DataComponentHolder { player.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 interactionResult; } - serverLevel.captureTreeGeneration = false; + worldData.captureTreeGeneration = false; // Folia - region threading if (player != null && interactionResult instanceof InteractionResult.Success success && success.wasItemInteraction()) { InteractionHand hand = context.getHand(); org.bukkit.event.block.BlockPlaceEvent placeEvent = null; - List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); - serverLevel.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(serverLevel, player, hand, blocks, clickedPos.getX(), clickedPos.getY(), clickedPos.getZ()); } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement @@ -455,17 +456,17 @@ public final class ItemStack implements DataComponentHolder { interactionResult = InteractionResult.FAIL; // cancel placement // PAIL: Remove this when MC-99075 fixed placeEvent.getPlayer().updateInventory(); - serverLevel.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 - serverLevel.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 - serverLevel.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 (org.bukkit.block.BlockState blockstate : blocks) { blockstate.update(true, false); } - serverLevel.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent - serverLevel.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 - 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(), previousPatch)) { @@ -473,7 +474,7 @@ public final class ItemStack implements DataComponentHolder { this.setCount(newCount); } - for (java.util.Map.Entry e : serverLevel.capturedTileEntities.entrySet()) { + for (java.util.Map.Entry e : worldData.capturedTileEntities.entrySet()) { // Folia - region threading serverLevel.setBlockEntity(e.getValue()); } @@ -508,15 +509,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 (serverLevel.getBlockEntity(SignItem.openSign) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) { - if (serverLevel.getBlockState(SignItem.openSign).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) { + if (serverLevel.getBlockEntity(SignItem.openSign.get()) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) { // Folia - region threading + if (serverLevel.getBlockState(SignItem.openSign.get()).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) { // Folia - region threading signBlock.openTextEdit(player, blockEntity, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // CraftBukkit // Paper - Add PlayerOpenSignEvent } } } finally { - SignItem.openSign = null; + SignItem.openSign.set(null); } } @@ -544,8 +545,8 @@ public final class ItemStack implements DataComponentHolder { player.awardStat(Stats.ITEM_USED.get(item)); } } - serverLevel.capturedTileEntities.clear(); - serverLevel.capturedBlockStates.clear(); + worldData.capturedTileEntities.clear(); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading // CraftBukkit end return interactionResult; diff --git a/net/minecraft/world/item/MapItem.java b/net/minecraft/world/item/MapItem.java index 8795d54cff569c911e0a535f38a0ec4130f7b4d5..9f07ce560e265582eec0fff5877a923f62a60e13 100644 --- a/net/minecraft/world/item/MapItem.java +++ b/net/minecraft/world/item/MapItem.java @@ -70,6 +70,7 @@ public class MapItem extends Item { } public void update(Level level, Entity viewer, MapItemSavedData data) { + synchronized (data) { // Folia - make map data thread-safe if (level.dimension() == data.dimension && viewer instanceof Player) { int i = 1 << data.scale; int i1 = data.centerX; @@ -99,8 +100,8 @@ public class MapItem extends Item { int i9 = (i1 / i + i6 - 64) * i; int i10 = (i2 / i + i7 - 64) * i; Multiset multiset = LinkedHashMultiset.create(); - LevelChunk chunk = level.getChunkIfLoaded(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10)); // Paper - Maps shouldn't load chunks - if (chunk != null && !chunk.isEmpty()) { // Paper - Maps shouldn't load chunks + LevelChunk chunk = level.getChunkIfLoaded(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10)); // Paper - Maps shouldn't load chunks // Folia - super important that it uses getChunkIfLoaded + if (chunk != null && !chunk.isEmpty() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, chunk.getPos())) { // Paper - Maps shouldn't load chunks // Folia - make sure chunk is owned int i11 = 0; double d1 = 0.0; if (level.dimensionType().hasCeiling()) { @@ -182,6 +183,7 @@ public class MapItem extends Item { } } } + } // Folia - make map data thread-safe } private BlockState getCorrectStateForFluidBlock(Level level, BlockState state, BlockPos pos) { @@ -196,6 +198,7 @@ public class MapItem extends Item { public static void renderBiomePreviewMap(ServerLevel serverLevel, ItemStack stack) { MapItemSavedData savedData = getSavedData(stack, serverLevel); if (savedData != null) { + synchronized (savedData) { // Folia - make map data thread-safe if (serverLevel.dimension() == savedData.dimension) { int i = 1 << savedData.scale; int i1 = savedData.centerX; @@ -265,6 +268,7 @@ public class MapItem extends Item { } } } + } // Folia - make map data thread-safe } } @@ -273,6 +277,7 @@ public class MapItem extends Item { if (!level.isClientSide) { MapItemSavedData savedData = getSavedData(stack, level); if (savedData != null) { + synchronized (savedData) { // Folia - region threading if (entity instanceof Player player) { savedData.tickCarriedBy(player, stack); } @@ -280,6 +285,7 @@ public class MapItem extends Item { if (!savedData.locked && (isSelected || entity instanceof Player && ((Player)entity).getOffhandItem() == stack)) { this.update(level, entity, savedData); } + } // Folia - region threading } } } diff --git a/net/minecraft/world/item/SignItem.java b/net/minecraft/world/item/SignItem.java index fffac12db30d4321981959a9149cc56f8b4f6df6..fdf4fa92a5ca98fae6266e29a54fb1b77e69407c 100644 --- a/net/minecraft/world/item/SignItem.java +++ b/net/minecraft/world/item/SignItem.java @@ -11,7 +11,7 @@ import net.minecraft.world.level.block.entity.SignBlockEntity; 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(Block standingBlock, Block wallBlock, Item.Properties properties) { super(standingBlock, wallBlock, Direction.DOWN, properties); } @@ -30,7 +30,7 @@ public class SignItem extends StandingAndWallBlockItem { && level.getBlockState(pos).getBlock() instanceof SignBlock signBlock) { // CraftBukkit start - SPIGOT-4678 // signBlock.openTextEdit(player, signBlockEntity, true); - SignItem.openSign = pos; + SignItem.openSign.set(pos); // Folia - region threading // CraftBukkit end } diff --git a/net/minecraft/world/item/component/LodestoneTracker.java b/net/minecraft/world/item/component/LodestoneTracker.java index 0c00c23743a4978e8dceed5bbee8ca44b0e0c8d6..b6de5d017bed5c71125f26881b95383386aa1a79 100644 --- a/net/minecraft/world/item/component/LodestoneTracker.java +++ b/net/minecraft/world/item/component/LodestoneTracker.java @@ -29,7 +29,10 @@ public record LodestoneTracker(Optional target, boolean tracked) { return this; } else { BlockPos blockPos = this.target.get().pos(); - return level.isInWorldBounds(blockPos) && (!level.hasChunkAt(blockPos) || level.getPoiManager().existsAtPosition(PoiTypes.LODESTONE, blockPos)) // Paper - Prevent compass from loading chunks + // Folia start - do not access the POI data off-region + net.minecraft.world.level.chunk.LevelChunk chunk = level.getChunkIfLoaded(blockPos); + return level.isInWorldBounds(blockPos) && (chunk == null || chunk.getBlockState(blockPos).getBlock() == net.minecraft.world.level.block.Blocks.LODESTONE) // Paper - Prevent compass from loading chunks + // Folia end - do not access the POI data off-region ? this : new LodestoneTracker(Optional.empty(), true); } diff --git a/net/minecraft/world/level/BaseCommandBlock.java b/net/minecraft/world/level/BaseCommandBlock.java index a67d40eb4bfa85888af8bf027a8859378d290cfa..b02b79ccedb8b87bc22270377dfc36e21ebe1724 100644 --- a/net/minecraft/world/level/BaseCommandBlock.java +++ b/net/minecraft/world/level/BaseCommandBlock.java @@ -21,7 +21,7 @@ import net.minecraft.world.level.block.entity.BlockEntity; 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; @@ -114,6 +114,7 @@ public abstract class BaseCommandBlock implements CommandSource { } public boolean performCommand(Level level) { + if (true) return false; // Folia - region threading if (level.isClientSide || level.getGameTime() == this.lastExecution) { return false; } else if ("Searge".equalsIgnoreCase(this.command)) { @@ -164,11 +165,14 @@ public abstract class BaseCommandBlock implements CommandSource { this.customName = customName; } + public void threadCheck() {} // Folia + @Override public void sendSystemMessage(Component component) { if (this.trackOutput) { org.spigotmc.AsyncCatcher.catchOp("sendSystemMessage to a command block"); // Paper - Don't broadcast messages to command blocks - this.lastOutput = Component.literal("[" + TIME_FORMAT.format(new Date()) + "] ").append(component); + this.threadCheck(); // Folia + this.lastOutput = Component.literal("[" + TIME_FORMAT.get().format(new Date()) + "] ").append(component); // Folia - region threading - SDF is not thread-safe this.onUpdated(); } } diff --git a/net/minecraft/world/level/EntityGetter.java b/net/minecraft/world/level/EntityGetter.java index 892a7c1eb1b321ca6d5ca709142e7feae1220815..c4c7993714ff4189b05e2b4653fd2a5eba6b7da9 100644 --- a/net/minecraft/world/level/EntityGetter.java +++ b/net/minecraft/world/level/EntityGetter.java @@ -24,6 +24,12 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst return this.getEntities(EntityTypeTest.forClass(entityClass), area, filter); } + // Folia start - region threading + default List getLocalPlayers() { + return java.util.Collections.emptyList(); + } + // Folia end - region threading + List players(); default List getEntities(@Nullable Entity entity, AABB area) { @@ -123,7 +129,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst double d = -1.0; Player player = null; - for (Player player1 : this.players()) { + for (Player player1 : this.getLocalPlayers()) { // Folia - region threading if (predicate == null || predicate.test(player1)) { double d1 = player1.distanceToSqr(x, y, z); if ((distance < 0.0 || d1 < distance * distance) && (d == -1.0 || d1 < d)) { @@ -144,7 +150,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); @@ -171,7 +177,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) { @@ -184,7 +190,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 distance) { - 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 (distance < 0.0 || d < distance * distance) { @@ -198,8 +204,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst @Nullable default Player getPlayerByUUID(UUID uniqueId) { - for (int i = 0; i < this.players().size(); i++) { - Player player = this.players().get(i); + for (Player player : this.getLocalPlayers()) { // Folia - region threading if (uniqueId.equals(player.getUUID())) { return player; } diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java index 1dbe7c7c1051c3972105534a07ce50d4cf98fc85..db4ce98706bf69dcd8144faba1780f83ca1f6787 100644 --- a/net/minecraft/world/level/Level.java +++ b/net/minecraft/world/level/Level.java @@ -115,10 +115,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; @@ -128,7 +128,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl public float rainLevel; protected float oThunderLevel; public float thunderLevel; - public final RandomSource random = new ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); // Paper - replace random + public final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Paper - replace random // Folia - region threading @Deprecated private final RandomSource threadSafeRandom = RandomSource.createThreadSafe(); private final Holder dimensionTypeRegistration; @@ -139,28 +139,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; @@ -173,9 +162,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; @@ -825,6 +814,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 levelData, @@ -888,7 +903,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl this.thread = Thread.currentThread(); this.biomeManager = new BiomeManager(this, biomeZoomSeed); this.isDebug = isDebug; - this.neighborUpdater = new CollectingNeighborUpdater(this, maxChainedNeighborUpdates); + this.neighbourUpdateMax = maxChainedNeighborUpdates; // Folia - region threading this.registryAccess = registryAccess; this.damageSources = new DamageSources(registryAccess); @@ -1035,8 +1050,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(); } @@ -1098,16 +1113,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Override public boolean setBlock(BlockPos pos, BlockState state, int flags, int recursionLeft) { + 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); @@ -1123,10 +1140,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl Block block = state.getBlock(); // 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 @@ -1136,8 +1153,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl if (blockState == 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; @@ -1174,7 +1191,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 { @@ -1219,7 +1236,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); @@ -1233,7 +1250,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 @@ -1322,7 +1339,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Override public void neighborShapeChanged(Direction direction, BlockPos pos, BlockPos neighborPos, BlockState neighborState, int flags, int recursionLeft) { - this.neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, recursionLeft); + this.getCurrentWorldData().neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, recursionLeft); // Folia - region threading } @Override @@ -1346,11 +1363,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(); } @@ -1454,17 +1494,16 @@ 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() { ProfilerFiller profilerFiller = Profiler.get(); profilerFiller.push("blockEntities"); - 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 // Spigot start boolean runsNormally = this.tickRateManager().runsNormally(); @@ -1472,9 +1511,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl int tickedEntities = 0; // Paper - rewrite chunk system 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 = this.blockEntityTickers.get(this.tileTickPosition); + for (int i = 0; i < blockEntityTickers.size(); i++) { // Paper - Disable tick limiters // Folia - regionised ticking + TickingBlockEntity tickingBlockEntity = blockEntityTickers.get(i); // Folia - regionised ticking // Spigot end if (tickingBlockEntity.isRemoved()) { toRemove.add(tickingBlockEntity); // Paper - Fix MC-117075; use removeAll @@ -1487,11 +1525,11 @@ 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.tickingBlockEntities = false; + regionizedWorldData.seTtickingBlockEntities(false); // Folia - regionised ticking profilerFiller.pop(); - this.spigotConfig.currentPrimedTnt = 0; // Spigot + regionizedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading } public void guardEntityTick(Consumer consumerEntity, T entity) { @@ -1502,7 +1540,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, var6); getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, var6))); // 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 @@ -1648,9 +1687,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Nullable public BlockEntity getBlockEntity(BlockPos pos, 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(pos)) != null) { + if (!this.getCurrentWorldData().capturedTileEntities.isEmpty() && (blockEntity = this.getCurrentWorldData().capturedTileEntities.get(pos)) != null) { // Folia - region threading return blockEntity; } // Paper end - Perf: Optimize capturedTileEntities lookup @@ -1668,8 +1712,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl BlockPos blockPos = blockEntity.getBlockPos(); if (!this.isOutsideBuildHeight(blockPos)) { // CraftBukkit start - if (this.captureBlockStates) { - this.capturedTileEntities.put(blockPos.immutable(), blockEntity); + if (this.getCurrentWorldData().captureBlockStates) { // Folia - region threading + this.getCurrentWorldData().capturedTileEntities.put(blockPos.immutable(), blockEntity); // Folia - region threading return; } // CraftBukkit end @@ -1749,6 +1793,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Override public List getEntities(@Nullable Entity entity, AABB boundingBox, Predicate predicate) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, boundingBox, "Cannot getEntities asynchronously"); // Folia - region threading Profiler.get().incrementCounter("getEntities"); List list = Lists.newArrayList(); @@ -1778,6 +1823,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 Profiler.get().incrementCounter("getEntities"); if (entityTypeTest instanceof net.minecraft.world.entity.EntityType byType) { @@ -1877,13 +1923,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl public void disconnect() { } + @Override // Folia - region threading public long getGameTime() { - return this.levelData.getGameTime(); + // Folia start - region threading + // 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(); + // Folia end - region threading } public long getDayTime() { - return this.levelData.getDayTime(); + // Folia start - region threading + // 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 end - region threading + } + + // 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; @@ -2061,8 +2128,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl public abstract RecipeAccess recipeAccess(); public BlockPos getBlockRandomPos(int x, int y, int z, int yMask) { - this.randValue = this.randValue * 3 + 1013904223; - int i = this.randValue >> 2; + int i = this.random.nextInt() >> 2; // Folia - region threading return new BlockPos(x + (i & 15), y + (i >> 16 & yMask), z + (i >> 8 & 15)); } @@ -2083,7 +2149,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl @Override public long nextSubTickCount() { - return this.subTickCount++; + return this.subTickCount.getAndIncrement(); // Folia - region threading } @Override diff --git a/net/minecraft/world/level/LevelAccessor.java b/net/minecraft/world/level/LevelAccessor.java index ee9d320da1b4c3aa66be6592867e95c706b65b3a..cd5bfa374b0b1af64bc8415ace94fa43955e5145 100644 --- a/net/minecraft/world/level/LevelAccessor.java +++ b/net/minecraft/world/level/LevelAccessor.java @@ -33,14 +33,24 @@ public interface LevelAccessor extends CommonLevelAccessor, LevelTimeAccess, Sch long nextSubTickCount(); + // Folia start - region threading + default long getGameTime() { + return this.getLevelData().getGameTime(); + } + + default long getRedstoneGameTime() { + return this.getLevelData().getGameTime(); + } + // Folia end - region threading + @Override default ScheduledTick createTick(BlockPos pos, T type, int delay, TickPriority priority) { - return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + delay, priority, this.nextSubTickCount()); + return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + delay, priority, this.nextSubTickCount()); // Folia - region threading } @Override default ScheduledTick createTick(BlockPos pos, T type, int delay) { - return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + delay, this.nextSubTickCount()); + return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + delay, this.nextSubTickCount()); // Folia - region threading } LevelData getLevelData(); diff --git a/net/minecraft/world/level/LevelReader.java b/net/minecraft/world/level/LevelReader.java index 26c8c1e5598daf3550aef05b12218c47bda6618b..e59e1bb91e446406e58cc8046a85b693adb11e86 100644 --- a/net/minecraft/world/level/LevelReader.java +++ b/net/minecraft/world/level/LevelReader.java @@ -204,6 +204,25 @@ public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_syste return toY >= this.getMinY() && fromY <= this.getMaxY() && this.hasChunksAt(fromX, fromZ, toX, toZ); } + // 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 fromX, int fromZ, int toX, int toZ) { int sectionPosCoord = SectionPos.blockToSectionCoord(fromX); diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java index 17ce115e887cbbb06ad02ab7ddb488e27342c0e4..5ce81eafee33d22b69029c088d4be497131338a2 100644 --- a/net/minecraft/world/level/NaturalSpawner.java +++ b/net/minecraft/world/level/NaturalSpawner.java @@ -137,7 +137,7 @@ public final class NaturalSpawner { int limit = mobCategory.getMaxInstancesPerChunk(); SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(mobCategory); if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { - spawnThisTick = level.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(spawnCategory) == 0; + spawnThisTick = level.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && level.getRedstoneGameTime() % level.ticksPerSpawnCategory.getLong(spawnCategory) == 0; // Folia - region threading limit = level.getWorld().getSpawnLimit(spawnCategory); } diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java index c4485f28def66264846a436cfba7bddccb66b82e..73456d625489302e28c0452bde4508db0efa126c 100644 --- a/net/minecraft/world/level/ServerExplosion.java +++ b/net/minecraft/world/level/ServerExplosion.java @@ -773,17 +773,18 @@ public class ServerExplosion implements Explosion { if (!this.level.paperConfig().environment.optimizeExplosions) { return this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations } + 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, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations - 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/net/minecraft/world/level/ServerLevelAccessor.java b/net/minecraft/world/level/ServerLevelAccessor.java index b4f14ff9ef0c212f4d0e0c2ccf20ce1e7af9e734..441ba6ae8885a968734ac0abdb8a9d09fa658430 100644 --- a/net/minecraft/world/level/ServerLevelAccessor.java +++ b/net/minecraft/world/level/ServerLevelAccessor.java @@ -6,6 +6,12 @@ import net.minecraft.world.entity.Entity; 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/net/minecraft/world/level/StructureManager.java b/net/minecraft/world/level/StructureManager.java index 8bc6a6c86cd8db53feefba7508b6031ba67e242e..9abfcfa3e8d8319e98866b2a81f2eb9ac7269055 100644 --- a/net/minecraft/world/level/StructureManager.java +++ b/net/minecraft/world/level/StructureManager.java @@ -48,12 +48,7 @@ public class StructureManager { } public List startsForStructure(ChunkPos chunkPos, Predicate structurePredicate) { - // Paper start - Fix swamp hut cat generation deadlock - return this.startsForStructure(chunkPos, structurePredicate, null); - } - - public List startsForStructure(ChunkPos chunkPos, Predicate structurePredicate, @Nullable ServerLevelAccessor levelAccessor) { - Map allReferences = (levelAccessor == null ? this.level : levelAccessor).getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); + Map allReferences = this.level.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); // Folia - region threading // Paper end - Fix swamp hut cat generation deadlock Builder builder = ImmutableList.builder(); @@ -124,20 +119,12 @@ 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) { + // Folia - region threading // Paper end - Fix swamp hut cat generation deadlock Registry registry = this.registryAccess().lookupOrThrow(Registries.STRUCTURE); for (StructureStart structureStart : this.startsForStructure( - new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false), levelAccessor // Paper - Fix swamp hut cat generation deadlock + new ChunkPos(pos), structure -> registry.get(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; @@ -182,7 +169,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/net/minecraft/world/level/block/BedBlock.java b/net/minecraft/world/level/block/BedBlock.java index 8c21e8aa4922691fa66cd22d631646c554251bdd..7328ab4bcb11b09713fed0625a0988cb1c9d43f2 100644 --- a/net/minecraft/world/level/block/BedBlock.java +++ b/net/minecraft/world/level/block/BedBlock.java @@ -346,7 +346,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock BlockPos blockPos = pos.relative(state.getValue(FACING)); level.setBlock(blockPos, state.setValue(PART, BedPart.HEAD), 3); // CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states - if (level.captureBlockStates) { + if (level.getCurrentWorldData().captureBlockStates) { // Folia - region threading return; } // CraftBukkit end diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java index 976de81d65b6494cdad20f4ec5125fceec86f951..aa09b2e8fac82ab954f581df3d41153c6244c2e8 100644 --- a/net/minecraft/world/level/block/Block.java +++ b/net/minecraft/world/level/block/Block.java @@ -362,8 +362,8 @@ public class Block extends BlockBehaviour implements ItemLike { ItemEntity itemEntity = itemEntitySupplier.get(); itemEntity.setDefaultPickUpDelay(); // CraftBukkit start - if (level.captureDrops != null) { - level.captureDrops.add(itemEntity); + if (level.getCurrentWorldData().captureDrops != null) { // Folia - region threading + level.getCurrentWorldData().captureDrops.add(itemEntity); // Folia - region threading } else { level.addFreshEntity(itemEntity); } diff --git a/net/minecraft/world/level/block/BushBlock.java b/net/minecraft/world/level/block/BushBlock.java index bc52568bfa56635300266424488e524d77d95e09..068e65fb7efd52b36ba7f49829da80d82753e78e 100644 --- a/net/minecraft/world/level/block/BushBlock.java +++ b/net/minecraft/world/level/block/BushBlock.java @@ -38,7 +38,7 @@ public abstract class BushBlock extends Block { // CraftBukkit start if (!state.canSurvive(level, pos)) { // Suppress during worldgen - if (!(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(serverLevel, pos).isCancelled()) { // Paper + if (!(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.getCurrentWorldData().hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(serverLevel, pos).isCancelled()) { // Paper // Folia - region threading return Blocks.AIR.defaultBlockState(); } } diff --git a/net/minecraft/world/level/block/DaylightDetectorBlock.java b/net/minecraft/world/level/block/DaylightDetectorBlock.java index a83d1dd4cac85d34f695333fd917a41f14dd5715..17532ef2cc5e21e68a1d51146641ae124a67f79e 100644 --- a/net/minecraft/world/level/block/DaylightDetectorBlock.java +++ b/net/minecraft/world/level/block/DaylightDetectorBlock.java @@ -110,7 +110,7 @@ public class DaylightDetectorBlock extends BaseEntityBlock { } private static void tickEntity(Level level, BlockPos pos, BlockState state, DaylightDetectorBlockEntity blockEntity) { - if (level.getGameTime() % 20L == 0L) { + if (level.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading updateSignalStrength(state, level, pos); } } diff --git a/net/minecraft/world/level/block/DispenserBlock.java b/net/minecraft/world/level/block/DispenserBlock.java index e0a4d41e5bcf144ea4c10d6f633c3a95ed2c5aec..0ff736a45776bbf16f32ac05f099bb656aa3b9a6 100644 --- a/net/minecraft/world/level/block/DispenserBlock.java +++ b/net/minecraft/world/level/block/DispenserBlock.java @@ -50,7 +50,7 @@ public class DispenserBlock extends BaseEntityBlock { private static final DefaultDispenseItemBehavior DEFAULT_BEHAVIOR = new DefaultDispenseItemBehavior(); public static final Map DISPENSER_REGISTRY = new IdentityHashMap<>(); 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() { @@ -96,7 +96,7 @@ public class DispenserBlock extends BaseEntityBlock { DispenseItemBehavior dispenseMethod = this.getDispenseMethod(level, item); if (dispenseMethod != DispenseItemBehavior.NOOP) { if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(level, pos, item, randomSlot)) return; // Paper - Add BlockPreDispenseEvent - DispenserBlock.eventFired = false; // CraftBukkit - reset event status + DispenserBlock.eventFired.set(Boolean.FALSE); // CraftBukkit - reset event status // Folia - region threading dispenserBlockEntity.setItem(randomSlot, dispenseMethod.dispense(blockSource, item)); } } diff --git a/net/minecraft/world/level/block/DoublePlantBlock.java b/net/minecraft/world/level/block/DoublePlantBlock.java index 7d033444ab5f89fae3c571a67ede6e7eff378945..e46c4071a955d880d61235d0861d8752ab3b860e 100644 --- a/net/minecraft/world/level/block/DoublePlantBlock.java +++ b/net/minecraft/world/level/block/DoublePlantBlock.java @@ -118,7 +118,7 @@ public class DoublePlantBlock extends BushBlock { protected static void preventDropFromBottomPart(Level level, BlockPos pos, BlockState state, Player player) { // CraftBukkit start - if (((net.minecraft.server.level.ServerLevel)level).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(level, pos).isCancelled()) { // Paper + if (((net.minecraft.server.level.ServerLevel)level).getCurrentWorldData().hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(level, pos).isCancelled()) { // Paper // Folia - region threading return; } // CraftBukkit end diff --git a/net/minecraft/world/level/block/EndGatewayBlock.java b/net/minecraft/world/level/block/EndGatewayBlock.java index 84a1bd5e40e635962d795506861447851e443eee..a7b8e2b702fbe512c9633075515da6a430e76861 100644 --- a/net/minecraft/world/level/block/EndGatewayBlock.java +++ b/net/minecraft/world/level/block/EndGatewayBlock.java @@ -111,17 +111,43 @@ public class EndGatewayBlock extends BaseEntityBlock implements Portal { if (portalPosition == null) { return null; } else { - return entity instanceof ThrownEnderpearl - ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit - : new TeleportTransition( - level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit - ); + return getTeleportTransition(level, entity, portalPosition); // Folia - region threading } } else { return null; } } + // Folia start - region threading + public static TeleportTransition getTeleportTransition(ServerLevel level, Entity entity, Vec3 portalPosition) { + return entity instanceof ThrownEnderpearl + ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit + : new TeleportTransition( + level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit + ); + } + + @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, TeleportTransition.PLACE_PORTAL_TICKET + ); + } + // Folia end - region threading + @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.INVISIBLE; diff --git a/net/minecraft/world/level/block/EndPortalBlock.java b/net/minecraft/world/level/block/EndPortalBlock.java index c11366dd69e1c51bdab45c625b07c15ce2e42cb6..554d75ac1374d7d93977a10e06fcf51259830c97 100644 --- a/net/minecraft/world/level/block/EndPortalBlock.java +++ b/net/minecraft/world/level/block/EndPortalBlock.java @@ -63,7 +63,7 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal { level.getCraftServer().getPluginManager().callEvent(event); if (event.isCancelled()) return; // Paper - make cancellable // CraftBukkit end - if (!level.isClientSide && level.dimension() == Level.END && entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) { + if (false && !level.isClientSide && level.dimension() == Level.END && entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) { // Folia - region threading - do not show credits if (level.paperConfig().misc.disableEndCredits) {serverPlayer.seenCredits = true; return;} // Paper - Option to disable end credits serverPlayer.showEndCredits(); } else { @@ -115,6 +115,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 level, BlockPos pos, RandomSource random) { double d = pos.getX() + random.nextDouble(); diff --git a/net/minecraft/world/level/block/FarmBlock.java b/net/minecraft/world/level/block/FarmBlock.java index 1fdede769b67cb5d2f9159c779f19e3639bb6ff5..81aa50abf396d7fe22dd95dacabf8b83786249fe 100644 --- a/net/minecraft/world/level/block/FarmBlock.java +++ b/net/minecraft/world/level/block/FarmBlock.java @@ -95,8 +95,8 @@ public class FarmBlock extends Block { @Override protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { int moistureValue = state.getValue(MOISTURE); - if (moistureValue > 0 && level.paperConfig().tickRates.wetFarmland != 1 && (level.paperConfig().tickRates.wetFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks - if (moistureValue == 0 && level.paperConfig().tickRates.dryFarmland != 1 && (level.paperConfig().tickRates.dryFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks + if (moistureValue > 0 && level.paperConfig().tickRates.wetFarmland != 1 && (level.paperConfig().tickRates.wetFarmland < 1 || (level.getRedstoneGameTime() + pos.hashCode()) % level.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading + if (moistureValue == 0 && level.paperConfig().tickRates.dryFarmland != 1 && (level.paperConfig().tickRates.dryFarmland < 1 || (level.getRedstoneGameTime() + pos.hashCode()) % level.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading if (!isNearWater(level, pos) && !level.isRainingAt(pos.above())) { if (moistureValue > 0) { org.bukkit.craftbukkit.event.CraftEventFactory.handleMoistureChangeEvent(level, pos, state.setValue(FarmBlock.MOISTURE, moistureValue - 1), 2); // CraftBukkit diff --git a/net/minecraft/world/level/block/FungusBlock.java b/net/minecraft/world/level/block/FungusBlock.java index 85f0eac75784565c658c5178c544f969db3d6f54..81edac1fa383c6875c7a0439f2a160c11ef77a41 100644 --- a/net/minecraft/world/level/block/FungusBlock.java +++ b/net/minecraft/world/level/block/FungusBlock.java @@ -76,9 +76,9 @@ public class FungusBlock extends BushBlock implements BonemealableBlock { // CraftBukkit start .map((value) -> { 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 } return value; }) diff --git a/net/minecraft/world/level/block/HoneyBlock.java b/net/minecraft/world/level/block/HoneyBlock.java index bab3ac2c4be08ea7589752b8472c1e13bcaab76a..776216db8097ceadc81d2f8401ea71447769b396 100644 --- a/net/minecraft/world/level/block/HoneyBlock.java +++ b/net/minecraft/world/level/block/HoneyBlock.java @@ -94,7 +94,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/net/minecraft/world/level/block/LightningRodBlock.java b/net/minecraft/world/level/block/LightningRodBlock.java index 534de49aec290766d6bc2523bb3975df775b5881..d79b3d328915096d723c0e3e6b6eb75cfe5bac51 100644 --- a/net/minecraft/world/level/block/LightningRodBlock.java +++ b/net/minecraft/world/level/block/LightningRodBlock.java @@ -116,7 +116,7 @@ public class LightningRodBlock extends RodBlock implements SimpleWaterloggedBloc @Override public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { if (level.isThundering() - && level.random.nextInt(200) <= level.getGameTime() % 200L + && level.random.nextInt(200) <= level.getRedstoneGameTime() % 200L // Folia - region threading && pos.getY() == level.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { ParticleUtils.spawnParticlesAlongAxis(state.getValue(FACING).getAxis(), level, pos, 0.125, ParticleTypes.ELECTRIC_SPARK, UniformInt.of(1, 2)); } diff --git a/net/minecraft/world/level/block/MushroomBlock.java b/net/minecraft/world/level/block/MushroomBlock.java index 904369f4d7db41026183f2de7c96c2f0f4dc204d..223b1789ba94f763e29fb5e74aade787681e9f5b 100644 --- a/net/minecraft/world/level/block/MushroomBlock.java +++ b/net/minecraft/world/level/block/MushroomBlock.java @@ -94,7 +94,7 @@ public class MushroomBlock extends BushBlock implements BonemealableBlock { return false; } else { level.removeBlock(pos, false); - SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM; // CraftBukkit + SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM); // CraftBukkit // Folia - region threading if (optional.get().value().place(level, level.getChunkSource().getGenerator(), random, pos)) { return true; } else { diff --git a/net/minecraft/world/level/block/NetherPortalBlock.java b/net/minecraft/world/level/block/NetherPortalBlock.java index e2eb693b0130513115392cb0cb5a829ede5be8c5..68e1b1737c8b7af39f22dd4d28b879b5c3d52f65 100644 --- a/net/minecraft/world/level/block/NetherPortalBlock.java +++ b/net/minecraft/world/level/block/NetherPortalBlock.java @@ -181,6 +181,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 TeleportTransition getExitPortal(ServerLevel level, Entity entity, BlockPos pos, BlockPos exitPos, boolean isNether, WorldBorder worldBorder, int searchRadius, boolean canCreatePortal, int createRadius) { // CraftBukkit Optional optional = level.getPortalForcer().findClosestPortalPosition(exitPos, worldBorder, searchRadius); // CraftBukkit @@ -188,14 +215,14 @@ public class NetherPortalBlock extends Block implements Portal { TeleportTransition.PostTeleportTransition postTeleportTransition; if (optional.isPresent()) { BlockPos blockPos = optional.get(); - BlockState blockState = level.getBlockState(blockPos); + BlockState blockState = level.getBlockStateFromEmptyChunk(blockPos); // Folia - region threading largestRectangleAround = BlockUtil.getLargestRectangleAround( blockPos, blockState.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, - blockPos1 -> level.getBlockState(blockPos1) == blockState + blockPos1 -> level.getBlockStateFromEmptyChunk(blockPos1) == blockState // Folia - region threading ); postTeleportTransition = TeleportTransition.PLAY_PORTAL_SOUND.then(entity1 -> entity1.placePortalTicket(blockPos)); } else if (canCreatePortal) { // CraftBukkit @@ -238,7 +265,7 @@ public class NetherPortalBlock extends Block implements Portal { return createDimensionTransition(level, rectangle, axis, relativePortalPosition, entity, postTeleportTransition); } - private static TeleportTransition createDimensionTransition( + public static TeleportTransition createDimensionTransition( // Folia - region threading - public ServerLevel level, BlockUtil.FoundRectangle rectangle, Direction.Axis axis, diff --git a/net/minecraft/world/level/block/Portal.java b/net/minecraft/world/level/block/Portal.java index c941b0e05d98fa59669757174887955e6319eddb..3883a437d99e5d8b13c55764613d630e29e75bc4 100644 --- a/net/minecraft/world/level/block/Portal.java +++ b/net/minecraft/world/level/block/Portal.java @@ -14,6 +14,10 @@ public interface Portal { @Nullable TeleportTransition getPortalDestination(ServerLevel level, 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/net/minecraft/world/level/block/RedStoneWireBlock.java b/net/minecraft/world/level/block/RedStoneWireBlock.java index 12c9d60314c99fb65e640d255a2d0c6b7790ad4d..6ba86c5e55d09fd99e81e40db4614ef14246bdc3 100644 --- a/net/minecraft/world/level/block/RedStoneWireBlock.java +++ b/net/minecraft/world/level/block/RedStoneWireBlock.java @@ -91,7 +91,7 @@ public class RedStoneWireBlock extends Block { private static final float PARTICLE_DENSITY = 0.2F; private final BlockState crossState; private final RedstoneWireEvaluator evaluator = new DefaultRedstoneWireEvaluator(this); - public boolean shouldSignal = true; + //public boolean shouldSignal = true; // Folia - region threading - move to regionised world data @Override public MapCodec codec() { @@ -293,6 +293,11 @@ public class RedStoneWireBlock extends Block { // Paper start - Optimize redstone (Eigencraft) // The bulk of the new functionality is found in RedstoneWireTurbo.java io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this); + // Folia start - region threading + private io.papermc.paper.redstone.RedstoneWireTurbo getTurbo(Level world) { + return world.getCurrentWorldData().turbo; + } + // Folia end - region threading /* * Modified version of pre-existing updateSurroundingRedstone, which is called from @@ -308,7 +313,7 @@ public class RedStoneWireBlock extends Block { if (orientation != null) { source = pos.relative(orientation.getFront().getOpposite()); } - turbo.updateSurroundingRedstone(worldIn, pos, state, source); + getTurbo(worldIn).updateSurroundingRedstone(worldIn, pos, state, source); // Folia - region threading return; } updatePowerStrength(worldIn, pos, state, orientation, blockAdded); @@ -336,7 +341,7 @@ public class RedStoneWireBlock extends Block { // [Space Walker] suppress shape updates and emit those manually to // bypass the new neighbor update stack. if (level.setBlock(pos, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) { - turbo.updateNeighborShapes(level, pos, state); + this.getTurbo(level).updateNeighborShapes(level, pos, state); // Folia - region threading } } } @@ -353,9 +358,9 @@ public class RedStoneWireBlock extends Block { } public int getBlockSignal(Level level, BlockPos pos) { - this.shouldSignal = false; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading int bestNeighborSignal = level.getBestNeighborSignal(pos); - this.shouldSignal = true; + io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading return bestNeighborSignal; } @@ -450,12 +455,12 @@ public class RedStoneWireBlock extends Block { @Override protected int getDirectSignal(BlockState blockState, BlockGetter blockAccess, BlockPos pos, Direction side) { - return !this.shouldSignal ? 0 : blockState.getSignal(blockAccess, pos, side); + return !io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal ? 0 : blockState.getSignal(blockAccess, pos, side); // Folia - region threading } @Override protected int getSignal(BlockState blockState, BlockGetter blockAccess, BlockPos pos, Direction side) { - if (this.shouldSignal && side != Direction.DOWN) { + if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal && side != Direction.DOWN) { // Folia - region threading int powerValue = blockState.getValue(POWER); if (powerValue == 0) { return 0; @@ -487,7 +492,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 power) { diff --git a/net/minecraft/world/level/block/RedstoneTorchBlock.java b/net/minecraft/world/level/block/RedstoneTorchBlock.java index 18420ec1f5776b018010f26e59aba00ae5bd0723..d5ac5d8fddeaff0def61a909faf2c909337ada57 100644 --- a/net/minecraft/world/level/block/RedstoneTorchBlock.java +++ b/net/minecraft/world/level/block/RedstoneTorchBlock.java @@ -73,10 +73,10 @@ public class RedstoneTorchBlock extends BaseTorchBlock { protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { boolean hasNeighborSignal = this.hasNeighborSignal(level, pos, state); // Paper start - Faster redstone torch rapid clock removal - java.util.ArrayDeque redstoneUpdateInfos = level.redstoneUpdateInfos; + java.util.ArrayDeque redstoneUpdateInfos = level.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading if (redstoneUpdateInfos != null) { RedstoneTorchBlock.Toggle curr; - while ((curr = redstoneUpdateInfos.peek()) != null && level.getGameTime() - curr.when > 60L) { + while ((curr = redstoneUpdateInfos.peek()) != null && level.getRedstoneGameTime() - curr.when > 60L) { // Folia - region threading redstoneUpdateInfos.poll(); } } @@ -154,13 +154,13 @@ public class RedstoneTorchBlock extends BaseTorchBlock { private static boolean isToggledTooFrequently(Level level, BlockPos pos, boolean logToggle) { // Paper start - Faster redstone torch rapid clock removal - java.util.ArrayDeque list = level.redstoneUpdateInfos; + java.util.ArrayDeque list = level.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading if (list == null) { - list = level.redstoneUpdateInfos = new java.util.ArrayDeque<>(); + list = level.getCurrentWorldData().redstoneUpdateInfos = new java.util.ArrayDeque<>(); // Folia - region threading } // Paper end - Faster redstone torch rapid clock removal if (logToggle) { - list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), level.getGameTime())); + list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), level.getRedstoneGameTime())); // Folia - region threading } int i = 0; @@ -182,12 +182,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 threading public Toggle(BlockPos pos, long when) { this.pos = pos; this.when = when; } + + // Folia start - region ticking + public void offsetTime(long offset) { + this.when += offset; + } + // Folia end - region ticking } } diff --git a/net/minecraft/world/level/block/SaplingBlock.java b/net/minecraft/world/level/block/SaplingBlock.java index e014f052e9b0f5ca6b28044e2389782b7d0e0cb8..cc9e253d3033d3e970891067329aa281e85464f7 100644 --- a/net/minecraft/world/level/block/SaplingBlock.java +++ b/net/minecraft/world/level/block/SaplingBlock.java @@ -26,7 +26,7 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { protected static final float AABB_OFFSET = 6.0F; protected static final VoxelShape SHAPE = Block.box(2.0, 0.0, 2.0, 14.0, 12.0, 14.0); protected final TreeGrower treeGrower; - public static org.bukkit.TreeType treeType; // CraftBukkit + public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // CraftBukkit // Folia - region threading @Override public MapCodec codec() { @@ -56,18 +56,19 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { level.setBlock(pos, state.cycle(STAGE), 4); } else { // CraftBukkit start - if (level.captureTreeGeneration) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + if (worldData.captureTreeGeneration) { // Folia - region threading this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); } else { - level.captureTreeGeneration = true; + worldData.captureTreeGeneration = true; // Folia - region threading this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); - level.captureTreeGeneration = false; - if (!level.capturedBlockStates.isEmpty()) { - org.bukkit.TreeType treeType = SaplingBlock.treeType; - SaplingBlock.treeType = null; + worldData.captureTreeGeneration = false; // Folia - region threading + if (!worldData.capturedBlockStates.isEmpty()) { // Folia - region threading + org.bukkit.TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading + SaplingBlock.treeTypeRT.set(null); // Folia - region threading org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level.getWorld()); - java.util.List blocks = new java.util.ArrayList<>(level.capturedBlockStates.values()); - level.capturedBlockStates.clear(); + java.util.List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading + worldData.capturedBlockStates.clear(); // Folia - region threading org.bukkit.event.world.StructureGrowEvent event = null; if (treeType != null) { event = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks); diff --git a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java index 722f2b9a24679e0fc67aae2cd27051f96f962efe..fb8c09b18ea4112cbbe6e93bf6b9804d79628d36 100644 --- a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +++ b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java @@ -50,7 +50,7 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { @Override protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { - if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks + if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + pos.hashCode()) % level.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 = level.getChunkIfLoaded(pos); if (cachedBlockChunk == null) { // Is this needed? diff --git a/net/minecraft/world/level/block/WitherSkullBlock.java b/net/minecraft/world/level/block/WitherSkullBlock.java index dc70aaa8d929c40c5f34c8facc1ad2bff4e98768..3ea53116725798a1eedb4802d6ebd7a32d8cccfd 100644 --- a/net/minecraft/world/level/block/WitherSkullBlock.java +++ b/net/minecraft/world/level/block/WitherSkullBlock.java @@ -51,7 +51,7 @@ public class WitherSkullBlock extends SkullBlock { } public static void checkSpawn(Level level, BlockPos pos, SkullBlockEntity blockEntity) { - if (level.captureBlockStates) return; // CraftBukkit + if (level.getCurrentWorldData().captureBlockStates) return; // CraftBukkit // Folia - region threading if (!level.isClientSide) { BlockState blockState = blockEntity.getBlockState(); boolean flag = blockState.is(Blocks.WITHER_SKELETON_SKULL) || blockState.is(Blocks.WITHER_SKELETON_WALL_SKULL); diff --git a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java index deef33d96db188cb297f04b581ab29e77e3716a9..413288e4a654b5ff8cc009b401d602731f63ec6d 100644 --- a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +++ b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java @@ -211,7 +211,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name } int i = blockEntity.levels; final int originalLevels = i; // Paper - OBFHELPER - if (level.getGameTime() % 80L == 0L) { + if (level.getRedstoneGameTime() % 80L == 0L) { // Folia - region threading if (!blockEntity.beamSections.isEmpty()) { blockEntity.levels = updateBase(level, x, y, z); } @@ -345,7 +345,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name list = level.getEntitiesOfClass(Player.class, aabb); // Diff from applyEffect } else { list = new java.util.ArrayList<>(); - for (final Player player : level.players()) { + for (final Player player : level.getLocalPlayers()) { // Folia - region threading if (!net.minecraft.world.entity.EntitySelector.NO_SPECTATORS.test(player)) continue; if (player.getBoundingBox().intersects(aabb)) { list.add(player); diff --git a/net/minecraft/world/level/block/entity/BlockEntity.java b/net/minecraft/world/level/block/entity/BlockEntity.java index 77618757c0e678532dbab814aceed83f7f1cd892..003e9db957023486278679803b313ce89d573587 100644 --- a/net/minecraft/world/level/block/entity/BlockEntity.java +++ b/net/minecraft/world/level/block/entity/BlockEntity.java @@ -26,7 +26,7 @@ import net.minecraft.world.level.block.state.BlockState; import org.slf4j.Logger; public abstract class BlockEntity { - static boolean ignoreBlockEntityUpdates; // Paper - Perf: Optimize Hoppers + static final ThreadLocal IGNORE_TILE_UPDATES = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading // CraftBukkit start - data containers private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); public org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer; @@ -40,6 +40,12 @@ public abstract class BlockEntity { private BlockState blockState; private DataComponentMap components = DataComponentMap.EMPTY; + // Folia start - region ticking + public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { + + } + // Folia end - region ticking + public BlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { this.type = type; this.worldPosition = pos.immutable(); @@ -197,7 +203,7 @@ public abstract class BlockEntity { public void setChanged() { if (this.level != null) { - if (ignoreBlockEntityUpdates) return; // Paper - Perf: Optimize Hoppers + if (IGNORE_TILE_UPDATES.get().booleanValue()) return; // Paper - Perf: Optimize Hoppers // Folia - region threading setChanged(this.level, this.worldPosition, this.blockState); } } diff --git a/net/minecraft/world/level/block/entity/CommandBlockEntity.java b/net/minecraft/world/level/block/entity/CommandBlockEntity.java index de75569d44855d9d6ec28cfee4403ecb6b45c4d3..c1e3a99a5fe917c728763be16c9a92d7252739a3 100644 --- a/net/minecraft/world/level/block/entity/CommandBlockEntity.java +++ b/net/minecraft/world/level/block/entity/CommandBlockEntity.java @@ -66,6 +66,13 @@ public class CommandBlockEntity extends BlockEntity { ); } + // 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/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java index 9d80625fc95e4968cf80492dc7ecf1fd27e585b8..2938c1d35d5b19cfe49e12607f1e1d9342114f2c 100644 --- a/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +++ b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java @@ -81,7 +81,7 @@ public class ConduitBlockEntity extends BlockEntity { public static void clientTick(Level level, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { blockEntity.tickCount++; - long gameTime = level.getGameTime(); + long gameTime = level.getRedstoneGameTime(); // Folia - region threading List list = blockEntity.effectBlocks; if (gameTime % 40L == 0L) { blockEntity.isActive = updateShape(level, pos, list); @@ -97,7 +97,7 @@ public class ConduitBlockEntity extends BlockEntity { public static void serverTick(Level level, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { blockEntity.tickCount++; - long gameTime = level.getGameTime(); + long gameTime = level.getRedstoneGameTime(); // Folia - region threading List list = blockEntity.effectBlocks; if (gameTime % 40L == 0L) { boolean flag = updateShape(level, pos, list); diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java index 5cd1326ad5d046c88b2b3449d610a78fa880b4cd..ae988c4910421fb720177178ef6136e595ae6946 100644 --- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java +++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java @@ -34,7 +34,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen private static final int[][] CACHED_SLOTS = new int[54][]; private NonNullList items = NonNullList.withSize(5, ItemStack.EMPTY); public int cooldownTime = -1; - private long tickedGameTime; + private long tickedGameTime = Long.MIN_VALUE; // Folia - region threading private Direction facing; // CraftBukkit start - add fields and methods @@ -67,6 +67,15 @@ 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 blockState) { super(BlockEntityType.HOPPER, pos, blockState); @@ -125,7 +134,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen public static void pushItemsTick(Level level, BlockPos pos, BlockState state, HopperBlockEntity blockEntity) { blockEntity.cooldownTime--; - blockEntity.tickedGameTime = level.getGameTime(); + blockEntity.tickedGameTime = level.getRedstoneGameTime(); // Folia - region threading if (!blockEntity.isOnCooldown()) { blockEntity.setCooldown(0); // Spigot start @@ -213,12 +222,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } // Paper start - Perf: Optimize Hoppers - public static boolean skipHopperEvents; - private static boolean skipPullModeEventFire; - private static boolean skipPushModeEventFire; + // 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); @@ -233,7 +241,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); @@ -263,13 +271,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); @@ -289,9 +298,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); } - ignoreBlockEntityUpdates = true; + IGNORE_TILE_UPDATES.set(true); // Folia - region threading container.setItem(i, origItemStack); - ignoreBlockEntityUpdates = false; + IGNORE_TILE_UPDATES.set(false); // Folia - region threading container.setChanged(); return true; } @@ -306,6 +315,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen @Nullable private static ItemStack callPushMoveEvent(Container destination, ItemStack itemStack, HopperBlockEntity hopper) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading final org.bukkit.inventory.Inventory destinationInventory = getInventory(destination); final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent( hopper.getOwner(false).getInventory(), @@ -315,7 +325,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen ); final boolean result = event.callEvent(); if (!event.calledGetItem && !event.calledSetItem) { - skipPushModeEventFire = true; + worldData.skipPushModeEventFire = true; // Folia - region threading } if (!result) { applyCooldown(hopper); @@ -331,6 +341,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 org.bukkit.inventory.Inventory sourceInventory = getInventory(container); final org.bukkit.inventory.Inventory destination = getInventory(hopper); @@ -338,7 +349,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, org.bukkit.craftbukkit.inventory.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) { applyCooldown(hopper); @@ -524,12 +535,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } public static boolean suckInItems(Level level, Hopper hopper) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading BlockPos blockPos = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ()); BlockState blockState = level.getBlockState(blockPos); Container sourceContainer = getSourceContainer(level, hopper, blockPos, blockState); if (sourceContainer != null) { Direction direction = Direction.DOWN; - skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers + worldData.skipPullModeEventFire = worldData.skipHopperEvents; // Paper - Perf: Optimize Hoppers // Folia - region threading for (int i : getSlots(sourceContainer, direction)) { if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction, level)) { // Spigot @@ -678,9 +690,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen stack = stack.split(destination.getMaxStackSize()); } // Spigot end - ignoreBlockEntityUpdates = true; // Paper - Perf: Optimize Hoppers + IGNORE_TILE_UPDATES.set(Boolean.TRUE); // Paper - Perf: Optimize Hoppers // Folia - region threading destination.setItem(slot, stack); - ignoreBlockEntityUpdates = 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 (canMergeItems(item, stack)) { diff --git a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java index 1638eccef431fb68775af624110f1968f0c6dabd..bd6693af6412fb08a28ca9a71d5c70d54f72c6e6 100644 --- a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +++ b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java @@ -43,9 +43,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi // Paper end - Fix NPE in SculkBloomEvent world access public static void serverTick(Level level, BlockPos pos, BlockState state, SculkCatalystBlockEntity sculkCatalyst) { - org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = sculkCatalyst.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(sculkCatalyst.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 sculkCatalyst.catalystListener.getSculkSpreader().updateCursors(level, pos, level.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/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java index 5bf39c542757bf97da8909b65c22786a8a30385a..61887e6b052bca715c90dff5d9cd657e0b3f6a78 100644 --- a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +++ b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java @@ -35,9 +35,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 blockState) { super(BlockEntityType.END_GATEWAY, pos, blockState); } @@ -129,6 +132,104 @@ 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.CallbackCompletable complete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); + + 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, + net.minecraft.world.level.portal.TeleportTransition.PostTeleportTransition post) { + // can we even teleport in this dimension? + if (portalTile.exitPortal == null && portalWorld.getTypeKey() != net.minecraft.world.level.dimension.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; + } + + // note: we handle the position from the TeleportTransition + net.minecraft.world.level.portal.TeleportTransition teleport = net.minecraft.world.level.block.EndGatewayBlock.getTeleportTransition( + portalWorld, toTeleport, Vec3.atCenterOf(teleportPos) + ); + + + if (isExactTeleport) { + // blind teleport + return toTeleport.teleportAsync( + teleport, net.minecraft.world.entity.Entity.TELEPORT_FLAG_LOAD_CHUNK | net.minecraft.world.entity.Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, + post == null ? null : (net.minecraft.world.entity.Entity teleportedEntity) -> { + 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( + teleport, 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 level, BlockPos pos) { if (this.exitPortal == null && level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { // CraftBukkit - work in alternate worlds @@ -174,6 +275,124 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { return findTallestBlock(level, blockPos, 16, true); } + // Folia start - region threading + private static void findOrCreateValidTeleportPosRegionThreading(ServerLevel world, BlockPos pos, + ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete) { + ca.spottedleaf.concurrentutil.completable.CallbackCompletable tentativeSelection = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); + + 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().lookup(Registries.CONFIGURED_FEATURE).flatMap((iregistry) -> { + return iregistry.get(EndFeatures.END_ISLAND); + }).ifPresent((holder_c) -> { + ((net.minecraft.world.level.levelgen.feature.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.util.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).above(GATEWAY_HEIGHT_ABOVE_SURFACE); + + // done + complete.complete(tpLoc); + } + ); + } + ); + }); + + // fire off chain + findExitPortalXZPosTentativeRegionThreading(world, pos, tentativeSelection); + } + + private static void findExitPortalXZPosTentativeRegionThreading(ServerLevel world, BlockPos pos, + ca.spottedleaf.concurrentutil.completable.CallbackCompletable 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.util.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.util.Priority.NORMAL, + (chunk) -> { + handle.run(); + } + ); + } + // Folia end - region threading + private static Vec3 findExitPortalXZPosTentative(ServerLevel level, BlockPos pos) { Vec3 vec3 = new Vec3(pos.getX(), 0.0, pos.getZ()).normalize(); int i = 1024; diff --git a/net/minecraft/world/level/block/entity/TickingBlockEntity.java b/net/minecraft/world/level/block/entity/TickingBlockEntity.java index 28e3b73507b988f7234cbf29c4024c88180d0aef..c8facee29ee08e0975528083f89b64f0b593957f 100644 --- a/net/minecraft/world/level/block/entity/TickingBlockEntity.java +++ b/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/net/minecraft/world/level/block/grower/TreeGrower.java b/net/minecraft/world/level/block/grower/TreeGrower.java index cf7311c507de09a8f89934e430b2201e8bdffe51..80de710b4e1528587b509e50bdd69983bcb608d0 100644 --- a/net/minecraft/world/level/block/grower/TreeGrower.java +++ b/net/minecraft/world/level/block/grower/TreeGrower.java @@ -203,56 +203,58 @@ public final class TreeGrower { // CraftBukkit start private void setTreeType(Holder> holder) { + org.bukkit.TreeType treeType; // Folia - region threading ResourceKey> treeFeature = holder.unwrapKey().get(); if (treeFeature == TreeFeatures.OAK || treeFeature == TreeFeatures.OAK_BEES_005) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TREE; + treeType = org.bukkit.TreeType.TREE; // Folia - region threading } else if (treeFeature == TreeFeatures.HUGE_RED_MUSHROOM) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_MUSHROOM; + treeType = org.bukkit.TreeType.RED_MUSHROOM; // Folia - region threading } else if (treeFeature == TreeFeatures.HUGE_BROWN_MUSHROOM) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BROWN_MUSHROOM; + treeType = org.bukkit.TreeType.BROWN_MUSHROOM; // Folia - region threading } else if (treeFeature == TreeFeatures.JUNGLE_TREE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.COCOA_TREE; + treeType = org.bukkit.TreeType.COCOA_TREE; // Folia - region threading } else if (treeFeature == TreeFeatures.JUNGLE_TREE_NO_VINE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SMALL_JUNGLE; + treeType = org.bukkit.TreeType.SMALL_JUNGLE; // Folia - region threading } else if (treeFeature == TreeFeatures.PINE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_REDWOOD; + treeType = org.bukkit.TreeType.TALL_REDWOOD; // Folia - region threading } else if (treeFeature == TreeFeatures.SPRUCE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD; + treeType = org.bukkit.TreeType.REDWOOD; // Folia - region threading } else if (treeFeature == TreeFeatures.ACACIA) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA; + treeType = org.bukkit.TreeType.ACACIA; // Folia - region threading } else if (treeFeature == TreeFeatures.BIRCH || treeFeature == TreeFeatures.BIRCH_BEES_005) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIRCH; + treeType = org.bukkit.TreeType.BIRCH; // Folia - region threading } else if (treeFeature == TreeFeatures.SUPER_BIRCH_BEES_0002) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_BIRCH; + treeType = org.bukkit.TreeType.TALL_BIRCH; // Folia - region threading } else if (treeFeature == TreeFeatures.SWAMP_OAK) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP; + treeType = org.bukkit.TreeType.SWAMP; // Folia - region threading } else if (treeFeature == TreeFeatures.FANCY_OAK || treeFeature == TreeFeatures.FANCY_OAK_BEES_005) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIG_TREE; + treeType = org.bukkit.TreeType.BIG_TREE; // Folia - region threading } else if (treeFeature == TreeFeatures.JUNGLE_BUSH) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE_BUSH; + treeType = org.bukkit.TreeType.JUNGLE_BUSH; // Folia - region threading } else if (treeFeature == TreeFeatures.DARK_OAK) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.DARK_OAK; + treeType = org.bukkit.TreeType.DARK_OAK; // Folia - region threading } else if (treeFeature == TreeFeatures.MEGA_SPRUCE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_REDWOOD; + treeType = org.bukkit.TreeType.MEGA_REDWOOD; // Folia - region threading } else if (treeFeature == TreeFeatures.MEGA_PINE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_PINE; + treeType = org.bukkit.TreeType.MEGA_PINE; // Folia - region threading } else if (treeFeature == TreeFeatures.MEGA_JUNGLE_TREE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE; + treeType = org.bukkit.TreeType.JUNGLE; // Folia - region threading } else if (treeFeature == TreeFeatures.AZALEA_TREE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA; + treeType = org.bukkit.TreeType.AZALEA; // Folia - region threading } else if (treeFeature == TreeFeatures.MANGROVE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE; + treeType = org.bukkit.TreeType.MANGROVE; // Folia - region threading } else if (treeFeature == TreeFeatures.TALL_MANGROVE) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_MANGROVE; + treeType = org.bukkit.TreeType.TALL_MANGROVE; // Folia - region threading } else if (treeFeature == TreeFeatures.CHERRY || treeFeature == TreeFeatures.CHERRY_BEES_005) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.CHERRY; + treeType = org.bukkit.TreeType.CHERRY; // Folia - region threading } else if (treeFeature == TreeFeatures.PALE_OAK || treeFeature == TreeFeatures.PALE_OAK_BONEMEAL) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK; + treeType = org.bukkit.TreeType.PALE_OAK; // Folia - region threading } else if (treeFeature == TreeFeatures.PALE_OAK_CREAKING) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; + treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; // Folia - region threading } else { throw new IllegalArgumentException("Unknown tree generator " + treeFeature); } + net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(treeType); // Folia - region threading } // CraftBukkit end } diff --git a/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/net/minecraft/world/level/block/piston/PistonBaseBlock.java index 41802482875bd4d4b505eb758740140de0db415a..aa7aefbc26db062e3ed731ca98229fa36a54b4ef 100644 --- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java +++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java @@ -139,7 +139,7 @@ public class PistonBaseBlock extends DirectionalBlock { && pistonMovingBlockEntity.isExtending() && ( pistonMovingBlockEntity.getProgress(0.0F) < 0.5F - || level.getGameTime() == pistonMovingBlockEntity.getLastTicked() + || level.getRedstoneGameTime() == pistonMovingBlockEntity.getLastTicked() // Folia - region threading || ((ServerLevel)level).isHandlingTick() )) { i = 2; diff --git a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java index ee2f8e8deb35059824b5730a1442f383dc79f01c..baf6322619bbe43ed136e01494fbf24e2f8e4604 100644 --- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java @@ -41,9 +41,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 blockState) { super(BlockEntityType.PISTON, pos, blockState); } @@ -150,8 +160,8 @@ public class PistonMovingBlockEntity extends BlockEntity { entity.setDeltaMovement(d1, d2, d3); // Paper - EAR items stuck 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; } @@ -292,7 +302,7 @@ public class PistonMovingBlockEntity extends BlockEntity { } public static void tick(Level level, BlockPos pos, BlockState state, PistonMovingBlockEntity blockEntity) { - blockEntity.lastTicked = level.getGameTime(); + blockEntity.lastTicked = level.getRedstoneGameTime(); // Folia - region threading blockEntity.progressO = blockEntity.progress; if (blockEntity.progressO >= 1.0F) { if (level.isClientSide && blockEntity.deathTicks < 5) { diff --git a/net/minecraft/world/level/border/WorldBorder.java b/net/minecraft/world/level/border/WorldBorder.java index 7249292e77b4a54f1f4f707c4dc55924c96dd23f..eb0d3cc606fb7bb06871ea61c240873ed7e67bc5 100644 --- a/net/minecraft/world/level/border/WorldBorder.java +++ b/net/minecraft/world/level/border/WorldBorder.java @@ -30,6 +30,8 @@ public class WorldBorder { public static final WorldBorder.Settings DEFAULT_SETTINGS = new WorldBorder.Settings(0.0, 0.0, 0.2, 5.0, 5, 15, 5.999997E7F, 0L, 0.0); public net.minecraft.server.level.ServerLevel world; // CraftBukkit + // Folia - region threading - TODO make this shit thread-safe + public boolean isWithinBounds(BlockPos pos) { return this.isWithinBounds(pos.getX(), pos.getZ()); } @@ -43,16 +45,14 @@ 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/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java index 6ed51cf42b5864194d671b5b56f5b9bdf0291dc0..b85c547f281c58bf45c9062d0b886cb4ff7b386b 100644 --- a/net/minecraft/world/level/chunk/ChunkGenerator.java +++ b/net/minecraft/world/level/chunk/ChunkGenerator.java @@ -327,7 +327,7 @@ public abstract class ChunkGenerator { } private static boolean tryAddReference(StructureManager structureManager, StructureStart structureStart) { - if (structureStart.canBeReferenced()) { + if (structureStart.tryReference()) { // Folia - region threading structureManager.addReference(structureStart); return true; } else { diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java index 761fdcd4a4e18f45547afd8edff44f61c6eeacb4..f83cfa85678d288ece2348aae41d315660095ad8 100644 --- a/net/minecraft/world/level/chunk/LevelChunk.java +++ b/net/minecraft/world/level/chunk/LevelChunk.java @@ -59,6 +59,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p public void tick() { } + // Folia start - region threading + @Override + public BlockEntity getTileEntity() { + return null; + } + // Folia end - region threading + @Override public boolean isRemoved() { return true; @@ -230,11 +237,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p @Override public void markUnsaved() { - boolean isUnsaved = this.isUnsaved(); - super.markUnsaved(); - if (!isUnsaved) { - this.unsavedListener.setUnsaved(this.chunkPos); - } + super.markUnsaved(); // Folia - region threading - unsavedListener is not really use } @Override @@ -360,6 +363,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p @Nullable public BlockState setBlockState(BlockPos pos, BlockState state, boolean isMoving, boolean doPlace) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, pos, "Updating block asynchronously"); // Folia - region threading // CraftBukkit end int y = pos.getY(); LevelChunkSection section = this.getSection(this.getSectionIndex(y)); @@ -395,7 +399,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p } boolean hasBlockEntity = blockState.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 blockState.onRemove(this.level, pos, state, isMoving); } else if (!blockState.is(block) && hasBlockEntity) { this.removeBlockEntity(pos); @@ -404,7 +408,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p if (!section.getBlockState(i, i1, i2).is(block)) { return null; } else { - if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // 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.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. // Folia - region threading state.onPlace(this.level, pos, blockState, isMoving); } @@ -459,7 +463,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p @Nullable public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) { // CraftBukkit start - BlockEntity blockEntity = this.level.capturedTileEntities.get(pos); + BlockEntity blockEntity = this.level.getCurrentWorldData().capturedTileEntities.get(pos); // Folia - region threading if (blockEntity == null) { blockEntity = this.blockEntities.get(pos); } @@ -646,13 +650,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)); @@ -678,7 +682,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; @@ -905,6 +909,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p this.ticker = ticker; } + // 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()) { @@ -983,6 +994,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p this.ticker = ticker; } + // 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(); diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java index 6b6aaeca14178b5b709e20ae13552d42217f15c0..950977f8d123f903630541ded35dd86a1889240f 100644 --- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java @@ -574,7 +574,7 @@ public record SerializableChunkData( } } - ChunkAccess.PackedTicks ticksForSerialization = chunk.getTicksForSerialization(level.getGameTime()); + ChunkAccess.PackedTicks ticksForSerialization = chunk.getTicksForSerialization(level.getRedstoneGameTime()); // Folia - region threading ShortList[] lists = Arrays.stream(chunk.getPostProcessing()) .map(list3 -> list3 != null ? new ShortArrayList(list3) : null) .toArray(ShortList[]::new); diff --git a/net/minecraft/world/level/dimension/end/EndDragonFight.java b/net/minecraft/world/level/dimension/end/EndDragonFight.java index 6e7e87c32734b3aae354bc34459e5f207da5c78f..2e156694b337760be986fdf1cbf863b0d896ef2d 100644 --- a/net/minecraft/world/level/dimension/end/EndDragonFight.java +++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java @@ -77,7 +77,7 @@ public class EndDragonFight { .setPlayBossMusic(true) .setCreateWorldFog(true); public final ServerLevel level; - private final BlockPos origin; + public final BlockPos origin; // Folia - region threading public final ObjectArrayList gateways = new ObjectArrayList<>(); private final BlockPattern exitPortalPattern; private int ticksSinceDragonSeen; @@ -162,7 +162,7 @@ public class EndDragonFight { if (!this.dragonEvent.getPlayers().isEmpty()) { this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE); - boolean isArenaLoaded = this.isArenaLoaded(); + boolean isArenaLoaded = this.isArenaLoaded(); if (!isArenaLoaded) { return; } // Folia - region threading - don't tick if we don't own the entire region if (this.needsStateScanning && isArenaLoaded) { this.scanState(); this.needsStateScanning = false; @@ -208,6 +208,12 @@ public class EndDragonFight { } List dragons = this.level.getDragons(); + // Folia start - region threading + // we do not want to deal with any dragons NOT nearby + dragons.removeIf((EnderDragon dragon) -> { + return !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(dragon); + }); + // Folia end - region threading if (dragons.isEmpty()) { this.dragonKilled = true; } else { @@ -323,8 +329,8 @@ public class EndDragonFight { for (int i = -8 + chunkPos.x; i <= 8 + chunkPos.x; i++) { for (int i1 = 8 + chunkPos.z; i1 <= 8 + chunkPos.z; i1++) { - ChunkAccess chunk = this.level.getChunk(i, i1, ChunkStatus.FULL, false); - if (!(chunk instanceof LevelChunk)) { + ChunkAccess chunk = this.level.getChunkIfLoaded(i, i1); // Folia - region threading + if (!(chunk instanceof LevelChunk) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, i, i1, this.level.regioniser.regionSectionChunkSize)) { return false; } @@ -496,6 +502,11 @@ public class EndDragonFight { } public void onCrystalDestroyed(EndCrystal crystal, DamageSource dmgSrc) { + // 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(crystal)) { LOGGER.debug("Aborting respawn sequence"); this.respawnStage = null; @@ -521,7 +532,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 blockPos = this.portalLocation; if (blockPos == null) { LOGGER.debug("Tried to respawn, but need to find the portal first."); diff --git a/net/minecraft/world/level/levelgen/PatrolSpawner.java b/net/minecraft/world/level/levelgen/PatrolSpawner.java index 082c9b340765e3e98055a3c4444af68264a54826..9608e06c56f0aded4d6b4e9cf3d7eec348945600 100644 --- a/net/minecraft/world/level/levelgen/PatrolSpawner.java +++ b/net/minecraft/world/level/levelgen/PatrolSpawner.java @@ -16,7 +16,7 @@ import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.state.BlockState; public class PatrolSpawner implements CustomSpawner { - private int nextTick; + //private int nextTick; // Folia - region threading @Override public int tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { @@ -27,6 +27,7 @@ public class PatrolSpawner implements CustomSpawner { return 0; } else { RandomSource randomSource = level.random; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading // this.nextTick--; // if (this.nextTick > 0) { // return 0; @@ -38,12 +39,12 @@ public class PatrolSpawner implements CustomSpawner { // } else if (randomSource.nextInt(5) != 0) { // Paper start - Pillager patrol spawn settings and per player options // Random player selection moved up for per player spawning and configuration - int size = level.players().size(); + int size = level.getLocalPlayers().size(); if (size < 1) { return 0; } - net.minecraft.server.level.ServerPlayer player = level.players().get(randomSource.nextInt(size)); + net.minecraft.server.level.ServerPlayer player = level.getLocalPlayers().get(randomSource.nextInt(size)); // Folia - region threading if (player.isSpectator()) { return 0; } @@ -53,8 +54,8 @@ public class PatrolSpawner implements CustomSpawner { --player.patrolSpawnDelay; patrolSpawnDelay = player.patrolSpawnDelay; } else { - this.nextTick--; - patrolSpawnDelay = this.nextTick; + worldData.patrolSpawnerNextTick--; // Folia - region threading + patrolSpawnDelay = worldData.patrolSpawnerNextTick; // Folia - region threading } if (patrolSpawnDelay > 0) { return 0; @@ -68,7 +69,7 @@ public class PatrolSpawner implements CustomSpawner { if (level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) { player.patrolSpawnDelay += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); } else { - this.nextTick += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); + worldData.patrolSpawnerNextTick += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); // Folia - region threading } if (days < level.paperConfig().entities.behavior.pillagerPatrols.start.day || !level.isDay()) { diff --git a/net/minecraft/world/level/levelgen/PhantomSpawner.java b/net/minecraft/world/level/levelgen/PhantomSpawner.java index 11d25e64349b27bf54dc1620e4cce444c79f581c..cef0474cf5f95bff717d49e58fe0a74ce6b7b345 100644 --- a/net/minecraft/world/level/levelgen/PhantomSpawner.java +++ b/net/minecraft/world/level/levelgen/PhantomSpawner.java @@ -19,7 +19,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.FluidState; public class PhantomSpawner implements CustomSpawner { - private int nextTick; + //private int nextTick; // Folia - region threading @Override public int tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { @@ -34,21 +34,22 @@ public class PhantomSpawner implements CustomSpawner { } // Paper end - Ability to control player's insomnia and phantoms RandomSource randomSource = level.random; - this.nextTick--; - if (this.nextTick > 0) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.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 = level.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds; int spawnAttemptMaxSeconds = level.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 (level.getSkyDarken() < 5 && level.dimensionType().hasSkyLight()) { return 0; } else { int i = 0; - for (ServerPlayer serverPlayer : level.players()) { + for (ServerPlayer serverPlayer : level.getLocalPlayers()) { // Folia - region threading if (!serverPlayer.isSpectator() && (!level.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !serverPlayer.isCreative())) { // Paper - Add phantom creative and insomniac controls BlockPos blockPos = serverPlayer.blockPosition(); if (!level.dimensionType().hasSkyLight() || blockPos.getY() >= level.getSeaLevel() && level.canSeeSky(blockPos)) { diff --git a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java index 1f7005b01b56929fb694b69b37143b8d8c7b2898..f96fc1391167dea48cac1caa464b9026657df89a 100644 --- a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java +++ b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java @@ -47,7 +47,7 @@ public class EndPlatformFeature extends Feature { // CraftBukkit start // SPIGOT-7746: Entity will only be null during world generation, which is async, so just generate without event - if (entity != null) { + if (false) { // Folia - region threading org.bukkit.World bworld = level.getLevel().getWorld(); org.bukkit.event.world.PortalCreateEvent portalEvent = new org.bukkit.event.world.PortalCreateEvent((java.util.List) (java.util.List) blockList.getList(), bworld, entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM); level.getLevel().getCraftServer().getPluginManager().callEvent(portalEvent); diff --git a/net/minecraft/world/level/levelgen/structure/StructureStart.java b/net/minecraft/world/level/levelgen/structure/StructureStart.java index 4dafa79dd4ec55a443ba3731a79e7cd6e8052f48..743b13693c8ef1d69751de42e9c6dadefe56395c 100644 --- a/net/minecraft/world/level/levelgen/structure/StructureStart.java +++ b/net/minecraft/world/level/levelgen/structure/StructureStart.java @@ -26,7 +26,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; @@ -39,7 +39,7 @@ public final class StructureStart { public StructureStart(Structure structure, ChunkPos chunkPos, int references, PiecesContainer pieceContainer) { this.structure = structure; this.chunkPos = chunkPos; - this.references = references; + this.references = new java.util.concurrent.atomic.AtomicInteger(references); // Folia - region threading this.pieceContainer = pieceContainer; } @@ -126,7 +126,7 @@ public final class StructureStart { compoundTag.putString("id", context.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(this.structure).toString()); compoundTag.putInt("ChunkX", chunkPos.x); compoundTag.putInt("ChunkZ", chunkPos.z); - compoundTag.putInt("references", this.references); + compoundTag.putInt("references", this.references.get()); // Folia - region threading compoundTag.put("Children", this.pieceContainer.save(context)); return compoundTag; } else { @@ -144,15 +144,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/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java index 028eae2f9a459b60e92f3344091083aa93b54485..e7ea9df8f404a6176435204a91edeefab8070c89 100644 --- a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +++ b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java @@ -47,6 +47,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { } private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates updates) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((net.minecraft.server.level.ServerLevel)this.level, pos, "Adding block without owning region"); // Folia - region threading boolean flag = this.count > 0; boolean flag1 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; this.count++; diff --git a/net/minecraft/world/level/saveddata/SavedData.java b/net/minecraft/world/level/saveddata/SavedData.java index b681a5ca1c4215d5afcc988c169e22a84996a88d..3879127f6c4a7977176bcea7ccc21561210addc6 100644 --- a/net/minecraft/world/level/saveddata/SavedData.java +++ b/net/minecraft/world/level/saveddata/SavedData.java @@ -8,7 +8,7 @@ import net.minecraft.nbt.NbtUtils; import net.minecraft.util.datafix.DataFixTypes; public abstract class SavedData { - private boolean dirty; + private volatile boolean dirty; // Folia - make map data thread-safe public abstract CompoundTag save(CompoundTag tag, HolderLookup.Provider registries); @@ -26,9 +26,10 @@ public abstract class SavedData { public CompoundTag save(HolderLookup.Provider registries) { CompoundTag compoundTag = new CompoundTag(); + this.setDirty(false); // Folia - make map data thread-safe - move before save, so that any changes after are not lost compoundTag.put("data", this.save(new CompoundTag(), registries)); NbtUtils.addCurrentDataVersion(compoundTag); - this.setDirty(false); + // Folia - make map data thread-safe - move before save, so that any changes after are not lost return compoundTag; } diff --git a/net/minecraft/world/level/saveddata/maps/MapIndex.java b/net/minecraft/world/level/saveddata/maps/MapIndex.java index ffe604f8397a002800e6ecc2f878d0f6f1c98703..7ee324c32efe1e63d310120e468a2f0d8ca262b4 100644 --- a/net/minecraft/world/level/saveddata/maps/MapIndex.java +++ b/net/minecraft/world/level/saveddata/maps/MapIndex.java @@ -34,17 +34,21 @@ public class MapIndex extends SavedData { @Override public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { + synchronized (this.usedAuxIds) { // Folia - make map data thread-safe for (Entry entry : this.usedAuxIds.object2IntEntrySet()) { tag.putInt(entry.getKey(), entry.getIntValue()); } + } // Folia - make map data thread-safe return tag; } 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/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java index 3c1c89aade5ff092b880ba1bf1de83f54d3d62cc..439d850053c35ba92ccd8ffbd177c6b9b75f00db 100644 --- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java @@ -201,7 +201,7 @@ public class MapItemSavedData extends SavedData { } @Override - public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { + public synchronized CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { // Folia - make map data thread-safe ResourceLocation.CODEC .encodeStart(NbtOps.INSTANCE, this.dimension.location()) .resultOrPartial(LOGGER::error) @@ -244,7 +244,7 @@ public class MapItemSavedData extends SavedData { return tag; } - public MapItemSavedData locked() { + public synchronized MapItemSavedData locked() { // Folia - make map data thread-safe MapItemSavedData mapItemSavedData = new MapItemSavedData( this.centerX, this.centerZ, this.scale, this.trackingPosition, this.unlimitedTracking, true, this.dimension ); @@ -255,7 +255,7 @@ public class MapItemSavedData extends SavedData { return mapItemSavedData; } - public MapItemSavedData scaled() { + public synchronized MapItemSavedData scaled() { // Folia - make map data thread-safe return createFresh(this.centerX, this.centerZ, (byte)Mth.clamp(this.scale + 1, 0, 4), this.trackingPosition, this.unlimitedTracking, this.dimension); } @@ -264,7 +264,8 @@ public class MapItemSavedData extends SavedData { return itemStack -> itemStack == stack || itemStack.is(stack.getItem()) && Objects.equals(mapId, itemStack.get(DataComponents.MAP_ID)); } - public void tickCarriedBy(Player player, ItemStack mapStack) { + public synchronized void tickCarriedBy(Player player, ItemStack mapStack) { // 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 holdingPlayer = new MapItemSavedData.HoldingPlayer(player); this.carriedByPlayers.put(player, holdingPlayer); @@ -413,7 +414,7 @@ public class MapItemSavedData extends SavedData { private byte calculateRotation(@Nullable LevelAccessor level, double yRot) { if (this.dimension == Level.NETHER && level != null) { - int i = (int)(level.getLevelData().getDayTime() / 10L); + int i = (int)(level.dayTime() / 10L); // Folia - region threading return (byte)(i * i * 34187121 + i * 121 >> 15 & 15); } else { double d = yRot < 0.0 ? yRot - 8.0 : yRot + 8.0; @@ -447,25 +448,27 @@ 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 holdingPlayer = this.carriedByPlayers.get(player); return holdingPlayer == null ? null : holdingPlayer.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 + //this.setDirty(); // Folia - make dirty only after updating data - moved down for (MapItemSavedData.HoldingPlayer holdingPlayer : this.carriedBy) { holdingPlayer.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 + //this.setDirty(); // 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 holdingPlayer = this.carriedByPlayers.get(player); if (holdingPlayer == null) { holdingPlayer = new MapItemSavedData.HoldingPlayer(player); @@ -476,7 +479,7 @@ public class MapItemSavedData extends SavedData { return holdingPlayer; } - public boolean toggleBanner(LevelAccessor accessor, BlockPos pos) { + public synchronized boolean toggleBanner(LevelAccessor accessor, BlockPos pos) { // Folia - make map data thread-safe double d = pos.getX() + 0.5; double d1 = pos.getZ() + 0.5; int i = 1 << this.scale; @@ -484,7 +487,7 @@ public class MapItemSavedData extends SavedData { double d3 = (d1 - this.centerZ) / i; int i1 = 63; if (d2 >= -63.0 && d3 >= -63.0 && d2 <= 63.0 && d3 <= 63.0) { - MapBanner mapBanner = MapBanner.fromWorld(accessor, pos); + MapBanner mapBanner = accessor.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(accessor.getMinecraftWorld(), pos) ? null : MapBanner.fromWorld(accessor, pos); // Folia - make map data thread-safe - don't sync load or read data we do not own if (mapBanner == null) { return false; } @@ -504,7 +507,7 @@ public class MapItemSavedData extends SavedData { return false; } - public void checkBanners(BlockGetter reader, int x, int z) { + public synchronized void checkBanners(BlockGetter reader, int x, int z) { // Folia - make map data thread-safe Iterator iterator = this.bannerMarkers.values().iterator(); while (iterator.hasNext()) { @@ -523,13 +526,13 @@ public class MapItemSavedData extends SavedData { return this.bannerMarkers.values(); } - public void removedFromFrame(BlockPos pos, int entityId) { + public synchronized void removedFromFrame(BlockPos pos, int entityId) { // Folia - make map data thread-safe this.removeDecoration(getFrameKey(entityId)); this.frameMarkers.remove(MapFrame.frameId(pos)); this.setDirty(); } - 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 b = this.colors[x + z * 128]; if (b != color) { this.setColor(x, z, color); @@ -539,12 +542,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 for (MapDecoration mapDecoration : this.decorations.values()) { if (mapDecoration.type().value().explorationMapElement()) { return true; @@ -554,7 +557,7 @@ public class MapItemSavedData extends SavedData { return false; } - public void addClientSideDecorations(List decorations) { + public synchronized void addClientSideDecorations(List decorations) { // Folia - make map data thread-safe this.decorations.clear(); this.trackedDecorationCount = 0; @@ -571,7 +574,7 @@ public class MapItemSavedData extends SavedData { return this.decorations.values(); } - public boolean isTrackedCountOverLimit(int trackedCount) { + public synchronized boolean isTrackedCountOverLimit(int trackedCount) { // Folia - make map data thread-safe return this.trackedDecorationCount >= trackedCount; } @@ -726,11 +729,13 @@ public class MapItemSavedData extends SavedData { } public void applyToMap(MapItemSavedData savedData) { + synchronized (savedData) { // Folia - make map data thread-safe for (int i = 0; i < this.width; i++) { for (int i1 = 0; i1 < this.height; i1++) { savedData.setColor(this.startX + i, this.startY + i1, this.mapColors[i + i1 * this.width]); } } + } // Folia - make map data thread-safe } } } diff --git a/net/minecraft/world/level/storage/DimensionDataStorage.java b/net/minecraft/world/level/storage/DimensionDataStorage.java index d9a3b5a2e6495b7e22c114506c2bd1e406f58f8f..ab572ac18fd02306210c87eb9ba5e5d4197ff997 100644 --- a/net/minecraft/world/level/storage/DimensionDataStorage.java +++ b/net/minecraft/world/level/storage/DimensionDataStorage.java @@ -51,6 +51,7 @@ public class DimensionDataStorage implements AutoCloseable { } public T computeIfAbsent(SavedData.Factory factory, String name) { + synchronized (this.cache) { // Folia - make map data thread-safe T savedData = this.get(factory, name); if (savedData != null) { return savedData; @@ -59,10 +60,12 @@ public class DimensionDataStorage implements AutoCloseable { this.set(name, savedData1); return savedData1; } + } // Folia - make map data thread-safe } @Nullable public T get(SavedData.Factory factory, String name) { + synchronized (this.cache) { // Folia - make map data thread-safe Optional optional = this.cache.get(name); if (optional == null) { optional = Optional.ofNullable(this.readSavedData(factory.deserializer(), factory.type(), name)); @@ -70,6 +73,7 @@ public class DimensionDataStorage implements AutoCloseable { } return (T)optional.orElse(null); + } // Folia - make map data thread-safe } @Nullable @@ -88,8 +92,10 @@ public class DimensionDataStorage implements AutoCloseable { } public void set(String name, SavedData savedData) { + synchronized (this.cache) { // Folia - make map data thread-safe this.cache.put(name, Optional.of(savedData)); savedData.setDirty(); + } // Folia - make map data thread-safe } public CompoundTag readTagFromDisk(String filename, DataFixTypes dataFixType, int version) throws IOException { diff --git a/net/minecraft/world/ticks/LevelChunkTicks.java b/net/minecraft/world/ticks/LevelChunkTicks.java index faf45ac459f7c25309d6ef6dce371d484a0dae7b..8a98064f2e44b27947c1af9c80ae0d7a397db7e4 100644 --- a/net/minecraft/world/ticks/LevelChunkTicks.java +++ b/net/minecraft/world/ticks/LevelChunkTicks.java @@ -48,6 +48,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/net/minecraft/world/ticks/LevelTicks.java b/net/minecraft/world/ticks/LevelTicks.java index 66abc2e7adee60fa98eed1ba36e018814fd02cad..2caedf1c12e5a388f7b14989310a2137bc1117c3 100644 --- a/net/minecraft/world/ticks/LevelTicks.java +++ b/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 = (levelChunkTicks, scheduledTick) -> { - if (scheduledTick.equals(levelChunkTicks.peek())) { - this.updateContainerScheduling(scheduledTick); + if (scheduledTick.equals(levelChunkTicks.peek())) { // Folia - diff on change + this.updateContainerScheduling(scheduledTick); // Folia - diff on change } }; - public LevelTicks(LongPredicate tickCheck) { + // 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 tickCheck, net.minecraft.server.level.ServerLevel world, boolean isBlock) { this.world = world; this.isBlock = isBlock; // Folia - add world and isBlock this.tickCheck = tickCheck; } @@ -56,7 +113,17 @@ public class LevelTicks implements LevelTickAccess { this.nextTickForContainer.put(packedChunkPos, scheduledTick.triggerTick()); } - chunkTicks.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 + chunkTicks.setOnTickAdded((LevelChunkTicks levelChunkTicks, ScheduledTick tick) -> { + if (tick.equals(levelChunkTicks.peek())) { + io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); + ((LevelTicks)(isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks())).updateContainerScheduling(tick); + } + }); + // Folia end - region threading } public void removeContainer(ChunkPos chunkPos) { @@ -70,6 +137,7 @@ public class LevelTicks implements LevelTickAccess { @Override public void schedule(ScheduledTick tick) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, tick.pos(), "Cannot schedule tick for another region!"); // Folia - region threading long packedChunkPos = ChunkPos.asLong(tick.pos()); LevelChunkTicks levelChunkTicks = this.allContainers.get(packedChunkPos); if (levelChunkTicks == null) {