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 42899a91b9061b85985fbbd0de4032c757f4aa8f..d9fd093f59987f471627ff20cfd2eb1a3c0dea64 100644 --- a/ca/spottedleaf/moonrise/paper/PaperHooks.java +++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java @@ -108,7 +108,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); } } @@ -130,7 +130,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; diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java index 975a2e4ea1f84e81fa3526395b43cb8157a06243..bd3ce123652af11974be4cbf8d2e96f1b2ee0a68 100644 --- a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java +++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java @@ -76,18 +76,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 ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener; + final 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 @@ -98,13 +103,13 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co @Override public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add(chunk); + chunk.getLevel().getCurrentWorldData().addChunk(chunk); // Folia - region threading chunk.loadCallback(); } @Override public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove(chunk); + chunk.getLevel().getCurrentWorldData().removeChunk(chunk); // Folia - region threading chunk.unloadCallback(); } @@ -116,7 +121,7 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co @Override public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add(chunk); + chunk.getLevel().getCurrentWorldData().addTickingChunk(chunk); // Folia - region threading if (!((ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { chunk.postProcessGeneration((ServerLevel)chunk.getLevel()); } @@ -126,18 +131,18 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co @Override public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove(chunk); + chunk.getLevel().getCurrentWorldData().removeTickingChunk(chunk); // Folia - region threading } @Override public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add(chunk); + chunk.getLevel().getCurrentWorldData().addEntityTickingChunk(chunk); // Folia - region threading ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$markChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration } @Override public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { - ((ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove(chunk); + chunk.getLevel().getCurrentWorldData().removeEntityTickingChunk(chunk); // Folia - region threading ((ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration } 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 2d24d03bbdb5ee0d862cbfff2219f58afffafe12..20941198b3a1227381c0a9039898a6506344a588 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java @@ -464,6 +464,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(); @@ -990,6 +1003,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 82824ae7ffbced513a8bcace684af94916135e84..e3a49083e75116622bd95fb17533730a6fb36752 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 @@ -22,14 +22,13 @@ 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 // Vanilla does not increment ticket timeouts if the chunk is progressing in generation. They made this change in 1.21.6 so that the ender pearl // ticket does not expire if the chunk fails to generate before the timeout expires. Rather than blindly adjusting the entire system behavior // to fix this small issue, we instead add non-expirable tickets here to keep ender pearls ticking. This is how the original feature should have // been implemented, but I don't think Vanilla has proper entity add/remove hooks like we do. Fixes MC-297591 - private static final TicketType ENDER_PEARL_TICKER = ChunkSystemTicketType.create("chunk_system:ender_pearl_ticker", null); - private final Long2IntOpenHashMap enderPearlChunkCount = new Long2IntOpenHashMap(); + private static final TicketType ENDER_PEARL_TICKER = ChunkSystemTicketType.create("chunk_system:ender_pearl_ticker", Integer::compareTo); // Folia - region threading private final boolean keepEnderPearlsTicking; public ServerEntityLookup(final ServerLevel world, final LevelCallback worldCallback) { @@ -78,8 +77,8 @@ public final class ServerEntityLookup extends EntityLookup { ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().tickPlayer(player); } if (entity instanceof ThrownEnderpearl enderpearl && (oldSectionX != newSectionX || oldSectionZ != newSectionZ)) { - this.removeEnderPearl(CoordinateUtils.getChunkKey(oldSectionX, oldSectionZ)); - this.addEnderPearl(CoordinateUtils.getChunkKey(newSectionX, newSectionZ)); + this.removeEnderPearl(CoordinateUtils.getChunkKey(oldSectionX, oldSectionZ), enderpearl.getId()); // Folia - region threading + this.addEnderPearl(CoordinateUtils.getChunkKey(newSectionX, newSectionZ), enderpearl.getId()); // Folia - region threading } PlatformHooks.get().entityMove( entity, @@ -90,11 +89,12 @@ public final class ServerEntityLookup extends EntityLookup { @Override protected void addEntityCallback(final Entity entity) { + this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading if (entity instanceof ServerPlayer player) { ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player); } if (entity instanceof ThrownEnderpearl enderpearl) { - this.addEnderPearl(CoordinateUtils.getChunkKey(enderpearl.chunkPosition())); + this.addEnderPearl(CoordinateUtils.getChunkKey(enderpearl.chunkPosition()), enderpearl.getId()); // Folia - region threading } entity.registerScheduler(); // Paper - optimise Folia entity scheduler } @@ -105,21 +105,22 @@ public final class ServerEntityLookup extends EntityLookup { ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().removePlayer(player); } if (entity instanceof ThrownEnderpearl enderpearl) { - this.removeEnderPearl(CoordinateUtils.getChunkKey(enderpearl.chunkPosition())); + this.removeEnderPearl(CoordinateUtils.getChunkKey(enderpearl.chunkPosition()), enderpearl.getId()); // Folia - region threading } + entity.getBukkitEntity().taskScheduler.registerTo(null); // Folia - region threading } @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 } @@ -138,30 +139,22 @@ public final class ServerEntityLookup extends EntityLookup { return PlatformHooks.get().screenEntity(this.serverWorld, entity, fromDisk, event); } - private void addEnderPearl(final long coordinate) { + private void addEnderPearl(final long coordinate, final int entityId) { // Folia - region threading if (!this.keepEnderPearlsTicking) { return; } - - final int oldCount = this.enderPearlChunkCount.addTo(coordinate, 1); - if (oldCount != 0) { - return; - } + // Folia - region threading ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager - .addTicketAtLevel(ENDER_PEARL_TICKER, coordinate, ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL, null); + .addTicketAtLevel(ENDER_PEARL_TICKER, coordinate, ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL, Integer.valueOf(entityId)); // Folia - region threading } - private void removeEnderPearl(final long coordinate) { + private void removeEnderPearl(final long coordinate, final int entityId) { // Folia - region threading if (!this.keepEnderPearlsTicking) { return; } - final int oldCount = this.enderPearlChunkCount.addTo(coordinate, -1); - if (oldCount != 1) { - return; - } - this.enderPearlChunkCount.remove(coordinate); + // Folia - region threading ((ChunkSystemServerLevel)this.serverWorld).moonrise$getChunkTaskScheduler().chunkHolderManager - .removeTicketAtLevel(ENDER_PEARL_TICKER, coordinate, ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL, null); + .removeTicketAtLevel(ENDER_PEARL_TICKER, coordinate, ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL, Integer.valueOf(entityId)); // Folia - region threading } } diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java index bdc1200ef5317fdaf58973bf580b0a672aee800f..cb75568c79380f4c38fe3b4a15da2df6342a8e27 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java @@ -218,7 +218,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(); @@ -306,7 +306,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 6ce4a98e4d3b633e3c87944c23b6b3f0ff58f159..396b72a20ec48831705935f2f4149307068f9bfa 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java @@ -59,6 +59,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(); @@ -80,30 +88,83 @@ public final class ChunkHolderManager { private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); private final ServerLevel world; private final ChunkTaskScheduler taskScheduler; - private long currentTick; + // Folia start - region threading + public static final class HolderManagerRegionData { + private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); + private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { + if (c1 == c2) { + return 0; + } + + final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); + + if (saveTickCompare != 0) { + return saveTickCompare; + } + + final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); + final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); + + if (coord1 == coord2) { + throw new IllegalStateException("Duplicate chunkholder in auto save queue"); + } + + return Long.compare(coord1, coord2); + }); - private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); - private final MultiThreadedQueue offThreadPendingFullLoadUpdate = new MultiThreadedQueue<>(); - private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { - if (c1 == c2) { - return 0; + public void merge(final HolderManagerRegionData into, final long tickOffset) { + // Order doesn't really matter for the pending full update... + into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate); + + // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating + // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant + // addition to every entry will not affect compareTo). + for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) { + holder.lastAutoSave += tickOffset; + into.autoSaveQueue.add(holder); + } } - final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); + public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, + final ReferenceOpenHashSet dataSet) { + for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) { + final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift; + final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift; + + final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); + if (data != null) { + data.pendingFullLoadUpdate.add(fullLoadUpdate); + } // else: fullLoadUpdate is an unloaded chunk holder + } + + for (final NewChunkHolder autoSave : this.autoSaveQueue) { + final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift; + final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift; - if (saveTickCompare != 0) { - return saveTickCompare; + final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); + if (data != null) { + data.autoSaveQueue.add(autoSave); + } // else: autoSave is an unloaded chunk holder + } } + } - final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); - final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); + private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() { + final ThreadedRegionizer.ThreadedRegion region = + TickRegionScheduler.getCurrentRegion(); - if (coord1 == coord2) { - throw new IllegalStateException("Duplicate chunkholder in auto save queue"); + if (region == null) { + return null; } - return Long.compare(coord1, coord2); - }); + if (this.world != null && this.world != region.getData().world) { + throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); + } + + return region.getData().getHolderManagerRegionData(); + } + // Folia end - region threading + private final ConcurrentLong2ReferenceChainedHashTable ticketCounters = new ConcurrentLong2ReferenceChainedHashTable<>(); @@ -189,8 +250,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) + "'"); @@ -200,9 +266,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) { @@ -224,28 +291,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) { @@ -259,15 +333,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) + "'"); } @@ -296,6 +393,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) { @@ -331,7 +434,7 @@ public final class ChunkHolderManager { } } } - if (flush) { + if (last && flush) { // Folia - region threading MoonriseRegionFileIO.flush(this.world); try { MoonriseRegionFileIO.flushRegionStorages(this.world); @@ -819,7 +922,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(); @@ -836,7 +945,7 @@ public final class ChunkHolderManager { final List scheduledTasks = new ArrayList<>(); final List changedFullStatus = new ArrayList<>(); - 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)) { @@ -1142,21 +1251,55 @@ public final class ChunkHolderManager { if (changedFullStatus.isEmpty()) { return; } - if (!TickThread.isTickThread()) { - // These will be handled on the next ServerChunkCache$MainThreadExecutor#pollTask, as it runs the distance manager update - // which will invoke processTicketUpdates - this.offThreadPendingFullLoadUpdate.addAll(changedFullStatus); - } 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)); } @@ -1169,7 +1312,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 @@ -1471,22 +1614,17 @@ public final class ChunkHolderManager { return CURRENT_TICKET_UPDATE_SCHEDULING.get(); } - // only call on tick thread - private void processOffThreadFullUpdates() { - final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; - final MultiThreadedQueue offThreadPendingFullLoadUpdate = this.offThreadPendingFullLoadUpdate; - - NewChunkHolder toUpdate; - while ((toUpdate = offThreadPendingFullLoadUpdate.poll()) != null) { - pendingFullLoadUpdate.add(toUpdate); - } - } + // Folia - region threading // only call on tick thread private boolean processPendingFullUpdate() { - this.processOffThreadFullUpdates(); - - 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; @@ -1497,9 +1635,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 8f8268924ac92fca5df8a11e08031fa8416c6e05..65716aa20fec95533249ba39bbf10dd570c7c64f 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java @@ -123,7 +123,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; @@ -338,14 +338,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) { @@ -830,7 +829,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) { @@ -839,7 +838,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) { @@ -848,9 +847,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 2cc0e7c72d2b2e562452138f2b41fd1dcaf0570a..c098e74c7c2f0eec6e5b97582a705a34a1183180 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java @@ -1361,10 +1361,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); }); } @@ -1385,7 +1385,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); @@ -1413,7 +1413,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 01cd38bba2deb6cf65c82b4e4ec352a2998fd339..864ce0bd822a2bd6fb39866103dc54b56bf0b63c 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 ae2bb9a73106febfe5f0d090abd4252bbb5fd27e..c0d23b0b5adca79ce451da8cca16dc3aeac2c799 100644 --- a/io/papermc/paper/entity/activation/ActivationRange.java +++ b/io/papermc/paper/entity/activation/ActivationRange.java @@ -54,33 +54,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. @@ -128,10 +129,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); @@ -141,30 +143,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 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 Marker) { continue; } - ActivationRange.activateEntity(entity); + ActivationRange.activateEntity(entity, bbByType); // Folia - threaded regions } } } @@ -174,14 +183,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 } } } @@ -195,6 +204,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; @@ -202,10 +212,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; } @@ -302,16 +312,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..e1842ab8f20271a8b20ab8462b09a4745a612a9a --- /dev/null +++ b/io/papermc/paper/threadedregions/RegionizedServer.java @@ -0,0 +1,469 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.completable.Completable; +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); + } + + public void blockOn(final Runnable run) { + if (isGlobalTickThread()) { + run.run(); + return; + } + Completable.suppliedAsync(() -> { + run.run(); + return null; + }, this::addTask).join(); + return; + } + + /** + * 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.ADVENTURE_CLICK_MANAGER.handleQueue((int)this.tickCount); // Paper // Folia - region threading - moved to global tick + io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.DIALOG_CLICK_MANAGER.handleQueue((int)this.tickCount); // Paper // Folia - region threading - moved to global tick + + // 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 != null; + } + + 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..25a0b1d56652e4455a188d0f413581638d5ef538 --- /dev/null +++ b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java @@ -0,0 +1,816 @@ +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 ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicketType; +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 = ChunkSystemTicketType.create("task_queue_ticket", Long::compareTo); + + 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, final long id) { + this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( + TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Long.valueOf(id) + ); + } + + private void addTicket(final long coord, final long id) { + this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( + TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Long.valueOf(id) + ); + } + + 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, referenceCountData.id); + this.processTicketUpdates(coord); + referenceCountData.addedTicket = true; + } + } + + private void decrementReference(final ReferenceCountData referenceCountData, final long coord) { + if (!referenceCountData.decreaseReferenceCount()) { + return; + } // else: need to remove ticket + + final ReferenceCountData[] toRemoveTicket = new ReferenceCountData[1]; + + // 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 + toRemoveTicket[0] = valueInMap; + + return null; + }); + + if (toRemoveTicket[0] != null) { + this.removeTicket(coord, toRemoveTicket[0].id); + } + } + + 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 { + + private static final AtomicLong ID_GENERATOR = new AtomicLong(); + + private final long id = ID_GENERATOR.getAndIncrement(); + + 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..01ea2e7133a41f1a75169c7e3edbd528b268412b --- /dev/null +++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java @@ -0,0 +1,771 @@ +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); + entity.getBukkitEntity().taskScheduler.registerTo(into.entitySchedulerTickList); + } + 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); + } + entity.getBukkitEntity().taskScheduler.registerTo(into.entitySchedulerTickList); + } + // 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 LevelChunk chunk = iterator.next(); + final ChunkPos pos = 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(chunk); + } + for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) { + final LevelChunk chunk = iterator.next(); + final ChunkPos pos = 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(chunk); + } + for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) { + final LevelChunk chunk = iterator.next(); + final ChunkPos pos = 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(chunk); + } + + // 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; + public final io.papermc.paper.threadedregions.EntityScheduler.EntitySchedulerTickList entitySchedulerTickList = new io.papermc.paper.threadedregions.EntityScheduler.EntitySchedulerTickList(); + 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 LevelChunk[] EMPTY_CHUNK_ARRAY = new LevelChunk[0]; + private final ReferenceList entityTickingChunks = new ReferenceList<>(EMPTY_CHUNK_ARRAY); + private final ReferenceList tickingChunks = new ReferenceList<>(EMPTY_CHUNK_ARRAY); + private final ReferenceList chunks = new ReferenceList<>(EMPTY_CHUNK_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 captureBlockStates = false; + public boolean captureTreeGeneration = false; + 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.level().moonrise$getChunkTaskScheduler().chunkHolderManager.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 LevelChunk chunk) { + this.entityTickingChunks.add(chunk); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void removeEntityTickingChunk(final LevelChunk chunk) { + this.entityTickingChunks.remove(chunk); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public ReferenceList getEntityTickingChunks() { + return this.entityTickingChunks; + } + + public void addTickingChunk(final LevelChunk chunk) { + this.tickingChunks.add(chunk); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void removeTickingChunk(final LevelChunk chunk) { + this.tickingChunks.remove(chunk); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public ReferenceList getTickingChunks() { + return this.tickingChunks; + } + + public void addChunk(final LevelChunk chunk) { + this.chunks.add(chunk); + TickRegions.RegionStats.updateCurrentRegion(); + } + + public void removeChunk(final LevelChunk chunk) { + this.chunks.remove(chunk); + 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..503342372f21966dab862e194b030c5951e9f023 --- /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.threadId()) : 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..81cdbcf00efca209b0567ea10d54d1a3add681d7 --- /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, null + ); + } + + 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, null + ); + } + + 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 3acfb2a78845dd8081dc3c01d653034232c76e60..8c005a7b9480da7c124c5bd5bfccd5ca83abbcb1 100644 --- a/net/minecraft/commands/CommandSourceStack.java +++ b/net/minecraft/commands/CommandSourceStack.java @@ -92,7 +92,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 ec1cced129ef42be65d7b2b622638bfae8bd895e..ad19e9e599b26dfdb53929d9410860cb1b97698c 100644 --- a/net/minecraft/commands/Commands.java +++ b/net/minecraft/commands/Commands.java @@ -185,15 +185,15 @@ 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, context); - DebugCommand.register(this.dispatcher); + //DataCommands.register(this.dispatcher); // Folia - region threading - TODO + //DataPackCommand.register(this.dispatcher, context); // Folia - region threading - TODO + //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO DefaultGameModeCommands.register(this.dispatcher); - DialogCommand.register(this.dispatcher, context); + //DialogCommand.register(this.dispatcher, context); // Folia - region threading - TODO DifficultyCommand.register(this.dispatcher); EffectCommands.register(this.dispatcher, context); EmoteCommands.register(this.dispatcher); @@ -202,50 +202,50 @@ 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); VersionCommand.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); - TestCommand.register(this.dispatcher, context); - TickCommand.register(this.dispatcher); + //TestCommand.register(this.dispatcher, context); // Folia - region threading + //TickCommand.register(this.dispatcher); // Folia - region threading - TODO later TimeCommand.register(this.dispatcher); TitleCommand.register(this.dispatcher, context); - TriggerCommand.register(this.dispatcher); - WaypointCommand.register(this.dispatcher, context); + //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later + //WaypointCommand.register(this.dispatcher, context); // Folia - region threading - TODO later WeatherCommand.register(this.dispatcher); WorldBorderCommand.register(this.dispatcher); if (JvmProfiler.INSTANCE.isAvailable()) { @@ -272,8 +272,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); @@ -496,9 +496,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/DispenseItemBehavior.java b/net/minecraft/core/dispenser/DispenseItemBehavior.java index ca02c4c71a0a5a1a0ae8bbb40f0b1b7eac64e6fd..8a147ec57497318851ebabcea220bc8ac4ab3543 100644 --- a/net/minecraft/core/dispenser/DispenseItemBehavior.java +++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java @@ -392,20 +392,21 @@ public interface DispenseItemBehavior { return result; } // Paper end - Call BlockDispenseEvent - level.captureTreeGeneration = true; // CraftBukkit + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + worldData.captureTreeGeneration = true; // CraftBukkit // Folia - region threading if (!BoneMealItem.growCrop(item, level, blockPos) && !BoneMealItem.growWaterPlant(item, level, blockPos, null)) { this.setSuccess(false); } else if (!level.isClientSide) { level.levelEvent(1505, blockPos, 15); } // CraftBukkit start - level.captureTreeGeneration = false; - if (!level.capturedBlockStates.isEmpty()) { - 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.isEmpty()) { // 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 states = new java.util.ArrayList<>(level.capturedBlockStates.values()); - level.capturedBlockStates.clear(); + List states = 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, states); diff --git a/net/minecraft/gametest/framework/GameTestHelper.java b/net/minecraft/gametest/framework/GameTestHelper.java index 9fb53bc1330bf5370769200599b022a985f88db2..0c3874f3bb1f0de81db81f394979bd4759542157 100644 --- a/net/minecraft/gametest/framework/GameTestHelper.java +++ b/net/minecraft/gametest/framework/GameTestHelper.java @@ -314,7 +314,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 340d5487fa778277b9560250271c5143d80d9987..fa4c69f13ba73b463c45195487c13ea2adde55c9 100644 --- a/net/minecraft/gametest/framework/GameTestServer.java +++ b/net/minecraft/gametest/framework/GameTestServer.java @@ -203,8 +203,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 41005f6b9f53a9c1125ad5e08538d9f5aacb3571..1626b932a5e46ee77465ef08c2f4430c2e2753c1 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; @@ -156,6 +156,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 details) {} + + 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 details) { + this.disconnectReqs.add(new DisconnectReq(details)); + // 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, io.papermc.paper.connection.DisconnectionReason cause) { + this.disconnectReqs.add(new DisconnectReq(new DisconnectionDetails(disconnectReason, java.util.Optional.empty(), java.util.Optional.empty(), java.util.Optional.empty(), java.util.Optional.ofNullable(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); @@ -165,6 +200,7 @@ public class Connection extends SimpleChannelInboundHandler> { if (this.delayedDisconnect != null) { this.disconnect(this.delayedDisconnect); } + this.becomeActive = true; // Folia - region threading } @Override @@ -432,7 +468,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, channelFutureListener, flag); @@ -461,11 +497,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 } } @@ -510,10 +547,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 } } @@ -527,53 +565,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 @@ -582,17 +628,36 @@ 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.details.reason()); + // this doesn't fail, so abort any further attempts + return; + } else if (packetlistener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { + commonPacketListener.disconnect(disconnectReq.details); + // may be cancelled by a plugin, if not cancelled then any further calls do nothing + continue; + } else { + this.disconnect(disconnectReq.details); + 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 { @@ -603,7 +668,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(); } @@ -654,6 +719,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() { @@ -845,10 +911,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) { @@ -877,6 +943,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 } } } @@ -896,15 +977,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..4a7de2ed6eabe919f0c33de49ed7fab75abac1b4 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,19 @@ 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.queuePacketTask(run); + } 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 9243bb11e3f968d0bf0eb2e3dc9295c0232bc15d..f63a2ee9445d6738fde1db21753aa13ced03efd1 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 + serverLevel.randomSpawnSelection.x, currZ + serverLevel.randomSpawnSelection.z); + serverLevel.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( + net.minecraft.server.level.TicketType.UNKNOWN, pos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, null + ); + } + } + // 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) { final ServerLevel serverLevel1 = serverLevel; @@ -890,7 +929,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) { @@ -1405,28 +1499,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; @@ -1446,6 +1536,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 { @@ -1517,24 +1611,56 @@ 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 + for (io.papermc.paper.threadedregions.EntityScheduler scheduler : region.world.getCurrentWorldData().entitySchedulerTickList.getAllSchedulers()) { + net.minecraft.world.entity.Entity handle = scheduler.entity.getHandleRaw(); + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(handle) || scheduler.isRetired()) { + continue; + } + + scheduler.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; @@ -1542,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 { @@ -1560,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()); @@ -1655,43 +1760,35 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop serverPlayer1.connection.suspendFlushing()); - this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit - // Paper start - optimise Folia entity scheduler - ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler)org.bukkit.Bukkit.getGlobalRegionScheduler()).tick(); - for (io.papermc.paper.threadedregions.EntityScheduler scheduler : this.entitySchedulerTickList.getAllSchedulers()) { - if (scheduler.isRetired()) { - continue; - } - - scheduler.executeTick(); - } - // Paper end - optimise Folia entity scheduler - io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.ADVENTURE_CLICK_MANAGER.handleQueue(this.tickCount); // Paper - io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.DIALOG_CLICK_MANAGER.handleQueue(this.tickCount); // Paper + //this.getPlayerList().getPlayers().forEach(serverPlayer1 -> serverPlayer1.connection.suspendFlushing()); // Folia - region threading + //this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit // Folia - region threading + // Folia - region threading - moved to global tick - and moved entity scheduler to tickRegion + //io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.ADVENTURE_CLICK_MANAGER.handleQueue(this.tickCount); // Paper // Folia - region threading - moved to global tick + //io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.DIALOG_CLICK_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; @@ -1704,12 +1801,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) { @@ -1722,7 +1816,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().invalidateStatus(); + }); + return; + } + // Folia end - region threading this.lastServerStatus = 0L; } @@ -2146,6 +2249,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 2d3e0ddb81b1ec6b08ceac29df4f05c1d01cfb53..d797f6bbb591fec5ecc000ff176fbfaa6a3b6eae 100644 --- a/net/minecraft/server/commands/AdvancementCommands.java +++ b/net/minecraft/server/commands/AdvancementCommands.java @@ -257,7 +257,12 @@ public class AdvancementCommands { int i = 0; for (ServerPlayer serverPlayer : targets) { - i += action.perform(serverPlayer, advancements, grantEverything); + // Folia start - region threading + i += 1; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + action.perform(player, advancements, grantEverything); + }, null, 1L); + // Folia end - region threading } if (i == 0) { @@ -321,9 +326,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 15279a43b064affe59dbb77ae4ef88368e0db85a..b5827b10024a428cc4ff7ec64f51e01bbd91fb32 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 b1866fdf0bacee9ce7ef1fcad4484df5c0d4c640..379001c4d52c356edd93c4297730e091dc7d5747 100644 --- a/net/minecraft/server/commands/ClearInventoryCommands.java +++ b/net/minecraft/server/commands/ClearInventoryCommands.java @@ -73,9 +73,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 b72273361be8c8c12354265f54ddc8810fe20896..b782cdb258a529de1fead944f8c7396f247d09fd 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 84baf29ad8af6a8af0cd00ce3921b3c5b8b89451..c2ab47bb97c4fbbbc48a8ff0538ab8fd38532a88 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 09ca06f6901e01845d193e77bc78a754aa23a093..aab7f73a1827c63ec3ffe6544a2470469034362f 100644 --- a/net/minecraft/server/commands/EffectCommands.java +++ b/net/minecraft/server/commands/EffectCommands.java @@ -182,7 +182,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++; } } @@ -212,7 +217,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++; } } @@ -237,7 +247,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 6f4fcee9fdba2248227708fcced1ce0f40aff6af..21781ab5ad7d31504f1bdbb4aae53a4a982e9156 100644 --- a/net/minecraft/server/commands/EnchantCommand.java +++ b/net/minecraft/server/commands/EnchantCommand.java @@ -68,50 +68,77 @@ 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) { - 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 { + 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 9b33678b109c227ec74c8a5a2204e7f73ff483c4..01a79199133ab4a800b4c89a0f672369ae9ff87c 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 ef0b95adc075004236369ebe4ac915f7568025de..c34fb96e837bbb781b94358456b89934f9d35d5f 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, holder -> true, supplier -> {}); } + // 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 dc808636a07f2fdec0108ff89de3b0773940e123..7665bff921eda626c0f1498a4ca03b62483d10ed 100644 --- a/net/minecraft/server/commands/FillCommand.java +++ b/net/minecraft/server/commands/FillCommand.java @@ -171,6 +171,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 box, BlockInput block, FillCommand.Mode mode, @Nullable Predicate filter, boolean strict ) throws CommandSyntaxException { @@ -187,6 +193,18 @@ public class FillCommand { if (level.isDebug()) { throw ERROR_FAILED.create(); } else { + // Folia start - region threading + int buffer = 32; + // physics may spill into neighbour chunks, so use a buffer + level.moonrise$loadChunksAsync( + (box.minX() - buffer) >> 4, + (box.maxX() + buffer) >> 4, + (box.minZ() - buffer) >> 4, + (box.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(box.minX(), box.minY(), box.minZ(), box.maxX(), box.maxY(), box.maxZ())) { @@ -225,8 +243,12 @@ 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 4f57ddb80f93183c17280b366473cd466af8cce9..60438a36e9e0b56277db2450ae4d47a365c110e4 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 forceLoadedChunks = level.getForceLoadedChunks(); int size = forceLoadedChunks.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 forceLoadedChunks = level.getForceLoadedChunks(); forceLoadedChunks.forEach(l -> level.setChunkForced(ChunkPos.getX(l), ChunkPos.getZ(l), 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 9da9ab3b835a8dcc7e1da286be1de88a3f0240fa..c7ea2441e99c3aedd403da04cbc350685b9fcc64 100644 --- a/net/minecraft/server/commands/GameModeCommand.java +++ b/net/minecraft/server/commands/GameModeCommand.java @@ -56,9 +56,11 @@ public class GameModeCommand { int i = 0; for (ServerPlayer serverPlayer : players) { - if (setGameMode(source.getSource(), serverPlayer, gameType)) { - i++; + serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer nmsEntity) -> { // Folia - region threading + if (setGameMode(source.getSource(), nmsEntity, gameType)) { // Folia - region threading + //i++; // Folia - region threading } + }, null, 1L); // Folia - region threading } return i; diff --git a/net/minecraft/server/commands/GiveCommand.java b/net/minecraft/server/commands/GiveCommand.java index 4f56cdcf658234b1ca92d38c3df3d31e9be34a15..f7002d6752aa03603eb06e298ba1d7130cf13826 100644 --- a/net/minecraft/server/commands/GiveCommand.java +++ b/net/minecraft/server/commands/GiveCommand.java @@ -68,32 +68,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, null); // Paper - do not fire PlayerDropItemEvent for /give command + ItemEntity itemEntity = nmsEntity.drop(itemStack, false, false, false, null); // Paper - do not fire PlayerDropItemEvent for /give command // 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, false, false, null); // Paper - do not fire PlayerDropItemEvent for /give command + ItemEntity itemEntity = nmsEntity.drop(itemStack1, false, false, false, null); // Paper - do not fire PlayerDropItemEvent for /give command // 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 f1a41d873c2eb2f519b0cd6adfcd9ef675e65195..5810f573e96908a83adf26a3a0487d644a782c24 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 1f59c08d92394273a485d008f9a83dd544f2de26..b36558c29d12862bf700872ba3db31b7cdfa6c1f 100644 --- a/net/minecraft/server/commands/PlaceCommand.java +++ b/net/minecraft/server/commands/PlaceCommand.java @@ -254,36 +254,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(), @@ -326,14 +369,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, boolean strict ) 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; @@ -363,9 +421,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 31219be4ffde609ddcdfce992b551300c91a4845..02bc043f8c333c898eedbe1108ed7fdc8ac37f44 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 cbd8e00b829ea4520fc626220a2a44dd22cd2668..07cff4e1e7af98cf3ead1f255557c1a994072610 100644 --- a/net/minecraft/server/commands/SetBlockCommand.java +++ b/net/minecraft/server/commands/SetBlockCommand.java @@ -96,10 +96,21 @@ public class SetBlockCommand { ); } + // Folia start - region threading + private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { + src.sendFailure((Component)ex.getRawMessage()); + } + // Folia end - region threading + private static int setBlock( CommandSourceStack source, BlockPos pos, BlockInput block, SetBlockCommand.Mode mode, @Nullable Predicate filter, boolean strict ) 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 (level.isDebug()) { throw ERROR_FAILED.create(); } else if (filter != null && !filter.test(new BlockInWorld(level, pos, true))) { @@ -122,9 +133,16 @@ public class SetBlockCommand { } 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 static enum Mode { diff --git a/net/minecraft/server/commands/SetSpawnCommand.java b/net/minecraft/server/commands/SetSpawnCommand.java index f037a8ceff0bb7dada06e98049a93a7d00593def..b8282d2a567cc18bb53c7660d55940fc4cf3987d 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(new ServerPlayer.RespawnConfig(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(new ServerPlayer.RespawnConfig(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 3d3d766b376891bdfb2d47a26284f805d0b01b92..2289f7383d85de023e062fe5c5ae37e9570e5c74 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 6d8ac5f80d5267a2159a464ec331e9b3c65313e2..178a67b24095af02e7f75e8d80abdfc7b462639a 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 ba4dffb47b01e83748f7c4cd396f6d959b658d51..b11b1ad2b67340c2d07bf01531b8d459526f572b 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 7559b915a8d9e38dc44a4578ac33209f29ffa84a..a1ac3fee95a2239781f09304be197cbb899f01ac 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 8e5a491b3bc5b8689bec7504c34dff4f9f883f6e..b003b63e61508623d445e72ac18d948067ac6c66 100644 --- a/net/minecraft/server/commands/WorldBorderCommand.java +++ b/net/minecraft/server/commands/WorldBorderCommand.java @@ -135,18 +135,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(); @@ -155,39 +176,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(); @@ -199,13 +260,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) { @@ -235,7 +307,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 98927d4a5fba2a0dcdb147ac10b82c3286ccdc6b..d8bab0ba6e2374fb74f3fa9f7db802f442240414 100644 --- a/net/minecraft/server/dedicated/DedicatedServer.java +++ b/net/minecraft/server/dedicated/DedicatedServer.java @@ -383,7 +383,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface @Override public void tickConnection() { super.tickConnection(); - this.handleConsoleInputs(); + // Folia - region threading } @Override @@ -690,7 +690,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); @@ -698,7 +699,16 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface return; } this.getCommands().performPrefixedCommand(wrapper, event.getCommand()); - }); + }; // 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 eb352aa4296abc3ed4cf31c590bc0be66daf4de3..40ad7be72795b7539dbebf54770e364cf58e0ead 100644 --- a/net/minecraft/server/level/ChunkMap.java +++ b/net/minecraft/server/level/ChunkMap.java @@ -133,8 +133,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; @@ -690,7 +690,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper start - optimise chunk tick iteration 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.ChunkSystemLevelChunk)levelChunk).moonrise$getChunkHolder().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; } @@ -725,7 +725,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper end - optimise chunk tick iteration void collectSpawningChunks(List output) { - final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); + final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = this.level.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading final LevelChunk[] raw = tickingChunks.getRawDataUnchecked(); final int size = tickingChunks.size(); @@ -859,12 +859,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 } @@ -872,9 +872,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 } @@ -892,27 +892,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 } @@ -942,9 +928,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 @@ -955,22 +941,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); } @@ -986,12 +978,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(); } @@ -1000,9 +999,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]; @@ -1010,7 +1011,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(); @@ -1028,44 +1029,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); } @@ -1301,8 +1276,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 50bc5b940812432bc472e5b272582efb8bbfc7a7..619cbb1a9296c814bbdad0780df91c8db01bc5cd 100644 --- a/net/minecraft/server/level/DistanceManager.java +++ b/net/minecraft/server/level/DistanceManager.java @@ -53,18 +53,18 @@ 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 // Note: Cannot do narrow tracking on Paper due to custom spawn range @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 // Note: Cannot do narrow tracking on Paper due to custom spawn range } @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 // Note: Cannot do narrow tracking on Paper due to custom spawn range } @@ -73,10 +73,10 @@ 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 // Note: Cannot do narrow tracking on Paper due to custom spawn range } 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 // Note: Cannot do narrow tracking on Paper due to custom spawn range } } @@ -160,12 +160,12 @@ 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 TriState hasPlayersNearby(long chunkPos) { // Note: Cannot do narrow tracking on Paper due to custom spawn range // Paper - chunk tick iteration optimisation - return this.spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) ? net.minecraft.util.TriState.DEFAULT : net.minecraft.util.TriState.FALSE; // 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)) ? net.minecraft.util.TriState.DEFAULT : net.minecraft.util.TriState.FALSE; // Paper - chunk tick iteration optimisation // Folia - region threading } public void forEachEntityTickingChunk(LongConsumer action) { @@ -184,7 +184,7 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches } 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 2882cd829d4d8e1f8615f085f6908efcdf68ac62..c79da00ff0428c32cdfb8cea2411ff8b8ecaaa00 100644 --- a/net/minecraft/server/level/ServerChunkCache.java +++ b/net/minecraft/server/level/ServerChunkCache.java @@ -65,18 +65,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon public final ChunkMap chunkMap; private final DimensionDataStorage dataStorage; private final TicketStorage ticketStorage; - 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 spawningChunks = new ObjectArrayList<>(); - private final Set chunkHoldersToBroadcast = new ReferenceOpenHashSet<>(); - @Nullable - @VisibleForDebug - private NaturalSpawner.SpawnState lastSpawnState; + // Folia - moved to regionised world data // Paper start public final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); public int getFullChunksCount() { @@ -102,6 +98,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( @@ -163,7 +164,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon // TODO check on update: impl of forEachBlockTickingChunk will only iterate ENTITY ticking chunks! // TODO check on update: consumer just runs tickChunk - final ca.spottedleaf.moonrise.common.list.ReferenceList entityTickingChunks = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getEntityTickingChunks(); + final ca.spottedleaf.moonrise.common.list.ReferenceList entityTickingChunks = world.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading // note: we can use the backing array here because: // 1. we do not care about new additions @@ -360,6 +361,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) { @@ -507,9 +509,10 @@ 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"); @@ -525,27 +528,29 @@ 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 tickChunks(ProfilerFiller profiler, long timeInhabited) { + io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threadin 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; @@ -558,32 +563,32 @@ 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(); } - List list = this.spawningChunks; + List list = regionizedWorldData.temporaryChunkTickList; // Folia - region threading try { profiler.push("filteringSpawningChunks"); @@ -667,18 +672,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 addTicket(Ticket ticket, ChunkPos chunkPos) { @@ -772,7 +782,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 deactivateTicketsOnClosing() { @@ -781,7 +792,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 } } @@ -815,20 +826,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 dda8d38ef61672cc714d9e5a475f9b0412ed5ff9..cd6f937fa4eaba6033bfc230e3483b6b9e1ae607 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -177,12 +177,12 @@ 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 private final ServerWaypointManager waypointManager; // Paper - rewrite chunk system private final GameEventDispatcher gameEventDispatcher; @@ -190,30 +190,28 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe 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 java.util.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 @Override public @Nullable LevelChunk getChunkIfLoaded(int x, int z) { @@ -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 LevelChunk[] EMPTY_LEVEL_CHUNKS = new LevelChunk[0]; - private final ca.spottedleaf.moonrise.common.list.ReferenceList loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_LEVEL_CHUNKS); - private final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_LEVEL_CHUNKS); - private final ca.spottedleaf.moonrise.common.list.ReferenceList entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_LEVEL_CHUNKS); + // 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; + throw new UnsupportedOperationException(); // Folia - region threading } @Override public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getTickingChunks() { - return this.tickingChunks; + throw new UnsupportedOperationException(); // Folia - region threading } @Override public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getEntityTickingChunks() { - return this.entityTickingChunks; + throw new UnsupportedOperationException(); // Folia - region threading } @Override @@ -494,74 +495,31 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } // Paper end - rewrite chunk system // Paper start - chunk tick iteration - private final ca.spottedleaf.moonrise.common.list.ReferenceList playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_LEVEL_CHUNKS); - 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(chunk); + // Folia - region threading } @Override public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) { - this.playerTickingChunks.remove(chunk); + // 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((LevelChunk)chunkHolder.getCurrentChunk()); + // 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"); - - final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); - final int val = this.playerTickingRequests.addTo(chunkKey, -1); - - if (val <= 0) { - throw new IllegalStateException("Negative counter"); - } - - if (val != 1) { - // still has at least one request - 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.remove((LevelChunk)chunkHolder.getCurrentChunk()); + // Folia - region threading } // Paper end - chunk tick iteration @@ -628,7 +586,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.getType(this.dimensionTypeRegistration())); @@ -675,8 +633,67 @@ 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 final io.papermc.paper.threadedregions.TickRegions tickRegions = new io.papermc.paper.threadedregions.TickRegions(); + public final io.papermc.paper.threadedregions.ThreadedRegionizer regioniser; + { + this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegionizer<>( + (int)Math.max(1L, (8L * 16L * 16L) / (1L << (2 * (io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())))), + (1.0 / 6.0), + Math.max(1, 8 / (1 << io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())), + 1, + io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(), + this, + this.tickRegions + ); + } + public final io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData(this); + public static final int WORLD_INIT_NOT_CHECKED = 0; + public static final int WORLD_INIT_CHECKING = 1; + public static final int WORLD_INIT_CHECKED = 2; + public final java.util.concurrent.atomic.AtomicInteger checkInitialised = new java.util.concurrent.atomic.AtomicInteger(WORLD_INIT_NOT_CHECKED); + public ChunkPos randomSpawnSelection; + + public static final record PendingTeleport(Entity.EntityTreeNode rootVehicle, Vec3 to) {} + private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet pendingTeleports = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); + + public void pushPendingTeleport(final PendingTeleport teleport) { + synchronized (this.pendingTeleports) { + this.pendingTeleports.add(teleport); + } + } + + public boolean removePendingTeleport(final PendingTeleport teleport) { + synchronized (this.pendingTeleports) { + return this.pendingTeleports.remove(teleport); + } } + public List removeAllRegionTeleports() { + final List ret = new ArrayList<>(); + + synchronized (this.pendingTeleports) { + for (final 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(); + } + } + } + + return ret; + } + // Folia end - region threading + // 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 public boolean hasChunk(int chunkX, int chunkZ) { @@ -703,61 +720,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 - } + this.tickSleep(); // Folia - region threading - move into tickSleep - 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.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(); } @@ -773,9 +768,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.getForceLoadedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this + boolean flag = true || !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForceLoadedChunks().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(); } @@ -783,19 +778,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.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( + TicketType.UNKNOWN, fightCenter, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, + null + ); + } // 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(); @@ -824,6 +829,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 @@ -834,12 +869,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); } } @@ -857,16 +893,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(); @@ -913,7 +960,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(); int minBlockX = pos.getMinBlockX(); int minBlockZ = pos.getMinBlockZ(); @@ -1047,7 +1094,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() { @@ -1073,6 +1120,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(); } @@ -1087,7 +1142,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe return this.waypointManager; } - 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)) { @@ -1173,7 +1228,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(); } @@ -1181,13 +1237,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); } @@ -1248,13 +1304,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 @@ -1262,9 +1315,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(); @@ -1275,7 +1326,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(); @@ -1284,9 +1344,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 } @@ -1294,7 +1352,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 @@ -1304,7 +1362,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(); @@ -1392,19 +1459,20 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } // Paper end - add close param - // CraftBukkit start - moved from MinecraftServer#saveAllChunks - ServerLevel serverLevel1 = this; - - this.serverLevelData.setWorldBorder(serverLevel1.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 serverLevel1 = this; + + this.serverLevelData.setWorldBorder(serverLevel1.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) { @@ -1460,6 +1528,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); @@ -1541,8 +1622,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 @@ -1733,13 +1814,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); @@ -1747,7 +1829,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); @@ -1764,13 +1847,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 @@ -1779,33 +1862,33 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public void updateNeighborsAt(BlockPos pos, Block block) { // CraftBukkit start - if (this.populating) { + if (this.getCurrentWorldData().populating) { // Folia - region threading return; } // CraftBukkit end - 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 @@ -1895,7 +1978,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)); @@ -1911,14 +1994,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 @@ -1934,11 +2020,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) { @@ -1948,12 +2034,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 @@ -2006,7 +2092,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, @@ -2084,12 +2170,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 @@ -2144,6 +2230,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(MapItemSavedData.type(mapId)); if (cacheEntry == null) { // Cache did not contain, try to load and may init final MapItemSavedData mapData = storage.get(MapItemSavedData.type(mapId)); // get populates the cache @@ -2162,6 +2249,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } return null; + } // Folia - region threading // Paper end - Call missing map initialize event and set id } @@ -2215,7 +2303,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe public boolean setChunkForced(int chunkX, int chunkZ, boolean add) { boolean flag = this.chunkSource.updateChunkForced(new ChunkPos(chunkX, chunkZ), add); if (add && flag) { - this.getChunk(chunkX, chunkZ); + //this.getChunk(chunkX, chunkZ); // Folia - region threading - we must let the chunk load asynchronously } return flag; @@ -2232,11 +2320,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe Optional> optional1 = PoiTypes.forState(newState); if (!Objects.equals(optional, optional1)) { BlockPos blockPos = pos.immutable(); - optional.ifPresent(holder -> this.getServer().execute(() -> { + optional.ifPresent(holder -> io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this, blockPos.getX() >> 4, blockPos.getZ() >> 4, () -> { // Folia - region threading this.getPoiManager().remove(blockPos); DebugPackets.sendPoiRemovedPacket(this, blockPos); })); - optional1.ifPresent(holder -> this.getServer().execute(() -> { + optional1.ifPresent(holder -> io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this, blockPos.getX() >> 4, blockPos.getZ() >> 4, () -> { // Folia - region threading // Paper start - Remove stale POIs if (optional.isEmpty() && this.getPoiManager().exists(blockPos, ignored -> true)) { this.getPoiManager().remove(blockPos); @@ -2274,7 +2362,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Nullable public Raid getRaidAt(BlockPos pos) { - return this.raids.getNearbyRaid(pos, 9216); + return this.raids.getNearbyRaid(this, pos, 9216); // Folia - make raids thread-safe - add ServerLevel param } public boolean isRaided(BlockPos pos) { @@ -2298,7 +2386,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"); @@ -2368,7 +2456,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()); } @@ -2376,7 +2464,7 @@ 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 @@ -2420,8 +2508,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() @@ -2473,15 +2561,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 } public void waitForChunkAndEntities(ChunkPos chunkPos, int i) { @@ -2617,7 +2705,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) @@ -2636,18 +2724,18 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe ServerLevel.this.getWaypointManager().untrackWaypoint(waypointTransmitter); } - 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.setOwner(null); @@ -2658,6 +2746,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); @@ -2680,12 +2769,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 } } @@ -2708,18 +2797,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 } } } @@ -2751,18 +2849,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); @@ -2790,11 +2889,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 8f94c963f2c59668d72d162f46f7505d6a6b06a5..d9a1884ea073579ae0b90192d9da0bee9f95095f 100644 --- a/net/minecraft/server/level/ServerPlayer.java +++ b/net/minecraft/server/level/ServerPlayer.java @@ -195,7 +195,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; @@ -478,8 +478,155 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc // CraftBukkit end } + // Folia start - region threading + private static final int SPAWN_RADIUS_SELECTION_SEARCH = 5; + + private static BlockPos getRandomSpawn(ServerLevel world, RandomSource random) { + BlockPos spawn = world.getSharedSpawnPos(); + double radius = (double)Math.max(0, world.getGameRules().getInt(GameRules.RULE_SPAWN_RADIUS)); + + double spawnX = (double)spawn.getX() + 0.5; + double spawnZ = (double)spawn.getZ() + 0.5; + + net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder(); + + double selectMinX = Math.max(worldBorder.getMinX() + 1.0, spawnX - radius); + double selectMinZ = Math.max(worldBorder.getMinZ() + 1.0, spawnZ - radius); + double selectMaxX = Math.min(worldBorder.getMaxX() - 1.0, spawnX + radius); + double selectMaxZ = Math.min(worldBorder.getMaxZ() - 1.0, spawnZ + radius); + + double amountX = selectMaxX - selectMinX; + double amountZ = selectMaxZ - selectMinZ; + + int selectX = amountX < 1.0 ? Mth.floor(worldBorder.getCenterX()) : (int)Mth.floor((amountX + 1.0) * random.nextDouble() + selectMinX); + int selectZ = amountZ < 1.0 ? Mth.floor(worldBorder.getCenterZ()) : (int)Mth.floor((amountZ + 1.0) * random.nextDouble() + selectMinZ); + + return new BlockPos(selectX, 0, selectZ); + } + + private static void completeSpawn(ServerLevel world, BlockPos selected, + ca.spottedleaf.concurrentutil.completable.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); + } + + } + + public void queuePacketTask(Runnable run) { + this.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { + run.run(); + }, null, 1L); + } + // 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 @@ -556,7 +703,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc .ifPresent(packed -> this.recipeBook.loadUntrusted(packed, key -> this.server.getRecipeManager().byKey(key).isPresent())); this.getBukkitEntity().readExtraData(input); // CraftBukkit if (this.isSleeping()) { - this.stopSleeping(); + this.stopSleepingRaw(); // Folia - do not modify or read worldstate during data deserialization } this.respawnConfig = input.read("respawn", ServerPlayer.RespawnConfig.CODEC).orElse(null); @@ -660,9 +807,16 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc if (!optional.isEmpty()) { ServerLevel level = this.level().getServer().getLevel(optional.get()); if (level != null) { - Entity entity = EntityType.loadEntityRecursive(input, level, EntitySpawnReason.LOAD, entity1 -> !level.addWithUUID(entity1) ? null : entity1); + Entity entity = EntityType.loadEntityRecursive(input, level, EntitySpawnReason.LOAD, java.util.function.Function.identity()); // 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()); } @@ -768,12 +922,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.absSnapTo(camera.getX(), camera.getY(), camera.getZ(), camera.getYRot(), camera.getXRot()); this.level().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); } @@ -1349,9 +1514,322 @@ 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, org.bukkit.event.player.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.level(); + ServerPlayer.RespawnConfig respawnConfig = this.getRespawnConfig(); + ServerLevel respawnWorld = this.server.getLevel(ServerPlayer.RespawnConfig.getDimensionOrDefault(respawnConfig)); + + // 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.level().getCurrentWorldData().connections.remove(this.connection.connection); + + 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.level().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 || respawnConfig == 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(respawnConfig.pos(), radiusBlocks, + ca.spottedleaf.concurrentutil.util.Priority.HIGHER, + (chunks) -> { + ServerPlayer.RespawnPosAngle spawnPos = ServerPlayer.findRespawnAndUseSpawnBlock( + respawnWorld, respawnConfig, !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, 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(respawnConfig.pos()).is(net.minecraft.world.level.block.Blocks.RESPAWN_ANCHOR); + boolean isBed = respawnWorld.getBlockState(respawnConfig.pos()).is(net.minecraft.tags.BlockTags.BEDS); + usedRespawnAnchor[0] = !alive && isRespawnAnchor; + + // 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.level().getCurrentWorldData().connections.remove(this.connection.connection); + this.level().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.level().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.level().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.stopUsingItem(); + + // 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 + + this.lastSentExp = -1; + this.lastSentHealth = -1.0F; + this.lastSentFood = -1; + + 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; @@ -2372,7 +2850,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) { @@ -2391,16 +2892,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.level().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 } } @@ -2778,11 +3282,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() { @@ -2952,7 +3456,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 cfbf34964288526e93d0a5b212c1b60296c10430..ee7bdc3adbeb04cf6ca630282f31a21e52bdb215 100644 --- a/net/minecraft/server/level/ServerPlayerGameMode.java +++ b/net/minecraft/server/level/ServerPlayerGameMode.java @@ -115,7 +115,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 { @@ -127,7 +127,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; @@ -366,7 +366,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); @@ -392,8 +392,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 6a7dae136691fb2476633fa12897424daa6bb5b3..3702c46c2ab07df3defd5fe2f231bb0e7a0b90bd 100644 --- a/net/minecraft/server/level/TicketType.java +++ b/net/minecraft/server/level/TicketType.java @@ -50,12 +50,20 @@ public final class TicketType implements ca.spottedleaf.moonrise.patches.chun public static final TicketType FORCED = register("forced", 0L, true, TicketType.TicketUse.LOADING_AND_SIMULATION); public static final TicketType PORTAL = register("portal", 300L, true, TicketType.TicketUse.LOADING_AND_SIMULATION); public static final TicketType ENDER_PEARL = register("ender_pearl", 40L, false, TicketType.TicketUse.LOADING_AND_SIMULATION); - public static final TicketType UNKNOWN = register("unknown", 1L, false, TicketType.TicketUse.LOADING); + public static final TicketType UNKNOWN = register("unknown", 5L, false, TicketType.TicketUse.LOADING); // Folia - region threading public static final TicketType PLUGIN = register("plugin", 0L, false, TicketType.TicketUse.LOADING_AND_SIMULATION); // CraftBukkit public static final TicketType POST_TELEPORT = register("post_teleport", 5L, false, TicketType.TicketUse.LOADING_AND_SIMULATION); // Paper public static final TicketType PLUGIN_TICKET = register("plugin_ticket", TicketType.NO_TIMEOUT, false, TicketType.TicketUse.LOADING_AND_SIMULATION); static { ((TicketType)PLUGIN_TICKET).moonrise$setIdentifierComparator((org.bukkit.plugin.Plugin p1, org.bukkit.plugin.Plugin p2) -> p1.getName().compareTo(p2.getName())); } // Paper // Paper - rewrite chunk system public static final TicketType FUTURE_AWAIT = register("future_await", TicketType.NO_TIMEOUT, false, TicketType.TicketUse.LOADING_AND_SIMULATION); // Paper public static final TicketType CHUNK_LOAD = register("chunk_load", TicketType.NO_TIMEOUT, false, TicketType.TicketUse.LOADING); // Paper - moonrise + // Folia start - region threading + public static final TicketType LOGIN = register("folia:login", 20L, false, TicketType.TicketUse.LOADING_AND_SIMULATION); + public static final TicketType DELAYED = register("folia:delay", 5L, false, TicketType.TicketUse.LOADING_AND_SIMULATION); + public static final TicketType END_GATEWAY_EXIT_SEARCH = ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicketType.create("folia:end_gateway_exit_search", Long::compareTo); + public static final TicketType NETHER_PORTAL_DOUBLE_CHECK = ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicketType.create("folia:nether_portal_double_check", Long::compareTo); + public static final TicketType TELEPORT_HOLD_TICKET = ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicketType.create("folia:teleport_hold_ticket", Long::compareTo); + public static final TicketType REGION_SCHEDULER_API_HOLD = register("folia:region_scheduler_api_hold", 0L, false, TicketType.TicketUse.LOADING_AND_SIMULATION); + // Folia end - region threading public static TicketType register(String name, long timeout, boolean persist, TicketType.TicketUse use) { return Registry.register(BuiltInRegistries.TICKET_TYPE, name, new TicketType(timeout, persist, use)); diff --git a/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java index 11fed81a4696ba18440e755c3b8a5ca39ed8a6b1..45d6a95196a791d366791e02d23c53b85c3aee16 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 f02800e4e941b05bde6f0d5fac76e2b6ec5b9832..8150b16c196edcb226be9268ea6e0012e44517fa 100644 --- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java @@ -85,8 +85,19 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack } } + // Folia start - region threading + private boolean handledDisconnect = false; + // Folia end - region threading + @Override public void onDisconnect(DisconnectionDetails details) { + // 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); @@ -350,24 +361,19 @@ 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); - 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); - } + // Folia start - region threading + boolean onMain; + if (this instanceof ServerGamePacketListenerImpl serverGamePacketListener) { + onMain = ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(serverGamePacketListener.player); + } else if (this instanceof ServerConfigurationPacketListenerImpl configurationPacketListener) { + net.minecraft.server.level.ServerPlayer player = configurationPacketListener.switchToMain; + onMain = player == null ? io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread() : ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player); + } else { + onMain = io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread(); + } + if (!onMain) { + this.connection.disconnectSafely(disconnectionDetails); // it HAS to be delayed/async to avoid deadlock if we try to wait for another region + // Folia end - region threading return; } @@ -421,7 +427,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack this.onDisconnect(disconnectionDetails); 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 diff --git a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java index 7f3379a617932f1e0b0344df75566d2dc6612359..884b7b8c4581742a1ca965d453773dc10b76d5f9 100644 --- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java @@ -48,6 +48,7 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis @Nullable private SynchronizeRegistriesTask synchronizeRegistriesTask; public io.papermc.paper.connection.PaperPlayerConfigurationConnection paperConnection; // Paper + public ServerPlayer switchToMain; // Folia - region threading - rewrite login process public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) { super(server, connection, cookie); @@ -166,7 +167,7 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis return; } - Component component = org.bukkit.craftbukkit.event.CraftEventFactory.handleLoginResult(playerList.canPlayerLogin(this.connection.getRemoteAddress(), this.gameProfile), this.paperConnection, this.connection, this.gameProfile, this.server, false); // Paper - Login event logic + Component component = org.bukkit.craftbukkit.event.CraftEventFactory.handleLoginResult(playerList.canPlayerLogin(this.connection.getRemoteAddress(), this.gameProfile, this.connection), this.paperConnection, this.connection, this.gameProfile, this.server, false); // Paper - Login event logic // Folia - region threading - add connection parameter if (component != null) { this.disconnect(component); return; @@ -182,7 +183,60 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis serverPlayer = new ServerPlayer(this.server, this.server.overworld(), this.gameProfile, this.clientInformation); } // Paper end - playerList.placeNewPlayer(this.connection, serverPlayer, 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 scopedCollector = new org.apache.commons.lang3.mutable.MutableObject<>(); + 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.moonrise$getChunkTaskScheduler().chunkHolderManager.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, + null + ); + + 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, + serverPlayer, + clientData, + scopedCollector.getValue(), + java.util.Optional.ofNullable(data.getValue()), + lastKnownName.getValue(), + loc + ); + }, + ca.spottedleaf.concurrentutil.util.Priority.HIGHER + ); + }); + this.switchToMain = serverPlayer; + try { + // now the connection responsibility is transferred on the region + playerList.loadSpawnForNewPlayer(this.connection, serverPlayer, clientData, scopedCollector, 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 = null; + 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 ca13dd72f173be6714965c506f2d48dcd3c9e569..be71521ec852ec007de7de2283d625241f86bf28 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -287,10 +287,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; @@ -309,11 +309,18 @@ public class ServerGamePacketListenerImpl private int nextChatIndex; 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 public final io.papermc.paper.connection.PaperPlayerGameConnection playerGameConnection; // Paper + // 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 = ca.spottedleaf.moonrise.patches.chunk_system.ticket.ChunkSystemTicketType.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); this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection()); @@ -340,6 +347,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; @@ -388,7 +401,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 @@ -424,6 +437,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 @@ -554,9 +580,10 @@ public class ServerGamePacketListenerImpl d7 = Math.max(d7, (otherFieldX * otherFieldX + otherFieldY * otherFieldY + otherFieldZ * otherFieldZ) - 1); // Paper end - fix large move vectors killing the server - 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; @@ -641,7 +668,13 @@ public class ServerGamePacketListenerImpl rootVehicle.absSnapTo(d, d1, d2, f, f1); // CraftBukkit start - fire PlayerMoveEvent TODO: this should be removed. - this.player.absSnapTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // Paper - TODO: This breaks alot of stuff + // 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 org.bukkit.entity.Player player = this.getCraftPlayer(); if (!this.hasMoved) { this.lastPosX = prevX; @@ -671,7 +704,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; } @@ -679,7 +712,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; } @@ -860,7 +893,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); @@ -1308,11 +1341,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) { @@ -1323,7 +1356,13 @@ public class ServerGamePacketListenerImpl Consumer> consumer = optional.isPresent() ? texts -> this.signBook(texts.get(0), texts.subList(1, texts.size()), slot) : list1 -> this.updateBookContents(list1, slot); - this.filterTextPacket(list).thenAcceptAsync(consumer, this.server); + // Folia start - region threading + this.filterTextPacket(list).thenAcceptAsync(consumer, this.player::queuePacketTask).whenComplete((Object res, Throwable thr) -> { + if (thr != null) { + LOGGER.error("Failed to handle book update packet", thr); + } + }); + // Folia end - region threading } } @@ -1455,9 +1494,10 @@ public class ServerGamePacketListenerImpl this.receivedMovePacketCount++; 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 = currTick; // Folia - region threading if (i > Math.max(this.allowedPlayerTicks, 5)) { // CraftBukkit end @@ -1650,7 +1690,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; } @@ -1658,7 +1698,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; } @@ -1919,9 +1959,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++; @@ -1952,7 +1992,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.level(), 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; } @@ -2035,7 +2075,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.level(), 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) { @@ -2151,7 +2191,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; } } @@ -2178,7 +2218,7 @@ public class ServerGamePacketListenerImpl LOGGER.info("{} lost connection: {}", this.player.getName().getString(), details.reason().getString()); // Paper start - Fix kick event leave message not being sent final net.kyori.adventure.text.Component quitMessage = details.quitMessage().map(io.papermc.paper.adventure.PaperAdventure::asAdventure).orElse(null); - this.removePlayerFromWorld(quitMessage); + if (!this.waitingForSwitchToConfig) this.removePlayerFromWorld(quitMessage); // Folia - region threading - don't double remove // Paper end - Fix kick event leave message not being sent super.onDisconnect(details); } @@ -2188,6 +2228,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(); @@ -2201,6 +2243,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.level().moonrise$getChunkTaskScheduler().chunkHolderManager.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 @@ -2442,7 +2486,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(); } @@ -2522,6 +2566,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 @@ -2727,8 +2772,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 this.removePlayerFromWorld(); + } finally { // Folia start - rewrite login process - move connection ownership to global region + io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.player.level().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); } @@ -2753,7 +2815,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; } @@ -2880,6 +2942,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(); @@ -2889,6 +2957,17 @@ public class ServerGamePacketListenerImpl return; } + // Folia start - region threading + if (true) { + this.player.respawn((ServerPlayer player) -> { + if (this.server.isHardcore()) { + this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent + this.player.level().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS).set(false, this.player.level()); // 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()) { @@ -3414,7 +3493,14 @@ public class ServerGamePacketListenerImpl } List list = Stream.of(lines).map(ChatFormatting::stripFormatting).collect(Collectors.toList()); // Paper end - Limit client sign length - this.filterTextPacket(list).thenAcceptAsync(texts -> this.updateSignText(packet, (List)texts), this.server); + // Folia start - region threading + this.filterTextPacket(list).thenAcceptAsync(texts -> this.updateSignText(packet, (List)texts), this.player::queuePacketTask) + .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) { @@ -3571,7 +3657,7 @@ public class ServerGamePacketListenerImpl this.chatMessageChain .append( () -> { - server.executeBlocking(() -> { // Paper - Broadcast chat session update sync + io.papermc.paper.threadedregions.RegionizedServer.getInstance().blockOn(() -> { // Paper - Broadcast chat session update sync // Folia - region threading this.player.setChatSession(chatSession); this.server .getPlayerList() @@ -3643,19 +3729,7 @@ public class ServerGamePacketListenerImpl @Override public void disconnectAsync(final net.minecraft.network.DisconnectionDetails disconnectionInfo) { - if (this.cserver.isPrimaryThread()) { - this.disconnect(disconnectionInfo); - return; - } - - this.connection.setReadOnly(); - this.server.scheduleOnMain(() -> { - this.disconnect(disconnectionInfo); - if (this.player.quitReason == null) { - // cancelled - this.connection.enableAutoRead(); - } - }); + this.disconnect(disconnectionInfo); // Folia - threaded regions } public final boolean isDisconnected() { diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java index ebf4d4516233c002b33084f1679044b23869d848..8f07aedb2233f8e43554bec77f9940a398b97941 100644 --- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java @@ -92,7 +92,13 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, // Paper end - login cookie API if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) { + // 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)); + } // Folia - region threading - rewrite login process } if (this.state == ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT @@ -204,7 +210,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, private void verifyLoginAndFinishConnectionSetup(GameProfile profile) { PlayerList playerList = this.server.getPlayerList(); - Component component = org.bukkit.craftbukkit.event.CraftEventFactory.handleLoginResult(playerList.canPlayerLogin(this.connection.getRemoteAddress(), profile), this.paperLoginConnection, this.connection, profile, this.server, true); // Paper + Component component = org.bukkit.craftbukkit.event.CraftEventFactory.handleLoginResult(playerList.canPlayerLogin(this.connection.getRemoteAddress(), profile, this.connection), this.paperLoginConnection, this.connection, profile, this.server, true); // Paper // Folia - region threading - add connection parameter if (component != null) { this.disconnect(component); } else { @@ -216,7 +222,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, ); } - boolean flag = playerList.disconnectAllPlayersWithProfile(profile); + boolean flag = false && playerList.disconnectAllPlayersWithProfile(profile); // Folia - rewrite login process - always false here if (flag) { this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT; } else { @@ -328,7 +334,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 194082763f41a58dc8ee0241a2eeb6dd39dff88d..484d3a17d22a2237e6102fde56890df95585981e 100644 --- a/net/minecraft/server/players/OldUsersConverter.java +++ b/net/minecraft/server/players/OldUsersConverter.java @@ -466,7 +466,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 e1fd699b33832c0f01b14023db7cc2b9d6797227..58e8e076facedd22a5c94d6de1344b45b2680228 100644 --- a/net/minecraft/server/players/PlayerList.java +++ b/net/minecraft/server/players/PlayerList.java @@ -109,10 +109,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); @@ -136,6 +136,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, io.papermc.paper.connection.DisconnectionReason.DUPLICATE_LOGIN_MESSAGE); + } + if (conflictingName != conflictingId && conflictingId != null && conflictingId.isPlayerConnected()) { + conflictingId.disconnectSafely(message, io.papermc.paper.connection.DisconnectionReason.DUPLICATE_LOGIN_MESSAGE); + } + } + + 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 @@ -148,7 +202,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(Connection connection, ServerPlayer player, CommonListenerCookie cookie, org.apache.commons.lang3.mutable.MutableObject scopedCollectorStore, 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(); @@ -161,8 +215,8 @@ public abstract class PlayerList { } else { string = gameProfile.getName(); } - - try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), LOGGER)) { + ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), LOGGER); scopedCollectorStore.setValue(scopedCollector); // Folia - region threading + class Dummy implements AutoCloseable { @Override public void close() {} } try (Dummy ignored = new Dummy()) { // Folia - region threading Optional optional1 = this.load(player, scopedCollector); final Optional loadedPlayerData = optional1; // Paper - OBFHELPER // CraftBukkit start - Better rename detection if (loadedPlayerData.isPresent()) { @@ -221,19 +275,36 @@ public abstract class PlayerList { player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login } // Paper end - Entity#getEntitySpawnReason + // Folia start - region threading - rewrite login process + // must write to these before toComplete is invoked + data.setValue(optional1.orElse(null)); + lastKnownName.setValue(string); + // Folia end - region threading - rewrite login process player.setServerLevel(serverLevel); if (loadedPlayerData.isEmpty() || invalidPlayerWorld[0]) { // Paper - reset to main world spawn if first spawn or invalid world - player.snapTo( - player.adjustSpawnLocation(serverLevel, serverLevel.getSharedSpawnPos()).getBottomCenter(), serverLevel.getSharedSpawnAngle(), 0.0F + ServerPlayer.fudgeSpawnLocation(serverLevel, player, toComplete); // Folia - region threading + } else { // Folia start - region threading - rewrite login process + serverLevel.moonrise$getChunkTaskScheduler().scheduleTickingState( + player.chunkPosition().x, player.chunkPosition().z, net.minecraft.server.level.FullChunkStatus.BLOCK_TICKING, true, + ca.spottedleaf.concurrentutil.util.Priority.HIGHER, (net.minecraft.world.level.chunk.LevelChunk chunk) -> { + toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(serverLevel, player.position())); + } ); } - - serverLevel.waitForChunkAndEntities(player.chunkPosition(), 1); + return; + } + } + public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie, net.minecraft.util.ProblemReporter.ScopedCollector scopedCollector, Optional optional1, String string, org.bukkit.Location selectedSpawn) { + try (ProblemReporter.ScopedCollector ignored = scopedCollector) { + 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 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(); @@ -256,6 +327,10 @@ public abstract class PlayerList { LevelData levelData = serverLevel.getLevelData(); player.loadGameTypes(optional1.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), serverGamePacketListenerImpl @@ -290,7 +365,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)) { @@ -332,7 +407,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(); @@ -347,8 +422,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 @@ -397,7 +471,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 @@ -489,7 +563,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) { @@ -524,7 +598,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) { @@ -589,8 +663,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); @@ -613,7 +686,7 @@ public abstract class PlayerList { } } // Paper end - PlayerLoginEvent - public LoginResult canPlayerLogin(SocketAddress socketAddress, GameProfile gameProfile) { // Paper - PlayerLoginEvent + public LoginResult canPlayerLogin(SocketAddress socketAddress, GameProfile gameProfile, Connection connection) { // Paper - PlayerLoginEvent // Folia - region threading - add connection parameter LoginResult whitelistEventResult; // Paper // Paper start - Fix MC-158900 UserBanListEntry userBanListEntry; @@ -622,7 +695,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 ); } @@ -636,13 +709,13 @@ 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 ); } return new LoginResult(mutableComponent, org.bukkit.event.player.PlayerLoginEvent.Result.KICK_BANNED); // Paper - PlayerLoginEvent } else { - return this.canBypassFullServerLogin(gameProfile, new LoginResult(Component.translatable("multiplayer.disconnect.server_full"), org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL)); // Paper - PlayerServerFullCheckEvent + return this.canBypassFullServerLogin(gameProfile, new LoginResult(Component.translatable("multiplayer.disconnect.server_full"), org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL), connection); // Paper - PlayerServerFullCheckEvent // Folia - region threading - add connection parameter } } @@ -656,14 +729,7 @@ public abstract class PlayerList { } } - ServerPlayer serverPlayer1 = this.playersByUUID.get(gameProfile.getId()); - if (serverPlayer1 != null) { - set.add(serverPlayer1); - } - - for (ServerPlayer serverPlayer2 : set) { - serverPlayer2.connection.disconnect(DUPLICATE_LOGIN_DISCONNECT_MESSAGE, io.papermc.paper.connection.DisconnectionReason.DUPLICATE_LOGIN_MESSAGE); // Paper - disconnect API - } + // Folia - region threading - rewrite login process - moved to pushPendingJoin return !set.isEmpty(); } @@ -674,6 +740,11 @@ public abstract class PlayerList { } public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, @Nullable org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, @Nullable 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 @@ -859,10 +930,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; @@ -880,8 +951,8 @@ public abstract class PlayerList { } 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 } } @@ -918,8 +989,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); } @@ -928,10 +998,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; @@ -949,7 +1020,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 } } @@ -957,7 +1030,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 } } @@ -988,8 +1063,8 @@ public abstract class PlayerList { } // Paper start - whitelist verify event / login event - public LoginResult canBypassFullServerLogin(final GameProfile profile, final LoginResult currentResult) { - final boolean shouldKick = this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(profile); + public LoginResult canBypassFullServerLogin(final GameProfile profile, final LoginResult currentResult, final Connection connection) { // Folia - region threading - add connection parameter + final boolean shouldKick = !this.countConnection(connection, this.maxPlayers) && !this.canBypassPlayerLimit(profile); // Folia - region threading - we control connection state here now async, not player list size final io.papermc.paper.event.player.PlayerServerFullCheckEvent fullCheckEvent = new io.papermc.paper.event.player.PlayerServerFullCheckEvent( com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(profile), io.papermc.paper.adventure.PaperAdventure.asAdventure(currentResult.message), @@ -1039,8 +1114,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; @@ -1065,10 +1139,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; @@ -1192,6 +1271,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) { @@ -1201,7 +1294,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/server/waypoints/ServerWaypointManager.java b/net/minecraft/server/waypoints/ServerWaypointManager.java index f9e7532f86122a379692561a639a209a126e8bba..0f8cacbb8fe55a60e2f0c98bf36c005b29f41a4b 100644 --- a/net/minecraft/server/waypoints/ServerWaypointManager.java +++ b/net/minecraft/server/waypoints/ServerWaypointManager.java @@ -22,11 +22,7 @@ public class ServerWaypointManager implements WaypointManager map = this.connections.row(player); - SetView set = Sets.difference(this.waypoints, map.keySet()); - - for (Entry entry : ImmutableSet.copyOf(map.entrySet())) { - this.updateConnection(player, entry.getKey(), entry.getValue()); - } - - for (WaypointTransmitter waypointTransmitter : set) { - this.createConnection(player, waypointTransmitter); - } + // Folia - region threading } public void removePlayer(ServerPlayer player) { - this.connections.row(player).values().removeIf(connection -> { - connection.disconnect(); - return true; - }); - this.untrackWaypoint((WaypointTransmitter)player); - this.players.remove(player); + // Folia - region threading } public void breakAllConnections() { diff --git a/net/minecraft/util/SpawnUtil.java b/net/minecraft/util/SpawnUtil.java index cbee99d782af257d04a7da739f956886146c9542..73100d3375ac7ee64b7aa5b2fe4e14d87e5e9565 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 55eac04821d9c9853354912e8c976c16fac21fee..543627d1409b4c5b5c85b3f0cae459b9df40a3fc 100644 --- a/net/minecraft/world/RandomSequences.java +++ b/net/minecraft/world/RandomSequences.java @@ -24,7 +24,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 RandomSequences(long seed) { this.worldSeed = seed; @@ -101,61 +101,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 4cec197634fac341cca1ed108f1ecb0561f72461..c7473920cc014646c2cc52bb5f716858aa1c242e 100644 --- a/net/minecraft/world/damagesource/CombatTracker.java +++ b/net/minecraft/world/damagesource/CombatTracker.java @@ -63,7 +63,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); @@ -90,7 +90,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 bb021fc9de91f8c4f79e6a753d57fa157efbbda7..111f79b1c5d0d041ce55feb4dd21c4ecc9824843 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 9a102b2c58446bd0aac5bd7f00e647f0270e7983..8a17bb73522bf16a35b3b15f318d2de97f2d007f 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -156,7 +156,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() { @@ -320,7 +320,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; @@ -532,6 +532,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.isRetiredOffThread(); + } + // Folia end - region ticking public Entity(EntityType entityType, Level level) { this.type = entityType; @@ -669,8 +686,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; } @@ -835,7 +851,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 @@ -853,7 +869,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(); } @@ -1108,8 +1124,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } else { if (type == MoverType.PISTON) { // Paper start - EAR 2 - this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); - this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); + this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Folia - region threading + this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Folia - region threading // Paper end - EAR 2 movement = this.limitPistonMovement(movement); if (movement.equals(Vec3.ZERO)) { @@ -1455,7 +1471,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; @@ -3190,6 +3206,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()); @@ -3211,6 +3228,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(); } @@ -3278,7 +3296,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 } } @@ -3292,6 +3310,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) { @@ -3319,6 +3338,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 { @@ -3326,7 +3346,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 } @@ -3401,7 +3421,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) { @@ -3409,21 +3429,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() { @@ -3563,6 +3582,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()); } @@ -3907,8 +3931,795 @@ 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) { + queue.add(passenger); + 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) { + queue.add(passenger); + curr.root.positionRider(passenger.root, teleport ? Entity::snapTo : 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.moonrise$getChunkTaskScheduler().chunkHolderManager.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.moonrise$getChunkTaskScheduler().chunkHolderManager.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.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( + TicketType.POST_TELEPORT, chunk.getPos(), + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, + null + ); + } + 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.snapTo(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.moonrise$getChunkTaskScheduler().chunkHolderManager.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.moonrise$getChunkTaskScheduler().chunkHolderManager.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.moonrise$getChunkTaskScheduler().chunkHolderManager.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.moonrise$getChunkTaskScheduler().chunkHolderManager.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 {} to {}:{}", this, teleportTransition.newLevel(), teleportTransition.position(), new Throwable()); @@ -4106,6 +4917,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 @@ -4436,6 +5253,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) { @@ -4445,6 +5268,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) { @@ -4977,7 +5806,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess return; } // Paper end - Block invalid positions and bounding box - 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 @@ -5010,7 +5840,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 @@ -5117,6 +5947,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; @@ -5132,6 +5968,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess // Paper end - rewrite chunk system org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); // CraftBukkit 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; } @@ -5155,6 +5994,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. @@ -5166,7 +6009,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess // Paper end - Folia schedulers // Paper start - optimise Folia entity scheduler public final void registerScheduler() { - this.getBukkitEntity().taskScheduler.registerTo(net.minecraft.server.MinecraftServer.getServer().entitySchedulerTickList); + this.getBukkitEntity().taskScheduler.registerTo(io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().entitySchedulerTickList); // Folia - region threading } // Paper end - optimise Folia entity scheduler diff --git a/net/minecraft/world/entity/EntityReference.java b/net/minecraft/world/entity/EntityReference.java index ff660a5756c76abdac5536df80caced7a00c83c6..6c48bf2742199184cd94d08f1d5367b0ea9dda56 100644 --- a/net/minecraft/world/entity/EntityReference.java +++ b/net/minecraft/world/entity/EntityReference.java @@ -15,22 +15,22 @@ import net.minecraft.world.level.entity.UniquelyIdentifyable; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; -public final class EntityReference { +public final class EntityReference { // Folia - region threading - force Entity Type private static final Codec> CODEC = UUIDUtil.CODEC.xmap(EntityReference::new, EntityReference::getUUID); private static final StreamCodec> STREAM_CODEC = UUIDUtil.STREAM_CODEC .map(EntityReference::new, EntityReference::getUUID); - private Either entity; + private Either entity; // Folia - region threading - force usage of CraftEntity - public static Codec> codec() { + public static Codec> codec() { // Folia - region threading - force Entity Type return (Codec>)CODEC; } - public static StreamCodec> streamCodec() { + public static StreamCodec> streamCodec() { // Folia - region threading - force Entity Type return (StreamCodec>)STREAM_CODEC; } public EntityReference(StoredEntityType entity) { - this.entity = Either.right(entity); + this.entity = Either.right(entity.getBukkitEntity()); // Folia - region threading - force usage of CraftEntity } public EntityReference(UUID uuid) { @@ -38,14 +38,14 @@ public final class EntityReference (UUID)uuid, UniquelyIdentifyable::getUUID); + return this.entity.map(uuid -> (UUID)uuid, org.bukkit.craftbukkit.entity.CraftEntity::getUniqueId); // Folia - region threading - force usage of CraftEntity } @Nullable public StoredEntityType getEntity(UUIDLookup uuidLookup, Class entityClass) { - Optional optional = this.entity.right(); + Optional optional = this.entity.right(); // Folia - region threading - force usage of CraftEntity if (optional.isPresent()) { - StoredEntityType uniquelyIdentifyable = optional.get(); + StoredEntityType uniquelyIdentifyable = (StoredEntityType)optional.get().getHandleRaw(); // Folia - region threading - force usage of CraftEntity if (!uniquelyIdentifyable.isRemoved()) { return uniquelyIdentifyable; } @@ -57,7 +57,7 @@ public final class EntityReference StoredEntityType get( + public static StoredEntityType get( // Folia - region threading - force Entity Type @Nullable EntityReference reference, UUIDLookup uuidLookup, Class entityClass ) { return reference != null ? reference.getEntity(uuidLookup, entityClass) : null; } @Nullable - public static EntityReference read(ValueInput input, String key) { + public static EntityReference read(ValueInput input, String key) { // Folia - region threading - force Entity Type return input.read(key, EntityReference.codec()).orElse(null); } @Nullable - public static EntityReference readWithOldOwnerConversion( + public static EntityReference readWithOldOwnerConversion( // Folia - region threading - force Entity Type ValueInput input, String key, Level level ) { Optional optional = input.read(key, UUIDUtil.CODEC); diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java index 0268e02d2ef2cb3d699644a804e23a6da4521f4c..5f58cbbc48045dfeb418e3b604e6172b4c39249d 100644 --- a/net/minecraft/world/entity/LivingEntity.java +++ b/net/minecraft/world/entity/LivingEntity.java @@ -261,7 +261,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin 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 @@ -287,6 +287,26 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin public net.kyori.adventure.util.TriState frictionState = net.kyori.adventure.util.TriState.NOT_SET; // Paper - Friction API public int shieldBlockingDelay = this.level().paperConfig().misc.shieldBlockingDelay; // Paper - Make shield blocking delay configurable // 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); @@ -489,7 +509,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin if (this.isDeadOrDying() && this.level().shouldTickDeath(this)) { this.tickDeath(); - } + } else { this.broadcastedDeath = false; } // Folia - region threading if (this.lastHurtByPlayerMemoryTime > 0) { this.lastHurtByPlayerMemoryTime--; @@ -576,11 +596,14 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin 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, org.bukkit.event.entity.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, org.bukkit.event.entity.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 } } @@ -883,7 +906,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin // CraftBukkit end this.setHealth(input.getFloatOr("Health", this.getMaxHealth())); this.hurtTime = input.getShortOr("HurtTime", (short)0); - this.deathTime = input.getShortOr("DeathTime", (short)0); + this.deathTime = input.getShortOr("DeathTime", (short)0); this.broadcastedDeath = false; // Folia - region threading this.lastHurtByMobTimestamp = input.getIntOr("HurtByTimestamp", 0); input.getString("Team").ifPresent(string -> { Scoreboard scoreboard = this.level().getScoreboard(); @@ -1150,6 +1173,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin 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; @@ -1537,7 +1561,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin 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); @@ -1767,7 +1791,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin @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; } @@ -2509,10 +2533,10 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin @Nullable public LivingEntity getKillCredit() { - if (this.lastHurtByPlayer != null) { + if (this.lastHurtByPlayer != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByPlayer.getEntity(this.level(), Player.class))) { // Folia - region threading return this.lastHurtByPlayer.getEntity(this.level(), Player.class); } else { - return this.lastHurtByMob != null ? this.lastHurtByMob.getEntity(this.level(), LivingEntity.class) : null; + return this.lastHurtByMob != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByMob.getEntity(this.level(), LivingEntity.class)) ? this.lastHurtByMob.getEntity(this.level(), LivingEntity.class) : null; // Folia - region threading } } @@ -2591,7 +2615,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin } this.lastDamageSource = damageSource; - this.lastDamageStamp = this.level().getGameTime(); + this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading } @Override @@ -3571,7 +3595,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin 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()); @@ -4270,7 +4294,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin 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()) { @@ -4432,6 +4456,11 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin 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 e0b3cb2b2694768803ed347a1026b881fd624951..975e0ccd6aac36457c26522a5dd1a256f78500c0 100644 --- a/net/minecraft/world/entity/Mob.java +++ b/net/minecraft/world/entity/Mob.java @@ -246,9 +246,21 @@ 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() { return this.getBrain().getMemory(MemoryModuleType.ATTACK_TARGET).orElse(null); @@ -260,7 +272,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab } public boolean setTarget(@Nullable LivingEntity target, @Nullable EntityTargetEvent.TargetReason reason) { - if (this.getTarget() == target) { + if (this.getTargetRaw() == target) { // Folia - region threading return false; } if (reason != null) { @@ -1579,10 +1591,10 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab this.goalSelector.removeAllGoals(filter); } + // Folia start - region threading @Override - protected void removeAfterChangingDimensions() { - super.removeAfterChangingDimensions(); - + protected void postRemoveAfterChangingDimensions() { + super.postRemoveAfterChangingDimensions(); for (EquipmentSlot equipmentSlot : EquipmentSlot.VALUES) { ItemStack itemBySlot = this.getItemBySlot(equipmentSlot); if (!itemBySlot.isEmpty()) { @@ -1590,6 +1602,20 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab } } } + // Folia end - region threading + + @Override + protected void removeAfterChangingDimensions() { + super.removeAfterChangingDimensions(); + // Folia start - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions +// for (EquipmentSlot equipmentSlot : EquipmentSlot.VALUES) { +// ItemStack itemBySlot = this.getItemBySlot(equipmentSlot); +// if (!itemBySlot.isEmpty()) { +// itemBySlot.setCount(0); +// } +// } + // Folia end - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions + } @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 c9bd7212644b86be6eacdba2e12b6a19516bd2b6..ff628c93d1cc3367272208efcb6b20b4a9999478 100644 --- a/net/minecraft/world/entity/TamableAnimal.java +++ b/net/minecraft/world/entity/TamableAnimal.java @@ -244,6 +244,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()); } } @@ -276,7 +281,22 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { return false; } org.bukkit.Location to = event.getTo(); - this.snapTo(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 98652c2c549bd7657a606d25ba4fe3cffa0548da..3eb1362e6597faa21b27c9d1611d64a236491f98 100644 --- a/net/minecraft/world/entity/ai/Brain.java +++ b/net/minecraft/world/entity/ai/Brain.java @@ -434,9 +434,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 91930fdf408d4be66ecc39d766c3ed4566ac6343..eced0648fdc7f0df64b69d20e5e31a8991da9f1d 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 44340aa26bc89faaf8842bb36048cf61dba34314..47d56d8f70102cf6beb8e9c23d7a7a6d17a755c1 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().profession().is(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 86fccf3617a32f3791b03d8067e2eaf6b8d8bebb..89a78ba2e96cf3d6c8cec850a6197d4ada18c9a1 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 dd1a95111d965bcd7f53be9d4224dd213e4e0705..599902da838f977e47b3b51a5de65503594b0ef6 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 eb3ec4be63e1710d0ed9021eb8158d56eae89f8d..ce9a369a603ed0fe2a119775b1e597a5dcde91b9 100644 --- a/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +++ b/net/minecraft/world/entity/ai/sensing/PlayerSensor.java @@ -26,7 +26,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 6074c091d0df7843c9f9dc691703eba1a24a7253..de27ce9d947d9845d4a07454b1f39b61f88db9f3 100644 --- a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java +++ b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java @@ -28,7 +28,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 69825fc6941047723919a6079ce4a0b9d1a06006..b0342aabda63ca7733590e8914753ad38929944f 100644 --- a/net/minecraft/world/entity/ai/village/VillageSiege.java +++ b/net/minecraft/world/entity/ai/village/VillageSiege.java @@ -18,62 +18,66 @@ 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 void tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { + 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.isBrightOutside() && spawnEnemies) { 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 (!this.hasSetupSiege) { + if (worldData.villageSiegeState.siegeState != VillageSiege.State.SIEGE_DONE) { // Folia - region threading + if (!worldData.villageSiegeState.hasSetupSiege) { // Folia - region threading if (!this.tryToSetupSiege(level)) { return; } - 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 } 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 } } } } 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 } } 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; } } @@ -87,11 +91,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.snapTo(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); @@ -99,7 +105,7 @@ public class VillageSiege implements CustomSpawner { return; } - zombie.snapTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); + //zombie.snapTo(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 } } @@ -119,7 +125,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 13f24836649790a34b988e2d63accb043e6ff080..8c17101ac5b73b44de6c64463d29b217a79d24ab 100644 --- a/net/minecraft/world/entity/animal/Bee.java +++ b/net/minecraft/world/entity/animal/Bee.java @@ -816,6 +816,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.hasHome() @@ -926,6 +931,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.hasHome() && this.wantsToGoToKnownFlower() && !Bee.this.closerThan(Bee.this.savedFlowerPos, 2); } diff --git a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java index a0e0fad40838fa7d835f31e5ce4ae3ab40e0bfa4..5a6e10db234d7e5e76420cea1a0ba0e746f1f496 100644 --- a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java +++ b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java @@ -54,7 +54,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 90fd3ca5ecd29befa9237222e9f86a8a79a011e4..324e150f9ae79d15a25710bc9da0368bbddda534 100644 --- a/net/minecraft/world/entity/decoration/ItemFrame.java +++ b/net/minecraft/world/entity/decoration/ItemFrame.java @@ -256,7 +256,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 595dee03e3a7d98d703e48fb53d82d7f392a2b3d..34e31fa13e5107f13e166f694fafe58e306330ae 100644 --- a/net/minecraft/world/entity/item/FallingBlockEntity.java +++ b/net/minecraft/world/entity/item/FallingBlockEntity.java @@ -165,7 +165,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 51804b611f469f2ab53e455e8c633b867b00cc88..2309d1a30459ff986ab5bc4316247efaa7472a41 100644 --- a/net/minecraft/world/entity/item/ItemEntity.java +++ b/net/minecraft/world/entity/item/ItemEntity.java @@ -503,13 +503,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 17d54d38dcec39eefeb989cd576cc640a36e82f5..732672aa7d915217eb4af44179b2f2ab1e796cca 100644 --- a/net/minecraft/world/entity/item/PrimedTnt.java +++ b/net/minecraft/world/entity/item/PrimedTnt.java @@ -100,8 +100,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(); @@ -220,13 +220,21 @@ public class PrimedTnt extends Entity implements TraceableEntity { @Override public Entity teleport(TeleportTransition teleportTransition) { Entity entity = super.teleport(teleportTransition); - if (entity instanceof PrimedTnt primedTnt) { - primedTnt.setUsedPortal(true); - } + // Folia - region threading - move to post change return entity; } + // Folia start - region threading + @Override + public void postChangeDimension() { + super.postChangeDimension(); + if (this instanceof PrimedTnt primedTnt) { // Folia - region threading - move to post change + primedTnt.setUsedPortal(true); + } + } + // Folia end - region threading + @Override public final boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) { return false; diff --git a/net/minecraft/world/entity/monster/Vex.java b/net/minecraft/world/entity/monster/Vex.java index a621e08fe4f88fcfa8da83bc44b23fc2ffaa5918..ccddac6b1bebbe164f4192d0d8b5a1aa14851169 100644 --- a/net/minecraft/world/entity/monster/Vex.java +++ b/net/minecraft/world/entity/monster/Vex.java @@ -346,7 +346,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/npc/AbstractVillager.java b/net/minecraft/world/entity/npc/AbstractVillager.java index f8782cdcf3015cad2693663a3c222bd60822f45b..66c6a65f39847956839de5c60836b3ec07c4ebef 100644 --- a/net/minecraft/world/entity/npc/AbstractVillager.java +++ b/net/minecraft/world/entity/npc/AbstractVillager.java @@ -202,10 +202,18 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa this.readInventoryFromTag(input); } + // 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 e282b6ab6d0d1c11ee40f5f436bd50fa90ddc88b..a92769d2c0a93afa85797c0e778f460cc6d28599 100644 --- a/net/minecraft/world/entity/npc/CatSpawner.java +++ b/net/minecraft/world/entity/npc/CatSpawner.java @@ -18,15 +18,16 @@ 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 void tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { if (spawnFriendlies && level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { - this.nextTick--; - if (this.nextTick <= 0) { - this.nextTick = 1200; - Player randomPlayer = level.getRandomPlayer(); + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + worldData.catSpawnerNextTick--; // Folia - region threading + if (worldData.catSpawnerNextTick <= 0) { // Folia - region threading + worldData.catSpawnerNextTick = 1200; // Folia - region threading + Player randomPlayer = level.getRandomLocalPlayer(); // Folia - region threading if (randomPlayer != null) { RandomSource randomSource = level.random; int i = (8 + randomSource.nextInt(24)) * (randomSource.nextBoolean() ? -1 : 1); diff --git a/net/minecraft/world/entity/npc/Villager.java b/net/minecraft/world/entity/npc/Villager.java index e1e2bdb35866a8f32a41f6efd24ad77cf916b2e9..7d02def8b650a7574c1a6b0c212d6973d2b46f45 100644 --- a/net/minecraft/world/entity/npc/Villager.java +++ b/net/minecraft/world/entity/npc/Villager.java @@ -249,7 +249,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 @@ -683,6 +683,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler this.brain.getMemory(moduleType).ifPresent(pos -> { ServerLevel level = server.getLevel(pos.dimension()); if (level != null) { + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( // Folia - region threading + level, pos.pos().getX() >> 4, pos.pos().getZ() >> 4, () -> { // Folia - region threading PoiManager poiManager = level.getPoiManager(); Optional> type = poiManager.getType(pos.pos()); BiPredicate> biPredicate = POI_MEMORIES.get(moduleType); @@ -690,6 +692,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler poiManager.release(pos.pos()); DebugPackets.sendPoiTicketCountPacket(level, pos.pos()); } + }); // Folia - region threading } }); } diff --git a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java index fce3f0f81d3195045cdc9ad7320f1d92f033c36d..f83020635a24174777d5266a7f23d1b0607a00e6 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,37 +51,38 @@ public class WanderingTraderSpawner implements CustomSpawner { @Override public void tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { + 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)) { - if (this.tickDelay - 1 <= 0) { // Paper - Prevent tickDelay going below 0 - this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; - this.spawnDelay = this.spawnDelay - level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; + if (worldData.wanderingTraderTickDelay - 1 <= 0) { // Paper - Prevent tickDelay going below 0 // Folia - region threading + 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) { - this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; + if (worldData.wanderingTraderSpawnDelay <= 0) { // Folia - region threading + worldData.wanderingTraderSpawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading if (level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { - 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) { 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 } } } } - } else { this.tickDelay--; } // Paper - Prevent tickDelay going below 0 + } else { worldData.wanderingTraderTickDelay--; } // Paper - Prevent tickDelay going below 0 } } 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) { @@ -106,7 +105,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.setHomeTo(blockPos1, 16); diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java index 8f4c45fbea2d7c1317aff36031bb834b07b23a82..6de2b7702b07edd89784a5554fee4f7a75c521f8 100644 --- a/net/minecraft/world/entity/player/Player.java +++ b/net/minecraft/world/entity/player/Player.java @@ -1438,6 +1438,14 @@ public abstract class Player extends LivingEntity { return this.isLocalPlayer(); } + // 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 11adb3eacce9d46f15e6c5216e9b2494df158baf..8ebad3f14c502ff949b8d5a90d3ec5ebe7e0c72a 100644 --- a/net/minecraft/world/entity/projectile/AbstractArrow.java +++ b/net/minecraft/world/entity/projectile/AbstractArrow.java @@ -173,6 +173,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 58ae0ebfe4721022a5c50d617bd6e65867be1d5b..54cdb5b927337e635f8f97f7c71a440edd37bbc7 100644 --- a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java +++ b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java @@ -81,6 +81,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 d8dc196ef92e97f831cf97cd1536a46f81f9d5d1..3e223b49878d2622711cb8791d653f789e07329a 100644 --- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java @@ -135,6 +135,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 5f3abbe943be394e9cb987945a238208940b5015..7760ab1fe365b4bf1ab6c2ff953e8659a28008a8 100644 --- a/net/minecraft/world/entity/projectile/FishingHook.java +++ b/net/minecraft/world/entity/projectile/FishingHook.java @@ -270,6 +270,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); @@ -612,10 +617,18 @@ public class FishingHook extends Projectile { @Override public void remove(Entity.RemovalReason reason, @Nullable org.bukkit.event.entity.EntityRemoveEvent.Cause cause) { // CraftBukkit - add Bukkit remove cause - 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 f736f72b8e76dd82236badcdd2756f0d4da89aa4..e0277e9c8037e90d0a479e8fb5013b88c214bd97 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 dae6619f4670c6f68dae7e4b10b0b6a8bbe6dc2e..3e1c8d9f8c246765cb9026b2560c30917585b363 100644 --- a/net/minecraft/world/entity/projectile/Projectile.java +++ b/net/minecraft/world/entity/projectile/Projectile.java @@ -71,9 +71,20 @@ public abstract class Projectile extends Entity implements TraceableEntity { } // Paper end - Refresh ProjectileSource for projectiles + // Folia start - region threading + // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner. + // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior. @Nullable @Override public Entity getOwner() { + Entity ret = this.getOwnerRaw(); + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(ret) && (ret == null || !ret.isRemoved()) ? ret : null; + } + // Folia end - region threading + + @Nullable + public Entity getOwnerRaw() { // Folia - region threading + if (!this.hasNullCallback()) ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot update owner state asynchronously"); // Folia - region threading return EntityReference.get(this.owner, this.level(), Entity.class); } @@ -436,7 +447,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 5104636be1de4bf1dc491673cad55854a106da53..a51bf62b9c72cf0092adafbdbfed06c1f355a4e5 100644 --- a/net/minecraft/world/entity/projectile/ThrowableProjectile.java +++ b/net/minecraft/world/entity/projectile/ThrowableProjectile.java @@ -44,6 +44,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/ThrownEgg.java b/net/minecraft/world/entity/projectile/ThrownEgg.java index 73ec34b43f3fb2aa3edc3f1cb48a923d1fa32036..07dd05ceb9700e91088326290d211d94c1c2a6a6 100644 --- a/net/minecraft/world/entity/projectile/ThrownEgg.java +++ b/net/minecraft/world/entity/projectile/ThrownEgg.java @@ -97,12 +97,13 @@ public class ThrownEgg extends ThrowableItemProjectile { for (int i1 = 0; i1 < i; i1++) { net.minecraft.world.entity.Entity chicken = newEntityType.create(this.level(), net.minecraft.world.entity.EntitySpawnReason.TRIGGERED); // CraftBukkit if (chicken != null) { + chicken.snapTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), 0.0F); // Folia - region threading - moved up // CraftBukkit start if (chicken.getBukkitEntity() instanceof org.bukkit.entity.Ageable ageable) { ageable.setBaby(); } // CraftBukkit end - chicken.snapTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), 0.0F); + // Folia - region threading - move up // CraftBukkit start if (chicken instanceof Chicken realChicken) { Optional.ofNullable(this.getItem().get(DataComponents.CHICKEN_VARIANT)) diff --git a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java index 5aa63c90e1c0605faef76588efdc7c3903be4605..a2b3a744acca66c1d4b2b73512b69e411c2580e4 100644 --- a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +++ b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java @@ -52,15 +52,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 @@ -96,6 +92,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.level(); + + // 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.snapTo(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.level(), 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); @@ -114,6 +185,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)) { Vec3 vec3 = this.oldPosition(); @@ -208,7 +293,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 b3a29ce523fb5de71589c7c17598bba17622f988..1910bff3f08147bfd63703097e30b9f88c84e337 100644 --- a/net/minecraft/world/entity/raid/Raid.java +++ b/net/minecraft/world/entity/raid/Raid.java @@ -127,6 +127,13 @@ public class Raid { private int celebrationTicks; private Optional waveSpawnPos = Optional.empty(); + // Folia start - make raids thread-safe + public boolean ownsRaid(ServerLevel world) { + BlockPos center = this.getCenter(); + return center != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, center.getX() >> 4, center.getZ() >> 4, 8); + } + // Folia end - make raids thread-safe + public Raid(BlockPos center, Difficulty difficulty) { this.active = true; this.raidCooldownTicks = 300; @@ -222,7 +229,7 @@ public class Raid { private Predicate validPlayer() { return player -> { BlockPos blockPos = player.blockPosition(); - return player.isAlive() && player.level().getRaidAt(blockPos) == this; + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) && player.isAlive() && player.level().getRaidAt(blockPos) == this; // Folia - make raids thread-safe }; } @@ -397,12 +404,21 @@ public class Raid { for (UUID uuid : this.heroesOfTheVillage) { Entity entity = level.getEntity(uuid); if (entity instanceof LivingEntity livingEntity && !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 } } org.bukkit.craftbukkit.event.CraftEventFactory.callRaidFinishEvent(level, this, winners); // CraftBukkit @@ -503,7 +519,7 @@ public class Raid { Collection players = this.raidEvent.getPlayers(); long randomLong = this.random.nextLong(); - for (ServerPlayer serverPlayer : level.players()) { + for (ServerPlayer serverPlayer : 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)); @@ -687,6 +703,11 @@ public class Raid { } private boolean addWaveMob(ServerLevel level, int wave, Raider raider) { + // Folia start - make raids thread-safe + if (!this.ownsRaid(level)) { + return false; + } + // Folia end - make raids thread-safe return this.addWaveMob(level, wave, raider, true); } diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java index a495789b2d21fa9a24d5dca4ecfa196ddce49466..3e30989c99cd11c8c94d6fd194d2969e3a7efda6 100644 --- a/net/minecraft/world/entity/raid/Raider.java +++ b/net/minecraft/world/entity/raid/Raider.java @@ -89,7 +89,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.getRaidAt(this.blockPosition()); if (raidAt != null && Raids.canJoinRaid(this)) { raidAt.joinRaid(serverLevel, raidAt.getGroupsSpawned(), this, null, true); diff --git a/net/minecraft/world/entity/raid/Raids.java b/net/minecraft/world/entity/raid/Raids.java index b8de00c5f13257d0b79b44b19fbfb88a3ca20353..cb383b943536c4a6c5a754f4ff2b71c48dfff103 100644 --- a/net/minecraft/world/entity/raid/Raids.java +++ b/net/minecraft/world/entity/raid/Raids.java @@ -34,16 +34,16 @@ public class Raids extends SavedData { Raids.RaidWithId.CODEC .listOf() .optionalFieldOf("raids", List.of()) - .forGetter(raids -> raids.raidMap.int2ObjectEntrySet().stream().map(Raids.RaidWithId::from).toList()), - Codec.INT.fieldOf("next_id").forGetter(raids -> raids.nextId), + .forGetter(raids -> raids.raidMap.entrySet().stream().map(Raids.RaidWithId::from).toList()), // Folia - make raids thread-safe + Codec.INT.fieldOf("next_id").forGetter(raids -> raids.nextAvailableID.get()), // Folia - make raids thread-safe Codec.INT.fieldOf("tick").forGetter(raids -> raids.tick) ) .apply(instance, Raids::new) ); public static final SavedDataType TYPE = new SavedDataType<>("raids", Raids::new, CODEC, DataFixTypes.SAVED_DATA_RAIDS); public static final SavedDataType TYPE_END = new SavedDataType<>("raids_end", Raids::new, CODEC, DataFixTypes.SAVED_DATA_RAIDS); - public final Int2ObjectMap raidMap = new Int2ObjectOpenHashMap<>(); - private int nextId = 1; + public final java.util.Map raidMap = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - make raids thread-safe + private final java.util.concurrent.atomic.AtomicInteger nextAvailableID = new java.util.concurrent.atomic.AtomicInteger(); // Folia - make raids thread-safe private int tick; public static SavedDataType getType(Holder dimension) { @@ -51,6 +51,7 @@ public class Raids extends SavedData { } public Raids() { + this.nextAvailableID.set(1); // Folia - make raids thread-safe this.setDirty(); } @@ -60,7 +61,7 @@ public class Raids extends SavedData { raidWithId.raid.idOrNegativeOne = raidWithId.id; // Paper - expose id of raids while method is kept around as deprecated for removal } - this.nextId = nextId; + this.nextAvailableID.set(nextId); // Folia - make raids thread-safe this.tick = tick; } @@ -70,21 +71,34 @@ public class Raids extends SavedData { } public OptionalInt getId(Raid raid) { - for (Entry entry : this.raidMap.int2ObjectEntrySet()) { + for (java.util.Map.Entry entry : this.raidMap.entrySet()) { // Folia - make raids thread-safe if (entry.getValue() == raid) { - return OptionalInt.of(entry.getIntKey()); + return OptionalInt.of(entry.getKey().intValue()); // Folia - make raids thread-safe } } return OptionalInt.empty(); } - public void tick(ServerLevel level) { + // Folia start - make raids thread-safe + public void globalTick() { this.tick++; + if (this.tick % 200 == 0) { + this.setDirty(); + } + } + + public void tick(ServerLevel level) { + // 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(level)) { + continue; + } + // Folia end - make raids thread-safe if (level.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS)) { raid.stop(); } @@ -97,9 +111,7 @@ public class Raids extends SavedData { } } - if (this.tick % 200 == 0) { - this.setDirty(); - } + // Folia - make raids thread-safe - move to globalTick() DebugPackets.sendRaids(level, this.raidMap.values()); } @@ -114,7 +126,7 @@ public class Raids extends SavedData { return null; } else { ServerLevel serverLevel = player.level(); - if (serverLevel.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS)) { + if (serverLevel.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(serverLevel, pos.getX() >> 4, pos.getZ() >> 4, 8) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(serverLevel, player.chunkPosition().x, player.chunkPosition().z, 8)) { // Folia - region threading return null; } else { DimensionType dimensionType = serverLevel.dimensionType(); @@ -155,8 +167,8 @@ public class Raids extends SavedData { } if (!raid.isStarted() && !this.raidMap.containsValue(raid)) { - this.raidMap.put(this.getUniqueId(), raid); - raid.idOrNegativeOne = this.nextId; // Paper - expose id of raids while method is kept around as deprecated for removal + int id = this.getUniqueId(); this.raidMap.put(id, raid); // Folia - region threading + raid.idOrNegativeOne = id; // Paper - expose id of raids while method is kept around as deprecated for removal // Folia - region threading } // CraftBukkit end raid.absorbRaidOmen(player); @@ -179,15 +191,20 @@ public class Raids extends SavedData { } private int getUniqueId() { - return ++this.nextId; + return this.nextAvailableID.incrementAndGet(); // Folia - make raids thread-safe } @Nullable - public Raid getNearbyRaid(BlockPos pos, int distance) { + public Raid getNearbyRaid(ServerLevel world, BlockPos pos, int distance) { // Folia - make raids thread-safe - add ServerLevel param Raid raid = null; double d = distance; for (Raid raid1 : this.raidMap.values()) { + // Folia start - make raids thread-safe + if (!raid1.ownsRaid(world)) { + continue; + } + // Folia end - make raids thread-safe double d1 = raid1.getCenter().distSqr(pos); if (raid1.isActive() && d1 < d) { raid = raid1; @@ -204,8 +221,8 @@ public class Raids extends SavedData { .apply(instance, Raids.RaidWithId::new) ); - public static Raids.RaidWithId from(Entry entry) { - return new Raids.RaidWithId(entry.getIntKey(), entry.getValue()); + public static Raids.RaidWithId from(java.util.Map.Entry entry) { // Folia - make raids thread-safe + return new Raids.RaidWithId(entry.getKey().intValue(), entry.getValue()); // Folia - make raids thread-safe } } } diff --git a/net/minecraft/world/entity/variant/StructureCheck.java b/net/minecraft/world/entity/variant/StructureCheck.java index 0f6866c7fb26a2be8314f55d601533545a3d3610..61906347ce64943acc2172d0f4a09c2443f4dd52 100644 --- a/net/minecraft/world/entity/variant/StructureCheck.java +++ b/net/minecraft/world/entity/variant/StructureCheck.java @@ -15,7 +15,7 @@ public record StructureCheck(HolderSet requiredStructures) implements @Override public boolean test(SpawnContext context) { - return context.level().getLevel().structureManager().getStructureWithPieceAt(context.pos(), this.requiredStructures, context.level()).isValid(); // Paper - Fix swamp hut cat generation deadlock + return context.level().getLevel().structureManager().getStructureWithPieceAt(context.pos(), this.requiredStructures).isValid(); // Paper - Fix swamp hut cat generation deadlock // Folia - region threading - properly fix this } @Override diff --git a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java index 5f31a04bc1c6b66c4f301fe68bf3e7ac2e3f1eee..c7846cc1be407184f2ae4fc3fbc586422824ed95 100644 --- a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java +++ b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java @@ -146,5 +146,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 41a6ec508a10a49a37539d2f10171d15c233b280..0358747fa4e69f3e5e0e15f0a731b38f8f5e3b49 100644 --- a/net/minecraft/world/entity/vehicle/MinecartHopper.java +++ b/net/minecraft/world/entity/vehicle/MinecartHopper.java @@ -147,7 +147,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 da16f4831c875e07c25d7ed041bed493db614658..6154da1158756cff072d7e8cae10e20eafed1eec 100644 --- a/net/minecraft/world/item/ItemStack.java +++ b/net/minecraft/world/item/ItemStack.java @@ -377,31 +377,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; @@ -427,15 +428,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); } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement @@ -446,13 +447,13 @@ public final class ItemStack implements DataComponentHolder { interactionResult = InteractionResult.FAIL; // cancel placement // PAIL: Remove this when MC-99075 fixed player.containerMenu.forceHeldSlot(hand); - 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 for (org.bukkit.block.BlockState blockstate : blocks) { ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).revertPlace(); } - 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)) { @@ -460,7 +461,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()); } @@ -495,15 +496,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); } } @@ -530,8 +531,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 ba0b254d43651bca1f29b5272af05d068fc37ba8..fc1337a5ca7ebed7ad93e077779a95affe467366 100644 --- a/net/minecraft/world/item/MapItem.java +++ b/net/minecraft/world/item/MapItem.java @@ -68,6 +68,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; @@ -97,8 +98,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()) { @@ -180,6 +181,7 @@ public class MapItem extends Item { } } } + } // Folia - make map data thread-safe } private BlockState getCorrectStateForFluidBlock(Level level, BlockState state, BlockPos pos) { @@ -194,6 +196,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; @@ -263,6 +266,7 @@ public class MapItem extends Item { } } } + } // Folia - make map data thread-safe } } @@ -270,6 +274,7 @@ public class MapItem extends Item { public void inventoryTick(ItemStack stack, ServerLevel level, Entity entity, @Nullable EquipmentSlot slot) { MapItemSavedData savedData = getSavedData(stack, level); if (savedData != null) { + synchronized (savedData) { // Folia - region threading if (entity instanceof Player player) { savedData.tickCarriedBy(player, stack); } @@ -277,6 +282,7 @@ public class MapItem extends Item { if (!savedData.locked && slot != null && slot.getType() == EquipmentSlot.Type.HAND) { 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 049bb1419dc2d5cf7182dc78a2806264ffe47400..3a28c5e7a58ceb2082b35df955a6d824267a1bf9 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 13950339598f4ec705c54275342fa17ff2e74ca9..8b8ce26de104211728d4b64e641a2b471ce89073 100644 --- a/net/minecraft/world/level/BaseCommandBlock.java +++ b/net/minecraft/world/level/BaseCommandBlock.java @@ -22,7 +22,7 @@ import net.minecraft.world.level.storage.ValueOutput; 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 static final int NO_LAST_EXECUTION = -1; private long lastExecution = -1L; @@ -96,6 +96,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)) { @@ -152,11 +153,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 9175a7e4e6076626cb46144c5858c2f2474f1858..639afaf7c492b43241e919489ba8e7b549dee530 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 be16627136f7b3979e62308f58b96a2b16ee9f62..8b60d3cccdd0278416b9eb72a4598c782b67800d 100644 --- a/net/minecraft/world/level/Level.java +++ b/net/minecraft/world/level/Level.java @@ -106,10 +106,10 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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(); - protected final NeighborUpdater neighborUpdater; - private final List pendingBlockEntityTickers = Lists.newArrayList(); - private boolean tickingBlockEntities; + //public final List blockEntityTickers = Lists.newArrayList(); // 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; @@ -119,7 +119,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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; @@ -130,7 +130,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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 public final io.papermc.paper.antixray.ChunkPacketBlockController chunkPacketBlockController; // Paper - Anti-Xray @@ -138,20 +138,10 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl public boolean pvpMode; public @Nullable org.bukkit.generator.ChunkGenerator generator; - public boolean captureBlockStates = false; - public boolean captureTreeGeneration = false; - public Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper - public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates - @Nullable - 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 - EAR 2 - public int wakeupInactiveRemainingAnimals; - public int wakeupInactiveRemainingFlying; - public int wakeupInactiveRemainingMonsters; - public int wakeupInactiveRemainingVillagers; - // Paper end - EAR 2 - 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; @@ -161,9 +151,9 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl // Paper end - add paper world config public static @Nullable BlockPos lastPhysicsProblem; // Spigot - private int tileTickPosition; - public final Map explosionDensityCache = new java.util.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 java.util.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; @@ -812,6 +802,32 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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, @@ -873,7 +889,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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); @@ -1018,8 +1034,8 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl @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(); } @@ -1083,16 +1099,18 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl @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 = org.bukkit.craftbukkit.block.CapturedBlockState.getTreeBlockState(this, pos, flags); - this.capturedBlockStates.put(pos.immutable(), blockstate); + worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading } blockstate.setData(state); blockstate.setFlags(flags); @@ -1108,14 +1126,14 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl Block block = state.getBlock(); // CraftBukkit start - capture blockstates boolean captured = false; - if (this.captureBlockStates) { + if (worldData.captureBlockStates) { // Folia - region threading final CraftBlockState snapshot; - if (!this.capturedBlockStates.containsKey(pos)) { + if (!worldData.capturedBlockStates.containsKey(pos)) { // Folia - region threading snapshot = (CraftBlockState) org.bukkit.craftbukkit.block.CraftBlock.at(this, pos).getState(); // Paper - use CB getState to get a suitable snapshot - this.capturedBlockStates.put(pos.immutable(), snapshot); + worldData.capturedBlockStates.put(pos.immutable(), snapshot); // Folia - region threading captured = true; } else { - snapshot = this.capturedBlockStates.get(pos); + snapshot = worldData.capturedBlockStates.get(pos); // Folia - region threading } snapshot.setFlags(flags); // Paper - always set the flag of the most recent call to mitigate issues with multiple update at the same pos with different flags } @@ -1124,8 +1142,8 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl // CraftBukkit end 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; @@ -1162,7 +1180,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl */ // 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 { @@ -1206,7 +1224,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl // CraftBukkit start blockState.updateIndirectNeighbourShapes(this, pos, i, recursionLeft - 1); // Don't call an event for the old block to limit event spam boolean cancelledUpdates = false; // Paper - Fix block place logic - if (((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent + if (((ServerLevel)this).getCurrentWorldData().hasPhysicsEvent) { // Paper - BlockPhysicsEvent // Folia - region threading org.bukkit.event.block.BlockPhysicsEvent event = new org.bukkit.event.block.BlockPhysicsEvent(org.bukkit.craftbukkit.block.CraftBlock.at(this, pos), CraftBlockData.fromData(state)); cancelledUpdates = !event.callEvent(); // Paper - Fix block place logic } @@ -1299,7 +1317,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl @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 @@ -1323,11 +1341,34 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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(); } @@ -1439,17 +1480,16 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl } 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(); @@ -1457,8 +1497,8 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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 (this.tileTickPosition = 0; this.tileTickPosition < this.blockEntityTickers.size(); this.tileTickPosition++) { // Paper - Disable tick limiters - 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 @@ -1471,11 +1511,11 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl // 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) { @@ -1486,7 +1526,8 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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 @@ -1626,9 +1667,14 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl @Nullable @Override public BlockEntity getBlockEntity(BlockPos pos) { + // 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 @@ -1645,8 +1691,8 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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 @@ -1726,6 +1772,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl @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(); @@ -1755,6 +1802,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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) { @@ -1864,13 +1912,34 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl public void onBlockEntityAdded(BlockEntity entity) { } + @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(Entity entity, BlockPos pos) { return true; @@ -2051,8 +2120,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl 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)); } @@ -2073,7 +2141,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl @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 1c769bb3de2c3271db574e9cd81ccdfdb669e5f9..f22146c86dd916c16f683fd5b0a8e1205d51ff3b 100644 --- a/net/minecraft/world/level/LevelAccessor.java +++ b/net/minecraft/world/level/LevelAccessor.java @@ -32,14 +32,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 0842fd6488c8b27d98c4344e1244996b4c0e9912..3f4a675f5da2947fd59fe9206fb32197a745e90f 100644 --- a/net/minecraft/world/level/LevelReader.java +++ b/net/minecraft/world/level/LevelReader.java @@ -208,6 +208,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 4f647507d06d45a65b4b1793219d90c65e110225..8911322506794413f9c9e065b6c080759ab862b6 100644 --- a/net/minecraft/world/level/NaturalSpawner.java +++ b/net/minecraft/world/level/NaturalSpawner.java @@ -131,7 +131,7 @@ public final class NaturalSpawner { int limit = mobCategory.getMaxInstancesPerChunk(); org.bukkit.entity.SpawnCategory spawnCategory = org.bukkit.craftbukkit.util.CraftSpawnCategory.toBukkit(mobCategory); if (org.bukkit.craftbukkit.util.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 07e4025ca6c9c31905db2e6921138a0ded479dde..b3e897a4afee742b5055d652ad7047a660d8ea04 100644 --- a/net/minecraft/world/level/ServerExplosion.java +++ b/net/minecraft/world/level/ServerExplosion.java @@ -758,17 +758,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 3399922d79a713484e16beb6e4e9985c284ddfb5..63bf43e263ff9070bb2a7b7cc06014b773e9cd75 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(); @@ -120,29 +115,16 @@ public class StructureManager { } public StructureStart getStructureWithPieceAt(BlockPos pos, HolderSet structures) { - // Paper start - Fix swamp hut cat generation deadlock - return this.getStructureWithPieceAt(pos, structures, null); - } - public StructureStart getStructureWithPieceAt(BlockPos pos, HolderSet structures, final @Nullable ServerLevelAccessor levelAccessor) { - return this.getStructureWithPieceAt(pos, structures::contains, levelAccessor); - // Paper end - Fix swamp hut cat generation deadlock + return this.getStructureWithPieceAt(pos, structures::contains); // Folia - region threading } 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; @@ -187,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 e72ead9ff6541f821a61ad3c50fe068a8c8bb69d..65c586322b4b695184a1314b973931a41f1ceb58 100644 --- a/net/minecraft/world/level/block/BedBlock.java +++ b/net/minecraft/world/level/block/BedBlock.java @@ -334,7 +334,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 be6f37f91569c659c609e5e8d38671ca86f8cd95..644b9fab1d6729bf3964ebee0d6e9c46f89b8c46 100644 --- a/net/minecraft/world/level/block/Block.java +++ b/net/minecraft/world/level/block/Block.java @@ -408,8 +408,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/DaylightDetectorBlock.java b/net/minecraft/world/level/block/DaylightDetectorBlock.java index a51f40f6f53d43c65837d60f5639b14f35e40b67..8764d272fcd8668bde185ebb7f23347b1bff1c60 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/DoublePlantBlock.java b/net/minecraft/world/level/block/DoublePlantBlock.java index c60f2f1fe80d6c7ffd25c90da4e757065a3bd138..625b4bdaf00dea483c60a02e2f081739d6dda192 100644 --- a/net/minecraft/world/level/block/DoublePlantBlock.java +++ b/net/minecraft/world/level/block/DoublePlantBlock.java @@ -116,7 +116,7 @@ public class DoublePlantBlock extends VegetationBlock { 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 68914268ca9350a6c1d794e011e1f9a8aecd609c..f36dafbe1e816736e1c0aeb7cf830b120292a8f5 100644 --- a/net/minecraft/world/level/block/EndGatewayBlock.java +++ b/net/minecraft/world/level/block/EndGatewayBlock.java @@ -112,17 +112,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 f6c64277c3d6e16250e2bf963b6427404e27aa9b..6e495b2c4176d5a30ea911f8e44a22655df92ef6 100644 --- a/net/minecraft/world/level/block/EndPortalBlock.java +++ b/net/minecraft/world/level/block/EndPortalBlock.java @@ -65,7 +65,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 { @@ -118,6 +118,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 a31ac95237515b874f59941ec24c74e2c357b3d8..c9c643f6313a051738ae3b27299c9c7bfb2d7195 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(MOISTURE, moistureValue - 1), 2); // CraftBukkit diff --git a/net/minecraft/world/level/block/FungusBlock.java b/net/minecraft/world/level/block/FungusBlock.java index 9711efb088bd0da9168e9bcd0496bd7caddd2974..98714fac58961d21636001bf422bae09223d14da 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 VegetationBlock 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 a98c308c5febd458d6489174b94898cd4b9bae69..7047ed121419a0461122d083139cdacfaaa29f2e 100644 --- a/net/minecraft/world/level/block/HoneyBlock.java +++ b/net/minecraft/world/level/block/HoneyBlock.java @@ -95,7 +95,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 13b92472d50a41446b86c62ec31919fe0a953374..d702d234111c2a34585f3532b5bb4bdb1501b82a 100644 --- a/net/minecraft/world/level/block/LightningRodBlock.java +++ b/net/minecraft/world/level/block/LightningRodBlock.java @@ -114,7 +114,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 d306f5f524dc64618df94c9783c2168dc561a5e3..54901fc126910b599d1dcf72b86269dd3df7f2bc 100644 --- a/net/minecraft/world/level/block/MushroomBlock.java +++ b/net/minecraft/world/level/block/MushroomBlock.java @@ -93,7 +93,7 @@ public class MushroomBlock extends VegetationBlock 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 6c5629a6f5f91496a55eb0bf281ceae1567915b1..d0b09c4588bf8ba4aac93d2895a585ef63360148 100644 --- a/net/minecraft/world/level/block/NetherPortalBlock.java +++ b/net/minecraft/world/level/block/NetherPortalBlock.java @@ -177,6 +177,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 @@ -184,14 +211,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 @@ -234,7 +261,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 1943a6aad888647953e2d9dbbeedb0bd81c6f9df..fde784fac9ee933d811d70701cb71684c816e966 100644 --- a/net/minecraft/world/level/block/RedStoneWireBlock.java +++ b/net/minecraft/world/level/block/RedStoneWireBlock.java @@ -65,7 +65,7 @@ public class RedStoneWireBlock extends Block { private final Function shapes; 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() { @@ -268,6 +268,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 @@ -283,7 +288,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); @@ -311,7 +316,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 } } } @@ -328,9 +333,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; } @@ -422,12 +427,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; @@ -459,7 +464,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 33e2f2de19208b7af6551132887e310744b4b702..df4f1b18deb8bb4a04e740bbbeb6a89cb1ff8e71 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 a22cb810622e0ae97bc2a0d6390d026d9482b783..cd2ae1005e73419216521beee9b8b5078823f221 100644 --- a/net/minecraft/world/level/block/SaplingBlock.java +++ b/net/minecraft/world/level/block/SaplingBlock.java @@ -25,7 +25,7 @@ public class SaplingBlock extends VegetationBlock implements BonemealableBlock { public static final IntegerProperty STAGE = BlockStateProperties.STAGE; private static final VoxelShape SHAPE = Block.column(12.0, 0.0, 12.0); protected final TreeGrower treeGrower; - public static @javax.annotation.Nullable org.bukkit.TreeType treeType; // CraftBukkit + public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // CraftBukkit // Folia - region threading @Override public MapCodec codec() { @@ -55,18 +55,19 @@ public class SaplingBlock extends VegetationBlock implements BonemealableBlock { level.setBlock(pos, state.cycle(STAGE), 260); } 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 f8cc3f870d88715386bb4de0da81a74fa04fd216..b428391a915197ce0a0ac9ee4d3e69092748faf4 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/VegetationBlock.java b/net/minecraft/world/level/block/VegetationBlock.java index 0aa7c1084fd6ed35260469572cdd914d87f3922a..55beb3206f499d6802e6eb011e8636190f36ab31 100644 --- a/net/minecraft/world/level/block/VegetationBlock.java +++ b/net/minecraft/world/level/block/VegetationBlock.java @@ -38,7 +38,7 @@ public abstract class VegetationBlock 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/WitherSkullBlock.java b/net/minecraft/world/level/block/WitherSkullBlock.java index 0b6debe0e55e404e6f34b3bc437fe7c7a30cec7c..1c172802b065d644823d4a9e89fbc912b2ccff3c 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 503c8625cd77a564e55fc1427137d43a5cc12d9e..b27a40e69046414d5302882b72ac8b3f848e18df 100644 --- a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +++ b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java @@ -215,7 +215,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); } @@ -350,7 +350,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 5986825d6a381eeb445dd424dd127864aa703163..63ac5759a8a4b59297d5eb1abd362fb2a54353d7 100644 --- a/net/minecraft/world/level/block/entity/BlockEntity.java +++ b/net/minecraft/world/level/block/entity/BlockEntity.java @@ -35,7 +35,7 @@ import net.minecraft.world.level.storage.ValueOutput; 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 final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer; @@ -50,6 +50,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(); @@ -228,7 +234,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 4bfe113d4444dced56e21353feb54b8df6e2ce62..d92564d51d81ad4a9d4a05bce69d9ce5eb552600 100644 --- a/net/minecraft/world/level/block/entity/CommandBlockEntity.java +++ b/net/minecraft/world/level/block/entity/CommandBlockEntity.java @@ -70,6 +70,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 0e87d20639c382be2221d73c7498480d21ebeafb..0d7d2fe54628ae6ef3d579d6c3d031b915d65cd2 100644 --- a/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +++ b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java @@ -77,7 +77,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); @@ -93,7 +93,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 01ed25d1f895d94485b5fecd98476534cbb26930..25ad8bb4099fefaf51f38a8bba70ea3594d0d914 100644 --- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java +++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java @@ -35,7 +35,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen private static final int NO_COOLDOWN_TIME = -1; 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 @@ -72,6 +72,16 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen } // CraftBukkit end + // Folia start - region threading + @Override + public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { + super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + if (this.tickedGameTime != Long.MIN_VALUE) { + this.tickedGameTime += fromRedstoneTimeOffset; + } + } + // Folia end - region threading + public HopperBlockEntity(BlockPos pos, BlockState blockState) { super(BlockEntityType.HOPPER, pos, blockState); this.facing = blockState.getValue(HopperBlock.FACING); @@ -129,7 +139,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 @@ -217,12 +227,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); @@ -237,7 +246,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); @@ -267,13 +276,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); @@ -293,9 +303,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; } @@ -310,6 +320,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(), @@ -319,7 +330,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); @@ -335,6 +346,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); @@ -342,7 +354,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); @@ -528,12 +540,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 @@ -682,9 +695,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 0a94670dc20bb9c521b0395633eb100393895f6a..91ddb30cd98b11f70b196a108e7cc11d39ca1a71 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 c8b577a5d5f059aed368fa082bc4518cc2dbd0b0..12b9a6c16501cb0c01983c44bb247071ce675274 100644 --- a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +++ b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java @@ -38,9 +38,12 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { public long age = 0L; private int teleportCooldown; @Nullable - public BlockPos exitPortal; + public volatile BlockPos exitPortal; // Folia - region threading - volatile public boolean exactTeleport = false; + 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.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( + net.minecraft.server.level.TicketType.DELAYED, access.getPos(), + ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, + null + ); + } + // 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 d23f255de9208f42125fa358a9e8194c984fe4d3..8e1644801b0c044e02cf536abc562f91e74becce 100644 --- a/net/minecraft/world/level/block/grower/TreeGrower.java +++ b/net/minecraft/world/level/block/grower/TreeGrower.java @@ -203,55 +203,57 @@ public final class TreeGrower { // CraftBukkit start private void setTreeType(Holder> feature) { + org.bukkit.TreeType treeType; // Folia - region threading if (feature.is(TreeFeatures.OAK) || feature.is(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 (feature.is(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 (feature.is(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 (feature.is(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 (feature.is(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 (feature.is(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 (feature.is(TreeFeatures.SPRUCE)) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD; + treeType = org.bukkit.TreeType.REDWOOD; // Folia - region threading } else if (feature.is(TreeFeatures.ACACIA)) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA; + treeType = org.bukkit.TreeType.ACACIA; // Folia - region threading } else if (feature.is(TreeFeatures.BIRCH) || feature.is(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 (feature.is(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 (feature.is(TreeFeatures.SWAMP_OAK)) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP; + treeType = org.bukkit.TreeType.SWAMP; // Folia - region threading } else if (feature.is(TreeFeatures.FANCY_OAK) || feature.is(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 (feature.is(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 (feature.is(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 (feature.is(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 (feature.is(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 (feature.is(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 (feature.is(TreeFeatures.AZALEA_TREE)) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA; + treeType = org.bukkit.TreeType.AZALEA; // Folia - region threading } else if (feature.is(TreeFeatures.MANGROVE)) { - net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE; + treeType = org.bukkit.TreeType.MANGROVE; // Folia - region threading } else if (feature.is(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 (feature.is(TreeFeatures.CHERRY) || feature.is(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 (feature.is(TreeFeatures.PALE_OAK) || feature.is(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 (feature.is(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 " + feature); } + 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 10484ee85482e86be5e0a09d8202df600a32092e..105ce7992d4159a8b9fba0866e2557ac148c911b 100644 --- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java +++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java @@ -116,7 +116,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 f1ce4cff1c03a0037ade2c8ef989cf327c973a7e..bff9574bece5f5581d9cc51a3f2b68b5c13c25c9 100644 --- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java @@ -44,9 +44,19 @@ public class PistonMovingBlockEntity extends BlockEntity { private static final ThreadLocal NOCLIP = ThreadLocal.withInitial(() -> null); private float progress = 0.0F; private float progressO = 0.0F; - 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); } @@ -153,8 +163,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; } @@ -302,7 +312,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 449c24a18209b111253e18f573ad2c067dfdf453..f46950552071508a6565771b2ade11aa33e8277b 100644 --- a/net/minecraft/world/level/border/WorldBorder.java +++ b/net/minecraft/world/level/border/WorldBorder.java @@ -33,6 +33,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()); } @@ -46,16 +48,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 854578c7880dc124980142941ee471072668c8e2..92d13f9b886114553b471271391255ba08c8108f 100644 --- a/net/minecraft/world/level/chunk/ChunkGenerator.java +++ b/net/minecraft/world/level/chunk/ChunkGenerator.java @@ -326,7 +326,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 75578e6ed7233a03d9b6cd3c6d3997f1c6148392..e8914d8c3c0e69d9e5ce97345eb9f24b05b4845f 100644 --- a/net/minecraft/world/level/chunk/LevelChunk.java +++ b/net/minecraft/world/level/chunk/LevelChunk.java @@ -62,6 +62,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; @@ -233,11 +240,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 @Override public BlockState setBlockState(BlockPos pos, BlockState state, int flags) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, pos, "Updating block asynchronously"); // Folia - region threading int y = pos.getY(); LevelChunkSection section = this.getSection(this.getSectionIndex(y)); boolean hasOnlyAir = section.hasOnlyAir(); @@ -414,7 +418,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 && (flags & 512) == 0 && (!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 && (flags & 512) == 0 && (!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, flag1); } @@ -469,7 +473,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); } @@ -657,13 +661,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)); @@ -689,7 +693,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; @@ -916,6 +920,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()) { @@ -994,6 +1005,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 e04d3479383cd480cf35ed7ac3c82e7f6fb69e28..b80433ebfd9a7a50eccaa4b4905f7913cab825a4 100644 --- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java @@ -543,7 +543,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 7f9bd256da15da915db1c45099572d9eb3286166..23072c9ced73311954863592b0458c9662a551aa 100644 --- a/net/minecraft/world/level/dimension/end/EndDragonFight.java +++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java @@ -76,7 +76,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; @@ -161,7 +161,7 @@ public class EndDragonFight { if (!this.dragonEvent.getPlayers().isEmpty()) { this.level.getChunkSource().addTicketWithRadius(TicketType.DRAGON, new ChunkPos(0, 0), 9); - 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; @@ -207,6 +207,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 { @@ -322,8 +328,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; } @@ -497,6 +503,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; @@ -522,7 +533,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 c2e37714bae008087b0b5086e51d675beb672ca4..4b8b71f07096b27f3508b637eedcf2be60174fff 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 void tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { @@ -24,14 +24,15 @@ public class PatrolSpawner implements CustomSpawner { if (spawnEnemies) { if (level.getGameRules().getBoolean(GameRules.RULE_DO_PATROL_SPAWNING)) { RandomSource randomSource = level.random; + io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading // this.nextTick--; // Paper start - Pillager patrol spawn settings and per player options - int size = level.players().size(); + int size = level.getLocalPlayers().size(); // Folia - region threading if (size < 1) { return; } - 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; } @@ -41,8 +42,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) { long days; @@ -54,7 +55,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.isBrightOutside()) { diff --git a/net/minecraft/world/level/levelgen/PhantomSpawner.java b/net/minecraft/world/level/levelgen/PhantomSpawner.java index b3cfab9c82fe12694f5d2f56bd922828e51df680..ecfd6a04ea1bac75bbb7e88554b73fc67e7f7358 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 void tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { @@ -31,15 +31,16 @@ 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 // 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()) { - 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 dfe8b815987a0ee385846616e4845428a81f2782..49b810ae9d9a8d0718a5f8c512e15a5573ed04fd 100644 --- a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java +++ b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java @@ -46,7 +46,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.getSnapshotBlocks(), 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 f9a15c3769f29af1952ef880f6fcd2612119ecf0..d250e7c6d4c31507096f5e87e4162dfe5872bc64 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 a705f7f81c5e426c2c0918e5262d49f9fb14262c..3aae5023727868bf3b1d51bb8b96acb6e7d68bb0 100644 --- a/net/minecraft/world/level/saveddata/SavedData.java +++ b/net/minecraft/world/level/saveddata/SavedData.java @@ -5,7 +5,7 @@ import javax.annotation.Nullable; import net.minecraft.server.level.ServerLevel; public abstract class SavedData { - private boolean dirty; + private volatile boolean dirty; // Folia - make map data thread-safe public void setDirty() { this.setDirty(true); diff --git a/net/minecraft/world/level/saveddata/maps/MapIndex.java b/net/minecraft/world/level/saveddata/maps/MapIndex.java index 06025d79cc2297119b22224d777aca79f9d3d9c1..610d16bf5a218796ffcd17789299a0b7f4d9e75d 100644 --- a/net/minecraft/world/level/saveddata/maps/MapIndex.java +++ b/net/minecraft/world/level/saveddata/maps/MapIndex.java @@ -9,21 +9,21 @@ import net.minecraft.world.level.saveddata.SavedDataType; public class MapIndex extends SavedData { private static final int NO_MAP_ID = -1; public static final Codec CODEC = RecordCodecBuilder.create( - instance -> instance.group(Codec.INT.optionalFieldOf("map", -1).forGetter(mapIndex -> mapIndex.lastMapId)).apply(instance, MapIndex::new) + instance -> instance.group(Codec.INT.optionalFieldOf("map", -1).forGetter(mapIndex -> mapIndex.lastMapId.get())).apply(instance, MapIndex::new) // Folia - make map data thread-safe ); public static final SavedDataType TYPE = new SavedDataType<>("idcounts", MapIndex::new, CODEC, DataFixTypes.SAVED_DATA_MAP_INDEX); - private int lastMapId; + private final java.util.concurrent.atomic.AtomicInteger lastMapId = new java.util.concurrent.atomic.AtomicInteger(); // Folia - make map data thread-safe public MapIndex() { this(-1); } public MapIndex(int lastMapId) { - this.lastMapId = lastMapId; + this.lastMapId.set(lastMapId); // Folia - make map data thread-safe } public MapId getNextMapId() { - MapId mapId = new MapId(++this.lastMapId); + MapId mapId = new MapId(this.lastMapId.incrementAndGet()); // Folia - make map data thread-safe this.setDirty(); return mapId; } diff --git a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java index c8f23011a8942a5be970c606f67142cbd202b97e..9f359db7ff21e6e80366b42ddfd98fa6ade100b1 100644 --- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java @@ -176,7 +176,7 @@ public class MapItemSavedData extends SavedData { return new MapItemSavedData(0, 0, scale, false, false, locked, dimension); } - 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 ); @@ -187,7 +187,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); } @@ -196,7 +196,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); @@ -348,7 +349,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; @@ -382,7 +383,7 @@ 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); } @@ -391,20 +392,21 @@ public class MapItemSavedData extends SavedData { // Paper start - Fix unnecessary map data saves this.setColorsDirty(x, z, true); } - public void setColorsDirty(int x, int z, boolean markFileDirty) { - if (markFileDirty) this.setDirty(); + public synchronized void setColorsDirty(int x, int z, boolean markFileDirty) { // Folia - make map data thread-safe + //if (markFileDirty) this.setDirty(); // Folia - make map data thread-safe - move down // Paper end - Fix unnecessary map data saves for (MapItemSavedData.HoldingPlayer holdingPlayer : this.carriedBy) { holdingPlayer.markColorsDirty(x, z); } + if (markFileDirty) this.setDirty(); // Folia - make map data thread-safe - moved down } - public void setDecorationsDirty() { + public synchronized void setDecorationsDirty() { // Folia - make map data thread-safe this.carriedBy.forEach(MapItemSavedData.HoldingPlayer::markDecorationsDirty); } - 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); @@ -415,7 +417,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; @@ -423,7 +425,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; } @@ -445,7 +447,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()) { @@ -465,13 +467,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); @@ -481,12 +483,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; @@ -496,7 +498,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; @@ -513,7 +515,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; } @@ -668,11 +670,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 1ff0b19a077333ad9da9cab7f5a891eeaa2109a9..225eb4479094771ad6c67a1298ede1c40f9adce9 100644 --- a/net/minecraft/world/level/storage/DimensionDataStorage.java +++ b/net/minecraft/world/level/storage/DimensionDataStorage.java @@ -57,6 +57,7 @@ public class DimensionDataStorage implements AutoCloseable { } public T computeIfAbsent(SavedDataType type) { + synchronized (this.cache) { // Folia - make map data thread-safe T savedData = this.get(type); if (savedData != null) { return savedData; @@ -65,10 +66,12 @@ public class DimensionDataStorage implements AutoCloseable { this.set(type, savedData1); return savedData1; } + } // Folia - make map data thread-safe } @Nullable public T get(SavedDataType type) { + synchronized (this.cache) { // Folia - make map data thread-safe Optional optional = this.cache.get(type); if (optional == null) { optional = Optional.ofNullable(this.readSavedData(type)); @@ -76,6 +79,7 @@ public class DimensionDataStorage implements AutoCloseable { } return (T)optional.orElse(null); + } // Folia - make map data thread-safe } @Nullable @@ -99,8 +103,10 @@ public class DimensionDataStorage implements AutoCloseable { } public void set(SavedDataType type, T value) { + synchronized (this.cache) { // Folia - make map data thread-safe this.cache.put(type, Optional.of(value)); value.setDirty(); + } // Folia - make map data thread-safe } public CompoundTag readTagFromDisk(String filename, DataFixTypes dataFixType, int version) throws IOException { @@ -194,7 +200,9 @@ public class DimensionDataStorage implements AutoCloseable { private CompoundTag encodeUnchecked(SavedDataType type, SavedData data, RegistryOps ops) { Codec codec = type.codec().apply(this.context); CompoundTag compoundTag = new CompoundTag(); + synchronized(data) { // Folia - make map data thread-safe compoundTag.put("data", codec.encodeStart(ops, (T)data).getOrThrow()); + } // Folia - make map data thread-safe NbtUtils.addCurrentDataVersion(compoundTag); return compoundTag; } diff --git a/net/minecraft/world/ticks/LevelChunkTicks.java b/net/minecraft/world/ticks/LevelChunkTicks.java index 66d0a6390febe929ef774b0a7813329015bc8cc2..cadbbb0091cf93e1fa6f5e14bdc0a288510d720d 100644 --- a/net/minecraft/world/ticks/LevelChunkTicks.java +++ b/net/minecraft/world/ticks/LevelChunkTicks.java @@ -44,6 +44,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 7ea0273018b1a364a5e2b7bf57da840383c8ffa5..58ec3956dcd04c26aad7863e34b3d6871f1821d6 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) {