From 0acb7b08b72b5755dbd33ae08b549a6f4ebebff2 Mon Sep 17 00:00:00 2001
From: Spottedleaf
Date: Sat, 11 Jan 2025 06:45:50 -0800
Subject: [PATCH] Split Minecraft patches into file patches
---
...ns.patch => 0001-Max-pending-logins.patch} | 0
.../features/0001-Threaded-Regions.patch | 19443 ----------------
...k-system-throughput-counters-to-tps.patch} | 0
...ates-in-non-loaded-or-non-owned-chu.patch} | 0
...world-tile-entities-on-worldgen-thr.patch} | 0
...-getHandle-and-overrides-perform-thr.patch | 45 -
...tion-to-player-position-on-player-d.patch} | 0
...filer.patch => 0006-Region-profiler.patch} | 4 +-
...d.patch => 0007-Add-watchdog-thread.patch} | 0
...access-when-waking-players-up-during.patch | 39 -
.../moonrise/paper/PaperHooks.java.patch | 20 +
.../util/BaseChunkSystemHooks.java.patch | 87 +
.../level/entity/EntityLookup.java.patch | 32 +
.../server/ServerEntityLookup.java.patch | 36 +
.../RegionizedPlayerChunkLoader.java.patch | 20 +
.../queue/ChunkUnloadQueue.java.patch | 42 +
.../scheduling/ChunkHolderManager.java.patch | 391 +
.../scheduling/ChunkTaskScheduler.java.patch | 75 +
.../scheduling/NewChunkHolder.java.patch | 33 +
.../collisions/CollisionUtil.java.patch | 11 +
.../activation/ActivationRange.java.patch | 174 +
.../redstone/RedstoneWireTurbo.java.patch | 19 +
.../RegionShutdownThread.java.patch | 229 +
.../threadedregions/RegionizedData.java.patch | 238 +
.../RegionizedServer.java.patch | 458 +
.../RegionizedTaskQueue.java.patch | 810 +
.../RegionizedWorldData.java.patch | 773 +
.../paper/threadedregions/Schedule.java.patch | 94 +
.../threadedregions/TeleportUtils.java.patch | 73 +
.../ThreadedRegionizer.java.patch | 1408 ++
.../paper/threadedregions/TickData.java.patch | 336 +
.../TickRegionScheduler.java.patch | 567 +
.../threadedregions/TickRegions.java.patch | 416 +
.../commands/CommandServerHealth.java.patch | 358 +
.../commands/CommandUtil.java.patch | 124 +
.../scheduler/FoliaRegionScheduler.java.patch | 427 +
.../SimpleThreadLocalRandomSource.java.patch | 82 +
.../util/ThreadLocalRandomSource.java.patch | 76 +
.../commands/CommandSourceStack.java.patch | 11 +
.../minecraft/commands/Commands.java.patch | 112 +
.../BoatDispenseItemBehavior.java.patch | 11 +
.../DefaultDispenseItemBehavior.java.patch | 11 +
.../dispenser/DispenseItemBehavior.java.patch | 149 +
.../EquipmentDispenseItemBehavior.java.patch | 11 +
.../MinecartDispenseItemBehavior.java.patch | 11 +
.../ProjectileDispenseBehavior.java.patch | 11 +
.../ShearsDispenseItemBehavior.java.patch | 11 +
.../ShulkerBoxDispenseBehavior.java.patch | 11 +
.../framework/GameTestHelper.java.patch | 11 +
.../framework/GameTestServer.java.patch | 17 +
.../minecraft/network/Connection.java.patch | 336 +
.../network/protocol/PacketUtils.java.patch | 37 +
.../server/MinecraftServer.java.patch | 779 +
.../commands/AdvancementCommands.java.patch | 32 +
.../commands/AttributeCommand.java.patch | 188 +
.../ClearInventoryCommands.java.patch | 20 +
.../server/commands/DamageCommand.java.patch | 35 +
.../DefaultGameModeCommands.java.patch | 18 +
.../server/commands/EffectCommands.java.patch | 44 +
.../server/commands/EnchantCommand.java.patch | 100 +
.../commands/ExperienceCommand.java.patch | 39 +
.../commands/FillBiomeCommand.java.patch | 49 +
.../server/commands/FillCommand.java.patch | 49 +
.../commands/ForceLoadCommand.java.patch | 93 +
.../commands/GameModeCommand.java.patch | 24 +
.../server/commands/GiveCommand.java.patch | 47 +
.../server/commands/KillCommand.java.patch | 13 +
.../server/commands/PlaceCommand.java.patch | 134 +
.../server/commands/RecipeCommand.java.patch | 30 +
.../commands/SetBlockCommand.java.patch | 42 +
.../commands/SetSpawnCommand.java.patch | 15 +
.../server/commands/SummonCommand.java.patch | 26 +
.../commands/TeleportCommand.java.patch | 47 +
.../server/commands/TimeCommand.java.patch | 33 +
.../server/commands/WeatherCommand.java.patch | 29 +
.../commands/WorldBorderCommand.java.patch | 169 +
.../dedicated/DedicatedServer.java.patch | 39 +
.../server/level/ChunkMap.java.patch | 217 +
.../server/level/DistanceManager.java.patch | 53 +
.../server/level/ServerChunkCache.java.patch | 265 +
.../level/ServerEntityGetter.java.patch | 32 +
.../server/level/ServerLevel.java.patch | 1133 +
.../server/level/ServerPlayer.java.patch | 550 +
.../level/ServerPlayerGameMode.java.patch | 40 +
.../server/level/TicketType.java.patch | 22 +
.../server/level/WorldGenRegion.java.patch | 24 +
.../ServerCommonPacketListenerImpl.java.patch | 89 +
...ConfigurationPacketListenerImpl.java.patch | 70 +
.../ServerConnectionListener.java.patch | 28 +
.../ServerGamePacketListenerImpl.java.patch | 396 +
.../ServerLoginPacketListenerImpl.java.patch | 33 +
.../server/players/BanListEntry.java.patch | 50 +
.../players/OldUsersConverter.java.patch | 11 +
.../server/players/PlayerList.java.patch | 419 +
.../server/players/StoredUserList.java.patch | 29 +
.../net/minecraft/util/SpawnUtil.java.patch | 11 +
.../world/RandomSequences.java.patch | 83 +
.../damagesource/CombatTracker.java.patch | 20 +
.../damagesource/DamageSource.java.patch | 17 +
.../damagesource/FallLocation.java.patch | 11 +
.../minecraft/world/entity/Entity.java.patch | 1047 +
.../world/entity/LivingEntity.java.patch | 148 +
.../net/minecraft/world/entity/Mob.java.patch | 61 +
.../world/entity/PortalProcessor.java.patch | 15 +
.../world/entity/TamableAnimal.java.patch | 38 +
.../world/entity/ai/Brain.java.patch | 21 +
.../ai/behavior/PoiCompetitorScan.java.patch | 14 +
.../entity/ai/goal/FollowOwnerGoal.java.patch | 11 +
.../GroundPathNavigation.java.patch | 14 +
.../ai/navigation/PathNavigation.java.patch | 34 +
.../entity/ai/sensing/PlayerSensor.java.patch | 11 +
.../ai/sensing/TemptingSensor.java.patch | 11 +
.../entity/ai/village/VillageSiege.java.patch | 134 +
.../ai/village/poi/PoiManager.java.patch | 39 +
.../world/entity/animal/Bee.java.patch | 26 +
.../world/entity/animal/Cat.java.patch | 11 +
.../boss/enderdragon/EndCrystal.java.patch | 11 +
.../entity/decoration/ItemFrame.java.patch | 12 +
.../entity/item/FallingBlockEntity.java.patch | 11 +
.../world/entity/item/ItemEntity.java.patch | 27 +
.../world/entity/item/PrimedTnt.java.patch | 22 +
.../world/entity/monster/Vex.java.patch | 11 +
.../entity/monster/ZombieVillager.java.patch | 20 +
.../entity/npc/AbstractVillager.java.patch | 22 +
.../world/entity/npc/CatSpawner.java.patch | 26 +
.../world/entity/npc/Villager.java.patch | 28 +
.../npc/WanderingTraderSpawner.java.patch | 90 +
.../world/entity/player/Player.java.patch | 17 +
.../projectile/AbstractArrow.java.patch | 14 +
.../AbstractHurtingProjectile.java.patch | 14 +
.../FireworkRocketEntity.java.patch | 14 +
.../entity/projectile/FishingHook.java.patch | 50 +
.../entity/projectile/LlamaSpit.java.patch | 14 +
.../entity/projectile/Projectile.java.patch | 87 +
.../projectile/SmallFireball.java.patch | 11 +
.../projectile/ThrowableProjectile.java.patch | 14 +
.../projectile/ThrownEnderpearl.java.patch | 122 +
.../world/entity/raid/Raid.java.patch} | 46 +-
.../world/entity/raid/Raider.java.patch | 11 +
.../world/entity/raid/Raids.java.patch | 119 +
.../vehicle/MinecartCommandBlock.java.patch | 14 +
.../entity/vehicle/MinecartHopper.java.patch | 11 +
.../minecraft/world/item/ItemStack.java.patch | 128 +
.../minecraft/world/item/MapItem.java.patch | 61 +
.../minecraft/world/item/SignItem.java.patch | 20 +
.../component/LodestoneTracker.java.patch} | 14 +-
.../world/level/BaseCommandBlock.java.patch | 35 +
.../world/level/EntityGetter.java.patch | 61 +
.../minecraft/world/level/Level.java.patch | 404 +
.../world/level/LevelAccessor.java.patch | 29 +
.../world/level/LevelReader.java.patch | 28 +
.../world/level/NaturalSpawner.java.patch | 11 +
.../world/level/ServerExplosion.java.patch | 24 +
.../level/ServerLevelAccessor.java.patch | 15 +
.../world/level/StructureManager.java.patch | 48 +
.../world/level/block/BedBlock.java.patch | 11 +
.../world/level/block/Block.java.patch | 13 +
.../world/level/block/BushBlock.java.patch | 11 +
.../block/DaylightDetectorBlock.java.patch | 11 +
.../level/block/DispenserBlock.java.patch | 20 +
.../level/block/DoublePlantBlock.java.patch | 11 +
.../level/block/EndGatewayBlock.java.patch | 50 +
.../level/block/EndPortalBlock.java.patch | 32 +
.../world/level/block/FarmBlock.java.patch | 13 +
.../world/level/block/FungusBlock.java.patch | 14 +
.../world/level/block/HoneyBlock.java.patch | 11 +
.../level/block/LightningRodBlock.java.patch | 11 +
.../level/block/MushroomBlock.java.patch | 11 +
.../level/block/NetherPortalBlock.java.patch | 62 +
.../world/level/block/Portal.java.patch | 13 +
.../level/block/RedStoneWireBlock.java.patch | 80 +
.../level/block/RedstoneTorchBlock.java.patch | 53 +
.../world/level/block/SaplingBlock.java.patch | 39 +
.../block/SpreadingSnowyDirtBlock.java.patch | 11 +
.../level/block/WitherSkullBlock.java.patch | 11 +
.../block/entity/BeaconBlockEntity.java.patch | 20 +
.../level/block/entity/BlockEntity.java.patch | 33 +
.../entity/CommandBlockEntity.java.patch | 16 +
.../entity/ConduitBlockEntity.java.patch | 20 +
.../block/entity/HopperBlockEntity.java.patch | 150 +
.../SculkCatalystBlockEntity.java.patch | 14 +
.../TheEndGatewayBlockEntity.java.patch | 246 +
.../entity/TickingBlockEntity.java.patch | 9 +
.../level/block/grower/TreeGrower.java.patch | 84 +
.../block/piston/PistonBaseBlock.java.patch | 11 +
.../piston/PistonMovingBlockEntity.java.patch | 43 +
.../world/level/border/WorldBorder.java.patch | 31 +
.../level/chunk/ChunkGenerator.java.patch | 11 +
.../world/level/chunk/LevelChunk.java.patch | 117 +
.../storage/SerializableChunkData.java.patch | 11 +
.../dimension/end/EndDragonFight.java.patch | 65 +
.../level/levelgen/PatrolSpawner.java.patch | 54 +
.../level/levelgen/PhantomSpawner.java.patch | 38 +
.../feature/EndPlatformFeature.java.patch | 11 +
.../structure/StructureStart.java.patch | 63 +
.../CollectingNeighborUpdater.java.patch | 10 +
.../level/saveddata/SavedData.java.patch | 23 +
.../level/saveddata/maps/MapIndex.java.patch | 24 +
.../maps/MapItemSavedData.java.patch | 172 +
.../storage/DimensionDataStorage.java.patch | 42 +
.../world/ticks/LevelChunkTicks.java.patch | 24 +
.../world/ticks/LevelTicks.java.patch | 102 +
202 files changed, 19115 insertions(+), 19555 deletions(-)
rename folia-server/minecraft-patches/features/{0002-Max-pending-logins.patch => 0001-Max-pending-logins.patch} (100%)
delete mode 100644 folia-server/minecraft-patches/features/0001-Threaded-Regions.patch
rename folia-server/minecraft-patches/features/{0003-Add-chunk-system-throughput-counters-to-tps.patch => 0002-Add-chunk-system-throughput-counters-to-tps.patch} (100%)
rename folia-server/minecraft-patches/features/{0005-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch => 0003-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch} (100%)
rename folia-server/minecraft-patches/features/{0006-Block-reading-in-world-tile-entities-on-worldgen-thr.patch => 0004-Block-reading-in-world-tile-entities-on-worldgen-thr.patch} (100%)
delete mode 100644 folia-server/minecraft-patches/features/0004-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch
rename folia-server/minecraft-patches/features/{0010-Sync-vehicle-position-to-player-position-on-player-d.patch => 0005-Sync-vehicle-position-to-player-position-on-player-d.patch} (100%)
rename folia-server/minecraft-patches/features/{0011-Region-profiler.patch => 0006-Region-profiler.patch} (99%)
rename folia-server/minecraft-patches/features/{0012-Add-watchdog-thread.patch => 0007-Add-watchdog-thread.patch} (100%)
delete mode 100644 folia-server/minecraft-patches/features/0007-Skip-worldstate-access-when-waking-players-up-during.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/PaperHooks.java.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java.patch
create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/redstone/RedstoneWireTurbo.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionShutdownThread.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedData.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedServer.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedTaskQueue.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedWorldData.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/Schedule.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TeleportUtils.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/ThreadedRegionizer.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickData.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegionScheduler.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegions.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandServerHealth.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandUtil.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java.patch
create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/commands/CommandSourceStack.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/commands/Commands.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestHelper.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestServer.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/network/protocol/PacketUtils.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/AdvancementCommands.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/AttributeCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/ClearInventoryCommands.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/DamageCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/DefaultGameModeCommands.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/EffectCommands.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/EnchantCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/ExperienceCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillBiomeCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/ForceLoadCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/GameModeCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/GiveCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/KillCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/PlaceCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/RecipeCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetBlockCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetSpawnCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/SummonCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/TeleportCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/TimeCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/WeatherCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/WorldBorderCommand.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ChunkMap.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/DistanceManager.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerEntityGetter.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayerGameMode.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/TicketType.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/WorldGenRegion.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/BanListEntry.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/PlayerList.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/StoredUserList.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/util/SpawnUtil.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/RandomSequences.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/CombatTracker.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/DamageSource.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/FallLocation.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/Mob.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/PortalProcessor.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/TamableAnimal.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/Brain.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/PlayerSensor.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/VillageSiege.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/poi/PoiManager.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Bee.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Cat.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/decoration/ItemFrame.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/FallingBlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/ItemEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/PrimedTnt.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/Vex.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/ZombieVillager.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/AbstractVillager.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/CatSpawner.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/Villager.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractArrow.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FishingHook.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/LlamaSpit.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/Projectile.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/SmallFireball.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch
rename folia-server/minecraft-patches/{features/0009-Fix-off-region-raid-heroes.patch => sources/net/minecraft/world/entity/raid/Raid.java.patch} (58%)
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raider.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raids.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartHopper.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/ItemStack.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/MapItem.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/SignItem.java.patch
rename folia-server/minecraft-patches/{features/0008-Do-not-access-POI-data-for-lodestone-compass.patch => sources/net/minecraft/world/item/component/LodestoneTracker.java.patch} (57%)
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/BaseCommandBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelAccessor.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelReader.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/NaturalSpawner.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerExplosion.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerLevelAccessor.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/StructureManager.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BedBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Block.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BushBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DispenserBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DoublePlantBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndGatewayBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndPortalBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FarmBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FungusBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/HoneyBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/LightningRodBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/MushroomBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/NetherPortalBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Portal.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedStoneWireBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SaplingBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TickingBlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/border/WorldBorder.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/ChunkGenerator.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/LevelChunk.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/SavedData.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapIndex.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/storage/DimensionDataStorage.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelChunkTicks.java.patch
create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelTicks.java.patch
diff --git a/folia-server/minecraft-patches/features/0002-Max-pending-logins.patch b/folia-server/minecraft-patches/features/0001-Max-pending-logins.patch
similarity index 100%
rename from folia-server/minecraft-patches/features/0002-Max-pending-logins.patch
rename to folia-server/minecraft-patches/features/0001-Max-pending-logins.patch
diff --git a/folia-server/minecraft-patches/features/0001-Threaded-Regions.patch b/folia-server/minecraft-patches/features/0001-Threaded-Regions.patch
deleted file mode 100644
index b42ccbe..0000000
--- a/folia-server/minecraft-patches/features/0001-Threaded-Regions.patch
+++ /dev/null
@@ -1,19443 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Spottedleaf
-Date: Sun, 2 Oct 2022 21:28:53 -0700
-Subject: [PATCH] Threaded Regions
-
-See https://docs.papermc.io/folia/reference/overview and
-https://docs.papermc.io/folia/reference/region-logic
-
-diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java
-index 4d344559a20a0c35c181e297e81788c747363ec9..e799155d2ccbfe9dd3e5a87c6b6c28278e9accce 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;
-diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
-index ece1261b67033e946dfc20a96872708755bffe0a..58986c62c485cae379180ba95ba0ea60145ef73f 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
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
-index 7554c109c35397bc1a43dd80e87764fd78645bbf..db16fe8d664f9b04710200d63439564cb97c0066 100644
---- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
-@@ -460,6 +460,19 @@ public abstract class EntityLookup implements LevelEntityGetter {
- return slices == null || !slices.isPreventingStatusUpdates();
- }
-
-+ // Folia start - region threading
-+ // only appropriate to use when in shutdown, as this performs no logic hooks to properly add to world
-+ public boolean addEntityForShutdownTeleportComplete(final Entity entity) {
-+ final BlockPos pos = entity.blockPosition();
-+ final int sectionX = pos.getX() >> 4;
-+ final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world));
-+ final int sectionZ = pos.getZ() >> 4;
-+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
-+
-+ return slices.addEntity(entity, sectionY);
-+ }
-+ // Folia end - region threading
-+
- protected void removeEntity(final Entity entity) {
- final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
- final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
-@@ -986,6 +999,9 @@ public abstract class EntityLookup implements LevelEntityGetter {
- EntityLookup.this.removeEntityCallback(entity);
-
- this.entity.setLevelCallback(NoOpCallback.INSTANCE);
-+
-+ // only AFTER full removal callbacks, so that thread checking will work. // Folia - region threading
-+ EntityLookup.this.world.getCurrentWorldData().removeEntity(entity); // Folia - region threading
- }
- }
-
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-index 26207443b1223119c03db478d7e816d9cdf8e618..a89ee24c6aed46af23c5de7ae2234c7902f1c0f4 100644
---- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
-@@ -17,7 +17,7 @@ public final class ServerEntityLookup extends EntityLookup {
- private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0];
-
- private final ServerLevel serverWorld;
-- public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
-+ // Folia - move to regionized world data
-
- public ServerEntityLookup(final ServerLevel world, final LevelCallback worldCallback) {
- super(world, worldCallback);
-@@ -75,6 +75,7 @@ public final class ServerEntityLookup extends EntityLookup {
- if (entity instanceof ServerPlayer player) {
- ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player);
- }
-+ this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading
- }
-
- @Override
-@@ -87,14 +88,14 @@ public final class ServerEntityLookup extends EntityLookup {
- @Override
- protected void entityStartLoaded(final Entity entity) {
- // Moonrise start - entity tracker
-- this.trackerEntities.add(entity);
-+ this.world.getCurrentWorldData().trackerEntities.add(entity); // Folia - region threading
- // Moonrise end - entity tracker
- }
-
- @Override
- protected void entityEndLoaded(final Entity entity) {
- // Moonrise start - entity tracker
-- this.trackerEntities.remove(entity);
-+ this.world.getCurrentWorldData().trackerEntities.remove(entity); // Folia - region threading
- // Moonrise end - entity tracker
- }
-
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-index dd2509996bfd08e8c3f9f2be042229eac6d7692d..f77dcf5a42ff34a1624ddf16bcce2abee81194bb 100644
---- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
-@@ -216,7 +216,7 @@ public final class RegionizedPlayerChunkLoader {
- final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
-
- if (loader == null) {
-- return;
-+ throw new IllegalStateException("Player is already removed from player chunk loader"); // Folia - region threading
- }
-
- loader.remove();
-@@ -304,7 +304,7 @@ public final class RegionizedPlayerChunkLoader {
- public void tick() {
- TickThread.ensureTickThread("Cannot tick player chunk loader async");
- long currTime = System.nanoTime();
-- for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
-+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.getLocalPlayers())) { // Folia - region threding
- final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
- if (loader == null || loader.removed || loader.world != this.world) {
- // not our problem anymore
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
-index 7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309..4bfcae47ed76346e6200514ebce5b04f907c5026 100644
---- a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
-@@ -29,6 +29,39 @@ public final class ChunkUnloadQueue {
-
- public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {}
-
-+ // Folia start - threaded regions
-+ public List retrieveForCurrentRegion() {
-+ final io.papermc.paper.threadedregions.ThreadedRegionizer.ThreadedRegion region =
-+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion();
-+ final io.papermc.paper.threadedregions.ThreadedRegionizer regionizer = region.regioniser;
-+ final int shift = this.coordinateShift;
-+
-+ final List ret = new ArrayList<>();
-+
-+ for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) {
-+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry entry = iterator.next();
-+ final long key = entry.getKey();
-+ final UnloadSection section = entry.getValue();
-+ final int sectionX = CoordinateUtils.getChunkX(key);
-+ final int sectionZ = CoordinateUtils.getChunkZ(key);
-+ final int chunkX = sectionX << shift;
-+ final int chunkZ = sectionZ << shift;
-+
-+ if (regionizer.getRegionAtUnsynchronised(chunkX, chunkZ) != region) {
-+ continue;
-+ }
-+
-+ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size()));
-+ }
-+
-+ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> {
-+ return Long.compare(s1.order, s2.order);
-+ });
-+
-+ return ret;
-+ }
-+ // Folia end - threaded regions
-+
- public List retrieveForAllRegions() {
- final List ret = new ArrayList<>();
-
-@@ -141,4 +174,4 @@ public final class ChunkUnloadQueue {
- this.order = order;
- }
- }
--}
-\ No newline at end of file
-+}
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-index b5817aa8f537593f6d9fc6b612c82ccccb250ac7..aae97116a22a87cffd4756d566da3acd96ce2ae0 100644
---- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
-@@ -56,6 +56,14 @@ import java.util.concurrent.atomic.AtomicReference;
- import java.util.concurrent.locks.LockSupport;
- import java.util.function.Predicate;
-
-+// Folia start - region threading
-+import io.papermc.paper.threadedregions.RegionizedServer;
-+import io.papermc.paper.threadedregions.ThreadedRegionizer;
-+import io.papermc.paper.threadedregions.TickRegionScheduler;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
-+// Folia end - region threading
-+
- public final class ChunkHolderManager {
-
- private static final Logger LOGGER = LogUtils.getClassLogger();
-@@ -78,29 +86,83 @@ public final class ChunkHolderManager {
- private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f);
- private final ServerLevel world;
- private final ChunkTaskScheduler taskScheduler;
-- private long currentTick;
-+ // Folia start - region threading
-+ public static final class HolderManagerRegionData {
-+ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>();
-+ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
-+ if (c1 == c2) {
-+ return 0;
-+ }
-
-- private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>();
-- private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
-- if (c1 == c2) {
-- return 0;
-+ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
-+
-+ if (saveTickCompare != 0) {
-+ return saveTickCompare;
-+ }
-+
-+ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
-+ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
-+
-+ if (coord1 == coord2) {
-+ throw new IllegalStateException("Duplicate chunkholder in auto save queue");
-+ }
-+
-+ return Long.compare(coord1, coord2);
-+ });
-+
-+ public void merge(final HolderManagerRegionData into, final long tickOffset) {
-+ // Order doesn't really matter for the pending full update...
-+ into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate);
-+
-+ // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating
-+ // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant
-+ // addition to every entry will not affect compareTo).
-+ for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) {
-+ holder.lastAutoSave += tickOffset;
-+ into.autoSaveQueue.add(holder);
-+ }
- }
-
-- final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
-+ public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData,
-+ final ReferenceOpenHashSet dataSet) {
-+ for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) {
-+ final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift;
-+ final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift;
-+
-+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
-+ if (data != null) {
-+ data.pendingFullLoadUpdate.add(fullLoadUpdate);
-+ } // else: fullLoadUpdate is an unloaded chunk holder
-+ }
-
-- if (saveTickCompare != 0) {
-- return saveTickCompare;
-+ for (final NewChunkHolder autoSave : this.autoSaveQueue) {
-+ final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift;
-+ final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift;
-+
-+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
-+ if (data != null) {
-+ data.autoSaveQueue.add(autoSave);
-+ } // else: autoSave is an unloaded chunk holder
-+ }
- }
-+ }
-
-- final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
-- final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
-+ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() {
-+ final ThreadedRegionizer.ThreadedRegion region =
-+ TickRegionScheduler.getCurrentRegion();
-
-- if (coord1 == coord2) {
-- throw new IllegalStateException("Duplicate chunkholder in auto save queue");
-+ if (region == null) {
-+ return null;
- }
-
-- return Long.compare(coord1, coord2);
-- });
-+ if (this.world != null && this.world != region.getData().world) {
-+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
-+ }
-+
-+ return region.getData().getHolderManagerRegionData();
-+ }
-+ // Folia end - region threading
-+
-
- public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
- this.world = world;
-@@ -185,8 +247,13 @@ public final class ChunkHolderManager {
- }
-
- public void close(final boolean save, final boolean halt) {
-+ // Folia start - region threading
-+ this.close(save, halt, true, true, true);
-+ }
-+ public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) {
-+ // Folia end - region threading
- TickThread.ensureTickThread("Closing world off-main");
-- if (halt) {
-+ if (first && halt) { // Folia - region threading
- LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
- if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
- LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
-@@ -196,9 +263,10 @@ public final class ChunkHolderManager {
- }
-
- if (save) {
-- this.saveAllChunks(true, true, true);
-+ this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading
- }
-
-+ if (last) { // Folia - region threading
- MoonriseRegionFileIO.flush(this.world);
-
- if (halt) {
-@@ -220,28 +288,35 @@ public final class ChunkHolderManager {
- }
-
- this.taskScheduler.setShutdown(true);
-+ } // Folia - region threading
- }
-
- void ensureInAutosave(final NewChunkHolder holder) {
-- if (!this.autoSaveQueue.contains(holder)) {
-- holder.lastAutoSave = this.currentTick;
-- this.autoSaveQueue.add(holder);
-+ // Folia start - region threading
-+ final HolderManagerRegionData regionData = this.getCurrentRegionData();
-+ if (!regionData.autoSaveQueue.contains(holder)) {
-+ holder.lastAutoSave = RegionizedServer.getCurrentTick();
-+ regionData.autoSaveQueue.add(holder);
-+ // Folia end - region threading
- }
- }
-
- public void autoSave() {
- final List reschedule = new ArrayList<>();
-- final long currentTick = this.currentTick;
-+ final long currentTick = RegionizedServer.getCurrentTick(); // Folia - region threading
- final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval(this.world));
- final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world);
-- for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
-- final NewChunkHolder holder = this.autoSaveQueue.first();
-+ // Folia start - region threading
-+ final HolderManagerRegionData regionData = this.getCurrentRegionData();
-+ for (int autoSaved = 0; autoSaved < maxToSave && !regionData.autoSaveQueue.isEmpty();) {
-+ final NewChunkHolder holder = regionData.autoSaveQueue.first();
-+ // Folia end - region threading
-
- if (holder.lastAutoSave > maxSaveTime) {
- break;
- }
-
-- this.autoSaveQueue.remove(holder);
-+ regionData.autoSaveQueue.remove(holder); // Folia - region threading
-
- holder.lastAutoSave = currentTick;
- if (holder.save(false) != null) {
-@@ -255,15 +330,38 @@ public final class ChunkHolderManager {
-
- for (final NewChunkHolder holder : reschedule) {
- if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
-- this.autoSaveQueue.add(holder);
-+ regionData.autoSaveQueue.add(holder); // Folia start - region threading
- }
- }
- }
-
- public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
-- final List holders = this.getChunkHolders();
-+ // Folia start - region threading
-+ this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true);
-+ }
-+ public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) {
-+ final List holders = new java.util.ArrayList<>(this.chunkHolders.size() / 10);
-+ // we could iterate through all chunk holders with thread checks, however for many regions the iteration cost alone
-+ // will multiply. to avoid this, we can simply iterate through all owned sections
-+ final int regionShift = this.world.moonrise$getRegionChunkShift();
-+ final int width = 1 << regionShift;
-+ for (final LongIterator iterator = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
-+ final long sectionKey = iterator.nextLong();
-+ final int offsetX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
-+ final int offsetZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
-+
-+ for (int dz = 0; dz < width; ++dz) {
-+ for (int dx = 0; dx < width; ++dx) {
-+ final NewChunkHolder holder = this.getChunkHolder(offsetX | dx, offsetZ | dz);
-+ if (holder != null) {
-+ holders.add(holder);
-+ }
-+ }
-+ }
-+ }
-+ // Folia end - region threading
-
-- if (logProgress) {
-+ if (first && logProgress) { // Folia - region threading
- LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'");
- }
-
-@@ -292,6 +390,12 @@ public final class ChunkHolderManager {
- }
- for (int i = 0, len = holders.size(); i < len; ++i) {
- final NewChunkHolder holder = holders.get(i);
-+ // Folia start - region threading
-+ if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) {
-+ // skip holders that would fail the thread check
-+ continue;
-+ }
-+ // Folia end - region threading
- try {
- final NewChunkHolder.SaveStat saveStat = holder.save(shutdown);
- if (saveStat != null) {
-@@ -327,7 +431,7 @@ public final class ChunkHolderManager {
- }
- }
- }
-- if (flush) {
-+ if (last && flush) { // Folia - region threading
- MoonriseRegionFileIO.flush(this.world);
- try {
- MoonriseRegionFileIO.flushRegionStorages(this.world);
-@@ -732,7 +836,13 @@ public final class ChunkHolderManager {
- }
-
- public void tick() {
-- ++this.currentTick;
-+ // Folia start - region threading
-+ final ThreadedRegionizer.ThreadedRegion region =
-+ TickRegionScheduler.getCurrentRegion();
-+ if (region == null) {
-+ throw new IllegalStateException("Not running tick() while on a region");
-+ }
-+ // Folia end - region threading
-
- final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
-
-@@ -746,7 +856,7 @@ public final class ChunkHolderManager {
- return removeDelay <= 0L;
- };
-
-- for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
-+ for (final LongIterator iterator = region.getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
- final long sectionKey = iterator.nextLong();
-
- if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) {
-@@ -1031,26 +1141,56 @@ public final class ChunkHolderManager {
- if (changedFullStatus.isEmpty()) {
- return;
- }
-- if (!TickThread.isTickThread()) {
-- this.taskScheduler.scheduleChunkTask(() -> {
-- final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
-- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-- pendingFullLoadUpdate.add(changedFullStatus.get(i));
-- }
-
-- ChunkHolderManager.this.processPendingFullUpdate();
-- }, Priority.HIGHEST);
-- } else {
-- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate;
-- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-- pendingFullLoadUpdate.add(changedFullStatus.get(i));
-+ // Folia start - region threading
-+ final Long2ObjectOpenHashMap> sectionToUpdates = new Long2ObjectOpenHashMap<>();
-+ final List thisRegionHolders = new ArrayList<>();
-+
-+ final int regionShift = this.world.moonrise$getRegionChunkShift();
-+ final ThreadedRegionizer.ThreadedRegion thisRegion
-+ = TickRegionScheduler.getCurrentRegion();
-+
-+ for (final NewChunkHolder holder : changedFullStatus) {
-+ final int regionX = holder.chunkX >> regionShift;
-+ final int regionZ = holder.chunkZ >> regionShift;
-+ final long holderSectionKey = CoordinateUtils.getChunkKey(regionX, regionZ);
-+
-+ // region may be null
-+ if (thisRegion != null && this.world.regioniser.getRegionAtUnsynchronised(holder.chunkX, holder.chunkZ) == thisRegion) {
-+ thisRegionHolders.add(holder);
-+ } else {
-+ sectionToUpdates.computeIfAbsent(holderSectionKey, (final long keyInMap) -> {
-+ return new ArrayList<>();
-+ }).add(holder);
-+ }
-+ }
-+ if (!thisRegionHolders.isEmpty()) {
-+ thisRegion.getData().getHolderManagerRegionData().pendingFullLoadUpdate.addAll(thisRegionHolders);
-+ }
-+
-+ if (!sectionToUpdates.isEmpty()) {
-+ for (final Iterator>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator();
-+ iterator.hasNext();) {
-+ final Long2ObjectMap.Entry> entry = iterator.next();
-+ final long sectionKey = entry.getLongKey();
-+
-+ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
-+ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
-+
-+ final List regionHolders = entry.getValue();
-+ this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> {
-+ ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders);
-+ ChunkHolderManager.this.processPendingFullUpdate();
-+ }, Priority.HIGHEST);
-+
- }
- }
-+ // Folia end - region threading
- }
-
- private void removeChunkHolder(final NewChunkHolder holder) {
- holder.onUnload();
-- this.autoSaveQueue.remove(holder);
-+ this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading
- PlatformHooks.get().onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
- this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
- }
-@@ -1063,7 +1203,7 @@ public final class ChunkHolderManager {
- throw new IllegalStateException("Cannot unload chunks recursively");
- }
- final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift
-- final List unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
-+ final List unloadSectionsForRegion = this.unloadQueue.retrieveForCurrentRegion(); // Folia - threaded regions
- int unloadCountTentative = 0;
- for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
- final ChunkUnloadQueue.UnloadSection section
-@@ -1381,7 +1521,13 @@ public final class ChunkHolderManager {
-
- // only call on tick thread
- private boolean processPendingFullUpdate() {
-- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate;
-+ // Folia start - region threading
-+ final HolderManagerRegionData data = this.getCurrentRegionData();
-+ if (data == null) {
-+ return false;
-+ }
-+ final ArrayDeque pendingFullLoadUpdate = data.pendingFullLoadUpdate;
-+ // Folia end - region threading
-
- boolean ret = false;
-
-@@ -1392,9 +1538,7 @@ public final class ChunkHolderManager {
- ret |= holder.handleFullStatusChange(changedFullStatus);
-
- if (!changedFullStatus.isEmpty()) {
-- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
-- pendingFullLoadUpdate.add(changedFullStatus.get(i));
-- }
-+ this.addChangedStatuses(changedFullStatus); // Folia - region threading
- changedFullStatus.clear();
- }
- }
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-index 67532b85073b7978254a0b04caadfe822679e61f..cba2d16c0cb5adc92952990ef95b1c979eafd40f 100644
---- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
-@@ -122,7 +122,7 @@ public final class ChunkTaskScheduler {
- public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor;
- public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor;
-
-- private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue();
-+ // Folia - regionised ticking
-
- public final ChunkHolderManager chunkHolderManager;
-
-@@ -337,14 +337,13 @@ public final class ChunkTaskScheduler {
- };
-
- // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
-- this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING);
-+ this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, Priority.BLOCKING); // Folia - region threading
- // so, make the main thread pick it up
- ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException));
- }
-
- public boolean executeMainThreadTask() {
-- TickThread.ensureTickThread("Cannot execute main thread task off-main");
-- return this.mainThreadExecutor.executeTask();
-+ throw new UnsupportedOperationException("Use regionised ticking hooks"); // Folia - regionised ticking
- }
-
- public void raisePriority(final int x, final int z, final Priority priority) {
-@@ -829,7 +828,7 @@ public final class ChunkTaskScheduler {
- */
- @Deprecated
- public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) {
-- return this.mainThreadExecutor.queueTask(run, priority);
-+ throw new UnsupportedOperationException(); // Folia - regionised ticking
- }
-
- public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
-@@ -838,7 +837,7 @@ public final class ChunkTaskScheduler {
-
- public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
- final Priority priority) {
-- return this.mainThreadExecutor.createTask(run, priority);
-+ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
- }
-
- public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
-@@ -847,9 +846,27 @@ public final class ChunkTaskScheduler {
-
- public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
- final Priority priority) {
-- return this.mainThreadExecutor.queueTask(run, priority);
-+ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
- }
-
-+ // Folia start - region threading
-+ // this function is guaranteed to never touch the ticket lock or schedule lock
-+ // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the
-+ // ticket lock in the schedule logic
-+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run) {
-+ return this.scheduleChunkTaskEventually(chunkX, chunkZ, run, Priority.NORMAL);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run,
-+ final Priority priority) {
-+ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority);
-+ this.world.taskQueueRegionData.pushGlobalChunkTask(() -> {
-+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority);
-+ });
-+ return ret;
-+ }
-+ // Folia end - region threading
-+
- public boolean halt(final boolean sync, final long maxWaitNS) {
- this.radiusAwareGenExecutor.halt();
- this.parallelGenExecutor.halt();
-diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-index e4a5fa25ed368fc4662c30934da2963ef446d782..601ed36413bbbf9c17e530b42906986e441237fd 100644
---- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
-@@ -1359,10 +1359,10 @@ public final class NewChunkHolder {
- private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) {
- // Update progress listener for LevelLoadingScreen
- if (chunk != null) {
-- final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener;
-+ final ChunkProgressListener progressListener = null; // Folia - threaded regions
- if (progressListener != null) {
- final ChunkStatus finalStatus = status;
-- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
-+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - threaded regions
- progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus);
- });
- }
-@@ -1383,7 +1383,7 @@ public final class NewChunkHolder {
- }
-
- // must be scheduled to main, we do not trust the callback to not do anything stupid
-- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
-+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading
- for (final Consumer consumer : consumers) {
- try {
- consumer.accept(chunk);
-@@ -1411,7 +1411,7 @@ public final class NewChunkHolder {
- }
-
- // must be scheduled to main, we do not trust the callback to not do anything stupid
-- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
-+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading
- for (final Consumer consumer : consumers) {
- try {
- consumer.accept(chunk);
-diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-index e04bd54744335fb5398c6e4f7ce8b981f35bfb7d..c7ce32b31fc4247e72baa5f2dedac7378fa708c3 100644
---- a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-+++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
-@@ -1940,7 +1940,7 @@ public final class CollisionUtil {
-
- for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
- for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
-- final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks);
-+ final ChunkAccess chunk = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)world, currChunkX, currChunkZ) ? null : chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); // Folia - region threading
-
- if (chunk == null) {
- if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) {
-diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java
-index bd888ef719b9bfc93bace0b1d0fb771ac659f515..ba8b5a0ebe652bfaf5c1498c19d12a91a192bf8e 100644
---- a/io/papermc/paper/entity/activation/ActivationRange.java
-+++ b/io/papermc/paper/entity/activation/ActivationRange.java
-@@ -48,33 +48,34 @@ public final class ActivationRange {
-
- private static int checkInactiveWakeup(final Entity entity) {
- final Level world = entity.level();
-+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
- final SpigotWorldConfig config = world.spigotConfig;
-- final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
-+ final long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions
- if (entity.activationType == ActivationType.VILLAGER) {
-- if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) {
-- world.wakeupInactiveRemainingVillagers--;
-+ if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions
-+ worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions
- return config.wakeUpInactiveVillagersFor;
- }
- } else if (entity.activationType == ActivationType.ANIMAL) {
-- if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) {
-- world.wakeupInactiveRemainingAnimals--;
-+ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions
-+ worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions
- return config.wakeUpInactiveAnimalsFor;
- }
- } else if (entity.activationType == ActivationType.FLYING_MONSTER) {
-- if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) {
-- world.wakeupInactiveRemainingFlying--;
-+ if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions
-+ worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions
- return config.wakeUpInactiveFlyingFor;
- }
- } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) {
-- if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) {
-- world.wakeupInactiveRemainingMonsters--;
-+ if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions
-+ worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions
- return config.wakeUpInactiveMonstersFor;
- }
- }
- return -1;
- }
-
-- static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0);
-+ //static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0); // Folia - threaded regions - replaced by local variable
-
- /**
- * These entities are excluded from Activation range checks.
-@@ -122,10 +123,11 @@ public final class ActivationRange {
- final int waterActivationRange = world.spigotConfig.waterActivationRange;
- final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange;
- final int villagerActivationRange = world.spigotConfig.villagerActivationRange;
-- world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals);
-- world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers);
-- world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters);
-- world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying);
-+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
-+ worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Folia - threaded regions
-+ worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Folia - threaded regions
-+ worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Folia - threaded regions
-+ worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Folia - threaded regions
-
- int maxRange = Math.max(monsterActivationRange, animalActivationRange);
- maxRange = Math.max(maxRange, raiderActivationRange);
-@@ -135,30 +137,37 @@ public final class ActivationRange {
- maxRange = Math.max(maxRange, villagerActivationRange);
- maxRange = Math.min((world.spigotConfig.simulationDistance << 4) - 8, maxRange);
-
-- for (final Player player : world.players()) {
-- player.activatedTick = MinecraftServer.currentTick;
-+ for (final Player player : world.getLocalPlayers()) { // Folia - region threading
-+ player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading
- if (world.spigotConfig.ignoreSpectatorActivation && player.isSpectator()) {
- continue;
- }
-
- final int worldHeight = world.getHeight();
-- ActivationRange.maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange);
-- ActivationType.MISC.boundingBox = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange);
-- ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange);
-- ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange);
-- ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange);
-- ActivationType.WATER.boundingBox = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange);
-- ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange);
-- ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange);
-+ final AABB maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange); // Folia - threaded regions
-+ final AABB[] bbByType = new AABB[ActivationType.values().length]; // Folia - threaded regions
-+ bbByType[ActivationType.MISC.ordinal()] = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange); // Folia - threaded regions
-+ bbByType[ActivationType.RAIDER.ordinal()] = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange); // Folia - threaded regions
-+ bbByType[ActivationType.ANIMAL.ordinal()] = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange); // Folia - threaded regions
-+ bbByType[ActivationType.MONSTER.ordinal()] = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange); // Folia - threaded regions
-+ bbByType[ActivationType.WATER.ordinal()] = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange); // Folia - threaded regions
-+ bbByType[ActivationType.FLYING_MONSTER.ordinal()] = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange); // Folia - threaded regions
-+ bbByType[ActivationType.VILLAGER.ordinal()] = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange); // Folia - threaded regions
-
-- final java.util.List entities = world.getEntities((Entity) null, ActivationRange.maxBB, e -> true);
-+ final java.util.List entities = new java.util.ArrayList<>(); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later
-+ ((net.minecraft.server.level.ServerLevel)world).moonrise$getEntityLookup().getEntities((Entity)null, maxBB, entities, null); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later
- final boolean tickMarkers = world.paperConfig().entities.markers.tick;
- for (final Entity entity : entities) {
-+ // Folia start - region ticking
-+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) {
-+ continue;
-+ }
-+ // Folia end - region ticking
- if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) {
- continue;
- }
-
-- ActivationRange.activateEntity(entity);
-+ ActivationRange.activateEntity(entity, bbByType); // Folia - threaded regions
- }
- }
- }
-@@ -168,14 +177,14 @@ public final class ActivationRange {
- *
- * @param entity
- */
-- private static void activateEntity(final Entity entity) {
-- if (MinecraftServer.currentTick > entity.activatedTick) {
-+ private static void activateEntity(final Entity entity, final AABB[] bbByType) { // Folia - threaded regions
-+ if (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() > entity.activatedTick) { // Folia - threaded regions
- if (entity.defaultActivationState) {
-- entity.activatedTick = MinecraftServer.currentTick;
-+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
- return;
- }
-- if (entity.activationType.boundingBox.intersects(entity.getBoundingBox())) {
-- entity.activatedTick = MinecraftServer.currentTick;
-+ if (bbByType[entity.activationType.ordinal()].intersects(entity.getBoundingBox())) { // Folia - threaded regions
-+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
- }
- }
- }
-@@ -189,6 +198,7 @@ public final class ActivationRange {
- */
- public static int checkEntityImmunities(final Entity entity) { // return # of ticks to get immunity
- final SpigotWorldConfig config = entity.level().spigotConfig;
-+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = entity.level().getCurrentWorldData(); // Folia - threaded regions
- final int inactiveWakeUpImmunity = checkInactiveWakeup(entity);
- if (inactiveWakeUpImmunity > -1) {
- return inactiveWakeUpImmunity;
-@@ -196,10 +206,10 @@ public final class ActivationRange {
- if (entity.getRemainingFireTicks() > 0) {
- return 2;
- }
-- if (entity.activatedImmunityTick >= MinecraftServer.currentTick) {
-+ if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - threaded regions
- return 1;
- }
-- final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
-+ final long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions
- if ((entity.activationType != ActivationType.WATER && entity.isInWater() && entity.isPushedByFluid())) {
- return 100;
- }
-@@ -296,16 +306,16 @@ public final class ActivationRange {
- return true;
- }
-
-- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick;
-+ boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
- entity.isTemporarilyActive = false;
-
- // Should this entity tick?
- if (!isActive) {
-- if ((MinecraftServer.currentTick - entity.activatedTick - 1) % 20 == 0) {
-+ if ((io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick - 1) % 20 == 0) { // Folia - threaded regions
- // Check immunities every 20 ticks.
- final int immunity = checkEntityImmunities(entity);
- if (immunity >= 0) {
-- entity.activatedTick = MinecraftServer.currentTick + immunity;
-+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + immunity; // Folia - threaded regions
- } else {
- entity.isTemporarilyActive = true;
- }
-diff --git a/io/papermc/paper/redstone/RedstoneWireTurbo.java b/io/papermc/paper/redstone/RedstoneWireTurbo.java
-index ff747a1ecdf3c888bca0d69de4f85dcd810b6139..5a76c93eada8db35b1ddbb562ccfbd2f0d35f0ca 100644
---- a/io/papermc/paper/redstone/RedstoneWireTurbo.java
-+++ b/io/papermc/paper/redstone/RedstoneWireTurbo.java
-@@ -829,14 +829,14 @@ public final class RedstoneWireTurbo {
- j = getMaxCurrentStrength(upd, j);
- int l = 0;
-
-- wire.shouldSignal = false;
-+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading
- // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated,
- // and I'm not ready to try to replicate even more functionality from
- // elsewhere in Minecraft into this accelerator. So sadly, we must
- // suffer the performance hit of this very expensive call. If there
- // is consistency to what this call returns, we may be able to cache it.
- final int k = worldIn.getBestNeighborSignal(upd.self);
-- wire.shouldSignal = true;
-+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading
-
- // The variable 'k' holds the maximum redstone power value of any adjacent blocks.
- // If 'k' has the highest level of all neighbors, then the power level of this
-diff --git a/io/papermc/paper/threadedregions/RegionShutdownThread.java b/io/papermc/paper/threadedregions/RegionShutdownThread.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..261b3019878c31a9e44e56b6611899de6c00ebee
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/RegionShutdownThread.java
-@@ -0,0 +1,226 @@
-+package io.papermc.paper.threadedregions;
-+
-+import ca.spottedleaf.moonrise.common.util.WorldUtil;
-+import com.mojang.logging.LogUtils;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.item.ItemStack;
-+import net.minecraft.world.level.ChunkPos;
-+import org.bukkit.event.inventory.InventoryCloseEvent;
-+import org.slf4j.Logger;
-+import java.util.ArrayList;
-+import java.util.List;
-+import java.util.concurrent.TimeUnit;
-+
-+public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.util.TickThread {
-+
-+ private static final Logger LOGGER = LogUtils.getClassLogger();
-+
-+ ThreadedRegionizer.ThreadedRegion shuttingDown;
-+
-+ public RegionShutdownThread(final String name) {
-+ super(name);
-+ this.setUncaughtExceptionHandler((thread, thr) -> {
-+ LOGGER.error("Error shutting down server", thr);
-+ });
-+ }
-+
-+ static ThreadedRegionizer.ThreadedRegion getRegion() {
-+ final Thread currentThread = Thread.currentThread();
-+ if (currentThread instanceof RegionShutdownThread shutdownThread) {
-+ return shutdownThread.shuttingDown;
-+ }
-+ return null;
-+ }
-+
-+
-+ static RegionizedWorldData getWorldData() {
-+ final Thread currentThread = Thread.currentThread();
-+ if (currentThread instanceof RegionShutdownThread shutdownThread) {
-+ // no fast path for shutting down
-+ if (shutdownThread.shuttingDown != null) {
-+ return shutdownThread.shuttingDown.getData().world.worldRegionData.get();
-+ }
-+ }
-+ return null;
-+ }
-+
-+ // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves
-+ // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because
-+ // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not.
-+ // Thus, the only operation that the shutdown thread will perform
-+
-+ private void saveLevelData(final ServerLevel world) {
-+ try {
-+ world.saveLevelData(true);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr);
-+ }
-+ }
-+
-+ private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region,
-+ final ServerLevel world) {
-+ try {
-+ this.shuttingDown = region;
-+ final List pendingTeleports = world.removeAllRegionTeleports();
-+ if (pendingTeleports.isEmpty()) {
-+ return;
-+ }
-+ final ChunkPos center = region.getCenterChunk();
-+ LOGGER.info("Completing " + pendingTeleports.size() + " pending teleports in region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'");
-+ for (final ServerLevel.PendingTeleport pendingTeleport : pendingTeleports) {
-+ LOGGER.info("Completing teleportation to target position " + pendingTeleport.to());
-+
-+ // first, add entities to entity chunk so that they will be saved
-+ for (final Entity.EntityTreeNode node : pendingTeleport.rootVehicle().getFullTree()) {
-+ // assume that world and position are set to destination here
-+ node.root.setLevel(world); // in case the pending teleport is from a portal before it finds the exact destination
-+ world.moonrise$getEntityLookup().addEntityForShutdownTeleportComplete(node.root);
-+ }
-+
-+ // then, rebuild the passenger tree so that when saving only the root vehicle will be written - and if
-+ // there are any player passengers, that the later player saving will save the tree
-+ pendingTeleport.rootVehicle().restore();
-+
-+ // now we are finished
-+ LOGGER.info("Completed teleportation to target position " + pendingTeleport.to());
-+ }
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to complete pending teleports", thr);
-+ } finally {
-+ this.shuttingDown = null;
-+ }
-+ }
-+
-+ private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region,
-+ final boolean last) {
-+ ChunkPos center = null;
-+ try {
-+ this.shuttingDown = region;
-+ center = region.getCenterChunk();
-+ LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'");
-+ region.regioniser.world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(true, true, false, last, false);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr);
-+ } finally {
-+ this.shuttingDown = null;
-+ }
-+ }
-+
-+ private void haltChunkSystem(final ServerLevel world) {
-+ try {
-+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(false, true, true, false, false);
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to halt chunk system for world '" + world.getWorld().getName() + "'", thr);
-+ }
-+ }
-+
-+ private void closePlayerInventories(final ThreadedRegionizer.ThreadedRegion region) {
-+ ChunkPos center = null;
-+ try {
-+ this.shuttingDown = region;
-+ center = region.getCenterChunk();
-+
-+ final RegionizedWorldData worldData = region.regioniser.world.worldRegionData.get();
-+
-+ for (final ServerPlayer player : worldData.getLocalPlayers()) {
-+ try {
-+ // close inventory
-+ if (player.containerMenu != player.inventoryMenu) {
-+ player.closeContainer(InventoryCloseEvent.Reason.DISCONNECT);
-+ }
-+
-+ // drop carried item
-+ if (!player.containerMenu.getCarried().isEmpty()) {
-+ ItemStack carried = player.containerMenu.getCarried();
-+ player.containerMenu.setCarried(ItemStack.EMPTY);
-+ player.drop(carried, false);
-+ }
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to close player inventory for player: " + player, thr);
-+ }
-+ }
-+ } catch (final Throwable thr) {
-+ LOGGER.error("Failed to close player inventories for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr);
-+ } finally {
-+ this.shuttingDown = null;
-+ }
-+ }
-+
-+ @Override
-+ public final void run() {
-+ // await scheduler termination
-+ LOGGER.info("Awaiting scheduler termination for 60s...");
-+ if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) {
-+ LOGGER.info("Scheduler halted");
-+ } else {
-+ LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways");
-+ TickRegions.getScheduler().dumpAliveThreadTraces("Did not shut down in time");
-+ }
-+
-+ MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc
-+ // halt all chunk systems first so that any in-progress chunk generation stops
-+ LOGGER.info("Halting chunk systems...");
-+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
-+ try {
-+ world.moonrise$getChunkTaskScheduler().halt(false, 0L);
-+ } catch (final Throwable throwable) {
-+ LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable);
-+ }
-+ }
-+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
-+ this.haltChunkSystem(world);
-+ }
-+ LOGGER.info("Halted chunk systems");
-+
-+ LOGGER.info("Finishing pending teleports...");
-+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
-+ final List>
-+ regions = new ArrayList<>();
-+ world.regioniser.computeForAllRegionsUnsynchronised(regions::add);
-+
-+ for (int i = 0, len = regions.size(); i < len; ++i) {
-+ this.finishTeleportations(regions.get(i), world);
-+ }
-+ }
-+ LOGGER.info("Finished pending teleports");
-+
-+ LOGGER.info("Saving all worlds");
-+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
-+ LOGGER.info("Saving world data for world '" + WorldUtil.getWorldName(world) + "'");
-+
-+ final List>
-+ regions = new ArrayList<>();
-+ world.regioniser.computeForAllRegionsUnsynchronised(regions::add);
-+
-+ LOGGER.info("Closing player inventories...");
-+ for (int i = 0, len = regions.size(); i < len; ++i) {
-+ this.closePlayerInventories(regions.get(i));
-+ }
-+ LOGGER.info("Closed player inventories");
-+
-+ LOGGER.info("Saving chunks...");
-+ for (int i = 0, len = regions.size(); i < len; ++i) {
-+ this.saveRegionChunks(regions.get(i), (i + 1) == len);
-+ }
-+ LOGGER.info("Saved chunks");
-+
-+ LOGGER.info("Saving level data...");
-+ this.saveLevelData(world);
-+ LOGGER.info("Saved level data");
-+
-+ LOGGER.info("Saved world data for world '" + WorldUtil.getWorldName(world) + "'");
-+ }
-+ LOGGER.info("Saved all worlds");
-+
-+ // Note: only save after world data and pending teleportations
-+ LOGGER.info("Saving all player data...");
-+ MinecraftServer.getServer().getPlayerList().saveAll();
-+ LOGGER.info("Saved all player data");
-+
-+ MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc)
-+ // done, part 2 should call exit()
-+ }
-+}
-diff --git a/io/papermc/paper/threadedregions/RegionizedData.java b/io/papermc/paper/threadedregions/RegionizedData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..1f48ada99d6d24880f9bda1cd05d41a4562e42f5
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/RegionizedData.java
-@@ -0,0 +1,235 @@
-+package io.papermc.paper.threadedregions;
-+
-+import ca.spottedleaf.concurrentutil.util.Validate;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
-+import net.minecraft.server.level.ServerLevel;
-+import javax.annotation.Nullable;
-+import java.util.function.Supplier;
-+
-+/**
-+ * Use to manage data that needs to be regionised.
-+ *
-+ * Note: that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionizedData} object is GC'd.
-+ * The data is held in reference to the world it resides in.
-+ *
-+ *
-+ * Note: Keep in mind that when regionised ticking is disabled, the entire server is considered a single region.
-+ * That is, the data may or may not cross worlds. As such, the {@code RegionizedData} object must be instanced
-+ * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions.
-+ * See below for more details on instancing per world.
-+ *
-+ *
-+ * Regionised data may be world-checked. That is, {@link #get()} may throw an exception if the current
-+ * region's world does not match the {@code RegionizedData}'s world. Consider the usages of {@code RegionizedData} below
-+ * see why the behavior may or may not be desirable:
-+ *
-+ * {@code
-+ * public class EntityTickList {
-+ * private final List entities = new ArrayList<>();
-+ *
-+ * public void addEntity(Entity e) {
-+ * this.entities.add(e);
-+ * }
-+ *
-+ * public void removeEntity(Entity e) {
-+ * this.entities.remove(e);
-+ * }
-+ * }
-+ *
-+ * public class World {
-+ *
-+ * // callback is left out of this example
-+ * // note: world != null here
-+ * public final RegionizedData entityTickLists =
-+ * new RegionizedData<>(this, () -> new EntityTickList(), ...);
-+ *
-+ * public void addTickingEntity(Entity e) {
-+ * // What we expect here is that this world is the
-+ * // current ticking region's world.
-+ * // If that is true, then calling this.entityTickLists.get()
-+ * // will retrieve the current region's EntityTickList
-+ * // for this world, which is fine since the current
-+ * // region is contained within this world.
-+ *
-+ * // But if the current region's world is not this world,
-+ * // and if the world check is disabled, then we will actually
-+ * // retrieve _this_ world's EntityTickList for the region,
-+ * // and NOT the EntityTickList for the region's world.
-+ * // This is because the RegionizedData object is instantiated
-+ * // per world.
-+ * this.entityTickLists.get().addEntity(e);
-+ * }
-+ * }
-+ *
-+ * public class TickTimes {
-+ *
-+ * private final List tickTimesNS = new ArrayList<>();
-+ *
-+ * public void completeTick(long timeNS) {
-+ * this.tickTimesNS.add(timeNS);
-+ * }
-+ *
-+ * public double getAverageTickLengthMS() {
-+ * double sum = 0.0;
-+ * for (long time : tickTimesNS) {
-+ * sum += (double)time;
-+ * }
-+ * return (sum / this.tickTimesNS.size()) / 1.0E6; // 1ms = 1 million ns
-+ * }
-+ * }
-+ *
-+ * public class Server {
-+ * public final List worlds = ...;
-+ *
-+ * // callback is left out of this example
-+ * // note: world == null here, because this RegionizedData object
-+ * // is not instantiated per world, but rather globally.
-+ * public final RegionizedData tickTimes =
-+ * new RegionizedData<>(null, () -> new TickTimes(), ...);
-+ * }
-+ * }
-+ *
-+ * In general, it is advised that if a RegionizedData object is instantiated per world, that world checking
-+ * is enabled for it by passing the world to the constructor.
-+ *
-+ */
-+public final class RegionizedData {
-+
-+ private final ServerLevel world;
-+ private final Supplier initialValueSupplier;
-+ private final RegioniserCallback callback;
-+
-+ /**
-+ * Creates a regionised data holder. The provided initial value supplier may not be null, and it must
-+ * never produce {@code null} values.
-+ *
-+ * Note that the supplier or regioniser callback may be used while the region lock is held, so any blocking
-+ * operations may deadlock the entire server and as such the function should be completely non-blocking
-+ * and must complete in a timely manner.
-+ *
-+ *
-+ * If the provided world is {@code null}, then the world checks are disabled. The world should only ever
-+ * be {@code null} if the data is specifically not specific to worlds. For example, using {@code null}
-+ * for an entity tick list is invalid since the entities are tied to a world and region,
-+ * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to
-+ * region only.
-+ *
-+ * @param world The world in which the region data resides.
-+ * @param supplier Initial value supplier used to lazy initialise region data.
-+ * @param callback Region callback to manage this regionised data.
-+ */
-+ public RegionizedData(final ServerLevel world, final Supplier supplier, final RegioniserCallback callback) {
-+ this.world = world;
-+ this.initialValueSupplier = Validate.notNull(supplier, "Supplier may not be null.");
-+ this.callback = Validate.notNull(callback, "Regioniser callback may not be null.");
-+ }
-+
-+ T createNewValue() {
-+ return Validate.notNull(this.initialValueSupplier.get(), "Initial value supplier may not return null");
-+ }
-+
-+ RegioniserCallback getCallback() {
-+ return this.callback;
-+ }
-+
-+ /**
-+ * Returns the current data type for the current ticking region. If there is no region, returns {@code null}.
-+ * @return the current data type for the current ticking region. If there is no region, returns {@code null}.
-+ * @throws IllegalStateException If the following are true: The server is in region ticking mode,
-+ * this {@code RegionizedData}'s world is not {@code null},
-+ * and the current ticking region's world does not match this {@code RegionizedData}'s world.
-+ */
-+ public @Nullable T get() {
-+ final ThreadedRegionizer.ThreadedRegion region =
-+ TickRegionScheduler.getCurrentRegion();
-+
-+ if (region == null) {
-+ return null;
-+ }
-+
-+ if (this.world != null && this.world != region.getData().world) {
-+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
-+ }
-+
-+ return region.getData().getOrCreateRegionizedData(this);
-+ }
-+
-+ /**
-+ * Class responsible for handling merge / split requests from the regioniser.
-+ *
-+ * It is critical to note that each function is called while holding the region lock.
-+ *
-+ */
-+ public static interface RegioniserCallback {
-+
-+ /**
-+ * Completely merges the data in {@code from} to {@code into}.
-+ *
-+ * Calculating Tick Offsets:
-+ * Sometimes data stores absolute tick deadlines, and since regions tick independently, absolute deadlines
-+ * are not comparable across regions. Consider absolute deadlines {@code deadlineFrom, deadlineTo} in
-+ * regions {@code from} and {@code into} respectively. We can calculate the relative deadline for the from
-+ * region with {@code relFrom = deadlineFrom - currentTickFrom}. Then, we can use the same equation for
-+ * computing the absolute deadline in region {@code into} that has the same relative deadline as {@code from}
-+ * as {@code deadlineTo = relFrom + currentTickTo}. By substituting {@code relFrom} as {@code deadlineFrom - currentTickFrom},
-+ * we finally have that {@code deadlineTo = deadlineFrom + (currentTickTo - currentTickFrom)} and
-+ * that we can use an offset {@code fromTickOffset = currentTickTo - currentTickFrom} to calculate
-+ * {@code deadlineTo} as {@code deadlineTo = deadlineFrom + fromTickOffset}.
-+ *
-+ *
-+ * Critical Notes:
-+ *
-+ *
-+ * This function is called while the region lock is held, so any blocking operations may
-+ * deadlock the entire server and as such the function should be completely non-blocking and must complete
-+ * in a timely manner.
-+ *
-+ *
-+ * This function may not throw any exceptions, or the server will be left in an unrecoverable state.
-+ *
-+ *
-+ *
-+ *
-+ * @param from The data to merge from.
-+ * @param into The data to merge into.
-+ * @param fromTickOffset The addend to absolute tick deadlines stored in the {@code from} region to adjust to the into region.
-+ */
-+ public void merge(final T from, final T into, final long fromTickOffset);
-+
-+ /**
-+ * Splits the data in {@code from} into {@code dataSet}.
-+ *
-+ * The chunk coordinate to region section coordinate bit shift amount is provided in {@code chunkToRegionShift}.
-+ * To convert from chunk coordinates to region coordinates and keys, see the code below:
-+ *
-+ * {@code
-+ * int chunkX = ...;
-+ * int chunkZ = ...;
-+ *
-+ * int regionSectionX = chunkX >> chunkToRegionShift;
-+ * int regionSectionZ = chunkZ >> chunkToRegionShift;
-+ * long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
-+ * }
-+ *
-+ *
-+ *
-+ * The {@code regionToData} hashtable provides a lookup from {@code regionSectionKey} (see above) to the
-+ * data that is owned by the region which occupies the region section.
-+ *
-+ *
-+ * Unlike {@link #merge(Object, Object, long)}, there is no absolute tick offset provided. This is because
-+ * the new regions formed from the split will start at the same tick number, and so no adjustment is required.
-+ *
-+ *
-+ * @param from The data to split from.
-+ * @param chunkToRegionShift The signed right-shift value used to convert chunk coordinates into region section coordinates.
-+ * @param regionToData Lookup hash table from region section key to .
-+ * @param dataSet The data set to split into.
-+ */
-+ public void split(
-+ final T from, final int chunkToRegionShift,
-+ final Long2ReferenceOpenHashMap regionToData, final ReferenceOpenHashSet dataSet
-+ );
-+ }
-+}
-diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..fc053ded0c14b76a1c6c82b59d3fd320372a3293
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/RegionizedServer.java
-@@ -0,0 +1,455 @@
-+package io.papermc.paper.threadedregions;
-+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
-+import ca.spottedleaf.moonrise.common.util.TickThread;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler;
-+import net.minecraft.CrashReport;
-+import net.minecraft.ReportedException;
-+import net.minecraft.network.Connection;
-+import net.minecraft.network.PacketListener;
-+import net.minecraft.network.PacketSendListener;
-+import net.minecraft.network.chat.Component;
-+import net.minecraft.network.chat.MutableComponent;
-+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.dedicated.DedicatedServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.network.ServerGamePacketListenerImpl;
-+import net.minecraft.world.level.GameRules;
-+import org.bukkit.Bukkit;
-+import org.slf4j.Logger;
-+import java.util.ArrayList;
-+import java.util.Collections;
-+import java.util.List;
-+import java.util.concurrent.CopyOnWriteArrayList;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.function.BooleanSupplier;
-+
-+public final class RegionizedServer {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+ private static final RegionizedServer INSTANCE = new RegionizedServer();
-+
-+ public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue();
-+
-+ private final CopyOnWriteArrayList worlds = new CopyOnWriteArrayList<>();
-+ private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>();
-+
-+ private final MultiThreadedQueue globalTickQueue = new MultiThreadedQueue<>();
-+
-+ private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this);
-+
-+ public static RegionizedServer getInstance() {
-+ return INSTANCE;
-+ }
-+
-+ public void addConnection(final Connection conn) {
-+ this.connections.add(conn);
-+ }
-+
-+ public boolean removeConnection(final Connection conn) {
-+ return this.connections.remove(conn);
-+ }
-+
-+ public void addWorld(final ServerLevel world) {
-+ this.worlds.add(world);
-+ }
-+
-+ public void init() {
-+ // call init event _before_ scheduling anything
-+ new RegionizedServerInitEvent().callEvent();
-+
-+ // now we can schedule
-+ this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS);
-+ TickRegions.getScheduler().scheduleRegion(this.tickHandle);
-+ TickRegions.getScheduler().init();
-+ }
-+
-+ public void invalidateStatus() {
-+ this.lastServerStatus = 0L;
-+ }
-+
-+ public void addTaskWithoutNotify(final Runnable run) {
-+ this.globalTickQueue.add(run);
-+ }
-+
-+ public void addTask(final Runnable run) {
-+ this.addTaskWithoutNotify(run);
-+ TickRegions.getScheduler().setHasTasks(this.tickHandle);
-+ }
-+
-+ /**
-+ * Returns the current tick of the region ticking.
-+ * @throws IllegalStateException If there is no current region.
-+ */
-+ public static long getCurrentTick() throws IllegalStateException {
-+ final ThreadedRegionizer.ThreadedRegion region =
-+ TickRegionScheduler.getCurrentRegion();
-+ if (region == null) {
-+ if (TickThread.isShutdownThread()) {
-+ return 0L;
-+ }
-+ throw new IllegalStateException("No currently ticking region");
-+ }
-+ return region.getData().getCurrentTick();
-+ }
-+
-+ public static boolean isGlobalTickThread() {
-+ return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask();
-+ }
-+
-+ public static void ensureGlobalTickThread(final String reason) {
-+ if (!isGlobalTickThread()) {
-+ throw new IllegalStateException(reason);
-+ }
-+ }
-+
-+ public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() {
-+ return INSTANCE.tickHandle;
-+ }
-+
-+ private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle {
-+
-+ private final RegionizedServer server;
-+
-+ private final AtomicBoolean scheduled = new AtomicBoolean();
-+ private final AtomicBoolean ticking = new AtomicBoolean();
-+
-+ public GlobalTickTickHandle(final RegionizedServer server) {
-+ super(null, SchedulerThreadPool.DEADLINE_NOT_SET);
-+ this.server = server;
-+ }
-+
-+ /**
-+ * Only valid to call BEFORE scheduled!!!!
-+ */
-+ final void setInitialStart(final long start) {
-+ if (this.scheduled.getAndSet(true)) {
-+ throw new IllegalStateException("Double scheduling global tick");
-+ }
-+ this.updateScheduledStart(start);
-+ }
-+
-+ @Override
-+ protected boolean tryMarkTicking() {
-+ return !this.ticking.getAndSet(true);
-+ }
-+
-+ @Override
-+ protected boolean markNotTicking() {
-+ return this.ticking.getAndSet(false);
-+ }
-+
-+ @Override
-+ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) {
-+ this.drainTasks();
-+ this.server.globalTick(tickCount);
-+ }
-+
-+ private void drainTasks() {
-+ while (this.runOneTask());
-+ }
-+
-+ private boolean runOneTask() {
-+ final Runnable run = this.server.globalTickQueue.poll();
-+ if (run == null) {
-+ return false;
-+ }
-+
-+ // TODO try catch?
-+ run.run();
-+
-+ return true;
-+ }
-+
-+ @Override
-+ protected boolean runRegionTasks(final BooleanSupplier canContinue) {
-+ do {
-+ if (!this.runOneTask()) {
-+ return false;
-+ }
-+ } while (canContinue.getAsBoolean());
-+
-+ return true;
-+ }
-+
-+ @Override
-+ protected boolean hasIntermediateTasks() {
-+ return !this.server.globalTickQueue.isEmpty();
-+ }
-+ }
-+
-+ private long lastServerStatus;
-+ private long tickCount;
-+
-+ /*
-+ private final java.util.Random random = new java.util.Random(4L);
-+ private final List> walkers =
-+ new java.util.ArrayList<>();
-+ static final int PLAYERS = 500;
-+ static final int RAD_BLOCKS = 1000;
-+ static final int RAD = RAD_BLOCKS >> 4;
-+ static final int RAD_BIG_BLOCKS = 100_000;
-+ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4;
-+ static final int VD = 4 + 12;
-+ static final int BIG_PLAYERS = 250;
-+ static final double WALK_CHANCE = 0.3;
-+ static final double TP_CHANCE = 0.2;
-+ static final double TASK_CHANCE = 0.2;
-+
-+ private ServerLevel getWorld() {
-+ return this.worlds.get(0);
-+ }
-+
-+ private void init2() {
-+ for (int i = 0; i < PLAYERS; ++i) {
-+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
-+ int posX = this.random.nextInt(-rad, rad + 1);
-+ int posZ = this.random.nextInt(-rad, rad + 1);
-+
-+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) {
-+ @Override
-+ protected void addCallback(Void parameter, int chunkX, int chunkZ) {
-+ ServerLevel world = RegionizedServer.this.getWorld();
-+ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) {
-+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {
-+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {});
-+ });
-+ }
-+ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel(
-+ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ)
-+ );
-+ }
-+
-+ @Override
-+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) {
-+ ServerLevel world = RegionizedServer.this.getWorld();
-+ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) {
-+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {
-+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {});
-+ });
-+ }
-+ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel(
-+ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ)
-+ );
-+ }
-+ };
-+
-+ map.add(posX, posZ, VD);
-+
-+ walkers.add(map);
-+ }
-+ }
-+
-+ private void randomWalk() {
-+ if (this.walkers.isEmpty()) {
-+ this.init2();
-+ return;
-+ }
-+
-+ for (int i = 0; i < PLAYERS; ++i) {
-+ if (this.random.nextDouble() > WALK_CHANCE) {
-+ continue;
-+ }
-+
-+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = this.walkers.get(i);
-+
-+ int updateX = this.random.nextInt(-1, 2);
-+ int updateZ = this.random.nextInt(-1, 2);
-+
-+ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD);
-+ }
-+
-+ for (int i = 0; i < PLAYERS; ++i) {
-+ if (random.nextDouble() >= TP_CHANCE) {
-+ continue;
-+ }
-+
-+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
-+ int posX = random.nextInt(-rad, rad + 1);
-+ int posZ = random.nextInt(-rad, rad + 1);
-+
-+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i);
-+
-+ map.update(posX, posZ, VD);
-+ }
-+ }
-+ */
-+
-+ private void globalTick(final int tickCount) {
-+ /*
-+ if (false) {
-+ io.papermc.paper.threadedregions.ThreadedTicketLevelPropagator.main(null);
-+ }
-+ this.randomWalk();
-+ */
-+ ++this.tickCount;
-+ // expire invalid click command callbacks
-+ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount);
-+
-+ // scheduler
-+ ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick();
-+
-+ // commands
-+ ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs();
-+
-+ // needs
-+ // player ping sample
-+ // world global tick
-+ // connection tick
-+
-+ // tick player ping sample
-+ this.tickPlayerSample();
-+
-+ // tick worlds
-+ for (final ServerLevel world : this.worlds) {
-+ this.globalTick(world, tickCount);
-+ }
-+
-+ // tick connections
-+ this.tickConnections();
-+
-+ // player list
-+ MinecraftServer.getServer().getPlayerList().tick();
-+ }
-+
-+ private void tickPlayerSample() {
-+ final MinecraftServer mcServer = MinecraftServer.getServer();
-+
-+ final long currtime = System.nanoTime();
-+
-+ // player ping sample
-+ // copied from MinecraftServer#tickServer
-+ // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing
-+ // an uncomplete status
-+ if (currtime - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) {
-+ this.lastServerStatus = currtime;
-+ mcServer.rebuildServerStatus();
-+ }
-+ }
-+
-+ public static boolean isNotOwnedByGlobalRegion(final Connection conn) {
-+ final PacketListener packetListener = conn.getPacketListener();
-+
-+ if (packetListener instanceof ServerGamePacketListenerImpl gamePacketListener) {
-+ return !gamePacketListener.waitingForSwitchToConfig;
-+ }
-+
-+ if (conn.getPacketListener() instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) {
-+ return configurationPacketListener.switchToMain;
-+ }
-+
-+ return false;
-+ }
-+
-+ private void tickConnections() {
-+ final List connections = new ArrayList<>(this.connections);
-+ Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging
-+ for (final Connection conn : connections) {
-+ if (!conn.becomeActive()) {
-+ continue;
-+ }
-+
-+ if (isNotOwnedByGlobalRegion(conn)) {
-+ // we actually require that the owning regions remove the connection for us, as it is possible
-+ // that ownership is transferred back to us
-+ continue;
-+ }
-+
-+ if (!conn.isConnected()) {
-+ this.removeConnection(conn);
-+ conn.handleDisconnection();
-+ continue;
-+ }
-+
-+ try {
-+ conn.tick();
-+ } catch (final Exception exception) {
-+ if (conn.isMemoryConnection()) {
-+ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection"));
-+ }
-+
-+ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception);
-+ MutableComponent ichatmutablecomponent = Component.literal("Internal server error");
-+
-+ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> {
-+ conn.disconnect(ichatmutablecomponent);
-+ }));
-+ conn.setReadOnly();
-+ continue;
-+ }
-+ }
-+ }
-+
-+ // A global tick only updates things like weather / worldborder, basically anything in the world that is
-+ // NOT tied to a specific region, but rather shared amongst all of them.
-+ private void globalTick(final ServerLevel world, final int tickCount) {
-+ // needs
-+ // worldborder tick
-+ // advancing the weather cycle
-+ // sleep status thing
-+ // updating sky brightness
-+ // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking
-+
-+ // Typically, we expect there to be a running region to drain a world's global chunk tasks. However,
-+ // this may not be the case - and thus, only the global tick thread can do anything.
-+ world.taskQueueRegionData.drainGlobalChunkTasks();
-+
-+ // worldborder tick
-+ this.tickWorldBorder(world);
-+
-+ // weather cycle
-+ this.advanceWeatherCycle(world);
-+
-+ // sleep status
-+ this.checkNightSkip(world);
-+
-+ // update raids
-+ this.updateRaids(world);
-+
-+ // sky brightness
-+ this.updateSkyBrightness(world);
-+
-+ // time ticking (TODO API synchronisation?)
-+ this.tickTime(world, tickCount);
-+
-+ world.updateTickData();
-+
-+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates
-+ }
-+
-+ private void updateRaids(final ServerLevel world) {
-+ world.getRaids().globalTick();
-+ }
-+
-+ private void checkNightSkip(final ServerLevel world) {
-+ world.tickSleep();
-+ }
-+
-+ private void advanceWeatherCycle(final ServerLevel world) {
-+ world.advanceWeatherCycle();
-+ }
-+
-+ private void updateSkyBrightness(final ServerLevel world) {
-+ world.updateSkyBrightness();
-+ }
-+
-+ private void tickWorldBorder(final ServerLevel world) {
-+ world.getWorldBorder().tick();
-+ }
-+
-+ private void tickTime(final ServerLevel world, final int tickCount) {
-+ if (world.tickTime) {
-+ if (world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
-+ world.setDayTime(world.levelData.getDayTime() + (long)tickCount);
-+ }
-+ world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount);
-+ }
-+ }
-+
-+ public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) {
-+
-+ }
-+}
-diff --git a/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..a2313b5b4c37e8536973a8ea0b371557ea912473
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
-@@ -0,0 +1,807 @@
-+package io.papermc.paper.threadedregions;
-+
-+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
-+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
-+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.concurrentutil.util.Priority;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.TicketType;
-+import net.minecraft.util.Unit;
-+import java.lang.invoke.VarHandle;
-+import java.util.ArrayDeque;
-+import java.util.Iterator;
-+import java.util.concurrent.atomic.AtomicLong;
-+
-+public final class RegionizedTaskQueue {
-+
-+ private static final TicketType TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0);
-+
-+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Runnable run) {
-+ return this.createChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Runnable run, final Priority priority) {
-+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Runnable run) {
-+ return this.createTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Runnable run, final Priority priority) {
-+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Runnable run) {
-+ return this.queueChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Runnable run, final Priority priority) {
-+ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority);
-+
-+ ret.queue();
-+
-+ return ret;
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Runnable run) {
-+ return this.queueTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL);
-+ }
-+
-+ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
-+ final Runnable run, final Priority priority) {
-+ final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority);
-+
-+ ret.queue();
-+
-+ return ret;
-+ }
-+
-+ public static final class WorldRegionTaskData {
-+ private final ServerLevel world;
-+ private final MultiThreadedQueue globalChunkTask = new MultiThreadedQueue<>();
-+ private final ConcurrentLong2ReferenceChainedHashTable referenceCounters = new ConcurrentLong2ReferenceChainedHashTable<>();
-+
-+ public WorldRegionTaskData(final ServerLevel world) {
-+ this.world = world;
-+ }
-+
-+ private boolean executeGlobalChunkTask() {
-+ final Runnable run = this.globalChunkTask.poll();
-+ if (run != null) {
-+ run.run();
-+ return true;
-+ }
-+ return false;
-+ }
-+
-+ public void drainGlobalChunkTasks() {
-+ while (this.executeGlobalChunkTask());
-+ }
-+
-+ public void pushGlobalChunkTask(final Runnable run) {
-+ this.globalChunkTask.add(run);
-+ }
-+
-+ private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) {
-+ final ThreadedRegionizer regioniser = this.world.regioniser;
-+ final ThreadedRegionizer.ThreadedRegion region
-+ = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ);
-+ if (region == null) {
-+ return null;
-+ }
-+ final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData();
-+ return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue);
-+ }
-+
-+ private void removeTicket(final long coord) {
-+ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel(
-+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
-+ );
-+ }
-+
-+ private void addTicket(final long coord) {
-+ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel(
-+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
-+ );
-+ }
-+
-+ private void processTicketUpdates(final long coord) {
-+ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(CoordinateUtils.getChunkX(coord), CoordinateUtils.getChunkZ(coord));
-+ }
-+
-+ // note: only call on acquired referenceCountData
-+ private void ensureTicketAdded(final long coord, final ReferenceCountData referenceCountData) {
-+ if (!referenceCountData.addedTicket) {
-+ // fine if multiple threads do this, no removeTicket may be called for this coord due to reference count inc
-+ this.addTicket(coord);
-+ this.processTicketUpdates(coord);
-+ referenceCountData.addedTicket = true;
-+ }
-+ }
-+
-+ private void decrementReference(final ReferenceCountData referenceCountData, final long coord) {
-+ if (!referenceCountData.decreaseReferenceCount()) {
-+ return;
-+ } // else: need to remove ticket
-+
-+ // note: it is possible that another thread increments and then removes the reference before we can, so
-+ // use ifPresent
-+ this.referenceCounters.computeIfPresent(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> {
-+ if (valueInMap.referenceCount.get() != 0L) {
-+ return valueInMap;
-+ }
-+
-+ // note: valueInMap may not be referenceCountData
-+
-+ // possible to invoke this outside of the compute call, but not required and requires additional logic
-+ WorldRegionTaskData.this.removeTicket(keyInMap);
-+
-+ return null;
-+ });
-+ }
-+
-+ private ReferenceCountData incrementReference(final long coord) {
-+ ReferenceCountData referenceCountData = this.referenceCounters.get(coord);
-+
-+ if (referenceCountData != null && referenceCountData.addCount()) {
-+ this.ensureTicketAdded(coord, referenceCountData);
-+ return referenceCountData;
-+ }
-+
-+ referenceCountData = this.referenceCounters.compute(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> {
-+ if (valueInMap == null) {
-+ // sets reference count to 1
-+ return new ReferenceCountData();
-+ }
-+ // OK if we add from 0, the remove call will use compute() and catch this race condition
-+ valueInMap.referenceCount.getAndIncrement();
-+
-+ return valueInMap;
-+ });
-+
-+ this.ensureTicketAdded(coord, referenceCountData);
-+
-+ return referenceCountData;
-+ }
-+ }
-+
-+ private static final class ReferenceCountData {
-+
-+ public final AtomicLong referenceCount = new AtomicLong(1L);
-+ public volatile boolean addedTicket;
-+
-+ // returns false if reference count is 0, otherwise increments ref count
-+ public boolean addCount() {
-+ int failures = 0;
-+ for (long curr = this.referenceCount.get();;) {
-+ for (int i = 0; i < failures; ++i) {
-+ Thread.onSpinWait();
-+ }
-+
-+ if (curr == 0L) {
-+ return false;
-+ }
-+
-+ if (curr == (curr = this.referenceCount.compareAndExchange(curr, curr + 1L))) {
-+ return true;
-+ }
-+
-+ ++failures;
-+ }
-+ }
-+
-+ // returns true if new reference count is 0
-+ public boolean decreaseReferenceCount() {
-+ final long res = this.referenceCount.decrementAndGet();
-+ if (res >= 0L) {
-+ return res == 0L;
-+ } else {
-+ throw new IllegalStateException("Negative reference count");
-+ }
-+ }
-+ }
-+
-+ public static final class RegionTaskQueueData {
-+ private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue();
-+ private final PrioritisedQueue chunkQueue = new PrioritisedQueue();
-+ private final WorldRegionTaskData worldRegionTaskData;
-+
-+ public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) {
-+ this.worldRegionTaskData = worldRegionTaskData;
-+ }
-+
-+ void mergeInto(final RegionTaskQueueData into) {
-+ this.tickTaskQueue.mergeInto(into.tickTaskQueue);
-+ this.chunkQueue.mergeInto(into.chunkQueue);
-+ }
-+
-+ public boolean executeTickTask() {
-+ return this.tickTaskQueue.executeTask();
-+ }
-+
-+ public boolean executeChunkTask() {
-+ return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask();
-+ }
-+
-+ void split(final ThreadedRegionizer regioniser,
-+ final Long2ReferenceOpenHashMap> into) {
-+ this.tickTaskQueue.split(
-+ false, regioniser, into
-+ );
-+ this.chunkQueue.split(
-+ true, regioniser, into
-+ );
-+ }
-+
-+ public void drainTasks() {
-+ final PrioritisedQueue tickTaskQueue = this.tickTaskQueue;
-+ final PrioritisedQueue chunkTaskQueue = this.chunkQueue;
-+
-+ int allowedTickTasks = tickTaskQueue.getScheduledTasks();
-+ int allowedChunkTasks = chunkTaskQueue.getScheduledTasks();
-+
-+ boolean executeTickTasks = allowedTickTasks > 0;
-+ boolean executeChunkTasks = allowedChunkTasks > 0;
-+ boolean executeGlobalTasks = true;
-+
-+ do {
-+ executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask();
-+ executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask();
-+ executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask();
-+ } while (executeTickTasks | executeChunkTasks | executeGlobalTasks);
-+
-+ if (allowedChunkTasks > 0) {
-+ // if we executed chunk tasks, we should try to process ticket updates for full status changes
-+ this.worldRegionTaskData.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates();
-+ }
-+ }
-+
-+ public boolean hasTasks() {
-+ return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty();
-+ }
-+ }
-+
-+ static final class PrioritisedQueue {
-+ private final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; {
-+ for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) {
-+ this.queues[i] = new ArrayDeque<>();
-+ }
-+ }
-+ private boolean isDestroyed;
-+
-+ public int getScheduledTasks() {
-+ synchronized (this) {
-+ int ret = 0;
-+
-+ for (final ArrayDeque queue : this.queues) {
-+ ret += queue.size();
-+ }
-+
-+ return ret;
-+ }
-+ }
-+
-+ public boolean isEmpty() {
-+ final ArrayDeque[] queues = this.queues;
-+ final int max = Priority.IDLE.priority;
-+ synchronized (this) {
-+ for (int i = 0; i <= max; ++i) {
-+ if (!queues[i].isEmpty()) {
-+ return false;
-+ }
-+ }
-+ return true;
-+ }
-+ }
-+
-+ public void mergeInto(final PrioritisedQueue target) {
-+ synchronized (this) {
-+ this.isDestroyed = true;
-+ mergeInto(target, this.queues);
-+ }
-+ }
-+
-+ private static void mergeInto(final PrioritisedQueue target, final ArrayDeque[] thisQueues) {
-+ synchronized (target) {
-+ final ArrayDeque[] otherQueues = target.queues;
-+ for (int i = 0; i < thisQueues.length; ++i) {
-+ final ArrayDeque fromQ = thisQueues[i];
-+ final ArrayDeque intoQ = otherQueues[i];
-+
-+ // it is possible for another thread to queue tasks into the target queue before we do
-+ // since only the ticking region can poll, we don't have to worry about it when they are being queued -
-+ // but when we are merging, we need to ensure order is maintained (notwithstanding priority changes)
-+ // we can ensure order is maintained by adding all of the tasks from the fromQ into the intoQ at the
-+ // front of the queue, but we need to use descending iterator to ensure we do not reverse
-+ // the order of elements from fromQ
-+ for (final Iterator iterator = fromQ.descendingIterator(); iterator.hasNext();) {
-+ intoQ.addFirst(iterator.next());
-+ }
-+ }
-+ }
-+ }
-+
-+ // into is a map of section coordinate to region
-+ public void split(final boolean isChunkData,
-+ final ThreadedRegionizer regioniser,
-+ final Long2ReferenceOpenHashMap> into) {
-+ final Reference2ReferenceOpenHashMap, ArrayDeque[]>
-+ split = new Reference2ReferenceOpenHashMap<>();
-+ final int shift = regioniser.sectionChunkShift;
-+ synchronized (this) {
-+ this.isDestroyed = true;
-+ // like mergeTarget, we need to be careful about insertion order so we can maintain order when splitting
-+
-+ // first, build the targets
-+ final ArrayDeque[] thisQueues = this.queues;
-+ for (int i = 0; i < thisQueues.length; ++i) {
-+ final ArrayDeque fromQ = thisQueues[i];
-+
-+ for (final ChunkBasedPriorityTask task : fromQ) {
-+ final int sectionX = task.chunkX >> shift;
-+ final int sectionZ = task.chunkZ >> shift;
-+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+ final ThreadedRegionizer.ThreadedRegion
-+ region = into.get(sectionKey);
-+ if (region == null) {
-+ throw new IllegalStateException();
-+ }
-+
-+ split.computeIfAbsent(region, (keyInMap) -> {
-+ final ArrayDeque[] ret = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES];
-+
-+ for (int k = 0; k < ret.length; ++k) {
-+ ret[k] = new ArrayDeque<>();
-+ }
-+
-+ return ret;
-+ })[i].add(task);
-+ }
-+ }
-+
-+ // merge the targets into their queues
-+ for (final Iterator, ArrayDeque[]>>
-+ iterator = split.reference2ReferenceEntrySet().fastIterator();
-+ iterator.hasNext();) {
-+ final Reference2ReferenceMap.Entry, ArrayDeque[]>
-+ entry = iterator.next();
-+ final RegionTaskQueueData taskQueueData = entry.getKey().getData().getTaskQueueData();
-+ mergeInto(isChunkData ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue, entry.getValue());
-+ }
-+ }
-+ }
-+
-+ /**
-+ * returns null if the task cannot be scheduled, returns false if this task queue is dead, and returns true
-+ * if the task was added
-+ */
-+ private Boolean tryPush(final ChunkBasedPriorityTask task) {
-+ final ArrayDeque[] queues = this.queues;
-+ synchronized (this) {
-+ final Priority priority = task.getPriority();
-+ if (priority == Priority.COMPLETING) {
-+ return null;
-+ }
-+ if (this.isDestroyed) {
-+ return Boolean.FALSE;
-+ }
-+ queues[priority.priority].addLast(task);
-+ return Boolean.TRUE;
-+ }
-+ }
-+
-+ private boolean executeTask() {
-+ final ArrayDeque[] queues = this.queues;
-+ final int max = Priority.IDLE.priority;
-+ ChunkBasedPriorityTask task = null;
-+ ReferenceCountData referenceCounter = null;
-+ synchronized (this) {
-+ if (this.isDestroyed) {
-+ throw new IllegalStateException("Attempting to poll from dead queue");
-+ }
-+
-+ search_loop:
-+ for (int i = 0; i <= max; ++i) {
-+ final ArrayDeque queue = queues[i];
-+ while ((task = queue.pollFirst()) != null) {
-+ if ((referenceCounter = task.trySetCompleting(i)) != null) {
-+ break search_loop;
-+ }
-+ }
-+ }
-+ }
-+
-+ if (task == null) {
-+ return false;
-+ }
-+
-+ try {
-+ task.executeInternal();
-+ } finally {
-+ task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord);
-+ }
-+
-+ return true;
-+ }
-+
-+ private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask {
-+
-+ private static final ReferenceCountData REFERENCE_COUNTER_NOT_SET = new ReferenceCountData();
-+ static {
-+ REFERENCE_COUNTER_NOT_SET.referenceCount.set((long)Integer.MIN_VALUE);
-+ }
-+
-+ private final WorldRegionTaskData world;
-+ private final int chunkX;
-+ private final int chunkZ;
-+ private final long sectionLowerLeftCoord; // chunk coordinate
-+ private final boolean isChunkTask;
-+
-+ private volatile ReferenceCountData referenceCounter;
-+ private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", ReferenceCountData.class);
-+ private Runnable run;
-+ private volatile Priority priority;
-+ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", Priority.class);
-+
-+ ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask,
-+ final Runnable run, final Priority priority) {
-+ this.world = world;
-+ this.chunkX = chunkX;
-+ this.chunkZ = chunkZ;
-+ this.isChunkTask = isChunkTask;
-+ this.run = run;
-+ this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET);
-+ this.setPriorityPlain(priority);
-+
-+ final int regionShift = world.world.regioniser.sectionChunkShift;
-+ final int regionMask = (1 << regionShift) - 1;
-+
-+ this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask);
-+ }
-+
-+ private Priority getPriorityVolatile() {
-+ return (Priority)PRIORITY_HANDLE.getVolatile(this);
-+ }
-+
-+ private void setPriorityPlain(final Priority priority) {
-+ PRIORITY_HANDLE.set(this, priority);
-+ }
-+
-+ private void setPriorityVolatile(final Priority priority) {
-+ PRIORITY_HANDLE.setVolatile(this, priority);
-+ }
-+
-+ private Priority compareAndExchangePriority(final Priority expect, final Priority update) {
-+ return (Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update);
-+ }
-+
-+ private void setReferenceCounterPlain(final ReferenceCountData value) {
-+ REFERENCE_COUNTER_HANDLE.set(this, value);
-+ }
-+
-+ private ReferenceCountData getReferenceCounterVolatile() {
-+ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.get(this);
-+ }
-+
-+ private ReferenceCountData compareAndExchangeReferenceCounter(final ReferenceCountData expect, final ReferenceCountData update) {
-+ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update);
-+ }
-+
-+ private void executeInternal() {
-+ try {
-+ this.run.run();
-+ } finally {
-+ this.run = null;
-+ }
-+ }
-+
-+ private void cancelInternal() {
-+ this.run = null;
-+ }
-+
-+ private boolean tryComplete(final boolean cancel) {
-+ int failures = 0;
-+ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) {
-+ if (curr == null) {
-+ return false;
-+ }
-+
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+
-+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
-+ ++failures;
-+ continue;
-+ }
-+
-+ // we have the reference count, we win no matter what.
-+ this.setPriorityVolatile(Priority.COMPLETING);
-+
-+ try {
-+ if (cancel) {
-+ this.cancelInternal();
-+ } else {
-+ this.executeInternal();
-+ }
-+ } finally {
-+ if (curr != REFERENCE_COUNTER_NOT_SET) {
-+ this.world.decrementReference(curr, this.sectionLowerLeftCoord);
-+ }
-+ }
-+
-+ return true;
-+ }
-+ }
-+
-+ @Override
-+ public PrioritisedExecutor getExecutor() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean isQueued() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean queue() {
-+ if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) {
-+ return false;
-+ }
-+
-+ final ReferenceCountData referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord);
-+ if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) {
-+ // we don't expect race conditions here, so it is OK if we have to needlessly reference count
-+ this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord);
-+ return false;
-+ }
-+
-+ boolean synchronise = false;
-+ for (;;) {
-+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
-+ // the same queue again, as the region lock will be given to us only when the merge/split operation
-+ // is done
-+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
-+
-+ if (queue == null) {
-+ if (!synchronise) {
-+ // may be incorrectly null when unsynchronised
-+ synchronise = true;
-+ continue;
-+ }
-+ // may have been cancelled before we got to the queue
-+ if (this.getReferenceCounterVolatile() != null) {
-+ throw new IllegalStateException("Expected null ref count when queue does not exist");
-+ }
-+ // the task never could be polled from the queue, so we return false
-+ // don't decrement reference count, as we were certainly cancelled by another thread, which
-+ // will decrement the reference count
-+ return false;
-+ }
-+
-+ synchronise = true;
-+
-+ final Boolean res = queue.tryPush(this);
-+ if (res == null) {
-+ // we were cancelled
-+ // don't decrement reference count, as we were certainly cancelled by another thread, which
-+ // will decrement the reference count
-+ return false;
-+ }
-+
-+ if (!res.booleanValue()) {
-+ // failed, try again
-+ continue;
-+ }
-+
-+ // successfully queued
-+ return true;
-+ }
-+ }
-+
-+ private ReferenceCountData trySetCompleting(final int minPriority) {
-+ // first, try to set priority to EXECUTING
-+ for (Priority curr = this.getPriorityVolatile();;) {
-+ if (curr.isLowerPriority(minPriority)) {
-+ return null;
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriority(curr, Priority.COMPLETING))) {
-+ break;
-+ } // else: continue
-+ }
-+
-+ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) {
-+ if (curr == null) {
-+ // something acquired before us
-+ return null;
-+ }
-+
-+ if (curr == REFERENCE_COUNTER_NOT_SET) {
-+ throw new IllegalStateException();
-+ }
-+
-+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
-+ continue;
-+ }
-+
-+ return curr;
-+ }
-+ }
-+
-+ private void updatePriorityInQueue() {
-+ boolean synchronise = false;
-+ for (;;) {
-+ final ReferenceCountData referenceCount = this.getReferenceCounterVolatile();
-+ if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) {
-+ // cancelled or not queued
-+ return;
-+ }
-+
-+ if (this.getPriorityVolatile() == Priority.COMPLETING) {
-+ // cancelled
-+ return;
-+ }
-+
-+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
-+ // the same queue again, as the region lock will be given to us only when the merge/split operation
-+ // is done
-+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
-+
-+ if (queue == null) {
-+ if (!synchronise) {
-+ // may be incorrectly null when unsynchronised
-+ synchronise = true;
-+ continue;
-+ }
-+ // must have been removed
-+ return;
-+ }
-+
-+ synchronise = true;
-+
-+ final Boolean res = queue.tryPush(this);
-+ if (res == null) {
-+ // we were cancelled
-+ return;
-+ }
-+
-+ if (!res.booleanValue()) {
-+ // failed, try again
-+ continue;
-+ }
-+
-+ // successfully queued
-+ return;
-+ }
-+ }
-+
-+ @Override
-+ public Priority getPriority() {
-+ return this.getPriorityVolatile();
-+ }
-+
-+ @Override
-+ public boolean lowerPriority(final Priority priority) {
-+ int failures = 0;
-+ for (Priority curr = this.getPriorityVolatile();;) {
-+ if (curr == Priority.COMPLETING) {
-+ return false;
-+ }
-+
-+ if (curr.isLowerOrEqualPriority(priority)) {
-+ return false;
-+ }
-+
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
-+ this.updatePriorityInQueue();
-+ return true;
-+ }
-+ ++failures;
-+ }
-+ }
-+
-+ @Override
-+ public long getSubOrder() {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean setSubOrder(final long subOrder) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean raiseSubOrder(final long subOrder) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean lowerSubOrder(final long subOrder) {
-+ throw new UnsupportedOperationException();
-+ }
-+
-+ @Override
-+ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
-+ return this.setPriority(priority);
-+ }
-+
-+ @Override
-+ public boolean setPriority(final Priority priority) {
-+ int failures = 0;
-+ for (Priority curr = this.getPriorityVolatile();;) {
-+ if (curr == Priority.COMPLETING) {
-+ return false;
-+ }
-+
-+ if (curr == priority) {
-+ return false;
-+ }
-+
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
-+ this.updatePriorityInQueue();
-+ return true;
-+ }
-+ ++failures;
-+ }
-+ }
-+
-+ @Override
-+ public boolean raisePriority(final Priority priority) {
-+ int failures = 0;
-+ for (Priority curr = this.getPriorityVolatile();;) {
-+ if (curr == Priority.COMPLETING) {
-+ return false;
-+ }
-+
-+ if (curr.isHigherOrEqualPriority(priority)) {
-+ return false;
-+ }
-+
-+ for (int i = 0; i < failures; ++i) {
-+ ConcurrentUtil.backoff();
-+ }
-+
-+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
-+ this.updatePriorityInQueue();
-+ return true;
-+ }
-+ ++failures;
-+ }
-+ }
-+
-+ @Override
-+ public boolean execute() {
-+ return this.tryComplete(false);
-+ }
-+
-+ @Override
-+ public boolean cancel() {
-+ return this.tryComplete(true);
-+ }
-+ }
-+ }
-+}
-diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..b49ca79c31e30e90c82349aecf2cc22df3b61ee6
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java
-@@ -0,0 +1,770 @@
-+package io.papermc.paper.threadedregions;
-+
-+import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet;
-+import ca.spottedleaf.moonrise.common.list.ReferenceList;
-+import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import ca.spottedleaf.moonrise.common.util.TickThread;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
-+import com.mojang.logging.LogUtils;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
-+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
-+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
-+import net.minecraft.CrashReport;
-+import net.minecraft.ReportedException;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.network.Connection;
-+import net.minecraft.network.PacketSendListener;
-+import net.minecraft.network.chat.Component;
-+import net.minecraft.network.chat.MutableComponent;
-+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ChunkHolder;
-+import net.minecraft.server.level.ServerChunkCache;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.server.level.ServerPlayer;
-+import net.minecraft.server.network.ServerGamePacketListenerImpl;
-+import net.minecraft.util.VisibleForDebug;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.entity.Mob;
-+import net.minecraft.world.entity.ai.village.VillageSiege;
-+import net.minecraft.world.entity.item.ItemEntity;
-+import net.minecraft.world.level.BlockEventData;
-+import net.minecraft.world.level.ChunkPos;
-+import net.minecraft.world.level.Explosion;
-+import net.minecraft.world.level.Level;
-+import net.minecraft.world.level.NaturalSpawner;
-+import net.minecraft.world.level.ServerExplosion;
-+import net.minecraft.world.level.block.Block;
-+import net.minecraft.world.level.block.Blocks;
-+import net.minecraft.world.level.block.RedStoneWireBlock;
-+import net.minecraft.world.level.block.entity.BlockEntity;
-+import net.minecraft.world.level.block.entity.TickingBlockEntity;
-+import net.minecraft.world.level.chunk.LevelChunk;
-+import net.minecraft.world.level.material.Fluid;
-+import net.minecraft.world.level.pathfinder.PathTypeCache;
-+import net.minecraft.world.level.redstone.CollectingNeighborUpdater;
-+import net.minecraft.world.level.redstone.NeighborUpdater;
-+import net.minecraft.world.ticks.LevelTicks;
-+import org.bukkit.craftbukkit.block.CraftBlockState;
-+import org.slf4j.Logger;
-+import javax.annotation.Nullable;
-+import java.util.ArrayDeque;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.Collection;
-+import java.util.Collections;
-+import java.util.HashMap;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.Map;
-+import java.util.Set;
-+import java.util.function.Consumer;
-+import java.util.function.Predicate;
-+
-+public final class RegionizedWorldData {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+
-+ private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0];
-+
-+ public static final RegionizedData.RegioniserCallback REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() {
-+ @Override
-+ public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) {
-+ // connections
-+ for (final Connection conn : from.connections) {
-+ into.connections.add(conn);
-+ }
-+ // time
-+ final long fromRedstoneTimeOffset = into.redstoneTime - from.redstoneTime;
-+ // entities
-+ for (final ServerPlayer player : from.localPlayers) {
-+ into.localPlayers.add(player);
-+ into.nearbyPlayers.addPlayer(player);
-+ }
-+ for (final Entity entity : from.allEntities) {
-+ into.allEntities.add(entity);
-+ entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
-+ }
-+ for (final Entity entity : from.loadedEntities) {
-+ into.loadedEntities.add(entity);
-+ }
-+ for (final Iterator iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) {
-+ into.entityTickList.add(iterator.next());
-+ }
-+ for (final Iterator iterator = from.navigatingMobs.unsafeIterator(); iterator.hasNext();) {
-+ into.navigatingMobs.add(iterator.next());
-+ }
-+ for (final Iterator iterator = from.trackerEntities.iterator(); iterator.hasNext();) {
-+ into.trackerEntities.add(iterator.next());
-+ }
-+ for (final Iterator iterator = from.trackerUnloadedEntities.iterator(); iterator.hasNext();) {
-+ into.trackerUnloadedEntities.add(iterator.next());
-+ }
-+ // block ticking
-+ into.blockEvents.addAll(from.blockEvents);
-+ // ticklists use game time
-+ from.blockLevelTicks.merge(into.blockLevelTicks, fromRedstoneTimeOffset);
-+ from.fluidLevelTicks.merge(into.fluidLevelTicks, fromRedstoneTimeOffset);
-+
-+ // tile entity ticking
-+ for (final TickingBlockEntity tileEntityWrapped : from.pendingBlockEntityTickers) {
-+ into.pendingBlockEntityTickers.add(tileEntityWrapped);
-+ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity();
-+ if (tileEntity != null) {
-+ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
-+ }
-+ }
-+ for (final TickingBlockEntity tileEntityWrapped : from.blockEntityTickers) {
-+ into.blockEntityTickers.add(tileEntityWrapped);
-+ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity();
-+ if (tileEntity != null) {
-+ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
-+ }
-+ }
-+
-+ // ticking chunks
-+ for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) {
-+ into.entityTickingChunks.add(iterator.next());
-+ }
-+ for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) {
-+ into.tickingChunks.add(iterator.next());
-+ }
-+ for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) {
-+ into.chunks.add(iterator.next());
-+ }
-+ // redstone torches
-+ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) {
-+ if (into.redstoneUpdateInfos == null) {
-+ into.redstoneUpdateInfos = new ArrayDeque<>();
-+ }
-+ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) {
-+ info.offsetTime(fromRedstoneTimeOffset);
-+ into.redstoneUpdateInfos.add(info);
-+ }
-+ }
-+ // mob spawning
-+ into.catSpawnerNextTick = Math.max(from.catSpawnerNextTick, into.catSpawnerNextTick);
-+ into.patrolSpawnerNextTick = Math.max(from.patrolSpawnerNextTick, into.patrolSpawnerNextTick);
-+ into.phantomSpawnerNextTick = Math.max(from.phantomSpawnerNextTick, into.phantomSpawnerNextTick);
-+ if (from.wanderingTraderTickDelay != Integer.MIN_VALUE && into.wanderingTraderTickDelay != Integer.MIN_VALUE) {
-+ into.wanderingTraderTickDelay = Math.max(from.wanderingTraderTickDelay, into.wanderingTraderTickDelay);
-+ into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay);
-+ into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance);
-+ }
-+ // chunkHoldersToBroadcast
-+ for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) {
-+ into.chunkHoldersToBroadcast.add(chunkHolder);
-+ }
-+ }
-+
-+ @Override
-+ public void split(final RegionizedWorldData from, final int chunkToRegionShift,
-+ final Long2ReferenceOpenHashMap regionToData,
-+ final ReferenceOpenHashSet dataSet) {
-+ // connections
-+ for (final Connection conn : from.connections) {
-+ final ServerPlayer player = conn.getPlayer();
-+ final ChunkPos pos = player.chunkPosition();
-+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
-+ // the chunk holder must _exist_, and so the region section exists.
-+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
-+ .connections.add(conn);
-+ }
-+ // entities
-+ for (final ServerPlayer player : from.localPlayers) {
-+ final ChunkPos pos = player.chunkPosition();
-+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
-+ // the chunk holder must _exist_, and so the region section exists.
-+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
-+ into.localPlayers.add(player);
-+ into.nearbyPlayers.addPlayer(player);
-+ }
-+ for (final Entity entity : from.allEntities) {
-+ final ChunkPos pos = entity.chunkPosition();
-+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
-+ // the chunk holder must _exist_, and so the region section exists.
-+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
-+ into.allEntities.add(entity);
-+ // Note: entityTickList is a subset of allEntities
-+ if (from.entityTickList.contains(entity)) {
-+ into.entityTickList.add(entity);
-+ }
-+ // Note: loadedEntities is a subset of allEntities
-+ if (from.loadedEntities.contains(entity)) {
-+ into.loadedEntities.add(entity);
-+ }
-+ // Note: navigatingMobs is a subset of allEntities
-+ if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) {
-+ into.navigatingMobs.add(mob);
-+ }
-+ if (from.trackerEntities.contains(entity)) {
-+ into.trackerEntities.add(entity);
-+ }
-+ if (from.trackerUnloadedEntities.contains(entity)) {
-+ into.trackerUnloadedEntities.add(entity);
-+ }
-+ }
-+ // block ticking
-+ for (final BlockEventData blockEventData : from.blockEvents) {
-+ final BlockPos pos = blockEventData.pos();
-+ final int chunkX = pos.getX() >> 4;
-+ final int chunkZ = pos.getZ() >> 4;
-+
-+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
-+ // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events
-+ // is just some list. So if it unloads, I guess it's just lost.
-+ if (into != null) {
-+ into.blockEvents.add(blockEventData);
-+ }
-+ }
-+
-+ final Long2ReferenceOpenHashMap> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
-+ final Long2ReferenceOpenHashMap> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
-+
-+ for (final Iterator> iterator = regionToData.long2ReferenceEntrySet().fastIterator();
-+ iterator.hasNext();) {
-+ final Long2ReferenceMap.Entry entry = iterator.next();
-+ final long key = entry.getLongKey();
-+ final RegionizedWorldData worldData = entry.getValue();
-+
-+ levelTicksBlockRegionData.put(key, worldData.blockLevelTicks);
-+ levelTicksFluidRegionData.put(key, worldData.fluidLevelTicks);
-+ }
-+
-+ from.blockLevelTicks.split(chunkToRegionShift, levelTicksBlockRegionData);
-+ from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData);
-+
-+ // tile entity ticking
-+ for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) {
-+ final BlockPos pos = tileEntity.getPos();
-+ final int chunkX = pos.getX() >> 4;
-+ final int chunkZ = pos.getZ() >> 4;
-+
-+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
-+ if (into != null) {
-+ into.pendingBlockEntityTickers.add(tileEntity);
-+ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets
-+ // marked as removed. So if there is no section, it's probably removed!
-+ }
-+ for (final TickingBlockEntity tileEntity : from.blockEntityTickers) {
-+ final BlockPos pos = tileEntity.getPos();
-+ final int chunkX = pos.getX() >> 4;
-+ final int chunkZ = pos.getZ() >> 4;
-+
-+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
-+ if (into != null) {
-+ into.blockEntityTickers.add(tileEntity);
-+ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets
-+ // marked as removed. So if there is no section, it's probably removed!
-+ }
-+ // time
-+ for (final RegionizedWorldData regionizedWorldData : dataSet) {
-+ regionizedWorldData.redstoneTime = from.redstoneTime;
-+ }
-+ // ticking chunks
-+ for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) {
-+ final ServerChunkCache.ChunkAndHolder holder = iterator.next();
-+ final ChunkPos pos = holder.chunk().getPos();
-+
-+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
-+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
-+ .entityTickingChunks.add(holder);
-+ }
-+ for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) {
-+ final ServerChunkCache.ChunkAndHolder holder = iterator.next();
-+ final ChunkPos pos = holder.chunk().getPos();
-+
-+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
-+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
-+ .tickingChunks.add(holder);
-+ }
-+ for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) {
-+ final ServerChunkCache.ChunkAndHolder holder = iterator.next();
-+ final ChunkPos pos = holder.chunk().getPos();
-+
-+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
-+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
-+ .chunks.add(holder);
-+ }
-+
-+ // redstone torches
-+ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) {
-+ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) {
-+ final BlockPos pos = info.pos;
-+
-+ final RegionizedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift));
-+ if (worldData != null) {
-+ if (worldData.redstoneUpdateInfos == null) {
-+ worldData.redstoneUpdateInfos = new ArrayDeque<>();
-+ }
-+ worldData.redstoneUpdateInfos.add(info);
-+ } // else: chunk unloaded
-+ }
-+ }
-+ // mob spawning
-+ for (final RegionizedWorldData regionizedWorldData : dataSet) {
-+ regionizedWorldData.catSpawnerNextTick = from.catSpawnerNextTick;
-+ regionizedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick;
-+ regionizedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick;
-+ regionizedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay;
-+ regionizedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance;
-+ regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay;
-+ regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid
-+ }
-+ // chunkHoldersToBroadcast
-+ for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) {
-+ final ChunkPos pos = chunkHolder.getPos();
-+
-+ // Possible for get() to return null, as the chunk holder is not removed during unload
-+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
-+ if (into != null) {
-+ into.chunkHoldersToBroadcast.add(chunkHolder);
-+ }
-+ }
-+ }
-+ };
-+
-+ public final ServerLevel world;
-+
-+ private RegionizedServer.WorldLevelData tickData;
-+
-+ // connections
-+ public final List connections = new ArrayList<>();
-+
-+ // misc. fields
-+ private boolean isHandlingTick;
-+
-+ public void setHandlingTick(final boolean to) {
-+ this.isHandlingTick = to;
-+ }
-+
-+ public boolean isHandlingTick() {
-+ return this.isHandlingTick;
-+ }
-+
-+ // entities
-+ private final List localPlayers = new ArrayList<>();
-+ private final NearbyPlayers nearbyPlayers;
-+ private final ReferenceList allEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY);
-+ private final ReferenceList loadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY);
-+ private final IteratorSafeOrderedReferenceSet entityTickList = new IteratorSafeOrderedReferenceSet<>();
-+ private final IteratorSafeOrderedReferenceSet navigatingMobs = new IteratorSafeOrderedReferenceSet<>();
-+ public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
-+ public final ReferenceList trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
-+
-+ // block ticking
-+ private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>();
-+ private final LevelTicks blockLevelTicks;
-+ private final LevelTicks fluidLevelTicks;
-+
-+ // tile entity ticking
-+ private final List pendingBlockEntityTickers = new ArrayList<>();
-+ private final List blockEntityTickers = new ArrayList<>();
-+ private boolean tickingBlockEntities;
-+
-+ // time
-+ private long redstoneTime = 1L;
-+
-+ public long getRedstoneGameTime() {
-+ return this.redstoneTime;
-+ }
-+
-+ public void setRedstoneGameTime(final long to) {
-+ this.redstoneTime = to;
-+ }
-+
-+ // ticking chunks
-+ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDER_ARRAY = new ServerChunkCache.ChunkAndHolder[0];
-+ private final ReferenceList entityTickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY);
-+ private final ReferenceList tickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY);
-+ private final ReferenceList chunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY);
-+
-+ // Paper/CB api hook misc
-+ // don't bother to merge/split these, no point
-+ // From ServerLevel
-+ public boolean hasPhysicsEvent = true; // Paper
-+ public boolean hasEntityMoveEvent = false; // Paper
-+ // Paper start - Optimize Hoppers
-+ public boolean skipPullModeEventFire = false;
-+ public boolean skipPushModeEventFire = false;
-+ public boolean skipHopperEvents = false;
-+ // Paper end - Optimize Hoppers
-+ public long lastMidTickExecute;
-+ public long lastMidTickExecuteFailure;
-+ // From Level
-+ public boolean populating;
-+ public final NeighborUpdater neighborUpdater;
-+ public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
-+ public boolean captureBlockStates = false;
-+ public boolean captureTreeGeneration = false;
-+ public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
-+ public final Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper
-+ public final Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper
-+ public List captureDrops;
-+ // Paper start
-+ public int wakeupInactiveRemainingAnimals;
-+ public int wakeupInactiveRemainingFlying;
-+ public int wakeupInactiveRemainingMonsters;
-+ public int wakeupInactiveRemainingVillagers;
-+ // Paper end
-+ public int currentPrimedTnt = 0; // Spigot
-+ @Nullable
-+ @VisibleForDebug
-+ public NaturalSpawner.SpawnState lastSpawnState;
-+ public boolean shouldSignal = true;
-+ public final Map explosionDensityCache = new HashMap<>(64, 0.25f);
-+ public final PathTypeCache pathTypesByPosCache = new PathTypeCache();
-+ public final List temporaryChunkTickList = new java.util.ArrayList<>();
-+ public final Set chunkHoldersToBroadcast = new ReferenceLinkedOpenHashSet<>();
-+
-+ // not transient
-+ public java.util.ArrayDeque redstoneUpdateInfos;
-+
-+ // Mob spawning
-+ public final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>();
-+ public int catSpawnerNextTick = 0;
-+ public int patrolSpawnerNextTick = 0;
-+ public int phantomSpawnerNextTick = 0;
-+ public int wanderingTraderTickDelay = Integer.MIN_VALUE;
-+ public int wanderingTraderSpawnDelay;
-+ public int wanderingTraderSpawnChance;
-+ public VillageSiegeState villageSiegeState = new VillageSiegeState();
-+
-+ public static final class VillageSiegeState {
-+ public boolean hasSetupSiege;
-+ public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE;
-+ public int zombiesToSpawn;
-+ public int nextSpawnTime;
-+ public int spawnX;
-+ public int spawnY;
-+ public int spawnZ;
-+ }
-+ // Redstone
-+ public final alternate.current.wire.WireHandler wireHandler;
-+ public final io.papermc.paper.redstone.RedstoneWireTurbo turbo;
-+
-+ public RegionizedWorldData(final ServerLevel world) {
-+ this.world = world;
-+ this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, true);
-+ this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, false);
-+ this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax);
-+ this.nearbyPlayers = new NearbyPlayers(world);
-+ this.wireHandler = new alternate.current.wire.WireHandler(world);
-+ this.turbo = new io.papermc.paper.redstone.RedstoneWireTurbo((RedStoneWireBlock)Blocks.REDSTONE_WIRE);
-+
-+ // tasks may be drained before the region ticks, so we must set up the tick data early just in case
-+ this.updateTickData();
-+ }
-+
-+ public void checkWorld(final Level against) {
-+ if (this.world != against) {
-+ throw new IllegalStateException("World mismatch: expected " + this.world.getWorld().getName() + " but got " + (against == null ? "null" : against.getWorld().getName()));
-+ }
-+ }
-+
-+ public RegionizedServer.WorldLevelData getTickData() {
-+ return this.tickData;
-+ }
-+
-+ private long lagCompensationTick;
-+
-+ public long getLagCompensationTick() {
-+ return this.lagCompensationTick;
-+ }
-+
-+ public void updateTickData() {
-+ this.tickData = this.world.tickData;
-+ this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
-+ this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
-+ this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers
-+ // always subtract from server init so that the tick starts at zero, allowing us to cast to int without much worry
-+ this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / TickRegionScheduler.TIME_BETWEEN_TICKS;
-+ }
-+
-+ public NearbyPlayers getNearbyPlayers() {
-+ return this.nearbyPlayers;
-+ }
-+
-+ private static void cleanUpConnection(final Connection conn) {
-+ // note: ALL connections HERE have a player
-+ final ServerPlayer player = conn.getPlayer();
-+ // now that the connection is removed, we can allow this region to die
-+ player.serverLevel().chunkSource.removeTicketAtLevel(
-+ ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos,
-+ ChunkHolderManager.MAX_TICKET_LEVEL,
-+ player.connection.disconnectTicketId
-+ );
-+ }
-+
-+ // connections
-+ public void tickConnections() {
-+ final List connections = new ArrayList<>(this.connections);
-+ Collections.shuffle(connections);
-+ for (final Connection conn : connections) {
-+ if (!conn.isConnected()) {
-+ conn.handleDisconnection();
-+ // global tick thread will not remove connections not owned by it, so we need to
-+ RegionizedServer.getInstance().removeConnection(conn);
-+ this.connections.remove(conn);
-+ cleanUpConnection(conn);
-+ continue;
-+ }
-+ if (!this.connections.contains(conn)) {
-+ // removed by connection tick?
-+ continue;
-+ }
-+
-+ try {
-+ conn.tick();
-+ } catch (final Exception exception) {
-+ if (conn.isMemoryConnection()) {
-+ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection"));
-+ }
-+
-+ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception);
-+ MutableComponent ichatmutablecomponent = Component.literal("Internal server error");
-+
-+ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> {
-+ conn.disconnect(ichatmutablecomponent);
-+ }));
-+ conn.setReadOnly();
-+ continue;
-+ }
-+ }
-+ }
-+
-+ // entities hooks
-+ public int getEntityCount() {
-+ return this.allEntities.size();
-+ }
-+
-+ public int getPlayerCount() {
-+ return this.localPlayers.size();
-+ }
-+
-+ public Iterable getLocalEntities() {
-+ return this.allEntities;
-+ }
-+
-+ public Entity[] getLocalEntitiesCopy() {
-+ return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class);
-+ }
-+
-+ public List getLocalPlayers() {
-+ return this.localPlayers;
-+ }
-+
-+ public void addLoadedEntity(final Entity entity) {
-+ this.loadedEntities.add(entity);
-+ }
-+
-+ public boolean hasLoadedEntity(final Entity entity) {
-+ return this.loadedEntities.contains(entity);
-+ }
-+
-+ public void removeLoadedEntity(final Entity entity) {
-+ this.loadedEntities.remove(entity);
-+ }
-+
-+ public Iterable getLoadedEntities() {
-+ return this.loadedEntities;
-+ }
-+
-+ public void addEntityTickingEntity(final Entity entity) {
-+ if (!TickThread.isTickThreadFor(entity)) {
-+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
-+ }
-+ this.entityTickList.add(entity);
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+
-+ public boolean hasEntityTickingEntity(final Entity entity) {
-+ return this.entityTickList.contains(entity);
-+ }
-+
-+ public void removeEntityTickingEntity(final Entity entity) {
-+ if (!TickThread.isTickThreadFor(entity)) {
-+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
-+ }
-+ this.entityTickList.remove(entity);
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+
-+ public void forEachTickingEntity(final Consumer action) {
-+ final IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickList.iterator();
-+ try {
-+ while (iterator.hasNext()) {
-+ action.accept(iterator.next());
-+ }
-+ } finally {
-+ iterator.finishedIterating();
-+ }
-+ }
-+
-+ public void addEntity(final Entity entity) {
-+ if (!TickThread.isTickThreadFor(this.world, entity.chunkPosition())) {
-+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
-+ }
-+ if (this.allEntities.add(entity)) {
-+ if (entity instanceof ServerPlayer player) {
-+ this.localPlayers.add(player);
-+ }
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+ }
-+
-+ public boolean hasEntity(final Entity entity) {
-+ return this.allEntities.contains(entity);
-+ }
-+
-+ public void removeEntity(final Entity entity) {
-+ if (!TickThread.isTickThreadFor(entity)) {
-+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
-+ }
-+ if (this.allEntities.remove(entity)) {
-+ if (entity instanceof ServerPlayer player) {
-+ this.localPlayers.remove(player);
-+ }
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+ }
-+
-+ public void addNavigatingMob(final Mob mob) {
-+ if (!TickThread.isTickThreadFor(mob)) {
-+ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control");
-+ }
-+ this.navigatingMobs.add(mob);
-+ }
-+
-+ public void removeNavigatingMob(final Mob mob) {
-+ if (!TickThread.isTickThreadFor(mob)) {
-+ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control");
-+ }
-+ this.navigatingMobs.remove(mob);
-+ }
-+
-+ public Iterator getNavigatingMobs() {
-+ return this.navigatingMobs.unsafeIterator();
-+ }
-+
-+ // block ticking hooks
-+ // Since block event data does not require chunk holders to be created for the chunk they reside in,
-+ // it's not actually guaranteed that when merging / splitting data that we actually own the data...
-+ // Note that we can only ever not own the event data when the chunk unloads, and so I've decided to
-+ // make the code easier by simply discarding it in such an event
-+ public void pushBlockEvent(final BlockEventData blockEventData) {
-+ TickThread.ensureTickThread(this.world, blockEventData.pos(), "Cannot queue block even data async");
-+ this.blockEvents.add(blockEventData);
-+ }
-+
-+ public void pushBlockEvents(final Collection extends BlockEventData> blockEvents) {
-+ for (final BlockEventData blockEventData : blockEvents) {
-+ this.pushBlockEvent(blockEventData);
-+ }
-+ }
-+
-+ public void removeIfBlockEvents(final Predicate super BlockEventData> predicate) {
-+ for (final Iterator iterator = this.blockEvents.iterator(); iterator.hasNext();) {
-+ final BlockEventData blockEventData = iterator.next();
-+ if (predicate.test(blockEventData)) {
-+ iterator.remove();
-+ }
-+ }
-+ }
-+
-+ public BlockEventData removeFirstBlockEvent() {
-+ BlockEventData ret;
-+ while (!this.blockEvents.isEmpty()) {
-+ ret = this.blockEvents.removeFirst();
-+ if (TickThread.isTickThreadFor(this.world, ret.pos())) {
-+ return ret;
-+ } // else: chunk must have been unloaded
-+ }
-+
-+ return null;
-+ }
-+
-+ public LevelTicks getBlockLevelTicks() {
-+ return this.blockLevelTicks;
-+ }
-+
-+ public LevelTicks getFluidLevelTicks() {
-+ return this.fluidLevelTicks;
-+ }
-+
-+ // tile entity ticking
-+ public void addBlockEntityTicker(final TickingBlockEntity ticker) {
-+ TickThread.ensureTickThread(this.world, ticker.getPos(), "Tile entity must be owned by current region");
-+
-+ (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker);
-+ }
-+
-+ public void seTtickingBlockEntities(final boolean to) {
-+ this.tickingBlockEntities = true;
-+ }
-+
-+ public List getBlockEntityTickers() {
-+ return this.blockEntityTickers;
-+ }
-+
-+ public void pushPendingTickingBlockEntities() {
-+ if (!this.pendingBlockEntityTickers.isEmpty()) {
-+ this.blockEntityTickers.addAll(this.pendingBlockEntityTickers);
-+ this.pendingBlockEntityTickers.clear();
-+ }
-+ }
-+
-+ // ticking chunks
-+ public void addEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) {
-+ this.entityTickingChunks.add(holder);
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+
-+ public void removeEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) {
-+ this.entityTickingChunks.remove(holder);
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+
-+ public ReferenceList getEntityTickingChunks() {
-+ return this.entityTickingChunks;
-+ }
-+
-+ public void addTickingChunk(final ServerChunkCache.ChunkAndHolder holder) {
-+ this.tickingChunks.add(holder);
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+
-+ public void removeTickingChunk(final ServerChunkCache.ChunkAndHolder holder) {
-+ this.tickingChunks.remove(holder);
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+
-+ public ReferenceList getTickingChunks() {
-+ return this.tickingChunks;
-+ }
-+
-+ public void addChunk(final ServerChunkCache.ChunkAndHolder holder) {
-+ this.chunks.add(holder);
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+
-+ public void removeChunk(final ServerChunkCache.ChunkAndHolder holder) {
-+ this.chunks.remove(holder);
-+ TickRegions.RegionStats.updateCurrentRegion();
-+ }
-+
-+ public ReferenceList getChunks() {
-+ return this.chunks;
-+ }
-+
-+ public int getEntityTickingChunkCount() {
-+ return this.entityTickingChunks.size();
-+ }
-+
-+ public int getChunkCount() {
-+ return this.chunks.size();
-+ }
-+}
-diff --git a/io/papermc/paper/threadedregions/Schedule.java b/io/papermc/paper/threadedregions/Schedule.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..112d24a93bddf3d81c9176c05340c94ecd1a40a3
---- /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;
-+ }
-+}
-diff --git a/io/papermc/paper/threadedregions/TeleportUtils.java b/io/papermc/paper/threadedregions/TeleportUtils.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..74ac328bf8d5f762f7060a6c5d49089dee1ddaea
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/TeleportUtils.java
-@@ -0,0 +1,70 @@
-+package io.papermc.paper.threadedregions;
-+
-+import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.phys.Vec3;
-+import org.bukkit.Location;
-+import org.bukkit.craftbukkit.CraftWorld;
-+import org.bukkit.event.player.PlayerTeleportEvent;
-+import java.util.function.Consumer;
-+
-+public final class TeleportUtils {
-+
-+ public static void teleport(final Entity from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch,
-+ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) {
-+ // retrieve coordinates
-+ final CallbackCompletable positionCompletable = new CallbackCompletable<>();
-+
-+ positionCompletable.addWaiter(
-+ (final Location loc, final Throwable thr) -> {
-+ if (loc == null) {
-+ if (onComplete != null) {
-+ onComplete.accept(null);
-+ }
-+ return;
-+ }
-+ final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule(
-+ (final Entity realFrom) -> {
-+ final Vec3 pos = new Vec3(
-+ loc.getX(), loc.getY(), loc.getZ()
-+ );
-+ (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync(
-+ ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null,
-+ cause, teleportFlags, onComplete
-+ );
-+ },
-+ (final Entity retired) -> {
-+ if (onComplete != null) {
-+ onComplete.accept(null);
-+ }
-+ },
-+ 1L
-+ );
-+ if (!scheduled) {
-+ if (onComplete != null) {
-+ onComplete.accept(null);
-+ }
-+ }
-+ }
-+ );
-+
-+ final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule(
-+ (final Entity target) -> {
-+ positionCompletable.complete(target.getBukkitEntity().getLocation());
-+ },
-+ (final Entity retired) -> {
-+ if (onComplete != null) {
-+ onComplete.accept(null);
-+ }
-+ },
-+ 1L
-+ );
-+ if (!scheduled) {
-+ if (onComplete != null) {
-+ onComplete.accept(null);
-+ }
-+ }
-+ }
-+
-+ private TeleportUtils() {}
-+}
-diff --git a/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/io/papermc/paper/threadedregions/ThreadedRegionizer.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..8e1b1df1c889d9235b10b86fc4cedbc06b7885c2
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/ThreadedRegionizer.java
-@@ -0,0 +1,1405 @@
-+package io.papermc.paper.threadedregions;
-+
-+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
-+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
-+import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
-+import com.destroystokyo.paper.util.SneakyThrow;
-+import com.mojang.logging.LogUtils;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.longs.LongArrayList;
-+import it.unimi.dsi.fastutil.longs.LongComparator;
-+import it.unimi.dsi.fastutil.longs.LongIterator;
-+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
-+import net.minecraft.core.BlockPos;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.entity.Entity;
-+import net.minecraft.world.level.ChunkPos;
-+import org.slf4j.Logger;
-+import java.lang.invoke.VarHandle;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.Iterator;
-+import java.util.List;
-+import java.util.Set;
-+import java.util.concurrent.atomic.AtomicLong;
-+import java.util.concurrent.locks.StampedLock;
-+import java.util.function.BooleanSupplier;
-+import java.util.function.Consumer;
-+
-+public final class ThreadedRegionizer, S extends ThreadedRegionizer.ThreadedRegionSectionData> {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+
-+ public final int regionSectionChunkSize;
-+ public final int sectionChunkShift;
-+ public final int minSectionRecalcCount;
-+ public final int emptySectionCreateRadius;
-+ public final int regionSectionMergeRadius;
-+ public final double maxDeadRegionPercent;
-+ public final ServerLevel world;
-+
-+ private final SWMRLong2ObjectHashTable> sections = new SWMRLong2ObjectHashTable<>();
-+ private final SWMRLong2ObjectHashTable> regionsById = new SWMRLong2ObjectHashTable<>();
-+ private final RegionCallbacks callbacks;
-+ private final StampedLock regionLock = new StampedLock();
-+ private Thread writeLockOwner;
-+
-+ /*
-+ static final record Operation(String type, int chunkX, int chunkZ) {}
-+ private final MultiThreadedQueue ops = new MultiThreadedQueue<>();
-+ */
-+
-+ /*
-+ * See REGION_LOGIC.md for complete details on what this class is doing
-+ */
-+
-+ public ThreadedRegionizer(final int minSectionRecalcCount, final double maxDeadRegionPercent,
-+ final int emptySectionCreateRadius, final int regionSectionMergeRadius,
-+ final int regionSectionChunkShift, final ServerLevel world,
-+ final RegionCallbacks callbacks) {
-+ if (emptySectionCreateRadius <= 0) {
-+ throw new IllegalStateException("Region section create radius must be > 0");
-+ }
-+ if (regionSectionMergeRadius <= 0) {
-+ throw new IllegalStateException("Region section merge radius must be > 0");
-+ }
-+ this.regionSectionChunkSize = 1 << regionSectionChunkShift;
-+ this.sectionChunkShift = regionSectionChunkShift;
-+ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount);
-+ this.maxDeadRegionPercent = maxDeadRegionPercent;
-+ this.emptySectionCreateRadius = emptySectionCreateRadius;
-+ this.regionSectionMergeRadius = regionSectionMergeRadius;
-+ this.world = world;
-+ this.callbacks = callbacks;
-+ //this.loadTestData();
-+ }
-+
-+ /*
-+ private static String substr(String val, String prefix, int from) {
-+ int idx = val.indexOf(prefix, from) + prefix.length();
-+ int idx2 = val.indexOf(',', idx);
-+ if (idx2 == -1) {
-+ idx2 = val.indexOf(']', idx);
-+ }
-+ return val.substring(idx, idx2);
-+ }
-+
-+ private void loadTestData() {
-+ if (true) {
-+ return;
-+ }
-+ try {
-+ final JsonArray arr = JsonParser.parseReader(new FileReader("test.json")).getAsJsonArray();
-+
-+ List ops = new ArrayList<>();
-+
-+ for (JsonElement elem : arr) {
-+ JsonObject obj = elem.getAsJsonObject();
-+ String val = obj.get("value").getAsString();
-+
-+ String type = substr(val, "type=", 0);
-+ String x = substr(val, "chunkX=", 0);
-+ String z = substr(val, "chunkZ=", 0);
-+
-+ ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z)));
-+ }
-+
-+ for (Operation op : ops) {
-+ switch (op.type) {
-+ case "add": {
-+ this.addChunk(op.chunkX, op.chunkZ);
-+ break;
-+ }
-+ case "remove": {
-+ this.removeChunk(op.chunkX, op.chunkZ);
-+ break;
-+ }
-+ case "mark_ticking": {
-+ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking();
-+ break;
-+ }
-+ case "rel_region": {
-+ if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) {
-+ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking();
-+ }
-+ break;
-+ }
-+ }
-+ }
-+
-+ } catch (final Exception ex) {
-+ throw new IllegalStateException(ex);
-+ }
-+ }
-+ */
-+
-+ public void acquireReadLock() {
-+ this.regionLock.readLock();
-+ }
-+
-+ public void releaseReadLock() {
-+ this.regionLock.tryUnlockRead();
-+ }
-+
-+ private void acquireWriteLock() {
-+ final Thread currentThread = Thread.currentThread();
-+ if (this.writeLockOwner == currentThread) {
-+ throw new IllegalStateException("Cannot recursively operate in the regioniser");
-+ }
-+ this.regionLock.writeLock();
-+ this.writeLockOwner = currentThread;
-+ }
-+
-+ private void releaseWriteLock() {
-+ this.writeLockOwner = null;
-+ this.regionLock.tryUnlockWrite();
-+ }
-+
-+ private void onRegionCreate(final ThreadedRegion region) {
-+ final ThreadedRegion conflict;
-+ if ((conflict = this.regionsById.putIfAbsent(region.id, region)) != null) {
-+ throw new IllegalStateException("Region " + region + " is already mapped to " + conflict);
-+ }
-+ }
-+
-+ private void onRegionDestroy(final ThreadedRegion region) {
-+ final ThreadedRegion removed = this.regionsById.remove(region.id);
-+ if (removed != region) {
-+ throw new IllegalStateException("Expected to remove " + region + ", but removed " + removed);
-+ }
-+ }
-+
-+ public int getSectionCoordinate(final int chunkCoordinate) {
-+ return chunkCoordinate >> this.sectionChunkShift;
-+ }
-+
-+ public long getSectionKey(final BlockPos pos) {
-+ return CoordinateUtils.getChunkKey((pos.getX() >> 4) >> this.sectionChunkShift, (pos.getZ() >> 4) >> this.sectionChunkShift);
-+ }
-+
-+ public long getSectionKey(final ChunkPos pos) {
-+ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift);
-+ }
-+
-+ public long getSectionKey(final Entity entity) {
-+ final ChunkPos pos = entity.chunkPosition();
-+ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift);
-+ }
-+
-+ public void computeForAllRegions(final Consumer super ThreadedRegion> consumer) {
-+ this.regionLock.readLock();
-+ try {
-+ this.regionsById.forEachValue(consumer);
-+ } finally {
-+ this.regionLock.tryUnlockRead();
-+ }
-+ }
-+
-+ public void computeForAllRegionsUnsynchronised(final Consumer super ThreadedRegion> consumer) {
-+ this.regionsById.forEachValue(consumer);
-+ }
-+
-+ public int computeForRegions(final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ,
-+ final Consumer>> consumer) {
-+ final int shift = this.sectionChunkShift;
-+ final int fromSectionX = fromChunkX >> shift;
-+ final int fromSectionZ = fromChunkZ >> shift;
-+ final int toSectionX = toChunkX >> shift;
-+ final int toSectionZ = toChunkZ >> shift;
-+ this.acquireWriteLock();
-+ try {
-+ final ReferenceOpenHashSet> set = new ReferenceOpenHashSet<>();
-+
-+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) {
-+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) {
-+ final ThreadedRegionSection section = this.sections.get(CoordinateUtils.getChunkKey(currX, currZ));
-+ if (section != null) {
-+ set.add(section.getRegionPlain());
-+ }
-+ }
-+ }
-+
-+ consumer.accept(set);
-+
-+ return set.size();
-+ } finally {
-+ this.releaseWriteLock();
-+ }
-+ }
-+
-+ public ThreadedRegion getRegionAtUnsynchronised(final int chunkX, final int chunkZ) {
-+ final int sectionX = chunkX >> this.sectionChunkShift;
-+ final int sectionZ = chunkZ >> this.sectionChunkShift;
-+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+
-+ final ThreadedRegionSection section = this.sections.get(sectionKey);
-+
-+ return section == null ? null : section.getRegion();
-+ }
-+
-+ public ThreadedRegion getRegionAtSynchronised(final int chunkX, final int chunkZ) {
-+ final int sectionX = chunkX >> this.sectionChunkShift;
-+ final int sectionZ = chunkZ >> this.sectionChunkShift;
-+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+
-+ // try an optimistic read
-+ {
-+ final long readAttempt = this.regionLock.tryOptimisticRead();
-+ final ThreadedRegionSection optimisticSection = this.sections.get(sectionKey);
-+ final ThreadedRegion optimisticRet =
-+ optimisticSection == null ? null : optimisticSection.getRegionPlain();
-+ if (this.regionLock.validate(readAttempt)) {
-+ return optimisticRet;
-+ }
-+ }
-+
-+ // failed, fall back to acquiring the lock
-+ this.regionLock.readLock();
-+ try {
-+ final ThreadedRegionSection section = this.sections.get(sectionKey);
-+
-+ return section == null ? null : section.getRegionPlain();
-+ } finally {
-+ this.regionLock.tryUnlockRead();
-+ }
-+ }
-+
-+ /**
-+ * Adds a chunk to the regioniser. Note that it is illegal to add a chunk unless
-+ * addChunk has not been called for it or removeChunk has been previously called.
-+ *
-+ *
-+ * Note that it is illegal to additionally call addChunk or removeChunk for the same
-+ * region section in parallel.
-+ *
-+ */
-+ public void addChunk(final int chunkX, final int chunkZ) {
-+ final int sectionX = chunkX >> this.sectionChunkShift;
-+ final int sectionZ = chunkZ >> this.sectionChunkShift;
-+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+
-+ // Given that for each section, no addChunk/removeChunk can occur in parallel,
-+ // we can avoid the lock IF the section exists AND it has a non-zero chunk count.
-+ {
-+ final ThreadedRegionSection existing = this.sections.get(sectionKey);
-+ if (existing != null && !existing.isEmpty()) {
-+ existing.addChunk(chunkX, chunkZ);
-+ return;
-+ } // else: just acquire the write lock
-+ }
-+
-+ this.acquireWriteLock();
-+ try {
-+ ThreadedRegionSection section = this.sections.get(sectionKey);
-+
-+ List> newSections = new ArrayList<>();
-+
-+ if (section == null) {
-+ // no section at all
-+ section = new ThreadedRegionSection<>(sectionX, sectionZ, this, chunkX, chunkZ);
-+ this.sections.put(sectionKey, section);
-+ newSections.add(section);
-+ } else {
-+ section.addChunk(chunkX, chunkZ);
-+ }
-+ // due to the fast check from above, we know the section is empty whether we needed to create it or not
-+
-+ // enforce the adjacency invariant by creating / updating neighbour sections
-+ final int createRadius = this.emptySectionCreateRadius;
-+ final int searchRadius = createRadius + this.regionSectionMergeRadius;
-+ ReferenceOpenHashSet> nearbyRegions = null;
-+ for (int dx = -searchRadius; dx <= searchRadius; ++dx) {
-+ for (int dz = -searchRadius; dz <= searchRadius; ++dz) {
-+ if ((dx | dz) == 0) {
-+ continue;
-+ }
-+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz));
-+ final boolean inCreateRange = squareDistance <= createRadius;
-+
-+ final int neighbourX = dx + sectionX;
-+ final int neighbourZ = dz + sectionZ;
-+ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
-+
-+ ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey);
-+
-+ if (neighbourSection != null) {
-+ if (nearbyRegions == null) {
-+ nearbyRegions = new ReferenceOpenHashSet<>(((searchRadius * 2 + 1) * (searchRadius * 2 + 1)) >> 1);
-+ }
-+ nearbyRegions.add(neighbourSection.getRegionPlain());
-+ }
-+
-+ if (!inCreateRange) {
-+ continue;
-+ }
-+
-+ // we need to ensure the section exists
-+ if (neighbourSection != null) {
-+ // nothing else to do
-+ neighbourSection.incrementNonEmptyNeighbours();
-+ continue;
-+ }
-+ neighbourSection = new ThreadedRegionSection<>(neighbourX, neighbourZ, this, 1);
-+ if (null != this.sections.put(neighbourKey, neighbourSection)) {
-+ throw new IllegalStateException("Failed to insert new section");
-+ }
-+ newSections.add(neighbourSection);
-+ }
-+ }
-+
-+ if (newSections.isEmpty()) {
-+ // if we didn't add any sections, then we don't need to merge any regions or create a region
-+ return;
-+ }
-+
-+ final ThreadedRegion regionOfInterest;
-+ final boolean regionOfInterestAlive;
-+ if (nearbyRegions == null) {
-+ // we can simply create a new region, don't have neighbours to worry about merging into
-+ regionOfInterest = new ThreadedRegion<>(this);
-+ regionOfInterestAlive = true;
-+
-+ for (int i = 0, len = newSections.size(); i < len; ++i) {
-+ regionOfInterest.addSection(newSections.get(i));
-+ }
-+
-+ // only call create callback after adding sections
-+ regionOfInterest.onCreate();
-+ } else {
-+ // need to merge the regions
-+ ThreadedRegion firstUnlockedRegion = null;
-+
-+ for (final ThreadedRegion region : nearbyRegions) {
-+ if (region.isTicking()) {
-+ continue;
-+ }
-+ firstUnlockedRegion = region;
-+ if (firstUnlockedRegion.state == ThreadedRegion.STATE_READY && (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty())) {
-+ throw new IllegalStateException("Illegal state for unlocked region " + firstUnlockedRegion);
-+ }
-+ break;
-+ }
-+
-+ if (firstUnlockedRegion != null) {
-+ regionOfInterest = firstUnlockedRegion;
-+ } else {
-+ regionOfInterest = new ThreadedRegion<>(this);
-+ }
-+
-+ for (int i = 0, len = newSections.size(); i < len; ++i) {
-+ regionOfInterest.addSection(newSections.get(i));
-+ }
-+
-+ // only call create callback after adding sections
-+ if (firstUnlockedRegion == null) {
-+ regionOfInterest.onCreate();
-+ }
-+
-+ if (firstUnlockedRegion != null && nearbyRegions.size() == 1) {
-+ // nothing to do further, no need to merge anything
-+ return;
-+ }
-+
-+ // we need to now tell all the other regions to merge into the region we just created,
-+ // and to merge all the ones we can immediately
-+
-+ for (final ThreadedRegion region : nearbyRegions) {
-+ if (region == regionOfInterest) {
-+ continue;
-+ }
-+
-+ if (!region.killAndMergeInto(regionOfInterest)) {
-+ // note: the region may already be a merge target
-+ regionOfInterest.mergeIntoLater(region);
-+ }
-+ }
-+
-+ if (firstUnlockedRegion != null && firstUnlockedRegion.state == ThreadedRegion.STATE_READY) {
-+ // we need to retire this region if the merges added other pending merges
-+ if (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty()) {
-+ firstUnlockedRegion.state = ThreadedRegion.STATE_TRANSIENT;
-+ this.callbacks.onRegionInactive(firstUnlockedRegion);
-+ }
-+ }
-+
-+ // need to set alive if we created it and there are no pending merges
-+ regionOfInterestAlive = firstUnlockedRegion == null && regionOfInterest.mergeIntoLater.isEmpty() && regionOfInterest.expectingMergeFrom.isEmpty();
-+ }
-+
-+ if (regionOfInterestAlive) {
-+ regionOfInterest.state = ThreadedRegion.STATE_READY;
-+ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) {
-+ throw new IllegalStateException("Should not happen on region " + this);
-+ }
-+ this.callbacks.onRegionActive(regionOfInterest);
-+ }
-+
-+ if (regionOfInterest.state == ThreadedRegion.STATE_READY) {
-+ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) {
-+ throw new IllegalStateException("Should not happen on region " + this);
-+ }
-+ }
-+ } catch (final Throwable throwable) {
-+ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable);
-+ SneakyThrow.sneaky(throwable);
-+ return; // unreachable
-+ } finally {
-+ this.releaseWriteLock();
-+ }
-+ }
-+
-+ public void removeChunk(final int chunkX, final int chunkZ) {
-+ final int sectionX = chunkX >> this.sectionChunkShift;
-+ final int sectionZ = chunkZ >> this.sectionChunkShift;
-+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+
-+ // Given that for each section, no addChunk/removeChunk can occur in parallel,
-+ // we can avoid the lock IF the section exists AND it has a chunk count > 1
-+ final ThreadedRegionSection section = this.sections.get(sectionKey);
-+ if (section == null) {
-+ throw new IllegalStateException("Chunk (" + chunkX + "," + chunkZ + ") has no section");
-+ }
-+ if (!section.hasOnlyOneChunk()) {
-+ // chunk will not go empty, so we don't need to acquire the lock
-+ section.removeChunk(chunkX, chunkZ);
-+ return;
-+ }
-+
-+ this.acquireWriteLock();
-+ try {
-+ section.removeChunk(chunkX, chunkZ);
-+
-+ final int searchRadius = this.emptySectionCreateRadius;
-+ for (int dx = -searchRadius; dx <= searchRadius; ++dx) {
-+ for (int dz = -searchRadius; dz <= searchRadius; ++dz) {
-+ if ((dx | dz) == 0) {
-+ continue;
-+ }
-+
-+ final int neighbourX = dx + sectionX;
-+ final int neighbourZ = dz + sectionZ;
-+ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ);
-+
-+ final ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey);
-+
-+ // should be non-null here always
-+ neighbourSection.decrementNonEmptyNeighbours();
-+ }
-+ }
-+ } catch (final Throwable throwable) {
-+ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable);
-+ SneakyThrow.sneaky(throwable);
-+ return; // unreachable
-+ } finally {
-+ this.releaseWriteLock();
-+ }
-+ }
-+
-+ // must hold regionLock
-+ private void onRegionRelease(final ThreadedRegion region) {
-+ if (!region.mergeIntoLater.isEmpty()) {
-+ throw new IllegalStateException("Region " + region + " should not have any regions to merge into!");
-+ }
-+
-+ final boolean hasExpectingMerges = !region.expectingMergeFrom.isEmpty();
-+
-+ // is this region supposed to merge into any other region?
-+ if (hasExpectingMerges) {
-+ // merge the regions into this one
-+ final ReferenceOpenHashSet> expectingMergeFrom = region.expectingMergeFrom.clone();
-+ for (final ThreadedRegion mergeFrom : expectingMergeFrom) {
-+ if (!mergeFrom.killAndMergeInto(region)) {
-+ throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + region);
-+ }
-+ }
-+
-+ if (!region.expectingMergeFrom.isEmpty()) {
-+ throw new IllegalStateException("Region " + region + " should no longer have merge requests after mering from " + expectingMergeFrom);
-+ }
-+
-+ if (!region.mergeIntoLater.isEmpty()) {
-+ // There is another nearby ticking region that we need to merge into
-+ region.state = ThreadedRegion.STATE_TRANSIENT;
-+ this.callbacks.onRegionInactive(region);
-+ // return to avoid removing dead sections or splitting, these actions will be performed
-+ // by the region we merge into
-+ return;
-+ }
-+ }
-+
-+ // now check whether we need to recalculate regions
-+ final boolean removeDeadSections = hasExpectingMerges || region.hasNoAliveSections()
-+ || (region.sectionByKey.size() >= this.minSectionRecalcCount && region.getDeadSectionPercent() >= this.maxDeadRegionPercent);
-+ final boolean removedDeadSections = removeDeadSections && !region.deadSections.isEmpty();
-+ if (removeDeadSections) {
-+ // kill dead sections
-+ for (final ThreadedRegionSection deadSection : region.deadSections) {
-+ final long key = CoordinateUtils.getChunkKey(deadSection.sectionX, deadSection.sectionZ);
-+
-+ if (!deadSection.isEmpty()) {
-+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!");
-+ }
-+ if (deadSection.hasNonEmptyNeighbours()) {
-+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has non-empty neighbours!");
-+ }
-+ if (!region.sectionByKey.remove(key, deadSection)) {
-+ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection);
-+ }
-+ if (this.sections.remove(key) != deadSection) {
-+ throw new IllegalStateException("Cannot remove dead section '" +
-+ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + this.sections.get(key));
-+ }
-+ }
-+ region.deadSections.clear();
-+ }
-+
-+ // if we removed dead sections, we should check if the region can be split into smaller ones
-+ // otherwise, the region remains alive
-+ if (!removedDeadSections) {
-+ // didn't remove dead sections, don't check for split
-+ region.state = ThreadedRegion.STATE_READY;
-+ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) {
-+ throw new IllegalStateException("Illegal state " + region);
-+ }
-+ return;
-+ }
-+
-+ // first, we need to build copy of coordinate->section map of all sections in recalculate
-+ final Long2ReferenceOpenHashMap> recalculateSections = region.sectionByKey.clone();
-+
-+ if (recalculateSections.isEmpty()) {
-+ // looks like the region's sections were all dead, and now there is no region at all
-+ region.state = ThreadedRegion.STATE_DEAD;
-+ region.onRemove(true);
-+ return;
-+ }
-+
-+ // merge radius is max, since recalculateSections includes the dead or empty sections
-+ final int mergeRadius = Math.max(this.regionSectionMergeRadius, this.emptySectionCreateRadius);
-+
-+ final List>> newRegions = new ArrayList<>();
-+ while (!recalculateSections.isEmpty()) {
-+ // select any section, then BFS around it to find all of its neighbours to form a region
-+ // once no more neighbours are found, the region is complete
-+ final List> currRegion = new ArrayList<>();
-+ final Iterator> firstIterator = recalculateSections.values().iterator();
-+
-+ currRegion.add(firstIterator.next());
-+ firstIterator.remove();
-+ search_loop:
-+ for (int idx = 0; idx < currRegion.size(); ++idx) {
-+ final ThreadedRegionSection curr = currRegion.get(idx);
-+ final int centerX = curr.sectionX;
-+ final int centerZ = curr.sectionZ;
-+
-+ // find neighbours in radius
-+ for (int dz = -mergeRadius; dz <= mergeRadius; ++dz) {
-+ for (int dx = -mergeRadius; dx <= mergeRadius; ++dx) {
-+ if ((dx | dz) == 0) {
-+ continue;
-+ }
-+
-+ final ThreadedRegionSection section = recalculateSections.remove(CoordinateUtils.getChunkKey(dx + centerX, dz + centerZ));
-+ if (section == null) {
-+ continue;
-+ }
-+
-+ currRegion.add(section);
-+
-+ if (recalculateSections.isEmpty()) {
-+ // no point in searching further
-+ break search_loop;
-+ }
-+ }
-+ }
-+ }
-+
-+ newRegions.add(currRegion);
-+ }
-+
-+ // now we have split the regions into separate parts, we can split recalculate
-+
-+ if (newRegions.size() == 1) {
-+ // no need to split anything, we're done here
-+ region.state = ThreadedRegion.STATE_READY;
-+ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) {
-+ throw new IllegalStateException("Illegal state " + region);
-+ }
-+ return;
-+ }
-+
-+ final List> newRegionObjects = new ArrayList<>(newRegions.size());
-+ for (int i = 0, len = newRegions.size(); i < len; ++i) {
-+ newRegionObjects.add(new ThreadedRegion<>(this));
-+ }
-+
-+ this.callbacks.preSplit(region, newRegionObjects);
-+
-+ // need to split the region, so we need to kill the old one first
-+ region.state = ThreadedRegion.STATE_DEAD;
-+ region.onRemove(true);
-+
-+ // create new regions
-+ final Long2ReferenceOpenHashMap> newRegionsMap = new Long2ReferenceOpenHashMap<>();
-+ final ReferenceOpenHashSet> newRegionsSet = new ReferenceOpenHashSet<>(newRegionObjects);
-+
-+ for (int i = 0, len = newRegions.size(); i < len; i++) {
-+ final List> sections = newRegions.get(i);
-+ final ThreadedRegion newRegion = newRegionObjects.get(i);
-+
-+ for (final ThreadedRegionSection section : sections) {
-+ section.setRegionRelease(null);
-+ newRegion.addSection(section);
-+ final ThreadedRegion curr = newRegionsMap.putIfAbsent(section.sectionKey, newRegion);
-+ if (curr != null) {
-+ throw new IllegalStateException("Expected no region at " + section + ", but got " + curr + ", should have put " + newRegion);
-+ }
-+ }
-+ }
-+
-+ region.split(newRegionsMap, newRegionsSet);
-+
-+ // only after invoking data callbacks
-+
-+ for (final ThreadedRegion newRegion : newRegionsSet) {
-+ newRegion.state = ThreadedRegion.STATE_READY;
-+ if (!newRegion.expectingMergeFrom.isEmpty() || !newRegion.mergeIntoLater.isEmpty()) {
-+ throw new IllegalStateException("Illegal state " + newRegion);
-+ }
-+ newRegion.onCreate();
-+ this.callbacks.onRegionActive(newRegion);
-+ }
-+ }
-+
-+ public static final class ThreadedRegion, S extends ThreadedRegionSectionData> {
-+
-+ private static final AtomicLong REGION_ID_GENERATOR = new AtomicLong();
-+
-+ private static final int STATE_TRANSIENT = 0;
-+ private static final int STATE_READY = 1;
-+ private static final int STATE_TICKING = 2;
-+ private static final int STATE_DEAD = 3;
-+
-+ public final long id;
-+
-+ private int state;
-+
-+ private final Long2ReferenceOpenHashMap> sectionByKey = new Long2ReferenceOpenHashMap<>();
-+ private final ReferenceOpenHashSet> deadSections = new ReferenceOpenHashSet<>();
-+
-+ public final ThreadedRegionizer regioniser;
-+
-+ private final R data;
-+
-+ private final ReferenceOpenHashSet> mergeIntoLater = new ReferenceOpenHashSet<>();
-+ private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>();
-+
-+ public ThreadedRegion(final ThreadedRegionizer regioniser) {
-+ this.regioniser = regioniser;
-+ this.id = REGION_ID_GENERATOR.getAndIncrement();
-+ this.state = STATE_TRANSIENT;
-+ this.data = regioniser.callbacks.createNewData(this);
-+ }
-+
-+ public LongArrayList getOwnedSections() {
-+ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread();
-+ if (lock) {
-+ this.regioniser.regionLock.readLock();
-+ }
-+ try {
-+ final LongArrayList ret = new LongArrayList(this.sectionByKey.size());
-+ ret.addAll(this.sectionByKey.keySet());
-+
-+ return ret;
-+ } finally {
-+ if (lock) {
-+ this.regioniser.regionLock.tryUnlockRead();
-+ }
-+ }
-+ }
-+
-+ /**
-+ * returns an iterator directly over the sections map. This is only to be used by a thread which is _ticking_
-+ * 'this' region.
-+ */
-+ public LongIterator getOwnedSectionsUnsynchronised() {
-+ return this.sectionByKey.keySet().iterator();
-+ }
-+
-+ public LongArrayList getOwnedChunks() {
-+ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread();
-+ if (lock) {
-+ this.regioniser.regionLock.readLock();
-+ }
-+ try {
-+ final LongArrayList ret = new LongArrayList();
-+ for (final ThreadedRegionSection section : this.sectionByKey.values()) {
-+ ret.addAll(section.getChunks());
-+ }
-+
-+ return ret;
-+ } finally {
-+ if (lock) {
-+ this.regioniser.regionLock.tryUnlockRead();
-+ }
-+ }
-+ }
-+
-+ public Long getCenterSection() {
-+ final LongArrayList sections = this.getOwnedSections();
-+
-+ final LongComparator comparator = (final long k1, final long k2) -> {
-+ final int x1 = CoordinateUtils.getChunkX(k1);
-+ final int x2 = CoordinateUtils.getChunkX(k2);
-+
-+ final int z1 = CoordinateUtils.getChunkZ(x1);
-+ final int z2 = CoordinateUtils.getChunkZ(x2);
-+
-+ final int zCompare = Integer.compare(z1, z2);
-+ if (zCompare != 0) {
-+ return zCompare;
-+ }
-+
-+ return Integer.compare(x1, x2);
-+ };
-+
-+ // note: regions don't always have a chunk section at this point, because the region may have been killed
-+ if (sections.isEmpty()) {
-+ return null;
-+ }
-+
-+ sections.sort(comparator);
-+
-+ return Long.valueOf(sections.getLong(sections.size() >> 1));
-+ }
-+
-+ public ChunkPos getCenterChunk() {
-+ final LongArrayList chunks = this.getOwnedChunks();
-+
-+ final LongComparator comparator = (final long k1, final long k2) -> {
-+ final int x1 = CoordinateUtils.getChunkX(k1);
-+ final int x2 = CoordinateUtils.getChunkX(k2);
-+
-+ final int z1 = CoordinateUtils.getChunkZ(k1);
-+ final int z2 = CoordinateUtils.getChunkZ(k2);
-+
-+ final int zCompare = Integer.compare(z1, z2);
-+ if (zCompare != 0) {
-+ return zCompare;
-+ }
-+
-+ return Integer.compare(x1, x2);
-+ };
-+ chunks.sort(comparator);
-+
-+ // note: regions don't always have a chunk at this point, because the region may have been killed
-+ if (chunks.isEmpty()) {
-+ return null;
-+ }
-+
-+ final long middle = chunks.getLong(chunks.size() >> 1);
-+
-+ return new ChunkPos(CoordinateUtils.getChunkX(middle), CoordinateUtils.getChunkZ(middle));
-+ }
-+
-+ private void onCreate() {
-+ this.regioniser.onRegionCreate(this);
-+ this.regioniser.callbacks.onRegionCreate(this);
-+ }
-+
-+ private void onRemove(final boolean wasActive) {
-+ if (wasActive) {
-+ this.regioniser.callbacks.onRegionInactive(this);
-+ }
-+ this.regioniser.callbacks.onRegionDestroy(this);
-+ this.regioniser.onRegionDestroy(this);
-+ }
-+
-+ private final boolean hasNoAliveSections() {
-+ return this.deadSections.size() == this.sectionByKey.size();
-+ }
-+
-+ private final double getDeadSectionPercent() {
-+ return (double)this.deadSections.size() / (double)this.sectionByKey.size();
-+ }
-+
-+ private void split(final Long2ReferenceOpenHashMap> into, final ReferenceOpenHashSet> regions) {
-+ if (this.data != null) {
-+ this.data.split(this.regioniser, into, regions);
-+ }
-+ }
-+
-+ boolean killAndMergeInto(final ThreadedRegion mergeTarget) {
-+ if (this.state == STATE_TICKING) {
-+ return false;
-+ }
-+
-+ this.regioniser.callbacks.preMerge(this, mergeTarget);
-+
-+ this.tryKill();
-+
-+ this.mergeInto(mergeTarget);
-+
-+ return true;
-+ }
-+
-+ private void mergeInto(final ThreadedRegion mergeTarget) {
-+ if (this == mergeTarget) {
-+ throw new IllegalStateException("Cannot merge a region onto itself");
-+ }
-+ if (!this.isDead()) {
-+ throw new IllegalStateException("Source region is not dead! Source " + this + ", target " + mergeTarget);
-+ } else if (mergeTarget.isDead()) {
-+ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget);
-+ }
-+
-+ for (final ThreadedRegionSection section : this.sectionByKey.values()) {
-+ section.setRegionRelease(null);
-+ mergeTarget.addSection(section);
-+ }
-+ for (final ThreadedRegionSection deadSection : this.deadSections) {
-+ if (this.sectionByKey.get(deadSection.sectionKey) != deadSection) {
-+ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this);
-+ }
-+ if (!mergeTarget.deadSections.add(deadSection)) {
-+ throw new IllegalStateException("Merge target contains dead section from source! Has " + deadSection + " from region " + this);
-+ }
-+ }
-+
-+ // forward merge expectations
-+ for (final ThreadedRegion region : this.expectingMergeFrom) {
-+ if (!region.mergeIntoLater.remove(this)) {
-+ throw new IllegalStateException("Region " + region + " was not supposed to merge into " + this + "?");
-+ }
-+ if (region != mergeTarget) {
-+ region.mergeIntoLater(mergeTarget);
-+ }
-+ }
-+
-+ // forward merge into
-+ for (final ThreadedRegion region : this.mergeIntoLater) {
-+ if (!region.expectingMergeFrom.remove(this)) {
-+ throw new IllegalStateException("Region " + this + " was not supposed to merge into " + region + "?");
-+ }
-+ if (region != mergeTarget) {
-+ mergeTarget.mergeIntoLater(region);
-+ }
-+ }
-+
-+ // finally, merge data
-+ if (this.data != null) {
-+ this.data.mergeInto(mergeTarget);
-+ }
-+ }
-+
-+ private void mergeIntoLater(final ThreadedRegion region) {
-+ if (region.isDead()) {
-+ throw new IllegalStateException("Trying to merge later into a dead region: " + region);
-+ }
-+ final boolean add1, add2;
-+ if ((add1 = this.mergeIntoLater.add(region)) != (add2 = region.expectingMergeFrom.add(this))) {
-+ throw new IllegalStateException("Inconsistent state between target merge " + region + " and this " + this + ": add1,add2:" + add1 + "," + add2);
-+ }
-+ }
-+
-+ private boolean tryKill() {
-+ switch (this.state) {
-+ case STATE_TRANSIENT: {
-+ this.state = STATE_DEAD;
-+ this.onRemove(false);
-+ return true;
-+ }
-+ case STATE_READY: {
-+ this.state = STATE_DEAD;
-+ this.onRemove(true);
-+ return true;
-+ }
-+ case STATE_TICKING: {
-+ return false;
-+ }
-+ case STATE_DEAD: {
-+ throw new IllegalStateException("Already dead");
-+ }
-+ default: {
-+ throw new IllegalStateException("Unknown state: " + this.state);
-+ }
-+ }
-+ }
-+
-+ private boolean isDead() {
-+ return this.state == STATE_DEAD;
-+ }
-+
-+ private boolean isTicking() {
-+ return this.state == STATE_TICKING;
-+ }
-+
-+ private void removeDeadSection(final ThreadedRegionSection section) {
-+ this.deadSections.remove(section);
-+ }
-+
-+ private void addDeadSection(final ThreadedRegionSection section) {
-+ this.deadSections.add(section);
-+ }
-+
-+ private void addSection(final ThreadedRegionSection section) {
-+ if (section.getRegionPlain() != null) {
-+ throw new IllegalStateException("Section already has region");
-+ }
-+ if (this.sectionByKey.putIfAbsent(section.sectionKey, section) != null) {
-+ throw new IllegalStateException("Already have section " + section + ", mapped to " + this.sectionByKey.get(section.sectionKey));
-+ }
-+ section.setRegionRelease(this);
-+ }
-+
-+ public R getData() {
-+ return this.data;
-+ }
-+
-+ public boolean tryMarkTicking(final BooleanSupplier abort) {
-+ this.regioniser.acquireWriteLock();
-+ try {
-+ if (this.state != STATE_READY || abort.getAsBoolean()) {
-+ return false;
-+ }
-+
-+ if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) {
-+ throw new IllegalStateException("Region " + this + " should not be ready");
-+ }
-+
-+ this.state = STATE_TICKING;
-+ return true;
-+ } finally {
-+ this.regioniser.releaseWriteLock();
-+ }
-+ }
-+
-+ public boolean markNotTicking() {
-+ this.regioniser.acquireWriteLock();
-+ try {
-+ if (this.state != STATE_TICKING) {
-+ throw new IllegalStateException("Attempting to release non-locked state");
-+ }
-+
-+ this.regioniser.onRegionRelease(this);
-+
-+ return this.state == STATE_READY;
-+ } catch (final Throwable throwable) {
-+ LOGGER.error("Failed to release region " + this, throwable);
-+ SneakyThrow.sneaky(throwable);
-+ return false; // unreachable
-+ } finally {
-+ this.regioniser.releaseWriteLock();
-+ }
-+ }
-+
-+ @Override
-+ public String toString() {
-+ final StringBuilder ret = new StringBuilder(128);
-+
-+ ret.append("ThreadedRegion{");
-+ ret.append("state=").append(this.state).append(',');
-+ // To avoid recursion in toString, maybe fix later?
-+ //ret.append("mergeIntoLater=").append(this.mergeIntoLater).append(',');
-+ //ret.append("expectingMergeFrom=").append(this.expectingMergeFrom).append(',');
-+
-+ ret.append("sectionCount=").append(this.sectionByKey.size()).append(',');
-+ ret.append("sections=[");
-+ for (final Iterator> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) {
-+ final ThreadedRegionSection section = iterator.next();
-+
-+ ret.append(section.toString());
-+ if (iterator.hasNext()) {
-+ ret.append(',');
-+ }
-+ }
-+ ret.append(']');
-+
-+ ret.append('}');
-+ return ret.toString();
-+ }
-+ }
-+
-+ public static final class ThreadedRegionSection, S extends ThreadedRegionSectionData> {
-+
-+ public final int sectionX;
-+ public final int sectionZ;
-+ public final long sectionKey;
-+ private final long[] chunksBitset;
-+ private int chunkCount;
-+ private int nonEmptyNeighbours;
-+
-+ private ThreadedRegion region;
-+ private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class);
-+
-+ public final ThreadedRegionizer regioniser;
-+
-+ private final int regionChunkShift;
-+ private final int regionChunkMask;
-+
-+ private final S data;
-+
-+ private ThreadedRegion getRegionPlain() {
-+ return (ThreadedRegion)REGION_HANDLE.get(this);
-+ }
-+
-+ private ThreadedRegion getRegionAcquire() {
-+ return (ThreadedRegion)REGION_HANDLE.getAcquire(this);
-+ }
-+
-+ private void setRegionRelease(final ThreadedRegion value) {
-+ REGION_HANDLE.setRelease(this, value);
-+ }
-+
-+ // creates an empty section with zero non-empty neighbours
-+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser) {
-+ this.sectionX = sectionX;
-+ this.sectionZ = sectionZ;
-+ this.sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
-+ this.chunksBitset = new long[Math.max(1, regioniser.regionSectionChunkSize * regioniser.regionSectionChunkSize / Long.SIZE)];
-+ this.regioniser = regioniser;
-+ this.regionChunkShift = regioniser.sectionChunkShift;
-+ this.regionChunkMask = regioniser.regionSectionChunkSize - 1;
-+ this.data = regioniser.callbacks
-+ .createNewSectionData(sectionX, sectionZ, this.regionChunkShift);
-+ }
-+
-+ // creates a section with an initial chunk with zero non-empty neighbours
-+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser,
-+ final int chunkXInit, final int chunkZInit) {
-+ this(sectionX, sectionZ, regioniser);
-+
-+ final int initIndex = this.getChunkIndex(chunkXInit, chunkZInit);
-+ this.chunkCount = 1;
-+ this.chunksBitset[initIndex >>> 6] = 1L << (initIndex & (Long.SIZE - 1)); // index / Long.SIZE
-+ }
-+
-+ // creates an empty section with the specified number of non-empty neighbours
-+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser,
-+ final int nonEmptyNeighbours) {
-+ this(sectionX, sectionZ, regioniser);
-+
-+ this.nonEmptyNeighbours = nonEmptyNeighbours;
-+ }
-+
-+ public LongArrayList getChunks() {
-+ final LongArrayList ret = new LongArrayList();
-+
-+ if (this.chunkCount == 0) {
-+ return ret;
-+ }
-+
-+ final int shift = this.regionChunkShift;
-+ final int mask = this.regionChunkMask;
-+ final int offsetX = this.sectionX << shift;
-+ final int offsetZ = this.sectionZ << shift;
-+
-+ final long[] bitset = this.chunksBitset;
-+ for (int arrIdx = 0, arrLen = bitset.length; arrIdx < arrLen; ++arrIdx) {
-+ long value = bitset[arrIdx];
-+
-+ for (int i = 0, bits = Long.bitCount(value); i < bits; ++i) {
-+ final int valueIdx = Long.numberOfTrailingZeros(value);
-+ value ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(value);
-+
-+ final int idx = valueIdx | (arrIdx << 6);
-+
-+ final int localX = idx & mask;
-+ final int localZ = (idx >>> shift) & mask;
-+
-+ ret.add(CoordinateUtils.getChunkKey(localX | offsetX, localZ | offsetZ));
-+ }
-+ }
-+
-+ return ret;
-+ }
-+
-+ private boolean isEmpty() {
-+ return this.chunkCount == 0;
-+ }
-+
-+ private boolean hasOnlyOneChunk() {
-+ return this.chunkCount == 1;
-+ }
-+
-+ public boolean hasNonEmptyNeighbours() {
-+ return this.nonEmptyNeighbours != 0;
-+ }
-+
-+ /**
-+ * Returns the section data associated with this region section. May be {@code null}.
-+ */
-+ public S getData() {
-+ return this.data;
-+ }
-+
-+ /**
-+ * Returns the region that owns this section. Unsynchronised access may produce outdateed or transient results.
-+ */
-+ public ThreadedRegion getRegion() {
-+ return this.getRegionAcquire();
-+ }
-+
-+ private int getChunkIndex(final int chunkX, final int chunkZ) {
-+ return (chunkX & this.regionChunkMask) | ((chunkZ & this.regionChunkMask) << this.regionChunkShift);
-+ }
-+
-+ private void markAlive() {
-+ this.getRegionPlain().removeDeadSection(this);
-+ }
-+
-+ private void markDead() {
-+ this.getRegionPlain().addDeadSection(this);
-+ }
-+
-+ private void incrementNonEmptyNeighbours() {
-+ if (++this.nonEmptyNeighbours == 1 && this.chunkCount == 0) {
-+ this.markAlive();
-+ }
-+ final int createRadius = this.regioniser.emptySectionCreateRadius;
-+ if (this.nonEmptyNeighbours >= ((createRadius * 2 + 1) * (createRadius * 2 + 1))) {
-+ throw new IllegalStateException("Non empty neighbours exceeded max value for radius " + createRadius);
-+ }
-+ }
-+
-+ private void decrementNonEmptyNeighbours() {
-+ if (--this.nonEmptyNeighbours == 0 && this.chunkCount == 0) {
-+ this.markDead();
-+ }
-+ if (this.nonEmptyNeighbours < 0) {
-+ throw new IllegalStateException("Non empty neighbours reached zero");
-+ }
-+ }
-+
-+ /**
-+ * Returns whether the chunk was zero. Effectively returns whether the caller needs to create
-+ * dead sections / increase non-empty neighbour count for neighbouring sections.
-+ */
-+ private boolean addChunk(final int chunkX, final int chunkZ) {
-+ final int index = this.getChunkIndex(chunkX, chunkZ);
-+ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE
-+ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1)));
-+ if (after == bitset) {
-+ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
-+ }
-+ final boolean notEmpty = ++this.chunkCount == 1;
-+ if (notEmpty && this.nonEmptyNeighbours == 0) {
-+ this.markAlive();
-+ }
-+ return notEmpty;
-+ }
-+
-+ /**
-+ * Returns whether the chunk count is now zero. Effectively returns whether
-+ * the caller needs to decrement the neighbour count for neighbouring sections.
-+ */
-+ private boolean removeChunk(final int chunkX, final int chunkZ) {
-+ final int index = this.getChunkIndex(chunkX, chunkZ);
-+ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE
-+ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1)));
-+ if (before == bitset) {
-+ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString());
-+ }
-+ final boolean empty = --this.chunkCount == 0;
-+ if (empty && this.nonEmptyNeighbours == 0) {
-+ this.markDead();
-+ }
-+ return empty;
-+ }
-+
-+ @Override
-+ public String toString() {
-+ return "RegionSection{" +
-+ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," +
-+ "chunkCount=" + this.chunkCount + "," +
-+ "chunksBitset=" + toString(this.chunksBitset) + "," +
-+ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," +
-+ "hash=" + this.hashCode() +
-+ "}";
-+ }
-+
-+ public String toStringWithRegion() {
-+ return "RegionSection{" +
-+ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," +
-+ "chunkCount=" + this.chunkCount + "," +
-+ "chunksBitset=" + toString(this.chunksBitset) + "," +
-+ "hash=" + this.hashCode() + "," +
-+ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," +
-+ "region=" + this.getRegionAcquire() +
-+ "}";
-+ }
-+
-+ private static String toString(final long[] array) {
-+ final StringBuilder ret = new StringBuilder();
-+ final char[] zeros = new char[Long.SIZE / 4];
-+ for (final long value : array) {
-+ // zero pad the hex string
-+ Arrays.fill(zeros, '0');
-+ final String string = Long.toHexString(value);
-+ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length());
-+
-+ ret.append(zeros);
-+ }
-+
-+ return ret.toString();
-+ }
-+ }
-+
-+ public static interface ThreadedRegionData, S extends ThreadedRegionSectionData> {
-+
-+ /**
-+ * Splits this region data into the specified regions set.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param regioniser Regioniser for which the regions reside in.
-+ * @param into A map of region section coordinate key to the region that owns the section.
-+ * @param regions The set of regions to split into.
-+ */
-+ public void split(final ThreadedRegionizer regioniser, final Long2ReferenceOpenHashMap> into,
-+ final ReferenceOpenHashSet> regions);
-+
-+ /**
-+ * Callback to merge {@code this} region data into the specified region. The state of the region is undefined
-+ * except that its region data is already created.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param into Specified region.
-+ */
-+ public void mergeInto(final ThreadedRegion into);
-+ }
-+
-+ public static interface ThreadedRegionSectionData {}
-+
-+ public static interface RegionCallbacks, S extends ThreadedRegionSectionData> {
-+
-+ /**
-+ * Creates new section data for the specified section x and section z.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param sectionX x coordinate of the section.
-+ * @param sectionZ z coordinate of the section.
-+ * @param sectionShift The signed right shift value that can be applied to any chunk coordinate that
-+ * produces a section coordinate.
-+ * @return New section data, may be {@code null}.
-+ */
-+ public S createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift);
-+
-+ /**
-+ * Creates new region data for the specified region.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param forRegion The region to create the data for.
-+ * @return New region data, may be {@code null}.
-+ */
-+ public R createNewData(final ThreadedRegion forRegion);
-+
-+ /**
-+ * Callback for when a region is created. This is invoked after the region is completely set up,
-+ * so its data and owned sections are reliable to inspect.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param region The region that was created.
-+ */
-+ public void onRegionCreate(final ThreadedRegion region);
-+
-+ /**
-+ * Callback for when a region is destroyed. This is invoked before the region is actually destroyed; so
-+ * its data and owned sections are reliable to inspect.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param region The region that is about to be destroyed.
-+ */
-+ public void onRegionDestroy(final ThreadedRegion region);
-+
-+ /**
-+ * Callback for when a region is considered "active." An active region x is a non-destroyed region which
-+ * is not scheduled to merge into another region y and there are no non-destroyed regions z which are
-+ * scheduled to merge into the region x. Equivalently, an active region is not directly adjacent to any
-+ * other region considering the regioniser's empty section radius.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param region The region that is now active.
-+ */
-+ public void onRegionActive(final ThreadedRegion region);
-+
-+ /**
-+ * Callback for when a region transistions becomes inactive. An inactive region is non-destroyed, but
-+ * has neighbouring adjacent regions considering the regioniser's empty section radius. Effectively,
-+ * an inactive region may not tick and needs to be merged into its neighbouring regions.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param region The region that is now inactive.
-+ */
-+ public void onRegionInactive(final ThreadedRegion region);
-+
-+ /**
-+ * Callback for when a region (from) is about to be merged into a target region (into). Note that
-+ * {@code from} is still alive and is a distinct region.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param from The region that will be merged into the target.
-+ * @param into The target of the merge.
-+ */
-+ public void preMerge(final ThreadedRegion from, final ThreadedRegion into);
-+
-+ /**
-+ * Callback for when a region (from) is about to be split into a list of target region (into). Note that
-+ * {@code from} is still alive, while the list of target regions are not initialised.
-+ *
-+ * Note:
-+ *
-+ *
-+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and
-+ * should NOT retrieve or modify ANY world state.
-+ *
-+ * @param from The region that will be merged into the target.
-+ * @param into The list of regions to split into.
-+ */
-+ public void preSplit(final ThreadedRegion from, final List> into);
-+ }
-+}
-diff --git a/io/papermc/paper/threadedregions/TickData.java b/io/papermc/paper/threadedregions/TickData.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..29f9fed5f02530b3256e6b993e607d4647daa7b6
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/TickData.java
-@@ -0,0 +1,333 @@
-+package io.papermc.paper.threadedregions;
-+
-+import ca.spottedleaf.concurrentutil.util.TimeUtil;
-+import io.papermc.paper.util.IntervalledCounter;
-+import it.unimi.dsi.fastutil.longs.LongArrayList;
-+
-+import java.util.ArrayDeque;
-+import java.util.ArrayList;
-+import java.util.Arrays;
-+import java.util.List;
-+
-+public final class TickData {
-+
-+ private final long interval; // ns
-+
-+ private final ArrayDeque timeData = new ArrayDeque<>();
-+
-+ public TickData(final long intervalNS) {
-+ this.interval = intervalNS;
-+ }
-+
-+ public void addDataFrom(final TickRegionScheduler.TickTime time) {
-+ final long start = time.tickStart();
-+
-+ TickRegionScheduler.TickTime first;
-+ while ((first = this.timeData.peekFirst()) != null) {
-+ // only remove data completely out of window
-+ if ((start - first.tickEnd()) <= this.interval) {
-+ break;
-+ }
-+ this.timeData.pollFirst();
-+ }
-+
-+ this.timeData.add(time);
-+ }
-+
-+ // fromIndex inclusive, toIndex exclusive
-+ // will throw if arr.length == 0
-+ private static double median(final long[] arr, final int fromIndex, final int toIndex) {
-+ final int len = toIndex - fromIndex;
-+ final int middle = fromIndex + (len >>> 1);
-+ if ((len & 1) == 0) {
-+ // even, average the two middle points
-+ return (double)(arr[middle - 1] + arr[middle]) / 2.0;
-+ } else {
-+ // odd, just grab the middle
-+ return (double)arr[middle];
-+ }
-+ }
-+
-+ // will throw if arr.length == 0
-+ private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex,
-+ final boolean inverse) {
-+ final int len = toIndex - fromIndex;
-+ long sum = 0L;
-+ final double median = median(arr, fromIndex, toIndex);
-+ long min = arr[0];
-+ long max = arr[0];
-+
-+ for (int i = fromIndex; i < toIndex; ++i) {
-+ final long val = arr[i];
-+ sum += val;
-+ if (val < min) {
-+ min = val;
-+ }
-+ if (val > max) {
-+ max = val;
-+ }
-+ }
-+
-+ if (inverse) {
-+ // for positive a,b we have that a >= b if and only if 1/a <= 1/b
-+ return new SegmentData(
-+ len,
-+ (double)len / ((double)sum / 1.0E9),
-+ 1.0E9 / median,
-+ 1.0E9 / (double)max,
-+ 1.0E9 / (double)min
-+ );
-+ } else {
-+ return new SegmentData(
-+ len,
-+ (double)sum / (double)len,
-+ median,
-+ (double)min,
-+ (double)max
-+ );
-+ }
-+ }
-+
-+ private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd,
-+ final int percent99BestStart, final int percent99BestEnd,
-+ final int percent95BestStart, final int percent95BestEnd,
-+ final int percent1WorstStart, final int percent1WorstEnd,
-+ final int percent5WorstStart, final int percent5WorstEnd,
-+ final boolean inverse) {
-+ return new SegmentedAverage(
-+ computeSegmentData(data, allStart, allEnd, inverse),
-+ computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse),
-+ computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse),
-+ computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse),
-+ computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse)
-+ );
-+ }
-+
-+ private static record TickInformation(
-+ long differenceFromLastTick,
-+ long tickTime,
-+ long tickTimeCPU
-+ ) {}
-+
-+ // rets null if there is no data
-+ public TickReportData generateTickReport(final TickRegionScheduler.TickTime inProgress, final long endTime) {
-+ if (this.timeData.isEmpty() && inProgress == null) {
-+ return null;
-+ }
-+
-+ final List allData = new ArrayList<>(this.timeData);
-+ if (inProgress != null) {
-+ allData.add(inProgress);
-+ }
-+
-+ final long intervalStart = allData.get(0).tickStart();
-+ final long intervalEnd = allData.get(allData.size() - 1).tickEnd();
-+
-+ // to make utilisation accurate, we need to take the total time used over the last interval period -
-+ // this means if a tick start before the measurement interval, but ends within the interval, then we
-+ // only consider the time it spent ticking inside the interval
-+ long totalTimeOverInterval = 0L;
-+ long measureStart = endTime - this.interval;
-+
-+ for (int i = 0, len = allData.size(); i < len; ++i) {
-+ final TickRegionScheduler.TickTime time = allData.get(i);
-+ if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) {
-+ final long diff = time.tickEnd() - measureStart;
-+ if (diff > 0L) {
-+ totalTimeOverInterval += diff;
-+ } // else: the time is entirely out of interval
-+ } else {
-+ totalTimeOverInterval += time.tickLength();
-+ }
-+ }
-+
-+ // we only care about ticks, but because of inbetween tick task execution
-+ // there will be data in allData that isn't ticks. But, that data cannot
-+ // be ignored since it contributes to utilisation.
-+ // So, we will "compact" the data by merging any inbetween tick times
-+ // the next tick.
-+ // If there is no "next tick", then we will create one.
-+ final List collapsedData = new ArrayList<>();
-+ for (int i = 0, len = allData.size(); i < len; ++i) {
-+ final List toCollapse = new ArrayList<>();
-+ TickRegionScheduler.TickTime lastTick = null;
-+ for (;i < len; ++i) {
-+ final TickRegionScheduler.TickTime time = allData.get(i);
-+ if (!time.isTickExecution()) {
-+ toCollapse.add(time);
-+ continue;
-+ }
-+ lastTick = time;
-+ break;
-+ }
-+
-+ if (toCollapse.isEmpty()) {
-+ // nothing to collapse
-+ final TickRegionScheduler.TickTime last = allData.get(i);
-+ collapsedData.add(
-+ new TickInformation(
-+ last.differenceFromLastTick(),
-+ last.tickLength(),
-+ last.supportCPUTime() ? last.tickCpuTime() : 0L
-+ )
-+ );
-+ } else {
-+ long totalTickTime = 0L;
-+ long totalCpuTime = 0L;
-+ for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) {
-+ final TickRegionScheduler.TickTime time = toCollapse.get(k);
-+ totalTickTime += time.tickLength();
-+ totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L;
-+ }
-+ if (i < len) {
-+ // we know there is a tick to collapse into
-+ final TickRegionScheduler.TickTime last = allData.get(i);
-+ collapsedData.add(
-+ new TickInformation(
-+ last.differenceFromLastTick(),
-+ last.tickLength() + totalTickTime,
-+ (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime
-+ )
-+ );
-+ } else {
-+ // we do not have a tick to collapse into, so we must make one up
-+ // we will assume that the tick is "starting now" and ongoing
-+
-+ // compute difference between imaginary tick and last tick
-+ final long differenceBetweenTicks;
-+ if (lastTick != null) {
-+ // we have a last tick, use it
-+ differenceBetweenTicks = lastTick.tickStart();
-+ } else {
-+ // we don't have a last tick, so we must make one up that makes sense
-+ // if the current interval exceeds the max tick time, then use it
-+
-+ // Otherwise use the interval length.
-+ // This is how differenceFromLastTick() works on TickTime when there is no previous interval.
-+ differenceBetweenTicks = Math.max(
-+ TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime
-+ );
-+ }
-+
-+ collapsedData.add(
-+ new TickInformation(
-+ differenceBetweenTicks,
-+ totalTickTime,
-+ totalCpuTime
-+ )
-+ );
-+ }
-+ }
-+ }
-+
-+
-+ final int collectedTicks = collapsedData.size();
-+ final long[] tickStartToStartDifferences = new long[collectedTicks];
-+ final long[] timePerTickDataRaw = new long[collectedTicks];
-+ final long[] missingCPUTimeDataRaw = new long[collectedTicks];
-+
-+ long totalTimeTicking = 0L;
-+
-+ int i = 0;
-+ for (final TickInformation time : collapsedData) {
-+ tickStartToStartDifferences[i] = time.differenceFromLastTick();
-+ final long timePerTick = timePerTickDataRaw[i] = time.tickTime();
-+ missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU());
-+
-+ ++i;
-+
-+ totalTimeTicking += timePerTick;
-+ }
-+
-+ Arrays.sort(tickStartToStartDifferences);
-+ Arrays.sort(timePerTickDataRaw);
-+ Arrays.sort(missingCPUTimeDataRaw);
-+
-+ // Note: computeSegmentData cannot take start == end
-+ final int allStart = 0;
-+ final int allEnd = collectedTicks;
-+ final int percent95BestStart = 0;
-+ final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks);
-+ final int percent99BestStart = 0;
-+ // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end
-+ final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks);
-+ final int percent1WorstStart = (int)(0.99 * collectedTicks);
-+ final int percent1WorstEnd = collectedTicks;
-+ final int percent5WorstStart = (int)(0.95 * collectedTicks);
-+ final int percent5WorstEnd = collectedTicks;
-+
-+ final SegmentedAverage tpsData = computeSegmentedAverage(
-+ tickStartToStartDifferences,
-+ allStart, allEnd,
-+ percent99BestStart, percent99BestEnd,
-+ percent95BestStart, percent95BestEnd,
-+ percent1WorstStart, percent1WorstEnd,
-+ percent5WorstStart, percent5WorstEnd,
-+ true
-+ );
-+
-+ final SegmentedAverage timePerTickData = computeSegmentedAverage(
-+ timePerTickDataRaw,
-+ allStart, allEnd,
-+ percent99BestStart, percent99BestEnd,
-+ percent95BestStart, percent95BestEnd,
-+ percent1WorstStart, percent1WorstEnd,
-+ percent5WorstStart, percent5WorstEnd,
-+ false
-+ );
-+
-+ final SegmentedAverage missingCPUTimeData = computeSegmentedAverage(
-+ missingCPUTimeDataRaw,
-+ allStart, allEnd,
-+ percent99BestStart, percent99BestEnd,
-+ percent95BestStart, percent95BestEnd,
-+ percent1WorstStart, percent1WorstEnd,
-+ percent5WorstStart, percent5WorstEnd,
-+ false
-+ );
-+
-+ final double utilisation = (double)totalTimeOverInterval / (double)this.interval;
-+
-+ return new TickReportData(
-+ collectedTicks,
-+ intervalStart,
-+ intervalEnd,
-+ totalTimeTicking,
-+ utilisation,
-+
-+ tpsData,
-+ timePerTickData,
-+ missingCPUTimeData
-+ );
-+ }
-+
-+ public static final record TickReportData(
-+ int collectedTicks,
-+ long collectedTickIntervalStart,
-+ long collectedTickIntervalEnd,
-+ long totalTimeTicking,
-+ double utilisation,
-+
-+ SegmentedAverage tpsData,
-+ // in ns
-+ SegmentedAverage timePerTickData,
-+ // in ns
-+ SegmentedAverage missingCPUTimeData
-+ ) {}
-+
-+ public static final record SegmentedAverage(
-+ SegmentData segmentAll,
-+ SegmentData segment99PercentBest,
-+ SegmentData segment95PercentBest,
-+ SegmentData segment5PercentWorst,
-+ SegmentData segment1PercentWorst
-+ ) {}
-+
-+ public static final record SegmentData(
-+ int count,
-+ double average,
-+ double median,
-+ double least,
-+ double greatest
-+ ) {}
-+}
-diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..4471285a4358e51da9912ed791a824527f1a2e8e
---- /dev/null
-+++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java
-@@ -0,0 +1,564 @@
-+package io.papermc.paper.threadedregions;
-+
-+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
-+import ca.spottedleaf.concurrentutil.util.TimeUtil;
-+import ca.spottedleaf.moonrise.common.util.TickThread;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.util.TraceUtil;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import net.minecraft.world.level.ChunkPos;
-+import org.slf4j.Logger;
-+import java.lang.management.ManagementFactory;
-+import java.lang.management.ThreadMXBean;
-+import java.util.concurrent.ThreadFactory;
-+import java.util.concurrent.TimeUnit;
-+import java.util.concurrent.atomic.AtomicBoolean;
-+import java.util.concurrent.atomic.AtomicInteger;
-+import java.util.function.BooleanSupplier;
-+
-+public final class TickRegionScheduler {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+ private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean();
-+ private static final boolean MEASURE_CPU_TIME;
-+ static {
-+ MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported();
-+ if (MEASURE_CPU_TIME) {
-+ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true);
-+ } else {
-+ LOGGER.warn("TickRegionScheduler CPU time measurement is not available");
-+ }
-+ }
-+
-+ public static final int TICK_RATE = 20;
-+ public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns
-+
-+ private final SchedulerThreadPool scheduler;
-+
-+ public TickRegionScheduler(final int threads) {
-+ this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() {
-+ private final AtomicInteger idGenerator = new AtomicInteger();
-+
-+ @Override
-+ public Thread newThread(final Runnable run) {
-+ final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement());
-+ ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException);
-+ return ret;
-+ }
-+ });
-+ }
-+
-+ public int getTotalThreadCount() {
-+ return this.scheduler.getThreads().length;
-+ }
-+
-+ private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion region) {
-+ final Thread currThread = Thread.currentThread();
-+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
-+ throw new IllegalStateException("Must be tick thread runner");
-+ }
-+ if (region != null && tickThreadRunner.currentTickingRegion != null) {
-+ throw new IllegalStateException("Trying to double set ticking region!");
-+ }
-+ if (region == null && tickThreadRunner.currentTickingRegion == null) {
-+ throw new IllegalStateException("Trying to double unset ticking region!");
-+ }
-+ tickThreadRunner.currentTickingRegion = region;
-+ if (region != null) {
-+ tickThreadRunner.currentTickingWorldRegionizedData = region.regioniser.world.worldRegionData.get();
-+ } else {
-+ tickThreadRunner.currentTickingWorldRegionizedData = null;
-+ }
-+ }
-+
-+ private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) {
-+ final Thread currThread = Thread.currentThread();
-+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
-+ throw new IllegalStateException("Must be tick thread runner");
-+ }
-+ if (task != null && tickThreadRunner.currentTickingTask != null) {
-+ throw new IllegalStateException("Trying to double set ticking task!");
-+ }
-+ if (task == null && tickThreadRunner.currentTickingTask == null) {
-+ throw new IllegalStateException("Trying to double unset ticking task!");
-+ }
-+ tickThreadRunner.currentTickingTask = task;
-+ }
-+
-+ /**
-+ * Returns the current ticking region, or {@code null} if there is no ticking region.
-+ * If this thread is not a TickThread, then returns {@code null}.
-+ */
-+ public static ThreadedRegionizer.ThreadedRegion getCurrentRegion() {
-+ final Thread currThread = Thread.currentThread();
-+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
-+ return RegionShutdownThread.getRegion();
-+ }
-+ return tickThreadRunner.currentTickingRegion;
-+ }
-+
-+ /**
-+ * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region.
-+ * This is a faster alternative to calling the {@link RegionizedData#get()} method.
-+ * If this thread is not a TickThread, then returns {@code null}.
-+ */
-+ public static RegionizedWorldData getCurrentRegionizedWorldData() {
-+ final Thread currThread = Thread.currentThread();
-+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
-+ return RegionShutdownThread.getWorldData();
-+ }
-+ return tickThreadRunner.currentTickingWorldRegionizedData;
-+ }
-+
-+ /**
-+ * Returns the current ticking task, or {@code null} if there is no ticking region.
-+ * If this thread is not a TickThread, then returns {@code null}.
-+ */
-+ public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() {
-+ final Thread currThread = Thread.currentThread();
-+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
-+ return null;
-+ }
-+ return tickThreadRunner.currentTickingTask;
-+ }
-+
-+ /**
-+ * Schedules the given region
-+ * @throws IllegalStateException If the region is already scheduled or is ticking
-+ */
-+ public void scheduleRegion(final RegionScheduleHandle region) {
-+ region.scheduler = this;
-+ this.scheduler.schedule(region);
-+ }
-+
-+ /**
-+ * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task
-+ * execution, then it will be cancelled after.
-+ */
-+ public void descheduleRegion(final RegionScheduleHandle region) {
-+ // To avoid acquiring any of the locks the scheduler may be using, we
-+ // simply cancel the next action.
-+ region.markNonSchedulable();
-+ }
-+
-+ /**
-+ * Updates the tick start to the farthest into the future of its current scheduled time and the
-+ * provided time.
-+ * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its
-+ * current start time, {@code true} if the next tick start was adjusted.
-+ */
-+ public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) {
-+ return this.scheduler.updateTickStartToMax(region, newStart);
-+ }
-+
-+ public boolean halt(final boolean sync, final long maxWaitNS) {
-+ return this.scheduler.halt(sync, maxWaitNS);
-+ }
-+
-+ void dumpAliveThreadTraces(final String reason) {
-+ for (final Thread thread : this.scheduler.getThreads()) {
-+ if (thread.isAlive()) {
-+ TraceUtil.dumpTraceForThread(thread, reason);
-+ }
-+ }
-+ }
-+
-+ public void setHasTasks(final RegionScheduleHandle region) {
-+ this.scheduler.notifyTasks(region);
-+ }
-+
-+ public void init() {
-+ this.scheduler.start();
-+ }
-+
-+ private void uncaughtException(final Thread thread, final Throwable thr) {
-+ LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr);
-+
-+ // prevent further ticks from occurring
-+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
-+ this.scheduler.halt(false, 0L);
-+
-+ MinecraftServer.getServer().stopServer();
-+ }
-+
-+ private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) {
-+ // when a region fails, we need to shut down the server gracefully
-+
-+ // prevent further ticks from occurring
-+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
-+ this.scheduler.halt(false, 0L);
-+
-+ final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk();
-+ final ServerLevel world = handle.region == null ? null : handle.region.world;
-+
-+ LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " in world '" + (world == null ? "null" : world.getWorld().getName()) + "' failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr);
-+
-+ MinecraftServer.getServer().stopServer();
-+ }
-+
-+ // By using our own thread object, we can use a field for the current region rather than a ThreadLocal.
-+ // This is much faster than a thread local, since the thread local has to use a map lookup.
-+ private static final class TickThreadRunner extends TickThread {
-+
-+ private ThreadedRegionizer.ThreadedRegion currentTickingRegion;
-+ private RegionizedWorldData currentTickingWorldRegionizedData;
-+ private SchedulerThreadPool.SchedulableTick currentTickingTask;
-+
-+ public TickThreadRunner(final Runnable run, final String name) {
-+ super(run, name);
-+ }
-+ }
-+
-+ public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick {
-+
-+ protected long currentTick;
-+ protected long lastTickStart;
-+
-+ protected final TickData tickTimes5s;
-+ protected final TickData tickTimes15s;
-+ protected final TickData tickTimes1m;
-+ protected final TickData tickTimes5m;
-+ protected final TickData tickTimes15m;
-+ protected TickTime currentTickData;
-+ protected Thread currentTickingThread;
-+
-+ public final TickRegions.TickRegionData region;
-+ private final AtomicBoolean cancelled = new AtomicBoolean();
-+
-+ protected final Schedule tickSchedule;
-+
-+ private TickRegionScheduler scheduler;
-+
-+ public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) {
-+ this.currentTick = 0L;
-+ this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET;
-+ this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L));
-+ this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L));
-+ this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L));
-+ this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L));
-+ this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L));
-+ this.region = region;
-+
-+ this.setScheduledStart(firstStart);
-+ this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS);
-+ }
-+
-+ /**
-+ * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)}
-+ * so that the tick schedule and scheduled start remain synchronised
-+ */
-+ protected final void updateScheduledStart(final long to) {
-+ this.setScheduledStart(to);
-+ this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS);
-+ }
-+
-+ public final void markNonSchedulable() {
-+ this.cancelled.set(true);
-+ }
-+
-+ public final boolean isMarkedAsNonSchedulable() {
-+ return this.cancelled.get();
-+ }
-+
-+ protected abstract boolean tryMarkTicking();
-+
-+ protected abstract boolean markNotTicking();
-+
-+ protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd);
-+
-+ protected abstract boolean runRegionTasks(final BooleanSupplier canContinue);
-+
-+ protected abstract boolean hasIntermediateTasks();
-+
-+ @Override
-+ public final boolean hasTasks() {
-+ return this.hasIntermediateTasks();
-+ }
-+
-+ @Override
-+ public final Boolean runTasks(final BooleanSupplier canContinue) {
-+ if (this.cancelled.get()) {
-+ return null;
-+ }
-+
-+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
-+ final long tickStart = System.nanoTime();
-+
-+ if (!this.tryMarkTicking()) {
-+ if (!this.cancelled.get()) {
-+ throw new IllegalStateException("Scheduled region should be acquirable");
-+ }
-+ // region was killed
-+ return null;
-+ }
-+
-+ TickRegionScheduler.setTickTask(this);
-+ if (this.region != null) {
-+ TickRegionScheduler.setTickingRegion(this.region.region);
-+ }
-+
-+ synchronized (this) {
-+ this.currentTickData = new TickTime(
-+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart,
-+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
-+ false
-+ );
-+ this.currentTickingThread = Thread.currentThread();
-+ }
-+
-+ final boolean ret;
-+ try {
-+ ret = this.runRegionTasks(() -> {
-+ return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean();
-+ });
-+ } catch (final Throwable thr) {
-+ this.scheduler.regionFailed(this, true, thr);
-+ // don't release region for another tick
-+ return null;
-+ } finally {
-+ final long tickEnd = System.nanoTime();
-+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
-+
-+ final TickTime time = new TickTime(
-+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET,
-+ tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false
-+ );
-+
-+ this.addTickTime(time);
-+ TickRegionScheduler.setTickTask(null);
-+ if (this.region != null) {
-+ TickRegionScheduler.setTickingRegion(null);
-+ }
-+ }
-+
-+ return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret);
-+ }
-+
-+ @Override
-+ public final boolean runTick() {
-+ // Remember, we are supposed use setScheduledStart if we return true here, otherwise
-+ // the scheduler will try to schedule for the same time.
-+ if (this.cancelled.get()) {
-+ return false;
-+ }
-+
-+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
-+ final long tickStart = System.nanoTime();
-+
-+ // use max(), don't assume that tickStart >= scheduledStart
-+ final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart));
-+
-+ if (!this.tryMarkTicking()) {
-+ if (!this.cancelled.get()) {
-+ throw new IllegalStateException("Scheduled region should be acquirable");
-+ }
-+ // region was killed
-+ return false;
-+ }
-+ if (this.cancelled.get()) {
-+ this.markNotTicking();
-+ // region should be killed
-+ return false;
-+ }
-+
-+ TickRegionScheduler.setTickTask(this);
-+ if (this.region != null) {
-+ TickRegionScheduler.setTickingRegion(this.region.region);
-+ }
-+ this.incrementTickCount();
-+ final long lastTickStart = this.lastTickStart;
-+ this.lastTickStart = tickStart;
-+
-+ final long scheduledStart = this.getScheduledStart();
-+ final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS;
-+
-+ synchronized (this) {
-+ this.currentTickData = new TickTime(
-+ lastTickStart, scheduledStart, tickStart, cpuStart,
-+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
-+ true
-+ );
-+ this.currentTickingThread = Thread.currentThread();
-+ }
-+
-+ try {
-+ // next start isn't updated until the end of this tick
-+ this.tickRegion(tickCount, tickStart, scheduledEnd);
-+ } catch (final Throwable thr) {
-+ this.scheduler.regionFailed(this, false, thr);
-+ // regionFailed will schedule a shutdown, so we should avoid letting this region tick further
-+ return false;
-+ } finally {
-+ final long tickEnd = System.nanoTime();
-+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
-+
-+ // in order to ensure all regions get their chance at scheduling, we have to ensure that regions
-+ // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest
-+ // of the current time and "ideal" next tick start.
-+ this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS);
-+ this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS)));
-+
-+ final TickTime time = new TickTime(
-+ lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true
-+ );
-+
-+ this.addTickTime(time);
-+ TickRegionScheduler.setTickTask(null);
-+ if (this.region != null) {
-+ TickRegionScheduler.setTickingRegion(null);
-+ }
-+ }
-+
-+ // Only AFTER updating the tickStart
-+ return this.markNotTicking() && !this.cancelled.get();
-+ }
-+
-+ /**
-+ * Only safe to call if this tick data matches the current ticking region.
-+ */
-+ protected void addTickTime(final TickTime time) {
-+ synchronized (this) {
-+ this.currentTickData = null;
-+ this.currentTickingThread = null;
-+ this.tickTimes5s.addDataFrom(time);
-+ this.tickTimes15s.addDataFrom(time);
-+ this.tickTimes1m.addDataFrom(time);
-+ this.tickTimes5m.addDataFrom(time);
-+ this.tickTimes15m.addDataFrom(time);
-+ }
-+ }
-+
-+ private TickTime adjustCurrentTickData(final long tickEnd) {
-+ final TickTime currentTickData = this.currentTickData;
-+ if (currentTickData == null) {
-+ return null;
-+ }
-+
-+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L;
-+
-+ return new TickTime(
-+ currentTickData.previousTickStart(), currentTickData.scheduledTickStart(),
-+ currentTickData.tickStart(), currentTickData.tickStartCPU(),
-+ tickEnd, cpuEnd,
-+ MEASURE_CPU_TIME, currentTickData.isTickExecution()
-+ );
-+ }
-+
-+ public final TickData.TickReportData getTickReport5s(final long currTime) {
-+ synchronized (this) {
-+ return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
-+ }
-+ }
-+
-+ public final TickData.TickReportData getTickReport15s(final long currTime) {
-+ synchronized (this) {
-+ return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
-+ }
-+ }
-+
-+ public final TickData.TickReportData getTickReport1m(final long currTime) {
-+ synchronized (this) {
-+ return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
-+ }
-+ }
-+
-+ public final TickData.TickReportData getTickReport5m(final long currTime) {
-+ synchronized (this) {
-+ return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
-+ }
-+ }
-+
-+ public final TickData.TickReportData getTickReport15m(final long currTime) {
-+ synchronized (this) {
-+ return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
-+ }
-+ }
-+
-+ /**
-+ * Only safe to call if this tick data matches the current ticking region.
-+ */
-+ private void incrementTickCount() {
-+ ++this.currentTick;
-+ }
-+
-+ /**
-+ * Only safe to call if this tick data matches the current ticking region.
-+ */
-+ public final long getCurrentTick() {
-+ return this.currentTick;
-+ }
-+
-+ protected final void setCurrentTick(final long value) {
-+ this.currentTick = value;
-+ }
-+ }
-+
-+ // All time units are in nanoseconds.
-+ public static final record TickTime(
-+ long previousTickStart,
-+ long scheduledTickStart,
-+ long tickStart,
-+ long tickStartCPU,
-+ long tickEnd,
-+ long tickEndCPU,
-+ boolean supportCPUTime,
-+ boolean isTickExecution
-+ ) {
-+ /**
-+ * The difference between the start tick time and the scheduled start tick time. This value is
-+ * < 0 if the tick started before the scheduled tick time.
-+ * Only valid when {@link #isTickExecution()} is {@code true}.
-+ */
-+ public final long startOvershoot() {
-+ return this.tickStart - this.scheduledTickStart;
-+ }
-+
-+ /**
-+ * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong).
-+ */
-+ public final long tickLength() {
-+ return this.tickEnd - this.tickStart;
-+ }
-+
-+ /**
-+ * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength,
-+ * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE
-+ * if CPU time measurement is not supported.
-+ */
-+ public final long tickCpuTime() {
-+ if (!this.supportCPUTime()) {
-+ return Long.MIN_VALUE;
-+ }
-+ return this.tickEndCPU - this.tickStartCPU;
-+ }
-+
-+ /**
-+ * The difference in time from the start of the last tick to the start of the current tick. If there is no
-+ * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength).
-+ * Only valid when {@link #isTickExecution()} is {@code true}.
-+ */
-+ public final long differenceFromLastTick() {
-+ if (this.hasLastTick()) {
-+ return this.tickStart - this.previousTickStart;
-+ }
-+ return Math.max(TIME_BETWEEN_TICKS, this.tickLength());
-+ }
-+
-+ /**
-+ * Returns whether there was a tick that occurred before this one.
-+ * Only valid when {@link #isTickExecution()} is {@code true}.
-+ */
-+ public boolean hasLastTick() {
-+ return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET;
-+ }
-+
-+ /*
-+ * Remember, this is the expected behavior of the following:
-+ *
-+ * MSPT: Time per tick. This does not include overshoot time, just the tickLength().
-+ *
-+ * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick).
-+ */
-+ }
-+}
-diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java
-index 8424cf9d4617b4732d44cc460d25b04481068989..df15b1139e71dfe10b8f24ec6d235b99f6d5006a 100644
---- a/io/papermc/paper/threadedregions/TickRegions.java
-+++ b/io/papermc/paper/threadedregions/TickRegions.java
-@@ -1,10 +1,410 @@
- package io.papermc.paper.threadedregions;
-
--// placeholder class for Folia
--public class TickRegions {
-+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
-+import ca.spottedleaf.concurrentutil.util.TimeUtil;
-+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.configuration.GlobalConfiguration;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
-+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
-+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
-+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
-+import net.minecraft.server.MinecraftServer;
-+import net.minecraft.server.level.ServerLevel;
-+import org.slf4j.Logger;
-+import java.util.Iterator;
-+import java.util.concurrent.TimeUnit;
-+import java.util.concurrent.atomic.AtomicInteger;
-+import java.util.concurrent.atomic.AtomicLong;
-+import java.util.function.BooleanSupplier;
-+
-+public final class TickRegions implements ThreadedRegionizer.RegionCallbacks {
-+
-+ private static final Logger LOGGER = LogUtils.getLogger();
-+ private static int regionShift = 31;
-
- public static int getRegionChunkShift() {
-- return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT;
-+ return regionShift;
-+ }
-+
-+ private static boolean initialised;
-+ private static TickRegionScheduler scheduler;
-+
-+ public static TickRegionScheduler getScheduler() {
-+ return scheduler;
-+ }
-+
-+ public static void init(final GlobalConfiguration.ThreadedRegions config) {
-+ if (initialised) {
-+ return;
-+ }
-+ initialised = true;
-+ int gridExponent = config.gridExponent;
-+ gridExponent = Math.max(0, gridExponent);
-+ gridExponent = Math.min(31, gridExponent);
-+ regionShift = gridExponent;
-+
-+ int tickThreads;
-+ if (config.threads <= 0) {
-+ tickThreads = Runtime.getRuntime().availableProcessors() / 2;
-+ if (tickThreads <= 4) {
-+ tickThreads = 1;
-+ } else {
-+ tickThreads = tickThreads / 4;
-+ }
-+ } else {
-+ tickThreads = config.threads;
-+ }
-+
-+ scheduler = new TickRegionScheduler(tickThreads);
-+ LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads");
-+ }
-+
-+ @Override
-+ public TickRegionData createNewData(final ThreadedRegionizer.ThreadedRegion region) {
-+ return new TickRegionData(region);
-+ }
-+
-+ @Override
-+ public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) {
-+ return null;
-+ }
-+
-+ @Override
-+ public void onRegionCreate(final ThreadedRegionizer.ThreadedRegion region) {
-+ final TickRegionData data = region.getData();
-+ // post-region merge/split regioninfo update
-+ data.getRegionStats().updateFrom(data.getOrCreateRegionizedData(data.world.worldRegionData));
-+ }
-+
-+ @Override
-+ public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion region) {
-+ // nothing for now
-+ }
-+
-+ @Override
-+ public void onRegionActive(final ThreadedRegionizer.ThreadedRegion region) {
-+ final TickRegionData data = region.getData();
-+
-+ data.tickHandle.checkInitialSchedule();
-+ scheduler.scheduleRegion(data.tickHandle);
-+ }
-+
-+ @Override
-+ public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion region) {
-+ final TickRegionData data = region.getData();
-+
-+ scheduler.descheduleRegion(data.tickHandle);
-+ // old handle cannot be scheduled anymore, copy to a new handle
-+ data.tickHandle = data.tickHandle.copy();
-+ }
-+
-+ @Override
-+ public void preMerge(final ThreadedRegionizer.ThreadedRegion from,
-+ final ThreadedRegionizer.ThreadedRegion into) {
-+
-+ }
-+
-+ @Override
-+ public void preSplit(final ThreadedRegionizer.ThreadedRegion from,
-+ final java.util.List> into) {
-+
-+ }
-+
-+ public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {}
-+
-+ public static final class RegionStats {
-+
-+ private final AtomicInteger entityCount = new AtomicInteger();
-+ private final AtomicInteger playerCount = new AtomicInteger();
-+ private final AtomicInteger chunkCount = new AtomicInteger();
-+
-+ public int getEntityCount() {
-+ return this.entityCount.get();
-+ }
-+
-+ public int getPlayerCount() {
-+ return this.playerCount.get();
-+ }
-+
-+ public int getChunkCount() {
-+ return this.chunkCount.get();
-+ }
-+
-+ void updateFrom(final RegionizedWorldData data) {
-+ this.entityCount.setRelease(data == null ? 0 : data.getEntityCount());
-+ this.playerCount.setRelease(data == null ? 0 : data.getPlayerCount());
-+ this.chunkCount.setRelease(data == null ? 0 : data.getChunkCount());
-+ }
-+
-+ static void updateCurrentRegion() {
-+ TickRegionScheduler.getCurrentRegion().getData().getRegionStats().updateFrom(TickRegionScheduler.getCurrentRegionizedWorldData());
-+ }
-+ }
-+
-+ public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData {
-+
-+ private static final AtomicLong ID_GENERATOR = new AtomicLong();
-+ /** Never 0L, since 0L is reserved for global region. */
-+ public final long id = ID_GENERATOR.incrementAndGet();
-+
-+ public final ThreadedRegionizer.ThreadedRegion region;
-+ public final ServerLevel world;
-+
-+ // generic regionised data
-+ private final Reference2ReferenceOpenHashMap, Object> regionizedData = new Reference2ReferenceOpenHashMap<>();
-+
-+ // tick data
-+ private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET);
-+
-+ // queue data
-+ private final RegionizedTaskQueue.RegionTaskQueueData taskQueueData;
-+
-+ // chunk holder manager data
-+ private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData();
-+
-+ // async-safe read-only region data
-+ private final RegionStats regionStats;
-+
-+ private TickRegionData(final ThreadedRegionizer.ThreadedRegion region) {
-+ this.region = region;
-+ this.world = region.regioniser.world;
-+ this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData);
-+ this.regionStats = new RegionStats();
-+ }
-+
-+ public RegionStats getRegionStats() {
-+ return this.regionStats;
-+ }
-+
-+ public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() {
-+ return this.taskQueueData;
-+ }
-+
-+ // the value returned can be invalidated at any time, except when the caller
-+ // is ticking this region
-+ public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() {
-+ return this.tickHandle;
-+ }
-+
-+ public long getCurrentTick() {
-+ return this.tickHandle.getCurrentTick();
-+ }
-+
-+ public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() {
-+ return this.holderManagerRegionData;
-+ }
-+
-+ T getRegionizedData(final RegionizedData regionizedData) {
-+ return (T)this.regionizedData.get(regionizedData);
-+ }
-+
-+ T getOrCreateRegionizedData(final RegionizedData regionizedData) {
-+ T ret = (T)this.regionizedData.get(regionizedData);
-+
-+ if (ret != null) {
-+ return ret;
-+ }
-+
-+ ret = regionizedData.createNewValue();
-+ this.regionizedData.put(regionizedData, ret);
-+
-+ return ret;
-+ }
-+
-+ @Override
-+ public void split(final ThreadedRegionizer regioniser,
-+ final Long2ReferenceOpenHashMap> into,
-+ final ReferenceOpenHashSet> regions) {
-+ final int shift = regioniser.sectionChunkShift;
-+
-+ // tick data
-+ // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled),
-+ // and the other regions to split into are not scheduled yet.
-+ for (final ThreadedRegionizer.ThreadedRegion region : regions) {
-+ final TickRegionData data = region.getData();
-+ data.tickHandle.copyDeadlineAndTickCount(this.tickHandle);
-+ }
-+
-+ // generic regionised data
-+ for (final Iterator, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator();
-+ dataIterator.hasNext();) {
-+ final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next();
-+ final RegionizedData> data = regionDataEntry.getKey();
-+ final Object from = regionDataEntry.getValue();
-+
-+ final ReferenceOpenHashSet