Folia/folia-server/minecraft-patches/features/0001-Region-Threading-Base.patch

19829 lines
1.0 MiB
Diff
Raw Normal View History

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sun, 20 Apr 1997 05:37:42 -0800
Subject: [PATCH] Region Threading Base
diff --git a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
index 1b8193587814225c2ef2c5d9e667436eb50ff6c5..0027a3896c0cfce2f46eca8a0a77a90223723dc7 100644
--- a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
@@ -244,7 +244,7 @@ public final class NearbyPlayers {
created.addPlayer(parameter, type);
type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
- ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created;
+ //((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created; // Folia - region threading
}
}
@@ -263,10 +263,7 @@ public final class NearbyPlayers {
if (chunk.isEmpty()) {
NearbyPlayers.this.byChunk.remove(chunkKey);
- final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey);
- if (chunkData != null) {
- chunkData.nearbyPlayers = null;
- }
+ // Folia - region threading
}
}
}
diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java
index 4d344559a20a0c35c181e297e81788c747363ec9..779e6b7d025da185b33a963e42e91a56908e46dc 100644
--- a/ca/spottedleaf/moonrise/paper/PaperHooks.java
+++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java
@@ -105,7 +105,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo
}
for (final EnderDragonPart part : parts) {
- if (part != entity && part.getBoundingBox().intersects(boundingBox) && (predicate == null || predicate.test(part))) {
+ if (part != entity && part.getBoundingBox().intersects(boundingBox) && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(part) && (predicate == null || predicate.test(part))) { // Folia - region threading
into.add(part);
}
}
@@ -127,7 +127,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo
continue;
}
final T casted = (T)entityTypeTest.tryCast(part);
- if (casted != null && (predicate == null || predicate.test(casted))) {
+ if (casted != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(part) && (predicate == null || predicate.test(casted))) { // Folia - region threading
into.add(casted);
if (into.size() >= maxCount) {
break;
@@ -275,4 +275,4 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo
public int modifyEntityTrackingRange(final Entity entity, final int currentRange) {
return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
}
-}
\ No newline at end of file
+}
diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
index ece1261b67033e946dfc20a96872708755bffe0a..8d67b4629c69d3039b199aaad45533d1acde114e 100644
--- a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
+++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
@@ -80,18 +80,23 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
@Override
public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
-
+ // Folia start - threaded regions
+ level.regioniser.addChunk(holder.getPos().x, holder.getPos().z);
+ // Folia end - threaded regions
}
@Override
public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
// Update progress listener for LevelLoadingScreen
- final net.minecraft.server.level.progress.ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener;
+ final net.minecraft.server.level.progress.ChunkProgressListener progressListener = null; // Folia - threaded regions - cannot schedule chunk task here; as it would create a chunkholder
if (progressListener != null) {
this.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> {
progressListener.onStatusChange(holder.getPos(), null);
});
}
+ // Folia start - threaded regions
+ level.regioniser.removeChunk(holder.getPos().x, holder.getPos().z);
+ // Folia end - threaded regions
}
@Override
@@ -102,17 +107,13 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
@Override
public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add(
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
- );
+ chunk.getLevel().getCurrentWorldData().addChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
chunk.loadCallback();
}
@Override
public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove(
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
- );
+ chunk.getLevel().getCurrentWorldData().removeChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
chunk.unloadCallback();
}
@@ -124,9 +125,7 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
@Override
public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add(
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
- );
+ chunk.getLevel().getCurrentWorldData().addTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
chunk.postProcessGeneration((ServerLevel)chunk.getLevel());
}
@@ -137,24 +136,18 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
@Override
public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove(
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
- );
+ chunk.getLevel().getCurrentWorldData().removeTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration
}
@Override
public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add(
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
- );
+ chunk.getLevel().getCurrentWorldData().addEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
}
@Override
public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove(
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
- );
+ chunk.getLevel().getCurrentWorldData().removeEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
}
@Override
@@ -191,4 +184,4 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
public void updateMaps(final ServerLevel world, final ServerPlayer player) {
((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
}
-}
\ No newline at end of file
+}
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
index 8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa..306216138e21c41937e4728e8004220a02d6ea4b 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
@@ -5,7 +5,7 @@ import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
public final class ChunkData {
private int referenceCount = 0;
- public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players
+ //public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players // Folia - region threading
public ChunkData() {
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
index 7554c109c35397bc1a43dd80e87764fd78645bbf..db16fe8d664f9b04710200d63439564cb97c0066 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
@@ -460,6 +460,19 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
return slices == null || !slices.isPreventingStatusUpdates();
}
+ // Folia start - region threading
+ // only appropriate to use when in shutdown, as this performs no logic hooks to properly add to world
+ public boolean addEntityForShutdownTeleportComplete(final Entity entity) {
+ final BlockPos pos = entity.blockPosition();
+ final int sectionX = pos.getX() >> 4;
+ final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world));
+ final int sectionZ = pos.getZ() >> 4;
+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
+
+ return slices.addEntity(entity, sectionY);
+ }
+ // Folia end - region threading
+
protected void removeEntity(final Entity entity) {
final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
@@ -986,6 +999,9 @@ public abstract class EntityLookup implements LevelEntityGetter<Entity> {
EntityLookup.this.removeEntityCallback(entity);
this.entity.setLevelCallback(NoOpCallback.INSTANCE);
+
+ // only AFTER full removal callbacks, so that thread checking will work. // Folia - region threading
+ EntityLookup.this.world.getCurrentWorldData().removeEntity(entity); // Folia - region threading
}
}
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
index 26207443b1223119c03db478d7e816d9cdf8e618..a89ee24c6aed46af23c5de7ae2234c7902f1c0f4 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
@@ -17,7 +17,7 @@ public final class ServerEntityLookup extends EntityLookup {
private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0];
private final ServerLevel serverWorld;
- public final ReferenceList<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
+ // Folia - move to regionized world data
public ServerEntityLookup(final ServerLevel world, final LevelCallback<Entity> worldCallback) {
super(world, worldCallback);
@@ -75,6 +75,7 @@ public final class ServerEntityLookup extends EntityLookup {
if (entity instanceof ServerPlayer player) {
((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player);
}
+ this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading
}
@Override
@@ -87,14 +88,14 @@ public final class ServerEntityLookup extends EntityLookup {
@Override
protected void entityStartLoaded(final Entity entity) {
// Moonrise start - entity tracker
- this.trackerEntities.add(entity);
+ this.world.getCurrentWorldData().trackerEntities.add(entity); // Folia - region threading
// Moonrise end - entity tracker
}
@Override
protected void entityEndLoaded(final Entity entity) {
// Moonrise start - entity tracker
- this.trackerEntities.remove(entity);
+ this.world.getCurrentWorldData().trackerEntities.remove(entity); // Folia - region threading
// Moonrise end - entity tracker
}
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
index dd2509996bfd08e8c3f9f2be042229eac6d7692d..f77dcf5a42ff34a1624ddf16bcce2abee81194bb 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
@@ -216,7 +216,7 @@ public final class RegionizedPlayerChunkLoader {
final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
if (loader == null) {
- return;
+ throw new IllegalStateException("Player is already removed from player chunk loader"); // Folia - region threading
}
loader.remove();
@@ -304,7 +304,7 @@ public final class RegionizedPlayerChunkLoader {
public void tick() {
TickThread.ensureTickThread("Cannot tick player chunk loader async");
long currTime = System.nanoTime();
- for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.getLocalPlayers())) { // Folia - region threding
final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
if (loader == null || loader.removed || loader.world != this.world) {
// not our problem anymore
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
index 7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309..4bfcae47ed76346e6200514ebce5b04f907c5026 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
@@ -29,6 +29,39 @@ public final class ChunkUnloadQueue {
public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {}
+ // Folia start - threaded regions
+ public List<SectionToUnload> retrieveForCurrentRegion() {
+ final io.papermc.paper.threadedregions.ThreadedRegionizer.ThreadedRegion<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> region =
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion();
+ final io.papermc.paper.threadedregions.ThreadedRegionizer<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> regionizer = region.regioniser;
+ final int shift = this.coordinateShift;
+
+ final List<SectionToUnload> ret = new ArrayList<>();
+
+ for (final Iterator<ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection>> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) {
+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry<UnloadSection> 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<SectionToUnload> retrieveForAllRegions() {
final List<SectionToUnload> ret = new ArrayList<>();
@@ -141,4 +174,4 @@ public final class ChunkUnloadQueue {
this.order = order;
}
}
-}
\ No newline at end of file
+}
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
index b5817aa8f537593f6d9fc6b612c82ccccb250ac7..aae97116a22a87cffd4756d566da3acd96ce2ae0 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
@@ -56,6 +56,14 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Predicate;
+// Folia start - region threading
+import io.papermc.paper.threadedregions.RegionizedServer;
+import io.papermc.paper.threadedregions.ThreadedRegionizer;
+import io.papermc.paper.threadedregions.TickRegionScheduler;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+// Folia end - region threading
+
public final class ChunkHolderManager {
private static final Logger LOGGER = LogUtils.getClassLogger();
@@ -78,29 +86,83 @@ public final class ChunkHolderManager {
private final ConcurrentLong2ReferenceChainedHashTable<NewChunkHolder> 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<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
+ private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
+ if (c1 == c2) {
+ return 0;
+ }
- private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
- private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
- if (c1 == c2) {
- return 0;
+ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
+
+ if (saveTickCompare != 0) {
+ return saveTickCompare;
+ }
+
+ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
+ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
+
+ if (coord1 == coord2) {
+ throw new IllegalStateException("Duplicate chunkholder in auto save queue");
+ }
+
+ return Long.compare(coord1, coord2);
+ });
+
+ public void merge(final HolderManagerRegionData into, final long tickOffset) {
+ // Order doesn't really matter for the pending full update...
+ into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate);
+
+ // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating
+ // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant
+ // addition to every entry will not affect compareTo).
+ for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) {
+ holder.lastAutoSave += tickOffset;
+ into.autoSaveQueue.add(holder);
+ }
}
- final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
+ public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap<HolderManagerRegionData> regionToData,
+ final ReferenceOpenHashSet<HolderManagerRegionData> dataSet) {
+ for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) {
+ final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift;
+ final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift;
+
+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
+ if (data != null) {
+ data.pendingFullLoadUpdate.add(fullLoadUpdate);
+ } // else: fullLoadUpdate is an unloaded chunk holder
+ }
- if (saveTickCompare != 0) {
- return saveTickCompare;
+ for (final NewChunkHolder autoSave : this.autoSaveQueue) {
+ final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift;
+ final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift;
+
+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
+ if (data != null) {
+ data.autoSaveQueue.add(autoSave);
+ } // else: autoSave is an unloaded chunk holder
+ }
}
+ }
- final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
- final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
+ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() {
+ final ThreadedRegionizer.ThreadedRegion<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> region =
+ TickRegionScheduler.getCurrentRegion();
- if (coord1 == coord2) {
- throw new IllegalStateException("Duplicate chunkholder in auto save queue");
+ if (region == null) {
+ return null;
}
- return Long.compare(coord1, coord2);
- });
+ if (this.world != null && this.world != region.getData().world) {
+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
+ }
+
+ return region.getData().getHolderManagerRegionData();
+ }
+ // Folia end - region threading
+
public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
this.world = world;
@@ -185,8 +247,13 @@ public final class ChunkHolderManager {
}
public void close(final boolean save, final boolean halt) {
+ // Folia start - region threading
+ this.close(save, halt, true, true, true);
+ }
+ public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) {
+ // Folia end - region threading
TickThread.ensureTickThread("Closing world off-main");
- if (halt) {
+ if (first && halt) { // Folia - region threading
LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
@@ -196,9 +263,10 @@ public final class ChunkHolderManager {
}
if (save) {
- this.saveAllChunks(true, true, true);
+ this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading
}
+ if (last) { // Folia - region threading
MoonriseRegionFileIO.flush(this.world);
if (halt) {
@@ -220,28 +288,35 @@ public final class ChunkHolderManager {
}
this.taskScheduler.setShutdown(true);
+ } // Folia - region threading
}
void ensureInAutosave(final NewChunkHolder holder) {
- if (!this.autoSaveQueue.contains(holder)) {
- holder.lastAutoSave = this.currentTick;
- this.autoSaveQueue.add(holder);
+ // Folia start - region threading
+ final HolderManagerRegionData regionData = this.getCurrentRegionData();
+ if (!regionData.autoSaveQueue.contains(holder)) {
+ holder.lastAutoSave = RegionizedServer.getCurrentTick();
+ regionData.autoSaveQueue.add(holder);
+ // Folia end - region threading
}
}
public void autoSave() {
final List<NewChunkHolder> reschedule = new ArrayList<>();
- final long currentTick = this.currentTick;
+ final long currentTick = RegionizedServer.getCurrentTick(); // Folia - region threading
final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval(this.world));
final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world);
- for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
- final NewChunkHolder holder = this.autoSaveQueue.first();
+ // Folia start - region threading
+ final HolderManagerRegionData regionData = this.getCurrentRegionData();
+ for (int autoSaved = 0; autoSaved < maxToSave && !regionData.autoSaveQueue.isEmpty();) {
+ final NewChunkHolder holder = regionData.autoSaveQueue.first();
+ // Folia end - region threading
if (holder.lastAutoSave > maxSaveTime) {
break;
}
- this.autoSaveQueue.remove(holder);
+ regionData.autoSaveQueue.remove(holder); // Folia - region threading
holder.lastAutoSave = currentTick;
if (holder.save(false) != null) {
@@ -255,15 +330,38 @@ public final class ChunkHolderManager {
for (final NewChunkHolder holder : reschedule) {
if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
- this.autoSaveQueue.add(holder);
+ regionData.autoSaveQueue.add(holder); // Folia start - region threading
}
}
}
public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
- final List<NewChunkHolder> 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<NewChunkHolder> holders = new java.util.ArrayList<>(this.chunkHolders.size() / 10);
+ // we could iterate through all chunk holders with thread checks, however for many regions the iteration cost alone
+ // will multiply. to avoid this, we can simply iterate through all owned sections
+ final int regionShift = this.world.moonrise$getRegionChunkShift();
+ final int width = 1 << regionShift;
+ for (final LongIterator iterator = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
+ final long sectionKey = iterator.nextLong();
+ final int offsetX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
+ final int offsetZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
+
+ for (int dz = 0; dz < width; ++dz) {
+ for (int dx = 0; dx < width; ++dx) {
+ final NewChunkHolder holder = this.getChunkHolder(offsetX | dx, offsetZ | dz);
+ if (holder != null) {
+ holders.add(holder);
+ }
+ }
+ }
+ }
+ // Folia end - region threading
- if (logProgress) {
+ if (first && logProgress) { // Folia - region threading
LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'");
}
@@ -292,6 +390,12 @@ public final class ChunkHolderManager {
}
for (int i = 0, len = holders.size(); i < len; ++i) {
final NewChunkHolder holder = holders.get(i);
+ // Folia start - region threading
+ if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) {
+ // skip holders that would fail the thread check
+ continue;
+ }
+ // Folia end - region threading
try {
final NewChunkHolder.SaveStat saveStat = holder.save(shutdown);
if (saveStat != null) {
@@ -327,7 +431,7 @@ public final class ChunkHolderManager {
}
}
}
- if (flush) {
+ if (last && flush) { // Folia - region threading
MoonriseRegionFileIO.flush(this.world);
try {
MoonriseRegionFileIO.flushRegionStorages(this.world);
@@ -732,7 +836,13 @@ public final class ChunkHolderManager {
}
public void tick() {
- ++this.currentTick;
+ // Folia start - region threading
+ final ThreadedRegionizer.ThreadedRegion<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> region =
+ TickRegionScheduler.getCurrentRegion();
+ if (region == null) {
+ throw new IllegalStateException("Not running tick() while on a region");
+ }
+ // Folia end - region threading
final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
@@ -746,7 +856,7 @@ public final class ChunkHolderManager {
return removeDelay <= 0L;
};
- for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
+ for (final LongIterator iterator = region.getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
final long sectionKey = iterator.nextLong();
if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) {
@@ -1031,26 +1141,56 @@ public final class ChunkHolderManager {
if (changedFullStatus.isEmpty()) {
return;
}
- if (!TickThread.isTickThread()) {
- this.taskScheduler.scheduleChunkTask(() -> {
- final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
- pendingFullLoadUpdate.add(changedFullStatus.get(i));
- }
- ChunkHolderManager.this.processPendingFullUpdate();
- }, Priority.HIGHEST);
- } else {
- final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
- pendingFullLoadUpdate.add(changedFullStatus.get(i));
+ // Folia start - region threading
+ final Long2ObjectOpenHashMap<List<NewChunkHolder>> sectionToUpdates = new Long2ObjectOpenHashMap<>();
+ final List<NewChunkHolder> thisRegionHolders = new ArrayList<>();
+
+ final int regionShift = this.world.moonrise$getRegionChunkShift();
+ final ThreadedRegionizer.ThreadedRegion<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> 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<Long2ObjectMap.Entry<List<NewChunkHolder>>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry<List<NewChunkHolder>> entry = iterator.next();
+ final long sectionKey = entry.getLongKey();
+
+ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
+ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
+
+ final List<NewChunkHolder> regionHolders = entry.getValue();
+ this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> {
+ ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders);
+ ChunkHolderManager.this.processPendingFullUpdate();
+ }, Priority.HIGHEST);
+
}
}
+ // Folia end - region threading
}
private void removeChunkHolder(final NewChunkHolder holder) {
holder.onUnload();
- this.autoSaveQueue.remove(holder);
+ this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading
PlatformHooks.get().onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
}
@@ -1063,7 +1203,7 @@ public final class ChunkHolderManager {
throw new IllegalStateException("Cannot unload chunks recursively");
}
final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift
- final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
+ final List<ChunkUnloadQueue.SectionToUnload> unloadSectionsForRegion = this.unloadQueue.retrieveForCurrentRegion(); // Folia - threaded regions
int unloadCountTentative = 0;
for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
final ChunkUnloadQueue.UnloadSection section
@@ -1381,7 +1521,13 @@ public final class ChunkHolderManager {
// only call on tick thread
private boolean processPendingFullUpdate() {
- final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+ // Folia start - region threading
+ final HolderManagerRegionData data = this.getCurrentRegionData();
+ if (data == null) {
+ return false;
+ }
+ final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = data.pendingFullLoadUpdate;
+ // Folia end - region threading
boolean ret = false;
@@ -1392,9 +1538,7 @@ public final class ChunkHolderManager {
ret |= holder.handleFullStatusChange(changedFullStatus);
if (!changedFullStatus.isEmpty()) {
- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
- pendingFullLoadUpdate.add(changedFullStatus.get(i));
- }
+ this.addChangedStatuses(changedFullStatus); // Folia - region threading
changedFullStatus.clear();
}
}
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
index 67532b85073b7978254a0b04caadfe822679e61f..cba2d16c0cb5adc92952990ef95b1c979eafd40f 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
@@ -122,7 +122,7 @@ public final class ChunkTaskScheduler {
public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor;
public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor;
- private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue();
+ // Folia - regionised ticking
public final ChunkHolderManager chunkHolderManager;
@@ -337,14 +337,13 @@ public final class ChunkTaskScheduler {
};
// this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
- this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING);
+ this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, Priority.BLOCKING); // Folia - region threading
// so, make the main thread pick it up
((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException));
}
public boolean executeMainThreadTask() {
- TickThread.ensureTickThread("Cannot execute main thread task off-main");
- return this.mainThreadExecutor.executeTask();
+ throw new UnsupportedOperationException("Use regionised ticking hooks"); // Folia - regionised ticking
}
public void raisePriority(final int x, final int z, final Priority priority) {
@@ -829,7 +828,7 @@ public final class ChunkTaskScheduler {
*/
@Deprecated
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) {
- return this.mainThreadExecutor.queueTask(run, priority);
+ throw new UnsupportedOperationException(); // Folia - regionised ticking
}
public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
@@ -838,7 +837,7 @@ public final class ChunkTaskScheduler {
public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
final Priority priority) {
- return this.mainThreadExecutor.createTask(run, priority);
+ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
}
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
@@ -847,9 +846,27 @@ public final class ChunkTaskScheduler {
public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
final Priority priority) {
- return this.mainThreadExecutor.queueTask(run, priority);
+ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
}
+ // Folia start - region threading
+ // this function is guaranteed to never touch the ticket lock or schedule lock
+ // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the
+ // ticket lock in the schedule logic
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run) {
+ return this.scheduleChunkTaskEventually(chunkX, chunkZ, run, Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run,
+ final Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority);
+ this.world.taskQueueRegionData.pushGlobalChunkTask(() -> {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority);
+ });
+ return ret;
+ }
+ // Folia end - region threading
+
public boolean halt(final boolean sync, final long maxWaitNS) {
this.radiusAwareGenExecutor.halt();
this.parallelGenExecutor.halt();
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
index e4a5fa25ed368fc4662c30934da2963ef446d782..601ed36413bbbf9c17e530b42906986e441237fd 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
@@ -1359,10 +1359,10 @@ public final class NewChunkHolder {
private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) {
// Update progress listener for LevelLoadingScreen
if (chunk != null) {
- final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener;
+ final ChunkProgressListener progressListener = null; // Folia - threaded regions
if (progressListener != null) {
final ChunkStatus finalStatus = status;
- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - threaded regions
progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus);
});
}
@@ -1383,7 +1383,7 @@ public final class NewChunkHolder {
}
// must be scheduled to main, we do not trust the callback to not do anything stupid
- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading
for (final Consumer<ChunkAccess> consumer : consumers) {
try {
consumer.accept(chunk);
@@ -1411,7 +1411,7 @@ public final class NewChunkHolder {
}
// must be scheduled to main, we do not trust the callback to not do anything stupid
- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading
for (final Consumer<LevelChunk> consumer : consumers) {
try {
consumer.accept(chunk);
diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
index e04bd54744335fb5398c6e4f7ce8b981f35bfb7d..471b6d49d77e03665ffc269d17ab46f225e3ce1c 100644
--- a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
+++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
@@ -1940,6 +1940,17 @@ public final class CollisionUtil {
for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
+ // Folia start - region threading
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, currChunkX, currChunkZ, 4)) {
+ if (checkOnly) {
+ return true;
+ } else {
+ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ));
+ ret = true;
+ continue;
+ }
+ }
+ // Folia end - region threading
final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks);
if (chunk == null) {
diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java
index ade6110cc6adb1263c0359ff7e96e96b959e61f3..c260741a87513b89a5cc62c543fb9f990f86491e 100644
--- a/io/papermc/paper/entity/activation/ActivationRange.java
+++ b/io/papermc/paper/entity/activation/ActivationRange.java
@@ -48,33 +48,34 @@ public final class ActivationRange {
private static int checkInactiveWakeup(final Entity entity) {
final Level world = entity.level();
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
final SpigotWorldConfig config = world.spigotConfig;
- final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
+ final long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions
if (entity.activationType == ActivationType.VILLAGER) {
- if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) {
- world.wakeupInactiveRemainingVillagers--;
+ if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions
+ worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions
return config.wakeUpInactiveVillagersFor;
}
} else if (entity.activationType == ActivationType.ANIMAL) {
- if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) {
- world.wakeupInactiveRemainingAnimals--;
+ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions
+ worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions
return config.wakeUpInactiveAnimalsFor;
}
} else if (entity.activationType == ActivationType.FLYING_MONSTER) {
- if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) {
- world.wakeupInactiveRemainingFlying--;
+ if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions
+ worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions
return config.wakeUpInactiveFlyingFor;
}
} else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) {
- if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) {
- world.wakeupInactiveRemainingMonsters--;
+ if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions
+ worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions
return config.wakeUpInactiveMonstersFor;
}
}
return -1;
}
- static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0);
+ //static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0); // Folia - threaded regions - replaced by local variable
/**
* These entities are excluded from Activation range checks.
@@ -122,10 +123,11 @@ public final class ActivationRange {
final int waterActivationRange = world.spigotConfig.waterActivationRange;
final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange;
final int villagerActivationRange = world.spigotConfig.villagerActivationRange;
- world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals);
- world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers);
- world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters);
- world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying);
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
+ worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Folia - threaded regions
+ worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Folia - threaded regions
+ worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Folia - threaded regions
+ worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Folia - threaded regions
int maxRange = Math.max(monsterActivationRange, animalActivationRange);
maxRange = Math.max(maxRange, raiderActivationRange);
@@ -135,30 +137,37 @@ public final class ActivationRange {
maxRange = Math.max(maxRange, villagerActivationRange);
maxRange = Math.min((world.spigotConfig.simulationDistance << 4) - 8, maxRange);
- for (final Player player : world.players()) {
- player.activatedTick = MinecraftServer.currentTick;
+ for (final Player player : world.getLocalPlayers()) { // Folia - region threading
+ player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading
if (world.spigotConfig.ignoreSpectatorActivation && player.isSpectator()) {
continue;
}
final int worldHeight = world.getHeight();
- ActivationRange.maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange);
- ActivationType.MISC.boundingBox = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange);
- ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange);
- ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange);
- ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange);
- ActivationType.WATER.boundingBox = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange);
- ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange);
- ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange);
+ final AABB maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange); // Folia - threaded regions
+ final AABB[] bbByType = new AABB[ActivationType.values().length]; // Folia - threaded regions
+ bbByType[ActivationType.MISC.ordinal()] = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange); // Folia - threaded regions
+ bbByType[ActivationType.RAIDER.ordinal()] = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange); // Folia - threaded regions
+ bbByType[ActivationType.ANIMAL.ordinal()] = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange); // Folia - threaded regions
+ bbByType[ActivationType.MONSTER.ordinal()] = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange); // Folia - threaded regions
+ bbByType[ActivationType.WATER.ordinal()] = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange); // Folia - threaded regions
+ bbByType[ActivationType.FLYING_MONSTER.ordinal()] = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange); // Folia - threaded regions
+ bbByType[ActivationType.VILLAGER.ordinal()] = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange); // Folia - threaded regions
- final java.util.List<Entity> entities = world.getEntities((Entity) null, ActivationRange.maxBB, e -> true);
+ final java.util.List<Entity> entities = new java.util.ArrayList<>(); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later
+ ((net.minecraft.server.level.ServerLevel)world).moonrise$getEntityLookup().getEntities((Entity)null, maxBB, entities, null); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later
final boolean tickMarkers = world.paperConfig().entities.markers.tick;
for (final Entity entity : entities) {
+ // Folia start - region ticking
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) {
+ continue;
+ }
+ // Folia end - region ticking
if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) {
continue;
}
- ActivationRange.activateEntity(entity);
+ ActivationRange.activateEntity(entity, bbByType); // Folia - threaded regions
}
}
}
@@ -168,14 +177,14 @@ public final class ActivationRange {
*
* @param entity
*/
- private static void activateEntity(final Entity entity) {
- if (MinecraftServer.currentTick > entity.activatedTick) {
+ private static void activateEntity(final Entity entity, final AABB[] bbByType) { // Folia - threaded regions
+ if (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() > entity.activatedTick) { // Folia - threaded regions
if (entity.defaultActivationState) {
- entity.activatedTick = MinecraftServer.currentTick;
+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
return;
}
- if (entity.activationType.boundingBox.intersects(entity.getBoundingBox())) {
- entity.activatedTick = MinecraftServer.currentTick;
+ if (bbByType[entity.activationType.ordinal()].intersects(entity.getBoundingBox())) { // Folia - threaded regions
+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
}
}
}
@@ -189,6 +198,7 @@ public final class ActivationRange {
*/
public static int checkEntityImmunities(final Entity entity) { // return # of ticks to get immunity
final SpigotWorldConfig config = entity.level().spigotConfig;
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = entity.level().getCurrentWorldData(); // Folia - threaded regions
final int inactiveWakeUpImmunity = checkInactiveWakeup(entity);
if (inactiveWakeUpImmunity > -1) {
return inactiveWakeUpImmunity;
@@ -196,10 +206,10 @@ public final class ActivationRange {
if (entity.getRemainingFireTicks() > 0) {
return 2;
}
- if (entity.activatedImmunityTick >= MinecraftServer.currentTick) {
+ if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - threaded regions
return 1;
}
- final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
+ final long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions
if ((entity.activationType != ActivationType.WATER && entity.isInWater() && entity.isPushedByFluid())) {
return 100;
}
@@ -296,16 +306,16 @@ public final class ActivationRange {
return true;
}
- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick;
+ boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
entity.isTemporarilyActive = false;
// Should this entity tick?
if (!isActive) {
- if ((MinecraftServer.currentTick - entity.activatedTick - 1) % 20 == 0) {
+ if ((io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick - 1) % 20 == 0) { // Folia - threaded regions
// Check immunities every 20 ticks.
final int immunity = checkEntityImmunities(entity);
if (immunity >= 0) {
- entity.activatedTick = MinecraftServer.currentTick + immunity;
+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + immunity; // Folia - threaded regions
} else {
entity.isTemporarilyActive = true;
}
diff --git a/io/papermc/paper/redstone/RedstoneWireTurbo.java b/io/papermc/paper/redstone/RedstoneWireTurbo.java
index ff747a1ecdf3c888bca0d69de4f85dcd810b6139..5a76c93eada8db35b1ddbb562ccfbd2f0d35f0ca 100644
--- a/io/papermc/paper/redstone/RedstoneWireTurbo.java
+++ b/io/papermc/paper/redstone/RedstoneWireTurbo.java
@@ -829,14 +829,14 @@ public final class RedstoneWireTurbo {
j = getMaxCurrentStrength(upd, j);
int l = 0;
- wire.shouldSignal = false;
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading
// Unfortunately, World.isBlockIndirectlyGettingPowered is complicated,
// and I'm not ready to try to replicate even more functionality from
// elsewhere in Minecraft into this accelerator. So sadly, we must
// suffer the performance hit of this very expensive call. If there
// is consistency to what this call returns, we may be able to cache it.
final int k = worldIn.getBestNeighborSignal(upd.self);
- wire.shouldSignal = true;
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading
// The variable 'k' holds the maximum redstone power value of any adjacent blocks.
// If 'k' has the highest level of all neighbors, then the power level of this
diff --git a/io/papermc/paper/threadedregions/RegionShutdownThread.java b/io/papermc/paper/threadedregions/RegionShutdownThread.java
new file mode 100644
index 0000000000000000000000000000000000000000..bbd14cf34438a9366f5ff29f1acba4282d77d983
--- /dev/null
+++ b/io/papermc/paper/threadedregions/RegionShutdownThread.java
@@ -0,0 +1,226 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.moonrise.common.util.WorldUtil;
+import com.mojang.logging.LogUtils;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.ChunkPos;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.slf4j.Logger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.util.TickThread {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+
+ ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> shuttingDown;
+
+ public RegionShutdownThread(final String name) {
+ super(name);
+ this.setUncaughtExceptionHandler((thread, thr) -> {
+ LOGGER.error("Error shutting down server", thr);
+ });
+ }
+
+ static ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region,
+ final ServerLevel world) {
+ try {
+ this.shuttingDown = region;
+ final List<ServerLevel.PendingTeleport> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>>
+ 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<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>>
+ 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.
+ * <p>
+ * <b>Note:</b> 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.
+ * </p>
+ * <P>
+ * <b>Note:</b> 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.
+ * </P>
+ * <p>
+ * Regionised data may be <b>world-checked</b>. 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:
+ * <pre>
+ * {@code
+ * public class EntityTickList {
+ * private final List<Entity> 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<EntityTickList> 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<Long> 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<World> 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> tickTimes =
+ * new RegionizedData<>(null, () -> new TickTimes(), ...);
+ * }
+ * }
+ * </pre>
+ * In general, it is advised that if a RegionizedData object is instantiated <i>per world</i>, that world checking
+ * is enabled for it by passing the world to the constructor.
+ * </p>
+ */
+public final class RegionizedData<T> {
+
+ private final ServerLevel world;
+ private final Supplier<T> initialValueSupplier;
+ private final RegioniserCallback<T> callback;
+
+ /**
+ * Creates a regionised data holder. The provided initial value supplier may not be null, and it must
+ * never produce {@code null} values.
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * 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 <b>and</b> region,
+ * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to
+ * region <b>only</b>.
+ * </p>
+ * @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<T> supplier, final RegioniserCallback<T> 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<T> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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.
+ * <p>
+ * It is critical to note that each function is called while holding the region lock.
+ * </p>
+ */
+ public static interface RegioniserCallback<T> {
+
+ /**
+ * Completely merges the data in {@code from} to {@code into}.
+ * <p>
+ * <b>Calculating Tick Offsets:</b>
+ * 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}.
+ * </p>
+ * <p>
+ * <b>Critical Notes:</b>
+ * <li>
+ * <ul>
+ * 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.
+ * </ul>
+ * <ul>
+ * This function may not throw any exceptions, or the server will be left in an unrecoverable state.
+ * </ul>
+ * </li>
+ * </p>
+ *
+ * @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}.
+ * <p>
+ * 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:
+ * <pre>
+ * {@code
+ * int chunkX = ...;
+ * int chunkZ = ...;
+ *
+ * int regionSectionX = chunkX >> chunkToRegionShift;
+ * int regionSectionZ = chunkZ >> chunkToRegionShift;
+ * long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
+ * }
+ * </pre>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ *
+ * @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<T> regionToData, final ReferenceOpenHashSet<T> dataSet
+ );
+ }
+}
\ No newline at end of file
diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..1382c695c4991488b113401e231875ddc74f6b01
--- /dev/null
+++ b/io/papermc/paper/threadedregions/RegionizedServer.java
@@ -0,0 +1,455 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
+import ca.spottedleaf.moonrise.common.util.TickThread;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler;
+import net.minecraft.CrashReport;
+import net.minecraft.ReportedException;
+import net.minecraft.network.Connection;
+import net.minecraft.network.PacketListener;
+import net.minecraft.network.PacketSendListener;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.dedicated.DedicatedServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import net.minecraft.world.level.GameRules;
+import org.bukkit.Bukkit;
+import org.slf4j.Logger;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.BooleanSupplier;
+
+public final class RegionizedServer {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final RegionizedServer INSTANCE = new RegionizedServer();
+
+ public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue();
+
+ private final CopyOnWriteArrayList<ServerLevel> worlds = new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<Connection> connections = new CopyOnWriteArrayList<>();
+
+ private final MultiThreadedQueue<Runnable> globalTickQueue = new MultiThreadedQueue<>();
+
+ private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this);
+
+ public static RegionizedServer getInstance() {
+ return INSTANCE;
+ }
+
+ public void addConnection(final Connection conn) {
+ this.connections.add(conn);
+ }
+
+ public boolean removeConnection(final Connection conn) {
+ return this.connections.remove(conn);
+ }
+
+ public void addWorld(final ServerLevel world) {
+ this.worlds.add(world);
+ }
+
+ public void init() {
+ // call init event _before_ scheduling anything
+ new RegionizedServerInitEvent().callEvent();
+
+ // now we can schedule
+ this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS);
+ TickRegions.getScheduler().scheduleRegion(this.tickHandle);
+ TickRegions.getScheduler().init();
+ }
+
+ public void invalidateStatus() {
+ this.lastServerStatus = 0L;
+ }
+
+ public void addTaskWithoutNotify(final Runnable run) {
+ this.globalTickQueue.add(run);
+ }
+
+ public void addTask(final Runnable run) {
+ this.addTaskWithoutNotify(run);
+ TickRegions.getScheduler().setHasTasks(this.tickHandle);
+ }
+
+ /**
+ * Returns the current tick of the region ticking.
+ * @throws IllegalStateException If there is no current region.
+ */
+ public static long getCurrentTick() throws IllegalStateException {
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<Void>> 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<Void> 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<Void> 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<Void> map = walkers.get(i);
+
+ map.update(posX, posZ, VD);
+ }
+ }
+ */
+
+ private void globalTick(final int tickCount) {
+ /*
+ if (false) {
+ io.papermc.paper.threadedregions.ThreadedTicketLevelPropagator.main(null);
+ }
+ this.randomWalk();
+ */
+ ++this.tickCount;
+ // expire invalid click command callbacks
+ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount);
+
+ // scheduler
+ ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick();
+
+ // commands
+ ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs();
+
+ // needs
+ // player ping sample
+ // world global tick
+ // connection tick
+
+ // tick player ping sample
+ this.tickPlayerSample();
+
+ // tick worlds
+ for (final ServerLevel world : this.worlds) {
+ this.globalTick(world, tickCount);
+ }
+
+ // tick connections
+ this.tickConnections();
+
+ // player list
+ MinecraftServer.getServer().getPlayerList().tick();
+ }
+
+ private void tickPlayerSample() {
+ final MinecraftServer mcServer = MinecraftServer.getServer();
+
+ final long currtime = System.nanoTime();
+
+ // player ping sample
+ // copied from MinecraftServer#tickServer
+ // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing
+ // an uncomplete status
+ if (currtime - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) {
+ this.lastServerStatus = currtime;
+ mcServer.rebuildServerStatus();
+ }
+ }
+
+ public static boolean isNotOwnedByGlobalRegion(final Connection conn) {
+ final PacketListener packetListener = conn.getPacketListener();
+
+ if (packetListener instanceof ServerGamePacketListenerImpl gamePacketListener) {
+ return !gamePacketListener.waitingForSwitchToConfig;
+ }
+
+ if (conn.getPacketListener() instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) {
+ return configurationPacketListener.switchToMain;
+ }
+
+ return false;
+ }
+
+ private void tickConnections() {
+ final List<Connection> connections = new ArrayList<>(this.connections);
+ Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging
+ for (final Connection conn : connections) {
+ if (!conn.becomeActive()) {
+ continue;
+ }
+
+ if (isNotOwnedByGlobalRegion(conn)) {
+ // we actually require that the owning regions remove the connection for us, as it is possible
+ // that ownership is transferred back to us
+ continue;
+ }
+
+ if (!conn.isConnected()) {
+ this.removeConnection(conn);
+ conn.handleDisconnection();
+ continue;
+ }
+
+ try {
+ conn.tick();
+ } catch (final Exception exception) {
+ if (conn.isMemoryConnection()) {
+ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection"));
+ }
+
+ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception);
+ MutableComponent ichatmutablecomponent = Component.literal("Internal server error");
+
+ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> {
+ conn.disconnect(ichatmutablecomponent);
+ }));
+ conn.setReadOnly();
+ continue;
+ }
+ }
+ }
+
+ // A global tick only updates things like weather / worldborder, basically anything in the world that is
+ // NOT tied to a specific region, but rather shared amongst all of them.
+ private void globalTick(final ServerLevel world, final int tickCount) {
+ // needs
+ // worldborder tick
+ // advancing the weather cycle
+ // sleep status thing
+ // updating sky brightness
+ // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking
+
+ // Typically, we expect there to be a running region to drain a world's global chunk tasks. However,
+ // this may not be the case - and thus, only the global tick thread can do anything.
+ world.taskQueueRegionData.drainGlobalChunkTasks();
+
+ // worldborder tick
+ this.tickWorldBorder(world);
+
+ // weather cycle
+ this.advanceWeatherCycle(world);
+
+ // sleep status
+ this.checkNightSkip(world);
+
+ // update raids
+ this.updateRaids(world);
+
+ // sky brightness
+ this.updateSkyBrightness(world);
+
+ // time ticking (TODO API synchronisation?)
+ this.tickTime(world, tickCount);
+
+ world.updateTickData();
+
+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates
+ }
+
+ private void updateRaids(final ServerLevel world) {
+ world.getRaids().globalTick();
+ }
+
+ private void checkNightSkip(final ServerLevel world) {
+ world.tickSleep();
+ }
+
+ private void advanceWeatherCycle(final ServerLevel world) {
+ world.advanceWeatherCycle();
+ }
+
+ private void updateSkyBrightness(final ServerLevel world) {
+ world.updateSkyBrightness();
+ }
+
+ private void tickWorldBorder(final ServerLevel world) {
+ world.getWorldBorder().tick();
+ }
+
+ private void tickTime(final ServerLevel world, final int tickCount) {
+ if (world.tickTime) {
+ if (world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
+ world.setDayTime(world.levelData.getDayTime() + (long)tickCount);
+ }
+ world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount);
+ }
+ }
+
+ public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) {
+
+ }
+}
\ No newline at end of file
diff --git a/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
new file mode 100644
index 0000000000000000000000000000000000000000..745ab870310733b569681f5280895bb9798620a4
--- /dev/null
+++ b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
@@ -0,0 +1,807 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.Priority;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.util.Unit;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayDeque;
+import java.util.Iterator;
+import java.util.concurrent.atomic.AtomicLong;
+
+public final class RegionizedTaskQueue {
+
+ private static final TicketType<Unit> TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0);
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run) {
+ return this.createChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run, final Priority priority) {
+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run) {
+ return this.createTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run, final Priority priority) {
+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run) {
+ return this.queueChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run, final Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run) {
+ return this.queueTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL);
+ }
+
+ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
+ final Runnable run, final Priority priority) {
+ final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority);
+
+ ret.queue();
+
+ return ret;
+ }
+
+ public static final class WorldRegionTaskData {
+ private final ServerLevel world;
+ private final MultiThreadedQueue<Runnable> globalChunkTask = new MultiThreadedQueue<>();
+ private final ConcurrentLong2ReferenceChainedHashTable<ReferenceCountData> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> regioniser = this.world.regioniser;
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region
+ = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ);
+ if (region == null) {
+ return null;
+ }
+ final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData();
+ return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue);
+ }
+
+ private void removeTicket(final long coord) {
+ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel(
+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
+ );
+ }
+
+ private void addTicket(final long coord) {
+ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel(
+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
+ );
+ }
+
+ private void processTicketUpdates(final long coord) {
+ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(CoordinateUtils.getChunkX(coord), CoordinateUtils.getChunkZ(coord));
+ }
+
+ // note: only call on acquired referenceCountData
+ private void ensureTicketAdded(final long coord, final ReferenceCountData referenceCountData) {
+ if (!referenceCountData.addedTicket) {
+ // fine if multiple threads do this, no removeTicket may be called for this coord due to reference count inc
+ this.addTicket(coord);
+ this.processTicketUpdates(coord);
+ referenceCountData.addedTicket = true;
+ }
+ }
+
+ private void decrementReference(final ReferenceCountData referenceCountData, final long coord) {
+ if (!referenceCountData.decreaseReferenceCount()) {
+ return;
+ } // else: need to remove ticket
+
+ // note: it is possible that another thread increments and then removes the reference before we can, so
+ // use ifPresent
+ this.referenceCounters.computeIfPresent(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> {
+ if (valueInMap.referenceCount.get() != 0L) {
+ return valueInMap;
+ }
+
+ // note: valueInMap may not be referenceCountData
+
+ // possible to invoke this outside of the compute call, but not required and requires additional logic
+ WorldRegionTaskData.this.removeTicket(keyInMap);
+
+ return null;
+ });
+ }
+
+ private ReferenceCountData incrementReference(final long coord) {
+ ReferenceCountData referenceCountData = this.referenceCounters.get(coord);
+
+ if (referenceCountData != null && referenceCountData.addCount()) {
+ this.ensureTicketAdded(coord, referenceCountData);
+ return referenceCountData;
+ }
+
+ referenceCountData = this.referenceCounters.compute(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> {
+ if (valueInMap == null) {
+ // sets reference count to 1
+ return new ReferenceCountData();
+ }
+ // OK if we add from 0, the remove call will use compute() and catch this race condition
+ valueInMap.referenceCount.getAndIncrement();
+
+ return valueInMap;
+ });
+
+ this.ensureTicketAdded(coord, referenceCountData);
+
+ return referenceCountData;
+ }
+ }
+
+ private static final class ReferenceCountData {
+
+ public final AtomicLong referenceCount = new AtomicLong(1L);
+ public volatile boolean addedTicket;
+
+ // returns false if reference count is 0, otherwise increments ref count
+ public boolean addCount() {
+ int failures = 0;
+ for (long curr = this.referenceCount.get();;) {
+ for (int i = 0; i < failures; ++i) {
+ Thread.onSpinWait();
+ }
+
+ if (curr == 0L) {
+ return false;
+ }
+
+ if (curr == (curr = this.referenceCount.compareAndExchange(curr, curr + 1L))) {
+ return true;
+ }
+
+ ++failures;
+ }
+ }
+
+ // returns true if new reference count is 0
+ public boolean decreaseReferenceCount() {
+ final long res = this.referenceCount.decrementAndGet();
+ if (res >= 0L) {
+ return res == 0L;
+ } else {
+ throw new IllegalStateException("Negative reference count");
+ }
+ }
+ }
+
+ public static final class RegionTaskQueueData {
+ private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue();
+ private final PrioritisedQueue chunkQueue = new PrioritisedQueue();
+ private final WorldRegionTaskData worldRegionTaskData;
+
+ public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) {
+ this.worldRegionTaskData = worldRegionTaskData;
+ }
+
+ void mergeInto(final RegionTaskQueueData into) {
+ this.tickTaskQueue.mergeInto(into.tickTaskQueue);
+ this.chunkQueue.mergeInto(into.chunkQueue);
+ }
+
+ public boolean executeTickTask() {
+ return this.tickTaskQueue.executeTask();
+ }
+
+ public boolean executeChunkTask() {
+ return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask();
+ }
+
+ void split(final ThreadedRegionizer<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> regioniser,
+ final Long2ReferenceOpenHashMap<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>> 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<ChunkBasedPriorityTask>[] 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<ChunkBasedPriorityTask> queue : this.queues) {
+ ret += queue.size();
+ }
+
+ return ret;
+ }
+ }
+
+ public boolean isEmpty() {
+ final ArrayDeque<ChunkBasedPriorityTask>[] 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<ChunkBasedPriorityTask>[] thisQueues) {
+ synchronized (target) {
+ final ArrayDeque<ChunkBasedPriorityTask>[] otherQueues = target.queues;
+ for (int i = 0; i < thisQueues.length; ++i) {
+ final ArrayDeque<ChunkBasedPriorityTask> fromQ = thisQueues[i];
+ final ArrayDeque<ChunkBasedPriorityTask> 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<ChunkBasedPriorityTask> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> regioniser,
+ final Long2ReferenceOpenHashMap<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>> into) {
+ final Reference2ReferenceOpenHashMap<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, ArrayDeque<ChunkBasedPriorityTask>[]>
+ 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<ChunkBasedPriorityTask>[] thisQueues = this.queues;
+ for (int i = 0; i < thisQueues.length; ++i) {
+ final ArrayDeque<ChunkBasedPriorityTask> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>
+ region = into.get(sectionKey);
+ if (region == null) {
+ throw new IllegalStateException();
+ }
+
+ split.computeIfAbsent(region, (keyInMap) -> {
+ final ArrayDeque<ChunkBasedPriorityTask>[] 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<Reference2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, ArrayDeque<ChunkBasedPriorityTask>[]>>
+ iterator = split.reference2ReferenceEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Reference2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, ArrayDeque<ChunkBasedPriorityTask>[]>
+ 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<ChunkBasedPriorityTask>[] 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<ChunkBasedPriorityTask>[] 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<ChunkBasedPriorityTask> queue = queues[i];
+ while ((task = queue.pollFirst()) != null) {
+ if ((referenceCounter = task.trySetCompleting(i)) != null) {
+ break search_loop;
+ }
+ }
+ }
+ }
+
+ if (task == null) {
+ return false;
+ }
+
+ try {
+ task.executeInternal();
+ } finally {
+ task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord);
+ }
+
+ return true;
+ }
+
+ private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask {
+
+ private static final ReferenceCountData REFERENCE_COUNTER_NOT_SET = new ReferenceCountData();
+ static {
+ REFERENCE_COUNTER_NOT_SET.referenceCount.set((long)Integer.MIN_VALUE);
+ }
+
+ private final WorldRegionTaskData world;
+ private final int chunkX;
+ private final int chunkZ;
+ private final long sectionLowerLeftCoord; // chunk coordinate
+ private final boolean isChunkTask;
+
+ private volatile ReferenceCountData referenceCounter;
+ private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", ReferenceCountData.class);
+ private Runnable run;
+ private volatile Priority priority;
+ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", Priority.class);
+
+ ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask,
+ final Runnable run, final Priority priority) {
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.isChunkTask = isChunkTask;
+ this.run = run;
+ this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET);
+ this.setPriorityPlain(priority);
+
+ final int regionShift = world.world.regioniser.sectionChunkShift;
+ final int regionMask = (1 << regionShift) - 1;
+
+ this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask);
+ }
+
+ private Priority getPriorityVolatile() {
+ return (Priority)PRIORITY_HANDLE.getVolatile(this);
+ }
+
+ private void setPriorityPlain(final Priority priority) {
+ PRIORITY_HANDLE.set(this, priority);
+ }
+
+ private void setPriorityVolatile(final Priority priority) {
+ PRIORITY_HANDLE.setVolatile(this, priority);
+ }
+
+ private Priority compareAndExchangePriority(final Priority expect, final Priority update) {
+ return (Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ private void setReferenceCounterPlain(final ReferenceCountData value) {
+ REFERENCE_COUNTER_HANDLE.set(this, value);
+ }
+
+ private ReferenceCountData getReferenceCounterVolatile() {
+ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.get(this);
+ }
+
+ private ReferenceCountData compareAndExchangeReferenceCounter(final ReferenceCountData expect, final ReferenceCountData update) {
+ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ private void executeInternal() {
+ try {
+ this.run.run();
+ } finally {
+ this.run = null;
+ }
+ }
+
+ private void cancelInternal() {
+ this.run = null;
+ }
+
+ private boolean tryComplete(final boolean cancel) {
+ int failures = 0;
+ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) {
+ if (curr == null) {
+ return false;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
+ ++failures;
+ continue;
+ }
+
+ // we have the reference count, we win no matter what.
+ this.setPriorityVolatile(Priority.COMPLETING);
+
+ try {
+ if (cancel) {
+ this.cancelInternal();
+ } else {
+ this.executeInternal();
+ }
+ } finally {
+ if (curr != REFERENCE_COUNTER_NOT_SET) {
+ this.world.decrementReference(curr, this.sectionLowerLeftCoord);
+ }
+ }
+
+ return true;
+ }
+ }
+
+ @Override
+ public PrioritisedExecutor getExecutor() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isQueued() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean queue() {
+ if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) {
+ return false;
+ }
+
+ final ReferenceCountData referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord);
+ if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) {
+ // we don't expect race conditions here, so it is OK if we have to needlessly reference count
+ this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord);
+ return false;
+ }
+
+ boolean synchronise = false;
+ for (;;) {
+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
+ // the same queue again, as the region lock will be given to us only when the merge/split operation
+ // is done
+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
+
+ if (queue == null) {
+ if (!synchronise) {
+ // may be incorrectly null when unsynchronised
+ synchronise = true;
+ continue;
+ }
+ // may have been cancelled before we got to the queue
+ if (this.getReferenceCounterVolatile() != null) {
+ throw new IllegalStateException("Expected null ref count when queue does not exist");
+ }
+ // the task never could be polled from the queue, so we return false
+ // don't decrement reference count, as we were certainly cancelled by another thread, which
+ // will decrement the reference count
+ return false;
+ }
+
+ synchronise = true;
+
+ final Boolean res = queue.tryPush(this);
+ if (res == null) {
+ // we were cancelled
+ // don't decrement reference count, as we were certainly cancelled by another thread, which
+ // will decrement the reference count
+ return false;
+ }
+
+ if (!res.booleanValue()) {
+ // failed, try again
+ continue;
+ }
+
+ // successfully queued
+ return true;
+ }
+ }
+
+ private ReferenceCountData trySetCompleting(final int minPriority) {
+ // first, try to set priority to EXECUTING
+ for (Priority curr = this.getPriorityVolatile();;) {
+ if (curr.isLowerPriority(minPriority)) {
+ return null;
+ }
+
+ if (curr == (curr = this.compareAndExchangePriority(curr, Priority.COMPLETING))) {
+ break;
+ } // else: continue
+ }
+
+ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) {
+ if (curr == null) {
+ // something acquired before us
+ return null;
+ }
+
+ if (curr == REFERENCE_COUNTER_NOT_SET) {
+ throw new IllegalStateException();
+ }
+
+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
+ continue;
+ }
+
+ return curr;
+ }
+ }
+
+ private void updatePriorityInQueue() {
+ boolean synchronise = false;
+ for (;;) {
+ final ReferenceCountData referenceCount = this.getReferenceCounterVolatile();
+ if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) {
+ // cancelled or not queued
+ return;
+ }
+
+ if (this.getPriorityVolatile() == Priority.COMPLETING) {
+ // cancelled
+ return;
+ }
+
+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
+ // the same queue again, as the region lock will be given to us only when the merge/split operation
+ // is done
+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
+
+ if (queue == null) {
+ if (!synchronise) {
+ // may be incorrectly null when unsynchronised
+ synchronise = true;
+ continue;
+ }
+ // must have been removed
+ return;
+ }
+
+ synchronise = true;
+
+ final Boolean res = queue.tryPush(this);
+ if (res == null) {
+ // we were cancelled
+ return;
+ }
+
+ if (!res.booleanValue()) {
+ // failed, try again
+ continue;
+ }
+
+ // successfully queued
+ return;
+ }
+ }
+
+ @Override
+ public Priority getPriority() {
+ return this.getPriorityVolatile();
+ }
+
+ @Override
+ public boolean lowerPriority(final Priority priority) {
+ int failures = 0;
+ for (Priority curr = this.getPriorityVolatile();;) {
+ if (curr == Priority.COMPLETING) {
+ return false;
+ }
+
+ if (curr.isLowerOrEqualPriority(priority)) {
+ return false;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
+ this.updatePriorityInQueue();
+ return true;
+ }
+ ++failures;
+ }
+ }
+
+ @Override
+ public long getSubOrder() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean setSubOrder(final long subOrder) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean raiseSubOrder(final long subOrder) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean lowerSubOrder(final long subOrder) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
+ return this.setPriority(priority);
+ }
+
+ @Override
+ public boolean setPriority(final Priority priority) {
+ int failures = 0;
+ for (Priority curr = this.getPriorityVolatile();;) {
+ if (curr == Priority.COMPLETING) {
+ return false;
+ }
+
+ if (curr == priority) {
+ return false;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
+ this.updatePriorityInQueue();
+ return true;
+ }
+ ++failures;
+ }
+ }
+
+ @Override
+ public boolean raisePriority(final Priority priority) {
+ int failures = 0;
+ for (Priority curr = this.getPriorityVolatile();;) {
+ if (curr == Priority.COMPLETING) {
+ return false;
+ }
+
+ if (curr.isHigherOrEqualPriority(priority)) {
+ return false;
+ }
+
+ for (int i = 0; i < failures; ++i) {
+ ConcurrentUtil.backoff();
+ }
+
+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
+ this.updatePriorityInQueue();
+ return true;
+ }
+ ++failures;
+ }
+ }
+
+ @Override
+ public boolean execute() {
+ return this.tryComplete(false);
+ }
+
+ @Override
+ public boolean cancel() {
+ return this.tryComplete(true);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java
new file mode 100644
index 0000000000000000000000000000000000000000..c6e487a4c14e6b82533881d01f32349b9ae28728
--- /dev/null
+++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java
@@ -0,0 +1,770 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet;
+import ca.spottedleaf.moonrise.common.list.ReferenceList;
+import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.common.util.TickThread;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
+import com.mojang.logging.LogUtils;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import net.minecraft.CrashReport;
+import net.minecraft.ReportedException;
+import net.minecraft.core.BlockPos;
+import net.minecraft.network.Connection;
+import net.minecraft.network.PacketSendListener;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ChunkHolder;
+import net.minecraft.server.level.ServerChunkCache;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import net.minecraft.util.VisibleForDebug;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.Mob;
+import net.minecraft.world.entity.ai.village.VillageSiege;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.level.BlockEventData;
+import net.minecraft.world.level.ChunkPos;
+import net.minecraft.world.level.Explosion;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.NaturalSpawner;
+import net.minecraft.world.level.ServerExplosion;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.RedStoneWireBlock;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.TickingBlockEntity;
+import net.minecraft.world.level.chunk.LevelChunk;
+import net.minecraft.world.level.material.Fluid;
+import net.minecraft.world.level.pathfinder.PathTypeCache;
+import net.minecraft.world.level.redstone.CollectingNeighborUpdater;
+import net.minecraft.world.level.redstone.NeighborUpdater;
+import net.minecraft.world.ticks.LevelTicks;
+import org.bukkit.craftbukkit.block.CraftBlockState;
+import org.slf4j.Logger;
+import javax.annotation.Nullable;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+public final class RegionizedWorldData {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0];
+
+ public static final RegionizedData.RegioniserCallback<RegionizedWorldData> REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() {
+ @Override
+ public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) {
+ // connections
+ for (final Connection conn : from.connections) {
+ into.connections.add(conn);
+ }
+ // time
+ final long fromRedstoneTimeOffset = into.redstoneTime - from.redstoneTime;
+ // entities
+ for (final ServerPlayer player : from.localPlayers) {
+ into.localPlayers.add(player);
+ into.nearbyPlayers.addPlayer(player);
+ }
+ for (final Entity entity : from.allEntities) {
+ into.allEntities.add(entity);
+ entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
+ }
+ for (final Entity entity : from.loadedEntities) {
+ into.loadedEntities.add(entity);
+ }
+ for (final Iterator<Entity> iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) {
+ into.entityTickList.add(iterator.next());
+ }
+ for (final Iterator<Mob> iterator = from.navigatingMobs.unsafeIterator(); iterator.hasNext();) {
+ into.navigatingMobs.add(iterator.next());
+ }
+ for (final Iterator<Entity> iterator = from.trackerEntities.iterator(); iterator.hasNext();) {
+ into.trackerEntities.add(iterator.next());
+ }
+ for (final Iterator<Entity> 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<ServerChunkCache.ChunkAndHolder> iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) {
+ into.entityTickingChunks.add(iterator.next());
+ }
+ for (final Iterator<ServerChunkCache.ChunkAndHolder> iterator = from.tickingChunks.iterator(); iterator.hasNext();) {
+ into.tickingChunks.add(iterator.next());
+ }
+ for (final Iterator<ServerChunkCache.ChunkAndHolder> 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<RegionizedWorldData> regionToData,
+ final ReferenceOpenHashSet<RegionizedWorldData> dataSet) {
+ // connections
+ for (final Connection conn : from.connections) {
+ final ServerPlayer player = conn.getPlayer();
+ final ChunkPos pos = player.chunkPosition();
+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
+ // the chunk holder must _exist_, and so the region section exists.
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
+ .connections.add(conn);
+ }
+ // entities
+ for (final ServerPlayer player : from.localPlayers) {
+ final ChunkPos pos = player.chunkPosition();
+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
+ // the chunk holder must _exist_, and so the region section exists.
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
+ into.localPlayers.add(player);
+ into.nearbyPlayers.addPlayer(player);
+ }
+ for (final Entity entity : from.allEntities) {
+ final ChunkPos pos = entity.chunkPosition();
+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
+ // the chunk holder must _exist_, and so the region section exists.
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
+ into.allEntities.add(entity);
+ // Note: entityTickList is a subset of allEntities
+ if (from.entityTickList.contains(entity)) {
+ into.entityTickList.add(entity);
+ }
+ // Note: loadedEntities is a subset of allEntities
+ if (from.loadedEntities.contains(entity)) {
+ into.loadedEntities.add(entity);
+ }
+ // Note: navigatingMobs is a subset of allEntities
+ if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) {
+ into.navigatingMobs.add(mob);
+ }
+ if (from.trackerEntities.contains(entity)) {
+ into.trackerEntities.add(entity);
+ }
+ if (from.trackerUnloadedEntities.contains(entity)) {
+ into.trackerUnloadedEntities.add(entity);
+ }
+ }
+ // block ticking
+ for (final BlockEventData blockEventData : from.blockEvents) {
+ final BlockPos pos = blockEventData.pos();
+ final int chunkX = pos.getX() >> 4;
+ final int chunkZ = pos.getZ() >> 4;
+
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
+ // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events
+ // is just some list. So if it unloads, I guess it's just lost.
+ if (into != null) {
+ into.blockEvents.add(blockEventData);
+ }
+ }
+
+ final Long2ReferenceOpenHashMap<LevelTicks<Block>> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
+ final Long2ReferenceOpenHashMap<LevelTicks<Fluid>> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
+
+ for (final Iterator<Long2ReferenceMap.Entry<RegionizedWorldData>> iterator = regionToData.long2ReferenceEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ReferenceMap.Entry<RegionizedWorldData> 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<ServerChunkCache.ChunkAndHolder> iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) {
+ final ServerChunkCache.ChunkAndHolder holder = iterator.next();
+ final ChunkPos pos = holder.chunk().getPos();
+
+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
+ .entityTickingChunks.add(holder);
+ }
+ for (final Iterator<ServerChunkCache.ChunkAndHolder> iterator = from.tickingChunks.iterator(); iterator.hasNext();) {
+ final ServerChunkCache.ChunkAndHolder holder = iterator.next();
+ final ChunkPos pos = holder.chunk().getPos();
+
+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
+ .tickingChunks.add(holder);
+ }
+ for (final Iterator<ServerChunkCache.ChunkAndHolder> iterator = from.chunks.iterator(); iterator.hasNext();) {
+ final ServerChunkCache.ChunkAndHolder holder = iterator.next();
+ final ChunkPos pos = holder.chunk().getPos();
+
+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
+ .chunks.add(holder);
+ }
+
+ // redstone torches
+ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) {
+ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) {
+ final BlockPos pos = info.pos;
+
+ final RegionizedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift));
+ if (worldData != null) {
+ if (worldData.redstoneUpdateInfos == null) {
+ worldData.redstoneUpdateInfos = new ArrayDeque<>();
+ }
+ worldData.redstoneUpdateInfos.add(info);
+ } // else: chunk unloaded
+ }
+ }
+ // mob spawning
+ for (final RegionizedWorldData regionizedWorldData : dataSet) {
+ regionizedWorldData.catSpawnerNextTick = from.catSpawnerNextTick;
+ regionizedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick;
+ regionizedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick;
+ regionizedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay;
+ regionizedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance;
+ regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay;
+ regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid
+ }
+ // chunkHoldersToBroadcast
+ for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) {
+ final ChunkPos pos = chunkHolder.getPos();
+
+ // Possible for get() to return null, as the chunk holder is not removed during unload
+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
+ if (into != null) {
+ into.chunkHoldersToBroadcast.add(chunkHolder);
+ }
+ }
+ }
+ };
+
+ public final ServerLevel world;
+
+ private RegionizedServer.WorldLevelData tickData;
+
+ // connections
+ public final List<Connection> 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<ServerPlayer> localPlayers = new ArrayList<>();
+ private final NearbyPlayers nearbyPlayers;
+ private final ReferenceList<Entity> allEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY);
+ private final ReferenceList<Entity> loadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY);
+ private final IteratorSafeOrderedReferenceSet<Entity> entityTickList = new IteratorSafeOrderedReferenceSet<>();
+ private final IteratorSafeOrderedReferenceSet<Mob> navigatingMobs = new IteratorSafeOrderedReferenceSet<>();
+ public final ReferenceList<Entity> trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
+ public final ReferenceList<Entity> trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
+
+ // block ticking
+ private final ObjectLinkedOpenHashSet<BlockEventData> blockEvents = new ObjectLinkedOpenHashSet<>();
+ private final LevelTicks<Block> blockLevelTicks;
+ private final LevelTicks<Fluid> fluidLevelTicks;
+
+ // tile entity ticking
+ private final List<TickingBlockEntity> pendingBlockEntityTickers = new ArrayList<>();
+ private final List<TickingBlockEntity> blockEntityTickers = new ArrayList<>();
+ private boolean tickingBlockEntities;
+
+ // time
+ private long redstoneTime = 1L;
+
+ public long getRedstoneGameTime() {
+ return this.redstoneTime;
+ }
+
+ public void setRedstoneGameTime(final long to) {
+ this.redstoneTime = to;
+ }
+
+ // ticking chunks
+ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDER_ARRAY = new ServerChunkCache.ChunkAndHolder[0];
+ private final ReferenceList<ServerChunkCache.ChunkAndHolder> entityTickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY);
+ private final ReferenceList<ServerChunkCache.ChunkAndHolder> tickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY);
+ private final ReferenceList<ServerChunkCache.ChunkAndHolder> chunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY);
+
+ // Paper/CB api hook misc
+ // don't bother to merge/split these, no point
+ // From ServerLevel
+ public boolean hasPhysicsEvent = true; // Paper
+ public boolean hasEntityMoveEvent = false; // Paper
+ // Paper start - Optimize Hoppers
+ public boolean skipPullModeEventFire = false;
+ public boolean skipPushModeEventFire = false;
+ public boolean skipHopperEvents = false;
+ // Paper end - Optimize Hoppers
+ public long lastMidTickExecute;
+ public long lastMidTickExecuteFailure;
+ // From Level
+ public boolean populating;
+ public final NeighborUpdater neighborUpdater;
+ public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
+ public boolean captureBlockStates = false;
+ public boolean captureTreeGeneration = false;
+ public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
+ public final Map<BlockPos, CraftBlockState> capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper
+ public final Map<BlockPos, BlockEntity> capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper
+ public List<ItemEntity> 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<ServerExplosion.CacheKey, Float> explosionDensityCache = new HashMap<>(64, 0.25f);
+ public final PathTypeCache pathTypesByPosCache = new PathTypeCache();
+ public final List<LevelChunk> temporaryChunkTickList = new java.util.ArrayList<>();
+ public final Set<ChunkHolder> chunkHoldersToBroadcast = new ReferenceLinkedOpenHashSet<>();
+
+ // not transient
+ public java.util.ArrayDeque<net.minecraft.world.level.block.RedstoneTorchBlock.Toggle> redstoneUpdateInfos;
+
+ // Mob spawning
+ public final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<ServerPlayer> spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>();
+ public int catSpawnerNextTick = 0;
+ public int patrolSpawnerNextTick = 0;
+ public int phantomSpawnerNextTick = 0;
+ public int wanderingTraderTickDelay = Integer.MIN_VALUE;
+ public int wanderingTraderSpawnDelay;
+ public int wanderingTraderSpawnChance;
+ public VillageSiegeState villageSiegeState = new VillageSiegeState();
+
+ public static final class VillageSiegeState {
+ public boolean hasSetupSiege;
+ public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE;
+ public int zombiesToSpawn;
+ public int nextSpawnTime;
+ public int spawnX;
+ public int spawnY;
+ public int spawnZ;
+ }
+ // Redstone
+ public final alternate.current.wire.WireHandler wireHandler;
+ public final io.papermc.paper.redstone.RedstoneWireTurbo turbo;
+
+ public RegionizedWorldData(final ServerLevel world) {
+ this.world = world;
+ this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, true);
+ this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, false);
+ this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax);
+ this.nearbyPlayers = new NearbyPlayers(world);
+ this.wireHandler = new alternate.current.wire.WireHandler(world);
+ this.turbo = new io.papermc.paper.redstone.RedstoneWireTurbo((RedStoneWireBlock)Blocks.REDSTONE_WIRE);
+
+ // tasks may be drained before the region ticks, so we must set up the tick data early just in case
+ this.updateTickData();
+ }
+
+ public void checkWorld(final Level against) {
+ if (this.world != against) {
+ throw new IllegalStateException("World mismatch: expected " + this.world.getWorld().getName() + " but got " + (against == null ? "null" : against.getWorld().getName()));
+ }
+ }
+
+ public RegionizedServer.WorldLevelData getTickData() {
+ return this.tickData;
+ }
+
+ private long lagCompensationTick;
+
+ public long getLagCompensationTick() {
+ return this.lagCompensationTick;
+ }
+
+ public void updateTickData() {
+ this.tickData = this.world.tickData;
+ this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
+ this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
+ this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers
+ // always subtract from server init so that the tick starts at zero, allowing us to cast to int without much worry
+ this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / TickRegionScheduler.TIME_BETWEEN_TICKS;
+ }
+
+ public NearbyPlayers getNearbyPlayers() {
+ return this.nearbyPlayers;
+ }
+
+ private static void cleanUpConnection(final Connection conn) {
+ // note: ALL connections HERE have a player
+ final ServerPlayer player = conn.getPlayer();
+ // now that the connection is removed, we can allow this region to die
+ player.serverLevel().chunkSource.removeTicketAtLevel(
+ ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos,
+ ChunkHolderManager.MAX_TICKET_LEVEL,
+ player.connection.disconnectTicketId
+ );
+ }
+
+ // connections
+ public void tickConnections() {
+ final List<Connection> 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<Entity> getLocalEntities() {
+ return this.allEntities;
+ }
+
+ public Entity[] getLocalEntitiesCopy() {
+ return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class);
+ }
+
+ public List<ServerPlayer> 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<Entity> 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<Entity> action) {
+ final IteratorSafeOrderedReferenceSet.Iterator<Entity> 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<Mob> 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<? extends BlockEventData> blockEvents) {
+ for (final BlockEventData blockEventData : blockEvents) {
+ this.pushBlockEvent(blockEventData);
+ }
+ }
+
+ public void removeIfBlockEvents(final Predicate<? super BlockEventData> predicate) {
+ for (final Iterator<BlockEventData> 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<Block> getBlockLevelTicks() {
+ return this.blockLevelTicks;
+ }
+
+ public LevelTicks<Fluid> 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<TickingBlockEntity> getBlockEntityTickers() {
+ return this.blockEntityTickers;
+ }
+
+ public void pushPendingTickingBlockEntities() {
+ if (!this.pendingBlockEntityTickers.isEmpty()) {
+ this.blockEntityTickers.addAll(this.pendingBlockEntityTickers);
+ this.pendingBlockEntityTickers.clear();
+ }
+ }
+
+ // ticking chunks
+ public void addEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) {
+ this.entityTickingChunks.add(holder);
+ TickRegions.RegionStats.updateCurrentRegion();
+ }
+
+ public void removeEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) {
+ this.entityTickingChunks.remove(holder);
+ TickRegions.RegionStats.updateCurrentRegion();
+ }
+
+ public ReferenceList<ServerChunkCache.ChunkAndHolder> getEntityTickingChunks() {
+ return this.entityTickingChunks;
+ }
+
+ public void addTickingChunk(final ServerChunkCache.ChunkAndHolder holder) {
+ this.tickingChunks.add(holder);
+ TickRegions.RegionStats.updateCurrentRegion();
+ }
+
+ public void removeTickingChunk(final ServerChunkCache.ChunkAndHolder holder) {
+ this.tickingChunks.remove(holder);
+ TickRegions.RegionStats.updateCurrentRegion();
+ }
+
+ public ReferenceList<ServerChunkCache.ChunkAndHolder> getTickingChunks() {
+ return this.tickingChunks;
+ }
+
+ public void addChunk(final ServerChunkCache.ChunkAndHolder holder) {
+ this.chunks.add(holder);
+ TickRegions.RegionStats.updateCurrentRegion();
+ }
+
+ public void removeChunk(final ServerChunkCache.ChunkAndHolder holder) {
+ this.chunks.remove(holder);
+ TickRegions.RegionStats.updateCurrentRegion();
+ }
+
+ public ReferenceList<ServerChunkCache.ChunkAndHolder> 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 <T extends Entity> 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<Entity> onComplete) {
+ teleport(from, useFromRootVehicle, to, yaw, pitch, teleportFlags, cause, onComplete, null);
+ }
+
+ public static <T extends Entity> 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<Entity> onComplete,
+ final java.util.function.Predicate<T> preTeleport) {
+ // retrieve coordinates
+ final CallbackCompletable<Location> 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<R extends ThreadedRegionizer.ThreadedRegionData<R, S>, 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<ThreadedRegionSection<R, S>> sections = new SWMRLong2ObjectHashTable<>();
+ private final SWMRLong2ObjectHashTable<ThreadedRegion<R, S>> regionsById = new SWMRLong2ObjectHashTable<>();
+ private final RegionCallbacks<R, S> callbacks;
+ private final StampedLock regionLock = new StampedLock();
+ private Thread writeLockOwner;
+
+ /*
+ static final record Operation(String type, int chunkX, int chunkZ) {}
+ private final MultiThreadedQueue<Operation> 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<R, S> 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<Operation> 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<R, S> region) {
+ final ThreadedRegion<R, S> 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<R, S> region) {
+ final ThreadedRegion<R, S> 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<? super ThreadedRegion<R, S>> consumer) {
+ this.regionLock.readLock();
+ try {
+ this.regionsById.forEachValue(consumer);
+ } finally {
+ this.regionLock.tryUnlockRead();
+ }
+ }
+
+ public void computeForAllRegionsUnsynchronised(final Consumer<? super ThreadedRegion<R, S>> consumer) {
+ this.regionsById.forEachValue(consumer);
+ }
+
+ public int computeForRegions(final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ,
+ final Consumer<Set<ThreadedRegion<R, S>>> 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<ThreadedRegion<R, S>> set = new ReferenceOpenHashSet<>();
+
+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
+ final ThreadedRegionSection<R, S> 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<R, S> 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<R, S> section = this.sections.get(sectionKey);
+
+ return section == null ? null : section.getRegion();
+ }
+
+ public ThreadedRegion<R, S> 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<R, S> optimisticSection = this.sections.get(sectionKey);
+ final ThreadedRegion<R, S> 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<R, S> 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.
+ *
+ * <p>
+ * Note that it is illegal to additionally call addChunk or removeChunk for the same
+ * region section in parallel.
+ * </p>
+ */
+ 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<R, S> 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<R, S> section = this.sections.get(sectionKey);
+
+ List<ThreadedRegionSection<R, S>> 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<ThreadedRegion<R, S>> 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<R, S> 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<R, S> 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<R, S> firstUnlockedRegion = null;
+
+ for (final ThreadedRegion<R, S> 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<R, S> 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<R, S> 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<R, S> 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<R, S> 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<ThreadedRegion<R, S>> expectingMergeFrom = region.expectingMergeFrom.clone();
+ for (final ThreadedRegion<R, S> 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<R, S> 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<ThreadedRegionSection<R, S>> 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<List<ThreadedRegionSection<R, S>>> 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<ThreadedRegionSection<R, S>> currRegion = new ArrayList<>();
+ final Iterator<ThreadedRegionSection<R, S>> firstIterator = recalculateSections.values().iterator();
+
+ currRegion.add(firstIterator.next());
+ firstIterator.remove();
+ search_loop:
+ for (int idx = 0; idx < currRegion.size(); ++idx) {
+ final ThreadedRegionSection<R, S> 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<R, S> 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<ThreadedRegion<R, S>> 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<ThreadedRegion<R, S>> newRegionsMap = new Long2ReferenceOpenHashMap<>();
+ final ReferenceOpenHashSet<ThreadedRegion<R, S>> newRegionsSet = new ReferenceOpenHashSet<>(newRegionObjects);
+
+ for (int i = 0, len = newRegions.size(); i < len; i++) {
+ final List<ThreadedRegionSection<R, S>> sections = newRegions.get(i);
+ final ThreadedRegion<R, S> newRegion = newRegionObjects.get(i);
+
+ for (final ThreadedRegionSection<R, S> section : sections) {
+ section.setRegionRelease(null);
+ newRegion.addSection(section);
+ final ThreadedRegion<R, S> 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<R, S> 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<R extends ThreadedRegionData<R, S>, 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<ThreadedRegionSection<R, S>> sectionByKey = new Long2ReferenceOpenHashMap<>();
+ private final ReferenceOpenHashSet<ThreadedRegionSection<R, S>> deadSections = new ReferenceOpenHashSet<>();
+
+ public final ThreadedRegionizer<R, S> regioniser;
+
+ private final R data;
+
+ private final ReferenceOpenHashSet<ThreadedRegion<R, S>> mergeIntoLater = new ReferenceOpenHashSet<>();
+ private final ReferenceOpenHashSet<ThreadedRegion<R, S>> expectingMergeFrom = new ReferenceOpenHashSet<>();
+
+ public ThreadedRegion(final ThreadedRegionizer<R, S> 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<R, S> 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<ThreadedRegion<R, S>> into, final ReferenceOpenHashSet<ThreadedRegion<R, S>> regions) {
+ if (this.data != null) {
+ this.data.split(this.regioniser, into, regions);
+ }
+ }
+
+ boolean killAndMergeInto(final ThreadedRegion<R, S> 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<R, S> 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<R, S> section : this.sectionByKey.values()) {
+ section.setRegionRelease(null);
+ mergeTarget.addSection(section);
+ }
+ for (final ThreadedRegionSection<R, S> 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<R, S> 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<R, S> 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<R, S> 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<R, S> section) {
+ this.deadSections.remove(section);
+ }
+
+ private void addDeadSection(final ThreadedRegionSection<R, S> section) {
+ this.deadSections.add(section);
+ }
+
+ private void addSection(final ThreadedRegionSection<R, S> 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<ThreadedRegionSection<R, S>> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) {
+ final ThreadedRegionSection<R, S> section = iterator.next();
+
+ ret.append(section.toString());
+ if (iterator.hasNext()) {
+ ret.append(',');
+ }
+ }
+ ret.append(']');
+
+ ret.append('}');
+ return ret.toString();
+ }
+ }
+
+ public static final class ThreadedRegionSection<R extends ThreadedRegionData<R, S>, 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<R, S> region;
+ private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class);
+
+ public final ThreadedRegionizer<R, S> regioniser;
+
+ private final int regionChunkShift;
+ private final int regionChunkMask;
+
+ private final S data;
+
+ private ThreadedRegion<R, S> getRegionPlain() {
+ return (ThreadedRegion<R, S>)REGION_HANDLE.get(this);
+ }
+
+ private ThreadedRegion<R, S> getRegionAcquire() {
+ return (ThreadedRegion<R, S>)REGION_HANDLE.getAcquire(this);
+ }
+
+ private void setRegionRelease(final ThreadedRegion<R, S> 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<R, S> 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<R, S> 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<R, S> 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<R, S> 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<R extends ThreadedRegionData<R, S>, S extends ThreadedRegionSectionData> {
+
+ /**
+ * Splits this region data into the specified regions set.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @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<R, S> regioniser, final Long2ReferenceOpenHashMap<ThreadedRegion<R, S>> into,
+ final ReferenceOpenHashSet<ThreadedRegion<R, S>> 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.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @param into Specified region.
+ */
+ public void mergeInto(final ThreadedRegion<R, S> into);
+ }
+
+ public static interface ThreadedRegionSectionData {}
+
+ public static interface RegionCallbacks<R extends ThreadedRegionData<R, S>, S extends ThreadedRegionSectionData> {
+
+ /**
+ * Creates new section data for the specified section x and section z.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @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.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @param forRegion The region to create the data for.
+ * @return New region data, may be {@code null}.
+ */
+ public R createNewData(final ThreadedRegion<R, S> 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.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @param region The region that was created.
+ */
+ public void onRegionCreate(final ThreadedRegion<R, S> 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.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @param region The region that is about to be destroyed.
+ */
+ public void onRegionDestroy(final ThreadedRegion<R, S> 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.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @param region The region that is now active.
+ */
+ public void onRegionActive(final ThreadedRegion<R, S> 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.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @param region The region that is now inactive.
+ */
+ public void onRegionInactive(final ThreadedRegion<R, S> 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.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @param from The region that will be merged into the target.
+ * @param into The target of the merge.
+ */
+ public void preMerge(final ThreadedRegion<R, S> from, final ThreadedRegion<R, S> 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.
+ * <p>
+ * <b>Note:</b>
+ * </p>
+ * <p>
+ * 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.
+ * </p>
+ * @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<R, S> from, final List<ThreadedRegion<R, S>> 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<TickRegionScheduler.TickTime> 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<TickRegionScheduler.TickTime> 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<TickInformation> collapsedData = new ArrayList<>();
+ for (int i = 0, len = allData.size(); i < len; ++i) {
+ final List<TickRegionScheduler.TickTime> toCollapse = new ArrayList<>();
+ TickRegionScheduler.TickTime lastTick = null;
+ for (;i < len; ++i) {
+ final TickRegionScheduler.TickTime time = allData.get(i);
+ if (!time.isTickExecution()) {
+ toCollapse.add(time);
+ continue;
+ }
+ lastTick = time;
+ break;
+ }
+
+ if (toCollapse.isEmpty()) {
+ // nothing to collapse
+ final TickRegionScheduler.TickTime last = allData.get(i);
+ collapsedData.add(
+ new TickInformation(
+ last.differenceFromLastTick(),
+ last.tickLength(),
+ last.supportCPUTime() ? last.tickCpuTime() : 0L
+ )
+ );
+ } else {
+ long totalTickTime = 0L;
+ long totalCpuTime = 0L;
+ for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) {
+ final TickRegionScheduler.TickTime time = toCollapse.get(k);
+ totalTickTime += time.tickLength();
+ totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L;
+ }
+ if (i < len) {
+ // we know there is a tick to collapse into
+ final TickRegionScheduler.TickTime last = allData.get(i);
+ collapsedData.add(
+ new TickInformation(
+ last.differenceFromLastTick(),
+ last.tickLength() + totalTickTime,
+ (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime
+ )
+ );
+ } else {
+ // we do not have a tick to collapse into, so we must make one up
+ // we will assume that the tick is "starting now" and ongoing
+
+ // compute difference between imaginary tick and last tick
+ final long differenceBetweenTicks;
+ if (lastTick != null) {
+ // we have a last tick, use it
+ differenceBetweenTicks = lastTick.tickStart();
+ } else {
+ // we don't have a last tick, so we must make one up that makes sense
+ // if the current interval exceeds the max tick time, then use it
+
+ // Otherwise use the interval length.
+ // This is how differenceFromLastTick() works on TickTime when there is no previous interval.
+ differenceBetweenTicks = Math.max(
+ TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime
+ );
+ }
+
+ collapsedData.add(
+ new TickInformation(
+ differenceBetweenTicks,
+ totalTickTime,
+ totalCpuTime
+ )
+ );
+ }
+ }
+ }
+
+
+ final int collectedTicks = collapsedData.size();
+ final long[] tickStartToStartDifferences = new long[collectedTicks];
+ final long[] timePerTickDataRaw = new long[collectedTicks];
+ final long[] missingCPUTimeDataRaw = new long[collectedTicks];
+
+ long totalTimeTicking = 0L;
+
+ int i = 0;
+ for (final TickInformation time : collapsedData) {
+ tickStartToStartDifferences[i] = time.differenceFromLastTick();
+ final long timePerTick = timePerTickDataRaw[i] = time.tickTime();
+ missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU());
+
+ ++i;
+
+ totalTimeTicking += timePerTick;
+ }
+
+ Arrays.sort(tickStartToStartDifferences);
+ Arrays.sort(timePerTickDataRaw);
+ Arrays.sort(missingCPUTimeDataRaw);
+
+ // Note: computeSegmentData cannot take start == end
+ final int allStart = 0;
+ final int allEnd = collectedTicks;
+ final int percent95BestStart = 0;
+ final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks);
+ final int percent99BestStart = 0;
+ // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end
+ final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks);
+ final int percent1WorstStart = (int)(0.99 * collectedTicks);
+ final int percent1WorstEnd = collectedTicks;
+ final int percent5WorstStart = (int)(0.95 * collectedTicks);
+ final int percent5WorstEnd = collectedTicks;
+
+ final SegmentedAverage tpsData = computeSegmentedAverage(
+ tickStartToStartDifferences,
+ allStart, allEnd,
+ percent99BestStart, percent99BestEnd,
+ percent95BestStart, percent95BestEnd,
+ percent1WorstStart, percent1WorstEnd,
+ percent5WorstStart, percent5WorstEnd,
+ true
+ );
+
+ final SegmentedAverage timePerTickData = computeSegmentedAverage(
+ timePerTickDataRaw,
+ allStart, allEnd,
+ percent99BestStart, percent99BestEnd,
+ percent95BestStart, percent95BestEnd,
+ percent1WorstStart, percent1WorstEnd,
+ percent5WorstStart, percent5WorstEnd,
+ false
+ );
+
+ final SegmentedAverage missingCPUTimeData = computeSegmentedAverage(
+ missingCPUTimeDataRaw,
+ allStart, allEnd,
+ percent99BestStart, percent99BestEnd,
+ percent95BestStart, percent95BestEnd,
+ percent1WorstStart, percent1WorstEnd,
+ percent5WorstStart, percent5WorstEnd,
+ false
+ );
+
+ final double utilisation = (double)totalTimeOverInterval / (double)this.interval;
+
+ return new TickReportData(
+ collectedTicks,
+ intervalStart,
+ intervalEnd,
+ totalTimeTicking,
+ utilisation,
+
+ tpsData,
+ timePerTickData,
+ missingCPUTimeData
+ );
+ }
+
+ public static final record TickReportData(
+ int collectedTicks,
+ long collectedTickIntervalStart,
+ long collectedTickIntervalEnd,
+ long totalTimeTicking,
+ double utilisation,
+
+ SegmentedAverage tpsData,
+ // in ns
+ SegmentedAverage timePerTickData,
+ // in ns
+ SegmentedAverage missingCPUTimeData
+ ) {}
+
+ public static final record SegmentedAverage(
+ SegmentData segmentAll,
+ SegmentData segment99PercentBest,
+ SegmentData segment95PercentBest,
+ SegmentData segment5PercentWorst,
+ SegmentData segment1PercentWorst
+ ) {}
+
+ public static final record SegmentData(
+ int count,
+ double average,
+ double median,
+ double least,
+ double greatest
+ ) {}
+}
\ No newline at end of file
diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java
new file mode 100644
index 0000000000000000000000000000000000000000..18f57216522a8e22ea6c217c05588f8be13f5c88
--- /dev/null
+++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java
@@ -0,0 +1,564 @@
+package io.papermc.paper.threadedregions;
+
+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
+import ca.spottedleaf.moonrise.common.util.TickThread;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.util.TraceUtil;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.world.level.ChunkPos;
+import org.slf4j.Logger;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadMXBean;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BooleanSupplier;
+
+public final class TickRegionScheduler {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean();
+ private static final boolean MEASURE_CPU_TIME;
+ static {
+ MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported();
+ if (MEASURE_CPU_TIME) {
+ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true);
+ } else {
+ LOGGER.warn("TickRegionScheduler CPU time measurement is not available");
+ }
+ }
+
+ public static final int TICK_RATE = 20;
+ public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns
+
+ private final SchedulerThreadPool scheduler;
+
+ public TickRegionScheduler(final int threads) {
+ this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() {
+ private final AtomicInteger idGenerator = new AtomicInteger();
+
+ @Override
+ public Thread newThread(final Runnable run) {
+ final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement());
+ ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException);
+ return ret;
+ }
+ });
+ }
+
+ public int getTotalThreadCount() {
+ return this.scheduler.getThreads().length;
+ }
+
+ private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> currentTickingRegion;
+ private RegionizedWorldData currentTickingWorldRegionizedData;
+ private SchedulerThreadPool.SchedulableTick currentTickingTask;
+
+ public TickThreadRunner(final Runnable run, final String name) {
+ super(run, name);
+ }
+ }
+
+ public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick {
+
+ protected long currentTick;
+ protected long lastTickStart;
+
+ protected final TickData tickTimes5s;
+ protected final TickData tickTimes15s;
+ protected final TickData tickTimes1m;
+ protected final TickData tickTimes5m;
+ protected final TickData tickTimes15m;
+ protected TickTime currentTickData;
+ protected Thread currentTickingThread;
+
+ public final TickRegions.TickRegionData region;
+ private final AtomicBoolean cancelled = new AtomicBoolean();
+
+ protected final Schedule tickSchedule;
+
+ private TickRegionScheduler scheduler;
+
+ public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) {
+ this.currentTick = 0L;
+ this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET;
+ this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L));
+ this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L));
+ this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L));
+ this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L));
+ this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L));
+ this.region = region;
+
+ this.setScheduledStart(firstStart);
+ this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS);
+ }
+
+ /**
+ * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)}
+ * so that the tick schedule and scheduled start remain synchronised
+ */
+ protected final void updateScheduledStart(final long to) {
+ this.setScheduledStart(to);
+ this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS);
+ }
+
+ public final void markNonSchedulable() {
+ this.cancelled.set(true);
+ }
+
+ public final boolean isMarkedAsNonSchedulable() {
+ return this.cancelled.get();
+ }
+
+ protected abstract boolean tryMarkTicking();
+
+ protected abstract boolean markNotTicking();
+
+ protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd);
+
+ protected abstract boolean runRegionTasks(final BooleanSupplier canContinue);
+
+ protected abstract boolean hasIntermediateTasks();
+
+ @Override
+ public final boolean hasTasks() {
+ return this.hasIntermediateTasks();
+ }
+
+ @Override
+ public final Boolean runTasks(final BooleanSupplier canContinue) {
+ if (this.cancelled.get()) {
+ return null;
+ }
+
+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
+ final long tickStart = System.nanoTime();
+
+ if (!this.tryMarkTicking()) {
+ if (!this.cancelled.get()) {
+ throw new IllegalStateException("Scheduled region should be acquirable");
+ }
+ // region was killed
+ return null;
+ }
+
+ TickRegionScheduler.setTickTask(this);
+ if (this.region != null) {
+ TickRegionScheduler.setTickingRegion(this.region.region);
+ }
+
+ synchronized (this) {
+ this.currentTickData = new TickTime(
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart,
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
+ false
+ );
+ this.currentTickingThread = Thread.currentThread();
+ }
+
+ final boolean ret;
+ try {
+ ret = this.runRegionTasks(() -> {
+ return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean();
+ });
+ } catch (final Throwable thr) {
+ this.scheduler.regionFailed(this, true, thr);
+ // don't release region for another tick
+ return null;
+ } finally {
+ final long tickEnd = System.nanoTime();
+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
+
+ final TickTime time = new TickTime(
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET,
+ tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false
+ );
+
+ this.addTickTime(time);
+ TickRegionScheduler.setTickTask(null);
+ if (this.region != null) {
+ TickRegionScheduler.setTickingRegion(null);
+ }
+ }
+
+ return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret);
+ }
+
+ @Override
+ public final boolean runTick() {
+ // Remember, we are supposed use setScheduledStart if we return true here, otherwise
+ // the scheduler will try to schedule for the same time.
+ if (this.cancelled.get()) {
+ return false;
+ }
+
+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
+ final long tickStart = System.nanoTime();
+
+ // use max(), don't assume that tickStart >= scheduledStart
+ final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart));
+
+ if (!this.tryMarkTicking()) {
+ if (!this.cancelled.get()) {
+ throw new IllegalStateException("Scheduled region should be acquirable");
+ }
+ // region was killed
+ return false;
+ }
+ if (this.cancelled.get()) {
+ this.markNotTicking();
+ // region should be killed
+ return false;
+ }
+
+ TickRegionScheduler.setTickTask(this);
+ if (this.region != null) {
+ TickRegionScheduler.setTickingRegion(this.region.region);
+ }
+ this.incrementTickCount();
+ final long lastTickStart = this.lastTickStart;
+ this.lastTickStart = tickStart;
+
+ final long scheduledStart = this.getScheduledStart();
+ final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS;
+
+ synchronized (this) {
+ this.currentTickData = new TickTime(
+ lastTickStart, scheduledStart, tickStart, cpuStart,
+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
+ true
+ );
+ this.currentTickingThread = Thread.currentThread();
+ }
+
+ try {
+ // next start isn't updated until the end of this tick
+ this.tickRegion(tickCount, tickStart, scheduledEnd);
+ } catch (final Throwable thr) {
+ this.scheduler.regionFailed(this, false, thr);
+ // regionFailed will schedule a shutdown, so we should avoid letting this region tick further
+ return false;
+ } finally {
+ final long tickEnd = System.nanoTime();
+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
+
+ // in order to ensure all regions get their chance at scheduling, we have to ensure that regions
+ // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest
+ // of the current time and "ideal" next tick start.
+ this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS);
+ this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS)));
+
+ final TickTime time = new TickTime(
+ lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true
+ );
+
+ this.addTickTime(time);
+ TickRegionScheduler.setTickTask(null);
+ if (this.region != null) {
+ TickRegionScheduler.setTickingRegion(null);
+ }
+ }
+
+ // Only AFTER updating the tickStart
+ return this.markNotTicking() && !this.cancelled.get();
+ }
+
+ /**
+ * Only safe to call if this tick data matches the current ticking region.
+ */
+ protected void addTickTime(final TickTime time) {
+ synchronized (this) {
+ this.currentTickData = null;
+ this.currentTickingThread = null;
+ this.tickTimes5s.addDataFrom(time);
+ this.tickTimes15s.addDataFrom(time);
+ this.tickTimes1m.addDataFrom(time);
+ this.tickTimes5m.addDataFrom(time);
+ this.tickTimes15m.addDataFrom(time);
+ }
+ }
+
+ private TickTime adjustCurrentTickData(final long tickEnd) {
+ final TickTime currentTickData = this.currentTickData;
+ if (currentTickData == null) {
+ return null;
+ }
+
+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L;
+
+ return new TickTime(
+ currentTickData.previousTickStart(), currentTickData.scheduledTickStart(),
+ currentTickData.tickStart(), currentTickData.tickStartCPU(),
+ tickEnd, cpuEnd,
+ MEASURE_CPU_TIME, currentTickData.isTickExecution()
+ );
+ }
+
+ public final TickData.TickReportData getTickReport5s(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ public final TickData.TickReportData getTickReport15s(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ public final TickData.TickReportData getTickReport1m(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ public final TickData.TickReportData getTickReport5m(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ public final TickData.TickReportData getTickReport15m(final long currTime) {
+ synchronized (this) {
+ return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
+ }
+ }
+
+ /**
+ * Only safe to call if this tick data matches the current ticking region.
+ */
+ private void incrementTickCount() {
+ ++this.currentTick;
+ }
+
+ /**
+ * Only safe to call if this tick data matches the current ticking region.
+ */
+ public final long getCurrentTick() {
+ return this.currentTick;
+ }
+
+ protected final void setCurrentTick(final long value) {
+ this.currentTick = value;
+ }
+ }
+
+ // All time units are in nanoseconds.
+ public static final record TickTime(
+ long previousTickStart,
+ long scheduledTickStart,
+ long tickStart,
+ long tickStartCPU,
+ long tickEnd,
+ long tickEndCPU,
+ boolean supportCPUTime,
+ boolean isTickExecution
+ ) {
+ /**
+ * The difference between the start tick time and the scheduled start tick time. This value is
+ * < 0 if the tick started before the scheduled tick time.
+ * Only valid when {@link #isTickExecution()} is {@code true}.
+ */
+ public final long startOvershoot() {
+ return this.tickStart - this.scheduledTickStart;
+ }
+
+ /**
+ * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong).
+ */
+ public final long tickLength() {
+ return this.tickEnd - this.tickStart;
+ }
+
+ /**
+ * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength,
+ * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE
+ * if CPU time measurement is not supported.
+ */
+ public final long tickCpuTime() {
+ if (!this.supportCPUTime()) {
+ return Long.MIN_VALUE;
+ }
+ return this.tickEndCPU - this.tickStartCPU;
+ }
+
+ /**
+ * The difference in time from the start of the last tick to the start of the current tick. If there is no
+ * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength).
+ * Only valid when {@link #isTickExecution()} is {@code true}.
+ */
+ public final long differenceFromLastTick() {
+ if (this.hasLastTick()) {
+ return this.tickStart - this.previousTickStart;
+ }
+ return Math.max(TIME_BETWEEN_TICKS, this.tickLength());
+ }
+
+ /**
+ * Returns whether there was a tick that occurred before this one.
+ * Only valid when {@link #isTickExecution()} is {@code true}.
+ */
+ public boolean hasLastTick() {
+ return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET;
+ }
+
+ /*
+ * Remember, this is the expected behavior of the following:
+ *
+ * MSPT: Time per tick. This does not include overshoot time, just the tickLength().
+ *
+ * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick).
+ */
+ }
+}
\ No newline at end of file
diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java
index 8424cf9d4617b4732d44cc460d25b04481068989..df15b1139e71dfe10b8f24ec6d235b99f6d5006a 100644
--- a/io/papermc/paper/threadedregions/TickRegions.java
+++ b/io/papermc/paper/threadedregions/TickRegions.java
@@ -1,10 +1,410 @@
package io.papermc.paper.threadedregions;
-// placeholder class for Folia
-public class TickRegions {
+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
+import ca.spottedleaf.concurrentutil.util.TimeUtil;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.configuration.GlobalConfiguration;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import org.slf4j.Logger;
+import java.util.Iterator;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BooleanSupplier;
+
+public final class TickRegions implements ThreadedRegionizer.RegionCallbacks<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> {
+
+ 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<TickRegionData, TickRegionSectionData> 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<TickRegionData, TickRegionSectionData> 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<TickRegionData, TickRegionSectionData> region) {
+ // nothing for now
+ }
+
+ @Override
+ public void onRegionActive(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region) {
+ final TickRegionData data = region.getData();
+
+ data.tickHandle.checkInitialSchedule();
+ scheduler.scheduleRegion(data.tickHandle);
+ }
+
+ @Override
+ public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> 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<TickRegionData, TickRegionSectionData> from,
+ final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> into) {
+
+ }
+
+ @Override
+ public void preSplit(final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> from,
+ final java.util.List<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> 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<TickRegionData, TickRegionSectionData> {
+
+ 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<TickRegionData, TickRegionSectionData> region;
+ public final ServerLevel world;
+
+ // generic regionised data
+ private final Reference2ReferenceOpenHashMap<RegionizedData<?>, 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<TickRegionData, TickRegionSectionData> 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> T getRegionizedData(final RegionizedData<T> regionizedData) {
+ return (T)this.regionizedData.get(regionizedData);
+ }
+
+ <T> T getOrCreateRegionizedData(final RegionizedData<T> 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<TickRegionData, TickRegionSectionData> regioniser,
+ final Long2ReferenceOpenHashMap<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> into,
+ final ReferenceOpenHashSet<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> 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<TickRegionData, TickRegionSectionData> region : regions) {
+ final TickRegionData data = region.getData();
+ data.tickHandle.copyDeadlineAndTickCount(this.tickHandle);
+ }
+
+ // generic regionised data
+ for (final Iterator<Reference2ReferenceMap.Entry<RegionizedData<?>, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator();
+ dataIterator.hasNext();) {
+ final Reference2ReferenceMap.Entry<RegionizedData<?>, Object> regionDataEntry = dataIterator.next();
+ final RegionizedData<?> data = regionDataEntry.getKey();
+ final Object from = regionDataEntry.getValue();
+
+ final ReferenceOpenHashSet<Object> dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f);
+
+ for (final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region : regions) {
+ dataSet.add(region.getData().getOrCreateRegionizedData(data));
+ }
+
+ final Long2ReferenceOpenHashMap<Object> regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f);
+
+ for (final Iterator<Long2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>>> regionIterator = into.long2ReferenceEntrySet().fastIterator();
+ regionIterator.hasNext();) {
+ final Long2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> entry = regionIterator.next();
+ final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region = entry.getValue();
+ final Object to = region.getData().getOrCreateRegionizedData(data);
+
+ regionToData.put(entry.getLongKey(), to);
+ }
+
+ ((RegionizedData<Object>)data).getCallback().split(from, shift, regionToData, dataSet);
+ }
+
+ // chunk holder manager data
+ {
+ final ReferenceOpenHashSet<ChunkHolderManager.HolderManagerRegionData> dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f);
+
+ for (final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> region : regions) {
+ dataSet.add(region.getData().holderManagerRegionData);
+ }
+
+ final Long2ReferenceOpenHashMap<ChunkHolderManager.HolderManagerRegionData> regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f);
+
+ for (final Iterator<Long2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>>> regionIterator = into.long2ReferenceEntrySet().fastIterator();
+ regionIterator.hasNext();) {
+ final Long2ReferenceMap.Entry<ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData>> entry = regionIterator.next();
+ final ThreadedRegionizer.ThreadedRegion<TickRegionData, TickRegionSectionData> 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<TickRegionData, TickRegionSectionData> 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<Reference2ReferenceMap.Entry<RegionizedData<?>, Object>> iterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Reference2ReferenceMap.Entry<RegionizedData<?>, Object> entry = iterator.next();
+ final RegionizedData<?> regionizedData = entry.getKey();
+ final Object from = entry.getValue();
+ final Object to = into.getData().getOrCreateRegionizedData(regionizedData);
+
+ ((RegionizedData<Object>)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<DecimalFormat> TWO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
+ return new DecimalFormat("#,##0.00");
+ });
+ private static final ThreadLocal<DecimalFormat> ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
+ return new DecimalFormat("#,##0.0");
+ });
+ private static final ThreadLocal<DecimalFormat> 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("/<command> [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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>> 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<TickData.TickReportData> 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<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<ObjectObjectImmutablePair<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, 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<ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData>, TickData.TickReportData>
+ pair = regionsBelowThreshold.get(i);
+
+ final TickData.TickReportData report = pair.right();
+ final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> 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<String> 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<String> getSortedList(final Iterable<String> iterable) {
+ final List<String> ret = new ArrayList<>();
+ for (final String val : iterable) {
+ ret.add(val);
+ }
+
+ ret.sort(String.CASE_INSENSITIVE_ORDER);
+
+ return ret;
+ }
+
+ public static List<String> getSortedList(final Iterable<String> iterable, final String prefix) {
+ final List<String> 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 <T> List<String> getSortedList(final Iterable<T> iterable, final Function<T, String> transform) {
+ final List<String> 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 <T> List<String> getSortedList(final Iterable<T> iterable, final Function<T, String> transform, final String prefix) {
+ final List<String> ret = new ArrayList<>();
+ for (final T val : iterable) {
+ final String string = transform.apply(val);
+ if (string != null && string.regionMatches(0, prefix, 0, prefix.length())) {
+ ret.add(string);
+ }
+ }
+
+ ret.sort(String.CASE_INSENSITIVE_ORDER);
+
+ return ret;
+ }
+
+ public static TextColor getColourForTPS(final double tps) {
+ final double difference = Math.min(Math.abs(20.0 - tps), 20.0);
+ final double coordinate;
+ if (difference <= 2.0) {
+ // >= 18 tps
+ coordinate = 70.0 + ((140.0 - 70.0)/(0.0 - 2.0)) * (difference - 2.0);
+ } else if (difference <= 5.0) {
+ // >= 15 tps
+ coordinate = 30.0 + ((70.0 - 30.0)/(2.0 - 5.0)) * (difference - 5.0);
+ } else if (difference <= 10.0) {
+ // >= 10 tps
+ coordinate = 10.0 + ((30.0 - 10.0)/(5.0 - 10.0)) * (difference - 10.0);
+ } else {
+ // >= 0.0 tps
+ coordinate = 0.0 + ((10.0 - 0.0)/(10.0 - 20.0)) * (difference - 20.0);
+ }
+
+ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f));
+ }
+
+ public static TextColor getColourForMSPT(final double mspt) {
+ final double clamped = Math.min(Math.abs(mspt), 50.0);
+ final double coordinate;
+ if (clamped <= 15.0) {
+ coordinate = 130.0 + ((140.0 - 130.0)/(0.0 - 15.0)) * (clamped - 15.0);
+ } else if (clamped <= 25.0) {
+ coordinate = 90.0 + ((130.0 - 90.0)/(15.0 - 25.0)) * (clamped - 25.0);
+ } else if (clamped <= 35.0) {
+ coordinate = 30.0 + ((90.0 - 30.0)/(25.0 - 35.0)) * (clamped - 35.0);
+ } else if (clamped <= 40.0) {
+ coordinate = 15.0 + ((30.0 - 15.0)/(35.0 - 40.0)) * (clamped - 40.0);
+ } else {
+ coordinate = 0.0 + ((15.0 - 0.0)/(40.0 - 50.0)) * (clamped - 50.0);
+ }
+
+ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f));
+ }
+
+ public static TextColor getUtilisationColourRegion(final double util) {
+ // TODO anything better?
+ // assume 20TPS
+ return getColourForMSPT(util * 50.0);
+ }
+
+ public static ServerPlayer getPlayer(final String name) {
+ for (final ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) {
+ if (player.getGameProfile().getName().equalsIgnoreCase(name)) {
+ return player;
+ }
+ }
+
+ return null;
+ }
+
+ private CommandUtil() {}
+}
\ No newline at end of file
diff --git a/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java b/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc8aa525b3488dc71e7ca0529c6a8c57eaa99e1e
--- /dev/null
+++ b/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java
@@ -0,0 +1,424 @@
+package io.papermc.paper.threadedregions.scheduler;
+
+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
+import ca.spottedleaf.concurrentutil.util.Validate;
+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
+import io.papermc.paper.threadedregions.RegionizedData;
+import io.papermc.paper.threadedregions.RegionizedServer;
+import io.papermc.paper.threadedregions.TickRegionScheduler;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.TicketType;
+import net.minecraft.util.Unit;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.plugin.IllegalPluginAccessException;
+import org.bukkit.plugin.Plugin;
+import java.lang.invoke.VarHandle;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+
+public final class FoliaRegionScheduler implements RegionScheduler {
+
+ private static Runnable wrap(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) {
+ return () -> {
+ try {
+ run.run();
+ } catch (final Throwable throwable) {
+ plugin.getLogger().log(Level.WARNING, "Location task for " + plugin.getDescription().getFullName()
+ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable);
+ }
+ };
+ }
+
+ private static final RegionizedData<Scheduler> 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<ScheduledTask> 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<ScheduledTask> 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<ScheduledTask> 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<Scheduler> REGIONISER_CALLBACK = new RegionizedData.RegioniserCallback<>() {
+ @Override
+ public void merge(final Scheduler from, final Scheduler into, final long fromTickOffset) {
+ for (final Iterator<Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator();
+ sectionIterator.hasNext();) {
+ final Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>> entry = sectionIterator.next();
+ final long sectionKey = entry.getLongKey();
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> section = entry.getValue();
+
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> sectionAdjusted = new Long2ObjectOpenHashMap<>(section.size());
+
+ for (final Iterator<Long2ObjectMap.Entry<List<LocationScheduledTask>>> iterator = section.long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry<List<LocationScheduledTask>> e = iterator.next();
+ final long newTick = e.getLongKey() + fromTickOffset;
+ final List<LocationScheduledTask> tasks = e.getValue();
+
+ sectionAdjusted.put(newTick, tasks);
+ }
+
+ into.tasksByDeadlineBySection.put(sectionKey, sectionAdjusted);
+ }
+ }
+
+ @Override
+ public void split(final Scheduler from, final int chunkToRegionShift, final Long2ReferenceOpenHashMap<Scheduler> regionToData,
+ final ReferenceOpenHashSet<Scheduler> dataSet) {
+ for (final Scheduler into : dataSet) {
+ into.tickCount = from.tickCount;
+ }
+
+ for (final Iterator<Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator();
+ sectionIterator.hasNext();) {
+ final Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>> entry = sectionIterator.next();
+ final long sectionKey = entry.getLongKey();
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> 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<Long2ObjectOpenHashMap<List<LocationScheduledTask>>> tasksByDeadlineBySection = new Long2ObjectOpenHashMap<>();
+
+ private void addTicket(final int sectionX, final int sectionZ) {
+ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world;
+ final int shift = world.moonrise$getRegionChunkShift();
+ final int chunkX = sectionX << shift;
+ final int chunkZ = sectionZ << shift;
+
+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel(
+ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
+ );
+ }
+
+ private void removeTicket(final long sectionKey) {
+ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world;
+ final int shift = world.moonrise$getRegionChunkShift();
+ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << shift;
+ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << shift;
+
+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel(
+ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
+ );
+ }
+
+ private void queueTask(final LocationScheduledTask task, final long delay) {
+ // note: must be on the thread that owns this scheduler
+ // note: delay > 0
+
+ final World world = task.world;
+ if (world == null) {
+ // cancelled
+ return;
+ }
+
+ final int shift = ((CraftWorld)world).getHandle().moonrise$getRegionChunkShift();
+ final int sectionX = task.chunkX >> shift;
+ final int sectionZ = task.chunkZ >> shift;
+
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> 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<LocationScheduledTask> run = new ArrayList<>();
+
+ for (final Iterator<Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>>> sectionIterator = this.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator();
+ sectionIterator.hasNext();) {
+ final Long2ObjectMap.Entry<Long2ObjectOpenHashMap<List<LocationScheduledTask>>> entry = sectionIterator.next();
+ final long sectionKey = entry.getLongKey();
+ final Long2ObjectOpenHashMap<List<LocationScheduledTask>> section = entry.getValue();
+
+ final List<LocationScheduledTask> 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<ScheduledTask> 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<ScheduledTask> run) {
+ this.plugin = plugin;
+ this.world = world;
+ this.chunkX = chunkX;
+ this.chunkZ = chunkZ;
+ this.repeatDelay = repeatDelay;
+ this.run = run;
+ }
+
+ private final int getStateVolatile() {
+ return (int)STATE_HANDLE.get(this);
+ }
+
+ private final int compareAndExchangeStateVolatile(final int expect, final int update) {
+ return (int)STATE_HANDLE.compareAndExchange(this, expect, update);
+ }
+
+ private final void setStateVolatile(final int value) {
+ STATE_HANDLE.setVolatile(this, value);
+ }
+
+ @Override
+ public void run() {
+ if (!this.plugin.isEnabled()) {
+ // don't execute if the plugin is disabled
+ return;
+ }
+
+ final boolean repeating = this.isRepeatingTask();
+ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) {
+ // cancelled
+ return;
+ }
+
+ try {
+ this.run.accept(this);
+ } catch (final Throwable throwable) {
+ this.plugin.getLogger().log(Level.WARNING, "Location task for " + this.plugin.getDescription().getFullName()
+ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable);
+ } finally {
+ boolean reschedule = false;
+ if (!repeating) {
+ this.setStateVolatile(STATE_FINISHED);
+ } else if (!this.plugin.isEnabled()) {
+ this.setStateVolatile(STATE_CANCELLED);
+ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) {
+ reschedule = true;
+ } // else: cancelled repeating task
+
+ if (!reschedule) {
+ this.run = null;
+ this.world = null;
+ } else {
+ FoliaRegionScheduler.scheduleInternalOnRegion(this, this.repeatDelay);
+ }
+ }
+ }
+
+ @Override
+ public Plugin getOwningPlugin() {
+ return this.plugin;
+ }
+
+ @Override
+ public boolean isRepeatingTask() {
+ return this.repeatDelay > 0;
+ }
+
+ @Override
+ public CancelledState cancel() {
+ for (int curr = this.getStateVolatile();;) {
+ switch (curr) {
+ case STATE_IDLE: {
+ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) {
+ this.state = STATE_CANCELLED;
+ this.run = null;
+ this.world = null;
+ return CancelledState.CANCELLED_BY_CALLER;
+ }
+ // try again
+ continue;
+ }
+ case STATE_EXECUTING: {
+ if (!this.isRepeatingTask()) {
+ return CancelledState.RUNNING;
+ }
+ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) {
+ return CancelledState.NEXT_RUNS_CANCELLED;
+ }
+ // try again
+ continue;
+ }
+ case STATE_EXECUTING_CANCELLED: {
+ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY;
+ }
+ case STATE_FINISHED: {
+ return CancelledState.ALREADY_EXECUTED;
+ }
+ case STATE_CANCELLED: {
+ return CancelledState.CANCELLED_ALREADY;
+ }
+ default: {
+ throw new IllegalStateException("Unknown state: " + curr);
+ }
+ }
+ }
+ }
+
+ @Override
+ public ExecutionState getExecutionState() {
+ final int state = this.getStateVolatile();
+ switch (state) {
+ case STATE_IDLE:
+ return ExecutionState.IDLE;
+ case STATE_EXECUTING:
+ return ExecutionState.RUNNING;
+ case STATE_EXECUTING_CANCELLED:
+ return ExecutionState.CANCELLED_RUNNING;
+ case STATE_FINISHED:
+ return ExecutionState.FINISHED;
+ case STATE_CANCELLED:
+ return ExecutionState.CANCELLED;
+ default: {
+ throw new IllegalStateException("Unknown state: " + state);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java b/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..14d20c996e9b25077f7e51c5d7d432c4a2b01671
--- /dev/null
+++ b/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java
@@ -0,0 +1,79 @@
+package io.papermc.paper.threadedregions.util;
+
+import net.minecraft.util.RandomSource;
+import net.minecraft.world.level.levelgen.BitRandomSource;
+import net.minecraft.world.level.levelgen.PositionalRandomFactory;
+import java.util.concurrent.ThreadLocalRandom;
+
+public final class SimpleThreadLocalRandomSource implements BitRandomSource {
+
+ public static final SimpleThreadLocalRandomSource INSTANCE = new SimpleThreadLocalRandomSource();
+
+ private final PositionalRandomFactory positionalRandomFactory = new SimpleThreadLocalRandomSource.SimpleThreadLocalRandomPositionalRandomFactory();
+
+ private SimpleThreadLocalRandomSource() {}
+
+ @Override
+ public int next(final int bits) {
+ return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits);
+ }
+
+ @Override
+ public int nextInt() {
+ return ThreadLocalRandom.current().nextInt();
+ }
+
+ @Override
+ public int nextInt(final int bound) {
+ if (bound <= 0) {
+ throw new IllegalArgumentException();
+ }
+
+ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
+ final long value = (long)this.nextInt() & 0xFFFFFFFFL;
+ return (int)((value * (long)bound) >>> Integer.SIZE);
+ }
+
+ @Override
+ public void setSeed(final long seed) {
+ // no-op
+ }
+
+ @Override
+ public double nextGaussian() {
+ return ThreadLocalRandom.current().nextGaussian();
+ }
+
+ @Override
+ public RandomSource fork() {
+ return this;
+ }
+
+ @Override
+ public PositionalRandomFactory forkPositional() {
+ return this.positionalRandomFactory;
+ }
+
+ private static final class SimpleThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory {
+
+ @Override
+ public RandomSource fromHashOf(final String seed) {
+ return SimpleThreadLocalRandomSource.INSTANCE;
+ }
+
+ @Override
+ public RandomSource fromSeed(final long seed) {
+ return SimpleThreadLocalRandomSource.INSTANCE;
+ }
+
+ @Override
+ public RandomSource at(final int x, final int y, final int z) {
+ return SimpleThreadLocalRandomSource.INSTANCE;
+ }
+
+ @Override
+ public void parityConfigString(final StringBuilder info) {
+ info.append("SimpleThreadLocalRandomPositionalRandomFactory{}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java b/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..c167179775ec09877808d91eb04b3cdb688c00a4
--- /dev/null
+++ b/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java
@@ -0,0 +1,73 @@
+package io.papermc.paper.threadedregions.util;
+
+import net.minecraft.util.RandomSource;
+import net.minecraft.world.level.levelgen.BitRandomSource;
+import net.minecraft.world.level.levelgen.PositionalRandomFactory;
+import java.util.concurrent.ThreadLocalRandom;
+
+public final class ThreadLocalRandomSource implements BitRandomSource {
+
+ public static final ThreadLocalRandomSource INSTANCE = new ThreadLocalRandomSource();
+
+ private final PositionalRandomFactory positionalRandomFactory = new ThreadLocalRandomPositionalRandomFactory();
+
+ private ThreadLocalRandomSource() {}
+
+ @Override
+ public int next(final int bits) {
+ return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits);
+ }
+
+ @Override
+ public int nextInt() {
+ return ThreadLocalRandom.current().nextInt();
+ }
+
+ @Override
+ public int nextInt(final int bound) {
+ return ThreadLocalRandom.current().nextInt(bound);
+ }
+
+ @Override
+ public void setSeed(final long seed) {
+ // no-op
+ }
+
+ @Override
+ public double nextGaussian() {
+ return ThreadLocalRandom.current().nextGaussian();
+ }
+
+ @Override
+ public RandomSource fork() {
+ return this;
+ }
+
+ @Override
+ public PositionalRandomFactory forkPositional() {
+ return this.positionalRandomFactory;
+ }
+
+ private static final class ThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory {
+
+ @Override
+ public RandomSource fromHashOf(final String seed) {
+ return ThreadLocalRandomSource.INSTANCE;
+ }
+
+ @Override
+ public RandomSource fromSeed(final long seed) {
+ return ThreadLocalRandomSource.INSTANCE;
+ }
+
+ @Override
+ public RandomSource at(final int x, final int y, final int z) {
+ return ThreadLocalRandomSource.INSTANCE;
+ }
+
+ @Override
+ public void parityConfigString(final StringBuilder info) {
+ info.append("ThreadLocalRandomPositionalRandomFactory{}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/net/minecraft/commands/CommandSourceStack.java b/net/minecraft/commands/CommandSourceStack.java
index c2b7164a1395842ab95428540782eeda4c7960b0..d5eefed0912c728ded360ddac4d9bcd1813730b2 100644
--- a/net/minecraft/commands/CommandSourceStack.java
+++ b/net/minecraft/commands/CommandSourceStack.java
@@ -91,7 +91,7 @@ public class CommandSourceStack implements ExecutionCommandSource<CommandSourceS
CommandResultCallback.EMPTY,
EntityAnchorArgument.Anchor.FEET,
CommandSigningContext.ANONYMOUS,
- TaskChainer.immediate(server)
+ TaskChainer.immediate((Runnable run) -> { 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
2025-02-17 15:51:04 -08:00
index f8969a68cd352ce4fe5109205e78f5e19ab6e020..009e6405a11a391adca41a7c4ecafbf3254d799d 100644
--- a/net/minecraft/commands/Commands.java
+++ b/net/minecraft/commands/Commands.java
2025-02-17 15:51:04 -08:00
@@ -158,13 +158,13 @@ public class Commands {
AdvancementCommands.register(this.dispatcher);
AttributeCommand.register(this.dispatcher, context);
ExecuteCommand.register(this.dispatcher, context);
- BossBarCommands.register(this.dispatcher, context);
+ //BossBarCommands.register(this.dispatcher, context); // Folia - region threading - TODO
ClearInventoryCommands.register(this.dispatcher, context);
- CloneCommands.register(this.dispatcher, context);
+ //CloneCommands.register(this.dispatcher, context); // Folia - region threading - TODO
DamageCommand.register(this.dispatcher, context);
- DataCommands.register(this.dispatcher);
- DataPackCommand.register(this.dispatcher);
- DebugCommand.register(this.dispatcher);
+ //DataCommands.register(this.dispatcher); // Folia - region threading - TODO
+ //DataPackCommand.register(this.dispatcher); // Folia - region threading - TODO
+ //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO
DefaultGameModeCommands.register(this.dispatcher);
DifficultyCommand.register(this.dispatcher);
EffectCommands.register(this.dispatcher, context);
2025-02-17 15:51:04 -08:00
@@ -174,47 +174,47 @@ public class Commands {
FillCommand.register(this.dispatcher, context);
FillBiomeCommand.register(this.dispatcher, context);
ForceLoadCommand.register(this.dispatcher);
- FunctionCommand.register(this.dispatcher);
+ //FunctionCommand.register(this.dispatcher); // Folia - region threading - TODO
GameModeCommand.register(this.dispatcher);
GameRuleCommand.register(this.dispatcher, context);
GiveCommand.register(this.dispatcher, context);
HelpCommand.register(this.dispatcher);
- ItemCommands.register(this.dispatcher, context);
+ //ItemCommands.register(this.dispatcher, context); // Folia - region threading - TODO later
KickCommand.register(this.dispatcher);
KillCommand.register(this.dispatcher);
ListPlayersCommand.register(this.dispatcher);
LocateCommand.register(this.dispatcher, context);
- LootCommand.register(this.dispatcher, context);
+ //LootCommand.register(this.dispatcher, context); // Folia - region threading - TODO later
MsgCommand.register(this.dispatcher);
ParticleCommand.register(this.dispatcher, context);
PlaceCommand.register(this.dispatcher);
PlaySoundCommand.register(this.dispatcher);
RandomCommand.register(this.dispatcher);
- ReloadCommand.register(this.dispatcher);
+ //ReloadCommand.register(this.dispatcher); // Folia - region threading
RecipeCommand.register(this.dispatcher);
- ReturnCommand.register(this.dispatcher);
- RideCommand.register(this.dispatcher);
- RotateCommand.register(this.dispatcher);
+ //ReturnCommand.register(this.dispatcher); // Folia - region threading - TODO later
+ //RideCommand.register(this.dispatcher); // Folia - region threading - TODO later
+ //RotateCommand.register(this.dispatcher); // Folia - region threading - TODO later
SayCommand.register(this.dispatcher);
- ScheduleCommand.register(this.dispatcher);
- ScoreboardCommand.register(this.dispatcher, context);
+ //ScheduleCommand.register(this.dispatcher); // Folia - region threading
+ //ScoreboardCommand.register(this.dispatcher, context); // Folia - region threading
SeedCommand.register(this.dispatcher, selection != Commands.CommandSelection.INTEGRATED);
SetBlockCommand.register(this.dispatcher, context);
SetSpawnCommand.register(this.dispatcher);
SetWorldSpawnCommand.register(this.dispatcher);
- SpectateCommand.register(this.dispatcher);
- SpreadPlayersCommand.register(this.dispatcher);
+ //SpectateCommand.register(this.dispatcher); // Folia - region threading - TODO later
+ //SpreadPlayersCommand.register(this.dispatcher); // Folia - region threading - TODO later
StopSoundCommand.register(this.dispatcher);
SummonCommand.register(this.dispatcher, context);
- TagCommand.register(this.dispatcher);
- TeamCommand.register(this.dispatcher, context);
- TeamMsgCommand.register(this.dispatcher);
+ //TagCommand.register(this.dispatcher); // Folia - region threading - TODO later
+ //TeamCommand.register(this.dispatcher, context); // Folia - region threading - TODO later
+ //TeamMsgCommand.register(this.dispatcher); // Folia - region threading - TODO later
TeleportCommand.register(this.dispatcher);
TellRawCommand.register(this.dispatcher, context);
- TickCommand.register(this.dispatcher);
+ //TickCommand.register(this.dispatcher); // Folia - region threading - TODO later
TimeCommand.register(this.dispatcher);
TitleCommand.register(this.dispatcher, context);
- TriggerCommand.register(this.dispatcher);
+ //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later
WeatherCommand.register(this.dispatcher);
WorldBorderCommand.register(this.dispatcher);
if (JvmProfiler.INSTANCE.isAvailable()) {
2025-02-17 15:51:04 -08:00
@@ -242,8 +242,8 @@ public class Commands {
OpCommand.register(this.dispatcher);
PardonCommand.register(this.dispatcher);
PardonIpCommand.register(this.dispatcher);
- PerfCommand.register(this.dispatcher);
- SaveAllCommand.register(this.dispatcher);
+ //PerfCommand.register(this.dispatcher); // Folia - region threading - TODO later
+ //SaveAllCommand.register(this.dispatcher); // Folia - region threading - TODO later
SaveOffCommand.register(this.dispatcher);
SaveOnCommand.register(this.dispatcher);
SetPlayerIdleTimeoutCommand.register(this.dispatcher);
2025-02-17 15:51:04 -08:00
@@ -495,9 +495,12 @@ public class Commands {
}
// Paper start - Perf: Async command map building
new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(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<String> bukkit, RootCommandNode<SharedSuggestionProvider> rootCommandNode) {
diff --git a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
index 4a881636ba21fae9e50950bbba2b4321b71d35ab..af35d667f7dc752df34c49fe675cd0a6cf8ffe4b 100644
--- a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
+++ b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java
@@ -46,7 +46,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
serverLevel.getCraftServer().getPluginManager().callEvent(event);
}
diff --git a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
index bd5bbc7e55c6bea77991fe5a3c0c2580313d16c5..907d3a5385b8b9098051f4ec0887d778fb85cf8d 100644
--- a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
+++ b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java
@@ -78,7 +78,7 @@ public class DefaultDispenseItemBehavior implements DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack);
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(itemEntity.getDeltaMovement()));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
level.getCraftServer().getPluginManager().callEvent(event);
}
diff --git a/net/minecraft/core/dispenser/DispenseItemBehavior.java b/net/minecraft/core/dispenser/DispenseItemBehavior.java
index 717c84165d5e25cd384f56b7cb976abf6669b6f0..ebcd1949266f29ca0c99ee26252c366c3f887546 100644
--- a/net/minecraft/core/dispenser/DispenseItemBehavior.java
+++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java
@@ -89,7 +89,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
serverLevel.getCraftServer().getPluginManager().callEvent(event);
}
@@ -147,7 +147,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
serverLevel.getCraftServer().getPluginManager().callEvent(event);
}
@@ -201,7 +201,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity());
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
world.getCraftServer().getPluginManager().callEvent(event);
}
@@ -251,7 +251,7 @@ public interface DispenseItemBehavior {
org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockSource.pos());
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleCopy);
org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), abstractChestedHorse.getBukkitLivingEntity());
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
world.getCraftServer().getPluginManager().callEvent(event);
}
@@ -329,7 +329,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
level.getCraftServer().getPluginManager().callEvent(event);
}
@@ -389,7 +389,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
levelAccessor.getMinecraftWorld().getCraftServer().getPluginManager().callEvent(event);
}
@@ -425,7 +425,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
serverLevel.getCraftServer().getPluginManager().callEvent(event);
}
@@ -482,7 +482,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
level.getCraftServer().getPluginManager().callEvent(event);
}
@@ -500,7 +500,8 @@ public interface DispenseItemBehavior {
}
}
- level.captureTreeGeneration = true;
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
+ worldData.captureTreeGeneration = true; // Folia - region threading
// CraftBukkit end
if (!BoneMealItem.growCrop(item, level, blockPos) && !BoneMealItem.growWaterPlant(item, level, blockPos, null)) {
this.setSuccess(false);
@@ -508,13 +509,13 @@ public interface DispenseItemBehavior {
level.levelEvent(1505, blockPos, 15);
}
// CraftBukkit start
- level.captureTreeGeneration = false;
- if (level.capturedBlockStates.size() > 0) {
- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType;
- net.minecraft.world.level.block.SaplingBlock.treeType = null;
+ worldData.captureTreeGeneration = false; // Folia - region threading
+ if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading
+ org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // Folia - region threading
+ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); // Folia - region threading
org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(blockPos, level.getWorld());
- List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(level.capturedBlockStates.values());
- level.capturedBlockStates.clear();
+ List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading
+ worldData.capturedBlockStates.clear(); // Folia - region threading
org.bukkit.event.world.StructureGrowEvent structureEvent = null;
if (treeType != null) {
structureEvent = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks);
@@ -548,7 +549,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack);
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockPos.getX() + 0.5D, (double) blockPos.getY(), (double) blockPos.getZ() + 0.5D));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
level.getCraftServer().getPluginManager().callEvent(event);
}
@@ -591,7 +592,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
level.getCraftServer().getPluginManager().callEvent(event);
}
@@ -644,7 +645,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
level.getCraftServer().getPluginManager().callEvent(event);
}
@@ -702,7 +703,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - only single item in event
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
serverLevel.getCraftServer().getPluginManager().callEvent(event);
}
@@ -783,7 +784,7 @@ public interface DispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity());
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
serverLevel.getCraftServer().getPluginManager().callEvent(event);
}
diff --git a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java
index 3595bbd05fb3e8fe57e38d4e2df5c6237046b726..9bcb803b761aef0bf29a76bd4bea22f22cbeda5d 100644
--- a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java
+++ b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java
@@ -39,7 +39,7 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack);
org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) livingEntity.getBukkitEntity());
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
world.getCraftServer().getPluginManager().callEvent(event);
}
diff --git a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
index 116395b6c00a0814922516707544a9ff26d68835..26c326080ee6fc80f0cc6af3e9fcbc1a508ba01a 100644
--- a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
+++ b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java
@@ -62,7 +62,7 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1);
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(vec31.x, vec31.y, vec31.z));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
serverLevel.getCraftServer().getPluginManager().callEvent(event);
}
diff --git a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
index 449d9b72ff4650961daa9d1bd25940f3914a6b12..b4f2dbe3dcdeac2a297b7909cedd54a8079938d8 100644
--- a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
+++ b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java
@@ -32,7 +32,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1);
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) direction.getStepX(), (double) direction.getStepY(), (double) direction.getStepZ()));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
serverLevel.getCraftServer().getPluginManager().callEvent(event);
}
diff --git a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
index 626e9feb6a6e7a2cbc7c63e30ba4fb6b923e85c7..eb63e114b666128df924dca46235ea8a7edbae54 100644
--- a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
+++ b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java
@@ -25,7 +25,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior {
org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos());
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
serverLevel.getCraftServer().getPluginManager().callEvent(event);
}
diff --git a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
index 5ab2c8333178335515e619b87ae420f948c83bd1..172f41f15e3f165b8faca85e7bc581082d330041 100644
--- a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
+++ b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java
@@ -27,7 +27,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior {
org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event
org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockPos.getX(), blockPos.getY(), blockPos.getZ()));
- if (!DispenserBlock.eventFired) {
+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading
blockSource.level().getCraftServer().getPluginManager().callEvent(event);
}
diff --git a/net/minecraft/gametest/framework/GameTestHelper.java b/net/minecraft/gametest/framework/GameTestHelper.java
index fe4ae6bcdcbb55c47e9f9a4d63ead4c39e6d63cf..36a5ca39214233f37ef7bfeb47331a7deb566e5c 100644
--- a/net/minecraft/gametest/framework/GameTestHelper.java
+++ b/net/minecraft/gametest/framework/GameTestHelper.java
@@ -306,7 +306,7 @@ public class GameTestHelper {
};
Connection connection = new Connection(PacketFlow.SERVERBOUND);
new EmbeddedChannel(connection);
- this.getLevel().getServer().getPlayerList().placeNewPlayer(connection, serverPlayer, commonListenerCookie);
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
return serverPlayer;
}
diff --git a/net/minecraft/gametest/framework/GameTestServer.java b/net/minecraft/gametest/framework/GameTestServer.java
index 54ca624a8194e7d1c0f3b1c0ddba81165523382c..a8cc20bfad1790f254c4793f09fc4dd3ddd4f25b 100644
--- a/net/minecraft/gametest/framework/GameTestServer.java
+++ b/net/minecraft/gametest/framework/GameTestServer.java
@@ -175,8 +175,12 @@ public class GameTestServer extends MinecraftServer {
}
@Override
- public void tickServer(BooleanSupplier hasTimeLeft) {
- super.tickServer(hasTimeLeft);
+ // Folia start - region threading
+ public void tickServer(long startTime, long scheduledEnd, long targetBuffer,
+ io.papermc.paper.threadedregions.TickRegions.TickRegionData region) {
+ if (true) throw new UnsupportedOperationException();
+ super.tickServer(startTime, scheduledEnd, targetBuffer, region);
+ // Folia end - region threading
ServerLevel serverLevel = this.overworld();
if (!this.haveTestsStarted()) {
this.startTests(serverLevel);
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
2025-03-08 06:39:17 -08:00
index 4ed9611994c5c8da01fede690197527c5b3a5731..6caa695417945dc5a534755d6590555e0e5c50d2 100644
--- a/net/minecraft/network/Connection.java
+++ b/net/minecraft/network/Connection.java
@@ -85,7 +85,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
private final PacketFlow receiving;
private volatile boolean sendLoginDisconnect = true;
- private final Queue<WrappedConsumer> pendingActions = Queues.newConcurrentLinkedQueue(); // Paper - Optimize network
+ private final Queue<WrappedConsumer> 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<Packet<?>> {
@Nullable
private DisconnectionDetails disconnectionDetails;
private boolean encrypted;
- private boolean disconnectionHandled;
+ private final java.util.concurrent.atomic.AtomicBoolean disconnectionHandled = new java.util.concurrent.atomic.AtomicBoolean(false); // Folia - region threading - may be called concurrently during configuration stage
private int receivedPackets;
private int sentPackets;
private float averageReceivedPackets;
@@ -154,6 +154,41 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
this.receiving = receiving;
}
+ // Folia start - region threading
+ private volatile boolean becomeActive;
+
+ public boolean becomeActive() {
+ return this.becomeActive;
+ }
+
+ private static record DisconnectReq(DisconnectionDetails disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {}
+
+ private final ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<DisconnectReq> disconnectReqs =
+ new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>();
+
+ /**
+ * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the
+ * same thread that could disconnect.
+ */
+ public final void disconnectSafely(DisconnectionDetails disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
+ this.disconnectReqs.add(new DisconnectReq(disconnectReason, cause));
+ // We can't halt packet processing here because a plugin could cancel a kick request.
+ }
+
+ /**
+ * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the
+ * same thread that could disconnect.
+ */
+ public final void disconnectSafely(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
+ this.disconnectReqs.add(new DisconnectReq(new DisconnectionDetails(disconnectReason), cause));
+ // We can't halt packet processing here because a plugin could cancel a kick request.
+ }
+
+ public final boolean isPlayerConnected() {
+ return this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl;
+ }
+ // Folia end - region threading
+
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
super.channelActive(context);
@@ -163,6 +198,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
if (this.delayedDisconnect != null) {
this.disconnect(this.delayedDisconnect);
}
+ this.becomeActive = true; // Folia - region threading
}
@Override
@@ -434,7 +470,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
packet.onPacketDispatch(this.getPlayer());
- if (connected && (InnerUtil.canSendImmediate(this, packet)
+ if (false && connected && (InnerUtil.canSendImmediate(this, packet) // Folia - region threading - connection fixes
|| (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty()
&& (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) {
this.sendPacket(packet, listener, flush);
@@ -463,11 +499,12 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
public void runOnceConnected(Consumer<Connection> action) {
- if (this.isConnected()) {
+ if (false && this.isConnected()) { // Folia - region threading - connection fixes
this.flushQueue();
action.accept(this);
} else {
this.pendingActions.add(new WrappedConsumer(action)); // Paper - Optimize network
+ this.flushQueue(); // Folia - region threading - connection fixes
}
}
@@ -518,10 +555,11 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
public void flushChannel() {
- if (this.isConnected()) {
+ if (false && this.isConnected()) { // Folia - region threading - connection fixes
this.flush();
} else {
this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network
+ this.flushQueue(); // Folia - region threading - connection fixes
}
}
@@ -535,53 +573,61 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
// 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<WrappedConsumer> 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<WrappedConsumer> queue =
+ (ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<WrappedConsumer>)this.pendingActions;
+ WrappedConsumer holder;
+ for (;;) {
+ // synchronise so that queue clears appear atomic
+ synchronized (queue) {
+ holder = queue.pollIf(Connection::canWrite);
+ }
+ if (holder == null) {
+ break;
+ }
- if (queued instanceof PacketSendAction packetSendAction) {
- final Packet<?> packet = packetSendAction.packet;
- if (!packet.isReady()) {
- return false;
+ holder.accept(this);
}
- }
- iterator.remove();
- if (queued.tryMarkConsumed()) {
- queued.accept(this);
+ } finally {
+ if (!set) {
+ this.flushingQueue.set(false);
+ }
}
}
+
return true;
+ // Folia end - region threading - connection fixes
}
// Paper end - Optimize network
@@ -590,17 +636,37 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
private static int currTick; // Paper - Buffer joins to world
public void tick() {
this.flushQueue();
- // Paper start - Buffer joins to world
- if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) {
- Connection.currTick = net.minecraft.server.MinecraftServer.currentTick;
- Connection.joinAttemptsThisTick = 0;
+ // Folia - this is broken
+ // Folia start - region threading
+ // handle disconnect requests, but only after flushQueue()
+ DisconnectReq disconnectReq;
+ while ((disconnectReq = this.disconnectReqs.poll()) != null) {
+ PacketListener packetlistener = this.packetListener;
+
+ if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) {
+ loginPacketListener.disconnect(disconnectReq.disconnectReason.reason());
+ // this doesn't fail, so abort any further attempts
+ return;
+ } else if (packetlistener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
+ commonPacketListener.disconnect(disconnectReq.disconnectReason, disconnectReq.cause);
+ // may be cancelled by a plugin, if not cancelled then any further calls do nothing
+ continue;
+ } else {
+ // no idea what packet to send
+ this.disconnect(disconnectReq.disconnectReason);
+ this.setReadOnly();
+ return;
+ }
}
- // Paper end - Buffer joins to world
+ if (!this.isConnected()) {
+ // disconnected from above
+ this.handleDisconnection();
+ return;
+ }
+ // Folia end - region threading
if (this.packetListener instanceof TickablePacketListener tickablePacketListener) {
// Paper start - Buffer joins to world
- if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener)
- || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING
- || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) {
+ if (true) { // Folia - region threading
// Paper start - detailed watchdog information
net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener);
try {
@@ -611,7 +677,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
} // Paper end - Buffer joins to world
}
- if (!this.isConnected() && !this.disconnectionHandled) {
+ if (!this.isConnected()) {// Folia - region threading - it's fine to call if it is already handled, as it no longer logs
this.handleDisconnection();
}
@@ -662,6 +728,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
this.channel.close(); // We can't wait as this may be called from an event loop.
this.disconnectionDetails = disconnectionDetails;
}
+ this.becomeActive = true; // Folia - region threading
}
public boolean isMemoryConnection() {
@@ -853,10 +920,10 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
public void handleDisconnection() {
if (this.channel != null && !this.channel.isOpen()) {
- if (this.disconnectionHandled) {
+ if (!this.disconnectionHandled.compareAndSet(false, true)) { // Folia - region threading - may be called concurrently during configuration stage
// LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message
} else {
- this.disconnectionHandled = true;
+ //this.disconnectionHandled = true; // Folia - region threading - may be called concurrently during configuration stage - set above
PacketListener packetListener = this.getPacketListener();
PacketListener packetListener1 = packetListener != null ? packetListener : this.disconnectListener;
if (packetListener1 != null) {
@@ -885,6 +952,21 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
}
}
// Paper end - Add PlayerConnectionCloseEvent
+ // Folia start - region threading
+ if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) {
+ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection(
+ commonPacketListener.getOwner().getName(),
+ commonPacketListener.getOwner().getId(), this
+ );
+ } else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) {
+ if (loginPacketListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING.ordinal()) {
+ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection(
+ loginPacketListener.authenticatedProfile.getName(),
+ loginPacketListener.authenticatedProfile.getId(), this
+ );
+ }
+ }
+ // Folia end - region threading
}
}
}
@@ -904,15 +986,25 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
// Paper start - Optimize network
public void clearPacketQueue() {
final net.minecraft.server.level.ServerPlayer player = getPlayer();
- for (final Consumer<Connection> 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<Connection.PacketSendAction> queuedPackets = new java.util.ArrayList<>();
+ // synchronise so that flushQueue does not poll values while the queue is being cleared
+ synchronized (this.pendingActions) {
+ Connection.WrappedConsumer consumer;
+ while ((consumer = this.pendingActions.poll()) != null) {
+ if (consumer instanceof Connection.PacketSendAction packetHolder) {
+ queuedPackets.add(packetHolder);
}
}
}
- this.pendingActions.clear();
+
+ for (Connection.PacketSendAction queuedPacket : queuedPackets) {
+ Packet<?> packet = queuedPacket.packet;
+ if (packet.hasFinishListener()) {
+ packet.onPacketDispatchFinish(player, null);
+ }
+ }
+ // Folia end - region threading - connection fixes
}
private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up.
diff --git a/net/minecraft/network/protocol/PacketUtils.java b/net/minecraft/network/protocol/PacketUtils.java
index 4535858701b2bb232b9d2feb2af6551526232ddc..b28ff2f18ab7e0e3a61e37ee46048ab5cb7ab45d 100644
--- a/net/minecraft/network/protocol/PacketUtils.java
+++ b/net/minecraft/network/protocol/PacketUtils.java
@@ -20,7 +20,7 @@ public class PacketUtils {
public static <T extends PacketListener> void ensureRunningOnSameThread(Packet<T> packet, T processor, BlockableEventLoop<?> executor) throws RunningOnDifferentThreadException {
if (!executor.isSameThread()) {
- executor.executeIfPossible(() -> {
+ Runnable run = () -> { // Folia - region threading
packetProcessing.push(processor); // Paper - detailed watchdog information
try { // Paper - detailed watchdog information
if (processor instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // Paper - Don't handle sync packets for kicked players
@@ -43,7 +43,24 @@ public class PacketUtils {
packetProcessing.pop();
}
// Paper end - detailed watchdog information
- });
+ // Folia start - region threading
+ };
+ // ignore retired state, if removed then we don't want the packet to be handled
+ if (processor instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) {
+ gamePacketListener.player.getBukkitEntity().taskScheduler.schedule(
+ (net.minecraft.server.level.ServerPlayer player) -> {
+ run.run();
+ },
+ null, 1L
+ );
+ } else if (processor instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);
+ } else if (processor instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);
+ } else {
+ throw new UnsupportedOperationException("Unknown listener: " + processor);
+ }
+ // Folia end - region threading
throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD;
}
}
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index ae220a732c78ab076261f20b5a54c71d7fceb407..9c9de462eb7187d6cc3562c796e3bcf69fb20783 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -184,7 +184,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private static final int OVERLOADED_TICKS_THRESHOLD = 20;
private static final long OVERLOADED_WARNING_INTERVAL_NANOS = 10L * TimeUtil.NANOSECONDS_PER_SECOND;
private static final int OVERLOADED_TICKS_WARNING_INTERVAL = 100;
- private static final long STATUS_EXPIRE_TIME_NANOS = 5L * TimeUtil.NANOSECONDS_PER_SECOND;
+ public static final long STATUS_EXPIRE_TIME_NANOS = 5L * TimeUtil.NANOSECONDS_PER_SECOND; // Folia - region threading - public
private static final long PREPARE_LEVELS_DEFAULT_DELAY_NANOS = 10L * TimeUtil.NANOSECONDS_PER_MILLISECOND;
private static final int MAX_STATUS_PLAYER_SAMPLE = 12;
private static final int SPAWN_POSITION_SEARCH_RADIUS = 5;
@@ -222,8 +222,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private volatile boolean running = true;
private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart
private boolean stopped;
- private int tickCount;
- private int ticksUntilAutosave = 6000;
+ // Folia - region threading
protected final Proxy proxy;
private boolean onlineMode;
private boolean preventProxyConnections;
@@ -283,7 +282,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
public org.bukkit.craftbukkit.CraftServer server;
public joptsimple.OptionSet options;
public org.bukkit.command.ConsoleCommandSender console;
- public static int currentTick; // Paper - improve tick loop
+ //public static int currentTick; // Paper - improve tick loop // Folia - region threading
public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>();
public int autosavePeriod;
// Paper - don't store the vanilla dispatcher
@@ -304,6 +303,50 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private final Set<String> 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 <V> CompletableFuture<V> submit(java.util.function.Supplier<V> task) {
+ if (true) {
+ throw new UnsupportedOperationException();
+ }
+ return super.submit(task);
+ }
+
+ @Override
+ public CompletableFuture<Void> 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 extends MinecraftServer> S spin(Function<Thread, S> threadFunction) {
ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system
AtomicReference<S> atomicReference = new AtomicReference<>();
@@ -332,46 +375,30 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private static final long MAX_CHUNK_EXEC_TIME = 1000L; // 1us
private static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us
- private long lastMidTickExecute;
- private long lastMidTickExecuteFailure;
-
- private boolean tickMidTickTasks() {
- // give all worlds a fair chance at by targeting them all.
- // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time.
- boolean executed = false;
- for (final ServerLevel world : this.getAllLevels()) {
- long currTime = System.nanoTime();
- if (currTime - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getLastMidTickFailure() <= TASK_EXECUTION_FAILURE_BACKOFF) {
- continue;
- }
- if (!world.getChunkSource().pollTask()) {
- // we need to back off if this fails
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$setLastMidTickFailure(currTime);
- } else {
- executed = true;
- }
- }
+ // Folia - region threading - moved to regionized data
- return executed;
+ private boolean tickMidTickTasks(final io.papermc.paper.threadedregions.RegionizedWorldData worldData) { // Folia - region threading
+ return worldData.world.getChunkSource().pollTask(); // Folia - region threading
}
@Override
public final void moonrise$executeMidTickTasks() {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading
final long startTime = System.nanoTime();
- if ((startTime - this.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - this.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) {
+ if ((startTime - worldData.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - worldData.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) { // Folia - region threading
// it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed.
// so, backoff to prevent this
return;
}
for (;;) {
- final boolean moreTasks = this.tickMidTickTasks();
+ final boolean moreTasks = this.tickMidTickTasks(worldData); // Folia - region threading
final long currTime = System.nanoTime();
final long diff = currTime - startTime;
if (!moreTasks || diff >= 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<TickTa
final double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME;
final long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME);
- this.lastMidTickExecute = currTime + extraSleep;
+ worldData.lastMidTickExecute = currTime + extraSleep; // Folia - region threading
return;
}
}
@@ -710,7 +737,21 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Back to the createLevels method without crazy modifications
primaryLevelData.setModdedInfo(this.getServerModName(), this.getModdedStatus().shouldReportAsModified());
this.addLevel(serverLevel); // Paper - Put world into worldlist before initing the world; move up
- this.initWorld(serverLevel, primaryLevelData, this.worldData, worldOptions);
+ // Folia start - region threading
+ // the spawn should be within ~1024 blocks, so we force add ticket levels to ensure the first thread
+ // to init spawn will not run into any ownership issues
+ // move init to start of tickServer
+ int loadRegionRadius = 1024 >> 4;
+ serverLevel.randomSpawnSelection = new ChunkPos(serverLevel.getChunkSource().randomState().sampler().findSpawnPosition());
+ for (int currX = -loadRegionRadius; currX <= loadRegionRadius; ++currX) {
+ for (int currZ = -loadRegionRadius; currZ <= loadRegionRadius; ++currZ) {
+ ChunkPos pos = new ChunkPos(currX, currZ);
+ serverLevel.chunkSource.addTicketAtLevel(
+ net.minecraft.server.level.TicketType.UNKNOWN, pos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, pos
+ );
+ }
+ }
+ // Folia end - region threading
// Paper - Put world into worldlist before initing the world; move up
this.getPlayerList().addWorldborderListener(serverLevel);
@@ -723,6 +764,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
for (ServerLevel serverLevel : this.getAllLevels()) {
this.prepareLevels(serverLevel.getChunkSource().chunkMap.progressListener, serverLevel);
// Paper - rewrite chunk system
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addWorld(serverLevel); // Folia - region threading
this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(serverLevel.getWorld()));
}
@@ -804,10 +846,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
// CraftBukkit end
- ChunkPos chunkPos = new ChunkPos(chunkSource.randomState().sampler().findSpawnPosition()); // Paper - Only attempt to find spawn position if there isn't a fixed spawn position set
+ ChunkPos chunkPos = level.randomSpawnSelection; // Paper - Only attempt to find spawn position if there isn't a fixed spawn position set // Folia - region threading
int spawnHeight = chunkSource.getGenerator().getSpawnHeight(level);
if (spawnHeight < level.getMinY()) {
BlockPos worldPosition = chunkPos.getWorldPosition();
+ level.getChunk(worldPosition.offset(8, 0, 8)); // Folia - region threading - sync load first
spawnHeight = level.getHeight(Heightmap.Types.WORLD_SURFACE, worldPosition.getX() + 8, worldPosition.getZ() + 8);
}
@@ -869,14 +912,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
int _int = serverLevel.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS); // CraftBukkit - per-world
int i = _int > 0 ? Mth.square(ChunkProgressListener.calculateDiameter(_int)) : 0;
- while (chunkSource.getTickingGenerated() < i) {
- // CraftBukkit start
- // this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
- this.executeModerately();
- }
+ // Folia - region threading
// this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
- this.executeModerately();
+ //this.executeModerately(); // Folia - region threading
if (true) {
ServerLevel serverLevel1 = serverLevel;
@@ -895,7 +934,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// CraftBukkit start
// this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS;
- this.executeModerately();
+ //this.executeModerately(); // Folia - region threading
// CraftBukkit end
listener.stop();
// CraftBukkit start
@@ -985,7 +1024,37 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
// CraftBukkit end
+ // Folia start - region threading
+ private final java.util.concurrent.atomic.AtomicBoolean hasStartedShutdownThread = new java.util.concurrent.atomic.AtomicBoolean();
+
+ private void haltServerRegionThreading() {
+ if (this.hasStartedShutdownThread.getAndSet(true)) {
+ // already started shutdown
+ return;
+ }
+ new io.papermc.paper.threadedregions.RegionShutdownThread("Region shutdown thread").start();
+ }
+
+ public void haltCurrentRegion() {
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isShutdownThread()) {
+ throw new IllegalStateException();
+ }
+ }
+ // Folia end - region threading
+
public void stopServer() {
+ // Folia start - region threading
+ // halt scheduler
+ // don't wait, we may be on a scheduler thread
+ io.papermc.paper.threadedregions.TickRegions.getScheduler().halt(false, 0L);
+ // cannot run shutdown logic on this thread, as it may be a scheduler
+ if (true) {
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isShutdownThread()) {
+ this.haltServerRegionThreading();
+ return;
+ } // else: fall through to regular stop logic
+ }
+ // Folia end - region threading
// CraftBukkit start - prevent double stopping on multiple threads
synchronized(this.stopLock) {
if (this.hasStopped) return;
@@ -1012,12 +1081,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.getConnection().stop();
this.isSaving = true;
if (this.playerList != null) {
- LOGGER.info("Saving players");
- this.playerList.saveAll();
+ //LOGGER.info("Saving players"); // Folia - move to shutdown thread logic
+ //this.playerList.saveAll(); // Folia - move to shutdown thread logic
this.playerList.removeAll(this.isRestarting); // Paper
try { Thread.sleep(100); } catch (InterruptedException ex) {} // CraftBukkit - SPIGOT-625 - give server at least a chance to send packets
}
+ // Folia start - region threading
+ if (true) {
+ // the rest till part 2 is handled by the region shutdown thread
+ return;
+ }
+ // Folia end - region threading
+
LOGGER.info("Saving worlds");
for (ServerLevel serverLevel : this.getAllLevels()) {
@@ -1040,6 +1116,11 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.saveAllChunks(false, true, false, true); // Paper - rewrite chunk system
this.isSaving = false;
+ // Folia start - region threading
+ this.stopPart2();
+ }
+ public void stopPart2() {
+ // Folia end - region threading
this.resources.close();
try {
@@ -1098,6 +1179,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
if (isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper - Debugging
// Paper end
this.running = false;
+ this.stopServer(); // Folia - region threading
if (waitForServer) {
try {
this.serverThread.join();
@@ -1169,6 +1251,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.status = this.buildServerStatus();
this.server.spark.enableBeforePlugins(); // Paper - spark
+ // Folia start - region threading
+ if (true) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().init(); // Folia - region threading - only after loading worlds
+ final long actualDoneTimeMs = System.currentTimeMillis() - org.bukkit.craftbukkit.Main.BOOT_TIME.toEpochMilli(); // Paper - Improve startup message
+ LOGGER.info("Done ({})! For help, type \"help\"", String.format(java.util.Locale.ROOT, "%.3fs", actualDoneTimeMs / 1000.00D)); // Paper - Improve startup message
+ for (;;) {
+ try {
+ Thread.sleep(Long.MAX_VALUE);
+ } catch (final InterruptedException ex) {}
+ }
+ }
+ // Folia end - region threading
// Spigot start
// Paper start
LOGGER.info("Running delayed init tasks");
@@ -1218,7 +1312,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Spigot start
// Paper start - further improve server tick loop
currentTime = Util.getNanos();
- if (++MinecraftServer.currentTick % MinecraftServer.SAMPLE_INTERVAL == 0) {
+ if (false) { // Folia - region threading
final long diff = currentTime - tickSection;
final java.math.BigDecimal currentTps = TPS_BASE.divide(new java.math.BigDecimal(diff), 30, java.math.RoundingMode.HALF_UP);
tps1.add(currentTps, diff);
@@ -1237,7 +1331,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
boolean flag = l == 0L;
if (this.debugCommandProfilerDelayStart) {
this.debugCommandProfilerDelayStart = false;
- this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount);
+ //this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount); // Folia - region threading
}
//MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time
@@ -1248,7 +1342,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
ProfilerFiller profilerFiller = Profiler.get();
profilerFiller.push("tick");
this.tickFrame.start();
- this.tickServer(flag ? () -> false : this::haveTime);
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
// Paper start - rewrite chunk system
final Throwable crash = this.chunkSystemCrash;
if (crash != null) {
@@ -1403,28 +1497,24 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@Override
public TickTask wrapRunnable(Runnable runnable) {
- // Paper start - anything that does try to post to main during watchdog crash, run on watchdog
- if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) {
- runnable.run();
- runnable = () -> {};
- }
- // Paper end
- return new TickTask(this.tickCount, runnable);
+ throw new UnsupportedOperationException(); // Folia - region threading
}
@Override
protected boolean shouldRun(TickTask runnable) {
- return runnable.getTick() + 3 < this.tickCount || this.haveTime();
+ throw new UnsupportedOperationException(); // Folia - region threading
}
@Override
public boolean pollTask() {
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
boolean flag = this.pollTaskInternal();
this.mayHaveDelayedTasks = flag;
return flag;
}
private boolean pollTaskInternal() {
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
if (super.pollTask()) {
this.moonrise$executeMidTickTasks(); // Paper - rewrite chunk system
return true;
@@ -1444,6 +1534,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@Override
public void doRunTask(TickTask task) {
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
Profiler.get().incrementCounter("runTask");
super.doRunTask(task);
}
@@ -1485,12 +1576,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return false;
}
- public void tickServer(BooleanSupplier hasTimeLeft) {
+ // Folia start - region threading
+ public void tickServer(long startTime, long scheduledEnd, long targetBuffer,
+ io.papermc.paper.threadedregions.TickRegions.TickRegionData region) {
+ // Folia end - region threading
org.spigotmc.WatchdogThread.tick(); // Spigot
- long nanos = Util.getNanos();
+ long nanos = startTime; // Folia - region threading
int i = this.pauseWhileEmptySeconds() * 20;
this.removeDisabledPluginsBlockingSleep(); // Paper - API to allow/disallow tick sleeping
- if (i > 0) {
+ if (false && i > 0) { // Folia - region threading - this is complicated to implement, and even if done correctly is messy
if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting() && this.pluginsBlockingSleep.isEmpty()) { // Paper - API to allow/disallow tick sleeping
this.emptyTicks++;
} else {
@@ -1515,24 +1609,58 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
level.getChunkSource().tick(() -> true, false);
}
// Paper end - avoid issues with certain tasks not processing during sleep
- this.server.spark.executeMainThreadTasks(); // Paper - spark
+ //this.server.spark.executeMainThreadTasks(); // Paper - spark // Folia - region threading
this.tickConnection();
this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark
return;
}
}
+ // Folia start - region threading
+ region.world.getCurrentWorldData().updateTickData();
+ if (region.world.checkInitialised.get() != ServerLevel.WORLD_INIT_CHECKED) {
+ synchronized (region.world.checkInitialised) {
+ if (region.world.checkInitialised.compareAndSet(ServerLevel.WORLD_INIT_NOT_CHECKED, ServerLevel.WORLD_INIT_CHECKING)) {
+ LOGGER.info("Initialising world '" + region.world.getWorld().getName() + "' before it can be ticked...");
+ this.initWorld(region.world, region.world.serverLevelData, worldData, region.world.serverLevelData.worldGenOptions()); // Folia - delayed until first tick of world
+ region.world.checkInitialised.set(ServerLevel.WORLD_INIT_CHECKED);
+ LOGGER.info("Initialised world '" + region.world.getWorld().getName() + "'");
+ } // else: must be checked
+ }
+ }
+ BooleanSupplier hasTimeLeft = () -> {
+ return scheduledEnd - System.nanoTime() > targetBuffer;
+ };
+ // Folia end - region threading
+
this.server.spark.tickStart(); // Paper - spark
- new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events
- this.tickCount++;
- this.tickRateManager.tick();
- this.tickChildren(hasTimeLeft);
- if (nanos - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) {
+ new com.destroystokyo.paper.event.server.ServerTickStartEvent((int)region.getCurrentTick()).callEvent(); // Paper - Server Tick Events // Folia - region threading
+ // Folia start - region threading
+ if (region != null) {
+ region.getTaskQueueData().drainTasks();
+ ((io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler)org.bukkit.Bukkit.getRegionScheduler()).tick();
+ // now run all the entity schedulers
+ // TODO there has got to be a more efficient variant of this crap
+ for (net.minecraft.world.entity.Entity entity : region.world.getCurrentWorldData().getLocalEntitiesCopy()) {
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) || entity.isRemoved()) {
+ continue;
+ }
+ org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw();
+ if (bukkit != null) {
+ bukkit.taskScheduler.executeTick();
+ }
+ }
+ }
+ // Folia end - region threading
+ //this.tickCount++; // Folia - region threading
+ //this.tickRateManager.tick(); // Folia - region threading
+ this.tickChildren(hasTimeLeft, region); // Folia - region threading
+ if (false && nanos - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) { // Folia - region threading
this.lastServerStatus = nanos;
this.status = this.buildServerStatus();
}
- this.ticksUntilAutosave--;
+ //this.ticksUntilAutosave--; // Folia - region threading
// Paper start - Incremental chunk and player saving
final ProfilerFiller profiler = Profiler.get();
int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate;
@@ -1540,15 +1668,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
playerSaveInterval = autosavePeriod;
}
profiler.push("save");
- final boolean fullSave = autosavePeriod > 0 && this.tickCount % autosavePeriod == 0;
+ final boolean fullSave = autosavePeriod > 0 && io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() % autosavePeriod == 0; // Folia - region threading
try {
this.isSaving = true;
if (playerSaveInterval > 0) {
this.playerList.saveAll(playerSaveInterval);
}
- for (final ServerLevel level : this.getAllLevels()) {
+ for (final ServerLevel level : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Folia - region threading
if (level.paperConfig().chunks.autoSaveInterval.value() > 0) {
- level.saveIncrementally(fullSave);
+ level.saveIncrementally(region == null && fullSave); // Folia - region threading - don't save level.dat
}
}
} finally {
@@ -1558,32 +1686,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Paper end - Incremental chunk and player saving
ProfilerFiller profilerFiller = Profiler.get();
- this.runAllTasks(); // Paper - move runAllTasks() into full server tick (previously for timings)
- this.server.spark.executeMainThreadTasks(); // Paper - spark
+ //this.runAllTasks(); // Paper - move runAllTasks() into full server tick (previously for timings) // Folia - region threading
+ //this.server.spark.executeMainThreadTasks(); // Paper - spark // Folia - region threading
// Paper start - Server Tick Events
long endTime = System.nanoTime();
- long remaining = (TICK_TIME - (endTime - lastTick)) - catchupTime;
- new com.destroystokyo.paper.event.server.ServerTickEndEvent(this.tickCount, ((double)(endTime - lastTick) / 1000000D), remaining).callEvent();
+ long remaining = scheduledEnd - endTime; // Folia - region ticking
+ new com.destroystokyo.paper.event.server.ServerTickEndEvent((int)io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(), ((double)(endTime - startTime) / 1000000D), remaining).callEvent(); // Folia - region ticking
// Paper end - Server Tick Events
- this.server.spark.tickEnd(((double)(endTime - lastTick) / 1000000D)); // Paper - spark
- profilerFiller.push("tallying");
- long l = Util.getNanos() - nanos;
- int i1 = this.tickCount % 100;
- this.aggregatedTickTimesNanos = this.aggregatedTickTimesNanos - this.tickTimesNanos[i1];
- this.aggregatedTickTimesNanos += l;
- this.tickTimesNanos[i1] = l;
- this.smoothedTickTimeMillis = this.smoothedTickTimeMillis * 0.8F + (float)l / (float)TimeUtil.NANOSECONDS_PER_MILLISECOND * 0.19999999F;
- // Paper start - Add tick times API and /mspt command
- this.tickTimes5s.add(this.tickCount, l);
- this.tickTimes10s.add(this.tickCount, l);
- this.tickTimes60s.add(this.tickCount, l);
- // Paper end - Add tick times API and /mspt command
- this.logTickMethodTime(nanos);
- profilerFiller.pop();
+ this.server.spark.tickEnd(((double)(endTime - startTime) / 1000000D)); // Paper - spark // Folia - region threading
+ // Folia - region threading
}
private void autoSave() {
- this.ticksUntilAutosave = this.autosavePeriod; // CraftBukkit
+ //this.ticksUntilAutosave = this.autosavePeriod; // CraftBukkit // Folia - region threading
LOGGER.debug("Autosave started");
ProfilerFiller profilerFiller = Profiler.get();
profilerFiller.push("save");
@@ -1598,30 +1713,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
- private int computeNextAutosaveInterval() {
- float f;
- if (this.tickRateManager.isSprinting()) {
- long l = this.getAverageTickTimeNanos() + 1L;
- f = (float)TimeUtil.NANOSECONDS_PER_SECOND / (float)l;
- } else {
- f = this.tickRateManager.tickrate();
- }
-
- int i = 300;
- return Math.max(100, (int)(f * 300.0F));
- }
+ // Folia - region threading - use absolute time instead of this
public void onTickRateChanged() {
- int i = this.computeNextAutosaveInterval();
- if (i < this.ticksUntilAutosave) {
- this.ticksUntilAutosave = i;
- }
+ // Folia - region threading - use absolute time instead of this
}
protected abstract SampleLogger getTickTimeLogger();
public abstract boolean isTickTimeLoggingEnabled();
+ // Folia start - region threading
+ public void rebuildServerStatus() {
+ this.status = this.buildServerStatus();
+ }
+ // Folia end - region threading
+
private ServerStatus buildServerStatus() {
ServerStatus.Players players = this.buildPlayerStatus();
return new ServerStatus(
@@ -1634,7 +1741,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
private ServerStatus.Players buildPlayerStatus() {
- List<ServerPlayer> players = this.playerList.getPlayers();
+ List<ServerPlayer> players = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading
int maxPlayers = this.getMaxPlayers();
if (this.hidesOnlinePlayers()) {
return new ServerStatus.Players(maxPlayers, players.size(), List.of());
@@ -1653,44 +1760,34 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
- protected void tickChildren(BooleanSupplier hasTimeLeft) {
+ protected void tickChildren(BooleanSupplier hasTimeLeft, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - region threading
+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - regionised ticking
ProfilerFiller profilerFiller = Profiler.get();
- this.getPlayerList().getPlayers().forEach(serverPlayer1 -> serverPlayer1.connection.suspendFlushing());
- this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit
+ //this.getPlayerList().getPlayers().forEach(serverPlayer1 -> serverPlayer1.connection.suspendFlushing()); // Folia - region threading
+ //this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit // Folia - region threading
// Paper start - Folia scheduler API
- ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) org.bukkit.Bukkit.getGlobalRegionScheduler()).tick();
- getAllLevels().forEach(level -> {
- for (final net.minecraft.world.entity.Entity entity : level.getEntities().getAll()) {
- if (entity.isRemoved()) {
- continue;
- }
- final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw();
- if (bukkit != null) {
- bukkit.taskScheduler.executeTick();
- }
- }
- });
+ // Folia - region threading - moved to global tick - and moved entity scheduler to tickRegion
// Paper end - Folia scheduler API
- io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper
+ //io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper // Folia - region threading - moved to global tick
profilerFiller.push("commandFunctions");
- this.getFunctions().tick();
+ //this.getFunctions().tick(); // Folia - region threading - TODO Purge functions
profilerFiller.popPush("levels");
// CraftBukkit start
// Run tasks that are waiting on processing
- while (!this.processQueue.isEmpty()) {
+ if (false) while (!this.processQueue.isEmpty()) { // Folia - region threading
this.processQueue.remove().run();
}
// Send time updates to everyone, it will get the right time from the world the player is in.
// Paper start - Perf: Optimize time updates
- for (final ServerLevel level : this.getAllLevels()) {
+ for (final ServerLevel level : Arrays.asList(region.world)) { // Folia - region threading
final boolean doDaylight = level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT);
final long dayTime = level.getDayTime();
long worldTime = level.getGameTime();
final ClientboundSetTimePacket worldPacket = new ClientboundSetTimePacket(worldTime, dayTime, doDaylight);
- for (Player entityhuman : level.players()) {
- if (!(entityhuman instanceof ServerPlayer) || (tickCount + entityhuman.getId()) % 20 != 0) {
+ for (Player entityhuman : level.getLocalPlayers()) { // Folia - region threading
+ if (!(entityhuman instanceof ServerPlayer) || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + entityhuman.getId()) % 20 != 0) { // Folia - region threading
continue;
}
ServerPlayer entityplayer = (ServerPlayer) entityhuman;
@@ -1703,12 +1800,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
}
- this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked
- for (ServerLevel serverLevel : this.getAllLevels()) {
- serverLevel.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
- serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
- serverLevel.updateLagCompensationTick(); // Paper - lag compensation
- net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = serverLevel.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers
+ //this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked // Folia - region threading
+ for (ServerLevel serverLevel : Arrays.asList(region.world)) { // Folia - region threading
+ // Folia - region threading
profilerFiller.push(() -> serverLevel + " " + serverLevel.dimension().location());
/* Drop global time updates
if (this.tickCount % 20 == 0) {
@@ -1721,7 +1815,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
profilerFiller.push("tick");
try {
- serverLevel.tick(hasTimeLeft);
+ serverLevel.tick(hasTimeLeft, region); // Folia - region threading
} catch (Throwable var7) {
CrashReport crashReport = CrashReport.forThrowable(var7, "Exception ticking world");
serverLevel.fillReportDetails(crashReport);
@@ -1730,27 +1824,27 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
profilerFiller.pop();
profilerFiller.pop();
- serverLevel.explosionDensityCache.clear(); // Paper - Optimize explosions
+ regionizedWorldData.explosionDensityCache.clear(); // Paper - Optimize explosions // Folia - region threading
}
- this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
+ //this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked // Folia - region threading
profilerFiller.popPush("connection");
- this.tickConnection();
+ regionizedWorldData.tickConnections(); // Folia - region threading
profilerFiller.popPush("players");
- this.playerList.tick();
+ //this.playerList.tick(); // Folia - region threading
if (SharedConstants.IS_RUNNING_IN_IDE && this.tickRateManager.runsNormally()) {
GameTestTicker.SINGLETON.tick();
}
profilerFiller.popPush("server gui refresh");
- for (int i = 0; i < this.tickables.size(); i++) {
+ if (false) for (int i = 0; i < this.tickables.size(); i++) { // Folia - region threading - TODO WTF is this?
this.tickables.get(i).run();
}
profilerFiller.popPush("send chunks");
- for (ServerPlayer serverPlayer : this.playerList.getPlayers()) {
+ if (false) for (ServerPlayer serverPlayer : this.playerList.getPlayers()) { // Folia - region threading
serverPlayer.connection.chunkSender.sendNextChunks(serverPlayer);
serverPlayer.connection.resumeFlushing();
}
@@ -2073,7 +2167,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
public int getTickCount() {
- return this.tickCount;
+ throw new UnsupportedOperationException(); // Folia - region threading
}
public int getSpawnProtectionRadius() {
@@ -2128,6 +2222,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
public void invalidateStatus() {
+ // Folia start - region threading
+ if (true) {
+ // we don't need this to notify the global tick region
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTaskWithoutNotify(() -> {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().invalidateStatus();
+ });
+ return;
+ }
+ // Folia end - region threading
this.lastServerStatus = 0L;
}
@@ -2142,6 +2245,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@Override
public void executeIfPossible(Runnable task) {
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
if (this.isStopped()) {
throw new io.papermc.paper.util.ServerStopRejectedExecutionException("Server already shutting down"); // Paper - do not prematurely disconnect players on stop
} else {
@@ -2455,7 +2559,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
public long getAverageTickTimeNanos() {
- return this.aggregatedTickTimesNanos / Math.min(100, Math.max(this.tickCount, 1));
+ // Folia start - region threading
+ if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentTickingTask() instanceof io.papermc.paper.threadedregions.TickRegionScheduler.RegionScheduleHandle handle) {
+ return (long)Math.ceil(handle.getTickReport5s(System.nanoTime()).timePerTickData().segmentAll().average());
+ }
+ return 0L;
+ // Folia end - region threading
}
public long[] getTickTimesNanos() {
@@ -2705,13 +2814,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
public ProfileResults stopTimeProfiler() {
- if (this.debugCommandProfiler == null) {
- return EmptyProfileResults.EMPTY;
- } else {
- ProfileResults profileResults = this.debugCommandProfiler.stop(Util.getNanos(), this.tickCount);
- this.debugCommandProfiler = null;
- return profileResults;
- }
+ throw new UnsupportedOperationException(); // Folia - region threading
}
public int getMaxChainedNeighborUpdates() {
@@ -2896,24 +2999,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
// Paper start - API to check if the server is sleeping
public boolean isTickPaused() {
- return this.emptyTicks > 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20;
+ return false; // Folia - region threading
}
public void addPluginAllowingSleep(final String pluginName, final boolean value) {
- if (!value) {
- this.pluginsBlockingSleep.add(pluginName);
- } else {
- this.pluginsBlockingSleep.remove(pluginName);
- }
+ // Folia - region threading
}
private void removeDisabledPluginsBlockingSleep() {
- if (this.pluginsBlockingSleep.isEmpty()) {
- return;
- }
- this.pluginsBlockingSleep.removeIf(plugin -> (
- !io.papermc.paper.plugin.manager.PaperPluginManagerImpl.getInstance().isPluginEnabled(plugin)
- ));
+ // Folia - region threading
}
// Paper end - API to check if the server is sleeping
}
diff --git a/net/minecraft/server/commands/AdvancementCommands.java b/net/minecraft/server/commands/AdvancementCommands.java
index 9157c1efef669795c8408d2e344a2bfeeabeb842..7873f11d7462ef88b5ba27d99988ac9e45689d3a 100644
--- a/net/minecraft/server/commands/AdvancementCommands.java
+++ b/net/minecraft/server/commands/AdvancementCommands.java
@@ -246,7 +246,12 @@ public class AdvancementCommands {
int i = 0;
for (ServerPlayer serverPlayer : targets) {
- i += action.perform(serverPlayer, advancements);
+ // Folia start - region threading
+ i += 1;
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
+ action.perform(player, advancements);
+ }, null, 1L);
+ // Folia end - region threading
}
if (i == 0) {
@@ -310,9 +315,12 @@ public class AdvancementCommands {
throw ERROR_CRITERION_NOT_FOUND.create(Advancement.name(advancement), criterionName);
} else {
for (ServerPlayer serverPlayer : targets) {
- if (action.performCriterion(serverPlayer, advancement, criterionName)) {
- i++;
- }
+ // Folia start - region threading
+ ++i;
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
+ action.performCriterion(player, advancement, criterionName);
+ }, null, 1L);
+ // Folia end - region threading
}
if (i == 0) {
diff --git a/net/minecraft/server/commands/AttributeCommand.java b/net/minecraft/server/commands/AttributeCommand.java
index 2f0e8b2b1dda17cf861f80f8c1e655a345b76d10..505f0ce1f7b453d7e30e07c13a6b7678e12b0fda 100644
--- a/net/minecraft/server/commands/AttributeCommand.java
+++ b/net/minecraft/server/commands/AttributeCommand.java
@@ -266,30 +266,62 @@ public class AttributeCommand {
}
}
+ // Folia start - region threading
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
+ src.sendFailure((Component)ex.getRawMessage());
+ }
+ // Folia end - region threading
+
private static int getAttributeValue(CommandSourceStack source, Entity entity, Holder<Attribute> 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> 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> 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<ResourceLocation> getAttributeModifiers(Entity entity, Holder<Attribute> attribute) throws CommandSyntaxException {
@@ -312,11 +351,22 @@ public class AttributeCommand {
}
private static int setAttributeBase(CommandSourceStack source, Entity entity, Holder<Attribute> 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> attribute) throws CommandSyntaxException {
@@ -338,35 +388,57 @@ public class AttributeCommand {
private static int addModifier(
CommandSourceStack source, Entity entity, Holder<Attribute> 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> 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> attribute) {
diff --git a/net/minecraft/server/commands/ClearInventoryCommands.java b/net/minecraft/server/commands/ClearInventoryCommands.java
index 73650c835ae3a8709d21462bc91a466167cd115f..3cbeaf2046bb0a41085a00134e69162df46d2081 100644
--- a/net/minecraft/server/commands/ClearInventoryCommands.java
+++ b/net/minecraft/server/commands/ClearInventoryCommands.java
@@ -65,9 +65,14 @@ public class ClearInventoryCommands {
int i = 0;
for (ServerPlayer serverPlayer : targetPlayers) {
- i += serverPlayer.getInventory().clearOrCountMatchingItems(itemPredicate, maxCount, serverPlayer.inventoryMenu.getCraftSlots());
- serverPlayer.containerMenu.broadcastChanges();
- serverPlayer.inventoryMenu.slotsChanged(serverPlayer.getInventory());
+ // Folia start - region threading
+ ++i;
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
+ player.getInventory().clearOrCountMatchingItems(itemPredicate, maxCount, player.inventoryMenu.getCraftSlots());
+ player.containerMenu.broadcastChanges();
+ player.inventoryMenu.slotsChanged(player.getInventory());
+ }, null, 1L);
+ // Folia end - region threading
}
if (i == 0) {
diff --git a/net/minecraft/server/commands/DamageCommand.java b/net/minecraft/server/commands/DamageCommand.java
index d99602f2c7e5463243dfaf83ada12c1d8e7d1192..5f3c886e2bc8a23e902cf8037ac8c871a601883f 100644
--- a/net/minecraft/server/commands/DamageCommand.java
+++ b/net/minecraft/server/commands/DamageCommand.java
@@ -102,12 +102,29 @@ public class DamageCommand {
);
}
+ // Folia start - region threading
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
+ src.sendFailure((Component)ex.getRawMessage());
+ }
+ // Folia end - region threading
+
private static int damage(CommandSourceStack source, Entity target, float amount, DamageSource damageType) throws CommandSyntaxException {
- if (target.hurtServer(source.getLevel(), damageType, amount)) {
- source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, target.getDisplayName()), true);
- return 1;
+ // Folia start - region threading
+ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> {
+ try {
+ // Folia end - region threading
+ if (nmsEntity.hurtServer(source.getLevel(), damageType, amount)) { // Folia - region threading
+ source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, nmsEntity.getDisplayName()), true); // Folia - region threading
+ return; // Folia - region threading
} else {
throw ERROR_INVULNERABLE.create();
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ }, null, 1L);
+ return 0;
+ // Folia end - region threading
}
}
diff --git a/net/minecraft/server/commands/DefaultGameModeCommands.java b/net/minecraft/server/commands/DefaultGameModeCommands.java
index fd42373ccfedf28ffc0fcf9b3153e5a308c561c5..8a8e51c6a63858df2eae4176df75a66636d3f458 100644
--- a/net/minecraft/server/commands/DefaultGameModeCommands.java
+++ b/net/minecraft/server/commands/DefaultGameModeCommands.java
@@ -28,12 +28,14 @@ public class DefaultGameModeCommands {
GameType forcedGameType = server.getForcedGameType();
if (forcedGameType != null) {
for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) {
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading
// Paper start - Expand PlayerGameModeChangeEvent
- org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty());
+ org.bukkit.event.player.PlayerGameModeChangeEvent event = player.setGameMode(gamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); // Folia - region threading
if (event != null && event.isCancelled()) {
commandSource.sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false);
}
// Paper end - Expand PlayerGameModeChangeEvent
+ }, null, 1L); // Folia - region threading
i++;
}
}
diff --git a/net/minecraft/server/commands/EffectCommands.java b/net/minecraft/server/commands/EffectCommands.java
index 0089ff5ca207278b829ec7530f50ec14681ab574..8d6e1dab63a6ef79d038fc6c3e9f7bf184b1d8c7 100644
--- a/net/minecraft/server/commands/EffectCommands.java
+++ b/net/minecraft/server/commands/EffectCommands.java
@@ -180,7 +180,12 @@ public class EffectCommands {
for (Entity entity : targets) {
if (entity instanceof LivingEntity) {
MobEffectInstance mobEffectInstance = new MobEffectInstance(effect, i1, amplifier, false, showParticles);
- if (((LivingEntity)entity).addEffect(mobEffectInstance, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
+ // Folia start - region threading
+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> {
+ ((LivingEntity)nmsEntity).addEffect(mobEffectInstance, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND);
+ }, null, 1L);
+ // Folia end - region threading
+ if (true) { // CraftBukkit // Folia - region threading
i++;
}
}
@@ -210,7 +215,12 @@ public class EffectCommands {
int i = 0;
for (Entity entity : targets) {
- if (entity instanceof LivingEntity && ((LivingEntity)entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
+ if (entity instanceof LivingEntity && true) { // CraftBukkit // Folia - region threading
+ // Folia start - region threading
+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> {
+ ((LivingEntity)nmsEntity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND);
+ }, null, 1L);
+ // Folia end - region threading
i++;
}
}
@@ -235,7 +245,12 @@ public class EffectCommands {
int i = 0;
for (Entity entity : targets) {
- if (entity instanceof LivingEntity && ((LivingEntity)entity).removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit
+ if (entity instanceof LivingEntity && true) { // CraftBukkit // Folia - region threading
+ // Folia start - region threading
+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> {
+ ((LivingEntity)nmsEntity).removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND);
+ }, null, 1L);
+ // Folia end - region threading
i++;
}
}
diff --git a/net/minecraft/server/commands/EnchantCommand.java b/net/minecraft/server/commands/EnchantCommand.java
index fe86823f1a02d66df143756f00ee56fb9f634475..b62ed9d5456ae2c050c4d502b10c5e50c7265b96 100644
--- a/net/minecraft/server/commands/EnchantCommand.java
+++ b/net/minecraft/server/commands/EnchantCommand.java
@@ -68,51 +68,78 @@ public class EnchantCommand {
);
}
+ // Folia start - region threading
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
+ src.sendFailure((Component)ex.getRawMessage());
+ }
+ // Folia end - region threading
+
private static int enchant(CommandSourceStack source, Collection<? extends Entity> targets, Holder<Enchantment> 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<Component> possibleSingleDisplayName = new java.util.concurrent.atomic.AtomicReference<>(); // Folia - region threading
for (Entity entity : targets) {
if (entity instanceof LivingEntity) {
- LivingEntity livingEntity = (LivingEntity)entity;
- ItemStack mainHandItem = livingEntity.getMainHandItem();
- if (!mainHandItem.isEmpty()) {
- if (enchantment1.canEnchant(mainHandItem)
- && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(mainHandItem).keySet(), enchantment)) {
- mainHandItem.enchant(enchantment, level);
- i++;
- } else if (targets.size() == 1) {
- throw ERROR_INCOMPATIBLE.create(mainHandItem.getHoverName().getString());
+ // Folia start - region threading
+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> {
+ try {
+ LivingEntity livingEntity = (LivingEntity)nmsEntity;
+ ItemStack mainHandItem = livingEntity.getMainHandItem();
+ if (!mainHandItem.isEmpty()) {
+ if (enchantment1.canEnchant(mainHandItem)
+ && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(mainHandItem).keySet(), enchantment)) {
+ mainHandItem.enchant(enchantment, level);
+ possibleSingleDisplayName.set(livingEntity.getDisplayName());
+ changed.incrementAndGet();
+ } else if (targets.size() == 1) {
+ throw ERROR_INCOMPATIBLE.create(mainHandItem.getHoverName().getString());
+ }
+ } else if (targets.size() == 1) {
+ throw ERROR_NO_ITEM.create(livingEntity.getName().getString());
+ }
+ } catch (final CommandSyntaxException exception) {
+ sendMessage(source, exception);
+ return; // don't send feedback twice
}
- } else if (targets.size() == 1) {
- throw ERROR_NO_ITEM.create(livingEntity.getName().getString());
- }
+ sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed);
+ }, ignored -> sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed), 1L);
} else if (targets.size() == 1) {
throw ERROR_NOT_LIVING_ENTITY.create(entity.getName().getString());
+ } else {
+ sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed);
+ // Folia end - region threading
}
}
+ return targets.size(); // Folia - region threading
+ }
+ }
+ // Folia start - region threading
+ private static void sendFeedback(final CommandSourceStack source, final Holder<Enchantment> enchantment, final int level, final java.util.concurrent.atomic.AtomicReference<Component> possibleSingleDisplayName, final java.util.concurrent.atomic.AtomicInteger count, final java.util.concurrent.atomic.AtomicInteger changed) {
+ if (count.decrementAndGet() == 0) {
+ final int i = changed.get();
if (i == 0) {
- throw ERROR_NOTHING_HAPPENED.create();
+ sendMessage(source, ERROR_NOTHING_HAPPENED.create());
} else {
- if (targets.size() == 1) {
+ if (i == 1) {
source.sendSuccess(
() -> Component.translatable(
- "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), targets.iterator().next().getDisplayName()
+ "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), possibleSingleDisplayName.get()
),
true
);
} else {
source.sendSuccess(
- () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), targets.size()), true
+ () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), i), true
);
}
-
- return i;
}
}
}
+ // Folia end - region threading
}
diff --git a/net/minecraft/server/commands/ExperienceCommand.java b/net/minecraft/server/commands/ExperienceCommand.java
index cb59af8018d3009876a47fae249885c00b6c7b57..e0d95f61e8a2841979bc9b5381dfaf7d3239beb7 100644
--- a/net/minecraft/server/commands/ExperienceCommand.java
+++ b/net/minecraft/server/commands/ExperienceCommand.java
@@ -131,14 +131,18 @@ public class ExperienceCommand {
}
private static int queryExperience(CommandSourceStack source, ServerPlayer player, ExperienceCommand.Type type) {
- int i = type.query.applyAsInt(player);
- source.sendSuccess(() -> Component.translatable("commands.experience.query." + type.name, player.getDisplayName(), i), false);
- return i;
+ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading
+ int i = type.query.applyAsInt(serverPlayer); // Folia - region threading
+ source.sendSuccess(() -> Component.translatable("commands.experience.query." + type.name, serverPlayer.getDisplayName(), i), false); // Folia - region threading
+ }, null, 1L); // Folia - region threading
+ return 0; // Folia - region threading
}
private static int addExperience(CommandSourceStack source, Collection<? extends ServerPlayer> targets, int amount, ExperienceCommand.Type type) {
for (ServerPlayer serverPlayer : targets) {
- type.add.accept(serverPlayer, amount);
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading
+ type.add.accept(player, amount);
+ }, null, 1L); // Folia - region threading
}
if (targets.size() == 1) {
@@ -157,9 +161,11 @@ public class ExperienceCommand {
int i = 0;
for (ServerPlayer serverPlayer : targets) {
- if (type.set.test(serverPlayer, amount)) {
- i++;
+ i++; serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading
+ if (type.set.test(player, amount)) { // Folia - region threading
+ //i++; // Folia - region threading
}
+ }, null, 1L); // Folia - region threading
}
if (i == 0) {
diff --git a/net/minecraft/server/commands/FillBiomeCommand.java b/net/minecraft/server/commands/FillBiomeCommand.java
index bb2c8612b27bb04758c467ec6245de1236fc4de1..d5ae0eeb504b9306015de37abc59bf1a76a23837 100644
--- a/net/minecraft/server/commands/FillBiomeCommand.java
+++ b/net/minecraft/server/commands/FillBiomeCommand.java
@@ -107,6 +107,16 @@ public class FillBiomeCommand {
return fill(level, from, to, biome, biome1 -> true, message -> {});
}
+ // Folia start - region threading
+ private static void sendMessage(Consumer<Supplier<Component>> src, Supplier<Either<Integer, CommandSyntaxException>> supplier) {
+ Either<Integer, CommandSyntaxException> 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<Integer, CommandSyntaxException> fill(
ServerLevel level, BlockPos from, BlockPos to, Holder<Biome> biome, Predicate<Holder<Biome>> filter, Consumer<Supplier<Component>> 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<ChunkAccess> list = new ArrayList<>();
for (int sectionPosMinZ = SectionPos.blockToSectionCoord(boundingBox.minZ());
@@ -158,6 +179,11 @@ public class FillBiomeCommand {
)
);
return Either.left(mutableInt.getValue());
+ // Folia start - region threading
+ }); // sendMessage
+ }); // loadChunksASync
+ return Either.left(Integer.valueOf(0));
+ // Folia end - region threading
}
}
diff --git a/net/minecraft/server/commands/FillCommand.java b/net/minecraft/server/commands/FillCommand.java
index a224f8cc122fc6d79b4abd08815f58f0e6aa340b..89154adfc659afa188cd771e70087e3b1a9c98b9 100644
--- a/net/minecraft/server/commands/FillCommand.java
+++ b/net/minecraft/server/commands/FillCommand.java
@@ -151,6 +151,12 @@ public class FillCommand {
);
}
+ // Folia start - region threading
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
+ src.sendFailure((Component)ex.getRawMessage());
+ }
+ // Folia end - region threading
+
private static int fillBlocks(
CommandSourceStack source, BoundingBox area, BlockInput newBlock, FillCommand.Mode mode, @Nullable Predicate<BlockInWorld> replacingPredicate
) throws CommandSyntaxException {
@@ -161,6 +167,18 @@ public class FillCommand {
} else {
List<BlockPos> list = Lists.newArrayList();
ServerLevel level = source.getLevel();
+ // Folia start - region threading
+ int buffer = 32;
+ // physics may spill into neighbour chunks, so use a buffer
+ level.moonrise$loadChunksAsync(
+ (area.minX() - buffer) >> 4,
+ (area.maxX() + buffer) >> 4,
+ (area.minZ() - buffer) >> 4,
+ (area.maxZ() + buffer) >> 4,
+ net.minecraft.world.level.chunk.status.ChunkStatus.FULL,
+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
+ (chunks) -> {
+ try { // Folia end - region threading
int i1 = 0;
for (BlockPos blockPos : BlockPos.betweenClosed(area.minX(), area.minY(), area.minZ(), area.maxX(), area.maxY(), area.maxZ())) {
@@ -187,8 +205,13 @@ public class FillCommand {
} else {
int i2 = i1;
source.sendSuccess(() -> Component.translatable("commands.fill.success", i2), true);
- return i1;
+ return; // Folia - region threading
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ }); return 0; // Folia end - region threading
}
}
diff --git a/net/minecraft/server/commands/ForceLoadCommand.java b/net/minecraft/server/commands/ForceLoadCommand.java
index 619ffb7846047d3e033378c750dc4ceaf9ac6239..6e174d54a3bf6a7a23a0aa6e7802b407e3969a47 100644
--- a/net/minecraft/server/commands/ForceLoadCommand.java
+++ b/net/minecraft/server/commands/ForceLoadCommand.java
@@ -97,7 +97,17 @@ public class ForceLoadCommand {
);
}
+ // Folia start - region threading
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
+ src.sendFailure((Component)ex.getRawMessage());
+ }
+ // Folia end - region threading
+
private static int queryForceLoad(CommandSourceStack source, ColumnPos pos) throws CommandSyntaxException {
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ try {
+ // Folia end - region threading
ChunkPos chunkPos = pos.toChunkPos();
ServerLevel level = source.getLevel();
ResourceKey<Level> 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<Level> resourceKey = level.dimension();
LongSet forcedChunks = level.getForcedChunks();
int size = forcedChunks.size();
@@ -134,20 +152,27 @@ public class ForceLoadCommand {
} else {
source.sendFailure(Component.translatable("commands.forceload.added.none", Component.translationArg(resourceKey.location())));
}
+ }); // Folia - region threading
- return size;
+ return 1; // Folia - region threading
}
private static int removeAll(CommandSourceStack source) {
ServerLevel level = source.getLevel();
ResourceKey<Level> resourceKey = level.dimension();
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
LongSet forcedChunks = level.getForcedChunks();
forcedChunks.forEach(packedChunkPos -> level.setChunkForced(ChunkPos.getX(packedChunkPos), ChunkPos.getZ(packedChunkPos), false));
source.sendSuccess(() -> Component.translatable("commands.forceload.removed.all", Component.translationArg(resourceKey.location())), true);
+ }); // Folia - region threading
return 0;
}
private static int changeForceLoad(CommandSourceStack source, ColumnPos from, ColumnPos to, boolean add) throws CommandSyntaxException {
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ try {
+ // Folia end - region threading
int min = Math.min(from.x(), to.x());
int min1 = Math.min(from.z(), to.z());
int max = Math.max(from.x(), to.x());
@@ -207,11 +232,18 @@ public class ForceLoadCommand {
);
}
- return i2x;
+ return; // Folia - region threading
}
}
} else {
throw BlockPosArgument.ERROR_OUT_OF_WORLD.create();
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ });
+ return 1;
+ // Folia end - region threading
}
}
diff --git a/net/minecraft/server/commands/GameModeCommand.java b/net/minecraft/server/commands/GameModeCommand.java
index c44cdbbdc06b25bd20a208386545a10af9b96df8..f6204e765afda2668ab394c570444fbb7f152b8b 100644
--- a/net/minecraft/server/commands/GameModeCommand.java
+++ b/net/minecraft/server/commands/GameModeCommand.java
@@ -54,15 +54,18 @@ public class GameModeCommand {
int i = 0;
for (ServerPlayer serverPlayer : players) {
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer nmsEntity) -> { // Folia - region threading
// Paper start - Expand PlayerGameModeChangeEvent
- org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty());
+ org.bukkit.event.player.PlayerGameModeChangeEvent event = nmsEntity.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); // Folia - region threading
if (event != null && !event.isCancelled()) {
- logGamemodeChange(source.getSource(), serverPlayer, gameType);
- i++;
+ logGamemodeChange(source.getSource(), nmsEntity, gameType); // Folia - region threading
+ //i++; // Folia - region threading
} else if (event != null && event.cancelMessage() != null) {
source.getSource().sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true);
// Paper end - Expand PlayerGameModeChangeEvent
}
+ }, null, 1L); // Folia - region threading
+ ++i; // Folia - region threading
}
return i;
diff --git a/net/minecraft/server/commands/GiveCommand.java b/net/minecraft/server/commands/GiveCommand.java
index 8b7af734ca4ed3cafa810460b2cea6c1e6342a69..6f5d88d83ad724fa2b7549075b687aebd4b24eed 100644
--- a/net/minecraft/server/commands/GiveCommand.java
+++ b/net/minecraft/server/commands/GiveCommand.java
@@ -65,32 +65,34 @@ public class GiveCommand {
int min = Math.min(maxStackSize, i1);
i1 -= min;
ItemStack itemStack1 = item.createItemStack(min, false);
- boolean flag = serverPlayer.getInventory().add(itemStack1);
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer nmsEntity) -> { // Folia - region threading
+ boolean flag = nmsEntity.getInventory().add(itemStack1); // Folia - region threading
if (flag && itemStack1.isEmpty()) {
- ItemEntity itemEntity = serverPlayer.drop(itemStack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event
+ ItemEntity itemEntity = nmsEntity.drop(itemStack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event // Folia - region threading
if (itemEntity != null) {
itemEntity.makeFakeItem();
}
- serverPlayer.level()
+ nmsEntity.level() // Folia - region threading
.playSound(
null,
- serverPlayer.getX(),
- serverPlayer.getY(),
- serverPlayer.getZ(),
+ nmsEntity.getX(), // Folia - region threading
+ nmsEntity.getY(), // Folia - region threading
+ nmsEntity.getZ(), // Folia - region threading
SoundEvents.ITEM_PICKUP,
SoundSource.PLAYERS,
0.2F,
- ((serverPlayer.getRandom().nextFloat() - serverPlayer.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F
+ ((nmsEntity.getRandom().nextFloat() - nmsEntity.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F // Folia - region threading
);
- serverPlayer.containerMenu.broadcastChanges();
+ nmsEntity.containerMenu.broadcastChanges(); // Folia - region threading
} else {
- ItemEntity itemEntity = serverPlayer.drop(itemStack1, false);
+ ItemEntity itemEntity = nmsEntity.drop(itemStack1, false); // Folia - region threading
if (itemEntity != null) {
itemEntity.setNoPickUpDelay();
- itemEntity.setTarget(serverPlayer.getUUID());
+ itemEntity.setTarget(nmsEntity.getUUID()); // Folia - region threading
}
}
+ }, null, 1L); // Folia - region threading
}
}
diff --git a/net/minecraft/server/commands/KillCommand.java b/net/minecraft/server/commands/KillCommand.java
index e8ab673921c8089a35a2e678d7a6efed1f728cd7..287681a351f49eabd4f480396314a882bee73645 100644
--- a/net/minecraft/server/commands/KillCommand.java
+++ b/net/minecraft/server/commands/KillCommand.java
@@ -24,7 +24,9 @@ public class KillCommand {
private static int kill(CommandSourceStack source, Collection<? extends Entity> targets) {
for (Entity entity : targets) {
- entity.kill(source.getLevel());
+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { // Folia - region threading
+ nmsEntity.kill((net.minecraft.server.level.ServerLevel)nmsEntity.level()); // Folia - region threading
+ }, null, 1L); // Folia - region threading
}
if (targets.size() == 1) {
diff --git a/net/minecraft/server/commands/PlaceCommand.java b/net/minecraft/server/commands/PlaceCommand.java
index f019285714cf6e7ac08d6b3b96fe705b8a564c28..4decfa02f0fa11a14abd48944e9cb2dd86bb96a2 100644
--- a/net/minecraft/server/commands/PlaceCommand.java
+++ b/net/minecraft/server/commands/PlaceCommand.java
@@ -233,36 +233,79 @@ public class PlaceCommand {
);
}
+ // Folia start - region threading
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
+ src.sendFailure((Component)ex.getRawMessage());
+ }
+ // Folia end - region threading
+
public static int placeFeature(CommandSourceStack source, Holder.Reference<ConfiguredFeature<?, ?>> 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<StructureTemplatePool> 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> structure, BlockPos pos) throws CommandSyntaxException {
ServerLevel level = source.getLevel();
Structure structure1 = structure.value();
ChunkGenerator generator = level.getChunkSource().getGenerator();
+ // Folia start - region threading
+ level.moonrise$loadChunksAsync(
+ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL,
+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
+ (chunks) -> {
+ try {
+ // Folia end - region threading
StructureStart structureStart = structure1.generate(
structure,
level.dimension(),
@@ -305,14 +348,29 @@ public class PlaceCommand {
);
String string = structure.key().location().toString();
source.sendSuccess(() -> Component.translatable("commands.place.structure.success", string, pos.getX(), pos.getY(), pos.getZ()), true);
- return 1;
+ return; // Folia - region threading
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ }
+ );
+ return 1;
+ // Folia end - region threading
}
public static int placeTemplate(
CommandSourceStack source, ResourceLocation template, BlockPos pos, Rotation rotation, Mirror mirror, float integrity, int seed
) throws CommandSyntaxException {
ServerLevel level = source.getLevel();
+ // Folia start - region threading
+ level.moonrise$loadChunksAsync(
+ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL,
+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
+ (chunks) -> {
+ try {
+ // Folia end - region threading
StructureTemplateManager structureManager = level.getStructureManager();
Optional<StructureTemplate> optional;
@@ -340,9 +398,17 @@ public class PlaceCommand {
() -> Component.translatable("commands.place.template.success", Component.translationArg(template), pos.getX(), pos.getY(), pos.getZ()),
true
);
- return 1;
+ return; // Folia - region threading
}
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ }
+ );
+ return 1;
+ // Folia end - region threading
}
private static void checkLoaded(ServerLevel level, ChunkPos start, ChunkPos end) throws CommandSyntaxException {
diff --git a/net/minecraft/server/commands/RecipeCommand.java b/net/minecraft/server/commands/RecipeCommand.java
index d171a5b8c1969f6a482f029afa5fb0228aefb04d..c8ab7f56c5e5af99b5410784a3ae33dedd7bf2f3 100644
--- a/net/minecraft/server/commands/RecipeCommand.java
+++ b/net/minecraft/server/commands/RecipeCommand.java
@@ -81,7 +81,12 @@ public class RecipeCommand {
int i = 0;
for (ServerPlayer serverPlayer : targets) {
- i += serverPlayer.awardRecipes(recipes);
+ // Folia start - region threading
+ ++i;
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
+ player.awardRecipes(recipes);
+ }, null, 1L);
+ // Folia end - region threading
}
if (i == 0) {
@@ -103,7 +108,12 @@ public class RecipeCommand {
int i = 0;
for (ServerPlayer serverPlayer : targets) {
- i += serverPlayer.resetRecipes(recipes);
+ // Folia start - region threading
+ ++i;
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
+ player.resetRecipes(recipes);
+ }, null, 1L);
+ // Folia end - region threading
}
if (i == 0) {
diff --git a/net/minecraft/server/commands/SetBlockCommand.java b/net/minecraft/server/commands/SetBlockCommand.java
index 8b72116b80da0497e255ce5a3f3c7bccb6321aec..05b824409546ba8bacf7efdaeac106af89ff0715 100644
--- a/net/minecraft/server/commands/SetBlockCommand.java
+++ b/net/minecraft/server/commands/SetBlockCommand.java
@@ -80,10 +80,21 @@ public class SetBlockCommand {
);
}
+ // Folia start - region threading
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
+ src.sendFailure((Component)ex.getRawMessage());
+ }
+ // Folia end - region threading
+
private static int setBlock(
CommandSourceStack source, BlockPos pos, BlockInput state, SetBlockCommand.Mode mode, @Nullable Predicate<BlockInWorld> predicate
) throws CommandSyntaxException {
ServerLevel level = source.getLevel();
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
+ level, pos.getX() >> 4, pos.getZ() >> 4, () -> {
+ try {
+ // Folia end - region threading
if (predicate != null && !predicate.test(new BlockInWorld(level, pos, true))) {
throw ERROR_FAILED.create();
} else {
@@ -102,9 +113,16 @@ public class SetBlockCommand {
} else {
level.blockUpdated(pos, state.getState().getBlock());
source.sendSuccess(() -> Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true);
- return 1;
+ return; // Folia - region threading
}
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ });
+ return 1;
+ // Folia end - region threading
}
public interface Filter {
diff --git a/net/minecraft/server/commands/SetSpawnCommand.java b/net/minecraft/server/commands/SetSpawnCommand.java
index e38c7f012098e46337561b2225b31a7097495647..6fc6a748a8096524440d32d692088a8176875786 100644
--- a/net/minecraft/server/commands/SetSpawnCommand.java
+++ b/net/minecraft/server/commands/SetSpawnCommand.java
@@ -69,7 +69,11 @@ public class SetSpawnCommand {
final Collection<ServerPlayer> actualTargets = new java.util.ArrayList<>(); // Paper - Add PlayerSetSpawnEvent
for (ServerPlayer serverPlayer : targets) {
// Paper start - Add PlayerSetSpawnEvent
- if (serverPlayer.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) {
+ // Folia start - region threading
+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
+ player.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND);
+ }, null, 1L);
+ if (true) { // Folia end - region threading
actualTargets.add(serverPlayer);
}
// Paper end - Add PlayerSetSpawnEvent
diff --git a/net/minecraft/server/commands/SummonCommand.java b/net/minecraft/server/commands/SummonCommand.java
index b68c0e617d3593cc9ba999ed25ea2c1b7c762597..2d4bf39f3f35811a7f48f361c91ee3d5722ba839 100644
--- a/net/minecraft/server/commands/SummonCommand.java
+++ b/net/minecraft/server/commands/SummonCommand.java
@@ -88,12 +88,18 @@ public class SummonCommand {
if (entity == null) {
throw ERROR_FAILED.create();
} else {
- if (randomizeProperties && entity instanceof Mob) {
- ((Mob)entity)
- .finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, null);
- }
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
+ level, entity.chunkPosition().x, entity.chunkPosition().z, () -> {
+ if (randomizeProperties && entity instanceof Mob) {
+ ((Mob)entity)
+ .finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, null);
+ }
+ level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND);
+ });
+ // Folia end - region threading
- if (!level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND"
+ if (false) { // CraftBukkit - pass a spawn reason of "COMMAND" // Folia - region threading
throw ERROR_DUPLICATE_UUID.create();
} else {
return entity;
diff --git a/net/minecraft/server/commands/TeleportCommand.java b/net/minecraft/server/commands/TeleportCommand.java
index 01f8e2fec232210c9311565197860cf0257081fd..174122905addbc88e818cd4946e831aec051b91a 100644
--- a/net/minecraft/server/commands/TeleportCommand.java
+++ b/net/minecraft/server/commands/TeleportCommand.java
@@ -154,18 +154,7 @@ public class TeleportCommand {
private static int teleportToEntity(CommandSourceStack source, Collection<? extends Entity> targets, Entity destination) throws CommandSyntaxException {
for (Entity entity : targets) {
- performTeleport(
- source,
- entity,
- (ServerLevel)destination.level(),
- destination.getX(),
- destination.getY(),
- destination.getZ(),
- EnumSet.noneOf(Relative.class),
- destination.getYRot(),
- destination.getXRot(),
- null
- );
+ io.papermc.paper.threadedregions.TeleportUtils.teleport(entity, false, destination, Float.valueOf(destination.getYRot()), Float.valueOf(destination.getXRot()), Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, null); // Folia - region threading
}
if (targets.size() == 1) {
@@ -290,6 +279,24 @@ public class TeleportCommand {
float f1 = relatives.contains(Relative.X_ROT) ? xRot - target.getXRot() : xRot;
float f2 = Mth.wrapDegrees(f);
float f3 = Mth.wrapDegrees(f1);
+ // Folia start - region threading
+ if (true) {
+ ServerLevel worldFinal = level;
+ Vec3 posFinal = new Vec3(x, y, z);
+ Float yawFinal = Float.valueOf(f);
+ Float pitchFinal = Float.valueOf(f1);
+ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> {
+ nmsEntity.unRide();
+ nmsEntity.teleportAsync(
+ worldFinal, posFinal, yawFinal, pitchFinal, Vec3.ZERO,
+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND,
+ Entity.TELEPORT_FLAG_LOAD_CHUNK,
+ null
+ );
+ }, null, 1L);
+ return;
+ }
+ // Folia end - region threading
// CraftBukkit start - Teleport event
boolean result;
if (target instanceof final net.minecraft.server.level.ServerPlayer player) {
diff --git a/net/minecraft/server/commands/TimeCommand.java b/net/minecraft/server/commands/TimeCommand.java
index e952ca088a2f36fc7f1eef4d9b217351569becc1..5d1fc3bb00abd177325a292f55d2cf1cddd3158b 100644
--- a/net/minecraft/server/commands/TimeCommand.java
+++ b/net/minecraft/server/commands/TimeCommand.java
@@ -56,6 +56,7 @@ public class TimeCommand {
}
public static int setTime(CommandSourceStack source, int time) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
// serverLevel.setDayTime(time);
// CraftBukkit start
@@ -69,10 +70,12 @@ public class TimeCommand {
source.getServer().forceTimeSynchronization();
source.sendSuccess(() -> Component.translatable("commands.time.set", time), true);
- return getDayTime(source.getLevel());
+ }); // Folia - region threading
+ return 0; // Folia - region threading
}
public static int addTime(CommandSourceStack source, int amount) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change
// CraftBukkit start
org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(serverLevel.getWorld(), org.bukkit.event.world.TimeSkipEvent.SkipReason.COMMAND, amount);
@@ -86,6 +89,7 @@ public class TimeCommand {
source.getServer().forceTimeSynchronization();
int dayTime = getDayTime(source.getLevel());
source.sendSuccess(() -> Component.translatable("commands.time.set", dayTime), true);
- return dayTime;
+ }); // Folia - region threading
+ return 0; // Folia - region threading
}
}
diff --git a/net/minecraft/server/commands/WeatherCommand.java b/net/minecraft/server/commands/WeatherCommand.java
index 9b14b6218b2673e9b13b749b566e3b8a6a8d9c7d..dade5adec00c081cb4def7464f0f04d2f5a6ae26 100644
--- a/net/minecraft/server/commands/WeatherCommand.java
+++ b/net/minecraft/server/commands/WeatherCommand.java
@@ -48,20 +48,26 @@ public class WeatherCommand {
}
private static int setClear(CommandSourceStack source, int time) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
source.getLevel().setWeatherParameters(getDuration(source, time, ServerLevel.RAIN_DELAY), 0, false, false); // CraftBukkit - SPIGOT-7680: per-world
source.sendSuccess(() -> Component.translatable("commands.weather.set.clear"), true);
+ }); // Folia - region threading
return time;
}
private static int setRain(CommandSourceStack source, int time) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.RAIN_DURATION), true, false); // CraftBukkit - SPIGOT-7680: per-world
source.sendSuccess(() -> Component.translatable("commands.weather.set.rain"), true);
+ }); // Folia - region threading
return time;
}
private static int setThunder(CommandSourceStack source, int time) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading
source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.THUNDER_DURATION), true, true); // CraftBukkit - SPIGOT-7680: per-world
source.sendSuccess(() -> Component.translatable("commands.weather.set.thunder"), true);
+ }); // Folia - region threading
return time;
}
}
diff --git a/net/minecraft/server/commands/WorldBorderCommand.java b/net/minecraft/server/commands/WorldBorderCommand.java
index e2697b03a0d204eea537e3aaec2dd8fb9f426722..f6af541a7076c3fefb237b865038d08919de35ed 100644
--- a/net/minecraft/server/commands/WorldBorderCommand.java
+++ b/net/minecraft/server/commands/WorldBorderCommand.java
@@ -134,18 +134,39 @@ public class WorldBorderCommand {
);
}
+ // Folia start - region threading
+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) {
+ src.sendFailure((Component)ex.getRawMessage());
+ }
+ // Folia end - region threading
+
private static int setDamageBuffer(CommandSourceStack source, float distance) throws CommandSyntaxException {
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ try {
+ // Folia end - region threading
WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
if (worldBorder.getDamageSafeZone() == distance) {
throw ERROR_SAME_DAMAGE_BUFFER.create();
} else {
worldBorder.setDamageSafeZone(distance);
source.sendSuccess(() -> Component.translatable("commands.worldborder.damage.buffer.success", String.format(Locale.ROOT, "%.2f", distance)), true);
- return (int)distance;
+ return; // Folia - region threading
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ });
+ return 1;
+ // Folia end - region threading
}
private static int setDamageAmount(CommandSourceStack source, float damagePerBlock) throws CommandSyntaxException {
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ try {
+ // Folia end - region threading
WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
if (worldBorder.getDamagePerBlock() == damagePerBlock) {
throw ERROR_SAME_DAMAGE_AMOUNT.create();
@@ -154,39 +175,79 @@ public class WorldBorderCommand {
source.sendSuccess(
() -> Component.translatable("commands.worldborder.damage.amount.success", String.format(Locale.ROOT, "%.2f", damagePerBlock)), true
);
- return (int)damagePerBlock;
+ return; // Folia - region threading
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ });
+ return 1;
+ // Folia end - region threading
}
private static int setWarningTime(CommandSourceStack source, int time) throws CommandSyntaxException {
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ try {
+ // Folia end - region threading
WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
if (worldBorder.getWarningTime() == time) {
throw ERROR_SAME_WARNING_TIME.create();
} else {
worldBorder.setWarningTime(time);
source.sendSuccess(() -> Component.translatable("commands.worldborder.warning.time.success", time), true);
- return time;
+ return; // Folia - region threading
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ });
+ return 1;
+ // Folia end - region threading
}
private static int setWarningDistance(CommandSourceStack source, int distance) throws CommandSyntaxException {
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ try {
+ // Folia end - region threading
WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
if (worldBorder.getWarningBlocks() == distance) {
throw ERROR_SAME_WARNING_DISTANCE.create();
} else {
worldBorder.setWarningBlocks(distance);
source.sendSuccess(() -> Component.translatable("commands.worldborder.warning.distance.success", distance), true);
- return distance;
+ return; // Folia - region threading
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ });
+ return 1;
+ // Folia end - region threading
}
private static int getSize(CommandSourceStack source) {
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ // Folia end - region threading
double size = source.getLevel().getWorldBorder().getSize(); // CraftBukkit
source.sendSuccess(() -> Component.translatable("commands.worldborder.get", String.format(Locale.ROOT, "%.0f", size)), false);
- return Mth.floor(size + 0.5);
+ return; // Folia - region threading
+ // Folia start - region threading
+ });
+ return 1;
+ // Folia end - region threading
}
private static int setCenter(CommandSourceStack source, Vec2 pos) throws CommandSyntaxException {
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ try {
+ // Folia end - region threading
WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
if (worldBorder.getCenterX() == pos.x && worldBorder.getCenterZ() == pos.y) {
throw ERROR_SAME_CENTER.create();
@@ -198,13 +259,24 @@ public class WorldBorderCommand {
),
true
);
- return 0;
+ return; // Folia - region threading
} else {
throw ERROR_TOO_FAR_OUT.create();
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ });
+ return 1;
+ // Folia end - region threading
}
private static int setSize(CommandSourceStack source, double newSize, long time) throws CommandSyntaxException {
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ try {
+ // Folia end - region threading
WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit
double size = worldBorder.getSize();
if (size == newSize) {
@@ -234,7 +306,14 @@ public class WorldBorderCommand {
source.sendSuccess(() -> Component.translatable("commands.worldborder.set.immediate", String.format(Locale.ROOT, "%.1f", newSize)), true);
}
- return (int)(newSize - size);
+ return; // Folia - region threading
}
+ // Folia start - region threading
+ } catch (CommandSyntaxException ex) {
+ sendMessage(source, ex);
+ }
+ });
+ return 1;
+ // Folia end - region threading
}
}
diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
2025-02-27 22:48:16 -08:00
index d2db6e3a4af13984b0a790fb38e83c253914a973..e3b7184e20bda4983ee1fc5463d91ca0cdd8ec1f 100644
--- a/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/net/minecraft/server/dedicated/DedicatedServer.java
@@ -425,7 +425,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
@Override
public void tickConnection() {
super.tickConnection();
- this.handleConsoleInputs();
+ // Folia - region threading
}
@Override
2025-02-27 22:48:16 -08:00
@@ -734,7 +734,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
if (s.isBlank()) return ""; // Paper - Do not process empty rcon commands
rconConsoleSource.prepareForCommand();
- this.executeBlocking(() -> {
+ final java.util.concurrent.atomic.AtomicReference<String> 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);
2025-02-27 22:48:16 -08:00
@@ -743,7 +744,16 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
}
ConsoleInput serverCommand = new ConsoleInput(event.getCommand(), wrapper);
this.server.dispatchServerCommand(event.getSender(), serverCommand);
- });
+ }; // Folia start - region threading
+ java.util.concurrent.CompletableFuture
+ .runAsync(sync, io.papermc.paper.threadedregions.RegionizedServer.getInstance()::addTask)
+ .whenComplete((Void r, Throwable t) -> {
+ if (t != null) {
+ LOGGER.error("Error handling command for rcon: " + s, t);
+ }
+ })
+ .join();
+ // Folia end - region threading
return rconConsoleSource.getCommandResponse();
// CraftBukkit end
}
diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java
index b3f498558614243cf633dcd71e3c49c2c55e6e0f..329e57af5cbd38425e80dba96eb972fdfb0ce5ce 100644
--- a/net/minecraft/server/level/ChunkMap.java
+++ b/net/minecraft/server/level/ChunkMap.java
@@ -128,8 +128,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
public final ChunkMap.DistanceManager distanceManager;
public final AtomicInteger tickingGenerated = new AtomicInteger(); // Paper - public
private final String storageName;
- private final PlayerMap playerMap = new PlayerMap();
- public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap = new Int2ObjectOpenHashMap<>();
+ //private final PlayerMap playerMap = new PlayerMap(); // Folia - region threading
+ //public final Int2ObjectMap<ChunkMap.TrackedEntity> entityMap = new Int2ObjectOpenHashMap<>(); // Folia - region threading
private final Long2ByteMap chunkTypeCache = new Long2ByteOpenHashMap();
// Paper - rewrite chunk system
public int serverViewDistance;
@@ -797,12 +797,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
void updatePlayerStatus(ServerPlayer player, boolean track) {
boolean flag = this.skipPlayer(player);
- boolean flag1 = this.playerMap.ignoredOrUnknown(player);
+ //boolean flag1 = this.playerMap.ignoredOrUnknown(player); // Folia - region threading
if (track) {
- this.playerMap.addPlayer(player, flag);
+ //this.playerMap.addPlayer(player, flag); // Folia - region threading
this.updatePlayerPos(player);
if (!flag) {
- this.distanceManager.addPlayer(SectionPos.of(player), player);
+ //this.distanceManager.addPlayer(SectionPos.of(player), player); // Folia - region threading
((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation
}
@@ -810,9 +810,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
ca.spottedleaf.moonrise.common.PlatformHooks.get().addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system
} else {
SectionPos lastSectionPos = player.getLastSectionPos();
- this.playerMap.removePlayer(player);
- if (!flag1) {
- this.distanceManager.removePlayer(lastSectionPos, player);
+ //this.playerMap.removePlayer(player); // Folia - region threading
+ if (true) { // Folia - region threading
+ //this.distanceManager.removePlayer(lastSectionPos, player); // Folia - region threading
((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation
}
@@ -830,27 +830,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
SectionPos lastSectionPos = player.getLastSectionPos();
SectionPos sectionPos = SectionPos.of(player);
- boolean flag = this.playerMap.ignored(player);
+ //boolean flag = this.playerMap.ignored(player); // Folia - region threading
boolean flag1 = this.skipPlayer(player);
- boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong();
- if (flag2 || flag != flag1) {
+ //boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong(); // Folia - region threading
+ if (true) { // Folia - region threading
this.updatePlayerPos(player);
- ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, lastSectionPos, sectionPos, flag, flag1); // Paper - chunk tick iteration optimisation
- if (!flag) {
- this.distanceManager.removePlayer(lastSectionPos, player);
- }
-
- if (!flag1) {
- this.distanceManager.addPlayer(sectionPos, player);
- }
-
- if (!flag && flag1) {
- this.playerMap.ignorePlayer(player);
- }
-
- if (flag && !flag1) {
- this.playerMap.unIgnorePlayer(player);
- }
+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, lastSectionPos, sectionPos, false, flag1); // Paper - chunk tick iteration optimisation // Folia - region threading
+ // Folia - region threading
// Paper - rewrite chunk system
}
@@ -880,9 +866,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
public void addEntity(Entity entity) {
org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot
// Paper start - ignore and warn about illegal addEntity calls instead of crashing server
- if (!entity.valid || entity.level() != this.level || this.entityMap.containsKey(entity.getId())) {
+ if (!entity.valid || entity.level() != this.level || entity.moonrise$getTrackedEntity() != null) { // Folia - region threading
LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName()
- + ": " + entity + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable());
+ + ": " + entity + (entity.moonrise$getTrackedEntity() != null ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); // Folia - region threading
return;
}
// Paper end - ignore and warn about illegal addEntity calls instead of crashing server
@@ -893,22 +879,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
i = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i); // Spigot
if (i != 0) {
int updateInterval = type.updateInterval();
- if (this.entityMap.containsKey(entity.getId())) {
+ if (entity.moonrise$getTrackedEntity() != null) { // Folia - region threading
throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("Entity is already tracked!"));
} else {
ChunkMap.TrackedEntity trackedEntity = new ChunkMap.TrackedEntity(entity, i, updateInterval, type.trackDeltas());
- this.entityMap.put(entity.getId(), trackedEntity);
+ //this.entityMap.put(entity.getId(), trackedEntity); // Folia - region threading
// Paper start - optimise entity tracker
if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) {
throw new IllegalStateException("Entity is already tracked");
}
((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(trackedEntity);
// Paper end - optimise entity tracker
- trackedEntity.updatePlayers(this.level.players());
+ trackedEntity.updatePlayers(this.level.getLocalPlayers()); // Folia - region threading
if (entity instanceof ServerPlayer serverPlayer) {
this.updatePlayerStatus(serverPlayer, true);
- for (ChunkMap.TrackedEntity trackedEntity1 : this.entityMap.values()) {
+ // Folia start - region threading
+ for (Entity possible : this.level.getCurrentWorldData().trackerEntities) {
+ ChunkMap.TrackedEntity trackedEntity1 = possible.moonrise$getTrackedEntity();
+ if (trackedEntity1 == null) {
+ continue;
+ }
+ // Folia end - region threading
if (trackedEntity1.entity != serverPlayer) {
trackedEntity1.updatePlayer(serverPlayer);
}
@@ -924,12 +916,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
if (entity instanceof ServerPlayer serverPlayer) {
this.updatePlayerStatus(serverPlayer, false);
- for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
+ // Folia start - region threading
+ for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) {
+ ChunkMap.TrackedEntity trackedEntity = possible.moonrise$getTrackedEntity();
+ if (trackedEntity == null) {
+ continue;
+ }
+ // Folia end - region threading
trackedEntity.removePlayer(serverPlayer);
}
+ // Folia end - region threading
}
- ChunkMap.TrackedEntity trackedEntity1 = this.entityMap.remove(entity.getId());
+ ChunkMap.TrackedEntity trackedEntity1 = entity.moonrise$getTrackedEntity(); // Folia - region threading
if (trackedEntity1 != null) {
trackedEntity1.broadcastRemoved();
}
@@ -938,9 +937,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// Paper start - optimise entity tracker
private void newTrackerTick() {
+ final io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading
final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();;
+ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = this.level.moonrise$getNearbyPlayers(); // Folia - region threading
- final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = entityLookup.trackerEntities;
+ final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = worldData.trackerEntities; // Folia - region threading
final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
for (int i = 0, len = trackerEntities.size(); i < len; ++i) {
final Entity entity = trackerEntitiesRaw[i];
@@ -948,7 +949,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
if (tracker == null) {
continue;
}
- ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkData().nearbyPlayers);
+ ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition())); // Folia - region threading
if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity)tracker).moonrise$hasPlayers()
|| ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity)entity).moonrise$getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
tracker.serverEntity.sendChanges();
@@ -966,44 +967,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// Paper end - optimise entity tracker
// Paper - rewrite chunk system
- List<ServerPlayer> list = Lists.newArrayList();
- List<ServerPlayer> list1 = this.level.players();
-
- for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
- SectionPos sectionPos = trackedEntity.lastSectionPos;
- SectionPos sectionPos1 = SectionPos.of(trackedEntity.entity);
- boolean flag = !Objects.equals(sectionPos, sectionPos1);
- if (flag) {
- trackedEntity.updatePlayers(list1);
- Entity entity = trackedEntity.entity;
- if (entity instanceof ServerPlayer) {
- list.add((ServerPlayer)entity);
- }
-
- trackedEntity.lastSectionPos = sectionPos1;
- }
-
- if (flag || this.distanceManager.inEntityTickingRange(sectionPos1.chunk().toLong())) {
- trackedEntity.serverEntity.sendChanges();
- }
- }
-
- if (!list.isEmpty()) {
- for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
- trackedEntity.updatePlayers(list);
- }
- }
+ // Folia - region threading
}
public void broadcast(Entity entity, Packet<?> packet) {
- ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId());
+ ChunkMap.TrackedEntity trackedEntity = entity.moonrise$getTrackedEntity(); // Folia - region threading
if (trackedEntity != null) {
trackedEntity.broadcast(packet);
}
}
protected void broadcastAndSend(Entity entity, Packet<?> packet) {
- ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId());
+ ChunkMap.TrackedEntity trackedEntity = entity.moonrise$getTrackedEntity(); // Folia - region threading
if (trackedEntity != null) {
trackedEntity.broadcastAndSend(packet);
}
@@ -1231,8 +1206,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
// Paper end - Configurable entity tracking range by Y
+ // Folia start - region threading
+ if (flag && (this.entity instanceof ServerPlayer thisEntity) && thisEntity.broadcastedDeath) {
+ flag = false;
+ }
+ // Folia end - region threading
// CraftBukkit start - respect vanish API
- if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits
+ if (flag && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !player.getBukkitEntity().canSee(this.entity.getBukkitEntity()))) { // Paper - only consider hits // Folia - region threading
flag = false;
}
// CraftBukkit end
diff --git a/net/minecraft/server/level/DistanceManager.java b/net/minecraft/server/level/DistanceManager.java
index 5eab6179ce3913cb4e4d424f910ba423faf21c85..338f9d047101619605cedab172358b4fd737af97 100644
--- a/net/minecraft/server/level/DistanceManager.java
+++ b/net/minecraft/server/level/DistanceManager.java
@@ -57,16 +57,16 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches
}
// Paper end - rewrite chunk system
// Paper start - chunk tick iteration optimisation
- private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<ServerPlayer> spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>();
+ // Folia - move to regionized world data
@Override
public final void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos) {
- this.spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE);
+ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading
}
@Override
public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) {
- this.spawnChunkTracker.remove(player);
+ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading
}
@Override
@@ -74,9 +74,9 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches
final SectionPos oldPos, final SectionPos newPos,
final boolean oldIgnore, final boolean newIgnore) {
if (newIgnore) {
- this.spawnChunkTracker.remove(player);
+ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading
} else {
- this.spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE);
+ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading
}
}
// Paper end - chunk tick iteration optimisation
@@ -208,15 +208,15 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches
}
public int getNaturalSpawnChunkCount() {
- return this.spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation
+ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation // Folia - region threading
}
public boolean hasPlayersNearby(long chunkPos) {
- return this.spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation
+ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation // Folia - region threading
}
public LongIterator getSpawnCandidateChunks() {
- return this.spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation
+ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation // Folia - region threading
}
public String getDebugStatus() {
diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
index 6540b2d6a1062d883811ce240c49d30d1925b291..548f5f0382c81ca86d238bfd7f94008bbd6e41bc 100644
--- a/net/minecraft/server/level/ServerChunkCache.java
+++ b/net/minecraft/server/level/ServerChunkCache.java
@@ -61,18 +61,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
public final ServerChunkCache.MainThreadExecutor mainThreadProcessor;
public final ChunkMap chunkMap;
private final DimensionDataStorage dataStorage;
- private long lastInhabitedUpdate;
+ //private long lastInhabitedUpdate; // Folia - region threading
public boolean spawnEnemies = true;
public boolean spawnFriendlies = true;
private static final int CACHE_SIZE = 4;
private final long[] lastChunkPos = new long[4];
private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4];
private final ChunkAccess[] lastChunk = new ChunkAccess[4];
- private final List<LevelChunk> tickingChunks = new ArrayList<>();
- private final Set<ChunkHolder> chunkHoldersToBroadcast = new ReferenceOpenHashSet<>();
- @Nullable
- @VisibleForDebug
- private NaturalSpawner.SpawnState lastSpawnState;
+ // Folia - moved to regionised world data
// Paper start
private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
public int getFullChunksCount() {
@@ -98,6 +94,11 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
}
private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) {
+ // Folia start - region threading
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Cannot asynchronously load chunks");
+ }
+ // Folia end - region threading
final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler();
final CompletableFuture<ChunkAccess> completable = new CompletableFuture<>();
chunkTaskScheduler.scheduleChunkLoad(
@@ -154,9 +155,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
// Paper start - chunk tick iteration optimisations
private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom shuffleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(0L);
private boolean isChunkNearPlayer(final ChunkMap chunkMap, final ChunkPos chunkPos, final LevelChunk levelChunk) {
- final ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkData chunkData = ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemChunkHolder)((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)levelChunk).moonrise$getChunkAndHolder().holder())
- .moonrise$getRealChunkHolder().holderData;
- final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk nearbyPlayers = chunkData.nearbyPlayers;
+ final ca.spottedleaf.moonrise.common.misc.NearbyPlayers.TrackedChunk nearbyPlayers = this.level.moonrise$getNearbyPlayers().getChunk(chunkPos); // Folia - region threading
if (nearbyPlayers == null) {
return false;
}
@@ -355,6 +354,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
}
public CompletableFuture<ChunkResult<ChunkAccess>> getChunkFuture(int x, int z, ChunkStatus chunkStatus, boolean requireChunk) {
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
boolean flag = Thread.currentThread() == this.mainThread;
CompletableFuture<ChunkResult<ChunkAccess>> chunkFutureMainThread;
if (flag) {
@@ -502,14 +502,15 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
}
private void tickChunks() {
- long gameTime = this.level.getGameTime();
- long l = gameTime - this.lastInhabitedUpdate;
- this.lastInhabitedUpdate = gameTime;
+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading
+ //long gameTime = this.level.getGameTime(); // Folia - region threading
+ long l = 1L; // Folia - region threading
+ //this.lastInhabitedUpdate = gameTime; // Folia - region threading
if (!this.level.isDebug()) {
ProfilerFiller profilerFiller = Profiler.get();
profilerFiller.push("pollingChunks");
if (this.level.tickRateManager().runsNormally()) {
- List<LevelChunk> list = this.tickingChunks;
+ List<LevelChunk> list = regionizedWorldData.temporaryChunkTickList; // Folia - region threading
try {
profilerFiller.push("filteringTickingChunks");
@@ -532,23 +533,24 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
}
private void broadcastChangedChunks(ProfilerFiller profiler) {
+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading
profiler.push("broadcast");
- for (ChunkHolder chunkHolder : this.chunkHoldersToBroadcast) {
+ for (ChunkHolder chunkHolder : regionizedWorldData.chunkHoldersToBroadcast) { // Folia - region threading - note: do not need to thread check, as getChunkToSend is only non-null when the chunkholder is loaded
LevelChunk tickingChunk = chunkHolder.getChunkToSend(); // Paper - rewrite chunk system
if (tickingChunk != null) {
chunkHolder.broadcastChanges(tickingChunk);
}
}
- this.chunkHoldersToBroadcast.clear();
+ regionizedWorldData.chunkHoldersToBroadcast.clear(); // Folia - region threading
profiler.pop();
}
private void collectTickingChunks(List<LevelChunk> output) {
// Paper start - chunk tick iteration optimisation
final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks =
- ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks();
+ this.level.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading
final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked();
final int size = tickingChunks.size();
@@ -569,13 +571,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
}
private void tickChunks(ProfilerFiller profiler, long timeInhabited, List<LevelChunk> chunks) {
+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading
profiler.popPush("naturalSpawnCount");
int naturalSpawnChunkCount = this.distanceManager.getNaturalSpawnChunkCount();
// Paper start - Optional per player mob spawns
NaturalSpawner.SpawnState spawnState;
if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled
// re-set mob counts
- for (ServerPlayer player : this.level.players) {
+ for (ServerPlayer player : this.level.getLocalPlayers()) { // Folia - region threading
// Paper start - per player mob spawning backoff
for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) {
player.mobCounts[ii] = 0;
@@ -588,26 +591,26 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
}
// Paper end - per player mob spawning backoff
}
- spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true);
+ spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all
} else {
- spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false);
+ spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); // Folia - region threading - note: function only cares about loaded entities, doesn't need all
}
// Paper end - Optional per player mob spawns
- this.lastSpawnState = spawnState;
+ regionizedWorldData.lastSpawnState = spawnState; // Folia - region threading
profiler.popPush("spawnAndTick");
- boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit
+ boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threading
int _int = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
List<MobCategory> filteredSpawningCategories;
if (_boolean && (this.spawnEnemies || this.spawnFriendlies)) {
// Paper start - PlayerNaturallySpawnCreaturesEvent
- for (ServerPlayer entityPlayer : this.level.players()) {
+ for (ServerPlayer entityPlayer : this.level.getLocalPlayers()) { // Folia - region threading
int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, entityPlayer.getBukkitEntity().getViewDistance());
chunkRange = Math.min(chunkRange, 8);
entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange);
entityPlayer.playerNaturallySpawnedEvent.callEvent();
}
// Paper end - PlayerNaturallySpawnCreaturesEvent
- boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit
+ boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getRedstoneGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit // Folia - region threading
filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag, this.level); // CraftBukkit
} else {
filteredSpawningCategories = List.of();
@@ -673,18 +676,23 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
int sectionPosZ = SectionPos.blockToSectionCoord(pos.getZ());
ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(ChunkPos.asLong(sectionPosX, sectionPosZ));
if (visibleChunkIfPresent != null && visibleChunkIfPresent.blockChanged(pos)) {
- this.chunkHoldersToBroadcast.add(visibleChunkIfPresent);
+ this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(visibleChunkIfPresent); // Folia - region threading
}
}
@Override
public void onLightUpdate(LightLayer type, SectionPos pos) {
- this.mainThreadProcessor.execute(() -> {
+ Runnable run = () -> { // Folia - region threading
ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(pos.chunk().toLong());
if (visibleChunkIfPresent != null && visibleChunkIfPresent.sectionLightChanged(type, pos.y())) {
- this.chunkHoldersToBroadcast.add(visibleChunkIfPresent);
+ this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(visibleChunkIfPresent); // Folia - region threading
}
- });
+ }; // Folia - region threading
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(
+ this.level, pos.getX(), pos.getZ(), run
+ );
+ // Folia end - region threading
}
public <T> void addRegionTicket(TicketType<T> type, ChunkPos pos, int distance, T value) {
@@ -766,7 +774,8 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
@Nullable
@VisibleForDebug
public NaturalSpawner.SpawnState getLastSpawnState() {
- return this.lastSpawnState;
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading
+ return worldData == null ? null : worldData.lastSpawnState; // Folia - region threading
}
public void removeTicketsOnClosing() {
@@ -775,7 +784,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
public void onChunkReadyToSend(ChunkHolder chunkHolder) {
if (chunkHolder.hasChangesToBroadcast()) {
- this.chunkHoldersToBroadcast.add(chunkHolder);
+ throw new UnsupportedOperationException(); // Folia - region threading
}
}
@@ -812,20 +821,76 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
return ServerChunkCache.this.mainThread;
}
+ // Folia start - region threading
+ @Override
+ public <V> CompletableFuture<V> submit(Supplier<V> task) {
+ if (true) {
+ throw new UnsupportedOperationException();
+ }
+ return super.submit(task);
+ }
+
+ @Override
+ public CompletableFuture<Void> 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<Player> getNearbyPlayers(TargetingConditions targetingConditions, LivingEntity source, AABB area) {
List<Player> 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
2025-03-08 06:39:17 -08:00
index d3c50acc5ca30b608825d4baff4b7e09a9e0f586..464bd9e968610ef9144f7dff1eead3db81be3caf 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -179,42 +179,40 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
private static final Logger LOGGER = LogUtils.getLogger();
private static final int EMPTY_TIME_NO_TICK = 300;
private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536;
- final List<ServerPlayer> players = Lists.newArrayList();
+ final List<ServerPlayer> players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - region threading
public final ServerChunkCache chunkSource;
private final MinecraftServer server;
public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type
private int lastSpawnChunkRadius;
- final EntityTickList entityTickList = new EntityTickList();
+ //final EntityTickList entityTickList = new EntityTickList(); // Folia - region threading
// Paper - rewrite chunk system
private final GameEventDispatcher gameEventDispatcher;
public boolean noSave;
private final SleepStatus sleepStatus;
private int emptyTime;
private final PortalForcer portalForcer;
- private final LevelTicks<Block> blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded);
- private final LevelTicks<Fluid> fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded);
- private final PathTypeCache pathTypesByPosCache = new PathTypeCache();
- final Set<Mob> navigatingMobs = new ObjectOpenHashSet<>();
+ //private final LevelTicks<Block> blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading
+ //private final LevelTicks<Fluid> fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading
+ //private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); // Folia - region threading
+ //final Set<Mob> navigatingMobs = new ObjectOpenHashSet<>(); // Folia - region threading
volatile boolean isUpdatingNavigations;
protected final Raids raids;
- private final ObjectLinkedOpenHashSet<BlockEventData> blockEvents = new ObjectLinkedOpenHashSet<>();
- private final List<BlockEventData> blockEventsToReschedule = new ArrayList<>(64);
- private boolean handlingTick;
+ //private final ObjectLinkedOpenHashSet<BlockEventData> blockEvents = new ObjectLinkedOpenHashSet<>(); // Folia - region threading
+ //private final List<BlockEventData> blockEventsToReschedule = new ArrayList<>(64); // Folia - region threading
+ //private boolean handlingTick; // Folia - region threading
private final List<CustomSpawner> customSpawners;
@Nullable
private EndDragonFight dragonFight;
- final Int2ObjectMap<EnderDragonPart> dragonParts = new Int2ObjectOpenHashMap<>();
+ final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.entity.boss.EnderDragonPart> dragonParts = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); // Folia - region threading
private final StructureManager structureManager;
private final StructureCheck structureCheck;
- private final boolean tickTime;
+ public final boolean tickTime; // Folia - region threading
private final RandomSequences randomSequences;
// CraftBukkit start
public final LevelStorageSource.LevelStorageAccess levelStorageAccess;
public final UUID uuid;
- public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent
- public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent
- private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current)
+ // Folia - region threading - move to regionised world data
public LevelChunk getChunkIfLoaded(int x, int z) {
return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately
@@ -242,6 +240,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
int minChunkZ = minBlockZ >> 4;
int maxChunkZ = maxBlockZ >> 4;
+ // Folia start - region threading
+ // don't let players move into regions not owned
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, minChunkX, minChunkZ, maxChunkX, maxChunkZ)) {
+ return false;
+ }
+ // Folia end - region threading
+
ServerChunkCache chunkProvider = this.getChunkSource();
for (int cx = minChunkX; cx <= maxChunkX; ++cx) {
@@ -297,11 +302,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler;
private long lastMidTickFailure;
private long tickedBlocksOrFluids;
- private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this);
- private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0];
- private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
- private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
- private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS);
+ // Folia - region threading - move to regionized data
@Override
public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) {
@@ -359,7 +360,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public final int moonrise$getRegionChunkShift() {
- return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift();
+ return this.regioniser.sectionChunkShift; // Folia - region threading
}
@Override
@@ -460,22 +461,22 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() {
- return this.nearbyPlayers;
+ return this.getCurrentWorldData().getNearbyPlayers(); // Folia - region threading
}
@Override
public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getLoadedChunks() {
- return this.loadedChunks;
+ return this.getCurrentWorldData().getChunks(); // Folia - region threading
}
@Override
public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getTickingChunks() {
- return this.tickingChunks;
+ return this.getCurrentWorldData().getTickingChunks(); // Folia - region threading
}
@Override
public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getEntityTickingChunks() {
- return this.entityTickingChunks;
+ return this.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading
}
@Override
@@ -495,80 +496,85 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// Paper end - rewrite chunk system
// Paper start - chunk tick iteration
private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0];
- private final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS);
- private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap();
+ // Folia - region threading
@Override
public final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.server.level.ServerChunkCache.ChunkAndHolder> moonrise$getPlayerTickingChunks() {
- return this.playerTickingChunks;
+ throw new UnsupportedOperationException(); // Folia - region threading
}
@Override
public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) {
- final ChunkPos pos = chunk.getPos();
- if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) {
- return;
- }
-
- this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder());
+ // Folia - region threading
}
@Override
public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) {
- this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder());
+ // Folia - region threading
}
@Override
public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) {
- ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async");
-
- final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ);
-
- if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) {
- // already added
- return;
- }
-
- final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler()
- .chunkHolderManager.getChunkHolder(chunkKey);
-
- if (chunkHolder == null || !chunkHolder.isTickingReady()) {
- return;
- }
-
- this.playerTickingChunks.add(
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder()
- );
+ // Folia - region threading
}
@Override
public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) {
- ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async");
+ // Folia - region threading
+ }
+ // Paper end - chunk tick iteration
+ // Folia start - region threading
+ public final io.papermc.paper.threadedregions.TickRegions tickRegions = new io.papermc.paper.threadedregions.TickRegions();
+ public final io.papermc.paper.threadedregions.ThreadedRegionizer<io.papermc.paper.threadedregions.TickRegions.TickRegionData, io.papermc.paper.threadedregions.TickRegions.TickRegionSectionData> regioniser;
+ {
+ this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegionizer<>(
+ (int)Math.max(1L, (8L * 16L * 16L) / (1L << (2 * (io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())))),
+ (1.0 / 6.0),
+ Math.max(1, 8 / (1 << io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())),
+ 1,
+ io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(),
+ this,
+ this.tickRegions
+ );
+ }
+ public final io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData(this);
+ public static final int WORLD_INIT_NOT_CHECKED = 0;
+ public static final int WORLD_INIT_CHECKING = 1;
+ public static final int WORLD_INIT_CHECKED = 2;
+ public final java.util.concurrent.atomic.AtomicInteger checkInitialised = new java.util.concurrent.atomic.AtomicInteger(WORLD_INIT_NOT_CHECKED);
+ public ChunkPos randomSpawnSelection;
- final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ);
- final int val = this.playerTickingRequests.addTo(chunkKey, -1);
+ public static final record PendingTeleport(Entity.EntityTreeNode rootVehicle, Vec3 to) {}
+ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<PendingTeleport> pendingTeleports = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>();
- if (val <= 0) {
- throw new IllegalStateException("Negative counter");
+ public void pushPendingTeleport(final PendingTeleport teleport) {
+ synchronized (this.pendingTeleports) {
+ this.pendingTeleports.add(teleport);
}
+ }
- if (val != 1) {
- // still has at least one request
- return;
+ public boolean removePendingTeleport(final PendingTeleport teleport) {
+ synchronized (this.pendingTeleports) {
+ return this.pendingTeleports.remove(teleport);
}
+ }
- final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler()
- .chunkHolderManager.getChunkHolder(chunkKey);
+ public List<PendingTeleport> removeAllRegionTeleports() {
+ final List<PendingTeleport> ret = new ArrayList<>();
- if (chunkHolder == null || !chunkHolder.isTickingReady()) {
- return;
+ synchronized (this.pendingTeleports) {
+ for (final java.util.Iterator<net.minecraft.server.level.ServerLevel.PendingTeleport> iterator = this.pendingTeleports.iterator(); iterator.hasNext(); ) {
+ final PendingTeleport pendingTeleport = iterator.next();
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, pendingTeleport.to())) {
+ ret.add(pendingTeleport);
+ iterator.remove();
+ }
+ }
}
- this.playerTickingChunks.remove(
- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder()
- );
+ return ret;
}
- // Paper end - chunk tick iteration
+ // Folia end - region threading
public ServerLevel(
MinecraftServer server,
@@ -633,7 +639,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
);
this.chunkSource.getGeneratorState().ensureStructuresGenerated();
this.portalForcer = new PortalForcer(this);
- this.updateSkyBrightness();
+ //this.updateSkyBrightness(); // Folia - region threading - delay until first tick
this.prepareWeather();
this.getWorldBorder().setAbsoluteMaxSize(server.getAbsoluteMaxWorldSize());
this.raids = this.getDataStorage().computeIfAbsent(Raids.factory(this), Raids.getFileId(this.dimensionTypeRegistration()));
@@ -681,7 +687,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler);
// Paper end - rewrite chunk system
this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit
+ this.updateTickData(); // Folia - region threading - make sure it is initialised before ticked
+ }
+
+ // Folia start - region threading
+ public void updateTickData() {
+ this.tickData = new io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData(this, this.serverLevelData.getGameTime(), this.serverLevelData.getDayTime());
}
+ // Folia end - region threading
// Paper start
@Override
@@ -709,61 +722,39 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
return this.getChunkSource().getGenerator().getBiomeSource().getNoiseBiome(x, y, z, this.getChunkSource().randomState().sampler());
}
+ @Override // Folia - region threading
public StructureManager structureManager() {
return this.structureManager;
}
- public void tick(BooleanSupplier hasTimeLeft) {
+ public void tick(BooleanSupplier hasTimeLeft, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking
+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking
ProfilerFiller profilerFiller = Profiler.get();
- this.handlingTick = true;
+ regionizedWorldData.setHandlingTick(true); // Folia - regionised ticking
TickRateManager tickRateManager = this.tickRateManager();
boolean runsNormally = tickRateManager.runsNormally();
if (runsNormally) {
profilerFiller.push("world border");
- this.getWorldBorder().tick();
+ //this.getWorldBorder().tick(); // Folia - regionised ticking
profilerFiller.popPush("weather");
- this.advanceWeatherCycle();
+ //this.advanceWeatherCycle(); // Folia - regionised ticking
profilerFiller.pop();
}
- int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE);
- if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) {
- // Paper start - create time skip event - move up calculations
- final long newDayTime = this.levelData.getDayTime() + 24000L;
- org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(
- this.getWorld(),
- org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP,
- (newDayTime - newDayTime % 24000L) - this.getDayTime()
- );
- // Paper end - create time skip event - move up calculations
- if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
- // Paper start - call time skip event if gamerule is enabled
- // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime
- // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param
- if (event.callEvent()) {
- this.setDayTime(this.getDayTime() + event.getSkipAmount());
- }
- // Paper end - call time skip event if gamerule is enabled
- }
-
- if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled
- if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) {
- this.resetWeatherCycle();
- }
- }
+ this.tickSleep(); // Folia - region threading - move into tickSleep
- this.updateSkyBrightness();
+ //this.updateSkyBrightness(); // Folia - region threading
if (runsNormally) {
this.tickTime();
}
profilerFiller.push("tickPending");
if (!this.isDebug() && runsNormally) {
- long l = this.getGameTime();
+ long l = regionizedWorldData.getRedstoneGameTime(); // Folia - region threading
profilerFiller.push("blockTicks");
- this.blockTicks.tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks
+ regionizedWorldData.getBlockLevelTicks().tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks // Folia - region ticking
profilerFiller.popPush("fluidTicks");
- this.fluidTicks.tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks
+ regionizedWorldData.getFluidLevelTicks().tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks // Folia - region ticking
profilerFiller.pop();
}
@@ -779,9 +770,9 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
this.runBlockEvents();
}
- this.handlingTick = false;
+ regionizedWorldData.setHandlingTick(false); // Folia - regionised ticking
profilerFiller.pop();
- boolean flag = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this
+ boolean flag = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this // Folia - unrestore this, we always need to tick empty worlds
if (flag) {
this.resetEmptyTime();
}
@@ -789,19 +780,29 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
if (flag || this.emptyTime++ < 300) {
profilerFiller.push("entities");
if (this.dragonFight != null && runsNormally) {
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, this.dragonFight.origin)) { // Folia - region threading
profilerFiller.push("dragonFight");
this.dragonFight.tick();
profilerFiller.pop();
+ } else { // Folia start - region threading
+ // try to load dragon fight
+ ChunkPos fightCenter = new ChunkPos(this.dragonFight.origin);
+ this.chunkSource.addTicketAtLevel(
+ TicketType.UNKNOWN, fightCenter, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
+ fightCenter
+ );
+ } // Folia end - region threading
}
io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR
- this.entityTickList
- .forEach(
+ regionizedWorldData // Folia - regionised ticking
+ .forEachTickingEntity( // Folia - regionised ticking
entity -> {
if (!entity.isRemoved()) {
if (!tickRateManager.isEntityFrozen(entity)) {
profilerFiller.push("checkDespawn");
entity.checkDespawn();
+ if (entity.isRemoved()) return; // Folia - region threading - if we despawned, DON'T TICK IT!
profilerFiller.pop();
if (true) { // Paper - rewrite chunk system
Entity vehicle = entity.getVehicle();
@@ -830,6 +831,36 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
profilerFiller.pop();
}
+ // Folia start - region threading
+ public void tickSleep() {
+ int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE);
+ if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) {
+ // Paper start - create time skip event - move up calculations
+ final long newDayTime = this.levelData.getDayTime() + 24000L;
+ org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(
+ this.getWorld(),
+ org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP,
+ (newDayTime - newDayTime % 24000L) - this.getDayTime()
+ );
+ // Paper end - create time skip event - move up calculations
+ if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
+ // Paper start - call time skip event if gamerule is enabled
+ // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime
+ // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param
+ if (event.callEvent()) {
+ this.setDayTime(this.getDayTime() + event.getSkipAmount());
+ }
+ // Paper end - call time skip event if gamerule is enabled
+ }
+
+ if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled
+ if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) {
+ this.resetWeatherCycle();
+ }
+ }
+ }
+ // Folia end - region threading
+
@Override
public boolean shouldTickBlocksAt(long chunkPos) {
// Paper start - rewrite chunk system
@@ -840,12 +871,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
protected void tickTime() {
if (this.tickTime) {
- long l = this.levelData.getGameTime() + 1L;
- this.serverLevelData.setGameTime(l);
+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading
+ long l = regionizedWorldData.getRedstoneGameTime() + 1L; // Folia - region threading
+ regionizedWorldData.setRedstoneGameTime(l); // Folia - region threading
Profiler.get().push("scheduledFunctions");
- this.serverLevelData.getScheduledEvents().tick(this.server, l);
+ //this.serverLevelData.getScheduledEvents().tick(this.server, l); // Folia - region threading - TODO any way to bring this in?
Profiler.get().pop();
- if (this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
+ if (false && this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { // Folia - region threading
this.setDayTime(this.levelData.getDayTime() + 1L);
}
}
@@ -863,16 +895,27 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
private void wakeUpAllPlayers() {
this.sleepStatus.removeAllSleepers();
- this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach(player -> player.stopSleepInBed(false, false));
+ // Folia start - region threading
+ this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach((ServerPlayer entityplayer) -> {
+ // Folia start - region threading
+ entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> {
+ if (player.level() != ServerLevel.this || !player.isSleeping()) {
+ return;
+ }
+ player.stopSleepInBed(false, false);
+ }, null, 1L);
+ }
+ );
+ // Folia end - region threading
}
// Paper start - optimise random ticking
- private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed());
+ private final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource.INSTANCE; // Folia - region threading
private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) {
final LevelChunkSection[] sections = chunk.getSections();
final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this);
- final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom;
+ final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Folia - region threading
final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294();
final ChunkPos cpos = chunk.getPos();
@@ -919,7 +962,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// Paper end - optimise random ticking
public void tickChunk(LevelChunk chunk, int randomTickSpeed) {
- final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking
+ final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Paper - optimise random ticking // Folia - region threading
ChunkPos pos = chunk.getPos();
boolean isRaining = this.isRaining();
int minBlockX = pos.getMinBlockX();
@@ -1044,7 +1087,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
public boolean isHandlingTick() {
- return this.handlingTick;
+ return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking
}
public boolean canSleepThroughNights() {
@@ -1070,6 +1113,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
public void updateSleepingPlayerList() {
+ // Folia start - region threading
+ if (!io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> {
+ ServerLevel.this.updateSleepingPlayerList();
+ });
+ return;
+ }
+ // Folia end - region threading
if (!this.players.isEmpty() && this.sleepStatus.update(this.players)) {
this.announceSleepStatus();
}
@@ -1080,7 +1131,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
return this.server.getScoreboard();
}
- private void advanceWeatherCycle() {
+ public void advanceWeatherCycle() { // Folia - region threading - public
boolean isRaining = this.isRaining();
if (this.dimensionType().hasSkyLight()) {
if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE)) {
@@ -1166,7 +1217,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, this.thunderLevel));
}
*/
- for (ServerPlayer player : this.players) {
+ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading
+ for (ServerPlayer player : players) { // Folia - region threading
if (player.level() == this) {
player.tickWeather();
}
@@ -1174,13 +1226,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
if (isRaining != this.isRaining()) {
// Only send weather packets to those affected
- for (ServerPlayer player : this.players) {
+ for (ServerPlayer player : players) { // Folia - region threading
if (player.level() == this) {
player.setPlayerWeather((!isRaining ? org.bukkit.WeatherType.DOWNFALL : org.bukkit.WeatherType.CLEAR), false);
}
}
}
- for (ServerPlayer player : this.players) {
+ for (ServerPlayer player : players) { // Folia - region threading
if (player.level() == this) {
player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel);
}
@@ -1241,13 +1293,10 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// Paper start - log detailed entity tick information
// TODO replace with varhandle
- static final java.util.concurrent.atomic.AtomicReference<Entity> currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>();
+ // Folia - region threading
public static List<Entity> getCurrentlyTickingEntities() {
- Entity ticking = currentlyTickingEntity.get();
- List<Entity> ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking });
-
- return ret;
+ throw new UnsupportedOperationException(); // Folia - region threading
}
// Paper end - log detailed entity tick information
@@ -1255,9 +1304,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// Paper start - log detailed entity tick information
ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main");
try {
- if (currentlyTickingEntity.get() == null) {
- currentlyTickingEntity.lazySet(entity);
- }
+ // Folia - region threading
// Paper end - log detailed entity tick information
entity.setOldPosAndRot();
ProfilerFiller profilerFiller = Profiler.get();
2025-02-16 12:23:20 -08:00
@@ -1268,7 +1315,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
final boolean isActive = io.papermc.paper.entity.activation.ActivationRange.checkIfActive(entity); // Paper - EAR 2
if (isActive) { // Paper - EAR 2
entity.tick();
- entity.postTick(); // CraftBukkit
+ // Folia start - region threading
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) {
+ // removed from region while ticking
+ return;
+ }
+ if (entity.handlePortal()) {
+ // portalled
+ return;
+ }
+ // Folia end - region threading
} else {entity.inactiveTick();} // Paper - EAR 2
profilerFiller.pop();
2025-02-16 12:23:20 -08:00
@@ -1277,9 +1333,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
// Paper start - log detailed entity tick information
} finally {
- if (currentlyTickingEntity.get() == entity) {
- currentlyTickingEntity.lazySet(null);
- }
+ // Folia - region threading
}
// Paper end - log detailed entity tick information
}
2025-02-16 12:23:20 -08:00
@@ -1287,7 +1341,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
private void tickPassenger(Entity ridingEntity, Entity passengerEntity, final boolean isActive) { // Paper - EAR 2
if (passengerEntity.isRemoved() || passengerEntity.getVehicle() != ridingEntity) {
passengerEntity.stopRiding();
- } else if (passengerEntity instanceof Player || this.entityTickList.contains(passengerEntity)) {
+ } else if (passengerEntity instanceof Player || this.getCurrentWorldData().hasEntityTickingEntity(passengerEntity)) { // Folia - region threading
passengerEntity.setOldPosAndRot();
passengerEntity.tickCount++;
2025-02-16 12:23:20 -08:00
passengerEntity.totalEntityAge++; // Paper - age-like counter for all entities
@@ -1297,7 +1351,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// Paper start - EAR 2
if (isActive) {
passengerEntity.rideTick();
- passengerEntity.postTick(); // CraftBukkit
+ // Folia start - region threading
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(passengerEntity)) {
+ // removed from region while ticking
+ return;
+ }
+ if (passengerEntity.handlePortal()) {
+ // portalled
+ return;
+ }
+ // Folia end - region threading
} else {
passengerEntity.setDeltaMovement(Vec3.ZERO);
passengerEntity.inactiveTick();
2025-02-16 12:23:20 -08:00
@@ -1371,19 +1434,20 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
// Paper end - add close param
- // CraftBukkit start - moved from MinecraftServer.saveChunks
- ServerLevel worldserver1 = this;
-
- this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings());
- this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess()));
- this.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
- // CraftBukkit end
+ // Folia - move into saveLevelData
}
- private void saveLevelData(boolean join) {
+ public void saveLevelData(boolean join) { // Folia - public
if (this.dragonFight != null) {
this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit
}
+ // Folia start - moved into saveLevelData
+ ServerLevel worldserver1 = this;
+
+ this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings());
+ this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess()));
+ this.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData());
+ // Folia end - moved into saveLevelData
DimensionDataStorage dataStorage = this.getChunkSource().getDataStorage();
if (join) {
2025-02-16 12:23:20 -08:00
@@ -1439,6 +1503,19 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
return list;
}
+ // Folia start - region threading
+ @Nullable
+ public ServerPlayer getRandomLocalPlayer() {
+ List<ServerPlayer> 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<ServerPlayer> players = this.getPlayers(LivingEntity::isAlive);
2025-02-16 12:23:20 -08:00
@@ -1520,8 +1597,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
} else {
if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added
// Paper start - capture all item additions to the world
- if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) {
- captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity);
+ if (this.getCurrentWorldData().captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { // Folia - region threading
+ this.getCurrentWorldData().captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); // Folia - region threading
return true;
}
// Paper end - capture all item additions to the world
2025-02-16 12:23:20 -08:00
@@ -1696,13 +1773,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) {
- if (this.isUpdatingNavigations) {
+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading
+ if (false && this.isUpdatingNavigations) { // Folia - region threading
String string = "recursive call to sendBlockUpdated";
Util.logAndPauseIfInIde("recursive call to sendBlockUpdated", new IllegalStateException("recursive call to sendBlockUpdated"));
}
this.getChunkSource().blockChanged(pos);
- this.pathTypesByPosCache.invalidate(pos);
+ regionizedWorldData.pathTypesByPosCache.invalidate(pos); // Folia - region threading
if (this.paperConfig().misc.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates
VoxelShape collisionShape = oldState.getCollisionShape(this, pos);
VoxelShape collisionShape1 = newState.getCollisionShape(this, pos);
2025-02-16 12:23:20 -08:00
@@ -1710,7 +1788,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
List<PathNavigation> list = new ObjectArrayList<>();
try { // Paper - catch CME see below why
- for (Mob mob : this.navigatingMobs) {
+ for (java.util.Iterator<Mob> 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);
2025-02-16 12:23:20 -08:00
@@ -1727,13 +1806,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// Paper end - catch CME see below why
try {
- this.isUpdatingNavigations = true;
+ //this.isUpdatingNavigations = true; // Folia - region threading
for (PathNavigation pathNavigation : list) {
pathNavigation.recomputePath();
}
} finally {
- this.isUpdatingNavigations = false;
+ //this.isUpdatingNavigations = false; // Folia - region threading
}
}
} // Paper - option to disable pathfinding updates
2025-02-16 12:23:20 -08:00
@@ -1741,29 +1820,29 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public void updateNeighborsAt(BlockPos pos, Block block) {
- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
+ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading
this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, null, null));
}
@Override
public void updateNeighborsAt(BlockPos pos, Block block, @Nullable Orientation orientation) {
- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement
- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation);
+ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading
+ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation); // Folia - region threading
}
@Override
public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block block, Direction facing, @Nullable Orientation orientation) {
- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, facing, orientation);
+ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, facing, orientation); // Folia - region threading
}
@Override
public void neighborChanged(BlockPos pos, Block block, @Nullable Orientation orientation) {
- this.neighborUpdater.neighborChanged(pos, block, orientation);
+ this.getCurrentWorldData().neighborUpdater.neighborChanged(pos, block, orientation); // Folia - region threading
}
@Override
public void neighborChanged(BlockState state, BlockPos pos, Block block, @Nullable Orientation orientation, boolean movedByPiston) {
- this.neighborUpdater.neighborChanged(state, pos, block, orientation, movedByPiston);
+ this.getCurrentWorldData().neighborUpdater.neighborChanged(state, pos, block, orientation, movedByPiston); // Folia - region threading
}
@Override
2025-02-16 12:23:20 -08:00
@@ -1853,7 +1932,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// CraftBukkit end
ParticleOptions particleOptions = serverExplosion.isSmall() ? smallExplosionParticles : largeExplosionParticles;
- for (ServerPlayer serverPlayer : this.players) {
+ for (ServerPlayer serverPlayer : this.getLocalPlayers()) { // Folia - region thraeding
if (serverPlayer.distanceToSqr(vec3) < 4096.0) {
Optional<Vec3> optional = Optional.ofNullable(serverExplosion.getHitPlayers().get(serverPlayer));
serverPlayer.connection.send(new ClientboundExplodePacket(vec3, optional, particleOptions, explosionSound));
2025-02-16 12:23:20 -08:00
@@ -1869,14 +1948,17 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public void blockEvent(BlockPos pos, Block block, int eventID, int eventParam) {
- this.blockEvents.add(new BlockEventData(pos, block, eventID, eventParam));
+ this.getCurrentWorldData().pushBlockEvent(new BlockEventData(pos, block, eventID, eventParam)); // Folia - regionised ticking
}
private void runBlockEvents() {
- this.blockEventsToReschedule.clear();
+ List<BlockEventData> 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
2025-02-16 12:23:20 -08:00
@@ -1892,11 +1974,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
);
}
} else {
- this.blockEventsToReschedule.add(blockEventData);
+ blockEventsToReschedule.add(blockEventData); // Folia - regionised ticking
}
}
- this.blockEvents.addAll(this.blockEventsToReschedule);
+ worldRegionData.pushBlockEvents(blockEventsToReschedule); // Folia - regionised ticking
}
private boolean doBlockEvent(BlockEventData event) {
2025-02-16 12:23:20 -08:00
@@ -1906,12 +1988,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public LevelTicks<Block> getBlockTicks() {
- return this.blockTicks;
+ return this.getCurrentWorldData().getBlockLevelTicks(); // Folia - region ticking
}
@Override
public LevelTicks<Fluid> getFluidTicks() {
- return this.fluidTicks;
+ return this.getCurrentWorldData().getFluidLevelTicks(); // Folia - region ticking
}
@Nonnull
2025-02-16 12:23:20 -08:00
@@ -1964,7 +2046,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
double zOffset,
double speed
) {
- return sendParticlesSource(this.players, sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed);
+ return sendParticlesSource(this.getLocalPlayers(), sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed); // Folia - region threading
}
public <T extends ParticleOptions> int sendParticlesSource(
List<ServerPlayer> receivers,
2025-02-16 12:23:20 -08:00
@@ -2047,12 +2129,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Nullable
public Entity getEntityOrPart(int id) {
Entity entity = this.getEntities().get(id);
- return entity != null ? entity : this.dragonParts.get(id);
+ return entity != null ? entity : this.dragonParts.get((long)id); // Folia - diff on change
}
@Override
public Collection<EnderDragonPart> dragonParts() {
- return this.dragonParts.values();
+ return this.dragonParts.values(); // Folia - diff on change
}
@Nullable
2025-02-16 12:23:20 -08:00
@@ -2107,6 +2189,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// Paper start - Call missing map initialize event and set id
final DimensionDataStorage storage = this.getServer().overworld().getDataStorage();
+ synchronized (storage.cache) { // Folia - region threading
final Optional<net.minecraft.world.level.saveddata.SavedData> cacheEntry = storage.cache.get(mapId.key());
if (cacheEntry == null) { // Cache did not contain, try to load and may init
final MapItemSavedData mapData = storage.get(MapItemSavedData.factory(), mapId.key()); // get populates the cache
2025-02-16 12:23:20 -08:00
@@ -2126,6 +2209,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
return null;
+ } // Folia - region threading
// Paper end - Call missing map initialize event and set id
}
2025-02-16 12:23:20 -08:00
@@ -2180,6 +2264,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
public boolean setChunkForced(int chunkX, int chunkZ, boolean add) {
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify force loaded chunks off of the global region"); // Folia - region threading
ForcedChunksSavedData forcedChunksSavedData = this.getDataStorage().computeIfAbsent(ForcedChunksSavedData.factory(), "chunks");
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
long packedChunkPos = chunkPos.toLong();
2025-02-16 12:23:20 -08:00
@@ -2187,7 +2272,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
if (add) {
flag = forcedChunksSavedData.getChunks().add(packedChunkPos);
if (flag) {
- this.getChunk(chunkX, chunkZ);
+ //this.getChunk(chunkX, chunkZ); // Folia - region threading - we must let the chunk load asynchronously
}
} else {
flag = forcedChunksSavedData.getChunks().remove(packedChunkPos);
2025-02-16 12:23:20 -08:00
@@ -2212,11 +2297,24 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
Optional<Holder<PoiType>> optional1 = PoiTypes.forState(newState);
if (!Objects.equals(optional, optional1)) {
BlockPos blockPos = pos.immutable();
- optional.ifPresent(poiType -> this.getServer().execute(() -> {
+ // Folia start - region threading
+ optional.ifPresent(poiType -> {
+ Runnable run = () -> {
+ // Folia end - region threading
this.getPoiManager().remove(blockPos);
DebugPackets.sendPoiRemovedPacket(this, blockPos);
- }));
- optional1.ifPresent(poiType -> this.getServer().execute(() -> {
+ // Folia start - region threading
+ };
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(
+ this, blockPos.getX() >> 4, blockPos.getZ() >> 4, run
+ );
+ });
+ // Folia end - region threading
+ // Folia start - region threading
+ optional1.ifPresent(poiType -> {
+ Runnable run = () -> {
+ // Folia end - region threading
// Paper start - Remove stale POIs
if (optional.isEmpty() && this.getPoiManager().exists(blockPos, ignored -> true)) {
this.getPoiManager().remove(blockPos);
2025-02-16 12:23:20 -08:00
@@ -2224,7 +2322,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// Paper end - Remove stale POIs
this.getPoiManager().add(blockPos, (Holder<PoiType>)poiType);
DebugPackets.sendPoiAddedPacket(this, blockPos);
- }));
+ // Folia start - region threading
+ };
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(
+ this, blockPos.getX() >> 4, blockPos.getZ() >> 4, run
+ );
+ // Folia end - region threading
+ });
+ // Folia end - region threading
}
}
2025-02-16 12:23:20 -08:00
@@ -2278,7 +2384,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
bufferedWriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system
- bufferedWriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size()));
+ //bufferedWriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); // Folia - region threading
bufferedWriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count()));
bufferedWriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count()));
bufferedWriter.write("distance_manager: " + chunkMap.getDistanceManager().getDebugStatus() + "\n");
2025-02-16 12:23:20 -08:00
@@ -2348,7 +2454,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
private void dumpBlockEntityTickers(Writer output) throws IOException {
CsvOutput csvOutput = CsvOutput.builder().addColumn("x").addColumn("y").addColumn("z").addColumn("type").build(output);
- for (TickingBlockEntity tickingBlockEntity : this.blockEntityTickers) {
+ for (TickingBlockEntity tickingBlockEntity : (Iterable<? extends net.minecraft.world.level.block.entity.TickingBlockEntity>)null) { // Folia - region threading
BlockPos pos = tickingBlockEntity.getPos();
csvOutput.writeRow(pos.getX(), pos.getY(), pos.getZ(), tickingBlockEntity.getType());
}
2025-02-16 12:23:20 -08:00
@@ -2356,14 +2462,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@VisibleForTesting
public void clearBlockEvents(BoundingBox boundingBox) {
- this.blockEvents.removeIf(blockEventData -> boundingBox.isInside(blockEventData.pos()));
+ this.getCurrentWorldData().removeIfBlockEvents(blockEventData -> boundingBox.isInside(blockEventData.pos())); // Folia - regionised ticking
}
@Override
public void blockUpdated(BlockPos pos, Block block) {
if (!this.isDebug()) {
// CraftBukkit start
- if (this.populating) {
+ if (this.getCurrentWorldData().populating) { // Folia - region threading
return;
}
// CraftBukkit end
2025-02-16 12:23:20 -08:00
@@ -2412,8 +2518,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
this.players.size(),
this.moonrise$getEntityLookup().getDebugInfo(), // Paper - rewrite chunk system
getTypeCount(this.moonrise$getEntityLookup().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()), // Paper - rewrite chunk system
- this.blockEntityTickers.size(),
- getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType),
+ 0, // Folia - region threading
+ "null", // Folia - region threading
this.getBlockTicks().count(),
this.getFluidTicks().count(),
this.gatherChunkSourceStats()
2025-02-16 12:23:20 -08:00
@@ -2465,15 +2571,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
}
public void startTickingChunk(LevelChunk chunk) {
- chunk.unpackTicks(this.getLevelData().getGameTime());
+ chunk.unpackTicks(this.getRedstoneGameTime()); // Folia - region threading
}
public void onStructureStartsAvailable(ChunkAccess chunk) {
- this.server.execute(() -> this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()));
+ this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); // Folia - region threading
}
public PathTypeCache getPathTypeCache() {
- return this.pathTypesByPosCache;
+ return this.getCurrentWorldData().pathTypesByPosCache; // Folia - region threading
}
@Override
2025-02-16 12:23:20 -08:00
@@ -2491,7 +2597,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system
}
- private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) {
+ public boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { // Folia - region threaded - make public
// Paper start - rewrite chunk system
final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos);
// isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded
2025-02-16 12:23:20 -08:00
@@ -2583,7 +2689,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
// Paper start - optimize redstone (Alternate Current)
@Override
public alternate.current.wire.WireHandler getWireHandler() {
- return wireHandler;
+ return this.getCurrentWorldData().wireHandler; // Folia - region threading
}
// Paper end - optimize redstone (Alternate Current)
2025-03-08 06:39:17 -08:00
@@ -2595,18 +2701,18 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public void onDestroyed(Entity entity) {
- ServerLevel.this.getScoreboard().entityRemoved(entity);
+ // ServerLevel.this.getScoreboard().entityRemoved(entity); // Folia - region threading
}
@Override
public void onTickingStart(Entity entity) {
if (entity instanceof net.minecraft.world.entity.Marker && !paperConfig().entities.markers.tick) return; // Paper - Configurable marker ticking
- ServerLevel.this.entityTickList.add(entity);
+ ServerLevel.this.getCurrentWorldData().addEntityTickingEntity(entity); // Folia - region threading
}
@Override
public void onTickingEnd(Entity entity) {
- ServerLevel.this.entityTickList.remove(entity);
+ ServerLevel.this.getCurrentWorldData().removeEntityTickingEntity(entity); // Folia - region threading
// Paper start - Reset pearls when they stop being ticked
if (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) {
pearl.cachedOwner = null;
2025-03-08 06:39:17 -08:00
@@ -2618,6 +2724,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public void onTrackingStart(Entity entity) {
org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot
+ ServerLevel.this.getCurrentWorldData().addLoadedEntity(entity); // Folia - region threading
// ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server; moved down below valid=true
if (entity instanceof ServerPlayer serverPlayer) {
ServerLevel.this.players.add(serverPlayer);
2025-03-08 06:39:17 -08:00
@@ -2632,12 +2739,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
);
}
- ServerLevel.this.navigatingMobs.add(mob);
+ ServerLevel.this.getCurrentWorldData().addNavigatingMob(mob); // Folia - region threading
}
if (entity instanceof EnderDragon enderDragon) {
for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) {
- ServerLevel.this.dragonParts.put(enderDragonPart.getId(), enderDragonPart);
+ ServerLevel.this.dragonParts.put((long)enderDragonPart.getId(), enderDragonPart); // Folia - diff on change
}
}
2025-03-08 06:39:17 -08:00
@@ -2660,18 +2767,27 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
@Override
public void onTrackingEnd(Entity entity) {
org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot
+ ServerLevel.this.getCurrentWorldData().removeLoadedEntity(entity); // Folia - region threading
// Spigot start // TODO I don't think this is needed anymore
if (entity instanceof Player player) {
for (final ServerLevel level : ServerLevel.this.getServer().getAllLevels()) {
- for (final Optional<net.minecraft.world.level.saveddata.SavedData> savedData : level.getDataStorage().cache.values()) {
+ // Folia start - make map data thread-safe
+ List<Optional<net.minecraft.world.level.saveddata.SavedData>> worldDataCache;
+ synchronized (level.getDataStorage().cache) {
+ worldDataCache = new java.util.ArrayList<>(level.getDataStorage().cache.values());
+ }
+ for (final Optional<net.minecraft.world.level.saveddata.SavedData> 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
}
}
}
2025-03-08 06:39:17 -08:00
@@ -2702,18 +2818,19 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
);
}
- ServerLevel.this.navigatingMobs.remove(mob);
+ ServerLevel.this.getCurrentWorldData().removeNavigatingMob(mob); // Folia - region threading
}
if (entity instanceof EnderDragon enderDragon) {
for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) {
- ServerLevel.this.dragonParts.remove(enderDragonPart.getId());
+ ServerLevel.this.dragonParts.remove((long)enderDragonPart.getId()); // Folia - diff on change
}
}
entity.updateDynamicGameEventListener(DynamicGameEventListener::remove);
// CraftBukkit start
entity.valid = false;
+ // Folia - region threading - TODO THIS SHIT
if (!(entity instanceof ServerPlayer)) {
for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players
player.getBukkitEntity().onEntityRemove(entity);
2025-03-08 06:39:17 -08:00
@@ -2741,11 +2858,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe
private long lagCompensationTick = MinecraftServer.SERVER_INIT;
public long getLagCompensationTick() {
- return this.lagCompensationTick;
+ return this.getCurrentWorldData().getLagCompensationTick(); // Folia - region threading
}
public void updateLagCompensationTick() {
- this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L));
+ throw new UnsupportedOperationException(); // Folia - region threading
}
// Paper end - lag compensation
}
diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
index 57d432dc9e8d8e9a3e088e7c40b35178c30fe786..f5615c7f7127edda460db9158d6bd4ddad9193f7 100644
--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -180,7 +180,7 @@ import org.slf4j.Logger;
public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system
private static final Logger LOGGER = LogUtils.getLogger();
- public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving
+ public static final long LAST_SAVE_ABSENT = Long.MIN_VALUE; public long lastSave = LAST_SAVE_ABSENT; // Paper // Folia - threaded regions - changed to nanoTime
private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32;
private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10;
private static final int FLY_STAT_RECORDING_SPEED = 25;
@@ -443,8 +443,149 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
this.maxHealthCache = this.getMaxHealth();
}
+ // Folia start - region threading
+ private static final int SPAWN_RADIUS_SELECTION_SEARCH = 5;
+
+ private static BlockPos getRandomSpawn(ServerLevel world, RandomSource random) {
+ BlockPos spawn = world.getSharedSpawnPos();
+ double radius = (double)Math.max(0, world.getGameRules().getInt(GameRules.RULE_SPAWN_RADIUS));
+
+ double spawnX = (double)spawn.getX() + 0.5;
+ double spawnZ = (double)spawn.getZ() + 0.5;
+
+ net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder();
+
+ double selectMinX = Math.max(worldBorder.getMinX() + 1.0, spawnX - radius);
+ double selectMinZ = Math.max(worldBorder.getMinZ() + 1.0, spawnZ - radius);
+ double selectMaxX = Math.min(worldBorder.getMaxX() - 1.0, spawnX + radius);
+ double selectMaxZ = Math.min(worldBorder.getMaxZ() - 1.0, spawnZ + radius);
+
+ double amountX = selectMaxX - selectMinX;
+ double amountZ = selectMaxZ - selectMinZ;
+
+ int selectX = amountX < 1.0 ? Mth.floor(worldBorder.getCenterX()) : (int)Mth.floor((amountX + 1.0) * random.nextDouble() + selectMinX);
+ int selectZ = amountZ < 1.0 ? Mth.floor(worldBorder.getCenterZ()) : (int)Mth.floor((amountZ + 1.0) * random.nextDouble() + selectMinZ);
+
+ return new BlockPos(selectX, 0, selectZ);
+ }
+
+ private static void completeSpawn(ServerLevel world, BlockPos selected,
+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable<org.bukkit.Location> 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<org.bukkit.Location> 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<org.bukkit.Location> 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<org.bukkit.Location> 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<org.bukkit.Location> toComplete) { // Folia - region threading
+ BlockPos blockposition = world.getSharedSpawnPos();
+
+ if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit
+ selectSpawn(world, player, player.random, new int[1], 500, toComplete);
+ } else {
+ selectSpawnWithoutRadius(world, player, blockposition, toComplete);
+ }
+
+ }
+ // Folia end - region threading
+
@Override
public BlockPos adjustSpawnLocation(ServerLevel level, BlockPos pos) {
+ // Folia start - region threading
+ if (true) {
+ throw new UnsupportedOperationException();
+ }
+ // Folia end - region threading
AABB aabb = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO);
BlockPos blockPos = pos;
if (level.dimensionType().hasSkyLight() && level.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit
@@ -533,7 +674,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
this.getBukkitEntity().readExtraData(compound); // CraftBukkit
if (this.isSleeping()) {
- this.stopSleeping();
+ this.stopSleepingRaw(); // Folia - do not modify or read worldstate during data deserialization
}
// CraftBukkit start
@@ -709,10 +850,17 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
ServerLevel level = this.level().getServer().getLevel(optional.get());
if (level != null) {
Entity entity = EntityType.loadEntityRecursive(
- compoundTag, level, EntitySpawnReason.LOAD, entity1 -> !level.addWithUUID(entity1) ? null : entity1
+ compoundTag, level, EntitySpawnReason.LOAD, entity1 -> entity1 // Folia - region threading - delay world add
);
if (entity != null) {
- placeEnderPearlTicket(level, entity.chunkPosition());
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
+ level, entity.chunkPosition().x, entity.chunkPosition().z, () -> {
+ level.addFreshEntityWithPassengers(entity);
+ ServerPlayer.placeEnderPearlTicket(level, entity.chunkPosition());
+ }
+ );
+ // Folia end - region threading
} else {
LOGGER.warn("Failed to spawn player ender pearl in level ({}), skipping", optional.get());
}
@@ -817,12 +965,23 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
Entity camera = this.getCamera();
if (camera != this) {
- if (camera.isAlive()) {
+ if (camera.canBeSpectated()) { // Folia - region threading - replace removed check
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(camera) && !camera.isRemoved()) { // Folia - region threading
this.absMoveTo(camera.getX(), camera.getY(), camera.getZ(), camera.getYRot(), camera.getXRot());
this.serverLevel().getChunkSource().move(this);
if (this.wantsToStopRiding()) {
this.setCamera(this);
}
+ } else { // Folia start - region threading
+ Entity realCamera = camera.getBukkitEntity().getHandleRaw();
+ if (realCamera != camera) {
+ this.setCamera(this);
+ this.setCamera(realCamera);
+ } else {
+ this.teleportToCameraOffRegion();
+ }
+ }
+ // Folia end - region threading
} else {
this.setCamera(this);
}
@@ -1357,9 +1516,332 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
}
}
+ // Folia start - region threading
+ /**
+ * Teleport flag indicating that the player is to be respawned, expected to only be used
+ * internally for {@link #respawn(java.util.function.Consumer, PlayerRespawnEvent.RespawnReason)}
+ */
+ public static final long TELEPORT_FLAGS_PLAYER_RESPAWN = Long.MIN_VALUE >>> 0;
+
+ public void exitEndCredits() {
+ if (!this.wonGame) {
+ // not in the end credits anymore
+ return;
+ }
+ this.wonGame = false;
+
+ this.respawn((player) -> {
+ CriteriaTriggers.CHANGED_DIMENSION.trigger(player, Level.END, Level.OVERWORLD);
+ }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.END_PORTAL, true);
+ }
+
+ public void respawn(java.util.function.Consumer<ServerPlayer> respawnComplete, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason reason) {
+ this.respawn(respawnComplete, reason, false);
+ }
+
+ private void respawn(java.util.function.Consumer<ServerPlayer> respawnComplete, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason reason, boolean alive) {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot respawn entity async");
+
+ this.getBukkitEntity(); // force bukkit entity to be created before TPing
+
+ if (alive != this.isAlive()) {
+ throw new IllegalStateException("isAlive expected = " + alive);
+ }
+
+ if (!this.hasNullCallback()) {
+ this.unRide();
+ }
+
+ if (this.isVehicle() || this.isPassenger()) {
+ throw new IllegalStateException("Dead player should not be a vehicle or passenger");
+ }
+
+ ServerLevel origin = this.serverLevel();
+ ServerLevel respawnWorld = this.server.getLevel(this.getRespawnDimension());
+
+ // modified based off PlayerList#respawn
+
+ EntityTreeNode passengerTree = this.makePassengerTree();
+
+ this.isChangingDimension = true;
+ origin.removePlayerImmediately(this, RemovalReason.CHANGED_DIMENSION);
+ // reset player if needed, only after removal from world
+ if (!alive) {
+ ServerPlayer.this.reset();
+ }
+ // must be manually removed from connections, delay until after reset() so that we do not trip any thread checks
+ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection);
+
+ BlockPos respawnPos = this.getRespawnPosition();
+ float respawnAngle = this.getRespawnAngle();
+ boolean isRespawnForced = this.isRespawnForced();
+
+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable<org.bukkit.Location> spawnPosComplete =
+ new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>();
+ boolean[] usedRespawnAnchor = new boolean[1];
+
+ // set up post spawn location logic
+ spawnPosComplete.addWaiter((spawnLoc, throwable) -> {
+ // update pos and velocity
+ ServerPlayer.this.setPosRaw(spawnLoc.getX(), spawnLoc.getY(), spawnLoc.getZ());
+ ServerPlayer.this.setYRot(spawnLoc.getYaw());
+ ServerPlayer.this.setYHeadRot(spawnLoc.getYaw());
+ ServerPlayer.this.setXRot(spawnLoc.getPitch());
+ ServerPlayer.this.setDeltaMovement(Vec3.ZERO);
+ // placeInAsync will update the world
+
+ this.placeInAsync(
+ origin,
+ // use the load chunk flag just in case the spawn loc isn't loaded, and to ensure the chunks
+ // stay loaded for a bit with the teleport ticket
+ ((org.bukkit.craftbukkit.CraftWorld)spawnLoc.getWorld()).getHandle(),
+ TELEPORT_FLAG_LOAD_CHUNK | TELEPORT_FLAGS_PLAYER_RESPAWN,
+ passengerTree, // note: we expect this to just be the player, no passengers
+ (entity) -> {
+ // now the player is in the world, and can receive sound
+ if (usedRespawnAnchor[0]) {
+ ServerPlayer.this.connection.send(
+ new ClientboundSoundPacket(
+ net.minecraft.sounds.SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS,
+ ServerPlayer.this.getX(), ServerPlayer.this.getY(), ServerPlayer.this.getZ(),
+ 1.0F, 1.0F, ServerPlayer.this.serverLevel().getRandom().nextLong()
+ )
+ );
+ }
+ // now the respawn logic is complete
+
+ // last, call the function callback
+ if (respawnComplete != null) {
+ respawnComplete.accept(ServerPlayer.this);
+ }
+ }
+ );
+ });
+
+ // find and modify respawn block state
+ if (respawnWorld == null || respawnPos == null) {
+ // default to regular spawn
+ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete);
+ } else {
+ // load chunk for block
+ // give at least 1 radius of loaded chunks so that we do not sync load anything
+ int radiusBlocks = 16;
+ respawnWorld.moonrise$loadChunksAsync(respawnPos, radiusBlocks,
+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER,
+ (chunks) -> {
+ ServerPlayer.RespawnPosAngle spawnPos = ServerPlayer.findRespawnAndUseSpawnBlock(
+ respawnWorld, respawnPos, respawnAngle, isRespawnForced, !alive
+ ).orElse(null);
+ if (spawnPos == null) {
+ // no spawn
+ ServerPlayer.this.connection.send(
+ new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F)
+ );
+ ServerPlayer.this.setRespawnPosition(
+ null, null, 0f, false, false,
+ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN
+ );
+ // default to regular spawn
+ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete);
+ return;
+ }
+
+ boolean isRespawnAnchor = respawnWorld.getBlockState(respawnPos).is(net.minecraft.world.level.block.Blocks.RESPAWN_ANCHOR);
+ boolean isBed = respawnWorld.getBlockState(respawnPos).is(net.minecraft.tags.BlockTags.BEDS);
+ usedRespawnAnchor[0] = !alive && isRespawnAnchor;
+
+ ServerPlayer.this.setRespawnPosition(
+ respawnWorld.dimension(), respawnPos, respawnAngle, isRespawnForced, false,
+ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN
+ );
+
+ // finished now, pass the location on
+ spawnPosComplete.complete(
+ io.papermc.paper.util.MCUtil.toLocation(respawnWorld, spawnPos.position(), spawnPos.yaw(), 0.0f)
+ );
+ return;
+ }
+ );
+ }
+ }
+
+ @Override
+ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) {
+ if (yaw != null) {
+ this.setYRot(yaw.floatValue());
+ this.setYHeadRot(yaw.floatValue());
+ }
+ if (pitch != null) {
+ this.setXRot(pitch.floatValue());
+ }
+ if (velocity != null) {
+ this.setDeltaMovement(velocity);
+ }
+ this.connection.internalTeleport(
+ new net.minecraft.world.entity.PositionMoveRotation(
+ pos, this.getDeltaMovement(), this.getYRot(), this.getXRot()
+ ),
+ java.util.Collections.emptySet()
+ );
+ this.connection.resetPosition();
+ this.setOldPosAndRot();
+ this.resetStoredPositions();
+ }
+
+ @Override
+ protected ServerPlayer transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) {
+ // must be manually removed from connections
+ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection);
+ this.serverLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION);
+
+ this.spawnIn(destination);
+ this.transform(pos, yaw, pitch, velocity);
+
+ return this;
+ }
+
+ @Override
+ public void preChangeDimension() {
+ super.preChangeDimension();
+ this.stopUsingItem();
+ }
+
+ @Override
+ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) {
+ if (destination == originWorld && (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L) {
+ this.unsetRemoved();
+ destination.addDuringTeleport(this);
+
+ // must be manually added to connections
+ this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection);
+
+ // required to set up the pending teleport stuff to the client, and to actually update
+ // the player's position clientside
+ this.connection.internalTeleport(
+ new net.minecraft.world.entity.PositionMoveRotation(
+ this.position(), this.getDeltaMovement(), this.getYRot(), this.getXRot()
+ ),
+ java.util.Collections.emptySet()
+ );
+ this.connection.resetPosition();
+
+ this.postChangeDimension();
+ } else {
+ // Modelled after PlayerList#respawn
+
+ // We avoid checking for disconnection here, which means we do not have to add/remove from
+ // the player list here. We can let this be properly handled by the connection handler
+
+ // pre-add logic
+ PlayerList playerlist = this.server.getPlayerList();
+ net.minecraft.world.level.storage.LevelData worlddata = destination.getLevelData();
+ this.connection.send(
+ new ClientboundRespawnPacket(
+ this.createCommonSpawnInfo(destination),
+ (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L ? (byte)1 : (byte)0
+ )
+ );
+ // don't bother with the chunk cache radius and simulation distance packets, they are handled
+ // by the chunk loader
+ this.spawnIn(destination); // important that destination != null
+ // we can delay teleport until later, the player position is already set up at the target
+ this.setShiftKeyDown(false);
+
+ this.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket(
+ destination.getSharedSpawnPos(), destination.getSharedSpawnAngle()
+ ));
+ this.connection.send(new ClientboundChangeDifficultyPacket(
+ worlddata.getDifficulty(), worlddata.isDifficultyLocked()
+ ));
+ this.connection.send(new ClientboundSetExperiencePacket(
+ this.experienceProgress, this.totalExperience, this.experienceLevel
+ ));
+
+ playerlist.sendActivePlayerEffects(this);
+ playerlist.sendLevelInfo(this, destination);
+ playerlist.sendPlayerPermissionLevel(this);
+
+ // regular world add logic
+ this.unsetRemoved();
+ destination.addDuringTeleport(this);
+
+ // must be manually added to connections
+ this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection);
+
+ // required to set up the pending teleport stuff to the client, and to actually update
+ // the player's position clientside
+ this.connection.internalTeleport(
+ new net.minecraft.world.entity.PositionMoveRotation(
+ this.position(), this.getDeltaMovement(), this.getYRot(), this.getXRot()
+ ),
+ java.util.Collections.emptySet()
+ );
+ this.connection.resetPosition();
+
+ // delay callback until after post add logic
+
+ // post add logic
+
+ // "Added from changeDimension"
+ this.setHealth(this.getHealth());
+ playerlist.sendAllPlayerInfo(this);
+ this.onUpdateAbilities();
+ /*for (MobEffectInstance mobEffect : this.getActiveEffects()) {
+ this.connection.send(new ClientboundUpdateMobEffectPacket(this.getId(), mobEffect, false));
+ }*/ // handled by sendActivePlayerEffects
+
+ // Paper start - Reset shield blocking on dimension change
+ if (this.isBlocking()) {
+ this.stopUsingItem();
+ }
+ // Paper end - Reset shield blocking on dimension change
+
+ this.triggerDimensionChangeTriggers(originWorld);
+
+ // finished
+
+ this.postChangeDimension();
+ }
+ }
+
+ @Override
+ public boolean endPortalLogicAsync(BlockPos portalPos) {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async");
+
+ if (this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) {
+ if (!this.canPortalAsync(null, false)) {
+ return false;
+ }
+ this.wonGame = true;
+ // TODO is there a better solution to this that DOESN'T skip the credits?
+ this.seenCredits = true;
+ if (!this.seenCredits) {
+ this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 0.0F));
+ }
+ this.exitEndCredits();
+ return true;
+ } else {
+ return super.endPortalLogicAsync(portalPos);
+ }
+ }
+
+ @Override
+ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) {
+ super.prePortalLogic(origin, destination, type);
+ if (origin.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.OVERWORLD && destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER) {
+ this.enteredNetherPosition = this.position();
+ }
+ }
+ // Folia end - region threading
+
@Nullable
@Override
public ServerPlayer teleport(TeleportTransition teleportTransition) {
+ // Folia start - region threading
+ if (true) {
+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading");
+ }
+ // Folia end - region threading
if (this.isSleeping()) return null; // CraftBukkit - SPIGOT-3154
if (this.isRemoved()) {
return null;
@@ -2397,7 +2879,30 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
return (Entity)(this.camera == null ? this : this.camera);
}
+ // Folia start - region threading
+ private void teleportToCameraOffRegion() {
+ Entity cameraFinal = this.camera;
+ // use the task scheduler, as we don't know where the caller is invoking from
+ if (this != cameraFinal) {
+ this.getBukkitEntity().taskScheduler.schedule((final ServerPlayer newPlayer) -> {
+ io.papermc.paper.threadedregions.TeleportUtils.teleport(
+ newPlayer, false, cameraFinal, null, null, 0L,
+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null,
+ (final ServerPlayer newerPlayer) -> {
+ return newerPlayer.camera == cameraFinal;
+ }
+ );
+ }, null, 1L);
+ } // else: do not bother teleporting to self
+ }
+ // Folia end - region threading
+
public void setCamera(@Nullable Entity entityToSpectate) {
+ // Folia start - region threading
+ if (entityToSpectate != null && (entityToSpectate != this && !entityToSpectate.canBeSpectated())) {
+ return;
+ }
+ // Folia end - region threading
Entity camera = this.getCamera();
this.camera = (Entity)(entityToSpectate == null ? this : entityToSpectate);
if (camera != this.camera) {
@@ -2416,16 +2921,19 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
}
}
// Paper end - Add PlayerStartSpectatingEntityEvent and PlayerStopSpectatingEntity
- if (this.camera.level() instanceof ServerLevel serverLevel) {
- this.teleportTo(serverLevel, this.camera.getX(), this.camera.getY(), this.camera.getZ(), Set.of(), this.getYRot(), this.getXRot(), false, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit
- }
+ // Folia - region threading - move down
- if (entityToSpectate != null) {
- this.serverLevel().getChunkSource().move(this);
- }
+ // Folia - region threading - not needed
+ // Folia start - region threading - handle camera setting better
+ if (this.camera == this
+ || (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.camera) && this.camera.moonrise$getTrackedEntity() != null
+ && this.camera.moonrise$getTrackedEntity().seenBy.contains(this.connection))) {
+ // Folia end - region threading - handle camera setting better
this.connection.send(new ClientboundSetCameraPacket(this.camera));
- this.connection.resetPosition();
+ } // Folia - region threading - handle camera setting better
+ //this.connection.resetPosition(); // Folia - region threading - not needed
+ this.teleportToCameraOffRegion(); // Folia - region threading - moved down
}
}
@@ -2896,11 +3404,11 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
}
public void registerEnderPearl(ThrownEnderpearl enderPearl) {
- this.enderPearls.add(enderPearl);
+ //this.enderPearls.add(enderPearl); // Folia - region threading - do not track ender pearls
}
public void deregisterEnderPearl(ThrownEnderpearl enderPearl) {
- this.enderPearls.remove(enderPearl);
+ //this.enderPearls.remove(enderPearl); // Folia - region threading - do not track ender pearls
}
public Set<ThrownEnderpearl> getEnderPearls() {
@@ -3054,7 +3562,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
this.experienceLevel = this.newLevel;
this.totalExperience = this.newTotalExp;
this.experienceProgress = 0;
- this.deathTime = 0;
+ this.deathTime = 0; this.broadcastedDeath = false; // Folia - region threading
this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent
this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH);
this.effectsDirty = true;
diff --git a/net/minecraft/server/level/ServerPlayerGameMode.java b/net/minecraft/server/level/ServerPlayerGameMode.java
index 623c069f1fe079e020c6391a3db1a3d95cd3dbf5..61804cdb6be06b1b3316e563df57f0b38268958a 100644
--- a/net/minecraft/server/level/ServerPlayerGameMode.java
+++ b/net/minecraft/server/level/ServerPlayerGameMode.java
@@ -114,7 +114,7 @@ public class ServerPlayerGameMode {
// this.gameTicks = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit
this.gameTicks = (int) this.level.getLagCompensationTick(); // Paper - lag compensate eating
if (this.hasDelayedDestroy) {
- BlockState blockState = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks
+ BlockState blockState = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.delayedDestroyPos) ? null : this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks // Folia - region threading - don't destroy blocks not owned
if (blockState == null || blockState.isAir()) { // Paper - Don't allow digging into unloaded chunks
this.hasDelayedDestroy = false;
} else {
@@ -126,7 +126,7 @@ public class ServerPlayerGameMode {
}
} else if (this.isDestroyingBlock) {
// Paper start - Don't allow digging into unloaded chunks; don't want to do same logic as above, return instead
- BlockState blockState = this.level.getBlockStateIfLoaded(this.destroyPos);
+ BlockState blockState = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.destroyPos) ? null : this.level.getBlockStateIfLoaded(this.destroyPos); // Folia - region threading - don't destroy blocks not owned
if (blockState == null) {
this.isDestroyingBlock = false;
return;
@@ -369,7 +369,7 @@ public class ServerPlayerGameMode {
} else {
// CraftBukkit start
org.bukkit.block.BlockState state = bblock.getState();
- this.level.captureDrops = new java.util.ArrayList<>();
+ this.level.getCurrentWorldData().captureDrops = new java.util.ArrayList<>(); // Folia - region threading
// CraftBukkit end
BlockState blockState1 = block.playerWillDestroy(this.level, pos, blockState, this.player);
boolean flag = this.level.removeBlock(pos, false);
@@ -395,8 +395,8 @@ public class ServerPlayerGameMode {
// return true; // CraftBukkit
}
// CraftBukkit start
- java.util.List<net.minecraft.world.entity.item.ItemEntity> 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<net.minecraft.world.entity.item.ItemEntity> itemsToDrop = this.level.getCurrentWorldData().captureDrops; // Paper - capture all item additions to the world // Folia - region threading
+ this.level.getCurrentWorldData().captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff // Folia - region threading
if (event.isDropItems()) {
org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - capture all item additions to the world
}
diff --git a/net/minecraft/server/level/TicketType.java b/net/minecraft/server/level/TicketType.java
index 8f12a4df5d63ecd11e6e615d910b6e3f6dde5f3c..f8b74eaf534c6264ce018a6826c3d035089e7d30 100644
--- a/net/minecraft/server/level/TicketType.java
+++ b/net/minecraft/server/level/TicketType.java
@@ -17,10 +17,18 @@ public class TicketType<T> {
public static final TicketType<ChunkPos> FORCED = create("forced", Comparator.comparingLong(ChunkPos::toLong));
public static final TicketType<BlockPos> PORTAL = create("portal", Vec3i::compareTo, 300);
public static final TicketType<ChunkPos> ENDER_PEARL = create("ender_pearl", Comparator.comparingLong(ChunkPos::toLong), 40);
- public static final TicketType<ChunkPos> UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1);
+ public static final TicketType<ChunkPos> UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 5); // Folia - region threading
public static final TicketType<Unit> PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit
public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
public static final TicketType<Integer> POST_TELEPORT = TicketType.create("post_teleport", Integer::compare, 5); // Paper - post teleport ticket type
+ // Folia start - region threading
+ public static final TicketType<Unit> LOGIN = create("folia:login", (u1, u2) -> 0, 20);
+ public static final TicketType<Unit> DELAYED = create("folia:delay", (u1, u2) -> 0, 5);
+ public static final TicketType<Long> END_GATEWAY_EXIT_SEARCH = create("folia:end_gateway_exit_search", Long::compareTo);
+ public static final TicketType<Long> NETHER_PORTAL_DOUBLE_CHECK = create("folia:nether_portal_double_check", Long::compareTo);
+ public static final TicketType<Long> TELEPORT_HOLD_TICKET = create("folia:teleport_hold_ticket", Long::compareTo);
+ public static final TicketType<Unit> REGION_SCHEDULER_API_HOLD = create("folia:region_scheduler_api_hold", (a, b) -> 0);
+ // Folia end - region threading
public static <T> TicketType<T> create(String name, Comparator<T> comparator) {
return new TicketType<>(name, comparator, 0L);
diff --git a/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java
index 7fa41dea184b01891f45d8e404bc1cba19cf1bcf..43de96cc3c2b1259b1edb5feae3f202dea65dcdf 100644
--- a/net/minecraft/server/level/WorldGenRegion.java
+++ b/net/minecraft/server/level/WorldGenRegion.java
@@ -107,6 +107,13 @@ public class WorldGenRegion implements WorldGenLevel {
return this.getLightEngine().getRawBrightness(blockPos, subtract);
}
// Paper end - rewrite chunk system
+ // Folia start - region threading
+ private final net.minecraft.world.level.StructureManager structureManager;
+ @Override
+ public net.minecraft.world.level.StructureManager structureManager() {
+ return this.structureManager;
+ }
+ // Folia end - region threading
public WorldGenRegion(ServerLevel level, StaticCache2D<GenerationChunkHolder> cache, ChunkStep generatingStep, ChunkAccess center) {
this.generatingStep = generatingStep;
@@ -118,6 +125,7 @@ public class WorldGenRegion implements WorldGenLevel {
this.random = level.getChunkSource().randomState().getOrCreateRandomFactory(WORLDGEN_REGION_RANDOM).at(this.center.getPos().getWorldPosition());
this.dimensionType = level.dimensionType();
this.biomeManager = new BiomeManager(this, BiomeManager.obfuscateSeed(this.seed));
+ this.structureManager = level.structureManager().forWorldGenRegion(this); // Folia - region threading
}
public boolean isOldChunkAround(ChunkPos pos, int radius) {
diff --git a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
index e71c1a564e5d4ac43460f89879ff709ee685706f..6eca15223b92aedac74233db886e2c1248750e2c 100644
--- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java
@@ -96,6 +96,10 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
}
}
+ // Folia start - region threading
+ private boolean handledDisconnect = false;
+ // Folia end - region threading
+
@Override
public void onDisconnect(DisconnectionDetails details) {
// Paper start - Fix kick event leave message not being sent
@@ -104,10 +108,18 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
public void onDisconnect(DisconnectionDetails info, @Nullable net.kyori.adventure.text.Component quitMessage) {
// Paper end - Fix kick event leave message not being sent
+ // Folia start - region threading
+ if (this.handledDisconnect) {
+ // avoid retiring scheduler twice
+ return;
+ }
+ this.handledDisconnect = true;
+ // Folia end - region threading
if (this.isSingleplayerOwner()) {
LOGGER.info("Stopping singleplayer server as player logged out");
this.server.halt(false);
}
+ this.player.getBukkitEntity().taskScheduler.retire(); // Folia - region threading
}
@Override
@@ -330,24 +342,8 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
if (this.processedDisconnect) {
return;
}
- if (!this.cserver.isPrimaryThread()) {
- org.bukkit.craftbukkit.util.Waitable waitable = new org.bukkit.craftbukkit.util.Waitable() {
- @Override
- protected Object evaluate() {
- ServerCommonPacketListenerImpl.this.disconnect(disconnectionDetails, cause); // Paper - kick event causes
- return null;
- }
- };
-
- this.server.processQueue.add(waitable);
-
- try {
- waitable.get();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- } catch (java.util.concurrent.ExecutionException e) {
- throw new RuntimeException(e);
- }
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player)) { // Folia - region threading
+ this.connection.disconnectSafely(disconnectionDetails, cause); // Folia - region threading - it HAS to be delayed/async to avoid deadlock if we try to wait for another region
return;
}
@@ -378,7 +374,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
this.onDisconnect(disconnectionDetails, leaveMessage); // CraftBukkit - fire quit instantly // Paper - use kick event leave message
this.connection.setReadOnly();
// CraftBukkit - Don't wait
- this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper
+ //this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper // Folia - region threading
}
// Paper start - add proper async disconnect
@@ -391,19 +387,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack
}
public void disconnectAsync(DisconnectionDetails disconnectionInfo, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
- if (this.cserver.isPrimaryThread()) {
- this.disconnect(disconnectionInfo, cause);
- return;
- }
-
- this.connection.setReadOnly();
- this.server.scheduleOnMain(() -> {
- ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause);
- if (ServerCommonPacketListenerImpl.this.player.quitReason == null) {
- // cancelled
- ServerCommonPacketListenerImpl.this.connection.enableAutoRead();
- }
- });
+ this.disconnect(disconnectionInfo, cause); // Folia - threaded regions
}
// Paper end - add proper async disconnect
diff --git a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
index 2e9eb04c7c4342393c05339906c267bca9ff29b1..00fb8a5dda1f305a0e0f947bbb75a3f40b5318cc 100644
--- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java
@@ -47,6 +47,7 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis
private ClientInformation clientInformation;
@Nullable
private SynchronizeRegistriesTask synchronizeRegistriesTask;
+ public boolean switchToMain = false; // Folia - region threading - rewrite login process
// CraftBukkit start
public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie, ServerPlayer player) {
@@ -160,7 +161,58 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis
}
ServerPlayer playerForLogin = playerList.getPlayerForLogin(this.gameProfile, this.clientInformation, this.player); // CraftBukkit
- playerList.placeNewPlayer(this.connection, playerForLogin, this.createCookie(this.clientInformation));
+ // Folia start - region threading - rewrite login process
+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot handle player login off global tick thread");
+ CommonListenerCookie clientData = this.createCookie(this.clientInformation);
+ org.apache.commons.lang3.mutable.MutableObject<net.minecraft.nbt.CompoundTag> data = new org.apache.commons.lang3.mutable.MutableObject<>();
+ org.apache.commons.lang3.mutable.MutableObject<String> lastKnownName = new org.apache.commons.lang3.mutable.MutableObject<>();
+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable<org.bukkit.Location> toComplete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>();
+ // note: need to call addWaiter before completion to ensure the callback is invoked synchronously
+ // the loadSpawnForNewPlayer function always completes the completable once the chunks were loaded,
+ // on the load callback for those chunks (so on the same region)
+ // this guarantees the chunk cannot unload under our feet
+ toComplete.addWaiter((org.bukkit.Location loc, Throwable t) -> {
+ int chunkX = net.minecraft.util.Mth.floor(loc.getX()) >> 4;
+ int chunkZ = net.minecraft.util.Mth.floor(loc.getZ()) >> 4;
+
+ net.minecraft.server.level.ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)loc.getWorld()).getHandle();
+ // we just need to hold the chunks at loaded until the next tick
+ // so we do not need to care about unique IDs for the ticket
+ world.getChunkSource().addTicketAtLevel(
+ net.minecraft.server.level.TicketType.LOGIN,
+ new net.minecraft.world.level.ChunkPos(chunkX, chunkZ),
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL,
+ net.minecraft.util.Unit.INSTANCE
+ );
+
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
+ world, chunkX, chunkZ,
+ () -> {
+ // once switchToMain is set, the current ticking region now owns the connection and is responsible
+ // for cleaning it up
+ playerList.placeNewPlayer(
+ ServerConfigurationPacketListenerImpl.this.connection,
+ playerForLogin,
+ clientData,
+ java.util.Optional.ofNullable(data.getValue()),
+ lastKnownName.getValue(),
+ loc
+ );
+ },
+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER
+ );
+ });
+ this.switchToMain = true;
+ try {
+ // now the connection responsibility is transferred on the region
+ playerList.loadSpawnForNewPlayer(this.connection, playerForLogin, clientData, data, lastKnownName, toComplete);
+ } catch (final Throwable throwable) {
+ // assume toComplete will not be invoked
+ // ensure global tick thread owns the connection again, to properly disconnect it
+ this.switchToMain = false;
+ throw new RuntimeException(throwable);
+ }
+ // Folia end - region threading - rewrite login process
} catch (Exception var5) {
LOGGER.error("Couldn't place player in world", (Throwable)var5);
// Paper start - Debugging
diff --git a/net/minecraft/server/network/ServerConnectionListener.java b/net/minecraft/server/network/ServerConnectionListener.java
index bd07e6a5aa1883786d789ea71711a0c0c0a95c26..09469ad131622158fe5579216fc4164251ff2220 100644
--- a/net/minecraft/server/network/ServerConnectionListener.java
+++ b/net/minecraft/server/network/ServerConnectionListener.java
@@ -167,12 +167,15 @@ public class ServerConnectionListener {
}
// Paper end - Add support for proxy protocol
// ServerConnectionListener.this.connections.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking
- ServerConnectionListener.this.pending.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking
+ //ServerConnectionListener.this.pending.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking // Folia - connection fixes - move down
connection.configurePacketHandler(channelPipeline);
connection.setListenerForServerboundHandshake(
new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, connection)
);
io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper - Add Channel initialization listeners
+ // Folia start - regionised threading
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addConnection(connection);
+ // Folia end - regionised threading
}
}
)
@@ -242,7 +245,7 @@ public class ServerConnectionListener {
// Spigot start
this.addPending(); // Paper - prevent blocking on adding a new connection while the server is ticking
// This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order
- if (org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0) {
+ if (org.spigotmc.SpigotConfig.playerShuffle > 0 && 0 % org.spigotmc.SpigotConfig.playerShuffle == 0) { // Folia - region threading
Collections.shuffle(this.connections);
}
// Spigot end
diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
2025-03-09 12:50:14 -07:00
index 1205307b7d2336fa6c5395a65be6643228c49d72..a2fe9286d432909ba0cb3731a166514af768dc37 100644
--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -292,10 +292,10 @@ public class ServerGamePacketListenerImpl
private int knownMovePacketCount;
private boolean receivedMovementThisTick;
// CraftBukkit start - add fields
- private int lastTick = MinecraftServer.currentTick;
+ private long lastTick = Util.getMillis() / 50L; // Folia - region threading
private int allowedPlayerTicks = 1;
- private int lastDropTick = MinecraftServer.currentTick;
- private int lastBookTick = MinecraftServer.currentTick;
+ private long lastDropTick = Util.getMillis() / 50L; // Folia - region threading
+ private long lastBookTick = Util.getMillis() / 50L; // Folia - region threading
private int dropCount = 0;
private boolean hasMoved = false;
2025-03-09 12:50:14 -07:00
@@ -313,10 +313,17 @@ public class ServerGamePacketListenerImpl
private final LastSeenMessagesValidator lastSeenMessages = new LastSeenMessagesValidator(20);
private final MessageSignatureCache messageSignatureCache = MessageSignatureCache.createDefault();
private final FutureChain chatMessageChain;
- private boolean waitingForSwitchToConfig;
+ public volatile boolean waitingForSwitchToConfig; // Folia - rewrite login process - fix bad ordering of this field write + public
private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length
2025-03-09 12:50:14 -07:00
private final io.papermc.paper.event.packet.ClientTickEndEvent tickEndEvent; // Paper - add client tick end event
+ // Folia start - region threading
+ public net.minecraft.world.level.ChunkPos disconnectPos;
+ private static final java.util.concurrent.atomic.AtomicLong DISCONNECT_TICKET_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong();
+ public static final net.minecraft.server.level.TicketType<Long> DISCONNECT_TICKET = net.minecraft.server.level.TicketType.create("disconnect_ticket", Long::compareTo);
+ public final Long disconnectTicketId = Long.valueOf(DISCONNECT_TICKET_ID_GENERATOR.getAndIncrement());
+ // Folia end - region threading
+
public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie cookie) {
super(server, connection, cookie, player); // CraftBukkit
this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection());
2025-03-09 12:50:14 -07:00
@@ -330,6 +337,12 @@ public class ServerGamePacketListenerImpl
@Override
public void tick() {
+ // Folia start - region threading
+ this.keepConnectionAlive();
+ if (this.processedDisconnect || this.player.wonGame) {
+ return;
+ }
+ // Folia end - region threading
if (this.ackBlockChangesUpTo > -1) {
this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo));
this.ackBlockChangesUpTo = -1;
2025-03-09 12:50:14 -07:00
@@ -378,7 +391,7 @@ public class ServerGamePacketListenerImpl
this.aboveGroundVehicleTickCount = 0;
}
- this.keepConnectionAlive();
+ // Folia - region threading - moved to beginning of method
this.chatSpamThrottler.tick();
this.dropSpamThrottler.tick();
this.tabSpamThrottler.tick(); // Paper - configurable tab spam limits
2025-03-09 12:50:14 -07:00
@@ -414,6 +427,19 @@ public class ServerGamePacketListenerImpl
this.lastGoodX = this.player.getX();
this.lastGoodY = this.player.getY();
this.lastGoodZ = this.player.getZ();
+ // Folia start - support vehicle teleportations
+ this.lastVehicle = this.player.getRootVehicle();
+ if (this.lastVehicle != this.player && this.lastVehicle.getControllingPassenger() == this.player) {
+ this.vehicleFirstGoodX = this.lastVehicle.getX();
+ this.vehicleFirstGoodY = this.lastVehicle.getY();
+ this.vehicleFirstGoodZ = this.lastVehicle.getZ();
+ this.vehicleLastGoodX = this.lastVehicle.getX();
+ this.vehicleLastGoodY = this.lastVehicle.getY();
+ this.vehicleLastGoodZ = this.lastVehicle.getZ();
+ } else {
+ this.lastVehicle = null;
+ }
+ // Folia end - support vehicle teleportations
}
@Override
2025-03-09 12:50:14 -07:00
@@ -521,9 +547,10 @@ public class ServerGamePacketListenerImpl
// Paper end - fix large move vectors killing the server
// CraftBukkit start - handle custom speeds and skipped ticks
- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick;
+ int currTick = (int)(Util.getMillis() / 50); // Folia - region threading
+ this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading
this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1);
- this.lastTick = (int) (System.currentTimeMillis() / 50);
+ this.lastTick = (int) currTick; // Folia - region threading
++this.receivedMovePacketCount;
int i = this.receivedMovePacketCount - this.knownMovePacketCount;
2025-03-09 12:50:14 -07:00
@@ -590,7 +617,7 @@ public class ServerGamePacketListenerImpl
}
rootVehicle.absMoveTo(d, d1, d2, f, f1);
- this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+ //this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - move to repositionAllPassengers
// Paper start - optimise out extra getCubes
boolean teleportBack = flag2; // violating this is always a fail
if (!teleportBack) {
2025-03-09 12:50:14 -07:00
@@ -602,11 +629,19 @@ public class ServerGamePacketListenerImpl
}
if (teleportBack) { // Paper end - optimise out extra getCubes
rootVehicle.absMoveTo(x, y, z, f, f1);
- this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+ //this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - not needed, the player is no longer updated
this.send(ClientboundMoveVehiclePacket.fromEntity(rootVehicle));
return;
}
+ // Folia start - move to positionRider
+ // this correction is required on folia since we move the connection tick to the beginning of the server
+ // tick, which would make any desync here visible
+ // this will correctly update the passenger positions for all mounted entities
+ // this prevents desync and ensures that all passengers have the correct rider-adjusted position
+ rootVehicle.repositionAllPassengers(false);
+ // Folia end - move to positionRider
+
// CraftBukkit start - fire PlayerMoveEvent
org.bukkit.entity.Player player = this.getCraftPlayer();
if (!this.hasMoved) {
2025-03-09 12:50:14 -07:00
@@ -637,7 +672,7 @@ public class ServerGamePacketListenerImpl
// If the event is cancelled we move the player back to their old location.
if (event.isCancelled()) {
- this.teleport(from);
+ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading
return;
}
2025-03-09 12:50:14 -07:00
@@ -645,7 +680,7 @@ public class ServerGamePacketListenerImpl
// there to avoid any 'Moved wrongly' or 'Moved too quickly' errors.
// We only do this if the Event was not cancelled.
if (!oldTo.equals(event.getTo()) && !event.isCancelled()) {
- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN);
+ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading
return;
}
2025-03-09 12:50:14 -07:00
@@ -826,7 +861,7 @@ public class ServerGamePacketListenerImpl
}
// This needs to be on main
- this.server.scheduleOnMain(() -> this.sendServerSuggestions(packet, stringReader));
+ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> this.sendServerSuggestions(packet, stringReader), null, 1L); // Folia - region threading
} else if (!completions.isEmpty()) {
final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packet.getCommand(), stringReader.getTotalLength());
final com.mojang.brigadier.suggestion.SuggestionsBuilder builder = builder0.createOffset(builder0.getInput().lastIndexOf(' ') + 1);
2025-03-09 12:50:14 -07:00
@@ -1210,11 +1245,11 @@ public class ServerGamePacketListenerImpl
}
// Paper end - Book size limits
// CraftBukkit start
- if (this.lastBookTick + 20 > MinecraftServer.currentTick) {
+ if (this.lastBookTick + 20 > this.lastTick) { // Folia - region threading
this.disconnectAsync(Component.literal("Book edited too quickly!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - add proper async disconnect
return;
}
- this.lastBookTick = MinecraftServer.currentTick;
+ this.lastBookTick = this.lastTick; // Folia - region threading
// CraftBukkit end
int slot = packet.slot();
if (Inventory.isHotbarSlot(slot) || slot == 40) {
2025-03-09 12:50:14 -07:00
@@ -1225,7 +1260,22 @@ public class ServerGamePacketListenerImpl
Consumer<List<FilteredText>> consumer = optional.isPresent()
? texts -> this.signBook(texts.get(0), texts.subList(1, texts.size()), slot)
: texts -> this.updateBookContents(texts, slot);
- this.filterTextPacket(list).thenAcceptAsync(consumer, this.server);
+ // Folia start - region threading
+ this.filterTextPacket(list).thenAcceptAsync(
+ consumer,
+ (Runnable run) -> {
+ this.player.getBukkitEntity().taskScheduler.schedule(
+ (player) -> {
+ run.run();
+ },
+ null, 1L);
+ }
+ ).whenComplete((Object res, Throwable thr) -> {
+ if (thr != null) {
+ LOGGER.error("Failed to handle book update packet", thr);
+ }
+ });
+ // Folia end - region threading
}
}
2025-03-09 12:50:14 -07:00
@@ -1351,9 +1401,10 @@ public class ServerGamePacketListenerImpl
int i = this.receivedMovePacketCount - this.knownMovePacketCount;
// CraftBukkit start - handle custom speeds and skipped ticks
- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick;
+ int currTick = (int)(Util.getMillis() / 50); // Folia - region threading
+ this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading
this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1);
- this.lastTick = (int) (System.currentTimeMillis() / 50);
+ this.lastTick = (int) currTick; // Folia - region threading
if (i > Math.max(this.allowedPlayerTicks, 5)) {
LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i);
2025-03-09 12:50:14 -07:00
@@ -1542,7 +1593,7 @@ public class ServerGamePacketListenerImpl
// If the event is cancelled we move the player back to their old location.
if (event.isCancelled()) {
- this.teleport(from);
+ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading
return;
}
2025-03-09 12:50:14 -07:00
@@ -1550,7 +1601,7 @@ public class ServerGamePacketListenerImpl
// there to avoid any 'Moved wrongly' or 'Moved too quickly' errors.
// We only do this if the Event was not cancelled.
if (!oldTo.equals(event.getTo()) && !event.isCancelled()) {
- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN);
+ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading
return;
}
2025-03-09 12:50:14 -07:00
@@ -1810,9 +1861,9 @@ public class ServerGamePacketListenerImpl
if (!this.player.isSpectator()) {
// limit how quickly items can be dropped
// If the ticks aren't the same then the count starts from 0 and we update the lastDropTick.
- if (this.lastDropTick != MinecraftServer.currentTick) {
+ if (this.lastDropTick != io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - region threading
this.dropCount = 0;
- this.lastDropTick = MinecraftServer.currentTick;
+ this.lastDropTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading
} else {
// Else we increment the drop count and check the amount.
this.dropCount++;
2025-03-09 12:50:14 -07:00
@@ -1843,7 +1894,7 @@ public class ServerGamePacketListenerImpl
case ABORT_DESTROY_BLOCK:
case STOP_DESTROY_BLOCK:
// Paper start - Don't allow digging into unloaded chunks
2025-03-09 12:50:14 -07:00
- if (this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !this.player.canInteractWithBlock(pos, 1.0)) {
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), pos.getX() >> 4, pos.getZ() >> 4, 8) || this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !this.player.canInteractWithBlock(pos, 1.0)) { // Folia - region threading - don't destroy blocks not owned
this.player.connection.ackBlockChangesUpTo(packet.getSequence());
return;
}
2025-03-09 12:50:14 -07:00
@@ -1926,7 +1977,7 @@ public class ServerGamePacketListenerImpl
}
// Paper end - improve distance check
BlockPos blockPos = hitResult.getBlockPos();
- if (this.player.canInteractWithBlock(blockPos, 1.0)) {
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), blockPos.getX() >> 4, blockPos.getZ() >> 4, 8) && this.player.canInteractWithBlock(blockPos, 1.0)) { // Folia - do not allow players to interact with blocks outside the current region
Vec3 vec3 = location.subtract(Vec3.atCenterOf(blockPos));
double d = 1.0000001;
if (Math.abs(vec3.x()) < 1.0000001 && Math.abs(vec3.y()) < 1.0000001 && Math.abs(vec3.z()) < 1.0000001) {
2025-03-09 12:50:14 -07:00
@@ -2048,7 +2099,7 @@ public class ServerGamePacketListenerImpl
for (ServerLevel serverLevel : this.server.getAllLevels()) {
Entity entity = packet.getEntity(serverLevel);
if (entity != null) {
- this.player.teleportTo(serverLevel, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit
+ io.papermc.paper.threadedregions.TeleportUtils.teleport(this.player, false, entity, null, null, Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null); // Folia - region threading
return;
}
}
2025-03-09 12:50:14 -07:00
@@ -2080,7 +2131,7 @@ public class ServerGamePacketListenerImpl
}
// CraftBukkit end
LOGGER.info("{} lost connection: {}", this.player.getName().getString(), details.reason().getString());
- this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent
+ if (!this.waitingForSwitchToConfig) this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent // Folia - region threading
super.onDisconnect(details, quitMessage); // Paper - Fix kick event leave message not being sent
}
2025-03-09 12:50:14 -07:00
@@ -2089,6 +2140,8 @@ public class ServerGamePacketListenerImpl
this.removePlayerFromWorld(null);
}
+ public boolean hackSwitchingConfig; // Folia - rewrite login process
+
private void removePlayerFromWorld(@Nullable net.kyori.adventure.text.Component quitMessage) {
// Paper end - Fix kick event leave message not being sent
this.chatMessageChain.close();
2025-03-09 12:50:14 -07:00
@@ -2102,6 +2155,8 @@ public class ServerGamePacketListenerImpl
this.player.disconnect();
// Paper start - Adventure
quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used
+ if (!this.hackSwitchingConfig) this.disconnectPos = this.player.chunkPosition(); // Folia - region threading - note: only set after removing, since it can tick the player
+ if (!this.hackSwitchingConfig) this.player.serverLevel().chunkSource.addTicketAtLevel(DISCONNECT_TICKET, this.disconnectPos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, this.disconnectTicketId); // Folia - region threading - force chunk to be loaded so that the region is not lost
if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) {
this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false);
// Paper end - Adventure
2025-03-09 12:50:14 -07:00
@@ -2341,7 +2396,7 @@ public class ServerGamePacketListenerImpl
this.player.resetLastActionTime();
// CraftBukkit start
if (sync) {
- this.server.execute(handler);
+ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> handler.run(), null, 1L); // Folia - region threading
} else {
handler.run();
}
2025-03-09 12:50:14 -07:00
@@ -2396,7 +2451,7 @@ public class ServerGamePacketListenerImpl
String originalFormat = event.getFormat(), originalMessage = event.getMessage();
this.cserver.getPluginManager().callEvent(event);
- if (PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) {
+ if (false && PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading
// Evil plugins still listening to deprecated event
final PlayerChatEvent queueEvent = new PlayerChatEvent(player, event.getMessage(), event.getFormat(), event.getRecipients());
queueEvent.setCancelled(event.isCancelled());
2025-03-09 12:50:14 -07:00
@@ -2493,6 +2548,7 @@ public class ServerGamePacketListenerImpl
if (rawMessage.isEmpty()) {
LOGGER.warn("{} tried to send an empty message", this.player.getScoreboardName());
} else if (this.getCraftPlayer().isConversing()) {
+ if (true) throw new UnsupportedOperationException(); // Folia - region threading
final String conversationInput = rawMessage;
this.server.processQueue.add(() -> ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput));
} else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { // Re-add "Command Only" flag check
2025-03-09 12:50:14 -07:00
@@ -2718,8 +2774,25 @@ public class ServerGamePacketListenerImpl
// Spigot end
public void switchToConfig() {
- this.waitingForSwitchToConfig = true;
+ // Folia start - rewrite login process
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.player, "Cannot switch config off-main");
+ if (io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) {
+ throw new IllegalStateException("Cannot switch config while on global tick thread");
+ }
+ // Folia end - rewrite login process
+ // Folia start - rewrite login process - fix bad ordering of this field write - move after removed from world
+ // the field write ordering is bad as it allows the client to send the response packet before the player is
+ // removed from the world
+ // Folia end - rewrite login process - fix bad ordering of this field write - move after removed from world
+ try { // Folia - rewrite login process - move connection ownership to global region
+ this.hackSwitchingConfig = true; // Folia - rewrite login process - avoid adding logout ticket here and retiring scheduler
this.removePlayerFromWorld();
+ } finally { // Folia start - rewrite login process - move connection ownership to global region
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.player.serverLevel().getCurrentWorldData();
+ worldData.connections.remove(this.connection);
+ // once waitingForSwitchToConfig is set, the global tick thread will own the connection
+ } // Folia end - rewrite login process - move connection ownership to global region
+ this.waitingForSwitchToConfig = true; // Folia - rewrite login process - fix bad ordering of this field write - moved down
this.send(ClientboundStartConfigurationPacket.INSTANCE);
this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND);
}
2025-03-09 12:50:14 -07:00
@@ -2744,7 +2817,7 @@ public class ServerGamePacketListenerImpl
// Spigot end
this.player.resetLastActionTime();
this.player.setShiftKeyDown(packet.isUsingSecondaryAction());
- if (target != null) {
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(target) && target != null) { // Folia - region threading - do not allow interaction of entities outside the current region
if (!serverLevel.getWorldBorder().isWithinBounds(target.blockPosition())) {
return;
}
2025-03-09 12:50:14 -07:00
@@ -2877,6 +2950,12 @@ public class ServerGamePacketListenerImpl
switch (action) {
case PERFORM_RESPAWN:
if (this.player.wonGame) {
+ // Folia start - region threading
+ if (true) {
+ this.player.exitEndCredits();
+ return;
+ }
+ // Folia end - region threading
this.player.wonGame = false;
this.player = this.server.getPlayerList().respawn(this.player, true, Entity.RemovalReason.CHANGED_DIMENSION, RespawnReason.END_PORTAL); // CraftBukkit
this.resetPosition();
2025-03-09 12:50:14 -07:00
@@ -2886,6 +2965,17 @@ public class ServerGamePacketListenerImpl
return;
}
+ // Folia start - region threading
+ if (true) {
+ this.player.respawn((ServerPlayer player) -> {
+ if (ServerGamePacketListenerImpl.this.server.isHardcore()) {
+ ServerGamePacketListenerImpl.this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent
+ ((GameRules.BooleanValue) ServerGamePacketListenerImpl.this.player.serverLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, ServerGamePacketListenerImpl.this.player.serverLevel()); // CraftBukkit - per-world
+ }
+ }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.DEATH);
+ return;
+ }
+ // Folia end - region threading
this.player = this.server.getPlayerList().respawn(this.player, false, Entity.RemovalReason.KILLED, RespawnReason.DEATH); // CraftBukkit
this.resetPosition();
if (this.server.isHardcore()) {
2025-03-09 12:50:14 -07:00
@@ -3462,7 +3552,21 @@ public class ServerGamePacketListenerImpl
}
List<String> list = Stream.of(lines).map(ChatFormatting::stripFormatting).collect(Collectors.toList());
// Paper end - Limit client sign length
- this.filterTextPacket(list).thenAcceptAsync(list1 -> this.updateSignText(packet, (List<FilteredText>)list1), this.server);
+ // Folia start - region threading
+ this.filterTextPacket(list).thenAcceptAsync((list1) -> {
+ this.updateSignText(packet, (List<FilteredText>)list1);
+ }, (Runnable run) -> {
+ this.player.getBukkitEntity().taskScheduler.schedule(
+ (player) -> {
+ run.run();
+ },
+ null, 1L);
+ }).whenComplete((Object res, Throwable thr) -> {
+ if (thr != null) {
+ LOGGER.error("Failed to handle sign update packet", thr);
+ }
+ });
+ // Folia end - region threading
}
private void updateSignText(ServerboundSignUpdatePacket packet, List<FilteredText> filteredText) {
diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
index 6689aeacf50d1498e8d23cce696fb4fecbd1cf39..6173f704b0d093813ec67eb231c75be49a462e7d 100644
--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
@@ -111,7 +111,11 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
// Paper end - Do not allow logins while the server is shutting down
if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) {
- if (this.connection.isConnected()) { // Paper - prevent logins to be processed even though disconnect was called
+ // Folia start - region threading - rewrite login process
+ String name = this.authenticatedProfile.getName();
+ java.util.UUID uniqueId = this.authenticatedProfile.getId();
+ if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) {
+ // Folia end - region threading - rewrite login process
this.verifyLoginAndFinishConnectionSetup(Objects.requireNonNull(this.authenticatedProfile));
} // Paper - prevent logins to be processed even though disconnect was called
}
@@ -250,7 +254,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
);
}
- boolean flag = playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference
+ boolean flag = false && playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference // Folia - rewrite login process - always false here
if (flag) {
this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT;
} else {
@@ -362,7 +366,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
uniqueId = gameprofile.getId();
// Paper end - Add more fields to AsyncPlayerPreLoginEvent
- if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) {
+ if (false && PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading
final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId);
if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) {
event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure
diff --git a/net/minecraft/server/players/BanListEntry.java b/net/minecraft/server/players/BanListEntry.java
index e111adec2116f922fe67ee434635e50c60dad15c..851d3ae5d37541e6455b83b3300d630e8f6d5c83 100644
--- a/net/minecraft/server/players/BanListEntry.java
+++ b/net/minecraft/server/players/BanListEntry.java
@@ -9,7 +9,7 @@ import javax.annotation.Nullable;
import net.minecraft.network.chat.Component;
public abstract class BanListEntry<T> extends StoredUserEntry<T> {
- public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT);
+ public static final ThreadLocal<SimpleDateFormat> 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<T> extends StoredUserEntry<T> {
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<T> extends StoredUserEntry<T> {
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<T> extends StoredUserEntry<T> {
@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<T> extends StoredUserEntry<T> {
Date expires = null;
try {
- expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.parse(jsonobject.get("expires").getAsString()) : null;
+ expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(jsonobject.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe
} catch (ParseException ex) {
// Guess we don't have a date
}
diff --git a/net/minecraft/server/players/OldUsersConverter.java b/net/minecraft/server/players/OldUsersConverter.java
index 7dbcd9d96f052bb10127ad2b061154c23cc9ffd4..20d895ed04cd2263560f91ef38dda6aa866bc603 100644
--- a/net/minecraft/server/players/OldUsersConverter.java
+++ b/net/minecraft/server/players/OldUsersConverter.java
@@ -469,7 +469,7 @@ public class OldUsersConverter {
static Date parseDate(String input, Date defaultValue) {
Date date;
try {
- date = BanListEntry.DATE_FORMAT.parse(input);
+ date = BanListEntry.DATE_FORMAT.get().parse(input); // Folia - region threading - SDF is not thread-safe
} catch (ParseException var4) {
date = defaultValue;
}
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
index 5a4960fdbd97d830ac79845697eea9372c48a13b..7b13a9e7d38efe7786023747f55ebf5a2ba80688 100644
--- a/net/minecraft/server/players/PlayerList.java
+++ b/net/minecraft/server/players/PlayerList.java
@@ -110,10 +110,10 @@ public abstract class PlayerList {
public static final Component DUPLICATE_LOGIN_DISCONNECT_MESSAGE = Component.translatable("multiplayer.disconnect.duplicate_login");
private static final Logger LOGGER = LogUtils.getLogger();
private static final int SEND_PLAYER_INFO_INTERVAL = 600;
- private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
+ private static final ThreadLocal<SimpleDateFormat> 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<ServerPlayer> players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety
- private final Map<UUID, ServerPlayer> playersByUUID = Maps.newHashMap();
+ private final Map<UUID, ServerPlayer> playersByUUID = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY!
private final UserBanList bans = new UserBanList(USERBANLIST_FILE);
private final IpBanList ipBans = new IpBanList(IPBANLIST_FILE);
private final ServerOpList ops = new ServerOpList(OPLIST_FILE);
@@ -137,6 +137,60 @@ public abstract class PlayerList {
private final Map<String,ServerPlayer> 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<String, Connection> connectionByName = new java.util.HashMap<>();
+ private final Map<UUID, Connection> connectionById = new java.util.HashMap<>();
+ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<Connection> usersCountedAgainstLimit = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>();
+
+ public boolean pushPendingJoin(String userName, UUID byId, Connection conn) {
+ userName = userName.toLowerCase(java.util.Locale.ROOT);
+ Connection conflictingName, conflictingId;
+ synchronized (this.connectionsStateLock) {
+ conflictingName = this.connectionByName.get(userName);
+ conflictingId = this.connectionById.get(byId);
+
+ if (conflictingName == null && conflictingId == null) {
+ this.connectionByName.put(userName, conn);
+ this.connectionById.put(byId, conn);
+ }
+ }
+
+ Component message = Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]);
+
+ if (conflictingId != null || conflictingName != null) {
+ if (conflictingName != null && conflictingName.isPlayerConnected()) {
+ conflictingName.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN);
+ }
+ if (conflictingName != conflictingId && conflictingId != null && conflictingId.isPlayerConnected()) {
+ conflictingId.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN);
+ }
+ }
+
+ return conflictingName == null && conflictingId == null;
+ }
+
+ public void removeConnection(String userName, UUID byId, Connection conn) {
+ userName = userName.toLowerCase(java.util.Locale.ROOT);
+ synchronized (this.connectionsStateLock) {
+ this.connectionByName.remove(userName, conn);
+ this.connectionById.remove(byId, conn);
+ this.usersCountedAgainstLimit.remove(conn);
+ }
+ }
+
+ private boolean countConnection(Connection conn, int limit) {
+ synchronized (this.connectionsStateLock) {
+ int count = this.usersCountedAgainstLimit.size();
+ if (count >= limit) {
+ return false;
+ }
+ this.usersCountedAgainstLimit.add(conn);
+ return true;
+ }
+ }
+ // Folia end - region threading
+
public PlayerList(MinecraftServer server, LayeredRegistryAccess<RegistryLayer> registries, PlayerDataStorage playerIo, int maxPlayers) {
this.cserver = server.server = new org.bukkit.craftbukkit.CraftServer((net.minecraft.server.dedicated.DedicatedServer) server, this);
server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper
@@ -149,7 +203,7 @@ public abstract class PlayerList {
abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor
- public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie) {
+ public void loadSpawnForNewPlayer(final Connection connection, final ServerPlayer player, final CommonListenerCookie cookie, org.apache.commons.lang3.mutable.MutableObject<CompoundTag> data, org.apache.commons.lang3.mutable.MutableObject<String> lastKnownName, ca.spottedleaf.concurrentutil.completable.CallbackCompletable<org.bukkit.Location> toComplete) { // Folia - region threading - rewrite login process
player.isRealPlayer = true; // Paper
player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed
GameProfile gameProfile = player.getGameProfile();
@@ -221,17 +275,41 @@ public abstract class PlayerList {
player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login
// Paper start - reset to main world spawn if first spawn or invalid world
}
+ // Folia start - region threading - rewrite login process
+ // must write to these before toComplete is invoked
+ data.setValue(optional.orElse(null));
+ lastKnownName.setValue(string);
+ // Folia end - region threading - rewrite login process
if (optional.isEmpty() || invalidPlayerWorld[0]) {
// Paper end - reset to main world spawn if first spawn or invalid world
- player.moveTo(player.adjustSpawnLocation(serverLevel, serverLevel.getSharedSpawnPos()).getBottomCenter(), serverLevel.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored
+ ServerPlayer.fudgeSpawnLocation(serverLevel, player, toComplete); // Paper - MC-200092 - fix first spawn pos yaw being ignored // Folia - region threading
+ } else {
+ serverLevel.loadChunksForMoveAsync(
+ player.getBoundingBox(),
+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER,
+ (c) -> {
+ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(serverLevel, player.position()));
+ }
+ );
}
+ // Folia end - region threading - rewrite login process
// Paper end - Entity#getEntitySpawnReason
+ // Folia start - region threading - rewrite login process
+ return;
+ }
+ // optional -> player data
+ // s -> last known name
+ public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie, Optional<CompoundTag> optional, String string, org.bukkit.Location selectedSpawn) {
+ ServerLevel serverLevel = ((org.bukkit.craftbukkit.CraftWorld)selectedSpawn.getWorld()).getHandle();
+ player.setPosRaw(selectedSpawn.getX(), selectedSpawn.getY(), selectedSpawn.getZ());
+ player.lastSave = System.nanoTime(); // changed to nanoTime
+ // Folia end - region threading - rewrite login process
player.setServerLevel(serverLevel);
String loggableAddress = connection.getLoggableAddress(this.server.logIPs());
// Spigot start - spawn location event
org.bukkit.entity.Player spawnPlayer = player.getBukkitEntity();
org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation());
- this.cserver.getPluginManager().callEvent(ev);
+ //this.cserver.getPluginManager().callEvent(ev); // Folia - region threading - TODO WTF TO DO WITH THIS EVENT?
org.bukkit.Location loc = ev.getSpawnLocation();
serverLevel = ((org.bukkit.craftbukkit.CraftWorld) loc.getWorld()).getHandle();
@@ -254,6 +332,11 @@ public abstract class PlayerList {
LevelData levelData = serverLevel.getLevelData();
player.loadGameTypes(optional.orElse(null));
ServerGamePacketListenerImpl serverGamePacketListenerImpl = new ServerGamePacketListenerImpl(this.server, connection, player, cookie);
+ // Folia start - rewrite login process
+ // only after setting the connection listener to game type, add the connection to this regions list
+ serverLevel.getCurrentWorldData().connections.add(connection);
+ // Folia end - rewrite login process
+
connection.setupInboundProtocol(
GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), serverGamePacketListenerImpl
);
@@ -287,7 +370,7 @@ public abstract class PlayerList {
this.sendPlayerPermissionLevel(player);
player.getStats().markAllDirty();
player.getRecipeBook().sendInitialRecipeBook(player);
- this.updateEntireScoreboard(serverLevel.getScoreboard(), player);
+ //this.updateEntireScoreboard(serverLevel.getScoreboard(), player); // Folia - region threading
this.server.invalidateStatus();
MutableComponent mutableComponent;
if (player.getGameProfile().getName().equalsIgnoreCase(string)) {
@@ -327,7 +410,7 @@ public abstract class PlayerList {
this.cserver.getPluginManager().callEvent(playerJoinEvent);
if (!player.connection.isAcceptingMessages()) {
- return;
+ //return; // Folia - region threading - must still allow the player to connect, as we must add to chunk map before handling disconnect
}
final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage();
@@ -342,8 +425,7 @@ public abstract class PlayerList {
ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper - Add Listing API for Player
final List<ServerPlayer> onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - Use single player info update packet on join
- for (int i = 0; i < this.players.size(); ++i) {
- ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i);
+ for (ServerPlayer entityplayer1 : this.players) { // Folia - region threading
if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) {
// Paper start - Add Listing API for Player
@@ -392,7 +474,7 @@ public abstract class PlayerList {
// Paper start - Configurable player collision; Add to collideRule team if needed
final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard();
final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName);
- if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) {
+ if (false && this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { // Folia - region threading
scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam);
}
// Paper end - Configurable player collision
@@ -482,7 +564,7 @@ public abstract class PlayerList {
protected void save(ServerPlayer player) {
if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit
- player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving
+ player.lastSave = System.nanoTime(); // Folia - region threading - changed to nanoTime tracking
this.playerIo.save(player);
ServerStatsCounter serverStatsCounter = player.getStats(); // CraftBukkit
if (serverStatsCounter != null) {
@@ -517,7 +599,7 @@ public abstract class PlayerList {
// CraftBukkit end
// Paper start - Configurable player collision; Remove from collideRule team if needed
- if (this.collideRuleTeamName != null) {
+ if (false && this.collideRuleTeamName != null) { // Folia - region threading
final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard();
final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName);
if (player.getTeam() == team && team != null) {
@@ -566,7 +648,7 @@ public abstract class PlayerList {
}
serverLevel.removePlayerImmediately(player, Entity.RemovalReason.UNLOADED_WITH_PLAYER);
- player.retireScheduler(); // Paper - Folia schedulers
+ //player.retireScheduler(); // Paper - Folia schedulers // Folia - region threading - move to onDisconnect of common packet listener
player.getAdvancements().stopListening();
this.players.remove(player);
this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
@@ -584,8 +666,7 @@ public abstract class PlayerList {
// CraftBukkit start
// this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())));
ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID()));
- for (int i = 0; i < this.players.size(); i++) {
- ServerPlayer otherPlayer = (ServerPlayer) this.players.get(i);
+ for (ServerPlayer otherPlayer : this.players) { // Folia - region threading
if (otherPlayer.getBukkitEntity().canSee(player.getBukkitEntity())) {
otherPlayer.connection.send(packet);
@@ -609,19 +690,12 @@ public abstract class PlayerList {
ServerPlayer entityplayer;
- for (int i = 0; i < this.players.size(); ++i) {
- entityplayer = (ServerPlayer) this.players.get(i);
- if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameProfile.getName()))) { // Paper - validate usernames
- list.add(entityplayer);
- }
- }
+ // Folia - region threading - rewrite login process - moved to pushPendingJoin
java.util.Iterator iterator = list.iterator();
while (iterator.hasNext()) {
- entityplayer = (ServerPlayer) iterator.next();
- this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved
- entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause
+ // Folia - moved to pushPendingJoin
}
// Instead of kicking then returning, we need to store the kick reason
@@ -641,7 +715,7 @@ public abstract class PlayerList {
MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned.reason", userBanListEntry.getReason());
if (userBanListEntry.getExpires() != null) {
mutableComponent.append(
- Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.format(userBanListEntry.getExpires()))
+ Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.get().format(userBanListEntry.getExpires())) // Folia - region threading - SDF is not thread-safe
);
}
@@ -655,7 +729,7 @@ public abstract class PlayerList {
MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipBanListEntry.getReason());
if (ipBanListEntry.getExpires() != null) {
mutableComponent.append(
- Component.translatable("multiplayer.disconnect.banned_ip.expiration", BAN_DATE_FORMAT.format(ipBanListEntry.getExpires()))
+ Component.translatable("multiplayer.disconnect.banned_ip.expiration", BAN_DATE_FORMAT.get().format(ipBanListEntry.getExpires())) // Folia - region threading - SDF is not thread-safe
);
}
@@ -665,7 +739,7 @@ public abstract class PlayerList {
// return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)
// ? Component.translatable("multiplayer.disconnect.server_full")
// : null;
- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)) {
+ if (!this.countConnection(loginlistener.connection, this.maxPlayers) && !this.canBypassPlayerLimit(gameProfile)) { // Folia - region threading - we control connection state here now async, not player list size
event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure
}
}
@@ -714,6 +788,11 @@ public abstract class PlayerList {
return this.respawn(player, keepInventory, reason, eventReason, null);
}
public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, org.bukkit.Location location) {
+ // Folia start - region threading
+ if (true) {
+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading");
+ }
+ // Folia end - region threading
player.stopRiding(); // CraftBukkit
this.players.remove(player);
this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
@@ -884,10 +963,10 @@ public abstract class PlayerList {
public void tick() {
if (++this.sendAllPlayerInfoIn > 600) {
// CraftBukkit start
- for (int i = 0; i < this.players.size(); ++i) {
- final ServerPlayer target = this.players.get(i);
+ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading
+ for (final ServerPlayer target : players) { // Folia - region threading
- target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), com.google.common.collect.Collections2.filter(this.players, t -> target.getBukkitEntity().canSee(t.getBukkitEntity()))));
+ target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), com.google.common.collect.Collections2.filter(java.util.Arrays.asList(players),t -> target.getBukkitEntity().canSee(t.getBukkitEntity())))); // Folia - region threading
}
// CraftBukkit end
this.sendAllPlayerInfoIn = 0;
@@ -896,18 +975,17 @@ public abstract class PlayerList {
// CraftBukkit start - add a world/entity limited version
public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) {
- for (int i = 0; i < this.players.size(); ++i) {
- ServerPlayer entityplayer = this.players.get(i);
+ for (ServerPlayer entityplayer : this.players) { // Folia - region threading
if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) {
continue;
}
- ((ServerPlayer) this.players.get(i)).connection.send(packet);
+ entityplayer.connection.send(packet); // Folia - region threading
}
}
public void broadcastAll(Packet packet, Level world) {
- for (int i = 0; i < world.players().size(); ++i) {
- ((ServerPlayer) world.players().get(i)).connection.send(packet);
+ for (net.minecraft.world.entity.player.Player player : world.players()) { // Folia - region threading
+ ((ServerPlayer) player).connection.send(packet); // Folia - region threading
}
}
@@ -944,8 +1022,7 @@ public abstract class PlayerList {
if (team == null) {
this.broadcastSystemMessage(message, false);
} else {
- for (int i = 0; i < this.players.size(); i++) {
- ServerPlayer serverPlayer = this.players.get(i);
+ for (ServerPlayer serverPlayer : this.players) { // Folia - region threading
if (serverPlayer.getTeam() != team) {
serverPlayer.sendSystemMessage(message);
}
@@ -954,10 +1031,11 @@ public abstract class PlayerList {
}
public String[] getPlayerNamesArray() {
+ List<ServerPlayer> players = new java.util.ArrayList<>(this.players); // Folia - region threading
String[] strings = new String[this.players.size()];
- for (int i = 0; i < this.players.size(); i++) {
- strings[i] = this.players.get(i).getGameProfile().getName();
+ for (int i = 0; i < players.size(); i++) { // Folia - region threading
+ strings[i] = players.get(i).getGameProfile().getName(); // Folia - region threading
}
return strings;
@@ -975,7 +1053,9 @@ public abstract class PlayerList {
this.ops.add(new ServerOpListEntry(profile, this.server.getOperatorUserPermissionLevel(), this.ops.canBypassPlayerLimit(profile)));
ServerPlayer player = this.getPlayer(profile.getId());
if (player != null) {
- this.sendPlayerPermissionLevel(player);
+ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading
+ this.sendPlayerPermissionLevel(serverPlayer); // Folia - region threading
+ }, null, 1L); // Folia - region threading
}
}
@@ -983,7 +1063,9 @@ public abstract class PlayerList {
this.ops.remove(profile);
ServerPlayer player = this.getPlayer(profile.getId());
if (player != null) {
- this.sendPlayerPermissionLevel(player);
+ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading
+ this.sendPlayerPermissionLevel(serverPlayer); // Folia - region threading
+ }, null, 1L); // Folia - region threading
}
}
@@ -1046,8 +1128,7 @@ public abstract class PlayerList {
}
public void broadcast(@Nullable Player except, double x, double y, double z, double radius, ResourceKey<Level> dimension, Packet<?> packet) {
- for (int i = 0; i < this.players.size(); i++) {
- ServerPlayer serverPlayer = this.players.get(i);
+ for (ServerPlayer serverPlayer : this.players) { // Folia - region threading
// CraftBukkit start - Test if player receiving packet can see the source of the packet
if (except != null && !serverPlayer.getBukkitEntity().canSee(except.getBukkitEntity())) {
continue;
@@ -1072,10 +1153,15 @@ public abstract class PlayerList {
public void saveAll(final int interval) {
io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main
int numSaved = 0;
- final long now = MinecraftServer.currentTick;
- for (int i = 0; i < this.players.size(); i++) {
- final ServerPlayer player = this.players.get(i);
- if (interval == -1 || now - player.lastSave >= interval) {
+ final long now = System.nanoTime(); // Folia - region threading
+ long timeInterval = (long)interval * io.papermc.paper.threadedregions.TickRegionScheduler.TIME_BETWEEN_TICKS; // Folia - region threading
+ for (final ServerPlayer player : this.players) { // Folia - region threading
+ // Folia start - region threading
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) {
+ continue;
+ }
+ // Folia end - region threading
+ if (interval == -1 || now - player.lastSave >= timeInterval) { // Folia - region threading
this.save(player);
if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) {
break;
@@ -1194,6 +1280,20 @@ public abstract class PlayerList {
}
public void removeAll(boolean isRestarting) {
+ // Folia start - region threading
+ // just send disconnect packet, don't modify state
+ for (ServerPlayer player : this.players) {
+ final Component shutdownMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure
+ // CraftBukkit end
+
+ player.connection.send(new net.minecraft.network.protocol.common.ClientboundDisconnectPacket(shutdownMessage), net.minecraft.network.PacketSendListener.thenRun(() -> {
+ player.connection.connection.disconnect(shutdownMessage);
+ }));
+ }
+ if (true) {
+ return;
+ }
+ // Folia end - region threading
// Paper end
// CraftBukkit start - disconnect safely
for (ServerPlayer player : this.players) {
@@ -1203,7 +1303,7 @@ public abstract class PlayerList {
// CraftBukkit end
// Paper start - Configurable player collision; Remove collideRule team if it exists
- if (this.collideRuleTeamName != null) {
+ if (false && this.collideRuleTeamName != null) { // Folia - region threading
final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard();
final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName);
if (team != null) scoreboard.removePlayerTeam(team);
diff --git a/net/minecraft/server/players/StoredUserList.java b/net/minecraft/server/players/StoredUserList.java
index d445e8f126f077d8419c52fa5436ea963a1a42a4..cabbc68f7cd5fd326c7ffd3b02b3ec4a4390f5b0 100644
--- a/net/minecraft/server/players/StoredUserList.java
+++ b/net/minecraft/server/players/StoredUserList.java
@@ -97,6 +97,7 @@ public abstract class StoredUserList<K, V extends StoredUserEntry<K>> {
}
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<K, V extends StoredUserEntry<K>> {
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<K, V extends StoredUserEntry<K>> {
}
// Spigot end
}
+ } // Folia - region threading
}
}
diff --git a/net/minecraft/util/SpawnUtil.java b/net/minecraft/util/SpawnUtil.java
index f6fad24af884c8a37723c57718cee0096443efe6..ef75a71aee2576254e2c0752cc759c9637af9d69 100644
--- a/net/minecraft/util/SpawnUtil.java
+++ b/net/minecraft/util/SpawnUtil.java
@@ -83,7 +83,7 @@ public class SpawnUtil {
return Optional.of(mob);
}
- mob.discard(null); // CraftBukkit - add Bukkit remove cause
+ //mob.discard(null); // CraftBukkit - add Bukkit remove cause // Folia - region threading
}
}
}
diff --git a/net/minecraft/world/RandomSequences.java b/net/minecraft/world/RandomSequences.java
index f8e93fe461794058a26c90510cbd7698fa43b8f7..c1a62556546b05f79dad37875993c19bf9bb7c67 100644
--- a/net/minecraft/world/RandomSequences.java
+++ b/net/minecraft/world/RandomSequences.java
@@ -21,7 +21,7 @@ public class RandomSequences extends SavedData {
private int salt;
private boolean includeWorldSeed = true;
private boolean includeSequenceId = true;
- private final Map<ResourceLocation, RandomSequence> sequences = new Object2ObjectOpenHashMap<>();
+ private final Map<ResourceLocation, RandomSequence> sequences = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading
public static SavedData.Factory<RandomSequences> factory(long seed) {
return new SavedData.Factory<>(
@@ -120,61 +120,61 @@ public class RandomSequences extends SavedData {
@Override
public RandomSource fork() {
RandomSequences.this.setDirty();
- return this.random.fork();
+ synchronized (this.random) { return this.random.fork(); } // Folia - region threading
}
@Override
public PositionalRandomFactory forkPositional() {
RandomSequences.this.setDirty();
- return this.random.forkPositional();
+ synchronized (this.random) { return this.random.forkPositional(); } // Folia - region threading
}
@Override
public void setSeed(long seed) {
RandomSequences.this.setDirty();
- this.random.setSeed(seed);
+ synchronized (this.random) { this.random.setSeed(seed); } // Folia - region threading
}
@Override
public int nextInt() {
RandomSequences.this.setDirty();
- return this.random.nextInt();
+ synchronized (this.random) { return this.random.nextInt(); } // Folia - region threading
}
@Override
public int nextInt(int bound) {
RandomSequences.this.setDirty();
- return this.random.nextInt(bound);
+ synchronized (this.random) { return this.random.nextInt(bound); } // Folia - region threading
}
@Override
public long nextLong() {
RandomSequences.this.setDirty();
- return this.random.nextLong();
+ synchronized (this.random) { return this.random.nextLong(); } // Folia - region threading
}
@Override
public boolean nextBoolean() {
RandomSequences.this.setDirty();
- return this.random.nextBoolean();
+ synchronized (this.random) { return this.random.nextBoolean(); } // Folia - region threading
}
@Override
public float nextFloat() {
RandomSequences.this.setDirty();
- return this.random.nextFloat();
+ synchronized (this.random) { return this.random.nextFloat(); } // Folia - region threading
}
@Override
public double nextDouble() {
RandomSequences.this.setDirty();
- return this.random.nextDouble();
+ synchronized (this.random) { return this.random.nextDouble(); } // Folia - region threading
}
@Override
public double nextGaussian() {
RandomSequences.this.setDirty();
- return this.random.nextGaussian();
+ synchronized (this.random) { return this.random.nextGaussian(); } // Folia - region threading
}
@Override
diff --git a/net/minecraft/world/damagesource/CombatTracker.java b/net/minecraft/world/damagesource/CombatTracker.java
index d3de87eaf0eb84af77165391c7b94085d425f21d..019d1435cfe7769ed85b40c1c19d2d271d96f913 100644
--- a/net/minecraft/world/damagesource/CombatTracker.java
+++ b/net/minecraft/world/damagesource/CombatTracker.java
@@ -53,7 +53,7 @@ public class CombatTracker {
}
private Component getMessageForAssistedFall(Entity entity, Component entityDisplayName, String hasWeaponTranslationKey, String noWeaponTranslationKey) {
- ItemStack itemStack = entity instanceof LivingEntity livingEntity ? livingEntity.getMainHandItem() : ItemStack.EMPTY;
+ ItemStack itemStack = entity instanceof LivingEntity livingEntity && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity) ? livingEntity.getMainHandItem() : ItemStack.EMPTY; // Folia - region threading
return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME)
? Component.translatable(hasWeaponTranslationKey, this.mob.getDisplayName(), entityDisplayName, itemStack.getDisplayName())
: Component.translatable(noWeaponTranslationKey, this.mob.getDisplayName(), entityDisplayName);
@@ -80,7 +80,7 @@ public class CombatTracker {
@Nullable
private static Component getDisplayName(@Nullable Entity entity) {
- return entity == null ? null : entity.getDisplayName();
+ return entity == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) ? null : entity.getDisplayName(); // Folia - region threading
}
public Component getDeathMessage() {
diff --git a/net/minecraft/world/damagesource/DamageSource.java b/net/minecraft/world/damagesource/DamageSource.java
index daeb344f9e21e8155326b47aeccd4cfc07da42cd..9a204bbd91f48a3097471d1273d65c185b353fcc 100644
--- a/net/minecraft/world/damagesource/DamageSource.java
+++ b/net/minecraft/world/damagesource/DamageSource.java
@@ -163,12 +163,12 @@ public class DamageSource {
if (this.causingEntity == null && this.directEntity == null) {
LivingEntity killCredit = livingEntity.getKillCredit();
String string1 = string + ".player";
- return killCredit != null
+ return killCredit != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(killCredit)
? Component.translatable(string1, livingEntity.getDisplayName(), killCredit.getDisplayName())
: Component.translatable(string, livingEntity.getDisplayName());
} else {
Component component = this.causingEntity == null ? this.directEntity.getDisplayName() : this.causingEntity.getDisplayName();
- ItemStack itemStack = this.causingEntity instanceof LivingEntity livingEntity1 ? livingEntity1.getMainHandItem() : ItemStack.EMPTY;
+ ItemStack itemStack = this.causingEntity instanceof LivingEntity livingEntity1 && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity1) ? livingEntity1.getMainHandItem() : ItemStack.EMPTY; // Folia - region threading
return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME)
? Component.translatable(string + ".item", livingEntity.getDisplayName(), component, itemStack.getDisplayName())
: Component.translatable(string, livingEntity.getDisplayName(), component);
diff --git a/net/minecraft/world/damagesource/FallLocation.java b/net/minecraft/world/damagesource/FallLocation.java
index a3c7d68469075bf8d33f2016149a181b0fb87e0e..73c581d3ee21d8fa96eae3e47afd6ce204e03160 100644
--- a/net/minecraft/world/damagesource/FallLocation.java
+++ b/net/minecraft/world/damagesource/FallLocation.java
@@ -35,7 +35,7 @@ public record FallLocation(String id) {
@Nullable
public static FallLocation getCurrentFallLocation(LivingEntity entity) {
Optional<BlockPos> 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
2025-03-08 06:39:17 -08:00
index b2b61203438bb1fad1ee807729781718d2467155..2cd2ce1060f567be6c72b7bc9d02651ec7166203 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -145,7 +145,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
// Paper start - Share random for entities to make them more random
- public static RandomSource SHARED_RANDOM = new RandomRandomSource();
+ public static RandomSource SHARED_RANDOM = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading
// Paper start - replace random
private static final class RandomRandomSource extends ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom {
public RandomRandomSource() {
@@ -175,7 +175,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason
public boolean collisionLoadChunks = false; // Paper
- private @Nullable org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity;
+ private volatile @Nullable org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity; // Folia - region threading
public org.bukkit.craftbukkit.entity.CraftEntity getBukkitEntity() {
if (this.bukkitEntity == null) {
@@ -294,7 +294,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
private boolean hasGlowingTag;
private final Set<String> 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;
2025-02-16 12:23:20 -08:00
@@ -522,6 +522,23 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
}
// Paper end - optimise entity tracker
+ // Folia start - region ticking
+ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) {
+ if (this.activatedTick != Integer.MIN_VALUE) {
+ this.activatedTick += fromTickOffset;
+ }
+ if (this.activatedImmunityTick != Integer.MIN_VALUE) {
+ this.activatedImmunityTick += fromTickOffset;
+ }
+ if (this.pistonDeltasGameTime != Long.MIN_VALUE) {
+ this.pistonDeltasGameTime += fromRedstoneTimeOffset;
+ }
+ }
+
+ public boolean canBeSpectated() {
+ return !this.getBukkitEntity().taskScheduler.isRetired();
+ }
+ // Folia end - region ticking
public Entity(EntityType<?> entityType, Level level) {
this.type = entityType;
2025-02-16 12:23:20 -08:00
@@ -652,8 +669,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
// due to interactions on the client.
public void resendPossiblyDesyncedEntityData(net.minecraft.server.level.ServerPlayer player) {
if (player.getBukkitEntity().canSee(this.getBukkitEntity())) {
- ServerLevel world = (net.minecraft.server.level.ServerLevel)this.level();
- net.minecraft.server.level.ChunkMap.TrackedEntity tracker = world == null ? null : world.getChunkSource().chunkMap.entityMap.get(this.getId());
+ net.minecraft.server.level.ChunkMap.TrackedEntity tracker = this.moonrise$getTrackedEntity(); // Folia - region threading
if (tracker == null) {
return;
}
2025-02-16 12:23:20 -08:00
@@ -820,7 +836,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
public void postTick() {
// No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle
if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities
- this.handlePortal();
+ //this.handlePortal(); // Folia - region threading
}
}
// CraftBukkit end
2025-02-16 12:23:20 -08:00
@@ -838,7 +854,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.boardingCooldown--;
}
- if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick
+ //if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick // Folia - region threading - ONLY allow in postTick()
if (this.canSpawnSprintParticle()) {
this.spawnSprintParticle();
}
2025-02-16 12:23:20 -08:00
@@ -1101,8 +1117,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
} else {
this.wasOnFire = this.isOnFire();
if (type == MoverType.PISTON) {
- this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper - EAR 2
- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper - EAR 2
+ this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading
+ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading
movement = this.limitPistonMovement(movement);
if (movement.equals(Vec3.ZERO)) {
return;
2025-02-16 12:23:20 -08:00
@@ -1401,7 +1417,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
if (pos.lengthSqr() <= 1.0E-7) {
return pos;
} else {
- long gameTime = this.level().getGameTime();
+ long gameTime = this.level().getRedstoneGameTime(); // Folia - region threading
if (gameTime != this.pistonDeltasGameTime) {
Arrays.fill(this.pistonDeltas, 0.0);
this.pistonDeltasGameTime = gameTime;
2025-02-16 12:23:20 -08:00
@@ -3035,6 +3051,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
if (force || this.canRide(vehicle) && vehicle.canAddPassenger(this)) {
+ if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen
// CraftBukkit start
if (vehicle.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && this.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) {
org.bukkit.event.vehicle.VehicleEnterEvent event = new org.bukkit.event.vehicle.VehicleEnterEvent((org.bukkit.entity.Vehicle) vehicle.getBukkitEntity(), this.getBukkitEntity());
2025-02-16 12:23:20 -08:00
@@ -3056,6 +3073,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return false;
}
// CraftBukkit end
+ } // Folia - region threading - suppress entire event logic during worldgen
if (this.isPassenger()) {
this.stopRiding();
}
2025-02-16 12:23:20 -08:00
@@ -3123,7 +3141,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.passengers = ImmutableList.copyOf(list);
}
- this.gameEvent(GameEvent.ENTITY_MOUNT, passenger);
+ if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); // Folia - region threading - do not fire game events for entities not added
}
}
2025-02-16 12:23:20 -08:00
@@ -3137,6 +3155,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
throw new IllegalStateException("Use x.stopRiding(y), not y.removePassenger(x)");
} else {
// CraftBukkit start
+ if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen
org.bukkit.craftbukkit.entity.CraftEntity craft = (org.bukkit.craftbukkit.entity.CraftEntity) passenger.getBukkitEntity().getVehicle();
Entity orig = craft == null ? null : craft.getHandle();
if (this.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && passenger.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) {
2025-02-16 12:23:20 -08:00
@@ -3164,6 +3183,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return false;
}
// CraftBukkit end
+ } // Folia - region threading - suppress entire event logic during worldgen
if (this.passengers.size() == 1 && this.passengers.get(0) == passenger) {
this.passengers = ImmutableList.of();
} else {
2025-02-16 12:23:20 -08:00
@@ -3171,7 +3191,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
passenger.boardingCooldown = 60;
- this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger);
+ if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger); // Folia - region threading - do not fire game events for entities not added
}
return true; // CraftBukkit
}
2025-02-16 12:23:20 -08:00
@@ -3255,7 +3275,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
}
- protected void handlePortal() {
+ public boolean handlePortal() { // Folia - region threading - public, ret type -> boolean
if (this.level() instanceof ServerLevel serverLevel) {
this.processPortalCooldown();
if (this.portalProcess != null) {
2025-02-16 12:23:20 -08:00
@@ -3263,21 +3283,20 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
ProfilerFiller profilerFiller = Profiler.get();
profilerFiller.push("portal");
this.setPortalCooldown();
- TeleportTransition portalDestination = this.portalProcess.getPortalDestination(serverLevel, this);
- if (portalDestination != null) {
- ServerLevel level = portalDestination.newLevel();
- if (this instanceof ServerPlayer // CraftBukkit - always call event for players
- || (level != null && (level.dimension() == serverLevel.dimension() || this.canTeleport(serverLevel, level)))) { // CraftBukkit
- this.teleport(portalDestination);
- }
+ // Folia start - region threading
+ try {
+ return this.portalProcess.portalAsync(serverLevel, this);
+ } finally {
+ profilerFiller.pop();
}
-
- profilerFiller.pop();
+ // Folia end - region threading
} else if (this.portalProcess.hasExpired()) {
this.portalProcess = null;
}
}
}
+
+ return false; // Folia - region threading
}
public int getDimensionChangingDelay() {
2025-02-16 12:23:20 -08:00
@@ -3417,6 +3436,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
@Nullable
public PlayerTeam getTeam() {
+ // Folia start - region threading
+ if (true) {
+ return null;
+ }
+ // Folia end - region threading
if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper - Perf: Disable Scoreboards for non players by default
return this.level().getScoreboard().getPlayersTeam(this.getScoreboardName());
}
2025-02-16 12:23:20 -08:00
@@ -3723,8 +3747,793 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.portalProcess = entity.portalProcess;
}
+ // Folia start - region threading
+ public static class EntityTreeNode {
+ @Nullable
+ public EntityTreeNode parent;
+ public Entity root;
+ @Nullable
+ public EntityTreeNode[] passengers;
+
+ public EntityTreeNode(EntityTreeNode parent, Entity root) {
+ this.parent = parent;
+ this.root = root;
+ }
+
+ public EntityTreeNode(EntityTreeNode parent, Entity root, EntityTreeNode[] passengers) {
+ this.parent = parent;
+ this.root = root;
+ this.passengers = passengers;
+ }
+
+ public List<EntityTreeNode> getFullTree() {
+ List<EntityTreeNode> 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<EntityTreeNode> queue = new java.util.ArrayDeque<>();
+ queue.add(this);
+
+ EntityTreeNode curr;
+ while ((curr = queue.pollFirst()) != null) {
+ EntityTreeNode[] passengers = curr.passengers;
+ if (passengers == null) {
+ continue;
+ }
+
+ List<Entity> newPassengers = new java.util.ArrayList<>();
+ for (EntityTreeNode passenger : passengers) {
+ newPassengers.add(passenger.root);
+ passenger.root.vehicle = curr.root;
+ }
+
+ curr.root.passengers = ImmutableList.copyOf(newPassengers);
+ }
+ }
+
+ public void addTracker() {
+ for (final EntityTreeNode node : this.getFullTree()) {
+ if (node.root.moonrise$getTrackedEntity() != null) {
+ for (final ServerPlayer player : node.root.level.getLocalPlayers()) {
+ node.root.moonrise$getTrackedEntity().updatePlayer(player);
+ }
+ }
+ }
+ }
+
+ public void clearTracker() {
+ for (final EntityTreeNode node : this.getFullTree()) {
+ if (node.root.moonrise$getTrackedEntity() != null) {
+ node.root.moonrise$getTrackedEntity().moonrise$removeNonTickThreadPlayers();
+ for (final ServerPlayer player : node.root.level.getLocalPlayers()) {
+ node.root.moonrise$getTrackedEntity().removePlayer(player);
+ }
+ }
+ }
+ }
+
+ public void adjustRiders(boolean teleport) {
+ java.util.ArrayDeque<EntityTreeNode> queue = new java.util.ArrayDeque<>();
+ queue.add(this);
+
+ EntityTreeNode curr;
+ while ((curr = queue.pollFirst()) != null) {
+ EntityTreeNode[] passengers = curr.passengers;
+ if (passengers == null) {
+ continue;
+ }
+
+ for (EntityTreeNode passenger : passengers) {
+ curr.root.positionRider(passenger.root, teleport ? Entity::moveTo : Entity::setPos);
+ }
+ }
+ }
+ }
+
+ public void repositionAllPassengers(boolean teleport) {
+ this.makePassengerTree().adjustRiders(teleport);
+ }
+
+ protected EntityTreeNode makePassengerTree() {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot read passengers off of the main thread");
+
+ EntityTreeNode root = new EntityTreeNode(null, this);
+ java.util.ArrayDeque<EntityTreeNode> queue = new java.util.ArrayDeque<>();
+ queue.add(root);
+ EntityTreeNode curr;
+ while ((curr = queue.pollFirst()) != null) {
+ Entity vehicle = curr.root;
+ List<Entity> 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<EntityTreeNode> queue = new java.util.ArrayDeque<>();
+ queue.add(root);
+ EntityTreeNode curr;
+ while ((curr = queue.pollFirst()) != null) {
+ Entity vehicle = curr.root;
+ List<Entity> 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<Entity> teleportComplete) {
+ Vec3 pos = this.position();
+ ChunkPos posChunk = new ChunkPos(
+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos),
+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos)
+ );
+
+ // ensure the region is always ticking in case of a shutdown
+ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region
+ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement());
+ originWorld.chunkSource.addTicketAtLevel(
+ TicketType.TELEPORT_HOLD_TICKET, posChunk,
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
+ teleportHoldId
+ );
+ final ServerLevel.PendingTeleport pendingTeleport = new ServerLevel.PendingTeleport(passengerTree, pos);
+ destination.pushPendingTeleport(pendingTeleport);
+
+ Runnable scheduleEntityJoin = () -> {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
+ destination,
+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos),
+ () -> {
+ if (!destination.removePendingTeleport(pendingTeleport)) {
+ // shutdown logic placed the entity already, and we are shutting down - do nothing to ensure
+ // we do not produce any errors here
+ return;
+ }
+ originWorld.chunkSource.removeTicketAtLevel(
+ TicketType.TELEPORT_HOLD_TICKET, posChunk,
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
+ teleportHoldId
+ );
+ List<EntityTreeNode> fullTree = passengerTree.getFullTree();
+ for (EntityTreeNode node : fullTree) {
+ node.root.placeSingleSync(originWorld, destination, node, teleportFlags);
+ }
+
+ // restore passenger tree
+ passengerTree.restore();
+ passengerTree.adjustRiders(true);
+
+ // invoke post dimension change now
+ for (EntityTreeNode node : fullTree) {
+ node.root.postChangeDimension();
+ }
+
+ if (teleportComplete != null) {
+ teleportComplete.accept(Entity.this);
+ }
+ }
+ );
+ };
+
+ if ((teleportFlags & TELEPORT_FLAG_LOAD_CHUNK) != 0L) {
+ destination.loadChunksForMoveAsync(
+ this.getBoundingBox(), ca.spottedleaf.concurrentutil.util.Priority.HIGHER,
+ (chunkList) -> {
+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunkList) {
+ destination.chunkSource.addTicketAtLevel(
+ TicketType.POST_TELEPORT, chunk.getPos(),
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL,
+ Integer.valueOf(Entity.this.getId())
+ );
+ }
+ scheduleEntityJoin.run();
+ }
+ );
+ } else {
+ scheduleEntityJoin.run();
+ }
+ }
+
+ protected boolean canTeleportAsync() {
+ return !this.hasNullCallback() && !this.isRemoved() && this.isAlive() && (!(this instanceof net.minecraft.world.entity.LivingEntity livingEntity) || !livingEntity.isSleeping());
+ }
+
+ // Mojang for whatever reason has started storing positions to cache certain physics properties that entities collide with
+ // As usual though, they don't properly do anything to prevent serious desync with respect to the current entity position
+ // We add additional logic to reset these before teleporting to prevent issues with them possibly tripping thread checks.
+ protected void resetStoredPositions() {
+ this.mainSupportingBlockPos = Optional.empty();
+ // this is copied from teleportSetPosition
+ this.movementThisTick.clear();
+ }
+
+ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) {
+ if (yaw != null) {
+ this.setYRot(yaw.floatValue());
+ this.setYHeadRot(yaw.floatValue());
+ }
+ if (pitch != null) {
+ this.setXRot(pitch.floatValue());
+ }
+ if (velocity != null) {
+ this.setDeltaMovement(velocity);
+ }
+ this.moveTo(pos.x, pos.y, pos.z);
+ this.setOldPosAndRot();
+ this.resetStoredPositions();
+ }
+
+ protected final void transform(TeleportTransition telpeort) {
+ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute(
+ PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives()
+ );
+ this.transform(
+ move.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement()
+ );
+ }
+
+ protected void transform(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) {
+ if (yaw != null) {
+ this.setYRot(yaw.floatValue());
+ this.setYHeadRot(yaw.floatValue());
+ }
+ if (pitch != null) {
+ this.setXRot(pitch.floatValue());
+ }
+ if (velocity != null) {
+ this.setDeltaMovement(velocity);
+ }
+ if (pos != null) {
+ this.setPosRaw(pos.x, pos.y, pos.z);
+ }
+ this.setOldPosAndRot();
+ }
+
+ protected final Entity transformForAsyncTeleport(TeleportTransition telpeort) {
+ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute(
+ PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives()
+ );
+ return this.transformForAsyncTeleport(
+ telpeort.newLevel(), telpeort.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement()
+ );
+ }
+
+ protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) {
+ this.removeAfterChangingDimensions(); // remove before so that any CBEntity#getHandle call affects this entity before copying
+
+ Entity copy = this.getType().create(destination, EntitySpawnReason.DIMENSION_TRAVEL);
+ copy.restoreFrom(this);
+ copy.transform(pos, yaw, pitch, velocity);
+ // vanilla code used to call remove _after_ copying, and some stuff is required to be after copy - so add hook here
+ // for example, clearing of inventory after switching dimensions
+ this.postRemoveAfterChangingDimensions();
+
+ return copy;
+ }
+
+ public final boolean teleportAsync(TeleportTransition teleportTarget, long teleportFlags,
+ java.util.function.Consumer<Entity> 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<Entity> 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<EntityTreeNode> 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<TeleportTransition> 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<BlockUtil.FoundRectangle> portalFound
+ = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>();
+
+ // post portal find/create logic
+ portalFound.addWaiter(
+ (BlockUtil.FoundRectangle portal, Throwable thr) -> {
+ // no portal could be created
+ if (portal == null) {
+ portalInfoCompletable.complete(
+ new TeleportTransition(destination, Vec3.atCenterOf(targetPos), Vec3.ZERO,
+ 90.0f, 0.0f,
+ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET))
+ );
+ return;
+ }
+
+ Vec3 relativePos = originalPortalRectangle == null ?
+ new Vec3(0.5, 0.0, 0.0) :
+ Entity.this.getRelativePortalPosition(originalPortalDirection, originalPortalRectangle);
+
+ portalInfoCompletable.complete(
+ net.minecraft.world.level.block.NetherPortalBlock.createDimensionTransition(
+ destination, portal, originalPortalDirection, relativePos,
+ Entity.this, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET)
+ )
+ );
+ }
+ );
+
+ // kick off search for existing portal or creation
+ destination.moonrise$loadChunksAsync(
+ // add 32 so that the final search for a portal frame doesn't load any chunks
+ targetPos, portalSearchRadius + 32,
+ net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY,
+ ca.spottedleaf.concurrentutil.util.Priority.HIGH,
+ (chunks) -> {
+ BlockUtil.FoundRectangle portal =
+ net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius);
+ if (portal != null) {
+ portalFound.complete(portal);
+ return;
+ }
+
+ // add tickets so that we can re-search for a portal once the chunks are loaded
+ Long ticketId = Long.valueOf(CREATE_PORTAL_DOUBLE_CHECK.getAndIncrement());
+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) {
+ destination.chunkSource.addTicketAtLevel(
+ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(),
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
+ ticketId
+ );
+ }
+
+ // no portal found - create one
+ destination.moonrise$loadChunksAsync(
+ targetPos, portalCreateRadius + 32,
+ ca.spottedleaf.concurrentutil.util.Priority.HIGH,
+ (chunks2) -> {
+ // don't need the tickets anymore
+ // note: we expect removeTicketsAtLevel to add an unknown ticket for us automatically
+ // if the ticket level were to decrease
+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) {
+ destination.chunkSource.removeTicketAtLevel(
+ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(),
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
+ ticketId
+ );
+ }
+
+ // when two entities portal at the same time, it is possible that both entities reach this
+ // part of the code - and create a double portal
+ // to fix this, we just issue another search to try and see if another entity created
+ // a portal nearby
+ BlockUtil.FoundRectangle existingTryAgain =
+ net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius);
+ if (existingTryAgain != null) {
+ portalFound.complete(existingTryAgain);
+ return;
+ }
+
+ // we do not have the correct entity reference here
+ BlockUtil.FoundRectangle createdPortal =
+ destination.getPortalForcer().createPortal(targetPos, originalPortalDirection, null, portalCreateRadius).orElse(null);
+ // if it wasn't created, passing null is expected here
+ portalFound.complete(createdPortal);
+ }
+ );
+ }
+ );
+ break;
+ }
+ default: {
+ throw new IllegalStateException("Unknown portal type " + type);
+ }
+ }
+ }
+
+ public boolean canPortalAsync(ServerLevel to, boolean considerPassengers) {
+ return this.canPortalAsync(to, considerPassengers, false);
+ }
+
+ protected boolean canPortalAsync(ServerLevel to, boolean considerPassengers, boolean skipPassengerCheck) {
+ if (considerPassengers) {
+ if (!skipPassengerCheck && this.isPassenger()) {
+ return false;
+ }
+ } else {
+ if (this.isVehicle() || (!skipPassengerCheck && this.isPassenger())) {
+ return false;
+ }
+ }
+ this.getBukkitEntity(); // force bukkit entity to be created before TPing
+ if (!this.canTeleportAsync()) {
+ return false;
+ }
+ if (considerPassengers) {
+ for (Entity entity : this.passengers) {
+ if (!entity.canPortalAsync(to, true, true)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) {
+
+ }
+
+ protected boolean portalToAsync(ServerLevel destination, BlockPos portalPos, boolean takePassengers,
+ PortalType type, java.util.function.Consumer<Entity> 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<EntityTreeNode> fullPassengerTree = passengerTree.getFullTree();
+ ServerLevel originWorld = (ServerLevel)this.level;
+
+ for (EntityTreeNode node : fullPassengerTree) {
+ node.root.preChangeDimension();
+ node.root.prePortalLogic(originWorld, destination, type);
+ }
+
+ for (EntityTreeNode node : fullPassengerTree) {
+ // we will update pos/rot/speed later
+ node.root = node.root.transformForAsyncTeleport(destination, null, null, null, null);
+ // set portal cooldown
+ node.root.setPortalCooldown();
+ }
+
+ // ensure the region is always ticking in case of a shutdown
+ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region
+ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement());
+ originWorld.chunkSource.addTicketAtLevel(
+ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk,
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
+ teleportHoldId
+ );
+
+ ServerLevel.PendingTeleport beforeFindDestination = new ServerLevel.PendingTeleport(passengerTree, initialPosition);
+ originWorld.pushPendingTeleport(beforeFindDestination);
+
+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable<TeleportTransition> portalInfoCompletable
+ = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>();
+
+ portalInfoCompletable.addWaiter((TeleportTransition info, Throwable throwable) -> {
+ if (!originWorld.removePendingTeleport(beforeFindDestination)) {
+ // the shutdown thread has placed us back into the origin world at the original position
+ // we just have to abandon this teleport to prevent duplication
+ return;
+ }
+ originWorld.chunkSource.removeTicketAtLevel(
+ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk,
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL,
+ teleportHoldId
+ );
+ // adjust passenger tree to final pos/rot/speed
+ for (EntityTreeNode node : fullPassengerTree) {
+ node.root.transform(info);
+ }
+
+ // place
+ passengerTree.root.placeInAsync(
+ originWorld, destination, Entity.TELEPORT_FLAG_LOAD_CHUNK | (takePassengers ? Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS : 0L),
+ passengerTree,
+ (Entity teleported) -> {
+ if (info.postTeleportTransition() != null) {
+ info.postTeleportTransition().onTransition(teleported);
+ }
+
+ if (teleportComplete != null) {
+ teleportComplete.accept(teleported);
+ }
+ }
+ );
+ });
+
+
+ passengerTree.root.findOrCreatePortalAsync(originWorld, portalPos, destination, type, portalInfoCompletable);
+
+ return true;
+ }
+ // Folia end - region threading
+
@Nullable
public Entity teleport(TeleportTransition teleportTransition) {
+ // Folia start - region threading
+ if (true) {
+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading");
+ }
+ // Folia end - region threading
// Paper start - Fix item duplication and teleport issues
if ((!this.isAlive() || !this.valid) && (teleportTransition.newLevel() != this.level)) {
LOGGER.warn("Illegal Entity Teleport " + this + " to " + teleportTransition.newLevel() + ":" + teleportTransition.position(), new Throwable());
2025-02-16 12:23:20 -08:00
@@ -3908,6 +4717,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
}
+ // Folia start - region threading - move inventory clearing until after the dimension change
+ protected void postRemoveAfterChangingDimensions() {
+
+ }
+ // Folia end - region threading - move inventory clearing until after the dimension change
+
protected void removeAfterChangingDimensions() {
this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION, null); // CraftBukkit - add Bukkit remove cause
if (this instanceof Leashable leashable && leashable.isLeashed()) { // Paper - only call if it is leashed
2025-02-16 12:23:20 -08:00
@@ -4243,6 +5058,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
public void startSeenByPlayer(ServerPlayer serverPlayer) {
+ // Folia start - region threading
+ if (serverPlayer.getCamera() == this) {
+ // set camera again
+ serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCameraPacket(this));
+ }
+ // Folia end - region threading
}
public void stopSeenByPlayer(ServerPlayer serverPlayer) {
2025-02-16 12:23:20 -08:00
@@ -4252,6 +5073,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
new io.papermc.paper.event.player.PlayerUntrackEntityEvent(serverPlayer.getBukkitEntity(), this.getBukkitEntity()).callEvent();
}
// Paper end - entity tracking events
+ // Folia start - region threading
+ if (serverPlayer.getCamera() == this) {
+ // unset camera, the player tick method should TP us close enough again to invoke startSeenByPlayer
+ serverPlayer.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCameraPacket(serverPlayer));
+ }
+ // Folia end - region threading
}
public float rotate(Rotation transformRotation) {
2025-02-16 12:23:20 -08:00
@@ -4787,7 +5614,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
}
// Paper end - Fix MC-4
- if (this.position.x != x || this.position.y != y || this.position.z != z) {
+ boolean posChanged = this.position.x != x || this.position.y != y || this.position.z != z;
+ if (posChanged) { // Folia - region threading
synchronized (this.posLock) { // Paper - detailed watchdog information
this.position = new Vec3(x, y, z);
} // Paper - detailed watchdog information
2025-02-16 12:23:20 -08:00
@@ -4806,7 +5634,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
}
// Paper start - Block invalid positions and bounding box; don't allow desync of pos and AABB
// hanging has its own special logic
- if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || this.position.x != x || this.position.y != y || this.position.z != z)) {
+ if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || posChanged)) {
this.setBoundingBox(this.makeBoundingBox());
}
// Paper end - Block invalid positions and bounding box
2025-02-16 12:23:20 -08:00
@@ -4890,6 +5718,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
return this.removalReason != null;
}
+ // Folia start - region threading
+ public final boolean hasNullCallback() {
+ return this.levelCallback == EntityInLevelCallback.NULL;
+ }
+ // Folia end - region threading
+
@Nullable
public Entity.RemovalReason getRemovalReason() {
return this.removalReason;
2025-02-16 12:23:20 -08:00
@@ -4912,6 +5746,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause);
// CraftBukkit end
final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers
+ // Folia start - region threading
+ this.preRemove(removalReason);
+ // Folia end - region threading
if (this.removalReason == null) {
this.removalReason = removalReason;
}
2025-02-16 12:23:20 -08:00
@@ -4935,6 +5772,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess
this.removalReason = null;
}
+ // Folia start - region threading
+ protected void preRemove(Entity.RemovalReason reason) {}
+ // Folia end - region threading
+
// Paper start - Folia schedulers
/**
* Invoked only when the entity is truly removed from the server, never to be added to any world.
diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java
2025-02-27 22:48:16 -08:00
index 4546aca8e2e144ec207653c713fc49f849908827..6258b008ad3e00d41e9f3014572d6f7a06b1847c 100644
--- a/net/minecraft/world/entity/LivingEntity.java
+++ b/net/minecraft/world/entity/LivingEntity.java
@@ -278,7 +278,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
private Optional<BlockPos> lastClimbablePos = Optional.empty();
@Nullable
private DamageSource lastDamageSource;
- private long lastDamageStamp;
+ private long lastDamageStamp = Long.MIN_VALUE; // Folia - region threading
protected int autoSpinAttackTicks;
protected float autoSpinAttackDmg;
@Nullable
@@ -307,6 +307,26 @@ public abstract class LivingEntity extends Entity implements Attackable {
return this.getYHeadRot();
}
// CraftBukkit end
+ // Folia start - region threading
+ @Override
+ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) {
+ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
+ if (this.lastDamageStamp != Long.MIN_VALUE) {
+ this.lastDamageStamp += fromRedstoneTimeOffset;
+ }
+ }
+
+ @Override
+ public boolean canBeSpectated() {
+ return super.canBeSpectated() && this.getHealth() > 0.0F;
+ }
+
+ @Override
+ protected void resetStoredPositions() {
+ super.resetStoredPositions();
+ this.lastClimbablePos = Optional.empty();
+ }
+ // Folia end - region threading
protected LivingEntity(EntityType<? extends LivingEntity> entityType, Level level) {
super(entityType, level);
@@ -528,7 +548,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
if (this.isDeadOrDying() && this.level().shouldTickDeath(this)) {
this.tickDeath();
- }
+ } else { this.broadcastedDeath = false; } // Folia - region threading
if (this.lastHurtByPlayerTime > 0) {
this.lastHurtByPlayerTime--;
@@ -611,11 +631,14 @@ public abstract class LivingEntity extends Entity implements Attackable {
return true;
}
+ public boolean broadcastedDeath = false; // Folia - region threading
protected void tickDeath() {
this.deathTime++;
if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved()) {
this.level().broadcastEntityEvent(this, (byte)60);
- this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause
+ this.broadcastedDeath = true; // Folia - region threading - death has been broadcasted
+ if (!(this instanceof ServerPlayer)) this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause // Folia - region threading - don't remove, we want the tick scheduler to be running
+ if ((this instanceof ServerPlayer)) this.unRide(); // Folia - region threading - unmount player when dead
}
}
@@ -851,9 +874,9 @@ public abstract class LivingEntity extends Entity implements Attackable {
}
this.hurtTime = compound.getShort("HurtTime");
- this.deathTime = compound.getShort("DeathTime");
+ this.deathTime = compound.getShort("DeathTime"); this.broadcastedDeath = false; // Folia - region threading
this.lastHurtByMobTimestamp = compound.getInt("HurtByTimestamp");
- if (compound.contains("Team", 8)) {
+ if (false && compound.contains("Team", 8)) { // Folia start - region threading
String string = compound.getString("Team");
Scoreboard scoreboard = this.level().getScoreboard();
PlayerTeam playerTeam = scoreboard.getPlayerTeam(string);
@@ -1115,6 +1138,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
public boolean addEffect(MobEffectInstance effectInstance, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) {
// Paper end - Don't fire sync event during generation
// org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot // Paper - move to API
+ if (!this.hasNullCallback()) ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot add effects to entities asynchronously"); // Folia - region threading
if (this.isTickingEffects) {
this.effectsToProcess.add(new ProcessableEffect(effectInstance, cause));
return true;
@@ -1502,7 +1526,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
boolean flag2 = !flag; // CraftBukkit - Ensure to return false if damage is blocked
if (flag2) {
this.lastDamageSource = damageSource;
- this.lastDamageStamp = this.level().getGameTime();
+ this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading
for (MobEffectInstance mobEffectInstance : this.getActiveEffects()) {
mobEffectInstance.onMobHurt(level, this, damageSource, amount);
@@ -1629,7 +1653,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
@Nullable
public DamageSource getLastDamageSource() {
- if (this.level().getGameTime() - this.lastDamageStamp > 40L) {
+ if (this.level().getRedstoneGameTime() - this.lastDamageStamp > 40L || this.lastDamageStamp == Long.MIN_VALUE) { // Folia - region threading
this.lastDamageSource = null;
}
2025-02-27 22:48:16 -08:00
@@ -2427,10 +2451,10 @@ public abstract class LivingEntity extends Entity implements Attackable {
@Nullable
public LivingEntity getKillCredit() {
- if (this.lastHurtByPlayer != null) {
+ if (this.lastHurtByPlayer != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByPlayer)) { // Folia - region threading
return this.lastHurtByPlayer;
} else {
- return this.lastHurtByMob != null ? this.lastHurtByMob : null;
+ return this.lastHurtByMob != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByMob) ? this.lastHurtByMob : null; // Folia - region threading
}
}
2025-02-27 22:48:16 -08:00
@@ -2509,7 +2533,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
}
this.lastDamageSource = damageSource;
- this.lastDamageStamp = this.level().getGameTime();
+ this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading
}
@Override
2025-02-27 22:48:16 -08:00
@@ -3504,7 +3528,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
this.pushEntities();
profilerFiller.pop();
// Paper start - Add EntityMoveEvent
- if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof Player)) {
+ if (((ServerLevel) this.level()).getCurrentWorldData().hasEntityMoveEvent && !(this instanceof Player)) { // Folia - region threading
if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) {
Location from = new Location(this.level().getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO);
Location to = new Location(this.level().getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot());
2025-02-27 22:48:16 -08:00
@@ -4177,7 +4201,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
boolean flag = false;
BlockPos blockPos = BlockPos.containing(x, y, z);
Level level = this.level();
- if (level.hasChunkAt(blockPos)) {
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)level, blockPos) && level.hasChunkAt(blockPos)) { // Folia - region threading
boolean flag1 = false;
while (!flag1 && blockPos.getY() > level.getMinY()) {
2025-02-27 22:48:16 -08:00
@@ -4339,6 +4363,11 @@ public abstract class LivingEntity extends Entity implements Attackable {
this.setXRot(0.0F);
}
});
+ // Folia start - separate out
+ this.stopSleepingRaw();
+ }
+ public void stopSleepingRaw() {
+ // Folia end - separate out
Vec3 vec3 = this.position();
this.setPose(Pose.STANDING);
this.setPos(vec3.x, vec3.y, vec3.z);
diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java
2025-03-08 06:39:17 -08:00
index e330bf990e4874baed1b21cd8c9b44d66ec5b823..da922d4c0ffa0f40d5e8dd69487bf30dbbbeed87 100644
--- a/net/minecraft/world/entity/Mob.java
+++ b/net/minecraft/world/entity/Mob.java
@@ -254,8 +254,20 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
@Nullable
@Override
public LivingEntity getTarget() {
+ // Folia start - region threading
+ if (this.target != null && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.target) || this.target.isRemoved())) {
+ this.target = null;
+ return null;
+ }
+ // Folia end - region threading
+ return this.target;
+ }
+
+ // Folia start - region threading
+ public LivingEntity getTargetRaw() {
return this.target;
}
+ // Folia end - region threading
@Nullable
protected final LivingEntity getTargetFromBrain() {
@@ -268,7 +280,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
}
public boolean setTarget(LivingEntity target, EntityTargetEvent.TargetReason reason, boolean fireEvent) {
- if (this.getTarget() == target) {
+ if (this.getTargetRaw() == target) { // Folia - region threading
return false;
}
if (fireEvent) {
2025-03-08 06:39:17 -08:00
@@ -1665,12 +1677,26 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab
@Override
protected void removeAfterChangingDimensions() {
super.removeAfterChangingDimensions();
+ // Folia start - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions
+// this.getAllSlots().forEach(itemStack -> {
+// if (!itemStack.isEmpty()) {
+// itemStack.setCount(0);
+// }
+// });
+ // Folia end - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions
+ }
+
+ // Folia start - region threading
+ @Override
+ protected void postRemoveAfterChangingDimensions() {
+ super.postRemoveAfterChangingDimensions();
this.getAllSlots().forEach(itemStack -> {
if (!itemStack.isEmpty()) {
itemStack.setCount(0);
}
});
}
+ // Folia end - region threading
@Nullable
@Override
diff --git a/net/minecraft/world/entity/PortalProcessor.java b/net/minecraft/world/entity/PortalProcessor.java
index 88b07fbb96b20124777889830afa480673629d43..46d989aef0eceebd98bfd93999153319de77a8a0 100644
--- a/net/minecraft/world/entity/PortalProcessor.java
+++ b/net/minecraft/world/entity/PortalProcessor.java
@@ -33,6 +33,12 @@ public class PortalProcessor {
return this.portal.getPortalDestination(level, entity, this.entryPosition);
}
+ // Folia start - region threading
+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget) {
+ return this.portal.portalAsync(sourceWorld, portalTarget, this.entryPosition);
+ }
+ // Folia end - region threading
+
public Portal.Transition getPortalLocalTransition() {
return this.portal.getLocalTransition();
}
diff --git a/net/minecraft/world/entity/TamableAnimal.java b/net/minecraft/world/entity/TamableAnimal.java
index fc3ba135ae502aaa5c3a9fa3297bf7b12c1ab063..b0b1e38f2b70ed548790fd0445db4541c34b0f34 100644
--- a/net/minecraft/world/entity/TamableAnimal.java
+++ b/net/minecraft/world/entity/TamableAnimal.java
@@ -263,6 +263,11 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity {
public void tryToTeleportToOwner() {
LivingEntity owner = this.getOwner();
if (owner != null) {
+ // Folia start - region threading
+ if (owner.isRemoved() || owner.level() != this.level()) {
+ return;
+ }
+ // Folia end - region threading
this.teleportToAroundBlockPos(owner.blockPosition());
}
}
@@ -295,7 +300,22 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity {
return false;
}
org.bukkit.Location to = event.getTo();
- this.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch());
+ // Folia start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick
+ // also, use teleportAsync so that crossing region boundaries will not blow up
+ org.bukkit.Location finalTo = to;
+ Level sourceWorld = this.level();
+ this.getBukkitEntity().taskScheduler.schedule((TamableAnimal nmsEntity) -> {
+ if (nmsEntity.level() == sourceWorld) {
+ nmsEntity.teleportAsync(
+ (net.minecraft.server.level.ServerLevel)nmsEntity.level(),
+ new net.minecraft.world.phys.Vec3(finalTo.getX(), finalTo.getY(), finalTo.getZ()),
+ Float.valueOf(finalTo.getYaw()), Float.valueOf(finalTo.getPitch()),
+ net.minecraft.world.phys.Vec3.ZERO, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN, Entity.TELEPORT_FLAG_LOAD_CHUNK,
+ null
+ );
+ }
+ }, null, 1L);
+ // Folia end - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick
// CraftBukkit end
this.navigation.stop();
return true;
diff --git a/net/minecraft/world/entity/ai/Brain.java b/net/minecraft/world/entity/ai/Brain.java
index 450396468b23fd90cb8036dbbdd0927051f907af..65b2b3ece213d901cdd585093e2fafcd2ef4a7cd 100644
--- a/net/minecraft/world/entity/ai/Brain.java
+++ b/net/minecraft/world/entity/ai/Brain.java
@@ -425,9 +425,17 @@ public class Brain<E extends LivingEntity> {
}
public void stopAll(ServerLevel level, E owner) {
+ // Folia start - region threading
+ List<BehaviorControl<? super E>> 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<? super E> behaviorControl : this.getRunningBehaviors()) {
+ for (BehaviorControl<? super E> 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<Villager> {
BlockPos blockPos = globalPos.pos();
ServerLevel level1 = level.getServer().getLevel(globalPos.dimension());
if (level1 != null) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(level1, blockPos.getX() >> 4, blockPos.getZ() >> 4, () -> { // Folia - region threading
PoiManager poiManager = level1.getPoiManager();
if (poiManager.exists(blockPos, holder -> true)) {
poiManager.release(blockPos);
}
DebugPackets.sendPoiTicketCountPacket(level, blockPos);
+ }); // Folia - region threading
}
});
entity.getBrain().eraseMemory(MemoryModuleType.POTENTIAL_JOB_SITE);
diff --git a/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java b/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java
index b11a16db0ea22ebd68db9c96e0ba0939b6596caf..9f5b5ad2fe08f25f4c922ae641d1a2e8bce18ccb 100644
--- a/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java
+++ b/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java
@@ -19,6 +19,11 @@ public class PoiCompetitorScan {
instance,
(jobSite, nearestLivingEntities) -> (level, villager, gameTime) -> {
GlobalPos globalPos = instance.get(jobSite);
+ // Folia start - region threading
+ if (globalPos.dimension() != level.dimension() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, globalPos.pos())) {
+ return true;
+ }
+ // Folia end - region threading
level.getPoiManager()
.getType(globalPos.pos())
.ifPresent(
diff --git a/net/minecraft/world/entity/ai/behavior/YieldJobSite.java b/net/minecraft/world/entity/ai/behavior/YieldJobSite.java
index 37ad79e201e36a1a9520219e3faa4dcffa7b4dfd..4cf9bb7ef73fbeb400991c3d8ff59141a0ea2575 100644
--- a/net/minecraft/world/entity/ai/behavior/YieldJobSite.java
+++ b/net/minecraft/world/entity/ai/behavior/YieldJobSite.java
@@ -33,7 +33,13 @@ public class YieldJobSite {
} else if (villager.getVillagerData().getProfession() != VillagerProfession.NONE) {
return false;
} else {
- BlockPos blockPos = instance.<GlobalPos>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<Holder<PoiType>> type = level.getPoiManager().getType(blockPos);
if (type.isEmpty()) {
return true;
diff --git a/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
index dde287e823f906681e3addf03fa821c8786c9900..e5e3ce6eeb01ac4387eaee20d09ef469d8b3bc5e 100644
--- a/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
+++ b/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
@@ -51,7 +51,7 @@ public class FollowOwnerGoal extends Goal {
public boolean canContinueToUse() {
return !this.navigation.isDone()
&& !this.tamable.unableToMoveToOwner()
- && !(this.tamable.distanceToSqr(this.owner) <= this.stopDistance * this.stopDistance);
+ && !(this.owner.level() == this.tamable.level() && this.tamable.distanceToSqr(this.owner) <= this.stopDistance * this.stopDistance); // Folia - region threading - check level
}
@Override
diff --git a/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java b/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
index 045cfafb3afe8271d60852ae3c7cdcb039b44d4f..a24e964aff5623e3d7f2b79c87b6067f565458c2 100644
--- a/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
+++ b/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java
@@ -42,6 +42,11 @@ public class GroundPathNavigation extends PathNavigation {
@Override
public Path createPath(BlockPos pos, @javax.annotation.Nullable Entity entity, int accuracy) { // Paper - EntityPathfindEvent
+ // Folia start - region threading
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level, pos)) {
+ return null;
+ }
+ // Folia end - region threading
LevelChunk chunkNow = this.level.getChunkSource().getChunkNow(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
if (chunkNow == null) {
return null;
diff --git a/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/net/minecraft/world/entity/ai/navigation/PathNavigation.java
index b44f2c49509d847817a78e9c4fb1499fb378054b..386580035e6789d6e668b924513ddfc81947a9b3 100644
--- a/net/minecraft/world/entity/ai/navigation/PathNavigation.java
+++ b/net/minecraft/world/entity/ai/navigation/PathNavigation.java
@@ -96,11 +96,11 @@ public abstract class PathNavigation {
}
public void recomputePath() {
- if (this.level.getGameTime() - this.timeLastRecompute > 20L) {
+ if (this.tick - this.timeLastRecompute > 20L) { // Folia - region threading
if (this.targetPos != null) {
this.path = null;
this.path = this.createPath(this.targetPos, this.reachRange);
- this.timeLastRecompute = this.level.getGameTime();
+ this.timeLastRecompute = this.tick; // Folia - region threading
this.hasDelayedRecomputation = false;
}
} else {
@@ -221,7 +221,7 @@ public abstract class PathNavigation {
public boolean moveTo(Entity entity, double speed) {
// Paper start - Perf: Optimise pathfinding
- if (this.pathfindFailures > 10 && this.path == null && net.minecraft.server.MinecraftServer.currentTick < this.lastFailure + 40) {
+ if (this.pathfindFailures > 10 && this.path == null && this.tick < this.lastFailure + 40) { // Folia - region threading
return false;
}
// Paper end - Perf: Optimise pathfinding
@@ -233,7 +233,7 @@ public abstract class PathNavigation {
return true;
} else {
this.pathfindFailures++;
- this.lastFailure = net.minecraft.server.MinecraftServer.currentTick;
+ this.lastFailure = this.tick; // Folia - region threading
return false;
}
// Paper end - Perf: Optimise pathfinding
diff --git a/net/minecraft/world/entity/ai/sensing/PlayerSensor.java b/net/minecraft/world/entity/ai/sensing/PlayerSensor.java
index 6233e6b48aaa69ba9f577d0b480b1cdf2f55d16e..a4810c8a3b18082543d06787722d4ed5821a1943 100644
--- a/net/minecraft/world/entity/ai/sensing/PlayerSensor.java
+++ b/net/minecraft/world/entity/ai/sensing/PlayerSensor.java
@@ -22,7 +22,7 @@ public class PlayerSensor extends Sensor<LivingEntity> {
@Override
protected void doTick(ServerLevel level, LivingEntity entity) {
- List<Player> list = level.players()
+ List<Player> list = level.getLocalPlayers() // Folia - region threading
.stream()
.filter(EntitySelector.NO_SPECTATORS)
.filter(serverPlayer -> entity.closerThan(serverPlayer, this.getFollowDistance(entity)))
diff --git a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
index 4b3ba795bc18417f983600f1edbc1895ccb7deab..d06c2c72e6166bc8b7822966092b17440125b814 100644
--- a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
+++ b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java
@@ -36,7 +36,7 @@ public class TemptingSensor extends Sensor<PathfinderMob> {
protected void doTick(ServerLevel level, PathfinderMob entity) {
Brain<?> brain = entity.getBrain();
TargetingConditions targetingConditions = TEMPT_TARGETING.copy().range((float)entity.getAttributeValue(Attributes.TEMPT_RANGE));
- List<Player> list = level.players()
+ List<Player> list = level.getLocalPlayers() // Folia - region threading
.stream()
.filter(EntitySelector.NO_SPECTATORS)
.filter(serverPlayer -> targetingConditions.test(level, entity, serverPlayer))
diff --git a/net/minecraft/world/entity/ai/village/VillageSiege.java b/net/minecraft/world/entity/ai/village/VillageSiege.java
index a1cea4a4f76a7bb771b8ab643bd9d473e16418bf..fa012c5b23f6fdd714d15282cc485492ae18672a 100644
--- a/net/minecraft/world/entity/ai/village/VillageSiege.java
+++ b/net/minecraft/world/entity/ai/village/VillageSiege.java
@@ -18,68 +18,72 @@ import org.slf4j.Logger;
public class VillageSiege implements CustomSpawner {
private static final Logger LOGGER = LogUtils.getLogger();
- private boolean hasSetupSiege;
- private VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE;
- private int zombiesToSpawn;
- private int nextSpawnTime;
- private int spawnX;
- private int spawnY;
- private int spawnZ;
+ // Folia - region threading
@Override
public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
+ // Folia start - region threading
+ // check if the spawn pos is no longer owned by this region
+ if (worldData.villageSiegeState.siegeState != State.SIEGE_DONE
+ && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, worldData.villageSiegeState.spawnX >> 4, worldData.villageSiegeState.spawnZ >> 4, 8)) {
+ // can't spawn here, just re-set
+ worldData.villageSiegeState = new io.papermc.paper.threadedregions.RegionizedWorldData.VillageSiegeState();
+ }
+ // Folia end - region threading
if (!level.isDay() && spawnHostiles) {
float timeOfDay = level.getTimeOfDay(0.0F);
if (timeOfDay == 0.5) {
- this.siegeState = level.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE;
+ worldData.villageSiegeState.siegeState = level.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; // Folia - region threading
}
- if (this.siegeState == VillageSiege.State.SIEGE_DONE) {
+ if (worldData.villageSiegeState.siegeState == VillageSiege.State.SIEGE_DONE) { // Folia - region threading
return 0;
} else {
- if (!this.hasSetupSiege) {
+ if (!worldData.villageSiegeState.hasSetupSiege) { // Folia - region threading
if (!this.tryToSetupSiege(level)) {
return 0;
}
- this.hasSetupSiege = true;
+ worldData.villageSiegeState.hasSetupSiege = true; // Folia - region threading
}
- if (this.nextSpawnTime > 0) {
- this.nextSpawnTime--;
+ if (worldData.villageSiegeState.nextSpawnTime > 0) { // Folia - region threading
+ worldData.villageSiegeState.nextSpawnTime--; // Folia - region threading
return 0;
} else {
- this.nextSpawnTime = 2;
- if (this.zombiesToSpawn > 0) {
+ worldData.villageSiegeState.nextSpawnTime = 2; // Folia - region threading
+ if (worldData.villageSiegeState.zombiesToSpawn > 0) { // Folia - region threading
this.trySpawn(level);
- this.zombiesToSpawn--;
+ worldData.villageSiegeState.zombiesToSpawn--; // Folia - region threading
} else {
- this.siegeState = VillageSiege.State.SIEGE_DONE;
+ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading
}
return 1;
}
}
} else {
- this.siegeState = VillageSiege.State.SIEGE_DONE;
- this.hasSetupSiege = false;
+ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading
+ worldData.villageSiegeState.hasSetupSiege = false; // Folia - region threading
return 0;
}
}
private boolean tryToSetupSiege(ServerLevel level) {
- for (Player player : level.players()) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
+ for (Player player : level.getLocalPlayers()) { // Folia - region threading
if (!player.isSpectator()) {
BlockPos blockPos = player.blockPosition();
if (level.isVillage(blockPos) && !level.getBiome(blockPos).is(BiomeTags.WITHOUT_ZOMBIE_SIEGES)) {
for (int i = 0; i < 10; i++) {
float f = level.random.nextFloat() * (float) (Math.PI * 2);
- this.spawnX = blockPos.getX() + Mth.floor(Mth.cos(f) * 32.0F);
- this.spawnY = blockPos.getY();
- this.spawnZ = blockPos.getZ() + Mth.floor(Mth.sin(f) * 32.0F);
- if (this.findRandomSpawnPos(level, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)) != null) {
- this.nextSpawnTime = 0;
- this.zombiesToSpawn = 20;
+ worldData.villageSiegeState.spawnX = blockPos.getX() + Mth.floor(Mth.cos(f) * 32.0F); // Folia - region threading
+ worldData.villageSiegeState.spawnY = blockPos.getY(); // Folia - region threading
+ worldData.villageSiegeState.spawnZ = blockPos.getZ() + Mth.floor(Mth.sin(f) * 32.0F); // Folia - region threading
+ if (this.findRandomSpawnPos(level, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)) != null) { // Folia - region threading
+ worldData.villageSiegeState.nextSpawnTime = 0; // Folia - region threading
+ worldData.villageSiegeState.zombiesToSpawn = 20; // Folia - region threading
break;
}
}
@@ -93,11 +97,13 @@ public class VillageSiege implements CustomSpawner {
}
private void trySpawn(ServerLevel level) {
- Vec3 vec3 = this.findRandomSpawnPos(level, new BlockPos(this.spawnX, this.spawnY, this.spawnZ));
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
+ Vec3 vec3 = this.findRandomSpawnPos(level, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)); // Folia - region threading
if (vec3 != null) {
Zombie zombie;
try {
zombie = new Zombie(level);
+ zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up
zombie.finalizeSpawn(level, level.getCurrentDifficultyAt(zombie.blockPosition()), EntitySpawnReason.EVENT, null);
} catch (Exception var5) {
LOGGER.warn("Failed to create zombie for village siege at {}", vec3, var5);
@@ -105,7 +111,7 @@ public class VillageSiege implements CustomSpawner {
return;
}
- zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F);
+ //zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up
level.addFreshEntityWithPassengers(zombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_INVASION); // CraftBukkit
}
}
@@ -125,7 +131,7 @@ public class VillageSiege implements CustomSpawner {
return null;
}
- static enum State {
+ public static enum State { // Folia - region threading
SIEGE_CAN_ACTIVATE,
SIEGE_TONIGHT,
SIEGE_DONE;
diff --git a/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/net/minecraft/world/entity/ai/village/poi/PoiManager.java
index 618fc0eb4fe70e46e55f3aa28e8eac1d2d01b6d9..c10810bf00d75f459c3c6a9415c1e09f0519d50e 100644
--- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java
+++ b/net/minecraft/world/entity/ai/village/poi/PoiManager.java
@@ -58,11 +58,13 @@ public class PoiManager extends SectionStorage<PoiSection, PoiSection.Packed> 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<PoiSection, PoiSection.Packed> 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<PoiSection, PoiSection.Packed> im
@Override
public void tick(BooleanSupplier aheadOfTime) {
+ synchronized (this.villageDistanceTracker) { // Folia - region threading
this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system
+ } // Folia - region threading
}
@Override
diff --git a/net/minecraft/world/entity/animal/Bee.java b/net/minecraft/world/entity/animal/Bee.java
index 94244b148533ef026bf5c56abbc2bb5cfa83c938..15360f560d9b6a762ebd4284b7d0ca0a3e13794e 100644
--- a/net/minecraft/world/entity/animal/Bee.java
+++ b/net/minecraft/world/entity/animal/Bee.java
@@ -815,6 +815,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
@Override
public boolean canBeeUse() {
+ // Folia start - region threading
+ if (Bee.this.hivePos != null && Bee.this.isTooFarAway(Bee.this.hivePos)) {
+ Bee.this.hivePos = null;
+ }
+ // Folia end - region threading
return Bee.this.hivePos != null
&& !Bee.this.isTooFarAway(Bee.this.hivePos)
&& !Bee.this.hasRestriction()
@@ -925,6 +930,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal {
@Override
public boolean canBeeUse() {
+ // Folia start - region threading
+ if (Bee.this.savedFlowerPos != null && Bee.this.isTooFarAway(Bee.this.savedFlowerPos)) {
+ Bee.this.savedFlowerPos = null;
+ }
+ // Folia end - region threading
return Bee.this.savedFlowerPos != null
&& !Bee.this.hasRestriction()
&& this.wantsToGoToKnownFlower()
diff --git a/net/minecraft/world/entity/animal/Cat.java b/net/minecraft/world/entity/animal/Cat.java
index 1a7a5c81a260cc740994d1a63c4775c41c238dea..740ab58c733d9e3f05157fef6e6725fd72f90653 100644
--- a/net/minecraft/world/entity/animal/Cat.java
+++ b/net/minecraft/world/entity/animal/Cat.java
@@ -342,7 +342,7 @@ public class Cat extends TamableAnimal implements VariantHolder<Holder<CatVarian
TagKey<CatVariant> tagKey = flag ? CatVariantTags.FULL_MOON_SPAWNS : CatVariantTags.DEFAULT_SPAWNS;
BuiltInRegistries.CAT_VARIANT.getRandomElementOf(tagKey, level.getRandom()).ifPresent(this::setVariant);
ServerLevel level1 = level.getLevel();
- if (level1.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK, level).isValid()) { // Paper - Fix swamp hut cat generation deadlock
+ if (level.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK).isValid()) { // Paper - Fix swamp hut cat generation deadlock // Folia - region threading - properly fix this
this.setVariant(BuiltInRegistries.CAT_VARIANT.getOrThrow(CatVariant.ALL_BLACK));
this.setPersistenceRequired();
}
diff --git a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
index ff1c84d37db48e1bd0283a900e199647c0e8eba1..fc64c36a01eb8efdcfa487059078787900e34d86 100644
--- a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
+++ b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java
@@ -53,7 +53,7 @@ public class EndCrystal extends Entity {
public void tick() {
this.time++;
this.applyEffectsFromBlocks();
- this.handlePortal();
+ //this.handlePortal(); // Folia - region threading
if (this.level() instanceof ServerLevel) {
BlockPos blockPos = this.blockPosition();
if (((ServerLevel)this.level()).getDragonFight() != null && this.level().getBlockState(blockPos).isAir()) {
diff --git a/net/minecraft/world/entity/decoration/ItemFrame.java b/net/minecraft/world/entity/decoration/ItemFrame.java
index 65e1d7c5ac94b1cfb921fa009be59d3e5872f0b5..5aefec7ecc2085659bebca25992dd3a76fff2b5e 100644
--- a/net/minecraft/world/entity/decoration/ItemFrame.java
+++ b/net/minecraft/world/entity/decoration/ItemFrame.java
@@ -242,7 +242,9 @@ public class ItemFrame extends HangingEntity {
if (framedMapId != null) {
MapItemSavedData savedData = MapItem.getSavedData(framedMapId, this.level());
if (savedData != null) {
+ synchronized (savedData) { // Folia - make map data thread-safe
savedData.removedFromFrame(this.pos, this.getId());
+ } // Folia - make map data thread-safe
}
}
diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java
index 5746587666c7cb788764aab2f6ccf0f3ac5c282f..1fa5e6a12b943e889bde566038a632a6adcf319e 100644
--- a/net/minecraft/world/entity/item/FallingBlockEntity.java
+++ b/net/minecraft/world/entity/item/FallingBlockEntity.java
@@ -162,7 +162,7 @@ public class FallingBlockEntity extends Entity {
return;
}
// Paper end - Configurable falling blocks height nerf
- this.handlePortal();
+ //this.handlePortal(); // Folia - region threading
if (this.level() instanceof ServerLevel serverLevel && (this.isAlive() || this.forceTickAfterTeleportToDuplicate)) {
BlockPos blockPos = this.blockPosition();
boolean flag = this.blockState.getBlock() instanceof ConcretePowderBlock;
diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java
index 52a7ed0d991758bad0dcedcb7f97fb15ac6c6d04..7587130e021d494ae5013f7992b8f3c96590cbd7 100644
--- a/net/minecraft/world/entity/item/ItemEntity.java
+++ b/net/minecraft/world/entity/item/ItemEntity.java
@@ -521,13 +521,21 @@ public class ItemEntity extends Entity implements TraceableEntity {
return false;
}
+ // Folia start - region threading
+ @Override
+ public void postChangeDimension() {
+ super.postChangeDimension();
+ if (!this.level().isClientSide) {
+ this.mergeWithNeighbours();
+ }
+ }
+ // Folia end - region threading
+
@Nullable
@Override
public Entity teleport(TeleportTransition teleportTransition) {
Entity entity = super.teleport(teleportTransition);
- if (!this.level().isClientSide && entity instanceof ItemEntity itemEntity) {
- itemEntity.mergeWithNeighbours();
- }
+ if (entity != null) entity.postChangeDimension(); // Folia - region threading - move to post change
return entity;
}
diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java
index 40da052e7fea1306a007b3cb5c9daa33e0ef523e..88570bb4aa02896545805d7721c45cf9599befea 100644
--- a/net/minecraft/world/entity/item/PrimedTnt.java
+++ b/net/minecraft/world/entity/item/PrimedTnt.java
@@ -98,8 +98,8 @@ public class PrimedTnt extends Entity implements TraceableEntity {
@Override
public void tick() {
- if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot
- this.handlePortal();
+ if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().getCurrentWorldData().currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading
+ //this.handlePortal(); // Folia - region threading
this.applyGravity();
this.move(MoverType.SELF, this.getDeltaMovement());
this.applyEffectsFromBlocks();
@@ -137,7 +137,7 @@ public class PrimedTnt extends Entity implements TraceableEntity {
*/
// Send position and velocity updates to nearby players on every tick while the TNT is in water.
// This does pretty well at keeping their clients in sync with the server.
- net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel) this.level()).getChunkSource().chunkMap.entityMap.get(this.getId());
+ net.minecraft.server.level.ChunkMap.TrackedEntity ete = this.moonrise$getTrackedEntity(); // Folia - region threading
if (ete != null) {
net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this);
net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket.teleport(this.getId(), net.minecraft.world.entity.PositionMoveRotation.of(this), java.util.Set.of(), this.onGround);
diff --git a/net/minecraft/world/entity/monster/Vex.java b/net/minecraft/world/entity/monster/Vex.java
index af3fef70998cff4e4832adfa2071832324ebd91c..8751f80d48d11c33ddb6c553894c31e8b7630623 100644
--- a/net/minecraft/world/entity/monster/Vex.java
+++ b/net/minecraft/world/entity/monster/Vex.java
@@ -349,7 +349,7 @@ public class Vex extends Monster implements TraceableEntity {
@Override
public void tick() {
BlockPos boundOrigin = Vex.this.getBoundOrigin();
- if (boundOrigin == null) {
+ if (boundOrigin == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)Vex.this.level(), boundOrigin)) { // Folia - region threading
boundOrigin = Vex.this.blockPosition();
}
diff --git a/net/minecraft/world/entity/monster/ZombieVillager.java b/net/minecraft/world/entity/monster/ZombieVillager.java
2025-03-08 06:39:17 -08:00
index 8403257d81367c7371fa94d458a59a4589dc0bd7..d0e7eb3394b4e5b244cbd28424ff47ddf1b8f2bb 100644
--- a/net/minecraft/world/entity/monster/ZombieVillager.java
+++ b/net/minecraft/world/entity/monster/ZombieVillager.java
@@ -69,7 +69,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder {
@Nullable
private MerchantOffers tradeOffers;
private int villagerXp;
- private int lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - add field
+ //private int lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers
public ZombieVillager(EntityType<? extends ZombieVillager> entityType, Level level) {
super(entityType, level);
@@ -149,7 +149,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder {
}
super.tick();
- this.lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit
+ //this.lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit // Folia - region threading - restore original timers
}
@Override
diff --git a/net/minecraft/world/entity/npc/AbstractVillager.java b/net/minecraft/world/entity/npc/AbstractVillager.java
index a71d16d968bb90fd7aca6f01a3dd56df4f9a7ce6..27ce04ecee778b73711ee55c7c75c541e1f86c38 100644
--- a/net/minecraft/world/entity/npc/AbstractVillager.java
+++ b/net/minecraft/world/entity/npc/AbstractVillager.java
@@ -218,10 +218,18 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa
this.readInventoryFromTag(compound, this.registryAccess());
}
+ // Folia start - region threading
+ @Override
+ public void preChangeDimension() {
+ super.preChangeDimension();
+ this.stopTrading();
+ }
+ // Folia end - region threading
+
@Nullable
@Override
public Entity teleport(TeleportTransition teleportTransition) {
- this.stopTrading();
+ this.preChangeDimension(); // Folia - region threading - move into preChangeDimension
return super.teleport(teleportTransition);
}
diff --git a/net/minecraft/world/entity/npc/CatSpawner.java b/net/minecraft/world/entity/npc/CatSpawner.java
index e6d368bc601357cfca694ce328c8e6e47491f3b5..010bee26dfdf5cad186fa57c030540693ff71f23 100644
--- a/net/minecraft/world/entity/npc/CatSpawner.java
+++ b/net/minecraft/world/entity/npc/CatSpawner.java
@@ -18,17 +18,18 @@ import net.minecraft.world.phys.AABB;
public class CatSpawner implements CustomSpawner {
private static final int TICK_DELAY = 1200;
- private int nextTick;
+ //private int nextTick; // Folia - region threading
@Override
public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) {
if (spawnPassives && level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
- this.nextTick--;
- if (this.nextTick > 0) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
+ worldData.catSpawnerNextTick--; // Folia - region threading
+ if (worldData.catSpawnerNextTick > 0) { // Folia - region threading
return 0;
} else {
- this.nextTick = 1200;
- Player randomPlayer = level.getRandomPlayer();
+ worldData.catSpawnerNextTick = 1200; // Folia - region threading
+ Player randomPlayer = level.getRandomLocalPlayer(); // Folia - region threading
if (randomPlayer == null) {
return 0;
} else {
diff --git a/net/minecraft/world/entity/npc/Villager.java b/net/minecraft/world/entity/npc/Villager.java
index 2b83262e4a13eae86df82913ce4f3121e3631a43..7ea74aeb905b95e5919d74df5fbc5e8f7a9985e3 100644
--- a/net/minecraft/world/entity/npc/Villager.java
+++ b/net/minecraft/world/entity/npc/Villager.java
@@ -246,7 +246,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
villagerBrain.setCoreActivities(ImmutableSet.of(Activity.CORE));
villagerBrain.setDefaultActivity(Activity.IDLE);
villagerBrain.setActiveActivityIfPossible(Activity.IDLE);
- villagerBrain.updateActivityFromSchedule(this.level().getDayTime(), this.level().getGameTime());
+ villagerBrain.updateActivityFromSchedule(this.level().getLevelData().getDayTime(), this.level().getLevelData().getGameTime()); // Folia - region threading - not in the world yet
}
@Override
@@ -693,6 +693,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
this.brain.getMemory(moduleType).ifPresent(globalPos -> {
ServerLevel level = server.getLevel(globalPos.dimension());
if (level != null) {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( // Folia - region threading
+ level, globalPos.pos().getX() >> 4, globalPos.pos().getZ() >> 4, () -> { // Folia - region threading
PoiManager poiManager = level.getPoiManager();
Optional<Holder<PoiType>> type = poiManager.getType(globalPos.pos());
BiPredicate<Villager, Holder<PoiType>> biPredicate = POI_MEMORIES.get(moduleType);
@@ -700,6 +702,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler
poiManager.release(globalPos.pos());
DebugPackets.sendPoiTicketCountPacket(level, globalPos.pos());
}
+ }); // Folia - region threading
}
});
}
diff --git a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
index ef2afb17a22a703470e13d12c989a685e72f0ab8..984ac8efa2ed45be614e04eab8247481e3a08525 100644
--- a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
+++ b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java
@@ -30,16 +30,14 @@ public class WanderingTraderSpawner implements CustomSpawner {
private static final int SPAWN_CHANCE_INCREASE = 25;
private static final int SPAWN_ONE_IN_X_CHANCE = 10;
private static final int NUMBER_OF_SPAWN_ATTEMPTS = 10;
- private final RandomSource random = RandomSource.create();
+ private final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading
private final ServerLevelData serverLevelData;
- private int tickDelay;
- private int spawnDelay;
- private int spawnChance;
+ // Folia - region threading
public WanderingTraderSpawner(ServerLevelData serverLevelData) {
this.serverLevelData = serverLevelData;
// Paper start - Add Wandering Trader spawn rate config options
- this.tickDelay = Integer.MIN_VALUE;
+ //this.tickDelay = Integer.MIN_VALUE; // Folia - region threading - moved to regionisedworlddata
// this.spawnDelay = serverLevelData.getWanderingTraderSpawnDelay();
// this.spawnChance = serverLevelData.getWanderingTraderSpawnChance();
// if (this.spawnDelay == 0 && this.spawnChance == 0) {
@@ -53,35 +51,36 @@ public class WanderingTraderSpawner implements CustomSpawner {
@Override
public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
// Paper start - Add Wandering Trader spawn rate config options
- if (this.tickDelay == Integer.MIN_VALUE) {
- this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
- this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
- this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin;
+ if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Folia - region threading
+ worldData.wanderingTraderTickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading
+ worldData.wanderingTraderSpawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading
+ worldData.wanderingTraderSpawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading
}
if (!level.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) {
return 0;
- } else if (--this.tickDelay - 1 > 0) {
- this.tickDelay = this.tickDelay - 1;
+ } else if (--worldData.wanderingTraderTickDelay - 1 > 0) { // Folia - region threading
+ worldData.wanderingTraderTickDelay = worldData.wanderingTraderTickDelay - 1; // Folia - region threading
return 0;
} else {
- this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
- this.spawnDelay = this.spawnDelay - level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength;
+ worldData.wanderingTraderTickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading
+ worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading
//this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways
- if (this.spawnDelay > 0) {
+ if (worldData.wanderingTraderSpawnDelay > 0) { // Folia - region threading
return 0;
} else {
- this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength;
+ worldData.wanderingTraderSpawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading
if (!level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
return 0;
} else {
- int i = this.spawnChance;
- this.spawnChance = Mth.clamp(this.spawnChance + level.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax);
+ int i = worldData.wanderingTraderSpawnChance; // Folia - region threading
+ worldData.wanderingTraderSpawnChance = Mth.clamp(worldData.wanderingTraderSpawnChance + level.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); // Folia - region threading
//this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways
if (this.random.nextInt(100) > i) {
return 0;
} else if (this.spawn(level)) {
- this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin;
+ worldData.wanderingTraderSpawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading
// Paper end - Add Wandering Trader spawn rate config options
return 1;
} else {
@@ -93,7 +92,7 @@ public class WanderingTraderSpawner implements CustomSpawner {
}
private boolean spawn(ServerLevel serverLevel) {
- Player randomPlayer = serverLevel.getRandomPlayer();
+ Player randomPlayer = serverLevel.getRandomLocalPlayer(); // Folia - region threading
if (randomPlayer == null) {
return true;
} else if (this.random.nextInt(10) != 0) {
@@ -116,7 +115,7 @@ public class WanderingTraderSpawner implements CustomSpawner {
this.tryToSpawnLlamaFor(serverLevel, wanderingTrader, 4);
}
- this.serverLevelData.setWanderingTraderId(wanderingTrader.getUUID());
+ //this.serverLevelData.setWanderingTraderId(wanderingTrader.getUUID()); // Folia - region threading - doesn't appear to be used anywhere, so avoid the race condition here...
// wanderingTrader.setDespawnDelay(48000); // Paper - moved above, modifiable by plugins on CreatureSpawnEvent
wanderingTrader.setWanderTarget(blockPos1);
wanderingTrader.restrictTo(blockPos1, 16);
diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java
2025-02-17 15:51:04 -08:00
index a0813aa9ebf5b32375b1bc9f294d8fc34cc867fe..e70919757dee4b02384ded3551c8f580d289584a 100644
--- a/net/minecraft/world/entity/player/Player.java
+++ b/net/minecraft/world/entity/player/Player.java
@@ -1506,6 +1506,14 @@ public abstract class Player extends LivingEntity {
}
}
+ // Folia start - region threading
+ @Override
+ protected void preRemove(RemovalReason reason) {
+ super.preRemove(reason);
+ this.fishing = null;
+ }
+ // Folia end - region threading
+
public boolean isLocalPlayer() {
return false;
}
diff --git a/net/minecraft/world/entity/projectile/AbstractArrow.java b/net/minecraft/world/entity/projectile/AbstractArrow.java
index 23a795eb4c7ad968448dd1405272056bac29c8f8..1a771e8510b2511945d253a1a5ad23054c464b0c 100644
--- a/net/minecraft/world/entity/projectile/AbstractArrow.java
+++ b/net/minecraft/world/entity/projectile/AbstractArrow.java
@@ -176,6 +176,11 @@ public abstract class AbstractArrow extends Projectile {
@Override
public void tick() {
+ // Folia start - region threading - make sure entities do not move into regions they do not own
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) {
+ return;
+ }
+ // Folia end - region threading - make sure entities do not move into regions they do not own
boolean flag = !this.isNoPhysics();
Vec3 deltaMovement = this.getDeltaMovement();
BlockPos blockPos = this.blockPosition();
diff --git a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
index 9a99b813de8b606fab26c87086a21372e5172ba3..4eeb1017576d23d206a7a47b9e9e74b19465b2ae 100644
--- a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
+++ b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java
@@ -80,6 +80,11 @@ public abstract class AbstractHurtingProjectile extends Projectile {
this.setPos(location);
this.applyEffectsFromBlocks();
super.tick();
+ // Folia start - region threading - make sure entities do not move into regions they do not own
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) {
+ return;
+ }
+ // Folia end - region threading - make sure entities do not move into regions they do not own
if (this.shouldBurn()) {
this.igniteForSeconds(1.0F);
}
diff --git a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
index 774ca9e0b56fd175ae246051de762d0c4256ca58..0cfd2c937f93f1acb4afc01251f882710baf2591 100644
--- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
+++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java
@@ -130,6 +130,11 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier {
}
});
}
+ // Folia start - region threading
+ if (this.attachedToEntity != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.attachedToEntity)) {
+ this.attachedToEntity = null;
+ }
+ // Folia end - region threading
if (this.attachedToEntity != null) {
Vec3 handHoldingItemAngle;
diff --git a/net/minecraft/world/entity/projectile/FishingHook.java b/net/minecraft/world/entity/projectile/FishingHook.java
index 1e012c7ef699a64ff3f1b00f897bb893ab25ecbd..f9d7514764850fd02ed5853ba2fdf8ada40ce756 100644
--- a/net/minecraft/world/entity/projectile/FishingHook.java
+++ b/net/minecraft/world/entity/projectile/FishingHook.java
@@ -94,7 +94,7 @@ public class FishingHook extends Projectile {
public FishingHook(Player player, Level level, int luck, int lureSpeed) {
this(EntityType.FISHING_BOBBER, level, luck, lureSpeed);
- this.setOwner(player);
+ //this.setOwner(player); // Folia - region threading - move this down after position so that thread-checks do not fail
float xRot = player.getXRot();
float yRot = player.getYRot();
float cos = Mth.cos(-yRot * (float) (Math.PI / 180.0) - (float) Math.PI);
@@ -105,6 +105,7 @@ public class FishingHook extends Projectile {
double eyeY = player.getEyeY();
double d1 = player.getZ() - cos * 0.3;
this.moveTo(d, eyeY, d1, yRot, xRot);
+ this.setOwner(player); // Folia - region threading - move this down after position so that thread-checks do not fail
Vec3 vec3 = new Vec3(-sin, Mth.clamp(-(sin1 / f), -5.0F, 5.0F), -cos);
double len = vec3.length();
vec3 = vec3.multiply(
@@ -260,6 +261,11 @@ public class FishingHook extends Projectile {
}
private boolean shouldStopFishing(Player player) {
+ // Folia start - region threading
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) {
+ return true;
+ }
+ // Folia end - region threading
ItemStack mainHandItem = player.getMainHandItem();
ItemStack offhandItem = player.getOffhandItem();
boolean isFishingRod = mainHandItem.is(Items.FISHING_ROD);
@@ -623,10 +629,18 @@ public class FishingHook extends Projectile {
@Override
public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) {
// CraftBukkit end
- this.updateOwnerInfo(null);
+ //this.updateOwnerInfo(null); // Folia - region threading - move into preRemove
super.remove(reason, cause); // CraftBukkit - add Bukkit remove cause
}
+ // Folia start - region threading
+ @Override
+ protected void preRemove(RemovalReason reason) {
+ super.preRemove(reason);
+ this.updateOwnerInfo(null);
+ }
+ // Folia end - region threading
+
@Override
public void onClientRemoval() {
this.updateOwnerInfo(null);
diff --git a/net/minecraft/world/entity/projectile/LlamaSpit.java b/net/minecraft/world/entity/projectile/LlamaSpit.java
index 4880db97135d54fa72f64c108b2bd4ded096438b..dc6ec52a513e2754a81733de5f389d6ada5215cc 100644
--- a/net/minecraft/world/entity/projectile/LlamaSpit.java
+++ b/net/minecraft/world/entity/projectile/LlamaSpit.java
@@ -41,6 +41,11 @@ public class LlamaSpit extends Projectile {
@Override
public void tick() {
super.tick();
+ // Folia start - region threading - make sure entities do not move into regions they do not own
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) {
+ return;
+ }
+ // Folia end - region threading - make sure entities do not move into regions they do not own
Vec3 deltaMovement = this.getDeltaMovement();
HitResult hitResultOnMoveVector = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity);
this.preHitTargetOrDeflectSelf(hitResultOnMoveVector); // CraftBukkit - projectile hit event
diff --git a/net/minecraft/world/entity/projectile/Projectile.java b/net/minecraft/world/entity/projectile/Projectile.java
index ad0bb896d6ea669ce88bfe6490319e8ba7a29001..abfe6765faec49d4b8897608582d738c7b09522d 100644
--- a/net/minecraft/world/entity/projectile/Projectile.java
+++ b/net/minecraft/world/entity/projectile/Projectile.java
@@ -38,7 +38,7 @@ public abstract class Projectile extends Entity implements TraceableEntity {
@Nullable
public UUID ownerUUID;
@Nullable
- public Entity cachedOwner;
+ public org.bukkit.craftbukkit.entity.CraftEntity cachedOwner; // Folia - region threading - replace with CraftEntity
public boolean leftOwner;
public boolean hasBeenShot;
@Nullable
@@ -52,7 +52,7 @@ public abstract class Projectile extends Entity implements TraceableEntity {
public void setOwner(@Nullable Entity owner) {
if (owner != null) {
this.ownerUUID = owner.getUUID();
- this.cachedOwner = owner;
+ this.cachedOwner = owner.getBukkitEntity(); // Folia - region threading
}
// Paper start - Refresh ProjectileSource for projectiles
else {
@@ -69,22 +69,38 @@ public abstract class Projectile extends Entity implements TraceableEntity {
if (fillCache) {
this.getOwner();
}
- if (this.cachedOwner != null && !this.cachedOwner.isRemoved() && this.projectileSource == null && this.cachedOwner.getBukkitEntity() instanceof org.bukkit.projectiles.ProjectileSource projSource) {
+ if (this.cachedOwner != null && !this.cachedOwner.getHandleRaw().isRemoved() && this.projectileSource == null && this.cachedOwner instanceof org.bukkit.projectiles.ProjectileSource projSource) { // Folia - region threading
this.projectileSource = projSource;
}
}
// Paper end - Refresh ProjectileSource for projectiles
+ // Folia start - region threading
+ // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner.
+ // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior.
@Nullable
@Override
public Entity getOwner() {
- if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) {
+ Entity ret = this.getOwnerRaw();
+ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(ret) && (ret == null || !ret.isRemoved()) ? ret : null;
+ }
+ // Folia end - region threading
+
+ @Nullable
+ public Entity getOwnerRaw() { // Folia - region threading
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot update owner state asynchronously"); // Folia - region threading
+ if (this.cachedOwner != null && !this.cachedOwner.isPurged()) { // Folia - region threading
this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles
- return this.cachedOwner;
+ return this.cachedOwner.getHandleRaw(); // Folia - region threading
} else if (this.ownerUUID != null) {
- this.cachedOwner = this.findOwner(this.ownerUUID);
+ // Folia start - region threading
+ Entity ret = this.findOwner(this.ownerUUID);
+ if (ret != null) {
+ this.cachedOwner = ret.getBukkitEntity();
+ }
+ // Folia end - region threading
this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles
- return this.cachedOwner;
+ return ret; // Folia - region threading
} else {
return null;
}
@@ -130,7 +146,12 @@ public abstract class Projectile extends Entity implements TraceableEntity {
protected void setOwnerThroughUUID(UUID uuid) {
if (this.ownerUUID != uuid) {
this.ownerUUID = uuid;
- this.cachedOwner = this.findOwner(uuid);
+ // Folia start - region threading
+ Entity cachedOwner = this.findOwner(this.ownerUUID);
+ if (cachedOwner != null) {
+ this.cachedOwner = cachedOwner.getBukkitEntity();
+ }
+ // Folia end - region threading
}
}
@@ -454,7 +475,7 @@ public abstract class Projectile extends Entity implements TraceableEntity {
@Override
public boolean mayInteract(ServerLevel level, BlockPos pos) {
Entity owner = this.getOwner();
- return owner instanceof Player ? owner.mayInteract(level, pos) : owner == null || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING);
+ return owner instanceof Player && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(owner) ? owner.mayInteract(level, pos) : owner == null || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Folia - region threading
}
public boolean mayBreak(ServerLevel level) {
diff --git a/net/minecraft/world/entity/projectile/SmallFireball.java b/net/minecraft/world/entity/projectile/SmallFireball.java
index 8c84cea43fc0e42a576004663670977eac99f1a6..ba70ce3df630532b646eab0a5fabca15d67c379b 100644
--- a/net/minecraft/world/entity/projectile/SmallFireball.java
+++ b/net/minecraft/world/entity/projectile/SmallFireball.java
@@ -24,7 +24,7 @@ public class SmallFireball extends Fireball {
public SmallFireball(Level level, LivingEntity owner, Vec3 movement) {
super(EntityType.SMALL_FIREBALL, owner, movement, level);
// CraftBukkit start
- if (this.getOwner() != null && this.getOwner() instanceof Mob) {
+ if (owner != null && this.getOwner() != null && this.getOwner() instanceof Mob) { // Folia - region threading
this.isIncendiary = (level instanceof ServerLevel serverLevel) && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING);
}
// CraftBukkit end
diff --git a/net/minecraft/world/entity/projectile/ThrowableProjectile.java b/net/minecraft/world/entity/projectile/ThrowableProjectile.java
index f9fa2866cb28622785b4fcd54c0e2989569a401a..74590ac276965543c2d78fe85090097c8d3a7aed 100644
--- a/net/minecraft/world/entity/projectile/ThrowableProjectile.java
+++ b/net/minecraft/world/entity/projectile/ThrowableProjectile.java
@@ -43,6 +43,11 @@ public abstract class ThrowableProjectile extends Projectile {
@Override
public void tick() {
+ // Folia start - region threading - make sure entities do not move into regions they do not own
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) {
+ return;
+ }
+ // Folia end - region threading - make sure entities do not move into regions they do not own
this.handleFirstTickBubbleColumn();
this.applyGravity();
this.applyInertia();
diff --git a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
index 1345097a2a417f95c44143fd7e0d4cec38990121..0a2b4d6da836d7907759b6cdc94afd031450018d 100644
--- a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
+++ b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java
@@ -58,15 +58,11 @@ public class ThrownEnderpearl extends ThrowableItemProjectile {
}
private void deregisterFromCurrentOwner() {
- if (this.getOwner() instanceof ServerPlayer serverPlayer) {
- serverPlayer.deregisterEnderPearl(this);
- }
+ // Folia - region threading - we remove the registration logic, we do not need to fetch the owner
}
private void registerToCurrentOwner() {
- if (this.getOwner() instanceof ServerPlayer serverPlayer) {
- serverPlayer.registerEnderPearl(this);
- }
+ // Folia - region threading - we remove the registration logic, we do not need to fetch the owner
}
@Nullable
@@ -99,6 +95,81 @@ public class ThrownEnderpearl extends ThrowableItemProjectile {
result.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F);
}
+ // Folia start - region threading
+ private static void attemptTeleport(Entity source, ServerLevel checkWorld, net.minecraft.world.phys.Vec3 to) {
+ final boolean onPortalCooldown = source.isOnPortalCooldown();
+ // ignore retired callback, in those cases we do not want to teleport
+ source.getBukkitEntity().taskScheduler.schedule(
+ (Entity entity) -> {
+ if (!isAllowedToTeleportOwner(entity, checkWorld)) {
+ return;
+ }
+ // source is now an invalid reference, do not use it, use the entity parameter
+ net.minecraft.world.phys.Vec3 endermitePos = entity.position();
+
+ // dismount from any vehicles, so we can teleport and to prevent desync
+ if (entity.isPassenger()) {
+ entity.unRide();
+ }
+
+ if (onPortalCooldown) {
+ entity.setPortalCooldown();
+ }
+
+ entity.teleportAsync(
+ checkWorld, to, null, null, null,
+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.ENDER_PEARL,
+ // chunk could have been unloaded
+ Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS | Entity.TELEPORT_FLAG_LOAD_CHUNK,
+ (Entity teleported) -> {
+ // entity is now an invalid reference, do not use it, instead use teleported
+ if (teleported instanceof ServerPlayer player) {
+ // connection teleport is already done
+ ServerLevel world = player.serverLevel();
+
+ // endermite spawn chance
+ if (world.random.nextFloat() < 0.05F && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) {
+ Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(world, EntitySpawnReason.TRIGGERED);
+
+ if (entityendermite != null) {
+ float yRot = teleported.getYRot();
+ float xRot = teleported.getXRot();
+ Runnable spawn = () -> {
+ entityendermite.moveTo(endermitePos.x, endermitePos.y, endermitePos.z, yRot, xRot);
+ world.addFreshEntity(entityendermite, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.ENDER_PEARL);
+ };
+
+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, endermitePos, net.minecraft.world.phys.Vec3.ZERO, 1)) {
+ spawn.run();
+ } else {
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
+ world,
+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.x),
+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.z),
+ spawn
+ );
+ }
+ }
+ }
+
+ // damage player
+ teleported.resetFallDistance();
+ player.resetCurrentImpulseContext();
+ player.hurtServer(player.serverLevel(), player.damageSources().enderPearl().eventEntityDamager(player), 5.0F); // CraftBukkit // Paper - fix DamageSource API
+ playSound(teleported.level(), to);
+ } else {
+ // reset fall damage so that if the entity was falling they do not instantly die
+ teleported.resetFallDistance();
+ playSound(teleported.level(), to);
+ }
+ }
+ );
+ },
+ null, 1L
+ );
+ }
+ // Folia end - region threading
+
@Override
protected void onHit(HitResult result) {
super.onHit(result);
@@ -117,6 +188,20 @@ public class ThrownEnderpearl extends ThrowableItemProjectile {
}
if (this.level() instanceof ServerLevel serverLevel && !this.isRemoved()) {
+ // Folia start - region threading
+ if (true) {
+ // we can't fire events, because we do not actually know where the other entity is located
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this)) {
+ throw new IllegalStateException("Must be on tick thread for ticking entity: " + this);
+ }
+ Entity entity = this.getOwnerRaw();
+ if (entity != null) {
+ attemptTeleport(entity, (ServerLevel)this.level(), this.position());
+ }
+ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT);
+ return;
+ }
+ // Folia end - region threading
Entity owner = this.getOwner();
if (owner != null && isAllowedToTeleportOwner(owner, serverLevel)) {
if (owner.isPassenger()) {
@@ -212,7 +297,15 @@ public class ThrownEnderpearl extends ThrowableItemProjectile {
}
}
- private void playSound(Level level, Vec3 pos) {
+ // Folia start - region threading
+ @Override
+ public void preChangeDimension() {
+ super.preChangeDimension();
+ // Don't change the owner here, since the tick logic will consider it anyways.
+ }
+ // Folia end - region threading
+
+ private static void playSound(Level level, Vec3 pos) { // Folia - region threading - static
level.playSound(null, pos.x, pos.y, pos.z, SoundEvents.PLAYER_TELEPORT, SoundSource.PLAYERS);
}
diff --git a/net/minecraft/world/entity/raid/Raid.java b/net/minecraft/world/entity/raid/Raid.java
index 41b0db439b425b052bd1469daa6620a435ca852b..2f45befbb50645f1bfb5961ad725f3670ff0d592 100644
--- a/net/minecraft/world/entity/raid/Raid.java
+++ b/net/minecraft/world/entity/raid/Raid.java
@@ -110,6 +110,13 @@ public class Raid {
public final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(PDC_TYPE_REGISTRY);
// Paper end
+ // Folia start - make raids thread-safe
+ public boolean ownsRaid() {
+ BlockPos center = this.getCenter();
+ return center != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, center.getX() >> 4, center.getZ() >> 4, 8);
+ }
+ // Folia end - make raids thread-safe
+
public Raid(int id, ServerLevel level, BlockPos center) {
this.id = id;
this.level = level;
@@ -207,7 +214,7 @@ public class Raid {
private Predicate<ServerPlayer> validPlayer() {
return player -> {
BlockPos blockPos = player.blockPosition();
- return player.isAlive() && this.level.getRaidAt(blockPos) == this;
+ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) && player.isAlive() && this.level.getRaidAt(blockPos) == this; // Folia - make raids thread-safe
};
}
@@ -384,14 +391,21 @@ public class Raid {
if (entity instanceof LivingEntity) {
LivingEntity livingEntity = (LivingEntity)entity;
if (!entity.isSpectator()) {
- livingEntity.addEffect(
- new MobEffectInstance(MobEffects.HERO_OF_THE_VILLAGE, 48000, this.raidOmenLevel - 1, false, false, true)
- );
+ //livingEntity.addEffect(new MobEffectInstance(MobEffects.HERO_OF_THE_VILLAGE, 48000, this.raidOmenLevel - 1, false, false, true)); // Folia start - Fix off region raid heroes - moved down
if (livingEntity instanceof ServerPlayer serverPlayer) {
- serverPlayer.awardStat(Stats.RAID_WIN);
- CriteriaTriggers.RAID_WIN.trigger(serverPlayer);
+ //serverPlayer.awardStat(Stats.RAID_WIN); // Folia start - Fix off region raid heroes - moved down
+ //CriteriaTriggers.RAID_WIN.trigger(serverPlayer); // Folia start - Fix off region raid heroes - moved down
winners.add(serverPlayer.getBukkitEntity()); // CraftBukkit
}
+ // Folia start - Fix off region raid heroes
+ livingEntity.getBukkitEntity().taskScheduler.schedule((LivingEntity lv) -> {
+ lv.addEffect(new MobEffectInstance(MobEffects.HERO_OF_THE_VILLAGE, 48000, this.raidOmenLevel - 1, false, false, true));
+ if (lv instanceof ServerPlayer serverPlayer) {
+ serverPlayer.awardStat(Stats.RAID_WIN);
+ CriteriaTriggers.RAID_WIN.trigger(serverPlayer);
+ }
+ }, null, 1L);
+ // Folia end - Fix off region raid heroes
}
}
}
@@ -496,7 +510,7 @@ public class Raid {
Collection<ServerPlayer> players = this.raidEvent.getPlayers();
long randomLong = this.random.nextLong();
- for (ServerPlayer serverPlayer : this.level.players()) {
+ for (ServerPlayer serverPlayer : this.level.getLocalPlayers()) { // Folia - region threading
Vec3 vec3 = serverPlayer.position();
Vec3 vec31 = Vec3.atCenterOf(pos);
double squareRoot = Math.sqrt((vec31.x - vec3.x) * (vec31.x - vec3.x) + (vec31.z - vec3.z) * (vec31.z - vec3.z));
diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java
index 8270d76a753bfd26a4c8ef6610bee5c24ee59cfe..7c385baae81b9a987c0e1e4deb017884600331bc 100644
--- a/net/minecraft/world/entity/raid/Raider.java
+++ b/net/minecraft/world/entity/raid/Raider.java
@@ -86,7 +86,7 @@ public abstract class Raider extends PatrollingMonster {
Raid currentRaid = this.getCurrentRaid();
if (this.canJoinRaid()) {
if (currentRaid == null) {
- if (this.level().getGameTime() % 20L == 0L) {
+ if (this.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading
Raid raidAt = ((ServerLevel)this.level()).getRaidAt(this.blockPosition());
if (raidAt != null && Raids.canJoinRaid(this, raidAt)) {
raidAt.joinRaid(raidAt.getGroupsSpawned(), this, null, true);
diff --git a/net/minecraft/world/entity/raid/Raids.java b/net/minecraft/world/entity/raid/Raids.java
index 34eb038725d1577f1a2d7c35c897b1270eac5749..0ffc1956d9e808871c5b36f6eb5ed750abaa880c 100644
--- a/net/minecraft/world/entity/raid/Raids.java
+++ b/net/minecraft/world/entity/raid/Raids.java
@@ -25,9 +25,9 @@ import net.minecraft.world.phys.Vec3;
public class Raids extends SavedData {
private static final String RAID_FILE_ID = "raids";
- public final Map<Integer, Raid> raidMap = Maps.newHashMap();
+ public final Map<Integer, Raid> raidMap = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - make raids thread-safe
private final ServerLevel level;
- private int nextAvailableID;
+ private final java.util.concurrent.atomic.AtomicInteger nextAvailableID = new java.util.concurrent.atomic.AtomicInteger(); // Folia - make raids thread-safe
private int tick;
public static SavedData.Factory<Raids> factory(ServerLevel level) {
@@ -36,7 +36,7 @@ public class Raids extends SavedData {
public Raids(ServerLevel level) {
this.level = level;
- this.nextAvailableID = 1;
+ this.nextAvailableID.set(1); // Folia - make raids thread-safe
this.setDirty();
}
@@ -44,12 +44,25 @@ public class Raids extends SavedData {
return this.raidMap.get(id);
}
+ // Folia start - make raids thread-safe
+ public void globalTick() {
+ ++this.tick;
+ if (this.tick % 200 == 0) {
+ this.setDirty();
+ }
+ }
+
public void tick() {
- this.tick++;
+ // Folia end - make raids thread-safe
Iterator<Raid> iterator = this.raidMap.values().iterator();
while (iterator.hasNext()) {
Raid raid = iterator.next();
+ // Folia start - make raids thread-safe
+ if (!raid.ownsRaid()) {
+ continue;
+ }
+ // Folia end - make raids thread-safe
if (this.level.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS)) {
raid.stop();
}
@@ -62,14 +75,17 @@ public class Raids extends SavedData {
}
}
- if (this.tick % 200 == 0) {
- this.setDirty();
- }
+ // Folia - make raids thread-safe - move to globalTick()
DebugPackets.sendRaids(this.level, this.raidMap.values());
}
public static boolean canJoinRaid(Raider raider, Raid raid) {
+ // Folia start - make raids thread-safe
+ if (!raid.ownsRaid()) {
+ return false;
+ }
+ // Folia end - make raids thread-safe
return raider != null
&& raid != null
&& raid.getLevel() != null
@@ -87,7 +103,7 @@ public class Raids extends SavedData {
return null;
} else {
DimensionType dimensionType = player.level().dimensionType();
- if (!dimensionType.hasRaids()) {
+ if (!dimensionType.hasRaids() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, pos.getX() >> 4, pos.getZ() >> 4, 8) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, player.chunkPosition().x, player.chunkPosition().z, 8)) { // Folia - region threading
return null;
} else {
List<PoiRecord> list = this.level
@@ -145,7 +161,7 @@ public class Raids extends SavedData {
public static Raids load(ServerLevel level, CompoundTag tag) {
Raids raids = new Raids(level);
- raids.nextAvailableID = tag.getInt("NextAvailableID");
+ raids.nextAvailableID.set(tag.getInt("NextAvailableID")); // Folia - make raids thread-safe
raids.tick = tag.getInt("Tick");
ListTag list = tag.getList("Raids", 10);
@@ -160,7 +176,7 @@ public class Raids extends SavedData {
@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
- tag.putInt("NextAvailableID", this.nextAvailableID);
+ tag.putInt("NextAvailableID", this.nextAvailableID.get()); // Folia - make raids thread-safe
tag.putInt("Tick", this.tick);
ListTag listTag = new ListTag();
@@ -179,7 +195,7 @@ public class Raids extends SavedData {
}
private int getUniqueId() {
- return ++this.nextAvailableID;
+ return this.nextAvailableID.incrementAndGet(); // Folia - make raids thread-safe
}
@Nullable
@@ -188,6 +204,11 @@ public class Raids extends SavedData {
double d = distance;
for (Raid raid1 : this.raidMap.values()) {
+ // Folia start - make raids thread-safe
+ if (!raid1.ownsRaid()) {
+ continue;
+ }
+ // Folia end - make raids thread-safe
double d1 = raid1.getCenter().distSqr(pos);
if (raid1.isActive() && d1 < d) {
raid = raid1;
diff --git a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
index 82421d3b4116ca406cdfffec5a3d65a99cbe294b..3a575ff4860c3b000a23e7754181f48d942441e9 100644
--- a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
+++ b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java
@@ -145,5 +145,11 @@ public class MinecartCommandBlock extends AbstractMinecart {
return net.minecraft.world.entity.vehicle.MinecartCommandBlock.this.getBukkitEntity();
}
// CraftBukkit end
+ // Folia start
+ @Override
+ public void threadCheck() {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(MinecartCommandBlock.this, "Asynchronous sendSystemMessage to a command block");
+ }
+ // Folia end
}
}
diff --git a/net/minecraft/world/entity/vehicle/MinecartHopper.java b/net/minecraft/world/entity/vehicle/MinecartHopper.java
index 8341e7f01606fca90e69384c16fc19bb9e20d1b7..c07f6fefdba5242c09c0081a0f074948f9df9ae6 100644
--- a/net/minecraft/world/entity/vehicle/MinecartHopper.java
+++ b/net/minecraft/world/entity/vehicle/MinecartHopper.java
@@ -145,7 +145,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper
// Paper start
public void immunize() {
- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20);
+ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20);
}
// Paper end
diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java
index 76f50437396f8f856381d0fbef52953ef7c263f6..d3b98f1d36b643989708ea22753a0c0d0d4243bc 100644
--- a/net/minecraft/world/item/ItemStack.java
+++ b/net/minecraft/world/item/ItemStack.java
@@ -386,31 +386,32 @@ public final class ItemStack implements DataComponentHolder {
DataComponentPatch previousPatch = this.components.asPatch();
int oldCount = this.getCount();
ServerLevel serverLevel = (ServerLevel) context.getLevel();
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = serverLevel.getCurrentWorldData(); // Folia - region threading
if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement
- serverLevel.captureBlockStates = true;
+ worldData.captureBlockStates = true; // Folia - region threading
// special case bonemeal
if (item == Items.BONE_MEAL) {
- serverLevel.captureTreeGeneration = true;
+ worldData.captureTreeGeneration = true; // Folia - region threading
}
}
InteractionResult interactionResult;
try {
interactionResult = item.useOn(context);
} finally {
- serverLevel.captureBlockStates = false;
+ worldData.captureBlockStates = false; // Folia - region threading
}
DataComponentPatch newPatch = this.components.asPatch();
int newCount = this.getCount();
this.setCount(oldCount);
this.restorePatch(previousPatch);
- if (interactionResult.consumesAction() && serverLevel.captureTreeGeneration && !serverLevel.capturedBlockStates.isEmpty()) {
- serverLevel.captureTreeGeneration = false;
+ if (interactionResult.consumesAction() && worldData.captureTreeGeneration && !worldData.capturedBlockStates.isEmpty()) { // Folia - region threading
+ worldData.captureTreeGeneration = false; // Folia - region threading
org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(clickedPos, serverLevel.getWorld());
- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType;
- net.minecraft.world.level.block.SaplingBlock.treeType = null;
- List<org.bukkit.craftbukkit.block.CraftBlockState> 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<org.bukkit.craftbukkit.block.CraftBlockState> blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading
+ worldData.capturedBlockStates.clear(); // Folia - region threading
org.bukkit.event.world.StructureGrowEvent structureEvent = null;
if (treeType != null) {
boolean isBonemeal = this.getItem() == Items.BONE_MEAL;
@@ -436,15 +437,15 @@ public final class ItemStack implements DataComponentHolder {
player.awardStat(Stats.ITEM_USED.get(item)); // SPIGOT-7236 - award stat
}
- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return
+ SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading
return interactionResult;
}
- serverLevel.captureTreeGeneration = false;
+ worldData.captureTreeGeneration = false; // Folia - region threading
if (player != null && interactionResult instanceof InteractionResult.Success success && success.wasItemInteraction()) {
InteractionHand hand = context.getHand();
org.bukkit.event.block.BlockPlaceEvent placeEvent = null;
- List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values());
- serverLevel.capturedBlockStates.clear();
+ List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading
+ worldData.capturedBlockStates.clear(); // Folia - region threading
if (blocks.size() > 1) {
placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(serverLevel, player, hand, blocks, clickedPos.getX(), clickedPos.getY(), clickedPos.getZ());
} else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement
@@ -455,17 +456,17 @@ public final class ItemStack implements DataComponentHolder {
interactionResult = InteractionResult.FAIL; // cancel placement
// PAIL: Remove this when MC-99075 fixed
placeEvent.getPlayer().updateInventory();
- serverLevel.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot
+ worldData.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot // Folia - region threading
// revert back all captured blocks
- serverLevel.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710
- serverLevel.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
+ worldData.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 // Folia - region threading
+ worldData.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading
for (org.bukkit.block.BlockState blockstate : blocks) {
blockstate.update(true, false);
}
- serverLevel.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
- serverLevel.preventPoiUpdated = false;
+ worldData.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading
+ worldData.preventPoiUpdated = false; // Folia - region threading
- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return
+ SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading
} else {
// Change the stack to its new contents if it hasn't been tampered with.
if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), previousPatch)) {
@@ -473,7 +474,7 @@ public final class ItemStack implements DataComponentHolder {
this.setCount(newCount);
}
- for (java.util.Map.Entry<BlockPos, net.minecraft.world.level.block.entity.BlockEntity> e : serverLevel.capturedTileEntities.entrySet()) {
+ for (java.util.Map.Entry<BlockPos, net.minecraft.world.level.block.entity.BlockEntity> e : worldData.capturedTileEntities.entrySet()) { // Folia - region threading
serverLevel.setBlockEntity(e.getValue());
}
@@ -508,15 +509,15 @@ public final class ItemStack implements DataComponentHolder {
}
// SPIGOT-4678
- if (this.item instanceof SignItem && SignItem.openSign != null) {
+ if (this.item instanceof SignItem && SignItem.openSign.get() != null) { // Folia - region threading
try {
- if (serverLevel.getBlockEntity(SignItem.openSign) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) {
- if (serverLevel.getBlockState(SignItem.openSign).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) {
+ if (serverLevel.getBlockEntity(SignItem.openSign.get()) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) { // Folia - region threading
+ if (serverLevel.getBlockState(SignItem.openSign.get()).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) { // Folia - region threading
signBlock.openTextEdit(player, blockEntity, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // CraftBukkit // Paper - Add PlayerOpenSignEvent
}
}
} finally {
- SignItem.openSign = null;
+ SignItem.openSign.set(null);
}
}
@@ -544,8 +545,8 @@ public final class ItemStack implements DataComponentHolder {
player.awardStat(Stats.ITEM_USED.get(item));
}
}
- serverLevel.capturedTileEntities.clear();
- serverLevel.capturedBlockStates.clear();
+ worldData.capturedTileEntities.clear(); // Folia - region threading
+ worldData.capturedBlockStates.clear(); // Folia - region threading
// CraftBukkit end
return interactionResult;
diff --git a/net/minecraft/world/item/MapItem.java b/net/minecraft/world/item/MapItem.java
index 8795d54cff569c911e0a535f38a0ec4130f7b4d5..9f07ce560e265582eec0fff5877a923f62a60e13 100644
--- a/net/minecraft/world/item/MapItem.java
+++ b/net/minecraft/world/item/MapItem.java
@@ -70,6 +70,7 @@ public class MapItem extends Item {
}
public void update(Level level, Entity viewer, MapItemSavedData data) {
+ synchronized (data) { // Folia - make map data thread-safe
if (level.dimension() == data.dimension && viewer instanceof Player) {
int i = 1 << data.scale;
int i1 = data.centerX;
@@ -99,8 +100,8 @@ public class MapItem extends Item {
int i9 = (i1 / i + i6 - 64) * i;
int i10 = (i2 / i + i7 - 64) * i;
Multiset<MapColor> multiset = LinkedHashMultiset.create();
- LevelChunk chunk = level.getChunkIfLoaded(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10)); // Paper - Maps shouldn't load chunks
- if (chunk != null && !chunk.isEmpty()) { // Paper - Maps shouldn't load chunks
+ LevelChunk chunk = level.getChunkIfLoaded(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10)); // Paper - Maps shouldn't load chunks // Folia - super important that it uses getChunkIfLoaded
+ if (chunk != null && !chunk.isEmpty() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, chunk.getPos())) { // Paper - Maps shouldn't load chunks // Folia - make sure chunk is owned
int i11 = 0;
double d1 = 0.0;
if (level.dimensionType().hasCeiling()) {
@@ -182,6 +183,7 @@ public class MapItem extends Item {
}
}
}
+ } // Folia - make map data thread-safe
}
private BlockState getCorrectStateForFluidBlock(Level level, BlockState state, BlockPos pos) {
@@ -196,6 +198,7 @@ public class MapItem extends Item {
public static void renderBiomePreviewMap(ServerLevel serverLevel, ItemStack stack) {
MapItemSavedData savedData = getSavedData(stack, serverLevel);
if (savedData != null) {
+ synchronized (savedData) { // Folia - make map data thread-safe
if (serverLevel.dimension() == savedData.dimension) {
int i = 1 << savedData.scale;
int i1 = savedData.centerX;
@@ -265,6 +268,7 @@ public class MapItem extends Item {
}
}
}
+ } // Folia - make map data thread-safe
}
}
@@ -273,6 +277,7 @@ public class MapItem extends Item {
if (!level.isClientSide) {
MapItemSavedData savedData = getSavedData(stack, level);
if (savedData != null) {
+ synchronized (savedData) { // Folia - region threading
if (entity instanceof Player player) {
savedData.tickCarriedBy(player, stack);
}
@@ -280,6 +285,7 @@ public class MapItem extends Item {
if (!savedData.locked && (isSelected || entity instanceof Player && ((Player)entity).getOffhandItem() == stack)) {
this.update(level, entity, savedData);
}
+ } // Folia - region threading
}
}
}
diff --git a/net/minecraft/world/item/SignItem.java b/net/minecraft/world/item/SignItem.java
index fffac12db30d4321981959a9149cc56f8b4f6df6..fdf4fa92a5ca98fae6266e29a54fb1b77e69407c 100644
--- a/net/minecraft/world/item/SignItem.java
+++ b/net/minecraft/world/item/SignItem.java
@@ -11,7 +11,7 @@ import net.minecraft.world.level.block.entity.SignBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
public class SignItem extends StandingAndWallBlockItem {
- public static BlockPos openSign; // CraftBukkit
+ public static final ThreadLocal<BlockPos> openSign = new ThreadLocal<>(); // CraftBukkit // Folia - region threading
public SignItem(Block standingBlock, Block wallBlock, Item.Properties properties) {
super(standingBlock, wallBlock, Direction.DOWN, properties);
}
@@ -30,7 +30,7 @@ public class SignItem extends StandingAndWallBlockItem {
&& level.getBlockState(pos).getBlock() instanceof SignBlock signBlock) {
// CraftBukkit start - SPIGOT-4678
// signBlock.openTextEdit(player, signBlockEntity, true);
- SignItem.openSign = pos;
+ SignItem.openSign.set(pos); // Folia - region threading
// CraftBukkit end
}
diff --git a/net/minecraft/world/item/component/LodestoneTracker.java b/net/minecraft/world/item/component/LodestoneTracker.java
index 0c00c23743a4978e8dceed5bbee8ca44b0e0c8d6..b6de5d017bed5c71125f26881b95383386aa1a79 100644
--- a/net/minecraft/world/item/component/LodestoneTracker.java
+++ b/net/minecraft/world/item/component/LodestoneTracker.java
@@ -29,7 +29,10 @@ public record LodestoneTracker(Optional<GlobalPos> target, boolean tracked) {
return this;
} else {
BlockPos blockPos = this.target.get().pos();
- return level.isInWorldBounds(blockPos) && (!level.hasChunkAt(blockPos) || level.getPoiManager().existsAtPosition(PoiTypes.LODESTONE, blockPos)) // Paper - Prevent compass from loading chunks
+ // Folia start - do not access the POI data off-region
+ net.minecraft.world.level.chunk.LevelChunk chunk = level.getChunkIfLoaded(blockPos);
+ return level.isInWorldBounds(blockPos) && (chunk == null || chunk.getBlockState(blockPos).getBlock() == net.minecraft.world.level.block.Blocks.LODESTONE) // Paper - Prevent compass from loading chunks
+ // Folia end - do not access the POI data off-region
? this
: new LodestoneTracker(Optional.empty(), true);
}
diff --git a/net/minecraft/world/level/BaseCommandBlock.java b/net/minecraft/world/level/BaseCommandBlock.java
index a67d40eb4bfa85888af8bf027a8859378d290cfa..b02b79ccedb8b87bc22270377dfc36e21ebe1724 100644
--- a/net/minecraft/world/level/BaseCommandBlock.java
+++ b/net/minecraft/world/level/BaseCommandBlock.java
@@ -21,7 +21,7 @@ import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.Vec3;
public abstract class BaseCommandBlock implements CommandSource {
- private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss");
+ private static final ThreadLocal<SimpleDateFormat> TIME_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss")); // Folia - region threading - SDF is not thread-safe
private static final Component DEFAULT_NAME = Component.literal("@");
private long lastExecution = -1L;
private boolean updateLastExecution = true;
@@ -114,6 +114,7 @@ public abstract class BaseCommandBlock implements CommandSource {
}
public boolean performCommand(Level level) {
+ if (true) return false; // Folia - region threading
if (level.isClientSide || level.getGameTime() == this.lastExecution) {
return false;
} else if ("Searge".equalsIgnoreCase(this.command)) {
@@ -164,11 +165,14 @@ public abstract class BaseCommandBlock implements CommandSource {
this.customName = customName;
}
+ public void threadCheck() {} // Folia
+
@Override
public void sendSystemMessage(Component component) {
if (this.trackOutput) {
org.spigotmc.AsyncCatcher.catchOp("sendSystemMessage to a command block"); // Paper - Don't broadcast messages to command blocks
- this.lastOutput = Component.literal("[" + TIME_FORMAT.format(new Date()) + "] ").append(component);
+ this.threadCheck(); // Folia
+ this.lastOutput = Component.literal("[" + TIME_FORMAT.get().format(new Date()) + "] ").append(component); // Folia - region threading - SDF is not thread-safe
this.onUpdated();
}
}
diff --git a/net/minecraft/world/level/EntityGetter.java b/net/minecraft/world/level/EntityGetter.java
index 892a7c1eb1b321ca6d5ca709142e7feae1220815..c4c7993714ff4189b05e2b4653fd2a5eba6b7da9 100644
--- a/net/minecraft/world/level/EntityGetter.java
+++ b/net/minecraft/world/level/EntityGetter.java
@@ -24,6 +24,12 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst
return this.getEntities(EntityTypeTest.forClass(entityClass), area, filter);
}
+ // Folia start - region threading
+ default List<? extends Player> getLocalPlayers() {
+ return java.util.Collections.emptyList();
+ }
+ // Folia end - region threading
+
List<? extends Player> players();
default List<Entity> 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<org.bukkit.entity.HumanEntity> findNearbyBukkitPlayers(double x, double y, double z, double radius, @Nullable Predicate<Entity> predicate) {
com.google.common.collect.ImmutableList.Builder<org.bukkit.entity.HumanEntity> builder = com.google.common.collect.ImmutableList.builder();
- for (Player human : this.players()) {
+ for (Player human : this.getLocalPlayers()) { // Folia - region threading
if (predicate == null || predicate.test(human)) {
double distanceSquared = human.distanceToSqr(x, y, z);
@@ -171,7 +177,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst
// Paper start - Affects Spawning API
default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) {
- for (Player player : this.players()) {
+ for (Player player : this.getLocalPlayers()) { // Folia - region threading
if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check
double distanceSqr = player.distanceToSqr(x, y, z);
if (range < 0.0D || distanceSqr < range * range) {
@@ -184,7 +190,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst
// Paper end - Affects Spawning API
default boolean hasNearbyAlivePlayer(double x, double y, double z, double distance) {
- for (Player player : this.players()) {
+ for (Player player : this.getLocalPlayers()) { // Folia - region threading
if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) {
double d = player.distanceToSqr(x, y, z);
if (distance < 0.0 || d < distance * distance) {
@@ -198,8 +204,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst
@Nullable
default Player getPlayerByUUID(UUID uniqueId) {
- for (int i = 0; i < this.players().size(); i++) {
- Player player = this.players().get(i);
+ for (Player player : this.getLocalPlayers()) { // Folia - region threading
if (uniqueId.equals(player.getUUID())) {
return player;
}
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index 1dbe7c7c1051c3972105534a07ce50d4cf98fc85..db4ce98706bf69dcd8144faba1780f83ca1f6787 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -115,10 +115,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
public static final int TICKS_PER_DAY = 24000;
public static final int MAX_ENTITY_SPAWN_Y = 20000000;
public static final int MIN_ENTITY_SPAWN_Y = -20000000;
- public final List<TickingBlockEntity> blockEntityTickers = Lists.newArrayList(); // Paper - public
- protected final NeighborUpdater neighborUpdater;
- private final List<TickingBlockEntity> pendingBlockEntityTickers = Lists.newArrayList();
- private boolean tickingBlockEntities;
+ //public final List<TickingBlockEntity> blockEntityTickers = Lists.newArrayList(); // Paper - public // Folia - region threading
+ public final int neighbourUpdateMax; //protected final NeighborUpdater neighborUpdater; // Folia - region threading
+ //private final List<TickingBlockEntity> pendingBlockEntityTickers = Lists.newArrayList(); // Folia - region threading
+ //private boolean tickingBlockEntities; // Folia - region threading
public final Thread thread;
private final boolean isDebug;
private int skyDarken;
@@ -128,7 +128,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
public float rainLevel;
protected float oThunderLevel;
public float thunderLevel;
- public final RandomSource random = new ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); // Paper - replace random
+ public final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Paper - replace random // Folia - region threading
@Deprecated
private final RandomSource threadSafeRandom = RandomSource.createThreadSafe();
private final Holder<DimensionType> dimensionTypeRegistration;
@@ -139,28 +139,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
private final ResourceKey<Level> dimension;
private final RegistryAccess registryAccess;
private final DamageSources damageSources;
- private long subTickCount;
+ private final java.util.concurrent.atomic.AtomicLong subTickCount = new java.util.concurrent.atomic.AtomicLong(); //private long subTickCount; // Folia - region threading
// CraftBukkit start Added the following
private final CraftWorld world;
public boolean pvpMode;
public org.bukkit.generator.ChunkGenerator generator;
- public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
- public boolean captureBlockStates = false;
- public boolean captureTreeGeneration = false;
- public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
- public Map<BlockPos, org.bukkit.craftbukkit.block.CraftBlockState> capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper
- public Map<BlockPos, BlockEntity> capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates
- public List<net.minecraft.world.entity.item.ItemEntity> captureDrops;
+ // Folia - region threading - moved to regionised data
public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
- // Paper start
- public int wakeupInactiveRemainingAnimals;
- public int wakeupInactiveRemainingFlying;
- public int wakeupInactiveRemainingMonsters;
- public int wakeupInactiveRemainingVillagers;
- // Paper end
- public boolean populating;
+ // Folia - region threading - moved to regionised data
+ // Folia - region threading
public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
// Paper start - add paper world config
private final io.papermc.paper.configuration.WorldConfiguration paperConfig;
@@ -173,9 +162,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
public static BlockPos lastPhysicsProblem; // Spigot
private org.spigotmc.TickLimiter entityLimiter;
private org.spigotmc.TickLimiter tileLimiter;
- private int tileTickPosition;
- public final Map<ServerExplosion.CacheKey, Float> explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions
- public java.util.ArrayDeque<net.minecraft.world.level.block.RedstoneTorchBlock.Toggle> redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here
+ //private int tileTickPosition; // Folia - region threading
+ //public final Map<ServerExplosion.CacheKey, Float> explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions // Folia - region threading
+ //public java.util.ArrayDeque<net.minecraft.world.level.block.RedstoneTorchBlock.Toggle> redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here // Folia - region threading
public CraftWorld getWorld() {
return this.world;
@@ -825,6 +814,32 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z);
}
// Paper end - optimise random ticking
+ // Folia start - region ticking
+ public final io.papermc.paper.threadedregions.RegionizedData<io.papermc.paper.threadedregions.RegionizedWorldData> 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<net.minecraft.server.level.ChunkHolder, Boolean> 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<net.minecraft.server.level.ServerPlayer> getLocalPlayers() {
+ return this.getCurrentWorldData().getLocalPlayers();
+ }
+ // Folia end - region ticking
protected Level(
WritableLevelData levelData,
@@ -888,7 +903,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
this.thread = Thread.currentThread();
this.biomeManager = new BiomeManager(this, biomeZoomSeed);
this.isDebug = isDebug;
- this.neighborUpdater = new CollectingNeighborUpdater(this, maxChainedNeighborUpdates);
+ this.neighbourUpdateMax = maxChainedNeighborUpdates; // Folia - region threading
this.registryAccess = registryAccess;
this.damageSources = new DamageSources(registryAccess);
@@ -1035,8 +1050,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@Nullable
public final BlockState getBlockStateIfLoaded(BlockPos pos) {
// CraftBukkit start - tree generation
- if (this.captureTreeGeneration) {
- CraftBlockState previous = this.capturedBlockStates.get(pos);
+ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading
+ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Folia - region threading
if (previous != null) {
return previous.getHandle();
}
@@ -1098,16 +1113,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@Override
public boolean setBlock(BlockPos pos, BlockState state, int flags, int recursionLeft) {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, pos, "Updating block asynchronously"); // Folia - region threading
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); // Folia - region threading
// CraftBukkit start - tree generation
- if (this.captureTreeGeneration) {
+ if (worldData.captureTreeGeneration) { // Folia - region threading
// Paper start - Protect Bedrock and End Portal/Frames from being destroyed
BlockState type = getBlockState(pos);
if (!type.isDestroyable()) return false;
// Paper end - Protect Bedrock and End Portal/Frames from being destroyed
- CraftBlockState blockstate = this.capturedBlockStates.get(pos);
+ CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Folia - region threading
if (blockstate == null) {
blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags);
- this.capturedBlockStates.put(pos.immutable(), blockstate);
+ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading
}
blockstate.setData(state);
blockstate.setFlag(flags);
@@ -1123,10 +1140,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
Block block = state.getBlock();
// CraftBukkit start - capture blockstates
boolean captured = false;
- if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) {
+ if (worldData.captureBlockStates && !worldData.capturedBlockStates.containsKey(pos)) { // Folia - region threading
CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot
blockstate.setFlag(flags); // Paper - set flag
- this.capturedBlockStates.put(pos.immutable(), blockstate);
+ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading
captured = true;
}
// CraftBukkit end
@@ -1136,8 +1153,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
if (blockState == null) {
// CraftBukkit start - remove blockstate if failed (or the same)
- if (this.captureBlockStates && captured) {
- this.capturedBlockStates.remove(pos);
+ if (worldData.captureBlockStates && captured) { // Folia - region threading
+ worldData.capturedBlockStates.remove(pos); // Folia - region threading
}
// CraftBukkit end
return false;
@@ -1174,7 +1191,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
*/
// CraftBukkit start
- if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates
+ if (!worldData.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates // Folia - region threading
// Modularize client and physic updates
// Spigot start
try {
@@ -1219,7 +1236,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam
CraftWorld world = ((ServerLevel) this).getWorld();
boolean cancelledUpdates = false; // Paper - Fix block place logic
- if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent
+ if (world != null && ((ServerLevel)this).getCurrentWorldData().hasPhysicsEvent) { // Paper - BlockPhysicsEvent // Folia - region threading
BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata));
this.getCraftServer().getPluginManager().callEvent(event);
@@ -1233,7 +1250,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
}
// CraftBukkit start - SPIGOT-5710
- if (!this.preventPoiUpdated) {
+ if (!this.getCurrentWorldData().preventPoiUpdated) { // Folia - region threading
this.onBlockStateChange(blockposition, iblockdata1, iblockdata2);
}
// CraftBukkit end
@@ -1322,7 +1339,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@Override
public void neighborShapeChanged(Direction direction, BlockPos pos, BlockPos neighborPos, BlockState neighborState, int flags, int recursionLeft) {
- this.neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, recursionLeft);
+ this.getCurrentWorldData().neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, recursionLeft); // Folia - region threading
}
@Override
@@ -1346,11 +1363,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
return this.getChunkSource().getLightEngine();
}
+ // Folia start - region threading
+ @Nullable
+ public BlockState getBlockStateFromEmptyChunkIfLoaded(BlockPos pos) {
+ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource();
+ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4);
+ if (chunk != null) {
+ return chunk.getBlockState(pos);
+ }
+ return null;
+ }
+
+ @Nullable
+ public BlockState getBlockStateFromEmptyChunk(BlockPos pos) {
+ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource();
+ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4);
+ if (chunk != null) {
+ return chunk.getBlockState(pos);
+ }
+ chunk = chunkProvider.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY, true);
+ return chunk.getBlockState(pos);
+ }
+ // Folia end - region threading
+
@Override
public BlockState getBlockState(BlockPos pos) {
// CraftBukkit start - tree generation
- if (this.captureTreeGeneration) {
- CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper
+ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading
+ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Paper // Folia - region threading
if (previous != null) {
return previous.getHandle();
}
@@ -1454,17 +1494,16 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
}
public void addBlockEntityTicker(TickingBlockEntity ticker) {
- (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker);
+ ((ServerLevel)this).getCurrentWorldData().addBlockEntityTicker(ticker); // Folia - regionised ticking
}
protected void tickBlockEntities() {
ProfilerFiller profilerFiller = Profiler.get();
profilerFiller.push("blockEntities");
- this.tickingBlockEntities = true;
- if (!this.pendingBlockEntityTickers.isEmpty()) {
- this.blockEntityTickers.addAll(this.pendingBlockEntityTickers);
- this.pendingBlockEntityTickers.clear();
- }
+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking
+ regionizedWorldData.seTtickingBlockEntities(true); // Folia - regionised ticking
+ regionizedWorldData.pushPendingTickingBlockEntities(); // Folia - regionised ticking
+ List<TickingBlockEntity> blockEntityTickers = regionizedWorldData.getBlockEntityTickers(); // Folia - regionised ticking
// Spigot start
boolean runsNormally = this.tickRateManager().runsNormally();
@@ -1472,9 +1511,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
int tickedEntities = 0; // Paper - rewrite chunk system
var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<TickingBlockEntity>(); // Paper - Fix MC-117075; use removeAll
toRemove.add(null); // Paper - Fix MC-117075
- for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters
- this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0;
- TickingBlockEntity tickingBlockEntity = this.blockEntityTickers.get(this.tileTickPosition);
+ for (int i = 0; i < blockEntityTickers.size(); i++) { // Paper - Disable tick limiters // Folia - regionised ticking
+ TickingBlockEntity tickingBlockEntity = blockEntityTickers.get(i); // Folia - regionised ticking
// Spigot end
if (tickingBlockEntity.isRemoved()) {
toRemove.add(tickingBlockEntity); // Paper - Fix MC-117075; use removeAll
@@ -1487,11 +1525,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
// Paper end - rewrite chunk system
}
}
- this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075
+ blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 // Folia - regionised ticking
- this.tickingBlockEntities = false;
+ regionizedWorldData.seTtickingBlockEntities(false); // Folia - regionised ticking
profilerFiller.pop();
- this.spigotConfig.currentPrimedTnt = 0; // Spigot
+ regionizedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading
}
public <T extends Entity> void guardEntityTick(Consumer<T> consumerEntity, T entity) {
@@ -1502,7 +1540,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ());
MinecraftServer.LOGGER.error(msg, var6);
getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, var6))); // Paper - ServerExceptionEvent
- entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
+ if (!(entity instanceof net.minecraft.server.level.ServerPlayer)) entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); // Folia - properly disconnect players
+ if (entity instanceof net.minecraft.server.level.ServerPlayer player) player.connection.disconnect(net.minecraft.network.chat.Component.translatable("multiplayer.disconnect.generic"), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Folia - properly disconnect players
// Paper end - Prevent block entity and entity crashes
}
this.moonrise$midTickTasks(); // Paper - rewrite chunk system
@@ -1648,9 +1687,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@Nullable
public BlockEntity getBlockEntity(BlockPos pos, boolean validate) {
+ // Folia start - region threading
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) {
+ return null;
+ }
+ // Folia end - region threading
// Paper start - Perf: Optimize capturedTileEntities lookup
net.minecraft.world.level.block.entity.BlockEntity blockEntity;
- if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(pos)) != null) {
+ if (!this.getCurrentWorldData().capturedTileEntities.isEmpty() && (blockEntity = this.getCurrentWorldData().capturedTileEntities.get(pos)) != null) { // Folia - region threading
return blockEntity;
}
// Paper end - Perf: Optimize capturedTileEntities lookup
@@ -1668,8 +1712,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
BlockPos blockPos = blockEntity.getBlockPos();
if (!this.isOutsideBuildHeight(blockPos)) {
// CraftBukkit start
- if (this.captureBlockStates) {
- this.capturedTileEntities.put(blockPos.immutable(), blockEntity);
+ if (this.getCurrentWorldData().captureBlockStates) { // Folia - region threading
+ this.getCurrentWorldData().capturedTileEntities.put(blockPos.immutable(), blockEntity); // Folia - region threading
return;
}
// CraftBukkit end
@@ -1749,6 +1793,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@Override
public List<Entity> getEntities(@Nullable Entity entity, AABB boundingBox, Predicate<? super Entity> predicate) {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, boundingBox, "Cannot getEntities asynchronously"); // Folia - region threading
Profiler.get().incrementCounter("getEntities");
List<Entity> list = Lists.newArrayList();
@@ -1778,6 +1823,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
public <T extends Entity> void getEntities(final EntityTypeTest<Entity, T> entityTypeTest,
final AABB boundingBox, final Predicate<? super T> predicate,
final List<? super T> 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<T> byType) {
@@ -1877,13 +1923,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
public void disconnect() {
}
+ @Override // Folia - region threading
public long getGameTime() {
- return this.levelData.getGameTime();
+ // Folia start - region threading
+ // Dumb world gen thread calls this for some reason. So, check for null.
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData();
+ return worldData == null ? this.getLevelData().getGameTime() : worldData.getTickData().nonRedstoneGameTime();
+ // Folia end - region threading
}
public long getDayTime() {
- return this.levelData.getDayTime();
+ // Folia start - region threading
+ // Dumb world gen thread calls this for some reason. So, check for null.
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData();
+ return worldData == null ? this.getLevelData().getDayTime() : worldData.getTickData().dayTime();
+ // Folia end - region threading
+ }
+
+ // Folia start - region threading
+ @Override
+ public long dayTime() {
+ return this.getDayTime();
+ }
+
+ @Override
+ public long getRedstoneGameTime() {
+ return this.getCurrentWorldData().getRedstoneGameTime();
}
+ // Folia end - region threading
public boolean mayInteract(Player player, BlockPos pos) {
return true;
@@ -2061,8 +2128,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
public abstract RecipeAccess recipeAccess();
public BlockPos getBlockRandomPos(int x, int y, int z, int yMask) {
- this.randValue = this.randValue * 3 + 1013904223;
- int i = this.randValue >> 2;
+ int i = this.random.nextInt() >> 2; // Folia - region threading
return new BlockPos(x + (i & 15), y + (i >> 16 & yMask), z + (i >> 8 & 15));
}
@@ -2083,7 +2149,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@Override
public long nextSubTickCount() {
- return this.subTickCount++;
+ return this.subTickCount.getAndIncrement(); // Folia - region threading
}
@Override
diff --git a/net/minecraft/world/level/LevelAccessor.java b/net/minecraft/world/level/LevelAccessor.java
index ee9d320da1b4c3aa66be6592867e95c706b65b3a..cd5bfa374b0b1af64bc8415ace94fa43955e5145 100644
--- a/net/minecraft/world/level/LevelAccessor.java
+++ b/net/minecraft/world/level/LevelAccessor.java
@@ -33,14 +33,24 @@ public interface LevelAccessor extends CommonLevelAccessor, LevelTimeAccess, Sch
long nextSubTickCount();
+ // Folia start - region threading
+ default long getGameTime() {
+ return this.getLevelData().getGameTime();
+ }
+
+ default long getRedstoneGameTime() {
+ return this.getLevelData().getGameTime();
+ }
+ // Folia end - region threading
+
@Override
default <T> ScheduledTick<T> 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 <T> ScheduledTick<T> createTick(BlockPos pos, T type, int delay) {
- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + delay, this.nextSubTickCount());
+ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + delay, this.nextSubTickCount()); // Folia - region threading
}
LevelData getLevelData();
diff --git a/net/minecraft/world/level/LevelReader.java b/net/minecraft/world/level/LevelReader.java
index 26c8c1e5598daf3550aef05b12218c47bda6618b..e59e1bb91e446406e58cc8046a85b693adb11e86 100644
--- a/net/minecraft/world/level/LevelReader.java
+++ b/net/minecraft/world/level/LevelReader.java
@@ -204,6 +204,25 @@ public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_syste
return toY >= this.getMinY() && fromY <= this.getMaxY() && this.hasChunksAt(fromX, fromZ, toX, toZ);
}
+ // Folia start - region threading
+ default boolean hasAndOwnsChunksAt(int minX, int minZ, int maxX, int maxZ) {
+ int i = SectionPos.blockToSectionCoord(minX);
+ int j = SectionPos.blockToSectionCoord(maxX);
+ int k = SectionPos.blockToSectionCoord(minZ);
+ int l = SectionPos.blockToSectionCoord(maxZ);
+
+ for(int m = i; m <= j; ++m) {
+ for(int n = k; n <= l; ++n) {
+ if (!this.hasChunk(m, n) || (this instanceof net.minecraft.server.level.ServerLevel world && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, m, n))) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ // Folia end - region threading
+
@Deprecated
default boolean hasChunksAt(int fromX, int fromZ, int toX, int toZ) {
int sectionPosCoord = SectionPos.blockToSectionCoord(fromX);
diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java
index 17ce115e887cbbb06ad02ab7ddb488e27342c0e4..5ce81eafee33d22b69029c088d4be497131338a2 100644
--- a/net/minecraft/world/level/NaturalSpawner.java
+++ b/net/minecraft/world/level/NaturalSpawner.java
@@ -137,7 +137,7 @@ public final class NaturalSpawner {
int limit = mobCategory.getMaxInstancesPerChunk();
SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(mobCategory);
if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
- spawnThisTick = level.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(spawnCategory) == 0;
+ spawnThisTick = level.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && level.getRedstoneGameTime() % level.ticksPerSpawnCategory.getLong(spawnCategory) == 0; // Folia - region threading
limit = level.getWorld().getSpawnLimit(spawnCategory);
}
diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java
index c4485f28def66264846a436cfba7bddccb66b82e..73456d625489302e28c0452bde4508db0efa126c 100644
--- a/net/minecraft/world/level/ServerExplosion.java
+++ b/net/minecraft/world/level/ServerExplosion.java
@@ -773,17 +773,18 @@ public class ServerExplosion implements Explosion {
if (!this.level.paperConfig().environment.optimizeExplosions) {
return this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations
}
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading
CacheKey key = new CacheKey(this, entity.getBoundingBox());
- Float blockDensity = this.level.explosionDensityCache.get(key);
+ Float blockDensity = worldData.explosionDensityCache.get(key); // Folia - region threading
if (blockDensity == null) {
blockDensity = this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations
- this.level.explosionDensityCache.put(key, blockDensity);
+ worldData.explosionDensityCache.put(key, blockDensity); // Folia - region threading
}
return blockDensity;
}
- static class CacheKey {
+ public static class CacheKey { // Folia - region threading - public
private final Level world;
private final double posX, posY, posZ;
private final double minX, minY, minZ;
diff --git a/net/minecraft/world/level/ServerLevelAccessor.java b/net/minecraft/world/level/ServerLevelAccessor.java
index b4f14ff9ef0c212f4d0e0c2ccf20ce1e7af9e734..441ba6ae8885a968734ac0abdb8a9d09fa658430 100644
--- a/net/minecraft/world/level/ServerLevelAccessor.java
+++ b/net/minecraft/world/level/ServerLevelAccessor.java
@@ -6,6 +6,12 @@ import net.minecraft.world.entity.Entity;
public interface ServerLevelAccessor extends LevelAccessor {
ServerLevel getLevel();
+ // Folia start - region threading
+ default public StructureManager structureManager() {
+ throw new UnsupportedOperationException();
+ }
+ // Folia end - region threading
+
default void addFreshEntityWithPassengers(Entity entity) {
// CraftBukkit start
this.addFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT);
diff --git a/net/minecraft/world/level/StructureManager.java b/net/minecraft/world/level/StructureManager.java
index 8bc6a6c86cd8db53feefba7508b6031ba67e242e..9abfcfa3e8d8319e98866b2a81f2eb9ac7269055 100644
--- a/net/minecraft/world/level/StructureManager.java
+++ b/net/minecraft/world/level/StructureManager.java
@@ -48,12 +48,7 @@ public class StructureManager {
}
public List<StructureStart> startsForStructure(ChunkPos chunkPos, Predicate<Structure> structurePredicate) {
- // Paper start - Fix swamp hut cat generation deadlock
- return this.startsForStructure(chunkPos, structurePredicate, null);
- }
-
- public List<StructureStart> startsForStructure(ChunkPos chunkPos, Predicate<Structure> structurePredicate, @Nullable ServerLevelAccessor levelAccessor) {
- Map<Structure, LongSet> allReferences = (levelAccessor == null ? this.level : levelAccessor).getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences();
+ Map<Structure, LongSet> allReferences = this.level.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); // Folia - region threading
// Paper end - Fix swamp hut cat generation deadlock
Builder<StructureStart> builder = ImmutableList.builder();
@@ -124,20 +119,12 @@ public class StructureManager {
}
public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate<Holder<Structure>> predicate) {
- // Paper start - Fix swamp hut cat generation deadlock
- return this.getStructureWithPieceAt(pos, predicate, null);
- }
-
- public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey<Structure> tag, @Nullable ServerLevelAccessor levelAccessor) {
- return this.getStructureWithPieceAt(pos, structure -> structure.is(tag), levelAccessor);
- }
-
- public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate<Holder<Structure>> predicate, @Nullable ServerLevelAccessor levelAccessor) {
+ // Folia - region threading
// Paper end - Fix swamp hut cat generation deadlock
Registry<Structure> registry = this.registryAccess().lookupOrThrow(Registries.STRUCTURE);
for (StructureStart structureStart : this.startsForStructure(
- new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false), levelAccessor // Paper - Fix swamp hut cat generation deadlock
+ new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false) // Paper - Fix swamp hut cat generation deadlock // Folia - region threading
)) {
if (this.structureHasPieceAt(pos, structureStart)) {
return structureStart;
@@ -182,7 +169,7 @@ public class StructureManager {
}
public void addReference(StructureStart structureStart) {
- structureStart.addReference();
+ //structureStart.addReference(); // Folia - region threading - move to caller
this.structureCheck.incrementReference(structureStart.getChunkPos(), structureStart.getStructure());
}
diff --git a/net/minecraft/world/level/block/BedBlock.java b/net/minecraft/world/level/block/BedBlock.java
index 8c21e8aa4922691fa66cd22d631646c554251bdd..7328ab4bcb11b09713fed0625a0988cb1c9d43f2 100644
--- a/net/minecraft/world/level/block/BedBlock.java
+++ b/net/minecraft/world/level/block/BedBlock.java
@@ -346,7 +346,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock
BlockPos blockPos = pos.relative(state.getValue(FACING));
level.setBlock(blockPos, state.setValue(PART, BedPart.HEAD), 3);
// CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states
- if (level.captureBlockStates) {
+ if (level.getCurrentWorldData().captureBlockStates) { // Folia - region threading
return;
}
// CraftBukkit end
diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java
index 976de81d65b6494cdad20f4ec5125fceec86f951..aa09b2e8fac82ab954f581df3d41153c6244c2e8 100644
--- a/net/minecraft/world/level/block/Block.java
+++ b/net/minecraft/world/level/block/Block.java
@@ -362,8 +362,8 @@ public class Block extends BlockBehaviour implements ItemLike {
ItemEntity itemEntity = itemEntitySupplier.get();
itemEntity.setDefaultPickUpDelay();
// CraftBukkit start
- if (level.captureDrops != null) {
- level.captureDrops.add(itemEntity);
+ if (level.getCurrentWorldData().captureDrops != null) { // Folia - region threading
+ level.getCurrentWorldData().captureDrops.add(itemEntity); // Folia - region threading
} else {
level.addFreshEntity(itemEntity);
}
diff --git a/net/minecraft/world/level/block/BushBlock.java b/net/minecraft/world/level/block/BushBlock.java
index bc52568bfa56635300266424488e524d77d95e09..068e65fb7efd52b36ba7f49829da80d82753e78e 100644
--- a/net/minecraft/world/level/block/BushBlock.java
+++ b/net/minecraft/world/level/block/BushBlock.java
@@ -38,7 +38,7 @@ public abstract class BushBlock extends Block {
// CraftBukkit start
if (!state.canSurvive(level, pos)) {
// Suppress during worldgen
- if (!(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(serverLevel, pos).isCancelled()) { // Paper
+ if (!(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.getCurrentWorldData().hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(serverLevel, pos).isCancelled()) { // Paper // Folia - region threading
return Blocks.AIR.defaultBlockState();
}
}
diff --git a/net/minecraft/world/level/block/DaylightDetectorBlock.java b/net/minecraft/world/level/block/DaylightDetectorBlock.java
index a83d1dd4cac85d34f695333fd917a41f14dd5715..17532ef2cc5e21e68a1d51146641ae124a67f79e 100644
--- a/net/minecraft/world/level/block/DaylightDetectorBlock.java
+++ b/net/minecraft/world/level/block/DaylightDetectorBlock.java
@@ -110,7 +110,7 @@ public class DaylightDetectorBlock extends BaseEntityBlock {
}
private static void tickEntity(Level level, BlockPos pos, BlockState state, DaylightDetectorBlockEntity blockEntity) {
- if (level.getGameTime() % 20L == 0L) {
+ if (level.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading
updateSignalStrength(state, level, pos);
}
}
diff --git a/net/minecraft/world/level/block/DispenserBlock.java b/net/minecraft/world/level/block/DispenserBlock.java
index e0a4d41e5bcf144ea4c10d6f633c3a95ed2c5aec..0ff736a45776bbf16f32ac05f099bb656aa3b9a6 100644
--- a/net/minecraft/world/level/block/DispenserBlock.java
+++ b/net/minecraft/world/level/block/DispenserBlock.java
@@ -50,7 +50,7 @@ public class DispenserBlock extends BaseEntityBlock {
private static final DefaultDispenseItemBehavior DEFAULT_BEHAVIOR = new DefaultDispenseItemBehavior();
public static final Map<Item, DispenseItemBehavior> DISPENSER_REGISTRY = new IdentityHashMap<>();
private static final int TRIGGER_DURATION = 4;
- public static boolean eventFired = false; // CraftBukkit
+ public static ThreadLocal<Boolean> eventFired = ThreadLocal.withInitial(() -> Boolean.FALSE); // CraftBukkit // Folia - region threading
@Override
public MapCodec<? extends DispenserBlock> codec() {
@@ -96,7 +96,7 @@ public class DispenserBlock extends BaseEntityBlock {
DispenseItemBehavior dispenseMethod = this.getDispenseMethod(level, item);
if (dispenseMethod != DispenseItemBehavior.NOOP) {
if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(level, pos, item, randomSlot)) return; // Paper - Add BlockPreDispenseEvent
- DispenserBlock.eventFired = false; // CraftBukkit - reset event status
+ DispenserBlock.eventFired.set(Boolean.FALSE); // CraftBukkit - reset event status // Folia - region threading
dispenserBlockEntity.setItem(randomSlot, dispenseMethod.dispense(blockSource, item));
}
}
diff --git a/net/minecraft/world/level/block/DoublePlantBlock.java b/net/minecraft/world/level/block/DoublePlantBlock.java
index 7d033444ab5f89fae3c571a67ede6e7eff378945..e46c4071a955d880d61235d0861d8752ab3b860e 100644
--- a/net/minecraft/world/level/block/DoublePlantBlock.java
+++ b/net/minecraft/world/level/block/DoublePlantBlock.java
@@ -118,7 +118,7 @@ public class DoublePlantBlock extends BushBlock {
protected static void preventDropFromBottomPart(Level level, BlockPos pos, BlockState state, Player player) {
// CraftBukkit start
- if (((net.minecraft.server.level.ServerLevel)level).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(level, pos).isCancelled()) { // Paper
+ if (((net.minecraft.server.level.ServerLevel)level).getCurrentWorldData().hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(level, pos).isCancelled()) { // Paper // Folia - region threading
return;
}
// CraftBukkit end
diff --git a/net/minecraft/world/level/block/EndGatewayBlock.java b/net/minecraft/world/level/block/EndGatewayBlock.java
index 84a1bd5e40e635962d795506861447851e443eee..a7b8e2b702fbe512c9633075515da6a430e76861 100644
--- a/net/minecraft/world/level/block/EndGatewayBlock.java
+++ b/net/minecraft/world/level/block/EndGatewayBlock.java
@@ -111,17 +111,43 @@ public class EndGatewayBlock extends BaseEntityBlock implements Portal {
if (portalPosition == null) {
return null;
} else {
- return entity instanceof ThrownEnderpearl
- ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit
- : new TeleportTransition(
- level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit
- );
+ return getTeleportTransition(level, entity, portalPosition); // Folia - region threading
}
} else {
return null;
}
}
+ // Folia start - region threading
+ public static TeleportTransition getTeleportTransition(ServerLevel level, Entity entity, Vec3 portalPosition) {
+ return entity instanceof ThrownEnderpearl
+ ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit
+ : new TeleportTransition(
+ level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit
+ );
+ }
+
+ @Override
+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) {
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) {
+ return false;
+ }
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) {
+ return false;
+ }
+
+ BlockEntity tile = sourceWorld.getBlockEntity(portalPos);
+
+ if (!(tile instanceof TheEndGatewayBlockEntity endGateway)) {
+ return false;
+ }
+
+ return TheEndGatewayBlockEntity.teleportRegionThreading(
+ sourceWorld, portalPos, portalTarget, endGateway, TeleportTransition.PLACE_PORTAL_TICKET
+ );
+ }
+ // Folia end - region threading
+
@Override
protected RenderShape getRenderShape(BlockState state) {
return RenderShape.INVISIBLE;
diff --git a/net/minecraft/world/level/block/EndPortalBlock.java b/net/minecraft/world/level/block/EndPortalBlock.java
2025-03-09 12:50:14 -07:00
index c11366dd69e1c51bdab45c625b07c15ce2e42cb6..554d75ac1374d7d93977a10e06fcf51259830c97 100644
--- a/net/minecraft/world/level/block/EndPortalBlock.java
+++ b/net/minecraft/world/level/block/EndPortalBlock.java
@@ -63,7 +63,7 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal {
level.getCraftServer().getPluginManager().callEvent(event);
if (event.isCancelled()) return; // Paper - make cancellable
// CraftBukkit end
- if (!level.isClientSide && level.dimension() == Level.END && entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) {
+ if (false && !level.isClientSide && level.dimension() == Level.END && entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) { // Folia - region threading - do not show credits
if (level.paperConfig().misc.disableEndCredits) {serverPlayer.seenCredits = true; return;} // Paper - Option to disable end credits
serverPlayer.showEndCredits();
} else {
2025-03-09 12:50:14 -07:00
@@ -115,6 +115,20 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal {
}
}
+ // Folia start - region threading
+ @Override
+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) {
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) {
+ return false;
+ }
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) {
+ return false;
+ }
+
+ return portalTarget.endPortalLogicAsync(portalPos);
+ }
+ // Folia end - region threading
+
@Override
public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) {
double d = pos.getX() + random.nextDouble();
diff --git a/net/minecraft/world/level/block/FarmBlock.java b/net/minecraft/world/level/block/FarmBlock.java
2025-02-17 15:51:04 -08:00
index 1fdede769b67cb5d2f9159c779f19e3639bb6ff5..81aa50abf396d7fe22dd95dacabf8b83786249fe 100644
--- a/net/minecraft/world/level/block/FarmBlock.java
+++ b/net/minecraft/world/level/block/FarmBlock.java
@@ -95,8 +95,8 @@ public class FarmBlock extends Block {
@Override
protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
int moistureValue = state.getValue(MOISTURE);
- if (moistureValue > 0 && level.paperConfig().tickRates.wetFarmland != 1 && (level.paperConfig().tickRates.wetFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks
- if (moistureValue == 0 && level.paperConfig().tickRates.dryFarmland != 1 && (level.paperConfig().tickRates.dryFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks
+ if (moistureValue > 0 && level.paperConfig().tickRates.wetFarmland != 1 && (level.paperConfig().tickRates.wetFarmland < 1 || (level.getRedstoneGameTime() + pos.hashCode()) % level.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading
+ if (moistureValue == 0 && level.paperConfig().tickRates.dryFarmland != 1 && (level.paperConfig().tickRates.dryFarmland < 1 || (level.getRedstoneGameTime() + pos.hashCode()) % level.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading
if (!isNearWater(level, pos) && !level.isRainingAt(pos.above())) {
if (moistureValue > 0) {
org.bukkit.craftbukkit.event.CraftEventFactory.handleMoistureChangeEvent(level, pos, state.setValue(FarmBlock.MOISTURE, moistureValue - 1), 2); // CraftBukkit
diff --git a/net/minecraft/world/level/block/FungusBlock.java b/net/minecraft/world/level/block/FungusBlock.java
index 85f0eac75784565c658c5178c544f969db3d6f54..81edac1fa383c6875c7a0439f2a160c11ef77a41 100644
--- a/net/minecraft/world/level/block/FungusBlock.java
+++ b/net/minecraft/world/level/block/FungusBlock.java
@@ -76,9 +76,9 @@ public class FungusBlock extends BushBlock implements BonemealableBlock {
// CraftBukkit start
.map((value) -> {
if (this == Blocks.WARPED_FUNGUS) {
- SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS;
+ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.WARPED_FUNGUS); // Folia - region threading
} else if (this == Blocks.CRIMSON_FUNGUS) {
- SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS;
+ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.CRIMSON_FUNGUS); // Folia - region threading
}
return value;
})
diff --git a/net/minecraft/world/level/block/HoneyBlock.java b/net/minecraft/world/level/block/HoneyBlock.java
index bab3ac2c4be08ea7589752b8472c1e13bcaab76a..776216db8097ceadc81d2f8401ea71447769b396 100644
--- a/net/minecraft/world/level/block/HoneyBlock.java
+++ b/net/minecraft/world/level/block/HoneyBlock.java
@@ -94,7 +94,7 @@ public class HoneyBlock extends HalfTransparentBlock {
}
private void maybeDoSlideAchievement(Entity entity, BlockPos pos) {
- if (entity instanceof ServerPlayer && entity.level().getGameTime() % 20L == 0L) {
+ if (entity instanceof ServerPlayer && entity.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading
CriteriaTriggers.HONEY_BLOCK_SLIDE.trigger((ServerPlayer)entity, entity.level().getBlockState(pos));
}
}
diff --git a/net/minecraft/world/level/block/LightningRodBlock.java b/net/minecraft/world/level/block/LightningRodBlock.java
index 534de49aec290766d6bc2523bb3975df775b5881..d79b3d328915096d723c0e3e6b6eb75cfe5bac51 100644
--- a/net/minecraft/world/level/block/LightningRodBlock.java
+++ b/net/minecraft/world/level/block/LightningRodBlock.java
@@ -116,7 +116,7 @@ public class LightningRodBlock extends RodBlock implements SimpleWaterloggedBloc
@Override
public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) {
if (level.isThundering()
- && level.random.nextInt(200) <= level.getGameTime() % 200L
+ && level.random.nextInt(200) <= level.getRedstoneGameTime() % 200L // Folia - region threading
&& pos.getY() == level.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) {
ParticleUtils.spawnParticlesAlongAxis(state.getValue(FACING).getAxis(), level, pos, 0.125, ParticleTypes.ELECTRIC_SPARK, UniformInt.of(1, 2));
}
diff --git a/net/minecraft/world/level/block/MushroomBlock.java b/net/minecraft/world/level/block/MushroomBlock.java
index 904369f4d7db41026183f2de7c96c2f0f4dc204d..223b1789ba94f763e29fb5e74aade787681e9f5b 100644
--- a/net/minecraft/world/level/block/MushroomBlock.java
+++ b/net/minecraft/world/level/block/MushroomBlock.java
@@ -94,7 +94,7 @@ public class MushroomBlock extends BushBlock implements BonemealableBlock {
return false;
} else {
level.removeBlock(pos, false);
- SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM; // CraftBukkit
+ SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM); // CraftBukkit // Folia - region threading
if (optional.get().value().place(level, level.getChunkSource().getGenerator(), random, pos)) {
return true;
} else {
diff --git a/net/minecraft/world/level/block/NetherPortalBlock.java b/net/minecraft/world/level/block/NetherPortalBlock.java
index e2eb693b0130513115392cb0cb5a829ede5be8c5..68e1b1737c8b7af39f22dd4d28b879b5c3d52f65 100644
--- a/net/minecraft/world/level/block/NetherPortalBlock.java
+++ b/net/minecraft/world/level/block/NetherPortalBlock.java
@@ -181,6 +181,33 @@ public class NetherPortalBlock extends Block implements Portal {
}
}
+ // Folia start - region threading
+ @Override
+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) {
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) {
+ return false;
+ }
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) {
+ return false;
+ }
+
+ return portalTarget.netherPortalLogicAsync(portalPos);
+ }
+
+ public static BlockUtil.FoundRectangle findPortalAround(ServerLevel world, BlockPos rough, WorldBorder worldBorder, int searchRadius) {
+ BlockPos found = world.getPortalForcer().findClosestPortalPosition(rough, worldBorder, searchRadius).orElse(null);
+ if (found == null) {
+ return null;
+ }
+
+ BlockState portalState = world.getBlockStateFromEmptyChunk(found);
+
+ return BlockUtil.getLargestRectangleAround(found, portalState.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (pos) -> {
+ return world.getBlockStateFromEmptyChunk(pos) == portalState;
+ });
+ }
+ // Folia end - region threading
+
@Nullable
private TeleportTransition getExitPortal(ServerLevel level, Entity entity, BlockPos pos, BlockPos exitPos, boolean isNether, WorldBorder worldBorder, int searchRadius, boolean canCreatePortal, int createRadius) { // CraftBukkit
Optional<BlockPos> optional = level.getPortalForcer().findClosestPortalPosition(exitPos, worldBorder, searchRadius); // CraftBukkit
@@ -188,14 +215,14 @@ public class NetherPortalBlock extends Block implements Portal {
TeleportTransition.PostTeleportTransition postTeleportTransition;
if (optional.isPresent()) {
BlockPos blockPos = optional.get();
- BlockState blockState = level.getBlockState(blockPos);
+ BlockState blockState = level.getBlockStateFromEmptyChunk(blockPos); // Folia - region threading
largestRectangleAround = BlockUtil.getLargestRectangleAround(
blockPos,
blockState.getValue(BlockStateProperties.HORIZONTAL_AXIS),
21,
Direction.Axis.Y,
21,
- blockPos1 -> level.getBlockState(blockPos1) == blockState
+ blockPos1 -> level.getBlockStateFromEmptyChunk(blockPos1) == blockState // Folia - region threading
);
postTeleportTransition = TeleportTransition.PLAY_PORTAL_SOUND.then(entity1 -> entity1.placePortalTicket(blockPos));
} else if (canCreatePortal) { // CraftBukkit
@@ -238,7 +265,7 @@ public class NetherPortalBlock extends Block implements Portal {
return createDimensionTransition(level, rectangle, axis, relativePortalPosition, entity, postTeleportTransition);
}
- private static TeleportTransition createDimensionTransition(
+ public static TeleportTransition createDimensionTransition( // Folia - region threading - public
ServerLevel level,
BlockUtil.FoundRectangle rectangle,
Direction.Axis axis,
diff --git a/net/minecraft/world/level/block/Portal.java b/net/minecraft/world/level/block/Portal.java
index c941b0e05d98fa59669757174887955e6319eddb..3883a437d99e5d8b13c55764613d630e29e75bc4 100644
--- a/net/minecraft/world/level/block/Portal.java
+++ b/net/minecraft/world/level/block/Portal.java
@@ -14,6 +14,10 @@ public interface Portal {
@Nullable
TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos);
+ // Folia start - region threading
+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos);
+ // Folia end - region threading
+
default Portal.Transition getLocalTransition() {
return Portal.Transition.NONE;
}
diff --git a/net/minecraft/world/level/block/RedStoneWireBlock.java b/net/minecraft/world/level/block/RedStoneWireBlock.java
index 12c9d60314c99fb65e640d255a2d0c6b7790ad4d..6ba86c5e55d09fd99e81e40db4614ef14246bdc3 100644
--- a/net/minecraft/world/level/block/RedStoneWireBlock.java
+++ b/net/minecraft/world/level/block/RedStoneWireBlock.java
@@ -91,7 +91,7 @@ public class RedStoneWireBlock extends Block {
private static final float PARTICLE_DENSITY = 0.2F;
private final BlockState crossState;
private final RedstoneWireEvaluator evaluator = new DefaultRedstoneWireEvaluator(this);
- public boolean shouldSignal = true;
+ //public boolean shouldSignal = true; // Folia - region threading - move to regionised world data
@Override
public MapCodec<RedStoneWireBlock> codec() {
@@ -293,6 +293,11 @@ public class RedStoneWireBlock extends Block {
// Paper start - Optimize redstone (Eigencraft)
// The bulk of the new functionality is found in RedstoneWireTurbo.java
io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this);
+ // Folia start - region threading
+ private io.papermc.paper.redstone.RedstoneWireTurbo getTurbo(Level world) {
+ return world.getCurrentWorldData().turbo;
+ }
+ // Folia end - region threading
/*
* Modified version of pre-existing updateSurroundingRedstone, which is called from
@@ -308,7 +313,7 @@ public class RedStoneWireBlock extends Block {
if (orientation != null) {
source = pos.relative(orientation.getFront().getOpposite());
}
- turbo.updateSurroundingRedstone(worldIn, pos, state, source);
+ getTurbo(worldIn).updateSurroundingRedstone(worldIn, pos, state, source); // Folia - region threading
return;
}
updatePowerStrength(worldIn, pos, state, orientation, blockAdded);
@@ -336,7 +341,7 @@ public class RedStoneWireBlock extends Block {
// [Space Walker] suppress shape updates and emit those manually to
// bypass the new neighbor update stack.
if (level.setBlock(pos, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) {
- turbo.updateNeighborShapes(level, pos, state);
+ this.getTurbo(level).updateNeighborShapes(level, pos, state); // Folia - region threading
}
}
}
@@ -353,9 +358,9 @@ public class RedStoneWireBlock extends Block {
}
public int getBlockSignal(Level level, BlockPos pos) {
- this.shouldSignal = false;
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading
int bestNeighborSignal = level.getBestNeighborSignal(pos);
- this.shouldSignal = true;
+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading
return bestNeighborSignal;
}
@@ -450,12 +455,12 @@ public class RedStoneWireBlock extends Block {
@Override
protected int getDirectSignal(BlockState blockState, BlockGetter blockAccess, BlockPos pos, Direction side) {
- return !this.shouldSignal ? 0 : blockState.getSignal(blockAccess, pos, side);
+ return !io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal ? 0 : blockState.getSignal(blockAccess, pos, side); // Folia - region threading
}
@Override
protected int getSignal(BlockState blockState, BlockGetter blockAccess, BlockPos pos, Direction side) {
- if (this.shouldSignal && side != Direction.DOWN) {
+ if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal && side != Direction.DOWN) { // Folia - region threading
int powerValue = blockState.getValue(POWER);
if (powerValue == 0) {
return 0;
@@ -487,7 +492,10 @@ public class RedStoneWireBlock extends Block {
@Override
protected boolean isSignalSource(BlockState state) {
- return this.shouldSignal;
+ // Folia start - region threading
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData();
+ return worldData == null || worldData.shouldSignal;
+ // Folia end - region threading
}
public static int getColorForPower(int power) {
diff --git a/net/minecraft/world/level/block/RedstoneTorchBlock.java b/net/minecraft/world/level/block/RedstoneTorchBlock.java
index 18420ec1f5776b018010f26e59aba00ae5bd0723..d5ac5d8fddeaff0def61a909faf2c909337ada57 100644
--- a/net/minecraft/world/level/block/RedstoneTorchBlock.java
+++ b/net/minecraft/world/level/block/RedstoneTorchBlock.java
@@ -73,10 +73,10 @@ public class RedstoneTorchBlock extends BaseTorchBlock {
protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
boolean hasNeighborSignal = this.hasNeighborSignal(level, pos, state);
// Paper start - Faster redstone torch rapid clock removal
- java.util.ArrayDeque<RedstoneTorchBlock.Toggle> redstoneUpdateInfos = level.redstoneUpdateInfos;
+ java.util.ArrayDeque<RedstoneTorchBlock.Toggle> 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<RedstoneTorchBlock.Toggle> list = level.redstoneUpdateInfos;
+ java.util.ArrayDeque<RedstoneTorchBlock.Toggle> list = level.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading
if (list == null) {
- list = level.redstoneUpdateInfos = new java.util.ArrayDeque<>();
+ list = level.getCurrentWorldData().redstoneUpdateInfos = new java.util.ArrayDeque<>(); // Folia - region threading
}
// Paper end - Faster redstone torch rapid clock removal
if (logToggle) {
- list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), level.getGameTime()));
+ list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), level.getRedstoneGameTime())); // Folia - region threading
}
int i = 0;
@@ -182,12 +182,18 @@ public class RedstoneTorchBlock extends BaseTorchBlock {
}
public static class Toggle {
- final BlockPos pos;
- final long when;
+ public final BlockPos pos; // Folia - region threading
+ long when; // Folia - region threading
public Toggle(BlockPos pos, long when) {
this.pos = pos;
this.when = when;
}
+
+ // Folia start - region ticking
+ public void offsetTime(long offset) {
+ this.when += offset;
+ }
+ // Folia end - region ticking
}
}
diff --git a/net/minecraft/world/level/block/SaplingBlock.java b/net/minecraft/world/level/block/SaplingBlock.java
index e014f052e9b0f5ca6b28044e2389782b7d0e0cb8..cc9e253d3033d3e970891067329aa281e85464f7 100644
--- a/net/minecraft/world/level/block/SaplingBlock.java
+++ b/net/minecraft/world/level/block/SaplingBlock.java
@@ -26,7 +26,7 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock {
protected static final float AABB_OFFSET = 6.0F;
protected static final VoxelShape SHAPE = Block.box(2.0, 0.0, 2.0, 14.0, 12.0, 14.0);
protected final TreeGrower treeGrower;
- public static org.bukkit.TreeType treeType; // CraftBukkit
+ public static final ThreadLocal<org.bukkit.TreeType> treeTypeRT = new ThreadLocal<>(); // CraftBukkit // Folia - region threading
@Override
public MapCodec<? extends SaplingBlock> codec() {
@@ -56,18 +56,19 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock {
level.setBlock(pos, state.cycle(STAGE), 4);
} else {
// CraftBukkit start
- if (level.captureTreeGeneration) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
+ if (worldData.captureTreeGeneration) { // Folia - region threading
this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random);
} else {
- level.captureTreeGeneration = true;
+ worldData.captureTreeGeneration = true; // Folia - region threading
this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random);
- level.captureTreeGeneration = false;
- if (!level.capturedBlockStates.isEmpty()) {
- org.bukkit.TreeType treeType = SaplingBlock.treeType;
- SaplingBlock.treeType = null;
+ worldData.captureTreeGeneration = false; // Folia - region threading
+ if (!worldData.capturedBlockStates.isEmpty()) { // Folia - region threading
+ org.bukkit.TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading
+ SaplingBlock.treeTypeRT.set(null); // Folia - region threading
org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level.getWorld());
- java.util.List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(level.capturedBlockStates.values());
- level.capturedBlockStates.clear();
+ java.util.List<org.bukkit.block.BlockState> blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading
+ worldData.capturedBlockStates.clear(); // Folia - region threading
org.bukkit.event.world.StructureGrowEvent event = null;
if (treeType != null) {
event = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks);
diff --git a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
index 722f2b9a24679e0fc67aae2cd27051f96f962efe..fb8c09b18ea4112cbbe6e93bf6b9804d79628d36 100644
--- a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
+++ b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java
@@ -50,7 +50,7 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock {
@Override
protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
- if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks
+ if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + pos.hashCode()) % level.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - regionised ticking
// Paper start - Perf: optimize dirt and snow spreading
final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = level.getChunkIfLoaded(pos);
if (cachedBlockChunk == null) { // Is this needed?
diff --git a/net/minecraft/world/level/block/WitherSkullBlock.java b/net/minecraft/world/level/block/WitherSkullBlock.java
index dc70aaa8d929c40c5f34c8facc1ad2bff4e98768..3ea53116725798a1eedb4802d6ebd7a32d8cccfd 100644
--- a/net/minecraft/world/level/block/WitherSkullBlock.java
+++ b/net/minecraft/world/level/block/WitherSkullBlock.java
@@ -51,7 +51,7 @@ public class WitherSkullBlock extends SkullBlock {
}
public static void checkSpawn(Level level, BlockPos pos, SkullBlockEntity blockEntity) {
- if (level.captureBlockStates) return; // CraftBukkit
+ if (level.getCurrentWorldData().captureBlockStates) return; // CraftBukkit // Folia - region threading
if (!level.isClientSide) {
BlockState blockState = blockEntity.getBlockState();
boolean flag = blockState.is(Blocks.WITHER_SKELETON_SKULL) || blockState.is(Blocks.WITHER_SKELETON_WALL_SKULL);
diff --git a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
index deef33d96db188cb297f04b581ab29e77e3716a9..413288e4a654b5ff8cc009b401d602731f63ec6d 100644
--- a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java
@@ -211,7 +211,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name
}
int i = blockEntity.levels; final int originalLevels = i; // Paper - OBFHELPER
- if (level.getGameTime() % 80L == 0L) {
+ if (level.getRedstoneGameTime() % 80L == 0L) { // Folia - region threading
if (!blockEntity.beamSections.isEmpty()) {
blockEntity.levels = updateBase(level, x, y, z);
}
@@ -345,7 +345,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name
list = level.getEntitiesOfClass(Player.class, aabb); // Diff from applyEffect
} else {
list = new java.util.ArrayList<>();
- for (final Player player : level.players()) {
+ for (final Player player : level.getLocalPlayers()) { // Folia - region threading
if (!net.minecraft.world.entity.EntitySelector.NO_SPECTATORS.test(player)) continue;
if (player.getBoundingBox().intersects(aabb)) {
list.add(player);
diff --git a/net/minecraft/world/level/block/entity/BlockEntity.java b/net/minecraft/world/level/block/entity/BlockEntity.java
index 77618757c0e678532dbab814aceed83f7f1cd892..003e9db957023486278679803b313ce89d573587 100644
--- a/net/minecraft/world/level/block/entity/BlockEntity.java
+++ b/net/minecraft/world/level/block/entity/BlockEntity.java
@@ -26,7 +26,7 @@ import net.minecraft.world.level.block.state.BlockState;
import org.slf4j.Logger;
public abstract class BlockEntity {
- static boolean ignoreBlockEntityUpdates; // Paper - Perf: Optimize Hoppers
+ static final ThreadLocal<Boolean> IGNORE_TILE_UPDATES = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading
// CraftBukkit start - data containers
private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry();
public org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer;
@@ -40,6 +40,12 @@ public abstract class BlockEntity {
private BlockState blockState;
private DataComponentMap components = DataComponentMap.EMPTY;
+ // Folia start - region ticking
+ public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) {
+
+ }
+ // Folia end - region ticking
+
public BlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState blockState) {
this.type = type;
this.worldPosition = pos.immutable();
@@ -197,7 +203,7 @@ public abstract class BlockEntity {
public void setChanged() {
if (this.level != null) {
- if (ignoreBlockEntityUpdates) return; // Paper - Perf: Optimize Hoppers
+ if (IGNORE_TILE_UPDATES.get().booleanValue()) return; // Paper - Perf: Optimize Hoppers // Folia - region threading
setChanged(this.level, this.worldPosition, this.blockState);
}
}
diff --git a/net/minecraft/world/level/block/entity/CommandBlockEntity.java b/net/minecraft/world/level/block/entity/CommandBlockEntity.java
index de75569d44855d9d6ec28cfee4403ecb6b45c4d3..c1e3a99a5fe917c728763be16c9a92d7252739a3 100644
--- a/net/minecraft/world/level/block/entity/CommandBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/CommandBlockEntity.java
@@ -66,6 +66,13 @@ public class CommandBlockEntity extends BlockEntity {
);
}
+ // Folia start
+ @Override
+ public void threadCheck() {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel) CommandBlockEntity.this.level, CommandBlockEntity.this.worldPosition, "Asynchronous sendSystemMessage to a command block");
+ }
+ // Folia end
+
@Override
public boolean isValid() {
return !CommandBlockEntity.this.isRemoved();
diff --git a/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
index 9d80625fc95e4968cf80492dc7ecf1fd27e585b8..2938c1d35d5b19cfe49e12607f1e1d9342114f2c 100644
--- a/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java
@@ -81,7 +81,7 @@ public class ConduitBlockEntity extends BlockEntity {
public static void clientTick(Level level, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) {
blockEntity.tickCount++;
- long gameTime = level.getGameTime();
+ long gameTime = level.getRedstoneGameTime(); // Folia - region threading
List<BlockPos> list = blockEntity.effectBlocks;
if (gameTime % 40L == 0L) {
blockEntity.isActive = updateShape(level, pos, list);
@@ -97,7 +97,7 @@ public class ConduitBlockEntity extends BlockEntity {
public static void serverTick(Level level, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) {
blockEntity.tickCount++;
- long gameTime = level.getGameTime();
+ long gameTime = level.getRedstoneGameTime(); // Folia - region threading
List<BlockPos> list = blockEntity.effectBlocks;
if (gameTime % 40L == 0L) {
boolean flag = updateShape(level, pos, list);
diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
index 5cd1326ad5d046c88b2b3449d610a78fa880b4cd..ae988c4910421fb720177178ef6136e595ae6946 100644
--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java
@@ -34,7 +34,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
private static final int[][] CACHED_SLOTS = new int[54][];
private NonNullList<ItemStack> items = NonNullList.withSize(5, ItemStack.EMPTY);
public int cooldownTime = -1;
- private long tickedGameTime;
+ private long tickedGameTime = Long.MIN_VALUE; // Folia - region threading
private Direction facing;
// CraftBukkit start - add fields and methods
@@ -67,6 +67,15 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
}
// CraftBukkit end
+ // Folia start - region threading
+ @Override
+ public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) {
+ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
+ if (this.tickedGameTime != Long.MIN_VALUE) {
+ this.tickedGameTime += fromRedstoneTimeOffset;
+ }
+ }
+ // Folia end - region threading
public HopperBlockEntity(BlockPos pos, BlockState blockState) {
super(BlockEntityType.HOPPER, pos, blockState);
@@ -125,7 +134,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
public static void pushItemsTick(Level level, BlockPos pos, BlockState state, HopperBlockEntity blockEntity) {
blockEntity.cooldownTime--;
- blockEntity.tickedGameTime = level.getGameTime();
+ blockEntity.tickedGameTime = level.getRedstoneGameTime(); // Folia - region threading
if (!blockEntity.isOnCooldown()) {
blockEntity.setCooldown(0);
// Spigot start
@@ -213,12 +222,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
}
// Paper start - Perf: Optimize Hoppers
- public static boolean skipHopperEvents;
- private static boolean skipPullModeEventFire;
- private static boolean skipPushModeEventFire;
+ // Folia - region threading - moved to RegionizedWorldData
private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) {
- skipPushModeEventFire = skipHopperEvents;
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
+ worldData.skipPushModeEventFire = worldData.skipHopperEvents; // Folia - region threading
boolean foundItem = false;
for (int i = 0; i < hopper.getContainerSize(); ++i) {
final ItemStack item = hopper.getItem(i);
@@ -233,7 +241,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
// We only need to fire the event once to give protection plugins a chance to cancel this event
// Because nothing uses getItem, every event call should end up the same result.
- if (!skipPushModeEventFire) {
+ if (!worldData.skipPushModeEventFire) { // Folia - region threading
movedItem = callPushMoveEvent(destination, movedItem, hopper);
if (movedItem == null) { // cancelled
origItemStack.setCount(originalItemCount);
@@ -263,13 +271,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
}
private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
ItemStack movedItem = origItemStack;
final int originalItemCount = origItemStack.getCount();
final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount);
container.setChanged(); // original logic always marks source inv as changed even if no move happens.
movedItem.setCount(movedItemCount);
- if (!skipPullModeEventFire) {
+ if (!worldData.skipPullModeEventFire) { // Folia - region threading
movedItem = callPullMoveEvent(hopper, container, movedItem);
if (movedItem == null) { // cancelled
origItemStack.setCount(originalItemCount);
@@ -289,9 +298,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount);
}
- ignoreBlockEntityUpdates = true;
+ IGNORE_TILE_UPDATES.set(true); // Folia - region threading
container.setItem(i, origItemStack);
- ignoreBlockEntityUpdates = false;
+ IGNORE_TILE_UPDATES.set(false); // Folia - region threading
container.setChanged();
return true;
}
@@ -306,6 +315,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
@Nullable
private static ItemStack callPushMoveEvent(Container destination, ItemStack itemStack, HopperBlockEntity hopper) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading
final org.bukkit.inventory.Inventory destinationInventory = getInventory(destination);
final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(
hopper.getOwner(false).getInventory(),
@@ -315,7 +325,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
);
final boolean result = event.callEvent();
if (!event.calledGetItem && !event.calledSetItem) {
- skipPushModeEventFire = true;
+ worldData.skipPushModeEventFire = true; // Folia - region threading
}
if (!result) {
applyCooldown(hopper);
@@ -331,6 +341,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
@Nullable
private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading
final org.bukkit.inventory.Inventory sourceInventory = getInventory(container);
final org.bukkit.inventory.Inventory destination = getInventory(hopper);
@@ -338,7 +349,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), destination, false);
final boolean result = event.callEvent();
if (!event.calledGetItem && !event.calledSetItem) {
- skipPullModeEventFire = true;
+ worldData.skipPullModeEventFire = true; // Folia - region threading
}
if (!result) {
applyCooldown(hopper);
@@ -524,12 +535,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
}
public static boolean suckInItems(Level level, Hopper hopper) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading
BlockPos blockPos = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ());
BlockState blockState = level.getBlockState(blockPos);
Container sourceContainer = getSourceContainer(level, hopper, blockPos, blockState);
if (sourceContainer != null) {
Direction direction = Direction.DOWN;
- skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers
+ worldData.skipPullModeEventFire = worldData.skipHopperEvents; // Paper - Perf: Optimize Hoppers // Folia - region threading
for (int i : getSlots(sourceContainer, direction)) {
if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction, level)) { // Spigot
@@ -678,9 +690,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen
stack = stack.split(destination.getMaxStackSize());
}
// Spigot end
- ignoreBlockEntityUpdates = true; // Paper - Perf: Optimize Hoppers
+ IGNORE_TILE_UPDATES.set(Boolean.TRUE); // Paper - Perf: Optimize Hoppers // Folia - region threading
destination.setItem(slot, stack);
- ignoreBlockEntityUpdates = false; // Paper - Perf: Optimize Hoppers
+ IGNORE_TILE_UPDATES.set(Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading
stack = leftover; // Paper - Make hoppers respect inventory max stack size
flag = true;
} else if (canMergeItems(item, stack)) {
diff --git a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
index 1638eccef431fb68775af624110f1968f0c6dabd..bd6693af6412fb08a28ca9a71d5c70d54f72c6e6 100644
--- a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java
@@ -43,9 +43,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi
// Paper end - Fix NPE in SculkBloomEvent world access
public static void serverTick(Level level, BlockPos pos, BlockState state, SculkCatalystBlockEntity sculkCatalyst) {
- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = sculkCatalyst.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep.
+ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(sculkCatalyst.getBlockPos()); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading
sculkCatalyst.catalystListener.getSculkSpreader().updateCursors(level, pos, level.getRandom(), true);
- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit
+ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(null); // CraftBukkit // Folia - region threading
}
@Override
diff --git a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
index 5bf39c542757bf97da8909b65c22786a8a30385a..61887e6b052bca715c90dff5d9cd657e0b3f6a78 100644
--- a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java
@@ -35,9 +35,12 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity {
public long age;
private int teleportCooldown;
@Nullable
- public BlockPos exitPortal;
+ public volatile BlockPos exitPortal; // Folia - region threading - volatile
public boolean exactTeleport;
+ private static final java.util.concurrent.atomic.AtomicLong SEARCHING_FOR_EXIT_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); // Folia - region threading
+ private Long searchingForExitId; // Folia - region threading
+
public TheEndGatewayBlockEntity(BlockPos pos, BlockState blockState) {
super(BlockEntityType.END_GATEWAY, pos, blockState);
}
@@ -129,6 +132,104 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity {
}
}
+ // Folia start - region threading
+ private void trySearchForExit(ServerLevel world, BlockPos fromPos) {
+ if (this.searchingForExitId != null) {
+ return;
+ }
+ this.searchingForExitId = Long.valueOf(SEARCHING_FOR_EXIT_ID_GENERATOR.getAndIncrement());
+ int chunkX = fromPos.getX() >> 4;
+ int chunkZ = fromPos.getZ() >> 4;
+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel(
+ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH,
+ chunkX, chunkZ,
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL,
+ this.searchingForExitId
+ );
+
+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable<BlockPos> 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<BlockPos> complete) {
+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable<Vec3> 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<net.minecraft.world.level.chunk.ChunkAccess> chunks) -> {
+ // make sure chunks are kept loaded
+ for (net.minecraft.world.level.chunk.ChunkAccess access : chunks) {
+ world.chunkSource.addTicketAtLevel(
+ net.minecraft.server.level.TicketType.DELAYED, access.getPos(),
+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL,
+ net.minecraft.util.Unit.INSTANCE
+ );
+ }
+ // now after the chunks are loaded, we can delay by one tick
+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue(
+ world, posX >> 4, posZ >> 4, () -> {
+ // find final location
+ BlockPos tpLoc = TheEndGatewayBlockEntity.findTallestBlock(world, finalBlockPosition1, radius, true).above(GATEWAY_HEIGHT_ABOVE_SURFACE);
+
+ // done
+ complete.complete(tpLoc);
+ }
+ );
+ }
+ );
+ });
+
+ // fire off chain
+ findExitPortalXZPosTentativeRegionThreading(world, pos, tentativeSelection);
+ }
+
+ private static void findExitPortalXZPosTentativeRegionThreading(ServerLevel world, BlockPos pos,
+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable<Vec3> complete) {
+ Vec3 posDirFromOrigin = new Vec3(pos.getX(), 0.0D, pos.getZ()).normalize();
+ Vec3 posDirExtruded = posDirFromOrigin.scale(1024.0D);
+
+ class Vars {
+ int i = 16;
+ boolean mode = false;
+ Vec3 currPos = posDirExtruded;
+ }
+ Vars vars = new Vars();
+
+ Runnable handle = new Runnable() {
+ @Override
+ public void run() {
+ if (vars.mode != TheEndGatewayBlockEntity.isChunkEmpty(world, vars.currPos)) {
+ vars.i = 0; // fall back to completing
+ }
+
+ // try to load next chunk
+ if (vars.i-- <= 0) {
+ if (vars.mode) {
+ complete.complete(vars.currPos);
+ return;
+ }
+ vars.mode = true;
+ vars.i = 16;
+ }
+
+ vars.currPos = vars.currPos.add(posDirFromOrigin.scale(vars.mode ? 16.0 : -16.0));
+ // schedule next iteration
+ world.moonrise$getChunkTaskScheduler().scheduleChunkLoad(
+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(vars.currPos),
+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(vars.currPos),
+ net.minecraft.world.level.chunk.status.ChunkStatus.FULL,
+ true,
+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
+ (chunk) -> {
+ this.run();
+ }
+ );
+ }
+ };
+
+ // kick off first chunk load
+ world.moonrise$getChunkTaskScheduler().scheduleChunkLoad(
+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(posDirExtruded),
+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(posDirExtruded),
+ net.minecraft.world.level.chunk.status.ChunkStatus.FULL,
+ true,
+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL,
+ (chunk) -> {
+ handle.run();
+ }
+ );
+ }
+ // Folia end - region threading
+
private static Vec3 findExitPortalXZPosTentative(ServerLevel level, BlockPos pos) {
Vec3 vec3 = new Vec3(pos.getX(), 0.0, pos.getZ()).normalize();
int i = 1024;
diff --git a/net/minecraft/world/level/block/entity/TickingBlockEntity.java b/net/minecraft/world/level/block/entity/TickingBlockEntity.java
index 28e3b73507b988f7234cbf29c4024c88180d0aef..c8facee29ee08e0975528083f89b64f0b593957f 100644
--- a/net/minecraft/world/level/block/entity/TickingBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/TickingBlockEntity.java
@@ -10,4 +10,6 @@ public interface TickingBlockEntity {
BlockPos getPos();
String getType();
+
+ BlockEntity getTileEntity(); // Folia - region threading
}
diff --git a/net/minecraft/world/level/block/grower/TreeGrower.java b/net/minecraft/world/level/block/grower/TreeGrower.java
index cf7311c507de09a8f89934e430b2201e8bdffe51..80de710b4e1528587b509e50bdd69983bcb608d0 100644
--- a/net/minecraft/world/level/block/grower/TreeGrower.java
+++ b/net/minecraft/world/level/block/grower/TreeGrower.java
@@ -203,56 +203,58 @@ public final class TreeGrower {
// CraftBukkit start
private void setTreeType(Holder<ConfiguredFeature<?, ?>> holder) {
+ org.bukkit.TreeType treeType; // Folia - region threading
ResourceKey<ConfiguredFeature<?, ?>> treeFeature = holder.unwrapKey().get();
if (treeFeature == TreeFeatures.OAK || treeFeature == TreeFeatures.OAK_BEES_005) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TREE;
+ treeType = org.bukkit.TreeType.TREE; // Folia - region threading
} else if (treeFeature == TreeFeatures.HUGE_RED_MUSHROOM) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_MUSHROOM;
+ treeType = org.bukkit.TreeType.RED_MUSHROOM; // Folia - region threading
} else if (treeFeature == TreeFeatures.HUGE_BROWN_MUSHROOM) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BROWN_MUSHROOM;
+ treeType = org.bukkit.TreeType.BROWN_MUSHROOM; // Folia - region threading
} else if (treeFeature == TreeFeatures.JUNGLE_TREE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.COCOA_TREE;
+ treeType = org.bukkit.TreeType.COCOA_TREE; // Folia - region threading
} else if (treeFeature == TreeFeatures.JUNGLE_TREE_NO_VINE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SMALL_JUNGLE;
+ treeType = org.bukkit.TreeType.SMALL_JUNGLE; // Folia - region threading
} else if (treeFeature == TreeFeatures.PINE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_REDWOOD;
+ treeType = org.bukkit.TreeType.TALL_REDWOOD; // Folia - region threading
} else if (treeFeature == TreeFeatures.SPRUCE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD;
+ treeType = org.bukkit.TreeType.REDWOOD; // Folia - region threading
} else if (treeFeature == TreeFeatures.ACACIA) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA;
+ treeType = org.bukkit.TreeType.ACACIA; // Folia - region threading
} else if (treeFeature == TreeFeatures.BIRCH || treeFeature == TreeFeatures.BIRCH_BEES_005) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIRCH;
+ treeType = org.bukkit.TreeType.BIRCH; // Folia - region threading
} else if (treeFeature == TreeFeatures.SUPER_BIRCH_BEES_0002) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_BIRCH;
+ treeType = org.bukkit.TreeType.TALL_BIRCH; // Folia - region threading
} else if (treeFeature == TreeFeatures.SWAMP_OAK) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP;
+ treeType = org.bukkit.TreeType.SWAMP; // Folia - region threading
} else if (treeFeature == TreeFeatures.FANCY_OAK || treeFeature == TreeFeatures.FANCY_OAK_BEES_005) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIG_TREE;
+ treeType = org.bukkit.TreeType.BIG_TREE; // Folia - region threading
} else if (treeFeature == TreeFeatures.JUNGLE_BUSH) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE_BUSH;
+ treeType = org.bukkit.TreeType.JUNGLE_BUSH; // Folia - region threading
} else if (treeFeature == TreeFeatures.DARK_OAK) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.DARK_OAK;
+ treeType = org.bukkit.TreeType.DARK_OAK; // Folia - region threading
} else if (treeFeature == TreeFeatures.MEGA_SPRUCE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_REDWOOD;
+ treeType = org.bukkit.TreeType.MEGA_REDWOOD; // Folia - region threading
} else if (treeFeature == TreeFeatures.MEGA_PINE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_PINE;
+ treeType = org.bukkit.TreeType.MEGA_PINE; // Folia - region threading
} else if (treeFeature == TreeFeatures.MEGA_JUNGLE_TREE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE;
+ treeType = org.bukkit.TreeType.JUNGLE; // Folia - region threading
} else if (treeFeature == TreeFeatures.AZALEA_TREE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA;
+ treeType = org.bukkit.TreeType.AZALEA; // Folia - region threading
} else if (treeFeature == TreeFeatures.MANGROVE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE;
+ treeType = org.bukkit.TreeType.MANGROVE; // Folia - region threading
} else if (treeFeature == TreeFeatures.TALL_MANGROVE) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_MANGROVE;
+ treeType = org.bukkit.TreeType.TALL_MANGROVE; // Folia - region threading
} else if (treeFeature == TreeFeatures.CHERRY || treeFeature == TreeFeatures.CHERRY_BEES_005) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.CHERRY;
+ treeType = org.bukkit.TreeType.CHERRY; // Folia - region threading
} else if (treeFeature == TreeFeatures.PALE_OAK || treeFeature == TreeFeatures.PALE_OAK_BONEMEAL) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK;
+ treeType = org.bukkit.TreeType.PALE_OAK; // Folia - region threading
} else if (treeFeature == TreeFeatures.PALE_OAK_CREAKING) {
- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING;
+ treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; // Folia - region threading
} else {
throw new IllegalArgumentException("Unknown tree generator " + treeFeature);
}
+ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(treeType); // Folia - region threading
}
// CraftBukkit end
}
diff --git a/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/net/minecraft/world/level/block/piston/PistonBaseBlock.java
index 41802482875bd4d4b505eb758740140de0db415a..aa7aefbc26db062e3ed731ca98229fa36a54b4ef 100644
--- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java
+++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java
@@ -139,7 +139,7 @@ public class PistonBaseBlock extends DirectionalBlock {
&& pistonMovingBlockEntity.isExtending()
&& (
pistonMovingBlockEntity.getProgress(0.0F) < 0.5F
- || level.getGameTime() == pistonMovingBlockEntity.getLastTicked()
+ || level.getRedstoneGameTime() == pistonMovingBlockEntity.getLastTicked() // Folia - region threading
|| ((ServerLevel)level).isHandlingTick()
)) {
i = 2;
diff --git a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
index ee2f8e8deb35059824b5730a1442f383dc79f01c..baf6322619bbe43ed136e01494fbf24e2f8e4604 100644
--- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
+++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java
@@ -41,9 +41,19 @@ public class PistonMovingBlockEntity extends BlockEntity {
private static final ThreadLocal<Direction> NOCLIP = ThreadLocal.withInitial(() -> null);
private float progress;
private float progressO;
- private long lastTicked;
+ private long lastTicked = Long.MIN_VALUE; // Folia - region threading
private int deathTicks;
+ // Folia start - region threading
+ @Override
+ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) {
+ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
+ if (this.lastTicked != Long.MIN_VALUE) {
+ this.lastTicked += fromRedstoneTimeOffset;
+ }
+ }
+ // Folia end - region threading
+
public PistonMovingBlockEntity(BlockPos pos, BlockState blockState) {
super(BlockEntityType.PISTON, pos, blockState);
}
@@ -150,8 +160,8 @@ public class PistonMovingBlockEntity extends BlockEntity {
entity.setDeltaMovement(d1, d2, d3);
// Paper - EAR items stuck in slime pushed by a piston
- entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10);
- entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10);
+ entity.activatedTick = Math.max(entity.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading
+ entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading
// Paper end
break;
}
@@ -292,7 +302,7 @@ public class PistonMovingBlockEntity extends BlockEntity {
}
public static void tick(Level level, BlockPos pos, BlockState state, PistonMovingBlockEntity blockEntity) {
- blockEntity.lastTicked = level.getGameTime();
+ blockEntity.lastTicked = level.getRedstoneGameTime(); // Folia - region threading
blockEntity.progressO = blockEntity.progress;
if (blockEntity.progressO >= 1.0F) {
if (level.isClientSide && blockEntity.deathTicks < 5) {
diff --git a/net/minecraft/world/level/border/WorldBorder.java b/net/minecraft/world/level/border/WorldBorder.java
index 7249292e77b4a54f1f4f707c4dc55924c96dd23f..eb0d3cc606fb7bb06871ea61c240873ed7e67bc5 100644
--- a/net/minecraft/world/level/border/WorldBorder.java
+++ b/net/minecraft/world/level/border/WorldBorder.java
@@ -30,6 +30,8 @@ public class WorldBorder {
public static final WorldBorder.Settings DEFAULT_SETTINGS = new WorldBorder.Settings(0.0, 0.0, 0.2, 5.0, 5, 15, 5.999997E7F, 0L, 0.0);
public net.minecraft.server.level.ServerLevel world; // CraftBukkit
+ // Folia - region threading - TODO make this shit thread-safe
+
public boolean isWithinBounds(BlockPos pos) {
return this.isWithinBounds(pos.getX(), pos.getZ());
}
@@ -43,16 +45,14 @@ public class WorldBorder {
}
// Paper start - Bound treasure maps to world border
- private final BlockPos.MutableBlockPos mutPos = new BlockPos.MutableBlockPos();
+ private static final ThreadLocal<BlockPos.MutableBlockPos> mutPos = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Folia - region threading
public boolean isBlockInBounds(int chunkX, int chunkZ) {
- this.mutPos.set(chunkX, 64, chunkZ);
- return this.isWithinBounds(this.mutPos);
+ return this.isWithinBounds(mutPos.get().set(chunkX, 64, chunkZ)); // Folia - region threading
}
public boolean isChunkInBounds(int chunkX, int chunkZ) {
- this.mutPos.set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15);
- return this.isWithinBounds(this.mutPos);
+ return this.isWithinBounds(mutPos.get().set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15)); // Folia - region threading
}
// Paper end - Bound treasure maps to world border
diff --git a/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java
index 6ed51cf42b5864194d671b5b56f5b9bdf0291dc0..b85c547f281c58bf45c9062d0b886cb4ff7b386b 100644
--- a/net/minecraft/world/level/chunk/ChunkGenerator.java
+++ b/net/minecraft/world/level/chunk/ChunkGenerator.java
@@ -327,7 +327,7 @@ public abstract class ChunkGenerator {
}
private static boolean tryAddReference(StructureManager structureManager, StructureStart structureStart) {
- if (structureStart.canBeReferenced()) {
+ if (structureStart.tryReference()) { // Folia - region threading
structureManager.addReference(structureStart);
return true;
} else {
diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java
index 761fdcd4a4e18f45547afd8edff44f61c6eeacb4..f83cfa85678d288ece2348aae41d315660095ad8 100644
--- a/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/net/minecraft/world/level/chunk/LevelChunk.java
@@ -59,6 +59,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
public void tick() {
}
+ // Folia start - region threading
+ @Override
+ public BlockEntity getTileEntity() {
+ return null;
+ }
+ // Folia end - region threading
+
@Override
public boolean isRemoved() {
return true;
@@ -230,11 +237,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
@Override
public void markUnsaved() {
- boolean isUnsaved = this.isUnsaved();
- super.markUnsaved();
- if (!isUnsaved) {
- this.unsavedListener.setUnsaved(this.chunkPos);
- }
+ super.markUnsaved(); // Folia - region threading - unsavedListener is not really use
}
@Override
@@ -360,6 +363,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
@Nullable
public BlockState setBlockState(BlockPos pos, BlockState state, boolean isMoving, boolean doPlace) {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, pos, "Updating block asynchronously"); // Folia - region threading
// CraftBukkit end
int y = pos.getY();
LevelChunkSection section = this.getSection(this.getSectionIndex(y));
@@ -395,7 +399,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
}
boolean hasBlockEntity = blockState.hasBlockEntity();
- if (!this.level.isClientSide && !this.level.isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
+ if (!this.level.isClientSide && !this.level.getCurrentWorldData().isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading
blockState.onRemove(this.level, pos, state, isMoving);
} else if (!blockState.is(block) && hasBlockEntity) {
this.removeBlockEntity(pos);
@@ -404,7 +408,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
if (!section.getBlockState(i, i1, i2).is(block)) {
return null;
} else {
- if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled.
+ if (!this.level.isClientSide && doPlace && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. // Folia - region threading
state.onPlace(this.level, pos, blockState, isMoving);
}
@@ -459,7 +463,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
@Nullable
public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) {
// CraftBukkit start
- BlockEntity blockEntity = this.level.capturedTileEntities.get(pos);
+ BlockEntity blockEntity = this.level.getCurrentWorldData().capturedTileEntities.get(pos); // Folia - region threading
if (blockEntity == null) {
blockEntity = this.blockEntities.get(pos);
}
@@ -646,13 +650,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
org.bukkit.World world = this.level.getWorld();
if (world != null) {
- this.level.populating = true;
+ this.level.getCurrentWorldData().populating = true; // Folia - region threading
try {
for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) {
populator.populate(world, random, bukkitChunk);
}
} finally {
- this.level.populating = false;
+ this.level.getCurrentWorldData().populating = false; // Folia - region threading
}
}
server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk));
@@ -678,7 +682,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
@Override
public boolean isUnsaved() {
// Paper start - rewrite chunk system
- final long gameTime = this.level.getGameTime();
+ final long gameTime = this.level.getRedstoneGameTime(); // Folia - region threading
if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime)
|| ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) {
return true;
@@ -905,6 +909,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
this.ticker = ticker;
}
+ // Folia start - region threading
+ @Override
+ public BlockEntity getTileEntity() {
+ return this.blockEntity;
+ }
+ // Folia end - region threading
+
@Override
public void tick() {
if (!this.blockEntity.isRemoved() && this.blockEntity.hasLevel()) {
@@ -983,6 +994,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p
this.ticker = ticker;
}
+ // Folia start - region threading
+ @Override
+ public BlockEntity getTileEntity() {
+ return this.ticker == null ? null : this.ticker.getTileEntity();
+ }
+ // Folia end - region threading
+
@Override
public void tick() {
this.ticker.tick();
diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
index 6b6aaeca14178b5b709e20ae13552d42217f15c0..950977f8d123f903630541ded35dd86a1889240f 100644
--- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
@@ -574,7 +574,7 @@ public record SerializableChunkData(
}
}
- ChunkAccess.PackedTicks ticksForSerialization = chunk.getTicksForSerialization(level.getGameTime());
+ ChunkAccess.PackedTicks ticksForSerialization = chunk.getTicksForSerialization(level.getRedstoneGameTime()); // Folia - region threading
ShortList[] lists = Arrays.stream(chunk.getPostProcessing())
.map(list3 -> list3 != null ? new ShortArrayList(list3) : null)
.toArray(ShortList[]::new);
diff --git a/net/minecraft/world/level/dimension/end/EndDragonFight.java b/net/minecraft/world/level/dimension/end/EndDragonFight.java
index 6e7e87c32734b3aae354bc34459e5f207da5c78f..2e156694b337760be986fdf1cbf863b0d896ef2d 100644
--- a/net/minecraft/world/level/dimension/end/EndDragonFight.java
+++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java
@@ -77,7 +77,7 @@ public class EndDragonFight {
.setPlayBossMusic(true)
.setCreateWorldFog(true);
public final ServerLevel level;
- private final BlockPos origin;
+ public final BlockPos origin; // Folia - region threading
public final ObjectArrayList<Integer> gateways = new ObjectArrayList<>();
private final BlockPattern exitPortalPattern;
private int ticksSinceDragonSeen;
@@ -162,7 +162,7 @@ public class EndDragonFight {
if (!this.dragonEvent.getPlayers().isEmpty()) {
this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE);
- boolean isArenaLoaded = this.isArenaLoaded();
+ boolean isArenaLoaded = this.isArenaLoaded(); if (!isArenaLoaded) { return; } // Folia - region threading - don't tick if we don't own the entire region
if (this.needsStateScanning && isArenaLoaded) {
this.scanState();
this.needsStateScanning = false;
@@ -208,6 +208,12 @@ public class EndDragonFight {
}
List<? extends EnderDragon> dragons = this.level.getDragons();
+ // Folia start - region threading
+ // we do not want to deal with any dragons NOT nearby
+ dragons.removeIf((EnderDragon dragon) -> {
+ return !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(dragon);
+ });
+ // Folia end - region threading
if (dragons.isEmpty()) {
this.dragonKilled = true;
} else {
@@ -323,8 +329,8 @@ public class EndDragonFight {
for (int i = -8 + chunkPos.x; i <= 8 + chunkPos.x; i++) {
for (int i1 = 8 + chunkPos.z; i1 <= 8 + chunkPos.z; i1++) {
- ChunkAccess chunk = this.level.getChunk(i, i1, ChunkStatus.FULL, false);
- if (!(chunk instanceof LevelChunk)) {
+ ChunkAccess chunk = this.level.getChunkIfLoaded(i, i1); // Folia - region threading
+ if (!(chunk instanceof LevelChunk) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, i, i1, this.level.regioniser.regionSectionChunkSize)) {
return false;
}
@@ -496,6 +502,11 @@ public class EndDragonFight {
}
public void onCrystalDestroyed(EndCrystal crystal, DamageSource dmgSrc) {
+ // Folia start - region threading
+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) {
+ return;
+ }
+ // Folia end - region threading
if (this.respawnStage != null && this.respawnCrystals.contains(crystal)) {
LOGGER.debug("Aborting respawn sequence");
this.respawnStage = null;
@@ -521,7 +532,7 @@ public class EndDragonFight {
public boolean tryRespawn(@Nullable BlockPos placedEndCrystalPos) { // placedEndCrystalPos is null if the tryRespawn() call was not caused by a placed end crystal
// Paper end - Perf: Do crystal-portal proximity check before entity lookup
- if (this.dragonKilled && this.respawnStage == null) {
+ if (this.dragonKilled && this.respawnStage == null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) { // Folia - region threading
BlockPos blockPos = this.portalLocation;
if (blockPos == null) {
LOGGER.debug("Tried to respawn, but need to find the portal first.");
diff --git a/net/minecraft/world/level/levelgen/PatrolSpawner.java b/net/minecraft/world/level/levelgen/PatrolSpawner.java
index 082c9b340765e3e98055a3c4444af68264a54826..9608e06c56f0aded4d6b4e9cf3d7eec348945600 100644
--- a/net/minecraft/world/level/levelgen/PatrolSpawner.java
+++ b/net/minecraft/world/level/levelgen/PatrolSpawner.java
@@ -16,7 +16,7 @@ import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.state.BlockState;
public class PatrolSpawner implements CustomSpawner {
- private int nextTick;
+ //private int nextTick; // Folia - region threading
@Override
public int tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) {
@@ -27,6 +27,7 @@ public class PatrolSpawner implements CustomSpawner {
return 0;
} else {
RandomSource randomSource = level.random;
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
// this.nextTick--;
// if (this.nextTick > 0) {
// return 0;
@@ -38,12 +39,12 @@ public class PatrolSpawner implements CustomSpawner {
// } else if (randomSource.nextInt(5) != 0) {
// Paper start - Pillager patrol spawn settings and per player options
// Random player selection moved up for per player spawning and configuration
- int size = level.players().size();
+ int size = level.getLocalPlayers().size();
if (size < 1) {
return 0;
}
- net.minecraft.server.level.ServerPlayer player = level.players().get(randomSource.nextInt(size));
+ net.minecraft.server.level.ServerPlayer player = level.getLocalPlayers().get(randomSource.nextInt(size)); // Folia - region threading
if (player.isSpectator()) {
return 0;
}
@@ -53,8 +54,8 @@ public class PatrolSpawner implements CustomSpawner {
--player.patrolSpawnDelay;
patrolSpawnDelay = player.patrolSpawnDelay;
} else {
- this.nextTick--;
- patrolSpawnDelay = this.nextTick;
+ worldData.patrolSpawnerNextTick--; // Folia - region threading
+ patrolSpawnDelay = worldData.patrolSpawnerNextTick; // Folia - region threading
}
if (patrolSpawnDelay > 0) {
return 0;
@@ -68,7 +69,7 @@ public class PatrolSpawner implements CustomSpawner {
if (level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) {
player.patrolSpawnDelay += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200);
} else {
- this.nextTick += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200);
+ worldData.patrolSpawnerNextTick += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); // Folia - region threading
}
if (days < level.paperConfig().entities.behavior.pillagerPatrols.start.day || !level.isDay()) {
diff --git a/net/minecraft/world/level/levelgen/PhantomSpawner.java b/net/minecraft/world/level/levelgen/PhantomSpawner.java
index 11d25e64349b27bf54dc1620e4cce444c79f581c..cef0474cf5f95bff717d49e58fe0a74ce6b7b345 100644
--- a/net/minecraft/world/level/levelgen/PhantomSpawner.java
+++ b/net/minecraft/world/level/levelgen/PhantomSpawner.java
@@ -19,7 +19,7 @@ import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
public class PhantomSpawner implements CustomSpawner {
- private int nextTick;
+ //private int nextTick; // Folia - region threading
@Override
public int tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) {
@@ -34,21 +34,22 @@ public class PhantomSpawner implements CustomSpawner {
}
// Paper end - Ability to control player's insomnia and phantoms
RandomSource randomSource = level.random;
- this.nextTick--;
- if (this.nextTick > 0) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading
+ worldData.phantomSpawnerNextTick--; // Folia - region threading
+ if (worldData.phantomSpawnerNextTick > 0) { // Folia - region threading
return 0;
} else {
// Paper start - Ability to control player's insomnia and phantoms
int spawnAttemptMinSeconds = level.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds;
int spawnAttemptMaxSeconds = level.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds;
- this.nextTick += (spawnAttemptMinSeconds + randomSource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20;
+ worldData.phantomSpawnerNextTick += (spawnAttemptMinSeconds + randomSource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; // Folia - region threading
// Paper end - Ability to control player's insomnia and phantoms
if (level.getSkyDarken() < 5 && level.dimensionType().hasSkyLight()) {
return 0;
} else {
int i = 0;
- for (ServerPlayer serverPlayer : level.players()) {
+ for (ServerPlayer serverPlayer : level.getLocalPlayers()) { // Folia - region threading
if (!serverPlayer.isSpectator() && (!level.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !serverPlayer.isCreative())) { // Paper - Add phantom creative and insomniac controls
BlockPos blockPos = serverPlayer.blockPosition();
if (!level.dimensionType().hasSkyLight() || blockPos.getY() >= level.getSeaLevel() && level.canSeeSky(blockPos)) {
diff --git a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java
index 1f7005b01b56929fb694b69b37143b8d8c7b2898..f96fc1391167dea48cac1caa464b9026657df89a 100644
--- a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java
+++ b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java
@@ -47,7 +47,7 @@ public class EndPlatformFeature extends Feature<NoneFeatureConfiguration> {
// 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<org.bukkit.block.BlockState>) (java.util.List) blockList.getList(), bworld, entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM);
level.getLevel().getCraftServer().getPluginManager().callEvent(portalEvent);
diff --git a/net/minecraft/world/level/levelgen/structure/StructureStart.java b/net/minecraft/world/level/levelgen/structure/StructureStart.java
index 4dafa79dd4ec55a443ba3731a79e7cd6e8052f48..743b13693c8ef1d69751de42e9c6dadefe56395c 100644
--- a/net/minecraft/world/level/levelgen/structure/StructureStart.java
+++ b/net/minecraft/world/level/levelgen/structure/StructureStart.java
@@ -26,7 +26,7 @@ public final class StructureStart {
private final Structure structure;
private final PiecesContainer pieceContainer;
private final ChunkPos chunkPos;
- private int references;
+ private final java.util.concurrent.atomic.AtomicInteger references; // Folia - region threading
@Nullable
private volatile BoundingBox cachedBoundingBox;
@@ -39,7 +39,7 @@ public final class StructureStart {
public StructureStart(Structure structure, ChunkPos chunkPos, int references, PiecesContainer pieceContainer) {
this.structure = structure;
this.chunkPos = chunkPos;
- this.references = references;
+ this.references = new java.util.concurrent.atomic.AtomicInteger(references); // Folia - region threading
this.pieceContainer = pieceContainer;
}
@@ -126,7 +126,7 @@ public final class StructureStart {
compoundTag.putString("id", context.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(this.structure).toString());
compoundTag.putInt("ChunkX", chunkPos.x);
compoundTag.putInt("ChunkZ", chunkPos.z);
- compoundTag.putInt("references", this.references);
+ compoundTag.putInt("references", this.references.get()); // Folia - region threading
compoundTag.put("Children", this.pieceContainer.save(context));
return compoundTag;
} else {
@@ -144,15 +144,29 @@ public final class StructureStart {
}
public boolean canBeReferenced() {
- return this.references < this.getMaxReferences();
+ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading
}
+ // Folia start - region threading
+ public boolean tryReference() {
+ for (int curr = this.references.get();;) {
+ if (curr >= this.getMaxReferences()) {
+ return false;
+ }
+
+ if (curr == (curr = this.references.compareAndExchange(curr, curr + 1))) {
+ return true;
+ } // else: try again
+ }
+ }
+ // Folia end - region threading
+
public void addReference() {
- this.references++;
+ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading
}
public int getReferences() {
- return this.references;
+ return this.references.get(); // Folia - region threading
}
protected int getMaxReferences() {
diff --git a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
index 028eae2f9a459b60e92f3344091083aa93b54485..e7ea9df8f404a6176435204a91edeefab8070c89 100644
--- a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
+++ b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
@@ -47,6 +47,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater {
}
private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates updates) {
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((net.minecraft.server.level.ServerLevel)this.level, pos, "Adding block without owning region"); // Folia - region threading
boolean flag = this.count > 0;
boolean flag1 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates;
this.count++;
diff --git a/net/minecraft/world/level/saveddata/SavedData.java b/net/minecraft/world/level/saveddata/SavedData.java
index b681a5ca1c4215d5afcc988c169e22a84996a88d..3879127f6c4a7977176bcea7ccc21561210addc6 100644
--- a/net/minecraft/world/level/saveddata/SavedData.java
+++ b/net/minecraft/world/level/saveddata/SavedData.java
@@ -8,7 +8,7 @@ import net.minecraft.nbt.NbtUtils;
import net.minecraft.util.datafix.DataFixTypes;
public abstract class SavedData {
- private boolean dirty;
+ private volatile boolean dirty; // Folia - make map data thread-safe
public abstract CompoundTag save(CompoundTag tag, HolderLookup.Provider registries);
@@ -26,9 +26,10 @@ public abstract class SavedData {
public CompoundTag save(HolderLookup.Provider registries) {
CompoundTag compoundTag = new CompoundTag();
+ this.setDirty(false); // Folia - make map data thread-safe - move before save, so that any changes after are not lost
compoundTag.put("data", this.save(new CompoundTag(), registries));
NbtUtils.addCurrentDataVersion(compoundTag);
- this.setDirty(false);
+ // Folia - make map data thread-safe - move before save, so that any changes after are not lost
return compoundTag;
}
diff --git a/net/minecraft/world/level/saveddata/maps/MapIndex.java b/net/minecraft/world/level/saveddata/maps/MapIndex.java
index ffe604f8397a002800e6ecc2f878d0f6f1c98703..7ee324c32efe1e63d310120e468a2f0d8ca262b4 100644
--- a/net/minecraft/world/level/saveddata/maps/MapIndex.java
+++ b/net/minecraft/world/level/saveddata/maps/MapIndex.java
@@ -34,17 +34,21 @@ public class MapIndex extends SavedData {
@Override
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
+ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe
for (Entry<String> entry : this.usedAuxIds.object2IntEntrySet()) {
tag.putInt(entry.getKey(), entry.getIntValue());
}
+ } // Folia - make map data thread-safe
return tag;
}
public MapId getFreeAuxValueForMap() {
+ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe
int i = this.usedAuxIds.getInt("map") + 1;
this.usedAuxIds.put("map", i);
this.setDirty();
return new MapId(i);
+ } // Folia - make map data thread-safe
}
}
diff --git a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
index 3c1c89aade5ff092b880ba1bf1de83f54d3d62cc..439d850053c35ba92ccd8ffbd177c6b9b75f00db 100644
--- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
+++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java
@@ -201,7 +201,7 @@ public class MapItemSavedData extends SavedData {
}
@Override
- public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
+ public synchronized CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { // Folia - make map data thread-safe
ResourceLocation.CODEC
.encodeStart(NbtOps.INSTANCE, this.dimension.location())
.resultOrPartial(LOGGER::error)
@@ -244,7 +244,7 @@ public class MapItemSavedData extends SavedData {
return tag;
}
- public MapItemSavedData locked() {
+ public synchronized MapItemSavedData locked() { // Folia - make map data thread-safe
MapItemSavedData mapItemSavedData = new MapItemSavedData(
this.centerX, this.centerZ, this.scale, this.trackingPosition, this.unlimitedTracking, true, this.dimension
);
@@ -255,7 +255,7 @@ public class MapItemSavedData extends SavedData {
return mapItemSavedData;
}
- public MapItemSavedData scaled() {
+ public synchronized MapItemSavedData scaled() { // Folia - make map data thread-safe
return createFresh(this.centerX, this.centerZ, (byte)Mth.clamp(this.scale + 1, 0, 4), this.trackingPosition, this.unlimitedTracking, this.dimension);
}
@@ -264,7 +264,8 @@ public class MapItemSavedData extends SavedData {
return itemStack -> itemStack == stack || itemStack.is(stack.getItem()) && Objects.equals(mapId, itemStack.get(DataComponents.MAP_ID));
}
- public void tickCarriedBy(Player player, ItemStack mapStack) {
+ public synchronized void tickCarriedBy(Player player, ItemStack mapStack) { // Folia - make map data thread-safe
+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(player, "Ticking map player in incorrect region"); // Folia - region threading
if (!this.carriedByPlayers.containsKey(player)) {
MapItemSavedData.HoldingPlayer holdingPlayer = new MapItemSavedData.HoldingPlayer(player);
this.carriedByPlayers.put(player, holdingPlayer);
@@ -413,7 +414,7 @@ public class MapItemSavedData extends SavedData {
private byte calculateRotation(@Nullable LevelAccessor level, double yRot) {
if (this.dimension == Level.NETHER && level != null) {
- int i = (int)(level.getLevelData().getDayTime() / 10L);
+ int i = (int)(level.dayTime() / 10L); // Folia - region threading
return (byte)(i * i * 34187121 + i * 121 >> 15 & 15);
} else {
double d = yRot < 0.0 ? yRot - 8.0 : yRot + 8.0;
@@ -447,25 +448,27 @@ public class MapItemSavedData extends SavedData {
}
@Nullable
- public Packet<?> getUpdatePacket(MapId mapId, Player player) {
+ public synchronized Packet<?> getUpdatePacket(MapId mapId, Player player) { // Folia - make map data thread-safe
MapItemSavedData.HoldingPlayer holdingPlayer = this.carriedByPlayers.get(player);
return holdingPlayer == null ? null : holdingPlayer.nextUpdatePacket(mapId);
}
- public void setColorsDirty(int x, int z) {
- this.setDirty();
+ public synchronized void setColorsDirty(int x, int z) { // Folia - make map data thread-safe
+ //this.setDirty(); // Folia - make dirty only after updating data - moved down
for (MapItemSavedData.HoldingPlayer holdingPlayer : this.carriedBy) {
holdingPlayer.markColorsDirty(x, z);
}
+ this.setDirty(); // Folia - make dirty only after updating data - moved from above
}
- public void setDecorationsDirty() {
- this.setDirty();
+ public synchronized void setDecorationsDirty() { // Folia - make map data thread-safe
+ //this.setDirty(); // Folia - make dirty only after updating data - moved down
this.carriedBy.forEach(MapItemSavedData.HoldingPlayer::markDecorationsDirty);
+ this.setDirty(); // Folia - make dirty only after updating data - moved from above
}
- public MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) {
+ public synchronized MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { // Folia - make map data thread-safe
MapItemSavedData.HoldingPlayer holdingPlayer = this.carriedByPlayers.get(player);
if (holdingPlayer == null) {
holdingPlayer = new MapItemSavedData.HoldingPlayer(player);
@@ -476,7 +479,7 @@ public class MapItemSavedData extends SavedData {
return holdingPlayer;
}
- public boolean toggleBanner(LevelAccessor accessor, BlockPos pos) {
+ public synchronized boolean toggleBanner(LevelAccessor accessor, BlockPos pos) { // Folia - make map data thread-safe
double d = pos.getX() + 0.5;
double d1 = pos.getZ() + 0.5;
int i = 1 << this.scale;
@@ -484,7 +487,7 @@ public class MapItemSavedData extends SavedData {
double d3 = (d1 - this.centerZ) / i;
int i1 = 63;
if (d2 >= -63.0 && d3 >= -63.0 && d2 <= 63.0 && d3 <= 63.0) {
- MapBanner mapBanner = MapBanner.fromWorld(accessor, pos);
+ MapBanner mapBanner = accessor.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(accessor.getMinecraftWorld(), pos) ? null : MapBanner.fromWorld(accessor, pos); // Folia - make map data thread-safe - don't sync load or read data we do not own
if (mapBanner == null) {
return false;
}
@@ -504,7 +507,7 @@ public class MapItemSavedData extends SavedData {
return false;
}
- public void checkBanners(BlockGetter reader, int x, int z) {
+ public synchronized void checkBanners(BlockGetter reader, int x, int z) { // Folia - make map data thread-safe
Iterator<MapBanner> iterator = this.bannerMarkers.values().iterator();
while (iterator.hasNext()) {
@@ -523,13 +526,13 @@ public class MapItemSavedData extends SavedData {
return this.bannerMarkers.values();
}
- public void removedFromFrame(BlockPos pos, int entityId) {
+ public synchronized void removedFromFrame(BlockPos pos, int entityId) { // Folia - make map data thread-safe
this.removeDecoration(getFrameKey(entityId));
this.frameMarkers.remove(MapFrame.frameId(pos));
this.setDirty();
}
- public boolean updateColor(int x, int z, byte color) {
+ public synchronized boolean updateColor(int x, int z, byte color) { // Folia - make map data thread-safe
byte b = this.colors[x + z * 128];
if (b != color) {
this.setColor(x, z, color);
@@ -539,12 +542,12 @@ public class MapItemSavedData extends SavedData {
}
}
- public void setColor(int x, int z, byte color) {
+ public synchronized void setColor(int x, int z, byte color) { // Folia - make map data thread-safe
this.colors[x + z * 128] = color;
this.setColorsDirty(x, z);
}
- public boolean isExplorationMap() {
+ public synchronized boolean isExplorationMap() { // Folia - make map data thread-safe
for (MapDecoration mapDecoration : this.decorations.values()) {
if (mapDecoration.type().value().explorationMapElement()) {
return true;
@@ -554,7 +557,7 @@ public class MapItemSavedData extends SavedData {
return false;
}
- public void addClientSideDecorations(List<MapDecoration> decorations) {
+ public synchronized void addClientSideDecorations(List<MapDecoration> decorations) { // Folia - make map data thread-safe
this.decorations.clear();
this.trackedDecorationCount = 0;
@@ -571,7 +574,7 @@ public class MapItemSavedData extends SavedData {
return this.decorations.values();
}
- public boolean isTrackedCountOverLimit(int trackedCount) {
+ public synchronized boolean isTrackedCountOverLimit(int trackedCount) { // Folia - make map data thread-safe
return this.trackedDecorationCount >= trackedCount;
}
@@ -726,11 +729,13 @@ public class MapItemSavedData extends SavedData {
}
public void applyToMap(MapItemSavedData savedData) {
+ synchronized (savedData) { // Folia - make map data thread-safe
for (int i = 0; i < this.width; i++) {
for (int i1 = 0; i1 < this.height; i1++) {
savedData.setColor(this.startX + i, this.startY + i1, this.mapColors[i + i1 * this.width]);
}
}
+ } // Folia - make map data thread-safe
}
}
}
diff --git a/net/minecraft/world/level/storage/DimensionDataStorage.java b/net/minecraft/world/level/storage/DimensionDataStorage.java
index d9a3b5a2e6495b7e22c114506c2bd1e406f58f8f..ab572ac18fd02306210c87eb9ba5e5d4197ff997 100644
--- a/net/minecraft/world/level/storage/DimensionDataStorage.java
+++ b/net/minecraft/world/level/storage/DimensionDataStorage.java
@@ -51,6 +51,7 @@ public class DimensionDataStorage implements AutoCloseable {
}
public <T extends SavedData> T computeIfAbsent(SavedData.Factory<T> factory, String name) {
+ synchronized (this.cache) { // Folia - make map data thread-safe
T savedData = this.get(factory, name);
if (savedData != null) {
return savedData;
@@ -59,10 +60,12 @@ public class DimensionDataStorage implements AutoCloseable {
this.set(name, savedData1);
return savedData1;
}
+ } // Folia - make map data thread-safe
}
@Nullable
public <T extends SavedData> T get(SavedData.Factory<T> factory, String name) {
+ synchronized (this.cache) { // Folia - make map data thread-safe
Optional<SavedData> optional = this.cache.get(name);
if (optional == null) {
optional = Optional.ofNullable(this.readSavedData(factory.deserializer(), factory.type(), name));
@@ -70,6 +73,7 @@ public class DimensionDataStorage implements AutoCloseable {
}
return (T)optional.orElse(null);
+ } // Folia - make map data thread-safe
}
@Nullable
@@ -88,8 +92,10 @@ public class DimensionDataStorage implements AutoCloseable {
}
public void set(String name, SavedData savedData) {
+ synchronized (this.cache) { // Folia - make map data thread-safe
this.cache.put(name, Optional.of(savedData));
savedData.setDirty();
+ } // Folia - make map data thread-safe
}
public CompoundTag readTagFromDisk(String filename, DataFixTypes dataFixType, int version) throws IOException {
diff --git a/net/minecraft/world/ticks/LevelChunkTicks.java b/net/minecraft/world/ticks/LevelChunkTicks.java
index faf45ac459f7c25309d6ef6dce371d484a0dae7b..8a98064f2e44b27947c1af9c80ae0d7a397db7e4 100644
--- a/net/minecraft/world/ticks/LevelChunkTicks.java
+++ b/net/minecraft/world/ticks/LevelChunkTicks.java
@@ -48,6 +48,21 @@ public class LevelChunkTicks<T> implements SerializableTickContainer<T>, 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<T>[] queue = this.tickQueue.toArray(new ScheduledTick[0]);
+ this.tickQueue.clear();
+ for (final ScheduledTick<T> entry : queue) {
+ final ScheduledTick<T> newEntry = new ScheduledTick<>(
+ entry.type(), entry.pos(), entry.triggerTick() + offset, entry.subTickOrder()
+ );
+ this.tickQueue.add(newEntry);
+ }
+ }
+ // Folia end - region threading
public LevelChunkTicks() {
}
diff --git a/net/minecraft/world/ticks/LevelTicks.java b/net/minecraft/world/ticks/LevelTicks.java
index 66abc2e7adee60fa98eed1ba36e018814fd02cad..2caedf1c12e5a388f7b14989310a2137bc1117c3 100644
--- a/net/minecraft/world/ticks/LevelTicks.java
+++ b/net/minecraft/world/ticks/LevelTicks.java
@@ -39,12 +39,69 @@ public class LevelTicks<T> implements LevelTickAccess<T> {
private final List<ScheduledTick<T>> alreadyRunThisTick = new ArrayList<>();
private final Set<ScheduledTick<?>> toRunThisTickSet = new ObjectOpenCustomHashSet<>(ScheduledTick.UNIQUE_TICK_HASH);
private final BiConsumer<LevelChunkTicks<T>, ScheduledTick<T>> 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<T> 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<Long2ObjectMap.Entry<LevelChunkTicks<T>>> iterator =
+ ((Long2ObjectOpenHashMap<LevelChunkTicks<T>>)this.allContainers).long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry<LevelChunkTicks<T>> entry = iterator.next();
+ final LevelChunkTicks<T> tickContainer = entry.getValue();
+ tickContainer.offsetTicks(tickOffset);
+ into.allContainers.put(entry.getLongKey(), tickContainer);
+ }
+ for (final java.util.Iterator<Long2LongMap.Entry> 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<LevelTicks<T>> regionToData) {
+ for (final java.util.Iterator<Long2ObjectMap.Entry<LevelChunkTicks<T>>> iterator =
+ ((Long2ObjectOpenHashMap<LevelChunkTicks<T>>)this.allContainers).long2ObjectEntrySet().fastIterator();
+ iterator.hasNext();) {
+ final Long2ObjectMap.Entry<LevelChunkTicks<T>> 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<Long2LongMap.Entry> 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<T> implements LevelTickAccess<T> {
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<T> levelChunkTicks, ScheduledTick<T> tick) -> {
+ if (tick.equals(levelChunkTicks.peek())) {
+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData();
+ ((LevelTicks<T>)(isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks())).updateContainerScheduling(tick);
+ }
+ });
+ // Folia end - region threading
}
public void removeContainer(ChunkPos chunkPos) {
@@ -70,6 +137,7 @@ public class LevelTicks<T> implements LevelTickAccess<T> {
@Override
public void schedule(ScheduledTick<T> 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<T> levelChunkTicks = this.allContainers.get(packedChunkPos);
if (levelChunkTicks == null) {