--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java
@@ -230,7 +_,7 @@
 
     @Override
     public int getTickableTileEntityCount() {
-        return world.blockEntityTickers.size();
+        throw new UnsupportedOperationException(); // Folia - region threading - TODO fix this?
     }
 
     @Override
@@ -297,7 +_,7 @@
         // Paper start - per world spawn limits
         for (SpawnCategory spawnCategory : SpawnCategory.values()) {
             if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
-                setSpawnLimit(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory)));
+                this.spawnCategoryLimit.put(spawnCategory, this.world.paperConfig().entities.spawning.spawnLimits.getInt(CraftSpawnCategory.toNMS(spawnCategory))); // Folia - region threading
             }
         }
         // Paper end - per world spawn limits
@@ -367,6 +_,7 @@
 
     @Override
     public Chunk getChunkAt(int x, int z) {
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.getHandle(), x, z, "Async chunk retrieval"); // Folia - region threading
         warnUnsafeChunk("getting a faraway chunk", x, z); // Paper
         net.minecraft.world.level.chunk.LevelChunk chunk = (net.minecraft.world.level.chunk.LevelChunk) this.world.getChunk(x, z, ChunkStatus.FULL, true);
         return new CraftChunk(chunk);
@@ -397,10 +_,10 @@
     @Override
     public boolean isChunkGenerated(int x, int z) {
         // Paper start - Fix this method
-        if (!Bukkit.isPrimaryThread()) {
+        if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.getHandle(), x, z)) { // Folia - region threading
             return java.util.concurrent.CompletableFuture.supplyAsync(() -> {
                 return CraftWorld.this.isChunkGenerated(x, z);
-            }, world.getChunkSource().mainThreadProcessor).join();
+            }, (run) -> { io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.getHandle(), x, z, run);}).join(); // Folia - region threading
         }
         ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z);
         if (chunk != null) {
@@ -457,7 +_,7 @@
     }
 
     private boolean unloadChunk0(int x, int z, boolean save) {
-        org.spigotmc.AsyncCatcher.catchOp("chunk unload"); // Spigot
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot unload chunk asynchronously"); // Folia - region threading
         if (!this.isChunkLoaded(x, z)) {
             return true;
         }
@@ -474,7 +_,7 @@
 
     @Override
     public boolean regenerateChunk(int x, int z) {
-        org.spigotmc.AsyncCatcher.catchOp("chunk regenerate"); // Spigot
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot regenerate chunk asynchronously"); // Folia - region threading
         throw new UnsupportedOperationException("Not supported in this Minecraft version! Unless you can fix it, this is not a bug :)");
         /*
         if (!unloadChunk0(x, z, false)) {
@@ -501,6 +_,7 @@
 
     @Override
     public boolean refreshChunk(int x, int z) {
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot refresh chunk asynchronously"); // Folia - region threading
         ChunkHolder playerChunk = this.world.getChunkSource().chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
         if (playerChunk == null) return false;
 
@@ -551,7 +_,7 @@
 
     @Override
     public boolean loadChunk(int x, int z, boolean generate) {
-        org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.getHandle(), x, z, "May not sync load chunks asynchronously"); // Folia - region threading
         warnUnsafeChunk("loading a faraway chunk", x, z); // Paper
         ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper
 
@@ -591,7 +_,7 @@
 
         final DistanceManager distanceManager = this.world.getChunkSource().chunkMap.distanceManager;
         if (distanceManager.addPluginRegionTicket(new ChunkPos(x, z), plugin)) {
-            this.getChunkAt(x, z); // ensure it's loaded
+            //this.getChunkAt(x, z); // ensure it's loaded // Folia - region threading - do not load chunks for tickets anymore to make this mt-safe
             return true;
         }
 
@@ -779,13 +_,15 @@
 
     @Override
     public boolean generateTree(Location loc, TreeType type, BlockChangeDelegate delegate) {
-        this.world.captureTreeGeneration = true;
-        this.world.captureBlockStates = true;
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, loc.getX(), loc.getZ(), "Cannot generate tree asynchronously"); // Folia - region threading
+        io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - region threading
+        worldData.captureTreeGeneration = true; // Folia - region threading
+        worldData.captureBlockStates = true; // Folia - region threading
         boolean grownTree = this.generateTree(loc, type);
-        this.world.captureBlockStates = false;
-        this.world.captureTreeGeneration = false;
+        worldData.captureBlockStates = false; // Folia - region threading
+        worldData.captureTreeGeneration = false; // Folia - region threading
         if (grownTree) { // Copy block data to delegate
-            for (BlockState blockstate : this.world.capturedBlockStates.values()) {
+            for (BlockState blockstate : worldData.capturedBlockStates.values()) { // Folia - region threading
                 BlockPos position = ((CraftBlockState) blockstate).getPosition();
                 net.minecraft.world.level.block.state.BlockState oldBlock = this.world.getBlockState(position);
                 int flag = ((CraftBlockState) blockstate).getFlag();
@@ -793,10 +_,10 @@
                 net.minecraft.world.level.block.state.BlockState newBlock = this.world.getBlockState(position);
                 this.world.notifyAndUpdatePhysics(position, null, oldBlock, newBlock, newBlock, flag, 512);
             }
-            this.world.capturedBlockStates.clear();
+            worldData.capturedBlockStates.clear(); // Folia - region threading
             return true;
         } else {
-            this.world.capturedBlockStates.clear();
+            worldData.capturedBlockStates.clear(); // Folia - region threading
             return false;
         }
     }
@@ -830,6 +_,7 @@
 
     @Override
     public void setTime(long time) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading
         long margin = (time - this.getFullTime()) % 24000;
         if (margin < 0) margin += 24000;
         this.setFullTime(this.getFullTime() + margin);
@@ -842,6 +_,7 @@
 
     @Override
     public void setFullTime(long time) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify time off of the global region"); // Folia - region threading
         // Notify anyone who's listening
         TimeSkipEvent event = new TimeSkipEvent(this, TimeSkipEvent.SkipReason.CUSTOM, time - this.world.getDayTime());
         this.server.getPluginManager().callEvent(event);
@@ -869,7 +_,7 @@
 
     @Override
     public long getGameTime() {
-        return this.world.levelData.getGameTime();
+        return this.getHandle().getGameTime(); // Folia - region threading
     }
 
     @Override
@@ -894,6 +_,7 @@
     }
     public boolean createExplosion(double x, double y, double z, float power, boolean setFire, boolean breakBlocks, Entity source, Consumer<net.minecraft.world.level.ServerExplosion> configurator) {
     // Paper end - expand explosion API
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously"); // Folia - region threading
         net.minecraft.world.level.Level.ExplosionInteraction explosionType;
         if (!breakBlocks) {
             explosionType = net.minecraft.world.level.Level.ExplosionInteraction.NONE; // Don't break blocks
@@ -903,6 +_,7 @@
             explosionType = net.minecraft.world.level.Level.ExplosionInteraction.MOB; // Respect mobGriefing gamerule
         }
 
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x, z, "Cannot create explosion asynchronously"); // Folia - region threading
         net.minecraft.world.entity.Entity entity = (source == null) ? null : ((CraftEntity) source).getHandle();
         return !this.world.explode0(entity, Explosion.getDefaultDamageSource(this.world, entity), null, x, y, z, power, setFire, explosionType, ParticleTypes.EXPLOSION, ParticleTypes.EXPLOSION_EMITTER, SoundEvents.GENERIC_EXPLODE, configurator).wasCanceled; // Paper - expand explosion API
     }
@@ -985,6 +_,7 @@
 
     @Override
     public int getHighestBlockYAt(int x, int z, org.bukkit.HeightMap heightMap) {
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, x >> 4, z >> 4, "Cannot retrieve chunk asynchronously"); // Folia - region threading
         warnUnsafeChunk("getting a faraway chunk", x >> 4, z >> 4); // Paper
         // Transient load for this tick
         return this.world.getChunk(x >> 4, z >> 4).getHeight(CraftHeightMap.toNMS(heightMap), x, z);
@@ -1015,6 +_,7 @@
     @Override
     public void setBiome(int x, int y, int z, Holder<net.minecraft.world.level.biome.Biome> bb) {
         BlockPos pos = new BlockPos(x, 0, z);
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, pos, "Cannot retrieve chunk asynchronously"); // Folia - region threading
         if (this.world.hasChunkAt(pos)) {
             net.minecraft.world.level.chunk.LevelChunk chunk = this.world.getChunkAt(pos);
 
@@ -1325,6 +_,7 @@
 
     @Override
     public void setStorm(boolean hasStorm) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
         this.world.serverLevelData.setRaining(hasStorm, org.bukkit.event.weather.WeatherChangeEvent.Cause.PLUGIN); // Paper - Add cause to Weather/ThunderChangeEvents
         this.setWeatherDuration(0); // Reset weather duration (legacy behaviour)
         this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands)
@@ -1337,6 +_,7 @@
 
     @Override
     public void setWeatherDuration(int duration) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
         this.world.serverLevelData.setRainTime(duration);
     }
 
@@ -1347,6 +_,7 @@
 
     @Override
     public void setThundering(boolean thundering) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
         this.world.serverLevelData.setThundering(thundering, org.bukkit.event.weather.ThunderChangeEvent.Cause.PLUGIN); // Paper - Add cause to Weather/ThunderChangeEvents
         this.setThunderDuration(0); // Reset weather duration (legacy behaviour)
         this.setClearWeatherDuration(0); // Reset clear weather duration (reset "/weather clear" commands)
@@ -1359,6 +_,7 @@
 
     @Override
     public void setThunderDuration(int duration) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
         this.world.serverLevelData.setThunderTime(duration);
     }
 
@@ -1369,6 +_,7 @@
 
     @Override
     public void setClearWeatherDuration(int duration) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify weather off of the global region"); // Folia - region threading
         this.world.serverLevelData.setClearWeatherTime(duration);
     }
 
@@ -1567,6 +_,7 @@
 
     @Override
     public void setKeepSpawnInMemory(boolean keepLoaded) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify keep spawn in memory off of the global region"); // Folia - region threading
         if (keepLoaded) {
             this.setGameRule(GameRule.SPAWN_CHUNK_RADIUS, this.getGameRuleDefault(GameRule.SPAWN_CHUNK_RADIUS));
         } else {
@@ -1635,6 +_,7 @@
 
     @Override
     public void setHardcore(boolean hardcore) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.world.serverLevelData.settings.hardcore = hardcore;
     }
 
@@ -1647,6 +_,7 @@
     @Override
     @Deprecated
     public void setTicksPerAnimalSpawns(int ticksPerAnimalSpawns) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setTicksPerSpawns(SpawnCategory.ANIMAL, ticksPerAnimalSpawns);
     }
 
@@ -1659,6 +_,7 @@
     @Override
     @Deprecated
     public void setTicksPerMonsterSpawns(int ticksPerMonsterSpawns) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setTicksPerSpawns(SpawnCategory.MONSTER, ticksPerMonsterSpawns);
     }
 
@@ -1671,6 +_,7 @@
     @Override
     @Deprecated
     public void setTicksPerWaterSpawns(int ticksPerWaterSpawns) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setTicksPerSpawns(SpawnCategory.WATER_ANIMAL, ticksPerWaterSpawns);
     }
 
@@ -1683,6 +_,7 @@
     @Override
     @Deprecated
     public void setTicksPerWaterAmbientSpawns(int ticksPerWaterAmbientSpawns) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setTicksPerSpawns(SpawnCategory.WATER_AMBIENT, ticksPerWaterAmbientSpawns);
     }
 
@@ -1695,6 +_,7 @@
     @Override
     @Deprecated
     public void setTicksPerWaterUndergroundCreatureSpawns(int ticksPerWaterUndergroundCreatureSpawns) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setTicksPerSpawns(SpawnCategory.WATER_UNDERGROUND_CREATURE, ticksPerWaterUndergroundCreatureSpawns);
     }
 
@@ -1707,11 +_,13 @@
     @Override
     @Deprecated
     public void setTicksPerAmbientSpawns(int ticksPerAmbientSpawns) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setTicksPerSpawns(SpawnCategory.AMBIENT, ticksPerAmbientSpawns);
     }
 
     @Override
     public void setTicksPerSpawns(SpawnCategory spawnCategory, int ticksPerCategorySpawn) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null");
         Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory);
 
@@ -1728,21 +_,25 @@
 
     @Override
     public void setMetadata(String metadataKey, MetadataValue newMetadataValue) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading
         this.server.getWorldMetadata().setMetadata(this, metadataKey, newMetadataValue);
     }
 
     @Override
     public List<MetadataValue> getMetadata(String metadataKey) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading
         return this.server.getWorldMetadata().getMetadata(this, metadataKey);
     }
 
     @Override
     public boolean hasMetadata(String metadataKey) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot retrieve metadata off of the global region"); // Folia - region threading
         return this.server.getWorldMetadata().hasMetadata(this, metadataKey);
     }
 
     @Override
     public void removeMetadata(String metadataKey, Plugin owningPlugin) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify metadata off of the global region"); // Folia - region threading
         this.server.getWorldMetadata().removeMetadata(this, metadataKey, owningPlugin);
     }
 
@@ -1755,6 +_,7 @@
     @Override
     @Deprecated
     public void setMonsterSpawnLimit(int limit) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setSpawnLimit(SpawnCategory.MONSTER, limit);
     }
 
@@ -1767,6 +_,7 @@
     @Override
     @Deprecated
     public void setAnimalSpawnLimit(int limit) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setSpawnLimit(SpawnCategory.ANIMAL, limit);
     }
 
@@ -1779,6 +_,7 @@
     @Override
     @Deprecated
     public void setWaterAnimalSpawnLimit(int limit) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setSpawnLimit(SpawnCategory.WATER_ANIMAL, limit);
     }
 
@@ -1791,6 +_,7 @@
     @Override
     @Deprecated
     public void setWaterAmbientSpawnLimit(int limit) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setSpawnLimit(SpawnCategory.WATER_AMBIENT, limit);
     }
 
@@ -1803,6 +_,7 @@
     @Override
     @Deprecated
     public void setWaterUndergroundCreatureSpawnLimit(int limit) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setSpawnLimit(SpawnCategory.WATER_UNDERGROUND_CREATURE, limit);
     }
 
@@ -1815,6 +_,7 @@
     @Override
     @Deprecated
     public void setAmbientSpawnLimit(int limit) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         this.setSpawnLimit(SpawnCategory.AMBIENT, limit);
     }
 
@@ -1837,6 +_,7 @@
 
     @Override
     public void setSpawnLimit(SpawnCategory spawnCategory, int limit) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         Preconditions.checkArgument(spawnCategory != null, "SpawnCategory cannot be null");
         Preconditions.checkArgument(CraftSpawnCategory.isValidForLimits(spawnCategory), "SpawnCategory.%s are not supported", spawnCategory);
 
@@ -1919,7 +_,7 @@
         if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return;
 
         ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(CraftSound.bukkitToMinecraftHolder(sound), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, seed);
-        ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId());
+        ChunkMap.TrackedEntity entityTracker = ((CraftEntity) entity).getHandle().moonrise$getTrackedEntity(); // Folia - region threading
         if (entityTracker != null) {
             entityTracker.broadcastAndSend(packet);
         }
@@ -1940,7 +_,7 @@
         if (!(entity instanceof CraftEntity craftEntity) || entity.getWorld() != this || sound == null || category == null) return;
 
         ClientboundSoundEntityPacket packet = new ClientboundSoundEntityPacket(Holder.direct(SoundEvent.createVariableRangeEvent(ResourceLocation.parse(sound))), net.minecraft.sounds.SoundSource.valueOf(category.name()), craftEntity.getHandle(), volume, pitch, seed);
-        ChunkMap.TrackedEntity entityTracker = this.getHandle().getChunkSource().chunkMap.entityMap.get(entity.getEntityId());
+        ChunkMap.TrackedEntity entityTracker = ((CraftEntity)entity).getHandle().moonrise$getTrackedEntity(); // Folia - region threading
         if (entityTracker != null) {
             entityTracker.broadcastAndSend(packet);
         }
@@ -2023,6 +_,7 @@
 
     @Override
     public boolean setGameRuleValue(String rule, String value) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         // No null values allowed
         if (rule == null || value == null) return false;
 
@@ -2065,6 +_,7 @@
 
     @Override
     public <T> boolean setGameRule(GameRule<T> rule, T newValue) {
+        io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify server settings off of the global region"); // Folia - region threading
         Preconditions.checkArgument(rule != null, "GameRule cannot be null");
         Preconditions.checkArgument(newValue != null, "GameRule value cannot be null");
 
@@ -2292,6 +_,12 @@
 
     @Override
     public void sendGameEvent(Entity sourceEntity, org.bukkit.GameEvent gameEvent, Vector position) {
+        // Folia start - region threading
+        if (sourceEntity != null && !Bukkit.isOwnedByCurrentRegion(sourceEntity)) {
+            throw new IllegalStateException("Cannot send game event asynchronously");
+        }
+        ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, position.getX(), position.getZ(), "Cannot send game event asynchronously");
+        // Folia end - region threading
         getHandle().gameEvent(sourceEntity != null ? ((CraftEntity) sourceEntity).getHandle(): null, net.minecraft.core.registries.BuiltInRegistries.GAME_EVENT.get(org.bukkit.craftbukkit.util.CraftNamespacedKey.toMinecraft(gameEvent.getKey())).orElseThrow(), org.bukkit.craftbukkit.util.CraftVector.toBlockPos(position));
     }
     // Paper end
