From d17fb532f1c53a00367eede072341fd1816bb051 Mon Sep 17 00:00:00 2001
From: Spottedleaf
Date: Sun, 16 Feb 2025 12:17:53 -0800
Subject: [PATCH] Move file patches to feature patches
This is to assist in updating upstream, as the current file patch
system is brittle and fails to handle conflicts well.
---
.../features/0001-Region-Threading-Base.patch | 19827 ++++++++++++++++
...ns.patch => 0002-Max-pending-logins.patch} | 2 +-
...k-system-throughput-counters-to-tps.patch} | 2 +-
...ates-in-non-loaded-or-non-owned-chu.patch} | 2 +-
...world-tile-entities-on-worldgen-thr.patch} | 0
...tion-to-player-position-on-player-d.patch} | 2 +-
...filer.patch => 0007-Region-profiler.patch} | 6 +-
...d.patch => 0008-Add-watchdog-thread.patch} | 2 +-
.../common/misc/NearbyPlayers.java.patch | 23 -
.../moonrise/paper/PaperHooks.java.patch | 20 -
.../util/BaseChunkSystemHooks.java.patch | 87 -
.../level/chunk/ChunkData.java.patch | 11 -
.../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 | 20 -
.../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 | 85 -
.../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 | 227 -
.../server/level/DistanceManager.java.patch | 53 -
.../server/level/ServerChunkCache.java.patch | 276 -
.../level/ServerEntityGetter.java.patch | 32 -
.../server/level/ServerLevel.java.patch | 1133 -
.../server/level/ServerPlayer.java.patch | 631 -
.../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 | 1088 -
.../world/entity/LivingEntity.java.patch | 153 -
.../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 -
.../behavior/GoToPotentialJobSite.java.patch | 17 -
.../ai/behavior/PoiCompetitorScan.java.patch | 14 -
.../ai/behavior/YieldJobSite.java.patch | 17 -
.../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 | 140 -
.../world/entity/raid/Raid.java.patch | 61 -
.../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 -
.../features/0001-Region-Threading-Base.patch | 5332 +++++
...date-Logo.patch => 0002-Update-Logo.patch} | 0
...changes.patch => 0003-Build-changes.patch} | 0
... => 0004-Fix-tests-by-removing-them.patch} | 0
...filer.patch => 0005-Region-profiler.patch} | 0
...d.patch => 0006-Add-watchdog-thread.patch} | 0
...n.patch => 0007-Add-TPS-From-Region.patch} | 0
.../common/util/TickThread.java.patch | 212 -
.../io/papermc/paper/SparksFly.java.patch | 57 -
.../paper/adventure/ChatProcessor.java.patch | 20 -
.../ClickCallbackProviderImpl.java.patch | 57 -
.../paper/command/PaperCommands.java.patch | 11 -
.../subcommands/EntityCommand.java.patch | 11 -
.../subcommands/HeapDumpCommand.java.patch | 12 -
.../subcommands/ReloadCommand.java.patch | 12 -
.../GlobalConfiguration.java.patch | 20 -
.../WorldConfiguration.java.patch | 17 -
.../entity/PaperSchoolableFish.java.patch | 19 -
.../activation/ActivationType.java.patch | 11 -
.../manager/PaperPermissionManager.java.patch | 177 -
.../PaperPluginInstanceManager.java.patch | 16 -
.../configuration/PaperPluginMeta.java.patch | 24 -
.../PaperPluginProviderFactory.java.patch | 14 -
.../SpigotPluginProviderFactory.java.patch | 19 -
.../EntityScheduler.java.patch | 17 -
.../io/papermc/paper/util/MCUtil.java.patch | 41 -
.../bukkit/craftbukkit/CraftServer.java.patch | 133 -
.../bukkit/craftbukkit/CraftWorld.java.patch | 432 -
.../craftbukkit/block/CraftBlock.java.patch | 201 -
.../block/CraftBlockEntityState.java.patch | 22 -
.../block/CraftBlockState.java.patch | 25 -
.../block/CraftBlockStates.java.patch | 22 -
.../ConsoleCommandCompleter.java.patch | 20 -
.../entity/AbstractProjectile.java.patch | 31 -
.../entity/CraftAbstractArrow.java.patch | 10 -
.../entity/CraftAbstractHorse.java.patch | 19 -
.../entity/CraftAbstractSkeleton.java.patch | 20 -
.../entity/CraftAbstractVillager.java.patch | 19 -
.../entity/CraftAbstractWindCharge.java.patch | 10 -
.../entity/CraftAgeable.java.patch | 19 -
.../craftbukkit/entity/CraftAllay.java.patch | 19 -
.../entity/CraftAmbient.java.patch | 19 -
.../entity/CraftAnimals.java.patch | 19 -
.../entity/CraftAreaEffectCloud.java.patch | 19 -
.../entity/CraftArmadillo.java.patch | 10 -
.../entity/CraftArmorStand.java.patch | 19 -
.../craftbukkit/entity/CraftArrow.java.patch | 24 -
.../entity/CraftAxolotl.java.patch | 19 -
.../craftbukkit/entity/CraftBat.java.patch | 19 -
.../craftbukkit/entity/CraftBee.java.patch | 19 -
.../craftbukkit/entity/CraftBlaze.java.patch | 19 -
.../CraftBlockAttachedEntity.java.patch | 19 -
.../entity/CraftBlockDisplay.java.patch | 19 -
.../craftbukkit/entity/CraftBoat.java.patch | 19 -
.../craftbukkit/entity/CraftBogged.java.patch | 10 -
.../craftbukkit/entity/CraftBreeze.java.patch | 10 -
.../entity/CraftBreezeWindCharge.java.patch | 10 -
.../craftbukkit/entity/CraftCamel.java.patch | 19 -
.../craftbukkit/entity/CraftCat.java.patch | 19 -
.../entity/CraftCaveSpider.java.patch | 19 -
.../entity/CraftChestBoat.java.patch | 19 -
.../entity/CraftChestedHorse.java.patch | 19 -
.../entity/CraftChicken.java.patch | 19 -
.../craftbukkit/entity/CraftCod.java.patch | 19 -
.../entity/CraftComplexPart.java.patch | 19 -
.../craftbukkit/entity/CraftCow.java.patch | 19 -
.../entity/CraftCreaking.java.patch | 19 -
.../entity/CraftCreature.java.patch | 19 -
.../entity/CraftCreeper.java.patch | 24 -
.../entity/CraftDisplay.java.patch | 19 -
.../entity/CraftDolphin.java.patch | 19 -
.../entity/CraftDrowned.java.patch | 19 -
.../craftbukkit/entity/CraftEgg.java.patch | 19 -
.../entity/CraftEnderCrystal.java.patch | 19 -
.../entity/CraftEnderDragon.java.patch | 19 -
.../entity/CraftEnderDragonPart.java.patch | 19 -
.../entity/CraftEnderPearl.java.patch | 19 -
.../entity/CraftEnderSignal.java.patch | 19 -
.../entity/CraftEnderman.java.patch | 19 -
.../entity/CraftEndermite.java.patch | 19 -
.../craftbukkit/entity/CraftEntity.java.patch | 134 -
.../craftbukkit/entity/CraftEvoker.java.patch | 19 -
.../entity/CraftEvokerFangs.java.patch | 19 -
.../entity/CraftExperienceOrb.java.patch | 19 -
.../entity/CraftFallingBlock.java.patch | 19 -
.../entity/CraftFireball.java.patch | 19 -
.../entity/CraftFirework.java.patch | 19 -
.../craftbukkit/entity/CraftFish.java.patch | 19 -
.../entity/CraftFishHook.java.patch | 19 -
.../craftbukkit/entity/CraftFlying.java.patch | 19 -
.../craftbukkit/entity/CraftFox.java.patch | 19 -
.../craftbukkit/entity/CraftFrog.java.patch | 19 -
.../craftbukkit/entity/CraftGhast.java.patch | 19 -
.../craftbukkit/entity/CraftGiant.java.patch | 19 -
.../entity/CraftGlowItemFrame.java.patch | 19 -
.../entity/CraftGlowSquid.java.patch | 19 -
.../craftbukkit/entity/CraftGoat.java.patch | 19 -
.../craftbukkit/entity/CraftGolem.java.patch | 19 -
.../entity/CraftGuardian.java.patch | 19 -
.../entity/CraftHanging.java.patch | 19 -
.../craftbukkit/entity/CraftHoglin.java.patch | 19 -
.../craftbukkit/entity/CraftHorse.java.patch | 19 -
.../entity/CraftHumanEntity.java.patch | 19 -
.../entity/CraftIllager.java.patch | 19 -
.../entity/CraftIllusioner.java.patch | 19 -
.../entity/CraftInteraction.java.patch | 19 -
.../entity/CraftIronGolem.java.patch | 19 -
.../craftbukkit/entity/CraftItem.java.patch | 19 -
.../entity/CraftItemDisplay.java.patch | 19 -
.../entity/CraftItemFrame.java.patch | 19 -
.../entity/CraftLargeFireball.java.patch | 19 -
.../craftbukkit/entity/CraftLeash.java.patch | 24 -
.../entity/CraftLightningStrike.java.patch | 19 -
.../entity/CraftLivingEntity.java.patch | 24 -
.../craftbukkit/entity/CraftLlama.java.patch | 19 -
.../entity/CraftLlamaSpit.java.patch | 19 -
.../entity/CraftMagmaCube.java.patch | 19 -
.../craftbukkit/entity/CraftMarker.java.patch | 19 -
.../entity/CraftMinecart.java.patch | 19 -
.../entity/CraftMinecartCommand.java.patch | 19 -
.../entity/CraftMinecartContainer.java.patch | 19 -
.../entity/CraftMinecartFurnace.java.patch | 19 -
.../entity/CraftMinecartHopper.java.patch | 20 -
.../entity/CraftMinecartMobSpawner.java.patch | 19 -
.../entity/CraftMinecartTNT.java.patch | 19 -
.../craftbukkit/entity/CraftMob.java.patch | 28 -
.../entity/CraftMonster.java.patch | 19 -
.../entity/CraftMushroomCow.java.patch | 24 -
.../craftbukkit/entity/CraftOcelot.java.patch | 19 -
.../entity/CraftOminousItemSpawner.java.patch | 10 -
.../entity/CraftPainting.java.patch | 19 -
.../craftbukkit/entity/CraftPanda.java.patch | 19 -
.../craftbukkit/entity/CraftParrot.java.patch | 19 -
.../entity/CraftPhantom.java.patch | 19 -
.../craftbukkit/entity/CraftPig.java.patch | 19 -
.../entity/CraftPigZombie.java.patch | 19 -
.../craftbukkit/entity/CraftPiglin.java.patch | 19 -
.../entity/CraftPiglinAbstract.java.patch | 19 -
.../entity/CraftPiglinBrute.java.patch | 19 -
.../entity/CraftPillager.java.patch | 19 -
.../craftbukkit/entity/CraftPlayer.java.patch | 77 -
.../entity/CraftPolarBear.java.patch | 20 -
.../entity/CraftProjectile.java.patch | 19 -
.../entity/CraftPufferFish.java.patch | 19 -
.../craftbukkit/entity/CraftRabbit.java.patch | 19 -
.../craftbukkit/entity/CraftRaider.java.patch | 19 -
.../entity/CraftRavager.java.patch | 19 -
.../craftbukkit/entity/CraftSalmon.java.patch | 19 -
.../craftbukkit/entity/CraftSheep.java.patch | 19 -
.../entity/CraftShulker.java.patch | 19 -
.../entity/CraftShulkerBullet.java.patch | 19 -
.../entity/CraftSilverfish.java.patch | 19 -
.../entity/CraftSizedFireball.java.patch | 19 -
.../entity/CraftSkeleton.java.patch | 19 -
.../entity/CraftSkeletonHorse.java.patch | 19 -
.../craftbukkit/entity/CraftSlime.java.patch | 19 -
.../entity/CraftSmallFireball.java.patch | 19 -
.../entity/CraftSniffer.java.patch | 19 -
.../entity/CraftSnowball.java.patch | 19 -
.../entity/CraftSnowman.java.patch | 19 -
.../entity/CraftSpectralArrow.java.patch | 19 -
.../entity/CraftSpellcaster.java.patch | 19 -
.../craftbukkit/entity/CraftSpider.java.patch | 19 -
.../craftbukkit/entity/CraftSquid.java.patch | 19 -
.../entity/CraftStrider.java.patch | 19 -
.../entity/CraftTNTPrimed.java.patch | 19 -
.../entity/CraftTadpole.java.patch | 19 -
.../entity/CraftTameableAnimal.java.patch | 19 -
.../entity/CraftTextDisplay.java.patch | 19 -
.../CraftThrowableProjectile.java.patch | 19 -
.../entity/CraftThrownExpBottle.java.patch | 19 -
.../entity/CraftThrownPotion.java.patch | 20 -
.../entity/CraftTraderLlama.java.patch | 19 -
.../entity/CraftTrident.java.patch | 19 -
.../entity/CraftTropicalFish.java.patch | 19 -
.../craftbukkit/entity/CraftTurtle.java.patch | 19 -
.../craftbukkit/entity/CraftVex.java.patch | 19 -
.../entity/CraftVillager.java.patch | 19 -
.../entity/CraftVillagerZombie.java.patch | 19 -
.../entity/CraftVindicator.java.patch | 19 -
.../entity/CraftWanderingTrader.java.patch | 19 -
.../craftbukkit/entity/CraftWarden.java.patch | 19 -
.../entity/CraftWaterMob.java.patch | 19 -
.../entity/CraftWindCharge.java.patch | 10 -
.../craftbukkit/entity/CraftWitch.java.patch | 19 -
.../craftbukkit/entity/CraftWither.java.patch | 19 -
.../entity/CraftWitherSkull.java.patch | 19 -
.../craftbukkit/entity/CraftWolf.java.patch | 19 -
.../craftbukkit/entity/CraftZoglin.java.patch | 19 -
.../craftbukkit/entity/CraftZombie.java.patch | 19 -
.../event/CraftEventFactory.java.patch | 29 -
.../scheduler/CraftScheduler.java.patch | 10 -
.../scoreboard/CraftScoreboard.java.patch | 26 -
.../CraftScoreboardManager.java.patch | 18 -
.../util/CraftMagicNumbers.java.patch | 15 -
.../util/DelegatedGeneratorAccess.java.patch | 21 -
.../org/spigotmc/SpigotCommand.java.patch | 18 -
.../java/org/spigotmc/SpigotConfig.java.patch | 20 -
.../org/spigotmc/SpigotWorldConfig.java.patch | 11 -
.../paper/plugin/TestPluginMeta.java.patch | 16 -
404 files changed, 25167 insertions(+), 24357 deletions(-)
create mode 100644 folia-server/minecraft-patches/features/0001-Region-Threading-Base.patch
rename folia-server/minecraft-patches/features/{0001-Max-pending-logins.patch => 0002-Max-pending-logins.patch} (96%)
rename folia-server/minecraft-patches/features/{0002-Add-chunk-system-throughput-counters-to-tps.patch => 0003-Add-chunk-system-throughput-counters-to-tps.patch} (98%)
rename folia-server/minecraft-patches/features/{0003-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch => 0004-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch} (98%)
rename folia-server/minecraft-patches/features/{0004-Block-reading-in-world-tile-entities-on-worldgen-thr.patch => 0005-Block-reading-in-world-tile-entities-on-worldgen-thr.patch} (100%)
rename folia-server/minecraft-patches/features/{0005-Sync-vehicle-position-to-player-position-on-player-d.patch => 0006-Sync-vehicle-position-to-player-position-on-player-d.patch} (95%)
rename folia-server/minecraft-patches/features/{0006-Region-profiler.patch => 0007-Region-profiler.patch} (99%)
rename folia-server/minecraft-patches/features/{0007-Add-watchdog-thread.patch => 0008-Add-watchdog-thread.patch} (98%)
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/PaperHooks.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/redstone/RedstoneWireTurbo.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionShutdownThread.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedData.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedServer.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedTaskQueue.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedWorldData.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/Schedule.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TeleportUtils.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/ThreadedRegionizer.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickData.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegionScheduler.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegions.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandServerHealth.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandUtil.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/commands/CommandSourceStack.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/commands/Commands.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestHelper.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestServer.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/network/protocol/PacketUtils.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/AdvancementCommands.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/AttributeCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/ClearInventoryCommands.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/DamageCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/DefaultGameModeCommands.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/EffectCommands.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/EnchantCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/ExperienceCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillBiomeCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/ForceLoadCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/GameModeCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/GiveCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/KillCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/PlaceCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/RecipeCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetBlockCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetSpawnCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/SummonCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/TeleportCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/TimeCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/WeatherCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/WorldBorderCommand.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ChunkMap.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/DistanceManager.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerEntityGetter.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayerGameMode.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/TicketType.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/WorldGenRegion.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/BanListEntry.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/PlayerList.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/StoredUserList.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/util/SpawnUtil.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/RandomSequences.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/CombatTracker.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/DamageSource.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/FallLocation.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/LivingEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/Mob.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/PortalProcessor.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/TamableAnimal.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/Brain.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/behavior/GoToPotentialJobSite.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/behavior/YieldJobSite.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/PlayerSensor.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/VillageSiege.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/poi/PoiManager.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Bee.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Cat.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/decoration/ItemFrame.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/FallingBlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/ItemEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/PrimedTnt.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/Vex.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/ZombieVillager.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/AbstractVillager.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/CatSpawner.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/Villager.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractArrow.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FishingHook.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/LlamaSpit.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/Projectile.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/SmallFireball.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raid.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raider.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raids.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartHopper.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/ItemStack.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/MapItem.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/SignItem.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/component/LodestoneTracker.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/BaseCommandBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelAccessor.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelReader.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/NaturalSpawner.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerExplosion.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerLevelAccessor.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/StructureManager.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BedBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Block.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BushBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DispenserBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DoublePlantBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndGatewayBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndPortalBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FarmBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FungusBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/HoneyBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/LightningRodBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/MushroomBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/NetherPortalBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Portal.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedStoneWireBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SaplingBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TickingBlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/border/WorldBorder.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/ChunkGenerator.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/LevelChunk.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/SavedData.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapIndex.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/storage/DimensionDataStorage.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelChunkTicks.java.patch
delete mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelTicks.java.patch
create mode 100644 folia-server/paper-patches/features/0001-Region-Threading-Base.patch
rename folia-server/paper-patches/features/{0001-Update-Logo.patch => 0002-Update-Logo.patch} (100%)
rename folia-server/paper-patches/features/{0002-Build-changes.patch => 0003-Build-changes.patch} (100%)
rename folia-server/paper-patches/features/{0003-Fix-tests-by-removing-them.patch => 0004-Fix-tests-by-removing-them.patch} (100%)
rename folia-server/paper-patches/features/{0004-Region-profiler.patch => 0005-Region-profiler.patch} (100%)
rename folia-server/paper-patches/features/{0005-Add-watchdog-thread.patch => 0006-Add-watchdog-thread.patch} (100%)
rename folia-server/paper-patches/features/{0006-Add-TPS-From-Region.patch => 0007-Add-TPS-From-Region.patch} (100%)
delete mode 100644 folia-server/paper-patches/files/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/SparksFly.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/adventure/ChatProcessor.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/adventure/providers/ClickCallbackProviderImpl.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/command/PaperCommands.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/configuration/WorldConfiguration.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/entity/PaperSchoolableFish.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/entity/activation/ActivationType.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/io/papermc/paper/util/MCUtil.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/CraftServer.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/CraftWorld.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/block/CraftBlockEntityState.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/block/CraftBlockState.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/block/CraftBlockStates.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/AbstractProjectile.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractArrow.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractHorse.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractSkeleton.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractVillager.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAbstractWindCharge.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAgeable.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAllay.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAmbient.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAnimals.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAreaEffectCloud.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftArmadillo.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftArmorStand.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftArrow.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftAxolotl.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftBat.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftBee.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftBlaze.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockAttachedEntity.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftBlockDisplay.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftBoat.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftBogged.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftBreeze.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftBreezeWindCharge.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftCamel.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftCat.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftCaveSpider.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftChestBoat.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftChestedHorse.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftChicken.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftCod.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftComplexPart.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftCow.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftCreaking.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftCreature.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftCreeper.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftDisplay.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftDolphin.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftDrowned.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEgg.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderCrystal.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragon.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderDragonPart.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderPearl.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderSignal.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEnderman.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEndermite.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEvoker.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftEvokerFangs.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftExperienceOrb.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftFallingBlock.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftFireball.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftFirework.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftFish.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftFishHook.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftFlying.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftFox.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftFrog.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftGhast.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftGiant.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowItemFrame.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftGlowSquid.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftGoat.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftGolem.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftGuardian.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftHanging.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftHoglin.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftHorse.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftIllager.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftIllusioner.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftInteraction.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftIronGolem.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftItemDisplay.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftItemFrame.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftLargeFireball.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftLeash.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftLightningStrike.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftLlama.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftLlamaSpit.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMagmaCube.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMarker.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecart.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartCommand.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartContainer.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartFurnace.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartHopper.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartMobSpawner.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMinecartTNT.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMob.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMonster.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftMushroomCow.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftOcelot.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftOminousItemSpawner.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPainting.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPanda.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftParrot.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPhantom.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPig.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPigZombie.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglin.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinAbstract.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPiglinBrute.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPillager.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPolarBear.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftProjectile.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPufferFish.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftRabbit.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftRaider.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftRavager.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSalmon.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSheep.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftShulker.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftShulkerBullet.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSilverfish.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSizedFireball.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeleton.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSkeletonHorse.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSlime.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSmallFireball.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSniffer.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowball.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSnowman.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSpectralArrow.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSpellcaster.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSpider.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftSquid.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftStrider.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftTNTPrimed.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftTadpole.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftTameableAnimal.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftTextDisplay.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftThrowableProjectile.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownExpBottle.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftThrownPotion.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftTraderLlama.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftTrident.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftTropicalFish.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftTurtle.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftVex.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftVillagerZombie.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftVindicator.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftWanderingTrader.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftWarden.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftWaterMob.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftWindCharge.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftWitch.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftWither.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftWitherSkull.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftWolf.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftZoglin.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftZombie.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboard.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftScoreboardManager.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/util/DelegatedGeneratorAccess.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/spigotmc/SpigotCommand.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/spigotmc/SpigotConfig.java.patch
delete mode 100644 folia-server/paper-patches/files/src/main/java/org/spigotmc/SpigotWorldConfig.java.patch
delete mode 100644 folia-server/paper-patches/files/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java.patch
diff --git a/folia-server/minecraft-patches/features/0001-Region-Threading-Base.patch b/folia-server/minecraft-patches/features/0001-Region-Threading-Base.patch
new file mode 100644
index 0000000..57325ab
--- /dev/null
+++ b/folia-server/minecraft-patches/features/0001-Region-Threading-Base.patch
@@ -0,0 +1,19827 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Spottedleaf
+Date: Sun, 20 Apr 1997 05:37:42 -0800
+Subject: [PATCH] Region Threading Base
+
+
+diff --git a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+index 1b8193587814225c2ef2c5d9e667436eb50ff6c5..0027a3896c0cfce2f46eca8a0a77a90223723dc7 100644
+--- a/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
++++ b/ca/spottedleaf/moonrise/common/misc/NearbyPlayers.java
+@@ -244,7 +244,7 @@ public final class NearbyPlayers {
+ created.addPlayer(parameter, type);
+ type.addTo(parameter, NearbyPlayers.this.world, chunkX, chunkZ);
+
+- ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created;
++ //((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$requestChunkData(chunkKey).nearbyPlayers = created; // Folia - region threading
+ }
+ }
+
+@@ -263,10 +263,7 @@ public final class NearbyPlayers {
+
+ if (chunk.isEmpty()) {
+ NearbyPlayers.this.byChunk.remove(chunkKey);
+- final ChunkData chunkData = ((ChunkSystemLevel)NearbyPlayers.this.world).moonrise$releaseChunkData(chunkKey);
+- if (chunkData != null) {
+- chunkData.nearbyPlayers = null;
+- }
++ // Folia - region threading
+ }
+ }
+ }
+diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java
+index 4d344559a20a0c35c181e297e81788c747363ec9..779e6b7d025da185b33a963e42e91a56908e46dc 100644
+--- a/ca/spottedleaf/moonrise/paper/PaperHooks.java
++++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java
+@@ -105,7 +105,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo
+ }
+
+ for (final EnderDragonPart part : parts) {
+- if (part != entity && part.getBoundingBox().intersects(boundingBox) && (predicate == null || predicate.test(part))) {
++ if (part != entity && part.getBoundingBox().intersects(boundingBox) && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(part) && (predicate == null || predicate.test(part))) { // Folia - region threading
+ into.add(part);
+ }
+ }
+@@ -127,7 +127,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo
+ continue;
+ }
+ final T casted = (T)entityTypeTest.tryCast(part);
+- if (casted != null && (predicate == null || predicate.test(casted))) {
++ if (casted != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(part) && (predicate == null || predicate.test(casted))) { // Folia - region threading
+ into.add(casted);
+ if (into.size() >= maxCount) {
+ break;
+@@ -275,4 +275,4 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo
+ public int modifyEntityTrackingRange(final Entity entity, final int currentRange) {
+ return org.spigotmc.TrackingRange.getEntityTrackingRange(entity, currentRange);
+ }
+-}
+\ No newline at end of file
++}
+diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
+index ece1261b67033e946dfc20a96872708755bffe0a..8d67b4629c69d3039b199aaad45533d1acde114e 100644
+--- a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
++++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java
+@@ -80,18 +80,23 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
+
+ @Override
+ public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) {
+-
++ // Folia start - threaded regions
++ level.regioniser.addChunk(holder.getPos().x, holder.getPos().z);
++ // Folia end - threaded regions
+ }
+
+ @Override
+ public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) {
+ // Update progress listener for LevelLoadingScreen
+- final net.minecraft.server.level.progress.ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener;
++ final net.minecraft.server.level.progress.ChunkProgressListener progressListener = null; // Folia - threaded regions - cannot schedule chunk task here; as it would create a chunkholder
+ if (progressListener != null) {
+ this.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> {
+ progressListener.onStatusChange(holder.getPos(), null);
+ });
+ }
++ // Folia start - threaded regions
++ level.regioniser.removeChunk(holder.getPos().x, holder.getPos().z);
++ // Folia end - threaded regions
+ }
+
+ @Override
+@@ -102,17 +107,13 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
+
+ @Override
+ public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) {
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add(
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+- );
++ chunk.getLevel().getCurrentWorldData().addChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
+ chunk.loadCallback();
+ }
+
+ @Override
+ public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) {
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove(
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+- );
++ chunk.getLevel().getCurrentWorldData().removeChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
+ chunk.unloadCallback();
+ }
+
+@@ -124,9 +125,7 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
+
+ @Override
+ public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) {
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add(
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+- );
++ chunk.getLevel().getCurrentWorldData().addTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
+ if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) {
+ chunk.postProcessGeneration((ServerLevel)chunk.getLevel());
+ }
+@@ -137,24 +136,18 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
+
+ @Override
+ public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) {
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove(
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+- );
++ chunk.getLevel().getCurrentWorldData().removeTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration
+ }
+
+ @Override
+ public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add(
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+- );
++ chunk.getLevel().getCurrentWorldData().addEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
+ }
+
+ @Override
+ public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) {
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove(
+- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()
+- );
++ chunk.getLevel().getCurrentWorldData().removeEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading
+ }
+
+ @Override
+@@ -191,4 +184,4 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co
+ public void updateMaps(final ServerLevel world, final ServerPlayer player) {
+ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getPlayerChunkLoader().updatePlayer(player);
+ }
+-}
+\ No newline at end of file
++}
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
+index 8b9dc582627b46843f4b5ea6f8c3df2d8cac46fa..306216138e21c41937e4728e8004220a02d6ea4b 100644
+--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/chunk/ChunkData.java
+@@ -5,7 +5,7 @@ import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
+ public final class ChunkData {
+
+ private int referenceCount = 0;
+- public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players
++ //public NearbyPlayers.TrackedChunk nearbyPlayers; // Moonrise - nearby players // Folia - region threading
+
+ public ChunkData() {
+
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
+index 7554c109c35397bc1a43dd80e87764fd78645bbf..db16fe8d664f9b04710200d63439564cb97c0066 100644
+--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java
+@@ -460,6 +460,19 @@ public abstract class EntityLookup implements LevelEntityGetter {
+ return slices == null || !slices.isPreventingStatusUpdates();
+ }
+
++ // Folia start - region threading
++ // only appropriate to use when in shutdown, as this performs no logic hooks to properly add to world
++ public boolean addEntityForShutdownTeleportComplete(final Entity entity) {
++ final BlockPos pos = entity.blockPosition();
++ final int sectionX = pos.getX() >> 4;
++ final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world));
++ final int sectionZ = pos.getZ() >> 4;
++ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ);
++
++ return slices.addEntity(entity, sectionY);
++ }
++ // Folia end - region threading
++
+ protected void removeEntity(final Entity entity) {
+ final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX();
+ final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY();
+@@ -986,6 +999,9 @@ public abstract class EntityLookup implements LevelEntityGetter {
+ EntityLookup.this.removeEntityCallback(entity);
+
+ this.entity.setLevelCallback(NoOpCallback.INSTANCE);
++
++ // only AFTER full removal callbacks, so that thread checking will work. // Folia - region threading
++ EntityLookup.this.world.getCurrentWorldData().removeEntity(entity); // Folia - region threading
+ }
+ }
+
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
+index 26207443b1223119c03db478d7e816d9cdf8e618..a89ee24c6aed46af23c5de7ae2234c7902f1c0f4 100644
+--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java
+@@ -17,7 +17,7 @@ public final class ServerEntityLookup extends EntityLookup {
+ private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0];
+
+ private final ServerLevel serverWorld;
+- public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
++ // Folia - move to regionized world data
+
+ public ServerEntityLookup(final ServerLevel world, final LevelCallback worldCallback) {
+ super(world, worldCallback);
+@@ -75,6 +75,7 @@ public final class ServerEntityLookup extends EntityLookup {
+ if (entity instanceof ServerPlayer player) {
+ ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player);
+ }
++ this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading
+ }
+
+ @Override
+@@ -87,14 +88,14 @@ public final class ServerEntityLookup extends EntityLookup {
+ @Override
+ protected void entityStartLoaded(final Entity entity) {
+ // Moonrise start - entity tracker
+- this.trackerEntities.add(entity);
++ this.world.getCurrentWorldData().trackerEntities.add(entity); // Folia - region threading
+ // Moonrise end - entity tracker
+ }
+
+ @Override
+ protected void entityEndLoaded(final Entity entity) {
+ // Moonrise start - entity tracker
+- this.trackerEntities.remove(entity);
++ this.world.getCurrentWorldData().trackerEntities.remove(entity); // Folia - region threading
+ // Moonrise end - entity tracker
+ }
+
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
+index dd2509996bfd08e8c3f9f2be042229eac6d7692d..f77dcf5a42ff34a1624ddf16bcce2abee81194bb 100644
+--- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java
+@@ -216,7 +216,7 @@ public final class RegionizedPlayerChunkLoader {
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+
+ if (loader == null) {
+- return;
++ throw new IllegalStateException("Player is already removed from player chunk loader"); // Folia - region threading
+ }
+
+ loader.remove();
+@@ -304,7 +304,7 @@ public final class RegionizedPlayerChunkLoader {
+ public void tick() {
+ TickThread.ensureTickThread("Cannot tick player chunk loader async");
+ long currTime = System.nanoTime();
+- for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) {
++ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.getLocalPlayers())) { // Folia - region threding
+ final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader();
+ if (loader == null || loader.removed || loader.world != this.world) {
+ // not our problem anymore
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
+index 7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309..4bfcae47ed76346e6200514ebce5b04f907c5026 100644
+--- a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java
+@@ -29,6 +29,39 @@ public final class ChunkUnloadQueue {
+
+ public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {}
+
++ // Folia start - threaded regions
++ public List retrieveForCurrentRegion() {
++ final io.papermc.paper.threadedregions.ThreadedRegionizer.ThreadedRegion region =
++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion();
++ final io.papermc.paper.threadedregions.ThreadedRegionizer regionizer = region.regioniser;
++ final int shift = this.coordinateShift;
++
++ final List ret = new ArrayList<>();
++
++ for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) {
++ final ConcurrentLong2ReferenceChainedHashTable.TableEntry entry = iterator.next();
++ final long key = entry.getKey();
++ final UnloadSection section = entry.getValue();
++ final int sectionX = CoordinateUtils.getChunkX(key);
++ final int sectionZ = CoordinateUtils.getChunkZ(key);
++ final int chunkX = sectionX << shift;
++ final int chunkZ = sectionZ << shift;
++
++ if (regionizer.getRegionAtUnsynchronised(chunkX, chunkZ) != region) {
++ continue;
++ }
++
++ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size()));
++ }
++
++ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> {
++ return Long.compare(s1.order, s2.order);
++ });
++
++ return ret;
++ }
++ // Folia end - threaded regions
++
+ public List retrieveForAllRegions() {
+ final List ret = new ArrayList<>();
+
+@@ -141,4 +174,4 @@ public final class ChunkUnloadQueue {
+ this.order = order;
+ }
+ }
+-}
+\ No newline at end of file
++}
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
+index b5817aa8f537593f6d9fc6b612c82ccccb250ac7..aae97116a22a87cffd4756d566da3acd96ce2ae0 100644
+--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java
+@@ -56,6 +56,14 @@ import java.util.concurrent.atomic.AtomicReference;
+ import java.util.concurrent.locks.LockSupport;
+ import java.util.function.Predicate;
+
++// Folia start - region threading
++import io.papermc.paper.threadedregions.RegionizedServer;
++import io.papermc.paper.threadedregions.ThreadedRegionizer;
++import io.papermc.paper.threadedregions.TickRegionScheduler;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
++// Folia end - region threading
++
+ public final class ChunkHolderManager {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+@@ -78,29 +86,83 @@ public final class ChunkHolderManager {
+ private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f);
+ private final ServerLevel world;
+ private final ChunkTaskScheduler taskScheduler;
+- private long currentTick;
++ // Folia start - region threading
++ public static final class HolderManagerRegionData {
++ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>();
++ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
++ if (c1 == c2) {
++ return 0;
++ }
+
+- private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>();
+- private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
+- if (c1 == c2) {
+- return 0;
++ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
++
++ if (saveTickCompare != 0) {
++ return saveTickCompare;
++ }
++
++ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
++ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
++
++ if (coord1 == coord2) {
++ throw new IllegalStateException("Duplicate chunkholder in auto save queue");
++ }
++
++ return Long.compare(coord1, coord2);
++ });
++
++ public void merge(final HolderManagerRegionData into, final long tickOffset) {
++ // Order doesn't really matter for the pending full update...
++ into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate);
++
++ // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating
++ // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant
++ // addition to every entry will not affect compareTo).
++ for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) {
++ holder.lastAutoSave += tickOffset;
++ into.autoSaveQueue.add(holder);
++ }
+ }
+
+- final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave);
++ public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData,
++ final ReferenceOpenHashSet dataSet) {
++ for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) {
++ final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift;
++ final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift;
++
++ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
++ if (data != null) {
++ data.pendingFullLoadUpdate.add(fullLoadUpdate);
++ } // else: fullLoadUpdate is an unloaded chunk holder
++ }
+
+- if (saveTickCompare != 0) {
+- return saveTickCompare;
++ for (final NewChunkHolder autoSave : this.autoSaveQueue) {
++ final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift;
++ final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift;
++
++ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ));
++ if (data != null) {
++ data.autoSaveQueue.add(autoSave);
++ } // else: autoSave is an unloaded chunk holder
++ }
+ }
++ }
+
+- final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ);
+- final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ);
++ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() {
++ final ThreadedRegionizer.ThreadedRegion region =
++ TickRegionScheduler.getCurrentRegion();
+
+- if (coord1 == coord2) {
+- throw new IllegalStateException("Duplicate chunkholder in auto save queue");
++ if (region == null) {
++ return null;
+ }
+
+- return Long.compare(coord1, coord2);
+- });
++ if (this.world != null && this.world != region.getData().world) {
++ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
++ }
++
++ return region.getData().getHolderManagerRegionData();
++ }
++ // Folia end - region threading
++
+
+ public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) {
+ this.world = world;
+@@ -185,8 +247,13 @@ public final class ChunkHolderManager {
+ }
+
+ public void close(final boolean save, final boolean halt) {
++ // Folia start - region threading
++ this.close(save, halt, true, true, true);
++ }
++ public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) {
++ // Folia end - region threading
+ TickThread.ensureTickThread("Closing world off-main");
+- if (halt) {
++ if (first && halt) { // Folia - region threading
+ LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'");
+ if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) {
+ LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'");
+@@ -196,9 +263,10 @@ public final class ChunkHolderManager {
+ }
+
+ if (save) {
+- this.saveAllChunks(true, true, true);
++ this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading
+ }
+
++ if (last) { // Folia - region threading
+ MoonriseRegionFileIO.flush(this.world);
+
+ if (halt) {
+@@ -220,28 +288,35 @@ public final class ChunkHolderManager {
+ }
+
+ this.taskScheduler.setShutdown(true);
++ } // Folia - region threading
+ }
+
+ void ensureInAutosave(final NewChunkHolder holder) {
+- if (!this.autoSaveQueue.contains(holder)) {
+- holder.lastAutoSave = this.currentTick;
+- this.autoSaveQueue.add(holder);
++ // Folia start - region threading
++ final HolderManagerRegionData regionData = this.getCurrentRegionData();
++ if (!regionData.autoSaveQueue.contains(holder)) {
++ holder.lastAutoSave = RegionizedServer.getCurrentTick();
++ regionData.autoSaveQueue.add(holder);
++ // Folia end - region threading
+ }
+ }
+
+ public void autoSave() {
+ final List reschedule = new ArrayList<>();
+- final long currentTick = this.currentTick;
++ final long currentTick = RegionizedServer.getCurrentTick(); // Folia - region threading
+ final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval(this.world));
+ final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world);
+- for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) {
+- final NewChunkHolder holder = this.autoSaveQueue.first();
++ // Folia start - region threading
++ final HolderManagerRegionData regionData = this.getCurrentRegionData();
++ for (int autoSaved = 0; autoSaved < maxToSave && !regionData.autoSaveQueue.isEmpty();) {
++ final NewChunkHolder holder = regionData.autoSaveQueue.first();
++ // Folia end - region threading
+
+ if (holder.lastAutoSave > maxSaveTime) {
+ break;
+ }
+
+- this.autoSaveQueue.remove(holder);
++ regionData.autoSaveQueue.remove(holder); // Folia - region threading
+
+ holder.lastAutoSave = currentTick;
+ if (holder.save(false) != null) {
+@@ -255,15 +330,38 @@ public final class ChunkHolderManager {
+
+ for (final NewChunkHolder holder : reschedule) {
+ if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) {
+- this.autoSaveQueue.add(holder);
++ regionData.autoSaveQueue.add(holder); // Folia start - region threading
+ }
+ }
+ }
+
+ public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) {
+- final List holders = this.getChunkHolders();
++ // Folia start - region threading
++ this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true);
++ }
++ public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) {
++ final List holders = new java.util.ArrayList<>(this.chunkHolders.size() / 10);
++ // we could iterate through all chunk holders with thread checks, however for many regions the iteration cost alone
++ // will multiply. to avoid this, we can simply iterate through all owned sections
++ final int regionShift = this.world.moonrise$getRegionChunkShift();
++ final int width = 1 << regionShift;
++ for (final LongIterator iterator = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
++ final long sectionKey = iterator.nextLong();
++ final int offsetX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
++ final int offsetZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
++
++ for (int dz = 0; dz < width; ++dz) {
++ for (int dx = 0; dx < width; ++dx) {
++ final NewChunkHolder holder = this.getChunkHolder(offsetX | dx, offsetZ | dz);
++ if (holder != null) {
++ holders.add(holder);
++ }
++ }
++ }
++ }
++ // Folia end - region threading
+
+- if (logProgress) {
++ if (first && logProgress) { // Folia - region threading
+ LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'");
+ }
+
+@@ -292,6 +390,12 @@ public final class ChunkHolderManager {
+ }
+ for (int i = 0, len = holders.size(); i < len; ++i) {
+ final NewChunkHolder holder = holders.get(i);
++ // Folia start - region threading
++ if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) {
++ // skip holders that would fail the thread check
++ continue;
++ }
++ // Folia end - region threading
+ try {
+ final NewChunkHolder.SaveStat saveStat = holder.save(shutdown);
+ if (saveStat != null) {
+@@ -327,7 +431,7 @@ public final class ChunkHolderManager {
+ }
+ }
+ }
+- if (flush) {
++ if (last && flush) { // Folia - region threading
+ MoonriseRegionFileIO.flush(this.world);
+ try {
+ MoonriseRegionFileIO.flushRegionStorages(this.world);
+@@ -732,7 +836,13 @@ public final class ChunkHolderManager {
+ }
+
+ public void tick() {
+- ++this.currentTick;
++ // Folia start - region threading
++ final ThreadedRegionizer.ThreadedRegion region =
++ TickRegionScheduler.getCurrentRegion();
++ if (region == null) {
++ throw new IllegalStateException("Not running tick() while on a region");
++ }
++ // Folia end - region threading
+
+ final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift();
+
+@@ -746,7 +856,7 @@ public final class ChunkHolderManager {
+ return removeDelay <= 0L;
+ };
+
+- for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
++ for (final LongIterator iterator = region.getOwnedSectionsUnsynchronised(); iterator.hasNext();) {
+ final long sectionKey = iterator.nextLong();
+
+ if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) {
+@@ -1031,26 +1141,56 @@ public final class ChunkHolderManager {
+ if (changedFullStatus.isEmpty()) {
+ return;
+ }
+- if (!TickThread.isTickThread()) {
+- this.taskScheduler.scheduleChunkTask(() -> {
+- final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
+- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
+- pendingFullLoadUpdate.add(changedFullStatus.get(i));
+- }
+
+- ChunkHolderManager.this.processPendingFullUpdate();
+- }, Priority.HIGHEST);
+- } else {
+- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate;
+- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
+- pendingFullLoadUpdate.add(changedFullStatus.get(i));
++ // Folia start - region threading
++ final Long2ObjectOpenHashMap> sectionToUpdates = new Long2ObjectOpenHashMap<>();
++ final List thisRegionHolders = new ArrayList<>();
++
++ final int regionShift = this.world.moonrise$getRegionChunkShift();
++ final ThreadedRegionizer.ThreadedRegion thisRegion
++ = TickRegionScheduler.getCurrentRegion();
++
++ for (final NewChunkHolder holder : changedFullStatus) {
++ final int regionX = holder.chunkX >> regionShift;
++ final int regionZ = holder.chunkZ >> regionShift;
++ final long holderSectionKey = CoordinateUtils.getChunkKey(regionX, regionZ);
++
++ // region may be null
++ if (thisRegion != null && this.world.regioniser.getRegionAtUnsynchronised(holder.chunkX, holder.chunkZ) == thisRegion) {
++ thisRegionHolders.add(holder);
++ } else {
++ sectionToUpdates.computeIfAbsent(holderSectionKey, (final long keyInMap) -> {
++ return new ArrayList<>();
++ }).add(holder);
++ }
++ }
++ if (!thisRegionHolders.isEmpty()) {
++ thisRegion.getData().getHolderManagerRegionData().pendingFullLoadUpdate.addAll(thisRegionHolders);
++ }
++
++ if (!sectionToUpdates.isEmpty()) {
++ for (final Iterator>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator();
++ iterator.hasNext();) {
++ final Long2ObjectMap.Entry> entry = iterator.next();
++ final long sectionKey = entry.getLongKey();
++
++ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift;
++ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift;
++
++ final List regionHolders = entry.getValue();
++ this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> {
++ ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders);
++ ChunkHolderManager.this.processPendingFullUpdate();
++ }, Priority.HIGHEST);
++
+ }
+ }
++ // Folia end - region threading
+ }
+
+ private void removeChunkHolder(final NewChunkHolder holder) {
+ holder.onUnload();
+- this.autoSaveQueue.remove(holder);
++ this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading
+ PlatformHooks.get().onChunkHolderDelete(this.world, holder.vanillaChunkHolder);
+ this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ));
+ }
+@@ -1063,7 +1203,7 @@ public final class ChunkHolderManager {
+ throw new IllegalStateException("Cannot unload chunks recursively");
+ }
+ final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift
+- final List unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions();
++ final List unloadSectionsForRegion = this.unloadQueue.retrieveForCurrentRegion(); // Folia - threaded regions
+ int unloadCountTentative = 0;
+ for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) {
+ final ChunkUnloadQueue.UnloadSection section
+@@ -1381,7 +1521,13 @@ public final class ChunkHolderManager {
+
+ // only call on tick thread
+ private boolean processPendingFullUpdate() {
+- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate;
++ // Folia start - region threading
++ final HolderManagerRegionData data = this.getCurrentRegionData();
++ if (data == null) {
++ return false;
++ }
++ final ArrayDeque pendingFullLoadUpdate = data.pendingFullLoadUpdate;
++ // Folia end - region threading
+
+ boolean ret = false;
+
+@@ -1392,9 +1538,7 @@ public final class ChunkHolderManager {
+ ret |= holder.handleFullStatusChange(changedFullStatus);
+
+ if (!changedFullStatus.isEmpty()) {
+- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
+- pendingFullLoadUpdate.add(changedFullStatus.get(i));
+- }
++ this.addChangedStatuses(changedFullStatus); // Folia - region threading
+ changedFullStatus.clear();
+ }
+ }
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
+index 67532b85073b7978254a0b04caadfe822679e61f..cba2d16c0cb5adc92952990ef95b1c979eafd40f 100644
+--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java
+@@ -122,7 +122,7 @@ public final class ChunkTaskScheduler {
+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor;
+ public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor;
+
+- private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue();
++ // Folia - regionised ticking
+
+ public final ChunkHolderManager chunkHolderManager;
+
+@@ -337,14 +337,13 @@ public final class ChunkTaskScheduler {
+ };
+
+ // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions
+- this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING);
++ this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, Priority.BLOCKING); // Folia - region threading
+ // so, make the main thread pick it up
+ ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException));
+ }
+
+ public boolean executeMainThreadTask() {
+- TickThread.ensureTickThread("Cannot execute main thread task off-main");
+- return this.mainThreadExecutor.executeTask();
++ throw new UnsupportedOperationException("Use regionised ticking hooks"); // Folia - regionised ticking
+ }
+
+ public void raisePriority(final int x, final int z, final Priority priority) {
+@@ -829,7 +828,7 @@ public final class ChunkTaskScheduler {
+ */
+ @Deprecated
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) {
+- return this.mainThreadExecutor.queueTask(run, priority);
++ throw new UnsupportedOperationException(); // Folia - regionised ticking
+ }
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
+@@ -838,7 +837,7 @@ public final class ChunkTaskScheduler {
+
+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run,
+ final Priority priority) {
+- return this.mainThreadExecutor.createTask(run, priority);
++ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
+ }
+
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) {
+@@ -847,9 +846,27 @@ public final class ChunkTaskScheduler {
+
+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run,
+ final Priority priority) {
+- return this.mainThreadExecutor.queueTask(run, priority);
++ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking
+ }
+
++ // Folia start - region threading
++ // this function is guaranteed to never touch the ticket lock or schedule lock
++ // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the
++ // ticket lock in the schedule logic
++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run) {
++ return this.scheduleChunkTaskEventually(chunkX, chunkZ, run, Priority.NORMAL);
++ }
++
++ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run,
++ final Priority priority) {
++ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority);
++ this.world.taskQueueRegionData.pushGlobalChunkTask(() -> {
++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority);
++ });
++ return ret;
++ }
++ // Folia end - region threading
++
+ public boolean halt(final boolean sync, final long maxWaitNS) {
+ this.radiusAwareGenExecutor.halt();
+ this.parallelGenExecutor.halt();
+diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
+index e4a5fa25ed368fc4662c30934da2963ef446d782..601ed36413bbbf9c17e530b42906986e441237fd 100644
+--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java
+@@ -1359,10 +1359,10 @@ public final class NewChunkHolder {
+ private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) {
+ // Update progress listener for LevelLoadingScreen
+ if (chunk != null) {
+- final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener;
++ final ChunkProgressListener progressListener = null; // Folia - threaded regions
+ if (progressListener != null) {
+ final ChunkStatus finalStatus = status;
+- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
++ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - threaded regions
+ progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus);
+ });
+ }
+@@ -1383,7 +1383,7 @@ public final class NewChunkHolder {
+ }
+
+ // must be scheduled to main, we do not trust the callback to not do anything stupid
+- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
++ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading
+ for (final Consumer consumer : consumers) {
+ try {
+ consumer.accept(chunk);
+@@ -1411,7 +1411,7 @@ public final class NewChunkHolder {
+ }
+
+ // must be scheduled to main, we do not trust the callback to not do anything stupid
+- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> {
++ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading
+ for (final Consumer consumer : consumers) {
+ try {
+ consumer.accept(chunk);
+diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
+index e04bd54744335fb5398c6e4f7ce8b981f35bfb7d..471b6d49d77e03665ffc269d17ab46f225e3ce1c 100644
+--- a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
++++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java
+@@ -1940,6 +1940,17 @@ public final class CollisionUtil {
+
+ for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) {
+ for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) {
++ // Folia start - region threading
++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, currChunkX, currChunkZ, 4)) {
++ if (checkOnly) {
++ return true;
++ } else {
++ intoAABB.add(getBoxForChunk(currChunkX, currChunkZ));
++ ret = true;
++ continue;
++ }
++ }
++ // Folia end - region threading
+ final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks);
+
+ if (chunk == null) {
+diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java
+index ade6110cc6adb1263c0359ff7e96e96b959e61f3..c260741a87513b89a5cc62c543fb9f990f86491e 100644
+--- a/io/papermc/paper/entity/activation/ActivationRange.java
++++ b/io/papermc/paper/entity/activation/ActivationRange.java
+@@ -48,33 +48,34 @@ public final class ActivationRange {
+
+ private static int checkInactiveWakeup(final Entity entity) {
+ final Level world = entity.level();
++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
+ final SpigotWorldConfig config = world.spigotConfig;
+- final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
++ final long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions
+ if (entity.activationType == ActivationType.VILLAGER) {
+- if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) {
+- world.wakeupInactiveRemainingVillagers--;
++ if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions
++ worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions
+ return config.wakeUpInactiveVillagersFor;
+ }
+ } else if (entity.activationType == ActivationType.ANIMAL) {
+- if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) {
+- world.wakeupInactiveRemainingAnimals--;
++ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions
++ worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions
+ return config.wakeUpInactiveAnimalsFor;
+ }
+ } else if (entity.activationType == ActivationType.FLYING_MONSTER) {
+- if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) {
+- world.wakeupInactiveRemainingFlying--;
++ if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions
++ worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions
+ return config.wakeUpInactiveFlyingFor;
+ }
+ } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) {
+- if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) {
+- world.wakeupInactiveRemainingMonsters--;
++ if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions
++ worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions
+ return config.wakeUpInactiveMonstersFor;
+ }
+ }
+ return -1;
+ }
+
+- static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0);
++ //static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0); // Folia - threaded regions - replaced by local variable
+
+ /**
+ * These entities are excluded from Activation range checks.
+@@ -122,10 +123,11 @@ public final class ActivationRange {
+ final int waterActivationRange = world.spigotConfig.waterActivationRange;
+ final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange;
+ final int villagerActivationRange = world.spigotConfig.villagerActivationRange;
+- world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals);
+- world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers);
+- world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters);
+- world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying);
++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions
++ worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Folia - threaded regions
++ worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Folia - threaded regions
++ worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Folia - threaded regions
++ worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Folia - threaded regions
+
+ int maxRange = Math.max(monsterActivationRange, animalActivationRange);
+ maxRange = Math.max(maxRange, raiderActivationRange);
+@@ -135,30 +137,37 @@ public final class ActivationRange {
+ maxRange = Math.max(maxRange, villagerActivationRange);
+ maxRange = Math.min((world.spigotConfig.simulationDistance << 4) - 8, maxRange);
+
+- for (final Player player : world.players()) {
+- player.activatedTick = MinecraftServer.currentTick;
++ for (final Player player : world.getLocalPlayers()) { // Folia - region threading
++ player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading
+ if (world.spigotConfig.ignoreSpectatorActivation && player.isSpectator()) {
+ continue;
+ }
+
+ final int worldHeight = world.getHeight();
+- ActivationRange.maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange);
+- ActivationType.MISC.boundingBox = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange);
+- ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange);
+- ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange);
+- ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange);
+- ActivationType.WATER.boundingBox = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange);
+- ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange);
+- ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange);
++ final AABB maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange); // Folia - threaded regions
++ final AABB[] bbByType = new AABB[ActivationType.values().length]; // Folia - threaded regions
++ bbByType[ActivationType.MISC.ordinal()] = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange); // Folia - threaded regions
++ bbByType[ActivationType.RAIDER.ordinal()] = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange); // Folia - threaded regions
++ bbByType[ActivationType.ANIMAL.ordinal()] = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange); // Folia - threaded regions
++ bbByType[ActivationType.MONSTER.ordinal()] = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange); // Folia - threaded regions
++ bbByType[ActivationType.WATER.ordinal()] = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange); // Folia - threaded regions
++ bbByType[ActivationType.FLYING_MONSTER.ordinal()] = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange); // Folia - threaded regions
++ bbByType[ActivationType.VILLAGER.ordinal()] = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange); // Folia - threaded regions
+
+- final java.util.List entities = world.getEntities((Entity) null, ActivationRange.maxBB, e -> true);
++ final java.util.List entities = new java.util.ArrayList<>(); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later
++ ((net.minecraft.server.level.ServerLevel)world).moonrise$getEntityLookup().getEntities((Entity)null, maxBB, entities, null); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later
+ final boolean tickMarkers = world.paperConfig().entities.markers.tick;
+ for (final Entity entity : entities) {
++ // Folia start - region ticking
++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) {
++ continue;
++ }
++ // Folia end - region ticking
+ if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) {
+ continue;
+ }
+
+- ActivationRange.activateEntity(entity);
++ ActivationRange.activateEntity(entity, bbByType); // Folia - threaded regions
+ }
+ }
+ }
+@@ -168,14 +177,14 @@ public final class ActivationRange {
+ *
+ * @param entity
+ */
+- private static void activateEntity(final Entity entity) {
+- if (MinecraftServer.currentTick > entity.activatedTick) {
++ private static void activateEntity(final Entity entity, final AABB[] bbByType) { // Folia - threaded regions
++ if (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() > entity.activatedTick) { // Folia - threaded regions
+ if (entity.defaultActivationState) {
+- entity.activatedTick = MinecraftServer.currentTick;
++ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
+ return;
+ }
+- if (entity.activationType.boundingBox.intersects(entity.getBoundingBox())) {
+- entity.activatedTick = MinecraftServer.currentTick;
++ if (bbByType[entity.activationType.ordinal()].intersects(entity.getBoundingBox())) { // Folia - threaded regions
++ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
+ }
+ }
+ }
+@@ -189,6 +198,7 @@ public final class ActivationRange {
+ */
+ public static int checkEntityImmunities(final Entity entity) { // return # of ticks to get immunity
+ final SpigotWorldConfig config = entity.level().spigotConfig;
++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = entity.level().getCurrentWorldData(); // Folia - threaded regions
+ final int inactiveWakeUpImmunity = checkInactiveWakeup(entity);
+ if (inactiveWakeUpImmunity > -1) {
+ return inactiveWakeUpImmunity;
+@@ -196,10 +206,10 @@ public final class ActivationRange {
+ if (entity.getRemainingFireTicks() > 0) {
+ return 2;
+ }
+- if (entity.activatedImmunityTick >= MinecraftServer.currentTick) {
++ if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - threaded regions
+ return 1;
+ }
+- final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick;
++ final long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions
+ if ((entity.activationType != ActivationType.WATER && entity.isInWater() && entity.isPushedByFluid())) {
+ return 100;
+ }
+@@ -296,16 +306,16 @@ public final class ActivationRange {
+ return true;
+ }
+
+- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick;
++ boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions
+ entity.isTemporarilyActive = false;
+
+ // Should this entity tick?
+ if (!isActive) {
+- if ((MinecraftServer.currentTick - entity.activatedTick - 1) % 20 == 0) {
++ if ((io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick - 1) % 20 == 0) { // Folia - threaded regions
+ // Check immunities every 20 ticks.
+ final int immunity = checkEntityImmunities(entity);
+ if (immunity >= 0) {
+- entity.activatedTick = MinecraftServer.currentTick + immunity;
++ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + immunity; // Folia - threaded regions
+ } else {
+ entity.isTemporarilyActive = true;
+ }
+diff --git a/io/papermc/paper/redstone/RedstoneWireTurbo.java b/io/papermc/paper/redstone/RedstoneWireTurbo.java
+index ff747a1ecdf3c888bca0d69de4f85dcd810b6139..5a76c93eada8db35b1ddbb562ccfbd2f0d35f0ca 100644
+--- a/io/papermc/paper/redstone/RedstoneWireTurbo.java
++++ b/io/papermc/paper/redstone/RedstoneWireTurbo.java
+@@ -829,14 +829,14 @@ public final class RedstoneWireTurbo {
+ j = getMaxCurrentStrength(upd, j);
+ int l = 0;
+
+- wire.shouldSignal = false;
++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading
+ // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated,
+ // and I'm not ready to try to replicate even more functionality from
+ // elsewhere in Minecraft into this accelerator. So sadly, we must
+ // suffer the performance hit of this very expensive call. If there
+ // is consistency to what this call returns, we may be able to cache it.
+ final int k = worldIn.getBestNeighborSignal(upd.self);
+- wire.shouldSignal = true;
++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading
+
+ // The variable 'k' holds the maximum redstone power value of any adjacent blocks.
+ // If 'k' has the highest level of all neighbors, then the power level of this
+diff --git a/io/papermc/paper/threadedregions/RegionShutdownThread.java b/io/papermc/paper/threadedregions/RegionShutdownThread.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..bbd14cf34438a9366f5ff29f1acba4282d77d983
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/RegionShutdownThread.java
+@@ -0,0 +1,226 @@
++package io.papermc.paper.threadedregions;
++
++import ca.spottedleaf.moonrise.common.util.WorldUtil;
++import com.mojang.logging.LogUtils;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.item.ItemStack;
++import net.minecraft.world.level.ChunkPos;
++import org.bukkit.event.inventory.InventoryCloseEvent;
++import org.slf4j.Logger;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.concurrent.TimeUnit;
++
++public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.util.TickThread {
++
++ private static final Logger LOGGER = LogUtils.getClassLogger();
++
++ ThreadedRegionizer.ThreadedRegion shuttingDown;
++
++ public RegionShutdownThread(final String name) {
++ super(name);
++ this.setUncaughtExceptionHandler((thread, thr) -> {
++ LOGGER.error("Error shutting down server", thr);
++ });
++ }
++
++ static ThreadedRegionizer.ThreadedRegion getRegion() {
++ final Thread currentThread = Thread.currentThread();
++ if (currentThread instanceof RegionShutdownThread shutdownThread) {
++ return shutdownThread.shuttingDown;
++ }
++ return null;
++ }
++
++
++ static RegionizedWorldData getWorldData() {
++ final Thread currentThread = Thread.currentThread();
++ if (currentThread instanceof RegionShutdownThread shutdownThread) {
++ // no fast path for shutting down
++ if (shutdownThread.shuttingDown != null) {
++ return shutdownThread.shuttingDown.getData().world.worldRegionData.get();
++ }
++ }
++ return null;
++ }
++
++ // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves
++ // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because
++ // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not.
++ // Thus, the only operation that the shutdown thread will perform
++
++ private void saveLevelData(final ServerLevel world) {
++ try {
++ world.saveLevelData(true);
++ } catch (final Throwable thr) {
++ LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr);
++ }
++ }
++
++ private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region,
++ final ServerLevel world) {
++ try {
++ this.shuttingDown = region;
++ final List pendingTeleports = world.removeAllRegionTeleports();
++ if (pendingTeleports.isEmpty()) {
++ return;
++ }
++ final ChunkPos center = region.getCenterChunk();
++ LOGGER.info("Completing " + pendingTeleports.size() + " pending teleports in region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'");
++ for (final ServerLevel.PendingTeleport pendingTeleport : pendingTeleports) {
++ LOGGER.info("Completing teleportation to target position " + pendingTeleport.to());
++
++ // first, add entities to entity chunk so that they will be saved
++ for (final Entity.EntityTreeNode node : pendingTeleport.rootVehicle().getFullTree()) {
++ // assume that world and position are set to destination here
++ node.root.setLevel(world); // in case the pending teleport is from a portal before it finds the exact destination
++ world.moonrise$getEntityLookup().addEntityForShutdownTeleportComplete(node.root);
++ }
++
++ // then, rebuild the passenger tree so that when saving only the root vehicle will be written - and if
++ // there are any player passengers, that the later player saving will save the tree
++ pendingTeleport.rootVehicle().restore();
++
++ // now we are finished
++ LOGGER.info("Completed teleportation to target position " + pendingTeleport.to());
++ }
++ } catch (final Throwable thr) {
++ LOGGER.error("Failed to complete pending teleports", thr);
++ } finally {
++ this.shuttingDown = null;
++ }
++ }
++
++ private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region,
++ final boolean last) {
++ ChunkPos center = null;
++ try {
++ this.shuttingDown = region;
++ center = region.getCenterChunk();
++ LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'");
++ region.regioniser.world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(true, true, false, last, false);
++ } catch (final Throwable thr) {
++ LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr);
++ } finally {
++ this.shuttingDown = null;
++ }
++ }
++
++ private void haltChunkSystem(final ServerLevel world) {
++ try {
++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(false, true, true, false, false);
++ } catch (final Throwable thr) {
++ LOGGER.error("Failed to halt chunk system for world '" + world.getWorld().getName() + "'", thr);
++ }
++ }
++
++ private void closePlayerInventories(final ThreadedRegionizer.ThreadedRegion region) {
++ ChunkPos center = null;
++ try {
++ this.shuttingDown = region;
++ center = region.getCenterChunk();
++
++ final RegionizedWorldData worldData = region.regioniser.world.worldRegionData.get();
++
++ for (final ServerPlayer player : worldData.getLocalPlayers()) {
++ try {
++ // close inventory
++ if (player.containerMenu != player.inventoryMenu) {
++ player.closeContainer(InventoryCloseEvent.Reason.DISCONNECT);
++ }
++
++ // drop carried item
++ if (!player.containerMenu.getCarried().isEmpty()) {
++ ItemStack carried = player.containerMenu.getCarried();
++ player.containerMenu.setCarried(ItemStack.EMPTY);
++ player.drop(carried, false);
++ }
++ } catch (final Throwable thr) {
++ LOGGER.error("Failed to close player inventory for player: " + player, thr);
++ }
++ }
++ } catch (final Throwable thr) {
++ LOGGER.error("Failed to close player inventories for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr);
++ } finally {
++ this.shuttingDown = null;
++ }
++ }
++
++ @Override
++ public final void run() {
++ // await scheduler termination
++ LOGGER.info("Awaiting scheduler termination for 60s...");
++ if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) {
++ LOGGER.info("Scheduler halted");
++ } else {
++ LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways");
++ TickRegions.getScheduler().dumpAliveThreadTraces("Did not shut down in time");
++ }
++
++ MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc
++ // halt all chunk systems first so that any in-progress chunk generation stops
++ LOGGER.info("Halting chunk systems...");
++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
++ try {
++ world.moonrise$getChunkTaskScheduler().halt(false, 0L);
++ } catch (final Throwable throwable) {
++ LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable);
++ }
++ }
++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
++ this.haltChunkSystem(world);
++ }
++ LOGGER.info("Halted chunk systems");
++
++ LOGGER.info("Finishing pending teleports...");
++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
++ final List>
++ regions = new ArrayList<>();
++ world.regioniser.computeForAllRegionsUnsynchronised(regions::add);
++
++ for (int i = 0, len = regions.size(); i < len; ++i) {
++ this.finishTeleportations(regions.get(i), world);
++ }
++ }
++ LOGGER.info("Finished pending teleports");
++
++ LOGGER.info("Saving all worlds");
++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) {
++ LOGGER.info("Saving world data for world '" + WorldUtil.getWorldName(world) + "'");
++
++ final List>
++ regions = new ArrayList<>();
++ world.regioniser.computeForAllRegionsUnsynchronised(regions::add);
++
++ LOGGER.info("Closing player inventories...");
++ for (int i = 0, len = regions.size(); i < len; ++i) {
++ this.closePlayerInventories(regions.get(i));
++ }
++ LOGGER.info("Closed player inventories");
++
++ LOGGER.info("Saving chunks...");
++ for (int i = 0, len = regions.size(); i < len; ++i) {
++ this.saveRegionChunks(regions.get(i), (i + 1) == len);
++ }
++ LOGGER.info("Saved chunks");
++
++ LOGGER.info("Saving level data...");
++ this.saveLevelData(world);
++ LOGGER.info("Saved level data");
++
++ LOGGER.info("Saved world data for world '" + WorldUtil.getWorldName(world) + "'");
++ }
++ LOGGER.info("Saved all worlds");
++
++ // Note: only save after world data and pending teleportations
++ LOGGER.info("Saving all player data...");
++ MinecraftServer.getServer().getPlayerList().saveAll();
++ LOGGER.info("Saved all player data");
++
++ MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc)
++ // done, part 2 should call exit()
++ }
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/RegionizedData.java b/io/papermc/paper/threadedregions/RegionizedData.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..a1043c426d031755b57b77a9b2eec685e9861b13
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/RegionizedData.java
+@@ -0,0 +1,235 @@
++package io.papermc.paper.threadedregions;
++
++import ca.spottedleaf.concurrentutil.util.Validate;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
++import net.minecraft.server.level.ServerLevel;
++import javax.annotation.Nullable;
++import java.util.function.Supplier;
++
++/**
++ * Use to manage data that needs to be regionised.
++ *
++ * Note: that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionizedData} object is GC'd.
++ * The data is held in reference to the world it resides in.
++ *
++ *
++ * Note: Keep in mind that when regionised ticking is disabled, the entire server is considered a single region.
++ * That is, the data may or may not cross worlds. As such, the {@code RegionizedData} object must be instanced
++ * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions.
++ * See below for more details on instancing per world.
++ *
++ *
++ * Regionised data may be world-checked. That is, {@link #get()} may throw an exception if the current
++ * region's world does not match the {@code RegionizedData}'s world. Consider the usages of {@code RegionizedData} below
++ * see why the behavior may or may not be desirable:
++ *
++ * {@code
++ * public class EntityTickList {
++ * private final List entities = new ArrayList<>();
++ *
++ * public void addEntity(Entity e) {
++ * this.entities.add(e);
++ * }
++ *
++ * public void removeEntity(Entity e) {
++ * this.entities.remove(e);
++ * }
++ * }
++ *
++ * public class World {
++ *
++ * // callback is left out of this example
++ * // note: world != null here
++ * public final RegionizedData entityTickLists =
++ * new RegionizedData<>(this, () -> new EntityTickList(), ...);
++ *
++ * public void addTickingEntity(Entity e) {
++ * // What we expect here is that this world is the
++ * // current ticking region's world.
++ * // If that is true, then calling this.entityTickLists.get()
++ * // will retrieve the current region's EntityTickList
++ * // for this world, which is fine since the current
++ * // region is contained within this world.
++ *
++ * // But if the current region's world is not this world,
++ * // and if the world check is disabled, then we will actually
++ * // retrieve _this_ world's EntityTickList for the region,
++ * // and NOT the EntityTickList for the region's world.
++ * // This is because the RegionizedData object is instantiated
++ * // per world.
++ * this.entityTickLists.get().addEntity(e);
++ * }
++ * }
++ *
++ * public class TickTimes {
++ *
++ * private final List tickTimesNS = new ArrayList<>();
++ *
++ * public void completeTick(long timeNS) {
++ * this.tickTimesNS.add(timeNS);
++ * }
++ *
++ * public double getAverageTickLengthMS() {
++ * double sum = 0.0;
++ * for (long time : tickTimesNS) {
++ * sum += (double)time;
++ * }
++ * return (sum / this.tickTimesNS.size()) / 1.0E6; // 1ms = 1 million ns
++ * }
++ * }
++ *
++ * public class Server {
++ * public final List worlds = ...;
++ *
++ * // callback is left out of this example
++ * // note: world == null here, because this RegionizedData object
++ * // is not instantiated per world, but rather globally.
++ * public final RegionizedData tickTimes =
++ * new RegionizedData<>(null, () -> new TickTimes(), ...);
++ * }
++ * }
++ *
++ * In general, it is advised that if a RegionizedData object is instantiated per world, that world checking
++ * is enabled for it by passing the world to the constructor.
++ *
++ */
++public final class RegionizedData {
++
++ private final ServerLevel world;
++ private final Supplier initialValueSupplier;
++ private final RegioniserCallback callback;
++
++ /**
++ * Creates a regionised data holder. The provided initial value supplier may not be null, and it must
++ * never produce {@code null} values.
++ *
++ * Note that the supplier or regioniser callback may be used while the region lock is held, so any blocking
++ * operations may deadlock the entire server and as such the function should be completely non-blocking
++ * and must complete in a timely manner.
++ *
++ *
++ * If the provided world is {@code null}, then the world checks are disabled. The world should only ever
++ * be {@code null} if the data is specifically not specific to worlds. For example, using {@code null}
++ * for an entity tick list is invalid since the entities are tied to a world and region,
++ * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to
++ * region only.
++ *
++ * @param world The world in which the region data resides.
++ * @param supplier Initial value supplier used to lazy initialise region data.
++ * @param callback Region callback to manage this regionised data.
++ */
++ public RegionizedData(final ServerLevel world, final Supplier supplier, final RegioniserCallback callback) {
++ this.world = world;
++ this.initialValueSupplier = Validate.notNull(supplier, "Supplier may not be null.");
++ this.callback = Validate.notNull(callback, "Regioniser callback may not be null.");
++ }
++
++ T createNewValue() {
++ return Validate.notNull(this.initialValueSupplier.get(), "Initial value supplier may not return null");
++ }
++
++ RegioniserCallback getCallback() {
++ return this.callback;
++ }
++
++ /**
++ * Returns the current data type for the current ticking region. If there is no region, returns {@code null}.
++ * @return the current data type for the current ticking region. If there is no region, returns {@code null}.
++ * @throws IllegalStateException If the following are true: The server is in region ticking mode,
++ * this {@code RegionizedData}'s world is not {@code null},
++ * and the current ticking region's world does not match this {@code RegionizedData}'s world.
++ */
++ public @Nullable T get() {
++ final ThreadedRegionizer.ThreadedRegion region =
++ TickRegionScheduler.getCurrentRegion();
++
++ if (region == null) {
++ return null;
++ }
++
++ if (this.world != null && this.world != region.getData().world) {
++ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey());
++ }
++
++ return region.getData().getOrCreateRegionizedData(this);
++ }
++
++ /**
++ * Class responsible for handling merge / split requests from the regioniser.
++ *
++ * It is critical to note that each function is called while holding the region lock.
++ *
++ */
++ public static interface RegioniserCallback {
++
++ /**
++ * Completely merges the data in {@code from} to {@code into}.
++ *
++ * Calculating Tick Offsets:
++ * Sometimes data stores absolute tick deadlines, and since regions tick independently, absolute deadlines
++ * are not comparable across regions. Consider absolute deadlines {@code deadlineFrom, deadlineTo} in
++ * regions {@code from} and {@code into} respectively. We can calculate the relative deadline for the from
++ * region with {@code relFrom = deadlineFrom - currentTickFrom}. Then, we can use the same equation for
++ * computing the absolute deadline in region {@code into} that has the same relative deadline as {@code from}
++ * as {@code deadlineTo = relFrom + currentTickTo}. By substituting {@code relFrom} as {@code deadlineFrom - currentTickFrom},
++ * we finally have that {@code deadlineTo = deadlineFrom + (currentTickTo - currentTickFrom)} and
++ * that we can use an offset {@code fromTickOffset = currentTickTo - currentTickFrom} to calculate
++ * {@code deadlineTo} as {@code deadlineTo = deadlineFrom + fromTickOffset}.
++ *
++ *
++ * Critical Notes:
++ *
++ *
++ * This function is called while the region lock is held, so any blocking operations may
++ * deadlock the entire server and as such the function should be completely non-blocking and must complete
++ * in a timely manner.
++ *
++ *
++ * This function may not throw any exceptions, or the server will be left in an unrecoverable state.
++ *
++ *
++ *
++ *
++ * @param from The data to merge from.
++ * @param into The data to merge into.
++ * @param fromTickOffset The addend to absolute tick deadlines stored in the {@code from} region to adjust to the into region.
++ */
++ public void merge(final T from, final T into, final long fromTickOffset);
++
++ /**
++ * Splits the data in {@code from} into {@code dataSet}.
++ *
++ * The chunk coordinate to region section coordinate bit shift amount is provided in {@code chunkToRegionShift}.
++ * To convert from chunk coordinates to region coordinates and keys, see the code below:
++ *
++ * {@code
++ * int chunkX = ...;
++ * int chunkZ = ...;
++ *
++ * int regionSectionX = chunkX >> chunkToRegionShift;
++ * int regionSectionZ = chunkZ >> chunkToRegionShift;
++ * long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
++ * }
++ *
++ *
++ *
++ * The {@code regionToData} hashtable provides a lookup from {@code regionSectionKey} (see above) to the
++ * data that is owned by the region which occupies the region section.
++ *
++ *
++ * Unlike {@link #merge(Object, Object, long)}, there is no absolute tick offset provided. This is because
++ * the new regions formed from the split will start at the same tick number, and so no adjustment is required.
++ *
++ *
++ * @param from The data to split from.
++ * @param chunkToRegionShift The signed right-shift value used to convert chunk coordinates into region section coordinates.
++ * @param regionToData Lookup hash table from region section key to .
++ * @param dataSet The data set to split into.
++ */
++ public void split(
++ final T from, final int chunkToRegionShift,
++ final Long2ReferenceOpenHashMap regionToData, final ReferenceOpenHashSet dataSet
++ );
++ }
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..1382c695c4991488b113401e231875ddc74f6b01
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/RegionizedServer.java
+@@ -0,0 +1,455 @@
++package io.papermc.paper.threadedregions;
++
++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
++import ca.spottedleaf.moonrise.common.util.TickThread;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler;
++import net.minecraft.CrashReport;
++import net.minecraft.ReportedException;
++import net.minecraft.network.Connection;
++import net.minecraft.network.PacketListener;
++import net.minecraft.network.PacketSendListener;
++import net.minecraft.network.chat.Component;
++import net.minecraft.network.chat.MutableComponent;
++import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.dedicated.DedicatedServer;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.network.ServerGamePacketListenerImpl;
++import net.minecraft.world.level.GameRules;
++import org.bukkit.Bukkit;
++import org.slf4j.Logger;
++import java.util.ArrayList;
++import java.util.Collections;
++import java.util.List;
++import java.util.concurrent.CopyOnWriteArrayList;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.function.BooleanSupplier;
++
++public final class RegionizedServer {
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++ private static final RegionizedServer INSTANCE = new RegionizedServer();
++
++ public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue();
++
++ private final CopyOnWriteArrayList worlds = new CopyOnWriteArrayList<>();
++ private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>();
++
++ private final MultiThreadedQueue globalTickQueue = new MultiThreadedQueue<>();
++
++ private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this);
++
++ public static RegionizedServer getInstance() {
++ return INSTANCE;
++ }
++
++ public void addConnection(final Connection conn) {
++ this.connections.add(conn);
++ }
++
++ public boolean removeConnection(final Connection conn) {
++ return this.connections.remove(conn);
++ }
++
++ public void addWorld(final ServerLevel world) {
++ this.worlds.add(world);
++ }
++
++ public void init() {
++ // call init event _before_ scheduling anything
++ new RegionizedServerInitEvent().callEvent();
++
++ // now we can schedule
++ this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS);
++ TickRegions.getScheduler().scheduleRegion(this.tickHandle);
++ TickRegions.getScheduler().init();
++ }
++
++ public void invalidateStatus() {
++ this.lastServerStatus = 0L;
++ }
++
++ public void addTaskWithoutNotify(final Runnable run) {
++ this.globalTickQueue.add(run);
++ }
++
++ public void addTask(final Runnable run) {
++ this.addTaskWithoutNotify(run);
++ TickRegions.getScheduler().setHasTasks(this.tickHandle);
++ }
++
++ /**
++ * Returns the current tick of the region ticking.
++ * @throws IllegalStateException If there is no current region.
++ */
++ public static long getCurrentTick() throws IllegalStateException {
++ final ThreadedRegionizer.ThreadedRegion region =
++ TickRegionScheduler.getCurrentRegion();
++ if (region == null) {
++ if (TickThread.isShutdownThread()) {
++ return 0L;
++ }
++ throw new IllegalStateException("No currently ticking region");
++ }
++ return region.getData().getCurrentTick();
++ }
++
++ public static boolean isGlobalTickThread() {
++ return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask();
++ }
++
++ public static void ensureGlobalTickThread(final String reason) {
++ if (!isGlobalTickThread()) {
++ throw new IllegalStateException(reason);
++ }
++ }
++
++ public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() {
++ return INSTANCE.tickHandle;
++ }
++
++ private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle {
++
++ private final RegionizedServer server;
++
++ private final AtomicBoolean scheduled = new AtomicBoolean();
++ private final AtomicBoolean ticking = new AtomicBoolean();
++
++ public GlobalTickTickHandle(final RegionizedServer server) {
++ super(null, SchedulerThreadPool.DEADLINE_NOT_SET);
++ this.server = server;
++ }
++
++ /**
++ * Only valid to call BEFORE scheduled!!!!
++ */
++ final void setInitialStart(final long start) {
++ if (this.scheduled.getAndSet(true)) {
++ throw new IllegalStateException("Double scheduling global tick");
++ }
++ this.updateScheduledStart(start);
++ }
++
++ @Override
++ protected boolean tryMarkTicking() {
++ return !this.ticking.getAndSet(true);
++ }
++
++ @Override
++ protected boolean markNotTicking() {
++ return this.ticking.getAndSet(false);
++ }
++
++ @Override
++ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) {
++ this.drainTasks();
++ this.server.globalTick(tickCount);
++ }
++
++ private void drainTasks() {
++ while (this.runOneTask());
++ }
++
++ private boolean runOneTask() {
++ final Runnable run = this.server.globalTickQueue.poll();
++ if (run == null) {
++ return false;
++ }
++
++ // TODO try catch?
++ run.run();
++
++ return true;
++ }
++
++ @Override
++ protected boolean runRegionTasks(final BooleanSupplier canContinue) {
++ do {
++ if (!this.runOneTask()) {
++ return false;
++ }
++ } while (canContinue.getAsBoolean());
++
++ return true;
++ }
++
++ @Override
++ protected boolean hasIntermediateTasks() {
++ return !this.server.globalTickQueue.isEmpty();
++ }
++ }
++
++ private long lastServerStatus;
++ private long tickCount;
++
++ /*
++ private final java.util.Random random = new java.util.Random(4L);
++ private final List> walkers =
++ new java.util.ArrayList<>();
++ static final int PLAYERS = 500;
++ static final int RAD_BLOCKS = 1000;
++ static final int RAD = RAD_BLOCKS >> 4;
++ static final int RAD_BIG_BLOCKS = 100_000;
++ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4;
++ static final int VD = 4 + 12;
++ static final int BIG_PLAYERS = 250;
++ static final double WALK_CHANCE = 0.3;
++ static final double TP_CHANCE = 0.2;
++ static final double TASK_CHANCE = 0.2;
++
++ private ServerLevel getWorld() {
++ return this.worlds.get(0);
++ }
++
++ private void init2() {
++ for (int i = 0; i < PLAYERS; ++i) {
++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
++ int posX = this.random.nextInt(-rad, rad + 1);
++ int posZ = this.random.nextInt(-rad, rad + 1);
++
++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) {
++ @Override
++ protected void addCallback(Void parameter, int chunkX, int chunkZ) {
++ ServerLevel world = RegionizedServer.this.getWorld();
++ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) {
++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {
++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {});
++ });
++ }
++ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel(
++ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ)
++ );
++ }
++
++ @Override
++ protected void removeCallback(Void parameter, int chunkX, int chunkZ) {
++ ServerLevel world = RegionizedServer.this.getWorld();
++ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) {
++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {
++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {});
++ });
++ }
++ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel(
++ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ)
++ );
++ }
++ };
++
++ map.add(posX, posZ, VD);
++
++ walkers.add(map);
++ }
++ }
++
++ private void randomWalk() {
++ if (this.walkers.isEmpty()) {
++ this.init2();
++ return;
++ }
++
++ for (int i = 0; i < PLAYERS; ++i) {
++ if (this.random.nextDouble() > WALK_CHANCE) {
++ continue;
++ }
++
++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = this.walkers.get(i);
++
++ int updateX = this.random.nextInt(-1, 2);
++ int updateZ = this.random.nextInt(-1, 2);
++
++ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD);
++ }
++
++ for (int i = 0; i < PLAYERS; ++i) {
++ if (random.nextDouble() >= TP_CHANCE) {
++ continue;
++ }
++
++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD;
++ int posX = random.nextInt(-rad, rad + 1);
++ int posZ = random.nextInt(-rad, rad + 1);
++
++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i);
++
++ map.update(posX, posZ, VD);
++ }
++ }
++ */
++
++ private void globalTick(final int tickCount) {
++ /*
++ if (false) {
++ io.papermc.paper.threadedregions.ThreadedTicketLevelPropagator.main(null);
++ }
++ this.randomWalk();
++ */
++ ++this.tickCount;
++ // expire invalid click command callbacks
++ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount);
++
++ // scheduler
++ ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick();
++
++ // commands
++ ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs();
++
++ // needs
++ // player ping sample
++ // world global tick
++ // connection tick
++
++ // tick player ping sample
++ this.tickPlayerSample();
++
++ // tick worlds
++ for (final ServerLevel world : this.worlds) {
++ this.globalTick(world, tickCount);
++ }
++
++ // tick connections
++ this.tickConnections();
++
++ // player list
++ MinecraftServer.getServer().getPlayerList().tick();
++ }
++
++ private void tickPlayerSample() {
++ final MinecraftServer mcServer = MinecraftServer.getServer();
++
++ final long currtime = System.nanoTime();
++
++ // player ping sample
++ // copied from MinecraftServer#tickServer
++ // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing
++ // an uncomplete status
++ if (currtime - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) {
++ this.lastServerStatus = currtime;
++ mcServer.rebuildServerStatus();
++ }
++ }
++
++ public static boolean isNotOwnedByGlobalRegion(final Connection conn) {
++ final PacketListener packetListener = conn.getPacketListener();
++
++ if (packetListener instanceof ServerGamePacketListenerImpl gamePacketListener) {
++ return !gamePacketListener.waitingForSwitchToConfig;
++ }
++
++ if (conn.getPacketListener() instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) {
++ return configurationPacketListener.switchToMain;
++ }
++
++ return false;
++ }
++
++ private void tickConnections() {
++ final List connections = new ArrayList<>(this.connections);
++ Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging
++ for (final Connection conn : connections) {
++ if (!conn.becomeActive()) {
++ continue;
++ }
++
++ if (isNotOwnedByGlobalRegion(conn)) {
++ // we actually require that the owning regions remove the connection for us, as it is possible
++ // that ownership is transferred back to us
++ continue;
++ }
++
++ if (!conn.isConnected()) {
++ this.removeConnection(conn);
++ conn.handleDisconnection();
++ continue;
++ }
++
++ try {
++ conn.tick();
++ } catch (final Exception exception) {
++ if (conn.isMemoryConnection()) {
++ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection"));
++ }
++
++ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception);
++ MutableComponent ichatmutablecomponent = Component.literal("Internal server error");
++
++ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> {
++ conn.disconnect(ichatmutablecomponent);
++ }));
++ conn.setReadOnly();
++ continue;
++ }
++ }
++ }
++
++ // A global tick only updates things like weather / worldborder, basically anything in the world that is
++ // NOT tied to a specific region, but rather shared amongst all of them.
++ private void globalTick(final ServerLevel world, final int tickCount) {
++ // needs
++ // worldborder tick
++ // advancing the weather cycle
++ // sleep status thing
++ // updating sky brightness
++ // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking
++
++ // Typically, we expect there to be a running region to drain a world's global chunk tasks. However,
++ // this may not be the case - and thus, only the global tick thread can do anything.
++ world.taskQueueRegionData.drainGlobalChunkTasks();
++
++ // worldborder tick
++ this.tickWorldBorder(world);
++
++ // weather cycle
++ this.advanceWeatherCycle(world);
++
++ // sleep status
++ this.checkNightSkip(world);
++
++ // update raids
++ this.updateRaids(world);
++
++ // sky brightness
++ this.updateSkyBrightness(world);
++
++ // time ticking (TODO API synchronisation?)
++ this.tickTime(world, tickCount);
++
++ world.updateTickData();
++
++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates
++ }
++
++ private void updateRaids(final ServerLevel world) {
++ world.getRaids().globalTick();
++ }
++
++ private void checkNightSkip(final ServerLevel world) {
++ world.tickSleep();
++ }
++
++ private void advanceWeatherCycle(final ServerLevel world) {
++ world.advanceWeatherCycle();
++ }
++
++ private void updateSkyBrightness(final ServerLevel world) {
++ world.updateSkyBrightness();
++ }
++
++ private void tickWorldBorder(final ServerLevel world) {
++ world.getWorldBorder().tick();
++ }
++
++ private void tickTime(final ServerLevel world, final int tickCount) {
++ if (world.tickTime) {
++ if (world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) {
++ world.setDayTime(world.levelData.getDayTime() + (long)tickCount);
++ }
++ world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount);
++ }
++ }
++
++ public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) {
++
++ }
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..745ab870310733b569681f5280895bb9798620a4
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java
+@@ -0,0 +1,807 @@
++package io.papermc.paper.threadedregions;
++
++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor;
++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.concurrentutil.util.Priority;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.TicketType;
++import net.minecraft.util.Unit;
++import java.lang.invoke.VarHandle;
++import java.util.ArrayDeque;
++import java.util.Iterator;
++import java.util.concurrent.atomic.AtomicLong;
++
++public final class RegionizedTaskQueue {
++
++ private static final TicketType TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0);
++
++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Runnable run) {
++ return this.createChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL);
++ }
++
++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Runnable run, final Priority priority) {
++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority);
++ }
++
++ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Runnable run) {
++ return this.createTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL);
++ }
++
++ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Runnable run, final Priority priority) {
++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority);
++ }
++
++ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Runnable run) {
++ return this.queueChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL);
++ }
++
++ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Runnable run, final Priority priority) {
++ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority);
++
++ ret.queue();
++
++ return ret;
++ }
++
++ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Runnable run) {
++ return this.queueTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL);
++ }
++
++ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ,
++ final Runnable run, final Priority priority) {
++ final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority);
++
++ ret.queue();
++
++ return ret;
++ }
++
++ public static final class WorldRegionTaskData {
++ private final ServerLevel world;
++ private final MultiThreadedQueue globalChunkTask = new MultiThreadedQueue<>();
++ private final ConcurrentLong2ReferenceChainedHashTable referenceCounters = new ConcurrentLong2ReferenceChainedHashTable<>();
++
++ public WorldRegionTaskData(final ServerLevel world) {
++ this.world = world;
++ }
++
++ private boolean executeGlobalChunkTask() {
++ final Runnable run = this.globalChunkTask.poll();
++ if (run != null) {
++ run.run();
++ return true;
++ }
++ return false;
++ }
++
++ public void drainGlobalChunkTasks() {
++ while (this.executeGlobalChunkTask());
++ }
++
++ public void pushGlobalChunkTask(final Runnable run) {
++ this.globalChunkTask.add(run);
++ }
++
++ private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) {
++ final ThreadedRegionizer regioniser = this.world.regioniser;
++ final ThreadedRegionizer.ThreadedRegion region
++ = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ);
++ if (region == null) {
++ return null;
++ }
++ final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData();
++ return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue);
++ }
++
++ private void removeTicket(final long coord) {
++ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel(
++ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
++ );
++ }
++
++ private void addTicket(final long coord) {
++ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel(
++ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE
++ );
++ }
++
++ private void processTicketUpdates(final long coord) {
++ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(CoordinateUtils.getChunkX(coord), CoordinateUtils.getChunkZ(coord));
++ }
++
++ // note: only call on acquired referenceCountData
++ private void ensureTicketAdded(final long coord, final ReferenceCountData referenceCountData) {
++ if (!referenceCountData.addedTicket) {
++ // fine if multiple threads do this, no removeTicket may be called for this coord due to reference count inc
++ this.addTicket(coord);
++ this.processTicketUpdates(coord);
++ referenceCountData.addedTicket = true;
++ }
++ }
++
++ private void decrementReference(final ReferenceCountData referenceCountData, final long coord) {
++ if (!referenceCountData.decreaseReferenceCount()) {
++ return;
++ } // else: need to remove ticket
++
++ // note: it is possible that another thread increments and then removes the reference before we can, so
++ // use ifPresent
++ this.referenceCounters.computeIfPresent(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> {
++ if (valueInMap.referenceCount.get() != 0L) {
++ return valueInMap;
++ }
++
++ // note: valueInMap may not be referenceCountData
++
++ // possible to invoke this outside of the compute call, but not required and requires additional logic
++ WorldRegionTaskData.this.removeTicket(keyInMap);
++
++ return null;
++ });
++ }
++
++ private ReferenceCountData incrementReference(final long coord) {
++ ReferenceCountData referenceCountData = this.referenceCounters.get(coord);
++
++ if (referenceCountData != null && referenceCountData.addCount()) {
++ this.ensureTicketAdded(coord, referenceCountData);
++ return referenceCountData;
++ }
++
++ referenceCountData = this.referenceCounters.compute(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> {
++ if (valueInMap == null) {
++ // sets reference count to 1
++ return new ReferenceCountData();
++ }
++ // OK if we add from 0, the remove call will use compute() and catch this race condition
++ valueInMap.referenceCount.getAndIncrement();
++
++ return valueInMap;
++ });
++
++ this.ensureTicketAdded(coord, referenceCountData);
++
++ return referenceCountData;
++ }
++ }
++
++ private static final class ReferenceCountData {
++
++ public final AtomicLong referenceCount = new AtomicLong(1L);
++ public volatile boolean addedTicket;
++
++ // returns false if reference count is 0, otherwise increments ref count
++ public boolean addCount() {
++ int failures = 0;
++ for (long curr = this.referenceCount.get();;) {
++ for (int i = 0; i < failures; ++i) {
++ Thread.onSpinWait();
++ }
++
++ if (curr == 0L) {
++ return false;
++ }
++
++ if (curr == (curr = this.referenceCount.compareAndExchange(curr, curr + 1L))) {
++ return true;
++ }
++
++ ++failures;
++ }
++ }
++
++ // returns true if new reference count is 0
++ public boolean decreaseReferenceCount() {
++ final long res = this.referenceCount.decrementAndGet();
++ if (res >= 0L) {
++ return res == 0L;
++ } else {
++ throw new IllegalStateException("Negative reference count");
++ }
++ }
++ }
++
++ public static final class RegionTaskQueueData {
++ private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue();
++ private final PrioritisedQueue chunkQueue = new PrioritisedQueue();
++ private final WorldRegionTaskData worldRegionTaskData;
++
++ public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) {
++ this.worldRegionTaskData = worldRegionTaskData;
++ }
++
++ void mergeInto(final RegionTaskQueueData into) {
++ this.tickTaskQueue.mergeInto(into.tickTaskQueue);
++ this.chunkQueue.mergeInto(into.chunkQueue);
++ }
++
++ public boolean executeTickTask() {
++ return this.tickTaskQueue.executeTask();
++ }
++
++ public boolean executeChunkTask() {
++ return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask();
++ }
++
++ void split(final ThreadedRegionizer regioniser,
++ final Long2ReferenceOpenHashMap> into) {
++ this.tickTaskQueue.split(
++ false, regioniser, into
++ );
++ this.chunkQueue.split(
++ true, regioniser, into
++ );
++ }
++
++ public void drainTasks() {
++ final PrioritisedQueue tickTaskQueue = this.tickTaskQueue;
++ final PrioritisedQueue chunkTaskQueue = this.chunkQueue;
++
++ int allowedTickTasks = tickTaskQueue.getScheduledTasks();
++ int allowedChunkTasks = chunkTaskQueue.getScheduledTasks();
++
++ boolean executeTickTasks = allowedTickTasks > 0;
++ boolean executeChunkTasks = allowedChunkTasks > 0;
++ boolean executeGlobalTasks = true;
++
++ do {
++ executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask();
++ executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask();
++ executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask();
++ } while (executeTickTasks | executeChunkTasks | executeGlobalTasks);
++
++ if (allowedChunkTasks > 0) {
++ // if we executed chunk tasks, we should try to process ticket updates for full status changes
++ this.worldRegionTaskData.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates();
++ }
++ }
++
++ public boolean hasTasks() {
++ return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty();
++ }
++ }
++
++ static final class PrioritisedQueue {
++ private final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; {
++ for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) {
++ this.queues[i] = new ArrayDeque<>();
++ }
++ }
++ private boolean isDestroyed;
++
++ public int getScheduledTasks() {
++ synchronized (this) {
++ int ret = 0;
++
++ for (final ArrayDeque queue : this.queues) {
++ ret += queue.size();
++ }
++
++ return ret;
++ }
++ }
++
++ public boolean isEmpty() {
++ final ArrayDeque[] queues = this.queues;
++ final int max = Priority.IDLE.priority;
++ synchronized (this) {
++ for (int i = 0; i <= max; ++i) {
++ if (!queues[i].isEmpty()) {
++ return false;
++ }
++ }
++ return true;
++ }
++ }
++
++ public void mergeInto(final PrioritisedQueue target) {
++ synchronized (this) {
++ this.isDestroyed = true;
++ mergeInto(target, this.queues);
++ }
++ }
++
++ private static void mergeInto(final PrioritisedQueue target, final ArrayDeque[] thisQueues) {
++ synchronized (target) {
++ final ArrayDeque[] otherQueues = target.queues;
++ for (int i = 0; i < thisQueues.length; ++i) {
++ final ArrayDeque fromQ = thisQueues[i];
++ final ArrayDeque intoQ = otherQueues[i];
++
++ // it is possible for another thread to queue tasks into the target queue before we do
++ // since only the ticking region can poll, we don't have to worry about it when they are being queued -
++ // but when we are merging, we need to ensure order is maintained (notwithstanding priority changes)
++ // we can ensure order is maintained by adding all of the tasks from the fromQ into the intoQ at the
++ // front of the queue, but we need to use descending iterator to ensure we do not reverse
++ // the order of elements from fromQ
++ for (final Iterator iterator = fromQ.descendingIterator(); iterator.hasNext();) {
++ intoQ.addFirst(iterator.next());
++ }
++ }
++ }
++ }
++
++ // into is a map of section coordinate to region
++ public void split(final boolean isChunkData,
++ final ThreadedRegionizer regioniser,
++ final Long2ReferenceOpenHashMap> into) {
++ final Reference2ReferenceOpenHashMap, ArrayDeque[]>
++ split = new Reference2ReferenceOpenHashMap<>();
++ final int shift = regioniser.sectionChunkShift;
++ synchronized (this) {
++ this.isDestroyed = true;
++ // like mergeTarget, we need to be careful about insertion order so we can maintain order when splitting
++
++ // first, build the targets
++ final ArrayDeque[] thisQueues = this.queues;
++ for (int i = 0; i < thisQueues.length; ++i) {
++ final ArrayDeque fromQ = thisQueues[i];
++
++ for (final ChunkBasedPriorityTask task : fromQ) {
++ final int sectionX = task.chunkX >> shift;
++ final int sectionZ = task.chunkZ >> shift;
++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ);
++ final ThreadedRegionizer.ThreadedRegion
++ region = into.get(sectionKey);
++ if (region == null) {
++ throw new IllegalStateException();
++ }
++
++ split.computeIfAbsent(region, (keyInMap) -> {
++ final ArrayDeque[] ret = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES];
++
++ for (int k = 0; k < ret.length; ++k) {
++ ret[k] = new ArrayDeque<>();
++ }
++
++ return ret;
++ })[i].add(task);
++ }
++ }
++
++ // merge the targets into their queues
++ for (final Iterator, ArrayDeque[]>>
++ iterator = split.reference2ReferenceEntrySet().fastIterator();
++ iterator.hasNext();) {
++ final Reference2ReferenceMap.Entry, ArrayDeque[]>
++ entry = iterator.next();
++ final RegionTaskQueueData taskQueueData = entry.getKey().getData().getTaskQueueData();
++ mergeInto(isChunkData ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue, entry.getValue());
++ }
++ }
++ }
++
++ /**
++ * returns null if the task cannot be scheduled, returns false if this task queue is dead, and returns true
++ * if the task was added
++ */
++ private Boolean tryPush(final ChunkBasedPriorityTask task) {
++ final ArrayDeque[] queues = this.queues;
++ synchronized (this) {
++ final Priority priority = task.getPriority();
++ if (priority == Priority.COMPLETING) {
++ return null;
++ }
++ if (this.isDestroyed) {
++ return Boolean.FALSE;
++ }
++ queues[priority.priority].addLast(task);
++ return Boolean.TRUE;
++ }
++ }
++
++ private boolean executeTask() {
++ final ArrayDeque[] queues = this.queues;
++ final int max = Priority.IDLE.priority;
++ ChunkBasedPriorityTask task = null;
++ ReferenceCountData referenceCounter = null;
++ synchronized (this) {
++ if (this.isDestroyed) {
++ throw new IllegalStateException("Attempting to poll from dead queue");
++ }
++
++ search_loop:
++ for (int i = 0; i <= max; ++i) {
++ final ArrayDeque queue = queues[i];
++ while ((task = queue.pollFirst()) != null) {
++ if ((referenceCounter = task.trySetCompleting(i)) != null) {
++ break search_loop;
++ }
++ }
++ }
++ }
++
++ if (task == null) {
++ return false;
++ }
++
++ try {
++ task.executeInternal();
++ } finally {
++ task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord);
++ }
++
++ return true;
++ }
++
++ private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask {
++
++ private static final ReferenceCountData REFERENCE_COUNTER_NOT_SET = new ReferenceCountData();
++ static {
++ REFERENCE_COUNTER_NOT_SET.referenceCount.set((long)Integer.MIN_VALUE);
++ }
++
++ private final WorldRegionTaskData world;
++ private final int chunkX;
++ private final int chunkZ;
++ private final long sectionLowerLeftCoord; // chunk coordinate
++ private final boolean isChunkTask;
++
++ private volatile ReferenceCountData referenceCounter;
++ private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", ReferenceCountData.class);
++ private Runnable run;
++ private volatile Priority priority;
++ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", Priority.class);
++
++ ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask,
++ final Runnable run, final Priority priority) {
++ this.world = world;
++ this.chunkX = chunkX;
++ this.chunkZ = chunkZ;
++ this.isChunkTask = isChunkTask;
++ this.run = run;
++ this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET);
++ this.setPriorityPlain(priority);
++
++ final int regionShift = world.world.regioniser.sectionChunkShift;
++ final int regionMask = (1 << regionShift) - 1;
++
++ this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask);
++ }
++
++ private Priority getPriorityVolatile() {
++ return (Priority)PRIORITY_HANDLE.getVolatile(this);
++ }
++
++ private void setPriorityPlain(final Priority priority) {
++ PRIORITY_HANDLE.set(this, priority);
++ }
++
++ private void setPriorityVolatile(final Priority priority) {
++ PRIORITY_HANDLE.setVolatile(this, priority);
++ }
++
++ private Priority compareAndExchangePriority(final Priority expect, final Priority update) {
++ return (Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update);
++ }
++
++ private void setReferenceCounterPlain(final ReferenceCountData value) {
++ REFERENCE_COUNTER_HANDLE.set(this, value);
++ }
++
++ private ReferenceCountData getReferenceCounterVolatile() {
++ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.get(this);
++ }
++
++ private ReferenceCountData compareAndExchangeReferenceCounter(final ReferenceCountData expect, final ReferenceCountData update) {
++ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update);
++ }
++
++ private void executeInternal() {
++ try {
++ this.run.run();
++ } finally {
++ this.run = null;
++ }
++ }
++
++ private void cancelInternal() {
++ this.run = null;
++ }
++
++ private boolean tryComplete(final boolean cancel) {
++ int failures = 0;
++ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) {
++ if (curr == null) {
++ return false;
++ }
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
++ ++failures;
++ continue;
++ }
++
++ // we have the reference count, we win no matter what.
++ this.setPriorityVolatile(Priority.COMPLETING);
++
++ try {
++ if (cancel) {
++ this.cancelInternal();
++ } else {
++ this.executeInternal();
++ }
++ } finally {
++ if (curr != REFERENCE_COUNTER_NOT_SET) {
++ this.world.decrementReference(curr, this.sectionLowerLeftCoord);
++ }
++ }
++
++ return true;
++ }
++ }
++
++ @Override
++ public PrioritisedExecutor getExecutor() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean isQueued() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean queue() {
++ if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) {
++ return false;
++ }
++
++ final ReferenceCountData referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord);
++ if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) {
++ // we don't expect race conditions here, so it is OK if we have to needlessly reference count
++ this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord);
++ return false;
++ }
++
++ boolean synchronise = false;
++ for (;;) {
++ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
++ // the same queue again, as the region lock will be given to us only when the merge/split operation
++ // is done
++ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
++
++ if (queue == null) {
++ if (!synchronise) {
++ // may be incorrectly null when unsynchronised
++ synchronise = true;
++ continue;
++ }
++ // may have been cancelled before we got to the queue
++ if (this.getReferenceCounterVolatile() != null) {
++ throw new IllegalStateException("Expected null ref count when queue does not exist");
++ }
++ // the task never could be polled from the queue, so we return false
++ // don't decrement reference count, as we were certainly cancelled by another thread, which
++ // will decrement the reference count
++ return false;
++ }
++
++ synchronise = true;
++
++ final Boolean res = queue.tryPush(this);
++ if (res == null) {
++ // we were cancelled
++ // don't decrement reference count, as we were certainly cancelled by another thread, which
++ // will decrement the reference count
++ return false;
++ }
++
++ if (!res.booleanValue()) {
++ // failed, try again
++ continue;
++ }
++
++ // successfully queued
++ return true;
++ }
++ }
++
++ private ReferenceCountData trySetCompleting(final int minPriority) {
++ // first, try to set priority to EXECUTING
++ for (Priority curr = this.getPriorityVolatile();;) {
++ if (curr.isLowerPriority(minPriority)) {
++ return null;
++ }
++
++ if (curr == (curr = this.compareAndExchangePriority(curr, Priority.COMPLETING))) {
++ break;
++ } // else: continue
++ }
++
++ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) {
++ if (curr == null) {
++ // something acquired before us
++ return null;
++ }
++
++ if (curr == REFERENCE_COUNTER_NOT_SET) {
++ throw new IllegalStateException();
++ }
++
++ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) {
++ continue;
++ }
++
++ return curr;
++ }
++ }
++
++ private void updatePriorityInQueue() {
++ boolean synchronise = false;
++ for (;;) {
++ final ReferenceCountData referenceCount = this.getReferenceCounterVolatile();
++ if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) {
++ // cancelled or not queued
++ return;
++ }
++
++ if (this.getPriorityVolatile() == Priority.COMPLETING) {
++ // cancelled
++ return;
++ }
++
++ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve
++ // the same queue again, as the region lock will be given to us only when the merge/split operation
++ // is done
++ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask);
++
++ if (queue == null) {
++ if (!synchronise) {
++ // may be incorrectly null when unsynchronised
++ synchronise = true;
++ continue;
++ }
++ // must have been removed
++ return;
++ }
++
++ synchronise = true;
++
++ final Boolean res = queue.tryPush(this);
++ if (res == null) {
++ // we were cancelled
++ return;
++ }
++
++ if (!res.booleanValue()) {
++ // failed, try again
++ continue;
++ }
++
++ // successfully queued
++ return;
++ }
++ }
++
++ @Override
++ public Priority getPriority() {
++ return this.getPriorityVolatile();
++ }
++
++ @Override
++ public boolean lowerPriority(final Priority priority) {
++ int failures = 0;
++ for (Priority curr = this.getPriorityVolatile();;) {
++ if (curr == Priority.COMPLETING) {
++ return false;
++ }
++
++ if (curr.isLowerOrEqualPriority(priority)) {
++ return false;
++ }
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
++ this.updatePriorityInQueue();
++ return true;
++ }
++ ++failures;
++ }
++ }
++
++ @Override
++ public long getSubOrder() {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean setSubOrder(final long subOrder) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean raiseSubOrder(final long subOrder) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean lowerSubOrder(final long subOrder) {
++ throw new UnsupportedOperationException();
++ }
++
++ @Override
++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) {
++ return this.setPriority(priority);
++ }
++
++ @Override
++ public boolean setPriority(final Priority priority) {
++ int failures = 0;
++ for (Priority curr = this.getPriorityVolatile();;) {
++ if (curr == Priority.COMPLETING) {
++ return false;
++ }
++
++ if (curr == priority) {
++ return false;
++ }
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
++ this.updatePriorityInQueue();
++ return true;
++ }
++ ++failures;
++ }
++ }
++
++ @Override
++ public boolean raisePriority(final Priority priority) {
++ int failures = 0;
++ for (Priority curr = this.getPriorityVolatile();;) {
++ if (curr == Priority.COMPLETING) {
++ return false;
++ }
++
++ if (curr.isHigherOrEqualPriority(priority)) {
++ return false;
++ }
++
++ for (int i = 0; i < failures; ++i) {
++ ConcurrentUtil.backoff();
++ }
++
++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) {
++ this.updatePriorityInQueue();
++ return true;
++ }
++ ++failures;
++ }
++ }
++
++ @Override
++ public boolean execute() {
++ return this.tryComplete(false);
++ }
++
++ @Override
++ public boolean cancel() {
++ return this.tryComplete(true);
++ }
++ }
++ }
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..c6e487a4c14e6b82533881d01f32349b9ae28728
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java
+@@ -0,0 +1,770 @@
++package io.papermc.paper.threadedregions;
++
++import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet;
++import ca.spottedleaf.moonrise.common.list.ReferenceList;
++import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import ca.spottedleaf.moonrise.common.util.TickThread;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
++import com.mojang.logging.LogUtils;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
++import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet;
++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
++import net.minecraft.CrashReport;
++import net.minecraft.ReportedException;
++import net.minecraft.core.BlockPos;
++import net.minecraft.network.Connection;
++import net.minecraft.network.PacketSendListener;
++import net.minecraft.network.chat.Component;
++import net.minecraft.network.chat.MutableComponent;
++import net.minecraft.network.protocol.common.ClientboundDisconnectPacket;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ChunkHolder;
++import net.minecraft.server.level.ServerChunkCache;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.server.level.ServerPlayer;
++import net.minecraft.server.network.ServerGamePacketListenerImpl;
++import net.minecraft.util.VisibleForDebug;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.entity.Mob;
++import net.minecraft.world.entity.ai.village.VillageSiege;
++import net.minecraft.world.entity.item.ItemEntity;
++import net.minecraft.world.level.BlockEventData;
++import net.minecraft.world.level.ChunkPos;
++import net.minecraft.world.level.Explosion;
++import net.minecraft.world.level.Level;
++import net.minecraft.world.level.NaturalSpawner;
++import net.minecraft.world.level.ServerExplosion;
++import net.minecraft.world.level.block.Block;
++import net.minecraft.world.level.block.Blocks;
++import net.minecraft.world.level.block.RedStoneWireBlock;
++import net.minecraft.world.level.block.entity.BlockEntity;
++import net.minecraft.world.level.block.entity.TickingBlockEntity;
++import net.minecraft.world.level.chunk.LevelChunk;
++import net.minecraft.world.level.material.Fluid;
++import net.minecraft.world.level.pathfinder.PathTypeCache;
++import net.minecraft.world.level.redstone.CollectingNeighborUpdater;
++import net.minecraft.world.level.redstone.NeighborUpdater;
++import net.minecraft.world.ticks.LevelTicks;
++import org.bukkit.craftbukkit.block.CraftBlockState;
++import org.slf4j.Logger;
++import javax.annotation.Nullable;
++import java.util.ArrayDeque;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Collection;
++import java.util.Collections;
++import java.util.HashMap;
++import java.util.Iterator;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import java.util.function.Consumer;
++import java.util.function.Predicate;
++
++public final class RegionizedWorldData {
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++
++ private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0];
++
++ public static final RegionizedData.RegioniserCallback REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() {
++ @Override
++ public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) {
++ // connections
++ for (final Connection conn : from.connections) {
++ into.connections.add(conn);
++ }
++ // time
++ final long fromRedstoneTimeOffset = into.redstoneTime - from.redstoneTime;
++ // entities
++ for (final ServerPlayer player : from.localPlayers) {
++ into.localPlayers.add(player);
++ into.nearbyPlayers.addPlayer(player);
++ }
++ for (final Entity entity : from.allEntities) {
++ into.allEntities.add(entity);
++ entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
++ }
++ for (final Entity entity : from.loadedEntities) {
++ into.loadedEntities.add(entity);
++ }
++ for (final Iterator iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) {
++ into.entityTickList.add(iterator.next());
++ }
++ for (final Iterator iterator = from.navigatingMobs.unsafeIterator(); iterator.hasNext();) {
++ into.navigatingMobs.add(iterator.next());
++ }
++ for (final Iterator iterator = from.trackerEntities.iterator(); iterator.hasNext();) {
++ into.trackerEntities.add(iterator.next());
++ }
++ for (final Iterator iterator = from.trackerUnloadedEntities.iterator(); iterator.hasNext();) {
++ into.trackerUnloadedEntities.add(iterator.next());
++ }
++ // block ticking
++ into.blockEvents.addAll(from.blockEvents);
++ // ticklists use game time
++ from.blockLevelTicks.merge(into.blockLevelTicks, fromRedstoneTimeOffset);
++ from.fluidLevelTicks.merge(into.fluidLevelTicks, fromRedstoneTimeOffset);
++
++ // tile entity ticking
++ for (final TickingBlockEntity tileEntityWrapped : from.pendingBlockEntityTickers) {
++ into.pendingBlockEntityTickers.add(tileEntityWrapped);
++ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity();
++ if (tileEntity != null) {
++ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
++ }
++ }
++ for (final TickingBlockEntity tileEntityWrapped : from.blockEntityTickers) {
++ into.blockEntityTickers.add(tileEntityWrapped);
++ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity();
++ if (tileEntity != null) {
++ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset);
++ }
++ }
++
++ // ticking chunks
++ for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) {
++ into.entityTickingChunks.add(iterator.next());
++ }
++ for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) {
++ into.tickingChunks.add(iterator.next());
++ }
++ for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) {
++ into.chunks.add(iterator.next());
++ }
++ // redstone torches
++ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) {
++ if (into.redstoneUpdateInfos == null) {
++ into.redstoneUpdateInfos = new ArrayDeque<>();
++ }
++ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) {
++ info.offsetTime(fromRedstoneTimeOffset);
++ into.redstoneUpdateInfos.add(info);
++ }
++ }
++ // mob spawning
++ into.catSpawnerNextTick = Math.max(from.catSpawnerNextTick, into.catSpawnerNextTick);
++ into.patrolSpawnerNextTick = Math.max(from.patrolSpawnerNextTick, into.patrolSpawnerNextTick);
++ into.phantomSpawnerNextTick = Math.max(from.phantomSpawnerNextTick, into.phantomSpawnerNextTick);
++ if (from.wanderingTraderTickDelay != Integer.MIN_VALUE && into.wanderingTraderTickDelay != Integer.MIN_VALUE) {
++ into.wanderingTraderTickDelay = Math.max(from.wanderingTraderTickDelay, into.wanderingTraderTickDelay);
++ into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay);
++ into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance);
++ }
++ // chunkHoldersToBroadcast
++ for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) {
++ into.chunkHoldersToBroadcast.add(chunkHolder);
++ }
++ }
++
++ @Override
++ public void split(final RegionizedWorldData from, final int chunkToRegionShift,
++ final Long2ReferenceOpenHashMap regionToData,
++ final ReferenceOpenHashSet dataSet) {
++ // connections
++ for (final Connection conn : from.connections) {
++ final ServerPlayer player = conn.getPlayer();
++ final ChunkPos pos = player.chunkPosition();
++ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
++ // the chunk holder must _exist_, and so the region section exists.
++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
++ .connections.add(conn);
++ }
++ // entities
++ for (final ServerPlayer player : from.localPlayers) {
++ final ChunkPos pos = player.chunkPosition();
++ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
++ // the chunk holder must _exist_, and so the region section exists.
++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
++ into.localPlayers.add(player);
++ into.nearbyPlayers.addPlayer(player);
++ }
++ for (final Entity entity : from.allEntities) {
++ final ChunkPos pos = entity.chunkPosition();
++ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means
++ // the chunk holder must _exist_, and so the region section exists.
++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
++ into.allEntities.add(entity);
++ // Note: entityTickList is a subset of allEntities
++ if (from.entityTickList.contains(entity)) {
++ into.entityTickList.add(entity);
++ }
++ // Note: loadedEntities is a subset of allEntities
++ if (from.loadedEntities.contains(entity)) {
++ into.loadedEntities.add(entity);
++ }
++ // Note: navigatingMobs is a subset of allEntities
++ if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) {
++ into.navigatingMobs.add(mob);
++ }
++ if (from.trackerEntities.contains(entity)) {
++ into.trackerEntities.add(entity);
++ }
++ if (from.trackerUnloadedEntities.contains(entity)) {
++ into.trackerUnloadedEntities.add(entity);
++ }
++ }
++ // block ticking
++ for (final BlockEventData blockEventData : from.blockEvents) {
++ final BlockPos pos = blockEventData.pos();
++ final int chunkX = pos.getX() >> 4;
++ final int chunkZ = pos.getZ() >> 4;
++
++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
++ // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events
++ // is just some list. So if it unloads, I guess it's just lost.
++ if (into != null) {
++ into.blockEvents.add(blockEventData);
++ }
++ }
++
++ final Long2ReferenceOpenHashMap> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
++ final Long2ReferenceOpenHashMap> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f);
++
++ for (final Iterator> iterator = regionToData.long2ReferenceEntrySet().fastIterator();
++ iterator.hasNext();) {
++ final Long2ReferenceMap.Entry entry = iterator.next();
++ final long key = entry.getLongKey();
++ final RegionizedWorldData worldData = entry.getValue();
++
++ levelTicksBlockRegionData.put(key, worldData.blockLevelTicks);
++ levelTicksFluidRegionData.put(key, worldData.fluidLevelTicks);
++ }
++
++ from.blockLevelTicks.split(chunkToRegionShift, levelTicksBlockRegionData);
++ from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData);
++
++ // tile entity ticking
++ for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) {
++ final BlockPos pos = tileEntity.getPos();
++ final int chunkX = pos.getX() >> 4;
++ final int chunkZ = pos.getZ() >> 4;
++
++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
++ if (into != null) {
++ into.pendingBlockEntityTickers.add(tileEntity);
++ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets
++ // marked as removed. So if there is no section, it's probably removed!
++ }
++ for (final TickingBlockEntity tileEntity : from.blockEntityTickers) {
++ final BlockPos pos = tileEntity.getPos();
++ final int chunkX = pos.getX() >> 4;
++ final int chunkZ = pos.getZ() >> 4;
++
++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift));
++ if (into != null) {
++ into.blockEntityTickers.add(tileEntity);
++ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets
++ // marked as removed. So if there is no section, it's probably removed!
++ }
++ // time
++ for (final RegionizedWorldData regionizedWorldData : dataSet) {
++ regionizedWorldData.redstoneTime = from.redstoneTime;
++ }
++ // ticking chunks
++ for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) {
++ final ServerChunkCache.ChunkAndHolder holder = iterator.next();
++ final ChunkPos pos = holder.chunk().getPos();
++
++ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
++ .entityTickingChunks.add(holder);
++ }
++ for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) {
++ final ServerChunkCache.ChunkAndHolder holder = iterator.next();
++ final ChunkPos pos = holder.chunk().getPos();
++
++ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
++ .tickingChunks.add(holder);
++ }
++ for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) {
++ final ServerChunkCache.ChunkAndHolder holder = iterator.next();
++ final ChunkPos pos = holder.chunk().getPos();
++
++ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded
++ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift))
++ .chunks.add(holder);
++ }
++
++ // redstone torches
++ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) {
++ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) {
++ final BlockPos pos = info.pos;
++
++ final RegionizedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift));
++ if (worldData != null) {
++ if (worldData.redstoneUpdateInfos == null) {
++ worldData.redstoneUpdateInfos = new ArrayDeque<>();
++ }
++ worldData.redstoneUpdateInfos.add(info);
++ } // else: chunk unloaded
++ }
++ }
++ // mob spawning
++ for (final RegionizedWorldData regionizedWorldData : dataSet) {
++ regionizedWorldData.catSpawnerNextTick = from.catSpawnerNextTick;
++ regionizedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick;
++ regionizedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick;
++ regionizedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay;
++ regionizedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance;
++ regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay;
++ regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid
++ }
++ // chunkHoldersToBroadcast
++ for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) {
++ final ChunkPos pos = chunkHolder.getPos();
++
++ // Possible for get() to return null, as the chunk holder is not removed during unload
++ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift));
++ if (into != null) {
++ into.chunkHoldersToBroadcast.add(chunkHolder);
++ }
++ }
++ }
++ };
++
++ public final ServerLevel world;
++
++ private RegionizedServer.WorldLevelData tickData;
++
++ // connections
++ public final List connections = new ArrayList<>();
++
++ // misc. fields
++ private boolean isHandlingTick;
++
++ public void setHandlingTick(final boolean to) {
++ this.isHandlingTick = to;
++ }
++
++ public boolean isHandlingTick() {
++ return this.isHandlingTick;
++ }
++
++ // entities
++ private final List localPlayers = new ArrayList<>();
++ private final NearbyPlayers nearbyPlayers;
++ private final ReferenceList allEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY);
++ private final ReferenceList loadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY);
++ private final IteratorSafeOrderedReferenceSet entityTickList = new IteratorSafeOrderedReferenceSet<>();
++ private final IteratorSafeOrderedReferenceSet navigatingMobs = new IteratorSafeOrderedReferenceSet<>();
++ public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
++ public final ReferenceList trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker
++
++ // block ticking
++ private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>();
++ private final LevelTicks blockLevelTicks;
++ private final LevelTicks fluidLevelTicks;
++
++ // tile entity ticking
++ private final List pendingBlockEntityTickers = new ArrayList<>();
++ private final List blockEntityTickers = new ArrayList<>();
++ private boolean tickingBlockEntities;
++
++ // time
++ private long redstoneTime = 1L;
++
++ public long getRedstoneGameTime() {
++ return this.redstoneTime;
++ }
++
++ public void setRedstoneGameTime(final long to) {
++ this.redstoneTime = to;
++ }
++
++ // ticking chunks
++ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDER_ARRAY = new ServerChunkCache.ChunkAndHolder[0];
++ private final ReferenceList entityTickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY);
++ private final ReferenceList tickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY);
++ private final ReferenceList chunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY);
++
++ // Paper/CB api hook misc
++ // don't bother to merge/split these, no point
++ // From ServerLevel
++ public boolean hasPhysicsEvent = true; // Paper
++ public boolean hasEntityMoveEvent = false; // Paper
++ // Paper start - Optimize Hoppers
++ public boolean skipPullModeEventFire = false;
++ public boolean skipPushModeEventFire = false;
++ public boolean skipHopperEvents = false;
++ // Paper end - Optimize Hoppers
++ public long lastMidTickExecute;
++ public long lastMidTickExecuteFailure;
++ // From Level
++ public boolean populating;
++ public final NeighborUpdater neighborUpdater;
++ public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710
++ public boolean captureBlockStates = false;
++ public boolean captureTreeGeneration = false;
++ public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent
++ public final Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper
++ public final Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper
++ public List captureDrops;
++ // Paper start
++ public int wakeupInactiveRemainingAnimals;
++ public int wakeupInactiveRemainingFlying;
++ public int wakeupInactiveRemainingMonsters;
++ public int wakeupInactiveRemainingVillagers;
++ // Paper end
++ public int currentPrimedTnt = 0; // Spigot
++ @Nullable
++ @VisibleForDebug
++ public NaturalSpawner.SpawnState lastSpawnState;
++ public boolean shouldSignal = true;
++ public final Map explosionDensityCache = new HashMap<>(64, 0.25f);
++ public final PathTypeCache pathTypesByPosCache = new PathTypeCache();
++ public final List temporaryChunkTickList = new java.util.ArrayList<>();
++ public final Set chunkHoldersToBroadcast = new ReferenceLinkedOpenHashSet<>();
++
++ // not transient
++ public java.util.ArrayDeque redstoneUpdateInfos;
++
++ // Mob spawning
++ public final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>();
++ public int catSpawnerNextTick = 0;
++ public int patrolSpawnerNextTick = 0;
++ public int phantomSpawnerNextTick = 0;
++ public int wanderingTraderTickDelay = Integer.MIN_VALUE;
++ public int wanderingTraderSpawnDelay;
++ public int wanderingTraderSpawnChance;
++ public VillageSiegeState villageSiegeState = new VillageSiegeState();
++
++ public static final class VillageSiegeState {
++ public boolean hasSetupSiege;
++ public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE;
++ public int zombiesToSpawn;
++ public int nextSpawnTime;
++ public int spawnX;
++ public int spawnY;
++ public int spawnZ;
++ }
++ // Redstone
++ public final alternate.current.wire.WireHandler wireHandler;
++ public final io.papermc.paper.redstone.RedstoneWireTurbo turbo;
++
++ public RegionizedWorldData(final ServerLevel world) {
++ this.world = world;
++ this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, true);
++ this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, false);
++ this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax);
++ this.nearbyPlayers = new NearbyPlayers(world);
++ this.wireHandler = new alternate.current.wire.WireHandler(world);
++ this.turbo = new io.papermc.paper.redstone.RedstoneWireTurbo((RedStoneWireBlock)Blocks.REDSTONE_WIRE);
++
++ // tasks may be drained before the region ticks, so we must set up the tick data early just in case
++ this.updateTickData();
++ }
++
++ public void checkWorld(final Level against) {
++ if (this.world != against) {
++ throw new IllegalStateException("World mismatch: expected " + this.world.getWorld().getName() + " but got " + (against == null ? "null" : against.getWorld().getName()));
++ }
++ }
++
++ public RegionizedServer.WorldLevelData getTickData() {
++ return this.tickData;
++ }
++
++ private long lagCompensationTick;
++
++ public long getLagCompensationTick() {
++ return this.lagCompensationTick;
++ }
++
++ public void updateTickData() {
++ this.tickData = this.world.tickData;
++ this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
++ this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
++ this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers
++ // always subtract from server init so that the tick starts at zero, allowing us to cast to int without much worry
++ this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / TickRegionScheduler.TIME_BETWEEN_TICKS;
++ }
++
++ public NearbyPlayers getNearbyPlayers() {
++ return this.nearbyPlayers;
++ }
++
++ private static void cleanUpConnection(final Connection conn) {
++ // note: ALL connections HERE have a player
++ final ServerPlayer player = conn.getPlayer();
++ // now that the connection is removed, we can allow this region to die
++ player.serverLevel().chunkSource.removeTicketAtLevel(
++ ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos,
++ ChunkHolderManager.MAX_TICKET_LEVEL,
++ player.connection.disconnectTicketId
++ );
++ }
++
++ // connections
++ public void tickConnections() {
++ final List connections = new ArrayList<>(this.connections);
++ Collections.shuffle(connections);
++ for (final Connection conn : connections) {
++ if (!conn.isConnected()) {
++ conn.handleDisconnection();
++ // global tick thread will not remove connections not owned by it, so we need to
++ RegionizedServer.getInstance().removeConnection(conn);
++ this.connections.remove(conn);
++ cleanUpConnection(conn);
++ continue;
++ }
++ if (!this.connections.contains(conn)) {
++ // removed by connection tick?
++ continue;
++ }
++
++ try {
++ conn.tick();
++ } catch (final Exception exception) {
++ if (conn.isMemoryConnection()) {
++ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection"));
++ }
++
++ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception);
++ MutableComponent ichatmutablecomponent = Component.literal("Internal server error");
++
++ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> {
++ conn.disconnect(ichatmutablecomponent);
++ }));
++ conn.setReadOnly();
++ continue;
++ }
++ }
++ }
++
++ // entities hooks
++ public int getEntityCount() {
++ return this.allEntities.size();
++ }
++
++ public int getPlayerCount() {
++ return this.localPlayers.size();
++ }
++
++ public Iterable getLocalEntities() {
++ return this.allEntities;
++ }
++
++ public Entity[] getLocalEntitiesCopy() {
++ return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class);
++ }
++
++ public List getLocalPlayers() {
++ return this.localPlayers;
++ }
++
++ public void addLoadedEntity(final Entity entity) {
++ this.loadedEntities.add(entity);
++ }
++
++ public boolean hasLoadedEntity(final Entity entity) {
++ return this.loadedEntities.contains(entity);
++ }
++
++ public void removeLoadedEntity(final Entity entity) {
++ this.loadedEntities.remove(entity);
++ }
++
++ public Iterable getLoadedEntities() {
++ return this.loadedEntities;
++ }
++
++ public void addEntityTickingEntity(final Entity entity) {
++ if (!TickThread.isTickThreadFor(entity)) {
++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
++ }
++ this.entityTickList.add(entity);
++ TickRegions.RegionStats.updateCurrentRegion();
++ }
++
++ public boolean hasEntityTickingEntity(final Entity entity) {
++ return this.entityTickList.contains(entity);
++ }
++
++ public void removeEntityTickingEntity(final Entity entity) {
++ if (!TickThread.isTickThreadFor(entity)) {
++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
++ }
++ this.entityTickList.remove(entity);
++ TickRegions.RegionStats.updateCurrentRegion();
++ }
++
++ public void forEachTickingEntity(final Consumer action) {
++ final IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickList.iterator();
++ try {
++ while (iterator.hasNext()) {
++ action.accept(iterator.next());
++ }
++ } finally {
++ iterator.finishedIterating();
++ }
++ }
++
++ public void addEntity(final Entity entity) {
++ if (!TickThread.isTickThreadFor(this.world, entity.chunkPosition())) {
++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
++ }
++ if (this.allEntities.add(entity)) {
++ if (entity instanceof ServerPlayer player) {
++ this.localPlayers.add(player);
++ }
++ TickRegions.RegionStats.updateCurrentRegion();
++ }
++ }
++
++ public boolean hasEntity(final Entity entity) {
++ return this.allEntities.contains(entity);
++ }
++
++ public void removeEntity(final Entity entity) {
++ if (!TickThread.isTickThreadFor(entity)) {
++ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control");
++ }
++ if (this.allEntities.remove(entity)) {
++ if (entity instanceof ServerPlayer player) {
++ this.localPlayers.remove(player);
++ }
++ TickRegions.RegionStats.updateCurrentRegion();
++ }
++ }
++
++ public void addNavigatingMob(final Mob mob) {
++ if (!TickThread.isTickThreadFor(mob)) {
++ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control");
++ }
++ this.navigatingMobs.add(mob);
++ }
++
++ public void removeNavigatingMob(final Mob mob) {
++ if (!TickThread.isTickThreadFor(mob)) {
++ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control");
++ }
++ this.navigatingMobs.remove(mob);
++ }
++
++ public Iterator getNavigatingMobs() {
++ return this.navigatingMobs.unsafeIterator();
++ }
++
++ // block ticking hooks
++ // Since block event data does not require chunk holders to be created for the chunk they reside in,
++ // it's not actually guaranteed that when merging / splitting data that we actually own the data...
++ // Note that we can only ever not own the event data when the chunk unloads, and so I've decided to
++ // make the code easier by simply discarding it in such an event
++ public void pushBlockEvent(final BlockEventData blockEventData) {
++ TickThread.ensureTickThread(this.world, blockEventData.pos(), "Cannot queue block even data async");
++ this.blockEvents.add(blockEventData);
++ }
++
++ public void pushBlockEvents(final Collection 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();
++ }
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/Schedule.java b/io/papermc/paper/threadedregions/Schedule.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..820b1c4dc1b19ee8602333295f2034362f885a37
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/Schedule.java
+@@ -0,0 +1,91 @@
++package io.papermc.paper.threadedregions;
++
++/**
++ * A Schedule is an object that can be used to maintain a periodic schedule for an event of interest.
++ */
++public final class Schedule {
++
++ private long lastPeriod;
++
++ /**
++ * Initialises a schedule with the provided period.
++ * @param firstPeriod The last time an event of interest occurred.
++ * @see #setLastPeriod(long)
++ */
++ public Schedule(final long firstPeriod) {
++ this.lastPeriod = firstPeriod;
++ }
++
++ /**
++ * Updates the last period to the specified value. This call sets the last "time" the event
++ * of interest took place at. Thus, the value returned by {@link #getDeadline(long)} is
++ * the provided time plus the period length provided to {@code getDeadline}.
++ * @param value The value to set the last period to.
++ */
++ public void setLastPeriod(final long value) {
++ this.lastPeriod = value;
++ }
++
++ /**
++ * Returns the last time the event of interest should have taken place.
++ */
++ public long getLastPeriod() {
++ return this.lastPeriod;
++ }
++
++ /**
++ * Returns the number of times the event of interest should have taken place between the last
++ * period and the provided time given the period between each event.
++ * @param periodLength The length of the period between events in ns.
++ * @param time The provided time.
++ */
++ public int getPeriodsAhead(final long periodLength, final long time) {
++ final long difference = time - this.lastPeriod;
++ final int ret = (int)(Math.abs(difference) / periodLength);
++ return difference >= 0 ? ret : -ret;
++ }
++
++ /**
++ * Returns the next starting deadline for the event of interest to take place,
++ * given the provided period length.
++ * @param periodLength The provided period length.
++ */
++ public long getDeadline(final long periodLength) {
++ return this.lastPeriod + periodLength;
++ }
++
++ /**
++ * Adjusts the last period so that the next starting deadline returned is the next period specified,
++ * given the provided period length.
++ * @param nextPeriod The specified next starting deadline.
++ * @param periodLength The specified period length.
++ */
++ public void setNextPeriod(final long nextPeriod, final long periodLength) {
++ this.lastPeriod = nextPeriod - periodLength;
++ }
++
++ /**
++ * Increases the last period by the specified number of periods and period length.
++ * The specified number of periods may be < 0, in which case the last period
++ * will decrease.
++ * @param periods The specified number of periods.
++ * @param periodLength The specified period length.
++ */
++ public void advanceBy(final int periods, final long periodLength) {
++ this.lastPeriod += (long)periods * periodLength;
++ }
++
++ /**
++ * Sets the last period so that it is the specified number of periods ahead
++ * given the specified time and period length.
++ * @param periodsToBeAhead Specified number of periods to be ahead by.
++ * @param periodLength The specified period length.
++ * @param time The specified time.
++ */
++ public void setPeriodsAhead(final int periodsToBeAhead, final long periodLength, final long time) {
++ final int periodsAhead = this.getPeriodsAhead(periodLength, time);
++ final int periodsToAdd = periodsToBeAhead - periodsAhead;
++
++ this.lastPeriod -= (long)periodsToAdd * periodLength;
++ }
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/TeleportUtils.java b/io/papermc/paper/threadedregions/TeleportUtils.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..2a64a5b2cf049661fe3f5a22ddfa39979624f5ec
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/TeleportUtils.java
+@@ -0,0 +1,82 @@
++package io.papermc.paper.threadedregions;
++
++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.phys.Vec3;
++import org.bukkit.Location;
++import org.bukkit.craftbukkit.CraftWorld;
++import org.bukkit.event.player.PlayerTeleportEvent;
++import java.util.function.Consumer;
++
++public final class TeleportUtils {
++
++ public static void teleport(final T from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch,
++ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) {
++ teleport(from, useFromRootVehicle, to, yaw, pitch, teleportFlags, cause, onComplete, null);
++ }
++
++ public static void teleport(final T from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch,
++ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete,
++ final java.util.function.Predicate preTeleport) {
++ // retrieve coordinates
++ final CallbackCompletable positionCompletable = new CallbackCompletable<>();
++
++ positionCompletable.addWaiter(
++ (final Location loc, final Throwable thr) -> {
++ if (loc == null) {
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
++ return;
++ }
++ final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule(
++ (final T realFrom) -> {
++ final Vec3 pos = new Vec3(
++ loc.getX(), loc.getY(), loc.getZ()
++ );
++ if (preTeleport != null && !preTeleport.test(realFrom)) {
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
++ return;
++ }
++ (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync(
++ ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null,
++ cause, teleportFlags, onComplete
++ );
++ },
++ (final Entity retired) -> {
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
++ },
++ 1L
++ );
++ if (!scheduled) {
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
++ }
++ }
++ );
++
++ final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule(
++ (final Entity target) -> {
++ positionCompletable.complete(target.getBukkitEntity().getLocation());
++ },
++ (final Entity retired) -> {
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
++ },
++ 1L
++ );
++ if (!scheduled) {
++ if (onComplete != null) {
++ onComplete.accept(null);
++ }
++ }
++ }
++
++ private TeleportUtils() {}
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/io/papermc/paper/threadedregions/ThreadedRegionizer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..604385af903845d966382ad0a4168798e4ed4a0e
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/ThreadedRegionizer.java
+@@ -0,0 +1,1405 @@
++package io.papermc.paper.threadedregions;
++
++import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable;
++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil;
++import ca.spottedleaf.moonrise.common.util.CoordinateUtils;
++import com.destroystokyo.paper.util.SneakyThrow;
++import com.mojang.logging.LogUtils;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.longs.LongArrayList;
++import it.unimi.dsi.fastutil.longs.LongComparator;
++import it.unimi.dsi.fastutil.longs.LongIterator;
++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
++import net.minecraft.core.BlockPos;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.entity.Entity;
++import net.minecraft.world.level.ChunkPos;
++import org.slf4j.Logger;
++import java.lang.invoke.VarHandle;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Iterator;
++import java.util.List;
++import java.util.Set;
++import java.util.concurrent.atomic.AtomicLong;
++import java.util.concurrent.locks.StampedLock;
++import java.util.function.BooleanSupplier;
++import java.util.function.Consumer;
++
++public final class ThreadedRegionizer, S extends ThreadedRegionizer.ThreadedRegionSectionData> {
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++
++ public final int regionSectionChunkSize;
++ public final int sectionChunkShift;
++ public final int minSectionRecalcCount;
++ public final int emptySectionCreateRadius;
++ public final int regionSectionMergeRadius;
++ public final double maxDeadRegionPercent;
++ public final ServerLevel world;
++
++ private final SWMRLong2ObjectHashTable> sections = new SWMRLong2ObjectHashTable<>();
++ private final SWMRLong2ObjectHashTable> regionsById = new SWMRLong2ObjectHashTable<>();
++ private final RegionCallbacks callbacks;
++ private final StampedLock regionLock = new StampedLock();
++ private Thread writeLockOwner;
++
++ /*
++ static final record Operation(String type, int chunkX, int chunkZ) {}
++ private final MultiThreadedQueue ops = new MultiThreadedQueue<>();
++ */
++
++ /*
++ * See REGION_LOGIC.md for complete details on what this class is doing
++ */
++
++ public ThreadedRegionizer(final int minSectionRecalcCount, final double maxDeadRegionPercent,
++ final int emptySectionCreateRadius, final int regionSectionMergeRadius,
++ final int regionSectionChunkShift, final ServerLevel world,
++ final RegionCallbacks callbacks) {
++ if (emptySectionCreateRadius <= 0) {
++ throw new IllegalStateException("Region section create radius must be > 0");
++ }
++ if (regionSectionMergeRadius <= 0) {
++ throw new IllegalStateException("Region section merge radius must be > 0");
++ }
++ this.regionSectionChunkSize = 1 << regionSectionChunkShift;
++ this.sectionChunkShift = regionSectionChunkShift;
++ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount);
++ this.maxDeadRegionPercent = maxDeadRegionPercent;
++ this.emptySectionCreateRadius = emptySectionCreateRadius;
++ this.regionSectionMergeRadius = regionSectionMergeRadius;
++ this.world = world;
++ this.callbacks = callbacks;
++ //this.loadTestData();
++ }
++
++ /*
++ private static String substr(String val, String prefix, int from) {
++ int idx = val.indexOf(prefix, from) + prefix.length();
++ int idx2 = val.indexOf(',', idx);
++ if (idx2 == -1) {
++ idx2 = val.indexOf(']', idx);
++ }
++ return val.substring(idx, idx2);
++ }
++
++ private void loadTestData() {
++ if (true) {
++ return;
++ }
++ try {
++ final JsonArray arr = JsonParser.parseReader(new FileReader("test.json")).getAsJsonArray();
++
++ List ops = new ArrayList<>();
++
++ for (JsonElement elem : arr) {
++ JsonObject obj = elem.getAsJsonObject();
++ String val = obj.get("value").getAsString();
++
++ String type = substr(val, "type=", 0);
++ String x = substr(val, "chunkX=", 0);
++ String z = substr(val, "chunkZ=", 0);
++
++ ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z)));
++ }
++
++ for (Operation op : ops) {
++ switch (op.type) {
++ case "add": {
++ this.addChunk(op.chunkX, op.chunkZ);
++ break;
++ }
++ case "remove": {
++ this.removeChunk(op.chunkX, op.chunkZ);
++ break;
++ }
++ case "mark_ticking": {
++ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking();
++ break;
++ }
++ case "rel_region": {
++ if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) {
++ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking();
++ }
++ break;
++ }
++ }
++ }
++
++ } catch (final Exception ex) {
++ throw new IllegalStateException(ex);
++ }
++ }
++ */
++
++ public void acquireReadLock() {
++ this.regionLock.readLock();
++ }
++
++ public void releaseReadLock() {
++ this.regionLock.tryUnlockRead();
++ }
++
++ private void acquireWriteLock() {
++ final Thread currentThread = Thread.currentThread();
++ if (this.writeLockOwner == currentThread) {
++ throw new IllegalStateException("Cannot recursively operate in the regioniser");
++ }
++ this.regionLock.writeLock();
++ this.writeLockOwner = currentThread;
++ }
++
++ private void releaseWriteLock() {
++ this.writeLockOwner = null;
++ this.regionLock.tryUnlockWrite();
++ }
++
++ private void onRegionCreate(final ThreadedRegion region) {
++ final ThreadedRegion conflict;
++ if ((conflict = this.regionsById.putIfAbsent(region.id, region)) != null) {
++ throw new IllegalStateException("Region " + region + " is already mapped to " + conflict);
++ }
++ }
++
++ private void onRegionDestroy(final ThreadedRegion region) {
++ final ThreadedRegion removed = this.regionsById.remove(region.id);
++ if (removed != region) {
++ throw new IllegalStateException("Expected to remove " + region + ", but removed " + removed);
++ }
++ }
++
++ public int getSectionCoordinate(final int chunkCoordinate) {
++ return chunkCoordinate >> this.sectionChunkShift;
++ }
++
++ public long getSectionKey(final BlockPos pos) {
++ return CoordinateUtils.getChunkKey((pos.getX() >> 4) >> this.sectionChunkShift, (pos.getZ() >> 4) >> this.sectionChunkShift);
++ }
++
++ public long getSectionKey(final ChunkPos pos) {
++ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift);
++ }
++
++ public long getSectionKey(final Entity entity) {
++ final ChunkPos pos = entity.chunkPosition();
++ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift);
++ }
++
++ public void computeForAllRegions(final Consumer 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);
++ }
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/TickData.java b/io/papermc/paper/threadedregions/TickData.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..d4d80a69488f57704f1b3dc74cb379de36e80ec0
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/TickData.java
+@@ -0,0 +1,333 @@
++package io.papermc.paper.threadedregions;
++
++import ca.spottedleaf.concurrentutil.util.TimeUtil;
++import io.papermc.paper.util.IntervalledCounter;
++import it.unimi.dsi.fastutil.longs.LongArrayList;
++
++import java.util.ArrayDeque;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.List;
++
++public final class TickData {
++
++ private final long interval; // ns
++
++ private final ArrayDeque timeData = new ArrayDeque<>();
++
++ public TickData(final long intervalNS) {
++ this.interval = intervalNS;
++ }
++
++ public void addDataFrom(final TickRegionScheduler.TickTime time) {
++ final long start = time.tickStart();
++
++ TickRegionScheduler.TickTime first;
++ while ((first = this.timeData.peekFirst()) != null) {
++ // only remove data completely out of window
++ if ((start - first.tickEnd()) <= this.interval) {
++ break;
++ }
++ this.timeData.pollFirst();
++ }
++
++ this.timeData.add(time);
++ }
++
++ // fromIndex inclusive, toIndex exclusive
++ // will throw if arr.length == 0
++ private static double median(final long[] arr, final int fromIndex, final int toIndex) {
++ final int len = toIndex - fromIndex;
++ final int middle = fromIndex + (len >>> 1);
++ if ((len & 1) == 0) {
++ // even, average the two middle points
++ return (double)(arr[middle - 1] + arr[middle]) / 2.0;
++ } else {
++ // odd, just grab the middle
++ return (double)arr[middle];
++ }
++ }
++
++ // will throw if arr.length == 0
++ private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex,
++ final boolean inverse) {
++ final int len = toIndex - fromIndex;
++ long sum = 0L;
++ final double median = median(arr, fromIndex, toIndex);
++ long min = arr[0];
++ long max = arr[0];
++
++ for (int i = fromIndex; i < toIndex; ++i) {
++ final long val = arr[i];
++ sum += val;
++ if (val < min) {
++ min = val;
++ }
++ if (val > max) {
++ max = val;
++ }
++ }
++
++ if (inverse) {
++ // for positive a,b we have that a >= b if and only if 1/a <= 1/b
++ return new SegmentData(
++ len,
++ (double)len / ((double)sum / 1.0E9),
++ 1.0E9 / median,
++ 1.0E9 / (double)max,
++ 1.0E9 / (double)min
++ );
++ } else {
++ return new SegmentData(
++ len,
++ (double)sum / (double)len,
++ median,
++ (double)min,
++ (double)max
++ );
++ }
++ }
++
++ private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd,
++ final int percent99BestStart, final int percent99BestEnd,
++ final int percent95BestStart, final int percent95BestEnd,
++ final int percent1WorstStart, final int percent1WorstEnd,
++ final int percent5WorstStart, final int percent5WorstEnd,
++ final boolean inverse) {
++ return new SegmentedAverage(
++ computeSegmentData(data, allStart, allEnd, inverse),
++ computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse),
++ computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse),
++ computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse),
++ computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse)
++ );
++ }
++
++ private static record TickInformation(
++ long differenceFromLastTick,
++ long tickTime,
++ long tickTimeCPU
++ ) {}
++
++ // rets null if there is no data
++ public TickReportData generateTickReport(final TickRegionScheduler.TickTime inProgress, final long endTime) {
++ if (this.timeData.isEmpty() && inProgress == null) {
++ return null;
++ }
++
++ final List allData = new ArrayList<>(this.timeData);
++ if (inProgress != null) {
++ allData.add(inProgress);
++ }
++
++ final long intervalStart = allData.get(0).tickStart();
++ final long intervalEnd = allData.get(allData.size() - 1).tickEnd();
++
++ // to make utilisation accurate, we need to take the total time used over the last interval period -
++ // this means if a tick start before the measurement interval, but ends within the interval, then we
++ // only consider the time it spent ticking inside the interval
++ long totalTimeOverInterval = 0L;
++ long measureStart = endTime - this.interval;
++
++ for (int i = 0, len = allData.size(); i < len; ++i) {
++ final TickRegionScheduler.TickTime time = allData.get(i);
++ if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) {
++ final long diff = time.tickEnd() - measureStart;
++ if (diff > 0L) {
++ totalTimeOverInterval += diff;
++ } // else: the time is entirely out of interval
++ } else {
++ totalTimeOverInterval += time.tickLength();
++ }
++ }
++
++ // we only care about ticks, but because of inbetween tick task execution
++ // there will be data in allData that isn't ticks. But, that data cannot
++ // be ignored since it contributes to utilisation.
++ // So, we will "compact" the data by merging any inbetween tick times
++ // the next tick.
++ // If there is no "next tick", then we will create one.
++ final List collapsedData = new ArrayList<>();
++ for (int i = 0, len = allData.size(); i < len; ++i) {
++ final List toCollapse = new ArrayList<>();
++ TickRegionScheduler.TickTime lastTick = null;
++ for (;i < len; ++i) {
++ final TickRegionScheduler.TickTime time = allData.get(i);
++ if (!time.isTickExecution()) {
++ toCollapse.add(time);
++ continue;
++ }
++ lastTick = time;
++ break;
++ }
++
++ if (toCollapse.isEmpty()) {
++ // nothing to collapse
++ final TickRegionScheduler.TickTime last = allData.get(i);
++ collapsedData.add(
++ new TickInformation(
++ last.differenceFromLastTick(),
++ last.tickLength(),
++ last.supportCPUTime() ? last.tickCpuTime() : 0L
++ )
++ );
++ } else {
++ long totalTickTime = 0L;
++ long totalCpuTime = 0L;
++ for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) {
++ final TickRegionScheduler.TickTime time = toCollapse.get(k);
++ totalTickTime += time.tickLength();
++ totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L;
++ }
++ if (i < len) {
++ // we know there is a tick to collapse into
++ final TickRegionScheduler.TickTime last = allData.get(i);
++ collapsedData.add(
++ new TickInformation(
++ last.differenceFromLastTick(),
++ last.tickLength() + totalTickTime,
++ (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime
++ )
++ );
++ } else {
++ // we do not have a tick to collapse into, so we must make one up
++ // we will assume that the tick is "starting now" and ongoing
++
++ // compute difference between imaginary tick and last tick
++ final long differenceBetweenTicks;
++ if (lastTick != null) {
++ // we have a last tick, use it
++ differenceBetweenTicks = lastTick.tickStart();
++ } else {
++ // we don't have a last tick, so we must make one up that makes sense
++ // if the current interval exceeds the max tick time, then use it
++
++ // Otherwise use the interval length.
++ // This is how differenceFromLastTick() works on TickTime when there is no previous interval.
++ differenceBetweenTicks = Math.max(
++ TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime
++ );
++ }
++
++ collapsedData.add(
++ new TickInformation(
++ differenceBetweenTicks,
++ totalTickTime,
++ totalCpuTime
++ )
++ );
++ }
++ }
++ }
++
++
++ final int collectedTicks = collapsedData.size();
++ final long[] tickStartToStartDifferences = new long[collectedTicks];
++ final long[] timePerTickDataRaw = new long[collectedTicks];
++ final long[] missingCPUTimeDataRaw = new long[collectedTicks];
++
++ long totalTimeTicking = 0L;
++
++ int i = 0;
++ for (final TickInformation time : collapsedData) {
++ tickStartToStartDifferences[i] = time.differenceFromLastTick();
++ final long timePerTick = timePerTickDataRaw[i] = time.tickTime();
++ missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU());
++
++ ++i;
++
++ totalTimeTicking += timePerTick;
++ }
++
++ Arrays.sort(tickStartToStartDifferences);
++ Arrays.sort(timePerTickDataRaw);
++ Arrays.sort(missingCPUTimeDataRaw);
++
++ // Note: computeSegmentData cannot take start == end
++ final int allStart = 0;
++ final int allEnd = collectedTicks;
++ final int percent95BestStart = 0;
++ final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks);
++ final int percent99BestStart = 0;
++ // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end
++ final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks);
++ final int percent1WorstStart = (int)(0.99 * collectedTicks);
++ final int percent1WorstEnd = collectedTicks;
++ final int percent5WorstStart = (int)(0.95 * collectedTicks);
++ final int percent5WorstEnd = collectedTicks;
++
++ final SegmentedAverage tpsData = computeSegmentedAverage(
++ tickStartToStartDifferences,
++ allStart, allEnd,
++ percent99BestStart, percent99BestEnd,
++ percent95BestStart, percent95BestEnd,
++ percent1WorstStart, percent1WorstEnd,
++ percent5WorstStart, percent5WorstEnd,
++ true
++ );
++
++ final SegmentedAverage timePerTickData = computeSegmentedAverage(
++ timePerTickDataRaw,
++ allStart, allEnd,
++ percent99BestStart, percent99BestEnd,
++ percent95BestStart, percent95BestEnd,
++ percent1WorstStart, percent1WorstEnd,
++ percent5WorstStart, percent5WorstEnd,
++ false
++ );
++
++ final SegmentedAverage missingCPUTimeData = computeSegmentedAverage(
++ missingCPUTimeDataRaw,
++ allStart, allEnd,
++ percent99BestStart, percent99BestEnd,
++ percent95BestStart, percent95BestEnd,
++ percent1WorstStart, percent1WorstEnd,
++ percent5WorstStart, percent5WorstEnd,
++ false
++ );
++
++ final double utilisation = (double)totalTimeOverInterval / (double)this.interval;
++
++ return new TickReportData(
++ collectedTicks,
++ intervalStart,
++ intervalEnd,
++ totalTimeTicking,
++ utilisation,
++
++ tpsData,
++ timePerTickData,
++ missingCPUTimeData
++ );
++ }
++
++ public static final record TickReportData(
++ int collectedTicks,
++ long collectedTickIntervalStart,
++ long collectedTickIntervalEnd,
++ long totalTimeTicking,
++ double utilisation,
++
++ SegmentedAverage tpsData,
++ // in ns
++ SegmentedAverage timePerTickData,
++ // in ns
++ SegmentedAverage missingCPUTimeData
++ ) {}
++
++ public static final record SegmentedAverage(
++ SegmentData segmentAll,
++ SegmentData segment99PercentBest,
++ SegmentData segment95PercentBest,
++ SegmentData segment5PercentWorst,
++ SegmentData segment1PercentWorst
++ ) {}
++
++ public static final record SegmentData(
++ int count,
++ double average,
++ double median,
++ double least,
++ double greatest
++ ) {}
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..18f57216522a8e22ea6c217c05588f8be13f5c88
+--- /dev/null
++++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java
+@@ -0,0 +1,564 @@
++package io.papermc.paper.threadedregions;
++
++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
++import ca.spottedleaf.concurrentutil.util.TimeUtil;
++import ca.spottedleaf.moonrise.common.util.TickThread;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.util.TraceUtil;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerLevel;
++import net.minecraft.world.level.ChunkPos;
++import org.slf4j.Logger;
++import java.lang.management.ManagementFactory;
++import java.lang.management.ThreadMXBean;
++import java.util.concurrent.ThreadFactory;
++import java.util.concurrent.TimeUnit;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.concurrent.atomic.AtomicInteger;
++import java.util.function.BooleanSupplier;
++
++public final class TickRegionScheduler {
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++ private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean();
++ private static final boolean MEASURE_CPU_TIME;
++ static {
++ MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported();
++ if (MEASURE_CPU_TIME) {
++ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true);
++ } else {
++ LOGGER.warn("TickRegionScheduler CPU time measurement is not available");
++ }
++ }
++
++ public static final int TICK_RATE = 20;
++ public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns
++
++ private final SchedulerThreadPool scheduler;
++
++ public TickRegionScheduler(final int threads) {
++ this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() {
++ private final AtomicInteger idGenerator = new AtomicInteger();
++
++ @Override
++ public Thread newThread(final Runnable run) {
++ final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement());
++ ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException);
++ return ret;
++ }
++ });
++ }
++
++ public int getTotalThreadCount() {
++ return this.scheduler.getThreads().length;
++ }
++
++ private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion region) {
++ final Thread currThread = Thread.currentThread();
++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
++ throw new IllegalStateException("Must be tick thread runner");
++ }
++ if (region != null && tickThreadRunner.currentTickingRegion != null) {
++ throw new IllegalStateException("Trying to double set ticking region!");
++ }
++ if (region == null && tickThreadRunner.currentTickingRegion == null) {
++ throw new IllegalStateException("Trying to double unset ticking region!");
++ }
++ tickThreadRunner.currentTickingRegion = region;
++ if (region != null) {
++ tickThreadRunner.currentTickingWorldRegionizedData = region.regioniser.world.worldRegionData.get();
++ } else {
++ tickThreadRunner.currentTickingWorldRegionizedData = null;
++ }
++ }
++
++ private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) {
++ final Thread currThread = Thread.currentThread();
++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
++ throw new IllegalStateException("Must be tick thread runner");
++ }
++ if (task != null && tickThreadRunner.currentTickingTask != null) {
++ throw new IllegalStateException("Trying to double set ticking task!");
++ }
++ if (task == null && tickThreadRunner.currentTickingTask == null) {
++ throw new IllegalStateException("Trying to double unset ticking task!");
++ }
++ tickThreadRunner.currentTickingTask = task;
++ }
++
++ /**
++ * Returns the current ticking region, or {@code null} if there is no ticking region.
++ * If this thread is not a TickThread, then returns {@code null}.
++ */
++ public static ThreadedRegionizer.ThreadedRegion getCurrentRegion() {
++ final Thread currThread = Thread.currentThread();
++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
++ return RegionShutdownThread.getRegion();
++ }
++ return tickThreadRunner.currentTickingRegion;
++ }
++
++ /**
++ * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region.
++ * This is a faster alternative to calling the {@link RegionizedData#get()} method.
++ * If this thread is not a TickThread, then returns {@code null}.
++ */
++ public static RegionizedWorldData getCurrentRegionizedWorldData() {
++ final Thread currThread = Thread.currentThread();
++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
++ return RegionShutdownThread.getWorldData();
++ }
++ return tickThreadRunner.currentTickingWorldRegionizedData;
++ }
++
++ /**
++ * Returns the current ticking task, or {@code null} if there is no ticking region.
++ * If this thread is not a TickThread, then returns {@code null}.
++ */
++ public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() {
++ final Thread currThread = Thread.currentThread();
++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) {
++ return null;
++ }
++ return tickThreadRunner.currentTickingTask;
++ }
++
++ /**
++ * Schedules the given region
++ * @throws IllegalStateException If the region is already scheduled or is ticking
++ */
++ public void scheduleRegion(final RegionScheduleHandle region) {
++ region.scheduler = this;
++ this.scheduler.schedule(region);
++ }
++
++ /**
++ * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task
++ * execution, then it will be cancelled after.
++ */
++ public void descheduleRegion(final RegionScheduleHandle region) {
++ // To avoid acquiring any of the locks the scheduler may be using, we
++ // simply cancel the next action.
++ region.markNonSchedulable();
++ }
++
++ /**
++ * Updates the tick start to the farthest into the future of its current scheduled time and the
++ * provided time.
++ * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its
++ * current start time, {@code true} if the next tick start was adjusted.
++ */
++ public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) {
++ return this.scheduler.updateTickStartToMax(region, newStart);
++ }
++
++ public boolean halt(final boolean sync, final long maxWaitNS) {
++ return this.scheduler.halt(sync, maxWaitNS);
++ }
++
++ void dumpAliveThreadTraces(final String reason) {
++ for (final Thread thread : this.scheduler.getThreads()) {
++ if (thread.isAlive()) {
++ TraceUtil.dumpTraceForThread(thread, reason);
++ }
++ }
++ }
++
++ public void setHasTasks(final RegionScheduleHandle region) {
++ this.scheduler.notifyTasks(region);
++ }
++
++ public void init() {
++ this.scheduler.start();
++ }
++
++ private void uncaughtException(final Thread thread, final Throwable thr) {
++ LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr);
++
++ // prevent further ticks from occurring
++ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
++ this.scheduler.halt(false, 0L);
++
++ MinecraftServer.getServer().stopServer();
++ }
++
++ private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) {
++ // when a region fails, we need to shut down the server gracefully
++
++ // prevent further ticks from occurring
++ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD
++ this.scheduler.halt(false, 0L);
++
++ final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk();
++ final ServerLevel world = handle.region == null ? null : handle.region.world;
++
++ LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " in world '" + (world == null ? "null" : world.getWorld().getName()) + "' failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr);
++
++ MinecraftServer.getServer().stopServer();
++ }
++
++ // By using our own thread object, we can use a field for the current region rather than a ThreadLocal.
++ // This is much faster than a thread local, since the thread local has to use a map lookup.
++ private static final class TickThreadRunner extends TickThread {
++
++ private ThreadedRegionizer.ThreadedRegion currentTickingRegion;
++ private RegionizedWorldData currentTickingWorldRegionizedData;
++ private SchedulerThreadPool.SchedulableTick currentTickingTask;
++
++ public TickThreadRunner(final Runnable run, final String name) {
++ super(run, name);
++ }
++ }
++
++ public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick {
++
++ protected long currentTick;
++ protected long lastTickStart;
++
++ protected final TickData tickTimes5s;
++ protected final TickData tickTimes15s;
++ protected final TickData tickTimes1m;
++ protected final TickData tickTimes5m;
++ protected final TickData tickTimes15m;
++ protected TickTime currentTickData;
++ protected Thread currentTickingThread;
++
++ public final TickRegions.TickRegionData region;
++ private final AtomicBoolean cancelled = new AtomicBoolean();
++
++ protected final Schedule tickSchedule;
++
++ private TickRegionScheduler scheduler;
++
++ public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) {
++ this.currentTick = 0L;
++ this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET;
++ this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L));
++ this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L));
++ this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L));
++ this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L));
++ this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L));
++ this.region = region;
++
++ this.setScheduledStart(firstStart);
++ this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS);
++ }
++
++ /**
++ * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)}
++ * so that the tick schedule and scheduled start remain synchronised
++ */
++ protected final void updateScheduledStart(final long to) {
++ this.setScheduledStart(to);
++ this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS);
++ }
++
++ public final void markNonSchedulable() {
++ this.cancelled.set(true);
++ }
++
++ public final boolean isMarkedAsNonSchedulable() {
++ return this.cancelled.get();
++ }
++
++ protected abstract boolean tryMarkTicking();
++
++ protected abstract boolean markNotTicking();
++
++ protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd);
++
++ protected abstract boolean runRegionTasks(final BooleanSupplier canContinue);
++
++ protected abstract boolean hasIntermediateTasks();
++
++ @Override
++ public final boolean hasTasks() {
++ return this.hasIntermediateTasks();
++ }
++
++ @Override
++ public final Boolean runTasks(final BooleanSupplier canContinue) {
++ if (this.cancelled.get()) {
++ return null;
++ }
++
++ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
++ final long tickStart = System.nanoTime();
++
++ if (!this.tryMarkTicking()) {
++ if (!this.cancelled.get()) {
++ throw new IllegalStateException("Scheduled region should be acquirable");
++ }
++ // region was killed
++ return null;
++ }
++
++ TickRegionScheduler.setTickTask(this);
++ if (this.region != null) {
++ TickRegionScheduler.setTickingRegion(this.region.region);
++ }
++
++ synchronized (this) {
++ this.currentTickData = new TickTime(
++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart,
++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
++ false
++ );
++ this.currentTickingThread = Thread.currentThread();
++ }
++
++ final boolean ret;
++ try {
++ ret = this.runRegionTasks(() -> {
++ return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean();
++ });
++ } catch (final Throwable thr) {
++ this.scheduler.regionFailed(this, true, thr);
++ // don't release region for another tick
++ return null;
++ } finally {
++ final long tickEnd = System.nanoTime();
++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
++
++ final TickTime time = new TickTime(
++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET,
++ tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false
++ );
++
++ this.addTickTime(time);
++ TickRegionScheduler.setTickTask(null);
++ if (this.region != null) {
++ TickRegionScheduler.setTickingRegion(null);
++ }
++ }
++
++ return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret);
++ }
++
++ @Override
++ public final boolean runTick() {
++ // Remember, we are supposed use setScheduledStart if we return true here, otherwise
++ // the scheduler will try to schedule for the same time.
++ if (this.cancelled.get()) {
++ return false;
++ }
++
++ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
++ final long tickStart = System.nanoTime();
++
++ // use max(), don't assume that tickStart >= scheduledStart
++ final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart));
++
++ if (!this.tryMarkTicking()) {
++ if (!this.cancelled.get()) {
++ throw new IllegalStateException("Scheduled region should be acquirable");
++ }
++ // region was killed
++ return false;
++ }
++ if (this.cancelled.get()) {
++ this.markNotTicking();
++ // region should be killed
++ return false;
++ }
++
++ TickRegionScheduler.setTickTask(this);
++ if (this.region != null) {
++ TickRegionScheduler.setTickingRegion(this.region.region);
++ }
++ this.incrementTickCount();
++ final long lastTickStart = this.lastTickStart;
++ this.lastTickStart = tickStart;
++
++ final long scheduledStart = this.getScheduledStart();
++ final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS;
++
++ synchronized (this) {
++ this.currentTickData = new TickTime(
++ lastTickStart, scheduledStart, tickStart, cpuStart,
++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME,
++ true
++ );
++ this.currentTickingThread = Thread.currentThread();
++ }
++
++ try {
++ // next start isn't updated until the end of this tick
++ this.tickRegion(tickCount, tickStart, scheduledEnd);
++ } catch (final Throwable thr) {
++ this.scheduler.regionFailed(this, false, thr);
++ // regionFailed will schedule a shutdown, so we should avoid letting this region tick further
++ return false;
++ } finally {
++ final long tickEnd = System.nanoTime();
++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L;
++
++ // in order to ensure all regions get their chance at scheduling, we have to ensure that regions
++ // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest
++ // of the current time and "ideal" next tick start.
++ this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS);
++ this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS)));
++
++ final TickTime time = new TickTime(
++ lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true
++ );
++
++ this.addTickTime(time);
++ TickRegionScheduler.setTickTask(null);
++ if (this.region != null) {
++ TickRegionScheduler.setTickingRegion(null);
++ }
++ }
++
++ // Only AFTER updating the tickStart
++ return this.markNotTicking() && !this.cancelled.get();
++ }
++
++ /**
++ * Only safe to call if this tick data matches the current ticking region.
++ */
++ protected void addTickTime(final TickTime time) {
++ synchronized (this) {
++ this.currentTickData = null;
++ this.currentTickingThread = null;
++ this.tickTimes5s.addDataFrom(time);
++ this.tickTimes15s.addDataFrom(time);
++ this.tickTimes1m.addDataFrom(time);
++ this.tickTimes5m.addDataFrom(time);
++ this.tickTimes15m.addDataFrom(time);
++ }
++ }
++
++ private TickTime adjustCurrentTickData(final long tickEnd) {
++ final TickTime currentTickData = this.currentTickData;
++ if (currentTickData == null) {
++ return null;
++ }
++
++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L;
++
++ return new TickTime(
++ currentTickData.previousTickStart(), currentTickData.scheduledTickStart(),
++ currentTickData.tickStart(), currentTickData.tickStartCPU(),
++ tickEnd, cpuEnd,
++ MEASURE_CPU_TIME, currentTickData.isTickExecution()
++ );
++ }
++
++ public final TickData.TickReportData getTickReport5s(final long currTime) {
++ synchronized (this) {
++ return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
++ }
++ }
++
++ public final TickData.TickReportData getTickReport15s(final long currTime) {
++ synchronized (this) {
++ return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
++ }
++ }
++
++ public final TickData.TickReportData getTickReport1m(final long currTime) {
++ synchronized (this) {
++ return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
++ }
++ }
++
++ public final TickData.TickReportData getTickReport5m(final long currTime) {
++ synchronized (this) {
++ return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
++ }
++ }
++
++ public final TickData.TickReportData getTickReport15m(final long currTime) {
++ synchronized (this) {
++ return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime);
++ }
++ }
++
++ /**
++ * Only safe to call if this tick data matches the current ticking region.
++ */
++ private void incrementTickCount() {
++ ++this.currentTick;
++ }
++
++ /**
++ * Only safe to call if this tick data matches the current ticking region.
++ */
++ public final long getCurrentTick() {
++ return this.currentTick;
++ }
++
++ protected final void setCurrentTick(final long value) {
++ this.currentTick = value;
++ }
++ }
++
++ // All time units are in nanoseconds.
++ public static final record TickTime(
++ long previousTickStart,
++ long scheduledTickStart,
++ long tickStart,
++ long tickStartCPU,
++ long tickEnd,
++ long tickEndCPU,
++ boolean supportCPUTime,
++ boolean isTickExecution
++ ) {
++ /**
++ * The difference between the start tick time and the scheduled start tick time. This value is
++ * < 0 if the tick started before the scheduled tick time.
++ * Only valid when {@link #isTickExecution()} is {@code true}.
++ */
++ public final long startOvershoot() {
++ return this.tickStart - this.scheduledTickStart;
++ }
++
++ /**
++ * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong).
++ */
++ public final long tickLength() {
++ return this.tickEnd - this.tickStart;
++ }
++
++ /**
++ * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength,
++ * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE
++ * if CPU time measurement is not supported.
++ */
++ public final long tickCpuTime() {
++ if (!this.supportCPUTime()) {
++ return Long.MIN_VALUE;
++ }
++ return this.tickEndCPU - this.tickStartCPU;
++ }
++
++ /**
++ * The difference in time from the start of the last tick to the start of the current tick. If there is no
++ * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength).
++ * Only valid when {@link #isTickExecution()} is {@code true}.
++ */
++ public final long differenceFromLastTick() {
++ if (this.hasLastTick()) {
++ return this.tickStart - this.previousTickStart;
++ }
++ return Math.max(TIME_BETWEEN_TICKS, this.tickLength());
++ }
++
++ /**
++ * Returns whether there was a tick that occurred before this one.
++ * Only valid when {@link #isTickExecution()} is {@code true}.
++ */
++ public boolean hasLastTick() {
++ return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET;
++ }
++
++ /*
++ * Remember, this is the expected behavior of the following:
++ *
++ * MSPT: Time per tick. This does not include overshoot time, just the tickLength().
++ *
++ * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick).
++ */
++ }
++}
+\ No newline at end of file
+diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java
+index 8424cf9d4617b4732d44cc460d25b04481068989..df15b1139e71dfe10b8f24ec6d235b99f6d5006a 100644
+--- a/io/papermc/paper/threadedregions/TickRegions.java
++++ b/io/papermc/paper/threadedregions/TickRegions.java
+@@ -1,10 +1,410 @@
+ package io.papermc.paper.threadedregions;
+
+-// placeholder class for Folia
+-public class TickRegions {
++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool;
++import ca.spottedleaf.concurrentutil.util.TimeUtil;
++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.configuration.GlobalConfiguration;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
++import net.minecraft.server.MinecraftServer;
++import net.minecraft.server.level.ServerLevel;
++import org.slf4j.Logger;
++import java.util.Iterator;
++import java.util.concurrent.TimeUnit;
++import java.util.concurrent.atomic.AtomicInteger;
++import java.util.concurrent.atomic.AtomicLong;
++import java.util.function.BooleanSupplier;
++
++public final class TickRegions implements ThreadedRegionizer.RegionCallbacks {
++
++ private static final Logger LOGGER = LogUtils.getLogger();
++ private static int regionShift = 31;
+
+ public static int getRegionChunkShift() {
+- return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT;
++ return regionShift;
++ }
++
++ private static boolean initialised;
++ private static TickRegionScheduler scheduler;
++
++ public static TickRegionScheduler getScheduler() {
++ return scheduler;
++ }
++
++ public static void init(final GlobalConfiguration.ThreadedRegions config) {
++ if (initialised) {
++ return;
++ }
++ initialised = true;
++ int gridExponent = config.gridExponent;
++ gridExponent = Math.max(0, gridExponent);
++ gridExponent = Math.min(31, gridExponent);
++ regionShift = gridExponent;
++
++ int tickThreads;
++ if (config.threads <= 0) {
++ tickThreads = Runtime.getRuntime().availableProcessors() / 2;
++ if (tickThreads <= 4) {
++ tickThreads = 1;
++ } else {
++ tickThreads = tickThreads / 4;
++ }
++ } else {
++ tickThreads = config.threads;
++ }
++
++ scheduler = new TickRegionScheduler(tickThreads);
++ LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads");
++ }
++
++ @Override
++ public TickRegionData createNewData(final ThreadedRegionizer.ThreadedRegion region) {
++ return new TickRegionData(region);
++ }
++
++ @Override
++ public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) {
++ return null;
++ }
++
++ @Override
++ public void onRegionCreate(final ThreadedRegionizer.ThreadedRegion region) {
++ final TickRegionData data = region.getData();
++ // post-region merge/split regioninfo update
++ data.getRegionStats().updateFrom(data.getOrCreateRegionizedData(data.world.worldRegionData));
++ }
++
++ @Override
++ public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion region) {
++ // nothing for now
++ }
++
++ @Override
++ public void onRegionActive(final ThreadedRegionizer.ThreadedRegion region) {
++ final TickRegionData data = region.getData();
++
++ data.tickHandle.checkInitialSchedule();
++ scheduler.scheduleRegion(data.tickHandle);
++ }
++
++ @Override
++ public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion region) {
++ final TickRegionData data = region.getData();
++
++ scheduler.descheduleRegion(data.tickHandle);
++ // old handle cannot be scheduled anymore, copy to a new handle
++ data.tickHandle = data.tickHandle.copy();
++ }
++
++ @Override
++ public void preMerge(final ThreadedRegionizer.ThreadedRegion from,
++ final ThreadedRegionizer.ThreadedRegion into) {
++
++ }
++
++ @Override
++ public void preSplit(final ThreadedRegionizer.ThreadedRegion from,
++ final java.util.List> into) {
++
++ }
++
++ public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {}
++
++ public static final class RegionStats {
++
++ private final AtomicInteger entityCount = new AtomicInteger();
++ private final AtomicInteger playerCount = new AtomicInteger();
++ private final AtomicInteger chunkCount = new AtomicInteger();
++
++ public int getEntityCount() {
++ return this.entityCount.get();
++ }
++
++ public int getPlayerCount() {
++ return this.playerCount.get();
++ }
++
++ public int getChunkCount() {
++ return this.chunkCount.get();
++ }
++
++ void updateFrom(final RegionizedWorldData data) {
++ this.entityCount.setRelease(data == null ? 0 : data.getEntityCount());
++ this.playerCount.setRelease(data == null ? 0 : data.getPlayerCount());
++ this.chunkCount.setRelease(data == null ? 0 : data.getChunkCount());
++ }
++
++ static void updateCurrentRegion() {
++ TickRegionScheduler.getCurrentRegion().getData().getRegionStats().updateFrom(TickRegionScheduler.getCurrentRegionizedWorldData());
++ }
++ }
++
++ public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData {
++
++ private static final AtomicLong ID_GENERATOR = new AtomicLong();
++ /** Never 0L, since 0L is reserved for global region. */
++ public final long id = ID_GENERATOR.incrementAndGet();
++
++ public final ThreadedRegionizer.ThreadedRegion region;
++ public final ServerLevel world;
++
++ // generic regionised data
++ private final Reference2ReferenceOpenHashMap, Object> regionizedData = new Reference2ReferenceOpenHashMap<>();
++
++ // tick data
++ private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET);
++
++ // queue data
++ private final RegionizedTaskQueue.RegionTaskQueueData taskQueueData;
++
++ // chunk holder manager data
++ private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData();
++
++ // async-safe read-only region data
++ private final RegionStats regionStats;
++
++ private TickRegionData(final ThreadedRegionizer.ThreadedRegion region) {
++ this.region = region;
++ this.world = region.regioniser.world;
++ this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData);
++ this.regionStats = new RegionStats();
++ }
++
++ public RegionStats getRegionStats() {
++ return this.regionStats;
++ }
++
++ public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() {
++ return this.taskQueueData;
++ }
++
++ // the value returned can be invalidated at any time, except when the caller
++ // is ticking this region
++ public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() {
++ return this.tickHandle;
++ }
++
++ public long getCurrentTick() {
++ return this.tickHandle.getCurrentTick();
++ }
++
++ public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() {
++ return this.holderManagerRegionData;
++ }
++
++ T getRegionizedData(final RegionizedData regionizedData) {
++ return (T)this.regionizedData.get(regionizedData);
++ }
++
++ T getOrCreateRegionizedData(final RegionizedData regionizedData) {
++ T ret = (T)this.regionizedData.get(regionizedData);
++
++ if (ret != null) {
++ return ret;
++ }
++
++ ret = regionizedData.createNewValue();
++ this.regionizedData.put(regionizedData, ret);
++
++ return ret;
++ }
++
++ @Override
++ public void split(final ThreadedRegionizer regioniser,
++ final Long2ReferenceOpenHashMap> into,
++ final ReferenceOpenHashSet> regions) {
++ final int shift = regioniser.sectionChunkShift;
++
++ // tick data
++ // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled),
++ // and the other regions to split into are not scheduled yet.
++ for (final ThreadedRegionizer.ThreadedRegion region : regions) {
++ final TickRegionData data = region.getData();
++ data.tickHandle.copyDeadlineAndTickCount(this.tickHandle);
++ }
++
++ // generic regionised data
++ for (final Iterator, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator();
++ dataIterator.hasNext();) {
++ final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next();
++ final RegionizedData> data = regionDataEntry.getKey();
++ final Object from = regionDataEntry.getValue();
++
++ final ReferenceOpenHashSet