From 0acb7b08b72b5755dbd33ae08b549a6f4ebebff2 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sat, 11 Jan 2025 06:45:50 -0800 Subject: [PATCH] Split Minecraft patches into file patches --- ...ns.patch => 0001-Max-pending-logins.patch} | 0 .../features/0001-Threaded-Regions.patch | 19443 ---------------- ...k-system-throughput-counters-to-tps.patch} | 0 ...ates-in-non-loaded-or-non-owned-chu.patch} | 0 ...world-tile-entities-on-worldgen-thr.patch} | 0 ...-getHandle-and-overrides-perform-thr.patch | 45 - ...tion-to-player-position-on-player-d.patch} | 0 ...filer.patch => 0006-Region-profiler.patch} | 4 +- ...d.patch => 0007-Add-watchdog-thread.patch} | 0 ...access-when-waking-players-up-during.patch | 39 - .../moonrise/paper/PaperHooks.java.patch | 20 + .../util/BaseChunkSystemHooks.java.patch | 87 + .../level/entity/EntityLookup.java.patch | 32 + .../server/ServerEntityLookup.java.patch | 36 + .../RegionizedPlayerChunkLoader.java.patch | 20 + .../queue/ChunkUnloadQueue.java.patch | 42 + .../scheduling/ChunkHolderManager.java.patch | 391 + .../scheduling/ChunkTaskScheduler.java.patch | 75 + .../scheduling/NewChunkHolder.java.patch | 33 + .../collisions/CollisionUtil.java.patch | 11 + .../activation/ActivationRange.java.patch | 174 + .../redstone/RedstoneWireTurbo.java.patch | 19 + .../RegionShutdownThread.java.patch | 229 + .../threadedregions/RegionizedData.java.patch | 238 + .../RegionizedServer.java.patch | 458 + .../RegionizedTaskQueue.java.patch | 810 + .../RegionizedWorldData.java.patch | 773 + .../paper/threadedregions/Schedule.java.patch | 94 + .../threadedregions/TeleportUtils.java.patch | 73 + .../ThreadedRegionizer.java.patch | 1408 ++ .../paper/threadedregions/TickData.java.patch | 336 + .../TickRegionScheduler.java.patch | 567 + .../threadedregions/TickRegions.java.patch | 416 + .../commands/CommandServerHealth.java.patch | 358 + .../commands/CommandUtil.java.patch | 124 + .../scheduler/FoliaRegionScheduler.java.patch | 427 + .../SimpleThreadLocalRandomSource.java.patch | 82 + .../util/ThreadLocalRandomSource.java.patch | 76 + .../commands/CommandSourceStack.java.patch | 11 + .../minecraft/commands/Commands.java.patch | 112 + .../BoatDispenseItemBehavior.java.patch | 11 + .../DefaultDispenseItemBehavior.java.patch | 11 + .../dispenser/DispenseItemBehavior.java.patch | 149 + .../EquipmentDispenseItemBehavior.java.patch | 11 + .../MinecartDispenseItemBehavior.java.patch | 11 + .../ProjectileDispenseBehavior.java.patch | 11 + .../ShearsDispenseItemBehavior.java.patch | 11 + .../ShulkerBoxDispenseBehavior.java.patch | 11 + .../framework/GameTestHelper.java.patch | 11 + .../framework/GameTestServer.java.patch | 17 + .../minecraft/network/Connection.java.patch | 336 + .../network/protocol/PacketUtils.java.patch | 37 + .../server/MinecraftServer.java.patch | 779 + .../commands/AdvancementCommands.java.patch | 32 + .../commands/AttributeCommand.java.patch | 188 + .../ClearInventoryCommands.java.patch | 20 + .../server/commands/DamageCommand.java.patch | 35 + .../DefaultGameModeCommands.java.patch | 18 + .../server/commands/EffectCommands.java.patch | 44 + .../server/commands/EnchantCommand.java.patch | 100 + .../commands/ExperienceCommand.java.patch | 39 + .../commands/FillBiomeCommand.java.patch | 49 + .../server/commands/FillCommand.java.patch | 49 + .../commands/ForceLoadCommand.java.patch | 93 + .../commands/GameModeCommand.java.patch | 24 + .../server/commands/GiveCommand.java.patch | 47 + .../server/commands/KillCommand.java.patch | 13 + .../server/commands/PlaceCommand.java.patch | 134 + .../server/commands/RecipeCommand.java.patch | 30 + .../commands/SetBlockCommand.java.patch | 42 + .../commands/SetSpawnCommand.java.patch | 15 + .../server/commands/SummonCommand.java.patch | 26 + .../commands/TeleportCommand.java.patch | 47 + .../server/commands/TimeCommand.java.patch | 33 + .../server/commands/WeatherCommand.java.patch | 29 + .../commands/WorldBorderCommand.java.patch | 169 + .../dedicated/DedicatedServer.java.patch | 39 + .../server/level/ChunkMap.java.patch | 217 + .../server/level/DistanceManager.java.patch | 53 + .../server/level/ServerChunkCache.java.patch | 265 + .../level/ServerEntityGetter.java.patch | 32 + .../server/level/ServerLevel.java.patch | 1133 + .../server/level/ServerPlayer.java.patch | 550 + .../level/ServerPlayerGameMode.java.patch | 40 + .../server/level/TicketType.java.patch | 22 + .../server/level/WorldGenRegion.java.patch | 24 + .../ServerCommonPacketListenerImpl.java.patch | 89 + ...ConfigurationPacketListenerImpl.java.patch | 70 + .../ServerConnectionListener.java.patch | 28 + .../ServerGamePacketListenerImpl.java.patch | 396 + .../ServerLoginPacketListenerImpl.java.patch | 33 + .../server/players/BanListEntry.java.patch | 50 + .../players/OldUsersConverter.java.patch | 11 + .../server/players/PlayerList.java.patch | 419 + .../server/players/StoredUserList.java.patch | 29 + .../net/minecraft/util/SpawnUtil.java.patch | 11 + .../world/RandomSequences.java.patch | 83 + .../damagesource/CombatTracker.java.patch | 20 + .../damagesource/DamageSource.java.patch | 17 + .../damagesource/FallLocation.java.patch | 11 + .../minecraft/world/entity/Entity.java.patch | 1047 + .../world/entity/LivingEntity.java.patch | 148 + .../net/minecraft/world/entity/Mob.java.patch | 61 + .../world/entity/PortalProcessor.java.patch | 15 + .../world/entity/TamableAnimal.java.patch | 38 + .../world/entity/ai/Brain.java.patch | 21 + .../ai/behavior/PoiCompetitorScan.java.patch | 14 + .../entity/ai/goal/FollowOwnerGoal.java.patch | 11 + .../GroundPathNavigation.java.patch | 14 + .../ai/navigation/PathNavigation.java.patch | 34 + .../entity/ai/sensing/PlayerSensor.java.patch | 11 + .../ai/sensing/TemptingSensor.java.patch | 11 + .../entity/ai/village/VillageSiege.java.patch | 134 + .../ai/village/poi/PoiManager.java.patch | 39 + .../world/entity/animal/Bee.java.patch | 26 + .../world/entity/animal/Cat.java.patch | 11 + .../boss/enderdragon/EndCrystal.java.patch | 11 + .../entity/decoration/ItemFrame.java.patch | 12 + .../entity/item/FallingBlockEntity.java.patch | 11 + .../world/entity/item/ItemEntity.java.patch | 27 + .../world/entity/item/PrimedTnt.java.patch | 22 + .../world/entity/monster/Vex.java.patch | 11 + .../entity/monster/ZombieVillager.java.patch | 20 + .../entity/npc/AbstractVillager.java.patch | 22 + .../world/entity/npc/CatSpawner.java.patch | 26 + .../world/entity/npc/Villager.java.patch | 28 + .../npc/WanderingTraderSpawner.java.patch | 90 + .../world/entity/player/Player.java.patch | 17 + .../projectile/AbstractArrow.java.patch | 14 + .../AbstractHurtingProjectile.java.patch | 14 + .../FireworkRocketEntity.java.patch | 14 + .../entity/projectile/FishingHook.java.patch | 50 + .../entity/projectile/LlamaSpit.java.patch | 14 + .../entity/projectile/Projectile.java.patch | 87 + .../projectile/SmallFireball.java.patch | 11 + .../projectile/ThrowableProjectile.java.patch | 14 + .../projectile/ThrownEnderpearl.java.patch | 122 + .../world/entity/raid/Raid.java.patch} | 46 +- .../world/entity/raid/Raider.java.patch | 11 + .../world/entity/raid/Raids.java.patch | 119 + .../vehicle/MinecartCommandBlock.java.patch | 14 + .../entity/vehicle/MinecartHopper.java.patch | 11 + .../minecraft/world/item/ItemStack.java.patch | 128 + .../minecraft/world/item/MapItem.java.patch | 61 + .../minecraft/world/item/SignItem.java.patch | 20 + .../component/LodestoneTracker.java.patch} | 14 +- .../world/level/BaseCommandBlock.java.patch | 35 + .../world/level/EntityGetter.java.patch | 61 + .../minecraft/world/level/Level.java.patch | 404 + .../world/level/LevelAccessor.java.patch | 29 + .../world/level/LevelReader.java.patch | 28 + .../world/level/NaturalSpawner.java.patch | 11 + .../world/level/ServerExplosion.java.patch | 24 + .../level/ServerLevelAccessor.java.patch | 15 + .../world/level/StructureManager.java.patch | 48 + .../world/level/block/BedBlock.java.patch | 11 + .../world/level/block/Block.java.patch | 13 + .../world/level/block/BushBlock.java.patch | 11 + .../block/DaylightDetectorBlock.java.patch | 11 + .../level/block/DispenserBlock.java.patch | 20 + .../level/block/DoublePlantBlock.java.patch | 11 + .../level/block/EndGatewayBlock.java.patch | 50 + .../level/block/EndPortalBlock.java.patch | 32 + .../world/level/block/FarmBlock.java.patch | 13 + .../world/level/block/FungusBlock.java.patch | 14 + .../world/level/block/HoneyBlock.java.patch | 11 + .../level/block/LightningRodBlock.java.patch | 11 + .../level/block/MushroomBlock.java.patch | 11 + .../level/block/NetherPortalBlock.java.patch | 62 + .../world/level/block/Portal.java.patch | 13 + .../level/block/RedStoneWireBlock.java.patch | 80 + .../level/block/RedstoneTorchBlock.java.patch | 53 + .../world/level/block/SaplingBlock.java.patch | 39 + .../block/SpreadingSnowyDirtBlock.java.patch | 11 + .../level/block/WitherSkullBlock.java.patch | 11 + .../block/entity/BeaconBlockEntity.java.patch | 20 + .../level/block/entity/BlockEntity.java.patch | 33 + .../entity/CommandBlockEntity.java.patch | 16 + .../entity/ConduitBlockEntity.java.patch | 20 + .../block/entity/HopperBlockEntity.java.patch | 150 + .../SculkCatalystBlockEntity.java.patch | 14 + .../TheEndGatewayBlockEntity.java.patch | 246 + .../entity/TickingBlockEntity.java.patch | 9 + .../level/block/grower/TreeGrower.java.patch | 84 + .../block/piston/PistonBaseBlock.java.patch | 11 + .../piston/PistonMovingBlockEntity.java.patch | 43 + .../world/level/border/WorldBorder.java.patch | 31 + .../level/chunk/ChunkGenerator.java.patch | 11 + .../world/level/chunk/LevelChunk.java.patch | 117 + .../storage/SerializableChunkData.java.patch | 11 + .../dimension/end/EndDragonFight.java.patch | 65 + .../level/levelgen/PatrolSpawner.java.patch | 54 + .../level/levelgen/PhantomSpawner.java.patch | 38 + .../feature/EndPlatformFeature.java.patch | 11 + .../structure/StructureStart.java.patch | 63 + .../CollectingNeighborUpdater.java.patch | 10 + .../level/saveddata/SavedData.java.patch | 23 + .../level/saveddata/maps/MapIndex.java.patch | 24 + .../maps/MapItemSavedData.java.patch | 172 + .../storage/DimensionDataStorage.java.patch | 42 + .../world/ticks/LevelChunkTicks.java.patch | 24 + .../world/ticks/LevelTicks.java.patch | 102 + 202 files changed, 19115 insertions(+), 19555 deletions(-) rename folia-server/minecraft-patches/features/{0002-Max-pending-logins.patch => 0001-Max-pending-logins.patch} (100%) delete mode 100644 folia-server/minecraft-patches/features/0001-Threaded-Regions.patch rename folia-server/minecraft-patches/features/{0003-Add-chunk-system-throughput-counters-to-tps.patch => 0002-Add-chunk-system-throughput-counters-to-tps.patch} (100%) rename folia-server/minecraft-patches/features/{0005-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch => 0003-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch} (100%) rename folia-server/minecraft-patches/features/{0006-Block-reading-in-world-tile-entities-on-worldgen-thr.patch => 0004-Block-reading-in-world-tile-entities-on-worldgen-thr.patch} (100%) delete mode 100644 folia-server/minecraft-patches/features/0004-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch rename folia-server/minecraft-patches/features/{0010-Sync-vehicle-position-to-player-position-on-player-d.patch => 0005-Sync-vehicle-position-to-player-position-on-player-d.patch} (100%) rename folia-server/minecraft-patches/features/{0011-Region-profiler.patch => 0006-Region-profiler.patch} (99%) rename folia-server/minecraft-patches/features/{0012-Add-watchdog-thread.patch => 0007-Add-watchdog-thread.patch} (100%) delete mode 100644 folia-server/minecraft-patches/features/0007-Skip-worldstate-access-when-waking-players-up-during.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/PaperHooks.java.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java.patch create mode 100644 folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/redstone/RedstoneWireTurbo.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionShutdownThread.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedData.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedServer.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedTaskQueue.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedWorldData.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/Schedule.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TeleportUtils.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/ThreadedRegionizer.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickData.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegionScheduler.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegions.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandServerHealth.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandUtil.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java.patch create mode 100644 folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/commands/CommandSourceStack.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/commands/Commands.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestHelper.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestServer.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/network/protocol/PacketUtils.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/AdvancementCommands.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/AttributeCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/ClearInventoryCommands.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/DamageCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/DefaultGameModeCommands.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/EffectCommands.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/EnchantCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/ExperienceCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillBiomeCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/ForceLoadCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/GameModeCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/GiveCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/KillCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/PlaceCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/RecipeCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetBlockCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetSpawnCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/SummonCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/TeleportCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/TimeCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/WeatherCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/commands/WorldBorderCommand.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ChunkMap.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/DistanceManager.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerEntityGetter.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayerGameMode.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/TicketType.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/level/WorldGenRegion.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/BanListEntry.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/PlayerList.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/server/players/StoredUserList.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/util/SpawnUtil.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/RandomSequences.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/CombatTracker.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/DamageSource.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/FallLocation.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/LivingEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/Mob.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/PortalProcessor.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/TamableAnimal.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/Brain.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/PlayerSensor.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/VillageSiege.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/poi/PoiManager.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Bee.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Cat.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/decoration/ItemFrame.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/FallingBlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/ItemEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/PrimedTnt.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/Vex.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/ZombieVillager.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/AbstractVillager.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/CatSpawner.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/Villager.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractArrow.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FishingHook.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/LlamaSpit.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/Projectile.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/SmallFireball.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch rename folia-server/minecraft-patches/{features/0009-Fix-off-region-raid-heroes.patch => sources/net/minecraft/world/entity/raid/Raid.java.patch} (58%) create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raider.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raids.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartHopper.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/ItemStack.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/MapItem.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/item/SignItem.java.patch rename folia-server/minecraft-patches/{features/0008-Do-not-access-POI-data-for-lodestone-compass.patch => sources/net/minecraft/world/item/component/LodestoneTracker.java.patch} (57%) create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/BaseCommandBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelAccessor.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelReader.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/NaturalSpawner.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerExplosion.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerLevelAccessor.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/StructureManager.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BedBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Block.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BushBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DispenserBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DoublePlantBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndGatewayBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndPortalBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FarmBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FungusBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/HoneyBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/LightningRodBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/MushroomBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/NetherPortalBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Portal.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedStoneWireBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SaplingBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TickingBlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/border/WorldBorder.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/ChunkGenerator.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/LevelChunk.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/SavedData.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapIndex.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/level/storage/DimensionDataStorage.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelChunkTicks.java.patch create mode 100644 folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelTicks.java.patch diff --git a/folia-server/minecraft-patches/features/0002-Max-pending-logins.patch b/folia-server/minecraft-patches/features/0001-Max-pending-logins.patch similarity index 100% rename from folia-server/minecraft-patches/features/0002-Max-pending-logins.patch rename to folia-server/minecraft-patches/features/0001-Max-pending-logins.patch diff --git a/folia-server/minecraft-patches/features/0001-Threaded-Regions.patch b/folia-server/minecraft-patches/features/0001-Threaded-Regions.patch deleted file mode 100644 index b42ccbe..0000000 --- a/folia-server/minecraft-patches/features/0001-Threaded-Regions.patch +++ /dev/null @@ -1,19443 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 2 Oct 2022 21:28:53 -0700 -Subject: [PATCH] Threaded Regions - -See https://docs.papermc.io/folia/reference/overview and -https://docs.papermc.io/folia/reference/region-logic - -diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java -index 4d344559a20a0c35c181e297e81788c747363ec9..e799155d2ccbfe9dd3e5a87c6b6c28278e9accce 100644 ---- a/ca/spottedleaf/moonrise/paper/PaperHooks.java -+++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java -@@ -105,7 +105,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo - } - - for (final EnderDragonPart part : parts) { -- if (part != entity && part.getBoundingBox().intersects(boundingBox) && (predicate == null || predicate.test(part))) { -+ if (part != entity && part.getBoundingBox().intersects(boundingBox) && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(part) && (predicate == null || predicate.test(part))) { // Folia - region threading - into.add(part); - } - } -@@ -127,7 +127,7 @@ public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHo - continue; - } - final T casted = (T)entityTypeTest.tryCast(part); -- if (casted != null && (predicate == null || predicate.test(casted))) { -+ if (casted != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(part) && (predicate == null || predicate.test(casted))) { // Folia - region threading - into.add(casted); - if (into.size() >= maxCount) { - break; -diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java -index ece1261b67033e946dfc20a96872708755bffe0a..58986c62c485cae379180ba95ba0ea60145ef73f 100644 ---- a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java -+++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java -@@ -80,18 +80,23 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co - - @Override - public void onChunkHolderCreate(final ServerLevel level, final ChunkHolder holder) { -- -+ // Folia start - threaded regions -+ level.regioniser.addChunk(holder.getPos().x, holder.getPos().z); -+ // Folia end - threaded regions - } - - @Override - public void onChunkHolderDelete(final ServerLevel level, final ChunkHolder holder) { - // Update progress listener for LevelLoadingScreen -- final net.minecraft.server.level.progress.ChunkProgressListener progressListener = level.getChunkSource().chunkMap.progressListener; -+ final net.minecraft.server.level.progress.ChunkProgressListener progressListener = null; // Folia - threaded regions - cannot schedule chunk task here; as it would create a chunkholder - if (progressListener != null) { - this.scheduleChunkTask(level, holder.getPos().x, holder.getPos().z, () -> { - progressListener.onStatusChange(holder.getPos(), null); - }); - } -+ // Folia start - threaded regions -+ level.regioniser.removeChunk(holder.getPos().x, holder.getPos().z); -+ // Folia end - threaded regions - } - - @Override -@@ -102,17 +107,13 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co - - @Override - public void onChunkBorder(final LevelChunk chunk, final ChunkHolder holder) { -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().add( -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -- ); -+ chunk.getLevel().getCurrentWorldData().addChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading - chunk.loadCallback(); - } - - @Override - public void onChunkNotBorder(final LevelChunk chunk, final ChunkHolder holder) { -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getLoadedChunks().remove( -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -- ); -+ chunk.getLevel().getCurrentWorldData().removeChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading - chunk.unloadCallback(); - } - -@@ -124,9 +125,7 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co - - @Override - public void onChunkTicking(final LevelChunk chunk, final ChunkHolder holder) { -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().add( -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -- ); -+ chunk.getLevel().getCurrentWorldData().addTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading - if (!((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$isPostProcessingDone()) { - chunk.postProcessGeneration((ServerLevel)chunk.getLevel()); - } -@@ -137,24 +136,18 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co - - @Override - public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove( -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -- ); -+ chunk.getLevel().getCurrentWorldData().removeTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading - ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration - } - - @Override - public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add( -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -- ); -+ chunk.getLevel().getCurrentWorldData().addEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading - } - - @Override - public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove( -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() -- ); -+ chunk.getLevel().getCurrentWorldData().removeEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading - } - - @Override -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java -index 7554c109c35397bc1a43dd80e87764fd78645bbf..db16fe8d664f9b04710200d63439564cb97c0066 100644 ---- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java -@@ -460,6 +460,19 @@ public abstract class EntityLookup implements LevelEntityGetter { - return slices == null || !slices.isPreventingStatusUpdates(); - } - -+ // Folia start - region threading -+ // only appropriate to use when in shutdown, as this performs no logic hooks to properly add to world -+ public boolean addEntityForShutdownTeleportComplete(final Entity entity) { -+ final BlockPos pos = entity.blockPosition(); -+ final int sectionX = pos.getX() >> 4; -+ final int sectionY = Mth.clamp(pos.getY() >> 4, WorldUtil.getMinSection(this.world), WorldUtil.getMaxSection(this.world)); -+ final int sectionZ = pos.getZ() >> 4; -+ final ChunkEntitySlices slices = this.getOrCreateChunk(sectionX, sectionZ); -+ -+ return slices.addEntity(entity, sectionY); -+ } -+ // Folia end - region threading -+ - protected void removeEntity(final Entity entity) { - final int sectionX = ((ChunkSystemEntity)entity).moonrise$getSectionX(); - final int sectionY = ((ChunkSystemEntity)entity).moonrise$getSectionY(); -@@ -986,6 +999,9 @@ public abstract class EntityLookup implements LevelEntityGetter { - EntityLookup.this.removeEntityCallback(entity); - - this.entity.setLevelCallback(NoOpCallback.INSTANCE); -+ -+ // only AFTER full removal callbacks, so that thread checking will work. // Folia - region threading -+ EntityLookup.this.world.getCurrentWorldData().removeEntity(entity); // Folia - region threading - } - } - -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java -index 26207443b1223119c03db478d7e816d9cdf8e618..a89ee24c6aed46af23c5de7ae2234c7902f1c0f4 100644 ---- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java -@@ -17,7 +17,7 @@ public final class ServerEntityLookup extends EntityLookup { - private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0]; - - private final ServerLevel serverWorld; -- public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker -+ // Folia - move to regionized world data - - public ServerEntityLookup(final ServerLevel world, final LevelCallback worldCallback) { - super(world, worldCallback); -@@ -75,6 +75,7 @@ public final class ServerEntityLookup extends EntityLookup { - if (entity instanceof ServerPlayer player) { - ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player); - } -+ this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading - } - - @Override -@@ -87,14 +88,14 @@ public final class ServerEntityLookup extends EntityLookup { - @Override - protected void entityStartLoaded(final Entity entity) { - // Moonrise start - entity tracker -- this.trackerEntities.add(entity); -+ this.world.getCurrentWorldData().trackerEntities.add(entity); // Folia - region threading - // Moonrise end - entity tracker - } - - @Override - protected void entityEndLoaded(final Entity entity) { - // Moonrise start - entity tracker -- this.trackerEntities.remove(entity); -+ this.world.getCurrentWorldData().trackerEntities.remove(entity); // Folia - region threading - // Moonrise end - entity tracker - } - -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -index dd2509996bfd08e8c3f9f2be042229eac6d7692d..f77dcf5a42ff34a1624ddf16bcce2abee81194bb 100644 ---- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java -@@ -216,7 +216,7 @@ public final class RegionizedPlayerChunkLoader { - final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); - - if (loader == null) { -- return; -+ throw new IllegalStateException("Player is already removed from player chunk loader"); // Folia - region threading - } - - loader.remove(); -@@ -304,7 +304,7 @@ public final class RegionizedPlayerChunkLoader { - public void tick() { - TickThread.ensureTickThread("Cannot tick player chunk loader async"); - long currTime = System.nanoTime(); -- for (final ServerPlayer player : new java.util.ArrayList<>(this.world.players())) { -+ for (final ServerPlayer player : new java.util.ArrayList<>(this.world.getLocalPlayers())) { // Folia - region threding - final PlayerChunkLoaderData loader = ((ChunkSystemServerPlayer)player).moonrise$getChunkLoader(); - if (loader == null || loader.removed || loader.world != this.world) { - // not our problem anymore -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java -index 7eafc5b7cba23d8dec92ecc1050afe3fd8c9e309..4bfcae47ed76346e6200514ebce5b04f907c5026 100644 ---- a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java -@@ -29,6 +29,39 @@ public final class ChunkUnloadQueue { - - public static record SectionToUnload(int sectionX, int sectionZ, long order, int count) {} - -+ // Folia start - threaded regions -+ public List retrieveForCurrentRegion() { -+ final io.papermc.paper.threadedregions.ThreadedRegionizer.ThreadedRegion region = -+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion(); -+ final io.papermc.paper.threadedregions.ThreadedRegionizer regionizer = region.regioniser; -+ final int shift = this.coordinateShift; -+ -+ final List ret = new ArrayList<>(); -+ -+ for (final Iterator> iterator = this.unloadSections.entryIterator(); iterator.hasNext();) { -+ final ConcurrentLong2ReferenceChainedHashTable.TableEntry entry = iterator.next(); -+ final long key = entry.getKey(); -+ final UnloadSection section = entry.getValue(); -+ final int sectionX = CoordinateUtils.getChunkX(key); -+ final int sectionZ = CoordinateUtils.getChunkZ(key); -+ final int chunkX = sectionX << shift; -+ final int chunkZ = sectionZ << shift; -+ -+ if (regionizer.getRegionAtUnsynchronised(chunkX, chunkZ) != region) { -+ continue; -+ } -+ -+ ret.add(new SectionToUnload(sectionX, sectionZ, section.order, section.chunks.size())); -+ } -+ -+ ret.sort((final SectionToUnload s1, final SectionToUnload s2) -> { -+ return Long.compare(s1.order, s2.order); -+ }); -+ -+ return ret; -+ } -+ // Folia end - threaded regions -+ - public List retrieveForAllRegions() { - final List ret = new ArrayList<>(); - -@@ -141,4 +174,4 @@ public final class ChunkUnloadQueue { - this.order = order; - } - } --} -\ No newline at end of file -+} -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java -index b5817aa8f537593f6d9fc6b612c82ccccb250ac7..aae97116a22a87cffd4756d566da3acd96ce2ae0 100644 ---- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java -@@ -56,6 +56,14 @@ import java.util.concurrent.atomic.AtomicReference; - import java.util.concurrent.locks.LockSupport; - import java.util.function.Predicate; - -+// Folia start - region threading -+import io.papermc.paper.threadedregions.RegionizedServer; -+import io.papermc.paper.threadedregions.ThreadedRegionizer; -+import io.papermc.paper.threadedregions.TickRegionScheduler; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -+// Folia end - region threading -+ - public final class ChunkHolderManager { - - private static final Logger LOGGER = LogUtils.getClassLogger(); -@@ -78,29 +86,83 @@ public final class ChunkHolderManager { - private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); - private final ServerLevel world; - private final ChunkTaskScheduler taskScheduler; -- private long currentTick; -+ // Folia start - region threading -+ public static final class HolderManagerRegionData { -+ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); -+ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { -+ if (c1 == c2) { -+ return 0; -+ } - -- private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); -- private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { -- if (c1 == c2) { -- return 0; -+ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); -+ -+ if (saveTickCompare != 0) { -+ return saveTickCompare; -+ } -+ -+ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); -+ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); -+ -+ if (coord1 == coord2) { -+ throw new IllegalStateException("Duplicate chunkholder in auto save queue"); -+ } -+ -+ return Long.compare(coord1, coord2); -+ }); -+ -+ public void merge(final HolderManagerRegionData into, final long tickOffset) { -+ // Order doesn't really matter for the pending full update... -+ into.pendingFullLoadUpdate.addAll(this.pendingFullLoadUpdate); -+ -+ // We need to copy the set to iterate over, because modifying the field used in compareTo while iterating -+ // will destroy the result from compareTo (However, the set is not destroyed _after_ iteration because a constant -+ // addition to every entry will not affect compareTo). -+ for (final NewChunkHolder holder : new ArrayList<>(this.autoSaveQueue)) { -+ holder.lastAutoSave += tickOffset; -+ into.autoSaveQueue.add(holder); -+ } - } - -- final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); -+ public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, -+ final ReferenceOpenHashSet dataSet) { -+ for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) { -+ final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift; -+ final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift; -+ -+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); -+ if (data != null) { -+ data.pendingFullLoadUpdate.add(fullLoadUpdate); -+ } // else: fullLoadUpdate is an unloaded chunk holder -+ } - -- if (saveTickCompare != 0) { -- return saveTickCompare; -+ for (final NewChunkHolder autoSave : this.autoSaveQueue) { -+ final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift; -+ final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift; -+ -+ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); -+ if (data != null) { -+ data.autoSaveQueue.add(autoSave); -+ } // else: autoSave is an unloaded chunk holder -+ } - } -+ } - -- final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); -- final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); -+ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() { -+ final ThreadedRegionizer.ThreadedRegion region = -+ TickRegionScheduler.getCurrentRegion(); - -- if (coord1 == coord2) { -- throw new IllegalStateException("Duplicate chunkholder in auto save queue"); -+ if (region == null) { -+ return null; - } - -- return Long.compare(coord1, coord2); -- }); -+ if (this.world != null && this.world != region.getData().world) { -+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); -+ } -+ -+ return region.getData().getHolderManagerRegionData(); -+ } -+ // Folia end - region threading -+ - - public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { - this.world = world; -@@ -185,8 +247,13 @@ public final class ChunkHolderManager { - } - - public void close(final boolean save, final boolean halt) { -+ // Folia start - region threading -+ this.close(save, halt, true, true, true); -+ } -+ public void close(final boolean save, final boolean halt, final boolean first, final boolean last, final boolean checkRegions) { -+ // Folia end - region threading - TickThread.ensureTickThread("Closing world off-main"); -- if (halt) { -+ if (first && halt) { // Folia - region threading - LOGGER.info("Waiting 60s for chunk system to halt for world '" + WorldUtil.getWorldName(this.world) + "'"); - if (!this.taskScheduler.halt(true, TimeUnit.SECONDS.toNanos(60L))) { - LOGGER.warn("Failed to halt generation/loading tasks for world '" + WorldUtil.getWorldName(this.world) + "'"); -@@ -196,9 +263,10 @@ public final class ChunkHolderManager { - } - - if (save) { -- this.saveAllChunks(true, true, true); -+ this.saveAllChunksRegionised(true, true, true, first, last, checkRegions); // Folia - region threading - } - -+ if (last) { // Folia - region threading - MoonriseRegionFileIO.flush(this.world); - - if (halt) { -@@ -220,28 +288,35 @@ public final class ChunkHolderManager { - } - - this.taskScheduler.setShutdown(true); -+ } // Folia - region threading - } - - void ensureInAutosave(final NewChunkHolder holder) { -- if (!this.autoSaveQueue.contains(holder)) { -- holder.lastAutoSave = this.currentTick; -- this.autoSaveQueue.add(holder); -+ // Folia start - region threading -+ final HolderManagerRegionData regionData = this.getCurrentRegionData(); -+ if (!regionData.autoSaveQueue.contains(holder)) { -+ holder.lastAutoSave = RegionizedServer.getCurrentTick(); -+ regionData.autoSaveQueue.add(holder); -+ // Folia end - region threading - } - } - - public void autoSave() { - final List reschedule = new ArrayList<>(); -- final long currentTick = this.currentTick; -+ final long currentTick = RegionizedServer.getCurrentTick(); // Folia - region threading - final long maxSaveTime = currentTick - Math.max(1L, PlatformHooks.get().configAutoSaveInterval(this.world)); - final int maxToSave = PlatformHooks.get().configMaxAutoSavePerTick(this.world); -- for (int autoSaved = 0; autoSaved < maxToSave && !this.autoSaveQueue.isEmpty();) { -- final NewChunkHolder holder = this.autoSaveQueue.first(); -+ // Folia start - region threading -+ final HolderManagerRegionData regionData = this.getCurrentRegionData(); -+ for (int autoSaved = 0; autoSaved < maxToSave && !regionData.autoSaveQueue.isEmpty();) { -+ final NewChunkHolder holder = regionData.autoSaveQueue.first(); -+ // Folia end - region threading - - if (holder.lastAutoSave > maxSaveTime) { - break; - } - -- this.autoSaveQueue.remove(holder); -+ regionData.autoSaveQueue.remove(holder); // Folia - region threading - - holder.lastAutoSave = currentTick; - if (holder.save(false) != null) { -@@ -255,15 +330,38 @@ public final class ChunkHolderManager { - - for (final NewChunkHolder holder : reschedule) { - if (holder.getChunkStatus().isOrAfter(FullChunkStatus.FULL)) { -- this.autoSaveQueue.add(holder); -+ regionData.autoSaveQueue.add(holder); // Folia start - region threading - } - } - } - - public void saveAllChunks(final boolean flush, final boolean shutdown, final boolean logProgress) { -- final List holders = this.getChunkHolders(); -+ // Folia start - region threading -+ this.saveAllChunksRegionised(flush, shutdown, logProgress, true, true, true); -+ } -+ public void saveAllChunksRegionised(final boolean flush, final boolean shutdown, final boolean logProgress, final boolean first, final boolean last, final boolean checkRegion) { -+ final List holders = new java.util.ArrayList<>(this.chunkHolders.size() / 10); -+ // we could iterate through all chunk holders with thread checks, however for many regions the iteration cost alone -+ // will multiply. to avoid this, we can simply iterate through all owned sections -+ final int regionShift = this.world.moonrise$getRegionChunkShift(); -+ final int width = 1 << regionShift; -+ for (final LongIterator iterator = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getOwnedSectionsUnsynchronised(); iterator.hasNext();) { -+ final long sectionKey = iterator.nextLong(); -+ final int offsetX = CoordinateUtils.getChunkX(sectionKey) << regionShift; -+ final int offsetZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift; -+ -+ for (int dz = 0; dz < width; ++dz) { -+ for (int dx = 0; dx < width; ++dx) { -+ final NewChunkHolder holder = this.getChunkHolder(offsetX | dx, offsetZ | dz); -+ if (holder != null) { -+ holders.add(holder); -+ } -+ } -+ } -+ } -+ // Folia end - region threading - -- if (logProgress) { -+ if (first && logProgress) { // Folia - region threading - LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'"); - } - -@@ -292,6 +390,12 @@ public final class ChunkHolderManager { - } - for (int i = 0, len = holders.size(); i < len; ++i) { - final NewChunkHolder holder = holders.get(i); -+ // Folia start - region threading -+ if (!checkRegion && !TickThread.isTickThreadFor(this.world, holder.chunkX, holder.chunkZ)) { -+ // skip holders that would fail the thread check -+ continue; -+ } -+ // Folia end - region threading - try { - final NewChunkHolder.SaveStat saveStat = holder.save(shutdown); - if (saveStat != null) { -@@ -327,7 +431,7 @@ public final class ChunkHolderManager { - } - } - } -- if (flush) { -+ if (last && flush) { // Folia - region threading - MoonriseRegionFileIO.flush(this.world); - try { - MoonriseRegionFileIO.flushRegionStorages(this.world); -@@ -732,7 +836,13 @@ public final class ChunkHolderManager { - } - - public void tick() { -- ++this.currentTick; -+ // Folia start - region threading -+ final ThreadedRegionizer.ThreadedRegion region = -+ TickRegionScheduler.getCurrentRegion(); -+ if (region == null) { -+ throw new IllegalStateException("Not running tick() while on a region"); -+ } -+ // Folia end - region threading - - final int sectionShift = ((ChunkSystemServerLevel)this.world).moonrise$getRegionChunkShift(); - -@@ -746,7 +856,7 @@ public final class ChunkHolderManager { - return removeDelay <= 0L; - }; - -- for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) { -+ for (final LongIterator iterator = region.getOwnedSectionsUnsynchronised(); iterator.hasNext();) { - final long sectionKey = iterator.nextLong(); - - if (!this.sectionToChunkToExpireCount.containsKey(sectionKey)) { -@@ -1031,26 +1141,56 @@ public final class ChunkHolderManager { - if (changedFullStatus.isEmpty()) { - return; - } -- if (!TickThread.isTickThread()) { -- this.taskScheduler.scheduleChunkTask(() -> { -- final ArrayDeque pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate; -- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { -- pendingFullLoadUpdate.add(changedFullStatus.get(i)); -- } - -- ChunkHolderManager.this.processPendingFullUpdate(); -- }, Priority.HIGHEST); -- } else { -- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; -- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { -- pendingFullLoadUpdate.add(changedFullStatus.get(i)); -+ // Folia start - region threading -+ final Long2ObjectOpenHashMap> sectionToUpdates = new Long2ObjectOpenHashMap<>(); -+ final List thisRegionHolders = new ArrayList<>(); -+ -+ final int regionShift = this.world.moonrise$getRegionChunkShift(); -+ final ThreadedRegionizer.ThreadedRegion thisRegion -+ = TickRegionScheduler.getCurrentRegion(); -+ -+ for (final NewChunkHolder holder : changedFullStatus) { -+ final int regionX = holder.chunkX >> regionShift; -+ final int regionZ = holder.chunkZ >> regionShift; -+ final long holderSectionKey = CoordinateUtils.getChunkKey(regionX, regionZ); -+ -+ // region may be null -+ if (thisRegion != null && this.world.regioniser.getRegionAtUnsynchronised(holder.chunkX, holder.chunkZ) == thisRegion) { -+ thisRegionHolders.add(holder); -+ } else { -+ sectionToUpdates.computeIfAbsent(holderSectionKey, (final long keyInMap) -> { -+ return new ArrayList<>(); -+ }).add(holder); -+ } -+ } -+ if (!thisRegionHolders.isEmpty()) { -+ thisRegion.getData().getHolderManagerRegionData().pendingFullLoadUpdate.addAll(thisRegionHolders); -+ } -+ -+ if (!sectionToUpdates.isEmpty()) { -+ for (final Iterator>> iterator = sectionToUpdates.long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry> entry = iterator.next(); -+ final long sectionKey = entry.getLongKey(); -+ -+ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << regionShift; -+ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << regionShift; -+ -+ final List regionHolders = entry.getValue(); -+ this.taskScheduler.scheduleChunkTaskEventually(chunkX, chunkZ, () -> { -+ ChunkHolderManager.this.getCurrentRegionData().pendingFullLoadUpdate.addAll(regionHolders); -+ ChunkHolderManager.this.processPendingFullUpdate(); -+ }, Priority.HIGHEST); -+ - } - } -+ // Folia end - region threading - } - - private void removeChunkHolder(final NewChunkHolder holder) { - holder.onUnload(); -- this.autoSaveQueue.remove(holder); -+ this.getCurrentRegionData().autoSaveQueue.remove(holder); // Folia - region threading - PlatformHooks.get().onChunkHolderDelete(this.world, holder.vanillaChunkHolder); - this.chunkHolders.remove(CoordinateUtils.getChunkKey(holder.chunkX, holder.chunkZ)); - } -@@ -1063,7 +1203,7 @@ public final class ChunkHolderManager { - throw new IllegalStateException("Cannot unload chunks recursively"); - } - final int sectionShift = this.unloadQueue.coordinateShift; // sectionShift <= lock shift -- final List unloadSectionsForRegion = this.unloadQueue.retrieveForAllRegions(); -+ final List unloadSectionsForRegion = this.unloadQueue.retrieveForCurrentRegion(); // Folia - threaded regions - int unloadCountTentative = 0; - for (final ChunkUnloadQueue.SectionToUnload sectionRef : unloadSectionsForRegion) { - final ChunkUnloadQueue.UnloadSection section -@@ -1381,7 +1521,13 @@ public final class ChunkHolderManager { - - // only call on tick thread - private boolean processPendingFullUpdate() { -- final ArrayDeque pendingFullLoadUpdate = this.pendingFullLoadUpdate; -+ // Folia start - region threading -+ final HolderManagerRegionData data = this.getCurrentRegionData(); -+ if (data == null) { -+ return false; -+ } -+ final ArrayDeque pendingFullLoadUpdate = data.pendingFullLoadUpdate; -+ // Folia end - region threading - - boolean ret = false; - -@@ -1392,9 +1538,7 @@ public final class ChunkHolderManager { - ret |= holder.handleFullStatusChange(changedFullStatus); - - if (!changedFullStatus.isEmpty()) { -- for (int i = 0, len = changedFullStatus.size(); i < len; ++i) { -- pendingFullLoadUpdate.add(changedFullStatus.get(i)); -- } -+ this.addChangedStatuses(changedFullStatus); // Folia - region threading - changedFullStatus.clear(); - } - } -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java -index 67532b85073b7978254a0b04caadfe822679e61f..cba2d16c0cb5adc92952990ef95b1c979eafd40f 100644 ---- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java -@@ -122,7 +122,7 @@ public final class ChunkTaskScheduler { - public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor compressionExecutor; - public final PrioritisedThreadPool.ExecutorGroup.ThreadPoolExecutor saveExecutor; - -- private final PrioritisedTaskQueue mainThreadExecutor = new PrioritisedTaskQueue(); -+ // Folia - regionised ticking - - public final ChunkHolderManager chunkHolderManager; - -@@ -337,14 +337,13 @@ public final class ChunkTaskScheduler { - }; - - // this may not be good enough, specifically thanks to stupid ass plugins swallowing exceptions -- this.scheduleChunkTask(chunkX, chunkZ, crash, Priority.BLOCKING); -+ this.scheduleChunkTaskEventually(chunkX, chunkZ, crash, Priority.BLOCKING); // Folia - region threading - // so, make the main thread pick it up - ((ChunkSystemMinecraftServer)this.world.getServer()).moonrise$setChunkSystemCrash(new RuntimeException("Chunk system crash propagated from unrecoverableChunkSystemFailure", reportedException)); - } - - public boolean executeMainThreadTask() { -- TickThread.ensureTickThread("Cannot execute main thread task off-main"); -- return this.mainThreadExecutor.executeTask(); -+ throw new UnsupportedOperationException("Use regionised ticking hooks"); // Folia - regionised ticking - } - - public void raisePriority(final int x, final int z, final Priority priority) { -@@ -829,7 +828,7 @@ public final class ChunkTaskScheduler { - */ - @Deprecated - public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final Runnable run, final Priority priority) { -- return this.mainThreadExecutor.queueTask(run, priority); -+ throw new UnsupportedOperationException(); // Folia - regionised ticking - } - - public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run) { -@@ -838,7 +837,7 @@ public final class ChunkTaskScheduler { - - public PrioritisedExecutor.PrioritisedTask createChunkTask(final int chunkX, final int chunkZ, final Runnable run, - final Priority priority) { -- return this.mainThreadExecutor.createTask(run, priority); -+ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.createChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking - } - - public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run) { -@@ -847,9 +846,27 @@ public final class ChunkTaskScheduler { - - public PrioritisedExecutor.PrioritisedTask scheduleChunkTask(final int chunkX, final int chunkZ, final Runnable run, - final Priority priority) { -- return this.mainThreadExecutor.queueTask(run, priority); -+ return io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(this.world, chunkX, chunkZ, run, priority); // Folia - regionised ticking - } - -+ // Folia start - region threading -+ // this function is guaranteed to never touch the ticket lock or schedule lock -+ // yes, this IS a hack so that we can avoid deadlock due to region threading introducing the -+ // ticket lock in the schedule logic -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run) { -+ return this.scheduleChunkTaskEventually(chunkX, chunkZ, run, Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask scheduleChunkTaskEventually(final int chunkX, final int chunkZ, final Runnable run, -+ final Priority priority) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(chunkX, chunkZ, run, priority); -+ this.world.taskQueueRegionData.pushGlobalChunkTask(() -> { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask(ChunkTaskScheduler.this.world, chunkX, chunkZ, run, priority); -+ }); -+ return ret; -+ } -+ // Folia end - region threading -+ - public boolean halt(final boolean sync, final long maxWaitNS) { - this.radiusAwareGenExecutor.halt(); - this.parallelGenExecutor.halt(); -diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java -index e4a5fa25ed368fc4662c30934da2963ef446d782..601ed36413bbbf9c17e530b42906986e441237fd 100644 ---- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java -+++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java -@@ -1359,10 +1359,10 @@ public final class NewChunkHolder { - private void completeStatusConsumers(ChunkStatus status, final ChunkAccess chunk) { - // Update progress listener for LevelLoadingScreen - if (chunk != null) { -- final ChunkProgressListener progressListener = this.world.getChunkSource().chunkMap.progressListener; -+ final ChunkProgressListener progressListener = null; // Folia - threaded regions - if (progressListener != null) { - final ChunkStatus finalStatus = status; -- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { -+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - threaded regions - progressListener.onStatusChange(this.vanillaChunkHolder.getPos(), finalStatus); - }); - } -@@ -1383,7 +1383,7 @@ public final class NewChunkHolder { - } - - // must be scheduled to main, we do not trust the callback to not do anything stupid -- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { -+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading - for (final Consumer consumer : consumers) { - try { - consumer.accept(chunk); -@@ -1411,7 +1411,7 @@ public final class NewChunkHolder { - } - - // must be scheduled to main, we do not trust the callback to not do anything stupid -- this.scheduler.scheduleChunkTask(this.chunkX, this.chunkZ, () -> { -+ this.scheduler.scheduleChunkTaskEventually(this.chunkX, this.chunkZ, () -> { // Folia - region threading - for (final Consumer consumer : consumers) { - try { - consumer.accept(chunk); -diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java -index e04bd54744335fb5398c6e4f7ce8b981f35bfb7d..c7ce32b31fc4247e72baa5f2dedac7378fa708c3 100644 ---- a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java -+++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java -@@ -1940,7 +1940,7 @@ public final class CollisionUtil { - - for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { - for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { -- final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); -+ final ChunkAccess chunk = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)world, currChunkX, currChunkZ) ? null : chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); // Folia - region threading - - if (chunk == null) { - if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { -diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java -index bd888ef719b9bfc93bace0b1d0fb771ac659f515..ba8b5a0ebe652bfaf5c1498c19d12a91a192bf8e 100644 ---- a/io/papermc/paper/entity/activation/ActivationRange.java -+++ b/io/papermc/paper/entity/activation/ActivationRange.java -@@ -48,33 +48,34 @@ public final class ActivationRange { - - private static int checkInactiveWakeup(final Entity entity) { - final Level world = entity.level(); -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions - final SpigotWorldConfig config = world.spigotConfig; -- final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; -+ final long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions - if (entity.activationType == ActivationType.VILLAGER) { -- if (inactiveFor > config.wakeUpInactiveVillagersEvery && world.wakeupInactiveRemainingVillagers > 0) { -- world.wakeupInactiveRemainingVillagers--; -+ if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions -+ worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions - return config.wakeUpInactiveVillagersFor; - } - } else if (entity.activationType == ActivationType.ANIMAL) { -- if (inactiveFor > config.wakeUpInactiveAnimalsEvery && world.wakeupInactiveRemainingAnimals > 0) { -- world.wakeupInactiveRemainingAnimals--; -+ if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions -+ worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions - return config.wakeUpInactiveAnimalsFor; - } - } else if (entity.activationType == ActivationType.FLYING_MONSTER) { -- if (inactiveFor > config.wakeUpInactiveFlyingEvery && world.wakeupInactiveRemainingFlying > 0) { -- world.wakeupInactiveRemainingFlying--; -+ if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions -+ worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions - return config.wakeUpInactiveFlyingFor; - } - } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) { -- if (inactiveFor > config.wakeUpInactiveMonstersEvery && world.wakeupInactiveRemainingMonsters > 0) { -- world.wakeupInactiveRemainingMonsters--; -+ if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions -+ worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions - return config.wakeUpInactiveMonstersFor; - } - } - return -1; - } - -- static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0); -+ //static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0); // Folia - threaded regions - replaced by local variable - - /** - * These entities are excluded from Activation range checks. -@@ -122,10 +123,11 @@ public final class ActivationRange { - final int waterActivationRange = world.spigotConfig.waterActivationRange; - final int flyingActivationRange = world.spigotConfig.flyingMonsterActivationRange; - final int villagerActivationRange = world.spigotConfig.villagerActivationRange; -- world.wakeupInactiveRemainingAnimals = Math.min(world.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); -- world.wakeupInactiveRemainingVillagers = Math.min(world.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); -- world.wakeupInactiveRemainingMonsters = Math.min(world.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); -- world.wakeupInactiveRemainingFlying = Math.min(world.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); // Folia - threaded regions -+ worldData.wakeupInactiveRemainingAnimals = Math.min(worldData.wakeupInactiveRemainingAnimals + 1, world.spigotConfig.wakeUpInactiveAnimals); // Folia - threaded regions -+ worldData.wakeupInactiveRemainingVillagers = Math.min(worldData.wakeupInactiveRemainingVillagers + 1, world.spigotConfig.wakeUpInactiveVillagers); // Folia - threaded regions -+ worldData.wakeupInactiveRemainingMonsters = Math.min(worldData.wakeupInactiveRemainingMonsters + 1, world.spigotConfig.wakeUpInactiveMonsters); // Folia - threaded regions -+ worldData.wakeupInactiveRemainingFlying = Math.min(worldData.wakeupInactiveRemainingFlying + 1, world.spigotConfig.wakeUpInactiveFlying); // Folia - threaded regions - - int maxRange = Math.max(monsterActivationRange, animalActivationRange); - maxRange = Math.max(maxRange, raiderActivationRange); -@@ -135,30 +137,37 @@ public final class ActivationRange { - maxRange = Math.max(maxRange, villagerActivationRange); - maxRange = Math.min((world.spigotConfig.simulationDistance << 4) - 8, maxRange); - -- for (final Player player : world.players()) { -- player.activatedTick = MinecraftServer.currentTick; -+ for (final Player player : world.getLocalPlayers()) { // Folia - region threading -+ player.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading - if (world.spigotConfig.ignoreSpectatorActivation && player.isSpectator()) { - continue; - } - - final int worldHeight = world.getHeight(); -- ActivationRange.maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange); -- ActivationType.MISC.boundingBox = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange); -- ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange); -- ActivationType.ANIMAL.boundingBox = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange); -- ActivationType.MONSTER.boundingBox = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange); -- ActivationType.WATER.boundingBox = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange); -- ActivationType.FLYING_MONSTER.boundingBox = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange); -- ActivationType.VILLAGER.boundingBox = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange); -+ final AABB maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange); // Folia - threaded regions -+ final AABB[] bbByType = new AABB[ActivationType.values().length]; // Folia - threaded regions -+ bbByType[ActivationType.MISC.ordinal()] = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange); // Folia - threaded regions -+ bbByType[ActivationType.RAIDER.ordinal()] = player.getBoundingBox().inflate(raiderActivationRange, worldHeight, raiderActivationRange); // Folia - threaded regions -+ bbByType[ActivationType.ANIMAL.ordinal()] = player.getBoundingBox().inflate(animalActivationRange, worldHeight, animalActivationRange); // Folia - threaded regions -+ bbByType[ActivationType.MONSTER.ordinal()] = player.getBoundingBox().inflate(monsterActivationRange, worldHeight, monsterActivationRange); // Folia - threaded regions -+ bbByType[ActivationType.WATER.ordinal()] = player.getBoundingBox().inflate(waterActivationRange, worldHeight, waterActivationRange); // Folia - threaded regions -+ bbByType[ActivationType.FLYING_MONSTER.ordinal()] = player.getBoundingBox().inflate(flyingActivationRange, worldHeight, flyingActivationRange); // Folia - threaded regions -+ bbByType[ActivationType.VILLAGER.ordinal()] = player.getBoundingBox().inflate(villagerActivationRange, worldHeight, villagerActivationRange); // Folia - threaded regions - -- final java.util.List entities = world.getEntities((Entity) null, ActivationRange.maxBB, e -> true); -+ final java.util.List entities = new java.util.ArrayList<>(); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later -+ ((net.minecraft.server.level.ServerLevel)world).moonrise$getEntityLookup().getEntities((Entity)null, maxBB, entities, null); // Folia - region ticking - bypass getEntities thread check, we perform a check on the entities later - final boolean tickMarkers = world.paperConfig().entities.markers.tick; - for (final Entity entity : entities) { -+ // Folia start - region ticking -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { -+ continue; -+ } -+ // Folia end - region ticking - if (!tickMarkers && entity instanceof net.minecraft.world.entity.Marker) { - continue; - } - -- ActivationRange.activateEntity(entity); -+ ActivationRange.activateEntity(entity, bbByType); // Folia - threaded regions - } - } - } -@@ -168,14 +177,14 @@ public final class ActivationRange { - * - * @param entity - */ -- private static void activateEntity(final Entity entity) { -- if (MinecraftServer.currentTick > entity.activatedTick) { -+ private static void activateEntity(final Entity entity, final AABB[] bbByType) { // Folia - threaded regions -+ if (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() > entity.activatedTick) { // Folia - threaded regions - if (entity.defaultActivationState) { -- entity.activatedTick = MinecraftServer.currentTick; -+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions - return; - } -- if (entity.activationType.boundingBox.intersects(entity.getBoundingBox())) { -- entity.activatedTick = MinecraftServer.currentTick; -+ if (bbByType[entity.activationType.ordinal()].intersects(entity.getBoundingBox())) { // Folia - threaded regions -+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions - } - } - } -@@ -189,6 +198,7 @@ public final class ActivationRange { - */ - public static int checkEntityImmunities(final Entity entity) { // return # of ticks to get immunity - final SpigotWorldConfig config = entity.level().spigotConfig; -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = entity.level().getCurrentWorldData(); // Folia - threaded regions - final int inactiveWakeUpImmunity = checkInactiveWakeup(entity); - if (inactiveWakeUpImmunity > -1) { - return inactiveWakeUpImmunity; -@@ -196,10 +206,10 @@ public final class ActivationRange { - if (entity.getRemainingFireTicks() > 0) { - return 2; - } -- if (entity.activatedImmunityTick >= MinecraftServer.currentTick) { -+ if (entity.activatedImmunityTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - threaded regions - return 1; - } -- final long inactiveFor = MinecraftServer.currentTick - entity.activatedTick; -+ final long inactiveFor = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick; // Folia - threaded regions - if ((entity.activationType != ActivationType.WATER && entity.isInWater() && entity.isPushedByFluid())) { - return 100; - } -@@ -296,16 +306,16 @@ public final class ActivationRange { - return true; - } - -- boolean isActive = entity.activatedTick >= MinecraftServer.currentTick; -+ boolean isActive = entity.activatedTick >= io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - threaded regions - entity.isTemporarilyActive = false; - - // Should this entity tick? - if (!isActive) { -- if ((MinecraftServer.currentTick - entity.activatedTick - 1) % 20 == 0) { -+ if ((io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() - entity.activatedTick - 1) % 20 == 0) { // Folia - threaded regions - // Check immunities every 20 ticks. - final int immunity = checkEntityImmunities(entity); - if (immunity >= 0) { -- entity.activatedTick = MinecraftServer.currentTick + immunity; -+ entity.activatedTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + immunity; // Folia - threaded regions - } else { - entity.isTemporarilyActive = true; - } -diff --git a/io/papermc/paper/redstone/RedstoneWireTurbo.java b/io/papermc/paper/redstone/RedstoneWireTurbo.java -index ff747a1ecdf3c888bca0d69de4f85dcd810b6139..5a76c93eada8db35b1ddbb562ccfbd2f0d35f0ca 100644 ---- a/io/papermc/paper/redstone/RedstoneWireTurbo.java -+++ b/io/papermc/paper/redstone/RedstoneWireTurbo.java -@@ -829,14 +829,14 @@ public final class RedstoneWireTurbo { - j = getMaxCurrentStrength(upd, j); - int l = 0; - -- wire.shouldSignal = false; -+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading - // Unfortunately, World.isBlockIndirectlyGettingPowered is complicated, - // and I'm not ready to try to replicate even more functionality from - // elsewhere in Minecraft into this accelerator. So sadly, we must - // suffer the performance hit of this very expensive call. If there - // is consistency to what this call returns, we may be able to cache it. - final int k = worldIn.getBestNeighborSignal(upd.self); -- wire.shouldSignal = true; -+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading - - // The variable 'k' holds the maximum redstone power value of any adjacent blocks. - // If 'k' has the highest level of all neighbors, then the power level of this -diff --git a/io/papermc/paper/threadedregions/RegionShutdownThread.java b/io/papermc/paper/threadedregions/RegionShutdownThread.java -new file mode 100644 -index 0000000000000000000000000000000000000000..261b3019878c31a9e44e56b6611899de6c00ebee ---- /dev/null -+++ b/io/papermc/paper/threadedregions/RegionShutdownThread.java -@@ -0,0 +1,226 @@ -+package io.papermc.paper.threadedregions; -+ -+import ca.spottedleaf.moonrise.common.util.WorldUtil; -+import com.mojang.logging.LogUtils; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.ChunkPos; -+import org.bukkit.event.inventory.InventoryCloseEvent; -+import org.slf4j.Logger; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.concurrent.TimeUnit; -+ -+public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.util.TickThread { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ ThreadedRegionizer.ThreadedRegion shuttingDown; -+ -+ public RegionShutdownThread(final String name) { -+ super(name); -+ this.setUncaughtExceptionHandler((thread, thr) -> { -+ LOGGER.error("Error shutting down server", thr); -+ }); -+ } -+ -+ static ThreadedRegionizer.ThreadedRegion getRegion() { -+ final Thread currentThread = Thread.currentThread(); -+ if (currentThread instanceof RegionShutdownThread shutdownThread) { -+ return shutdownThread.shuttingDown; -+ } -+ return null; -+ } -+ -+ -+ static RegionizedWorldData getWorldData() { -+ final Thread currentThread = Thread.currentThread(); -+ if (currentThread instanceof RegionShutdownThread shutdownThread) { -+ // no fast path for shutting down -+ if (shutdownThread.shuttingDown != null) { -+ return shutdownThread.shuttingDown.getData().world.worldRegionData.get(); -+ } -+ } -+ return null; -+ } -+ -+ // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves -+ // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because -+ // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not. -+ // Thus, the only operation that the shutdown thread will perform -+ -+ private void saveLevelData(final ServerLevel world) { -+ try { -+ world.saveLevelData(true); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr); -+ } -+ } -+ -+ private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region, -+ final ServerLevel world) { -+ try { -+ this.shuttingDown = region; -+ final List pendingTeleports = world.removeAllRegionTeleports(); -+ if (pendingTeleports.isEmpty()) { -+ return; -+ } -+ final ChunkPos center = region.getCenterChunk(); -+ LOGGER.info("Completing " + pendingTeleports.size() + " pending teleports in region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); -+ for (final ServerLevel.PendingTeleport pendingTeleport : pendingTeleports) { -+ LOGGER.info("Completing teleportation to target position " + pendingTeleport.to()); -+ -+ // first, add entities to entity chunk so that they will be saved -+ for (final Entity.EntityTreeNode node : pendingTeleport.rootVehicle().getFullTree()) { -+ // assume that world and position are set to destination here -+ node.root.setLevel(world); // in case the pending teleport is from a portal before it finds the exact destination -+ world.moonrise$getEntityLookup().addEntityForShutdownTeleportComplete(node.root); -+ } -+ -+ // then, rebuild the passenger tree so that when saving only the root vehicle will be written - and if -+ // there are any player passengers, that the later player saving will save the tree -+ pendingTeleport.rootVehicle().restore(); -+ -+ // now we are finished -+ LOGGER.info("Completed teleportation to target position " + pendingTeleport.to()); -+ } -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to complete pending teleports", thr); -+ } finally { -+ this.shuttingDown = null; -+ } -+ } -+ -+ private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region, -+ final boolean last) { -+ ChunkPos center = null; -+ try { -+ this.shuttingDown = region; -+ center = region.getCenterChunk(); -+ LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); -+ region.regioniser.world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(true, true, false, last, false); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); -+ } finally { -+ this.shuttingDown = null; -+ } -+ } -+ -+ private void haltChunkSystem(final ServerLevel world) { -+ try { -+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(false, true, true, false, false); -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to halt chunk system for world '" + world.getWorld().getName() + "'", thr); -+ } -+ } -+ -+ private void closePlayerInventories(final ThreadedRegionizer.ThreadedRegion region) { -+ ChunkPos center = null; -+ try { -+ this.shuttingDown = region; -+ center = region.getCenterChunk(); -+ -+ final RegionizedWorldData worldData = region.regioniser.world.worldRegionData.get(); -+ -+ for (final ServerPlayer player : worldData.getLocalPlayers()) { -+ try { -+ // close inventory -+ if (player.containerMenu != player.inventoryMenu) { -+ player.closeContainer(InventoryCloseEvent.Reason.DISCONNECT); -+ } -+ -+ // drop carried item -+ if (!player.containerMenu.getCarried().isEmpty()) { -+ ItemStack carried = player.containerMenu.getCarried(); -+ player.containerMenu.setCarried(ItemStack.EMPTY); -+ player.drop(carried, false); -+ } -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to close player inventory for player: " + player, thr); -+ } -+ } -+ } catch (final Throwable thr) { -+ LOGGER.error("Failed to close player inventories for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); -+ } finally { -+ this.shuttingDown = null; -+ } -+ } -+ -+ @Override -+ public final void run() { -+ // await scheduler termination -+ LOGGER.info("Awaiting scheduler termination for 60s..."); -+ if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) { -+ LOGGER.info("Scheduler halted"); -+ } else { -+ LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways"); -+ TickRegions.getScheduler().dumpAliveThreadTraces("Did not shut down in time"); -+ } -+ -+ MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc -+ // halt all chunk systems first so that any in-progress chunk generation stops -+ LOGGER.info("Halting chunk systems..."); -+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { -+ try { -+ world.moonrise$getChunkTaskScheduler().halt(false, 0L); -+ } catch (final Throwable throwable) { -+ LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable); -+ } -+ } -+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { -+ this.haltChunkSystem(world); -+ } -+ LOGGER.info("Halted chunk systems"); -+ -+ LOGGER.info("Finishing pending teleports..."); -+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { -+ final List> -+ regions = new ArrayList<>(); -+ world.regioniser.computeForAllRegionsUnsynchronised(regions::add); -+ -+ for (int i = 0, len = regions.size(); i < len; ++i) { -+ this.finishTeleportations(regions.get(i), world); -+ } -+ } -+ LOGGER.info("Finished pending teleports"); -+ -+ LOGGER.info("Saving all worlds"); -+ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { -+ LOGGER.info("Saving world data for world '" + WorldUtil.getWorldName(world) + "'"); -+ -+ final List> -+ regions = new ArrayList<>(); -+ world.regioniser.computeForAllRegionsUnsynchronised(regions::add); -+ -+ LOGGER.info("Closing player inventories..."); -+ for (int i = 0, len = regions.size(); i < len; ++i) { -+ this.closePlayerInventories(regions.get(i)); -+ } -+ LOGGER.info("Closed player inventories"); -+ -+ LOGGER.info("Saving chunks..."); -+ for (int i = 0, len = regions.size(); i < len; ++i) { -+ this.saveRegionChunks(regions.get(i), (i + 1) == len); -+ } -+ LOGGER.info("Saved chunks"); -+ -+ LOGGER.info("Saving level data..."); -+ this.saveLevelData(world); -+ LOGGER.info("Saved level data"); -+ -+ LOGGER.info("Saved world data for world '" + WorldUtil.getWorldName(world) + "'"); -+ } -+ LOGGER.info("Saved all worlds"); -+ -+ // Note: only save after world data and pending teleportations -+ LOGGER.info("Saving all player data..."); -+ MinecraftServer.getServer().getPlayerList().saveAll(); -+ LOGGER.info("Saved all player data"); -+ -+ MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc) -+ // done, part 2 should call exit() -+ } -+} -diff --git a/io/papermc/paper/threadedregions/RegionizedData.java b/io/papermc/paper/threadedregions/RegionizedData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1f48ada99d6d24880f9bda1cd05d41a4562e42f5 ---- /dev/null -+++ b/io/papermc/paper/threadedregions/RegionizedData.java -@@ -0,0 +1,235 @@ -+package io.papermc.paper.threadedregions; -+ -+import ca.spottedleaf.concurrentutil.util.Validate; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -+import net.minecraft.server.level.ServerLevel; -+import javax.annotation.Nullable; -+import java.util.function.Supplier; -+ -+/** -+ * Use to manage data that needs to be regionised. -+ *

-+ * Note: that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionizedData} object is GC'd. -+ * The data is held in reference to the world it resides in. -+ *

-+ *

-+ * Note: Keep in mind that when regionised ticking is disabled, the entire server is considered a single region. -+ * That is, the data may or may not cross worlds. As such, the {@code RegionizedData} object must be instanced -+ * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions. -+ * See below for more details on instancing per world. -+ *

-+ *

-+ * Regionised data may be world-checked. That is, {@link #get()} may throw an exception if the current -+ * region's world does not match the {@code RegionizedData}'s world. Consider the usages of {@code RegionizedData} below -+ * see why the behavior may or may not be desirable: -+ *

-+ *         {@code
-+ *         public class EntityTickList {
-+ *             private final List entities = new ArrayList<>();
-+ *
-+ *             public void addEntity(Entity e) {
-+ *                 this.entities.add(e);
-+ *             }
-+ *
-+ *             public void removeEntity(Entity e) {
-+ *                 this.entities.remove(e);
-+ *             }
-+ *         }
-+ *
-+ *         public class World {
-+ *
-+ *             // callback is left out of this example
-+ *             // note: world != null here
-+ *             public final RegionizedData entityTickLists =
-+ *                 new RegionizedData<>(this, () -> new EntityTickList(), ...);
-+ *
-+ *             public void addTickingEntity(Entity e) {
-+ *                 // What we expect here is that this world is the
-+ *                 // current ticking region's world.
-+ *                 // If that is true, then calling this.entityTickLists.get()
-+ *                 // will retrieve the current region's EntityTickList
-+ *                 // for this world, which is fine since the current
-+ *                 // region is contained within this world.
-+ *
-+ *                 // But if the current region's world is not this world,
-+ *                 // and if the world check is disabled, then we will actually
-+ *                 // retrieve _this_ world's EntityTickList for the region,
-+ *                 // and NOT the EntityTickList for the region's world.
-+ *                 // This is because the RegionizedData object is instantiated
-+ *                 // per world.
-+ *                 this.entityTickLists.get().addEntity(e);
-+ *             }
-+ *         }
-+ *
-+ *         public class TickTimes {
-+ *
-+ *             private final List tickTimesNS = new ArrayList<>();
-+ *
-+ *             public void completeTick(long timeNS) {
-+ *                 this.tickTimesNS.add(timeNS);
-+ *             }
-+ *
-+ *             public double getAverageTickLengthMS() {
-+ *                 double sum = 0.0;
-+ *                 for (long time : tickTimesNS) {
-+ *                     sum += (double)time;
-+ *                 }
-+ *                 return (sum / this.tickTimesNS.size()) / 1.0E6; // 1ms = 1 million ns
-+ *             }
-+ *         }
-+ *
-+ *         public class Server {
-+ *             public final List worlds = ...;
-+ *
-+ *             // callback is left out of this example
-+ *             // note: world == null here, because this RegionizedData object
-+ *             // is not instantiated per world, but rather globally.
-+ *             public final RegionizedData tickTimes =
-+ *                  new RegionizedData<>(null, () -> new TickTimes(), ...);
-+ *         }
-+ *         }
-+ *     
-+ * In general, it is advised that if a RegionizedData object is instantiated per world, that world checking -+ * is enabled for it by passing the world to the constructor. -+ *

-+ */ -+public final class RegionizedData { -+ -+ private final ServerLevel world; -+ private final Supplier initialValueSupplier; -+ private final RegioniserCallback callback; -+ -+ /** -+ * Creates a regionised data holder. The provided initial value supplier may not be null, and it must -+ * never produce {@code null} values. -+ *

-+ * Note that the supplier or regioniser callback may be used while the region lock is held, so any blocking -+ * operations may deadlock the entire server and as such the function should be completely non-blocking -+ * and must complete in a timely manner. -+ *

-+ *

-+ * If the provided world is {@code null}, then the world checks are disabled. The world should only ever -+ * be {@code null} if the data is specifically not specific to worlds. For example, using {@code null} -+ * for an entity tick list is invalid since the entities are tied to a world and region, -+ * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to -+ * region only. -+ *

-+ * @param world The world in which the region data resides. -+ * @param supplier Initial value supplier used to lazy initialise region data. -+ * @param callback Region callback to manage this regionised data. -+ */ -+ public RegionizedData(final ServerLevel world, final Supplier supplier, final RegioniserCallback callback) { -+ this.world = world; -+ this.initialValueSupplier = Validate.notNull(supplier, "Supplier may not be null."); -+ this.callback = Validate.notNull(callback, "Regioniser callback may not be null."); -+ } -+ -+ T createNewValue() { -+ return Validate.notNull(this.initialValueSupplier.get(), "Initial value supplier may not return null"); -+ } -+ -+ RegioniserCallback getCallback() { -+ return this.callback; -+ } -+ -+ /** -+ * Returns the current data type for the current ticking region. If there is no region, returns {@code null}. -+ * @return the current data type for the current ticking region. If there is no region, returns {@code null}. -+ * @throws IllegalStateException If the following are true: The server is in region ticking mode, -+ * this {@code RegionizedData}'s world is not {@code null}, -+ * and the current ticking region's world does not match this {@code RegionizedData}'s world. -+ */ -+ public @Nullable T get() { -+ final ThreadedRegionizer.ThreadedRegion region = -+ TickRegionScheduler.getCurrentRegion(); -+ -+ if (region == null) { -+ return null; -+ } -+ -+ if (this.world != null && this.world != region.getData().world) { -+ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); -+ } -+ -+ return region.getData().getOrCreateRegionizedData(this); -+ } -+ -+ /** -+ * Class responsible for handling merge / split requests from the regioniser. -+ *

-+ * It is critical to note that each function is called while holding the region lock. -+ *

-+ */ -+ public static interface RegioniserCallback { -+ -+ /** -+ * Completely merges the data in {@code from} to {@code into}. -+ *

-+ * Calculating Tick Offsets: -+ * Sometimes data stores absolute tick deadlines, and since regions tick independently, absolute deadlines -+ * are not comparable across regions. Consider absolute deadlines {@code deadlineFrom, deadlineTo} in -+ * regions {@code from} and {@code into} respectively. We can calculate the relative deadline for the from -+ * region with {@code relFrom = deadlineFrom - currentTickFrom}. Then, we can use the same equation for -+ * computing the absolute deadline in region {@code into} that has the same relative deadline as {@code from} -+ * as {@code deadlineTo = relFrom + currentTickTo}. By substituting {@code relFrom} as {@code deadlineFrom - currentTickFrom}, -+ * we finally have that {@code deadlineTo = deadlineFrom + (currentTickTo - currentTickFrom)} and -+ * that we can use an offset {@code fromTickOffset = currentTickTo - currentTickFrom} to calculate -+ * {@code deadlineTo} as {@code deadlineTo = deadlineFrom + fromTickOffset}. -+ *

-+ *

-+ * Critical Notes: -+ *

  • -+ *
      -+ * This function is called while the region lock is held, so any blocking operations may -+ * deadlock the entire server and as such the function should be completely non-blocking and must complete -+ * in a timely manner. -+ *
    -+ *
      -+ * This function may not throw any exceptions, or the server will be left in an unrecoverable state. -+ *
    -+ *
  • -+ *

    -+ * -+ * @param from The data to merge from. -+ * @param into The data to merge into. -+ * @param fromTickOffset The addend to absolute tick deadlines stored in the {@code from} region to adjust to the into region. -+ */ -+ public void merge(final T from, final T into, final long fromTickOffset); -+ -+ /** -+ * Splits the data in {@code from} into {@code dataSet}. -+ *

    -+ * The chunk coordinate to region section coordinate bit shift amount is provided in {@code chunkToRegionShift}. -+ * To convert from chunk coordinates to region coordinates and keys, see the code below: -+ *

    -+         *         {@code
    -+         *         int chunkX = ...;
    -+         *         int chunkZ = ...;
    -+         *
    -+         *         int regionSectionX = chunkX >> chunkToRegionShift;
    -+         *         int regionSectionZ = chunkZ >> chunkToRegionShift;
    -+         *         long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
    -+         *         }
    -+         *     
    -+ *

    -+ *

    -+ * The {@code regionToData} hashtable provides a lookup from {@code regionSectionKey} (see above) to the -+ * data that is owned by the region which occupies the region section. -+ *

    -+ *

    -+ * Unlike {@link #merge(Object, Object, long)}, there is no absolute tick offset provided. This is because -+ * the new regions formed from the split will start at the same tick number, and so no adjustment is required. -+ *

    -+ * -+ * @param from The data to split from. -+ * @param chunkToRegionShift The signed right-shift value used to convert chunk coordinates into region section coordinates. -+ * @param regionToData Lookup hash table from region section key to . -+ * @param dataSet The data set to split into. -+ */ -+ public void split( -+ final T from, final int chunkToRegionShift, -+ final Long2ReferenceOpenHashMap regionToData, final ReferenceOpenHashSet dataSet -+ ); -+ } -+} -diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fc053ded0c14b76a1c6c82b59d3fd320372a3293 ---- /dev/null -+++ b/io/papermc/paper/threadedregions/RegionizedServer.java -@@ -0,0 +1,455 @@ -+package io.papermc.paper.threadedregions; -+ -+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; -+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; -+import ca.spottedleaf.moonrise.common.util.TickThread; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler; -+import net.minecraft.CrashReport; -+import net.minecraft.ReportedException; -+import net.minecraft.network.Connection; -+import net.minecraft.network.PacketListener; -+import net.minecraft.network.PacketSendListener; -+import net.minecraft.network.chat.Component; -+import net.minecraft.network.chat.MutableComponent; -+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.dedicated.DedicatedServer; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.network.ServerGamePacketListenerImpl; -+import net.minecraft.world.level.GameRules; -+import org.bukkit.Bukkit; -+import org.slf4j.Logger; -+import java.util.ArrayList; -+import java.util.Collections; -+import java.util.List; -+import java.util.concurrent.CopyOnWriteArrayList; -+import java.util.concurrent.atomic.AtomicBoolean; -+import java.util.function.BooleanSupplier; -+ -+public final class RegionizedServer { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ private static final RegionizedServer INSTANCE = new RegionizedServer(); -+ -+ public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue(); -+ -+ private final CopyOnWriteArrayList worlds = new CopyOnWriteArrayList<>(); -+ private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>(); -+ -+ private final MultiThreadedQueue globalTickQueue = new MultiThreadedQueue<>(); -+ -+ private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this); -+ -+ public static RegionizedServer getInstance() { -+ return INSTANCE; -+ } -+ -+ public void addConnection(final Connection conn) { -+ this.connections.add(conn); -+ } -+ -+ public boolean removeConnection(final Connection conn) { -+ return this.connections.remove(conn); -+ } -+ -+ public void addWorld(final ServerLevel world) { -+ this.worlds.add(world); -+ } -+ -+ public void init() { -+ // call init event _before_ scheduling anything -+ new RegionizedServerInitEvent().callEvent(); -+ -+ // now we can schedule -+ this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); -+ TickRegions.getScheduler().scheduleRegion(this.tickHandle); -+ TickRegions.getScheduler().init(); -+ } -+ -+ public void invalidateStatus() { -+ this.lastServerStatus = 0L; -+ } -+ -+ public void addTaskWithoutNotify(final Runnable run) { -+ this.globalTickQueue.add(run); -+ } -+ -+ public void addTask(final Runnable run) { -+ this.addTaskWithoutNotify(run); -+ TickRegions.getScheduler().setHasTasks(this.tickHandle); -+ } -+ -+ /** -+ * Returns the current tick of the region ticking. -+ * @throws IllegalStateException If there is no current region. -+ */ -+ public static long getCurrentTick() throws IllegalStateException { -+ final ThreadedRegionizer.ThreadedRegion region = -+ TickRegionScheduler.getCurrentRegion(); -+ if (region == null) { -+ if (TickThread.isShutdownThread()) { -+ return 0L; -+ } -+ throw new IllegalStateException("No currently ticking region"); -+ } -+ return region.getData().getCurrentTick(); -+ } -+ -+ public static boolean isGlobalTickThread() { -+ return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask(); -+ } -+ -+ public static void ensureGlobalTickThread(final String reason) { -+ if (!isGlobalTickThread()) { -+ throw new IllegalStateException(reason); -+ } -+ } -+ -+ public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() { -+ return INSTANCE.tickHandle; -+ } -+ -+ private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle { -+ -+ private final RegionizedServer server; -+ -+ private final AtomicBoolean scheduled = new AtomicBoolean(); -+ private final AtomicBoolean ticking = new AtomicBoolean(); -+ -+ public GlobalTickTickHandle(final RegionizedServer server) { -+ super(null, SchedulerThreadPool.DEADLINE_NOT_SET); -+ this.server = server; -+ } -+ -+ /** -+ * Only valid to call BEFORE scheduled!!!! -+ */ -+ final void setInitialStart(final long start) { -+ if (this.scheduled.getAndSet(true)) { -+ throw new IllegalStateException("Double scheduling global tick"); -+ } -+ this.updateScheduledStart(start); -+ } -+ -+ @Override -+ protected boolean tryMarkTicking() { -+ return !this.ticking.getAndSet(true); -+ } -+ -+ @Override -+ protected boolean markNotTicking() { -+ return this.ticking.getAndSet(false); -+ } -+ -+ @Override -+ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { -+ this.drainTasks(); -+ this.server.globalTick(tickCount); -+ } -+ -+ private void drainTasks() { -+ while (this.runOneTask()); -+ } -+ -+ private boolean runOneTask() { -+ final Runnable run = this.server.globalTickQueue.poll(); -+ if (run == null) { -+ return false; -+ } -+ -+ // TODO try catch? -+ run.run(); -+ -+ return true; -+ } -+ -+ @Override -+ protected boolean runRegionTasks(final BooleanSupplier canContinue) { -+ do { -+ if (!this.runOneTask()) { -+ return false; -+ } -+ } while (canContinue.getAsBoolean()); -+ -+ return true; -+ } -+ -+ @Override -+ protected boolean hasIntermediateTasks() { -+ return !this.server.globalTickQueue.isEmpty(); -+ } -+ } -+ -+ private long lastServerStatus; -+ private long tickCount; -+ -+ /* -+ private final java.util.Random random = new java.util.Random(4L); -+ private final List> walkers = -+ new java.util.ArrayList<>(); -+ static final int PLAYERS = 500; -+ static final int RAD_BLOCKS = 1000; -+ static final int RAD = RAD_BLOCKS >> 4; -+ static final int RAD_BIG_BLOCKS = 100_000; -+ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; -+ static final int VD = 4 + 12; -+ static final int BIG_PLAYERS = 250; -+ static final double WALK_CHANCE = 0.3; -+ static final double TP_CHANCE = 0.2; -+ static final double TASK_CHANCE = 0.2; -+ -+ private ServerLevel getWorld() { -+ return this.worlds.get(0); -+ } -+ -+ private void init2() { -+ for (int i = 0; i < PLAYERS; ++i) { -+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; -+ int posX = this.random.nextInt(-rad, rad + 1); -+ int posZ = this.random.nextInt(-rad, rad + 1); -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { -+ @Override -+ protected void addCallback(Void parameter, int chunkX, int chunkZ) { -+ ServerLevel world = RegionizedServer.this.getWorld(); -+ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) { -+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> { -+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {}); -+ }); -+ } -+ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( -+ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ) -+ ); -+ } -+ -+ @Override -+ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { -+ ServerLevel world = RegionizedServer.this.getWorld(); -+ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) { -+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> { -+ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {}); -+ }); -+ } -+ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( -+ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ) -+ ); -+ } -+ }; -+ -+ map.add(posX, posZ, VD); -+ -+ walkers.add(map); -+ } -+ } -+ -+ private void randomWalk() { -+ if (this.walkers.isEmpty()) { -+ this.init2(); -+ return; -+ } -+ -+ for (int i = 0; i < PLAYERS; ++i) { -+ if (this.random.nextDouble() > WALK_CHANCE) { -+ continue; -+ } -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = this.walkers.get(i); -+ -+ int updateX = this.random.nextInt(-1, 2); -+ int updateZ = this.random.nextInt(-1, 2); -+ -+ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); -+ } -+ -+ for (int i = 0; i < PLAYERS; ++i) { -+ if (random.nextDouble() >= TP_CHANCE) { -+ continue; -+ } -+ -+ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; -+ int posX = random.nextInt(-rad, rad + 1); -+ int posZ = random.nextInt(-rad, rad + 1); -+ -+ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); -+ -+ map.update(posX, posZ, VD); -+ } -+ } -+ */ -+ -+ private void globalTick(final int tickCount) { -+ /* -+ if (false) { -+ io.papermc.paper.threadedregions.ThreadedTicketLevelPropagator.main(null); -+ } -+ this.randomWalk(); -+ */ -+ ++this.tickCount; -+ // expire invalid click command callbacks -+ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount); -+ -+ // scheduler -+ ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick(); -+ -+ // commands -+ ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs(); -+ -+ // needs -+ // player ping sample -+ // world global tick -+ // connection tick -+ -+ // tick player ping sample -+ this.tickPlayerSample(); -+ -+ // tick worlds -+ for (final ServerLevel world : this.worlds) { -+ this.globalTick(world, tickCount); -+ } -+ -+ // tick connections -+ this.tickConnections(); -+ -+ // player list -+ MinecraftServer.getServer().getPlayerList().tick(); -+ } -+ -+ private void tickPlayerSample() { -+ final MinecraftServer mcServer = MinecraftServer.getServer(); -+ -+ final long currtime = System.nanoTime(); -+ -+ // player ping sample -+ // copied from MinecraftServer#tickServer -+ // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing -+ // an uncomplete status -+ if (currtime - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) { -+ this.lastServerStatus = currtime; -+ mcServer.rebuildServerStatus(); -+ } -+ } -+ -+ public static boolean isNotOwnedByGlobalRegion(final Connection conn) { -+ final PacketListener packetListener = conn.getPacketListener(); -+ -+ if (packetListener instanceof ServerGamePacketListenerImpl gamePacketListener) { -+ return !gamePacketListener.waitingForSwitchToConfig; -+ } -+ -+ if (conn.getPacketListener() instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { -+ return configurationPacketListener.switchToMain; -+ } -+ -+ return false; -+ } -+ -+ private void tickConnections() { -+ final List connections = new ArrayList<>(this.connections); -+ Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging -+ for (final Connection conn : connections) { -+ if (!conn.becomeActive()) { -+ continue; -+ } -+ -+ if (isNotOwnedByGlobalRegion(conn)) { -+ // we actually require that the owning regions remove the connection for us, as it is possible -+ // that ownership is transferred back to us -+ continue; -+ } -+ -+ if (!conn.isConnected()) { -+ this.removeConnection(conn); -+ conn.handleDisconnection(); -+ continue; -+ } -+ -+ try { -+ conn.tick(); -+ } catch (final Exception exception) { -+ if (conn.isMemoryConnection()) { -+ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); -+ } -+ -+ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception); -+ MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); -+ -+ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { -+ conn.disconnect(ichatmutablecomponent); -+ })); -+ conn.setReadOnly(); -+ continue; -+ } -+ } -+ } -+ -+ // A global tick only updates things like weather / worldborder, basically anything in the world that is -+ // NOT tied to a specific region, but rather shared amongst all of them. -+ private void globalTick(final ServerLevel world, final int tickCount) { -+ // needs -+ // worldborder tick -+ // advancing the weather cycle -+ // sleep status thing -+ // updating sky brightness -+ // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking -+ -+ // Typically, we expect there to be a running region to drain a world's global chunk tasks. However, -+ // this may not be the case - and thus, only the global tick thread can do anything. -+ world.taskQueueRegionData.drainGlobalChunkTasks(); -+ -+ // worldborder tick -+ this.tickWorldBorder(world); -+ -+ // weather cycle -+ this.advanceWeatherCycle(world); -+ -+ // sleep status -+ this.checkNightSkip(world); -+ -+ // update raids -+ this.updateRaids(world); -+ -+ // sky brightness -+ this.updateSkyBrightness(world); -+ -+ // time ticking (TODO API synchronisation?) -+ this.tickTime(world, tickCount); -+ -+ world.updateTickData(); -+ -+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates -+ } -+ -+ private void updateRaids(final ServerLevel world) { -+ world.getRaids().globalTick(); -+ } -+ -+ private void checkNightSkip(final ServerLevel world) { -+ world.tickSleep(); -+ } -+ -+ private void advanceWeatherCycle(final ServerLevel world) { -+ world.advanceWeatherCycle(); -+ } -+ -+ private void updateSkyBrightness(final ServerLevel world) { -+ world.updateSkyBrightness(); -+ } -+ -+ private void tickWorldBorder(final ServerLevel world) { -+ world.getWorldBorder().tick(); -+ } -+ -+ private void tickTime(final ServerLevel world, final int tickCount) { -+ if (world.tickTime) { -+ if (world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { -+ world.setDayTime(world.levelData.getDayTime() + (long)tickCount); -+ } -+ world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount); -+ } -+ } -+ -+ public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) { -+ -+ } -+} -diff --git a/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a2313b5b4c37e8536973a8ea0b371557ea912473 ---- /dev/null -+++ b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java -@@ -0,0 +1,807 @@ -+package io.papermc.paper.threadedregions; -+ -+import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; -+import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; -+import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import ca.spottedleaf.concurrentutil.util.Priority; -+import ca.spottedleaf.moonrise.common.util.CoordinateUtils; -+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; -+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.TicketType; -+import net.minecraft.util.Unit; -+import java.lang.invoke.VarHandle; -+import java.util.ArrayDeque; -+import java.util.Iterator; -+import java.util.concurrent.atomic.AtomicLong; -+ -+public final class RegionizedTaskQueue { -+ -+ private static final TicketType TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0); -+ -+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Runnable run) { -+ return this.createChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Runnable run, final Priority priority) { -+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Runnable run) { -+ return this.createTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Runnable run, final Priority priority) { -+ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Runnable run) { -+ return this.queueChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Runnable run, final Priority priority) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Runnable run) { -+ return this.queueTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL); -+ } -+ -+ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, -+ final Runnable run, final Priority priority) { -+ final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority); -+ -+ ret.queue(); -+ -+ return ret; -+ } -+ -+ public static final class WorldRegionTaskData { -+ private final ServerLevel world; -+ private final MultiThreadedQueue globalChunkTask = new MultiThreadedQueue<>(); -+ private final ConcurrentLong2ReferenceChainedHashTable referenceCounters = new ConcurrentLong2ReferenceChainedHashTable<>(); -+ -+ public WorldRegionTaskData(final ServerLevel world) { -+ this.world = world; -+ } -+ -+ private boolean executeGlobalChunkTask() { -+ final Runnable run = this.globalChunkTask.poll(); -+ if (run != null) { -+ run.run(); -+ return true; -+ } -+ return false; -+ } -+ -+ public void drainGlobalChunkTasks() { -+ while (this.executeGlobalChunkTask()); -+ } -+ -+ public void pushGlobalChunkTask(final Runnable run) { -+ this.globalChunkTask.add(run); -+ } -+ -+ private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) { -+ final ThreadedRegionizer regioniser = this.world.regioniser; -+ final ThreadedRegionizer.ThreadedRegion region -+ = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); -+ if (region == null) { -+ return null; -+ } -+ final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData(); -+ return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue); -+ } -+ -+ private void removeTicket(final long coord) { -+ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( -+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE -+ ); -+ } -+ -+ private void addTicket(final long coord) { -+ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( -+ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE -+ ); -+ } -+ -+ private void processTicketUpdates(final long coord) { -+ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(CoordinateUtils.getChunkX(coord), CoordinateUtils.getChunkZ(coord)); -+ } -+ -+ // note: only call on acquired referenceCountData -+ private void ensureTicketAdded(final long coord, final ReferenceCountData referenceCountData) { -+ if (!referenceCountData.addedTicket) { -+ // fine if multiple threads do this, no removeTicket may be called for this coord due to reference count inc -+ this.addTicket(coord); -+ this.processTicketUpdates(coord); -+ referenceCountData.addedTicket = true; -+ } -+ } -+ -+ private void decrementReference(final ReferenceCountData referenceCountData, final long coord) { -+ if (!referenceCountData.decreaseReferenceCount()) { -+ return; -+ } // else: need to remove ticket -+ -+ // note: it is possible that another thread increments and then removes the reference before we can, so -+ // use ifPresent -+ this.referenceCounters.computeIfPresent(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> { -+ if (valueInMap.referenceCount.get() != 0L) { -+ return valueInMap; -+ } -+ -+ // note: valueInMap may not be referenceCountData -+ -+ // possible to invoke this outside of the compute call, but not required and requires additional logic -+ WorldRegionTaskData.this.removeTicket(keyInMap); -+ -+ return null; -+ }); -+ } -+ -+ private ReferenceCountData incrementReference(final long coord) { -+ ReferenceCountData referenceCountData = this.referenceCounters.get(coord); -+ -+ if (referenceCountData != null && referenceCountData.addCount()) { -+ this.ensureTicketAdded(coord, referenceCountData); -+ return referenceCountData; -+ } -+ -+ referenceCountData = this.referenceCounters.compute(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> { -+ if (valueInMap == null) { -+ // sets reference count to 1 -+ return new ReferenceCountData(); -+ } -+ // OK if we add from 0, the remove call will use compute() and catch this race condition -+ valueInMap.referenceCount.getAndIncrement(); -+ -+ return valueInMap; -+ }); -+ -+ this.ensureTicketAdded(coord, referenceCountData); -+ -+ return referenceCountData; -+ } -+ } -+ -+ private static final class ReferenceCountData { -+ -+ public final AtomicLong referenceCount = new AtomicLong(1L); -+ public volatile boolean addedTicket; -+ -+ // returns false if reference count is 0, otherwise increments ref count -+ public boolean addCount() { -+ int failures = 0; -+ for (long curr = this.referenceCount.get();;) { -+ for (int i = 0; i < failures; ++i) { -+ Thread.onSpinWait(); -+ } -+ -+ if (curr == 0L) { -+ return false; -+ } -+ -+ if (curr == (curr = this.referenceCount.compareAndExchange(curr, curr + 1L))) { -+ return true; -+ } -+ -+ ++failures; -+ } -+ } -+ -+ // returns true if new reference count is 0 -+ public boolean decreaseReferenceCount() { -+ final long res = this.referenceCount.decrementAndGet(); -+ if (res >= 0L) { -+ return res == 0L; -+ } else { -+ throw new IllegalStateException("Negative reference count"); -+ } -+ } -+ } -+ -+ public static final class RegionTaskQueueData { -+ private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue(); -+ private final PrioritisedQueue chunkQueue = new PrioritisedQueue(); -+ private final WorldRegionTaskData worldRegionTaskData; -+ -+ public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) { -+ this.worldRegionTaskData = worldRegionTaskData; -+ } -+ -+ void mergeInto(final RegionTaskQueueData into) { -+ this.tickTaskQueue.mergeInto(into.tickTaskQueue); -+ this.chunkQueue.mergeInto(into.chunkQueue); -+ } -+ -+ public boolean executeTickTask() { -+ return this.tickTaskQueue.executeTask(); -+ } -+ -+ public boolean executeChunkTask() { -+ return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask(); -+ } -+ -+ void split(final ThreadedRegionizer regioniser, -+ final Long2ReferenceOpenHashMap> into) { -+ this.tickTaskQueue.split( -+ false, regioniser, into -+ ); -+ this.chunkQueue.split( -+ true, regioniser, into -+ ); -+ } -+ -+ public void drainTasks() { -+ final PrioritisedQueue tickTaskQueue = this.tickTaskQueue; -+ final PrioritisedQueue chunkTaskQueue = this.chunkQueue; -+ -+ int allowedTickTasks = tickTaskQueue.getScheduledTasks(); -+ int allowedChunkTasks = chunkTaskQueue.getScheduledTasks(); -+ -+ boolean executeTickTasks = allowedTickTasks > 0; -+ boolean executeChunkTasks = allowedChunkTasks > 0; -+ boolean executeGlobalTasks = true; -+ -+ do { -+ executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask(); -+ executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask(); -+ executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask(); -+ } while (executeTickTasks | executeChunkTasks | executeGlobalTasks); -+ -+ if (allowedChunkTasks > 0) { -+ // if we executed chunk tasks, we should try to process ticket updates for full status changes -+ this.worldRegionTaskData.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); -+ } -+ } -+ -+ public boolean hasTasks() { -+ return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty(); -+ } -+ } -+ -+ static final class PrioritisedQueue { -+ private final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { -+ for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { -+ this.queues[i] = new ArrayDeque<>(); -+ } -+ } -+ private boolean isDestroyed; -+ -+ public int getScheduledTasks() { -+ synchronized (this) { -+ int ret = 0; -+ -+ for (final ArrayDeque queue : this.queues) { -+ ret += queue.size(); -+ } -+ -+ return ret; -+ } -+ } -+ -+ public boolean isEmpty() { -+ final ArrayDeque[] queues = this.queues; -+ final int max = Priority.IDLE.priority; -+ synchronized (this) { -+ for (int i = 0; i <= max; ++i) { -+ if (!queues[i].isEmpty()) { -+ return false; -+ } -+ } -+ return true; -+ } -+ } -+ -+ public void mergeInto(final PrioritisedQueue target) { -+ synchronized (this) { -+ this.isDestroyed = true; -+ mergeInto(target, this.queues); -+ } -+ } -+ -+ private static void mergeInto(final PrioritisedQueue target, final ArrayDeque[] thisQueues) { -+ synchronized (target) { -+ final ArrayDeque[] otherQueues = target.queues; -+ for (int i = 0; i < thisQueues.length; ++i) { -+ final ArrayDeque fromQ = thisQueues[i]; -+ final ArrayDeque intoQ = otherQueues[i]; -+ -+ // it is possible for another thread to queue tasks into the target queue before we do -+ // since only the ticking region can poll, we don't have to worry about it when they are being queued - -+ // but when we are merging, we need to ensure order is maintained (notwithstanding priority changes) -+ // we can ensure order is maintained by adding all of the tasks from the fromQ into the intoQ at the -+ // front of the queue, but we need to use descending iterator to ensure we do not reverse -+ // the order of elements from fromQ -+ for (final Iterator iterator = fromQ.descendingIterator(); iterator.hasNext();) { -+ intoQ.addFirst(iterator.next()); -+ } -+ } -+ } -+ } -+ -+ // into is a map of section coordinate to region -+ public void split(final boolean isChunkData, -+ final ThreadedRegionizer regioniser, -+ final Long2ReferenceOpenHashMap> into) { -+ final Reference2ReferenceOpenHashMap, ArrayDeque[]> -+ split = new Reference2ReferenceOpenHashMap<>(); -+ final int shift = regioniser.sectionChunkShift; -+ synchronized (this) { -+ this.isDestroyed = true; -+ // like mergeTarget, we need to be careful about insertion order so we can maintain order when splitting -+ -+ // first, build the targets -+ final ArrayDeque[] thisQueues = this.queues; -+ for (int i = 0; i < thisQueues.length; ++i) { -+ final ArrayDeque fromQ = thisQueues[i]; -+ -+ for (final ChunkBasedPriorityTask task : fromQ) { -+ final int sectionX = task.chunkX >> shift; -+ final int sectionZ = task.chunkZ >> shift; -+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ final ThreadedRegionizer.ThreadedRegion -+ region = into.get(sectionKey); -+ if (region == null) { -+ throw new IllegalStateException(); -+ } -+ -+ split.computeIfAbsent(region, (keyInMap) -> { -+ final ArrayDeque[] ret = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; -+ -+ for (int k = 0; k < ret.length; ++k) { -+ ret[k] = new ArrayDeque<>(); -+ } -+ -+ return ret; -+ })[i].add(task); -+ } -+ } -+ -+ // merge the targets into their queues -+ for (final Iterator, ArrayDeque[]>> -+ iterator = split.reference2ReferenceEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Reference2ReferenceMap.Entry, ArrayDeque[]> -+ entry = iterator.next(); -+ final RegionTaskQueueData taskQueueData = entry.getKey().getData().getTaskQueueData(); -+ mergeInto(isChunkData ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue, entry.getValue()); -+ } -+ } -+ } -+ -+ /** -+ * returns null if the task cannot be scheduled, returns false if this task queue is dead, and returns true -+ * if the task was added -+ */ -+ private Boolean tryPush(final ChunkBasedPriorityTask task) { -+ final ArrayDeque[] queues = this.queues; -+ synchronized (this) { -+ final Priority priority = task.getPriority(); -+ if (priority == Priority.COMPLETING) { -+ return null; -+ } -+ if (this.isDestroyed) { -+ return Boolean.FALSE; -+ } -+ queues[priority.priority].addLast(task); -+ return Boolean.TRUE; -+ } -+ } -+ -+ private boolean executeTask() { -+ final ArrayDeque[] queues = this.queues; -+ final int max = Priority.IDLE.priority; -+ ChunkBasedPriorityTask task = null; -+ ReferenceCountData referenceCounter = null; -+ synchronized (this) { -+ if (this.isDestroyed) { -+ throw new IllegalStateException("Attempting to poll from dead queue"); -+ } -+ -+ search_loop: -+ for (int i = 0; i <= max; ++i) { -+ final ArrayDeque queue = queues[i]; -+ while ((task = queue.pollFirst()) != null) { -+ if ((referenceCounter = task.trySetCompleting(i)) != null) { -+ break search_loop; -+ } -+ } -+ } -+ } -+ -+ if (task == null) { -+ return false; -+ } -+ -+ try { -+ task.executeInternal(); -+ } finally { -+ task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord); -+ } -+ -+ return true; -+ } -+ -+ private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask { -+ -+ private static final ReferenceCountData REFERENCE_COUNTER_NOT_SET = new ReferenceCountData(); -+ static { -+ REFERENCE_COUNTER_NOT_SET.referenceCount.set((long)Integer.MIN_VALUE); -+ } -+ -+ private final WorldRegionTaskData world; -+ private final int chunkX; -+ private final int chunkZ; -+ private final long sectionLowerLeftCoord; // chunk coordinate -+ private final boolean isChunkTask; -+ -+ private volatile ReferenceCountData referenceCounter; -+ private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", ReferenceCountData.class); -+ private Runnable run; -+ private volatile Priority priority; -+ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", Priority.class); -+ -+ ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask, -+ final Runnable run, final Priority priority) { -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.isChunkTask = isChunkTask; -+ this.run = run; -+ this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET); -+ this.setPriorityPlain(priority); -+ -+ final int regionShift = world.world.regioniser.sectionChunkShift; -+ final int regionMask = (1 << regionShift) - 1; -+ -+ this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask); -+ } -+ -+ private Priority getPriorityVolatile() { -+ return (Priority)PRIORITY_HANDLE.getVolatile(this); -+ } -+ -+ private void setPriorityPlain(final Priority priority) { -+ PRIORITY_HANDLE.set(this, priority); -+ } -+ -+ private void setPriorityVolatile(final Priority priority) { -+ PRIORITY_HANDLE.setVolatile(this, priority); -+ } -+ -+ private Priority compareAndExchangePriority(final Priority expect, final Priority update) { -+ return (Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update); -+ } -+ -+ private void setReferenceCounterPlain(final ReferenceCountData value) { -+ REFERENCE_COUNTER_HANDLE.set(this, value); -+ } -+ -+ private ReferenceCountData getReferenceCounterVolatile() { -+ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.get(this); -+ } -+ -+ private ReferenceCountData compareAndExchangeReferenceCounter(final ReferenceCountData expect, final ReferenceCountData update) { -+ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update); -+ } -+ -+ private void executeInternal() { -+ try { -+ this.run.run(); -+ } finally { -+ this.run = null; -+ } -+ } -+ -+ private void cancelInternal() { -+ this.run = null; -+ } -+ -+ private boolean tryComplete(final boolean cancel) { -+ int failures = 0; -+ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) { -+ if (curr == null) { -+ return false; -+ } -+ -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ -+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { -+ ++failures; -+ continue; -+ } -+ -+ // we have the reference count, we win no matter what. -+ this.setPriorityVolatile(Priority.COMPLETING); -+ -+ try { -+ if (cancel) { -+ this.cancelInternal(); -+ } else { -+ this.executeInternal(); -+ } -+ } finally { -+ if (curr != REFERENCE_COUNTER_NOT_SET) { -+ this.world.decrementReference(curr, this.sectionLowerLeftCoord); -+ } -+ } -+ -+ return true; -+ } -+ } -+ -+ @Override -+ public PrioritisedExecutor getExecutor() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean isQueued() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean queue() { -+ if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) { -+ return false; -+ } -+ -+ final ReferenceCountData referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord); -+ if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) { -+ // we don't expect race conditions here, so it is OK if we have to needlessly reference count -+ this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord); -+ return false; -+ } -+ -+ boolean synchronise = false; -+ for (;;) { -+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve -+ // the same queue again, as the region lock will be given to us only when the merge/split operation -+ // is done -+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); -+ -+ if (queue == null) { -+ if (!synchronise) { -+ // may be incorrectly null when unsynchronised -+ synchronise = true; -+ continue; -+ } -+ // may have been cancelled before we got to the queue -+ if (this.getReferenceCounterVolatile() != null) { -+ throw new IllegalStateException("Expected null ref count when queue does not exist"); -+ } -+ // the task never could be polled from the queue, so we return false -+ // don't decrement reference count, as we were certainly cancelled by another thread, which -+ // will decrement the reference count -+ return false; -+ } -+ -+ synchronise = true; -+ -+ final Boolean res = queue.tryPush(this); -+ if (res == null) { -+ // we were cancelled -+ // don't decrement reference count, as we were certainly cancelled by another thread, which -+ // will decrement the reference count -+ return false; -+ } -+ -+ if (!res.booleanValue()) { -+ // failed, try again -+ continue; -+ } -+ -+ // successfully queued -+ return true; -+ } -+ } -+ -+ private ReferenceCountData trySetCompleting(final int minPriority) { -+ // first, try to set priority to EXECUTING -+ for (Priority curr = this.getPriorityVolatile();;) { -+ if (curr.isLowerPriority(minPriority)) { -+ return null; -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriority(curr, Priority.COMPLETING))) { -+ break; -+ } // else: continue -+ } -+ -+ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) { -+ if (curr == null) { -+ // something acquired before us -+ return null; -+ } -+ -+ if (curr == REFERENCE_COUNTER_NOT_SET) { -+ throw new IllegalStateException(); -+ } -+ -+ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { -+ continue; -+ } -+ -+ return curr; -+ } -+ } -+ -+ private void updatePriorityInQueue() { -+ boolean synchronise = false; -+ for (;;) { -+ final ReferenceCountData referenceCount = this.getReferenceCounterVolatile(); -+ if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) { -+ // cancelled or not queued -+ return; -+ } -+ -+ if (this.getPriorityVolatile() == Priority.COMPLETING) { -+ // cancelled -+ return; -+ } -+ -+ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve -+ // the same queue again, as the region lock will be given to us only when the merge/split operation -+ // is done -+ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); -+ -+ if (queue == null) { -+ if (!synchronise) { -+ // may be incorrectly null when unsynchronised -+ synchronise = true; -+ continue; -+ } -+ // must have been removed -+ return; -+ } -+ -+ synchronise = true; -+ -+ final Boolean res = queue.tryPush(this); -+ if (res == null) { -+ // we were cancelled -+ return; -+ } -+ -+ if (!res.booleanValue()) { -+ // failed, try again -+ continue; -+ } -+ -+ // successfully queued -+ return; -+ } -+ } -+ -+ @Override -+ public Priority getPriority() { -+ return this.getPriorityVolatile(); -+ } -+ -+ @Override -+ public boolean lowerPriority(final Priority priority) { -+ int failures = 0; -+ for (Priority curr = this.getPriorityVolatile();;) { -+ if (curr == Priority.COMPLETING) { -+ return false; -+ } -+ -+ if (curr.isLowerOrEqualPriority(priority)) { -+ return false; -+ } -+ -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { -+ this.updatePriorityInQueue(); -+ return true; -+ } -+ ++failures; -+ } -+ } -+ -+ @Override -+ public long getSubOrder() { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean setSubOrder(final long subOrder) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean raiseSubOrder(final long subOrder) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean lowerSubOrder(final long subOrder) { -+ throw new UnsupportedOperationException(); -+ } -+ -+ @Override -+ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { -+ return this.setPriority(priority); -+ } -+ -+ @Override -+ public boolean setPriority(final Priority priority) { -+ int failures = 0; -+ for (Priority curr = this.getPriorityVolatile();;) { -+ if (curr == Priority.COMPLETING) { -+ return false; -+ } -+ -+ if (curr == priority) { -+ return false; -+ } -+ -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { -+ this.updatePriorityInQueue(); -+ return true; -+ } -+ ++failures; -+ } -+ } -+ -+ @Override -+ public boolean raisePriority(final Priority priority) { -+ int failures = 0; -+ for (Priority curr = this.getPriorityVolatile();;) { -+ if (curr == Priority.COMPLETING) { -+ return false; -+ } -+ -+ if (curr.isHigherOrEqualPriority(priority)) { -+ return false; -+ } -+ -+ for (int i = 0; i < failures; ++i) { -+ ConcurrentUtil.backoff(); -+ } -+ -+ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { -+ this.updatePriorityInQueue(); -+ return true; -+ } -+ ++failures; -+ } -+ } -+ -+ @Override -+ public boolean execute() { -+ return this.tryComplete(false); -+ } -+ -+ @Override -+ public boolean cancel() { -+ return this.tryComplete(true); -+ } -+ } -+ } -+} -diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b49ca79c31e30e90c82349aecf2cc22df3b61ee6 ---- /dev/null -+++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java -@@ -0,0 +1,770 @@ -+package io.papermc.paper.threadedregions; -+ -+import ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet; -+import ca.spottedleaf.moonrise.common.list.ReferenceList; -+import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; -+import ca.spottedleaf.moonrise.common.util.CoordinateUtils; -+import ca.spottedleaf.moonrise.common.util.TickThread; -+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; -+import com.mojang.logging.LogUtils; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet; -+import it.unimi.dsi.fastutil.objects.ReferenceLinkedOpenHashSet; -+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -+import net.minecraft.CrashReport; -+import net.minecraft.ReportedException; -+import net.minecraft.core.BlockPos; -+import net.minecraft.network.Connection; -+import net.minecraft.network.PacketSendListener; -+import net.minecraft.network.chat.Component; -+import net.minecraft.network.chat.MutableComponent; -+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ChunkHolder; -+import net.minecraft.server.level.ServerChunkCache; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.network.ServerGamePacketListenerImpl; -+import net.minecraft.util.VisibleForDebug; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.Mob; -+import net.minecraft.world.entity.ai.village.VillageSiege; -+import net.minecraft.world.entity.item.ItemEntity; -+import net.minecraft.world.level.BlockEventData; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.Explosion; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.NaturalSpawner; -+import net.minecraft.world.level.ServerExplosion; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.RedStoneWireBlock; -+import net.minecraft.world.level.block.entity.BlockEntity; -+import net.minecraft.world.level.block.entity.TickingBlockEntity; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.material.Fluid; -+import net.minecraft.world.level.pathfinder.PathTypeCache; -+import net.minecraft.world.level.redstone.CollectingNeighborUpdater; -+import net.minecraft.world.level.redstone.NeighborUpdater; -+import net.minecraft.world.ticks.LevelTicks; -+import org.bukkit.craftbukkit.block.CraftBlockState; -+import org.slf4j.Logger; -+import javax.annotation.Nullable; -+import java.util.ArrayDeque; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Collection; -+import java.util.Collections; -+import java.util.HashMap; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Map; -+import java.util.Set; -+import java.util.function.Consumer; -+import java.util.function.Predicate; -+ -+public final class RegionizedWorldData { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private static final Entity[] EMPTY_ENTITY_ARRAY = new Entity[0]; -+ -+ public static final RegionizedData.RegioniserCallback REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() { -+ @Override -+ public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) { -+ // connections -+ for (final Connection conn : from.connections) { -+ into.connections.add(conn); -+ } -+ // time -+ final long fromRedstoneTimeOffset = into.redstoneTime - from.redstoneTime; -+ // entities -+ for (final ServerPlayer player : from.localPlayers) { -+ into.localPlayers.add(player); -+ into.nearbyPlayers.addPlayer(player); -+ } -+ for (final Entity entity : from.allEntities) { -+ into.allEntities.add(entity); -+ entity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); -+ } -+ for (final Entity entity : from.loadedEntities) { -+ into.loadedEntities.add(entity); -+ } -+ for (final Iterator iterator = from.entityTickList.unsafeIterator(); iterator.hasNext();) { -+ into.entityTickList.add(iterator.next()); -+ } -+ for (final Iterator iterator = from.navigatingMobs.unsafeIterator(); iterator.hasNext();) { -+ into.navigatingMobs.add(iterator.next()); -+ } -+ for (final Iterator iterator = from.trackerEntities.iterator(); iterator.hasNext();) { -+ into.trackerEntities.add(iterator.next()); -+ } -+ for (final Iterator iterator = from.trackerUnloadedEntities.iterator(); iterator.hasNext();) { -+ into.trackerUnloadedEntities.add(iterator.next()); -+ } -+ // block ticking -+ into.blockEvents.addAll(from.blockEvents); -+ // ticklists use game time -+ from.blockLevelTicks.merge(into.blockLevelTicks, fromRedstoneTimeOffset); -+ from.fluidLevelTicks.merge(into.fluidLevelTicks, fromRedstoneTimeOffset); -+ -+ // tile entity ticking -+ for (final TickingBlockEntity tileEntityWrapped : from.pendingBlockEntityTickers) { -+ into.pendingBlockEntityTickers.add(tileEntityWrapped); -+ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); -+ if (tileEntity != null) { -+ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); -+ } -+ } -+ for (final TickingBlockEntity tileEntityWrapped : from.blockEntityTickers) { -+ into.blockEntityTickers.add(tileEntityWrapped); -+ final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); -+ if (tileEntity != null) { -+ tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); -+ } -+ } -+ -+ // ticking chunks -+ for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) { -+ into.entityTickingChunks.add(iterator.next()); -+ } -+ for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) { -+ into.tickingChunks.add(iterator.next()); -+ } -+ for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) { -+ into.chunks.add(iterator.next()); -+ } -+ // redstone torches -+ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { -+ if (into.redstoneUpdateInfos == null) { -+ into.redstoneUpdateInfos = new ArrayDeque<>(); -+ } -+ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { -+ info.offsetTime(fromRedstoneTimeOffset); -+ into.redstoneUpdateInfos.add(info); -+ } -+ } -+ // mob spawning -+ into.catSpawnerNextTick = Math.max(from.catSpawnerNextTick, into.catSpawnerNextTick); -+ into.patrolSpawnerNextTick = Math.max(from.patrolSpawnerNextTick, into.patrolSpawnerNextTick); -+ into.phantomSpawnerNextTick = Math.max(from.phantomSpawnerNextTick, into.phantomSpawnerNextTick); -+ if (from.wanderingTraderTickDelay != Integer.MIN_VALUE && into.wanderingTraderTickDelay != Integer.MIN_VALUE) { -+ into.wanderingTraderTickDelay = Math.max(from.wanderingTraderTickDelay, into.wanderingTraderTickDelay); -+ into.wanderingTraderSpawnDelay = Math.max(from.wanderingTraderSpawnDelay, into.wanderingTraderSpawnDelay); -+ into.wanderingTraderSpawnChance = Math.max(from.wanderingTraderSpawnChance, into.wanderingTraderSpawnChance); -+ } -+ // chunkHoldersToBroadcast -+ for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) { -+ into.chunkHoldersToBroadcast.add(chunkHolder); -+ } -+ } -+ -+ @Override -+ public void split(final RegionizedWorldData from, final int chunkToRegionShift, -+ final Long2ReferenceOpenHashMap regionToData, -+ final ReferenceOpenHashSet dataSet) { -+ // connections -+ for (final Connection conn : from.connections) { -+ final ServerPlayer player = conn.getPlayer(); -+ final ChunkPos pos = player.chunkPosition(); -+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means -+ // the chunk holder must _exist_, and so the region section exists. -+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) -+ .connections.add(conn); -+ } -+ // entities -+ for (final ServerPlayer player : from.localPlayers) { -+ final ChunkPos pos = player.chunkPosition(); -+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means -+ // the chunk holder must _exist_, and so the region section exists. -+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); -+ into.localPlayers.add(player); -+ into.nearbyPlayers.addPlayer(player); -+ } -+ for (final Entity entity : from.allEntities) { -+ final ChunkPos pos = entity.chunkPosition(); -+ // Note: It is impossible for an entity in the world to _not_ be in an entity chunk, which means -+ // the chunk holder must _exist_, and so the region section exists. -+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); -+ into.allEntities.add(entity); -+ // Note: entityTickList is a subset of allEntities -+ if (from.entityTickList.contains(entity)) { -+ into.entityTickList.add(entity); -+ } -+ // Note: loadedEntities is a subset of allEntities -+ if (from.loadedEntities.contains(entity)) { -+ into.loadedEntities.add(entity); -+ } -+ // Note: navigatingMobs is a subset of allEntities -+ if (entity instanceof Mob mob && from.navigatingMobs.contains(mob)) { -+ into.navigatingMobs.add(mob); -+ } -+ if (from.trackerEntities.contains(entity)) { -+ into.trackerEntities.add(entity); -+ } -+ if (from.trackerUnloadedEntities.contains(entity)) { -+ into.trackerUnloadedEntities.add(entity); -+ } -+ } -+ // block ticking -+ for (final BlockEventData blockEventData : from.blockEvents) { -+ final BlockPos pos = blockEventData.pos(); -+ final int chunkX = pos.getX() >> 4; -+ final int chunkZ = pos.getZ() >> 4; -+ -+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); -+ // Unlike entities, the chunk holder is not guaranteed to exist for block events, because the block events -+ // is just some list. So if it unloads, I guess it's just lost. -+ if (into != null) { -+ into.blockEvents.add(blockEventData); -+ } -+ } -+ -+ final Long2ReferenceOpenHashMap> levelTicksBlockRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); -+ final Long2ReferenceOpenHashMap> levelTicksFluidRegionData = new Long2ReferenceOpenHashMap<>(regionToData.size(), 0.75f); -+ -+ for (final Iterator> iterator = regionToData.long2ReferenceEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ReferenceMap.Entry entry = iterator.next(); -+ final long key = entry.getLongKey(); -+ final RegionizedWorldData worldData = entry.getValue(); -+ -+ levelTicksBlockRegionData.put(key, worldData.blockLevelTicks); -+ levelTicksFluidRegionData.put(key, worldData.fluidLevelTicks); -+ } -+ -+ from.blockLevelTicks.split(chunkToRegionShift, levelTicksBlockRegionData); -+ from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData); -+ -+ // tile entity ticking -+ for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) { -+ final BlockPos pos = tileEntity.getPos(); -+ final int chunkX = pos.getX() >> 4; -+ final int chunkZ = pos.getZ() >> 4; -+ -+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); -+ if (into != null) { -+ into.pendingBlockEntityTickers.add(tileEntity); -+ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets -+ // marked as removed. So if there is no section, it's probably removed! -+ } -+ for (final TickingBlockEntity tileEntity : from.blockEntityTickers) { -+ final BlockPos pos = tileEntity.getPos(); -+ final int chunkX = pos.getX() >> 4; -+ final int chunkZ = pos.getZ() >> 4; -+ -+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift)); -+ if (into != null) { -+ into.blockEntityTickers.add(tileEntity); -+ } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets -+ // marked as removed. So if there is no section, it's probably removed! -+ } -+ // time -+ for (final RegionizedWorldData regionizedWorldData : dataSet) { -+ regionizedWorldData.redstoneTime = from.redstoneTime; -+ } -+ // ticking chunks -+ for (final Iterator iterator = from.entityTickingChunks.iterator(); iterator.hasNext();) { -+ final ServerChunkCache.ChunkAndHolder holder = iterator.next(); -+ final ChunkPos pos = holder.chunk().getPos(); -+ -+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded -+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) -+ .entityTickingChunks.add(holder); -+ } -+ for (final Iterator iterator = from.tickingChunks.iterator(); iterator.hasNext();) { -+ final ServerChunkCache.ChunkAndHolder holder = iterator.next(); -+ final ChunkPos pos = holder.chunk().getPos(); -+ -+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded -+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) -+ .tickingChunks.add(holder); -+ } -+ for (final Iterator iterator = from.chunks.iterator(); iterator.hasNext();) { -+ final ServerChunkCache.ChunkAndHolder holder = iterator.next(); -+ final ChunkPos pos = holder.chunk().getPos(); -+ -+ // Impossible for get() to return null, as the chunk is entity ticking - thus the chunk holder is loaded -+ regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)) -+ .chunks.add(holder); -+ } -+ -+ // redstone torches -+ if (from.redstoneUpdateInfos != null && !from.redstoneUpdateInfos.isEmpty()) { -+ for (final net.minecraft.world.level.block.RedstoneTorchBlock.Toggle info : from.redstoneUpdateInfos) { -+ final BlockPos pos = info.pos; -+ -+ final RegionizedWorldData worldData = regionToData.get(CoordinateUtils.getChunkKey((pos.getX() >> 4) >> chunkToRegionShift, (pos.getZ() >> 4) >> chunkToRegionShift)); -+ if (worldData != null) { -+ if (worldData.redstoneUpdateInfos == null) { -+ worldData.redstoneUpdateInfos = new ArrayDeque<>(); -+ } -+ worldData.redstoneUpdateInfos.add(info); -+ } // else: chunk unloaded -+ } -+ } -+ // mob spawning -+ for (final RegionizedWorldData regionizedWorldData : dataSet) { -+ regionizedWorldData.catSpawnerNextTick = from.catSpawnerNextTick; -+ regionizedWorldData.patrolSpawnerNextTick = from.patrolSpawnerNextTick; -+ regionizedWorldData.phantomSpawnerNextTick = from.phantomSpawnerNextTick; -+ regionizedWorldData.wanderingTraderTickDelay = from.wanderingTraderTickDelay; -+ regionizedWorldData.wanderingTraderSpawnChance = from.wanderingTraderSpawnChance; -+ regionizedWorldData.wanderingTraderSpawnDelay = from.wanderingTraderSpawnDelay; -+ regionizedWorldData.villageSiegeState = new VillageSiegeState(); // just re set it, as the spawn pos will be invalid -+ } -+ // chunkHoldersToBroadcast -+ for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) { -+ final ChunkPos pos = chunkHolder.getPos(); -+ -+ // Possible for get() to return null, as the chunk holder is not removed during unload -+ final RegionizedWorldData into = regionToData.get(CoordinateUtils.getChunkKey(pos.x >> chunkToRegionShift, pos.z >> chunkToRegionShift)); -+ if (into != null) { -+ into.chunkHoldersToBroadcast.add(chunkHolder); -+ } -+ } -+ } -+ }; -+ -+ public final ServerLevel world; -+ -+ private RegionizedServer.WorldLevelData tickData; -+ -+ // connections -+ public final List connections = new ArrayList<>(); -+ -+ // misc. fields -+ private boolean isHandlingTick; -+ -+ public void setHandlingTick(final boolean to) { -+ this.isHandlingTick = to; -+ } -+ -+ public boolean isHandlingTick() { -+ return this.isHandlingTick; -+ } -+ -+ // entities -+ private final List localPlayers = new ArrayList<>(); -+ private final NearbyPlayers nearbyPlayers; -+ private final ReferenceList allEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); -+ private final ReferenceList loadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); -+ private final IteratorSafeOrderedReferenceSet entityTickList = new IteratorSafeOrderedReferenceSet<>(); -+ private final IteratorSafeOrderedReferenceSet navigatingMobs = new IteratorSafeOrderedReferenceSet<>(); -+ public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker -+ public final ReferenceList trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker -+ -+ // block ticking -+ private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); -+ private final LevelTicks blockLevelTicks; -+ private final LevelTicks fluidLevelTicks; -+ -+ // tile entity ticking -+ private final List pendingBlockEntityTickers = new ArrayList<>(); -+ private final List blockEntityTickers = new ArrayList<>(); -+ private boolean tickingBlockEntities; -+ -+ // time -+ private long redstoneTime = 1L; -+ -+ public long getRedstoneGameTime() { -+ return this.redstoneTime; -+ } -+ -+ public void setRedstoneGameTime(final long to) { -+ this.redstoneTime = to; -+ } -+ -+ // ticking chunks -+ private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDER_ARRAY = new ServerChunkCache.ChunkAndHolder[0]; -+ private final ReferenceList entityTickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY); -+ private final ReferenceList tickingChunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY); -+ private final ReferenceList chunks = new ReferenceList<>(EMPTY_CHUNK_AND_HOLDER_ARRAY); -+ -+ // Paper/CB api hook misc -+ // don't bother to merge/split these, no point -+ // From ServerLevel -+ public boolean hasPhysicsEvent = true; // Paper -+ public boolean hasEntityMoveEvent = false; // Paper -+ // Paper start - Optimize Hoppers -+ public boolean skipPullModeEventFire = false; -+ public boolean skipPushModeEventFire = false; -+ public boolean skipHopperEvents = false; -+ // Paper end - Optimize Hoppers -+ public long lastMidTickExecute; -+ public long lastMidTickExecuteFailure; -+ // From Level -+ public boolean populating; -+ public final NeighborUpdater neighborUpdater; -+ public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 -+ public boolean captureBlockStates = false; -+ public boolean captureTreeGeneration = false; -+ public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent -+ public final Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper -+ public final Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper -+ public List captureDrops; -+ // Paper start -+ public int wakeupInactiveRemainingAnimals; -+ public int wakeupInactiveRemainingFlying; -+ public int wakeupInactiveRemainingMonsters; -+ public int wakeupInactiveRemainingVillagers; -+ // Paper end -+ public int currentPrimedTnt = 0; // Spigot -+ @Nullable -+ @VisibleForDebug -+ public NaturalSpawner.SpawnState lastSpawnState; -+ public boolean shouldSignal = true; -+ public final Map explosionDensityCache = new HashMap<>(64, 0.25f); -+ public final PathTypeCache pathTypesByPosCache = new PathTypeCache(); -+ public final List temporaryChunkTickList = new java.util.ArrayList<>(); -+ public final Set chunkHoldersToBroadcast = new ReferenceLinkedOpenHashSet<>(); -+ -+ // not transient -+ public java.util.ArrayDeque redstoneUpdateInfos; -+ -+ // Mob spawning -+ public final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>(); -+ public int catSpawnerNextTick = 0; -+ public int patrolSpawnerNextTick = 0; -+ public int phantomSpawnerNextTick = 0; -+ public int wanderingTraderTickDelay = Integer.MIN_VALUE; -+ public int wanderingTraderSpawnDelay; -+ public int wanderingTraderSpawnChance; -+ public VillageSiegeState villageSiegeState = new VillageSiegeState(); -+ -+ public static final class VillageSiegeState { -+ public boolean hasSetupSiege; -+ public VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE; -+ public int zombiesToSpawn; -+ public int nextSpawnTime; -+ public int spawnX; -+ public int spawnY; -+ public int spawnZ; -+ } -+ // Redstone -+ public final alternate.current.wire.WireHandler wireHandler; -+ public final io.papermc.paper.redstone.RedstoneWireTurbo turbo; -+ -+ public RegionizedWorldData(final ServerLevel world) { -+ this.world = world; -+ this.blockLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, true); -+ this.fluidLevelTicks = new LevelTicks<>(world::isPositionTickingWithEntitiesLoaded, world, false); -+ this.neighborUpdater = new CollectingNeighborUpdater(world, world.neighbourUpdateMax); -+ this.nearbyPlayers = new NearbyPlayers(world); -+ this.wireHandler = new alternate.current.wire.WireHandler(world); -+ this.turbo = new io.papermc.paper.redstone.RedstoneWireTurbo((RedStoneWireBlock)Blocks.REDSTONE_WIRE); -+ -+ // tasks may be drained before the region ticks, so we must set up the tick data early just in case -+ this.updateTickData(); -+ } -+ -+ public void checkWorld(final Level against) { -+ if (this.world != against) { -+ throw new IllegalStateException("World mismatch: expected " + this.world.getWorld().getName() + " but got " + (against == null ? "null" : against.getWorld().getName())); -+ } -+ } -+ -+ public RegionizedServer.WorldLevelData getTickData() { -+ return this.tickData; -+ } -+ -+ private long lagCompensationTick; -+ -+ public long getLagCompensationTick() { -+ return this.lagCompensationTick; -+ } -+ -+ public void updateTickData() { -+ this.tickData = this.world.tickData; -+ this.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent -+ this.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent -+ this.skipHopperEvents = this.world.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers -+ // always subtract from server init so that the tick starts at zero, allowing us to cast to int without much worry -+ this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / TickRegionScheduler.TIME_BETWEEN_TICKS; -+ } -+ -+ public NearbyPlayers getNearbyPlayers() { -+ return this.nearbyPlayers; -+ } -+ -+ private static void cleanUpConnection(final Connection conn) { -+ // note: ALL connections HERE have a player -+ final ServerPlayer player = conn.getPlayer(); -+ // now that the connection is removed, we can allow this region to die -+ player.serverLevel().chunkSource.removeTicketAtLevel( -+ ServerGamePacketListenerImpl.DISCONNECT_TICKET, player.connection.disconnectPos, -+ ChunkHolderManager.MAX_TICKET_LEVEL, -+ player.connection.disconnectTicketId -+ ); -+ } -+ -+ // connections -+ public void tickConnections() { -+ final List connections = new ArrayList<>(this.connections); -+ Collections.shuffle(connections); -+ for (final Connection conn : connections) { -+ if (!conn.isConnected()) { -+ conn.handleDisconnection(); -+ // global tick thread will not remove connections not owned by it, so we need to -+ RegionizedServer.getInstance().removeConnection(conn); -+ this.connections.remove(conn); -+ cleanUpConnection(conn); -+ continue; -+ } -+ if (!this.connections.contains(conn)) { -+ // removed by connection tick? -+ continue; -+ } -+ -+ try { -+ conn.tick(); -+ } catch (final Exception exception) { -+ if (conn.isMemoryConnection()) { -+ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); -+ } -+ -+ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception); -+ MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); -+ -+ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { -+ conn.disconnect(ichatmutablecomponent); -+ })); -+ conn.setReadOnly(); -+ continue; -+ } -+ } -+ } -+ -+ // entities hooks -+ public int getEntityCount() { -+ return this.allEntities.size(); -+ } -+ -+ public int getPlayerCount() { -+ return this.localPlayers.size(); -+ } -+ -+ public Iterable getLocalEntities() { -+ return this.allEntities; -+ } -+ -+ public Entity[] getLocalEntitiesCopy() { -+ return Arrays.copyOf(this.allEntities.getRawData(), this.allEntities.size(), Entity[].class); -+ } -+ -+ public List getLocalPlayers() { -+ return this.localPlayers; -+ } -+ -+ public void addLoadedEntity(final Entity entity) { -+ this.loadedEntities.add(entity); -+ } -+ -+ public boolean hasLoadedEntity(final Entity entity) { -+ return this.loadedEntities.contains(entity); -+ } -+ -+ public void removeLoadedEntity(final Entity entity) { -+ this.loadedEntities.remove(entity); -+ } -+ -+ public Iterable getLoadedEntities() { -+ return this.loadedEntities; -+ } -+ -+ public void addEntityTickingEntity(final Entity entity) { -+ if (!TickThread.isTickThreadFor(entity)) { -+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); -+ } -+ this.entityTickList.add(entity); -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ -+ public boolean hasEntityTickingEntity(final Entity entity) { -+ return this.entityTickList.contains(entity); -+ } -+ -+ public void removeEntityTickingEntity(final Entity entity) { -+ if (!TickThread.isTickThreadFor(entity)) { -+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); -+ } -+ this.entityTickList.remove(entity); -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ -+ public void forEachTickingEntity(final Consumer action) { -+ final IteratorSafeOrderedReferenceSet.Iterator iterator = this.entityTickList.iterator(); -+ try { -+ while (iterator.hasNext()) { -+ action.accept(iterator.next()); -+ } -+ } finally { -+ iterator.finishedIterating(); -+ } -+ } -+ -+ public void addEntity(final Entity entity) { -+ if (!TickThread.isTickThreadFor(this.world, entity.chunkPosition())) { -+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); -+ } -+ if (this.allEntities.add(entity)) { -+ if (entity instanceof ServerPlayer player) { -+ this.localPlayers.add(player); -+ } -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ } -+ -+ public boolean hasEntity(final Entity entity) { -+ return this.allEntities.contains(entity); -+ } -+ -+ public void removeEntity(final Entity entity) { -+ if (!TickThread.isTickThreadFor(entity)) { -+ throw new IllegalArgumentException("Entity " + entity + " is not under this region's control"); -+ } -+ if (this.allEntities.remove(entity)) { -+ if (entity instanceof ServerPlayer player) { -+ this.localPlayers.remove(player); -+ } -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ } -+ -+ public void addNavigatingMob(final Mob mob) { -+ if (!TickThread.isTickThreadFor(mob)) { -+ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); -+ } -+ this.navigatingMobs.add(mob); -+ } -+ -+ public void removeNavigatingMob(final Mob mob) { -+ if (!TickThread.isTickThreadFor(mob)) { -+ throw new IllegalArgumentException("Entity " + mob + " is not under this region's control"); -+ } -+ this.navigatingMobs.remove(mob); -+ } -+ -+ public Iterator getNavigatingMobs() { -+ return this.navigatingMobs.unsafeIterator(); -+ } -+ -+ // block ticking hooks -+ // Since block event data does not require chunk holders to be created for the chunk they reside in, -+ // it's not actually guaranteed that when merging / splitting data that we actually own the data... -+ // Note that we can only ever not own the event data when the chunk unloads, and so I've decided to -+ // make the code easier by simply discarding it in such an event -+ public void pushBlockEvent(final BlockEventData blockEventData) { -+ TickThread.ensureTickThread(this.world, blockEventData.pos(), "Cannot queue block even data async"); -+ this.blockEvents.add(blockEventData); -+ } -+ -+ public void pushBlockEvents(final Collection blockEvents) { -+ for (final BlockEventData blockEventData : blockEvents) { -+ this.pushBlockEvent(blockEventData); -+ } -+ } -+ -+ public void removeIfBlockEvents(final Predicate predicate) { -+ for (final Iterator iterator = this.blockEvents.iterator(); iterator.hasNext();) { -+ final BlockEventData blockEventData = iterator.next(); -+ if (predicate.test(blockEventData)) { -+ iterator.remove(); -+ } -+ } -+ } -+ -+ public BlockEventData removeFirstBlockEvent() { -+ BlockEventData ret; -+ while (!this.blockEvents.isEmpty()) { -+ ret = this.blockEvents.removeFirst(); -+ if (TickThread.isTickThreadFor(this.world, ret.pos())) { -+ return ret; -+ } // else: chunk must have been unloaded -+ } -+ -+ return null; -+ } -+ -+ public LevelTicks getBlockLevelTicks() { -+ return this.blockLevelTicks; -+ } -+ -+ public LevelTicks getFluidLevelTicks() { -+ return this.fluidLevelTicks; -+ } -+ -+ // tile entity ticking -+ public void addBlockEntityTicker(final TickingBlockEntity ticker) { -+ TickThread.ensureTickThread(this.world, ticker.getPos(), "Tile entity must be owned by current region"); -+ -+ (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); -+ } -+ -+ public void seTtickingBlockEntities(final boolean to) { -+ this.tickingBlockEntities = true; -+ } -+ -+ public List getBlockEntityTickers() { -+ return this.blockEntityTickers; -+ } -+ -+ public void pushPendingTickingBlockEntities() { -+ if (!this.pendingBlockEntityTickers.isEmpty()) { -+ this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); -+ this.pendingBlockEntityTickers.clear(); -+ } -+ } -+ -+ // ticking chunks -+ public void addEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { -+ this.entityTickingChunks.add(holder); -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ -+ public void removeEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { -+ this.entityTickingChunks.remove(holder); -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ -+ public ReferenceList getEntityTickingChunks() { -+ return this.entityTickingChunks; -+ } -+ -+ public void addTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { -+ this.tickingChunks.add(holder); -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ -+ public void removeTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { -+ this.tickingChunks.remove(holder); -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ -+ public ReferenceList getTickingChunks() { -+ return this.tickingChunks; -+ } -+ -+ public void addChunk(final ServerChunkCache.ChunkAndHolder holder) { -+ this.chunks.add(holder); -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ -+ public void removeChunk(final ServerChunkCache.ChunkAndHolder holder) { -+ this.chunks.remove(holder); -+ TickRegions.RegionStats.updateCurrentRegion(); -+ } -+ -+ public ReferenceList getChunks() { -+ return this.chunks; -+ } -+ -+ public int getEntityTickingChunkCount() { -+ return this.entityTickingChunks.size(); -+ } -+ -+ public int getChunkCount() { -+ return this.chunks.size(); -+ } -+} -diff --git a/io/papermc/paper/threadedregions/Schedule.java b/io/papermc/paper/threadedregions/Schedule.java -new file mode 100644 -index 0000000000000000000000000000000000000000..112d24a93bddf3d81c9176c05340c94ecd1a40a3 ---- /dev/null -+++ b/io/papermc/paper/threadedregions/Schedule.java -@@ -0,0 +1,91 @@ -+package io.papermc.paper.threadedregions; -+ -+/** -+ * A Schedule is an object that can be used to maintain a periodic schedule for an event of interest. -+ */ -+public final class Schedule { -+ -+ private long lastPeriod; -+ -+ /** -+ * Initialises a schedule with the provided period. -+ * @param firstPeriod The last time an event of interest occurred. -+ * @see #setLastPeriod(long) -+ */ -+ public Schedule(final long firstPeriod) { -+ this.lastPeriod = firstPeriod; -+ } -+ -+ /** -+ * Updates the last period to the specified value. This call sets the last "time" the event -+ * of interest took place at. Thus, the value returned by {@link #getDeadline(long)} is -+ * the provided time plus the period length provided to {@code getDeadline}. -+ * @param value The value to set the last period to. -+ */ -+ public void setLastPeriod(final long value) { -+ this.lastPeriod = value; -+ } -+ -+ /** -+ * Returns the last time the event of interest should have taken place. -+ */ -+ public long getLastPeriod() { -+ return this.lastPeriod; -+ } -+ -+ /** -+ * Returns the number of times the event of interest should have taken place between the last -+ * period and the provided time given the period between each event. -+ * @param periodLength The length of the period between events in ns. -+ * @param time The provided time. -+ */ -+ public int getPeriodsAhead(final long periodLength, final long time) { -+ final long difference = time - this.lastPeriod; -+ final int ret = (int)(Math.abs(difference) / periodLength); -+ return difference >= 0 ? ret : -ret; -+ } -+ -+ /** -+ * Returns the next starting deadline for the event of interest to take place, -+ * given the provided period length. -+ * @param periodLength The provided period length. -+ */ -+ public long getDeadline(final long periodLength) { -+ return this.lastPeriod + periodLength; -+ } -+ -+ /** -+ * Adjusts the last period so that the next starting deadline returned is the next period specified, -+ * given the provided period length. -+ * @param nextPeriod The specified next starting deadline. -+ * @param periodLength The specified period length. -+ */ -+ public void setNextPeriod(final long nextPeriod, final long periodLength) { -+ this.lastPeriod = nextPeriod - periodLength; -+ } -+ -+ /** -+ * Increases the last period by the specified number of periods and period length. -+ * The specified number of periods may be < 0, in which case the last period -+ * will decrease. -+ * @param periods The specified number of periods. -+ * @param periodLength The specified period length. -+ */ -+ public void advanceBy(final int periods, final long periodLength) { -+ this.lastPeriod += (long)periods * periodLength; -+ } -+ -+ /** -+ * Sets the last period so that it is the specified number of periods ahead -+ * given the specified time and period length. -+ * @param periodsToBeAhead Specified number of periods to be ahead by. -+ * @param periodLength The specified period length. -+ * @param time The specified time. -+ */ -+ public void setPeriodsAhead(final int periodsToBeAhead, final long periodLength, final long time) { -+ final int periodsAhead = this.getPeriodsAhead(periodLength, time); -+ final int periodsToAdd = periodsToBeAhead - periodsAhead; -+ -+ this.lastPeriod -= (long)periodsToAdd * periodLength; -+ } -+} -diff --git a/io/papermc/paper/threadedregions/TeleportUtils.java b/io/papermc/paper/threadedregions/TeleportUtils.java -new file mode 100644 -index 0000000000000000000000000000000000000000..74ac328bf8d5f762f7060a6c5d49089dee1ddaea ---- /dev/null -+++ b/io/papermc/paper/threadedregions/TeleportUtils.java -@@ -0,0 +1,70 @@ -+package io.papermc.paper.threadedregions; -+ -+import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.phys.Vec3; -+import org.bukkit.Location; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.bukkit.event.player.PlayerTeleportEvent; -+import java.util.function.Consumer; -+ -+public final class TeleportUtils { -+ -+ public static void teleport(final Entity from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch, -+ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) { -+ // retrieve coordinates -+ final CallbackCompletable positionCompletable = new CallbackCompletable<>(); -+ -+ positionCompletable.addWaiter( -+ (final Location loc, final Throwable thr) -> { -+ if (loc == null) { -+ if (onComplete != null) { -+ onComplete.accept(null); -+ } -+ return; -+ } -+ final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule( -+ (final Entity realFrom) -> { -+ final Vec3 pos = new Vec3( -+ loc.getX(), loc.getY(), loc.getZ() -+ ); -+ (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync( -+ ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null, -+ cause, teleportFlags, onComplete -+ ); -+ }, -+ (final Entity retired) -> { -+ if (onComplete != null) { -+ onComplete.accept(null); -+ } -+ }, -+ 1L -+ ); -+ if (!scheduled) { -+ if (onComplete != null) { -+ onComplete.accept(null); -+ } -+ } -+ } -+ ); -+ -+ final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule( -+ (final Entity target) -> { -+ positionCompletable.complete(target.getBukkitEntity().getLocation()); -+ }, -+ (final Entity retired) -> { -+ if (onComplete != null) { -+ onComplete.accept(null); -+ } -+ }, -+ 1L -+ ); -+ if (!scheduled) { -+ if (onComplete != null) { -+ onComplete.accept(null); -+ } -+ } -+ } -+ -+ private TeleportUtils() {} -+} -diff --git a/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/io/papermc/paper/threadedregions/ThreadedRegionizer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8e1b1df1c889d9235b10b86fc4cedbc06b7885c2 ---- /dev/null -+++ b/io/papermc/paper/threadedregions/ThreadedRegionizer.java -@@ -0,0 +1,1405 @@ -+package io.papermc.paper.threadedregions; -+ -+import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import ca.spottedleaf.moonrise.common.util.CoordinateUtils; -+import com.destroystokyo.paper.util.SneakyThrow; -+import com.mojang.logging.LogUtils; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.longs.LongArrayList; -+import it.unimi.dsi.fastutil.longs.LongComparator; -+import it.unimi.dsi.fastutil.longs.LongIterator; -+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.level.ChunkPos; -+import org.slf4j.Logger; -+import java.lang.invoke.VarHandle; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Iterator; -+import java.util.List; -+import java.util.Set; -+import java.util.concurrent.atomic.AtomicLong; -+import java.util.concurrent.locks.StampedLock; -+import java.util.function.BooleanSupplier; -+import java.util.function.Consumer; -+ -+public final class ThreadedRegionizer, S extends ThreadedRegionizer.ThreadedRegionSectionData> { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ public final int regionSectionChunkSize; -+ public final int sectionChunkShift; -+ public final int minSectionRecalcCount; -+ public final int emptySectionCreateRadius; -+ public final int regionSectionMergeRadius; -+ public final double maxDeadRegionPercent; -+ public final ServerLevel world; -+ -+ private final SWMRLong2ObjectHashTable> sections = new SWMRLong2ObjectHashTable<>(); -+ private final SWMRLong2ObjectHashTable> regionsById = new SWMRLong2ObjectHashTable<>(); -+ private final RegionCallbacks callbacks; -+ private final StampedLock regionLock = new StampedLock(); -+ private Thread writeLockOwner; -+ -+ /* -+ static final record Operation(String type, int chunkX, int chunkZ) {} -+ private final MultiThreadedQueue ops = new MultiThreadedQueue<>(); -+ */ -+ -+ /* -+ * See REGION_LOGIC.md for complete details on what this class is doing -+ */ -+ -+ public ThreadedRegionizer(final int minSectionRecalcCount, final double maxDeadRegionPercent, -+ final int emptySectionCreateRadius, final int regionSectionMergeRadius, -+ final int regionSectionChunkShift, final ServerLevel world, -+ final RegionCallbacks callbacks) { -+ if (emptySectionCreateRadius <= 0) { -+ throw new IllegalStateException("Region section create radius must be > 0"); -+ } -+ if (regionSectionMergeRadius <= 0) { -+ throw new IllegalStateException("Region section merge radius must be > 0"); -+ } -+ this.regionSectionChunkSize = 1 << regionSectionChunkShift; -+ this.sectionChunkShift = regionSectionChunkShift; -+ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); -+ this.maxDeadRegionPercent = maxDeadRegionPercent; -+ this.emptySectionCreateRadius = emptySectionCreateRadius; -+ this.regionSectionMergeRadius = regionSectionMergeRadius; -+ this.world = world; -+ this.callbacks = callbacks; -+ //this.loadTestData(); -+ } -+ -+ /* -+ private static String substr(String val, String prefix, int from) { -+ int idx = val.indexOf(prefix, from) + prefix.length(); -+ int idx2 = val.indexOf(',', idx); -+ if (idx2 == -1) { -+ idx2 = val.indexOf(']', idx); -+ } -+ return val.substring(idx, idx2); -+ } -+ -+ private void loadTestData() { -+ if (true) { -+ return; -+ } -+ try { -+ final JsonArray arr = JsonParser.parseReader(new FileReader("test.json")).getAsJsonArray(); -+ -+ List ops = new ArrayList<>(); -+ -+ for (JsonElement elem : arr) { -+ JsonObject obj = elem.getAsJsonObject(); -+ String val = obj.get("value").getAsString(); -+ -+ String type = substr(val, "type=", 0); -+ String x = substr(val, "chunkX=", 0); -+ String z = substr(val, "chunkZ=", 0); -+ -+ ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z))); -+ } -+ -+ for (Operation op : ops) { -+ switch (op.type) { -+ case "add": { -+ this.addChunk(op.chunkX, op.chunkZ); -+ break; -+ } -+ case "remove": { -+ this.removeChunk(op.chunkX, op.chunkZ); -+ break; -+ } -+ case "mark_ticking": { -+ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking(); -+ break; -+ } -+ case "rel_region": { -+ if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) { -+ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking(); -+ } -+ break; -+ } -+ } -+ } -+ -+ } catch (final Exception ex) { -+ throw new IllegalStateException(ex); -+ } -+ } -+ */ -+ -+ public void acquireReadLock() { -+ this.regionLock.readLock(); -+ } -+ -+ public void releaseReadLock() { -+ this.regionLock.tryUnlockRead(); -+ } -+ -+ private void acquireWriteLock() { -+ final Thread currentThread = Thread.currentThread(); -+ if (this.writeLockOwner == currentThread) { -+ throw new IllegalStateException("Cannot recursively operate in the regioniser"); -+ } -+ this.regionLock.writeLock(); -+ this.writeLockOwner = currentThread; -+ } -+ -+ private void releaseWriteLock() { -+ this.writeLockOwner = null; -+ this.regionLock.tryUnlockWrite(); -+ } -+ -+ private void onRegionCreate(final ThreadedRegion region) { -+ final ThreadedRegion conflict; -+ if ((conflict = this.regionsById.putIfAbsent(region.id, region)) != null) { -+ throw new IllegalStateException("Region " + region + " is already mapped to " + conflict); -+ } -+ } -+ -+ private void onRegionDestroy(final ThreadedRegion region) { -+ final ThreadedRegion removed = this.regionsById.remove(region.id); -+ if (removed != region) { -+ throw new IllegalStateException("Expected to remove " + region + ", but removed " + removed); -+ } -+ } -+ -+ public int getSectionCoordinate(final int chunkCoordinate) { -+ return chunkCoordinate >> this.sectionChunkShift; -+ } -+ -+ public long getSectionKey(final BlockPos pos) { -+ return CoordinateUtils.getChunkKey((pos.getX() >> 4) >> this.sectionChunkShift, (pos.getZ() >> 4) >> this.sectionChunkShift); -+ } -+ -+ public long getSectionKey(final ChunkPos pos) { -+ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); -+ } -+ -+ public long getSectionKey(final Entity entity) { -+ final ChunkPos pos = entity.chunkPosition(); -+ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); -+ } -+ -+ public void computeForAllRegions(final Consumer> consumer) { -+ this.regionLock.readLock(); -+ try { -+ this.regionsById.forEachValue(consumer); -+ } finally { -+ this.regionLock.tryUnlockRead(); -+ } -+ } -+ -+ public void computeForAllRegionsUnsynchronised(final Consumer> consumer) { -+ this.regionsById.forEachValue(consumer); -+ } -+ -+ public int computeForRegions(final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ, -+ final Consumer>> consumer) { -+ final int shift = this.sectionChunkShift; -+ final int fromSectionX = fromChunkX >> shift; -+ final int fromSectionZ = fromChunkZ >> shift; -+ final int toSectionX = toChunkX >> shift; -+ final int toSectionZ = toChunkZ >> shift; -+ this.acquireWriteLock(); -+ try { -+ final ReferenceOpenHashSet> set = new ReferenceOpenHashSet<>(); -+ -+ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { -+ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { -+ final ThreadedRegionSection section = this.sections.get(CoordinateUtils.getChunkKey(currX, currZ)); -+ if (section != null) { -+ set.add(section.getRegionPlain()); -+ } -+ } -+ } -+ -+ consumer.accept(set); -+ -+ return set.size(); -+ } finally { -+ this.releaseWriteLock(); -+ } -+ } -+ -+ public ThreadedRegion getRegionAtUnsynchronised(final int chunkX, final int chunkZ) { -+ final int sectionX = chunkX >> this.sectionChunkShift; -+ final int sectionZ = chunkZ >> this.sectionChunkShift; -+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ -+ final ThreadedRegionSection section = this.sections.get(sectionKey); -+ -+ return section == null ? null : section.getRegion(); -+ } -+ -+ public ThreadedRegion getRegionAtSynchronised(final int chunkX, final int chunkZ) { -+ final int sectionX = chunkX >> this.sectionChunkShift; -+ final int sectionZ = chunkZ >> this.sectionChunkShift; -+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ -+ // try an optimistic read -+ { -+ final long readAttempt = this.regionLock.tryOptimisticRead(); -+ final ThreadedRegionSection optimisticSection = this.sections.get(sectionKey); -+ final ThreadedRegion optimisticRet = -+ optimisticSection == null ? null : optimisticSection.getRegionPlain(); -+ if (this.regionLock.validate(readAttempt)) { -+ return optimisticRet; -+ } -+ } -+ -+ // failed, fall back to acquiring the lock -+ this.regionLock.readLock(); -+ try { -+ final ThreadedRegionSection section = this.sections.get(sectionKey); -+ -+ return section == null ? null : section.getRegionPlain(); -+ } finally { -+ this.regionLock.tryUnlockRead(); -+ } -+ } -+ -+ /** -+ * Adds a chunk to the regioniser. Note that it is illegal to add a chunk unless -+ * addChunk has not been called for it or removeChunk has been previously called. -+ * -+ *

    -+ * Note that it is illegal to additionally call addChunk or removeChunk for the same -+ * region section in parallel. -+ *

    -+ */ -+ public void addChunk(final int chunkX, final int chunkZ) { -+ final int sectionX = chunkX >> this.sectionChunkShift; -+ final int sectionZ = chunkZ >> this.sectionChunkShift; -+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ -+ // Given that for each section, no addChunk/removeChunk can occur in parallel, -+ // we can avoid the lock IF the section exists AND it has a non-zero chunk count. -+ { -+ final ThreadedRegionSection existing = this.sections.get(sectionKey); -+ if (existing != null && !existing.isEmpty()) { -+ existing.addChunk(chunkX, chunkZ); -+ return; -+ } // else: just acquire the write lock -+ } -+ -+ this.acquireWriteLock(); -+ try { -+ ThreadedRegionSection section = this.sections.get(sectionKey); -+ -+ List> newSections = new ArrayList<>(); -+ -+ if (section == null) { -+ // no section at all -+ section = new ThreadedRegionSection<>(sectionX, sectionZ, this, chunkX, chunkZ); -+ this.sections.put(sectionKey, section); -+ newSections.add(section); -+ } else { -+ section.addChunk(chunkX, chunkZ); -+ } -+ // due to the fast check from above, we know the section is empty whether we needed to create it or not -+ -+ // enforce the adjacency invariant by creating / updating neighbour sections -+ final int createRadius = this.emptySectionCreateRadius; -+ final int searchRadius = createRadius + this.regionSectionMergeRadius; -+ ReferenceOpenHashSet> nearbyRegions = null; -+ for (int dx = -searchRadius; dx <= searchRadius; ++dx) { -+ for (int dz = -searchRadius; dz <= searchRadius; ++dz) { -+ if ((dx | dz) == 0) { -+ continue; -+ } -+ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); -+ final boolean inCreateRange = squareDistance <= createRadius; -+ -+ final int neighbourX = dx + sectionX; -+ final int neighbourZ = dz + sectionZ; -+ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); -+ -+ ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); -+ -+ if (neighbourSection != null) { -+ if (nearbyRegions == null) { -+ nearbyRegions = new ReferenceOpenHashSet<>(((searchRadius * 2 + 1) * (searchRadius * 2 + 1)) >> 1); -+ } -+ nearbyRegions.add(neighbourSection.getRegionPlain()); -+ } -+ -+ if (!inCreateRange) { -+ continue; -+ } -+ -+ // we need to ensure the section exists -+ if (neighbourSection != null) { -+ // nothing else to do -+ neighbourSection.incrementNonEmptyNeighbours(); -+ continue; -+ } -+ neighbourSection = new ThreadedRegionSection<>(neighbourX, neighbourZ, this, 1); -+ if (null != this.sections.put(neighbourKey, neighbourSection)) { -+ throw new IllegalStateException("Failed to insert new section"); -+ } -+ newSections.add(neighbourSection); -+ } -+ } -+ -+ if (newSections.isEmpty()) { -+ // if we didn't add any sections, then we don't need to merge any regions or create a region -+ return; -+ } -+ -+ final ThreadedRegion regionOfInterest; -+ final boolean regionOfInterestAlive; -+ if (nearbyRegions == null) { -+ // we can simply create a new region, don't have neighbours to worry about merging into -+ regionOfInterest = new ThreadedRegion<>(this); -+ regionOfInterestAlive = true; -+ -+ for (int i = 0, len = newSections.size(); i < len; ++i) { -+ regionOfInterest.addSection(newSections.get(i)); -+ } -+ -+ // only call create callback after adding sections -+ regionOfInterest.onCreate(); -+ } else { -+ // need to merge the regions -+ ThreadedRegion firstUnlockedRegion = null; -+ -+ for (final ThreadedRegion region : nearbyRegions) { -+ if (region.isTicking()) { -+ continue; -+ } -+ firstUnlockedRegion = region; -+ if (firstUnlockedRegion.state == ThreadedRegion.STATE_READY && (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty())) { -+ throw new IllegalStateException("Illegal state for unlocked region " + firstUnlockedRegion); -+ } -+ break; -+ } -+ -+ if (firstUnlockedRegion != null) { -+ regionOfInterest = firstUnlockedRegion; -+ } else { -+ regionOfInterest = new ThreadedRegion<>(this); -+ } -+ -+ for (int i = 0, len = newSections.size(); i < len; ++i) { -+ regionOfInterest.addSection(newSections.get(i)); -+ } -+ -+ // only call create callback after adding sections -+ if (firstUnlockedRegion == null) { -+ regionOfInterest.onCreate(); -+ } -+ -+ if (firstUnlockedRegion != null && nearbyRegions.size() == 1) { -+ // nothing to do further, no need to merge anything -+ return; -+ } -+ -+ // we need to now tell all the other regions to merge into the region we just created, -+ // and to merge all the ones we can immediately -+ -+ for (final ThreadedRegion region : nearbyRegions) { -+ if (region == regionOfInterest) { -+ continue; -+ } -+ -+ if (!region.killAndMergeInto(regionOfInterest)) { -+ // note: the region may already be a merge target -+ regionOfInterest.mergeIntoLater(region); -+ } -+ } -+ -+ if (firstUnlockedRegion != null && firstUnlockedRegion.state == ThreadedRegion.STATE_READY) { -+ // we need to retire this region if the merges added other pending merges -+ if (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty()) { -+ firstUnlockedRegion.state = ThreadedRegion.STATE_TRANSIENT; -+ this.callbacks.onRegionInactive(firstUnlockedRegion); -+ } -+ } -+ -+ // need to set alive if we created it and there are no pending merges -+ regionOfInterestAlive = firstUnlockedRegion == null && regionOfInterest.mergeIntoLater.isEmpty() && regionOfInterest.expectingMergeFrom.isEmpty(); -+ } -+ -+ if (regionOfInterestAlive) { -+ regionOfInterest.state = ThreadedRegion.STATE_READY; -+ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { -+ throw new IllegalStateException("Should not happen on region " + this); -+ } -+ this.callbacks.onRegionActive(regionOfInterest); -+ } -+ -+ if (regionOfInterest.state == ThreadedRegion.STATE_READY) { -+ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { -+ throw new IllegalStateException("Should not happen on region " + this); -+ } -+ } -+ } catch (final Throwable throwable) { -+ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); -+ SneakyThrow.sneaky(throwable); -+ return; // unreachable -+ } finally { -+ this.releaseWriteLock(); -+ } -+ } -+ -+ public void removeChunk(final int chunkX, final int chunkZ) { -+ final int sectionX = chunkX >> this.sectionChunkShift; -+ final int sectionZ = chunkZ >> this.sectionChunkShift; -+ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ -+ // Given that for each section, no addChunk/removeChunk can occur in parallel, -+ // we can avoid the lock IF the section exists AND it has a chunk count > 1 -+ final ThreadedRegionSection section = this.sections.get(sectionKey); -+ if (section == null) { -+ throw new IllegalStateException("Chunk (" + chunkX + "," + chunkZ + ") has no section"); -+ } -+ if (!section.hasOnlyOneChunk()) { -+ // chunk will not go empty, so we don't need to acquire the lock -+ section.removeChunk(chunkX, chunkZ); -+ return; -+ } -+ -+ this.acquireWriteLock(); -+ try { -+ section.removeChunk(chunkX, chunkZ); -+ -+ final int searchRadius = this.emptySectionCreateRadius; -+ for (int dx = -searchRadius; dx <= searchRadius; ++dx) { -+ for (int dz = -searchRadius; dz <= searchRadius; ++dz) { -+ if ((dx | dz) == 0) { -+ continue; -+ } -+ -+ final int neighbourX = dx + sectionX; -+ final int neighbourZ = dz + sectionZ; -+ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); -+ -+ final ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); -+ -+ // should be non-null here always -+ neighbourSection.decrementNonEmptyNeighbours(); -+ } -+ } -+ } catch (final Throwable throwable) { -+ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); -+ SneakyThrow.sneaky(throwable); -+ return; // unreachable -+ } finally { -+ this.releaseWriteLock(); -+ } -+ } -+ -+ // must hold regionLock -+ private void onRegionRelease(final ThreadedRegion region) { -+ if (!region.mergeIntoLater.isEmpty()) { -+ throw new IllegalStateException("Region " + region + " should not have any regions to merge into!"); -+ } -+ -+ final boolean hasExpectingMerges = !region.expectingMergeFrom.isEmpty(); -+ -+ // is this region supposed to merge into any other region? -+ if (hasExpectingMerges) { -+ // merge the regions into this one -+ final ReferenceOpenHashSet> expectingMergeFrom = region.expectingMergeFrom.clone(); -+ for (final ThreadedRegion mergeFrom : expectingMergeFrom) { -+ if (!mergeFrom.killAndMergeInto(region)) { -+ throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + region); -+ } -+ } -+ -+ if (!region.expectingMergeFrom.isEmpty()) { -+ throw new IllegalStateException("Region " + region + " should no longer have merge requests after mering from " + expectingMergeFrom); -+ } -+ -+ if (!region.mergeIntoLater.isEmpty()) { -+ // There is another nearby ticking region that we need to merge into -+ region.state = ThreadedRegion.STATE_TRANSIENT; -+ this.callbacks.onRegionInactive(region); -+ // return to avoid removing dead sections or splitting, these actions will be performed -+ // by the region we merge into -+ return; -+ } -+ } -+ -+ // now check whether we need to recalculate regions -+ final boolean removeDeadSections = hasExpectingMerges || region.hasNoAliveSections() -+ || (region.sectionByKey.size() >= this.minSectionRecalcCount && region.getDeadSectionPercent() >= this.maxDeadRegionPercent); -+ final boolean removedDeadSections = removeDeadSections && !region.deadSections.isEmpty(); -+ if (removeDeadSections) { -+ // kill dead sections -+ for (final ThreadedRegionSection deadSection : region.deadSections) { -+ final long key = CoordinateUtils.getChunkKey(deadSection.sectionX, deadSection.sectionZ); -+ -+ if (!deadSection.isEmpty()) { -+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); -+ } -+ if (deadSection.hasNonEmptyNeighbours()) { -+ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has non-empty neighbours!"); -+ } -+ if (!region.sectionByKey.remove(key, deadSection)) { -+ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); -+ } -+ if (this.sections.remove(key) != deadSection) { -+ throw new IllegalStateException("Cannot remove dead section '" + -+ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + this.sections.get(key)); -+ } -+ } -+ region.deadSections.clear(); -+ } -+ -+ // if we removed dead sections, we should check if the region can be split into smaller ones -+ // otherwise, the region remains alive -+ if (!removedDeadSections) { -+ // didn't remove dead sections, don't check for split -+ region.state = ThreadedRegion.STATE_READY; -+ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { -+ throw new IllegalStateException("Illegal state " + region); -+ } -+ return; -+ } -+ -+ // first, we need to build copy of coordinate->section map of all sections in recalculate -+ final Long2ReferenceOpenHashMap> recalculateSections = region.sectionByKey.clone(); -+ -+ if (recalculateSections.isEmpty()) { -+ // looks like the region's sections were all dead, and now there is no region at all -+ region.state = ThreadedRegion.STATE_DEAD; -+ region.onRemove(true); -+ return; -+ } -+ -+ // merge radius is max, since recalculateSections includes the dead or empty sections -+ final int mergeRadius = Math.max(this.regionSectionMergeRadius, this.emptySectionCreateRadius); -+ -+ final List>> newRegions = new ArrayList<>(); -+ while (!recalculateSections.isEmpty()) { -+ // select any section, then BFS around it to find all of its neighbours to form a region -+ // once no more neighbours are found, the region is complete -+ final List> currRegion = new ArrayList<>(); -+ final Iterator> firstIterator = recalculateSections.values().iterator(); -+ -+ currRegion.add(firstIterator.next()); -+ firstIterator.remove(); -+ search_loop: -+ for (int idx = 0; idx < currRegion.size(); ++idx) { -+ final ThreadedRegionSection curr = currRegion.get(idx); -+ final int centerX = curr.sectionX; -+ final int centerZ = curr.sectionZ; -+ -+ // find neighbours in radius -+ for (int dz = -mergeRadius; dz <= mergeRadius; ++dz) { -+ for (int dx = -mergeRadius; dx <= mergeRadius; ++dx) { -+ if ((dx | dz) == 0) { -+ continue; -+ } -+ -+ final ThreadedRegionSection section = recalculateSections.remove(CoordinateUtils.getChunkKey(dx + centerX, dz + centerZ)); -+ if (section == null) { -+ continue; -+ } -+ -+ currRegion.add(section); -+ -+ if (recalculateSections.isEmpty()) { -+ // no point in searching further -+ break search_loop; -+ } -+ } -+ } -+ } -+ -+ newRegions.add(currRegion); -+ } -+ -+ // now we have split the regions into separate parts, we can split recalculate -+ -+ if (newRegions.size() == 1) { -+ // no need to split anything, we're done here -+ region.state = ThreadedRegion.STATE_READY; -+ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { -+ throw new IllegalStateException("Illegal state " + region); -+ } -+ return; -+ } -+ -+ final List> newRegionObjects = new ArrayList<>(newRegions.size()); -+ for (int i = 0, len = newRegions.size(); i < len; ++i) { -+ newRegionObjects.add(new ThreadedRegion<>(this)); -+ } -+ -+ this.callbacks.preSplit(region, newRegionObjects); -+ -+ // need to split the region, so we need to kill the old one first -+ region.state = ThreadedRegion.STATE_DEAD; -+ region.onRemove(true); -+ -+ // create new regions -+ final Long2ReferenceOpenHashMap> newRegionsMap = new Long2ReferenceOpenHashMap<>(); -+ final ReferenceOpenHashSet> newRegionsSet = new ReferenceOpenHashSet<>(newRegionObjects); -+ -+ for (int i = 0, len = newRegions.size(); i < len; i++) { -+ final List> sections = newRegions.get(i); -+ final ThreadedRegion newRegion = newRegionObjects.get(i); -+ -+ for (final ThreadedRegionSection section : sections) { -+ section.setRegionRelease(null); -+ newRegion.addSection(section); -+ final ThreadedRegion curr = newRegionsMap.putIfAbsent(section.sectionKey, newRegion); -+ if (curr != null) { -+ throw new IllegalStateException("Expected no region at " + section + ", but got " + curr + ", should have put " + newRegion); -+ } -+ } -+ } -+ -+ region.split(newRegionsMap, newRegionsSet); -+ -+ // only after invoking data callbacks -+ -+ for (final ThreadedRegion newRegion : newRegionsSet) { -+ newRegion.state = ThreadedRegion.STATE_READY; -+ if (!newRegion.expectingMergeFrom.isEmpty() || !newRegion.mergeIntoLater.isEmpty()) { -+ throw new IllegalStateException("Illegal state " + newRegion); -+ } -+ newRegion.onCreate(); -+ this.callbacks.onRegionActive(newRegion); -+ } -+ } -+ -+ public static final class ThreadedRegion, S extends ThreadedRegionSectionData> { -+ -+ private static final AtomicLong REGION_ID_GENERATOR = new AtomicLong(); -+ -+ private static final int STATE_TRANSIENT = 0; -+ private static final int STATE_READY = 1; -+ private static final int STATE_TICKING = 2; -+ private static final int STATE_DEAD = 3; -+ -+ public final long id; -+ -+ private int state; -+ -+ private final Long2ReferenceOpenHashMap> sectionByKey = new Long2ReferenceOpenHashMap<>(); -+ private final ReferenceOpenHashSet> deadSections = new ReferenceOpenHashSet<>(); -+ -+ public final ThreadedRegionizer regioniser; -+ -+ private final R data; -+ -+ private final ReferenceOpenHashSet> mergeIntoLater = new ReferenceOpenHashSet<>(); -+ private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>(); -+ -+ public ThreadedRegion(final ThreadedRegionizer regioniser) { -+ this.regioniser = regioniser; -+ this.id = REGION_ID_GENERATOR.getAndIncrement(); -+ this.state = STATE_TRANSIENT; -+ this.data = regioniser.callbacks.createNewData(this); -+ } -+ -+ public LongArrayList getOwnedSections() { -+ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); -+ if (lock) { -+ this.regioniser.regionLock.readLock(); -+ } -+ try { -+ final LongArrayList ret = new LongArrayList(this.sectionByKey.size()); -+ ret.addAll(this.sectionByKey.keySet()); -+ -+ return ret; -+ } finally { -+ if (lock) { -+ this.regioniser.regionLock.tryUnlockRead(); -+ } -+ } -+ } -+ -+ /** -+ * returns an iterator directly over the sections map. This is only to be used by a thread which is _ticking_ -+ * 'this' region. -+ */ -+ public LongIterator getOwnedSectionsUnsynchronised() { -+ return this.sectionByKey.keySet().iterator(); -+ } -+ -+ public LongArrayList getOwnedChunks() { -+ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); -+ if (lock) { -+ this.regioniser.regionLock.readLock(); -+ } -+ try { -+ final LongArrayList ret = new LongArrayList(); -+ for (final ThreadedRegionSection section : this.sectionByKey.values()) { -+ ret.addAll(section.getChunks()); -+ } -+ -+ return ret; -+ } finally { -+ if (lock) { -+ this.regioniser.regionLock.tryUnlockRead(); -+ } -+ } -+ } -+ -+ public Long getCenterSection() { -+ final LongArrayList sections = this.getOwnedSections(); -+ -+ final LongComparator comparator = (final long k1, final long k2) -> { -+ final int x1 = CoordinateUtils.getChunkX(k1); -+ final int x2 = CoordinateUtils.getChunkX(k2); -+ -+ final int z1 = CoordinateUtils.getChunkZ(x1); -+ final int z2 = CoordinateUtils.getChunkZ(x2); -+ -+ final int zCompare = Integer.compare(z1, z2); -+ if (zCompare != 0) { -+ return zCompare; -+ } -+ -+ return Integer.compare(x1, x2); -+ }; -+ -+ // note: regions don't always have a chunk section at this point, because the region may have been killed -+ if (sections.isEmpty()) { -+ return null; -+ } -+ -+ sections.sort(comparator); -+ -+ return Long.valueOf(sections.getLong(sections.size() >> 1)); -+ } -+ -+ public ChunkPos getCenterChunk() { -+ final LongArrayList chunks = this.getOwnedChunks(); -+ -+ final LongComparator comparator = (final long k1, final long k2) -> { -+ final int x1 = CoordinateUtils.getChunkX(k1); -+ final int x2 = CoordinateUtils.getChunkX(k2); -+ -+ final int z1 = CoordinateUtils.getChunkZ(k1); -+ final int z2 = CoordinateUtils.getChunkZ(k2); -+ -+ final int zCompare = Integer.compare(z1, z2); -+ if (zCompare != 0) { -+ return zCompare; -+ } -+ -+ return Integer.compare(x1, x2); -+ }; -+ chunks.sort(comparator); -+ -+ // note: regions don't always have a chunk at this point, because the region may have been killed -+ if (chunks.isEmpty()) { -+ return null; -+ } -+ -+ final long middle = chunks.getLong(chunks.size() >> 1); -+ -+ return new ChunkPos(CoordinateUtils.getChunkX(middle), CoordinateUtils.getChunkZ(middle)); -+ } -+ -+ private void onCreate() { -+ this.regioniser.onRegionCreate(this); -+ this.regioniser.callbacks.onRegionCreate(this); -+ } -+ -+ private void onRemove(final boolean wasActive) { -+ if (wasActive) { -+ this.regioniser.callbacks.onRegionInactive(this); -+ } -+ this.regioniser.callbacks.onRegionDestroy(this); -+ this.regioniser.onRegionDestroy(this); -+ } -+ -+ private final boolean hasNoAliveSections() { -+ return this.deadSections.size() == this.sectionByKey.size(); -+ } -+ -+ private final double getDeadSectionPercent() { -+ return (double)this.deadSections.size() / (double)this.sectionByKey.size(); -+ } -+ -+ private void split(final Long2ReferenceOpenHashMap> into, final ReferenceOpenHashSet> regions) { -+ if (this.data != null) { -+ this.data.split(this.regioniser, into, regions); -+ } -+ } -+ -+ boolean killAndMergeInto(final ThreadedRegion mergeTarget) { -+ if (this.state == STATE_TICKING) { -+ return false; -+ } -+ -+ this.regioniser.callbacks.preMerge(this, mergeTarget); -+ -+ this.tryKill(); -+ -+ this.mergeInto(mergeTarget); -+ -+ return true; -+ } -+ -+ private void mergeInto(final ThreadedRegion mergeTarget) { -+ if (this == mergeTarget) { -+ throw new IllegalStateException("Cannot merge a region onto itself"); -+ } -+ if (!this.isDead()) { -+ throw new IllegalStateException("Source region is not dead! Source " + this + ", target " + mergeTarget); -+ } else if (mergeTarget.isDead()) { -+ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); -+ } -+ -+ for (final ThreadedRegionSection section : this.sectionByKey.values()) { -+ section.setRegionRelease(null); -+ mergeTarget.addSection(section); -+ } -+ for (final ThreadedRegionSection deadSection : this.deadSections) { -+ if (this.sectionByKey.get(deadSection.sectionKey) != deadSection) { -+ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); -+ } -+ if (!mergeTarget.deadSections.add(deadSection)) { -+ throw new IllegalStateException("Merge target contains dead section from source! Has " + deadSection + " from region " + this); -+ } -+ } -+ -+ // forward merge expectations -+ for (final ThreadedRegion region : this.expectingMergeFrom) { -+ if (!region.mergeIntoLater.remove(this)) { -+ throw new IllegalStateException("Region " + region + " was not supposed to merge into " + this + "?"); -+ } -+ if (region != mergeTarget) { -+ region.mergeIntoLater(mergeTarget); -+ } -+ } -+ -+ // forward merge into -+ for (final ThreadedRegion region : this.mergeIntoLater) { -+ if (!region.expectingMergeFrom.remove(this)) { -+ throw new IllegalStateException("Region " + this + " was not supposed to merge into " + region + "?"); -+ } -+ if (region != mergeTarget) { -+ mergeTarget.mergeIntoLater(region); -+ } -+ } -+ -+ // finally, merge data -+ if (this.data != null) { -+ this.data.mergeInto(mergeTarget); -+ } -+ } -+ -+ private void mergeIntoLater(final ThreadedRegion region) { -+ if (region.isDead()) { -+ throw new IllegalStateException("Trying to merge later into a dead region: " + region); -+ } -+ final boolean add1, add2; -+ if ((add1 = this.mergeIntoLater.add(region)) != (add2 = region.expectingMergeFrom.add(this))) { -+ throw new IllegalStateException("Inconsistent state between target merge " + region + " and this " + this + ": add1,add2:" + add1 + "," + add2); -+ } -+ } -+ -+ private boolean tryKill() { -+ switch (this.state) { -+ case STATE_TRANSIENT: { -+ this.state = STATE_DEAD; -+ this.onRemove(false); -+ return true; -+ } -+ case STATE_READY: { -+ this.state = STATE_DEAD; -+ this.onRemove(true); -+ return true; -+ } -+ case STATE_TICKING: { -+ return false; -+ } -+ case STATE_DEAD: { -+ throw new IllegalStateException("Already dead"); -+ } -+ default: { -+ throw new IllegalStateException("Unknown state: " + this.state); -+ } -+ } -+ } -+ -+ private boolean isDead() { -+ return this.state == STATE_DEAD; -+ } -+ -+ private boolean isTicking() { -+ return this.state == STATE_TICKING; -+ } -+ -+ private void removeDeadSection(final ThreadedRegionSection section) { -+ this.deadSections.remove(section); -+ } -+ -+ private void addDeadSection(final ThreadedRegionSection section) { -+ this.deadSections.add(section); -+ } -+ -+ private void addSection(final ThreadedRegionSection section) { -+ if (section.getRegionPlain() != null) { -+ throw new IllegalStateException("Section already has region"); -+ } -+ if (this.sectionByKey.putIfAbsent(section.sectionKey, section) != null) { -+ throw new IllegalStateException("Already have section " + section + ", mapped to " + this.sectionByKey.get(section.sectionKey)); -+ } -+ section.setRegionRelease(this); -+ } -+ -+ public R getData() { -+ return this.data; -+ } -+ -+ public boolean tryMarkTicking(final BooleanSupplier abort) { -+ this.regioniser.acquireWriteLock(); -+ try { -+ if (this.state != STATE_READY || abort.getAsBoolean()) { -+ return false; -+ } -+ -+ if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) { -+ throw new IllegalStateException("Region " + this + " should not be ready"); -+ } -+ -+ this.state = STATE_TICKING; -+ return true; -+ } finally { -+ this.regioniser.releaseWriteLock(); -+ } -+ } -+ -+ public boolean markNotTicking() { -+ this.regioniser.acquireWriteLock(); -+ try { -+ if (this.state != STATE_TICKING) { -+ throw new IllegalStateException("Attempting to release non-locked state"); -+ } -+ -+ this.regioniser.onRegionRelease(this); -+ -+ return this.state == STATE_READY; -+ } catch (final Throwable throwable) { -+ LOGGER.error("Failed to release region " + this, throwable); -+ SneakyThrow.sneaky(throwable); -+ return false; // unreachable -+ } finally { -+ this.regioniser.releaseWriteLock(); -+ } -+ } -+ -+ @Override -+ public String toString() { -+ final StringBuilder ret = new StringBuilder(128); -+ -+ ret.append("ThreadedRegion{"); -+ ret.append("state=").append(this.state).append(','); -+ // To avoid recursion in toString, maybe fix later? -+ //ret.append("mergeIntoLater=").append(this.mergeIntoLater).append(','); -+ //ret.append("expectingMergeFrom=").append(this.expectingMergeFrom).append(','); -+ -+ ret.append("sectionCount=").append(this.sectionByKey.size()).append(','); -+ ret.append("sections=["); -+ for (final Iterator> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) { -+ final ThreadedRegionSection section = iterator.next(); -+ -+ ret.append(section.toString()); -+ if (iterator.hasNext()) { -+ ret.append(','); -+ } -+ } -+ ret.append(']'); -+ -+ ret.append('}'); -+ return ret.toString(); -+ } -+ } -+ -+ public static final class ThreadedRegionSection, S extends ThreadedRegionSectionData> { -+ -+ public final int sectionX; -+ public final int sectionZ; -+ public final long sectionKey; -+ private final long[] chunksBitset; -+ private int chunkCount; -+ private int nonEmptyNeighbours; -+ -+ private ThreadedRegion region; -+ private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class); -+ -+ public final ThreadedRegionizer regioniser; -+ -+ private final int regionChunkShift; -+ private final int regionChunkMask; -+ -+ private final S data; -+ -+ private ThreadedRegion getRegionPlain() { -+ return (ThreadedRegion)REGION_HANDLE.get(this); -+ } -+ -+ private ThreadedRegion getRegionAcquire() { -+ return (ThreadedRegion)REGION_HANDLE.getAcquire(this); -+ } -+ -+ private void setRegionRelease(final ThreadedRegion value) { -+ REGION_HANDLE.setRelease(this, value); -+ } -+ -+ // creates an empty section with zero non-empty neighbours -+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser) { -+ this.sectionX = sectionX; -+ this.sectionZ = sectionZ; -+ this.sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); -+ this.chunksBitset = new long[Math.max(1, regioniser.regionSectionChunkSize * regioniser.regionSectionChunkSize / Long.SIZE)]; -+ this.regioniser = regioniser; -+ this.regionChunkShift = regioniser.sectionChunkShift; -+ this.regionChunkMask = regioniser.regionSectionChunkSize - 1; -+ this.data = regioniser.callbacks -+ .createNewSectionData(sectionX, sectionZ, this.regionChunkShift); -+ } -+ -+ // creates a section with an initial chunk with zero non-empty neighbours -+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, -+ final int chunkXInit, final int chunkZInit) { -+ this(sectionX, sectionZ, regioniser); -+ -+ final int initIndex = this.getChunkIndex(chunkXInit, chunkZInit); -+ this.chunkCount = 1; -+ this.chunksBitset[initIndex >>> 6] = 1L << (initIndex & (Long.SIZE - 1)); // index / Long.SIZE -+ } -+ -+ // creates an empty section with the specified number of non-empty neighbours -+ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, -+ final int nonEmptyNeighbours) { -+ this(sectionX, sectionZ, regioniser); -+ -+ this.nonEmptyNeighbours = nonEmptyNeighbours; -+ } -+ -+ public LongArrayList getChunks() { -+ final LongArrayList ret = new LongArrayList(); -+ -+ if (this.chunkCount == 0) { -+ return ret; -+ } -+ -+ final int shift = this.regionChunkShift; -+ final int mask = this.regionChunkMask; -+ final int offsetX = this.sectionX << shift; -+ final int offsetZ = this.sectionZ << shift; -+ -+ final long[] bitset = this.chunksBitset; -+ for (int arrIdx = 0, arrLen = bitset.length; arrIdx < arrLen; ++arrIdx) { -+ long value = bitset[arrIdx]; -+ -+ for (int i = 0, bits = Long.bitCount(value); i < bits; ++i) { -+ final int valueIdx = Long.numberOfTrailingZeros(value); -+ value ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(value); -+ -+ final int idx = valueIdx | (arrIdx << 6); -+ -+ final int localX = idx & mask; -+ final int localZ = (idx >>> shift) & mask; -+ -+ ret.add(CoordinateUtils.getChunkKey(localX | offsetX, localZ | offsetZ)); -+ } -+ } -+ -+ return ret; -+ } -+ -+ private boolean isEmpty() { -+ return this.chunkCount == 0; -+ } -+ -+ private boolean hasOnlyOneChunk() { -+ return this.chunkCount == 1; -+ } -+ -+ public boolean hasNonEmptyNeighbours() { -+ return this.nonEmptyNeighbours != 0; -+ } -+ -+ /** -+ * Returns the section data associated with this region section. May be {@code null}. -+ */ -+ public S getData() { -+ return this.data; -+ } -+ -+ /** -+ * Returns the region that owns this section. Unsynchronised access may produce outdateed or transient results. -+ */ -+ public ThreadedRegion getRegion() { -+ return this.getRegionAcquire(); -+ } -+ -+ private int getChunkIndex(final int chunkX, final int chunkZ) { -+ return (chunkX & this.regionChunkMask) | ((chunkZ & this.regionChunkMask) << this.regionChunkShift); -+ } -+ -+ private void markAlive() { -+ this.getRegionPlain().removeDeadSection(this); -+ } -+ -+ private void markDead() { -+ this.getRegionPlain().addDeadSection(this); -+ } -+ -+ private void incrementNonEmptyNeighbours() { -+ if (++this.nonEmptyNeighbours == 1 && this.chunkCount == 0) { -+ this.markAlive(); -+ } -+ final int createRadius = this.regioniser.emptySectionCreateRadius; -+ if (this.nonEmptyNeighbours >= ((createRadius * 2 + 1) * (createRadius * 2 + 1))) { -+ throw new IllegalStateException("Non empty neighbours exceeded max value for radius " + createRadius); -+ } -+ } -+ -+ private void decrementNonEmptyNeighbours() { -+ if (--this.nonEmptyNeighbours == 0 && this.chunkCount == 0) { -+ this.markDead(); -+ } -+ if (this.nonEmptyNeighbours < 0) { -+ throw new IllegalStateException("Non empty neighbours reached zero"); -+ } -+ } -+ -+ /** -+ * Returns whether the chunk was zero. Effectively returns whether the caller needs to create -+ * dead sections / increase non-empty neighbour count for neighbouring sections. -+ */ -+ private boolean addChunk(final int chunkX, final int chunkZ) { -+ final int index = this.getChunkIndex(chunkX, chunkZ); -+ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE -+ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1))); -+ if (after == bitset) { -+ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); -+ } -+ final boolean notEmpty = ++this.chunkCount == 1; -+ if (notEmpty && this.nonEmptyNeighbours == 0) { -+ this.markAlive(); -+ } -+ return notEmpty; -+ } -+ -+ /** -+ * Returns whether the chunk count is now zero. Effectively returns whether -+ * the caller needs to decrement the neighbour count for neighbouring sections. -+ */ -+ private boolean removeChunk(final int chunkX, final int chunkZ) { -+ final int index = this.getChunkIndex(chunkX, chunkZ); -+ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE -+ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1))); -+ if (before == bitset) { -+ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); -+ } -+ final boolean empty = --this.chunkCount == 0; -+ if (empty && this.nonEmptyNeighbours == 0) { -+ this.markDead(); -+ } -+ return empty; -+ } -+ -+ @Override -+ public String toString() { -+ return "RegionSection{" + -+ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + -+ "chunkCount=" + this.chunkCount + "," + -+ "chunksBitset=" + toString(this.chunksBitset) + "," + -+ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + -+ "hash=" + this.hashCode() + -+ "}"; -+ } -+ -+ public String toStringWithRegion() { -+ return "RegionSection{" + -+ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + -+ "chunkCount=" + this.chunkCount + "," + -+ "chunksBitset=" + toString(this.chunksBitset) + "," + -+ "hash=" + this.hashCode() + "," + -+ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + -+ "region=" + this.getRegionAcquire() + -+ "}"; -+ } -+ -+ private static String toString(final long[] array) { -+ final StringBuilder ret = new StringBuilder(); -+ final char[] zeros = new char[Long.SIZE / 4]; -+ for (final long value : array) { -+ // zero pad the hex string -+ Arrays.fill(zeros, '0'); -+ final String string = Long.toHexString(value); -+ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length()); -+ -+ ret.append(zeros); -+ } -+ -+ return ret.toString(); -+ } -+ } -+ -+ public static interface ThreadedRegionData, S extends ThreadedRegionSectionData> { -+ -+ /** -+ * Splits this region data into the specified regions set. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param regioniser Regioniser for which the regions reside in. -+ * @param into A map of region section coordinate key to the region that owns the section. -+ * @param regions The set of regions to split into. -+ */ -+ public void split(final ThreadedRegionizer regioniser, final Long2ReferenceOpenHashMap> into, -+ final ReferenceOpenHashSet> regions); -+ -+ /** -+ * Callback to merge {@code this} region data into the specified region. The state of the region is undefined -+ * except that its region data is already created. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param into Specified region. -+ */ -+ public void mergeInto(final ThreadedRegion into); -+ } -+ -+ public static interface ThreadedRegionSectionData {} -+ -+ public static interface RegionCallbacks, S extends ThreadedRegionSectionData> { -+ -+ /** -+ * Creates new section data for the specified section x and section z. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param sectionX x coordinate of the section. -+ * @param sectionZ z coordinate of the section. -+ * @param sectionShift The signed right shift value that can be applied to any chunk coordinate that -+ * produces a section coordinate. -+ * @return New section data, may be {@code null}. -+ */ -+ public S createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift); -+ -+ /** -+ * Creates new region data for the specified region. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param forRegion The region to create the data for. -+ * @return New region data, may be {@code null}. -+ */ -+ public R createNewData(final ThreadedRegion forRegion); -+ -+ /** -+ * Callback for when a region is created. This is invoked after the region is completely set up, -+ * so its data and owned sections are reliable to inspect. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param region The region that was created. -+ */ -+ public void onRegionCreate(final ThreadedRegion region); -+ -+ /** -+ * Callback for when a region is destroyed. This is invoked before the region is actually destroyed; so -+ * its data and owned sections are reliable to inspect. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param region The region that is about to be destroyed. -+ */ -+ public void onRegionDestroy(final ThreadedRegion region); -+ -+ /** -+ * Callback for when a region is considered "active." An active region x is a non-destroyed region which -+ * is not scheduled to merge into another region y and there are no non-destroyed regions z which are -+ * scheduled to merge into the region x. Equivalently, an active region is not directly adjacent to any -+ * other region considering the regioniser's empty section radius. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param region The region that is now active. -+ */ -+ public void onRegionActive(final ThreadedRegion region); -+ -+ /** -+ * Callback for when a region transistions becomes inactive. An inactive region is non-destroyed, but -+ * has neighbouring adjacent regions considering the regioniser's empty section radius. Effectively, -+ * an inactive region may not tick and needs to be merged into its neighbouring regions. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param region The region that is now inactive. -+ */ -+ public void onRegionInactive(final ThreadedRegion region); -+ -+ /** -+ * Callback for when a region (from) is about to be merged into a target region (into). Note that -+ * {@code from} is still alive and is a distinct region. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param from The region that will be merged into the target. -+ * @param into The target of the merge. -+ */ -+ public void preMerge(final ThreadedRegion from, final ThreadedRegion into); -+ -+ /** -+ * Callback for when a region (from) is about to be split into a list of target region (into). Note that -+ * {@code from} is still alive, while the list of target regions are not initialised. -+ *

    -+ * Note: -+ *

    -+ *

    -+ * This function is always called while holding critical locks and as such should not attempt to block on anything, and -+ * should NOT retrieve or modify ANY world state. -+ *

    -+ * @param from The region that will be merged into the target. -+ * @param into The list of regions to split into. -+ */ -+ public void preSplit(final ThreadedRegion from, final List> into); -+ } -+} -diff --git a/io/papermc/paper/threadedregions/TickData.java b/io/papermc/paper/threadedregions/TickData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..29f9fed5f02530b3256e6b993e607d4647daa7b6 ---- /dev/null -+++ b/io/papermc/paper/threadedregions/TickData.java -@@ -0,0 +1,333 @@ -+package io.papermc.paper.threadedregions; -+ -+import ca.spottedleaf.concurrentutil.util.TimeUtil; -+import io.papermc.paper.util.IntervalledCounter; -+import it.unimi.dsi.fastutil.longs.LongArrayList; -+ -+import java.util.ArrayDeque; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.List; -+ -+public final class TickData { -+ -+ private final long interval; // ns -+ -+ private final ArrayDeque timeData = new ArrayDeque<>(); -+ -+ public TickData(final long intervalNS) { -+ this.interval = intervalNS; -+ } -+ -+ public void addDataFrom(final TickRegionScheduler.TickTime time) { -+ final long start = time.tickStart(); -+ -+ TickRegionScheduler.TickTime first; -+ while ((first = this.timeData.peekFirst()) != null) { -+ // only remove data completely out of window -+ if ((start - first.tickEnd()) <= this.interval) { -+ break; -+ } -+ this.timeData.pollFirst(); -+ } -+ -+ this.timeData.add(time); -+ } -+ -+ // fromIndex inclusive, toIndex exclusive -+ // will throw if arr.length == 0 -+ private static double median(final long[] arr, final int fromIndex, final int toIndex) { -+ final int len = toIndex - fromIndex; -+ final int middle = fromIndex + (len >>> 1); -+ if ((len & 1) == 0) { -+ // even, average the two middle points -+ return (double)(arr[middle - 1] + arr[middle]) / 2.0; -+ } else { -+ // odd, just grab the middle -+ return (double)arr[middle]; -+ } -+ } -+ -+ // will throw if arr.length == 0 -+ private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex, -+ final boolean inverse) { -+ final int len = toIndex - fromIndex; -+ long sum = 0L; -+ final double median = median(arr, fromIndex, toIndex); -+ long min = arr[0]; -+ long max = arr[0]; -+ -+ for (int i = fromIndex; i < toIndex; ++i) { -+ final long val = arr[i]; -+ sum += val; -+ if (val < min) { -+ min = val; -+ } -+ if (val > max) { -+ max = val; -+ } -+ } -+ -+ if (inverse) { -+ // for positive a,b we have that a >= b if and only if 1/a <= 1/b -+ return new SegmentData( -+ len, -+ (double)len / ((double)sum / 1.0E9), -+ 1.0E9 / median, -+ 1.0E9 / (double)max, -+ 1.0E9 / (double)min -+ ); -+ } else { -+ return new SegmentData( -+ len, -+ (double)sum / (double)len, -+ median, -+ (double)min, -+ (double)max -+ ); -+ } -+ } -+ -+ private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd, -+ final int percent99BestStart, final int percent99BestEnd, -+ final int percent95BestStart, final int percent95BestEnd, -+ final int percent1WorstStart, final int percent1WorstEnd, -+ final int percent5WorstStart, final int percent5WorstEnd, -+ final boolean inverse) { -+ return new SegmentedAverage( -+ computeSegmentData(data, allStart, allEnd, inverse), -+ computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse), -+ computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse), -+ computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse), -+ computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse) -+ ); -+ } -+ -+ private static record TickInformation( -+ long differenceFromLastTick, -+ long tickTime, -+ long tickTimeCPU -+ ) {} -+ -+ // rets null if there is no data -+ public TickReportData generateTickReport(final TickRegionScheduler.TickTime inProgress, final long endTime) { -+ if (this.timeData.isEmpty() && inProgress == null) { -+ return null; -+ } -+ -+ final List allData = new ArrayList<>(this.timeData); -+ if (inProgress != null) { -+ allData.add(inProgress); -+ } -+ -+ final long intervalStart = allData.get(0).tickStart(); -+ final long intervalEnd = allData.get(allData.size() - 1).tickEnd(); -+ -+ // to make utilisation accurate, we need to take the total time used over the last interval period - -+ // this means if a tick start before the measurement interval, but ends within the interval, then we -+ // only consider the time it spent ticking inside the interval -+ long totalTimeOverInterval = 0L; -+ long measureStart = endTime - this.interval; -+ -+ for (int i = 0, len = allData.size(); i < len; ++i) { -+ final TickRegionScheduler.TickTime time = allData.get(i); -+ if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) { -+ final long diff = time.tickEnd() - measureStart; -+ if (diff > 0L) { -+ totalTimeOverInterval += diff; -+ } // else: the time is entirely out of interval -+ } else { -+ totalTimeOverInterval += time.tickLength(); -+ } -+ } -+ -+ // we only care about ticks, but because of inbetween tick task execution -+ // there will be data in allData that isn't ticks. But, that data cannot -+ // be ignored since it contributes to utilisation. -+ // So, we will "compact" the data by merging any inbetween tick times -+ // the next tick. -+ // If there is no "next tick", then we will create one. -+ final List collapsedData = new ArrayList<>(); -+ for (int i = 0, len = allData.size(); i < len; ++i) { -+ final List toCollapse = new ArrayList<>(); -+ TickRegionScheduler.TickTime lastTick = null; -+ for (;i < len; ++i) { -+ final TickRegionScheduler.TickTime time = allData.get(i); -+ if (!time.isTickExecution()) { -+ toCollapse.add(time); -+ continue; -+ } -+ lastTick = time; -+ break; -+ } -+ -+ if (toCollapse.isEmpty()) { -+ // nothing to collapse -+ final TickRegionScheduler.TickTime last = allData.get(i); -+ collapsedData.add( -+ new TickInformation( -+ last.differenceFromLastTick(), -+ last.tickLength(), -+ last.supportCPUTime() ? last.tickCpuTime() : 0L -+ ) -+ ); -+ } else { -+ long totalTickTime = 0L; -+ long totalCpuTime = 0L; -+ for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) { -+ final TickRegionScheduler.TickTime time = toCollapse.get(k); -+ totalTickTime += time.tickLength(); -+ totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L; -+ } -+ if (i < len) { -+ // we know there is a tick to collapse into -+ final TickRegionScheduler.TickTime last = allData.get(i); -+ collapsedData.add( -+ new TickInformation( -+ last.differenceFromLastTick(), -+ last.tickLength() + totalTickTime, -+ (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime -+ ) -+ ); -+ } else { -+ // we do not have a tick to collapse into, so we must make one up -+ // we will assume that the tick is "starting now" and ongoing -+ -+ // compute difference between imaginary tick and last tick -+ final long differenceBetweenTicks; -+ if (lastTick != null) { -+ // we have a last tick, use it -+ differenceBetweenTicks = lastTick.tickStart(); -+ } else { -+ // we don't have a last tick, so we must make one up that makes sense -+ // if the current interval exceeds the max tick time, then use it -+ -+ // Otherwise use the interval length. -+ // This is how differenceFromLastTick() works on TickTime when there is no previous interval. -+ differenceBetweenTicks = Math.max( -+ TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime -+ ); -+ } -+ -+ collapsedData.add( -+ new TickInformation( -+ differenceBetweenTicks, -+ totalTickTime, -+ totalCpuTime -+ ) -+ ); -+ } -+ } -+ } -+ -+ -+ final int collectedTicks = collapsedData.size(); -+ final long[] tickStartToStartDifferences = new long[collectedTicks]; -+ final long[] timePerTickDataRaw = new long[collectedTicks]; -+ final long[] missingCPUTimeDataRaw = new long[collectedTicks]; -+ -+ long totalTimeTicking = 0L; -+ -+ int i = 0; -+ for (final TickInformation time : collapsedData) { -+ tickStartToStartDifferences[i] = time.differenceFromLastTick(); -+ final long timePerTick = timePerTickDataRaw[i] = time.tickTime(); -+ missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU()); -+ -+ ++i; -+ -+ totalTimeTicking += timePerTick; -+ } -+ -+ Arrays.sort(tickStartToStartDifferences); -+ Arrays.sort(timePerTickDataRaw); -+ Arrays.sort(missingCPUTimeDataRaw); -+ -+ // Note: computeSegmentData cannot take start == end -+ final int allStart = 0; -+ final int allEnd = collectedTicks; -+ final int percent95BestStart = 0; -+ final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks); -+ final int percent99BestStart = 0; -+ // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end -+ final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks); -+ final int percent1WorstStart = (int)(0.99 * collectedTicks); -+ final int percent1WorstEnd = collectedTicks; -+ final int percent5WorstStart = (int)(0.95 * collectedTicks); -+ final int percent5WorstEnd = collectedTicks; -+ -+ final SegmentedAverage tpsData = computeSegmentedAverage( -+ tickStartToStartDifferences, -+ allStart, allEnd, -+ percent99BestStart, percent99BestEnd, -+ percent95BestStart, percent95BestEnd, -+ percent1WorstStart, percent1WorstEnd, -+ percent5WorstStart, percent5WorstEnd, -+ true -+ ); -+ -+ final SegmentedAverage timePerTickData = computeSegmentedAverage( -+ timePerTickDataRaw, -+ allStart, allEnd, -+ percent99BestStart, percent99BestEnd, -+ percent95BestStart, percent95BestEnd, -+ percent1WorstStart, percent1WorstEnd, -+ percent5WorstStart, percent5WorstEnd, -+ false -+ ); -+ -+ final SegmentedAverage missingCPUTimeData = computeSegmentedAverage( -+ missingCPUTimeDataRaw, -+ allStart, allEnd, -+ percent99BestStart, percent99BestEnd, -+ percent95BestStart, percent95BestEnd, -+ percent1WorstStart, percent1WorstEnd, -+ percent5WorstStart, percent5WorstEnd, -+ false -+ ); -+ -+ final double utilisation = (double)totalTimeOverInterval / (double)this.interval; -+ -+ return new TickReportData( -+ collectedTicks, -+ intervalStart, -+ intervalEnd, -+ totalTimeTicking, -+ utilisation, -+ -+ tpsData, -+ timePerTickData, -+ missingCPUTimeData -+ ); -+ } -+ -+ public static final record TickReportData( -+ int collectedTicks, -+ long collectedTickIntervalStart, -+ long collectedTickIntervalEnd, -+ long totalTimeTicking, -+ double utilisation, -+ -+ SegmentedAverage tpsData, -+ // in ns -+ SegmentedAverage timePerTickData, -+ // in ns -+ SegmentedAverage missingCPUTimeData -+ ) {} -+ -+ public static final record SegmentedAverage( -+ SegmentData segmentAll, -+ SegmentData segment99PercentBest, -+ SegmentData segment95PercentBest, -+ SegmentData segment5PercentWorst, -+ SegmentData segment1PercentWorst -+ ) {} -+ -+ public static final record SegmentData( -+ int count, -+ double average, -+ double median, -+ double least, -+ double greatest -+ ) {} -+} -diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4471285a4358e51da9912ed791a824527f1a2e8e ---- /dev/null -+++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java -@@ -0,0 +1,564 @@ -+package io.papermc.paper.threadedregions; -+ -+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; -+import ca.spottedleaf.concurrentutil.util.TimeUtil; -+import ca.spottedleaf.moonrise.common.util.TickThread; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.util.TraceUtil; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.ChunkPos; -+import org.slf4j.Logger; -+import java.lang.management.ManagementFactory; -+import java.lang.management.ThreadMXBean; -+import java.util.concurrent.ThreadFactory; -+import java.util.concurrent.TimeUnit; -+import java.util.concurrent.atomic.AtomicBoolean; -+import java.util.concurrent.atomic.AtomicInteger; -+import java.util.function.BooleanSupplier; -+ -+public final class TickRegionScheduler { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); -+ private static final boolean MEASURE_CPU_TIME; -+ static { -+ MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported(); -+ if (MEASURE_CPU_TIME) { -+ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true); -+ } else { -+ LOGGER.warn("TickRegionScheduler CPU time measurement is not available"); -+ } -+ } -+ -+ public static final int TICK_RATE = 20; -+ public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns -+ -+ private final SchedulerThreadPool scheduler; -+ -+ public TickRegionScheduler(final int threads) { -+ this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() { -+ private final AtomicInteger idGenerator = new AtomicInteger(); -+ -+ @Override -+ public Thread newThread(final Runnable run) { -+ final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement()); -+ ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException); -+ return ret; -+ } -+ }); -+ } -+ -+ public int getTotalThreadCount() { -+ return this.scheduler.getThreads().length; -+ } -+ -+ private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion region) { -+ final Thread currThread = Thread.currentThread(); -+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { -+ throw new IllegalStateException("Must be tick thread runner"); -+ } -+ if (region != null && tickThreadRunner.currentTickingRegion != null) { -+ throw new IllegalStateException("Trying to double set ticking region!"); -+ } -+ if (region == null && tickThreadRunner.currentTickingRegion == null) { -+ throw new IllegalStateException("Trying to double unset ticking region!"); -+ } -+ tickThreadRunner.currentTickingRegion = region; -+ if (region != null) { -+ tickThreadRunner.currentTickingWorldRegionizedData = region.regioniser.world.worldRegionData.get(); -+ } else { -+ tickThreadRunner.currentTickingWorldRegionizedData = null; -+ } -+ } -+ -+ private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) { -+ final Thread currThread = Thread.currentThread(); -+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { -+ throw new IllegalStateException("Must be tick thread runner"); -+ } -+ if (task != null && tickThreadRunner.currentTickingTask != null) { -+ throw new IllegalStateException("Trying to double set ticking task!"); -+ } -+ if (task == null && tickThreadRunner.currentTickingTask == null) { -+ throw new IllegalStateException("Trying to double unset ticking task!"); -+ } -+ tickThreadRunner.currentTickingTask = task; -+ } -+ -+ /** -+ * Returns the current ticking region, or {@code null} if there is no ticking region. -+ * If this thread is not a TickThread, then returns {@code null}. -+ */ -+ public static ThreadedRegionizer.ThreadedRegion getCurrentRegion() { -+ final Thread currThread = Thread.currentThread(); -+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { -+ return RegionShutdownThread.getRegion(); -+ } -+ return tickThreadRunner.currentTickingRegion; -+ } -+ -+ /** -+ * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region. -+ * This is a faster alternative to calling the {@link RegionizedData#get()} method. -+ * If this thread is not a TickThread, then returns {@code null}. -+ */ -+ public static RegionizedWorldData getCurrentRegionizedWorldData() { -+ final Thread currThread = Thread.currentThread(); -+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { -+ return RegionShutdownThread.getWorldData(); -+ } -+ return tickThreadRunner.currentTickingWorldRegionizedData; -+ } -+ -+ /** -+ * Returns the current ticking task, or {@code null} if there is no ticking region. -+ * If this thread is not a TickThread, then returns {@code null}. -+ */ -+ public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() { -+ final Thread currThread = Thread.currentThread(); -+ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { -+ return null; -+ } -+ return tickThreadRunner.currentTickingTask; -+ } -+ -+ /** -+ * Schedules the given region -+ * @throws IllegalStateException If the region is already scheduled or is ticking -+ */ -+ public void scheduleRegion(final RegionScheduleHandle region) { -+ region.scheduler = this; -+ this.scheduler.schedule(region); -+ } -+ -+ /** -+ * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task -+ * execution, then it will be cancelled after. -+ */ -+ public void descheduleRegion(final RegionScheduleHandle region) { -+ // To avoid acquiring any of the locks the scheduler may be using, we -+ // simply cancel the next action. -+ region.markNonSchedulable(); -+ } -+ -+ /** -+ * Updates the tick start to the farthest into the future of its current scheduled time and the -+ * provided time. -+ * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its -+ * current start time, {@code true} if the next tick start was adjusted. -+ */ -+ public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) { -+ return this.scheduler.updateTickStartToMax(region, newStart); -+ } -+ -+ public boolean halt(final boolean sync, final long maxWaitNS) { -+ return this.scheduler.halt(sync, maxWaitNS); -+ } -+ -+ void dumpAliveThreadTraces(final String reason) { -+ for (final Thread thread : this.scheduler.getThreads()) { -+ if (thread.isAlive()) { -+ TraceUtil.dumpTraceForThread(thread, reason); -+ } -+ } -+ } -+ -+ public void setHasTasks(final RegionScheduleHandle region) { -+ this.scheduler.notifyTasks(region); -+ } -+ -+ public void init() { -+ this.scheduler.start(); -+ } -+ -+ private void uncaughtException(final Thread thread, final Throwable thr) { -+ LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr); -+ -+ // prevent further ticks from occurring -+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD -+ this.scheduler.halt(false, 0L); -+ -+ MinecraftServer.getServer().stopServer(); -+ } -+ -+ private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) { -+ // when a region fails, we need to shut down the server gracefully -+ -+ // prevent further ticks from occurring -+ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD -+ this.scheduler.halt(false, 0L); -+ -+ final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk(); -+ final ServerLevel world = handle.region == null ? null : handle.region.world; -+ -+ LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " in world '" + (world == null ? "null" : world.getWorld().getName()) + "' failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr); -+ -+ MinecraftServer.getServer().stopServer(); -+ } -+ -+ // By using our own thread object, we can use a field for the current region rather than a ThreadLocal. -+ // This is much faster than a thread local, since the thread local has to use a map lookup. -+ private static final class TickThreadRunner extends TickThread { -+ -+ private ThreadedRegionizer.ThreadedRegion currentTickingRegion; -+ private RegionizedWorldData currentTickingWorldRegionizedData; -+ private SchedulerThreadPool.SchedulableTick currentTickingTask; -+ -+ public TickThreadRunner(final Runnable run, final String name) { -+ super(run, name); -+ } -+ } -+ -+ public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick { -+ -+ protected long currentTick; -+ protected long lastTickStart; -+ -+ protected final TickData tickTimes5s; -+ protected final TickData tickTimes15s; -+ protected final TickData tickTimes1m; -+ protected final TickData tickTimes5m; -+ protected final TickData tickTimes15m; -+ protected TickTime currentTickData; -+ protected Thread currentTickingThread; -+ -+ public final TickRegions.TickRegionData region; -+ private final AtomicBoolean cancelled = new AtomicBoolean(); -+ -+ protected final Schedule tickSchedule; -+ -+ private TickRegionScheduler scheduler; -+ -+ public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) { -+ this.currentTick = 0L; -+ this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET; -+ this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L)); -+ this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L)); -+ this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L)); -+ this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L)); -+ this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L)); -+ this.region = region; -+ -+ this.setScheduledStart(firstStart); -+ this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS); -+ } -+ -+ /** -+ * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)} -+ * so that the tick schedule and scheduled start remain synchronised -+ */ -+ protected final void updateScheduledStart(final long to) { -+ this.setScheduledStart(to); -+ this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS); -+ } -+ -+ public final void markNonSchedulable() { -+ this.cancelled.set(true); -+ } -+ -+ public final boolean isMarkedAsNonSchedulable() { -+ return this.cancelled.get(); -+ } -+ -+ protected abstract boolean tryMarkTicking(); -+ -+ protected abstract boolean markNotTicking(); -+ -+ protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd); -+ -+ protected abstract boolean runRegionTasks(final BooleanSupplier canContinue); -+ -+ protected abstract boolean hasIntermediateTasks(); -+ -+ @Override -+ public final boolean hasTasks() { -+ return this.hasIntermediateTasks(); -+ } -+ -+ @Override -+ public final Boolean runTasks(final BooleanSupplier canContinue) { -+ if (this.cancelled.get()) { -+ return null; -+ } -+ -+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; -+ final long tickStart = System.nanoTime(); -+ -+ if (!this.tryMarkTicking()) { -+ if (!this.cancelled.get()) { -+ throw new IllegalStateException("Scheduled region should be acquirable"); -+ } -+ // region was killed -+ return null; -+ } -+ -+ TickRegionScheduler.setTickTask(this); -+ if (this.region != null) { -+ TickRegionScheduler.setTickingRegion(this.region.region); -+ } -+ -+ synchronized (this) { -+ this.currentTickData = new TickTime( -+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart, -+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, -+ false -+ ); -+ this.currentTickingThread = Thread.currentThread(); -+ } -+ -+ final boolean ret; -+ try { -+ ret = this.runRegionTasks(() -> { -+ return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean(); -+ }); -+ } catch (final Throwable thr) { -+ this.scheduler.regionFailed(this, true, thr); -+ // don't release region for another tick -+ return null; -+ } finally { -+ final long tickEnd = System.nanoTime(); -+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; -+ -+ final TickTime time = new TickTime( -+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, -+ tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false -+ ); -+ -+ this.addTickTime(time); -+ TickRegionScheduler.setTickTask(null); -+ if (this.region != null) { -+ TickRegionScheduler.setTickingRegion(null); -+ } -+ } -+ -+ return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret); -+ } -+ -+ @Override -+ public final boolean runTick() { -+ // Remember, we are supposed use setScheduledStart if we return true here, otherwise -+ // the scheduler will try to schedule for the same time. -+ if (this.cancelled.get()) { -+ return false; -+ } -+ -+ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; -+ final long tickStart = System.nanoTime(); -+ -+ // use max(), don't assume that tickStart >= scheduledStart -+ final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart)); -+ -+ if (!this.tryMarkTicking()) { -+ if (!this.cancelled.get()) { -+ throw new IllegalStateException("Scheduled region should be acquirable"); -+ } -+ // region was killed -+ return false; -+ } -+ if (this.cancelled.get()) { -+ this.markNotTicking(); -+ // region should be killed -+ return false; -+ } -+ -+ TickRegionScheduler.setTickTask(this); -+ if (this.region != null) { -+ TickRegionScheduler.setTickingRegion(this.region.region); -+ } -+ this.incrementTickCount(); -+ final long lastTickStart = this.lastTickStart; -+ this.lastTickStart = tickStart; -+ -+ final long scheduledStart = this.getScheduledStart(); -+ final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS; -+ -+ synchronized (this) { -+ this.currentTickData = new TickTime( -+ lastTickStart, scheduledStart, tickStart, cpuStart, -+ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, -+ true -+ ); -+ this.currentTickingThread = Thread.currentThread(); -+ } -+ -+ try { -+ // next start isn't updated until the end of this tick -+ this.tickRegion(tickCount, tickStart, scheduledEnd); -+ } catch (final Throwable thr) { -+ this.scheduler.regionFailed(this, false, thr); -+ // regionFailed will schedule a shutdown, so we should avoid letting this region tick further -+ return false; -+ } finally { -+ final long tickEnd = System.nanoTime(); -+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; -+ -+ // in order to ensure all regions get their chance at scheduling, we have to ensure that regions -+ // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest -+ // of the current time and "ideal" next tick start. -+ this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS); -+ this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS))); -+ -+ final TickTime time = new TickTime( -+ lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true -+ ); -+ -+ this.addTickTime(time); -+ TickRegionScheduler.setTickTask(null); -+ if (this.region != null) { -+ TickRegionScheduler.setTickingRegion(null); -+ } -+ } -+ -+ // Only AFTER updating the tickStart -+ return this.markNotTicking() && !this.cancelled.get(); -+ } -+ -+ /** -+ * Only safe to call if this tick data matches the current ticking region. -+ */ -+ protected void addTickTime(final TickTime time) { -+ synchronized (this) { -+ this.currentTickData = null; -+ this.currentTickingThread = null; -+ this.tickTimes5s.addDataFrom(time); -+ this.tickTimes15s.addDataFrom(time); -+ this.tickTimes1m.addDataFrom(time); -+ this.tickTimes5m.addDataFrom(time); -+ this.tickTimes15m.addDataFrom(time); -+ } -+ } -+ -+ private TickTime adjustCurrentTickData(final long tickEnd) { -+ final TickTime currentTickData = this.currentTickData; -+ if (currentTickData == null) { -+ return null; -+ } -+ -+ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L; -+ -+ return new TickTime( -+ currentTickData.previousTickStart(), currentTickData.scheduledTickStart(), -+ currentTickData.tickStart(), currentTickData.tickStartCPU(), -+ tickEnd, cpuEnd, -+ MEASURE_CPU_TIME, currentTickData.isTickExecution() -+ ); -+ } -+ -+ public final TickData.TickReportData getTickReport5s(final long currTime) { -+ synchronized (this) { -+ return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); -+ } -+ } -+ -+ public final TickData.TickReportData getTickReport15s(final long currTime) { -+ synchronized (this) { -+ return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); -+ } -+ } -+ -+ public final TickData.TickReportData getTickReport1m(final long currTime) { -+ synchronized (this) { -+ return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); -+ } -+ } -+ -+ public final TickData.TickReportData getTickReport5m(final long currTime) { -+ synchronized (this) { -+ return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); -+ } -+ } -+ -+ public final TickData.TickReportData getTickReport15m(final long currTime) { -+ synchronized (this) { -+ return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); -+ } -+ } -+ -+ /** -+ * Only safe to call if this tick data matches the current ticking region. -+ */ -+ private void incrementTickCount() { -+ ++this.currentTick; -+ } -+ -+ /** -+ * Only safe to call if this tick data matches the current ticking region. -+ */ -+ public final long getCurrentTick() { -+ return this.currentTick; -+ } -+ -+ protected final void setCurrentTick(final long value) { -+ this.currentTick = value; -+ } -+ } -+ -+ // All time units are in nanoseconds. -+ public static final record TickTime( -+ long previousTickStart, -+ long scheduledTickStart, -+ long tickStart, -+ long tickStartCPU, -+ long tickEnd, -+ long tickEndCPU, -+ boolean supportCPUTime, -+ boolean isTickExecution -+ ) { -+ /** -+ * The difference between the start tick time and the scheduled start tick time. This value is -+ * < 0 if the tick started before the scheduled tick time. -+ * Only valid when {@link #isTickExecution()} is {@code true}. -+ */ -+ public final long startOvershoot() { -+ return this.tickStart - this.scheduledTickStart; -+ } -+ -+ /** -+ * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong). -+ */ -+ public final long tickLength() { -+ return this.tickEnd - this.tickStart; -+ } -+ -+ /** -+ * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength, -+ * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE -+ * if CPU time measurement is not supported. -+ */ -+ public final long tickCpuTime() { -+ if (!this.supportCPUTime()) { -+ return Long.MIN_VALUE; -+ } -+ return this.tickEndCPU - this.tickStartCPU; -+ } -+ -+ /** -+ * The difference in time from the start of the last tick to the start of the current tick. If there is no -+ * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength). -+ * Only valid when {@link #isTickExecution()} is {@code true}. -+ */ -+ public final long differenceFromLastTick() { -+ if (this.hasLastTick()) { -+ return this.tickStart - this.previousTickStart; -+ } -+ return Math.max(TIME_BETWEEN_TICKS, this.tickLength()); -+ } -+ -+ /** -+ * Returns whether there was a tick that occurred before this one. -+ * Only valid when {@link #isTickExecution()} is {@code true}. -+ */ -+ public boolean hasLastTick() { -+ return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET; -+ } -+ -+ /* -+ * Remember, this is the expected behavior of the following: -+ * -+ * MSPT: Time per tick. This does not include overshoot time, just the tickLength(). -+ * -+ * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick). -+ */ -+ } -+} -diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java -index 8424cf9d4617b4732d44cc460d25b04481068989..df15b1139e71dfe10b8f24ec6d235b99f6d5006a 100644 ---- a/io/papermc/paper/threadedregions/TickRegions.java -+++ b/io/papermc/paper/threadedregions/TickRegions.java -@@ -1,10 +1,410 @@ - package io.papermc.paper.threadedregions; - --// placeholder class for Folia --public class TickRegions { -+import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; -+import ca.spottedleaf.concurrentutil.util.TimeUtil; -+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.configuration.GlobalConfiguration; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; -+import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerLevel; -+import org.slf4j.Logger; -+import java.util.Iterator; -+import java.util.concurrent.TimeUnit; -+import java.util.concurrent.atomic.AtomicInteger; -+import java.util.concurrent.atomic.AtomicLong; -+import java.util.function.BooleanSupplier; -+ -+public final class TickRegions implements ThreadedRegionizer.RegionCallbacks { -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ private static int regionShift = 31; - - public static int getRegionChunkShift() { -- return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT; -+ return regionShift; -+ } -+ -+ private static boolean initialised; -+ private static TickRegionScheduler scheduler; -+ -+ public static TickRegionScheduler getScheduler() { -+ return scheduler; -+ } -+ -+ public static void init(final GlobalConfiguration.ThreadedRegions config) { -+ if (initialised) { -+ return; -+ } -+ initialised = true; -+ int gridExponent = config.gridExponent; -+ gridExponent = Math.max(0, gridExponent); -+ gridExponent = Math.min(31, gridExponent); -+ regionShift = gridExponent; -+ -+ int tickThreads; -+ if (config.threads <= 0) { -+ tickThreads = Runtime.getRuntime().availableProcessors() / 2; -+ if (tickThreads <= 4) { -+ tickThreads = 1; -+ } else { -+ tickThreads = tickThreads / 4; -+ } -+ } else { -+ tickThreads = config.threads; -+ } -+ -+ scheduler = new TickRegionScheduler(tickThreads); -+ LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads"); -+ } -+ -+ @Override -+ public TickRegionData createNewData(final ThreadedRegionizer.ThreadedRegion region) { -+ return new TickRegionData(region); -+ } -+ -+ @Override -+ public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) { -+ return null; -+ } -+ -+ @Override -+ public void onRegionCreate(final ThreadedRegionizer.ThreadedRegion region) { -+ final TickRegionData data = region.getData(); -+ // post-region merge/split regioninfo update -+ data.getRegionStats().updateFrom(data.getOrCreateRegionizedData(data.world.worldRegionData)); -+ } -+ -+ @Override -+ public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion region) { -+ // nothing for now -+ } -+ -+ @Override -+ public void onRegionActive(final ThreadedRegionizer.ThreadedRegion region) { -+ final TickRegionData data = region.getData(); -+ -+ data.tickHandle.checkInitialSchedule(); -+ scheduler.scheduleRegion(data.tickHandle); -+ } -+ -+ @Override -+ public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion region) { -+ final TickRegionData data = region.getData(); -+ -+ scheduler.descheduleRegion(data.tickHandle); -+ // old handle cannot be scheduled anymore, copy to a new handle -+ data.tickHandle = data.tickHandle.copy(); -+ } -+ -+ @Override -+ public void preMerge(final ThreadedRegionizer.ThreadedRegion from, -+ final ThreadedRegionizer.ThreadedRegion into) { -+ -+ } -+ -+ @Override -+ public void preSplit(final ThreadedRegionizer.ThreadedRegion from, -+ final java.util.List> into) { -+ -+ } -+ -+ public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {} -+ -+ public static final class RegionStats { -+ -+ private final AtomicInteger entityCount = new AtomicInteger(); -+ private final AtomicInteger playerCount = new AtomicInteger(); -+ private final AtomicInteger chunkCount = new AtomicInteger(); -+ -+ public int getEntityCount() { -+ return this.entityCount.get(); -+ } -+ -+ public int getPlayerCount() { -+ return this.playerCount.get(); -+ } -+ -+ public int getChunkCount() { -+ return this.chunkCount.get(); -+ } -+ -+ void updateFrom(final RegionizedWorldData data) { -+ this.entityCount.setRelease(data == null ? 0 : data.getEntityCount()); -+ this.playerCount.setRelease(data == null ? 0 : data.getPlayerCount()); -+ this.chunkCount.setRelease(data == null ? 0 : data.getChunkCount()); -+ } -+ -+ static void updateCurrentRegion() { -+ TickRegionScheduler.getCurrentRegion().getData().getRegionStats().updateFrom(TickRegionScheduler.getCurrentRegionizedWorldData()); -+ } -+ } -+ -+ public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData { -+ -+ private static final AtomicLong ID_GENERATOR = new AtomicLong(); -+ /** Never 0L, since 0L is reserved for global region. */ -+ public final long id = ID_GENERATOR.incrementAndGet(); -+ -+ public final ThreadedRegionizer.ThreadedRegion region; -+ public final ServerLevel world; -+ -+ // generic regionised data -+ private final Reference2ReferenceOpenHashMap, Object> regionizedData = new Reference2ReferenceOpenHashMap<>(); -+ -+ // tick data -+ private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET); -+ -+ // queue data -+ private final RegionizedTaskQueue.RegionTaskQueueData taskQueueData; -+ -+ // chunk holder manager data -+ private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData(); -+ -+ // async-safe read-only region data -+ private final RegionStats regionStats; -+ -+ private TickRegionData(final ThreadedRegionizer.ThreadedRegion region) { -+ this.region = region; -+ this.world = region.regioniser.world; -+ this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData); -+ this.regionStats = new RegionStats(); -+ } -+ -+ public RegionStats getRegionStats() { -+ return this.regionStats; -+ } -+ -+ public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() { -+ return this.taskQueueData; -+ } -+ -+ // the value returned can be invalidated at any time, except when the caller -+ // is ticking this region -+ public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() { -+ return this.tickHandle; -+ } -+ -+ public long getCurrentTick() { -+ return this.tickHandle.getCurrentTick(); -+ } -+ -+ public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() { -+ return this.holderManagerRegionData; -+ } -+ -+ T getRegionizedData(final RegionizedData regionizedData) { -+ return (T)this.regionizedData.get(regionizedData); -+ } -+ -+ T getOrCreateRegionizedData(final RegionizedData regionizedData) { -+ T ret = (T)this.regionizedData.get(regionizedData); -+ -+ if (ret != null) { -+ return ret; -+ } -+ -+ ret = regionizedData.createNewValue(); -+ this.regionizedData.put(regionizedData, ret); -+ -+ return ret; -+ } -+ -+ @Override -+ public void split(final ThreadedRegionizer regioniser, -+ final Long2ReferenceOpenHashMap> into, -+ final ReferenceOpenHashSet> regions) { -+ final int shift = regioniser.sectionChunkShift; -+ -+ // tick data -+ // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled), -+ // and the other regions to split into are not scheduled yet. -+ for (final ThreadedRegionizer.ThreadedRegion region : regions) { -+ final TickRegionData data = region.getData(); -+ data.tickHandle.copyDeadlineAndTickCount(this.tickHandle); -+ } -+ -+ // generic regionised data -+ for (final Iterator, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); -+ dataIterator.hasNext();) { -+ final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next(); -+ final RegionizedData data = regionDataEntry.getKey(); -+ final Object from = regionDataEntry.getValue(); -+ -+ final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); -+ -+ for (final ThreadedRegionizer.ThreadedRegion region : regions) { -+ dataSet.add(region.getData().getOrCreateRegionizedData(data)); -+ } -+ -+ final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); -+ -+ for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); -+ regionIterator.hasNext();) { -+ final Long2ReferenceMap.Entry> entry = regionIterator.next(); -+ final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); -+ final Object to = region.getData().getOrCreateRegionizedData(data); -+ -+ regionToData.put(entry.getLongKey(), to); -+ } -+ -+ ((RegionizedData)data).getCallback().split(from, shift, regionToData, dataSet); -+ } -+ -+ // chunk holder manager data -+ { -+ final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); -+ -+ for (final ThreadedRegionizer.ThreadedRegion region : regions) { -+ dataSet.add(region.getData().holderManagerRegionData); -+ } -+ -+ final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); -+ -+ for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); -+ regionIterator.hasNext();) { -+ final Long2ReferenceMap.Entry> entry = regionIterator.next(); -+ final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); -+ final ChunkHolderManager.HolderManagerRegionData to = region.getData().holderManagerRegionData; -+ -+ regionToData.put(entry.getLongKey(), to); -+ } -+ -+ this.holderManagerRegionData.split(shift, regionToData, dataSet); -+ } -+ -+ // task queue -+ this.taskQueueData.split(regioniser, into); -+ } -+ -+ @Override -+ public void mergeInto(final ThreadedRegionizer.ThreadedRegion into) { -+ // Note: merge target is always a region being released from ticking -+ final TickRegionData data = into.getData(); -+ final long currentTickTo = data.getCurrentTick(); -+ final long currentTickFrom = this.getCurrentTick(); -+ -+ // here we can access tickHandle because the target (into) is the region being released, so it is -+ // not actually scheduled -+ // there's not really a great solution to the tick problem, no matter what it'll be messed up -+ // we will pick the greatest time delay so that tps will not exceed TICK_RATE -+ data.tickHandle.updateSchedulingToMax(this.tickHandle); -+ -+ // generic regionised data -+ final long fromTickOffset = currentTickTo - currentTickFrom; // see merge jd -+ for (final Iterator, Object>> iterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Reference2ReferenceMap.Entry, Object> entry = iterator.next(); -+ final RegionizedData regionizedData = entry.getKey(); -+ final Object from = entry.getValue(); -+ final Object to = into.getData().getOrCreateRegionizedData(regionizedData); -+ -+ ((RegionizedData)regionizedData).getCallback().merge(from, to, fromTickOffset); -+ } -+ -+ // chunk holder manager data -+ this.holderManagerRegionData.merge(into.getData().holderManagerRegionData, fromTickOffset); -+ -+ // task queue -+ this.taskQueueData.mergeInto(data.taskQueueData); -+ } -+ } -+ -+ private static final class ConcreteRegionTickHandle extends TickRegionScheduler.RegionScheduleHandle { -+ -+ private final TickRegionData region; -+ -+ private ConcreteRegionTickHandle(final TickRegionData region, final long start) { -+ super(region, start); -+ this.region = region; -+ } -+ -+ private ConcreteRegionTickHandle copy() { -+ final ConcreteRegionTickHandle ret = new ConcreteRegionTickHandle(this.region, this.getScheduledStart()); -+ -+ ret.currentTick = this.currentTick; -+ ret.lastTickStart = this.lastTickStart; -+ ret.tickSchedule.setLastPeriod(this.tickSchedule.getLastPeriod()); -+ -+ return ret; -+ } -+ -+ private void updateSchedulingToMax(final ConcreteRegionTickHandle from) { -+ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { -+ return; -+ } -+ -+ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { -+ this.updateScheduledStart(from.getScheduledStart()); -+ return; -+ } -+ -+ this.updateScheduledStart(TimeUtil.getGreatestTime(from.getScheduledStart(), this.getScheduledStart())); -+ } -+ -+ private void copyDeadlineAndTickCount(final ConcreteRegionTickHandle from) { -+ this.currentTick = from.currentTick; -+ -+ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { -+ return; -+ } -+ -+ this.tickSchedule.setLastPeriod(from.tickSchedule.getLastPeriod()); -+ this.setScheduledStart(from.getScheduledStart()); -+ } -+ -+ private void checkInitialSchedule() { -+ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { -+ this.updateScheduledStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); -+ } -+ } -+ -+ @Override -+ protected boolean tryMarkTicking() { -+ return this.region.region.tryMarkTicking(ConcreteRegionTickHandle.this::isMarkedAsNonSchedulable); -+ } -+ -+ @Override -+ protected boolean markNotTicking() { -+ return this.region.region.markNotTicking(); -+ } -+ -+ @Override -+ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { -+ MinecraftServer.getServer().tickServer(startTime, scheduledEnd, TimeUnit.MILLISECONDS.toMillis(10L), this.region); -+ } -+ -+ @Override -+ protected boolean runRegionTasks(final BooleanSupplier canContinue) { -+ final RegionizedTaskQueue.RegionTaskQueueData queue = this.region.taskQueueData; -+ -+ boolean processedChunkTask = false; -+ -+ boolean executeChunkTask = true; -+ boolean executeTickTask = true; -+ do { -+ if (executeTickTask) { -+ executeTickTask = queue.executeTickTask(); -+ } -+ if (executeChunkTask) { -+ processedChunkTask |= (executeChunkTask = queue.executeChunkTask()); -+ } -+ } while ((executeChunkTask | executeTickTask) && canContinue.getAsBoolean()); -+ -+ if (processedChunkTask) { -+ // if we processed any chunk tasks, try to process ticket level updates for full status changes -+ this.region.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); -+ } -+ return true; -+ } -+ -+ @Override -+ protected boolean hasIntermediateTasks() { -+ return this.region.taskQueueData.hasTasks(); -+ } - } - - } -diff --git a/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/io/papermc/paper/threadedregions/commands/CommandServerHealth.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3bcb1dc98c61e025874cc9e008faa722581a530c ---- /dev/null -+++ b/io/papermc/paper/threadedregions/commands/CommandServerHealth.java -@@ -0,0 +1,355 @@ -+package io.papermc.paper.threadedregions.commands; -+ -+import io.papermc.paper.threadedregions.RegionizedServer; -+import io.papermc.paper.threadedregions.RegionizedWorldData; -+import io.papermc.paper.threadedregions.ThreadedRegionizer; -+import io.papermc.paper.threadedregions.TickData; -+import io.papermc.paper.threadedregions.TickRegionScheduler; -+import io.papermc.paper.threadedregions.TickRegions; -+import it.unimi.dsi.fastutil.doubles.DoubleArrayList; -+import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.TextComponent; -+import net.kyori.adventure.text.event.ClickEvent; -+import net.kyori.adventure.text.event.HoverEvent; -+import net.kyori.adventure.text.format.NamedTextColor; -+import net.kyori.adventure.text.format.TextColor; -+import net.kyori.adventure.text.format.TextDecoration; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.ChunkPos; -+import org.bukkit.Bukkit; -+import org.bukkit.World; -+import org.bukkit.command.Command; -+import org.bukkit.command.CommandSender; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.bukkit.entity.Entity; -+import org.bukkit.entity.Player; -+import java.text.DecimalFormat; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.List; -+import java.util.Locale; -+ -+public final class CommandServerHealth extends Command { -+ -+ private static final ThreadLocal TWO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { -+ return new DecimalFormat("#,##0.00"); -+ }); -+ private static final ThreadLocal ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { -+ return new DecimalFormat("#,##0.0"); -+ }); -+ private static final ThreadLocal NO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { -+ return new DecimalFormat("#,##0"); -+ }); -+ -+ private static final TextColor HEADER = TextColor.color(79, 164, 240); -+ private static final TextColor PRIMARY = TextColor.color(48, 145, 237); -+ private static final TextColor SECONDARY = TextColor.color(104, 177, 240); -+ private static final TextColor INFORMATION = TextColor.color(145, 198, 243); -+ private static final TextColor LIST = TextColor.color(33, 97, 188); -+ -+ public CommandServerHealth() { -+ super("tps"); -+ this.setUsage("/ [server/region] [lowest regions to display]"); -+ this.setDescription("Reports information about server health."); -+ this.setPermission("bukkit.command.tps"); -+ } -+ -+ private static Component formatRegionInfo(final String prefix, final double util, final double mspt, final double tps, -+ final boolean newline) { -+ return Component.text() -+ .append(Component.text(prefix, PRIMARY, TextDecoration.BOLD)) -+ .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) -+ .append(Component.text("% util at ", PRIMARY)) -+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) -+ .append(Component.text(" MSPT at ", PRIMARY)) -+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) -+ .append(Component.text(" TPS" + (newline ? "\n" : ""), PRIMARY)) -+ .build(); -+ } -+ -+ private static Component formatRegionStats(final TickRegions.RegionStats stats, final boolean newline) { -+ return Component.text() -+ .append(Component.text("Chunks: ", PRIMARY)) -+ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getChunkCount()), INFORMATION)) -+ .append(Component.text(" Players: ", PRIMARY)) -+ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getPlayerCount()), INFORMATION)) -+ .append(Component.text(" Entities: ", PRIMARY)) -+ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getEntityCount()) + (newline ? "\n" : ""), INFORMATION)) -+ .build(); -+ } -+ -+ private static boolean executeRegion(final CommandSender sender, final String commandLabel, final String[] args) { -+ final ThreadedRegionizer.ThreadedRegion region = -+ TickRegionScheduler.getCurrentRegion(); -+ if (region == null) { -+ sender.sendMessage(Component.text("You are not in a region currently", NamedTextColor.RED)); -+ return true; -+ } -+ -+ final long currTime = System.nanoTime(); -+ -+ final TickData.TickReportData report15s = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); -+ final TickData.TickReportData report1m = region.getData().getRegionSchedulingHandle().getTickReport1m(currTime); -+ -+ final ServerLevel world = region.regioniser.world; -+ final ChunkPos chunkCenter = region.getCenterChunk(); -+ final int centerBlockX = ((chunkCenter.x << 4) | 7); -+ final int centerBlockZ = ((chunkCenter.z << 4) | 7); -+ -+ final double util15s = report15s.utilisation(); -+ final double tps15s = report15s.tpsData().segmentAll().average(); -+ final double mspt15s = report15s.timePerTickData().segmentAll().average() / 1.0E6; -+ -+ final double util1m = report1m.utilisation(); -+ final double tps1m = report1m.tpsData().segmentAll().average(); -+ final double mspt1m = report1m.timePerTickData().segmentAll().average() / 1.0E6; -+ -+ final int yLoc = 80; -+ final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; -+ -+ final Component line = Component.text() -+ .append(Component.text("Region around block ", PRIMARY)) -+ .append(Component.text(location, INFORMATION)) -+ .append(Component.text(":\n", PRIMARY)) -+ -+ .append( -+ formatRegionInfo("15s: ", util15s, mspt15s, tps15s, true) -+ ) -+ .append( -+ formatRegionInfo("1m: ", util1m, mspt1m, tps1m, true) -+ ) -+ .append( -+ formatRegionStats(region.getData().getRegionStats(), false) -+ ) -+ -+ .build(); -+ -+ sender.sendMessage(line); -+ -+ return true; -+ } -+ -+ private static boolean executeServer(final CommandSender sender, final String commandLabel, final String[] args) { -+ final int lowestRegionsCount; -+ if (args.length < 2) { -+ lowestRegionsCount = 3; -+ } else { -+ try { -+ lowestRegionsCount = Integer.parseInt(args[1]); -+ } catch (final NumberFormatException ex) { -+ sender.sendMessage(Component.text("Highest utilisation count '" + args[1] + "' must be an integer", NamedTextColor.RED)); -+ return true; -+ } -+ } -+ -+ final List> regions = -+ new ArrayList<>(); -+ -+ for (final World bukkitWorld : Bukkit.getWorlds()) { -+ final ServerLevel world = ((CraftWorld)bukkitWorld).getHandle(); -+ world.regioniser.computeForAllRegions(regions::add); -+ } -+ -+ final double minTps; -+ final double medianTps; -+ final double maxTps; -+ double totalUtil = 0.0; -+ -+ final DoubleArrayList tpsByRegion = new DoubleArrayList(); -+ final List reportsByRegion = new ArrayList<>(); -+ final int maxThreadCount = TickRegions.getScheduler().getTotalThreadCount(); -+ -+ final long currTime = System.nanoTime(); -+ final TickData.TickReportData globalTickReport = RegionizedServer.getGlobalTickData().getTickReport15s(currTime); -+ -+ for (final ThreadedRegionizer.ThreadedRegion region : regions) { -+ final TickData.TickReportData report = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); -+ tpsByRegion.add(report == null ? 20.0 : report.tpsData().segmentAll().average()); -+ reportsByRegion.add(report); -+ totalUtil += (report == null ? 0.0 : report.utilisation()); -+ } -+ -+ totalUtil += globalTickReport.utilisation(); -+ -+ tpsByRegion.sort(null); -+ if (!tpsByRegion.isEmpty()) { -+ minTps = tpsByRegion.getDouble(0); -+ maxTps = tpsByRegion.getDouble(tpsByRegion.size() - 1); -+ -+ final int middle = tpsByRegion.size() >> 1; -+ if ((tpsByRegion.size() & 1) == 0) { -+ // even, average the two middle points -+ medianTps = (tpsByRegion.getDouble(middle - 1) + tpsByRegion.getDouble(middle)) / 2.0; -+ } else { -+ // odd, can just grab middle -+ medianTps = tpsByRegion.getDouble(middle); -+ } -+ } else { -+ // no regions = green -+ minTps = medianTps = maxTps = 20.0; -+ } -+ -+ final List, TickData.TickReportData>> -+ regionsBelowThreshold = new ArrayList<>(); -+ -+ for (int i = 0, len = regions.size(); i < len; ++i) { -+ final TickData.TickReportData report = reportsByRegion.get(i); -+ -+ regionsBelowThreshold.add(new ObjectObjectImmutablePair<>(regions.get(i), report)); -+ } -+ -+ regionsBelowThreshold.sort((p1, p2) -> { -+ final TickData.TickReportData report1 = p1.right(); -+ final TickData.TickReportData report2 = p2.right(); -+ final double util1 = report1 == null ? 0.0 : report1.utilisation(); -+ final double util2 = report2 == null ? 0.0 : report2.utilisation(); -+ -+ // we want the largest first -+ return Double.compare(util2, util1); -+ }); -+ -+ final TextComponent.Builder lowestRegionsBuilder = Component.text(); -+ -+ if (sender instanceof Player) { -+ lowestRegionsBuilder.append(Component.text(" Click to teleport\n", SECONDARY)); -+ } -+ for (int i = 0, len = Math.min(lowestRegionsCount, regionsBelowThreshold.size()); i < len; ++i) { -+ final ObjectObjectImmutablePair, TickData.TickReportData> -+ pair = regionsBelowThreshold.get(i); -+ -+ final TickData.TickReportData report = pair.right(); -+ final ThreadedRegionizer.ThreadedRegion region = -+ pair.left(); -+ -+ if (report == null) { -+ // skip regions with no data -+ continue; -+ } -+ -+ final ServerLevel world = region.regioniser.world; -+ final ChunkPos chunkCenter = region.getCenterChunk(); -+ if (chunkCenter == null) { -+ // region does not exist anymore -+ continue; -+ } -+ final int centerBlockX = ((chunkCenter.x << 4) | 7); -+ final int centerBlockZ = ((chunkCenter.z << 4) | 7); -+ final double util = report.utilisation(); -+ final double tps = report.tpsData().segmentAll().average(); -+ final double mspt = report.timePerTickData().segmentAll().average() / 1.0E6; -+ -+ final int yLoc = 80; -+ final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; -+ final Component line = Component.text() -+ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) -+ .append(Component.text("Region around block ", PRIMARY)) -+ .append(Component.text(location, INFORMATION)) -+ .append(Component.text(":\n", PRIMARY)) -+ -+ .append(Component.text(" ", PRIMARY)) -+ .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) -+ .append(Component.text("% util at ", PRIMARY)) -+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) -+ .append(Component.text(" MSPT at ", PRIMARY)) -+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) -+ .append(Component.text(" TPS\n", PRIMARY)) -+ -+ .append(Component.text(" ", PRIMARY)) -+ .append(formatRegionStats(region.getData().getRegionStats(), (i + 1) != len)) -+ .build() -+ -+ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey().toString() + " run tp " + centerBlockX + ".5 " + yLoc + " " + centerBlockZ + ".5")) -+ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Click to teleport to " + location, SECONDARY))); -+ -+ lowestRegionsBuilder.append(line); -+ } -+ -+ sender.sendMessage( -+ Component.text() -+ .append(Component.text("Server Health Report\n", HEADER, TextDecoration.BOLD)) -+ -+ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) -+ .append(Component.text("Online Players: ", PRIMARY)) -+ .append(Component.text(Bukkit.getOnlinePlayers().size() + "\n", INFORMATION)) -+ -+ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) -+ .append(Component.text("Total regions: ", PRIMARY)) -+ .append(Component.text(regions.size() + "\n", INFORMATION)) -+ -+ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) -+ .append(Component.text("Utilisation: ", PRIMARY)) -+ .append(Component.text(ONE_DECIMAL_PLACES.get().format(totalUtil * 100.0), CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount))) -+ .append(Component.text("% / ", PRIMARY)) -+ .append(Component.text(ONE_DECIMAL_PLACES.get().format(maxThreadCount * 100.0), INFORMATION)) -+ .append(Component.text("%\n", PRIMARY)) -+ -+ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) -+ .append(Component.text("Lowest Region TPS: ", PRIMARY)) -+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(minTps) + "\n", CommandUtil.getColourForTPS(minTps))) -+ -+ -+ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) -+ .append(Component.text("Median Region TPS: ", PRIMARY)) -+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(medianTps) + "\n", CommandUtil.getColourForTPS(medianTps))) -+ -+ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) -+ .append(Component.text("Highest Region TPS: ", PRIMARY)) -+ .append(Component.text(TWO_DECIMAL_PLACES.get().format(maxTps) + "\n", CommandUtil.getColourForTPS(maxTps))) -+ -+ .append(Component.text("Highest ", HEADER, TextDecoration.BOLD)) -+ .append(Component.text(Integer.toString(lowestRegionsCount), INFORMATION, TextDecoration.BOLD)) -+ .append(Component.text(" utilisation regions\n", HEADER, TextDecoration.BOLD)) -+ -+ .append(lowestRegionsBuilder.build()) -+ .build() -+ ); -+ -+ return true; -+ } -+ -+ @Override -+ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { -+ final String type; -+ if (args.length < 1) { -+ type = "server"; -+ } else { -+ type = args[0]; -+ } -+ -+ switch (type.toLowerCase(Locale.ROOT)) { -+ case "server": { -+ return executeServer(sender, commandLabel, args); -+ } -+ case "region": { -+ if (!(sender instanceof Entity)) { -+ sender.sendMessage(Component.text("Cannot see current region information as console", NamedTextColor.RED)); -+ return true; -+ } -+ return executeRegion(sender, commandLabel, args); -+ } -+ default: { -+ sender.sendMessage(Component.text("Type '" + args[0] + "' must be one of: [server, region]", NamedTextColor.RED)); -+ return true; -+ } -+ } -+ } -+ -+ @Override -+ public List tabComplete(final CommandSender sender, final String alias, final String[] args) throws IllegalArgumentException { -+ if (args.length == 0) { -+ if (sender instanceof Entity) { -+ return CommandUtil.getSortedList(Arrays.asList("server", "region")); -+ } else { -+ return CommandUtil.getSortedList(Arrays.asList("server")); -+ } -+ } else if (args.length == 1) { -+ if (sender instanceof Entity) { -+ return CommandUtil.getSortedList(Arrays.asList("server", "region"), args[0]); -+ } else { -+ return CommandUtil.getSortedList(Arrays.asList("server"), args[0]); -+ } -+ } -+ return new ArrayList<>(); -+ } -+} -diff --git a/io/papermc/paper/threadedregions/commands/CommandUtil.java b/io/papermc/paper/threadedregions/commands/CommandUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d016294fc7eafbddf6d2a758e5803498dfa207b8 ---- /dev/null -+++ b/io/papermc/paper/threadedregions/commands/CommandUtil.java -@@ -0,0 +1,121 @@ -+package io.papermc.paper.threadedregions.commands; -+ -+import net.kyori.adventure.text.format.NamedTextColor; -+import net.kyori.adventure.text.format.TextColor; -+import net.kyori.adventure.util.HSVLike; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerPlayer; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.function.Function; -+ -+public final class CommandUtil { -+ -+ public static List getSortedList(final Iterable iterable) { -+ final List ret = new ArrayList<>(); -+ for (final String val : iterable) { -+ ret.add(val); -+ } -+ -+ ret.sort(String.CASE_INSENSITIVE_ORDER); -+ -+ return ret; -+ } -+ -+ public static List getSortedList(final Iterable iterable, final String prefix) { -+ final List ret = new ArrayList<>(); -+ for (final String val : iterable) { -+ if (val.regionMatches(0, prefix, 0, prefix.length())) { -+ ret.add(val); -+ } -+ } -+ -+ ret.sort(String.CASE_INSENSITIVE_ORDER); -+ -+ return ret; -+ } -+ -+ public static List getSortedList(final Iterable iterable, final Function transform) { -+ final List ret = new ArrayList<>(); -+ for (final T val : iterable) { -+ final String transformed = transform.apply(val); -+ if (transformed != null) { -+ ret.add(transformed); -+ } -+ } -+ -+ ret.sort(String.CASE_INSENSITIVE_ORDER); -+ -+ return ret; -+ } -+ -+ public static List getSortedList(final Iterable iterable, final Function transform, final String prefix) { -+ final List ret = new ArrayList<>(); -+ for (final T val : iterable) { -+ final String string = transform.apply(val); -+ if (string != null && string.regionMatches(0, prefix, 0, prefix.length())) { -+ ret.add(string); -+ } -+ } -+ -+ ret.sort(String.CASE_INSENSITIVE_ORDER); -+ -+ return ret; -+ } -+ -+ public static TextColor getColourForTPS(final double tps) { -+ final double difference = Math.min(Math.abs(20.0 - tps), 20.0); -+ final double coordinate; -+ if (difference <= 2.0) { -+ // >= 18 tps -+ coordinate = 70.0 + ((140.0 - 70.0)/(0.0 - 2.0)) * (difference - 2.0); -+ } else if (difference <= 5.0) { -+ // >= 15 tps -+ coordinate = 30.0 + ((70.0 - 30.0)/(2.0 - 5.0)) * (difference - 5.0); -+ } else if (difference <= 10.0) { -+ // >= 10 tps -+ coordinate = 10.0 + ((30.0 - 10.0)/(5.0 - 10.0)) * (difference - 10.0); -+ } else { -+ // >= 0.0 tps -+ coordinate = 0.0 + ((10.0 - 0.0)/(10.0 - 20.0)) * (difference - 20.0); -+ } -+ -+ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); -+ } -+ -+ public static TextColor getColourForMSPT(final double mspt) { -+ final double clamped = Math.min(Math.abs(mspt), 50.0); -+ final double coordinate; -+ if (clamped <= 15.0) { -+ coordinate = 130.0 + ((140.0 - 130.0)/(0.0 - 15.0)) * (clamped - 15.0); -+ } else if (clamped <= 25.0) { -+ coordinate = 90.0 + ((130.0 - 90.0)/(15.0 - 25.0)) * (clamped - 25.0); -+ } else if (clamped <= 35.0) { -+ coordinate = 30.0 + ((90.0 - 30.0)/(25.0 - 35.0)) * (clamped - 35.0); -+ } else if (clamped <= 40.0) { -+ coordinate = 15.0 + ((30.0 - 15.0)/(35.0 - 40.0)) * (clamped - 40.0); -+ } else { -+ coordinate = 0.0 + ((15.0 - 0.0)/(40.0 - 50.0)) * (clamped - 50.0); -+ } -+ -+ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); -+ } -+ -+ public static TextColor getUtilisationColourRegion(final double util) { -+ // TODO anything better? -+ // assume 20TPS -+ return getColourForMSPT(util * 50.0); -+ } -+ -+ public static ServerPlayer getPlayer(final String name) { -+ for (final ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) { -+ if (player.getGameProfile().getName().equalsIgnoreCase(name)) { -+ return player; -+ } -+ } -+ -+ return null; -+ } -+ -+ private CommandUtil() {} -+} -diff --git a/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java b/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..85d3965a67cfb59790c664baa7840b50436a5e28 ---- /dev/null -+++ b/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java -@@ -0,0 +1,424 @@ -+package io.papermc.paper.threadedregions.scheduler; -+ -+import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -+import ca.spottedleaf.concurrentutil.util.Validate; -+import ca.spottedleaf.moonrise.common.util.CoordinateUtils; -+import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; -+import io.papermc.paper.threadedregions.RegionizedData; -+import io.papermc.paper.threadedregions.RegionizedServer; -+import io.papermc.paper.threadedregions.TickRegionScheduler; -+import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -+import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.TicketType; -+import net.minecraft.util.Unit; -+import org.bukkit.Bukkit; -+import org.bukkit.World; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.bukkit.plugin.IllegalPluginAccessException; -+import org.bukkit.plugin.Plugin; -+import java.lang.invoke.VarHandle; -+import java.util.ArrayList; -+import java.util.Iterator; -+import java.util.List; -+import java.util.function.Consumer; -+import java.util.logging.Level; -+ -+public final class FoliaRegionScheduler implements RegionScheduler { -+ -+ private static Runnable wrap(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { -+ return () -> { -+ try { -+ run.run(); -+ } catch (final Throwable throwable) { -+ plugin.getLogger().log(Level.WARNING, "Location task for " + plugin.getDescription().getFullName() -+ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); -+ } -+ }; -+ } -+ -+ private static final RegionizedData SCHEDULER_DATA = new RegionizedData<>(null, Scheduler::new, Scheduler.REGIONISER_CALLBACK); -+ -+ private static void scheduleInternalOnRegion(final LocationScheduledTask task, final long delay) { -+ SCHEDULER_DATA.get().queueTask(task, delay); -+ } -+ -+ private static void scheduleInternalOffRegion(final LocationScheduledTask task, final long delay) { -+ final World world = task.world; -+ if (world == null) { -+ // cancelled -+ return; -+ } -+ -+ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ ((CraftWorld) world).getHandle(), task.chunkX, task.chunkZ, () -> { -+ scheduleInternalOnRegion(task, delay); -+ } -+ ); -+ } -+ -+ @Override -+ public void execute(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { -+ Validate.notNull(plugin, "Plugin may not be null"); -+ Validate.notNull(world, "World may not be null"); -+ Validate.notNull(run, "Runnable may not be null"); -+ -+ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ ((CraftWorld) world).getHandle(), chunkX, chunkZ, wrap(plugin, world, chunkX, chunkZ, run) -+ ); -+ } -+ -+ @Override -+ public ScheduledTask run(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Consumer task) { -+ return this.runDelayed(plugin, world, chunkX, chunkZ, task, 1); -+ } -+ -+ @Override -+ public ScheduledTask runDelayed(final Plugin plugin, final World world, final int chunkX, final int chunkZ, -+ final Consumer task, final long delayTicks) { -+ Validate.notNull(plugin, "Plugin may not be null"); -+ Validate.notNull(world, "World may not be null"); -+ Validate.notNull(task, "Task may not be null"); -+ if (delayTicks <= 0) { -+ throw new IllegalArgumentException("Delay ticks may not be <= 0"); -+ } -+ -+ if (!plugin.isEnabled()) { -+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); -+ } -+ -+ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, -1, task); -+ -+ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { -+ scheduleInternalOnRegion(ret, delayTicks); -+ } else { -+ scheduleInternalOffRegion(ret, delayTicks); -+ } -+ -+ if (!plugin.isEnabled()) { -+ // handle race condition where plugin is disabled asynchronously -+ ret.cancel(); -+ } -+ -+ return ret; -+ } -+ -+ @Override -+ public ScheduledTask runAtFixedRate(final Plugin plugin, final World world, final int chunkX, final int chunkZ, -+ final Consumer task, final long initialDelayTicks, final long periodTicks) { -+ Validate.notNull(plugin, "Plugin may not be null"); -+ Validate.notNull(world, "World may not be null"); -+ Validate.notNull(task, "Task may not be null"); -+ if (initialDelayTicks <= 0) { -+ throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); -+ } -+ if (periodTicks <= 0) { -+ throw new IllegalArgumentException("Period ticks may not be <= 0"); -+ } -+ -+ if (!plugin.isEnabled()) { -+ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); -+ } -+ -+ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, periodTicks, task); -+ -+ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { -+ scheduleInternalOnRegion(ret, initialDelayTicks); -+ } else { -+ scheduleInternalOffRegion(ret, initialDelayTicks); -+ } -+ -+ if (!plugin.isEnabled()) { -+ // handle race condition where plugin is disabled asynchronously -+ ret.cancel(); -+ } -+ -+ return ret; -+ } -+ -+ public void tick() { -+ SCHEDULER_DATA.get().tick(); -+ } -+ -+ private static final class Scheduler { -+ private static final RegionizedData.RegioniserCallback REGIONISER_CALLBACK = new RegionizedData.RegioniserCallback<>() { -+ @Override -+ public void merge(final Scheduler from, final Scheduler into, final long fromTickOffset) { -+ for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); -+ sectionIterator.hasNext();) { -+ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); -+ final long sectionKey = entry.getLongKey(); -+ final Long2ObjectOpenHashMap> section = entry.getValue(); -+ -+ final Long2ObjectOpenHashMap> sectionAdjusted = new Long2ObjectOpenHashMap<>(section.size()); -+ -+ for (final Iterator>> iterator = section.long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry> e = iterator.next(); -+ final long newTick = e.getLongKey() + fromTickOffset; -+ final List tasks = e.getValue(); -+ -+ sectionAdjusted.put(newTick, tasks); -+ } -+ -+ into.tasksByDeadlineBySection.put(sectionKey, sectionAdjusted); -+ } -+ } -+ -+ @Override -+ public void split(final Scheduler from, final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, -+ final ReferenceOpenHashSet dataSet) { -+ for (final Scheduler into : dataSet) { -+ into.tickCount = from.tickCount; -+ } -+ -+ for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); -+ sectionIterator.hasNext();) { -+ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); -+ final long sectionKey = entry.getLongKey(); -+ final Long2ObjectOpenHashMap> section = entry.getValue(); -+ -+ final Scheduler into = regionToData.get(sectionKey); -+ -+ into.tasksByDeadlineBySection.put(sectionKey, section); -+ } -+ } -+ }; -+ -+ private long tickCount = 0L; -+ // map of region section -> map of deadline -> list of tasks -+ private final Long2ObjectOpenHashMap>> tasksByDeadlineBySection = new Long2ObjectOpenHashMap<>(); -+ -+ private void addTicket(final int sectionX, final int sectionZ) { -+ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; -+ final int shift = world.moonrise$getRegionChunkShift(); -+ final int chunkX = sectionX << shift; -+ final int chunkZ = sectionZ << shift; -+ -+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( -+ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE -+ ); -+ } -+ -+ private void removeTicket(final long sectionKey) { -+ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; -+ final int shift = world.moonrise$getRegionChunkShift(); -+ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << shift; -+ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << shift; -+ -+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( -+ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE -+ ); -+ } -+ -+ private void queueTask(final LocationScheduledTask task, final long delay) { -+ // note: must be on the thread that owns this scheduler -+ // note: delay > 0 -+ -+ final World world = task.world; -+ if (world == null) { -+ // cancelled -+ return; -+ } -+ -+ final int shift = ((CraftWorld)world).getHandle().moonrise$getRegionChunkShift(); -+ final int sectionX = task.chunkX >> shift; -+ final int sectionZ = task.chunkZ >> shift; -+ -+ final Long2ObjectOpenHashMap> section = -+ this.tasksByDeadlineBySection.computeIfAbsent(CoordinateUtils.getChunkKey(sectionX, sectionZ), (final long keyInMap) -> { -+ return new Long2ObjectOpenHashMap<>(); -+ } -+ ); -+ -+ if (section.isEmpty()) { -+ // need to keep the scheduler loaded for this location in order for tick() to be called... -+ this.addTicket(sectionX, sectionZ); -+ } -+ -+ section.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> { -+ return new ArrayList<>(); -+ }).add(task); -+ } -+ -+ public void tick() { -+ ++this.tickCount; -+ -+ final List run = new ArrayList<>(); -+ -+ for (final Iterator>>> sectionIterator = this.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); -+ sectionIterator.hasNext();) { -+ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); -+ final long sectionKey = entry.getLongKey(); -+ final Long2ObjectOpenHashMap> section = entry.getValue(); -+ -+ final List tasks = section.remove(this.tickCount); -+ -+ if (tasks == null) { -+ continue; -+ } -+ -+ run.addAll(tasks); -+ -+ if (section.isEmpty()) { -+ this.removeTicket(sectionKey); -+ sectionIterator.remove(); -+ } -+ } -+ -+ for (int i = 0, len = run.size(); i < len; ++i) { -+ run.get(i).run(); -+ } -+ } -+ } -+ -+ private static final class LocationScheduledTask implements ScheduledTask, Runnable { -+ -+ private static final int STATE_IDLE = 0; -+ private static final int STATE_EXECUTING = 1; -+ private static final int STATE_EXECUTING_CANCELLED = 2; -+ private static final int STATE_FINISHED = 3; -+ private static final int STATE_CANCELLED = 4; -+ -+ private final Plugin plugin; -+ private final int chunkX; -+ private final int chunkZ; -+ private final long repeatDelay; // in ticks -+ private World world; -+ private Consumer run; -+ -+ private volatile int state; -+ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(LocationScheduledTask.class, "state", int.class); -+ -+ private LocationScheduledTask(final Plugin plugin, final World world, final int chunkX, final int chunkZ, -+ final long repeatDelay, final Consumer run) { -+ this.plugin = plugin; -+ this.world = world; -+ this.chunkX = chunkX; -+ this.chunkZ = chunkZ; -+ this.repeatDelay = repeatDelay; -+ this.run = run; -+ } -+ -+ private final int getStateVolatile() { -+ return (int)STATE_HANDLE.get(this); -+ } -+ -+ private final int compareAndExchangeStateVolatile(final int expect, final int update) { -+ return (int)STATE_HANDLE.compareAndExchange(this, expect, update); -+ } -+ -+ private final void setStateVolatile(final int value) { -+ STATE_HANDLE.setVolatile(this, value); -+ } -+ -+ @Override -+ public void run() { -+ if (!this.plugin.isEnabled()) { -+ // don't execute if the plugin is disabled -+ return; -+ } -+ -+ final boolean repeating = this.isRepeatingTask(); -+ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { -+ // cancelled -+ return; -+ } -+ -+ try { -+ this.run.accept(this); -+ } catch (final Throwable throwable) { -+ this.plugin.getLogger().log(Level.WARNING, "Location task for " + this.plugin.getDescription().getFullName() -+ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); -+ } finally { -+ boolean reschedule = false; -+ if (!repeating) { -+ this.setStateVolatile(STATE_FINISHED); -+ } else if (!this.plugin.isEnabled()) { -+ this.setStateVolatile(STATE_CANCELLED); -+ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { -+ reschedule = true; -+ } // else: cancelled repeating task -+ -+ if (!reschedule) { -+ this.run = null; -+ this.world = null; -+ } else { -+ FoliaRegionScheduler.scheduleInternalOnRegion(this, this.repeatDelay); -+ } -+ } -+ } -+ -+ @Override -+ public Plugin getOwningPlugin() { -+ return this.plugin; -+ } -+ -+ @Override -+ public boolean isRepeatingTask() { -+ return this.repeatDelay > 0; -+ } -+ -+ @Override -+ public CancelledState cancel() { -+ for (int curr = this.getStateVolatile();;) { -+ switch (curr) { -+ case STATE_IDLE: { -+ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { -+ this.state = STATE_CANCELLED; -+ this.run = null; -+ this.world = null; -+ return CancelledState.CANCELLED_BY_CALLER; -+ } -+ // try again -+ continue; -+ } -+ case STATE_EXECUTING: { -+ if (!this.isRepeatingTask()) { -+ return CancelledState.RUNNING; -+ } -+ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { -+ return CancelledState.NEXT_RUNS_CANCELLED; -+ } -+ // try again -+ continue; -+ } -+ case STATE_EXECUTING_CANCELLED: { -+ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; -+ } -+ case STATE_FINISHED: { -+ return CancelledState.ALREADY_EXECUTED; -+ } -+ case STATE_CANCELLED: { -+ return CancelledState.CANCELLED_ALREADY; -+ } -+ default: { -+ throw new IllegalStateException("Unknown state: " + curr); -+ } -+ } -+ } -+ } -+ -+ @Override -+ public ExecutionState getExecutionState() { -+ final int state = this.getStateVolatile(); -+ switch (state) { -+ case STATE_IDLE: -+ return ExecutionState.IDLE; -+ case STATE_EXECUTING: -+ return ExecutionState.RUNNING; -+ case STATE_EXECUTING_CANCELLED: -+ return ExecutionState.CANCELLED_RUNNING; -+ case STATE_FINISHED: -+ return ExecutionState.FINISHED; -+ case STATE_CANCELLED: -+ return ExecutionState.CANCELLED; -+ default: { -+ throw new IllegalStateException("Unknown state: " + state); -+ } -+ } -+ } -+ } -+} -diff --git a/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java b/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java -new file mode 100644 -index 0000000000000000000000000000000000000000..97cd0e767ed36eeb211ecdf125e8d2bfff19a15e ---- /dev/null -+++ b/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java -@@ -0,0 +1,79 @@ -+package io.papermc.paper.threadedregions.util; -+ -+import net.minecraft.util.RandomSource; -+import net.minecraft.world.level.levelgen.BitRandomSource; -+import net.minecraft.world.level.levelgen.PositionalRandomFactory; -+import java.util.concurrent.ThreadLocalRandom; -+ -+public final class SimpleThreadLocalRandomSource implements BitRandomSource { -+ -+ public static final SimpleThreadLocalRandomSource INSTANCE = new SimpleThreadLocalRandomSource(); -+ -+ private final PositionalRandomFactory positionalRandomFactory = new SimpleThreadLocalRandomSource.SimpleThreadLocalRandomPositionalRandomFactory(); -+ -+ private SimpleThreadLocalRandomSource() {} -+ -+ @Override -+ public int next(final int bits) { -+ return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits); -+ } -+ -+ @Override -+ public int nextInt() { -+ return ThreadLocalRandom.current().nextInt(); -+ } -+ -+ @Override -+ public int nextInt(final int bound) { -+ if (bound <= 0) { -+ throw new IllegalArgumentException(); -+ } -+ -+ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ -+ final long value = (long)this.nextInt() & 0xFFFFFFFFL; -+ return (int)((value * (long)bound) >>> Integer.SIZE); -+ } -+ -+ @Override -+ public void setSeed(final long seed) { -+ // no-op -+ } -+ -+ @Override -+ public double nextGaussian() { -+ return ThreadLocalRandom.current().nextGaussian(); -+ } -+ -+ @Override -+ public RandomSource fork() { -+ return this; -+ } -+ -+ @Override -+ public PositionalRandomFactory forkPositional() { -+ return this.positionalRandomFactory; -+ } -+ -+ private static final class SimpleThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory { -+ -+ @Override -+ public RandomSource fromHashOf(final String seed) { -+ return SimpleThreadLocalRandomSource.INSTANCE; -+ } -+ -+ @Override -+ public RandomSource fromSeed(final long seed) { -+ return SimpleThreadLocalRandomSource.INSTANCE; -+ } -+ -+ @Override -+ public RandomSource at(final int x, final int y, final int z) { -+ return SimpleThreadLocalRandomSource.INSTANCE; -+ } -+ -+ @Override -+ public void parityConfigString(final StringBuilder info) { -+ info.append("SimpleThreadLocalRandomPositionalRandomFactory{}"); -+ } -+ } -+} -diff --git a/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java b/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java -new file mode 100644 -index 0000000000000000000000000000000000000000..eda02661b1c09e5303d3912c2562bb1c4ccc04fe ---- /dev/null -+++ b/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java -@@ -0,0 +1,73 @@ -+package io.papermc.paper.threadedregions.util; -+ -+import net.minecraft.util.RandomSource; -+import net.minecraft.world.level.levelgen.BitRandomSource; -+import net.minecraft.world.level.levelgen.PositionalRandomFactory; -+import java.util.concurrent.ThreadLocalRandom; -+ -+public final class ThreadLocalRandomSource implements BitRandomSource { -+ -+ public static final ThreadLocalRandomSource INSTANCE = new ThreadLocalRandomSource(); -+ -+ private final PositionalRandomFactory positionalRandomFactory = new ThreadLocalRandomPositionalRandomFactory(); -+ -+ private ThreadLocalRandomSource() {} -+ -+ @Override -+ public int next(final int bits) { -+ return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits); -+ } -+ -+ @Override -+ public int nextInt() { -+ return ThreadLocalRandom.current().nextInt(); -+ } -+ -+ @Override -+ public int nextInt(final int bound) { -+ return ThreadLocalRandom.current().nextInt(bound); -+ } -+ -+ @Override -+ public void setSeed(final long seed) { -+ // no-op -+ } -+ -+ @Override -+ public double nextGaussian() { -+ return ThreadLocalRandom.current().nextGaussian(); -+ } -+ -+ @Override -+ public RandomSource fork() { -+ return this; -+ } -+ -+ @Override -+ public PositionalRandomFactory forkPositional() { -+ return this.positionalRandomFactory; -+ } -+ -+ private static final class ThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory { -+ -+ @Override -+ public RandomSource fromHashOf(final String seed) { -+ return ThreadLocalRandomSource.INSTANCE; -+ } -+ -+ @Override -+ public RandomSource fromSeed(final long seed) { -+ return ThreadLocalRandomSource.INSTANCE; -+ } -+ -+ @Override -+ public RandomSource at(final int x, final int y, final int z) { -+ return ThreadLocalRandomSource.INSTANCE; -+ } -+ -+ @Override -+ public void parityConfigString(final StringBuilder info) { -+ info.append("ThreadLocalRandomPositionalRandomFactory{}"); -+ } -+ } -+} -diff --git a/net/minecraft/commands/CommandSourceStack.java b/net/minecraft/commands/CommandSourceStack.java -index 75262c8c9eaecb4a88a94f4076d67119c67a97da..f5d20a831f931b344d37d1fb885556c65671323a 100644 ---- a/net/minecraft/commands/CommandSourceStack.java -+++ b/net/minecraft/commands/CommandSourceStack.java -@@ -91,7 +91,7 @@ public class CommandSourceStack implements ExecutionCommandSource { io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);}) // Folia - region threading - ); - } - -diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java -index 19ccf3abf14c67f72a1ca065e4a304f50e645ef4..7fc9a62b59a21b2ec96e5a661af75f4c7bf20cd0 100644 ---- a/net/minecraft/commands/Commands.java -+++ b/net/minecraft/commands/Commands.java -@@ -153,13 +153,13 @@ public class Commands { - AdvancementCommands.register(this.dispatcher); - AttributeCommand.register(this.dispatcher, context); - ExecuteCommand.register(this.dispatcher, context); -- BossBarCommands.register(this.dispatcher, context); -+ //BossBarCommands.register(this.dispatcher, context); // Folia - region threading - TODO - ClearInventoryCommands.register(this.dispatcher, context); -- CloneCommands.register(this.dispatcher, context); -+ //CloneCommands.register(this.dispatcher, context); // Folia - region threading - TODO - DamageCommand.register(this.dispatcher, context); -- DataCommands.register(this.dispatcher); -- DataPackCommand.register(this.dispatcher); -- DebugCommand.register(this.dispatcher); -+ //DataCommands.register(this.dispatcher); // Folia - region threading - TODO -+ //DataPackCommand.register(this.dispatcher); // Folia - region threading - TODO -+ //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO - DefaultGameModeCommands.register(this.dispatcher); - DifficultyCommand.register(this.dispatcher); - EffectCommands.register(this.dispatcher, context); -@@ -169,47 +169,47 @@ public class Commands { - FillCommand.register(this.dispatcher, context); - FillBiomeCommand.register(this.dispatcher, context); - ForceLoadCommand.register(this.dispatcher); -- FunctionCommand.register(this.dispatcher); -+ //FunctionCommand.register(this.dispatcher); // Folia - region threading - TODO - GameModeCommand.register(this.dispatcher); - GameRuleCommand.register(this.dispatcher, context); - GiveCommand.register(this.dispatcher, context); - HelpCommand.register(this.dispatcher); -- ItemCommands.register(this.dispatcher, context); -+ //ItemCommands.register(this.dispatcher, context); // Folia - region threading - TODO later - KickCommand.register(this.dispatcher); - KillCommand.register(this.dispatcher); - ListPlayersCommand.register(this.dispatcher); - LocateCommand.register(this.dispatcher, context); -- LootCommand.register(this.dispatcher, context); -+ //LootCommand.register(this.dispatcher, context); // Folia - region threading - TODO later - MsgCommand.register(this.dispatcher); - ParticleCommand.register(this.dispatcher, context); - PlaceCommand.register(this.dispatcher); - PlaySoundCommand.register(this.dispatcher); - RandomCommand.register(this.dispatcher); -- ReloadCommand.register(this.dispatcher); -+ //ReloadCommand.register(this.dispatcher); // Folia - region threading - RecipeCommand.register(this.dispatcher); -- ReturnCommand.register(this.dispatcher); -- RideCommand.register(this.dispatcher); -- RotateCommand.register(this.dispatcher); -+ //ReturnCommand.register(this.dispatcher); // Folia - region threading - TODO later -+ //RideCommand.register(this.dispatcher); // Folia - region threading - TODO later -+ //RotateCommand.register(this.dispatcher); // Folia - region threading - TODO later - SayCommand.register(this.dispatcher); -- ScheduleCommand.register(this.dispatcher); -- ScoreboardCommand.register(this.dispatcher, context); -+ //ScheduleCommand.register(this.dispatcher); // Folia - region threading -+ //ScoreboardCommand.register(this.dispatcher, context); // Folia - region threading - SeedCommand.register(this.dispatcher, selection != Commands.CommandSelection.INTEGRATED); - SetBlockCommand.register(this.dispatcher, context); - SetSpawnCommand.register(this.dispatcher); - SetWorldSpawnCommand.register(this.dispatcher); -- SpectateCommand.register(this.dispatcher); -- SpreadPlayersCommand.register(this.dispatcher); -+ //SpectateCommand.register(this.dispatcher); // Folia - region threading - TODO later -+ //SpreadPlayersCommand.register(this.dispatcher); // Folia - region threading - TODO later - StopSoundCommand.register(this.dispatcher); - SummonCommand.register(this.dispatcher, context); -- TagCommand.register(this.dispatcher); -- TeamCommand.register(this.dispatcher, context); -- TeamMsgCommand.register(this.dispatcher); -+ //TagCommand.register(this.dispatcher); // Folia - region threading - TODO later -+ //TeamCommand.register(this.dispatcher, context); // Folia - region threading - TODO later -+ //TeamMsgCommand.register(this.dispatcher); // Folia - region threading - TODO later - TeleportCommand.register(this.dispatcher); - TellRawCommand.register(this.dispatcher, context); -- TickCommand.register(this.dispatcher); -+ //TickCommand.register(this.dispatcher); // Folia - region threading - TODO later - TimeCommand.register(this.dispatcher); - TitleCommand.register(this.dispatcher, context); -- TriggerCommand.register(this.dispatcher); -+ //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later - WeatherCommand.register(this.dispatcher); - WorldBorderCommand.register(this.dispatcher); - if (JvmProfiler.INSTANCE.isAvailable()) { -@@ -237,8 +237,8 @@ public class Commands { - OpCommand.register(this.dispatcher); - PardonCommand.register(this.dispatcher); - PardonIpCommand.register(this.dispatcher); -- PerfCommand.register(this.dispatcher); -- SaveAllCommand.register(this.dispatcher); -+ //PerfCommand.register(this.dispatcher); // Folia - region threading - TODO later -+ //SaveAllCommand.register(this.dispatcher); // Folia - region threading - TODO later - SaveOffCommand.register(this.dispatcher); - SaveOnCommand.register(this.dispatcher); - SetPlayerIdleTimeoutCommand.register(this.dispatcher); -@@ -480,9 +480,12 @@ public class Commands { - } - // Paper start - Perf: Async command map building - new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootCommandNode, false).callEvent(); // Paper - Brigadier API -- net.minecraft.server.MinecraftServer.getServer().execute(() -> { -- runSync(player, bukkit, rootCommandNode); -- }); -+ // Folia start - region threading -+ // ignore if retired -+ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer updatedPlayer) -> { -+ runSync((ServerPlayer)updatedPlayer, bukkit, rootCommandNode); -+ }, null, 1L); -+ // Folia end - region threading - } - - private void runSync(ServerPlayer player, java.util.Collection bukkit, RootCommandNode rootCommandNode) { -diff --git a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java -index 4a881636ba21fae9e50950bbba2b4321b71d35ab..af35d667f7dc752df34c49fe675cd0a6cf8ffe4b 100644 ---- a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java -+++ b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java -@@ -46,7 +46,7 @@ public class BoatDispenseItemBehavior extends DefaultDispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - serverLevel.getCraftServer().getPluginManager().callEvent(event); - } - -diff --git a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java -index bd5bbc7e55c6bea77991fe5a3c0c2580313d16c5..907d3a5385b8b9098051f4ec0887d778fb85cf8d 100644 ---- a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java -+++ b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java -@@ -78,7 +78,7 @@ public class DefaultDispenseItemBehavior implements DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack); - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(itemEntity.getDeltaMovement())); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - level.getCraftServer().getPluginManager().callEvent(event); - } - -diff --git a/net/minecraft/core/dispenser/DispenseItemBehavior.java b/net/minecraft/core/dispenser/DispenseItemBehavior.java -index 717c84165d5e25cd384f56b7cb976abf6669b6f0..ebcd1949266f29ca0c99ee26252c366c3f887546 100644 ---- a/net/minecraft/core/dispenser/DispenseItemBehavior.java -+++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java -@@ -89,7 +89,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - serverLevel.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -147,7 +147,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - serverLevel.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -201,7 +201,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); - - org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity()); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - world.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -251,7 +251,7 @@ public interface DispenseItemBehavior { - org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockSource.pos()); - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleCopy); - org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), abstractChestedHorse.getBukkitLivingEntity()); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - world.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -329,7 +329,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - level.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -389,7 +389,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - levelAccessor.getMinecraftWorld().getCraftServer().getPluginManager().callEvent(event); - } - -@@ -425,7 +425,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - serverLevel.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -482,7 +482,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - level.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -500,7 +500,8 @@ public interface DispenseItemBehavior { - } - } - -- level.captureTreeGeneration = true; -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading -+ worldData.captureTreeGeneration = true; // Folia - region threading - // CraftBukkit end - if (!BoneMealItem.growCrop(item, level, blockPos) && !BoneMealItem.growWaterPlant(item, level, blockPos, null)) { - this.setSuccess(false); -@@ -508,13 +509,13 @@ public interface DispenseItemBehavior { - level.levelEvent(1505, blockPos, 15); - } - // CraftBukkit start -- level.captureTreeGeneration = false; -- if (level.capturedBlockStates.size() > 0) { -- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; -- net.minecraft.world.level.block.SaplingBlock.treeType = null; -+ worldData.captureTreeGeneration = false; // Folia - region threading -+ if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading -+ org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // Folia - region threading -+ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); // Folia - region threading - org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(blockPos, level.getWorld()); -- List blocks = new java.util.ArrayList<>(level.capturedBlockStates.values()); -- level.capturedBlockStates.clear(); -+ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading -+ worldData.capturedBlockStates.clear(); // Folia - region threading - org.bukkit.event.world.StructureGrowEvent structureEvent = null; - if (treeType != null) { - structureEvent = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks); -@@ -548,7 +549,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockPos.getX() + 0.5D, (double) blockPos.getY(), (double) blockPos.getZ() + 0.5D)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - level.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -591,7 +592,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - level.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -644,7 +645,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - level.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -702,7 +703,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - only single item in event - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - serverLevel.getCraftServer().getPluginManager().callEvent(event); - } - -@@ -783,7 +784,7 @@ public interface DispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity()); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - serverLevel.getCraftServer().getPluginManager().callEvent(event); - } - -diff --git a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java -index 3595bbd05fb3e8fe57e38d4e2df5c6237046b726..9bcb803b761aef0bf29a76bd4bea22f22cbeda5d 100644 ---- a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java -+++ b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java -@@ -39,7 +39,7 @@ public class EquipmentDispenseItemBehavior extends DefaultDispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack); - - org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) livingEntity.getBukkitEntity()); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - world.getCraftServer().getPluginManager().callEvent(event); - } - -diff --git a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java -index 116395b6c00a0814922516707544a9ff26d68835..26c326080ee6fc80f0cc6af3e9fcbc1a508ba01a 100644 ---- a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java -+++ b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java -@@ -62,7 +62,7 @@ public class MinecartDispenseItemBehavior extends DefaultDispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1); - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(vec31.x, vec31.y, vec31.z)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - serverLevel.getCraftServer().getPluginManager().callEvent(event); - } - -diff --git a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java -index 449d9b72ff4650961daa9d1bd25940f3914a6b12..b4f2dbe3dcdeac2a297b7909cedd54a8079938d8 100644 ---- a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java -+++ b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java -@@ -32,7 +32,7 @@ public class ProjectileDispenseBehavior extends DefaultDispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1); - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) direction.getStepX(), (double) direction.getStepY(), (double) direction.getStepZ())); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - serverLevel.getCraftServer().getPluginManager().callEvent(event); - } - -diff --git a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -index 626e9feb6a6e7a2cbc7c63e30ba4fb6b923e85c7..eb63e114b666128df924dca46235ea8a7edbae54 100644 ---- a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -+++ b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -@@ -25,7 +25,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { - org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos()); - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - serverLevel.getCraftServer().getPluginManager().callEvent(event); - } - -diff --git a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java -index 5ab2c8333178335515e619b87ae420f948c83bd1..172f41f15e3f165b8faca85e7bc581082d330041 100644 ---- a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java -+++ b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java -@@ -27,7 +27,7 @@ public class ShulkerBoxDispenseBehavior extends OptionalDispenseItemBehavior { - org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event - - org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockPos.getX(), blockPos.getY(), blockPos.getZ())); -- if (!DispenserBlock.eventFired) { -+ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading - blockSource.level().getCraftServer().getPluginManager().callEvent(event); - } - -diff --git a/net/minecraft/gametest/framework/GameTestHelper.java b/net/minecraft/gametest/framework/GameTestHelper.java -index fe4ae6bcdcbb55c47e9f9a4d63ead4c39e6d63cf..36a5ca39214233f37ef7bfeb47331a7deb566e5c 100644 ---- a/net/minecraft/gametest/framework/GameTestHelper.java -+++ b/net/minecraft/gametest/framework/GameTestHelper.java -@@ -306,7 +306,7 @@ public class GameTestHelper { - }; - Connection connection = new Connection(PacketFlow.SERVERBOUND); - new EmbeddedChannel(connection); -- this.getLevel().getServer().getPlayerList().placeNewPlayer(connection, serverPlayer, commonListenerCookie); -+ if (true) throw new UnsupportedOperationException(); // Folia - region threading - return serverPlayer; - } - -diff --git a/net/minecraft/gametest/framework/GameTestServer.java b/net/minecraft/gametest/framework/GameTestServer.java -index 54ca624a8194e7d1c0f3b1c0ddba81165523382c..a8cc20bfad1790f254c4793f09fc4dd3ddd4f25b 100644 ---- a/net/minecraft/gametest/framework/GameTestServer.java -+++ b/net/minecraft/gametest/framework/GameTestServer.java -@@ -175,8 +175,12 @@ public class GameTestServer extends MinecraftServer { - } - - @Override -- public void tickServer(BooleanSupplier hasTimeLeft) { -- super.tickServer(hasTimeLeft); -+ // Folia start - region threading -+ public void tickServer(long startTime, long scheduledEnd, long targetBuffer, -+ io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { -+ if (true) throw new UnsupportedOperationException(); -+ super.tickServer(startTime, scheduledEnd, targetBuffer, region); -+ // Folia end - region threading - ServerLevel serverLevel = this.overworld(); - if (!this.haveTestsStarted()) { - this.startTests(serverLevel); -diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java -index 208efae06c7c44f220d4192219a86ec55c98a2fe..0a219143a9cd593be68fa23661b653b6c1f6a9fe 100644 ---- a/net/minecraft/network/Connection.java -+++ b/net/minecraft/network/Connection.java -@@ -85,7 +85,7 @@ public class Connection extends SimpleChannelInboundHandler> { - private static final ProtocolInfo INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND; - private final PacketFlow receiving; - private volatile boolean sendLoginDisconnect = true; -- private final Queue pendingActions = Queues.newConcurrentLinkedQueue(); // Paper - Optimize network -+ private final Queue pendingActions = new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); // Paper - Optimize network // Folia - region threading - connection fixes - public Channel channel; - public SocketAddress address; - // Spigot start -@@ -100,7 +100,7 @@ public class Connection extends SimpleChannelInboundHandler> { - @Nullable - private DisconnectionDetails disconnectionDetails; - private boolean encrypted; -- private boolean disconnectionHandled; -+ private final java.util.concurrent.atomic.AtomicBoolean disconnectionHandled = new java.util.concurrent.atomic.AtomicBoolean(false); // Folia - region threading - may be called concurrently during configuration stage - private int receivedPackets; - private int sentPackets; - private float averageReceivedPackets; -@@ -154,6 +154,41 @@ public class Connection extends SimpleChannelInboundHandler> { - this.receiving = receiving; - } - -+ // Folia start - region threading -+ private volatile boolean becomeActive; -+ -+ public boolean becomeActive() { -+ return this.becomeActive; -+ } -+ -+ private static record DisconnectReq(DisconnectionDetails disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {} -+ -+ private final ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue disconnectReqs = -+ new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); -+ -+ /** -+ * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the -+ * same thread that could disconnect. -+ */ -+ public final void disconnectSafely(DisconnectionDetails disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { -+ this.disconnectReqs.add(new DisconnectReq(disconnectReason, cause)); -+ // We can't halt packet processing here because a plugin could cancel a kick request. -+ } -+ -+ /** -+ * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the -+ * same thread that could disconnect. -+ */ -+ public final void disconnectSafely(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { -+ this.disconnectReqs.add(new DisconnectReq(new DisconnectionDetails(disconnectReason), cause)); -+ // We can't halt packet processing here because a plugin could cancel a kick request. -+ } -+ -+ public final boolean isPlayerConnected() { -+ return this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl; -+ } -+ // Folia end - region threading -+ - @Override - public void channelActive(ChannelHandlerContext context) throws Exception { - super.channelActive(context); -@@ -163,6 +198,7 @@ public class Connection extends SimpleChannelInboundHandler> { - if (this.delayedDisconnect != null) { - this.disconnect(this.delayedDisconnect); - } -+ this.becomeActive = true; // Folia - region threading - } - - @Override -@@ -434,7 +470,7 @@ public class Connection extends SimpleChannelInboundHandler> { - } - - packet.onPacketDispatch(this.getPlayer()); -- if (connected && (InnerUtil.canSendImmediate(this, packet) -+ if (false && connected && (InnerUtil.canSendImmediate(this, packet) // Folia - region threading - connection fixes - || (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty() - && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) { - this.sendPacket(packet, listener, flush); -@@ -463,11 +499,12 @@ public class Connection extends SimpleChannelInboundHandler> { - } - - public void runOnceConnected(Consumer action) { -- if (this.isConnected()) { -+ if (false && this.isConnected()) { // Folia - region threading - connection fixes - this.flushQueue(); - action.accept(this); - } else { - this.pendingActions.add(new WrappedConsumer(action)); // Paper - Optimize network -+ this.flushQueue(); // Folia - region threading - connection fixes - } - } - -@@ -518,10 +555,11 @@ public class Connection extends SimpleChannelInboundHandler> { - } - - public void flushChannel() { -- if (this.isConnected()) { -+ if (false && this.isConnected()) { // Folia - region threading - connection fixes - this.flush(); - } else { - this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network -+ this.flushQueue(); // Folia - region threading - connection fixes - } - } - -@@ -535,53 +573,61 @@ public class Connection extends SimpleChannelInboundHandler> { - - // Paper start - Optimize network: Rewrite this to be safer if ran off main thread - private boolean flushQueue() { -- if (!this.isConnected()) { -- return true; -- } -- if (io.papermc.paper.util.MCUtil.isMainThread()) { -- return this.processQueue(); -- } else if (this.isPending) { -- // Should only happen during login/status stages -- synchronized (this.pendingActions) { -- return this.processQueue(); -- } -- } -- return false; -+ return this.processQueue(); // Folia - region threading - connection fixes -+ } -+ -+ // Folia start - region threading - connection fixes -+ // allow only one thread to be flushing the queue at once to ensure packets are written in the order they are sent -+ // into the queue -+ private final java.util.concurrent.atomic.AtomicBoolean flushingQueue = new java.util.concurrent.atomic.AtomicBoolean(); -+ -+ private static boolean canWrite(WrappedConsumer queued) { -+ return queued != null && (!(queued instanceof PacketSendAction packet) || packet.packet.isReady()); - } - -+ private boolean canWritePackets() { -+ return canWrite(this.pendingActions.peek()); -+ } -+ // Folia end - region threading - connection fixes -+ - private boolean processQueue() { -- if (this.pendingActions.isEmpty()) { -+ // Folia start - region threading - connection fixes -+ if (!this.isConnected()) { - return true; - } - -- // If we are on main, we are safe here in that nothing else should be processing queue off main anymore -- // But if we are not on main due to login/status, the parent is synchronized on packetQueue -- final java.util.Iterator iterator = this.pendingActions.iterator(); -- while (iterator.hasNext()) { -- final WrappedConsumer queued = iterator.next(); // poll -> peek -- -- // Fix NPE (Spigot bug caused by handleDisconnection()) -- if (queued == null) { -- return true; -- } -+ while (this.canWritePackets()) { -+ final boolean set = this.flushingQueue.getAndSet(true); -+ try { -+ if (set) { -+ // we didn't acquire the lock, break -+ return false; -+ } - -- if (queued.isConsumed()) { -- continue; -- } -+ ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue queue = -+ (ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue)this.pendingActions; -+ WrappedConsumer holder; -+ for (;;) { -+ // synchronise so that queue clears appear atomic -+ synchronized (queue) { -+ holder = queue.pollIf(Connection::canWrite); -+ } -+ if (holder == null) { -+ break; -+ } - -- if (queued instanceof PacketSendAction packetSendAction) { -- final Packet packet = packetSendAction.packet; -- if (!packet.isReady()) { -- return false; -+ holder.accept(this); - } -- } - -- iterator.remove(); -- if (queued.tryMarkConsumed()) { -- queued.accept(this); -+ } finally { -+ if (!set) { -+ this.flushingQueue.set(false); -+ } - } - } -+ - return true; -+ // Folia end - region threading - connection fixes - } - // Paper end - Optimize network - -@@ -590,17 +636,37 @@ public class Connection extends SimpleChannelInboundHandler> { - private static int currTick; // Paper - Buffer joins to world - public void tick() { - this.flushQueue(); -- // Paper start - Buffer joins to world -- if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) { -- Connection.currTick = net.minecraft.server.MinecraftServer.currentTick; -- Connection.joinAttemptsThisTick = 0; -+ // Folia - this is broken -+ // Folia start - region threading -+ // handle disconnect requests, but only after flushQueue() -+ DisconnectReq disconnectReq; -+ while ((disconnectReq = this.disconnectReqs.poll()) != null) { -+ PacketListener packetlistener = this.packetListener; -+ -+ if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { -+ loginPacketListener.disconnect(disconnectReq.disconnectReason.reason()); -+ // this doesn't fail, so abort any further attempts -+ return; -+ } else if (packetlistener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { -+ commonPacketListener.disconnect(disconnectReq.disconnectReason, disconnectReq.cause); -+ // may be cancelled by a plugin, if not cancelled then any further calls do nothing -+ continue; -+ } else { -+ // no idea what packet to send -+ this.disconnect(disconnectReq.disconnectReason); -+ this.setReadOnly(); -+ return; -+ } - } -- // Paper end - Buffer joins to world -+ if (!this.isConnected()) { -+ // disconnected from above -+ this.handleDisconnection(); -+ return; -+ } -+ // Folia end - region threading - if (this.packetListener instanceof TickablePacketListener tickablePacketListener) { - // Paper start - Buffer joins to world -- if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) -- || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING -- || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { -+ if (true) { // Folia - region threading - // Paper start - detailed watchdog information - net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); - try { -@@ -611,7 +677,7 @@ public class Connection extends SimpleChannelInboundHandler> { - } // Paper end - Buffer joins to world - } - -- if (!this.isConnected() && !this.disconnectionHandled) { -+ if (!this.isConnected()) {// Folia - region threading - it's fine to call if it is already handled, as it no longer logs - this.handleDisconnection(); - } - -@@ -662,6 +728,7 @@ public class Connection extends SimpleChannelInboundHandler> { - this.channel.close(); // We can't wait as this may be called from an event loop. - this.disconnectionDetails = disconnectionDetails; - } -+ this.becomeActive = true; // Folia - region threading - } - - public boolean isMemoryConnection() { -@@ -853,10 +920,10 @@ public class Connection extends SimpleChannelInboundHandler> { - - public void handleDisconnection() { - if (this.channel != null && !this.channel.isOpen()) { -- if (this.disconnectionHandled) { -+ if (!this.disconnectionHandled.compareAndSet(false, true)) { // Folia - region threading - may be called concurrently during configuration stage - // LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message - } else { -- this.disconnectionHandled = true; -+ //this.disconnectionHandled = true; // Folia - region threading - may be called concurrently during configuration stage - set above - PacketListener packetListener = this.getPacketListener(); - PacketListener packetListener1 = packetListener != null ? packetListener : this.disconnectListener; - if (packetListener1 != null) { -@@ -885,6 +952,21 @@ public class Connection extends SimpleChannelInboundHandler> { - } - } - // Paper end - Add PlayerConnectionCloseEvent -+ // Folia start - region threading -+ if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { -+ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( -+ commonPacketListener.getOwner().getName(), -+ commonPacketListener.getOwner().getId(), this -+ ); -+ } else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { -+ if (loginPacketListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING.ordinal()) { -+ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( -+ loginPacketListener.authenticatedProfile.getName(), -+ loginPacketListener.authenticatedProfile.getId(), this -+ ); -+ } -+ } -+ // Folia end - region threading - } - } - } -@@ -904,15 +986,25 @@ public class Connection extends SimpleChannelInboundHandler> { - // Paper start - Optimize network - public void clearPacketQueue() { - final net.minecraft.server.level.ServerPlayer player = getPlayer(); -- for (final Consumer queuedAction : this.pendingActions) { -- if (queuedAction instanceof PacketSendAction packetSendAction) { -- final Packet packet = packetSendAction.packet; -- if (packet.hasFinishListener()) { -- packet.onPacketDispatchFinish(player, null); -+ // Folia start - region threading - connection fixes -+ java.util.List queuedPackets = new java.util.ArrayList<>(); -+ // synchronise so that flushQueue does not poll values while the queue is being cleared -+ synchronized (this.pendingActions) { -+ Connection.WrappedConsumer consumer; -+ while ((consumer = this.pendingActions.poll()) != null) { -+ if (consumer instanceof Connection.PacketSendAction packetHolder) { -+ queuedPackets.add(packetHolder); - } - } - } -- this.pendingActions.clear(); -+ -+ for (Connection.PacketSendAction queuedPacket : queuedPackets) { -+ Packet packet = queuedPacket.packet; -+ if (packet.hasFinishListener()) { -+ packet.onPacketDispatchFinish(player, null); -+ } -+ } -+ // Folia end - region threading - connection fixes - } - - private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up. -diff --git a/net/minecraft/network/protocol/PacketUtils.java b/net/minecraft/network/protocol/PacketUtils.java -index 4535858701b2bb232b9d2feb2af6551526232ddc..b28ff2f18ab7e0e3a61e37ee46048ab5cb7ab45d 100644 ---- a/net/minecraft/network/protocol/PacketUtils.java -+++ b/net/minecraft/network/protocol/PacketUtils.java -@@ -20,7 +20,7 @@ public class PacketUtils { - - public static void ensureRunningOnSameThread(Packet packet, T processor, BlockableEventLoop executor) throws RunningOnDifferentThreadException { - if (!executor.isSameThread()) { -- executor.executeIfPossible(() -> { -+ Runnable run = () -> { // Folia - region threading - packetProcessing.push(processor); // Paper - detailed watchdog information - try { // Paper - detailed watchdog information - if (processor instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // Paper - Don't handle sync packets for kicked players -@@ -43,7 +43,24 @@ public class PacketUtils { - packetProcessing.pop(); - } - // Paper end - detailed watchdog information -- }); -+ // Folia start - region threading -+ }; -+ // ignore retired state, if removed then we don't want the packet to be handled -+ if (processor instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) { -+ gamePacketListener.player.getBukkitEntity().taskScheduler.schedule( -+ (net.minecraft.server.level.ServerPlayer player) -> { -+ run.run(); -+ }, -+ null, 1L -+ ); -+ } else if (processor instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); -+ } else if (processor instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); -+ } else { -+ throw new UnsupportedOperationException("Unknown listener: " + processor); -+ } -+ // Folia end - region threading - throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; - } - } -diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index ae220a732c78ab076261f20b5a54c71d7fceb407..9c9de462eb7187d6cc3562c796e3bcf69fb20783 100644 ---- a/net/minecraft/server/MinecraftServer.java -+++ b/net/minecraft/server/MinecraftServer.java -@@ -184,7 +184,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); - public int autosavePeriod; - // Paper - don't store the vanilla dispatcher -@@ -304,6 +303,50 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping - public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation - -+ // Folia start - regionised ticking -+ public final io.papermc.paper.threadedregions.RegionizedServer regionizedServer = new io.papermc.paper.threadedregions.RegionizedServer(); -+ -+ @Override -+ public CompletableFuture submit(java.util.function.Supplier task) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ return super.submit(task); -+ } -+ -+ @Override -+ public CompletableFuture submit(Runnable task) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ return super.submit(task); -+ } -+ -+ @Override -+ public void schedule(TickTask task) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ super.schedule(task); -+ } -+ -+ @Override -+ public void executeBlocking(Runnable runnable) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ super.executeBlocking(runnable); -+ } -+ -+ @Override -+ public void execute(Runnable runnable) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ super.execute(runnable); -+ } -+ // Folia end - regionised ticking -+ - public static S spin(Function threadFunction) { - ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system - AtomicReference atomicReference = new AtomicReference<>(); -@@ -332,46 +375,30 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= MAX_CHUNK_EXEC_TIME) { - if (!moreTasks) { -- this.lastMidTickExecuteFailure = currTime; -+ worldData.lastMidTickExecuteFailure = currTime; // Folia - region threading - } - - // note: negative values reduce the time -@@ -384,7 +411,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> 4; -+ serverLevel.randomSpawnSelection = new ChunkPos(serverLevel.getChunkSource().randomState().sampler().findSpawnPosition()); -+ for (int currX = -loadRegionRadius; currX <= loadRegionRadius; ++currX) { -+ for (int currZ = -loadRegionRadius; currZ <= loadRegionRadius; ++currZ) { -+ ChunkPos pos = new ChunkPos(currX, currZ); -+ serverLevel.chunkSource.addTicketAtLevel( -+ net.minecraft.server.level.TicketType.UNKNOWN, pos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, pos -+ ); -+ } -+ } -+ // Folia end - region threading - - // Paper - Put world into worldlist before initing the world; move up - this.getPlayerList().addWorldborderListener(serverLevel); -@@ -723,6 +764,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 ? Mth.square(ChunkProgressListener.calculateDiameter(_int)) : 0; - -- while (chunkSource.getTickingGenerated() < i) { -- // CraftBukkit start -- // this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS; -- this.executeModerately(); -- } -+ // Folia - region threading - - // this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS; -- this.executeModerately(); -+ //this.executeModerately(); // Folia - region threading - - if (true) { - ServerLevel serverLevel1 = serverLevel; -@@ -895,7 +934,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop false : this::haveTime); -+ if (true) throw new UnsupportedOperationException(); // Folia - region threading - // Paper start - rewrite chunk system - final Throwable crash = this.chunkSystemCrash; - if (crash != null) { -@@ -1403,28 +1497,24 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop {}; -- } -- // Paper end -- return new TickTask(this.tickCount, runnable); -+ throw new UnsupportedOperationException(); // Folia - region threading - } - - @Override - protected boolean shouldRun(TickTask runnable) { -- return runnable.getTick() + 3 < this.tickCount || this.haveTime(); -+ throw new UnsupportedOperationException(); // Folia - region threading - } - - @Override - public boolean pollTask() { -+ if (true) throw new UnsupportedOperationException(); // Folia - region threading - boolean flag = this.pollTaskInternal(); - this.mayHaveDelayedTasks = flag; - return flag; - } - - private boolean pollTaskInternal() { -+ if (true) throw new UnsupportedOperationException(); // Folia - region threading - if (super.pollTask()) { - this.moonrise$executeMidTickTasks(); // Paper - rewrite chunk system - return true; -@@ -1444,6 +1534,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { -+ if (false && i > 0) { // Folia - region threading - this is complicated to implement, and even if done correctly is messy - if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting() && this.pluginsBlockingSleep.isEmpty()) { // Paper - API to allow/disallow tick sleeping - this.emptyTicks++; - } else { -@@ -1515,24 +1609,58 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop true, false); - } - // Paper end - avoid issues with certain tasks not processing during sleep -- this.server.spark.executeMainThreadTasks(); // Paper - spark -+ //this.server.spark.executeMainThreadTasks(); // Paper - spark // Folia - region threading - this.tickConnection(); - this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark - return; - } - } - -+ // Folia start - region threading -+ region.world.getCurrentWorldData().updateTickData(); -+ if (region.world.checkInitialised.get() != ServerLevel.WORLD_INIT_CHECKED) { -+ synchronized (region.world.checkInitialised) { -+ if (region.world.checkInitialised.compareAndSet(ServerLevel.WORLD_INIT_NOT_CHECKED, ServerLevel.WORLD_INIT_CHECKING)) { -+ LOGGER.info("Initialising world '" + region.world.getWorld().getName() + "' before it can be ticked..."); -+ this.initWorld(region.world, region.world.serverLevelData, worldData, region.world.serverLevelData.worldGenOptions()); // Folia - delayed until first tick of world -+ region.world.checkInitialised.set(ServerLevel.WORLD_INIT_CHECKED); -+ LOGGER.info("Initialised world '" + region.world.getWorld().getName() + "'"); -+ } // else: must be checked -+ } -+ } -+ BooleanSupplier hasTimeLeft = () -> { -+ return scheduledEnd - System.nanoTime() > targetBuffer; -+ }; -+ // Folia end - region threading -+ - this.server.spark.tickStart(); // Paper - spark -- new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events -- this.tickCount++; -- this.tickRateManager.tick(); -- this.tickChildren(hasTimeLeft); -- if (nanos - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) { -+ new com.destroystokyo.paper.event.server.ServerTickStartEvent((int)region.getCurrentTick()).callEvent(); // Paper - Server Tick Events // Folia - region threading -+ // Folia start - region threading -+ if (region != null) { -+ region.getTaskQueueData().drainTasks(); -+ ((io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler)org.bukkit.Bukkit.getRegionScheduler()).tick(); -+ // now run all the entity schedulers -+ // TODO there has got to be a more efficient variant of this crap -+ for (net.minecraft.world.entity.Entity entity : region.world.getCurrentWorldData().getLocalEntitiesCopy()) { -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) || entity.isRemoved()) { -+ continue; -+ } -+ org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); -+ if (bukkit != null) { -+ bukkit.taskScheduler.executeTick(); -+ } -+ } -+ } -+ // Folia end - region threading -+ //this.tickCount++; // Folia - region threading -+ //this.tickRateManager.tick(); // Folia - region threading -+ this.tickChildren(hasTimeLeft, region); // Folia - region threading -+ if (false && nanos - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) { // Folia - region threading - this.lastServerStatus = nanos; - this.status = this.buildServerStatus(); - } - -- this.ticksUntilAutosave--; -+ //this.ticksUntilAutosave--; // Folia - region threading - // Paper start - Incremental chunk and player saving - final ProfilerFiller profiler = Profiler.get(); - int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate; -@@ -1540,15 +1668,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.tickCount % autosavePeriod == 0; -+ final boolean fullSave = autosavePeriod > 0 && io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() % autosavePeriod == 0; // Folia - region threading - try { - this.isSaving = true; - if (playerSaveInterval > 0) { - this.playerList.saveAll(playerSaveInterval); - } -- for (final ServerLevel level : this.getAllLevels()) { -+ for (final ServerLevel level : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Folia - region threading - if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { -- level.saveIncrementally(fullSave); -+ level.saveIncrementally(region == null && fullSave); // Folia - region threading - don't save level.dat - } - } - } finally { -@@ -1558,32 +1686,19 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop players = this.playerList.getPlayers(); -+ List players = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading - int maxPlayers = this.getMaxPlayers(); - if (this.hidesOnlinePlayers()) { - return new ServerStatus.Players(maxPlayers, players.size(), List.of()); -@@ -1653,44 +1760,34 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop serverPlayer1.connection.suspendFlushing()); -- this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit -+ //this.getPlayerList().getPlayers().forEach(serverPlayer1 -> serverPlayer1.connection.suspendFlushing()); // Folia - region threading -+ //this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit // Folia - region threading - // Paper start - Folia scheduler API -- ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) org.bukkit.Bukkit.getGlobalRegionScheduler()).tick(); -- getAllLevels().forEach(level -> { -- for (final net.minecraft.world.entity.Entity entity : level.getEntities().getAll()) { -- if (entity.isRemoved()) { -- continue; -- } -- final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); -- if (bukkit != null) { -- bukkit.taskScheduler.executeTick(); -- } -- } -- }); -+ // Folia - region threading - moved to global tick - and moved entity scheduler to tickRegion - // Paper end - Folia scheduler API -- io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper -+ //io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper // Folia - region threading - moved to global tick - profilerFiller.push("commandFunctions"); -- this.getFunctions().tick(); -+ //this.getFunctions().tick(); // Folia - region threading - TODO Purge functions - profilerFiller.popPush("levels"); - - // CraftBukkit start - // Run tasks that are waiting on processing -- while (!this.processQueue.isEmpty()) { -+ if (false) while (!this.processQueue.isEmpty()) { // Folia - region threading - this.processQueue.remove().run(); - } - - // Send time updates to everyone, it will get the right time from the world the player is in. - // Paper start - Perf: Optimize time updates -- for (final ServerLevel level : this.getAllLevels()) { -+ for (final ServerLevel level : Arrays.asList(region.world)) { // Folia - region threading - final boolean doDaylight = level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT); - final long dayTime = level.getDayTime(); - long worldTime = level.getGameTime(); - final ClientboundSetTimePacket worldPacket = new ClientboundSetTimePacket(worldTime, dayTime, doDaylight); -- for (Player entityhuman : level.players()) { -- if (!(entityhuman instanceof ServerPlayer) || (tickCount + entityhuman.getId()) % 20 != 0) { -+ for (Player entityhuman : level.getLocalPlayers()) { // Folia - region threading -+ if (!(entityhuman instanceof ServerPlayer) || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + entityhuman.getId()) % 20 != 0) { // Folia - region threading - continue; - } - ServerPlayer entityplayer = (ServerPlayer) entityhuman; -@@ -1703,12 +1800,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0; // Paper - BlockPhysicsEvent -- serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent -- serverLevel.updateLagCompensationTick(); // Paper - lag compensation -- net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = serverLevel.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers -+ //this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked // Folia - region threading -+ for (ServerLevel serverLevel : Arrays.asList(region.world)) { // Folia - region threading -+ // Folia - region threading - profilerFiller.push(() -> serverLevel + " " + serverLevel.dimension().location()); - /* Drop global time updates - if (this.tickCount % 20 == 0) { -@@ -1721,7 +1815,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().invalidateStatus(); -+ }); -+ return; -+ } -+ // Folia end - region threading - this.lastServerStatus = 0L; - } - -@@ -2142,6 +2245,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20; -+ return false; // Folia - region threading - } - - public void addPluginAllowingSleep(final String pluginName, final boolean value) { -- if (!value) { -- this.pluginsBlockingSleep.add(pluginName); -- } else { -- this.pluginsBlockingSleep.remove(pluginName); -- } -+ // Folia - region threading - } - - private void removeDisabledPluginsBlockingSleep() { -- if (this.pluginsBlockingSleep.isEmpty()) { -- return; -- } -- this.pluginsBlockingSleep.removeIf(plugin -> ( -- !io.papermc.paper.plugin.manager.PaperPluginManagerImpl.getInstance().isPluginEnabled(plugin) -- )); -+ // Folia - region threading - } - // Paper end - API to check if the server is sleeping - } -diff --git a/net/minecraft/server/commands/AdvancementCommands.java b/net/minecraft/server/commands/AdvancementCommands.java -index 9157c1efef669795c8408d2e344a2bfeeabeb842..7873f11d7462ef88b5ba27d99988ac9e45689d3a 100644 ---- a/net/minecraft/server/commands/AdvancementCommands.java -+++ b/net/minecraft/server/commands/AdvancementCommands.java -@@ -246,7 +246,12 @@ public class AdvancementCommands { - int i = 0; - - for (ServerPlayer serverPlayer : targets) { -- i += action.perform(serverPlayer, advancements); -+ // Folia start - region threading -+ i += 1; -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { -+ action.perform(player, advancements); -+ }, null, 1L); -+ // Folia end - region threading - } - - if (i == 0) { -@@ -310,9 +315,12 @@ public class AdvancementCommands { - throw ERROR_CRITERION_NOT_FOUND.create(Advancement.name(advancement), criterionName); - } else { - for (ServerPlayer serverPlayer : targets) { -- if (action.performCriterion(serverPlayer, advancement, criterionName)) { -- i++; -- } -+ // Folia start - region threading -+ ++i; -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { -+ action.performCriterion(player, advancement, criterionName); -+ }, null, 1L); -+ // Folia end - region threading - } - - if (i == 0) { -diff --git a/net/minecraft/server/commands/AttributeCommand.java b/net/minecraft/server/commands/AttributeCommand.java -index 2f0e8b2b1dda17cf861f80f8c1e655a345b76d10..505f0ce1f7b453d7e30e07c13a6b7678e12b0fda 100644 ---- a/net/minecraft/server/commands/AttributeCommand.java -+++ b/net/minecraft/server/commands/AttributeCommand.java -@@ -266,30 +266,62 @@ public class AttributeCommand { - } - } - -+ // Folia start - region threading -+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { -+ src.sendFailure((Component)ex.getRawMessage()); -+ } -+ // Folia end - region threading -+ - private static int getAttributeValue(CommandSourceStack source, Entity entity, Holder attribute, double scale) throws CommandSyntaxException { -- LivingEntity entityWithAttribute = getEntityWithAttribute(entity, attribute); -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { -+ try { -+ // Folia end - region threading -+ LivingEntity entityWithAttribute = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading - double attributeValue = entityWithAttribute.getAttributeValue(attribute); - source.sendSuccess( -- () -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), entity.getName(), attributeValue), false -+ () -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), attributeValue), false // Folia - region threading - ); -- return (int)(attributeValue * scale); -+ return; // Folia - region threading -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }, null, 1L); -+ return 0; -+ // Folia end - region threading - } - - private static int getAttributeBase(CommandSourceStack source, Entity entity, Holder attribute, double scale) throws CommandSyntaxException { -- LivingEntity entityWithAttribute = getEntityWithAttribute(entity, attribute); -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { -+ try { -+ // Folia end - region threading -+ LivingEntity entityWithAttribute = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading - double attributeBaseValue = entityWithAttribute.getAttributeBaseValue(attribute); - source.sendSuccess( -- () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), entity.getName(), attributeBaseValue), -+ () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), attributeBaseValue), // Folia - region threading - false - ); -- return (int)(attributeBaseValue * scale); -+ return; // Folia - region threading -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }, null, 1L); -+ return 0; -+ // Folia end - region threading - } - - private static int getAttributeModifier(CommandSourceStack source, Entity entity, Holder attribute, ResourceLocation id, double scale) throws CommandSyntaxException { -- LivingEntity entityWithAttribute = getEntityWithAttribute(entity, attribute); -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { -+ try { -+ // Folia end - region threading -+ LivingEntity entityWithAttribute = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading - AttributeMap attributes = entityWithAttribute.getAttributes(); - if (!attributes.hasModifier(attribute, id)) { -- throw ERROR_NO_SUCH_MODIFIER.create(entity.getName(), getAttributeDescription(attribute), id); -+ throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading - } else { - double modifierValue = attributes.getModifierValue(attribute, id); - source.sendSuccess( -@@ -297,13 +329,20 @@ public class AttributeCommand { - "commands.attribute.modifier.value.get.success", - Component.translationArg(id), - getAttributeDescription(attribute), -- entity.getName(), -+ nmsEntity.getName(), // Folia - region threading - modifierValue - ), - false - ); -- return (int)(modifierValue * scale); -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }, null, 1L); -+ return 0; -+ // Folia end - region threading - } - - private static Stream getAttributeModifiers(Entity entity, Holder attribute) throws CommandSyntaxException { -@@ -312,11 +351,22 @@ public class AttributeCommand { - } - - private static int setAttributeBase(CommandSourceStack source, Entity entity, Holder attribute, double value) throws CommandSyntaxException { -- getAttributeInstance(entity, attribute).setBaseValue(value); -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { -+ try { -+ // Folia end - region threading -+ getAttributeInstance(nmsEntity, attribute).setBaseValue(value); // Folia - region threading - source.sendSuccess( -- () -> Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), entity.getName(), value), false -+ () -> Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), nmsEntity.getName(), value), false // Folia - region threading - ); -- return 1; -+ return; // Folia - region threading -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }, null, 1L); -+ return 0; -+ // Folia end - region threading - } - - private static int resetAttributeBase(CommandSourceStack source, Entity entity, Holder attribute) throws CommandSyntaxException { -@@ -338,35 +388,57 @@ public class AttributeCommand { - private static int addModifier( - CommandSourceStack source, Entity entity, Holder attribute, ResourceLocation id, double amount, AttributeModifier.Operation operation - ) throws CommandSyntaxException { -- AttributeInstance attributeInstance = getAttributeInstance(entity, attribute); -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { -+ try { -+ // Folia end - region threading -+ AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); // Folia - region threading - AttributeModifier attributeModifier = new AttributeModifier(id, amount, operation); - if (attributeInstance.hasModifier(id)) { -- throw ERROR_MODIFIER_ALREADY_PRESENT.create(entity.getName(), getAttributeDescription(attribute), id); -+ throw ERROR_MODIFIER_ALREADY_PRESENT.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading - } else { - attributeInstance.addPermanentModifier(attributeModifier); - source.sendSuccess( - () -> Component.translatable( -- "commands.attribute.modifier.add.success", Component.translationArg(id), getAttributeDescription(attribute), entity.getName() -+ "commands.attribute.modifier.add.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName() // Folia - region threading - ), - false - ); -- return 1; -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }, null, 1L); -+ return 0; -+ // Folia end - region threading - } - - private static int removeModifier(CommandSourceStack source, Entity entity, Holder attribute, ResourceLocation id) throws CommandSyntaxException { -- AttributeInstance attributeInstance = getAttributeInstance(entity, attribute); -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { -+ try { -+ // Folia end - region threading -+ AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); // Folia - region threading - if (attributeInstance.removeModifier(id)) { - source.sendSuccess( - () -> Component.translatable( -- "commands.attribute.modifier.remove.success", Component.translationArg(id), getAttributeDescription(attribute), entity.getName() -+ "commands.attribute.modifier.remove.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName() // Folia - region threading - ), - false - ); -- return 1; -+ return; // Folia - region threading - } else { -- throw ERROR_NO_SUCH_MODIFIER.create(entity.getName(), getAttributeDescription(attribute), id); -+ throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }, null, 1L); -+ return 0; -+ // Folia end - region threading - } - - private static Component getAttributeDescription(Holder attribute) { -diff --git a/net/minecraft/server/commands/ClearInventoryCommands.java b/net/minecraft/server/commands/ClearInventoryCommands.java -index 73650c835ae3a8709d21462bc91a466167cd115f..3cbeaf2046bb0a41085a00134e69162df46d2081 100644 ---- a/net/minecraft/server/commands/ClearInventoryCommands.java -+++ b/net/minecraft/server/commands/ClearInventoryCommands.java -@@ -65,9 +65,14 @@ public class ClearInventoryCommands { - int i = 0; - - for (ServerPlayer serverPlayer : targetPlayers) { -- i += serverPlayer.getInventory().clearOrCountMatchingItems(itemPredicate, maxCount, serverPlayer.inventoryMenu.getCraftSlots()); -- serverPlayer.containerMenu.broadcastChanges(); -- serverPlayer.inventoryMenu.slotsChanged(serverPlayer.getInventory()); -+ // Folia start - region threading -+ ++i; -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { -+ player.getInventory().clearOrCountMatchingItems(itemPredicate, maxCount, player.inventoryMenu.getCraftSlots()); -+ player.containerMenu.broadcastChanges(); -+ player.inventoryMenu.slotsChanged(player.getInventory()); -+ }, null, 1L); -+ // Folia end - region threading - } - - if (i == 0) { -diff --git a/net/minecraft/server/commands/DamageCommand.java b/net/minecraft/server/commands/DamageCommand.java -index d99602f2c7e5463243dfaf83ada12c1d8e7d1192..5f3c886e2bc8a23e902cf8037ac8c871a601883f 100644 ---- a/net/minecraft/server/commands/DamageCommand.java -+++ b/net/minecraft/server/commands/DamageCommand.java -@@ -102,12 +102,29 @@ public class DamageCommand { - ); - } - -+ // Folia start - region threading -+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { -+ src.sendFailure((Component)ex.getRawMessage()); -+ } -+ // Folia end - region threading -+ - private static int damage(CommandSourceStack source, Entity target, float amount, DamageSource damageType) throws CommandSyntaxException { -- if (target.hurtServer(source.getLevel(), damageType, amount)) { -- source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, target.getDisplayName()), true); -- return 1; -+ // Folia start - region threading -+ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { -+ try { -+ // Folia end - region threading -+ if (nmsEntity.hurtServer(source.getLevel(), damageType, amount)) { // Folia - region threading -+ source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, nmsEntity.getDisplayName()), true); // Folia - region threading -+ return; // Folia - region threading - } else { - throw ERROR_INVULNERABLE.create(); - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }, null, 1L); -+ return 0; -+ // Folia end - region threading - } - } -diff --git a/net/minecraft/server/commands/DefaultGameModeCommands.java b/net/minecraft/server/commands/DefaultGameModeCommands.java -index fd42373ccfedf28ffc0fcf9b3153e5a308c561c5..8a8e51c6a63858df2eae4176df75a66636d3f458 100644 ---- a/net/minecraft/server/commands/DefaultGameModeCommands.java -+++ b/net/minecraft/server/commands/DefaultGameModeCommands.java -@@ -28,12 +28,14 @@ public class DefaultGameModeCommands { - GameType forcedGameType = server.getForcedGameType(); - if (forcedGameType != null) { - for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) { -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading - // Paper start - Expand PlayerGameModeChangeEvent -- org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); -+ org.bukkit.event.player.PlayerGameModeChangeEvent event = player.setGameMode(gamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); // Folia - region threading - if (event != null && event.isCancelled()) { - commandSource.sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false); - } - // Paper end - Expand PlayerGameModeChangeEvent -+ }, null, 1L); // Folia - region threading - i++; - } - } -diff --git a/net/minecraft/server/commands/EffectCommands.java b/net/minecraft/server/commands/EffectCommands.java -index 0089ff5ca207278b829ec7530f50ec14681ab574..8d6e1dab63a6ef79d038fc6c3e9f7bf184b1d8c7 100644 ---- a/net/minecraft/server/commands/EffectCommands.java -+++ b/net/minecraft/server/commands/EffectCommands.java -@@ -180,7 +180,12 @@ public class EffectCommands { - for (Entity entity : targets) { - if (entity instanceof LivingEntity) { - MobEffectInstance mobEffectInstance = new MobEffectInstance(effect, i1, amplifier, false, showParticles); -- if (((LivingEntity)entity).addEffect(mobEffectInstance, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { -+ ((LivingEntity)nmsEntity).addEffect(mobEffectInstance, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); -+ }, null, 1L); -+ // Folia end - region threading -+ if (true) { // CraftBukkit // Folia - region threading - i++; - } - } -@@ -210,7 +215,12 @@ public class EffectCommands { - int i = 0; - - for (Entity entity : targets) { -- if (entity instanceof LivingEntity && ((LivingEntity)entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit -+ if (entity instanceof LivingEntity && true) { // CraftBukkit // Folia - region threading -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { -+ ((LivingEntity)nmsEntity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); -+ }, null, 1L); -+ // Folia end - region threading - i++; - } - } -@@ -235,7 +245,12 @@ public class EffectCommands { - int i = 0; - - for (Entity entity : targets) { -- if (entity instanceof LivingEntity && ((LivingEntity)entity).removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit -+ if (entity instanceof LivingEntity && true) { // CraftBukkit // Folia - region threading -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { -+ ((LivingEntity)nmsEntity).removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); -+ }, null, 1L); -+ // Folia end - region threading - i++; - } - } -diff --git a/net/minecraft/server/commands/EnchantCommand.java b/net/minecraft/server/commands/EnchantCommand.java -index fe86823f1a02d66df143756f00ee56fb9f634475..b62ed9d5456ae2c050c4d502b10c5e50c7265b96 100644 ---- a/net/minecraft/server/commands/EnchantCommand.java -+++ b/net/minecraft/server/commands/EnchantCommand.java -@@ -68,51 +68,78 @@ public class EnchantCommand { - ); - } - -+ // Folia start - region threading -+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { -+ src.sendFailure((Component)ex.getRawMessage()); -+ } -+ // Folia end - region threading -+ - private static int enchant(CommandSourceStack source, Collection targets, Holder enchantment, int level) throws CommandSyntaxException { - Enchantment enchantment1 = enchantment.value(); - if (level > enchantment1.getMaxLevel()) { - throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment1.getMaxLevel()); - } else { -- int i = 0; -+ final java.util.concurrent.atomic.AtomicInteger changed = new java.util.concurrent.atomic.AtomicInteger(0); // Folia - region threading -+ final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(targets.size()); // Folia - region threading -+ final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName = new java.util.concurrent.atomic.AtomicReference<>(); // Folia - region threading - - for (Entity entity : targets) { - if (entity instanceof LivingEntity) { -- LivingEntity livingEntity = (LivingEntity)entity; -- ItemStack mainHandItem = livingEntity.getMainHandItem(); -- if (!mainHandItem.isEmpty()) { -- if (enchantment1.canEnchant(mainHandItem) -- && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(mainHandItem).keySet(), enchantment)) { -- mainHandItem.enchant(enchantment, level); -- i++; -- } else if (targets.size() == 1) { -- throw ERROR_INCOMPATIBLE.create(mainHandItem.getHoverName().getString()); -+ // Folia start - region threading -+ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { -+ try { -+ LivingEntity livingEntity = (LivingEntity)nmsEntity; -+ ItemStack mainHandItem = livingEntity.getMainHandItem(); -+ if (!mainHandItem.isEmpty()) { -+ if (enchantment1.canEnchant(mainHandItem) -+ && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(mainHandItem).keySet(), enchantment)) { -+ mainHandItem.enchant(enchantment, level); -+ possibleSingleDisplayName.set(livingEntity.getDisplayName()); -+ changed.incrementAndGet(); -+ } else if (targets.size() == 1) { -+ throw ERROR_INCOMPATIBLE.create(mainHandItem.getHoverName().getString()); -+ } -+ } else if (targets.size() == 1) { -+ throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); -+ } -+ } catch (final CommandSyntaxException exception) { -+ sendMessage(source, exception); -+ return; // don't send feedback twice - } -- } else if (targets.size() == 1) { -- throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); -- } -+ sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed); -+ }, ignored -> sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed), 1L); - } else if (targets.size() == 1) { - throw ERROR_NOT_LIVING_ENTITY.create(entity.getName().getString()); -+ } else { -+ sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed); -+ // Folia end - region threading - } - } -+ return targets.size(); // Folia - region threading -+ } -+ } - -+ // Folia start - region threading -+ private static void sendFeedback(final CommandSourceStack source, final Holder enchantment, final int level, final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName, final java.util.concurrent.atomic.AtomicInteger count, final java.util.concurrent.atomic.AtomicInteger changed) { -+ if (count.decrementAndGet() == 0) { -+ final int i = changed.get(); - if (i == 0) { -- throw ERROR_NOTHING_HAPPENED.create(); -+ sendMessage(source, ERROR_NOTHING_HAPPENED.create()); - } else { -- if (targets.size() == 1) { -+ if (i == 1) { - source.sendSuccess( - () -> Component.translatable( -- "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), targets.iterator().next().getDisplayName() -+ "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), possibleSingleDisplayName.get() - ), - true - ); - } else { - source.sendSuccess( -- () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), targets.size()), true -+ () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), i), true - ); - } -- -- return i; - } - } - } -+ // Folia end - region threading - } -diff --git a/net/minecraft/server/commands/ExperienceCommand.java b/net/minecraft/server/commands/ExperienceCommand.java -index cb59af8018d3009876a47fae249885c00b6c7b57..e0d95f61e8a2841979bc9b5381dfaf7d3239beb7 100644 ---- a/net/minecraft/server/commands/ExperienceCommand.java -+++ b/net/minecraft/server/commands/ExperienceCommand.java -@@ -131,14 +131,18 @@ public class ExperienceCommand { - } - - private static int queryExperience(CommandSourceStack source, ServerPlayer player, ExperienceCommand.Type type) { -- int i = type.query.applyAsInt(player); -- source.sendSuccess(() -> Component.translatable("commands.experience.query." + type.name, player.getDisplayName(), i), false); -- return i; -+ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading -+ int i = type.query.applyAsInt(serverPlayer); // Folia - region threading -+ source.sendSuccess(() -> Component.translatable("commands.experience.query." + type.name, serverPlayer.getDisplayName(), i), false); // Folia - region threading -+ }, null, 1L); // Folia - region threading -+ return 0; // Folia - region threading - } - - private static int addExperience(CommandSourceStack source, Collection targets, int amount, ExperienceCommand.Type type) { - for (ServerPlayer serverPlayer : targets) { -- type.add.accept(serverPlayer, amount); -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading -+ type.add.accept(player, amount); -+ }, null, 1L); // Folia - region threading - } - - if (targets.size() == 1) { -@@ -157,9 +161,11 @@ public class ExperienceCommand { - int i = 0; - - for (ServerPlayer serverPlayer : targets) { -- if (type.set.test(serverPlayer, amount)) { -- i++; -+ i++; serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading -+ if (type.set.test(player, amount)) { // Folia - region threading -+ //i++; // Folia - region threading - } -+ }, null, 1L); // Folia - region threading - } - - if (i == 0) { -diff --git a/net/minecraft/server/commands/FillBiomeCommand.java b/net/minecraft/server/commands/FillBiomeCommand.java -index bb2c8612b27bb04758c467ec6245de1236fc4de1..d5ae0eeb504b9306015de37abc59bf1a76a23837 100644 ---- a/net/minecraft/server/commands/FillBiomeCommand.java -+++ b/net/minecraft/server/commands/FillBiomeCommand.java -@@ -107,6 +107,16 @@ public class FillBiomeCommand { - return fill(level, from, to, biome, biome1 -> true, message -> {}); - } - -+ // Folia start - region threading -+ private static void sendMessage(Consumer> src, Supplier> supplier) { -+ Either either = supplier.get(); -+ CommandSyntaxException ex = either == null ? null : either.right().orElse(null); -+ if (ex != null) { -+ src.accept(() -> (Component)ex.getRawMessage()); -+ } -+ } -+ // Folia end - region threading -+ - public static Either fill( - ServerLevel level, BlockPos from, BlockPos to, Holder biome, Predicate> filter, Consumer> messageOutput - ) { -@@ -118,6 +128,17 @@ public class FillBiomeCommand { - if (i > _int) { - return Either.right(ERROR_VOLUME_TOO_LARGE.create(_int, i)); - } else { -+ // Folia start - region threading -+ int buffer = 0; // no buffer, we do not touch neighbours -+ level.moonrise$loadChunksAsync( -+ (boundingBox.minX() - buffer) >> 4, -+ (boundingBox.maxX() + buffer) >> 4, -+ (boundingBox.minZ() - buffer) >> 4, -+ (boundingBox.maxZ() + buffer) >> 4, -+ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, -+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, -+ (chunks) -> { -+ sendMessage(messageOutput, () -> { - List list = new ArrayList<>(); - - for (int sectionPosMinZ = SectionPos.blockToSectionCoord(boundingBox.minZ()); -@@ -158,6 +179,11 @@ public class FillBiomeCommand { - ) - ); - return Either.left(mutableInt.getValue()); -+ // Folia start - region threading -+ }); // sendMessage -+ }); // loadChunksASync -+ return Either.left(Integer.valueOf(0)); -+ // Folia end - region threading - } - } - -diff --git a/net/minecraft/server/commands/FillCommand.java b/net/minecraft/server/commands/FillCommand.java -index a224f8cc122fc6d79b4abd08815f58f0e6aa340b..89154adfc659afa188cd771e70087e3b1a9c98b9 100644 ---- a/net/minecraft/server/commands/FillCommand.java -+++ b/net/minecraft/server/commands/FillCommand.java -@@ -151,6 +151,12 @@ public class FillCommand { - ); - } - -+ // Folia start - region threading -+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { -+ src.sendFailure((Component)ex.getRawMessage()); -+ } -+ // Folia end - region threading -+ - private static int fillBlocks( - CommandSourceStack source, BoundingBox area, BlockInput newBlock, FillCommand.Mode mode, @Nullable Predicate replacingPredicate - ) throws CommandSyntaxException { -@@ -161,6 +167,18 @@ public class FillCommand { - } else { - List list = Lists.newArrayList(); - ServerLevel level = source.getLevel(); -+ // Folia start - region threading -+ int buffer = 32; -+ // physics may spill into neighbour chunks, so use a buffer -+ level.moonrise$loadChunksAsync( -+ (area.minX() - buffer) >> 4, -+ (area.maxX() + buffer) >> 4, -+ (area.minZ() - buffer) >> 4, -+ (area.maxZ() + buffer) >> 4, -+ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, -+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, -+ (chunks) -> { -+ try { // Folia end - region threading - int i1 = 0; - - for (BlockPos blockPos : BlockPos.betweenClosed(area.minX(), area.minY(), area.minZ(), area.maxX(), area.maxY(), area.maxZ())) { -@@ -187,8 +205,13 @@ public class FillCommand { - } else { - int i2 = i1; - source.sendSuccess(() -> Component.translatable("commands.fill.success", i2), true); -- return i1; -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); return 0; // Folia end - region threading - } - } - -diff --git a/net/minecraft/server/commands/ForceLoadCommand.java b/net/minecraft/server/commands/ForceLoadCommand.java -index 619ffb7846047d3e033378c750dc4ceaf9ac6239..6e174d54a3bf6a7a23a0aa6e7802b407e3969a47 100644 ---- a/net/minecraft/server/commands/ForceLoadCommand.java -+++ b/net/minecraft/server/commands/ForceLoadCommand.java -@@ -97,7 +97,17 @@ public class ForceLoadCommand { - ); - } - -+ // Folia start - region threading -+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { -+ src.sendFailure((Component)ex.getRawMessage()); -+ } -+ // Folia end - region threading -+ - private static int queryForceLoad(CommandSourceStack source, ColumnPos pos) throws CommandSyntaxException { -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ try { -+ // Folia end - region threading - ChunkPos chunkPos = pos.toChunkPos(); - ServerLevel level = source.getLevel(); - ResourceKey resourceKey = level.dimension(); -@@ -109,14 +119,22 @@ public class ForceLoadCommand { - ), - false - ); -- return 1; -+ return; // Folia - region threading - } else { - throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location()); - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); -+ return 1; -+ // Folia end - region threading - } - - private static int listForceLoad(CommandSourceStack source) { - ServerLevel level = source.getLevel(); -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading - ResourceKey resourceKey = level.dimension(); - LongSet forcedChunks = level.getForcedChunks(); - int size = forcedChunks.size(); -@@ -134,20 +152,27 @@ public class ForceLoadCommand { - } else { - source.sendFailure(Component.translatable("commands.forceload.added.none", Component.translationArg(resourceKey.location()))); - } -+ }); // Folia - region threading - -- return size; -+ return 1; // Folia - region threading - } - - private static int removeAll(CommandSourceStack source) { - ServerLevel level = source.getLevel(); - ResourceKey resourceKey = level.dimension(); -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading - LongSet forcedChunks = level.getForcedChunks(); - forcedChunks.forEach(packedChunkPos -> level.setChunkForced(ChunkPos.getX(packedChunkPos), ChunkPos.getZ(packedChunkPos), false)); - source.sendSuccess(() -> Component.translatable("commands.forceload.removed.all", Component.translationArg(resourceKey.location())), true); -+ }); // Folia - region threading - return 0; - } - - private static int changeForceLoad(CommandSourceStack source, ColumnPos from, ColumnPos to, boolean add) throws CommandSyntaxException { -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ try { -+ // Folia end - region threading - int min = Math.min(from.x(), to.x()); - int min1 = Math.min(from.z(), to.z()); - int max = Math.max(from.x(), to.x()); -@@ -207,11 +232,18 @@ public class ForceLoadCommand { - ); - } - -- return i2x; -+ return; // Folia - region threading - } - } - } else { - throw BlockPosArgument.ERROR_OUT_OF_WORLD.create(); - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); -+ return 1; -+ // Folia end - region threading - } - } -diff --git a/net/minecraft/server/commands/GameModeCommand.java b/net/minecraft/server/commands/GameModeCommand.java -index c44cdbbdc06b25bd20a208386545a10af9b96df8..f6204e765afda2668ab394c570444fbb7f152b8b 100644 ---- a/net/minecraft/server/commands/GameModeCommand.java -+++ b/net/minecraft/server/commands/GameModeCommand.java -@@ -54,15 +54,18 @@ public class GameModeCommand { - int i = 0; - - for (ServerPlayer serverPlayer : players) { -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer nmsEntity) -> { // Folia - region threading - // Paper start - Expand PlayerGameModeChangeEvent -- org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); -+ org.bukkit.event.player.PlayerGameModeChangeEvent event = nmsEntity.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); // Folia - region threading - if (event != null && !event.isCancelled()) { -- logGamemodeChange(source.getSource(), serverPlayer, gameType); -- i++; -+ logGamemodeChange(source.getSource(), nmsEntity, gameType); // Folia - region threading -+ //i++; // Folia - region threading - } else if (event != null && event.cancelMessage() != null) { - source.getSource().sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true); - // Paper end - Expand PlayerGameModeChangeEvent - } -+ }, null, 1L); // Folia - region threading -+ ++i; // Folia - region threading - } - - return i; -diff --git a/net/minecraft/server/commands/GiveCommand.java b/net/minecraft/server/commands/GiveCommand.java -index 8b7af734ca4ed3cafa810460b2cea6c1e6342a69..6f5d88d83ad724fa2b7549075b687aebd4b24eed 100644 ---- a/net/minecraft/server/commands/GiveCommand.java -+++ b/net/minecraft/server/commands/GiveCommand.java -@@ -65,32 +65,34 @@ public class GiveCommand { - int min = Math.min(maxStackSize, i1); - i1 -= min; - ItemStack itemStack1 = item.createItemStack(min, false); -- boolean flag = serverPlayer.getInventory().add(itemStack1); -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer nmsEntity) -> { // Folia - region threading -+ boolean flag = nmsEntity.getInventory().add(itemStack1); // Folia - region threading - if (flag && itemStack1.isEmpty()) { -- ItemEntity itemEntity = serverPlayer.drop(itemStack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event -+ ItemEntity itemEntity = nmsEntity.drop(itemStack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event // Folia - region threading - if (itemEntity != null) { - itemEntity.makeFakeItem(); - } - -- serverPlayer.level() -+ nmsEntity.level() // Folia - region threading - .playSound( - null, -- serverPlayer.getX(), -- serverPlayer.getY(), -- serverPlayer.getZ(), -+ nmsEntity.getX(), // Folia - region threading -+ nmsEntity.getY(), // Folia - region threading -+ nmsEntity.getZ(), // Folia - region threading - SoundEvents.ITEM_PICKUP, - SoundSource.PLAYERS, - 0.2F, -- ((serverPlayer.getRandom().nextFloat() - serverPlayer.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F -+ ((nmsEntity.getRandom().nextFloat() - nmsEntity.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F // Folia - region threading - ); -- serverPlayer.containerMenu.broadcastChanges(); -+ nmsEntity.containerMenu.broadcastChanges(); // Folia - region threading - } else { -- ItemEntity itemEntity = serverPlayer.drop(itemStack1, false); -+ ItemEntity itemEntity = nmsEntity.drop(itemStack1, false); // Folia - region threading - if (itemEntity != null) { - itemEntity.setNoPickUpDelay(); -- itemEntity.setTarget(serverPlayer.getUUID()); -+ itemEntity.setTarget(nmsEntity.getUUID()); // Folia - region threading - } - } -+ }, null, 1L); // Folia - region threading - } - } - -diff --git a/net/minecraft/server/commands/KillCommand.java b/net/minecraft/server/commands/KillCommand.java -index e8ab673921c8089a35a2e678d7a6efed1f728cd7..287681a351f49eabd4f480396314a882bee73645 100644 ---- a/net/minecraft/server/commands/KillCommand.java -+++ b/net/minecraft/server/commands/KillCommand.java -@@ -24,7 +24,9 @@ public class KillCommand { - - private static int kill(CommandSourceStack source, Collection targets) { - for (Entity entity : targets) { -- entity.kill(source.getLevel()); -+ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { // Folia - region threading -+ nmsEntity.kill((net.minecraft.server.level.ServerLevel)nmsEntity.level()); // Folia - region threading -+ }, null, 1L); // Folia - region threading - } - - if (targets.size() == 1) { -diff --git a/net/minecraft/server/commands/PlaceCommand.java b/net/minecraft/server/commands/PlaceCommand.java -index f019285714cf6e7ac08d6b3b96fe705b8a564c28..4decfa02f0fa11a14abd48944e9cb2dd86bb96a2 100644 ---- a/net/minecraft/server/commands/PlaceCommand.java -+++ b/net/minecraft/server/commands/PlaceCommand.java -@@ -233,36 +233,79 @@ public class PlaceCommand { - ); - } - -+ // Folia start - region threading -+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { -+ src.sendFailure((Component)ex.getRawMessage()); -+ } -+ // Folia end - region threading -+ - public static int placeFeature(CommandSourceStack source, Holder.Reference> feature, BlockPos pos) throws CommandSyntaxException { - ServerLevel level = source.getLevel(); - ConfiguredFeature configuredFeature = feature.value(); - ChunkPos chunkPos = new ChunkPos(pos); - checkLoaded(level, new ChunkPos(chunkPos.x - 1, chunkPos.z - 1), new ChunkPos(chunkPos.x + 1, chunkPos.z + 1)); -+ // Folia start - region threading -+ level.moonrise$loadChunksAsync( -+ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, -+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, -+ (chunks) -> { -+ try { -+ // Folia end - region threading - if (!configuredFeature.place(level, level.getChunkSource().getGenerator(), level.getRandom(), pos)) { - throw ERROR_FEATURE_FAILED.create(); - } else { - String string = feature.key().location().toString(); - source.sendSuccess(() -> Component.translatable("commands.place.feature.success", string, pos.getX(), pos.getY(), pos.getZ()), true); -- return 1; -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ } -+ ); -+ return 1; -+ // Folia end - region threading - } - - public static int placeJigsaw(CommandSourceStack source, Holder templatePool, ResourceLocation target, int maxDepth, BlockPos pos) throws CommandSyntaxException { - ServerLevel level = source.getLevel(); - ChunkPos chunkPos = new ChunkPos(pos); - checkLoaded(level, chunkPos, chunkPos); -+ // Folia start - region threading -+ level.moonrise$loadChunksAsync( -+ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, -+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, -+ (chunks) -> { -+ try { -+ // Folia end - region threading - if (!JigsawPlacement.generateJigsaw(level, templatePool, target, maxDepth, pos, false)) { - throw ERROR_JIGSAW_FAILED.create(); - } else { - source.sendSuccess(() -> Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()), true); -- return 1; -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ } -+ ); -+ return 1; -+ // Folia end - region threading - } - - public static int placeStructure(CommandSourceStack source, Holder.Reference structure, BlockPos pos) throws CommandSyntaxException { - ServerLevel level = source.getLevel(); - Structure structure1 = structure.value(); - ChunkGenerator generator = level.getChunkSource().getGenerator(); -+ // Folia start - region threading -+ level.moonrise$loadChunksAsync( -+ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, -+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, -+ (chunks) -> { -+ try { -+ // Folia end - region threading - StructureStart structureStart = structure1.generate( - structure, - level.dimension(), -@@ -305,14 +348,29 @@ public class PlaceCommand { - ); - String string = structure.key().location().toString(); - source.sendSuccess(() -> Component.translatable("commands.place.structure.success", string, pos.getX(), pos.getY(), pos.getZ()), true); -- return 1; -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ } -+ ); -+ return 1; -+ // Folia end - region threading - } - - public static int placeTemplate( - CommandSourceStack source, ResourceLocation template, BlockPos pos, Rotation rotation, Mirror mirror, float integrity, int seed - ) throws CommandSyntaxException { - ServerLevel level = source.getLevel(); -+ // Folia start - region threading -+ level.moonrise$loadChunksAsync( -+ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, -+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, -+ (chunks) -> { -+ try { -+ // Folia end - region threading - StructureTemplateManager structureManager = level.getStructureManager(); - - Optional optional; -@@ -340,9 +398,17 @@ public class PlaceCommand { - () -> Component.translatable("commands.place.template.success", Component.translationArg(template), pos.getX(), pos.getY(), pos.getZ()), - true - ); -- return 1; -+ return; // Folia - region threading - } - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ } -+ ); -+ return 1; -+ // Folia end - region threading - } - - private static void checkLoaded(ServerLevel level, ChunkPos start, ChunkPos end) throws CommandSyntaxException { -diff --git a/net/minecraft/server/commands/RecipeCommand.java b/net/minecraft/server/commands/RecipeCommand.java -index d171a5b8c1969f6a482f029afa5fb0228aefb04d..c8ab7f56c5e5af99b5410784a3ae33dedd7bf2f3 100644 ---- a/net/minecraft/server/commands/RecipeCommand.java -+++ b/net/minecraft/server/commands/RecipeCommand.java -@@ -81,7 +81,12 @@ public class RecipeCommand { - int i = 0; - - for (ServerPlayer serverPlayer : targets) { -- i += serverPlayer.awardRecipes(recipes); -+ // Folia start - region threading -+ ++i; -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { -+ player.awardRecipes(recipes); -+ }, null, 1L); -+ // Folia end - region threading - } - - if (i == 0) { -@@ -103,7 +108,12 @@ public class RecipeCommand { - int i = 0; - - for (ServerPlayer serverPlayer : targets) { -- i += serverPlayer.resetRecipes(recipes); -+ // Folia start - region threading -+ ++i; -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { -+ player.resetRecipes(recipes); -+ }, null, 1L); -+ // Folia end - region threading - } - - if (i == 0) { -diff --git a/net/minecraft/server/commands/SetBlockCommand.java b/net/minecraft/server/commands/SetBlockCommand.java -index 8b72116b80da0497e255ce5a3f3c7bccb6321aec..05b824409546ba8bacf7efdaeac106af89ff0715 100644 ---- a/net/minecraft/server/commands/SetBlockCommand.java -+++ b/net/minecraft/server/commands/SetBlockCommand.java -@@ -80,10 +80,21 @@ public class SetBlockCommand { - ); - } - -+ // Folia start - region threading -+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { -+ src.sendFailure((Component)ex.getRawMessage()); -+ } -+ // Folia end - region threading -+ - private static int setBlock( - CommandSourceStack source, BlockPos pos, BlockInput state, SetBlockCommand.Mode mode, @Nullable Predicate predicate - ) throws CommandSyntaxException { - ServerLevel level = source.getLevel(); -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ level, pos.getX() >> 4, pos.getZ() >> 4, () -> { -+ try { -+ // Folia end - region threading - if (predicate != null && !predicate.test(new BlockInWorld(level, pos, true))) { - throw ERROR_FAILED.create(); - } else { -@@ -102,9 +113,16 @@ public class SetBlockCommand { - } else { - level.blockUpdated(pos, state.getState().getBlock()); - source.sendSuccess(() -> Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true); -- return 1; -+ return; // Folia - region threading - } - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); -+ return 1; -+ // Folia end - region threading - } - - public interface Filter { -diff --git a/net/minecraft/server/commands/SetSpawnCommand.java b/net/minecraft/server/commands/SetSpawnCommand.java -index e38c7f012098e46337561b2225b31a7097495647..6fc6a748a8096524440d32d692088a8176875786 100644 ---- a/net/minecraft/server/commands/SetSpawnCommand.java -+++ b/net/minecraft/server/commands/SetSpawnCommand.java -@@ -69,7 +69,11 @@ public class SetSpawnCommand { - final Collection actualTargets = new java.util.ArrayList<>(); // Paper - Add PlayerSetSpawnEvent - for (ServerPlayer serverPlayer : targets) { - // Paper start - Add PlayerSetSpawnEvent -- if (serverPlayer.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) { -+ // Folia start - region threading -+ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { -+ player.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND); -+ }, null, 1L); -+ if (true) { // Folia end - region threading - actualTargets.add(serverPlayer); - } - // Paper end - Add PlayerSetSpawnEvent -diff --git a/net/minecraft/server/commands/SummonCommand.java b/net/minecraft/server/commands/SummonCommand.java -index b68c0e617d3593cc9ba999ed25ea2c1b7c762597..2d4bf39f3f35811a7f48f361c91ee3d5722ba839 100644 ---- a/net/minecraft/server/commands/SummonCommand.java -+++ b/net/minecraft/server/commands/SummonCommand.java -@@ -88,12 +88,18 @@ public class SummonCommand { - if (entity == null) { - throw ERROR_FAILED.create(); - } else { -- if (randomizeProperties && entity instanceof Mob) { -- ((Mob)entity) -- .finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, null); -- } -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ level, entity.chunkPosition().x, entity.chunkPosition().z, () -> { -+ if (randomizeProperties && entity instanceof Mob) { -+ ((Mob)entity) -+ .finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, null); -+ } -+ level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND); -+ }); -+ // Folia end - region threading - -- if (!level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND" -+ if (false) { // CraftBukkit - pass a spawn reason of "COMMAND" // Folia - region threading - throw ERROR_DUPLICATE_UUID.create(); - } else { - return entity; -diff --git a/net/minecraft/server/commands/TeleportCommand.java b/net/minecraft/server/commands/TeleportCommand.java -index 01f8e2fec232210c9311565197860cf0257081fd..174122905addbc88e818cd4946e831aec051b91a 100644 ---- a/net/minecraft/server/commands/TeleportCommand.java -+++ b/net/minecraft/server/commands/TeleportCommand.java -@@ -154,18 +154,7 @@ public class TeleportCommand { - - private static int teleportToEntity(CommandSourceStack source, Collection targets, Entity destination) throws CommandSyntaxException { - for (Entity entity : targets) { -- performTeleport( -- source, -- entity, -- (ServerLevel)destination.level(), -- destination.getX(), -- destination.getY(), -- destination.getZ(), -- EnumSet.noneOf(Relative.class), -- destination.getYRot(), -- destination.getXRot(), -- null -- ); -+ io.papermc.paper.threadedregions.TeleportUtils.teleport(entity, false, destination, Float.valueOf(destination.getYRot()), Float.valueOf(destination.getXRot()), Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, null); // Folia - region threading - } - - if (targets.size() == 1) { -@@ -290,6 +279,24 @@ public class TeleportCommand { - float f1 = relatives.contains(Relative.X_ROT) ? xRot - target.getXRot() : xRot; - float f2 = Mth.wrapDegrees(f); - float f3 = Mth.wrapDegrees(f1); -+ // Folia start - region threading -+ if (true) { -+ ServerLevel worldFinal = level; -+ Vec3 posFinal = new Vec3(x, y, z); -+ Float yawFinal = Float.valueOf(f); -+ Float pitchFinal = Float.valueOf(f1); -+ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { -+ nmsEntity.unRide(); -+ nmsEntity.teleportAsync( -+ worldFinal, posFinal, yawFinal, pitchFinal, Vec3.ZERO, -+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, -+ Entity.TELEPORT_FLAG_LOAD_CHUNK, -+ null -+ ); -+ }, null, 1L); -+ return; -+ } -+ // Folia end - region threading - // CraftBukkit start - Teleport event - boolean result; - if (target instanceof final net.minecraft.server.level.ServerPlayer player) { -diff --git a/net/minecraft/server/commands/TimeCommand.java b/net/minecraft/server/commands/TimeCommand.java -index e952ca088a2f36fc7f1eef4d9b217351569becc1..5d1fc3bb00abd177325a292f55d2cf1cddd3158b 100644 ---- a/net/minecraft/server/commands/TimeCommand.java -+++ b/net/minecraft/server/commands/TimeCommand.java -@@ -56,6 +56,7 @@ public class TimeCommand { - } - - public static int setTime(CommandSourceStack source, int time) { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading - for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change - // serverLevel.setDayTime(time); - // CraftBukkit start -@@ -69,10 +70,12 @@ public class TimeCommand { - - source.getServer().forceTimeSynchronization(); - source.sendSuccess(() -> Component.translatable("commands.time.set", time), true); -- return getDayTime(source.getLevel()); -+ }); // Folia - region threading -+ return 0; // Folia - region threading - } - - public static int addTime(CommandSourceStack source, int amount) { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading - for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change - // CraftBukkit start - org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(serverLevel.getWorld(), org.bukkit.event.world.TimeSkipEvent.SkipReason.COMMAND, amount); -@@ -86,6 +89,7 @@ public class TimeCommand { - source.getServer().forceTimeSynchronization(); - int dayTime = getDayTime(source.getLevel()); - source.sendSuccess(() -> Component.translatable("commands.time.set", dayTime), true); -- return dayTime; -+ }); // Folia - region threading -+ return 0; // Folia - region threading - } - } -diff --git a/net/minecraft/server/commands/WeatherCommand.java b/net/minecraft/server/commands/WeatherCommand.java -index 9b14b6218b2673e9b13b749b566e3b8a6a8d9c7d..dade5adec00c081cb4def7464f0f04d2f5a6ae26 100644 ---- a/net/minecraft/server/commands/WeatherCommand.java -+++ b/net/minecraft/server/commands/WeatherCommand.java -@@ -48,20 +48,26 @@ public class WeatherCommand { - } - - private static int setClear(CommandSourceStack source, int time) { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading - source.getLevel().setWeatherParameters(getDuration(source, time, ServerLevel.RAIN_DELAY), 0, false, false); // CraftBukkit - SPIGOT-7680: per-world - source.sendSuccess(() -> Component.translatable("commands.weather.set.clear"), true); -+ }); // Folia - region threading - return time; - } - - private static int setRain(CommandSourceStack source, int time) { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading - source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.RAIN_DURATION), true, false); // CraftBukkit - SPIGOT-7680: per-world - source.sendSuccess(() -> Component.translatable("commands.weather.set.rain"), true); -+ }); // Folia - region threading - return time; - } - - private static int setThunder(CommandSourceStack source, int time) { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading - source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.THUNDER_DURATION), true, true); // CraftBukkit - SPIGOT-7680: per-world - source.sendSuccess(() -> Component.translatable("commands.weather.set.thunder"), true); -+ }); // Folia - region threading - return time; - } - } -diff --git a/net/minecraft/server/commands/WorldBorderCommand.java b/net/minecraft/server/commands/WorldBorderCommand.java -index e2697b03a0d204eea537e3aaec2dd8fb9f426722..f6af541a7076c3fefb237b865038d08919de35ed 100644 ---- a/net/minecraft/server/commands/WorldBorderCommand.java -+++ b/net/minecraft/server/commands/WorldBorderCommand.java -@@ -134,18 +134,39 @@ public class WorldBorderCommand { - ); - } - -+ // Folia start - region threading -+ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { -+ src.sendFailure((Component)ex.getRawMessage()); -+ } -+ // Folia end - region threading -+ - private static int setDamageBuffer(CommandSourceStack source, float distance) throws CommandSyntaxException { -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ try { -+ // Folia end - region threading - WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit - if (worldBorder.getDamageSafeZone() == distance) { - throw ERROR_SAME_DAMAGE_BUFFER.create(); - } else { - worldBorder.setDamageSafeZone(distance); - source.sendSuccess(() -> Component.translatable("commands.worldborder.damage.buffer.success", String.format(Locale.ROOT, "%.2f", distance)), true); -- return (int)distance; -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); -+ return 1; -+ // Folia end - region threading - } - - private static int setDamageAmount(CommandSourceStack source, float damagePerBlock) throws CommandSyntaxException { -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ try { -+ // Folia end - region threading - WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit - if (worldBorder.getDamagePerBlock() == damagePerBlock) { - throw ERROR_SAME_DAMAGE_AMOUNT.create(); -@@ -154,39 +175,79 @@ public class WorldBorderCommand { - source.sendSuccess( - () -> Component.translatable("commands.worldborder.damage.amount.success", String.format(Locale.ROOT, "%.2f", damagePerBlock)), true - ); -- return (int)damagePerBlock; -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); -+ return 1; -+ // Folia end - region threading - } - - private static int setWarningTime(CommandSourceStack source, int time) throws CommandSyntaxException { -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ try { -+ // Folia end - region threading - WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit - if (worldBorder.getWarningTime() == time) { - throw ERROR_SAME_WARNING_TIME.create(); - } else { - worldBorder.setWarningTime(time); - source.sendSuccess(() -> Component.translatable("commands.worldborder.warning.time.success", time), true); -- return time; -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); -+ return 1; -+ // Folia end - region threading - } - - private static int setWarningDistance(CommandSourceStack source, int distance) throws CommandSyntaxException { -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ try { -+ // Folia end - region threading - WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit - if (worldBorder.getWarningBlocks() == distance) { - throw ERROR_SAME_WARNING_DISTANCE.create(); - } else { - worldBorder.setWarningBlocks(distance); - source.sendSuccess(() -> Component.translatable("commands.worldborder.warning.distance.success", distance), true); -- return distance; -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); -+ return 1; -+ // Folia end - region threading - } - - private static int getSize(CommandSourceStack source) { -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ // Folia end - region threading - double size = source.getLevel().getWorldBorder().getSize(); // CraftBukkit - source.sendSuccess(() -> Component.translatable("commands.worldborder.get", String.format(Locale.ROOT, "%.0f", size)), false); -- return Mth.floor(size + 0.5); -+ return; // Folia - region threading -+ // Folia start - region threading -+ }); -+ return 1; -+ // Folia end - region threading - } - - private static int setCenter(CommandSourceStack source, Vec2 pos) throws CommandSyntaxException { -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ try { -+ // Folia end - region threading - WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit - if (worldBorder.getCenterX() == pos.x && worldBorder.getCenterZ() == pos.y) { - throw ERROR_SAME_CENTER.create(); -@@ -198,13 +259,24 @@ public class WorldBorderCommand { - ), - true - ); -- return 0; -+ return; // Folia - region threading - } else { - throw ERROR_TOO_FAR_OUT.create(); - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); -+ return 1; -+ // Folia end - region threading - } - - private static int setSize(CommandSourceStack source, double newSize, long time) throws CommandSyntaxException { -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ try { -+ // Folia end - region threading - WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit - double size = worldBorder.getSize(); - if (size == newSize) { -@@ -234,7 +306,14 @@ public class WorldBorderCommand { - source.sendSuccess(() -> Component.translatable("commands.worldborder.set.immediate", String.format(Locale.ROOT, "%.1f", newSize)), true); - } - -- return (int)(newSize - size); -+ return; // Folia - region threading - } -+ // Folia start - region threading -+ } catch (CommandSyntaxException ex) { -+ sendMessage(source, ex); -+ } -+ }); -+ return 1; -+ // Folia end - region threading - } - } -diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java -index 97a294d2f5c1ddf0af7ffec3e1425eb329c5751b..341e400f789e0eda29827e2c45c483a470d2e982 100644 ---- a/net/minecraft/server/dedicated/DedicatedServer.java -+++ b/net/minecraft/server/dedicated/DedicatedServer.java -@@ -425,7 +425,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - @Override - public void tickConnection() { - super.tickConnection(); -- this.handleConsoleInputs(); -+ // Folia - region threading - } - - @Override -@@ -732,7 +732,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - - public String runCommand(RconConsoleSource rconConsoleSource, String s) { - rconConsoleSource.prepareForCommand(); -- this.executeBlocking(() -> { -+ final java.util.concurrent.atomic.AtomicReference command = new java.util.concurrent.atomic.AtomicReference<>(s); // Folia start - region threading -+ Runnable sync = () -> { // Folia - region threading - CommandSourceStack wrapper = rconConsoleSource.createCommandSourceStack(); - org.bukkit.event.server.RemoteServerCommandEvent event = new org.bukkit.event.server.RemoteServerCommandEvent(rconConsoleSource.getBukkitSender(wrapper), s); - this.server.getPluginManager().callEvent(event); -@@ -741,7 +742,16 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - } - ConsoleInput serverCommand = new ConsoleInput(event.getCommand(), wrapper); - this.server.dispatchServerCommand(event.getSender(), serverCommand); -- }); -+ }; // Folia start - region threading -+ java.util.concurrent.CompletableFuture -+ .runAsync(sync, io.papermc.paper.threadedregions.RegionizedServer.getInstance()::addTask) -+ .whenComplete((Void r, Throwable t) -> { -+ if (t != null) { -+ LOGGER.error("Error handling command for rcon: " + s, t); -+ } -+ }) -+ .join(); -+ // Folia end - region threading - return rconConsoleSource.getCommandResponse(); - // CraftBukkit end - } -diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java -index b3f498558614243cf633dcd71e3c49c2c55e6e0f..06cc9d69220b09667db30a5c1e161333d2563b23 100644 ---- a/net/minecraft/server/level/ChunkMap.java -+++ b/net/minecraft/server/level/ChunkMap.java -@@ -128,8 +128,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - public final ChunkMap.DistanceManager distanceManager; - public final AtomicInteger tickingGenerated = new AtomicInteger(); // Paper - public - private final String storageName; -- private final PlayerMap playerMap = new PlayerMap(); -- public final Int2ObjectMap entityMap = new Int2ObjectOpenHashMap<>(); -+ //private final PlayerMap playerMap = new PlayerMap(); // Folia - region threading -+ //public final Int2ObjectMap entityMap = new Int2ObjectOpenHashMap<>(); // Folia - region threading - private final Long2ByteMap chunkTypeCache = new Long2ByteOpenHashMap(); - // Paper - rewrite chunk system - public int serverViewDistance; -@@ -797,12 +797,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - void updatePlayerStatus(ServerPlayer player, boolean track) { - boolean flag = this.skipPlayer(player); -- boolean flag1 = this.playerMap.ignoredOrUnknown(player); -+ //boolean flag1 = this.playerMap.ignoredOrUnknown(player); // Folia - region threading - if (track) { -- this.playerMap.addPlayer(player, flag); -+ //this.playerMap.addPlayer(player, flag); // Folia - region threading - this.updatePlayerPos(player); - if (!flag) { -- this.distanceManager.addPlayer(SectionPos.of(player), player); -+ //this.distanceManager.addPlayer(SectionPos.of(player), player); // Folia - region threading - ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation - } - -@@ -810,9 +810,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - ca.spottedleaf.moonrise.common.PlatformHooks.get().addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system - } else { - SectionPos lastSectionPos = player.getLastSectionPos(); -- this.playerMap.removePlayer(player); -- if (!flag1) { -- this.distanceManager.removePlayer(lastSectionPos, player); -+ //this.playerMap.removePlayer(player); // Folia - region threading -+ if (true) { // Folia - region threading -+ //this.distanceManager.removePlayer(lastSectionPos, player); // Folia - region threading - ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation - } - -@@ -830,27 +830,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - SectionPos lastSectionPos = player.getLastSectionPos(); - SectionPos sectionPos = SectionPos.of(player); -- boolean flag = this.playerMap.ignored(player); -+ //boolean flag = this.playerMap.ignored(player); // Folia - region threading - boolean flag1 = this.skipPlayer(player); -- boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong(); -- if (flag2 || flag != flag1) { -+ //boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong(); // Folia - region threading -+ if (true) { // Folia - region threading - this.updatePlayerPos(player); -- ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, lastSectionPos, sectionPos, flag, flag1); // Paper - chunk tick iteration optimisation -- if (!flag) { -- this.distanceManager.removePlayer(lastSectionPos, player); -- } -- -- if (!flag1) { -- this.distanceManager.addPlayer(sectionPos, player); -- } -- -- if (!flag && flag1) { -- this.playerMap.ignorePlayer(player); -- } -- -- if (flag && !flag1) { -- this.playerMap.unIgnorePlayer(player); -- } -+ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, lastSectionPos, sectionPos, false, flag1); // Paper - chunk tick iteration optimisation // Folia - region threading -+ // Folia - region threading - - // Paper - rewrite chunk system - } -@@ -880,9 +866,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - public void addEntity(Entity entity) { - org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot - // Paper start - ignore and warn about illegal addEntity calls instead of crashing server -- if (!entity.valid || entity.level() != this.level || this.entityMap.containsKey(entity.getId())) { -+ if (!entity.valid || entity.level() != this.level || entity.moonrise$getTrackedEntity() != null) { // Folia - region threading - LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName() -- + ": " + entity + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); -+ + ": " + entity + (entity.moonrise$getTrackedEntity() != null ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); // Folia - region threading - return; - } - // Paper end - ignore and warn about illegal addEntity calls instead of crashing server -@@ -893,22 +879,28 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - i = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i); // Spigot - if (i != 0) { - int updateInterval = type.updateInterval(); -- if (this.entityMap.containsKey(entity.getId())) { -+ if (entity.moonrise$getTrackedEntity() != null) { // Folia - region threading - throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("Entity is already tracked!")); - } else { - ChunkMap.TrackedEntity trackedEntity = new ChunkMap.TrackedEntity(entity, i, updateInterval, type.trackDeltas()); -- this.entityMap.put(entity.getId(), trackedEntity); -+ //this.entityMap.put(entity.getId(), trackedEntity); // Folia - region threading - // Paper start - optimise entity tracker - if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) { - throw new IllegalStateException("Entity is already tracked"); - } - ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(trackedEntity); - // Paper end - optimise entity tracker -- trackedEntity.updatePlayers(this.level.players()); -+ trackedEntity.updatePlayers(this.level.getLocalPlayers()); // Folia - region threading - if (entity instanceof ServerPlayer serverPlayer) { - this.updatePlayerStatus(serverPlayer, true); - -- for (ChunkMap.TrackedEntity trackedEntity1 : this.entityMap.values()) { -+ // Folia start - region threading -+ for (Entity possible : this.level.getCurrentWorldData().trackerEntities) { -+ ChunkMap.TrackedEntity trackedEntity1 = possible.moonrise$getTrackedEntity(); -+ if (trackedEntity == null) { -+ continue; -+ } -+ // Folia end - region threading - if (trackedEntity1.entity != serverPlayer) { - trackedEntity1.updatePlayer(serverPlayer); - } -@@ -924,12 +916,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - if (entity instanceof ServerPlayer serverPlayer) { - this.updatePlayerStatus(serverPlayer, false); - -- for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { -+ // Folia start - region threading -+ for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) { -+ ChunkMap.TrackedEntity trackedEntity = possible.moonrise$getTrackedEntity(); -+ if (trackedEntity == null) { -+ continue; -+ } -+ // Folia end - region threading - trackedEntity.removePlayer(serverPlayer); - } -+ // Folia end - region threading - } - -- ChunkMap.TrackedEntity trackedEntity1 = this.entityMap.remove(entity.getId()); -+ ChunkMap.TrackedEntity trackedEntity1 = entity.moonrise$getTrackedEntity(); // Folia - region threading - if (trackedEntity1 != null) { - trackedEntity1.broadcastRemoved(); - } -@@ -938,9 +937,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - // Paper start - optimise entity tracker - private void newTrackerTick() { -+ final io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading - final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();; - -- final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = entityLookup.trackerEntities; -+ final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = worldData.trackerEntities; // Folia - region threading - final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked(); - for (int i = 0, len = trackerEntities.size(); i < len; ++i) { - final Entity entity = trackerEntitiesRaw[i]; -@@ -966,44 +966,18 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Paper end - optimise entity tracker - // Paper - rewrite chunk system - -- List list = Lists.newArrayList(); -- List list1 = this.level.players(); -- -- for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { -- SectionPos sectionPos = trackedEntity.lastSectionPos; -- SectionPos sectionPos1 = SectionPos.of(trackedEntity.entity); -- boolean flag = !Objects.equals(sectionPos, sectionPos1); -- if (flag) { -- trackedEntity.updatePlayers(list1); -- Entity entity = trackedEntity.entity; -- if (entity instanceof ServerPlayer) { -- list.add((ServerPlayer)entity); -- } -- -- trackedEntity.lastSectionPos = sectionPos1; -- } -- -- if (flag || this.distanceManager.inEntityTickingRange(sectionPos1.chunk().toLong())) { -- trackedEntity.serverEntity.sendChanges(); -- } -- } -- -- if (!list.isEmpty()) { -- for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { -- trackedEntity.updatePlayers(list); -- } -- } -+ // Folia - region threading - } - - public void broadcast(Entity entity, Packet packet) { -- ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId()); -+ ChunkMap.TrackedEntity trackedEntity = entity.moonrise$getTrackedEntity(); // Folia - region threading - if (trackedEntity != null) { - trackedEntity.broadcast(packet); - } - } - - protected void broadcastAndSend(Entity entity, Packet packet) { -- ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId()); -+ ChunkMap.TrackedEntity trackedEntity = entity.moonrise$getTrackedEntity(); // Folia - region threading - if (trackedEntity != null) { - trackedEntity.broadcastAndSend(packet); - } -@@ -1231,8 +1205,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z); - // Paper end - Configurable entity tracking range by Y -+ // Folia start - region threading -+ if (flag && (this.entity instanceof ServerPlayer thisEntity) && thisEntity.broadcastedDeath) { -+ flag = false; -+ } -+ // Folia end - region threading - // CraftBukkit start - respect vanish API -- if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits -+ if (flag && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !player.getBukkitEntity().canSee(this.entity.getBukkitEntity()))) { // Paper - only consider hits // Folia - region threading - flag = false; - } - // CraftBukkit end -diff --git a/net/minecraft/server/level/DistanceManager.java b/net/minecraft/server/level/DistanceManager.java -index 5eab6179ce3913cb4e4d424f910ba423faf21c85..338f9d047101619605cedab172358b4fd737af97 100644 ---- a/net/minecraft/server/level/DistanceManager.java -+++ b/net/minecraft/server/level/DistanceManager.java -@@ -57,16 +57,16 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches - } - // Paper end - rewrite chunk system - // Paper start - chunk tick iteration optimisation -- private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>(); -+ // Folia - move to regionized world data - - @Override - public final void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos) { -- this.spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); -+ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading - } - - @Override - public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) { -- this.spawnChunkTracker.remove(player); -+ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading - } - - @Override -@@ -74,9 +74,9 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches - final SectionPos oldPos, final SectionPos newPos, - final boolean oldIgnore, final boolean newIgnore) { - if (newIgnore) { -- this.spawnChunkTracker.remove(player); -+ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading - } else { -- this.spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); -+ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading - } - } - // Paper end - chunk tick iteration optimisation -@@ -208,15 +208,15 @@ public abstract class DistanceManager implements ca.spottedleaf.moonrise.patches - } - - public int getNaturalSpawnChunkCount() { -- return this.spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation -+ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation // Folia - region threading - } - - public boolean hasPlayersNearby(long chunkPos) { -- return this.spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation -+ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation // Folia - region threading - } - - public LongIterator getSpawnCandidateChunks() { -- return this.spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation -+ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation // Folia - region threading - } - - public String getDebugStatus() { -diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java -index 6540b2d6a1062d883811ce240c49d30d1925b291..c340d537749c49d83f50f6cec84ac75e1ace2bbd 100644 ---- a/net/minecraft/server/level/ServerChunkCache.java -+++ b/net/minecraft/server/level/ServerChunkCache.java -@@ -61,18 +61,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; - public final ChunkMap chunkMap; - private final DimensionDataStorage dataStorage; -- private long lastInhabitedUpdate; -+ //private long lastInhabitedUpdate; // Folia - region threading - public boolean spawnEnemies = true; - public boolean spawnFriendlies = true; - private static final int CACHE_SIZE = 4; - private final long[] lastChunkPos = new long[4]; - private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4]; - private final ChunkAccess[] lastChunk = new ChunkAccess[4]; -- private final List tickingChunks = new ArrayList<>(); -- private final Set chunkHoldersToBroadcast = new ReferenceOpenHashSet<>(); -- @Nullable -- @VisibleForDebug -- private NaturalSpawner.SpawnState lastSpawnState; -+ // Folia - moved to regionised world data - // Paper start - private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); - public int getFullChunksCount() { -@@ -98,6 +94,11 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - } - - private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) { -+ // Folia start - region threading -+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Cannot asynchronously load chunks"); -+ } -+ // Folia end - region threading - final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); - final CompletableFuture completable = new CompletableFuture<>(); - chunkTaskScheduler.scheduleChunkLoad( -@@ -355,6 +356,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - } - - public CompletableFuture> getChunkFuture(int x, int z, ChunkStatus chunkStatus, boolean requireChunk) { -+ if (true) throw new UnsupportedOperationException(); // Folia - region threading - boolean flag = Thread.currentThread() == this.mainThread; - CompletableFuture> chunkFutureMainThread; - if (flag) { -@@ -502,14 +504,15 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - } - - private void tickChunks() { -- long gameTime = this.level.getGameTime(); -- long l = gameTime - this.lastInhabitedUpdate; -- this.lastInhabitedUpdate = gameTime; -+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading -+ //long gameTime = this.level.getGameTime(); // Folia - region threading -+ long l = 1L; // Folia - region threading -+ //this.lastInhabitedUpdate = gameTime; // Folia - region threading - if (!this.level.isDebug()) { - ProfilerFiller profilerFiller = Profiler.get(); - profilerFiller.push("pollingChunks"); - if (this.level.tickRateManager().runsNormally()) { -- List list = this.tickingChunks; -+ List list = regionizedWorldData.temporaryChunkTickList; // Folia - region threading - - try { - profilerFiller.push("filteringTickingChunks"); -@@ -532,23 +535,24 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - } - - private void broadcastChangedChunks(ProfilerFiller profiler) { -+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading - profiler.push("broadcast"); - -- for (ChunkHolder chunkHolder : this.chunkHoldersToBroadcast) { -+ for (ChunkHolder chunkHolder : regionizedWorldData.chunkHoldersToBroadcast) { // Folia - region threading - note: do not need to thread check, as getChunkToSend is only non-null when the chunkholder is loaded - LevelChunk tickingChunk = chunkHolder.getChunkToSend(); // Paper - rewrite chunk system - if (tickingChunk != null) { - chunkHolder.broadcastChanges(tickingChunk); - } - } - -- this.chunkHoldersToBroadcast.clear(); -+ regionizedWorldData.chunkHoldersToBroadcast.clear(); // Folia - region threading - profiler.pop(); - } - - private void collectTickingChunks(List output) { - // Paper start - chunk tick iteration optimisation - final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = -- ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); -+ this.level.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading - - final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked(); - final int size = tickingChunks.size(); -@@ -569,13 +573,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - } - - private void tickChunks(ProfilerFiller profiler, long timeInhabited, List chunks) { -+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading - profiler.popPush("naturalSpawnCount"); - int naturalSpawnChunkCount = this.distanceManager.getNaturalSpawnChunkCount(); - // Paper start - Optional per player mob spawns - NaturalSpawner.SpawnState spawnState; - if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled - // re-set mob counts -- for (ServerPlayer player : this.level.players) { -+ for (ServerPlayer player : this.level.getLocalPlayers()) { // Folia - region threading - // Paper start - per player mob spawning backoff - for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { - player.mobCounts[ii] = 0; -@@ -588,26 +593,26 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - } - // Paper end - per player mob spawning backoff - } -- spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); -+ spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all - } else { -- spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); -+ spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); // Folia - region threading - note: function only cares about loaded entities, doesn't need all - } - // Paper end - Optional per player mob spawns -- this.lastSpawnState = spawnState; -+ regionizedWorldData.lastSpawnState = spawnState; // Folia - region threading - profiler.popPush("spawnAndTick"); -- boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit -+ boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threading - int _int = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); - List filteredSpawningCategories; - if (_boolean && (this.spawnEnemies || this.spawnFriendlies)) { - // Paper start - PlayerNaturallySpawnCreaturesEvent -- for (ServerPlayer entityPlayer : this.level.players()) { -+ for (ServerPlayer entityPlayer : this.level.getLocalPlayers()) { // Folia - region threading - int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, entityPlayer.getBukkitEntity().getViewDistance()); - chunkRange = Math.min(chunkRange, 8); - entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange); - entityPlayer.playerNaturallySpawnedEvent.callEvent(); - } - // Paper end - PlayerNaturallySpawnCreaturesEvent -- boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit -+ boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getRedstoneGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit // Folia - region threading - filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag, this.level); // CraftBukkit - } else { - filteredSpawningCategories = List.of(); -@@ -673,18 +678,23 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - int sectionPosZ = SectionPos.blockToSectionCoord(pos.getZ()); - ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(ChunkPos.asLong(sectionPosX, sectionPosZ)); - if (visibleChunkIfPresent != null && visibleChunkIfPresent.blockChanged(pos)) { -- this.chunkHoldersToBroadcast.add(visibleChunkIfPresent); -+ this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(visibleChunkIfPresent); // Folia - region threading - } - } - - @Override - public void onLightUpdate(LightLayer type, SectionPos pos) { -- this.mainThreadProcessor.execute(() -> { -+ Runnable run = () -> { // Folia - region threading - ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(pos.chunk().toLong()); - if (visibleChunkIfPresent != null && visibleChunkIfPresent.sectionLightChanged(type, pos.y())) { -- this.chunkHoldersToBroadcast.add(visibleChunkIfPresent); -+ this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(visibleChunkIfPresent); // Folia - region threading - } -- }); -+ }; // Folia - region threading -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( -+ this.level, pos.getX(), pos.getZ(), run -+ ); -+ // Folia end - region threading - } - - public void addRegionTicket(TicketType type, ChunkPos pos, int distance, T value) { -@@ -766,7 +776,8 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - @Nullable - @VisibleForDebug - public NaturalSpawner.SpawnState getLastSpawnState() { -- return this.lastSpawnState; -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading -+ return worldData == null ? null : worldData.lastSpawnState; // Folia - region threading - } - - public void removeTicketsOnClosing() { -@@ -775,7 +786,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - - public void onChunkReadyToSend(ChunkHolder chunkHolder) { - if (chunkHolder.hasChangesToBroadcast()) { -- this.chunkHoldersToBroadcast.add(chunkHolder); -+ throw new UnsupportedOperationException(); // Folia - region threading - } - } - -@@ -812,20 +823,76 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - return ServerChunkCache.this.mainThread; - } - -+ // Folia start - region threading -+ @Override -+ public CompletableFuture submit(Supplier task) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ return super.submit(task); -+ } -+ -+ @Override -+ public CompletableFuture submit(Runnable task) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ return super.submit(task); -+ } -+ -+ @Override -+ public void schedule(Runnable runnable) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ super.schedule(runnable); -+ } -+ -+ @Override -+ public void executeBlocking(Runnable runnable) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ super.executeBlocking(runnable); -+ } -+ -+ @Override -+ public void execute(Runnable runnable) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ super.execute(runnable); -+ } -+ -+ @Override -+ public void executeIfPossible(Runnable runnable) { -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ super.executeIfPossible(runnable); -+ } -+ // Folia end - region threading -+ - @Override - protected void doRunTask(Runnable task) { -+ if (true) throw new UnsupportedOperationException(); // Folia - region threading - Profiler.get().incrementCounter("runTask"); - super.doRunTask(task); - } - - @Override - public boolean pollTask() { -+ // Folia start - region threading -+ if (ServerChunkCache.this.level != io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().world) { -+ throw new IllegalStateException("Polling tasks from non-owned region"); -+ } -+ // Folia end - region threading - // Paper start - rewrite chunk system - final ServerChunkCache serverChunkCache = ServerChunkCache.this; - if (serverChunkCache.runDistanceManagerUpdates()) { - return true; - } else { -- return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask(); -+ return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getData().getTaskQueueData().executeChunkTask(); // Folia - region threading - } - // Paper end - rewrite chunk system - } -diff --git a/net/minecraft/server/level/ServerEntityGetter.java b/net/minecraft/server/level/ServerEntityGetter.java -index 794770985c261fd56806188237921b5ec5e548e6..b715d1fbde9db81a2515249bb9a0fc7a5fee40f0 100644 ---- a/net/minecraft/server/level/ServerEntityGetter.java -+++ b/net/minecraft/server/level/ServerEntityGetter.java -@@ -14,17 +14,17 @@ public interface ServerEntityGetter extends EntityGetter { - - @Nullable - default Player getNearestPlayer(TargetingConditions targetingConditions, LivingEntity source) { -- return this.getNearestEntity(this.players(), targetingConditions, source, source.getX(), source.getY(), source.getZ()); -+ return this.getNearestEntity(this.getLocalPlayers(), targetingConditions, source, source.getX(), source.getY(), source.getZ()); // Folia - region threading - } - - @Nullable - default Player getNearestPlayer(TargetingConditions targetingConditions, LivingEntity source, double x, double y, double z) { -- return this.getNearestEntity(this.players(), targetingConditions, source, x, y, z); -+ return this.getNearestEntity(this.getLocalPlayers(), targetingConditions, source, x, y, z); // Folia - region threading - } - - @Nullable - default Player getNearestPlayer(TargetingConditions targetingConditions, double x, double y, double z) { -- return this.getNearestEntity(this.players(), targetingConditions, null, x, y, z); -+ return this.getNearestEntity(this.getLocalPlayers(), targetingConditions, null, x, y, z); // Folia - region threading - } - - @Nullable -@@ -57,7 +57,7 @@ public interface ServerEntityGetter extends EntityGetter { - default List getNearbyPlayers(TargetingConditions targetingConditions, LivingEntity source, AABB area) { - List list = new ArrayList<>(); - -- for (Player player : this.players()) { -+ for (Player player : this.getLocalPlayers()) { // Folia - region threading - if (area.contains(player.getX(), player.getY(), player.getZ()) && targetingConditions.test(this.getLevel(), source, player)) { - list.add(player); - } -diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index ebeeb63c3dca505a3ce8b88feaa5d2ca20ec24a2..49a385261deef774575dfd7a5b259d8ed31ed91a 100644 ---- a/net/minecraft/server/level/ServerLevel.java -+++ b/net/minecraft/server/level/ServerLevel.java -@@ -179,42 +179,40 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - private static final Logger LOGGER = LogUtils.getLogger(); - private static final int EMPTY_TIME_NO_TICK = 300; - private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536; -- final List players = Lists.newArrayList(); -+ final List players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - region threading - public final ServerChunkCache chunkSource; - private final MinecraftServer server; - public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type - private int lastSpawnChunkRadius; -- final EntityTickList entityTickList = new EntityTickList(); -+ //final EntityTickList entityTickList = new EntityTickList(); // Folia - region threading - // Paper - rewrite chunk system - private final GameEventDispatcher gameEventDispatcher; - public boolean noSave; - private final SleepStatus sleepStatus; - private int emptyTime; - private final PortalForcer portalForcer; -- private final LevelTicks blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); -- private final LevelTicks fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); -- private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); -- final Set navigatingMobs = new ObjectOpenHashSet<>(); -+ //private final LevelTicks blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading -+ //private final LevelTicks fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading -+ //private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); // Folia - region threading -+ //final Set navigatingMobs = new ObjectOpenHashSet<>(); // Folia - region threading - volatile boolean isUpdatingNavigations; - protected final Raids raids; -- private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); -- private final List blockEventsToReschedule = new ArrayList<>(64); -- private boolean handlingTick; -+ //private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); // Folia - region threading -+ //private final List blockEventsToReschedule = new ArrayList<>(64); // Folia - region threading -+ //private boolean handlingTick; // Folia - region threading - private final List customSpawners; - @Nullable - private EndDragonFight dragonFight; -- final Int2ObjectMap dragonParts = new Int2ObjectOpenHashMap<>(); -+ final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable dragonParts = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); // Folia - region threading - private final StructureManager structureManager; - private final StructureCheck structureCheck; -- private final boolean tickTime; -+ public final boolean tickTime; // Folia - region threading - private final RandomSequences randomSequences; - - // CraftBukkit start - public final LevelStorageSource.LevelStorageAccess levelStorageAccess; - public final UUID uuid; -- public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent -- public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent -- private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) -+ // Folia - region threading - move to regionised world data - - public LevelChunk getChunkIfLoaded(int x, int z) { - return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately -@@ -242,6 +240,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - int minChunkZ = minBlockZ >> 4; - int maxChunkZ = maxBlockZ >> 4; - -+ // Folia start - region threading -+ // don't let players move into regions not owned -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, minChunkX, minChunkZ, maxChunkX, maxChunkZ)) { -+ return false; -+ } -+ // Folia end - region threading -+ - ServerChunkCache chunkProvider = this.getChunkSource(); - - for (int cx = minChunkX; cx <= maxChunkX; ++cx) { -@@ -297,11 +302,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler; - private long lastMidTickFailure; - private long tickedBlocksOrFluids; -- private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this); -- private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; -- private final ca.spottedleaf.moonrise.common.list.ReferenceList loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); -- private final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); -- private final ca.spottedleaf.moonrise.common.list.ReferenceList entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); -+ // Folia - region threading - move to regionized data - - @Override - public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { -@@ -359,7 +360,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - @Override - public final int moonrise$getRegionChunkShift() { -- return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); -+ return this.regioniser.sectionChunkShift; // Folia - region threading - } - - @Override -@@ -460,22 +461,22 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - @Override - public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() { -- return this.nearbyPlayers; -+ return this.getCurrentWorldData().getNearbyPlayers(); // Folia - region threading - } - - @Override - public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getLoadedChunks() { -- return this.loadedChunks; -+ return this.getCurrentWorldData().getChunks(); // Folia - region threading - } - - @Override - public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getTickingChunks() { -- return this.tickingChunks; -+ return this.getCurrentWorldData().getTickingChunks(); // Folia - region threading - } - - @Override - public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getEntityTickingChunks() { -- return this.entityTickingChunks; -+ return this.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading - } - - @Override -@@ -495,80 +496,85 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // Paper end - rewrite chunk system - // Paper start - chunk tick iteration - private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; -- private final ca.spottedleaf.moonrise.common.list.ReferenceList playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS); -- private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); -+ // Folia - region threading - - @Override - public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getPlayerTickingChunks() { -- return this.playerTickingChunks; -+ throw new UnsupportedOperationException(); // Folia - region threading - } - - @Override - public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) { -- final ChunkPos pos = chunk.getPos(); -- if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) { -- return; -- } -- -- this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); -+ // Folia - region threading - } - - @Override - public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) { -- this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); -+ // Folia - region threading - } - - @Override - public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) { -- ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async"); -- -- final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); -- -- if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) { -- // already added -- return; -- } -- -- final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() -- .chunkHolderManager.getChunkHolder(chunkKey); -- -- if (chunkHolder == null || !chunkHolder.isTickingReady()) { -- return; -- } -- -- this.playerTickingChunks.add( -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() -- ); -+ // Folia - region threading - } - - @Override - public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) { -- ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async"); -+ // Folia - region threading -+ } -+ // Paper end - chunk tick iteration -+ // Folia start - region threading -+ public final io.papermc.paper.threadedregions.TickRegions tickRegions = new io.papermc.paper.threadedregions.TickRegions(); -+ public final io.papermc.paper.threadedregions.ThreadedRegionizer regioniser; -+ { -+ this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegionizer<>( -+ (int)Math.max(1L, (8L * 16L * 16L) / (1L << (2 * (io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())))), -+ (1.0 / 6.0), -+ Math.max(1, 8 / (1 << io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())), -+ 1, -+ io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(), -+ this, -+ this.tickRegions -+ ); -+ } -+ public final io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData(this); -+ public static final int WORLD_INIT_NOT_CHECKED = 0; -+ public static final int WORLD_INIT_CHECKING = 1; -+ public static final int WORLD_INIT_CHECKED = 2; -+ public final java.util.concurrent.atomic.AtomicInteger checkInitialised = new java.util.concurrent.atomic.AtomicInteger(WORLD_INIT_NOT_CHECKED); -+ public ChunkPos randomSpawnSelection; - -- final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); -- final int val = this.playerTickingRequests.addTo(chunkKey, -1); -+ public static final record PendingTeleport(Entity.EntityTreeNode rootVehicle, Vec3 to) {} -+ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet pendingTeleports = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); - -- if (val <= 0) { -- throw new IllegalStateException("Negative counter"); -+ public void pushPendingTeleport(final PendingTeleport teleport) { -+ synchronized (this.pendingTeleports) { -+ this.pendingTeleports.add(teleport); - } -+ } - -- if (val != 1) { -- // still has at least one request -- return; -+ public boolean removePendingTeleport(final PendingTeleport teleport) { -+ synchronized (this.pendingTeleports) { -+ return this.pendingTeleports.remove(teleport); - } -+ } - -- final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() -- .chunkHolderManager.getChunkHolder(chunkKey); -+ public List removeAllRegionTeleports() { -+ final List ret = new ArrayList<>(); - -- if (chunkHolder == null || !chunkHolder.isTickingReady()) { -- return; -+ synchronized (this.pendingTeleports) { -+ for (final java.util.Iterator iterator = this.pendingTeleports.iterator(); iterator.hasNext(); ) { -+ final PendingTeleport pendingTeleport = iterator.next(); -+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, pendingTeleport.to())) { -+ ret.add(pendingTeleport); -+ iterator.remove(); -+ } -+ } - } - -- this.playerTickingChunks.remove( -- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() -- ); -+ return ret; - } -- // Paper end - chunk tick iteration -+ // Folia end - region threading - - public ServerLevel( - MinecraftServer server, -@@ -633,7 +639,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - ); - this.chunkSource.getGeneratorState().ensureStructuresGenerated(); - this.portalForcer = new PortalForcer(this); -- this.updateSkyBrightness(); -+ //this.updateSkyBrightness(); // Folia - region threading - delay until first tick - this.prepareWeather(); - this.getWorldBorder().setAbsoluteMaxSize(server.getAbsoluteMaxWorldSize()); - this.raids = this.getDataStorage().computeIfAbsent(Raids.factory(this), Raids.getFileId(this.dimensionTypeRegistration())); -@@ -681,7 +687,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); - // Paper end - rewrite chunk system - this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit -+ this.updateTickData(); // Folia - region threading - make sure it is initialised before ticked -+ } -+ -+ // Folia start - region threading -+ public void updateTickData() { -+ this.tickData = new io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData(this, this.serverLevelData.getGameTime(), this.serverLevelData.getDayTime()); - } -+ // Folia end - region threading - - // Paper start - @Override -@@ -709,61 +722,39 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - return this.getChunkSource().getGenerator().getBiomeSource().getNoiseBiome(x, y, z, this.getChunkSource().randomState().sampler()); - } - -+ @Override // Folia - region threading - public StructureManager structureManager() { - return this.structureManager; - } - -- public void tick(BooleanSupplier hasTimeLeft) { -+ public void tick(BooleanSupplier hasTimeLeft, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking -+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking - ProfilerFiller profilerFiller = Profiler.get(); -- this.handlingTick = true; -+ regionizedWorldData.setHandlingTick(true); // Folia - regionised ticking - TickRateManager tickRateManager = this.tickRateManager(); - boolean runsNormally = tickRateManager.runsNormally(); - if (runsNormally) { - profilerFiller.push("world border"); -- this.getWorldBorder().tick(); -+ //this.getWorldBorder().tick(); // Folia - regionised ticking - profilerFiller.popPush("weather"); -- this.advanceWeatherCycle(); -+ //this.advanceWeatherCycle(); // Folia - regionised ticking - profilerFiller.pop(); - } - -- int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); -- if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) { -- // Paper start - create time skip event - move up calculations -- final long newDayTime = this.levelData.getDayTime() + 24000L; -- org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent( -- this.getWorld(), -- org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP, -- (newDayTime - newDayTime % 24000L) - this.getDayTime() -- ); -- // Paper end - create time skip event - move up calculations -- if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { -- // Paper start - call time skip event if gamerule is enabled -- // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime -- // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param -- if (event.callEvent()) { -- this.setDayTime(this.getDayTime() + event.getSkipAmount()); -- } -- // Paper end - call time skip event if gamerule is enabled -- } -- -- if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled -- if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { -- this.resetWeatherCycle(); -- } -- } -+ this.tickSleep(); // Folia - region threading - move into tickSleep - -- this.updateSkyBrightness(); -+ //this.updateSkyBrightness(); // Folia - region threading - if (runsNormally) { - this.tickTime(); - } - - profilerFiller.push("tickPending"); - if (!this.isDebug() && runsNormally) { -- long l = this.getGameTime(); -+ long l = regionizedWorldData.getRedstoneGameTime(); // Folia - region threading - profilerFiller.push("blockTicks"); -- this.blockTicks.tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks -+ regionizedWorldData.getBlockLevelTicks().tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks // Folia - region ticking - profilerFiller.popPush("fluidTicks"); -- this.fluidTicks.tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks -+ regionizedWorldData.getFluidLevelTicks().tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks // Folia - region ticking - profilerFiller.pop(); - } - -@@ -779,9 +770,9 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - this.runBlockEvents(); - } - -- this.handlingTick = false; -+ regionizedWorldData.setHandlingTick(false); // Folia - regionised ticking - profilerFiller.pop(); -- boolean flag = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this -+ boolean flag = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this // Folia - unrestore this, we always need to tick empty worlds - if (flag) { - this.resetEmptyTime(); - } -@@ -789,19 +780,29 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - if (flag || this.emptyTime++ < 300) { - profilerFiller.push("entities"); - if (this.dragonFight != null && runsNormally) { -+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, this.dragonFight.origin)) { // Folia - region threading - profilerFiller.push("dragonFight"); - this.dragonFight.tick(); - profilerFiller.pop(); -+ } else { // Folia start - region threading -+ // try to load dragon fight -+ ChunkPos fightCenter = new ChunkPos(this.dragonFight.origin); -+ this.chunkSource.addTicketAtLevel( -+ TicketType.UNKNOWN, fightCenter, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, -+ fightCenter -+ ); -+ } // Folia end - region threading - } - - io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR -- this.entityTickList -- .forEach( -+ regionizedWorldData // Folia - regionised ticking -+ .forEachTickingEntity( // Folia - regionised ticking - entity -> { - if (!entity.isRemoved()) { - if (!tickRateManager.isEntityFrozen(entity)) { - profilerFiller.push("checkDespawn"); - entity.checkDespawn(); -+ if (entity.isRemoved()) return; // Folia - region threading - if we despawned, DON'T TICK IT! - profilerFiller.pop(); - if (true) { // Paper - rewrite chunk system - Entity vehicle = entity.getVehicle(); -@@ -830,6 +831,36 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - profilerFiller.pop(); - } - -+ // Folia start - region threading -+ public void tickSleep() { -+ int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); -+ if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) { -+ // Paper start - create time skip event - move up calculations -+ final long newDayTime = this.levelData.getDayTime() + 24000L; -+ org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent( -+ this.getWorld(), -+ org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP, -+ (newDayTime - newDayTime % 24000L) - this.getDayTime() -+ ); -+ // Paper end - create time skip event - move up calculations -+ if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { -+ // Paper start - call time skip event if gamerule is enabled -+ // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime -+ // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param -+ if (event.callEvent()) { -+ this.setDayTime(this.getDayTime() + event.getSkipAmount()); -+ } -+ // Paper end - call time skip event if gamerule is enabled -+ } -+ -+ if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled -+ if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { -+ this.resetWeatherCycle(); -+ } -+ } -+ } -+ // Folia end - region threading -+ - @Override - public boolean shouldTickBlocksAt(long chunkPos) { - // Paper start - rewrite chunk system -@@ -840,12 +871,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - protected void tickTime() { - if (this.tickTime) { -- long l = this.levelData.getGameTime() + 1L; -- this.serverLevelData.setGameTime(l); -+ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading -+ long l = regionizedWorldData.getRedstoneGameTime() + 1L; // Folia - region threading -+ regionizedWorldData.setRedstoneGameTime(l); // Folia - region threading - Profiler.get().push("scheduledFunctions"); -- this.serverLevelData.getScheduledEvents().tick(this.server, l); -+ //this.serverLevelData.getScheduledEvents().tick(this.server, l); // Folia - region threading - TODO any way to bring this in? - Profiler.get().pop(); -- if (this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { -+ if (false && this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { // Folia - region threading - this.setDayTime(this.levelData.getDayTime() + 1L); - } - } -@@ -863,16 +895,27 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - private void wakeUpAllPlayers() { - this.sleepStatus.removeAllSleepers(); -- this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach(player -> player.stopSleepInBed(false, false)); -+ // Folia start - region threading -+ this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach((ServerPlayer entityplayer) -> { -+ // Folia start - region threading -+ entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { -+ if (player.level() != ServerLevel.this || !player.isSleeping()) { -+ return; -+ } -+ player.stopSleepInBed(false, false); -+ }, null, 1L); -+ } -+ ); -+ // Folia end - region threading - } - - // Paper start - optimise random ticking -- private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); -+ private final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource.INSTANCE; // Folia - region threading - - private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) { - final LevelChunkSection[] sections = chunk.getSections(); - final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this); -- final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; -+ final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Folia - region threading - final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294(); - - final ChunkPos cpos = chunk.getPos(); -@@ -919,7 +962,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // Paper end - optimise random ticking - - public void tickChunk(LevelChunk chunk, int randomTickSpeed) { -- final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking -+ final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Paper - optimise random ticking // Folia - region threading - ChunkPos pos = chunk.getPos(); - boolean isRaining = this.isRaining(); - int minBlockX = pos.getMinBlockX(); -@@ -1044,7 +1087,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - - public boolean isHandlingTick() { -- return this.handlingTick; -+ return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking - } - - public boolean canSleepThroughNights() { -@@ -1070,6 +1113,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - - public void updateSleepingPlayerList() { -+ // Folia start - region threading -+ if (!io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ ServerLevel.this.updateSleepingPlayerList(); -+ }); -+ return; -+ } -+ // Folia end - region threading - if (!this.players.isEmpty() && this.sleepStatus.update(this.players)) { - this.announceSleepStatus(); - } -@@ -1080,7 +1131,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - return this.server.getScoreboard(); - } - -- private void advanceWeatherCycle() { -+ public void advanceWeatherCycle() { // Folia - region threading - public - boolean isRaining = this.isRaining(); - if (this.dimensionType().hasSkyLight()) { - if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE)) { -@@ -1166,7 +1217,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, this.thunderLevel)); - } - */ -- for (ServerPlayer player : this.players) { -+ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading -+ for (ServerPlayer player : players) { // Folia - region threading - if (player.level() == this) { - player.tickWeather(); - } -@@ -1174,13 +1226,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - if (isRaining != this.isRaining()) { - // Only send weather packets to those affected -- for (ServerPlayer player : this.players) { -+ for (ServerPlayer player : players) { // Folia - region threading - if (player.level() == this) { - player.setPlayerWeather((!isRaining ? org.bukkit.WeatherType.DOWNFALL : org.bukkit.WeatherType.CLEAR), false); - } - } - } -- for (ServerPlayer player : this.players) { -+ for (ServerPlayer player : players) { // Folia - region threading - if (player.level() == this) { - player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); - } -@@ -1241,13 +1293,10 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - // Paper start - log detailed entity tick information - // TODO replace with varhandle -- static final java.util.concurrent.atomic.AtomicReference currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>(); -+ // Folia - region threading - - public static List getCurrentlyTickingEntities() { -- Entity ticking = currentlyTickingEntity.get(); -- List ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking }); -- -- return ret; -+ throw new UnsupportedOperationException(); // Folia - region threading - } - // Paper end - log detailed entity tick information - -@@ -1255,9 +1304,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // Paper start - log detailed entity tick information - ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); - try { -- if (currentlyTickingEntity.get() == null) { -- currentlyTickingEntity.lazySet(entity); -- } -+ // Folia - region threading - // Paper end - log detailed entity tick information - entity.setOldPosAndRot(); - ProfilerFiller profilerFiller = Profiler.get(); -@@ -1267,7 +1314,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - final boolean isActive = io.papermc.paper.entity.activation.ActivationRange.checkIfActive(entity); // Paper - EAR 2 - if (isActive) { // Paper - EAR 2 - entity.tick(); -- entity.postTick(); // CraftBukkit -+ // Folia start - region threading -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { -+ // removed from region while ticking -+ return; -+ } -+ if (entity.handlePortal()) { -+ // portalled -+ return; -+ } -+ // Folia end - region threading - } else {entity.inactiveTick();} // Paper - EAR 2 - profilerFiller.pop(); - -@@ -1276,9 +1332,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - // Paper start - log detailed entity tick information - } finally { -- if (currentlyTickingEntity.get() == entity) { -- currentlyTickingEntity.lazySet(null); -- } -+ // Folia - region threading - } - // Paper end - log detailed entity tick information - } -@@ -1286,7 +1340,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - private void tickPassenger(Entity ridingEntity, Entity passengerEntity, final boolean isActive) { // Paper - EAR 2 - if (passengerEntity.isRemoved() || passengerEntity.getVehicle() != ridingEntity) { - passengerEntity.stopRiding(); -- } else if (passengerEntity instanceof Player || this.entityTickList.contains(passengerEntity)) { -+ } else if (passengerEntity instanceof Player || this.getCurrentWorldData().hasEntityTickingEntity(passengerEntity)) { // Folia - region threading - passengerEntity.setOldPosAndRot(); - passengerEntity.tickCount++; - ProfilerFiller profilerFiller = Profiler.get(); -@@ -1295,7 +1349,16 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // Paper start - EAR 2 - if (isActive) { - passengerEntity.rideTick(); -- passengerEntity.postTick(); // CraftBukkit -+ // Folia start - region threading -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(passengerEntity)) { -+ // removed from region while ticking -+ return; -+ } -+ if (passengerEntity.handlePortal()) { -+ // portalled -+ return; -+ } -+ // Folia end - region threading - } else { - passengerEntity.setDeltaMovement(Vec3.ZERO); - passengerEntity.inactiveTick(); -@@ -1369,19 +1432,20 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - // Paper end - add close param - -- // CraftBukkit start - moved from MinecraftServer.saveChunks -- ServerLevel worldserver1 = this; -- -- this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); -- this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); -- this.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); -- // CraftBukkit end -+ // Folia - move into saveLevelData - } - -- private void saveLevelData(boolean join) { -+ public void saveLevelData(boolean join) { // Folia - public - if (this.dragonFight != null) { - this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit - } -+ // Folia start - moved into saveLevelData -+ ServerLevel worldserver1 = this; -+ -+ this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); -+ this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); -+ this.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); -+ // Folia end - moved into saveLevelData - - DimensionDataStorage dataStorage = this.getChunkSource().getDataStorage(); - if (join) { -@@ -1437,6 +1501,19 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - return list; - } - -+ // Folia start - region threading -+ @Nullable -+ public ServerPlayer getRandomLocalPlayer() { -+ List list = this.getLocalPlayers(); -+ list = new java.util.ArrayList<>(list); -+ list.removeIf((ServerPlayer player) -> { -+ return !player.isAlive(); -+ }); -+ -+ return list.isEmpty() ? null : (ServerPlayer) list.get(this.random.nextInt(list.size())); -+ } -+ // Folia end - region threading -+ - @Nullable - public ServerPlayer getRandomPlayer() { - List players = this.getPlayers(LivingEntity::isAlive); -@@ -1518,8 +1595,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } else { - if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added - // Paper start - capture all item additions to the world -- if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { -- captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); -+ if (this.getCurrentWorldData().captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { // Folia - region threading -+ this.getCurrentWorldData().captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); // Folia - region threading - return true; - } - // Paper end - capture all item additions to the world -@@ -1694,13 +1771,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - @Override - public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) { -- if (this.isUpdatingNavigations) { -+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading -+ if (false && this.isUpdatingNavigations) { // Folia - region threading - String string = "recursive call to sendBlockUpdated"; - Util.logAndPauseIfInIde("recursive call to sendBlockUpdated", new IllegalStateException("recursive call to sendBlockUpdated")); - } - - this.getChunkSource().blockChanged(pos); -- this.pathTypesByPosCache.invalidate(pos); -+ regionizedWorldData.pathTypesByPosCache.invalidate(pos); // Folia - region threading - if (this.paperConfig().misc.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates - VoxelShape collisionShape = oldState.getCollisionShape(this, pos); - VoxelShape collisionShape1 = newState.getCollisionShape(this, pos); -@@ -1708,7 +1786,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - List list = new ObjectArrayList<>(); - - try { // Paper - catch CME see below why -- for (Mob mob : this.navigatingMobs) { -+ for (java.util.Iterator iterator = regionizedWorldData.getNavigatingMobs(); iterator.hasNext();) { // Folia - region threading -+ Mob mob = iterator.next(); // Folia - region threading - PathNavigation navigation = mob.getNavigation(); - if (navigation.shouldRecomputePath(pos)) { - list.add(navigation); -@@ -1725,13 +1804,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // Paper end - catch CME see below why - - try { -- this.isUpdatingNavigations = true; -+ //this.isUpdatingNavigations = true; // Folia - region threading - - for (PathNavigation pathNavigation : list) { - pathNavigation.recomputePath(); - } - } finally { -- this.isUpdatingNavigations = false; -+ //this.isUpdatingNavigations = false; // Folia - region threading - } - } - } // Paper - option to disable pathfinding updates -@@ -1739,29 +1818,29 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - @Override - public void updateNeighborsAt(BlockPos pos, Block block) { -- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement -+ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading - this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, null, null)); - } - - @Override - public void updateNeighborsAt(BlockPos pos, Block block, @Nullable Orientation orientation) { -- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement -- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation); -+ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading -+ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation); // Folia - region threading - } - - @Override - public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block block, Direction facing, @Nullable Orientation orientation) { -- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, facing, orientation); -+ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, facing, orientation); // Folia - region threading - } - - @Override - public void neighborChanged(BlockPos pos, Block block, @Nullable Orientation orientation) { -- this.neighborUpdater.neighborChanged(pos, block, orientation); -+ this.getCurrentWorldData().neighborUpdater.neighborChanged(pos, block, orientation); // Folia - region threading - } - - @Override - public void neighborChanged(BlockState state, BlockPos pos, Block block, @Nullable Orientation orientation, boolean movedByPiston) { -- this.neighborUpdater.neighborChanged(state, pos, block, orientation, movedByPiston); -+ this.getCurrentWorldData().neighborUpdater.neighborChanged(state, pos, block, orientation, movedByPiston); // Folia - region threading - } - - @Override -@@ -1851,7 +1930,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // CraftBukkit end - ParticleOptions particleOptions = serverExplosion.isSmall() ? smallExplosionParticles : largeExplosionParticles; - -- for (ServerPlayer serverPlayer : this.players) { -+ for (ServerPlayer serverPlayer : this.getLocalPlayers()) { // Folia - region thraeding - if (serverPlayer.distanceToSqr(vec3) < 4096.0) { - Optional optional = Optional.ofNullable(serverExplosion.getHitPlayers().get(serverPlayer)); - serverPlayer.connection.send(new ClientboundExplodePacket(vec3, optional, particleOptions, explosionSound)); -@@ -1867,14 +1946,17 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - @Override - public void blockEvent(BlockPos pos, Block block, int eventID, int eventParam) { -- this.blockEvents.add(new BlockEventData(pos, block, eventID, eventParam)); -+ this.getCurrentWorldData().pushBlockEvent(new BlockEventData(pos, block, eventID, eventParam)); // Folia - regionised ticking - } - - private void runBlockEvents() { -- this.blockEventsToReschedule.clear(); -+ List blockEventsToReschedule = new ArrayList<>(64); // Folia - regionised ticking - -- while (!this.blockEvents.isEmpty()) { -- BlockEventData blockEventData = this.blockEvents.removeFirst(); -+ // Folia start - regionised ticking -+ io.papermc.paper.threadedregions.RegionizedWorldData worldRegionData = this.getCurrentWorldData(); -+ BlockEventData blockEventData; -+ while ((blockEventData = worldRegionData.removeFirstBlockEvent()) != null) { -+ // Folia end - regionised ticking - if (this.shouldTickBlocksAt(blockEventData.pos())) { - if (this.doBlockEvent(blockEventData)) { - this.server -@@ -1890,11 +1972,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - ); - } - } else { -- this.blockEventsToReschedule.add(blockEventData); -+ blockEventsToReschedule.add(blockEventData); // Folia - regionised ticking - } - } - -- this.blockEvents.addAll(this.blockEventsToReschedule); -+ worldRegionData.pushBlockEvents(blockEventsToReschedule); // Folia - regionised ticking - } - - private boolean doBlockEvent(BlockEventData event) { -@@ -1904,12 +1986,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - @Override - public LevelTicks getBlockTicks() { -- return this.blockTicks; -+ return this.getCurrentWorldData().getBlockLevelTicks(); // Folia - region ticking - } - - @Override - public LevelTicks getFluidTicks() { -- return this.fluidTicks; -+ return this.getCurrentWorldData().getFluidLevelTicks(); // Folia - region ticking - } - - @Nonnull -@@ -1962,7 +2044,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - double zOffset, - double speed - ) { -- return sendParticlesSource(this.players, sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed); -+ return sendParticlesSource(this.getLocalPlayers(), sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed); // Folia - region threading - } - public int sendParticlesSource( - List receivers, -@@ -2045,12 +2127,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - @Nullable - public Entity getEntityOrPart(int id) { - Entity entity = this.getEntities().get(id); -- return entity != null ? entity : this.dragonParts.get(id); -+ return entity != null ? entity : this.dragonParts.get((long)id); // Folia - diff on change - } - - @Override - public Collection dragonParts() { -- return this.dragonParts.values(); -+ return this.dragonParts.values(); // Folia - diff on change - } - - @Nullable -@@ -2105,6 +2187,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // Paper start - Call missing map initialize event and set id - final DimensionDataStorage storage = this.getServer().overworld().getDataStorage(); - -+ synchronized (storage.cache) { // Folia - region threading - final Optional cacheEntry = storage.cache.get(mapId.key()); - if (cacheEntry == null) { // Cache did not contain, try to load and may init - final MapItemSavedData mapData = storage.get(MapItemSavedData.factory(), mapId.key()); // get populates the cache -@@ -2124,6 +2207,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - - return null; -+ } // Folia - region threading - // Paper end - Call missing map initialize event and set id - } - -@@ -2178,6 +2262,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - - public boolean setChunkForced(int chunkX, int chunkZ, boolean add) { -+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify force loaded chunks off of the global region"); // Folia - region threading - ForcedChunksSavedData forcedChunksSavedData = this.getDataStorage().computeIfAbsent(ForcedChunksSavedData.factory(), "chunks"); - ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); - long packedChunkPos = chunkPos.toLong(); -@@ -2185,7 +2270,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - if (add) { - flag = forcedChunksSavedData.getChunks().add(packedChunkPos); - if (flag) { -- this.getChunk(chunkX, chunkZ); -+ //this.getChunk(chunkX, chunkZ); // Folia - region threading - we must let the chunk load asynchronously - } - } else { - flag = forcedChunksSavedData.getChunks().remove(packedChunkPos); -@@ -2210,11 +2295,24 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - Optional> optional1 = PoiTypes.forState(newState); - if (!Objects.equals(optional, optional1)) { - BlockPos blockPos = pos.immutable(); -- optional.ifPresent(poiType -> this.getServer().execute(() -> { -+ // Folia start - region threading -+ optional.ifPresent(poiType -> { -+ Runnable run = () -> { -+ // Folia end - region threading - this.getPoiManager().remove(blockPos); - DebugPackets.sendPoiRemovedPacket(this, blockPos); -- })); -- optional1.ifPresent(poiType -> this.getServer().execute(() -> { -+ // Folia start - region threading -+ }; -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( -+ this, blockPos.getX() >> 4, blockPos.getZ() >> 4, run -+ ); -+ }); -+ // Folia end - region threading -+ // Folia start - region threading -+ optional1.ifPresent(poiType -> { -+ Runnable run = () -> { -+ // Folia end - region threading - // Paper start - Remove stale POIs - if (optional.isEmpty() && this.getPoiManager().exists(blockPos, ignored -> true)) { - this.getPoiManager().remove(blockPos); -@@ -2222,7 +2320,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // Paper end - Remove stale POIs - this.getPoiManager().add(blockPos, (Holder)poiType); - DebugPackets.sendPoiAddedPacket(this, blockPos); -- })); -+ // Folia start - region threading -+ }; -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( -+ this, blockPos.getX() >> 4, blockPos.getZ() >> 4, run -+ ); -+ // Folia end - region threading -+ }); -+ // Folia end - region threading - } - } - -@@ -2276,7 +2382,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - - bufferedWriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system -- bufferedWriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); -+ //bufferedWriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); // Folia - region threading - bufferedWriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count())); - bufferedWriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count())); - bufferedWriter.write("distance_manager: " + chunkMap.getDistanceManager().getDebugStatus() + "\n"); -@@ -2346,7 +2452,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - private void dumpBlockEntityTickers(Writer output) throws IOException { - CsvOutput csvOutput = CsvOutput.builder().addColumn("x").addColumn("y").addColumn("z").addColumn("type").build(output); - -- for (TickingBlockEntity tickingBlockEntity : this.blockEntityTickers) { -+ for (TickingBlockEntity tickingBlockEntity : (Iterable)null) { // Folia - region threading - BlockPos pos = tickingBlockEntity.getPos(); - csvOutput.writeRow(pos.getX(), pos.getY(), pos.getZ(), tickingBlockEntity.getType()); - } -@@ -2354,14 +2460,14 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - @VisibleForTesting - public void clearBlockEvents(BoundingBox boundingBox) { -- this.blockEvents.removeIf(blockEventData -> boundingBox.isInside(blockEventData.pos())); -+ this.getCurrentWorldData().removeIfBlockEvents(blockEventData -> boundingBox.isInside(blockEventData.pos())); // Folia - regionised ticking - } - - @Override - public void blockUpdated(BlockPos pos, Block block) { - if (!this.isDebug()) { - // CraftBukkit start -- if (this.populating) { -+ if (this.getCurrentWorldData().populating) { // Folia - region threading - return; - } - // CraftBukkit end -@@ -2410,8 +2516,8 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - this.players.size(), - this.moonrise$getEntityLookup().getDebugInfo(), // Paper - rewrite chunk system - getTypeCount(this.moonrise$getEntityLookup().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()), // Paper - rewrite chunk system -- this.blockEntityTickers.size(), -- getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), -+ 0, // Folia - region threading -+ "null", // Folia - region threading - this.getBlockTicks().count(), - this.getFluidTicks().count(), - this.gatherChunkSourceStats() -@@ -2463,15 +2569,15 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - - public void startTickingChunk(LevelChunk chunk) { -- chunk.unpackTicks(this.getLevelData().getGameTime()); -+ chunk.unpackTicks(this.getRedstoneGameTime()); // Folia - region threading - } - - public void onStructureStartsAvailable(ChunkAccess chunk) { -- this.server.execute(() -> this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts())); -+ this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); // Folia - region threading - } - - public PathTypeCache getPathTypeCache() { -- return this.pathTypesByPosCache; -+ return this.getCurrentWorldData().pathTypesByPosCache; // Folia - region threading - } - - @Override -@@ -2489,7 +2595,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system - } - -- private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { -+ public boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { // Folia - region threaded - make public - // Paper start - rewrite chunk system - final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos); - // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded -@@ -2581,7 +2687,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // Paper start - optimize redstone (Alternate Current) - @Override - public alternate.current.wire.WireHandler getWireHandler() { -- return wireHandler; -+ return this.getCurrentWorldData().wireHandler; // Folia - region threading - } - // Paper end - optimize redstone (Alternate Current) - -@@ -2592,18 +2698,18 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - @Override - public void onDestroyed(Entity entity) { -- ServerLevel.this.getScoreboard().entityRemoved(entity); -+ // ServerLevel.this.getScoreboard().entityRemoved(entity); // Folia - region threading - } - - @Override - public void onTickingStart(Entity entity) { - if (entity instanceof net.minecraft.world.entity.Marker && !paperConfig().entities.markers.tick) return; // Paper - Configurable marker ticking -- ServerLevel.this.entityTickList.add(entity); -+ ServerLevel.this.getCurrentWorldData().addEntityTickingEntity(entity); // Folia - region threading - } - - @Override - public void onTickingEnd(Entity entity) { -- ServerLevel.this.entityTickList.remove(entity); -+ ServerLevel.this.getCurrentWorldData().removeEntityTickingEntity(entity); // Folia - region threading - // Paper start - Reset pearls when they stop being ticked - if (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { - pearl.cachedOwner = null; -@@ -2615,6 +2721,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - @Override - public void onTrackingStart(Entity entity) { - org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot -+ ServerLevel.this.getCurrentWorldData().addLoadedEntity(entity); // Folia - region threading - // ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server; moved down below valid=true - if (entity instanceof ServerPlayer serverPlayer) { - ServerLevel.this.players.add(serverPlayer); -@@ -2629,12 +2736,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - ); - } - -- ServerLevel.this.navigatingMobs.add(mob); -+ ServerLevel.this.getCurrentWorldData().addNavigatingMob(mob); // Folia - region threading - } - - if (entity instanceof EnderDragon enderDragon) { - for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) { -- ServerLevel.this.dragonParts.put(enderDragonPart.getId(), enderDragonPart); -+ ServerLevel.this.dragonParts.put((long)enderDragonPart.getId(), enderDragonPart); // Folia - diff on change - } - } - -@@ -2657,18 +2764,27 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - @Override - public void onTrackingEnd(Entity entity) { - org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot -+ ServerLevel.this.getCurrentWorldData().removeLoadedEntity(entity); // Folia - region threading - // Spigot start // TODO I don't think this is needed anymore - if (entity instanceof Player player) { - for (final ServerLevel level : ServerLevel.this.getServer().getAllLevels()) { -- for (final Optional savedData : level.getDataStorage().cache.values()) { -+ // Folia start - make map data thread-safe -+ List> worldDataCache; -+ synchronized (level.getDataStorage().cache) { -+ worldDataCache = new java.util.ArrayList<>(level.getDataStorage().cache.values()); -+ } -+ for (final Optional savedData : worldDataCache) { -+ // Folia end - make map data thread-safe - if (savedData.isEmpty() || !(savedData.get() instanceof MapItemSavedData map)) { - continue; - } - -+ synchronized (map) { // Folia - make map data thread-safe - map.carriedByPlayers.remove(player); - if (map.carriedBy.removeIf(holdingPlayer -> holdingPlayer.player == player)) { - map.decorations.remove(player.getName().getString()); - } -+ } // Folia - make map data thread-safe - } - } - } -@@ -2699,18 +2815,19 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - ); - } - -- ServerLevel.this.navigatingMobs.remove(mob); -+ ServerLevel.this.getCurrentWorldData().removeNavigatingMob(mob); // Folia - region threading - } - - if (entity instanceof EnderDragon enderDragon) { - for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) { -- ServerLevel.this.dragonParts.remove(enderDragonPart.getId()); -+ ServerLevel.this.dragonParts.remove((long)enderDragonPart.getId()); // Folia - diff on change - } - } - - entity.updateDynamicGameEventListener(DynamicGameEventListener::remove); - // CraftBukkit start - entity.valid = false; -+ // Folia - region threading - TODO THIS SHIT - if (!(entity instanceof ServerPlayer)) { - for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players - player.getBukkitEntity().onEntityRemove(entity); -@@ -2738,11 +2855,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - private long lagCompensationTick = MinecraftServer.SERVER_INIT; - - public long getLagCompensationTick() { -- return this.lagCompensationTick; -+ return this.getCurrentWorldData().getLagCompensationTick(); // Folia - region threading - } - - public void updateLagCompensationTick() { -- this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); -+ throw new UnsupportedOperationException(); // Folia - region threading - } - // Paper end - lag compensation - } -diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java -index f347ff8d863f4bcef46604c757de112cb3fe445c..ab85c5acc63abc07a55ff7c5e207527bb18d50b2 100644 ---- a/net/minecraft/server/level/ServerPlayer.java -+++ b/net/minecraft/server/level/ServerPlayer.java -@@ -180,7 +180,7 @@ import org.slf4j.Logger; - - public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system - private static final Logger LOGGER = LogUtils.getLogger(); -- public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving -+ public static final long LAST_SAVE_ABSENT = Long.MIN_VALUE; public long lastSave = LAST_SAVE_ABSENT; // Paper // Folia - threaded regions - changed to nanoTime - private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; - private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; - private static final int FLY_STAT_RECORDING_SPEED = 25; -@@ -443,8 +443,149 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc - this.maxHealthCache = this.getMaxHealth(); - } - -+ // Folia start - region threading -+ private static final int SPAWN_RADIUS_SELECTION_SEARCH = 5; -+ -+ private static BlockPos getRandomSpawn(ServerLevel world, RandomSource random) { -+ BlockPos spawn = world.getSharedSpawnPos(); -+ double radius = (double)Math.max(0, world.getGameRules().getInt(GameRules.RULE_SPAWN_RADIUS)); -+ -+ double spawnX = (double)spawn.getX() + 0.5; -+ double spawnZ = (double)spawn.getZ() + 0.5; -+ -+ net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder(); -+ -+ double selectMinX = Math.max(worldBorder.getMinX() + 1.0, spawnX - radius); -+ double selectMinZ = Math.max(worldBorder.getMinZ() + 1.0, spawnZ - radius); -+ double selectMaxX = Math.min(worldBorder.getMaxX() - 1.0, spawnX + radius); -+ double selectMaxZ = Math.min(worldBorder.getMaxZ() - 1.0, spawnZ + radius); -+ -+ double amountX = selectMaxX - selectMinX; -+ double amountZ = selectMaxZ - selectMinZ; -+ -+ int selectX = amountX < 1.0 ? Mth.floor(worldBorder.getCenterX()) : (int)Mth.floor((amountX + 1.0) * random.nextDouble() + selectMinX); -+ int selectZ = amountZ < 1.0 ? Mth.floor(worldBorder.getCenterZ()) : (int)Mth.floor((amountZ + 1.0) * random.nextDouble() + selectMinZ); -+ -+ return new BlockPos(selectX, 0, selectZ); -+ } -+ -+ private static void completeSpawn(ServerLevel world, BlockPos selected, -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { -+ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(selected), world.levelData.getSpawnAngle(), 0.0f)); -+ } -+ -+ private static BlockPos findSpawnAround(ServerLevel world, ServerPlayer player, BlockPos selected) { -+ // try hard to find, so that we don't attempt another chunk load -+ for (int dz = -SPAWN_RADIUS_SELECTION_SEARCH; dz <= SPAWN_RADIUS_SELECTION_SEARCH; ++dz) { -+ for (int dx = -SPAWN_RADIUS_SELECTION_SEARCH; dx <= SPAWN_RADIUS_SELECTION_SEARCH; ++dx) { -+ BlockPos inChunk = PlayerRespawnLogic.getOverworldRespawnPos(world, selected.getX() + dx, selected.getZ() + dz); -+ if (inChunk == null) { -+ continue; -+ } -+ -+ AABB checkVolume = player.getBoundingBoxAt((double)inChunk.getX() + 0.5, (double)inChunk.getY(), (double)inChunk.getZ() + 0.5); -+ -+ if (!player.noCollisionNoLiquid(world, checkVolume)) { -+ continue; -+ } -+ -+ return inChunk; -+ } -+ } -+ -+ return null; -+ } -+ -+ // rets false when another attempt is required -+ private static boolean trySpawnOrSchedule(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts, -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { -+ ++attemptCount[0]; -+ -+ BlockPos rough = getRandomSpawn(world, random); -+ -+ // add 2 to ensure that the chunks are loaded for collision checks -+ int minX = (rough.getX() - (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; -+ int minZ = (rough.getZ() - (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; -+ int maxX = (rough.getX() + (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; -+ int maxZ = (rough.getZ() + (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; -+ -+ // we could short circuit this check, but it would possibly recurse. Then, it could end up causing a stack overflow -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, minX, minZ, maxX, maxZ) || !world.moonrise$areChunksLoaded(minX, minZ, maxX, maxZ)) { -+ world.moonrise$loadChunksAsync(minX, maxX, minZ, maxZ, ca.spottedleaf.concurrentutil.util.Priority.HIGHER, -+ (unused) -> { -+ BlockPos selected = findSpawnAround(world, player, rough); -+ if (selected == null) { -+ // run more spawn attempts -+ selectSpawn(world, player, random, attemptCount, maxAttempts, toComplete); -+ return; -+ } -+ -+ completeSpawn(world, selected, toComplete); -+ return; -+ } -+ ); -+ return true; -+ } -+ -+ BlockPos selected = findSpawnAround(world, player, rough); -+ if (selected == null) { -+ return false; -+ } -+ -+ completeSpawn(world, selected, toComplete); -+ return true; -+ } -+ -+ private static void selectSpawn(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts, -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { -+ do { -+ if (attemptCount[0] >= maxAttempts) { -+ BlockPos sharedSpawn = world.getSharedSpawnPos(); -+ -+ LOGGER.warn("Found no spawn in radius for player '" + player.getName() + "', ignoring radius"); -+ -+ selectSpawnWithoutRadius(world, player, sharedSpawn, toComplete); -+ return; -+ } -+ } while (!trySpawnOrSchedule(world, player, random, attemptCount, maxAttempts, toComplete)); -+ } -+ -+ -+ private static void selectSpawnWithoutRadius(ServerLevel world, ServerPlayer player, BlockPos spawn, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { -+ world.loadChunksForMoveAsync(player.getBoundingBoxAt(spawn.getX() + 0.5, spawn.getY(), spawn.getZ() + 0.5), -+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, -+ (c) -> { -+ BlockPos ret = spawn; -+ while (!player.noCollisionNoLiquid(world, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY(), ret.getZ() + 0.5)) && ret.getY() < (double)world.getMaxY()) { -+ ret = ret.above(); -+ } -+ while (player.noCollisionNoLiquid(world, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY() - 1, ret.getZ() + 0.5)) && ret.getY() > (double)(world.getMinY() + 1)) { -+ ret = ret.below(); -+ } -+ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(ret), world.levelData.getSpawnAngle(), 0.0f)); -+ } -+ ); -+ } -+ -+ public static void fudgeSpawnLocation(ServerLevel world, ServerPlayer player, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { // Folia - region threading -+ BlockPos blockposition = world.getSharedSpawnPos(); -+ -+ if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit -+ selectSpawn(world, player, player.random, new int[1], 500, toComplete); -+ } else { -+ selectSpawnWithoutRadius(world, player, blockposition, toComplete); -+ } -+ -+ } -+ // Folia end - region threading -+ - @Override - public BlockPos adjustSpawnLocation(ServerLevel level, BlockPos pos) { -+ // Folia start - region threading -+ if (true) { -+ throw new UnsupportedOperationException(); -+ } -+ // Folia end - region threading - AABB aabb = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO); - BlockPos blockPos = pos; - if (level.dimensionType().hasSkyLight() && level.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit -@@ -709,10 +850,17 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc - ServerLevel level = this.level().getServer().getLevel(optional.get()); - if (level != null) { - Entity entity = EntityType.loadEntityRecursive( -- compoundTag, level, EntitySpawnReason.LOAD, entity1 -> !level.addWithUUID(entity1) ? null : entity1 -+ compoundTag, level, EntitySpawnReason.LOAD, entity1 -> entity1 // Folia - region threading - delay world add - ); - if (entity != null) { -- placeEnderPearlTicket(level, entity.chunkPosition()); -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ level, entity.chunkPosition().x, entity.chunkPosition().z, () -> { -+ level.addFreshEntityWithPassengers(entity); -+ ServerPlayer.placeEnderPearlTicket(level, entity.chunkPosition()); -+ } -+ ); -+ // Folia end - region threading - } else { - LOGGER.warn("Failed to spawn player ender pearl in level ({}), skipping", optional.get()); - } -@@ -1357,6 +1505,324 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc - } - } - -+ // Folia start - region threading -+ /** -+ * Teleport flag indicating that the player is to be respawned, expected to only be used -+ * internally for {@link #respawn(java.util.function.Consumer, PlayerRespawnEvent.RespawnReason)} -+ */ -+ public static final long TELEPORT_FLAGS_PLAYER_RESPAWN = Long.MIN_VALUE >>> 0; -+ -+ public void exitEndCredits() { -+ if (!this.wonGame) { -+ // not in the end credits anymore -+ return; -+ } -+ this.wonGame = false; -+ -+ this.respawn((player) -> { -+ CriteriaTriggers.CHANGED_DIMENSION.trigger(player, Level.END, Level.OVERWORLD); -+ }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.END_PORTAL, true); -+ } -+ -+ public void respawn(java.util.function.Consumer respawnComplete, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason reason) { -+ this.respawn(respawnComplete, reason, false); -+ } -+ -+ private void respawn(java.util.function.Consumer respawnComplete, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason reason, boolean alive) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot respawn entity async"); -+ -+ this.getBukkitEntity(); // force bukkit entity to be created before TPing -+ -+ if (alive != this.isAlive()) { -+ throw new IllegalStateException("isAlive expected = " + alive); -+ } -+ -+ if (!this.hasNullCallback()) { -+ this.unRide(); -+ } -+ -+ if (this.isVehicle() || this.isPassenger()) { -+ throw new IllegalStateException("Dead player should not be a vehicle or passenger"); -+ } -+ -+ ServerLevel origin = this.serverLevel(); -+ ServerLevel respawnWorld = this.server.getLevel(this.getRespawnDimension()); -+ -+ // modified based off PlayerList#respawn -+ -+ EntityTreeNode passengerTree = this.makePassengerTree(); -+ -+ this.isChangingDimension = true; -+ origin.removePlayerImmediately(this, RemovalReason.CHANGED_DIMENSION); -+ // reset player if needed, only after removal from world -+ if (!alive) { -+ ServerPlayer.this.reset(); -+ } -+ // must be manually removed from connections, delay until after reset() so that we do not trip any thread checks -+ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection); -+ -+ BlockPos respawnPos = this.getRespawnPosition(); -+ float respawnAngle = this.getRespawnAngle(); -+ boolean isRespawnForced = this.isRespawnForced(); -+ -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable spawnPosComplete = -+ new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); -+ boolean[] usedRespawnAnchor = new boolean[1]; -+ -+ // set up post spawn location logic -+ spawnPosComplete.addWaiter((spawnLoc, throwable) -> { -+ // update pos and velocity -+ ServerPlayer.this.setPosRaw(spawnLoc.getX(), spawnLoc.getY(), spawnLoc.getZ()); -+ ServerPlayer.this.setYRot(spawnLoc.getYaw()); -+ ServerPlayer.this.setYHeadRot(spawnLoc.getYaw()); -+ ServerPlayer.this.setXRot(spawnLoc.getPitch()); -+ ServerPlayer.this.setDeltaMovement(Vec3.ZERO); -+ // placeInAsync will update the world -+ -+ this.placeInAsync( -+ origin, -+ // use the load chunk flag just in case the spawn loc isn't loaded, and to ensure the chunks -+ // stay loaded for a bit with the teleport ticket -+ ((org.bukkit.craftbukkit.CraftWorld)spawnLoc.getWorld()).getHandle(), -+ TELEPORT_FLAG_LOAD_CHUNK | TELEPORT_FLAGS_PLAYER_RESPAWN, -+ passengerTree, // note: we expect this to just be the player, no passengers -+ (entity) -> { -+ // now the player is in the world, and can receive sound -+ if (usedRespawnAnchor[0]) { -+ ServerPlayer.this.connection.send( -+ new ClientboundSoundPacket( -+ net.minecraft.sounds.SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS, -+ ServerPlayer.this.getX(), ServerPlayer.this.getY(), ServerPlayer.this.getZ(), -+ 1.0F, 1.0F, ServerPlayer.this.serverLevel().getRandom().nextLong() -+ ) -+ ); -+ } -+ // now the respawn logic is complete -+ -+ // last, call the function callback -+ if (respawnComplete != null) { -+ respawnComplete.accept(ServerPlayer.this); -+ } -+ } -+ ); -+ }); -+ -+ // find and modify respawn block state -+ if (respawnWorld == null || respawnPos == null) { -+ // default to regular spawn -+ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); -+ } else { -+ // load chunk for block -+ // give at least 1 radius of loaded chunks so that we do not sync load anything -+ int radiusBlocks = 16; -+ respawnWorld.moonrise$loadChunksAsync(respawnPos, radiusBlocks, -+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, -+ (chunks) -> { -+ ServerPlayer.RespawnPosAngle spawnPos = ServerPlayer.findRespawnAndUseSpawnBlock( -+ respawnWorld, respawnPos, respawnAngle, isRespawnForced, !alive -+ ).orElse(null); -+ if (spawnPos == null) { -+ // no spawn -+ ServerPlayer.this.connection.send( -+ new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F) -+ ); -+ ServerPlayer.this.setRespawnPosition( -+ null, null, 0f, false, false, -+ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN -+ ); -+ // default to regular spawn -+ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); -+ return; -+ } -+ -+ boolean isRespawnAnchor = respawnWorld.getBlockState(respawnPos).is(net.minecraft.world.level.block.Blocks.RESPAWN_ANCHOR); -+ boolean isBed = respawnWorld.getBlockState(respawnPos).is(net.minecraft.tags.BlockTags.BEDS); -+ usedRespawnAnchor[0] = !alive && isRespawnAnchor; -+ -+ ServerPlayer.this.setRespawnPosition( -+ respawnWorld.dimension(), respawnPos, respawnAngle, isRespawnForced, false, -+ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN -+ ); -+ -+ // finished now, pass the location on -+ spawnPosComplete.complete( -+ io.papermc.paper.util.MCUtil.toLocation(respawnWorld, spawnPos.position(), spawnPos.yaw(), 0.0f) -+ ); -+ return; -+ } -+ ); -+ } -+ } -+ -+ @Override -+ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { -+ if (yaw != null) { -+ this.setYRot(yaw.floatValue()); -+ this.setYHeadRot(yaw.floatValue()); -+ } -+ if (pitch != null) { -+ this.setXRot(pitch.floatValue()); -+ } -+ if (velocity != null) { -+ this.setDeltaMovement(velocity); -+ } -+ this.connection.internalTeleport( -+ new net.minecraft.world.entity.PositionMoveRotation( -+ pos, this.getDeltaMovement(), this.getYRot(), this.getXRot() -+ ), -+ java.util.Collections.emptySet() -+ ); -+ this.connection.resetPosition(); -+ this.setOldPosAndRot(); -+ this.resetStoredPositions(); -+ } -+ -+ @Override -+ protected ServerPlayer transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { -+ // must be manually removed from connections -+ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection); -+ this.serverLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); -+ -+ this.spawnIn(destination); -+ this.transform(pos, yaw, pitch, velocity); -+ -+ return this; -+ } -+ -+ @Override -+ public void preChangeDimension() { -+ super.preChangeDimension(); -+ this.stopUsingItem(); -+ } -+ -+ @Override -+ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { -+ if (destination == originWorld && (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L) { -+ this.unsetRemoved(); -+ destination.addDuringTeleport(this); -+ -+ // must be manually added to connections -+ this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection); -+ -+ // required to set up the pending teleport stuff to the client, and to actually update -+ // the player's position clientside -+ this.connection.internalTeleport( -+ new net.minecraft.world.entity.PositionMoveRotation( -+ this.position(), this.getDeltaMovement(), this.getYRot(), this.getXRot() -+ ), -+ java.util.Collections.emptySet() -+ ); -+ this.connection.resetPosition(); -+ -+ this.postChangeDimension(); -+ } else { -+ // Modelled after PlayerList#respawn -+ -+ // We avoid checking for disconnection here, which means we do not have to add/remove from -+ // the player list here. We can let this be properly handled by the connection handler -+ -+ // pre-add logic -+ PlayerList playerlist = this.server.getPlayerList(); -+ net.minecraft.world.level.storage.LevelData worlddata = destination.getLevelData(); -+ this.connection.send( -+ new ClientboundRespawnPacket( -+ this.createCommonSpawnInfo(destination), -+ (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L ? (byte)1 : (byte)0 -+ ) -+ ); -+ // don't bother with the chunk cache radius and simulation distance packets, they are handled -+ // by the chunk loader -+ this.spawnIn(destination); // important that destination != null -+ // we can delay teleport until later, the player position is already set up at the target -+ this.setShiftKeyDown(false); -+ -+ this.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket( -+ destination.getSharedSpawnPos(), destination.getSharedSpawnAngle() -+ )); -+ this.connection.send(new ClientboundChangeDifficultyPacket( -+ worlddata.getDifficulty(), worlddata.isDifficultyLocked() -+ )); -+ this.connection.send(new ClientboundSetExperiencePacket( -+ this.experienceProgress, this.totalExperience, this.experienceLevel -+ )); -+ -+ playerlist.sendActivePlayerEffects(this); -+ playerlist.sendLevelInfo(this, destination); -+ playerlist.sendPlayerPermissionLevel(this); -+ -+ // regular world add logic -+ this.unsetRemoved(); -+ destination.addDuringTeleport(this); -+ -+ // must be manually added to connections -+ this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection); -+ -+ // required to set up the pending teleport stuff to the client, and to actually update -+ // the player's position clientside -+ this.connection.internalTeleport( -+ new net.minecraft.world.entity.PositionMoveRotation( -+ this.position(), this.getDeltaMovement(), this.getYRot(), this.getXRot() -+ ), -+ java.util.Collections.emptySet() -+ ); -+ this.connection.resetPosition(); -+ -+ // delay callback until after post add logic -+ -+ // post add logic -+ -+ // "Added from changeDimension" -+ this.setHealth(this.getHealth()); -+ playerlist.sendAllPlayerInfo(this); -+ this.onUpdateAbilities(); -+ /*for (MobEffectInstance mobEffect : this.getActiveEffects()) { -+ this.connection.send(new ClientboundUpdateMobEffectPacket(this.getId(), mobEffect, false)); -+ }*/ // handled by sendActivePlayerEffects -+ -+ // Paper start - Reset shield blocking on dimension change -+ if (this.isBlocking()) { -+ this.stopUsingItem(); -+ } -+ // Paper end - Reset shield blocking on dimension change -+ -+ this.triggerDimensionChangeTriggers(originWorld); -+ -+ // finished -+ -+ this.postChangeDimension(); -+ } -+ } -+ -+ @Override -+ public boolean endPortalLogicAsync(BlockPos portalPos) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); -+ -+ if (this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { -+ if (!this.canPortalAsync(null, false)) { -+ return false; -+ } -+ this.wonGame = true; -+ // TODO is there a better solution to this that DOESN'T skip the credits? -+ this.seenCredits = true; -+ if (!this.seenCredits) { -+ this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 0.0F)); -+ } -+ this.exitEndCredits(); -+ return true; -+ } else { -+ return super.endPortalLogicAsync(portalPos); -+ } -+ } -+ -+ @Override -+ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { -+ super.prePortalLogic(origin, destination, type); -+ if (origin.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.OVERWORLD && destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER) { -+ this.enteredNetherPosition = this.position(); -+ } -+ } -+ // Folia end - region threading -+ - @Nullable - @Override - public ServerPlayer teleport(TeleportTransition teleportTransition) { -@@ -2398,6 +2864,11 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc - } - - public void setCamera(@Nullable Entity entityToSpectate) { -+ // Folia start - region threading -+ if (entityToSpectate != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entityToSpectate)) { -+ return; -+ } -+ // Folia end - region threading - Entity camera = this.getCamera(); - this.camera = (Entity)(entityToSpectate == null ? this : entityToSpectate); - if (camera != this.camera) { -@@ -2896,11 +3367,11 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc - } - - public void registerEnderPearl(ThrownEnderpearl enderPearl) { -- this.enderPearls.add(enderPearl); -+ //this.enderPearls.add(enderPearl); // Folia - region threading - do not track ender pearls - } - - public void deregisterEnderPearl(ThrownEnderpearl enderPearl) { -- this.enderPearls.remove(enderPearl); -+ //this.enderPearls.remove(enderPearl); // Folia - region threading - do not track ender pearls - } - - public Set getEnderPearls() { -@@ -3054,7 +3525,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc - this.experienceLevel = this.newLevel; - this.totalExperience = this.newTotalExp; - this.experienceProgress = 0; -- this.deathTime = 0; -+ this.deathTime = 0; this.broadcastedDeath = false; // Folia - region threading - this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent - this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH); - this.effectsDirty = true; -diff --git a/net/minecraft/server/level/ServerPlayerGameMode.java b/net/minecraft/server/level/ServerPlayerGameMode.java -index 623c069f1fe079e020c6391a3db1a3d95cd3dbf5..61804cdb6be06b1b3316e563df57f0b38268958a 100644 ---- a/net/minecraft/server/level/ServerPlayerGameMode.java -+++ b/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -114,7 +114,7 @@ public class ServerPlayerGameMode { - // this.gameTicks = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - this.gameTicks = (int) this.level.getLagCompensationTick(); // Paper - lag compensate eating - if (this.hasDelayedDestroy) { -- BlockState blockState = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks -+ BlockState blockState = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.delayedDestroyPos) ? null : this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks // Folia - region threading - don't destroy blocks not owned - if (blockState == null || blockState.isAir()) { // Paper - Don't allow digging into unloaded chunks - this.hasDelayedDestroy = false; - } else { -@@ -126,7 +126,7 @@ public class ServerPlayerGameMode { - } - } else if (this.isDestroyingBlock) { - // Paper start - Don't allow digging into unloaded chunks; don't want to do same logic as above, return instead -- BlockState blockState = this.level.getBlockStateIfLoaded(this.destroyPos); -+ BlockState blockState = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.destroyPos) ? null : this.level.getBlockStateIfLoaded(this.destroyPos); // Folia - region threading - don't destroy blocks not owned - if (blockState == null) { - this.isDestroyingBlock = false; - return; -@@ -369,7 +369,7 @@ public class ServerPlayerGameMode { - } else { - // CraftBukkit start - org.bukkit.block.BlockState state = bblock.getState(); -- this.level.captureDrops = new java.util.ArrayList<>(); -+ this.level.getCurrentWorldData().captureDrops = new java.util.ArrayList<>(); // Folia - region threading - // CraftBukkit end - BlockState blockState1 = block.playerWillDestroy(this.level, pos, blockState, this.player); - boolean flag = this.level.removeBlock(pos, false); -@@ -395,8 +395,8 @@ public class ServerPlayerGameMode { - // return true; // CraftBukkit - } - // CraftBukkit start -- java.util.List itemsToDrop = this.level.captureDrops; // Paper - capture all item additions to the world -- this.level.captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff -+ java.util.List itemsToDrop = this.level.getCurrentWorldData().captureDrops; // Paper - capture all item additions to the world // Folia - region threading -+ this.level.getCurrentWorldData().captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff // Folia - region threading - if (event.isDropItems()) { - org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - capture all item additions to the world - } -diff --git a/net/minecraft/server/level/TicketType.java b/net/minecraft/server/level/TicketType.java -index 8f12a4df5d63ecd11e6e615d910b6e3f6dde5f3c..f8b74eaf534c6264ce018a6826c3d035089e7d30 100644 ---- a/net/minecraft/server/level/TicketType.java -+++ b/net/minecraft/server/level/TicketType.java -@@ -17,10 +17,18 @@ public class TicketType { - public static final TicketType FORCED = create("forced", Comparator.comparingLong(ChunkPos::toLong)); - public static final TicketType PORTAL = create("portal", Vec3i::compareTo, 300); - public static final TicketType ENDER_PEARL = create("ender_pearl", Comparator.comparingLong(ChunkPos::toLong), 40); -- public static final TicketType UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1); -+ public static final TicketType UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 5); // Folia - region threading - public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit - public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit - public static final TicketType POST_TELEPORT = TicketType.create("post_teleport", Integer::compare, 5); // Paper - post teleport ticket type -+ // Folia start - region threading -+ public static final TicketType LOGIN = create("folia:login", (u1, u2) -> 0, 20); -+ public static final TicketType DELAYED = create("folia:delay", (u1, u2) -> 0, 5); -+ public static final TicketType END_GATEWAY_EXIT_SEARCH = create("folia:end_gateway_exit_search", Long::compareTo); -+ public static final TicketType NETHER_PORTAL_DOUBLE_CHECK = create("folia:nether_portal_double_check", Long::compareTo); -+ public static final TicketType TELEPORT_HOLD_TICKET = create("folia:teleport_hold_ticket", Long::compareTo); -+ public static final TicketType REGION_SCHEDULER_API_HOLD = create("folia:region_scheduler_api_hold", (a, b) -> 0); -+ // Folia end - region threading - - public static TicketType create(String name, Comparator comparator) { - return new TicketType<>(name, comparator, 0L); -diff --git a/net/minecraft/server/level/WorldGenRegion.java b/net/minecraft/server/level/WorldGenRegion.java -index 7fa41dea184b01891f45d8e404bc1cba19cf1bcf..43de96cc3c2b1259b1edb5feae3f202dea65dcdf 100644 ---- a/net/minecraft/server/level/WorldGenRegion.java -+++ b/net/minecraft/server/level/WorldGenRegion.java -@@ -107,6 +107,13 @@ public class WorldGenRegion implements WorldGenLevel { - return this.getLightEngine().getRawBrightness(blockPos, subtract); - } - // Paper end - rewrite chunk system -+ // Folia start - region threading -+ private final net.minecraft.world.level.StructureManager structureManager; -+ @Override -+ public net.minecraft.world.level.StructureManager structureManager() { -+ return this.structureManager; -+ } -+ // Folia end - region threading - - public WorldGenRegion(ServerLevel level, StaticCache2D cache, ChunkStep generatingStep, ChunkAccess center) { - this.generatingStep = generatingStep; -@@ -118,6 +125,7 @@ public class WorldGenRegion implements WorldGenLevel { - this.random = level.getChunkSource().randomState().getOrCreateRandomFactory(WORLDGEN_REGION_RANDOM).at(this.center.getPos().getWorldPosition()); - this.dimensionType = level.dimensionType(); - this.biomeManager = new BiomeManager(this, BiomeManager.obfuscateSeed(this.seed)); -+ this.structureManager = level.structureManager().forWorldGenRegion(this); // Folia - region threading - } - - public boolean isOldChunkAround(ChunkPos pos, int radius) { -diff --git a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java -index e71c1a564e5d4ac43460f89879ff709ee685706f..6eca15223b92aedac74233db886e2c1248750e2c 100644 ---- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java -+++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java -@@ -96,6 +96,10 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack - } - } - -+ // Folia start - region threading -+ private boolean handledDisconnect = false; -+ // Folia end - region threading -+ - @Override - public void onDisconnect(DisconnectionDetails details) { - // Paper start - Fix kick event leave message not being sent -@@ -104,10 +108,18 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack - - public void onDisconnect(DisconnectionDetails info, @Nullable net.kyori.adventure.text.Component quitMessage) { - // Paper end - Fix kick event leave message not being sent -+ // Folia start - region threading -+ if (this.handledDisconnect) { -+ // avoid retiring scheduler twice -+ return; -+ } -+ this.handledDisconnect = true; -+ // Folia end - region threading - if (this.isSingleplayerOwner()) { - LOGGER.info("Stopping singleplayer server as player logged out"); - this.server.halt(false); - } -+ this.player.getBukkitEntity().taskScheduler.retire(); // Folia - region threading - } - - @Override -@@ -330,24 +342,8 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack - if (this.processedDisconnect) { - return; - } -- if (!this.cserver.isPrimaryThread()) { -- org.bukkit.craftbukkit.util.Waitable waitable = new org.bukkit.craftbukkit.util.Waitable() { -- @Override -- protected Object evaluate() { -- ServerCommonPacketListenerImpl.this.disconnect(disconnectionDetails, cause); // Paper - kick event causes -- return null; -- } -- }; -- -- this.server.processQueue.add(waitable); -- -- try { -- waitable.get(); -- } catch (InterruptedException e) { -- Thread.currentThread().interrupt(); -- } catch (java.util.concurrent.ExecutionException e) { -- throw new RuntimeException(e); -- } -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player)) { // Folia - region threading -+ this.connection.disconnectSafely(disconnectionDetails, cause); // Folia - region threading - it HAS to be delayed/async to avoid deadlock if we try to wait for another region - return; - } - -@@ -378,7 +374,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack - this.onDisconnect(disconnectionDetails, leaveMessage); // CraftBukkit - fire quit instantly // Paper - use kick event leave message - this.connection.setReadOnly(); - // CraftBukkit - Don't wait -- this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper -+ //this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper // Folia - region threading - } - - // Paper start - add proper async disconnect -@@ -391,19 +387,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack - } - - public void disconnectAsync(DisconnectionDetails disconnectionInfo, org.bukkit.event.player.PlayerKickEvent.Cause cause) { -- if (this.cserver.isPrimaryThread()) { -- this.disconnect(disconnectionInfo, cause); -- return; -- } -- -- this.connection.setReadOnly(); -- this.server.scheduleOnMain(() -> { -- ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause); -- if (ServerCommonPacketListenerImpl.this.player.quitReason == null) { -- // cancelled -- ServerCommonPacketListenerImpl.this.connection.enableAutoRead(); -- } -- }); -+ this.disconnect(disconnectionInfo, cause); // Folia - threaded regions - } - // Paper end - add proper async disconnect - -diff --git a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java -index 2e9eb04c7c4342393c05339906c267bca9ff29b1..00fb8a5dda1f305a0e0f947bbb75a3f40b5318cc 100644 ---- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java -+++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java -@@ -47,6 +47,7 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis - private ClientInformation clientInformation; - @Nullable - private SynchronizeRegistriesTask synchronizeRegistriesTask; -+ public boolean switchToMain = false; // Folia - region threading - rewrite login process - - // CraftBukkit start - public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie, ServerPlayer player) { -@@ -160,7 +161,58 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis - } - - ServerPlayer playerForLogin = playerList.getPlayerForLogin(this.gameProfile, this.clientInformation, this.player); // CraftBukkit -- playerList.placeNewPlayer(this.connection, playerForLogin, this.createCookie(this.clientInformation)); -+ // Folia start - region threading - rewrite login process -+ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot handle player login off global tick thread"); -+ CommonListenerCookie clientData = this.createCookie(this.clientInformation); -+ org.apache.commons.lang3.mutable.MutableObject data = new org.apache.commons.lang3.mutable.MutableObject<>(); -+ org.apache.commons.lang3.mutable.MutableObject lastKnownName = new org.apache.commons.lang3.mutable.MutableObject<>(); -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); -+ // note: need to call addWaiter before completion to ensure the callback is invoked synchronously -+ // the loadSpawnForNewPlayer function always completes the completable once the chunks were loaded, -+ // on the load callback for those chunks (so on the same region) -+ // this guarantees the chunk cannot unload under our feet -+ toComplete.addWaiter((org.bukkit.Location loc, Throwable t) -> { -+ int chunkX = net.minecraft.util.Mth.floor(loc.getX()) >> 4; -+ int chunkZ = net.minecraft.util.Mth.floor(loc.getZ()) >> 4; -+ -+ net.minecraft.server.level.ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)loc.getWorld()).getHandle(); -+ // we just need to hold the chunks at loaded until the next tick -+ // so we do not need to care about unique IDs for the ticket -+ world.getChunkSource().addTicketAtLevel( -+ net.minecraft.server.level.TicketType.LOGIN, -+ new net.minecraft.world.level.ChunkPos(chunkX, chunkZ), -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, -+ net.minecraft.util.Unit.INSTANCE -+ ); -+ -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ world, chunkX, chunkZ, -+ () -> { -+ // once switchToMain is set, the current ticking region now owns the connection and is responsible -+ // for cleaning it up -+ playerList.placeNewPlayer( -+ ServerConfigurationPacketListenerImpl.this.connection, -+ playerForLogin, -+ clientData, -+ java.util.Optional.ofNullable(data.getValue()), -+ lastKnownName.getValue(), -+ loc -+ ); -+ }, -+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER -+ ); -+ }); -+ this.switchToMain = true; -+ try { -+ // now the connection responsibility is transferred on the region -+ playerList.loadSpawnForNewPlayer(this.connection, playerForLogin, clientData, data, lastKnownName, toComplete); -+ } catch (final Throwable throwable) { -+ // assume toComplete will not be invoked -+ // ensure global tick thread owns the connection again, to properly disconnect it -+ this.switchToMain = false; -+ throw new RuntimeException(throwable); -+ } -+ // Folia end - region threading - rewrite login process - } catch (Exception var5) { - LOGGER.error("Couldn't place player in world", (Throwable)var5); - // Paper start - Debugging -diff --git a/net/minecraft/server/network/ServerConnectionListener.java b/net/minecraft/server/network/ServerConnectionListener.java -index bd07e6a5aa1883786d789ea71711a0c0c0a95c26..09469ad131622158fe5579216fc4164251ff2220 100644 ---- a/net/minecraft/server/network/ServerConnectionListener.java -+++ b/net/minecraft/server/network/ServerConnectionListener.java -@@ -167,12 +167,15 @@ public class ServerConnectionListener { - } - // Paper end - Add support for proxy protocol - // ServerConnectionListener.this.connections.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking -- ServerConnectionListener.this.pending.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking -+ //ServerConnectionListener.this.pending.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking // Folia - connection fixes - move down - connection.configurePacketHandler(channelPipeline); - connection.setListenerForServerboundHandshake( - new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, connection) - ); - io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper - Add Channel initialization listeners -+ // Folia start - regionised threading -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addConnection(connection); -+ // Folia end - regionised threading - } - } - ) -@@ -242,7 +245,7 @@ public class ServerConnectionListener { - // Spigot start - this.addPending(); // Paper - prevent blocking on adding a new connection while the server is ticking - // This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order -- if (org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0) { -+ if (org.spigotmc.SpigotConfig.playerShuffle > 0 && 0 % org.spigotmc.SpigotConfig.playerShuffle == 0) { // Folia - region threading - Collections.shuffle(this.connections); - } - // Spigot end -diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index d248671b2e1c6256fc4d74320bdb29ca078bad0b..8154cec88f2222b7f75c695332e942cf4839b4d9 100644 ---- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -292,10 +292,10 @@ public class ServerGamePacketListenerImpl - private int knownMovePacketCount; - private boolean receivedMovementThisTick; - // CraftBukkit start - add fields -- private int lastTick = MinecraftServer.currentTick; -+ private long lastTick = Util.getMillis() / 50L; // Folia - region threading - private int allowedPlayerTicks = 1; -- private int lastDropTick = MinecraftServer.currentTick; -- private int lastBookTick = MinecraftServer.currentTick; -+ private long lastDropTick = Util.getMillis() / 50L; // Folia - region threading -+ private long lastBookTick = Util.getMillis() / 50L; // Folia - region threading - private int dropCount = 0; - - private boolean hasMoved = false; -@@ -313,9 +313,16 @@ public class ServerGamePacketListenerImpl - private final LastSeenMessagesValidator lastSeenMessages = new LastSeenMessagesValidator(20); - private final MessageSignatureCache messageSignatureCache = MessageSignatureCache.createDefault(); - private final FutureChain chatMessageChain; -- private boolean waitingForSwitchToConfig; -+ public volatile boolean waitingForSwitchToConfig; // Folia - rewrite login process - fix bad ordering of this field write + public - private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length - -+ // Folia start - region threading -+ public net.minecraft.world.level.ChunkPos disconnectPos; -+ private static final java.util.concurrent.atomic.AtomicLong DISCONNECT_TICKET_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); -+ public static final net.minecraft.server.level.TicketType DISCONNECT_TICKET = net.minecraft.server.level.TicketType.create("disconnect_ticket", Long::compareTo); -+ public final Long disconnectTicketId = Long.valueOf(DISCONNECT_TICKET_ID_GENERATOR.getAndIncrement()); -+ // Folia end - region threading -+ - public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie cookie) { - super(server, connection, cookie, player); // CraftBukkit - this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection()); -@@ -328,6 +335,12 @@ public class ServerGamePacketListenerImpl - - @Override - public void tick() { -+ // Folia start - region threading -+ this.keepConnectionAlive(); -+ if (this.processedDisconnect || this.player.wonGame) { -+ return; -+ } -+ // Folia end - region threading - if (this.ackBlockChangesUpTo > -1) { - this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo)); - this.ackBlockChangesUpTo = -1; -@@ -376,7 +389,7 @@ public class ServerGamePacketListenerImpl - this.aboveGroundVehicleTickCount = 0; - } - -- this.keepConnectionAlive(); -+ // Folia - region threading - moved to beginning of method - this.chatSpamThrottler.tick(); - this.dropSpamThrottler.tick(); - this.tabSpamThrottler.tick(); // Paper - configurable tab spam limits -@@ -412,6 +425,19 @@ public class ServerGamePacketListenerImpl - this.lastGoodX = this.player.getX(); - this.lastGoodY = this.player.getY(); - this.lastGoodZ = this.player.getZ(); -+ // Folia start - support vehicle teleportations -+ this.lastVehicle = this.player.getRootVehicle(); -+ if (this.lastVehicle != this.player && this.lastVehicle.getControllingPassenger() == this.player) { -+ this.vehicleFirstGoodX = this.lastVehicle.getX(); -+ this.vehicleFirstGoodY = this.lastVehicle.getY(); -+ this.vehicleFirstGoodZ = this.lastVehicle.getZ(); -+ this.vehicleLastGoodX = this.lastVehicle.getX(); -+ this.vehicleLastGoodY = this.lastVehicle.getY(); -+ this.vehicleLastGoodZ = this.lastVehicle.getZ(); -+ } else { -+ this.lastVehicle = null; -+ } -+ // Folia end - support vehicle teleportations - } - - @Override -@@ -519,9 +545,10 @@ public class ServerGamePacketListenerImpl - // Paper end - fix large move vectors killing the server - - // CraftBukkit start - handle custom speeds and skipped ticks -- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; -+ int currTick = (int)(Util.getMillis() / 50); // Folia - region threading -+ this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading - this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); -- this.lastTick = (int) (System.currentTimeMillis() / 50); -+ this.lastTick = (int) currTick; // Folia - region threading - - ++this.receivedMovePacketCount; - int i = this.receivedMovePacketCount - this.knownMovePacketCount; -@@ -588,7 +615,7 @@ public class ServerGamePacketListenerImpl - } - - rootVehicle.absMoveTo(d, d1, d2, f, f1); -- this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit -+ //this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - move to repositionAllPassengers - // Paper start - optimise out extra getCubes - boolean teleportBack = flag2; // violating this is always a fail - if (!teleportBack) { -@@ -600,11 +627,19 @@ public class ServerGamePacketListenerImpl - } - if (teleportBack) { // Paper end - optimise out extra getCubes - rootVehicle.absMoveTo(x, y, z, f, f1); -- this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit -+ //this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - not needed, the player is no longer updated - this.send(ClientboundMoveVehiclePacket.fromEntity(rootVehicle)); - return; - } - -+ // Folia start - move to positionRider -+ // this correction is required on folia since we move the connection tick to the beginning of the server -+ // tick, which would make any desync here visible -+ // this will correctly update the passenger positions for all mounted entities -+ // this prevents desync and ensures that all passengers have the correct rider-adjusted position -+ rootVehicle.repositionAllPassengers(false); -+ // Folia end - move to positionRider -+ - // CraftBukkit start - fire PlayerMoveEvent - org.bukkit.entity.Player player = this.getCraftPlayer(); - if (!this.hasMoved) { -@@ -635,7 +670,7 @@ public class ServerGamePacketListenerImpl - - // If the event is cancelled we move the player back to their old location. - if (event.isCancelled()) { -- this.teleport(from); -+ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading - return; - } - -@@ -643,7 +678,7 @@ public class ServerGamePacketListenerImpl - // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. - // We only do this if the Event was not cancelled. - if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { -- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); -+ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading - return; - } - -@@ -817,7 +852,7 @@ public class ServerGamePacketListenerImpl - } - - // This needs to be on main -- this.server.scheduleOnMain(() -> this.sendServerSuggestions(packet, stringReader)); -+ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> this.sendServerSuggestions(packet, stringReader), null, 1L); // Folia - region threading - } else if (!completions.isEmpty()) { - final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packet.getCommand(), stringReader.getTotalLength()); - final com.mojang.brigadier.suggestion.SuggestionsBuilder builder = builder0.createOffset(builder0.getInput().lastIndexOf(' ') + 1); -@@ -1200,11 +1235,11 @@ public class ServerGamePacketListenerImpl - } - // Paper end - Book size limits - // CraftBukkit start -- if (this.lastBookTick + 20 > MinecraftServer.currentTick) { -+ if (this.lastBookTick + 20 > this.lastTick) { // Folia - region threading - this.disconnectAsync(Component.literal("Book edited too quickly!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - add proper async disconnect - return; - } -- this.lastBookTick = MinecraftServer.currentTick; -+ this.lastBookTick = this.lastTick; // Folia - region threading - // CraftBukkit end - int slot = packet.slot(); - if (Inventory.isHotbarSlot(slot) || slot == 40) { -@@ -1215,7 +1250,22 @@ public class ServerGamePacketListenerImpl - Consumer> consumer = optional.isPresent() - ? texts -> this.signBook(texts.get(0), texts.subList(1, texts.size()), slot) - : texts -> this.updateBookContents(texts, slot); -- this.filterTextPacket(list).thenAcceptAsync(consumer, this.server); -+ // Folia start - region threading -+ this.filterTextPacket(list).thenAcceptAsync( -+ consumer, -+ (Runnable run) -> { -+ this.player.getBukkitEntity().taskScheduler.schedule( -+ (player) -> { -+ run.run(); -+ }, -+ null, 1L); -+ } -+ ).whenComplete((Object res, Throwable thr) -> { -+ if (thr != null) { -+ LOGGER.error("Failed to handle book update packet", thr); -+ } -+ }); -+ // Folia end - region threading - } - } - -@@ -1341,9 +1391,10 @@ public class ServerGamePacketListenerImpl - int i = this.receivedMovePacketCount - this.knownMovePacketCount; - - // CraftBukkit start - handle custom speeds and skipped ticks -- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; -+ int currTick = (int)(Util.getMillis() / 50); // Folia - region threading -+ this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading - this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); -- this.lastTick = (int) (System.currentTimeMillis() / 50); -+ this.lastTick = (int) currTick; // Folia - region threading - - if (i > Math.max(this.allowedPlayerTicks, 5)) { - LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i); -@@ -1532,7 +1583,7 @@ public class ServerGamePacketListenerImpl - - // If the event is cancelled we move the player back to their old location. - if (event.isCancelled()) { -- this.teleport(from); -+ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading - return; - } - -@@ -1540,7 +1591,7 @@ public class ServerGamePacketListenerImpl - // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. - // We only do this if the Event was not cancelled. - if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { -- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); -+ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading - return; - } - -@@ -1799,9 +1850,9 @@ public class ServerGamePacketListenerImpl - if (!this.player.isSpectator()) { - // limit how quickly items can be dropped - // If the ticks aren't the same then the count starts from 0 and we update the lastDropTick. -- if (this.lastDropTick != MinecraftServer.currentTick) { -+ if (this.lastDropTick != io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - region threading - this.dropCount = 0; -- this.lastDropTick = MinecraftServer.currentTick; -+ this.lastDropTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading - } else { - // Else we increment the drop count and check the amount. - this.dropCount++; -@@ -1829,7 +1880,7 @@ public class ServerGamePacketListenerImpl - case ABORT_DESTROY_BLOCK: - case STOP_DESTROY_BLOCK: - // Paper start - Don't allow digging into unloaded chunks -- if (this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null) { -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), pos.getX() >> 4, pos.getZ() >> 4, 8) || this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null) { // Folia - region threading - don't destroy blocks not owned - this.player.connection.ackBlockChangesUpTo(packet.getSequence()); - return; - } -@@ -1911,7 +1962,7 @@ public class ServerGamePacketListenerImpl - } - // Paper end - improve distance check - BlockPos blockPos = hitResult.getBlockPos(); -- if (this.player.canInteractWithBlock(blockPos, 1.0)) { -+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), blockPos.getX() >> 4, blockPos.getZ() >> 4, 8) && this.player.canInteractWithBlock(blockPos, 1.0)) { // Folia - do not allow players to interact with blocks outside the current region - Vec3 vec3 = location.subtract(Vec3.atCenterOf(blockPos)); - double d = 1.0000001; - if (Math.abs(vec3.x()) < 1.0000001 && Math.abs(vec3.y()) < 1.0000001 && Math.abs(vec3.z()) < 1.0000001) { -@@ -2032,7 +2083,7 @@ public class ServerGamePacketListenerImpl - for (ServerLevel serverLevel : this.server.getAllLevels()) { - Entity entity = packet.getEntity(serverLevel); - if (entity != null) { -- this.player.teleportTo(serverLevel, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit -+ io.papermc.paper.threadedregions.TeleportUtils.teleport(this.player, false, entity, null, null, Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null); // Folia - region threading - return; - } - } -@@ -2064,7 +2115,7 @@ public class ServerGamePacketListenerImpl - } - // CraftBukkit end - LOGGER.info("{} lost connection: {}", this.player.getName().getString(), details.reason().getString()); -- this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent -+ if (!this.waitingForSwitchToConfig) this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent // Folia - region threading - super.onDisconnect(details, quitMessage); // Paper - Fix kick event leave message not being sent - } - -@@ -2073,6 +2124,8 @@ public class ServerGamePacketListenerImpl - this.removePlayerFromWorld(null); - } - -+ public boolean hackSwitchingConfig; // Folia - rewrite login process -+ - private void removePlayerFromWorld(@Nullable net.kyori.adventure.text.Component quitMessage) { - // Paper end - Fix kick event leave message not being sent - this.chatMessageChain.close(); -@@ -2086,6 +2139,8 @@ public class ServerGamePacketListenerImpl - this.player.disconnect(); - // Paper start - Adventure - quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used -+ if (!this.hackSwitchingConfig) this.disconnectPos = this.player.chunkPosition(); // Folia - region threading - note: only set after removing, since it can tick the player -+ if (!this.hackSwitchingConfig) this.player.serverLevel().chunkSource.addTicketAtLevel(DISCONNECT_TICKET, this.disconnectPos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, this.disconnectTicketId); // Folia - region threading - force chunk to be loaded so that the region is not lost - if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) { - this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false); - // Paper end - Adventure -@@ -2324,7 +2379,7 @@ public class ServerGamePacketListenerImpl - this.player.resetLastActionTime(); - // CraftBukkit start - if (sync) { -- this.server.execute(handler); -+ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> handler.run(), null, 1L); // Folia - region threading - } else { - handler.run(); - } -@@ -2379,7 +2434,7 @@ public class ServerGamePacketListenerImpl - String originalFormat = event.getFormat(), originalMessage = event.getMessage(); - this.cserver.getPluginManager().callEvent(event); - -- if (PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { -+ if (false && PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading - // Evil plugins still listening to deprecated event - final PlayerChatEvent queueEvent = new PlayerChatEvent(player, event.getMessage(), event.getFormat(), event.getRecipients()); - queueEvent.setCancelled(event.isCancelled()); -@@ -2476,6 +2531,7 @@ public class ServerGamePacketListenerImpl - if (rawMessage.isEmpty()) { - LOGGER.warn("{} tried to send an empty message", this.player.getScoreboardName()); - } else if (this.getCraftPlayer().isConversing()) { -+ if (true) throw new UnsupportedOperationException(); // Folia - region threading - final String conversationInput = rawMessage; - this.server.processQueue.add(() -> ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput)); - } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { // Re-add "Command Only" flag check -@@ -2701,8 +2757,25 @@ public class ServerGamePacketListenerImpl - // Spigot end - - public void switchToConfig() { -- this.waitingForSwitchToConfig = true; -+ // Folia start - rewrite login process -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.player, "Cannot switch config off-main"); -+ if (io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { -+ throw new IllegalStateException("Cannot switch config while on global tick thread"); -+ } -+ // Folia end - rewrite login process -+ // Folia start - rewrite login process - fix bad ordering of this field write - move after removed from world -+ // the field write ordering is bad as it allows the client to send the response packet before the player is -+ // removed from the world -+ // Folia end - rewrite login process - fix bad ordering of this field write - move after removed from world -+ try { // Folia - rewrite login process - move connection ownership to global region -+ this.hackSwitchingConfig = true; // Folia - rewrite login process - avoid adding logout ticket here and retiring scheduler - this.removePlayerFromWorld(); -+ } finally { // Folia start - rewrite login process - move connection ownership to global region -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.player.serverLevel().getCurrentWorldData(); -+ worldData.connections.remove(this.connection); -+ // once waitingForSwitchToConfig is set, the global tick thread will own the connection -+ } // Folia end - rewrite login process - move connection ownership to global region -+ this.waitingForSwitchToConfig = true; // Folia - rewrite login process - fix bad ordering of this field write - moved down - this.send(ClientboundStartConfigurationPacket.INSTANCE); - this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); - } -@@ -2727,7 +2800,7 @@ public class ServerGamePacketListenerImpl - // Spigot end - this.player.resetLastActionTime(); - this.player.setShiftKeyDown(packet.isUsingSecondaryAction()); -- if (target != null) { -+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(target) && target != null) { // Folia - region threading - do not allow interaction of entities outside the current region - if (!serverLevel.getWorldBorder().isWithinBounds(target.blockPosition())) { - return; - } -@@ -2859,6 +2932,12 @@ public class ServerGamePacketListenerImpl - switch (action) { - case PERFORM_RESPAWN: - if (this.player.wonGame) { -+ // Folia start - region threading -+ if (true) { -+ this.player.exitEndCredits(); -+ return; -+ } -+ // Folia end - region threading - this.player.wonGame = false; - this.player = this.server.getPlayerList().respawn(this.player, true, Entity.RemovalReason.CHANGED_DIMENSION, RespawnReason.END_PORTAL); // CraftBukkit - this.resetPosition(); -@@ -2868,6 +2947,17 @@ public class ServerGamePacketListenerImpl - return; - } - -+ // Folia start - region threading -+ if (true) { -+ this.player.respawn((ServerPlayer player) -> { -+ if (ServerGamePacketListenerImpl.this.server.isHardcore()) { -+ ServerGamePacketListenerImpl.this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent -+ ((GameRules.BooleanValue) ServerGamePacketListenerImpl.this.player.serverLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, ServerGamePacketListenerImpl.this.player.serverLevel()); // CraftBukkit - per-world -+ } -+ }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.DEATH); -+ return; -+ } -+ // Folia end - region threading - this.player = this.server.getPlayerList().respawn(this.player, false, Entity.RemovalReason.KILLED, RespawnReason.DEATH); // CraftBukkit - this.resetPosition(); - if (this.server.isHardcore()) { -@@ -3413,7 +3503,21 @@ public class ServerGamePacketListenerImpl - } - List list = Stream.of(lines).map(ChatFormatting::stripFormatting).collect(Collectors.toList()); - // Paper end - Limit client sign length -- this.filterTextPacket(list).thenAcceptAsync(list1 -> this.updateSignText(packet, (List)list1), this.server); -+ // Folia start - region threading -+ this.filterTextPacket(list).thenAcceptAsync((list1) -> { -+ this.updateSignText(packet, (List)list1); -+ }, (Runnable run) -> { -+ this.player.getBukkitEntity().taskScheduler.schedule( -+ (player) -> { -+ run.run(); -+ }, -+ null, 1L); -+ }).whenComplete((Object res, Throwable thr) -> { -+ if (thr != null) { -+ LOGGER.error("Failed to handle sign update packet", thr); -+ } -+ }); -+ // Folia end - region threading - } - - private void updateSignText(ServerboundSignUpdatePacket packet, List filteredText) { -diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -index 6689aeacf50d1498e8d23cce696fb4fecbd1cf39..6173f704b0d093813ec67eb231c75be49a462e7d 100644 ---- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -@@ -111,7 +111,11 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, - // Paper end - Do not allow logins while the server is shutting down - - if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) { -- if (this.connection.isConnected()) { // Paper - prevent logins to be processed even though disconnect was called -+ // Folia start - region threading - rewrite login process -+ String name = this.authenticatedProfile.getName(); -+ java.util.UUID uniqueId = this.authenticatedProfile.getId(); -+ if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) { -+ // Folia end - region threading - rewrite login process - this.verifyLoginAndFinishConnectionSetup(Objects.requireNonNull(this.authenticatedProfile)); - } // Paper - prevent logins to be processed even though disconnect was called - } -@@ -250,7 +254,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, - ); - } - -- boolean flag = playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference -+ boolean flag = false && playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference // Folia - rewrite login process - always false here - if (flag) { - this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT; - } else { -@@ -362,7 +366,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, - uniqueId = gameprofile.getId(); - // Paper end - Add more fields to AsyncPlayerPreLoginEvent - -- if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { -+ if (false && PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading - final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId); - if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) { - event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure -diff --git a/net/minecraft/server/players/BanListEntry.java b/net/minecraft/server/players/BanListEntry.java -index e111adec2116f922fe67ee434635e50c60dad15c..851d3ae5d37541e6455b83b3300d630e8f6d5c83 100644 ---- a/net/minecraft/server/players/BanListEntry.java -+++ b/net/minecraft/server/players/BanListEntry.java -@@ -9,7 +9,7 @@ import javax.annotation.Nullable; - import net.minecraft.network.chat.Component; - - public abstract class BanListEntry extends StoredUserEntry { -- public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT); -+ public static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)); // Folia - region threading - SDF is not thread-safe - public static final String EXPIRES_NEVER = "forever"; - protected final Date created; - protected final String source; -@@ -30,7 +30,7 @@ public abstract class BanListEntry extends StoredUserEntry { - - Date date; - try { -- date = entryData.has("created") ? DATE_FORMAT.parse(entryData.get("created").getAsString()) : new Date(); -+ date = entryData.has("created") ? DATE_FORMAT.get().parse(entryData.get("created").getAsString()) : new Date(); // Folia - region threading - SDF is not thread-safe - } catch (ParseException var7) { - date = new Date(); - } -@@ -40,7 +40,7 @@ public abstract class BanListEntry extends StoredUserEntry { - - Date date1; - try { -- date1 = entryData.has("expires") ? DATE_FORMAT.parse(entryData.get("expires").getAsString()) : null; -+ date1 = entryData.has("expires") ? DATE_FORMAT.get().parse(entryData.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe - } catch (ParseException var6) { - date1 = null; - } -@@ -75,9 +75,9 @@ public abstract class BanListEntry extends StoredUserEntry { - - @Override - protected void serialize(JsonObject data) { -- data.addProperty("created", DATE_FORMAT.format(this.created)); -+ data.addProperty("created", DATE_FORMAT.get().format(this.created)); // Folia - region threading - SDF is not thread-safe - data.addProperty("source", this.source); -- data.addProperty("expires", this.expires == null ? "forever" : DATE_FORMAT.format(this.expires)); -+ data.addProperty("expires", this.expires == null ? "forever" : DATE_FORMAT.get().format(this.expires)); // Folia - region threading - SDF is not thread-safe - data.addProperty("reason", this.reason); - } - -@@ -86,7 +86,7 @@ public abstract class BanListEntry extends StoredUserEntry { - Date expires = null; - - try { -- expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.parse(jsonobject.get("expires").getAsString()) : null; -+ expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(jsonobject.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe - } catch (ParseException ex) { - // Guess we don't have a date - } -diff --git a/net/minecraft/server/players/OldUsersConverter.java b/net/minecraft/server/players/OldUsersConverter.java -index 7dbcd9d96f052bb10127ad2b061154c23cc9ffd4..20d895ed04cd2263560f91ef38dda6aa866bc603 100644 ---- a/net/minecraft/server/players/OldUsersConverter.java -+++ b/net/minecraft/server/players/OldUsersConverter.java -@@ -469,7 +469,7 @@ public class OldUsersConverter { - static Date parseDate(String input, Date defaultValue) { - Date date; - try { -- date = BanListEntry.DATE_FORMAT.parse(input); -+ date = BanListEntry.DATE_FORMAT.get().parse(input); // Folia - region threading - SDF is not thread-safe - } catch (ParseException var4) { - date = defaultValue; - } -diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java -index 03feaf0adb8ee87e33744a4615dc2507a02f92d7..65835fa09cdcd3bb158025f7d8b3cb29ac8b2549 100644 ---- a/net/minecraft/server/players/PlayerList.java -+++ b/net/minecraft/server/players/PlayerList.java -@@ -110,10 +110,10 @@ public abstract class PlayerList { - public static final Component DUPLICATE_LOGIN_DISCONNECT_MESSAGE = Component.translatable("multiplayer.disconnect.duplicate_login"); - private static final Logger LOGGER = LogUtils.getLogger(); - private static final int SEND_PLAYER_INFO_INTERVAL = 600; -- private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); -+ private static final ThreadLocal BAN_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z")); // Folia - region threading - SDF is not thread-safe - private final MinecraftServer server; - public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety -- private final Map playersByUUID = Maps.newHashMap(); -+ private final Map playersByUUID = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! - private final UserBanList bans = new UserBanList(USERBANLIST_FILE); - private final IpBanList ipBans = new IpBanList(IPBANLIST_FILE); - private final ServerOpList ops = new ServerOpList(OPLIST_FILE); -@@ -137,6 +137,60 @@ public abstract class PlayerList { - private final Map playersByName = new java.util.HashMap<>(); - public @Nullable String collideRuleTeamName; // Paper - Configurable player collision - -+ // Folia start - region threading -+ private final Object connectionsStateLock = new Object(); -+ private final Map connectionByName = new java.util.HashMap<>(); -+ private final Map connectionById = new java.util.HashMap<>(); -+ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet usersCountedAgainstLimit = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); -+ -+ public boolean pushPendingJoin(String userName, UUID byId, Connection conn) { -+ userName = userName.toLowerCase(java.util.Locale.ROOT); -+ Connection conflictingName, conflictingId; -+ synchronized (this.connectionsStateLock) { -+ conflictingName = this.connectionByName.get(userName); -+ conflictingId = this.connectionById.get(byId); -+ -+ if (conflictingName == null && conflictingId == null) { -+ this.connectionByName.put(userName, conn); -+ this.connectionById.put(byId, conn); -+ } -+ } -+ -+ Component message = Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]); -+ -+ if (conflictingId != null || conflictingName != null) { -+ if (conflictingName != null && conflictingName.isPlayerConnected()) { -+ conflictingName.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); -+ } -+ if (conflictingName != conflictingId && conflictingId != null && conflictingId.isPlayerConnected()) { -+ conflictingId.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); -+ } -+ } -+ -+ return conflictingName == null && conflictingId == null; -+ } -+ -+ public void removeConnection(String userName, UUID byId, Connection conn) { -+ userName = userName.toLowerCase(java.util.Locale.ROOT); -+ synchronized (this.connectionsStateLock) { -+ this.connectionByName.remove(userName, conn); -+ this.connectionById.remove(byId, conn); -+ this.usersCountedAgainstLimit.remove(conn); -+ } -+ } -+ -+ private boolean countConnection(Connection conn, int limit) { -+ synchronized (this.connectionsStateLock) { -+ int count = this.usersCountedAgainstLimit.size(); -+ if (count >= limit) { -+ return false; -+ } -+ this.usersCountedAgainstLimit.add(conn); -+ return true; -+ } -+ } -+ // Folia end - region threading -+ - public PlayerList(MinecraftServer server, LayeredRegistryAccess registries, PlayerDataStorage playerIo, int maxPlayers) { - this.cserver = server.server = new org.bukkit.craftbukkit.CraftServer((net.minecraft.server.dedicated.DedicatedServer) server, this); - server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper -@@ -149,7 +203,7 @@ public abstract class PlayerList { - - abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor - -- public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie) { -+ public void loadSpawnForNewPlayer(final Connection connection, final ServerPlayer player, final CommonListenerCookie cookie, org.apache.commons.lang3.mutable.MutableObject data, org.apache.commons.lang3.mutable.MutableObject lastKnownName, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { // Folia - region threading - rewrite login process - player.isRealPlayer = true; // Paper - player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed - GameProfile gameProfile = player.getGameProfile(); -@@ -221,17 +275,41 @@ public abstract class PlayerList { - player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login - // Paper start - reset to main world spawn if first spawn or invalid world - } -+ // Folia start - region threading - rewrite login process -+ // must write to these before toComplete is invoked -+ data.setValue(optional.orElse(null)); -+ lastKnownName.setValue(string); -+ // Folia end - region threading - rewrite login process - if (optional.isEmpty() || invalidPlayerWorld[0]) { - // Paper end - reset to main world spawn if first spawn or invalid world -- player.moveTo(player.adjustSpawnLocation(serverLevel, serverLevel.getSharedSpawnPos()).getBottomCenter(), serverLevel.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored -+ ServerPlayer.fudgeSpawnLocation(serverLevel, player, toComplete); // Paper - MC-200092 - fix first spawn pos yaw being ignored // Folia - region threading -+ } else { -+ serverLevel.loadChunksForMoveAsync( -+ player.getBoundingBox(), -+ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, -+ (c) -> { -+ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(serverLevel, player.position())); -+ } -+ ); - } -+ // Folia end - region threading - rewrite login process - // Paper end - Entity#getEntitySpawnReason -+ // Folia start - region threading - rewrite login process -+ return; -+ } -+ // optional -> player data -+ // s -> last known name -+ public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie, Optional optional, String string, org.bukkit.Location selectedSpawn) { -+ ServerLevel serverLevel = ((org.bukkit.craftbukkit.CraftWorld)selectedSpawn.getWorld()).getHandle(); -+ player.setPosRaw(selectedSpawn.getX(), selectedSpawn.getY(), selectedSpawn.getZ()); -+ player.lastSave = System.nanoTime(); // changed to nanoTime -+ // Folia end - region threading - rewrite login process - player.setServerLevel(serverLevel); - String loggableAddress = connection.getLoggableAddress(this.server.logIPs()); - // Spigot start - spawn location event - org.bukkit.entity.Player spawnPlayer = player.getBukkitEntity(); - org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation()); -- this.cserver.getPluginManager().callEvent(ev); -+ //this.cserver.getPluginManager().callEvent(ev); // Folia - region threading - TODO WTF TO DO WITH THIS EVENT? - - org.bukkit.Location loc = ev.getSpawnLocation(); - serverLevel = ((org.bukkit.craftbukkit.CraftWorld) loc.getWorld()).getHandle(); -@@ -254,6 +332,11 @@ public abstract class PlayerList { - LevelData levelData = serverLevel.getLevelData(); - player.loadGameTypes(optional.orElse(null)); - ServerGamePacketListenerImpl serverGamePacketListenerImpl = new ServerGamePacketListenerImpl(this.server, connection, player, cookie); -+ // Folia start - rewrite login process -+ // only after setting the connection listener to game type, add the connection to this regions list -+ serverLevel.getCurrentWorldData().connections.add(connection); -+ // Folia end - rewrite login process -+ - connection.setupInboundProtocol( - GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), serverGamePacketListenerImpl - ); -@@ -287,7 +370,7 @@ public abstract class PlayerList { - this.sendPlayerPermissionLevel(player); - player.getStats().markAllDirty(); - player.getRecipeBook().sendInitialRecipeBook(player); -- this.updateEntireScoreboard(serverLevel.getScoreboard(), player); -+ //this.updateEntireScoreboard(serverLevel.getScoreboard(), player); // Folia - region threading - this.server.invalidateStatus(); - MutableComponent mutableComponent; - if (player.getGameProfile().getName().equalsIgnoreCase(string)) { -@@ -327,7 +410,7 @@ public abstract class PlayerList { - this.cserver.getPluginManager().callEvent(playerJoinEvent); - - if (!player.connection.isAcceptingMessages()) { -- return; -+ //return; // Folia - region threading - must still allow the player to connect, as we must add to chunk map before handling disconnect - } - - final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); -@@ -342,8 +425,7 @@ public abstract class PlayerList { - ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper - Add Listing API for Player - - final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - Use single player info update packet on join -- for (int i = 0; i < this.players.size(); ++i) { -- ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i); -+ for (ServerPlayer entityplayer1 : this.players) { // Folia - region threading - - if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) { - // Paper start - Add Listing API for Player -@@ -392,7 +474,7 @@ public abstract class PlayerList { - // Paper start - Configurable player collision; Add to collideRule team if needed - final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); - final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName); -- if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { -+ if (false && this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { // Folia - region threading - scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); - } - // Paper end - Configurable player collision -@@ -482,7 +564,7 @@ public abstract class PlayerList { - - protected void save(ServerPlayer player) { - if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit -- player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving -+ player.lastSave = System.nanoTime(); // Folia - region threading - changed to nanoTime tracking - this.playerIo.save(player); - ServerStatsCounter serverStatsCounter = player.getStats(); // CraftBukkit - if (serverStatsCounter != null) { -@@ -517,7 +599,7 @@ public abstract class PlayerList { - // CraftBukkit end - - // Paper start - Configurable player collision; Remove from collideRule team if needed -- if (this.collideRuleTeamName != null) { -+ if (false && this.collideRuleTeamName != null) { // Folia - region threading - final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); - final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); - if (player.getTeam() == team && team != null) { -@@ -566,7 +648,7 @@ public abstract class PlayerList { - } - - serverLevel.removePlayerImmediately(player, Entity.RemovalReason.UNLOADED_WITH_PLAYER); -- player.retireScheduler(); // Paper - Folia schedulers -+ //player.retireScheduler(); // Paper - Folia schedulers // Folia - region threading - move to onDisconnect of common packet listener - player.getAdvancements().stopListening(); - this.players.remove(player); - this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot -@@ -584,8 +666,7 @@ public abstract class PlayerList { - // CraftBukkit start - // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID()))); - ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())); -- for (int i = 0; i < this.players.size(); i++) { -- ServerPlayer otherPlayer = (ServerPlayer) this.players.get(i); -+ for (ServerPlayer otherPlayer : this.players) { // Folia - region threading - - if (otherPlayer.getBukkitEntity().canSee(player.getBukkitEntity())) { - otherPlayer.connection.send(packet); -@@ -609,19 +690,12 @@ public abstract class PlayerList { - - ServerPlayer entityplayer; - -- for (int i = 0; i < this.players.size(); ++i) { -- entityplayer = (ServerPlayer) this.players.get(i); -- if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameProfile.getName()))) { // Paper - validate usernames -- list.add(entityplayer); -- } -- } -+ // Folia - region threading - rewrite login process - moved to pushPendingJoin - - java.util.Iterator iterator = list.iterator(); - - while (iterator.hasNext()) { -- entityplayer = (ServerPlayer) iterator.next(); -- this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved -- entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause -+ // Folia - moved to pushPendingJoin - } - - // Instead of kicking then returning, we need to store the kick reason -@@ -641,7 +715,7 @@ public abstract class PlayerList { - MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned.reason", userBanListEntry.getReason()); - if (userBanListEntry.getExpires() != null) { - mutableComponent.append( -- Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.format(userBanListEntry.getExpires())) -+ Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.get().format(userBanListEntry.getExpires())) // Folia - region threading - SDF is not thread-safe - ); - } - -@@ -655,7 +729,7 @@ public abstract class PlayerList { - MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipBanListEntry.getReason()); - if (ipBanListEntry.getExpires() != null) { - mutableComponent.append( -- Component.translatable("multiplayer.disconnect.banned_ip.expiration", BAN_DATE_FORMAT.format(ipBanListEntry.getExpires())) -+ Component.translatable("multiplayer.disconnect.banned_ip.expiration", BAN_DATE_FORMAT.get().format(ipBanListEntry.getExpires())) // Folia - region threading - SDF is not thread-safe - ); - } - -@@ -665,7 +739,7 @@ public abstract class PlayerList { - // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile) - // ? Component.translatable("multiplayer.disconnect.server_full") - // : null; -- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)) { -+ if (!this.countConnection(loginlistener.connection, this.maxPlayers) && !this.canBypassPlayerLimit(gameProfile)) { // Folia - region threading - we control connection state here now async, not player list size - event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure - } - } -@@ -714,6 +788,11 @@ public abstract class PlayerList { - return this.respawn(player, keepInventory, reason, eventReason, null); - } - public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, org.bukkit.Location location) { -+ // Folia start - region threading -+ if (true) { -+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); -+ } -+ // Folia end - region threading - player.stopRiding(); // CraftBukkit - this.players.remove(player); - this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot -@@ -884,10 +963,10 @@ public abstract class PlayerList { - public void tick() { - if (++this.sendAllPlayerInfoIn > 600) { - // CraftBukkit start -- for (int i = 0; i < this.players.size(); ++i) { -- final ServerPlayer target = this.players.get(i); -+ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading -+ for (final ServerPlayer target : players) { // Folia - region threading - -- target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), com.google.common.collect.Collections2.filter(this.players, t -> target.getBukkitEntity().canSee(t.getBukkitEntity())))); -+ target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), com.google.common.collect.Collections2.filter(java.util.Arrays.asList(players),t -> target.getBukkitEntity().canSee(t.getBukkitEntity())))); // Folia - region threading - } - // CraftBukkit end - this.sendAllPlayerInfoIn = 0; -@@ -896,18 +975,17 @@ public abstract class PlayerList { - - // CraftBukkit start - add a world/entity limited version - public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) { -- for (int i = 0; i < this.players.size(); ++i) { -- ServerPlayer entityplayer = this.players.get(i); -+ for (ServerPlayer entityplayer : this.players) { // Folia - region threading - if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) { - continue; - } -- ((ServerPlayer) this.players.get(i)).connection.send(packet); -+ entityplayer.connection.send(packet); // Folia - region threading - } - } - - public void broadcastAll(Packet packet, Level world) { -- for (int i = 0; i < world.players().size(); ++i) { -- ((ServerPlayer) world.players().get(i)).connection.send(packet); -+ for (net.minecraft.world.entity.player.Player player : world.players()) { // Folia - region threading -+ ((ServerPlayer) player).connection.send(packet); // Folia - region threading - } - - } -@@ -944,8 +1022,7 @@ public abstract class PlayerList { - if (team == null) { - this.broadcastSystemMessage(message, false); - } else { -- for (int i = 0; i < this.players.size(); i++) { -- ServerPlayer serverPlayer = this.players.get(i); -+ for (ServerPlayer serverPlayer : this.players) { // Folia - region threading - if (serverPlayer.getTeam() != team) { - serverPlayer.sendSystemMessage(message); - } -@@ -954,10 +1031,11 @@ public abstract class PlayerList { - } - - public String[] getPlayerNamesArray() { -+ List players = new java.util.ArrayList<>(this.players); // Folia - region threading - String[] strings = new String[this.players.size()]; - -- for (int i = 0; i < this.players.size(); i++) { -- strings[i] = this.players.get(i).getGameProfile().getName(); -+ for (int i = 0; i < players.size(); i++) { // Folia - region threading -+ strings[i] = players.get(i).getGameProfile().getName(); // Folia - region threading - } - - return strings; -@@ -975,7 +1053,9 @@ public abstract class PlayerList { - this.ops.add(new ServerOpListEntry(profile, this.server.getOperatorUserPermissionLevel(), this.ops.canBypassPlayerLimit(profile))); - ServerPlayer player = this.getPlayer(profile.getId()); - if (player != null) { -- this.sendPlayerPermissionLevel(player); -+ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading -+ this.sendPlayerPermissionLevel(serverPlayer); // Folia - region threading -+ }, null, 1L); // Folia - region threading - } - } - -@@ -983,7 +1063,9 @@ public abstract class PlayerList { - this.ops.remove(profile); - ServerPlayer player = this.getPlayer(profile.getId()); - if (player != null) { -- this.sendPlayerPermissionLevel(player); -+ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading -+ this.sendPlayerPermissionLevel(serverPlayer); // Folia - region threading -+ }, null, 1L); // Folia - region threading - } - } - -@@ -1046,8 +1128,7 @@ public abstract class PlayerList { - } - - public void broadcast(@Nullable Player except, double x, double y, double z, double radius, ResourceKey dimension, Packet packet) { -- for (int i = 0; i < this.players.size(); i++) { -- ServerPlayer serverPlayer = this.players.get(i); -+ for (ServerPlayer serverPlayer : this.players) { // Folia - region threading - // CraftBukkit start - Test if player receiving packet can see the source of the packet - if (except != null && !serverPlayer.getBukkitEntity().canSee(except.getBukkitEntity())) { - continue; -@@ -1072,10 +1153,15 @@ public abstract class PlayerList { - public void saveAll(final int interval) { - io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main - int numSaved = 0; -- final long now = MinecraftServer.currentTick; -- for (int i = 0; i < this.players.size(); i++) { -- final ServerPlayer player = this.players.get(i); -- if (interval == -1 || now - player.lastSave >= interval) { -+ final long now = System.nanoTime(); // Folia - region threading -+ long timeInterval = (long)interval * io.papermc.paper.threadedregions.TickRegionScheduler.TIME_BETWEEN_TICKS; // Folia - region threading -+ for (final ServerPlayer player : this.players) { // Folia - region threading -+ // Folia start - region threading -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { -+ continue; -+ } -+ // Folia end - region threading -+ if (interval == -1 || now - player.lastSave >= timeInterval) { // Folia - region threading - this.save(player); - if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { - break; -@@ -1194,6 +1280,20 @@ public abstract class PlayerList { - } - - public void removeAll(boolean isRestarting) { -+ // Folia start - region threading -+ // just send disconnect packet, don't modify state -+ for (ServerPlayer player : this.players) { -+ final Component shutdownMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure -+ // CraftBukkit end -+ -+ player.connection.send(new net.minecraft.network.protocol.common.ClientboundDisconnectPacket(shutdownMessage), net.minecraft.network.PacketSendListener.thenRun(() -> { -+ player.connection.connection.disconnect(shutdownMessage); -+ })); -+ } -+ if (true) { -+ return; -+ } -+ // Folia end - region threading - // Paper end - // CraftBukkit start - disconnect safely - for (ServerPlayer player : this.players) { -@@ -1203,7 +1303,7 @@ public abstract class PlayerList { - // CraftBukkit end - - // Paper start - Configurable player collision; Remove collideRule team if it exists -- if (this.collideRuleTeamName != null) { -+ if (false && this.collideRuleTeamName != null) { // Folia - region threading - final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); - final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName); - if (team != null) scoreboard.removePlayerTeam(team); -diff --git a/net/minecraft/server/players/StoredUserList.java b/net/minecraft/server/players/StoredUserList.java -index d445e8f126f077d8419c52fa5436ea963a1a42a4..cabbc68f7cd5fd326c7ffd3b02b3ec4a4390f5b0 100644 ---- a/net/minecraft/server/players/StoredUserList.java -+++ b/net/minecraft/server/players/StoredUserList.java -@@ -97,6 +97,7 @@ public abstract class StoredUserList> { - } - - public void save() throws IOException { -+ synchronized (this) { // Folia - region threading - this.removeExpired(); // Paper - remove expired values before saving - JsonArray jsonArray = new JsonArray(); - this.map.values().stream().map(storedEntry -> Util.make(new JsonObject(), storedEntry::serialize)).forEach(jsonArray::add); -@@ -104,9 +105,11 @@ public abstract class StoredUserList> { - try (BufferedWriter writer = Files.newWriter(this.file, StandardCharsets.UTF_8)) { - GSON.toJson(jsonArray, GSON.newJsonWriter(writer)); - } -+ } // Folia - region threading - } - - public void load() throws IOException { -+ synchronized (this) { // Folia - region threading - if (this.file.exists()) { - try (BufferedReader reader = Files.newReader(this.file, StandardCharsets.UTF_8)) { - this.map.clear(); -@@ -131,5 +134,6 @@ public abstract class StoredUserList> { - } - // Spigot end - } -+ } // Folia - region threading - } - } -diff --git a/net/minecraft/util/SpawnUtil.java b/net/minecraft/util/SpawnUtil.java -index f6fad24af884c8a37723c57718cee0096443efe6..ef75a71aee2576254e2c0752cc759c9637af9d69 100644 ---- a/net/minecraft/util/SpawnUtil.java -+++ b/net/minecraft/util/SpawnUtil.java -@@ -83,7 +83,7 @@ public class SpawnUtil { - return Optional.of(mob); - } - -- mob.discard(null); // CraftBukkit - add Bukkit remove cause -+ //mob.discard(null); // CraftBukkit - add Bukkit remove cause // Folia - region threading - } - } - } -diff --git a/net/minecraft/world/RandomSequences.java b/net/minecraft/world/RandomSequences.java -index f8e93fe461794058a26c90510cbd7698fa43b8f7..c1a62556546b05f79dad37875993c19bf9bb7c67 100644 ---- a/net/minecraft/world/RandomSequences.java -+++ b/net/minecraft/world/RandomSequences.java -@@ -21,7 +21,7 @@ public class RandomSequences extends SavedData { - private int salt; - private boolean includeWorldSeed = true; - private boolean includeSequenceId = true; -- private final Map sequences = new Object2ObjectOpenHashMap<>(); -+ private final Map sequences = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - - public static SavedData.Factory factory(long seed) { - return new SavedData.Factory<>( -@@ -120,61 +120,61 @@ public class RandomSequences extends SavedData { - @Override - public RandomSource fork() { - RandomSequences.this.setDirty(); -- return this.random.fork(); -+ synchronized (this.random) { return this.random.fork(); } // Folia - region threading - } - - @Override - public PositionalRandomFactory forkPositional() { - RandomSequences.this.setDirty(); -- return this.random.forkPositional(); -+ synchronized (this.random) { return this.random.forkPositional(); } // Folia - region threading - } - - @Override - public void setSeed(long seed) { - RandomSequences.this.setDirty(); -- this.random.setSeed(seed); -+ synchronized (this.random) { this.random.setSeed(seed); } // Folia - region threading - } - - @Override - public int nextInt() { - RandomSequences.this.setDirty(); -- return this.random.nextInt(); -+ synchronized (this.random) { return this.random.nextInt(); } // Folia - region threading - } - - @Override - public int nextInt(int bound) { - RandomSequences.this.setDirty(); -- return this.random.nextInt(bound); -+ synchronized (this.random) { return this.random.nextInt(bound); } // Folia - region threading - } - - @Override - public long nextLong() { - RandomSequences.this.setDirty(); -- return this.random.nextLong(); -+ synchronized (this.random) { return this.random.nextLong(); } // Folia - region threading - } - - @Override - public boolean nextBoolean() { - RandomSequences.this.setDirty(); -- return this.random.nextBoolean(); -+ synchronized (this.random) { return this.random.nextBoolean(); } // Folia - region threading - } - - @Override - public float nextFloat() { - RandomSequences.this.setDirty(); -- return this.random.nextFloat(); -+ synchronized (this.random) { return this.random.nextFloat(); } // Folia - region threading - } - - @Override - public double nextDouble() { - RandomSequences.this.setDirty(); -- return this.random.nextDouble(); -+ synchronized (this.random) { return this.random.nextDouble(); } // Folia - region threading - } - - @Override - public double nextGaussian() { - RandomSequences.this.setDirty(); -- return this.random.nextGaussian(); -+ synchronized (this.random) { return this.random.nextGaussian(); } // Folia - region threading - } - - @Override -diff --git a/net/minecraft/world/damagesource/CombatTracker.java b/net/minecraft/world/damagesource/CombatTracker.java -index d3de87eaf0eb84af77165391c7b94085d425f21d..019d1435cfe7769ed85b40c1c19d2d271d96f913 100644 ---- a/net/minecraft/world/damagesource/CombatTracker.java -+++ b/net/minecraft/world/damagesource/CombatTracker.java -@@ -53,7 +53,7 @@ public class CombatTracker { - } - - private Component getMessageForAssistedFall(Entity entity, Component entityDisplayName, String hasWeaponTranslationKey, String noWeaponTranslationKey) { -- ItemStack itemStack = entity instanceof LivingEntity livingEntity ? livingEntity.getMainHandItem() : ItemStack.EMPTY; -+ ItemStack itemStack = entity instanceof LivingEntity livingEntity && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity) ? livingEntity.getMainHandItem() : ItemStack.EMPTY; // Folia - region threading - return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME) - ? Component.translatable(hasWeaponTranslationKey, this.mob.getDisplayName(), entityDisplayName, itemStack.getDisplayName()) - : Component.translatable(noWeaponTranslationKey, this.mob.getDisplayName(), entityDisplayName); -@@ -80,7 +80,7 @@ public class CombatTracker { - - @Nullable - private static Component getDisplayName(@Nullable Entity entity) { -- return entity == null ? null : entity.getDisplayName(); -+ return entity == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) ? null : entity.getDisplayName(); // Folia - region threading - } - - public Component getDeathMessage() { -diff --git a/net/minecraft/world/damagesource/DamageSource.java b/net/minecraft/world/damagesource/DamageSource.java -index 48c9b26e023ad236b0bcb6441e8aee8f107ae381..08779c641b3b8d8e2dcd734a4124a3c57a032cbd 100644 ---- a/net/minecraft/world/damagesource/DamageSource.java -+++ b/net/minecraft/world/damagesource/DamageSource.java -@@ -178,12 +178,12 @@ public class DamageSource { - if (this.causingEntity == null && this.directEntity == null) { - LivingEntity killCredit = livingEntity.getKillCredit(); - String string1 = string + ".player"; -- return killCredit != null -+ return killCredit != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(killCredit) - ? Component.translatable(string1, livingEntity.getDisplayName(), killCredit.getDisplayName()) - : Component.translatable(string, livingEntity.getDisplayName()); - } else { - Component component = this.causingEntity == null ? this.directEntity.getDisplayName() : this.causingEntity.getDisplayName(); -- ItemStack itemStack = this.causingEntity instanceof LivingEntity livingEntity1 ? livingEntity1.getMainHandItem() : ItemStack.EMPTY; -+ ItemStack itemStack = this.causingEntity instanceof LivingEntity livingEntity1 && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity1) ? livingEntity1.getMainHandItem() : ItemStack.EMPTY; // Folia - region threading - return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME) - ? Component.translatable(string + ".item", livingEntity.getDisplayName(), component, itemStack.getDisplayName()) - : Component.translatable(string, livingEntity.getDisplayName(), component); -diff --git a/net/minecraft/world/damagesource/FallLocation.java b/net/minecraft/world/damagesource/FallLocation.java -index a3c7d68469075bf8d33f2016149a181b0fb87e0e..73c581d3ee21d8fa96eae3e47afd6ce204e03160 100644 ---- a/net/minecraft/world/damagesource/FallLocation.java -+++ b/net/minecraft/world/damagesource/FallLocation.java -@@ -35,7 +35,7 @@ public record FallLocation(String id) { - @Nullable - public static FallLocation getCurrentFallLocation(LivingEntity entity) { - Optional lastClimbablePos = entity.getLastClimbablePos(); -- if (lastClimbablePos.isPresent()) { -+ if (lastClimbablePos.isPresent() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)entity.level(), lastClimbablePos.get())) { // Folia - region threading - BlockState blockState = entity.level().getBlockState(lastClimbablePos.get()); - return blockToFallLocation(blockState); - } else { -diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index 1d0151a042ed5de4e235ef0bdac1a0e8240e85e7..dded82842fb0fc8f4b176c69f37f07e8400b7605 100644 ---- a/net/minecraft/world/entity/Entity.java -+++ b/net/minecraft/world/entity/Entity.java -@@ -145,7 +145,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - // Paper start - Share random for entities to make them more random -- public static RandomSource SHARED_RANDOM = new RandomRandomSource(); -+ public static RandomSource SHARED_RANDOM = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading - // Paper start - replace random - private static final class RandomRandomSource extends ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom { - public RandomRandomSource() { -@@ -175,7 +175,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason - - public boolean collisionLoadChunks = false; // Paper -- private org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity; -+ private volatile org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity; // Folia - region threading - - public org.bukkit.craftbukkit.entity.CraftEntity getBukkitEntity() { - if (this.bukkitEntity == null) { -@@ -294,7 +294,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - private boolean hasGlowingTag; - private final Set tags = new io.papermc.paper.util.SizeLimitedSet<>(new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(), MAX_ENTITY_TAG_COUNT); // Paper - fully limit tag size - replace set impl - private final double[] pistonDeltas = new double[]{0.0, 0.0, 0.0}; -- private long pistonDeltasGameTime; -+ private long pistonDeltasGameTime = Long.MIN_VALUE; // Folia - region threading - private EntityDimensions dimensions; - private float eyeHeight; - public boolean isInPowderSnow; -@@ -525,6 +525,19 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - } - // Paper end - optimise entity tracker -+ // Folia start - region ticking -+ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { -+ if (this.activatedTick != Integer.MIN_VALUE) { -+ this.activatedTick += fromTickOffset; -+ } -+ if (this.activatedImmunityTick != Integer.MIN_VALUE) { -+ this.activatedImmunityTick += fromTickOffset; -+ } -+ if (this.pistonDeltasGameTime != Long.MIN_VALUE) { -+ this.pistonDeltasGameTime += fromRedstoneTimeOffset; -+ } -+ } -+ // Folia end - region ticking - - public Entity(EntityType entityType, Level level) { - this.type = entityType; -@@ -655,8 +668,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - // due to interactions on the client. - public void resendPossiblyDesyncedEntityData(net.minecraft.server.level.ServerPlayer player) { - if (player.getBukkitEntity().canSee(this.getBukkitEntity())) { -- ServerLevel world = (net.minecraft.server.level.ServerLevel)this.level(); -- net.minecraft.server.level.ChunkMap.TrackedEntity tracker = world == null ? null : world.getChunkSource().chunkMap.entityMap.get(this.getId()); -+ net.minecraft.server.level.ChunkMap.TrackedEntity tracker = this.moonrise$getTrackedEntity(); // Folia - region threading - if (tracker == null) { - return; - } -@@ -823,7 +835,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - public void postTick() { - // No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle - if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities -- this.handlePortal(); -+ //this.handlePortal(); // Folia - region threading - } - } - // CraftBukkit end -@@ -841,7 +853,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.boardingCooldown--; - } - -- if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick -+ //if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick // Folia - region threading - ONLY allow in postTick() - if (this.canSpawnSprintParticle()) { - this.spawnSprintParticle(); - } -@@ -1104,8 +1116,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } else { - this.wasOnFire = this.isOnFire(); - if (type == MoverType.PISTON) { -- this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper - EAR 2 -- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper - EAR 2 -+ this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading -+ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading - movement = this.limitPistonMovement(movement); - if (movement.equals(Vec3.ZERO)) { - return; -@@ -1404,7 +1416,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - if (pos.lengthSqr() <= 1.0E-7) { - return pos; - } else { -- long gameTime = this.level().getGameTime(); -+ long gameTime = this.level().getRedstoneGameTime(); // Folia - region threading - if (gameTime != this.pistonDeltasGameTime) { - Arrays.fill(this.pistonDeltas, 0.0); - this.pistonDeltasGameTime = gameTime; -@@ -3126,7 +3138,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.passengers = ImmutableList.copyOf(list); - } - -- this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); -+ if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); // Folia - region threading - do not fire game events for entities not added - } - } - -@@ -3174,7 +3186,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - passenger.boardingCooldown = 60; -- this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger); -+ if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger); // Folia - region threading - do not fire game events for entities not added - } - return true; // CraftBukkit - } -@@ -3258,7 +3270,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - } - -- protected void handlePortal() { -+ public boolean handlePortal() { // Folia - region threading - public, ret type -> boolean - if (this.level() instanceof ServerLevel serverLevel) { - this.processPortalCooldown(); - if (this.portalProcess != null) { -@@ -3266,21 +3278,20 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - ProfilerFiller profilerFiller = Profiler.get(); - profilerFiller.push("portal"); - this.setPortalCooldown(); -- TeleportTransition portalDestination = this.portalProcess.getPortalDestination(serverLevel, this); -- if (portalDestination != null) { -- ServerLevel level = portalDestination.newLevel(); -- if (this instanceof ServerPlayer // CraftBukkit - always call event for players -- || (level != null && (level.dimension() == serverLevel.dimension() || this.canTeleport(serverLevel, level)))) { // CraftBukkit -- this.teleport(portalDestination); -- } -+ // Folia start - region threading -+ try { -+ return this.portalProcess.portalAsync(serverLevel, this); -+ } finally { -+ profilerFiller.pop(); - } -- -- profilerFiller.pop(); -+ // Folia end - region threading - } else if (this.portalProcess.hasExpired()) { - this.portalProcess = null; - } - } - } -+ -+ return false; // Folia - region threading - } - - public int getDimensionChangingDelay() { -@@ -3420,6 +3431,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - - @Nullable - public PlayerTeam getTeam() { -+ // Folia start - region threading -+ if (true) { -+ return null; -+ } -+ // Folia end - region threading - if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper - Perf: Disable Scoreboards for non players by default - return this.level().getScoreboard().getPlayersTeam(this.getScoreboardName()); - } -@@ -3726,8 +3742,782 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.portalProcess = entity.portalProcess; - } - -+ // Folia start - region threading -+ public static class EntityTreeNode { -+ @Nullable -+ public EntityTreeNode parent; -+ public Entity root; -+ @Nullable -+ public EntityTreeNode[] passengers; -+ -+ public EntityTreeNode(EntityTreeNode parent, Entity root) { -+ this.parent = parent; -+ this.root = root; -+ } -+ -+ public EntityTreeNode(EntityTreeNode parent, Entity root, EntityTreeNode[] passengers) { -+ this.parent = parent; -+ this.root = root; -+ this.passengers = passengers; -+ } -+ -+ public List getFullTree() { -+ List ret = new java.util.ArrayList<>(); -+ ret.add(this); -+ -+ // this is just a BFS except we don't remove from head, we just advance down the list -+ for (int i = 0; i < ret.size(); ++i) { -+ EntityTreeNode node = ret.get(i); -+ -+ EntityTreeNode[] passengers = node.passengers; -+ if (passengers == null) { -+ continue; -+ } -+ for (EntityTreeNode passenger : passengers) { -+ ret.add(passenger); -+ } -+ } -+ -+ return ret; -+ } -+ -+ public void restore() { -+ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); -+ queue.add(this); -+ -+ EntityTreeNode curr; -+ while ((curr = queue.pollFirst()) != null) { -+ EntityTreeNode[] passengers = curr.passengers; -+ if (passengers == null) { -+ continue; -+ } -+ -+ List newPassengers = new java.util.ArrayList<>(); -+ for (EntityTreeNode passenger : passengers) { -+ newPassengers.add(passenger.root); -+ passenger.root.vehicle = curr.root; -+ } -+ -+ curr.root.passengers = ImmutableList.copyOf(newPassengers); -+ } -+ } -+ -+ public void addTracker() { -+ for (final EntityTreeNode node : this.getFullTree()) { -+ if (node.root.moonrise$getTrackedEntity() != null) { -+ for (final ServerPlayer player : node.root.level.getLocalPlayers()) { -+ node.root.moonrise$getTrackedEntity().updatePlayer(player); -+ } -+ } -+ } -+ } -+ -+ public void clearTracker() { -+ for (final EntityTreeNode node : this.getFullTree()) { -+ if (node.root.moonrise$getTrackedEntity() != null) { -+ node.root.moonrise$getTrackedEntity().moonrise$removeNonTickThreadPlayers(); -+ for (final ServerPlayer player : node.root.level.getLocalPlayers()) { -+ node.root.moonrise$getTrackedEntity().removePlayer(player); -+ } -+ } -+ } -+ } -+ -+ public void adjustRiders(boolean teleport) { -+ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); -+ queue.add(this); -+ -+ EntityTreeNode curr; -+ while ((curr = queue.pollFirst()) != null) { -+ EntityTreeNode[] passengers = curr.passengers; -+ if (passengers == null) { -+ continue; -+ } -+ -+ for (EntityTreeNode passenger : passengers) { -+ curr.root.positionRider(passenger.root, teleport ? Entity::moveTo : Entity::setPos); -+ } -+ } -+ } -+ } -+ -+ public void repositionAllPassengers(boolean teleport) { -+ this.makePassengerTree().adjustRiders(teleport); -+ } -+ -+ protected EntityTreeNode makePassengerTree() { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot read passengers off of the main thread"); -+ -+ EntityTreeNode root = new EntityTreeNode(null, this); -+ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); -+ queue.add(root); -+ EntityTreeNode curr; -+ while ((curr = queue.pollFirst()) != null) { -+ Entity vehicle = curr.root; -+ List passengers = vehicle.passengers; -+ if (passengers.isEmpty()) { -+ continue; -+ } -+ -+ EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; -+ curr.passengers = treePassengers; -+ -+ for (int i = 0; i < passengers.size(); ++i) { -+ Entity passenger = passengers.get(i); -+ queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); -+ } -+ } -+ -+ return root; -+ } -+ -+ protected EntityTreeNode detachPassengers() { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot adjust passengers/vehicle off of the main thread"); -+ -+ EntityTreeNode root = new EntityTreeNode(null, this); -+ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); -+ queue.add(root); -+ EntityTreeNode curr; -+ while ((curr = queue.pollFirst()) != null) { -+ Entity vehicle = curr.root; -+ List passengers = vehicle.passengers; -+ if (passengers.isEmpty()) { -+ continue; -+ } -+ -+ vehicle.passengers = ImmutableList.of(); -+ -+ EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; -+ curr.passengers = treePassengers; -+ -+ for (int i = 0; i < passengers.size(); ++i) { -+ Entity passenger = passengers.get(i); -+ passenger.vehicle = null; -+ queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); -+ } -+ } -+ -+ return root; -+ } -+ -+ /** -+ * This flag will perform an async load on the chunks determined by -+ * the entity's bounding box before teleporting the entity. -+ */ -+ public static final long TELEPORT_FLAG_LOAD_CHUNK = 1L << 0; -+ /** -+ * This flag requires the entity being teleported to be a root vehicle. -+ * Thus, if you want to teleport a non-root vehicle, you must dismount -+ * the target entity before calling teleport, otherwise the -+ * teleport will be refused. -+ */ -+ public static final long TELEPORT_FLAG_TELEPORT_PASSENGERS = 1L << 1; -+ /** -+ * The flag will dismount any passengers and dismout from the current vehicle -+ * to teleport if and only if dismounting would result in the teleport being allowed. -+ */ -+ public static final long TELEPORT_FLAG_UNMOUNT = 1L << 2; -+ -+ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { -+ destination.addDuringTeleport(this); -+ } -+ -+ protected final void placeInAsync(ServerLevel originWorld, ServerLevel destination, long teleportFlags, -+ EntityTreeNode passengerTree, java.util.function.Consumer teleportComplete) { -+ Vec3 pos = this.position(); -+ ChunkPos posChunk = new ChunkPos( -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos) -+ ); -+ -+ // ensure the region is always ticking in case of a shutdown -+ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region -+ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); -+ originWorld.chunkSource.addTicketAtLevel( -+ TicketType.TELEPORT_HOLD_TICKET, posChunk, -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, -+ teleportHoldId -+ ); -+ final ServerLevel.PendingTeleport pendingTeleport = new ServerLevel.PendingTeleport(passengerTree, pos); -+ destination.pushPendingTeleport(pendingTeleport); -+ -+ Runnable scheduleEntityJoin = () -> { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ destination, -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos), -+ () -> { -+ if (!destination.removePendingTeleport(pendingTeleport)) { -+ // shutdown logic placed the entity already, and we are shutting down - do nothing to ensure -+ // we do not produce any errors here -+ return; -+ } -+ originWorld.chunkSource.removeTicketAtLevel( -+ TicketType.TELEPORT_HOLD_TICKET, posChunk, -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, -+ teleportHoldId -+ ); -+ List fullTree = passengerTree.getFullTree(); -+ for (EntityTreeNode node : fullTree) { -+ node.root.placeSingleSync(originWorld, destination, node, teleportFlags); -+ } -+ -+ // restore passenger tree -+ passengerTree.restore(); -+ passengerTree.adjustRiders(true); -+ -+ // invoke post dimension change now -+ for (EntityTreeNode node : fullTree) { -+ node.root.postChangeDimension(); -+ } -+ -+ if (teleportComplete != null) { -+ teleportComplete.accept(Entity.this); -+ } -+ } -+ ); -+ }; -+ -+ if ((teleportFlags & TELEPORT_FLAG_LOAD_CHUNK) != 0L) { -+ destination.loadChunksForMoveAsync( -+ this.getBoundingBox(), ca.spottedleaf.concurrentutil.util.Priority.HIGHER, -+ (chunkList) -> { -+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunkList) { -+ destination.chunkSource.addTicketAtLevel( -+ TicketType.POST_TELEPORT, chunk.getPos(), -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, -+ Integer.valueOf(Entity.this.getId()) -+ ); -+ } -+ scheduleEntityJoin.run(); -+ } -+ ); -+ } else { -+ scheduleEntityJoin.run(); -+ } -+ } -+ -+ protected boolean canTeleportAsync() { -+ return !this.hasNullCallback() && !this.isRemoved() && this.isAlive() && (!(this instanceof net.minecraft.world.entity.LivingEntity livingEntity) || !livingEntity.isSleeping()); -+ } -+ -+ // Mojang for whatever reason has started storing positions to cache certain physics properties that entities collide with -+ // As usual though, they don't properly do anything to prevent serious desync with respect to the current entity position -+ // We add additional logic to reset these before teleporting to prevent issues with them possibly tripping thread checks. -+ protected void resetStoredPositions() { -+ this.mainSupportingBlockPos = Optional.empty(); -+ } -+ -+ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { -+ if (yaw != null) { -+ this.setYRot(yaw.floatValue()); -+ this.setYHeadRot(yaw.floatValue()); -+ } -+ if (pitch != null) { -+ this.setXRot(pitch.floatValue()); -+ } -+ if (velocity != null) { -+ this.setDeltaMovement(velocity); -+ } -+ this.moveTo(pos.x, pos.y, pos.z); -+ this.setOldPosAndRot(); -+ this.resetStoredPositions(); -+ } -+ -+ protected final void transform(TeleportTransition telpeort) { -+ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute( -+ PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives() -+ ); -+ this.transform( -+ move.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement() -+ ); -+ } -+ -+ protected void transform(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { -+ if (yaw != null) { -+ this.setYRot(yaw.floatValue()); -+ this.setYHeadRot(yaw.floatValue()); -+ } -+ if (pitch != null) { -+ this.setXRot(pitch.floatValue()); -+ } -+ if (velocity != null) { -+ this.setDeltaMovement(velocity); -+ } -+ if (pos != null) { -+ this.setPosRaw(pos.x, pos.y, pos.z); -+ } -+ this.setOldPosAndRot(); -+ } -+ -+ protected final Entity transformForAsyncTeleport(TeleportTransition telpeort) { -+ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute( -+ PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives() -+ ); -+ return this.transformForAsyncTeleport( -+ telpeort.newLevel(), telpeort.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement() -+ ); -+ } -+ -+ protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { -+ this.removeAfterChangingDimensions(); // remove before so that any CBEntity#getHandle call affects this entity before copying -+ -+ Entity copy = this.getType().create(destination, EntitySpawnReason.DIMENSION_TRAVEL); -+ copy.restoreFrom(this); -+ copy.transform(pos, yaw, pitch, velocity); -+ // vanilla code used to call remove _after_ copying, and some stuff is required to be after copy - so add hook here -+ // for example, clearing of inventory after switching dimensions -+ this.postRemoveAfterChangingDimensions(); -+ -+ return copy; -+ } -+ -+ public final boolean teleportAsync(TeleportTransition teleportTarget, long teleportFlags, -+ java.util.function.Consumer teleportComplete) { -+ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this), PositionMoveRotation.of(teleportTarget), teleportTarget.relatives()); -+ -+ return this.teleportAsync( -+ teleportTarget.newLevel(), move.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement(), -+ teleportTarget.cause(), teleportFlags, teleportComplete -+ ); -+ } -+ -+ public final boolean teleportAsync(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity, -+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, long teleportFlags, -+ java.util.function.Consumer teleportComplete) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot teleport entity async"); -+ -+ if (!ServerLevel.isInSpawnableBounds(new BlockPos(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockY(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockZ(pos)))) { -+ return false; -+ } -+ -+ if (!this.canTeleportAsync()) { -+ return false; -+ } -+ this.getBukkitEntity(); // force bukkit entity to be created before TPing -+ if ((teleportFlags & TELEPORT_FLAG_UNMOUNT) == 0L) { -+ for (Entity entity : this.getIndirectPassengers()) { -+ if (!entity.canTeleportAsync()) { -+ return false; -+ } -+ entity.getBukkitEntity(); // force bukkit entity to be created before TPing -+ } -+ } else { -+ this.unRide(); -+ } -+ -+ if ((teleportFlags & TELEPORT_FLAG_TELEPORT_PASSENGERS) != 0L) { -+ if (this.isPassenger()) { -+ return false; -+ } -+ } else { -+ if (this.isVehicle() || this.isPassenger()) { -+ return false; -+ } -+ } -+ -+ // TODO any events that can modify go HERE -+ -+ // check for same region -+ if (destination == this.level()) { -+ Vec3 currPos = this.position(); -+ if ( -+ destination.regioniser.getRegionAtUnsynchronised( -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(currPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(currPos) -+ ) == destination.regioniser.getRegionAtUnsynchronised( -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos) -+ ) -+ ) { -+ EntityTreeNode passengerTree = this.detachPassengers(); -+ // Note: The client does not accept position updates for controlled entities. So, we must -+ // perform a lot of tracker updates here to make it all work out. -+ -+ // first, clear the tracker -+ passengerTree.clearTracker(); -+ for (EntityTreeNode entity : passengerTree.getFullTree()) { -+ entity.root.teleportSyncSameRegion(pos, yaw, pitch, velocity); -+ } -+ -+ passengerTree.restore(); -+ // re-add to the tracker once the tree is restored -+ passengerTree.addTracker(); -+ -+ // adjust entities to final position -+ passengerTree.adjustRiders(true); -+ -+ // the tracker clear/add logic is only used in the same region, as the other logic -+ // performs add/remove from world logic which will also perform add/remove tracker logic -+ -+ if (teleportComplete != null) { -+ teleportComplete.accept(this); -+ } -+ return true; -+ } -+ } -+ -+ EntityTreeNode passengerTree = this.detachPassengers(); -+ List fullPassengerTree = passengerTree.getFullTree(); -+ ServerLevel originWorld = (ServerLevel)this.level; -+ -+ for (EntityTreeNode node : fullPassengerTree) { -+ node.root.preChangeDimension(); -+ } -+ -+ for (EntityTreeNode node : fullPassengerTree) { -+ node.root = node.root.transformForAsyncTeleport(destination, pos, yaw, pitch, velocity); -+ } -+ -+ passengerTree.root.placeInAsync(originWorld, destination, teleportFlags, passengerTree, teleportComplete); -+ -+ return true; -+ } -+ -+ public void preChangeDimension() { -+ if (this instanceof Leashable leashable) { -+ leashable.dropLeash(); -+ } -+ } -+ -+ public void postChangeDimension() { -+ this.resetStoredPositions(); -+ } -+ -+ protected static enum PortalType { -+ NETHER, END; -+ } -+ -+ public boolean endPortalLogicAsync(BlockPos portalPos) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); -+ -+ ServerLevel destination = this.getServer().getLevel(this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END ? Level.OVERWORLD : Level.END); -+ if (destination == null) { -+ // wat -+ return false; -+ } -+ -+ return this.portalToAsync(destination, portalPos, true, PortalType.END, null); -+ } -+ -+ public boolean netherPortalLogicAsync(BlockPos portalPos) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); -+ -+ ServerLevel destination = this.getServer().getLevel(this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER ? Level.OVERWORLD : Level.NETHER); -+ if (destination == null) { -+ // wat -+ return false; -+ } -+ -+ return this.portalToAsync(destination, portalPos, true, PortalType.NETHER, null); -+ } -+ -+ private static final java.util.concurrent.atomic.AtomicLong CREATE_PORTAL_DOUBLE_CHECK = new java.util.concurrent.atomic.AtomicLong(); -+ private static final java.util.concurrent.atomic.AtomicLong TELEPORT_HOLD_TICKET_GEN = new java.util.concurrent.atomic.AtomicLong(); -+ -+ // To simplify portal logic, in region threading both players -+ // and non-player entities will create portals. By guaranteeing -+ // that the teleportation can take place, we can simply -+ // remove the entity, find/create the portal, and place async. -+ // If we have to worry about whether the entity may not teleport, -+ // we need to first search, then report back, ... -+ protected void findOrCreatePortalAsync(ServerLevel origin, BlockPos originPortal, ServerLevel destination, PortalType type, -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalInfoCompletable) { -+ switch (type) { -+ // end portal logic is quite simple, the spawn in the end is fixed and when returning to the overworld -+ // we just select the spawn position -+ case END: { -+ if (destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { -+ BlockPos targetPos = ServerLevel.END_SPAWN_POINT; -+ // need to load chunks so we can create the platform -+ destination.moonrise$loadChunksAsync( -+ targetPos, 16, // load 16 blocks to be safe from block physics -+ ca.spottedleaf.concurrentutil.util.Priority.HIGH, -+ (chunks) -> { -+ net.minecraft.world.level.levelgen.feature.EndPlatformFeature.createEndPlatform(destination, targetPos.below(), true, null); -+ -+ // the portal obsidian is placed at targetPos.y - 2, so if we want to place the entity -+ // on the obsidian, we need to spawn at targetPos.y - 1 -+ portalInfoCompletable.complete( -+ new net.minecraft.world.level.portal.TeleportTransition( -+ destination, Vec3.atBottomCenterOf(targetPos.below()), Vec3.ZERO, 90.0f, 0.0f, -+ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), -+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL -+ ) -+ ); -+ } -+ ); -+ } else { -+ BlockPos spawnPos = destination.getSharedSpawnPos(); -+ // need to load chunk for heightmap -+ destination.moonrise$loadChunksAsync( -+ spawnPos, 0, -+ ca.spottedleaf.concurrentutil.util.Priority.HIGH, -+ (chunks) -> { -+ BlockPos adjustedSpawn = destination.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, spawnPos); -+ -+ // done -+ portalInfoCompletable.complete( -+ new net.minecraft.world.level.portal.TeleportTransition( -+ destination, Vec3.atBottomCenterOf(adjustedSpawn), Vec3.ZERO, 90.0f, 0.0f, -+ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), -+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL -+ ) -+ ); -+ } -+ ); -+ } -+ -+ break; -+ } -+ // for the nether logic, we need to first load the chunks in radius to empty (so that POI is created) -+ // then we can search for an existing portal using the POI routines -+ // if we don't find a portal, then we bring the chunks in the create radius to full and -+ // create it -+ case NETHER: { -+ // hoisted from the create fallback, so that we can avoid the sync load later if we need it -+ BlockState originalPortalBlock = origin.getBlockStateIfLoaded(originPortal); -+ Direction.Axis originalPortalDirection = originalPortalBlock == null ? Direction.Axis.X : -+ originalPortalBlock.getOptionalValue(net.minecraft.world.level.block.NetherPortalBlock.AXIS).orElse(Direction.Axis.X); -+ BlockUtil.FoundRectangle originalPortalRectangle = -+ originalPortalBlock == null || !originalPortalBlock.hasProperty(net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_AXIS) -+ ? null -+ : BlockUtil.getLargestRectangleAround( -+ originPortal, originalPortalDirection, 21, Direction.Axis.Y, 21, -+ (blockpos) -> { -+ return origin.getBlockStateFromEmptyChunkIfLoaded(blockpos) == originalPortalBlock; -+ } -+ ); -+ -+ boolean destinationIsNether = destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER; -+ -+ int portalSearchRadius = origin.paperConfig().environment.portalSearchVanillaDimensionScaling && destinationIsNether ? -+ (int)(destination.paperConfig().environment.portalSearchRadius / destination.dimensionType().coordinateScale()) : -+ destination.paperConfig().environment.portalSearchRadius; -+ int portalCreateRadius = destination.paperConfig().environment.portalCreateRadius; -+ -+ WorldBorder destinationBorder = destination.getWorldBorder(); -+ double dimensionScale = net.minecraft.world.level.dimension.DimensionType.getTeleportationScale(origin.dimensionType(), destination.dimensionType()); -+ BlockPos targetPos = destination.getWorldBorder().clampToBounds(this.getX() * dimensionScale, this.getY(), this.getZ() * dimensionScale); -+ -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalFound -+ = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); -+ -+ // post portal find/create logic -+ portalFound.addWaiter( -+ (BlockUtil.FoundRectangle portal, Throwable thr) -> { -+ // no portal could be created -+ if (portal == null) { -+ portalInfoCompletable.complete( -+ new TeleportTransition(destination, Vec3.atCenterOf(targetPos), Vec3.ZERO, -+ 90.0f, 0.0f, -+ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET)) -+ ); -+ return; -+ } -+ -+ Vec3 relativePos = originalPortalRectangle == null ? -+ new Vec3(0.5, 0.0, 0.0) : -+ Entity.this.getRelativePortalPosition(originalPortalDirection, originalPortalRectangle); -+ -+ portalInfoCompletable.complete( -+ net.minecraft.world.level.block.NetherPortalBlock.createDimensionTransition( -+ destination, portal, originalPortalDirection, relativePos, -+ Entity.this, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET) -+ ) -+ ); -+ } -+ ); -+ -+ // kick off search for existing portal or creation -+ destination.moonrise$loadChunksAsync( -+ // add 32 so that the final search for a portal frame doesn't load any chunks -+ targetPos, portalSearchRadius + 32, -+ net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY, -+ ca.spottedleaf.concurrentutil.util.Priority.HIGH, -+ (chunks) -> { -+ BlockUtil.FoundRectangle portal = -+ net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius); -+ if (portal != null) { -+ portalFound.complete(portal); -+ return; -+ } -+ -+ // add tickets so that we can re-search for a portal once the chunks are loaded -+ Long ticketId = Long.valueOf(CREATE_PORTAL_DOUBLE_CHECK.getAndIncrement()); -+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { -+ destination.chunkSource.addTicketAtLevel( -+ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, -+ ticketId -+ ); -+ } -+ -+ // no portal found - create one -+ destination.moonrise$loadChunksAsync( -+ targetPos, portalCreateRadius + 32, -+ ca.spottedleaf.concurrentutil.util.Priority.HIGH, -+ (chunks2) -> { -+ // don't need the tickets anymore -+ // note: we expect removeTicketsAtLevel to add an unknown ticket for us automatically -+ // if the ticket level were to decrease -+ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { -+ destination.chunkSource.removeTicketAtLevel( -+ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, -+ ticketId -+ ); -+ } -+ -+ // when two entities portal at the same time, it is possible that both entities reach this -+ // part of the code - and create a double portal -+ // to fix this, we just issue another search to try and see if another entity created -+ // a portal nearby -+ BlockUtil.FoundRectangle existingTryAgain = -+ net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius); -+ if (existingTryAgain != null) { -+ portalFound.complete(existingTryAgain); -+ return; -+ } -+ -+ // we do not have the correct entity reference here -+ BlockUtil.FoundRectangle createdPortal = -+ destination.getPortalForcer().createPortal(targetPos, originalPortalDirection, null, portalCreateRadius).orElse(null); -+ // if it wasn't created, passing null is expected here -+ portalFound.complete(createdPortal); -+ } -+ ); -+ } -+ ); -+ break; -+ } -+ default: { -+ throw new IllegalStateException("Unknown portal type " + type); -+ } -+ } -+ } -+ -+ public boolean canPortalAsync(ServerLevel to, boolean considerPassengers) { -+ return this.canPortalAsync(to, considerPassengers, false); -+ } -+ -+ protected boolean canPortalAsync(ServerLevel to, boolean considerPassengers, boolean skipPassengerCheck) { -+ if (considerPassengers) { -+ if (!skipPassengerCheck && this.isPassenger()) { -+ return false; -+ } -+ } else { -+ if (this.isVehicle() || (!skipPassengerCheck && this.isPassenger())) { -+ return false; -+ } -+ } -+ this.getBukkitEntity(); // force bukkit entity to be created before TPing -+ if (!this.canTeleportAsync()) { -+ return false; -+ } -+ if (considerPassengers) { -+ for (Entity entity : this.passengers) { -+ if (!entity.canPortalAsync(to, true, true)) { -+ return false; -+ } -+ } -+ } -+ -+ return true; -+ } -+ -+ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { -+ -+ } -+ -+ protected boolean portalToAsync(ServerLevel destination, BlockPos portalPos, boolean takePassengers, -+ PortalType type, java.util.function.Consumer teleportComplete) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); -+ if (!this.canPortalAsync(destination, takePassengers)) { -+ return false; -+ } -+ -+ Vec3 initialPosition = this.position(); -+ ChunkPos initialPositionChunk = new ChunkPos( -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(initialPosition), -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(initialPosition) -+ ); -+ -+ // first, remove entity/passengers from world -+ EntityTreeNode passengerTree = this.detachPassengers(); -+ List fullPassengerTree = passengerTree.getFullTree(); -+ ServerLevel originWorld = (ServerLevel)this.level; -+ -+ for (EntityTreeNode node : fullPassengerTree) { -+ node.root.preChangeDimension(); -+ node.root.prePortalLogic(originWorld, destination, type); -+ } -+ -+ for (EntityTreeNode node : fullPassengerTree) { -+ // we will update pos/rot/speed later -+ node.root = node.root.transformForAsyncTeleport(destination, null, null, null, null); -+ // set portal cooldown -+ node.root.setPortalCooldown(); -+ } -+ -+ // ensure the region is always ticking in case of a shutdown -+ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region -+ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); -+ originWorld.chunkSource.addTicketAtLevel( -+ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, -+ teleportHoldId -+ ); -+ -+ ServerLevel.PendingTeleport beforeFindDestination = new ServerLevel.PendingTeleport(passengerTree, initialPosition); -+ originWorld.pushPendingTeleport(beforeFindDestination); -+ -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalInfoCompletable -+ = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); -+ -+ portalInfoCompletable.addWaiter((TeleportTransition info, Throwable throwable) -> { -+ if (!originWorld.removePendingTeleport(beforeFindDestination)) { -+ // the shutdown thread has placed us back into the origin world at the original position -+ // we just have to abandon this teleport to prevent duplication -+ return; -+ } -+ originWorld.chunkSource.removeTicketAtLevel( -+ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, -+ teleportHoldId -+ ); -+ // adjust passenger tree to final pos/rot/speed -+ for (EntityTreeNode node : fullPassengerTree) { -+ node.root.transform(info); -+ } -+ -+ // place -+ passengerTree.root.placeInAsync( -+ originWorld, destination, Entity.TELEPORT_FLAG_LOAD_CHUNK | (takePassengers ? Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS : 0L), -+ passengerTree, -+ (Entity teleported) -> { -+ if (info.postTeleportTransition() != null) { -+ info.postTeleportTransition().onTransition(teleported); -+ } -+ -+ if (teleportComplete != null) { -+ teleportComplete.accept(teleported); -+ } -+ } -+ ); -+ }); -+ -+ -+ passengerTree.root.findOrCreatePortalAsync(originWorld, portalPos, destination, type, portalInfoCompletable); -+ -+ return true; -+ } -+ // Folia end - region threading -+ - @Nullable - public Entity teleport(TeleportTransition teleportTransition) { -+ // Folia start - region threading -+ if (true) { -+ throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); -+ } -+ // Folia end - region threading - // Paper start - Fix item duplication and teleport issues - if ((!this.isAlive() || !this.valid) && (teleportTransition.newLevel() != this.level)) { - LOGGER.warn("Illegal Entity Teleport " + this + " to " + teleportTransition.newLevel() + ":" + teleportTransition.position(), new Throwable()); -@@ -3911,6 +4701,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - } - -+ // Folia start - region threading - move inventory clearing until after the dimension change -+ protected void postRemoveAfterChangingDimensions() { -+ -+ } -+ // Folia end - region threading - move inventory clearing until after the dimension change -+ - protected void removeAfterChangingDimensions() { - this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION, null); // CraftBukkit - add Bukkit remove cause - if (this instanceof Leashable leashable && leashable.isLeashed()) { // Paper - only call if it is leashed -@@ -4790,7 +5586,8 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - } - // Paper end - Fix MC-4 -- if (this.position.x != x || this.position.y != y || this.position.z != z) { -+ boolean posChanged = this.position.x != x || this.position.y != y || this.position.z != z; -+ if (posChanged) { // Folia - region threading - synchronized (this.posLock) { // Paper - detailed watchdog information - this.position = new Vec3(x, y, z); - } // Paper - detailed watchdog information -@@ -4809,7 +5606,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - // Paper start - Block invalid positions and bounding box; don't allow desync of pos and AABB - // hanging has its own special logic -- if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || this.position.x != x || this.position.y != y || this.position.z != z)) { -+ if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || posChanged)) { - this.setBoundingBox(this.makeBoundingBox()); - } - // Paper end - Block invalid positions and bounding box -@@ -4893,6 +5690,12 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return this.removalReason != null; - } - -+ // Folia start - region threading -+ public final boolean hasNullCallback() { -+ return this.levelCallback == EntityInLevelCallback.NULL; -+ } -+ // Folia end - region threading -+ - @Nullable - public Entity.RemovalReason getRemovalReason() { - return this.removalReason; -@@ -4915,6 +5718,9 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); - // CraftBukkit end - final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers -+ // Folia start - region threading -+ this.preRemove(removalReason); -+ // Folia end - region threading - if (this.removalReason == null) { - this.removalReason = removalReason; - } -@@ -4938,6 +5744,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.removalReason = null; - } - -+ // Folia start - region threading -+ protected void preRemove(Entity.RemovalReason reason) {} -+ // Folia end - region threading -+ - // Paper start - Folia schedulers - /** - * Invoked only when the entity is truly removed from the server, never to be added to any world. -diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java -index 239c443ddc9bacc08a39a8ef2ab17016a2480549..8f9e64590400039566ee5c9628d82a0eb9e56be1 100644 ---- a/net/minecraft/world/entity/LivingEntity.java -+++ b/net/minecraft/world/entity/LivingEntity.java -@@ -278,7 +278,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - private Optional lastClimbablePos = Optional.empty(); - @Nullable - private DamageSource lastDamageSource; -- private long lastDamageStamp; -+ private long lastDamageStamp = Long.MIN_VALUE; // Folia - region threading - protected int autoSpinAttackTicks; - protected float autoSpinAttackDmg; - @Nullable -@@ -307,6 +307,21 @@ public abstract class LivingEntity extends Entity implements Attackable { - return this.getYHeadRot(); - } - // CraftBukkit end -+ // Folia start - region threading -+ @Override -+ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { -+ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); -+ if (this.lastDamageStamp != Long.MIN_VALUE) { -+ this.lastDamageStamp += fromRedstoneTimeOffset; -+ } -+ } -+ -+ @Override -+ protected void resetStoredPositions() { -+ super.resetStoredPositions(); -+ this.lastClimbablePos = Optional.empty(); -+ } -+ // Folia end - region threading - - protected LivingEntity(EntityType entityType, Level level) { - super(entityType, level); -@@ -528,7 +543,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - - if (this.isDeadOrDying() && this.level().shouldTickDeath(this)) { - this.tickDeath(); -- } -+ } else { this.broadcastedDeath = false; } // Folia - region threading - - if (this.lastHurtByPlayerTime > 0) { - this.lastHurtByPlayerTime--; -@@ -611,11 +626,14 @@ public abstract class LivingEntity extends Entity implements Attackable { - return true; - } - -+ public boolean broadcastedDeath = false; // Folia - region threading - protected void tickDeath() { - this.deathTime++; - if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved()) { - this.level().broadcastEntityEvent(this, (byte)60); -- this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause -+ this.broadcastedDeath = true; // Folia - region threading - death has been broadcasted -+ if (!(this instanceof ServerPlayer)) this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause // Folia - region threading - don't remove, we want the tick scheduler to be running -+ if ((this instanceof ServerPlayer)) this.unRide(); // Folia - region threading - unmount player when dead - } - } - -@@ -851,9 +869,9 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - this.hurtTime = compound.getShort("HurtTime"); -- this.deathTime = compound.getShort("DeathTime"); -+ this.deathTime = compound.getShort("DeathTime"); this.broadcastedDeath = false; // Folia - region threading - this.lastHurtByMobTimestamp = compound.getInt("HurtByTimestamp"); -- if (compound.contains("Team", 8)) { -+ if (false && compound.contains("Team", 8)) { // Folia start - region threading - String string = compound.getString("Team"); - Scoreboard scoreboard = this.level().getScoreboard(); - PlayerTeam playerTeam = scoreboard.getPlayerTeam(string); -@@ -1115,6 +1133,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - public boolean addEffect(MobEffectInstance effectInstance, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) { - // Paper end - Don't fire sync event during generation - // org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot // Paper - move to API -+ if (!this.hasNullCallback()) ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot add effects to entities asynchronously"); // Folia - region threading - if (this.isTickingEffects) { - this.effectsToProcess.add(new ProcessableEffect(effectInstance, cause)); - return true; -@@ -1502,7 +1521,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - boolean flag2 = !flag; // CraftBukkit - Ensure to return false if damage is blocked - if (flag2) { - this.lastDamageSource = damageSource; -- this.lastDamageStamp = this.level().getGameTime(); -+ this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading - - for (MobEffectInstance mobEffectInstance : this.getActiveEffects()) { - mobEffectInstance.onMobHurt(level, this, damageSource, amount); -@@ -1629,7 +1648,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - - @Nullable - public DamageSource getLastDamageSource() { -- if (this.level().getGameTime() - this.lastDamageStamp > 40L) { -+ if (this.level().getRedstoneGameTime() - this.lastDamageStamp > 40L || this.lastDamageStamp == Long.MIN_VALUE) { // Folia - region threading - this.lastDamageSource = null; - } - -@@ -2420,10 +2439,10 @@ public abstract class LivingEntity extends Entity implements Attackable { - - @Nullable - public LivingEntity getKillCredit() { -- if (this.lastHurtByPlayer != null) { -+ if (this.lastHurtByPlayer != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByPlayer)) { // Folia - region threading - return this.lastHurtByPlayer; - } else { -- return this.lastHurtByMob != null ? this.lastHurtByMob : null; -+ return this.lastHurtByMob != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByMob) ? this.lastHurtByMob : null; // Folia - region threading - } - } - -@@ -2502,7 +2521,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - this.lastDamageSource = damageSource; -- this.lastDamageStamp = this.level().getGameTime(); -+ this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading - } - - @Override -@@ -3479,7 +3498,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - this.pushEntities(); - profilerFiller.pop(); - // Paper start - Add EntityMoveEvent -- if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof Player)) { -+ if (((ServerLevel) this.level()).getCurrentWorldData().hasEntityMoveEvent && !(this instanceof Player)) { // Folia - region threading - if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { - Location from = new Location(this.level().getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO); - Location to = new Location(this.level().getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); -@@ -4152,7 +4171,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - boolean flag = false; - BlockPos blockPos = BlockPos.containing(x, y, z); - Level level = this.level(); -- if (level.hasChunkAt(blockPos)) { -+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)level, blockPos) && level.hasChunkAt(blockPos)) { // Folia - region threading - boolean flag1 = false; - - while (!flag1 && blockPos.getY() > level.getMinY()) { -diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java -index 1ed07fd23985a6bf8cf8300f74c92b7531a79fc6..6394b0899095b047ca9266135fc44aa0c32467cf 100644 ---- a/net/minecraft/world/entity/Mob.java -+++ b/net/minecraft/world/entity/Mob.java -@@ -254,8 +254,20 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab - @Nullable - @Override - public LivingEntity getTarget() { -+ // Folia start - region threading -+ if (this.target != null && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.target) || this.target.isRemoved())) { -+ this.target = null; -+ return null; -+ } -+ // Folia end - region threading -+ return this.target; -+ } -+ -+ // Folia start - region threading -+ public LivingEntity getTargetRaw() { - return this.target; - } -+ // Folia end - region threading - - @Nullable - protected final LivingEntity getTargetFromBrain() { -@@ -268,7 +280,7 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab - } - - public boolean setTarget(LivingEntity target, EntityTargetEvent.TargetReason reason, boolean fireEvent) { -- if (this.getTarget() == target) { -+ if (this.getTargetRaw() == target) { // Folia - region threading - return false; - } - if (fireEvent) { -@@ -1663,12 +1675,26 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab - @Override - protected void removeAfterChangingDimensions() { - super.removeAfterChangingDimensions(); -+ // Folia start - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions -+// this.getAllSlots().forEach(itemStack -> { -+// if (!itemStack.isEmpty()) { -+// itemStack.setCount(0); -+// } -+// }); -+ // Folia end - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions -+ } -+ -+ // Folia start - region threading -+ @Override -+ protected void postRemoveAfterChangingDimensions() { -+ super.postRemoveAfterChangingDimensions(); - this.getAllSlots().forEach(itemStack -> { - if (!itemStack.isEmpty()) { - itemStack.setCount(0); - } - }); - } -+ // Folia end - region threading - - @Nullable - @Override -diff --git a/net/minecraft/world/entity/PortalProcessor.java b/net/minecraft/world/entity/PortalProcessor.java -index 88b07fbb96b20124777889830afa480673629d43..46d989aef0eceebd98bfd93999153319de77a8a0 100644 ---- a/net/minecraft/world/entity/PortalProcessor.java -+++ b/net/minecraft/world/entity/PortalProcessor.java -@@ -33,6 +33,12 @@ public class PortalProcessor { - return this.portal.getPortalDestination(level, entity, this.entryPosition); - } - -+ // Folia start - region threading -+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget) { -+ return this.portal.portalAsync(sourceWorld, portalTarget, this.entryPosition); -+ } -+ // Folia end - region threading -+ - public Portal.Transition getPortalLocalTransition() { - return this.portal.getLocalTransition(); - } -diff --git a/net/minecraft/world/entity/TamableAnimal.java b/net/minecraft/world/entity/TamableAnimal.java -index fc3ba135ae502aaa5c3a9fa3297bf7b12c1ab063..b0b1e38f2b70ed548790fd0445db4541c34b0f34 100644 ---- a/net/minecraft/world/entity/TamableAnimal.java -+++ b/net/minecraft/world/entity/TamableAnimal.java -@@ -263,6 +263,11 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { - public void tryToTeleportToOwner() { - LivingEntity owner = this.getOwner(); - if (owner != null) { -+ // Folia start - region threading -+ if (owner.isRemoved() || owner.level() != this.level()) { -+ return; -+ } -+ // Folia end - region threading - this.teleportToAroundBlockPos(owner.blockPosition()); - } - } -@@ -295,7 +300,22 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { - return false; - } - org.bukkit.Location to = event.getTo(); -- this.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); -+ // Folia start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick -+ // also, use teleportAsync so that crossing region boundaries will not blow up -+ org.bukkit.Location finalTo = to; -+ Level sourceWorld = this.level(); -+ this.getBukkitEntity().taskScheduler.schedule((TamableAnimal nmsEntity) -> { -+ if (nmsEntity.level() == sourceWorld) { -+ nmsEntity.teleportAsync( -+ (net.minecraft.server.level.ServerLevel)nmsEntity.level(), -+ new net.minecraft.world.phys.Vec3(finalTo.getX(), finalTo.getY(), finalTo.getZ()), -+ Float.valueOf(finalTo.getYaw()), Float.valueOf(finalTo.getPitch()), -+ net.minecraft.world.phys.Vec3.ZERO, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN, Entity.TELEPORT_FLAG_LOAD_CHUNK, -+ null -+ ); -+ } -+ }, null, 1L); -+ // Folia end - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick - // CraftBukkit end - this.navigation.stop(); - return true; -diff --git a/net/minecraft/world/entity/ai/Brain.java b/net/minecraft/world/entity/ai/Brain.java -index 450396468b23fd90cb8036dbbdd0927051f907af..65b2b3ece213d901cdd585093e2fafcd2ef4a7cd 100644 ---- a/net/minecraft/world/entity/ai/Brain.java -+++ b/net/minecraft/world/entity/ai/Brain.java -@@ -425,9 +425,17 @@ public class Brain { - } - - public void stopAll(ServerLevel level, E owner) { -+ // Folia start - region threading -+ List> behaviors = this.getRunningBehaviors(); -+ if (behaviors.isEmpty()) { -+ // avoid calling getGameTime, as this may be called while portalling an entity - which will cause -+ // the world data retrieval to fail -+ return; -+ } -+ // Folia end - region threading - long gameTime = owner.level().getGameTime(); - -- for (BehaviorControl behaviorControl : this.getRunningBehaviors()) { -+ for (BehaviorControl behaviorControl : behaviors) { // Folia - region threading - behaviorControl.doStop(level, owner, gameTime); - } - } -diff --git a/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java b/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java -index b11a16db0ea22ebd68db9c96e0ba0939b6596caf..9f5b5ad2fe08f25f4c922ae641d1a2e8bce18ccb 100644 ---- a/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java -+++ b/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java -@@ -19,6 +19,11 @@ public class PoiCompetitorScan { - instance, - (jobSite, nearestLivingEntities) -> (level, villager, gameTime) -> { - GlobalPos globalPos = instance.get(jobSite); -+ // Folia start - region threading -+ if (globalPos.dimension() != level.dimension() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, globalPos.pos())) { -+ return true; -+ } -+ // Folia end - region threading - level.getPoiManager() - .getType(globalPos.pos()) - .ifPresent( -diff --git a/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java b/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java -index dde287e823f906681e3addf03fa821c8786c9900..e5e3ce6eeb01ac4387eaee20d09ef469d8b3bc5e 100644 ---- a/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java -+++ b/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java -@@ -51,7 +51,7 @@ public class FollowOwnerGoal extends Goal { - public boolean canContinueToUse() { - return !this.navigation.isDone() - && !this.tamable.unableToMoveToOwner() -- && !(this.tamable.distanceToSqr(this.owner) <= this.stopDistance * this.stopDistance); -+ && !(this.owner.level() == this.tamable.level() && this.tamable.distanceToSqr(this.owner) <= this.stopDistance * this.stopDistance); // Folia - region threading - check level - } - - @Override -diff --git a/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java b/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java -index 045cfafb3afe8271d60852ae3c7cdcb039b44d4f..a24e964aff5623e3d7f2b79c87b6067f565458c2 100644 ---- a/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java -+++ b/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java -@@ -42,6 +42,11 @@ public class GroundPathNavigation extends PathNavigation { - - @Override - public Path createPath(BlockPos pos, @javax.annotation.Nullable Entity entity, int accuracy) { // Paper - EntityPathfindEvent -+ // Folia start - region threading -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level, pos)) { -+ return null; -+ } -+ // Folia end - region threading - LevelChunk chunkNow = this.level.getChunkSource().getChunkNow(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())); - if (chunkNow == null) { - return null; -diff --git a/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/net/minecraft/world/entity/ai/navigation/PathNavigation.java -index b44f2c49509d847817a78e9c4fb1499fb378054b..386580035e6789d6e668b924513ddfc81947a9b3 100644 ---- a/net/minecraft/world/entity/ai/navigation/PathNavigation.java -+++ b/net/minecraft/world/entity/ai/navigation/PathNavigation.java -@@ -96,11 +96,11 @@ public abstract class PathNavigation { - } - - public void recomputePath() { -- if (this.level.getGameTime() - this.timeLastRecompute > 20L) { -+ if (this.tick - this.timeLastRecompute > 20L) { // Folia - region threading - if (this.targetPos != null) { - this.path = null; - this.path = this.createPath(this.targetPos, this.reachRange); -- this.timeLastRecompute = this.level.getGameTime(); -+ this.timeLastRecompute = this.tick; // Folia - region threading - this.hasDelayedRecomputation = false; - } - } else { -@@ -221,7 +221,7 @@ public abstract class PathNavigation { - - public boolean moveTo(Entity entity, double speed) { - // Paper start - Perf: Optimise pathfinding -- if (this.pathfindFailures > 10 && this.path == null && net.minecraft.server.MinecraftServer.currentTick < this.lastFailure + 40) { -+ if (this.pathfindFailures > 10 && this.path == null && this.tick < this.lastFailure + 40) { // Folia - region threading - return false; - } - // Paper end - Perf: Optimise pathfinding -@@ -233,7 +233,7 @@ public abstract class PathNavigation { - return true; - } else { - this.pathfindFailures++; -- this.lastFailure = net.minecraft.server.MinecraftServer.currentTick; -+ this.lastFailure = this.tick; // Folia - region threading - return false; - } - // Paper end - Perf: Optimise pathfinding -diff --git a/net/minecraft/world/entity/ai/sensing/PlayerSensor.java b/net/minecraft/world/entity/ai/sensing/PlayerSensor.java -index 6233e6b48aaa69ba9f577d0b480b1cdf2f55d16e..a4810c8a3b18082543d06787722d4ed5821a1943 100644 ---- a/net/minecraft/world/entity/ai/sensing/PlayerSensor.java -+++ b/net/minecraft/world/entity/ai/sensing/PlayerSensor.java -@@ -22,7 +22,7 @@ public class PlayerSensor extends Sensor { - - @Override - protected void doTick(ServerLevel level, LivingEntity entity) { -- List list = level.players() -+ List list = level.getLocalPlayers() // Folia - region threading - .stream() - .filter(EntitySelector.NO_SPECTATORS) - .filter(serverPlayer -> entity.closerThan(serverPlayer, this.getFollowDistance(entity))) -diff --git a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java -index 4b3ba795bc18417f983600f1edbc1895ccb7deab..d06c2c72e6166bc8b7822966092b17440125b814 100644 ---- a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java -+++ b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java -@@ -36,7 +36,7 @@ public class TemptingSensor extends Sensor { - protected void doTick(ServerLevel level, PathfinderMob entity) { - Brain brain = entity.getBrain(); - TargetingConditions targetingConditions = TEMPT_TARGETING.copy().range((float)entity.getAttributeValue(Attributes.TEMPT_RANGE)); -- List list = level.players() -+ List list = level.getLocalPlayers() // Folia - region threading - .stream() - .filter(EntitySelector.NO_SPECTATORS) - .filter(serverPlayer -> targetingConditions.test(level, entity, serverPlayer)) -diff --git a/net/minecraft/world/entity/ai/village/VillageSiege.java b/net/minecraft/world/entity/ai/village/VillageSiege.java -index a1cea4a4f76a7bb771b8ab643bd9d473e16418bf..fa012c5b23f6fdd714d15282cc485492ae18672a 100644 ---- a/net/minecraft/world/entity/ai/village/VillageSiege.java -+++ b/net/minecraft/world/entity/ai/village/VillageSiege.java -@@ -18,68 +18,72 @@ import org.slf4j.Logger; - - public class VillageSiege implements CustomSpawner { - private static final Logger LOGGER = LogUtils.getLogger(); -- private boolean hasSetupSiege; -- private VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE; -- private int zombiesToSpawn; -- private int nextSpawnTime; -- private int spawnX; -- private int spawnY; -- private int spawnZ; -+ // Folia - region threading - - @Override - public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading -+ // Folia start - region threading -+ // check if the spawn pos is no longer owned by this region -+ if (worldData.villageSiegeState.siegeState != State.SIEGE_DONE -+ && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, worldData.villageSiegeState.spawnX >> 4, worldData.villageSiegeState.spawnZ >> 4, 8)) { -+ // can't spawn here, just re-set -+ worldData.villageSiegeState = new io.papermc.paper.threadedregions.RegionizedWorldData.VillageSiegeState(); -+ } -+ // Folia end - region threading - if (!level.isDay() && spawnHostiles) { - float timeOfDay = level.getTimeOfDay(0.0F); - if (timeOfDay == 0.5) { -- this.siegeState = level.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; -+ worldData.villageSiegeState.siegeState = level.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; // Folia - region threading - } - -- if (this.siegeState == VillageSiege.State.SIEGE_DONE) { -+ if (worldData.villageSiegeState.siegeState == VillageSiege.State.SIEGE_DONE) { // Folia - region threading - return 0; - } else { -- if (!this.hasSetupSiege) { -+ if (!worldData.villageSiegeState.hasSetupSiege) { // Folia - region threading - if (!this.tryToSetupSiege(level)) { - return 0; - } - -- this.hasSetupSiege = true; -+ worldData.villageSiegeState.hasSetupSiege = true; // Folia - region threading - } - -- if (this.nextSpawnTime > 0) { -- this.nextSpawnTime--; -+ if (worldData.villageSiegeState.nextSpawnTime > 0) { // Folia - region threading -+ worldData.villageSiegeState.nextSpawnTime--; // Folia - region threading - return 0; - } else { -- this.nextSpawnTime = 2; -- if (this.zombiesToSpawn > 0) { -+ worldData.villageSiegeState.nextSpawnTime = 2; // Folia - region threading -+ if (worldData.villageSiegeState.zombiesToSpawn > 0) { // Folia - region threading - this.trySpawn(level); -- this.zombiesToSpawn--; -+ worldData.villageSiegeState.zombiesToSpawn--; // Folia - region threading - } else { -- this.siegeState = VillageSiege.State.SIEGE_DONE; -+ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading - } - - return 1; - } - } - } else { -- this.siegeState = VillageSiege.State.SIEGE_DONE; -- this.hasSetupSiege = false; -+ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading -+ worldData.villageSiegeState.hasSetupSiege = false; // Folia - region threading - return 0; - } - } - - private boolean tryToSetupSiege(ServerLevel level) { -- for (Player player : level.players()) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading -+ for (Player player : level.getLocalPlayers()) { // Folia - region threading - if (!player.isSpectator()) { - BlockPos blockPos = player.blockPosition(); - if (level.isVillage(blockPos) && !level.getBiome(blockPos).is(BiomeTags.WITHOUT_ZOMBIE_SIEGES)) { - for (int i = 0; i < 10; i++) { - float f = level.random.nextFloat() * (float) (Math.PI * 2); -- this.spawnX = blockPos.getX() + Mth.floor(Mth.cos(f) * 32.0F); -- this.spawnY = blockPos.getY(); -- this.spawnZ = blockPos.getZ() + Mth.floor(Mth.sin(f) * 32.0F); -- if (this.findRandomSpawnPos(level, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)) != null) { -- this.nextSpawnTime = 0; -- this.zombiesToSpawn = 20; -+ worldData.villageSiegeState.spawnX = blockPos.getX() + Mth.floor(Mth.cos(f) * 32.0F); // Folia - region threading -+ worldData.villageSiegeState.spawnY = blockPos.getY(); // Folia - region threading -+ worldData.villageSiegeState.spawnZ = blockPos.getZ() + Mth.floor(Mth.sin(f) * 32.0F); // Folia - region threading -+ if (this.findRandomSpawnPos(level, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)) != null) { // Folia - region threading -+ worldData.villageSiegeState.nextSpawnTime = 0; // Folia - region threading -+ worldData.villageSiegeState.zombiesToSpawn = 20; // Folia - region threading - break; - } - } -@@ -93,11 +97,13 @@ public class VillageSiege implements CustomSpawner { - } - - private void trySpawn(ServerLevel level) { -- Vec3 vec3 = this.findRandomSpawnPos(level, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)); -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading -+ Vec3 vec3 = this.findRandomSpawnPos(level, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)); // Folia - region threading - if (vec3 != null) { - Zombie zombie; - try { - zombie = new Zombie(level); -+ zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up - zombie.finalizeSpawn(level, level.getCurrentDifficultyAt(zombie.blockPosition()), EntitySpawnReason.EVENT, null); - } catch (Exception var5) { - LOGGER.warn("Failed to create zombie for village siege at {}", vec3, var5); -@@ -105,7 +111,7 @@ public class VillageSiege implements CustomSpawner { - return; - } - -- zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); -+ //zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up - level.addFreshEntityWithPassengers(zombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_INVASION); // CraftBukkit - } - } -@@ -125,7 +131,7 @@ public class VillageSiege implements CustomSpawner { - return null; - } - -- static enum State { -+ public static enum State { // Folia - region threading - SIEGE_CAN_ACTIVATE, - SIEGE_TONIGHT, - SIEGE_DONE; -diff --git a/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/net/minecraft/world/entity/ai/village/poi/PoiManager.java -index 618fc0eb4fe70e46e55f3aa28e8eac1d2d01b6d9..c10810bf00d75f459c3c6a9415c1e09f0519d50e 100644 ---- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java -+++ b/net/minecraft/world/entity/ai/village/poi/PoiManager.java -@@ -58,11 +58,13 @@ public class PoiManager extends SectionStorage im - } - - private void updateDistanceTracking(long section) { -+ synchronized (this.villageDistanceTracker) { // Folia - region threading - if (this.isVillageCenter(section)) { - this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); - } else { - this.villageDistanceTracker.removeSource(section); - } -+ } // Folia - region threading - } - - @Override -@@ -347,10 +349,12 @@ public class PoiManager extends SectionStorage im - } - - public int sectionsToVillage(SectionPos sectionPos) { -+ synchronized (this.villageDistanceTracker) { // Folia - region threading - // Paper start - rewrite chunk system - this.villageDistanceTracker.propagateUpdates(); - return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(sectionPos))); - // Paper end - rewrite chunk system -+ } // Folia - region threading - } - - boolean isVillageCenter(long chunkPos) { -@@ -364,7 +368,9 @@ public class PoiManager extends SectionStorage im - - @Override - public void tick(BooleanSupplier aheadOfTime) { -+ synchronized (this.villageDistanceTracker) { // Folia - region threading - this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system -+ } // Folia - region threading - } - - @Override -diff --git a/net/minecraft/world/entity/animal/Bee.java b/net/minecraft/world/entity/animal/Bee.java -index 94244b148533ef026bf5c56abbc2bb5cfa83c938..15360f560d9b6a762ebd4284b7d0ca0a3e13794e 100644 ---- a/net/minecraft/world/entity/animal/Bee.java -+++ b/net/minecraft/world/entity/animal/Bee.java -@@ -815,6 +815,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { - - @Override - public boolean canBeeUse() { -+ // Folia start - region threading -+ if (Bee.this.hivePos != null && Bee.this.isTooFarAway(Bee.this.hivePos)) { -+ Bee.this.hivePos = null; -+ } -+ // Folia end - region threading - return Bee.this.hivePos != null - && !Bee.this.isTooFarAway(Bee.this.hivePos) - && !Bee.this.hasRestriction() -@@ -925,6 +930,11 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { - - @Override - public boolean canBeeUse() { -+ // Folia start - region threading -+ if (Bee.this.savedFlowerPos != null && Bee.this.isTooFarAway(Bee.this.savedFlowerPos)) { -+ Bee.this.savedFlowerPos = null; -+ } -+ // Folia end - region threading - return Bee.this.savedFlowerPos != null - && !Bee.this.hasRestriction() - && this.wantsToGoToKnownFlower() -diff --git a/net/minecraft/world/entity/animal/Cat.java b/net/minecraft/world/entity/animal/Cat.java -index 1a7a5c81a260cc740994d1a63c4775c41c238dea..740ab58c733d9e3f05157fef6e6725fd72f90653 100644 ---- a/net/minecraft/world/entity/animal/Cat.java -+++ b/net/minecraft/world/entity/animal/Cat.java -@@ -342,7 +342,7 @@ public class Cat extends TamableAnimal implements VariantHolder tagKey = flag ? CatVariantTags.FULL_MOON_SPAWNS : CatVariantTags.DEFAULT_SPAWNS; - BuiltInRegistries.CAT_VARIANT.getRandomElementOf(tagKey, level.getRandom()).ifPresent(this::setVariant); - ServerLevel level1 = level.getLevel(); -- if (level1.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK, level).isValid()) { // Paper - Fix swamp hut cat generation deadlock -+ if (level.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK).isValid()) { // Paper - Fix swamp hut cat generation deadlock // Folia - region threading - properly fix this - this.setVariant(BuiltInRegistries.CAT_VARIANT.getOrThrow(CatVariant.ALL_BLACK)); - this.setPersistenceRequired(); - } -diff --git a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java -index ff1c84d37db48e1bd0283a900e199647c0e8eba1..fc64c36a01eb8efdcfa487059078787900e34d86 100644 ---- a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java -+++ b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java -@@ -53,7 +53,7 @@ public class EndCrystal extends Entity { - public void tick() { - this.time++; - this.applyEffectsFromBlocks(); -- this.handlePortal(); -+ //this.handlePortal(); // Folia - region threading - if (this.level() instanceof ServerLevel) { - BlockPos blockPos = this.blockPosition(); - if (((ServerLevel)this.level()).getDragonFight() != null && this.level().getBlockState(blockPos).isAir()) { -diff --git a/net/minecraft/world/entity/decoration/ItemFrame.java b/net/minecraft/world/entity/decoration/ItemFrame.java -index 65e1d7c5ac94b1cfb921fa009be59d3e5872f0b5..5aefec7ecc2085659bebca25992dd3a76fff2b5e 100644 ---- a/net/minecraft/world/entity/decoration/ItemFrame.java -+++ b/net/minecraft/world/entity/decoration/ItemFrame.java -@@ -242,7 +242,9 @@ public class ItemFrame extends HangingEntity { - if (framedMapId != null) { - MapItemSavedData savedData = MapItem.getSavedData(framedMapId, this.level()); - if (savedData != null) { -+ synchronized (savedData) { // Folia - make map data thread-safe - savedData.removedFromFrame(this.pos, this.getId()); -+ } // Folia - make map data thread-safe - } - } - -diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java -index 5746587666c7cb788764aab2f6ccf0f3ac5c282f..1fa5e6a12b943e889bde566038a632a6adcf319e 100644 ---- a/net/minecraft/world/entity/item/FallingBlockEntity.java -+++ b/net/minecraft/world/entity/item/FallingBlockEntity.java -@@ -162,7 +162,7 @@ public class FallingBlockEntity extends Entity { - return; - } - // Paper end - Configurable falling blocks height nerf -- this.handlePortal(); -+ //this.handlePortal(); // Folia - region threading - if (this.level() instanceof ServerLevel serverLevel && (this.isAlive() || this.forceTickAfterTeleportToDuplicate)) { - BlockPos blockPos = this.blockPosition(); - boolean flag = this.blockState.getBlock() instanceof ConcretePowderBlock; -diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java -index 52a7ed0d991758bad0dcedcb7f97fb15ac6c6d04..7587130e021d494ae5013f7992b8f3c96590cbd7 100644 ---- a/net/minecraft/world/entity/item/ItemEntity.java -+++ b/net/minecraft/world/entity/item/ItemEntity.java -@@ -521,13 +521,21 @@ public class ItemEntity extends Entity implements TraceableEntity { - return false; - } - -+ // Folia start - region threading -+ @Override -+ public void postChangeDimension() { -+ super.postChangeDimension(); -+ if (!this.level().isClientSide) { -+ this.mergeWithNeighbours(); -+ } -+ } -+ // Folia end - region threading -+ - @Nullable - @Override - public Entity teleport(TeleportTransition teleportTransition) { - Entity entity = super.teleport(teleportTransition); -- if (!this.level().isClientSide && entity instanceof ItemEntity itemEntity) { -- itemEntity.mergeWithNeighbours(); -- } -+ if (entity != null) entity.postChangeDimension(); // Folia - region threading - move to post change - - return entity; - } -diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java -index 40da052e7fea1306a007b3cb5c9daa33e0ef523e..88570bb4aa02896545805d7721c45cf9599befea 100644 ---- a/net/minecraft/world/entity/item/PrimedTnt.java -+++ b/net/minecraft/world/entity/item/PrimedTnt.java -@@ -98,8 +98,8 @@ public class PrimedTnt extends Entity implements TraceableEntity { - - @Override - public void tick() { -- if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot -- this.handlePortal(); -+ if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().getCurrentWorldData().currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading -+ //this.handlePortal(); // Folia - region threading - this.applyGravity(); - this.move(MoverType.SELF, this.getDeltaMovement()); - this.applyEffectsFromBlocks(); -@@ -137,7 +137,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { - */ - // Send position and velocity updates to nearby players on every tick while the TNT is in water. - // This does pretty well at keeping their clients in sync with the server. -- net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel) this.level()).getChunkSource().chunkMap.entityMap.get(this.getId()); -+ net.minecraft.server.level.ChunkMap.TrackedEntity ete = this.moonrise$getTrackedEntity(); // Folia - region threading - if (ete != null) { - net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this); - net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket.teleport(this.getId(), net.minecraft.world.entity.PositionMoveRotation.of(this), java.util.Set.of(), this.onGround); -diff --git a/net/minecraft/world/entity/monster/Vex.java b/net/minecraft/world/entity/monster/Vex.java -index 7f1cdea810db24182f8f87076c42a19b1b43e98a..c41901c98687c25f8ff7e5eb3a052aeb9744640a 100644 ---- a/net/minecraft/world/entity/monster/Vex.java -+++ b/net/minecraft/world/entity/monster/Vex.java -@@ -349,7 +349,7 @@ public class Vex extends Monster implements TraceableEntity { - @Override - public void tick() { - BlockPos boundOrigin = Vex.this.getBoundOrigin(); -- if (boundOrigin == null) { -+ if (boundOrigin == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)Vex.this.level(), boundOrigin)) { // Folia - region threading - boundOrigin = Vex.this.blockPosition(); - } - -diff --git a/net/minecraft/world/entity/monster/ZombieVillager.java b/net/minecraft/world/entity/monster/ZombieVillager.java -index 9061e0b6544d6a31a4dc5b51037f608031a00553..76fa0a25fe084f17045f72a1750c6e8b1eb7cb14 100644 ---- a/net/minecraft/world/entity/monster/ZombieVillager.java -+++ b/net/minecraft/world/entity/monster/ZombieVillager.java -@@ -69,7 +69,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { - @Nullable - private MerchantOffers tradeOffers; - private int villagerXp; -- private int lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - add field -+ //private int lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers - - public ZombieVillager(EntityType entityType, Level level) { - super(entityType, level); -@@ -149,7 +149,7 @@ public class ZombieVillager extends Zombie implements VillagerDataHolder { - } - - super.tick(); -- this.lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit -+ //this.lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit // Folia - region threading - restore original timers - } - - @Override -diff --git a/net/minecraft/world/entity/npc/AbstractVillager.java b/net/minecraft/world/entity/npc/AbstractVillager.java -index a71d16d968bb90fd7aca6f01a3dd56df4f9a7ce6..27ce04ecee778b73711ee55c7c75c541e1f86c38 100644 ---- a/net/minecraft/world/entity/npc/AbstractVillager.java -+++ b/net/minecraft/world/entity/npc/AbstractVillager.java -@@ -218,10 +218,18 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa - this.readInventoryFromTag(compound, this.registryAccess()); - } - -+ // Folia start - region threading -+ @Override -+ public void preChangeDimension() { -+ super.preChangeDimension(); -+ this.stopTrading(); -+ } -+ // Folia end - region threading -+ - @Nullable - @Override - public Entity teleport(TeleportTransition teleportTransition) { -- this.stopTrading(); -+ this.preChangeDimension(); // Folia - region threading - move into preChangeDimension - return super.teleport(teleportTransition); - } - -diff --git a/net/minecraft/world/entity/npc/CatSpawner.java b/net/minecraft/world/entity/npc/CatSpawner.java -index e6d368bc601357cfca694ce328c8e6e47491f3b5..010bee26dfdf5cad186fa57c030540693ff71f23 100644 ---- a/net/minecraft/world/entity/npc/CatSpawner.java -+++ b/net/minecraft/world/entity/npc/CatSpawner.java -@@ -18,17 +18,18 @@ import net.minecraft.world.phys.AABB; - - public class CatSpawner implements CustomSpawner { - private static final int TICK_DELAY = 1200; -- private int nextTick; -+ //private int nextTick; // Folia - region threading - - @Override - public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) { - if (spawnPassives && level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { -- this.nextTick--; -- if (this.nextTick > 0) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading -+ worldData.catSpawnerNextTick--; // Folia - region threading -+ if (worldData.catSpawnerNextTick > 0) { // Folia - region threading - return 0; - } else { -- this.nextTick = 1200; -- Player randomPlayer = level.getRandomPlayer(); -+ worldData.catSpawnerNextTick = 1200; // Folia - region threading -+ Player randomPlayer = level.getRandomLocalPlayer(); // Folia - region threading - if (randomPlayer == null) { - return 0; - } else { -diff --git a/net/minecraft/world/entity/npc/Villager.java b/net/minecraft/world/entity/npc/Villager.java -index 2b83262e4a13eae86df82913ce4f3121e3631a43..7ea74aeb905b95e5919d74df5fbc5e8f7a9985e3 100644 ---- a/net/minecraft/world/entity/npc/Villager.java -+++ b/net/minecraft/world/entity/npc/Villager.java -@@ -246,7 +246,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler - villagerBrain.setCoreActivities(ImmutableSet.of(Activity.CORE)); - villagerBrain.setDefaultActivity(Activity.IDLE); - villagerBrain.setActiveActivityIfPossible(Activity.IDLE); -- villagerBrain.updateActivityFromSchedule(this.level().getDayTime(), this.level().getGameTime()); -+ villagerBrain.updateActivityFromSchedule(this.level().getLevelData().getDayTime(), this.level().getLevelData().getGameTime()); // Folia - region threading - not in the world yet - } - - @Override -@@ -693,6 +693,8 @@ public class Villager extends AbstractVillager implements ReputationEventHandler - this.brain.getMemory(moduleType).ifPresent(globalPos -> { - ServerLevel level = server.getLevel(globalPos.dimension()); - if (level != null) { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( // Folia - region threading -+ level, globalPos.pos().getX() >> 4, globalPos.pos().getZ() >> 4, () -> { // Folia - region threading - PoiManager poiManager = level.getPoiManager(); - Optional> type = poiManager.getType(globalPos.pos()); - BiPredicate> biPredicate = POI_MEMORIES.get(moduleType); -@@ -700,6 +702,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler - poiManager.release(globalPos.pos()); - DebugPackets.sendPoiTicketCountPacket(level, globalPos.pos()); - } -+ }); // Folia - region threading - } - }); - } -diff --git a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java -index ef2afb17a22a703470e13d12c989a685e72f0ab8..984ac8efa2ed45be614e04eab8247481e3a08525 100644 ---- a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java -+++ b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java -@@ -30,16 +30,14 @@ public class WanderingTraderSpawner implements CustomSpawner { - private static final int SPAWN_CHANCE_INCREASE = 25; - private static final int SPAWN_ONE_IN_X_CHANCE = 10; - private static final int NUMBER_OF_SPAWN_ATTEMPTS = 10; -- private final RandomSource random = RandomSource.create(); -+ private final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading - private final ServerLevelData serverLevelData; -- private int tickDelay; -- private int spawnDelay; -- private int spawnChance; -+ // Folia - region threading - - public WanderingTraderSpawner(ServerLevelData serverLevelData) { - this.serverLevelData = serverLevelData; - // Paper start - Add Wandering Trader spawn rate config options -- this.tickDelay = Integer.MIN_VALUE; -+ //this.tickDelay = Integer.MIN_VALUE; // Folia - region threading - moved to regionisedworlddata - // this.spawnDelay = serverLevelData.getWanderingTraderSpawnDelay(); - // this.spawnChance = serverLevelData.getWanderingTraderSpawnChance(); - // if (this.spawnDelay == 0 && this.spawnChance == 0) { -@@ -53,35 +51,36 @@ public class WanderingTraderSpawner implements CustomSpawner { - - @Override - public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading - // Paper start - Add Wandering Trader spawn rate config options -- if (this.tickDelay == Integer.MIN_VALUE) { -- this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; -- this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; -- this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; -+ if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Folia - region threading -+ worldData.wanderingTraderTickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading -+ worldData.wanderingTraderSpawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading -+ worldData.wanderingTraderSpawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading - } - if (!level.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) { - return 0; -- } else if (--this.tickDelay - 1 > 0) { -- this.tickDelay = this.tickDelay - 1; -+ } else if (--worldData.wanderingTraderTickDelay - 1 > 0) { // Folia - region threading -+ worldData.wanderingTraderTickDelay = worldData.wanderingTraderTickDelay - 1; // Folia - region threading - return 0; - } else { -- this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; -- this.spawnDelay = this.spawnDelay - level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; -+ worldData.wanderingTraderTickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading -+ worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading - //this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways -- if (this.spawnDelay > 0) { -+ if (worldData.wanderingTraderSpawnDelay > 0) { // Folia - region threading - return 0; - } else { -- this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; -+ worldData.wanderingTraderSpawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading - if (!level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { - return 0; - } else { -- int i = this.spawnChance; -- this.spawnChance = Mth.clamp(this.spawnChance + level.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); -+ int i = worldData.wanderingTraderSpawnChance; // Folia - region threading -+ worldData.wanderingTraderSpawnChance = Mth.clamp(worldData.wanderingTraderSpawnChance + level.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); // Folia - region threading - //this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways - if (this.random.nextInt(100) > i) { - return 0; - } else if (this.spawn(level)) { -- this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; -+ worldData.wanderingTraderSpawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading - // Paper end - Add Wandering Trader spawn rate config options - return 1; - } else { -@@ -93,7 +92,7 @@ public class WanderingTraderSpawner implements CustomSpawner { - } - - private boolean spawn(ServerLevel serverLevel) { -- Player randomPlayer = serverLevel.getRandomPlayer(); -+ Player randomPlayer = serverLevel.getRandomLocalPlayer(); // Folia - region threading - if (randomPlayer == null) { - return true; - } else if (this.random.nextInt(10) != 0) { -@@ -116,7 +115,7 @@ public class WanderingTraderSpawner implements CustomSpawner { - this.tryToSpawnLlamaFor(serverLevel, wanderingTrader, 4); - } - -- this.serverLevelData.setWanderingTraderId(wanderingTrader.getUUID()); -+ //this.serverLevelData.setWanderingTraderId(wanderingTrader.getUUID()); // Folia - region threading - doesn't appear to be used anywhere, so avoid the race condition here... - // wanderingTrader.setDespawnDelay(48000); // Paper - moved above, modifiable by plugins on CreatureSpawnEvent - wanderingTrader.setWanderTarget(blockPos1); - wanderingTrader.restrictTo(blockPos1, 16); -diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java -index 3ae542153bf1538d17e7c0fe6acc9e7f8605750c..eaa77200d6bc33faeefdc2d07b73ee7ddcd3afe8 100644 ---- a/net/minecraft/world/entity/player/Player.java -+++ b/net/minecraft/world/entity/player/Player.java -@@ -1504,6 +1504,14 @@ public abstract class Player extends LivingEntity { - } - } - -+ // Folia start - region threading -+ @Override -+ protected void preRemove(RemovalReason reason) { -+ super.preRemove(reason); -+ this.fishing = null; -+ } -+ // Folia end - region threading -+ - public boolean isLocalPlayer() { - return false; - } -diff --git a/net/minecraft/world/entity/projectile/AbstractArrow.java b/net/minecraft/world/entity/projectile/AbstractArrow.java -index 541ee32182b595de7dd6717f8faea00d53c105a3..4758f7063b0da1731899f8f6b4de97ae7b7fce55 100644 ---- a/net/minecraft/world/entity/projectile/AbstractArrow.java -+++ b/net/minecraft/world/entity/projectile/AbstractArrow.java -@@ -176,6 +176,11 @@ public abstract class AbstractArrow extends Projectile { - - @Override - public void tick() { -+ // Folia start - region threading - make sure entities do not move into regions they do not own -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { -+ return; -+ } -+ // Folia end - region threading - make sure entities do not move into regions they do not own - boolean flag = !this.isNoPhysics(); - Vec3 deltaMovement = this.getDeltaMovement(); - BlockPos blockPos = this.blockPosition(); -diff --git a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java -index 9a99b813de8b606fab26c87086a21372e5172ba3..4eeb1017576d23d206a7a47b9e9e74b19465b2ae 100644 ---- a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java -+++ b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java -@@ -80,6 +80,11 @@ public abstract class AbstractHurtingProjectile extends Projectile { - this.setPos(location); - this.applyEffectsFromBlocks(); - super.tick(); -+ // Folia start - region threading - make sure entities do not move into regions they do not own -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { -+ return; -+ } -+ // Folia end - region threading - make sure entities do not move into regions they do not own - if (this.shouldBurn()) { - this.igniteForSeconds(1.0F); - } -diff --git a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java -index 774ca9e0b56fd175ae246051de762d0c4256ca58..0cfd2c937f93f1acb4afc01251f882710baf2591 100644 ---- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java -+++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java -@@ -130,6 +130,11 @@ public class FireworkRocketEntity extends Projectile implements ItemSupplier { - } - }); - } -+ // Folia start - region threading -+ if (this.attachedToEntity != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.attachedToEntity)) { -+ this.attachedToEntity = null; -+ } -+ // Folia end - region threading - - if (this.attachedToEntity != null) { - Vec3 handHoldingItemAngle; -diff --git a/net/minecraft/world/entity/projectile/FishingHook.java b/net/minecraft/world/entity/projectile/FishingHook.java -index 1e012c7ef699a64ff3f1b00f897bb893ab25ecbd..f9d7514764850fd02ed5853ba2fdf8ada40ce756 100644 ---- a/net/minecraft/world/entity/projectile/FishingHook.java -+++ b/net/minecraft/world/entity/projectile/FishingHook.java -@@ -94,7 +94,7 @@ public class FishingHook extends Projectile { - - public FishingHook(Player player, Level level, int luck, int lureSpeed) { - this(EntityType.FISHING_BOBBER, level, luck, lureSpeed); -- this.setOwner(player); -+ //this.setOwner(player); // Folia - region threading - move this down after position so that thread-checks do not fail - float xRot = player.getXRot(); - float yRot = player.getYRot(); - float cos = Mth.cos(-yRot * (float) (Math.PI / 180.0) - (float) Math.PI); -@@ -105,6 +105,7 @@ public class FishingHook extends Projectile { - double eyeY = player.getEyeY(); - double d1 = player.getZ() - cos * 0.3; - this.moveTo(d, eyeY, d1, yRot, xRot); -+ this.setOwner(player); // Folia - region threading - move this down after position so that thread-checks do not fail - Vec3 vec3 = new Vec3(-sin, Mth.clamp(-(sin1 / f), -5.0F, 5.0F), -cos); - double len = vec3.length(); - vec3 = vec3.multiply( -@@ -260,6 +261,11 @@ public class FishingHook extends Projectile { - } - - private boolean shouldStopFishing(Player player) { -+ // Folia start - region threading -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { -+ return true; -+ } -+ // Folia end - region threading - ItemStack mainHandItem = player.getMainHandItem(); - ItemStack offhandItem = player.getOffhandItem(); - boolean isFishingRod = mainHandItem.is(Items.FISHING_ROD); -@@ -623,10 +629,18 @@ public class FishingHook extends Projectile { - @Override - public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) { - // CraftBukkit end -- this.updateOwnerInfo(null); -+ //this.updateOwnerInfo(null); // Folia - region threading - move into preRemove - super.remove(reason, cause); // CraftBukkit - add Bukkit remove cause - } - -+ // Folia start - region threading -+ @Override -+ protected void preRemove(RemovalReason reason) { -+ super.preRemove(reason); -+ this.updateOwnerInfo(null); -+ } -+ // Folia end - region threading -+ - @Override - public void onClientRemoval() { - this.updateOwnerInfo(null); -diff --git a/net/minecraft/world/entity/projectile/LlamaSpit.java b/net/minecraft/world/entity/projectile/LlamaSpit.java -index 4880db97135d54fa72f64c108b2bd4ded096438b..dc6ec52a513e2754a81733de5f389d6ada5215cc 100644 ---- a/net/minecraft/world/entity/projectile/LlamaSpit.java -+++ b/net/minecraft/world/entity/projectile/LlamaSpit.java -@@ -41,6 +41,11 @@ public class LlamaSpit extends Projectile { - @Override - public void tick() { - super.tick(); -+ // Folia start - region threading - make sure entities do not move into regions they do not own -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { -+ return; -+ } -+ // Folia end - region threading - make sure entities do not move into regions they do not own - Vec3 deltaMovement = this.getDeltaMovement(); - HitResult hitResultOnMoveVector = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity); - this.preHitTargetOrDeflectSelf(hitResultOnMoveVector); // CraftBukkit - projectile hit event -diff --git a/net/minecraft/world/entity/projectile/Projectile.java b/net/minecraft/world/entity/projectile/Projectile.java -index af71a71ff11b418a43728fd464b1e673d593140f..20f8aed59ac953cac3029115f35e496f9784bec4 100644 ---- a/net/minecraft/world/entity/projectile/Projectile.java -+++ b/net/minecraft/world/entity/projectile/Projectile.java -@@ -38,7 +38,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { - @Nullable - public UUID ownerUUID; - @Nullable -- public Entity cachedOwner; -+ public org.bukkit.craftbukkit.entity.CraftEntity cachedOwner; // Folia - region threading - replace with CraftEntity - public boolean leftOwner; - public boolean hasBeenShot; - @Nullable -@@ -52,7 +52,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { - public void setOwner(@Nullable Entity owner) { - if (owner != null) { - this.ownerUUID = owner.getUUID(); -- this.cachedOwner = owner; -+ this.cachedOwner = owner.getBukkitEntity(); // Folia - region threading - } - // Paper start - Refresh ProjectileSource for projectiles - else { -@@ -69,22 +69,38 @@ public abstract class Projectile extends Entity implements TraceableEntity { - if (fillCache) { - this.getOwner(); - } -- if (this.cachedOwner != null && !this.cachedOwner.isRemoved() && this.projectileSource == null && this.cachedOwner.getBukkitEntity() instanceof org.bukkit.projectiles.ProjectileSource projSource) { -+ if (this.cachedOwner != null && !this.cachedOwner.getHandleRaw().isRemoved() && this.projectileSource == null && this.cachedOwner instanceof org.bukkit.projectiles.ProjectileSource projSource) { // Folia - region threading - this.projectileSource = projSource; - } - } - // Paper end - Refresh ProjectileSource for projectiles - -+ // Folia start - region threading -+ // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner. -+ // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior. - @Nullable - @Override - public Entity getOwner() { -- if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) { -+ Entity ret = this.getOwnerRaw(); -+ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(ret) && (ret == null || !ret.isRemoved()) ? ret : null; -+ } -+ // Folia end - region threading -+ -+ @Nullable -+ public Entity getOwnerRaw() { // Folia - region threading -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot update owner state asynchronously"); // Folia - region threading -+ if (this.cachedOwner != null && !this.cachedOwner.isPurged()) { // Folia - region threading - this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles -- return this.cachedOwner; -+ return this.cachedOwner.getHandleRaw(); // Folia - region threading - } else if (this.ownerUUID != null) { -- this.cachedOwner = this.findOwner(this.ownerUUID); -+ // Folia start - region threading -+ Entity ret = this.findOwner(this.ownerUUID); -+ if (ret != null) { -+ this.cachedOwner = ret.getBukkitEntity(); -+ } -+ // Folia end - region threading - this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles -- return this.cachedOwner; -+ return ret; // Folia - region threading - } else { - return null; - } -@@ -130,7 +146,12 @@ public abstract class Projectile extends Entity implements TraceableEntity { - protected void setOwnerThroughUUID(UUID uuid) { - if (this.ownerUUID != uuid) { - this.ownerUUID = uuid; -- this.cachedOwner = this.findOwner(uuid); -+ // Folia start - region threading -+ Entity cachedOwner = this.findOwner(this.ownerUUID); -+ if (cachedOwner != null) { -+ this.cachedOwner = cachedOwner.getBukkitEntity(); -+ } -+ // Folia end - region threading - } - } - -@@ -451,7 +472,7 @@ public abstract class Projectile extends Entity implements TraceableEntity { - @Override - public boolean mayInteract(ServerLevel level, BlockPos pos) { - Entity owner = this.getOwner(); -- return owner instanceof Player ? owner.mayInteract(level, pos) : owner == null || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); -+ return owner instanceof Player && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(owner) ? owner.mayInteract(level, pos) : owner == null || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Folia - region threading - } - - public boolean mayBreak(ServerLevel level) { -diff --git a/net/minecraft/world/entity/projectile/SmallFireball.java b/net/minecraft/world/entity/projectile/SmallFireball.java -index 8c84cea43fc0e42a576004663670977eac99f1a6..ba70ce3df630532b646eab0a5fabca15d67c379b 100644 ---- a/net/minecraft/world/entity/projectile/SmallFireball.java -+++ b/net/minecraft/world/entity/projectile/SmallFireball.java -@@ -24,7 +24,7 @@ public class SmallFireball extends Fireball { - public SmallFireball(Level level, LivingEntity owner, Vec3 movement) { - super(EntityType.SMALL_FIREBALL, owner, movement, level); - // CraftBukkit start -- if (this.getOwner() != null && this.getOwner() instanceof Mob) { -+ if (owner != null && this.getOwner() != null && this.getOwner() instanceof Mob) { // Folia - region threading - this.isIncendiary = (level instanceof ServerLevel serverLevel) && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); - } - // CraftBukkit end -diff --git a/net/minecraft/world/entity/projectile/ThrowableProjectile.java b/net/minecraft/world/entity/projectile/ThrowableProjectile.java -index f9fa2866cb28622785b4fcd54c0e2989569a401a..74590ac276965543c2d78fe85090097c8d3a7aed 100644 ---- a/net/minecraft/world/entity/projectile/ThrowableProjectile.java -+++ b/net/minecraft/world/entity/projectile/ThrowableProjectile.java -@@ -43,6 +43,11 @@ public abstract class ThrowableProjectile extends Projectile { - - @Override - public void tick() { -+ // Folia start - region threading - make sure entities do not move into regions they do not own -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { -+ return; -+ } -+ // Folia end - region threading - make sure entities do not move into regions they do not own - this.handleFirstTickBubbleColumn(); - this.applyGravity(); - this.applyInertia(); -diff --git a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java -index 128bf1555b996c454e753b1ed004cfeae4b0436f..12e563b04e6afcd227f3ef6cbdfcedf59be9509e 100644 ---- a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java -+++ b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java -@@ -99,6 +99,81 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { - result.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F); - } - -+ // Folia start - region threading -+ private static void attemptTeleport(Entity source, ServerLevel checkWorld, net.minecraft.world.phys.Vec3 to) { -+ final boolean onPortalCooldown = source.isOnPortalCooldown(); -+ // ignore retired callback, in those cases we do not want to teleport -+ source.getBukkitEntity().taskScheduler.schedule( -+ (Entity entity) -> { -+ if (!isAllowedToTeleportOwner(entity, checkWorld)) { -+ return; -+ } -+ // source is now an invalid reference, do not use it, use the entity parameter -+ net.minecraft.world.phys.Vec3 endermitePos = entity.position(); -+ -+ // dismount from any vehicles, so we can teleport and to prevent desync -+ if (entity.isPassenger()) { -+ entity.unRide(); -+ } -+ -+ if (onPortalCooldown) { -+ entity.setPortalCooldown(); -+ } -+ -+ entity.teleportAsync( -+ checkWorld, to, null, null, null, -+ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.ENDER_PEARL, -+ // chunk could have been unloaded -+ Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS | Entity.TELEPORT_FLAG_LOAD_CHUNK, -+ (Entity teleported) -> { -+ // entity is now an invalid reference, do not use it, instead use teleported -+ if (teleported instanceof ServerPlayer player) { -+ // connection teleport is already done -+ ServerLevel world = player.serverLevel(); -+ -+ // endermite spawn chance -+ if (world.random.nextFloat() < 0.05F && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { -+ Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(world, EntitySpawnReason.TRIGGERED); -+ -+ if (entityendermite != null) { -+ float yRot = teleported.getYRot(); -+ float xRot = teleported.getXRot(); -+ Runnable spawn = () -> { -+ entityendermite.moveTo(endermitePos.x, endermitePos.y, endermitePos.z, yRot, xRot); -+ world.addFreshEntity(entityendermite, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.ENDER_PEARL); -+ }; -+ -+ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, endermitePos, net.minecraft.world.phys.Vec3.ZERO, 1)) { -+ spawn.run(); -+ } else { -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ world, -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.x), -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.z), -+ spawn -+ ); -+ } -+ } -+ } -+ -+ // damage player -+ teleported.resetFallDistance(); -+ player.resetCurrentImpulseContext(); -+ player.hurtServer(player.serverLevel(), player.damageSources().enderPearl().customEventDamager(player), 5.0F); // CraftBukkit // Paper - fix DamageSource API -+ playSound(teleported.level(), to); -+ } else { -+ // reset fall damage so that if the entity was falling they do not instantly die -+ teleported.resetFallDistance(); -+ playSound(teleported.level(), to); -+ } -+ } -+ ); -+ }, -+ null, 1L -+ ); -+ } -+ // Folia end - region threading -+ - @Override - protected void onHit(HitResult result) { - super.onHit(result); -@@ -117,6 +192,20 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { - } - - if (this.level() instanceof ServerLevel serverLevel && !this.isRemoved()) { -+ // Folia start - region threading -+ if (true) { -+ // we can't fire events, because we do not actually know where the other entity is located -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this)) { -+ throw new IllegalStateException("Must be on tick thread for ticking entity: " + this); -+ } -+ Entity entity = this.getOwnerRaw(); -+ if (entity != null) { -+ attemptTeleport(entity, (ServerLevel)this.level(), this.position()); -+ } -+ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); -+ return; -+ } -+ // Folia end - region threading - Entity owner = this.getOwner(); - if (owner != null && isAllowedToTeleportOwner(owner, serverLevel)) { - if (owner.isPassenger()) { -@@ -212,7 +301,15 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { - } - } - -- private void playSound(Level level, Vec3 pos) { -+ // Folia start - region threading -+ @Override -+ public void preChangeDimension() { -+ super.preChangeDimension(); -+ // Don't change the owner here, since the tick logic will consider it anyways. -+ } -+ // Folia end - region threading -+ -+ private static void playSound(Level level, Vec3 pos) { // Folia - region threading - static - level.playSound(null, pos.x, pos.y, pos.z, SoundEvents.PLAYER_TELEPORT, SoundSource.PLAYERS); - } - -diff --git a/net/minecraft/world/entity/raid/Raid.java b/net/minecraft/world/entity/raid/Raid.java -index 6e8c1a2863ac6e5137a26815ecf5142f0fcc9893..b7045b2d4665c72d0c4849c711be4e44f7d17ad3 100644 ---- a/net/minecraft/world/entity/raid/Raid.java -+++ b/net/minecraft/world/entity/raid/Raid.java -@@ -110,6 +110,13 @@ public class Raid { - public final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(PDC_TYPE_REGISTRY); - // Paper end - -+ // Folia start - make raids thread-safe -+ public boolean ownsRaid() { -+ BlockPos center = this.getCenter(); -+ return center != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, center.getX() >> 4, center.getZ() >> 4, 8); -+ } -+ // Folia end - make raids thread-safe -+ - public Raid(int id, ServerLevel level, BlockPos center) { - this.id = id; - this.level = level; -@@ -207,7 +214,7 @@ public class Raid { - private Predicate validPlayer() { - return player -> { - BlockPos blockPos = player.blockPosition(); -- return player.isAlive() && this.level.getRaidAt(blockPos) == this; -+ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) && player.isAlive() && this.level.getRaidAt(blockPos) == this; // Folia - make raids thread-safe - }; - } - -@@ -496,7 +503,7 @@ public class Raid { - Collection players = this.raidEvent.getPlayers(); - long randomLong = this.random.nextLong(); - -- for (ServerPlayer serverPlayer : this.level.players()) { -+ for (ServerPlayer serverPlayer : this.level.getLocalPlayers()) { // Folia - region threading - Vec3 vec3 = serverPlayer.position(); - Vec3 vec31 = Vec3.atCenterOf(pos); - double squareRoot = Math.sqrt((vec31.x - vec3.x) * (vec31.x - vec3.x) + (vec31.z - vec3.z) * (vec31.z - vec3.z)); -diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java -index 8270d76a753bfd26a4c8ef6610bee5c24ee59cfe..7c385baae81b9a987c0e1e4deb017884600331bc 100644 ---- a/net/minecraft/world/entity/raid/Raider.java -+++ b/net/minecraft/world/entity/raid/Raider.java -@@ -86,7 +86,7 @@ public abstract class Raider extends PatrollingMonster { - Raid currentRaid = this.getCurrentRaid(); - if (this.canJoinRaid()) { - if (currentRaid == null) { -- if (this.level().getGameTime() % 20L == 0L) { -+ if (this.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading - Raid raidAt = ((ServerLevel)this.level()).getRaidAt(this.blockPosition()); - if (raidAt != null && Raids.canJoinRaid(this, raidAt)) { - raidAt.joinRaid(raidAt.getGroupsSpawned(), this, null, true); -diff --git a/net/minecraft/world/entity/raid/Raids.java b/net/minecraft/world/entity/raid/Raids.java -index 34eb038725d1577f1a2d7c35c897b1270eac5749..0ffc1956d9e808871c5b36f6eb5ed750abaa880c 100644 ---- a/net/minecraft/world/entity/raid/Raids.java -+++ b/net/minecraft/world/entity/raid/Raids.java -@@ -25,9 +25,9 @@ import net.minecraft.world.phys.Vec3; - - public class Raids extends SavedData { - private static final String RAID_FILE_ID = "raids"; -- public final Map raidMap = Maps.newHashMap(); -+ public final Map raidMap = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - make raids thread-safe - private final ServerLevel level; -- private int nextAvailableID; -+ private final java.util.concurrent.atomic.AtomicInteger nextAvailableID = new java.util.concurrent.atomic.AtomicInteger(); // Folia - make raids thread-safe - private int tick; - - public static SavedData.Factory factory(ServerLevel level) { -@@ -36,7 +36,7 @@ public class Raids extends SavedData { - - public Raids(ServerLevel level) { - this.level = level; -- this.nextAvailableID = 1; -+ this.nextAvailableID.set(1); // Folia - make raids thread-safe - this.setDirty(); - } - -@@ -44,12 +44,25 @@ public class Raids extends SavedData { - return this.raidMap.get(id); - } - -+ // Folia start - make raids thread-safe -+ public void globalTick() { -+ ++this.tick; -+ if (this.tick % 200 == 0) { -+ this.setDirty(); -+ } -+ } -+ - public void tick() { -- this.tick++; -+ // Folia end - make raids thread-safe - Iterator iterator = this.raidMap.values().iterator(); - - while (iterator.hasNext()) { - Raid raid = iterator.next(); -+ // Folia start - make raids thread-safe -+ if (!raid.ownsRaid()) { -+ continue; -+ } -+ // Folia end - make raids thread-safe - if (this.level.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS)) { - raid.stop(); - } -@@ -62,14 +75,17 @@ public class Raids extends SavedData { - } - } - -- if (this.tick % 200 == 0) { -- this.setDirty(); -- } -+ // Folia - make raids thread-safe - move to globalTick() - - DebugPackets.sendRaids(this.level, this.raidMap.values()); - } - - public static boolean canJoinRaid(Raider raider, Raid raid) { -+ // Folia start - make raids thread-safe -+ if (!raid.ownsRaid()) { -+ return false; -+ } -+ // Folia end - make raids thread-safe - return raider != null - && raid != null - && raid.getLevel() != null -@@ -87,7 +103,7 @@ public class Raids extends SavedData { - return null; - } else { - DimensionType dimensionType = player.level().dimensionType(); -- if (!dimensionType.hasRaids()) { -+ if (!dimensionType.hasRaids() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, pos.getX() >> 4, pos.getZ() >> 4, 8) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, player.chunkPosition().x, player.chunkPosition().z, 8)) { // Folia - region threading - return null; - } else { - List list = this.level -@@ -145,7 +161,7 @@ public class Raids extends SavedData { - - public static Raids load(ServerLevel level, CompoundTag tag) { - Raids raids = new Raids(level); -- raids.nextAvailableID = tag.getInt("NextAvailableID"); -+ raids.nextAvailableID.set(tag.getInt("NextAvailableID")); // Folia - make raids thread-safe - raids.tick = tag.getInt("Tick"); - ListTag list = tag.getList("Raids", 10); - -@@ -160,7 +176,7 @@ public class Raids extends SavedData { - - @Override - public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { -- tag.putInt("NextAvailableID", this.nextAvailableID); -+ tag.putInt("NextAvailableID", this.nextAvailableID.get()); // Folia - make raids thread-safe - tag.putInt("Tick", this.tick); - ListTag listTag = new ListTag(); - -@@ -179,7 +195,7 @@ public class Raids extends SavedData { - } - - private int getUniqueId() { -- return ++this.nextAvailableID; -+ return this.nextAvailableID.incrementAndGet(); // Folia - make raids thread-safe - } - - @Nullable -@@ -188,6 +204,11 @@ public class Raids extends SavedData { - double d = distance; - - for (Raid raid1 : this.raidMap.values()) { -+ // Folia start - make raids thread-safe -+ if (!raid1.ownsRaid()) { -+ continue; -+ } -+ // Folia end - make raids thread-safe - double d1 = raid1.getCenter().distSqr(pos); - if (raid1.isActive() && d1 < d) { - raid = raid1; -diff --git a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java -index 82421d3b4116ca406cdfffec5a3d65a99cbe294b..3a575ff4860c3b000a23e7754181f48d942441e9 100644 ---- a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java -+++ b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java -@@ -145,5 +145,11 @@ public class MinecartCommandBlock extends AbstractMinecart { - return net.minecraft.world.entity.vehicle.MinecartCommandBlock.this.getBukkitEntity(); - } - // CraftBukkit end -+ // Folia start -+ @Override -+ public void threadCheck() { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(MinecartCommandBlock.this, "Asynchronous sendSystemMessage to a command block"); -+ } -+ // Folia end - } - } -diff --git a/net/minecraft/world/entity/vehicle/MinecartHopper.java b/net/minecraft/world/entity/vehicle/MinecartHopper.java -index 8341e7f01606fca90e69384c16fc19bb9e20d1b7..c07f6fefdba5242c09c0081a0f074948f9df9ae6 100644 ---- a/net/minecraft/world/entity/vehicle/MinecartHopper.java -+++ b/net/minecraft/world/entity/vehicle/MinecartHopper.java -@@ -145,7 +145,7 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper - - // Paper start - public void immunize() { -- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20); -+ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); - } - // Paper end - -diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java -index b511fe8295369ac4014beb351cd2e3f770c10170..264d61426630f6c9c77d39c50006981fd3cd4948 100644 ---- a/net/minecraft/world/item/ItemStack.java -+++ b/net/minecraft/world/item/ItemStack.java -@@ -386,31 +386,32 @@ public final class ItemStack implements DataComponentHolder { - DataComponentPatch previousPatch = this.components.asPatch(); - int oldCount = this.getCount(); - ServerLevel serverLevel = (ServerLevel) context.getLevel(); -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = serverLevel.getCurrentWorldData(); // Folia - region threading - - if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement -- serverLevel.captureBlockStates = true; -+ worldData.captureBlockStates = true; // Folia - region threading - // special case bonemeal - if (item == Items.BONE_MEAL) { -- serverLevel.captureTreeGeneration = true; -+ worldData.captureTreeGeneration = true; // Folia - region threading - } - } - InteractionResult interactionResult; - try { - interactionResult = item.useOn(context); - } finally { -- serverLevel.captureBlockStates = false; -+ worldData.captureBlockStates = false; // Folia - region threading - } - DataComponentPatch newPatch = this.components.asPatch(); - int newCount = this.getCount(); - this.setCount(oldCount); - this.restorePatch(previousPatch); -- if (interactionResult.consumesAction() && serverLevel.captureTreeGeneration && !serverLevel.capturedBlockStates.isEmpty()) { -- serverLevel.captureTreeGeneration = false; -+ if (interactionResult.consumesAction() && worldData.captureTreeGeneration && !worldData.capturedBlockStates.isEmpty()) { // Folia - region threading -+ worldData.captureTreeGeneration = false; // Folia - region threading - org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(clickedPos, serverLevel.getWorld()); -- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; -- net.minecraft.world.level.block.SaplingBlock.treeType = null; -- List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); -- serverLevel.capturedBlockStates.clear(); -+ org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // Folia - region threading -+ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); // Folia - region threading -+ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading -+ worldData.capturedBlockStates.clear(); // Folia - region threading - org.bukkit.event.world.StructureGrowEvent structureEvent = null; - if (treeType != null) { - boolean isBonemeal = this.getItem() == Items.BONE_MEAL; -@@ -436,15 +437,15 @@ public final class ItemStack implements DataComponentHolder { - player.awardStat(Stats.ITEM_USED.get(item)); // SPIGOT-7236 - award stat - } - -- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return -+ SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading - return interactionResult; - } -- serverLevel.captureTreeGeneration = false; -+ worldData.captureTreeGeneration = false; // Folia - region threading - if (player != null && interactionResult instanceof InteractionResult.Success success && success.wasItemInteraction()) { - InteractionHand hand = context.getHand(); - org.bukkit.event.block.BlockPlaceEvent placeEvent = null; -- List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); -- serverLevel.capturedBlockStates.clear(); -+ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading -+ worldData.capturedBlockStates.clear(); // Folia - region threading - if (blocks.size() > 1) { - placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(serverLevel, player, hand, blocks, clickedPos.getX(), clickedPos.getY(), clickedPos.getZ()); - } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement -@@ -455,17 +456,17 @@ public final class ItemStack implements DataComponentHolder { - interactionResult = InteractionResult.FAIL; // cancel placement - // PAIL: Remove this when MC-99075 fixed - placeEvent.getPlayer().updateInventory(); -- serverLevel.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot -+ worldData.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot // Folia - region threading - // revert back all captured blocks -- serverLevel.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 -- serverLevel.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent -+ worldData.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 // Folia - region threading -+ worldData.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading - for (org.bukkit.block.BlockState blockstate : blocks) { - blockstate.update(true, false); - } -- serverLevel.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent -- serverLevel.preventPoiUpdated = false; -+ worldData.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading -+ worldData.preventPoiUpdated = false; // Folia - region threading - -- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return -+ SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading - } else { - // Change the stack to its new contents if it hasn't been tampered with. - if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), previousPatch)) { -@@ -473,7 +474,7 @@ public final class ItemStack implements DataComponentHolder { - this.setCount(newCount); - } - -- for (java.util.Map.Entry e : serverLevel.capturedTileEntities.entrySet()) { -+ for (java.util.Map.Entry e : worldData.capturedTileEntities.entrySet()) { // Folia - region threading - serverLevel.setBlockEntity(e.getValue()); - } - -@@ -508,15 +509,15 @@ public final class ItemStack implements DataComponentHolder { - } - - // SPIGOT-4678 -- if (this.item instanceof SignItem && SignItem.openSign != null) { -+ if (this.item instanceof SignItem && SignItem.openSign.get() != null) { // Folia - region threading - try { -- if (serverLevel.getBlockEntity(SignItem.openSign) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) { -- if (serverLevel.getBlockState(SignItem.openSign).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) { -+ if (serverLevel.getBlockEntity(SignItem.openSign.get()) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) { // Folia - region threading -+ if (serverLevel.getBlockState(SignItem.openSign.get()).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) { // Folia - region threading - signBlock.openTextEdit(player, blockEntity, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // CraftBukkit // Paper - Add PlayerOpenSignEvent - } - } - } finally { -- SignItem.openSign = null; -+ SignItem.openSign.set(null); - } - } - -@@ -544,8 +545,8 @@ public final class ItemStack implements DataComponentHolder { - player.awardStat(Stats.ITEM_USED.get(item)); - } - } -- serverLevel.capturedTileEntities.clear(); -- serverLevel.capturedBlockStates.clear(); -+ worldData.capturedTileEntities.clear(); // Folia - region threading -+ worldData.capturedBlockStates.clear(); // Folia - region threading - // CraftBukkit end - - return interactionResult; -diff --git a/net/minecraft/world/item/MapItem.java b/net/minecraft/world/item/MapItem.java -index 8795d54cff569c911e0a535f38a0ec4130f7b4d5..9f07ce560e265582eec0fff5877a923f62a60e13 100644 ---- a/net/minecraft/world/item/MapItem.java -+++ b/net/minecraft/world/item/MapItem.java -@@ -70,6 +70,7 @@ public class MapItem extends Item { - } - - public void update(Level level, Entity viewer, MapItemSavedData data) { -+ synchronized (data) { // Folia - make map data thread-safe - if (level.dimension() == data.dimension && viewer instanceof Player) { - int i = 1 << data.scale; - int i1 = data.centerX; -@@ -99,8 +100,8 @@ public class MapItem extends Item { - int i9 = (i1 / i + i6 - 64) * i; - int i10 = (i2 / i + i7 - 64) * i; - Multiset multiset = LinkedHashMultiset.create(); -- LevelChunk chunk = level.getChunkIfLoaded(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10)); // Paper - Maps shouldn't load chunks -- if (chunk != null && !chunk.isEmpty()) { // Paper - Maps shouldn't load chunks -+ LevelChunk chunk = level.getChunkIfLoaded(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10)); // Paper - Maps shouldn't load chunks // Folia - super important that it uses getChunkIfLoaded -+ if (chunk != null && !chunk.isEmpty() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, chunk.getPos())) { // Paper - Maps shouldn't load chunks // Folia - make sure chunk is owned - int i11 = 0; - double d1 = 0.0; - if (level.dimensionType().hasCeiling()) { -@@ -182,6 +183,7 @@ public class MapItem extends Item { - } - } - } -+ } // Folia - make map data thread-safe - } - - private BlockState getCorrectStateForFluidBlock(Level level, BlockState state, BlockPos pos) { -@@ -196,6 +198,7 @@ public class MapItem extends Item { - public static void renderBiomePreviewMap(ServerLevel serverLevel, ItemStack stack) { - MapItemSavedData savedData = getSavedData(stack, serverLevel); - if (savedData != null) { -+ synchronized (savedData) { // Folia - make map data thread-safe - if (serverLevel.dimension() == savedData.dimension) { - int i = 1 << savedData.scale; - int i1 = savedData.centerX; -@@ -265,6 +268,7 @@ public class MapItem extends Item { - } - } - } -+ } // Folia - make map data thread-safe - } - } - -@@ -273,6 +277,7 @@ public class MapItem extends Item { - if (!level.isClientSide) { - MapItemSavedData savedData = getSavedData(stack, level); - if (savedData != null) { -+ synchronized (savedData) { // Folia - region threading - if (entity instanceof Player player) { - savedData.tickCarriedBy(player, stack); - } -@@ -280,6 +285,7 @@ public class MapItem extends Item { - if (!savedData.locked && (isSelected || entity instanceof Player && ((Player)entity).getOffhandItem() == stack)) { - this.update(level, entity, savedData); - } -+ } // Folia - region threading - } - } - } -diff --git a/net/minecraft/world/item/SignItem.java b/net/minecraft/world/item/SignItem.java -index fffac12db30d4321981959a9149cc56f8b4f6df6..fdf4fa92a5ca98fae6266e29a54fb1b77e69407c 100644 ---- a/net/minecraft/world/item/SignItem.java -+++ b/net/minecraft/world/item/SignItem.java -@@ -11,7 +11,7 @@ import net.minecraft.world.level.block.entity.SignBlockEntity; - import net.minecraft.world.level.block.state.BlockState; - - public class SignItem extends StandingAndWallBlockItem { -- public static BlockPos openSign; // CraftBukkit -+ public static final ThreadLocal openSign = new ThreadLocal<>(); // CraftBukkit // Folia - region threading - public SignItem(Block standingBlock, Block wallBlock, Item.Properties properties) { - super(standingBlock, wallBlock, Direction.DOWN, properties); - } -@@ -30,7 +30,7 @@ public class SignItem extends StandingAndWallBlockItem { - && level.getBlockState(pos).getBlock() instanceof SignBlock signBlock) { - // CraftBukkit start - SPIGOT-4678 - // signBlock.openTextEdit(player, signBlockEntity, true); -- SignItem.openSign = pos; -+ SignItem.openSign.set(pos); // Folia - region threading - // CraftBukkit end - } - -diff --git a/net/minecraft/world/level/BaseCommandBlock.java b/net/minecraft/world/level/BaseCommandBlock.java -index a67d40eb4bfa85888af8bf027a8859378d290cfa..b02b79ccedb8b87bc22270377dfc36e21ebe1724 100644 ---- a/net/minecraft/world/level/BaseCommandBlock.java -+++ b/net/minecraft/world/level/BaseCommandBlock.java -@@ -21,7 +21,7 @@ import net.minecraft.world.level.block.entity.BlockEntity; - import net.minecraft.world.phys.Vec3; - - public abstract class BaseCommandBlock implements CommandSource { -- private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); -+ private static final ThreadLocal TIME_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss")); // Folia - region threading - SDF is not thread-safe - private static final Component DEFAULT_NAME = Component.literal("@"); - private long lastExecution = -1L; - private boolean updateLastExecution = true; -@@ -114,6 +114,7 @@ public abstract class BaseCommandBlock implements CommandSource { - } - - public boolean performCommand(Level level) { -+ if (true) return false; // Folia - region threading - if (level.isClientSide || level.getGameTime() == this.lastExecution) { - return false; - } else if ("Searge".equalsIgnoreCase(this.command)) { -@@ -164,11 +165,14 @@ public abstract class BaseCommandBlock implements CommandSource { - this.customName = customName; - } - -+ public void threadCheck() {} // Folia -+ - @Override - public void sendSystemMessage(Component component) { - if (this.trackOutput) { - org.spigotmc.AsyncCatcher.catchOp("sendSystemMessage to a command block"); // Paper - Don't broadcast messages to command blocks -- this.lastOutput = Component.literal("[" + TIME_FORMAT.format(new Date()) + "] ").append(component); -+ this.threadCheck(); // Folia -+ this.lastOutput = Component.literal("[" + TIME_FORMAT.get().format(new Date()) + "] ").append(component); // Folia - region threading - SDF is not thread-safe - this.onUpdated(); - } - } -diff --git a/net/minecraft/world/level/EntityGetter.java b/net/minecraft/world/level/EntityGetter.java -index e81195df621159da67136f020fa7a6d39d1ee5ed..b9d4f766f30612add79974cfbf123d1b4684dc1c 100644 ---- a/net/minecraft/world/level/EntityGetter.java -+++ b/net/minecraft/world/level/EntityGetter.java -@@ -24,6 +24,12 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst - return this.getEntities(EntityTypeTest.forClass(entityClass), area, filter); - } - -+ // Folia start - region threading -+ default List getLocalPlayers() { -+ return java.util.Collections.emptyList(); -+ } -+ // Folia end - region threading -+ - List players(); - - default List getEntities(@Nullable Entity entity, AABB area) { -@@ -123,7 +129,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst - double d = -1.0; - Player player = null; - -- for (Player player1 : this.players()) { -+ for (Player player1 : this.getLocalPlayers()) { // Folia - region threading - if (predicate == null || predicate.test(player1)) { - double d1 = player1.distanceToSqr(x, y, z); - if ((distance < 0.0 || d1 < distance * distance) && (d == -1.0 || d1 < d)) { -@@ -144,7 +150,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst - default List findNearbyBukkitPlayers(double x, double y, double z, double radius, @Nullable Predicate predicate) { - com.google.common.collect.ImmutableList.Builder builder = com.google.common.collect.ImmutableList.builder(); - -- for (Player human : this.players()) { -+ for (Player human : this.getLocalPlayers()) { // Folia - region threading - if (predicate == null || predicate.test(human)) { - double distanceSquared = human.distanceToSqr(x, y, z); - -@@ -171,7 +177,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst - - // Paper start - Affects Spawning API - default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) { -- for (Player player : this.players()) { -+ for (Player player : this.getLocalPlayers()) { // Folia - region threading - if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check - double distanceSqr = player.distanceToSqr(x, y, z); - if (range < 0.0D || distanceSqr < range * range) { -@@ -184,7 +190,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst - // Paper end - Affects Spawning API - - default boolean hasNearbyAlivePlayer(double x, double y, double z, double distance) { -- for (Player player : this.players()) { -+ for (Player player : this.getLocalPlayers()) { // Folia - region threading - if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { - double d = player.distanceToSqr(x, y, z); - if (distance < 0.0 || d < distance * distance) { -@@ -198,8 +204,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst - - @Nullable - default Player getPlayerByUUID(UUID uniqueId) { -- for (int i = 0; i < this.players().size(); i++) { -- Player player = this.players().get(i); -+ for (Player player : this.getLocalPlayers()) { // Folia - region threading - if (uniqueId.equals(player.getUUID())) { - return player; - } -diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java -index 2bbebb4335d927f240abcac67a5b423e38dc33d7..d36b5ad7b386391e617895a33b919e29266220e2 100644 ---- a/net/minecraft/world/level/Level.java -+++ b/net/minecraft/world/level/Level.java -@@ -115,10 +115,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - public static final int TICKS_PER_DAY = 24000; - public static final int MAX_ENTITY_SPAWN_Y = 20000000; - public static final int MIN_ENTITY_SPAWN_Y = -20000000; -- public final List blockEntityTickers = Lists.newArrayList(); // Paper - public -- protected final NeighborUpdater neighborUpdater; -- private final List pendingBlockEntityTickers = Lists.newArrayList(); -- private boolean tickingBlockEntities; -+ //public final List blockEntityTickers = Lists.newArrayList(); // Paper - public // Folia - region threading -+ public final int neighbourUpdateMax; //protected final NeighborUpdater neighborUpdater; // Folia - region threading -+ //private final List pendingBlockEntityTickers = Lists.newArrayList(); // Folia - region threading -+ //private boolean tickingBlockEntities; // Folia - region threading - public final Thread thread; - private final boolean isDebug; - private int skyDarken; -@@ -128,7 +128,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - public float rainLevel; - protected float oThunderLevel; - public float thunderLevel; -- public final RandomSource random = new ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); // Paper - replace random -+ public final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Paper - replace random // Folia - region threading - @Deprecated - private final RandomSource threadSafeRandom = RandomSource.createThreadSafe(); - private final Holder dimensionTypeRegistration; -@@ -139,28 +139,17 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - private final ResourceKey dimension; - private final RegistryAccess registryAccess; - private final DamageSources damageSources; -- private long subTickCount; -+ private final java.util.concurrent.atomic.AtomicLong subTickCount = new java.util.concurrent.atomic.AtomicLong(); //private long subTickCount; // Folia - region threading - - // CraftBukkit start Added the following - private final CraftWorld world; - public boolean pvpMode; - public org.bukkit.generator.ChunkGenerator generator; - -- public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 -- public boolean captureBlockStates = false; -- public boolean captureTreeGeneration = false; -- public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent -- public Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper -- public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates -- public List captureDrops; -+ // Folia - region threading - moved to regionised data - public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>(); -- // Paper start -- public int wakeupInactiveRemainingAnimals; -- public int wakeupInactiveRemainingFlying; -- public int wakeupInactiveRemainingMonsters; -- public int wakeupInactiveRemainingVillagers; -- // Paper end -- public boolean populating; -+ // Folia - region threading - moved to regionised data -+ // Folia - region threading - public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot - // Paper start - add paper world config - private final io.papermc.paper.configuration.WorldConfiguration paperConfig; -@@ -173,9 +162,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - public static BlockPos lastPhysicsProblem; // Spigot - private org.spigotmc.TickLimiter entityLimiter; - private org.spigotmc.TickLimiter tileLimiter; -- private int tileTickPosition; -- public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions -- public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here -+ //private int tileTickPosition; // Folia - region threading -+ //public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions // Folia - region threading -+ //public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here // Folia - region threading - - public CraftWorld getWorld() { - return this.world; -@@ -825,6 +814,32 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z); - } - // Paper end - optimise random ticking -+ // Folia start - region ticking -+ public final io.papermc.paper.threadedregions.RegionizedData worldRegionData -+ = new io.papermc.paper.threadedregions.RegionizedData<>( -+ (ServerLevel)this, () -> new io.papermc.paper.threadedregions.RegionizedWorldData((ServerLevel)Level.this), -+ io.papermc.paper.threadedregions.RegionizedWorldData.REGION_CALLBACK -+ ); -+ public volatile io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData tickData; -+ public final java.util.concurrent.ConcurrentHashMap.KeySetView needsChangeBroadcasting = java.util.concurrent.ConcurrentHashMap.newKeySet(); -+ -+ public io.papermc.paper.threadedregions.RegionizedWorldData getCurrentWorldData() { -+ final io.papermc.paper.threadedregions.RegionizedWorldData ret = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); -+ if (ret == null) { -+ return ret; -+ } -+ Level world = ret.world; -+ if (world != this) { -+ throw new IllegalStateException("World mismatch: expected " + this.getWorld().getName() + " but got " + world.getWorld().getName()); -+ } -+ return ret; -+ } -+ -+ @Override -+ public List getLocalPlayers() { -+ return this.getCurrentWorldData().getLocalPlayers(); -+ } -+ // Folia end - region ticking - - protected Level( - WritableLevelData levelData, -@@ -888,7 +903,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - this.thread = Thread.currentThread(); - this.biomeManager = new BiomeManager(this, biomeZoomSeed); - this.isDebug = isDebug; -- this.neighborUpdater = new CollectingNeighborUpdater(this, maxChainedNeighborUpdates); -+ this.neighbourUpdateMax = maxChainedNeighborUpdates; // Folia - region threading - this.registryAccess = registryAccess; - this.damageSources = new DamageSources(registryAccess); - -@@ -1035,8 +1050,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - @Nullable - public final BlockState getBlockStateIfLoaded(BlockPos pos) { - // CraftBukkit start - tree generation -- if (this.captureTreeGeneration) { -- CraftBlockState previous = this.capturedBlockStates.get(pos); -+ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading -+ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Folia - region threading - if (previous != null) { - return previous.getHandle(); - } -@@ -1098,16 +1113,18 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - - @Override - public boolean setBlock(BlockPos pos, BlockState state, int flags, int recursionLeft) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, pos, "Updating block asynchronously"); // Folia - region threading -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); // Folia - region threading - // CraftBukkit start - tree generation -- if (this.captureTreeGeneration) { -+ if (worldData.captureTreeGeneration) { // Folia - region threading - // Paper start - Protect Bedrock and End Portal/Frames from being destroyed - BlockState type = getBlockState(pos); - if (!type.isDestroyable()) return false; - // Paper end - Protect Bedrock and End Portal/Frames from being destroyed -- CraftBlockState blockstate = this.capturedBlockStates.get(pos); -+ CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Folia - region threading - if (blockstate == null) { - blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags); -- this.capturedBlockStates.put(pos.immutable(), blockstate); -+ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading - } - blockstate.setData(state); - blockstate.setFlag(flags); -@@ -1123,10 +1140,10 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - Block block = state.getBlock(); - // CraftBukkit start - capture blockstates - boolean captured = false; -- if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) { -+ if (worldData.captureBlockStates && !worldData.capturedBlockStates.containsKey(pos)) { // Folia - region threading - CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot - blockstate.setFlag(flags); // Paper - set flag -- this.capturedBlockStates.put(pos.immutable(), blockstate); -+ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading - captured = true; - } - // CraftBukkit end -@@ -1136,8 +1153,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - - if (blockState == null) { - // CraftBukkit start - remove blockstate if failed (or the same) -- if (this.captureBlockStates && captured) { -- this.capturedBlockStates.remove(pos); -+ if (worldData.captureBlockStates && captured) { // Folia - region threading -+ worldData.capturedBlockStates.remove(pos); // Folia - region threading - } - // CraftBukkit end - return false; -@@ -1174,7 +1191,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - */ - - // CraftBukkit start -- if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates -+ if (!worldData.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates // Folia - region threading - // Modularize client and physic updates - // Spigot start - try { -@@ -1219,7 +1236,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam - CraftWorld world = ((ServerLevel) this).getWorld(); - boolean cancelledUpdates = false; // Paper - Fix block place logic -- if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent -+ if (world != null && ((ServerLevel)this).getCurrentWorldData().hasPhysicsEvent) { // Paper - BlockPhysicsEvent // Folia - region threading - BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata)); - this.getCraftServer().getPluginManager().callEvent(event); - -@@ -1233,7 +1250,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - } - - // CraftBukkit start - SPIGOT-5710 -- if (!this.preventPoiUpdated) { -+ if (!this.getCurrentWorldData().preventPoiUpdated) { // Folia - region threading - this.onBlockStateChange(blockposition, iblockdata1, iblockdata2); - } - // CraftBukkit end -@@ -1322,7 +1339,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - - @Override - public void neighborShapeChanged(Direction direction, BlockPos pos, BlockPos neighborPos, BlockState neighborState, int flags, int recursionLeft) { -- this.neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, recursionLeft); -+ this.getCurrentWorldData().neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, recursionLeft); // Folia - region threading - } - - @Override -@@ -1346,11 +1363,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - return this.getChunkSource().getLightEngine(); - } - -+ // Folia start - region threading -+ @Nullable -+ public BlockState getBlockStateFromEmptyChunkIfLoaded(BlockPos pos) { -+ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); -+ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); -+ if (chunk != null) { -+ return chunk.getBlockState(pos); -+ } -+ return null; -+ } -+ -+ @Nullable -+ public BlockState getBlockStateFromEmptyChunk(BlockPos pos) { -+ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); -+ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); -+ if (chunk != null) { -+ return chunk.getBlockState(pos); -+ } -+ chunk = chunkProvider.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY, true); -+ return chunk.getBlockState(pos); -+ } -+ // Folia end - region threading -+ - @Override - public BlockState getBlockState(BlockPos pos) { - // CraftBukkit start - tree generation -- if (this.captureTreeGeneration) { -- CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper -+ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading -+ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Paper // Folia - region threading - if (previous != null) { - return previous.getHandle(); - } -@@ -1454,17 +1494,16 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - } - - public void addBlockEntityTicker(TickingBlockEntity ticker) { -- (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); -+ ((ServerLevel)this).getCurrentWorldData().addBlockEntityTicker(ticker); // Folia - regionised ticking - } - - protected void tickBlockEntities() { - ProfilerFiller profilerFiller = Profiler.get(); - profilerFiller.push("blockEntities"); -- this.tickingBlockEntities = true; -- if (!this.pendingBlockEntityTickers.isEmpty()) { -- this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); -- this.pendingBlockEntityTickers.clear(); -- } -+ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking -+ regionizedWorldData.seTtickingBlockEntities(true); // Folia - regionised ticking -+ regionizedWorldData.pushPendingTickingBlockEntities(); // Folia - regionised ticking -+ List blockEntityTickers = regionizedWorldData.getBlockEntityTickers(); // Folia - regionised ticking - - // Spigot start - boolean runsNormally = this.tickRateManager().runsNormally(); -@@ -1472,9 +1511,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - int tickedEntities = 0; // Paper - rewrite chunk system - var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet(); // Paper - Fix MC-117075; use removeAll - toRemove.add(null); // Paper - Fix MC-117075 -- for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters -- this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0; -- TickingBlockEntity tickingBlockEntity = this.blockEntityTickers.get(this.tileTickPosition); -+ for (int i = 0; i < blockEntityTickers.size(); i++) { // Paper - Disable tick limiters // Folia - regionised ticking -+ TickingBlockEntity tickingBlockEntity = blockEntityTickers.get(i); // Folia - regionised ticking - // Spigot end - if (tickingBlockEntity.isRemoved()) { - toRemove.add(tickingBlockEntity); // Paper - Fix MC-117075; use removeAll -@@ -1487,11 +1525,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - // Paper end - rewrite chunk system - } - } -- this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 -+ blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 // Folia - regionised ticking - -- this.tickingBlockEntities = false; -+ regionizedWorldData.seTtickingBlockEntities(false); // Folia - regionised ticking - profilerFiller.pop(); -- this.spigotConfig.currentPrimedTnt = 0; // Spigot -+ regionizedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading - } - - public void guardEntityTick(Consumer consumerEntity, T entity) { -@@ -1502,7 +1540,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); - MinecraftServer.LOGGER.error(msg, var6); - getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, var6))); // Paper - ServerExceptionEvent -- entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); -+ if (!(entity instanceof net.minecraft.server.level.ServerPlayer)) entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); // Folia - properly disconnect players -+ if (entity instanceof net.minecraft.server.level.ServerPlayer player) player.connection.disconnect(net.minecraft.network.chat.Component.translatable("multiplayer.disconnect.generic"), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Folia - properly disconnect players - // Paper end - Prevent block entity and entity crashes - } - this.moonrise$midTickTasks(); // Paper - rewrite chunk system -@@ -1648,9 +1687,14 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - - @Nullable - public BlockEntity getBlockEntity(BlockPos pos, boolean validate) { -+ // Folia start - region threading -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { -+ return null; -+ } -+ // Folia end - region threading - // Paper start - Perf: Optimize capturedTileEntities lookup - net.minecraft.world.level.block.entity.BlockEntity blockEntity; -- if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(pos)) != null) { -+ if (!this.getCurrentWorldData().capturedTileEntities.isEmpty() && (blockEntity = this.getCurrentWorldData().capturedTileEntities.get(pos)) != null) { // Folia - region threading - return blockEntity; - } - // Paper end - Perf: Optimize capturedTileEntities lookup -@@ -1668,8 +1712,8 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - BlockPos blockPos = blockEntity.getBlockPos(); - if (!this.isOutsideBuildHeight(blockPos)) { - // CraftBukkit start -- if (this.captureBlockStates) { -- this.capturedTileEntities.put(blockPos.immutable(), blockEntity); -+ if (this.getCurrentWorldData().captureBlockStates) { // Folia - region threading -+ this.getCurrentWorldData().capturedTileEntities.put(blockPos.immutable(), blockEntity); // Folia - region threading - return; - } - // CraftBukkit end -@@ -1749,6 +1793,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - - @Override - public List getEntities(@Nullable Entity entity, AABB boundingBox, Predicate predicate) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, boundingBox, "Cannot getEntities asynchronously"); // Folia - region threading - Profiler.get().incrementCounter("getEntities"); - List list = Lists.newArrayList(); - -@@ -1778,6 +1823,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - public void getEntities(final EntityTypeTest entityTypeTest, - final AABB boundingBox, final Predicate predicate, - final List into, final int maxCount) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, boundingBox, "Cannot getEntities asynchronously"); // Folia - region threading - Profiler.get().incrementCounter("getEntities"); - - if (entityTypeTest instanceof net.minecraft.world.entity.EntityType byType) { -@@ -1877,13 +1923,34 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - public void disconnect() { - } - -+ @Override // Folia - region threading - public long getGameTime() { -- return this.levelData.getGameTime(); -+ // Folia start - region threading -+ // Dumb world gen thread calls this for some reason. So, check for null. -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); -+ return worldData == null ? this.getLevelData().getGameTime() : worldData.getTickData().nonRedstoneGameTime(); -+ // Folia end - region threading - } - - public long getDayTime() { -- return this.levelData.getDayTime(); -+ // Folia start - region threading -+ // Dumb world gen thread calls this for some reason. So, check for null. -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); -+ return worldData == null ? this.getLevelData().getDayTime() : worldData.getTickData().dayTime(); -+ // Folia end - region threading -+ } -+ -+ // Folia start - region threading -+ @Override -+ public long dayTime() { -+ return this.getDayTime(); -+ } -+ -+ @Override -+ public long getRedstoneGameTime() { -+ return this.getCurrentWorldData().getRedstoneGameTime(); - } -+ // Folia end - region threading - - public boolean mayInteract(Player player, BlockPos pos) { - return true; -@@ -2061,8 +2128,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - public abstract RecipeAccess recipeAccess(); - - public BlockPos getBlockRandomPos(int x, int y, int z, int yMask) { -- this.randValue = this.randValue * 3 + 1013904223; -- int i = this.randValue >> 2; -+ int i = this.random.nextInt() >> 2; // Folia - region threading - return new BlockPos(x + (i & 15), y + (i >> 16 & yMask), z + (i >> 8 & 15)); - } - -@@ -2083,7 +2149,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl - - @Override - public long nextSubTickCount() { -- return this.subTickCount++; -+ return this.subTickCount.getAndIncrement(); // Folia - region threading - } - - @Override -diff --git a/net/minecraft/world/level/LevelAccessor.java b/net/minecraft/world/level/LevelAccessor.java -index ee9d320da1b4c3aa66be6592867e95c706b65b3a..cd5bfa374b0b1af64bc8415ace94fa43955e5145 100644 ---- a/net/minecraft/world/level/LevelAccessor.java -+++ b/net/minecraft/world/level/LevelAccessor.java -@@ -33,14 +33,24 @@ public interface LevelAccessor extends CommonLevelAccessor, LevelTimeAccess, Sch - - long nextSubTickCount(); - -+ // Folia start - region threading -+ default long getGameTime() { -+ return this.getLevelData().getGameTime(); -+ } -+ -+ default long getRedstoneGameTime() { -+ return this.getLevelData().getGameTime(); -+ } -+ // Folia end - region threading -+ - @Override - default ScheduledTick createTick(BlockPos pos, T type, int delay, TickPriority priority) { -- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + delay, priority, this.nextSubTickCount()); -+ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + delay, priority, this.nextSubTickCount()); // Folia - region threading - } - - @Override - default ScheduledTick createTick(BlockPos pos, T type, int delay) { -- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + delay, this.nextSubTickCount()); -+ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + delay, this.nextSubTickCount()); // Folia - region threading - } - - LevelData getLevelData(); -diff --git a/net/minecraft/world/level/LevelReader.java b/net/minecraft/world/level/LevelReader.java -index 26c8c1e5598daf3550aef05b12218c47bda6618b..e59e1bb91e446406e58cc8046a85b693adb11e86 100644 ---- a/net/minecraft/world/level/LevelReader.java -+++ b/net/minecraft/world/level/LevelReader.java -@@ -204,6 +204,25 @@ public interface LevelReader extends ca.spottedleaf.moonrise.patches.chunk_syste - return toY >= this.getMinY() && fromY <= this.getMaxY() && this.hasChunksAt(fromX, fromZ, toX, toZ); - } - -+ // Folia start - region threading -+ default boolean hasAndOwnsChunksAt(int minX, int minZ, int maxX, int maxZ) { -+ int i = SectionPos.blockToSectionCoord(minX); -+ int j = SectionPos.blockToSectionCoord(maxX); -+ int k = SectionPos.blockToSectionCoord(minZ); -+ int l = SectionPos.blockToSectionCoord(maxZ); -+ -+ for(int m = i; m <= j; ++m) { -+ for(int n = k; n <= l; ++n) { -+ if (!this.hasChunk(m, n) || (this instanceof net.minecraft.server.level.ServerLevel world && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, m, n))) { -+ return false; -+ } -+ } -+ } -+ -+ return true; -+ } -+ // Folia end - region threading -+ - @Deprecated - default boolean hasChunksAt(int fromX, int fromZ, int toX, int toZ) { - int sectionPosCoord = SectionPos.blockToSectionCoord(fromX); -diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java -index 17ce115e887cbbb06ad02ab7ddb488e27342c0e4..5ce81eafee33d22b69029c088d4be497131338a2 100644 ---- a/net/minecraft/world/level/NaturalSpawner.java -+++ b/net/minecraft/world/level/NaturalSpawner.java -@@ -137,7 +137,7 @@ public final class NaturalSpawner { - int limit = mobCategory.getMaxInstancesPerChunk(); - SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(mobCategory); - if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { -- spawnThisTick = level.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(spawnCategory) == 0; -+ spawnThisTick = level.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && level.getRedstoneGameTime() % level.ticksPerSpawnCategory.getLong(spawnCategory) == 0; // Folia - region threading - limit = level.getWorld().getSpawnLimit(spawnCategory); - } - -diff --git a/net/minecraft/world/level/ServerExplosion.java b/net/minecraft/world/level/ServerExplosion.java -index 7b132c55caf9d3c3df3b0a123f4b5bfc7ae35984..cd584fd5ed007f3c05d60cf46adbf6ae40163db4 100644 ---- a/net/minecraft/world/level/ServerExplosion.java -+++ b/net/minecraft/world/level/ServerExplosion.java -@@ -773,17 +773,18 @@ public class ServerExplosion implements Explosion { - if (!this.level.paperConfig().environment.optimizeExplosions) { - return this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations - } -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading - CacheKey key = new CacheKey(this, entity.getBoundingBox()); -- Float blockDensity = this.level.explosionDensityCache.get(key); -+ Float blockDensity = worldData.explosionDensityCache.get(key); // Folia - region threading - if (blockDensity == null) { - blockDensity = this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations -- this.level.explosionDensityCache.put(key, blockDensity); -+ worldData.explosionDensityCache.put(key, blockDensity); // Folia - region threading - } - - return blockDensity; - } - -- static class CacheKey { -+ public static class CacheKey { // Folia - region threading - public - private final Level world; - private final double posX, posY, posZ; - private final double minX, minY, minZ; -diff --git a/net/minecraft/world/level/ServerLevelAccessor.java b/net/minecraft/world/level/ServerLevelAccessor.java -index b4f14ff9ef0c212f4d0e0c2ccf20ce1e7af9e734..441ba6ae8885a968734ac0abdb8a9d09fa658430 100644 ---- a/net/minecraft/world/level/ServerLevelAccessor.java -+++ b/net/minecraft/world/level/ServerLevelAccessor.java -@@ -6,6 +6,12 @@ import net.minecraft.world.entity.Entity; - public interface ServerLevelAccessor extends LevelAccessor { - ServerLevel getLevel(); - -+ // Folia start - region threading -+ default public StructureManager structureManager() { -+ throw new UnsupportedOperationException(); -+ } -+ // Folia end - region threading -+ - default void addFreshEntityWithPassengers(Entity entity) { - // CraftBukkit start - this.addFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT); -diff --git a/net/minecraft/world/level/StructureManager.java b/net/minecraft/world/level/StructureManager.java -index 8bc6a6c86cd8db53feefba7508b6031ba67e242e..9abfcfa3e8d8319e98866b2a81f2eb9ac7269055 100644 ---- a/net/minecraft/world/level/StructureManager.java -+++ b/net/minecraft/world/level/StructureManager.java -@@ -48,12 +48,7 @@ public class StructureManager { - } - - public List startsForStructure(ChunkPos chunkPos, Predicate structurePredicate) { -- // Paper start - Fix swamp hut cat generation deadlock -- return this.startsForStructure(chunkPos, structurePredicate, null); -- } -- -- public List startsForStructure(ChunkPos chunkPos, Predicate structurePredicate, @Nullable ServerLevelAccessor levelAccessor) { -- Map allReferences = (levelAccessor == null ? this.level : levelAccessor).getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); -+ Map allReferences = this.level.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); // Folia - region threading - // Paper end - Fix swamp hut cat generation deadlock - Builder builder = ImmutableList.builder(); - -@@ -124,20 +119,12 @@ public class StructureManager { - } - - public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate> predicate) { -- // Paper start - Fix swamp hut cat generation deadlock -- return this.getStructureWithPieceAt(pos, predicate, null); -- } -- -- public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey tag, @Nullable ServerLevelAccessor levelAccessor) { -- return this.getStructureWithPieceAt(pos, structure -> structure.is(tag), levelAccessor); -- } -- -- public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate> predicate, @Nullable ServerLevelAccessor levelAccessor) { -+ // Folia - region threading - // Paper end - Fix swamp hut cat generation deadlock - Registry registry = this.registryAccess().lookupOrThrow(Registries.STRUCTURE); - - for (StructureStart structureStart : this.startsForStructure( -- new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false), levelAccessor // Paper - Fix swamp hut cat generation deadlock -+ new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false) // Paper - Fix swamp hut cat generation deadlock // Folia - region threading - )) { - if (this.structureHasPieceAt(pos, structureStart)) { - return structureStart; -@@ -182,7 +169,7 @@ public class StructureManager { - } - - public void addReference(StructureStart structureStart) { -- structureStart.addReference(); -+ //structureStart.addReference(); // Folia - region threading - move to caller - this.structureCheck.incrementReference(structureStart.getChunkPos(), structureStart.getStructure()); - } - -diff --git a/net/minecraft/world/level/block/BedBlock.java b/net/minecraft/world/level/block/BedBlock.java -index c23c255cefe7c5be618bbe97a99ae3215d8e48c0..7425ef8a41aaac946f57d1b2105281339a7ba8ff 100644 ---- a/net/minecraft/world/level/block/BedBlock.java -+++ b/net/minecraft/world/level/block/BedBlock.java -@@ -346,7 +346,7 @@ public class BedBlock extends HorizontalDirectionalBlock implements EntityBlock - BlockPos blockPos = pos.relative(state.getValue(FACING)); - level.setBlock(blockPos, state.setValue(PART, BedPart.HEAD), 3); - // CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states -- if (level.captureBlockStates) { -+ if (level.getCurrentWorldData().captureBlockStates) { // Folia - region threading - return; - } - // CraftBukkit end -diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java -index 976de81d65b6494cdad20f4ec5125fceec86f951..aa09b2e8fac82ab954f581df3d41153c6244c2e8 100644 ---- a/net/minecraft/world/level/block/Block.java -+++ b/net/minecraft/world/level/block/Block.java -@@ -362,8 +362,8 @@ public class Block extends BlockBehaviour implements ItemLike { - ItemEntity itemEntity = itemEntitySupplier.get(); - itemEntity.setDefaultPickUpDelay(); - // CraftBukkit start -- if (level.captureDrops != null) { -- level.captureDrops.add(itemEntity); -+ if (level.getCurrentWorldData().captureDrops != null) { // Folia - region threading -+ level.getCurrentWorldData().captureDrops.add(itemEntity); // Folia - region threading - } else { - level.addFreshEntity(itemEntity); - } -diff --git a/net/minecraft/world/level/block/BushBlock.java b/net/minecraft/world/level/block/BushBlock.java -index bc52568bfa56635300266424488e524d77d95e09..068e65fb7efd52b36ba7f49829da80d82753e78e 100644 ---- a/net/minecraft/world/level/block/BushBlock.java -+++ b/net/minecraft/world/level/block/BushBlock.java -@@ -38,7 +38,7 @@ public abstract class BushBlock extends Block { - // CraftBukkit start - if (!state.canSurvive(level, pos)) { - // Suppress during worldgen -- if (!(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(serverLevel, pos).isCancelled()) { // Paper -+ if (!(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.getCurrentWorldData().hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(serverLevel, pos).isCancelled()) { // Paper // Folia - region threading - return Blocks.AIR.defaultBlockState(); - } - } -diff --git a/net/minecraft/world/level/block/DaylightDetectorBlock.java b/net/minecraft/world/level/block/DaylightDetectorBlock.java -index a83d1dd4cac85d34f695333fd917a41f14dd5715..17532ef2cc5e21e68a1d51146641ae124a67f79e 100644 ---- a/net/minecraft/world/level/block/DaylightDetectorBlock.java -+++ b/net/minecraft/world/level/block/DaylightDetectorBlock.java -@@ -110,7 +110,7 @@ public class DaylightDetectorBlock extends BaseEntityBlock { - } - - private static void tickEntity(Level level, BlockPos pos, BlockState state, DaylightDetectorBlockEntity blockEntity) { -- if (level.getGameTime() % 20L == 0L) { -+ if (level.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading - updateSignalStrength(state, level, pos); - } - } -diff --git a/net/minecraft/world/level/block/DispenserBlock.java b/net/minecraft/world/level/block/DispenserBlock.java -index e0a4d41e5bcf144ea4c10d6f633c3a95ed2c5aec..0ff736a45776bbf16f32ac05f099bb656aa3b9a6 100644 ---- a/net/minecraft/world/level/block/DispenserBlock.java -+++ b/net/minecraft/world/level/block/DispenserBlock.java -@@ -50,7 +50,7 @@ public class DispenserBlock extends BaseEntityBlock { - private static final DefaultDispenseItemBehavior DEFAULT_BEHAVIOR = new DefaultDispenseItemBehavior(); - public static final Map DISPENSER_REGISTRY = new IdentityHashMap<>(); - private static final int TRIGGER_DURATION = 4; -- public static boolean eventFired = false; // CraftBukkit -+ public static ThreadLocal eventFired = ThreadLocal.withInitial(() -> Boolean.FALSE); // CraftBukkit // Folia - region threading - - @Override - public MapCodec codec() { -@@ -96,7 +96,7 @@ public class DispenserBlock extends BaseEntityBlock { - DispenseItemBehavior dispenseMethod = this.getDispenseMethod(level, item); - if (dispenseMethod != DispenseItemBehavior.NOOP) { - if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(level, pos, item, randomSlot)) return; // Paper - Add BlockPreDispenseEvent -- DispenserBlock.eventFired = false; // CraftBukkit - reset event status -+ DispenserBlock.eventFired.set(Boolean.FALSE); // CraftBukkit - reset event status // Folia - region threading - dispenserBlockEntity.setItem(randomSlot, dispenseMethod.dispense(blockSource, item)); - } - } -diff --git a/net/minecraft/world/level/block/DoublePlantBlock.java b/net/minecraft/world/level/block/DoublePlantBlock.java -index 7d033444ab5f89fae3c571a67ede6e7eff378945..e46c4071a955d880d61235d0861d8752ab3b860e 100644 ---- a/net/minecraft/world/level/block/DoublePlantBlock.java -+++ b/net/minecraft/world/level/block/DoublePlantBlock.java -@@ -118,7 +118,7 @@ public class DoublePlantBlock extends BushBlock { - - protected static void preventDropFromBottomPart(Level level, BlockPos pos, BlockState state, Player player) { - // CraftBukkit start -- if (((net.minecraft.server.level.ServerLevel)level).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(level, pos).isCancelled()) { // Paper -+ if (((net.minecraft.server.level.ServerLevel)level).getCurrentWorldData().hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(level, pos).isCancelled()) { // Paper // Folia - region threading - return; - } - // CraftBukkit end -diff --git a/net/minecraft/world/level/block/EndGatewayBlock.java b/net/minecraft/world/level/block/EndGatewayBlock.java -index 84a1bd5e40e635962d795506861447851e443eee..a7b8e2b702fbe512c9633075515da6a430e76861 100644 ---- a/net/minecraft/world/level/block/EndGatewayBlock.java -+++ b/net/minecraft/world/level/block/EndGatewayBlock.java -@@ -111,17 +111,43 @@ public class EndGatewayBlock extends BaseEntityBlock implements Portal { - if (portalPosition == null) { - return null; - } else { -- return entity instanceof ThrownEnderpearl -- ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit -- : new TeleportTransition( -- level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit -- ); -+ return getTeleportTransition(level, entity, portalPosition); // Folia - region threading - } - } else { - return null; - } - } - -+ // Folia start - region threading -+ public static TeleportTransition getTeleportTransition(ServerLevel level, Entity entity, Vec3 portalPosition) { -+ return entity instanceof ThrownEnderpearl -+ ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit -+ : new TeleportTransition( -+ level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit -+ ); -+ } -+ -+ @Override -+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { -+ return false; -+ } -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { -+ return false; -+ } -+ -+ BlockEntity tile = sourceWorld.getBlockEntity(portalPos); -+ -+ if (!(tile instanceof TheEndGatewayBlockEntity endGateway)) { -+ return false; -+ } -+ -+ return TheEndGatewayBlockEntity.teleportRegionThreading( -+ sourceWorld, portalPos, portalTarget, endGateway, TeleportTransition.PLACE_PORTAL_TICKET -+ ); -+ } -+ // Folia end - region threading -+ - @Override - protected RenderShape getRenderShape(BlockState state) { - return RenderShape.INVISIBLE; -diff --git a/net/minecraft/world/level/block/EndPortalBlock.java b/net/minecraft/world/level/block/EndPortalBlock.java -index 01cddd7001b4a7f99c1b1d147fac904d3064d733..177735cf744e564081e4c140a0f8210c3a07e037 100644 ---- a/net/minecraft/world/level/block/EndPortalBlock.java -+++ b/net/minecraft/world/level/block/EndPortalBlock.java -@@ -63,7 +63,7 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal { - level.getCraftServer().getPluginManager().callEvent(event); - if (event.isCancelled()) return; // Paper - make cancellable - // CraftBukkit end -- if (!level.isClientSide && level.dimension() == Level.END && entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) { -+ if (false && !level.isClientSide && level.dimension() == Level.END && entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) { // Folia - region threading - do not show credits - if (level.paperConfig().misc.disableEndCredits) {serverPlayer.seenCredits = true; return;} // Paper - Option to disable end credits - serverPlayer.showEndCredits(); - } else { -@@ -113,6 +113,20 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal { - } - } - -+ // Folia start - region threading -+ @Override -+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { -+ return false; -+ } -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { -+ return false; -+ } -+ -+ return portalTarget.endPortalLogicAsync(portalPos); -+ } -+ // Folia end - region threading -+ - @Override - public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { - double d = pos.getX() + random.nextDouble(); -diff --git a/net/minecraft/world/level/block/FarmBlock.java b/net/minecraft/world/level/block/FarmBlock.java -index 47c9b32c89e7e6f84a279c2f6098ada77dc58b6b..1d97daccd595df427104aadf37eaa2861e6cb6e1 100644 ---- a/net/minecraft/world/level/block/FarmBlock.java -+++ b/net/minecraft/world/level/block/FarmBlock.java -@@ -95,8 +95,8 @@ public class FarmBlock extends Block { - @Override - protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { - int moistureValue = state.getValue(MOISTURE); -- if (moistureValue > 0 && level.paperConfig().tickRates.wetFarmland != 1 && (level.paperConfig().tickRates.wetFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks -- if (moistureValue == 0 && level.paperConfig().tickRates.dryFarmland != 1 && (level.paperConfig().tickRates.dryFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks -+ if (moistureValue > 0 && level.paperConfig().tickRates.wetFarmland != 1 && (level.paperConfig().tickRates.wetFarmland < 1 || (level.getRedstoneGameTime() + pos.hashCode()) % level.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading -+ if (moistureValue == 0 && level.paperConfig().tickRates.dryFarmland != 1 && (level.paperConfig().tickRates.dryFarmland < 1 || (level.getRedstoneGameTime() + pos.hashCode()) % level.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading - if (!isNearWater(level, pos) && !level.isRainingAt(pos.above())) { - if (moistureValue > 0) { - org.bukkit.craftbukkit.event.CraftEventFactory.handleMoistureChangeEvent(level, pos, state.setValue(FarmBlock.MOISTURE, moistureValue - 1), 2); // CraftBukkit -diff --git a/net/minecraft/world/level/block/FungusBlock.java b/net/minecraft/world/level/block/FungusBlock.java -index 85f0eac75784565c658c5178c544f969db3d6f54..81edac1fa383c6875c7a0439f2a160c11ef77a41 100644 ---- a/net/minecraft/world/level/block/FungusBlock.java -+++ b/net/minecraft/world/level/block/FungusBlock.java -@@ -76,9 +76,9 @@ public class FungusBlock extends BushBlock implements BonemealableBlock { - // CraftBukkit start - .map((value) -> { - if (this == Blocks.WARPED_FUNGUS) { -- SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS; -+ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.WARPED_FUNGUS); // Folia - region threading - } else if (this == Blocks.CRIMSON_FUNGUS) { -- SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS; -+ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.CRIMSON_FUNGUS); // Folia - region threading - } - return value; - }) -diff --git a/net/minecraft/world/level/block/HoneyBlock.java b/net/minecraft/world/level/block/HoneyBlock.java -index bab3ac2c4be08ea7589752b8472c1e13bcaab76a..776216db8097ceadc81d2f8401ea71447769b396 100644 ---- a/net/minecraft/world/level/block/HoneyBlock.java -+++ b/net/minecraft/world/level/block/HoneyBlock.java -@@ -94,7 +94,7 @@ public class HoneyBlock extends HalfTransparentBlock { - } - - private void maybeDoSlideAchievement(Entity entity, BlockPos pos) { -- if (entity instanceof ServerPlayer && entity.level().getGameTime() % 20L == 0L) { -+ if (entity instanceof ServerPlayer && entity.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading - CriteriaTriggers.HONEY_BLOCK_SLIDE.trigger((ServerPlayer)entity, entity.level().getBlockState(pos)); - } - } -diff --git a/net/minecraft/world/level/block/LightningRodBlock.java b/net/minecraft/world/level/block/LightningRodBlock.java -index 534de49aec290766d6bc2523bb3975df775b5881..d79b3d328915096d723c0e3e6b6eb75cfe5bac51 100644 ---- a/net/minecraft/world/level/block/LightningRodBlock.java -+++ b/net/minecraft/world/level/block/LightningRodBlock.java -@@ -116,7 +116,7 @@ public class LightningRodBlock extends RodBlock implements SimpleWaterloggedBloc - @Override - public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { - if (level.isThundering() -- && level.random.nextInt(200) <= level.getGameTime() % 200L -+ && level.random.nextInt(200) <= level.getRedstoneGameTime() % 200L // Folia - region threading - && pos.getY() == level.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { - ParticleUtils.spawnParticlesAlongAxis(state.getValue(FACING).getAxis(), level, pos, 0.125, ParticleTypes.ELECTRIC_SPARK, UniformInt.of(1, 2)); - } -diff --git a/net/minecraft/world/level/block/MushroomBlock.java b/net/minecraft/world/level/block/MushroomBlock.java -index 904369f4d7db41026183f2de7c96c2f0f4dc204d..223b1789ba94f763e29fb5e74aade787681e9f5b 100644 ---- a/net/minecraft/world/level/block/MushroomBlock.java -+++ b/net/minecraft/world/level/block/MushroomBlock.java -@@ -94,7 +94,7 @@ public class MushroomBlock extends BushBlock implements BonemealableBlock { - return false; - } else { - level.removeBlock(pos, false); -- SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM; // CraftBukkit -+ SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM); // CraftBukkit // Folia - region threading - if (optional.get().value().place(level, level.getChunkSource().getGenerator(), random, pos)) { - return true; - } else { -diff --git a/net/minecraft/world/level/block/NetherPortalBlock.java b/net/minecraft/world/level/block/NetherPortalBlock.java -index e2eb693b0130513115392cb0cb5a829ede5be8c5..68e1b1737c8b7af39f22dd4d28b879b5c3d52f65 100644 ---- a/net/minecraft/world/level/block/NetherPortalBlock.java -+++ b/net/minecraft/world/level/block/NetherPortalBlock.java -@@ -181,6 +181,33 @@ public class NetherPortalBlock extends Block implements Portal { - } - } - -+ // Folia start - region threading -+ @Override -+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { -+ return false; -+ } -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { -+ return false; -+ } -+ -+ return portalTarget.netherPortalLogicAsync(portalPos); -+ } -+ -+ public static BlockUtil.FoundRectangle findPortalAround(ServerLevel world, BlockPos rough, WorldBorder worldBorder, int searchRadius) { -+ BlockPos found = world.getPortalForcer().findClosestPortalPosition(rough, worldBorder, searchRadius).orElse(null); -+ if (found == null) { -+ return null; -+ } -+ -+ BlockState portalState = world.getBlockStateFromEmptyChunk(found); -+ -+ return BlockUtil.getLargestRectangleAround(found, portalState.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (pos) -> { -+ return world.getBlockStateFromEmptyChunk(pos) == portalState; -+ }); -+ } -+ // Folia end - region threading -+ - @Nullable - private TeleportTransition getExitPortal(ServerLevel level, Entity entity, BlockPos pos, BlockPos exitPos, boolean isNether, WorldBorder worldBorder, int searchRadius, boolean canCreatePortal, int createRadius) { // CraftBukkit - Optional optional = level.getPortalForcer().findClosestPortalPosition(exitPos, worldBorder, searchRadius); // CraftBukkit -@@ -188,14 +215,14 @@ public class NetherPortalBlock extends Block implements Portal { - TeleportTransition.PostTeleportTransition postTeleportTransition; - if (optional.isPresent()) { - BlockPos blockPos = optional.get(); -- BlockState blockState = level.getBlockState(blockPos); -+ BlockState blockState = level.getBlockStateFromEmptyChunk(blockPos); // Folia - region threading - largestRectangleAround = BlockUtil.getLargestRectangleAround( - blockPos, - blockState.getValue(BlockStateProperties.HORIZONTAL_AXIS), - 21, - Direction.Axis.Y, - 21, -- blockPos1 -> level.getBlockState(blockPos1) == blockState -+ blockPos1 -> level.getBlockStateFromEmptyChunk(blockPos1) == blockState // Folia - region threading - ); - postTeleportTransition = TeleportTransition.PLAY_PORTAL_SOUND.then(entity1 -> entity1.placePortalTicket(blockPos)); - } else if (canCreatePortal) { // CraftBukkit -@@ -238,7 +265,7 @@ public class NetherPortalBlock extends Block implements Portal { - return createDimensionTransition(level, rectangle, axis, relativePortalPosition, entity, postTeleportTransition); - } - -- private static TeleportTransition createDimensionTransition( -+ public static TeleportTransition createDimensionTransition( // Folia - region threading - public - ServerLevel level, - BlockUtil.FoundRectangle rectangle, - Direction.Axis axis, -diff --git a/net/minecraft/world/level/block/Portal.java b/net/minecraft/world/level/block/Portal.java -index c941b0e05d98fa59669757174887955e6319eddb..3883a437d99e5d8b13c55764613d630e29e75bc4 100644 ---- a/net/minecraft/world/level/block/Portal.java -+++ b/net/minecraft/world/level/block/Portal.java -@@ -14,6 +14,10 @@ public interface Portal { - @Nullable - TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos); - -+ // Folia start - region threading -+ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos); -+ // Folia end - region threading -+ - default Portal.Transition getLocalTransition() { - return Portal.Transition.NONE; - } -diff --git a/net/minecraft/world/level/block/RedStoneWireBlock.java b/net/minecraft/world/level/block/RedStoneWireBlock.java -index 12c9d60314c99fb65e640d255a2d0c6b7790ad4d..6ba86c5e55d09fd99e81e40db4614ef14246bdc3 100644 ---- a/net/minecraft/world/level/block/RedStoneWireBlock.java -+++ b/net/minecraft/world/level/block/RedStoneWireBlock.java -@@ -91,7 +91,7 @@ public class RedStoneWireBlock extends Block { - private static final float PARTICLE_DENSITY = 0.2F; - private final BlockState crossState; - private final RedstoneWireEvaluator evaluator = new DefaultRedstoneWireEvaluator(this); -- public boolean shouldSignal = true; -+ //public boolean shouldSignal = true; // Folia - region threading - move to regionised world data - - @Override - public MapCodec codec() { -@@ -293,6 +293,11 @@ public class RedStoneWireBlock extends Block { - // Paper start - Optimize redstone (Eigencraft) - // The bulk of the new functionality is found in RedstoneWireTurbo.java - io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this); -+ // Folia start - region threading -+ private io.papermc.paper.redstone.RedstoneWireTurbo getTurbo(Level world) { -+ return world.getCurrentWorldData().turbo; -+ } -+ // Folia end - region threading - - /* - * Modified version of pre-existing updateSurroundingRedstone, which is called from -@@ -308,7 +313,7 @@ public class RedStoneWireBlock extends Block { - if (orientation != null) { - source = pos.relative(orientation.getFront().getOpposite()); - } -- turbo.updateSurroundingRedstone(worldIn, pos, state, source); -+ getTurbo(worldIn).updateSurroundingRedstone(worldIn, pos, state, source); // Folia - region threading - return; - } - updatePowerStrength(worldIn, pos, state, orientation, blockAdded); -@@ -336,7 +341,7 @@ public class RedStoneWireBlock extends Block { - // [Space Walker] suppress shape updates and emit those manually to - // bypass the new neighbor update stack. - if (level.setBlock(pos, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) { -- turbo.updateNeighborShapes(level, pos, state); -+ this.getTurbo(level).updateNeighborShapes(level, pos, state); // Folia - region threading - } - } - } -@@ -353,9 +358,9 @@ public class RedStoneWireBlock extends Block { - } - - public int getBlockSignal(Level level, BlockPos pos) { -- this.shouldSignal = false; -+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading - int bestNeighborSignal = level.getBestNeighborSignal(pos); -- this.shouldSignal = true; -+ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading - return bestNeighborSignal; - } - -@@ -450,12 +455,12 @@ public class RedStoneWireBlock extends Block { - - @Override - protected int getDirectSignal(BlockState blockState, BlockGetter blockAccess, BlockPos pos, Direction side) { -- return !this.shouldSignal ? 0 : blockState.getSignal(blockAccess, pos, side); -+ return !io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal ? 0 : blockState.getSignal(blockAccess, pos, side); // Folia - region threading - } - - @Override - protected int getSignal(BlockState blockState, BlockGetter blockAccess, BlockPos pos, Direction side) { -- if (this.shouldSignal && side != Direction.DOWN) { -+ if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal && side != Direction.DOWN) { // Folia - region threading - int powerValue = blockState.getValue(POWER); - if (powerValue == 0) { - return 0; -@@ -487,7 +492,10 @@ public class RedStoneWireBlock extends Block { - - @Override - protected boolean isSignalSource(BlockState state) { -- return this.shouldSignal; -+ // Folia start - region threading -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); -+ return worldData == null || worldData.shouldSignal; -+ // Folia end - region threading - } - - public static int getColorForPower(int power) { -diff --git a/net/minecraft/world/level/block/RedstoneTorchBlock.java b/net/minecraft/world/level/block/RedstoneTorchBlock.java -index 18420ec1f5776b018010f26e59aba00ae5bd0723..d5ac5d8fddeaff0def61a909faf2c909337ada57 100644 ---- a/net/minecraft/world/level/block/RedstoneTorchBlock.java -+++ b/net/minecraft/world/level/block/RedstoneTorchBlock.java -@@ -73,10 +73,10 @@ public class RedstoneTorchBlock extends BaseTorchBlock { - protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { - boolean hasNeighborSignal = this.hasNeighborSignal(level, pos, state); - // Paper start - Faster redstone torch rapid clock removal -- java.util.ArrayDeque redstoneUpdateInfos = level.redstoneUpdateInfos; -+ java.util.ArrayDeque redstoneUpdateInfos = level.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading - if (redstoneUpdateInfos != null) { - RedstoneTorchBlock.Toggle curr; -- while ((curr = redstoneUpdateInfos.peek()) != null && level.getGameTime() - curr.when > 60L) { -+ while ((curr = redstoneUpdateInfos.peek()) != null && level.getRedstoneGameTime() - curr.when > 60L) { // Folia - region threading - redstoneUpdateInfos.poll(); - } - } -@@ -154,13 +154,13 @@ public class RedstoneTorchBlock extends BaseTorchBlock { - - private static boolean isToggledTooFrequently(Level level, BlockPos pos, boolean logToggle) { - // Paper start - Faster redstone torch rapid clock removal -- java.util.ArrayDeque list = level.redstoneUpdateInfos; -+ java.util.ArrayDeque list = level.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading - if (list == null) { -- list = level.redstoneUpdateInfos = new java.util.ArrayDeque<>(); -+ list = level.getCurrentWorldData().redstoneUpdateInfos = new java.util.ArrayDeque<>(); // Folia - region threading - } - // Paper end - Faster redstone torch rapid clock removal - if (logToggle) { -- list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), level.getGameTime())); -+ list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), level.getRedstoneGameTime())); // Folia - region threading - } - - int i = 0; -@@ -182,12 +182,18 @@ public class RedstoneTorchBlock extends BaseTorchBlock { - } - - public static class Toggle { -- final BlockPos pos; -- final long when; -+ public final BlockPos pos; // Folia - region threading -+ long when; // Folia - region threading - - public Toggle(BlockPos pos, long when) { - this.pos = pos; - this.when = when; - } -+ -+ // Folia start - region ticking -+ public void offsetTime(long offset) { -+ this.when += offset; -+ } -+ // Folia end - region ticking - } - } -diff --git a/net/minecraft/world/level/block/SaplingBlock.java b/net/minecraft/world/level/block/SaplingBlock.java -index e014f052e9b0f5ca6b28044e2389782b7d0e0cb8..cc9e253d3033d3e970891067329aa281e85464f7 100644 ---- a/net/minecraft/world/level/block/SaplingBlock.java -+++ b/net/minecraft/world/level/block/SaplingBlock.java -@@ -26,7 +26,7 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { - protected static final float AABB_OFFSET = 6.0F; - protected static final VoxelShape SHAPE = Block.box(2.0, 0.0, 2.0, 14.0, 12.0, 14.0); - protected final TreeGrower treeGrower; -- public static org.bukkit.TreeType treeType; // CraftBukkit -+ public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // CraftBukkit // Folia - region threading - - @Override - public MapCodec codec() { -@@ -56,18 +56,19 @@ public class SaplingBlock extends BushBlock implements BonemealableBlock { - level.setBlock(pos, state.cycle(STAGE), 4); - } else { - // CraftBukkit start -- if (level.captureTreeGeneration) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading -+ if (worldData.captureTreeGeneration) { // Folia - region threading - this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); - } else { -- level.captureTreeGeneration = true; -+ worldData.captureTreeGeneration = true; // Folia - region threading - this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); -- level.captureTreeGeneration = false; -- if (!level.capturedBlockStates.isEmpty()) { -- org.bukkit.TreeType treeType = SaplingBlock.treeType; -- SaplingBlock.treeType = null; -+ worldData.captureTreeGeneration = false; // Folia - region threading -+ if (!worldData.capturedBlockStates.isEmpty()) { // Folia - region threading -+ org.bukkit.TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading -+ SaplingBlock.treeTypeRT.set(null); // Folia - region threading - org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level.getWorld()); -- java.util.List blocks = new java.util.ArrayList<>(level.capturedBlockStates.values()); -- level.capturedBlockStates.clear(); -+ java.util.List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading -+ worldData.capturedBlockStates.clear(); // Folia - region threading - org.bukkit.event.world.StructureGrowEvent event = null; - if (treeType != null) { - event = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks); -diff --git a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java -index 722f2b9a24679e0fc67aae2cd27051f96f962efe..fb8c09b18ea4112cbbe6e93bf6b9804d79628d36 100644 ---- a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java -+++ b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java -@@ -50,7 +50,7 @@ public abstract class SpreadingSnowyDirtBlock extends SnowyDirtBlock { - - @Override - protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { -- if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks -+ if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + pos.hashCode()) % level.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - regionised ticking - // Paper start - Perf: optimize dirt and snow spreading - final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = level.getChunkIfLoaded(pos); - if (cachedBlockChunk == null) { // Is this needed? -diff --git a/net/minecraft/world/level/block/WitherSkullBlock.java b/net/minecraft/world/level/block/WitherSkullBlock.java -index dc70aaa8d929c40c5f34c8facc1ad2bff4e98768..3ea53116725798a1eedb4802d6ebd7a32d8cccfd 100644 ---- a/net/minecraft/world/level/block/WitherSkullBlock.java -+++ b/net/minecraft/world/level/block/WitherSkullBlock.java -@@ -51,7 +51,7 @@ public class WitherSkullBlock extends SkullBlock { - } - - public static void checkSpawn(Level level, BlockPos pos, SkullBlockEntity blockEntity) { -- if (level.captureBlockStates) return; // CraftBukkit -+ if (level.getCurrentWorldData().captureBlockStates) return; // CraftBukkit // Folia - region threading - if (!level.isClientSide) { - BlockState blockState = blockEntity.getBlockState(); - boolean flag = blockState.is(Blocks.WITHER_SKELETON_SKULL) || blockState.is(Blocks.WITHER_SKELETON_WALL_SKULL); -diff --git a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java -index deef33d96db188cb297f04b581ab29e77e3716a9..413288e4a654b5ff8cc009b401d602731f63ec6d 100644 ---- a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java -+++ b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java -@@ -211,7 +211,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name - } - - int i = blockEntity.levels; final int originalLevels = i; // Paper - OBFHELPER -- if (level.getGameTime() % 80L == 0L) { -+ if (level.getRedstoneGameTime() % 80L == 0L) { // Folia - region threading - if (!blockEntity.beamSections.isEmpty()) { - blockEntity.levels = updateBase(level, x, y, z); - } -@@ -345,7 +345,7 @@ public class BeaconBlockEntity extends BlockEntity implements MenuProvider, Name - list = level.getEntitiesOfClass(Player.class, aabb); // Diff from applyEffect - } else { - list = new java.util.ArrayList<>(); -- for (final Player player : level.players()) { -+ for (final Player player : level.getLocalPlayers()) { // Folia - region threading - if (!net.minecraft.world.entity.EntitySelector.NO_SPECTATORS.test(player)) continue; - if (player.getBoundingBox().intersects(aabb)) { - list.add(player); -diff --git a/net/minecraft/world/level/block/entity/BlockEntity.java b/net/minecraft/world/level/block/entity/BlockEntity.java -index 77618757c0e678532dbab814aceed83f7f1cd892..003e9db957023486278679803b313ce89d573587 100644 ---- a/net/minecraft/world/level/block/entity/BlockEntity.java -+++ b/net/minecraft/world/level/block/entity/BlockEntity.java -@@ -26,7 +26,7 @@ import net.minecraft.world.level.block.state.BlockState; - import org.slf4j.Logger; - - public abstract class BlockEntity { -- static boolean ignoreBlockEntityUpdates; // Paper - Perf: Optimize Hoppers -+ static final ThreadLocal IGNORE_TILE_UPDATES = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading - // CraftBukkit start - data containers - private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); - public org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer; -@@ -40,6 +40,12 @@ public abstract class BlockEntity { - private BlockState blockState; - private DataComponentMap components = DataComponentMap.EMPTY; - -+ // Folia start - region ticking -+ public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { -+ -+ } -+ // Folia end - region ticking -+ - public BlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { - this.type = type; - this.worldPosition = pos.immutable(); -@@ -197,7 +203,7 @@ public abstract class BlockEntity { - - public void setChanged() { - if (this.level != null) { -- if (ignoreBlockEntityUpdates) return; // Paper - Perf: Optimize Hoppers -+ if (IGNORE_TILE_UPDATES.get().booleanValue()) return; // Paper - Perf: Optimize Hoppers // Folia - region threading - setChanged(this.level, this.worldPosition, this.blockState); - } - } -diff --git a/net/minecraft/world/level/block/entity/CommandBlockEntity.java b/net/minecraft/world/level/block/entity/CommandBlockEntity.java -index de75569d44855d9d6ec28cfee4403ecb6b45c4d3..c1e3a99a5fe917c728763be16c9a92d7252739a3 100644 ---- a/net/minecraft/world/level/block/entity/CommandBlockEntity.java -+++ b/net/minecraft/world/level/block/entity/CommandBlockEntity.java -@@ -66,6 +66,13 @@ public class CommandBlockEntity extends BlockEntity { - ); - } - -+ // Folia start -+ @Override -+ public void threadCheck() { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel) CommandBlockEntity.this.level, CommandBlockEntity.this.worldPosition, "Asynchronous sendSystemMessage to a command block"); -+ } -+ // Folia end -+ - @Override - public boolean isValid() { - return !CommandBlockEntity.this.isRemoved(); -diff --git a/net/minecraft/world/level/block/entity/ConduitBlockEntity.java b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java -index 269994df977801c67b8c576bde2478624b2631a1..719d3de54d440f09ba6cc6407b38e17ddc00ef88 100644 ---- a/net/minecraft/world/level/block/entity/ConduitBlockEntity.java -+++ b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java -@@ -81,7 +81,7 @@ public class ConduitBlockEntity extends BlockEntity { - - public static void clientTick(Level level, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { - blockEntity.tickCount++; -- long gameTime = level.getGameTime(); -+ long gameTime = level.getRedstoneGameTime(); // Folia - region threading - List list = blockEntity.effectBlocks; - if (gameTime % 40L == 0L) { - blockEntity.isActive = updateShape(level, pos, list); -@@ -97,7 +97,7 @@ public class ConduitBlockEntity extends BlockEntity { - - public static void serverTick(Level level, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { - blockEntity.tickCount++; -- long gameTime = level.getGameTime(); -+ long gameTime = level.getRedstoneGameTime(); // Folia - region threading - List list = blockEntity.effectBlocks; - if (gameTime % 40L == 0L) { - boolean flag = updateShape(level, pos, list); -diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java -index 5cd1326ad5d046c88b2b3449d610a78fa880b4cd..ae988c4910421fb720177178ef6136e595ae6946 100644 ---- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java -+++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java -@@ -34,7 +34,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - private static final int[][] CACHED_SLOTS = new int[54][]; - private NonNullList items = NonNullList.withSize(5, ItemStack.EMPTY); - public int cooldownTime = -1; -- private long tickedGameTime; -+ private long tickedGameTime = Long.MIN_VALUE; // Folia - region threading - private Direction facing; - - // CraftBukkit start - add fields and methods -@@ -67,6 +67,15 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - } - // CraftBukkit end - -+ // Folia start - region threading -+ @Override -+ public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { -+ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); -+ if (this.tickedGameTime != Long.MIN_VALUE) { -+ this.tickedGameTime += fromRedstoneTimeOffset; -+ } -+ } -+ // Folia end - region threading - - public HopperBlockEntity(BlockPos pos, BlockState blockState) { - super(BlockEntityType.HOPPER, pos, blockState); -@@ -125,7 +134,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - public static void pushItemsTick(Level level, BlockPos pos, BlockState state, HopperBlockEntity blockEntity) { - blockEntity.cooldownTime--; -- blockEntity.tickedGameTime = level.getGameTime(); -+ blockEntity.tickedGameTime = level.getRedstoneGameTime(); // Folia - region threading - if (!blockEntity.isOnCooldown()) { - blockEntity.setCooldown(0); - // Spigot start -@@ -213,12 +222,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - } - - // Paper start - Perf: Optimize Hoppers -- public static boolean skipHopperEvents; -- private static boolean skipPullModeEventFire; -- private static boolean skipPushModeEventFire; -+ // Folia - region threading - moved to RegionizedWorldData - - private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) { -- skipPushModeEventFire = skipHopperEvents; -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading -+ worldData.skipPushModeEventFire = worldData.skipHopperEvents; // Folia - region threading - boolean foundItem = false; - for (int i = 0; i < hopper.getContainerSize(); ++i) { - final ItemStack item = hopper.getItem(i); -@@ -233,7 +241,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - // We only need to fire the event once to give protection plugins a chance to cancel this event - // Because nothing uses getItem, every event call should end up the same result. -- if (!skipPushModeEventFire) { -+ if (!worldData.skipPushModeEventFire) { // Folia - region threading - movedItem = callPushMoveEvent(destination, movedItem, hopper); - if (movedItem == null) { // cancelled - origItemStack.setCount(originalItemCount); -@@ -263,13 +271,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - } - - private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading - ItemStack movedItem = origItemStack; - final int originalItemCount = origItemStack.getCount(); - final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); - container.setChanged(); // original logic always marks source inv as changed even if no move happens. - movedItem.setCount(movedItemCount); - -- if (!skipPullModeEventFire) { -+ if (!worldData.skipPullModeEventFire) { // Folia - region threading - movedItem = callPullMoveEvent(hopper, container, movedItem); - if (movedItem == null) { // cancelled - origItemStack.setCount(originalItemCount); -@@ -289,9 +298,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); - } - -- ignoreBlockEntityUpdates = true; -+ IGNORE_TILE_UPDATES.set(true); // Folia - region threading - container.setItem(i, origItemStack); -- ignoreBlockEntityUpdates = false; -+ IGNORE_TILE_UPDATES.set(false); // Folia - region threading - container.setChanged(); - return true; - } -@@ -306,6 +315,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - @Nullable - private static ItemStack callPushMoveEvent(Container destination, ItemStack itemStack, HopperBlockEntity hopper) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading - final org.bukkit.inventory.Inventory destinationInventory = getInventory(destination); - final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent( - hopper.getOwner(false).getInventory(), -@@ -315,7 +325,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - ); - final boolean result = event.callEvent(); - if (!event.calledGetItem && !event.calledSetItem) { -- skipPushModeEventFire = true; -+ worldData.skipPushModeEventFire = true; // Folia - region threading - } - if (!result) { - applyCooldown(hopper); -@@ -331,6 +341,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - - @Nullable - private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading - final org.bukkit.inventory.Inventory sourceInventory = getInventory(container); - final org.bukkit.inventory.Inventory destination = getInventory(hopper); - -@@ -338,7 +349,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), destination, false); - final boolean result = event.callEvent(); - if (!event.calledGetItem && !event.calledSetItem) { -- skipPullModeEventFire = true; -+ worldData.skipPullModeEventFire = true; // Folia - region threading - } - if (!result) { - applyCooldown(hopper); -@@ -524,12 +535,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - } - - public static boolean suckInItems(Level level, Hopper hopper) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading - BlockPos blockPos = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ()); - BlockState blockState = level.getBlockState(blockPos); - Container sourceContainer = getSourceContainer(level, hopper, blockPos, blockState); - if (sourceContainer != null) { - Direction direction = Direction.DOWN; -- skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers -+ worldData.skipPullModeEventFire = worldData.skipHopperEvents; // Paper - Perf: Optimize Hoppers // Folia - region threading - - for (int i : getSlots(sourceContainer, direction)) { - if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction, level)) { // Spigot -@@ -678,9 +690,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - stack = stack.split(destination.getMaxStackSize()); - } - // Spigot end -- ignoreBlockEntityUpdates = true; // Paper - Perf: Optimize Hoppers -+ IGNORE_TILE_UPDATES.set(Boolean.TRUE); // Paper - Perf: Optimize Hoppers // Folia - region threading - destination.setItem(slot, stack); -- ignoreBlockEntityUpdates = false; // Paper - Perf: Optimize Hoppers -+ IGNORE_TILE_UPDATES.set(Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading - stack = leftover; // Paper - Make hoppers respect inventory max stack size - flag = true; - } else if (canMergeItems(item, stack)) { -diff --git a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java -index 1638eccef431fb68775af624110f1968f0c6dabd..bd6693af6412fb08a28ca9a71d5c70d54f72c6e6 100644 ---- a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java -+++ b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java -@@ -43,9 +43,9 @@ public class SculkCatalystBlockEntity extends BlockEntity implements GameEventLi - // Paper end - Fix NPE in SculkBloomEvent world access - - public static void serverTick(Level level, BlockPos pos, BlockState state, SculkCatalystBlockEntity sculkCatalyst) { -- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = sculkCatalyst.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. -+ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(sculkCatalyst.getBlockPos()); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading - sculkCatalyst.catalystListener.getSculkSpreader().updateCursors(level, pos, level.getRandom(), true); -- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit -+ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(null); // CraftBukkit // Folia - region threading - } - - @Override -diff --git a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java -index 5bf39c542757bf97da8909b65c22786a8a30385a..61887e6b052bca715c90dff5d9cd657e0b3f6a78 100644 ---- a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java -+++ b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java -@@ -35,9 +35,12 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { - public long age; - private int teleportCooldown; - @Nullable -- public BlockPos exitPortal; -+ public volatile BlockPos exitPortal; // Folia - region threading - volatile - public boolean exactTeleport; - -+ private static final java.util.concurrent.atomic.AtomicLong SEARCHING_FOR_EXIT_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); // Folia - region threading -+ private Long searchingForExitId; // Folia - region threading -+ - public TheEndGatewayBlockEntity(BlockPos pos, BlockState blockState) { - super(BlockEntityType.END_GATEWAY, pos, blockState); - } -@@ -129,6 +132,104 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { - } - } - -+ // Folia start - region threading -+ private void trySearchForExit(ServerLevel world, BlockPos fromPos) { -+ if (this.searchingForExitId != null) { -+ return; -+ } -+ this.searchingForExitId = Long.valueOf(SEARCHING_FOR_EXIT_ID_GENERATOR.getAndIncrement()); -+ int chunkX = fromPos.getX() >> 4; -+ int chunkZ = fromPos.getZ() >> 4; -+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( -+ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, -+ chunkX, chunkZ, -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, -+ this.searchingForExitId -+ ); -+ -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); -+ -+ complete.addWaiter((tpLoc, throwable) -> { -+ // create the exit portal -+ TheEndGatewayBlockEntity.LOGGER.debug("Creating portal at {}", tpLoc); -+ TheEndGatewayBlockEntity.spawnGatewayPortal(world, tpLoc, EndGatewayConfiguration.knownExit(fromPos, false)); -+ -+ // need to go onto the tick thread to avoid saving issues -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ world, chunkX, chunkZ, -+ () -> { -+ // update the exit portal location -+ TheEndGatewayBlockEntity.this.exitPortal = tpLoc; -+ -+ // remove ticket keeping the gateway loaded -+ world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( -+ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, -+ chunkX, chunkZ, -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, -+ this.searchingForExitId -+ ); -+ TheEndGatewayBlockEntity.this.searchingForExitId = null; -+ } -+ ); -+ }); -+ -+ findOrCreateValidTeleportPosRegionThreading(world, fromPos, complete); -+ } -+ -+ public static boolean teleportRegionThreading(ServerLevel portalWorld, BlockPos portalPos, -+ net.minecraft.world.entity.Entity toTeleport, -+ TheEndGatewayBlockEntity portalTile, -+ net.minecraft.world.level.portal.TeleportTransition.PostTeleportTransition post) { -+ // can we even teleport in this dimension? -+ if (portalTile.exitPortal == null && portalWorld.getTypeKey() != net.minecraft.world.level.dimension.LevelStem.END) { -+ return false; -+ } -+ -+ // First, find the position we are trying to teleport to -+ BlockPos teleportPos = portalTile.exitPortal; -+ boolean isExactTeleport = portalTile.exactTeleport; -+ -+ if (teleportPos == null) { -+ portalTile.trySearchForExit(portalWorld, portalPos); -+ return false; -+ } -+ -+ // note: we handle the position from the TeleportTransition -+ net.minecraft.world.level.portal.TeleportTransition teleport = net.minecraft.world.level.block.EndGatewayBlock.getTeleportTransition( -+ portalWorld, toTeleport, Vec3.atCenterOf(teleportPos) -+ ); -+ -+ -+ if (isExactTeleport) { -+ // blind teleport -+ return toTeleport.teleportAsync( -+ teleport, net.minecraft.world.entity.Entity.TELEPORT_FLAG_LOAD_CHUNK | net.minecraft.world.entity.Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, -+ post == null ? null : (net.minecraft.world.entity.Entity teleportedEntity) -> { -+ post.onTransition(teleportedEntity); -+ } -+ ); -+ } else { -+ // we could hack around by first loading the chunks, then calling back to here and checking if the entity -+ // should be teleported, something something else... -+ // however, we know the target location cannot differ by one region section: so we can -+ // just teleport and adjust the position after -+ return toTeleport.teleportAsync( -+ teleport, net.minecraft.world.entity.Entity.TELEPORT_FLAG_LOAD_CHUNK | net.minecraft.world.entity.Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, -+ (net.minecraft.world.entity.Entity teleportedEntity) -> { -+ // adjust to the final exit position -+ Vec3 adjusted = Vec3.atCenterOf(TheEndGatewayBlockEntity.findExitPosition(portalWorld, teleportPos)); -+ // teleportTo will adjust rider positions -+ teleportedEntity.teleportTo(adjusted.x, adjusted.y, adjusted.z); -+ -+ if (post != null) { -+ post.onTransition(teleportedEntity); -+ } -+ } -+ ); -+ } -+ } -+ // Folia end - region threading -+ - @Nullable - public Vec3 getPortalPosition(ServerLevel level, BlockPos pos) { - if (this.exitPortal == null && level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { // CraftBukkit - work in alternate worlds -@@ -174,6 +275,124 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { - return findTallestBlock(level, blockPos, 16, true); - } - -+ // Folia start - region threading -+ private static void findOrCreateValidTeleportPosRegionThreading(ServerLevel world, BlockPos pos, -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete) { -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable tentativeSelection = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); -+ -+ tentativeSelection.addWaiter((vec3d, throwable) -> { -+ LevelChunk chunk = TheEndGatewayBlockEntity.getChunk(world, vec3d); -+ BlockPos blockposition1 = TheEndGatewayBlockEntity.findValidSpawnInChunk(chunk); -+ if (blockposition1 == null) { -+ BlockPos blockposition2 = BlockPos.containing(vec3d.x + 0.5D, 75.0D, vec3d.z + 0.5D); -+ -+ TheEndGatewayBlockEntity.LOGGER.debug("Failed to find a suitable block to teleport to, spawning an island on {}", blockposition2); -+ world.registryAccess().lookup(Registries.CONFIGURED_FEATURE).flatMap((iregistry) -> { -+ return iregistry.get(EndFeatures.END_ISLAND); -+ }).ifPresent((holder_c) -> { -+ ((net.minecraft.world.level.levelgen.feature.ConfiguredFeature) holder_c.value()).place(world, world.getChunkSource().getGenerator(), RandomSource.create(blockposition2.asLong()), blockposition2); -+ }); -+ blockposition1 = blockposition2; -+ } else { -+ TheEndGatewayBlockEntity.LOGGER.debug("Found suitable block to teleport to: {}", blockposition1); -+ } -+ -+ // Here, there is no guarantee the chunks in 1 radius are in this region due to the fact that we just chained -+ // possibly 16x chunk loads along an axis (findExitPortalXZPosTentativeRegionThreading) using the chunk queue -+ // (regioniser only guarantees at least 8 chunks along a single axis) -+ // so, we need to schedule for the next tick -+ int posX = blockposition1.getX(); -+ int posZ = blockposition1.getZ(); -+ int radius = 16; -+ -+ BlockPos finalBlockPosition1 = blockposition1; -+ world.moonrise$loadChunksAsync(blockposition1, radius, -+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, -+ (java.util.List chunks) -> { -+ // make sure chunks are kept loaded -+ for (net.minecraft.world.level.chunk.ChunkAccess access : chunks) { -+ world.chunkSource.addTicketAtLevel( -+ net.minecraft.server.level.TicketType.DELAYED, access.getPos(), -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, -+ net.minecraft.util.Unit.INSTANCE -+ ); -+ } -+ // now after the chunks are loaded, we can delay by one tick -+ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( -+ world, posX >> 4, posZ >> 4, () -> { -+ // find final location -+ BlockPos tpLoc = TheEndGatewayBlockEntity.findTallestBlock(world, finalBlockPosition1, radius, true).above(GATEWAY_HEIGHT_ABOVE_SURFACE); -+ -+ // done -+ complete.complete(tpLoc); -+ } -+ ); -+ } -+ ); -+ }); -+ -+ // fire off chain -+ findExitPortalXZPosTentativeRegionThreading(world, pos, tentativeSelection); -+ } -+ -+ private static void findExitPortalXZPosTentativeRegionThreading(ServerLevel world, BlockPos pos, -+ ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete) { -+ Vec3 posDirFromOrigin = new Vec3(pos.getX(), 0.0D, pos.getZ()).normalize(); -+ Vec3 posDirExtruded = posDirFromOrigin.scale(1024.0D); -+ -+ class Vars { -+ int i = 16; -+ boolean mode = false; -+ Vec3 currPos = posDirExtruded; -+ } -+ Vars vars = new Vars(); -+ -+ Runnable handle = new Runnable() { -+ @Override -+ public void run() { -+ if (vars.mode != TheEndGatewayBlockEntity.isChunkEmpty(world, vars.currPos)) { -+ vars.i = 0; // fall back to completing -+ } -+ -+ // try to load next chunk -+ if (vars.i-- <= 0) { -+ if (vars.mode) { -+ complete.complete(vars.currPos); -+ return; -+ } -+ vars.mode = true; -+ vars.i = 16; -+ } -+ -+ vars.currPos = vars.currPos.add(posDirFromOrigin.scale(vars.mode ? 16.0 : -16.0)); -+ // schedule next iteration -+ world.moonrise$getChunkTaskScheduler().scheduleChunkLoad( -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(vars.currPos), -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(vars.currPos), -+ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, -+ true, -+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, -+ (chunk) -> { -+ this.run(); -+ } -+ ); -+ } -+ }; -+ -+ // kick off first chunk load -+ world.moonrise$getChunkTaskScheduler().scheduleChunkLoad( -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(posDirExtruded), -+ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(posDirExtruded), -+ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, -+ true, -+ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, -+ (chunk) -> { -+ handle.run(); -+ } -+ ); -+ } -+ // Folia end - region threading -+ - private static Vec3 findExitPortalXZPosTentative(ServerLevel level, BlockPos pos) { - Vec3 vec3 = new Vec3(pos.getX(), 0.0, pos.getZ()).normalize(); - int i = 1024; -diff --git a/net/minecraft/world/level/block/entity/TickingBlockEntity.java b/net/minecraft/world/level/block/entity/TickingBlockEntity.java -index 28e3b73507b988f7234cbf29c4024c88180d0aef..c8facee29ee08e0975528083f89b64f0b593957f 100644 ---- a/net/minecraft/world/level/block/entity/TickingBlockEntity.java -+++ b/net/minecraft/world/level/block/entity/TickingBlockEntity.java -@@ -10,4 +10,6 @@ public interface TickingBlockEntity { - BlockPos getPos(); - - String getType(); -+ -+ BlockEntity getTileEntity(); // Folia - region threading - } -diff --git a/net/minecraft/world/level/block/grower/TreeGrower.java b/net/minecraft/world/level/block/grower/TreeGrower.java -index cf7311c507de09a8f89934e430b2201e8bdffe51..80de710b4e1528587b509e50bdd69983bcb608d0 100644 ---- a/net/minecraft/world/level/block/grower/TreeGrower.java -+++ b/net/minecraft/world/level/block/grower/TreeGrower.java -@@ -203,56 +203,58 @@ public final class TreeGrower { - - // CraftBukkit start - private void setTreeType(Holder> holder) { -+ org.bukkit.TreeType treeType; // Folia - region threading - ResourceKey> treeFeature = holder.unwrapKey().get(); - if (treeFeature == TreeFeatures.OAK || treeFeature == TreeFeatures.OAK_BEES_005) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TREE; -+ treeType = org.bukkit.TreeType.TREE; // Folia - region threading - } else if (treeFeature == TreeFeatures.HUGE_RED_MUSHROOM) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_MUSHROOM; -+ treeType = org.bukkit.TreeType.RED_MUSHROOM; // Folia - region threading - } else if (treeFeature == TreeFeatures.HUGE_BROWN_MUSHROOM) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BROWN_MUSHROOM; -+ treeType = org.bukkit.TreeType.BROWN_MUSHROOM; // Folia - region threading - } else if (treeFeature == TreeFeatures.JUNGLE_TREE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.COCOA_TREE; -+ treeType = org.bukkit.TreeType.COCOA_TREE; // Folia - region threading - } else if (treeFeature == TreeFeatures.JUNGLE_TREE_NO_VINE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SMALL_JUNGLE; -+ treeType = org.bukkit.TreeType.SMALL_JUNGLE; // Folia - region threading - } else if (treeFeature == TreeFeatures.PINE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_REDWOOD; -+ treeType = org.bukkit.TreeType.TALL_REDWOOD; // Folia - region threading - } else if (treeFeature == TreeFeatures.SPRUCE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD; -+ treeType = org.bukkit.TreeType.REDWOOD; // Folia - region threading - } else if (treeFeature == TreeFeatures.ACACIA) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA; -+ treeType = org.bukkit.TreeType.ACACIA; // Folia - region threading - } else if (treeFeature == TreeFeatures.BIRCH || treeFeature == TreeFeatures.BIRCH_BEES_005) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIRCH; -+ treeType = org.bukkit.TreeType.BIRCH; // Folia - region threading - } else if (treeFeature == TreeFeatures.SUPER_BIRCH_BEES_0002) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_BIRCH; -+ treeType = org.bukkit.TreeType.TALL_BIRCH; // Folia - region threading - } else if (treeFeature == TreeFeatures.SWAMP_OAK) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP; -+ treeType = org.bukkit.TreeType.SWAMP; // Folia - region threading - } else if (treeFeature == TreeFeatures.FANCY_OAK || treeFeature == TreeFeatures.FANCY_OAK_BEES_005) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIG_TREE; -+ treeType = org.bukkit.TreeType.BIG_TREE; // Folia - region threading - } else if (treeFeature == TreeFeatures.JUNGLE_BUSH) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE_BUSH; -+ treeType = org.bukkit.TreeType.JUNGLE_BUSH; // Folia - region threading - } else if (treeFeature == TreeFeatures.DARK_OAK) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.DARK_OAK; -+ treeType = org.bukkit.TreeType.DARK_OAK; // Folia - region threading - } else if (treeFeature == TreeFeatures.MEGA_SPRUCE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_REDWOOD; -+ treeType = org.bukkit.TreeType.MEGA_REDWOOD; // Folia - region threading - } else if (treeFeature == TreeFeatures.MEGA_PINE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_PINE; -+ treeType = org.bukkit.TreeType.MEGA_PINE; // Folia - region threading - } else if (treeFeature == TreeFeatures.MEGA_JUNGLE_TREE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE; -+ treeType = org.bukkit.TreeType.JUNGLE; // Folia - region threading - } else if (treeFeature == TreeFeatures.AZALEA_TREE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA; -+ treeType = org.bukkit.TreeType.AZALEA; // Folia - region threading - } else if (treeFeature == TreeFeatures.MANGROVE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE; -+ treeType = org.bukkit.TreeType.MANGROVE; // Folia - region threading - } else if (treeFeature == TreeFeatures.TALL_MANGROVE) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_MANGROVE; -+ treeType = org.bukkit.TreeType.TALL_MANGROVE; // Folia - region threading - } else if (treeFeature == TreeFeatures.CHERRY || treeFeature == TreeFeatures.CHERRY_BEES_005) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.CHERRY; -+ treeType = org.bukkit.TreeType.CHERRY; // Folia - region threading - } else if (treeFeature == TreeFeatures.PALE_OAK || treeFeature == TreeFeatures.PALE_OAK_BONEMEAL) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK; -+ treeType = org.bukkit.TreeType.PALE_OAK; // Folia - region threading - } else if (treeFeature == TreeFeatures.PALE_OAK_CREAKING) { -- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; -+ treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; // Folia - region threading - } else { - throw new IllegalArgumentException("Unknown tree generator " + treeFeature); - } -+ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(treeType); // Folia - region threading - } - // CraftBukkit end - } -diff --git a/net/minecraft/world/level/block/piston/PistonBaseBlock.java b/net/minecraft/world/level/block/piston/PistonBaseBlock.java -index 2b1d5328072710784d2399b523afcbcfb1d7f0cd..bbb6d89da4b2fb90d8c1bc7c0cea6c76da623f1a 100644 ---- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java -+++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java -@@ -139,7 +139,7 @@ public class PistonBaseBlock extends DirectionalBlock { - && pistonMovingBlockEntity.isExtending() - && ( - pistonMovingBlockEntity.getProgress(0.0F) < 0.5F -- || level.getGameTime() == pistonMovingBlockEntity.getLastTicked() -+ || level.getRedstoneGameTime() == pistonMovingBlockEntity.getLastTicked() // Folia - region threading - || ((ServerLevel)level).isHandlingTick() - )) { - i = 2; -diff --git a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java -index 1e6e940fca9d96ef410c7bf05524bd9b24db4a79..3df23feff6937b6a2dbeff82e489a9a4ff644843 100644 ---- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java -+++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java -@@ -41,9 +41,19 @@ public class PistonMovingBlockEntity extends BlockEntity { - private static final ThreadLocal NOCLIP = ThreadLocal.withInitial(() -> null); - private float progress; - private float progressO; -- private long lastTicked; -+ private long lastTicked = Long.MIN_VALUE; // Folia - region threading - private int deathTicks; - -+ // Folia start - region threading -+ @Override -+ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { -+ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); -+ if (this.lastTicked != Long.MIN_VALUE) { -+ this.lastTicked += fromRedstoneTimeOffset; -+ } -+ } -+ // Folia end - region threading -+ - public PistonMovingBlockEntity(BlockPos pos, BlockState blockState) { - super(BlockEntityType.PISTON, pos, blockState); - } -@@ -150,8 +160,8 @@ public class PistonMovingBlockEntity extends BlockEntity { - - entity.setDeltaMovement(d1, d2, d3); - // Paper - EAR items stuck in slime pushed by a piston -- entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10); -- entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10); -+ entity.activatedTick = Math.max(entity.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading -+ entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading - // Paper end - break; - } -@@ -292,7 +302,7 @@ public class PistonMovingBlockEntity extends BlockEntity { - } - - public static void tick(Level level, BlockPos pos, BlockState state, PistonMovingBlockEntity blockEntity) { -- blockEntity.lastTicked = level.getGameTime(); -+ blockEntity.lastTicked = level.getRedstoneGameTime(); // Folia - region threading - blockEntity.progressO = blockEntity.progress; - if (blockEntity.progressO >= 1.0F) { - if (level.isClientSide && blockEntity.deathTicks < 5) { -diff --git a/net/minecraft/world/level/border/WorldBorder.java b/net/minecraft/world/level/border/WorldBorder.java -index 7249292e77b4a54f1f4f707c4dc55924c96dd23f..eb0d3cc606fb7bb06871ea61c240873ed7e67bc5 100644 ---- a/net/minecraft/world/level/border/WorldBorder.java -+++ b/net/minecraft/world/level/border/WorldBorder.java -@@ -30,6 +30,8 @@ public class WorldBorder { - public static final WorldBorder.Settings DEFAULT_SETTINGS = new WorldBorder.Settings(0.0, 0.0, 0.2, 5.0, 5, 15, 5.999997E7F, 0L, 0.0); - public net.minecraft.server.level.ServerLevel world; // CraftBukkit - -+ // Folia - region threading - TODO make this shit thread-safe -+ - public boolean isWithinBounds(BlockPos pos) { - return this.isWithinBounds(pos.getX(), pos.getZ()); - } -@@ -43,16 +45,14 @@ public class WorldBorder { - } - - // Paper start - Bound treasure maps to world border -- private final BlockPos.MutableBlockPos mutPos = new BlockPos.MutableBlockPos(); -+ private static final ThreadLocal mutPos = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Folia - region threading - - public boolean isBlockInBounds(int chunkX, int chunkZ) { -- this.mutPos.set(chunkX, 64, chunkZ); -- return this.isWithinBounds(this.mutPos); -+ return this.isWithinBounds(mutPos.get().set(chunkX, 64, chunkZ)); // Folia - region threading - } - - public boolean isChunkInBounds(int chunkX, int chunkZ) { -- this.mutPos.set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15); -- return this.isWithinBounds(this.mutPos); -+ return this.isWithinBounds(mutPos.get().set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15)); // Folia - region threading - } - // Paper end - Bound treasure maps to world border - -diff --git a/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java -index 6ed51cf42b5864194d671b5b56f5b9bdf0291dc0..b85c547f281c58bf45c9062d0b886cb4ff7b386b 100644 ---- a/net/minecraft/world/level/chunk/ChunkGenerator.java -+++ b/net/minecraft/world/level/chunk/ChunkGenerator.java -@@ -327,7 +327,7 @@ public abstract class ChunkGenerator { - } - - private static boolean tryAddReference(StructureManager structureManager, StructureStart structureStart) { -- if (structureStart.canBeReferenced()) { -+ if (structureStart.tryReference()) { // Folia - region threading - structureManager.addReference(structureStart); - return true; - } else { -diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java -index 761fdcd4a4e18f45547afd8edff44f61c6eeacb4..f83cfa85678d288ece2348aae41d315660095ad8 100644 ---- a/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/net/minecraft/world/level/chunk/LevelChunk.java -@@ -59,6 +59,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - public void tick() { - } - -+ // Folia start - region threading -+ @Override -+ public BlockEntity getTileEntity() { -+ return null; -+ } -+ // Folia end - region threading -+ - @Override - public boolean isRemoved() { - return true; -@@ -230,11 +237,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - - @Override - public void markUnsaved() { -- boolean isUnsaved = this.isUnsaved(); -- super.markUnsaved(); -- if (!isUnsaved) { -- this.unsavedListener.setUnsaved(this.chunkPos); -- } -+ super.markUnsaved(); // Folia - region threading - unsavedListener is not really use - } - - @Override -@@ -360,6 +363,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - - @Nullable - public BlockState setBlockState(BlockPos pos, BlockState state, boolean isMoving, boolean doPlace) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, pos, "Updating block asynchronously"); // Folia - region threading - // CraftBukkit end - int y = pos.getY(); - LevelChunkSection section = this.getSection(this.getSectionIndex(y)); -@@ -395,7 +399,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - } - - boolean hasBlockEntity = blockState.hasBlockEntity(); -- if (!this.level.isClientSide && !this.level.isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent -+ if (!this.level.isClientSide && !this.level.getCurrentWorldData().isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading - blockState.onRemove(this.level, pos, state, isMoving); - } else if (!blockState.is(block) && hasBlockEntity) { - this.removeBlockEntity(pos); -@@ -404,7 +408,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - if (!section.getBlockState(i, i1, i2).is(block)) { - return null; - } else { -- if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. -+ if (!this.level.isClientSide && doPlace && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. // Folia - region threading - state.onPlace(this.level, pos, blockState, isMoving); - } - -@@ -459,7 +463,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - @Nullable - public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) { - // CraftBukkit start -- BlockEntity blockEntity = this.level.capturedTileEntities.get(pos); -+ BlockEntity blockEntity = this.level.getCurrentWorldData().capturedTileEntities.get(pos); // Folia - region threading - if (blockEntity == null) { - blockEntity = this.blockEntities.get(pos); - } -@@ -646,13 +650,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - - org.bukkit.World world = this.level.getWorld(); - if (world != null) { -- this.level.populating = true; -+ this.level.getCurrentWorldData().populating = true; // Folia - region threading - try { - for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) { - populator.populate(world, random, bukkitChunk); - } - } finally { -- this.level.populating = false; -+ this.level.getCurrentWorldData().populating = false; // Folia - region threading - } - } - server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk)); -@@ -678,7 +682,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - @Override - public boolean isUnsaved() { - // Paper start - rewrite chunk system -- final long gameTime = this.level.getGameTime(); -+ final long gameTime = this.level.getRedstoneGameTime(); // Folia - region threading - if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime) - || ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) { - return true; -@@ -905,6 +909,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - this.ticker = ticker; - } - -+ // Folia start - region threading -+ @Override -+ public BlockEntity getTileEntity() { -+ return this.blockEntity; -+ } -+ // Folia end - region threading -+ - @Override - public void tick() { - if (!this.blockEntity.isRemoved() && this.blockEntity.hasLevel()) { -@@ -983,6 +994,13 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - this.ticker = ticker; - } - -+ // Folia start - region threading -+ @Override -+ public BlockEntity getTileEntity() { -+ return this.ticker == null ? null : this.ticker.getTileEntity(); -+ } -+ // Folia end - region threading -+ - @Override - public void tick() { - this.ticker.tick(); -diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java -index 6b6aaeca14178b5b709e20ae13552d42217f15c0..950977f8d123f903630541ded35dd86a1889240f 100644 ---- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java -+++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java -@@ -574,7 +574,7 @@ public record SerializableChunkData( - } - } - -- ChunkAccess.PackedTicks ticksForSerialization = chunk.getTicksForSerialization(level.getGameTime()); -+ ChunkAccess.PackedTicks ticksForSerialization = chunk.getTicksForSerialization(level.getRedstoneGameTime()); // Folia - region threading - ShortList[] lists = Arrays.stream(chunk.getPostProcessing()) - .map(list3 -> list3 != null ? new ShortArrayList(list3) : null) - .toArray(ShortList[]::new); -diff --git a/net/minecraft/world/level/dimension/end/EndDragonFight.java b/net/minecraft/world/level/dimension/end/EndDragonFight.java -index 6e7e87c32734b3aae354bc34459e5f207da5c78f..2e156694b337760be986fdf1cbf863b0d896ef2d 100644 ---- a/net/minecraft/world/level/dimension/end/EndDragonFight.java -+++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java -@@ -77,7 +77,7 @@ public class EndDragonFight { - .setPlayBossMusic(true) - .setCreateWorldFog(true); - public final ServerLevel level; -- private final BlockPos origin; -+ public final BlockPos origin; // Folia - region threading - public final ObjectArrayList gateways = new ObjectArrayList<>(); - private final BlockPattern exitPortalPattern; - private int ticksSinceDragonSeen; -@@ -162,7 +162,7 @@ public class EndDragonFight { - - if (!this.dragonEvent.getPlayers().isEmpty()) { - this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE); -- boolean isArenaLoaded = this.isArenaLoaded(); -+ boolean isArenaLoaded = this.isArenaLoaded(); if (!isArenaLoaded) { return; } // Folia - region threading - don't tick if we don't own the entire region - if (this.needsStateScanning && isArenaLoaded) { - this.scanState(); - this.needsStateScanning = false; -@@ -208,6 +208,12 @@ public class EndDragonFight { - } - - List dragons = this.level.getDragons(); -+ // Folia start - region threading -+ // we do not want to deal with any dragons NOT nearby -+ dragons.removeIf((EnderDragon dragon) -> { -+ return !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(dragon); -+ }); -+ // Folia end - region threading - if (dragons.isEmpty()) { - this.dragonKilled = true; - } else { -@@ -323,8 +329,8 @@ public class EndDragonFight { - - for (int i = -8 + chunkPos.x; i <= 8 + chunkPos.x; i++) { - for (int i1 = 8 + chunkPos.z; i1 <= 8 + chunkPos.z; i1++) { -- ChunkAccess chunk = this.level.getChunk(i, i1, ChunkStatus.FULL, false); -- if (!(chunk instanceof LevelChunk)) { -+ ChunkAccess chunk = this.level.getChunkIfLoaded(i, i1); // Folia - region threading -+ if (!(chunk instanceof LevelChunk) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, i, i1, this.level.regioniser.regionSectionChunkSize)) { - return false; - } - -@@ -496,6 +502,11 @@ public class EndDragonFight { - } - - public void onCrystalDestroyed(EndCrystal crystal, DamageSource dmgSrc) { -+ // Folia start - region threading -+ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) { -+ return; -+ } -+ // Folia end - region threading - if (this.respawnStage != null && this.respawnCrystals.contains(crystal)) { - LOGGER.debug("Aborting respawn sequence"); - this.respawnStage = null; -@@ -521,7 +532,7 @@ public class EndDragonFight { - - public boolean tryRespawn(@Nullable BlockPos placedEndCrystalPos) { // placedEndCrystalPos is null if the tryRespawn() call was not caused by a placed end crystal - // Paper end - Perf: Do crystal-portal proximity check before entity lookup -- if (this.dragonKilled && this.respawnStage == null) { -+ if (this.dragonKilled && this.respawnStage == null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) { // Folia - region threading - BlockPos blockPos = this.portalLocation; - if (blockPos == null) { - LOGGER.debug("Tried to respawn, but need to find the portal first."); -diff --git a/net/minecraft/world/level/levelgen/PatrolSpawner.java b/net/minecraft/world/level/levelgen/PatrolSpawner.java -index 082c9b340765e3e98055a3c4444af68264a54826..9608e06c56f0aded4d6b4e9cf3d7eec348945600 100644 ---- a/net/minecraft/world/level/levelgen/PatrolSpawner.java -+++ b/net/minecraft/world/level/levelgen/PatrolSpawner.java -@@ -16,7 +16,7 @@ import net.minecraft.world.level.biome.Biome; - import net.minecraft.world.level.block.state.BlockState; - - public class PatrolSpawner implements CustomSpawner { -- private int nextTick; -+ //private int nextTick; // Folia - region threading - - @Override - public int tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { -@@ -27,6 +27,7 @@ public class PatrolSpawner implements CustomSpawner { - return 0; - } else { - RandomSource randomSource = level.random; -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading - // this.nextTick--; - // if (this.nextTick > 0) { - // return 0; -@@ -38,12 +39,12 @@ public class PatrolSpawner implements CustomSpawner { - // } else if (randomSource.nextInt(5) != 0) { - // Paper start - Pillager patrol spawn settings and per player options - // Random player selection moved up for per player spawning and configuration -- int size = level.players().size(); -+ int size = level.getLocalPlayers().size(); - if (size < 1) { - return 0; - } - -- net.minecraft.server.level.ServerPlayer player = level.players().get(randomSource.nextInt(size)); -+ net.minecraft.server.level.ServerPlayer player = level.getLocalPlayers().get(randomSource.nextInt(size)); // Folia - region threading - if (player.isSpectator()) { - return 0; - } -@@ -53,8 +54,8 @@ public class PatrolSpawner implements CustomSpawner { - --player.patrolSpawnDelay; - patrolSpawnDelay = player.patrolSpawnDelay; - } else { -- this.nextTick--; -- patrolSpawnDelay = this.nextTick; -+ worldData.patrolSpawnerNextTick--; // Folia - region threading -+ patrolSpawnDelay = worldData.patrolSpawnerNextTick; // Folia - region threading - } - if (patrolSpawnDelay > 0) { - return 0; -@@ -68,7 +69,7 @@ public class PatrolSpawner implements CustomSpawner { - if (level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) { - player.patrolSpawnDelay += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); - } else { -- this.nextTick += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); -+ worldData.patrolSpawnerNextTick += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); // Folia - region threading - } - - if (days < level.paperConfig().entities.behavior.pillagerPatrols.start.day || !level.isDay()) { -diff --git a/net/minecraft/world/level/levelgen/PhantomSpawner.java b/net/minecraft/world/level/levelgen/PhantomSpawner.java -index 11d25e64349b27bf54dc1620e4cce444c79f581c..cef0474cf5f95bff717d49e58fe0a74ce6b7b345 100644 ---- a/net/minecraft/world/level/levelgen/PhantomSpawner.java -+++ b/net/minecraft/world/level/levelgen/PhantomSpawner.java -@@ -19,7 +19,7 @@ import net.minecraft.world.level.block.state.BlockState; - import net.minecraft.world.level.material.FluidState; - - public class PhantomSpawner implements CustomSpawner { -- private int nextTick; -+ //private int nextTick; // Folia - region threading - - @Override - public int tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { -@@ -34,21 +34,22 @@ public class PhantomSpawner implements CustomSpawner { - } - // Paper end - Ability to control player's insomnia and phantoms - RandomSource randomSource = level.random; -- this.nextTick--; -- if (this.nextTick > 0) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading -+ worldData.phantomSpawnerNextTick--; // Folia - region threading -+ if (worldData.phantomSpawnerNextTick > 0) { // Folia - region threading - return 0; - } else { - // Paper start - Ability to control player's insomnia and phantoms - int spawnAttemptMinSeconds = level.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds; - int spawnAttemptMaxSeconds = level.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds; -- this.nextTick += (spawnAttemptMinSeconds + randomSource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; -+ worldData.phantomSpawnerNextTick += (spawnAttemptMinSeconds + randomSource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; // Folia - region threading - // Paper end - Ability to control player's insomnia and phantoms - if (level.getSkyDarken() < 5 && level.dimensionType().hasSkyLight()) { - return 0; - } else { - int i = 0; - -- for (ServerPlayer serverPlayer : level.players()) { -+ for (ServerPlayer serverPlayer : level.getLocalPlayers()) { // Folia - region threading - if (!serverPlayer.isSpectator() && (!level.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !serverPlayer.isCreative())) { // Paper - Add phantom creative and insomniac controls - BlockPos blockPos = serverPlayer.blockPosition(); - if (!level.dimensionType().hasSkyLight() || blockPos.getY() >= level.getSeaLevel() && level.canSeeSky(blockPos)) { -diff --git a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java -index 1f7005b01b56929fb694b69b37143b8d8c7b2898..f96fc1391167dea48cac1caa464b9026657df89a 100644 ---- a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java -+++ b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java -@@ -47,7 +47,7 @@ public class EndPlatformFeature extends Feature { - - // CraftBukkit start - // SPIGOT-7746: Entity will only be null during world generation, which is async, so just generate without event -- if (entity != null) { -+ if (false) { // Folia - region threading - org.bukkit.World bworld = level.getLevel().getWorld(); - org.bukkit.event.world.PortalCreateEvent portalEvent = new org.bukkit.event.world.PortalCreateEvent((java.util.List) (java.util.List) blockList.getList(), bworld, entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM); - level.getLevel().getCraftServer().getPluginManager().callEvent(portalEvent); -diff --git a/net/minecraft/world/level/levelgen/structure/StructureStart.java b/net/minecraft/world/level/levelgen/structure/StructureStart.java -index 4dafa79dd4ec55a443ba3731a79e7cd6e8052f48..743b13693c8ef1d69751de42e9c6dadefe56395c 100644 ---- a/net/minecraft/world/level/levelgen/structure/StructureStart.java -+++ b/net/minecraft/world/level/levelgen/structure/StructureStart.java -@@ -26,7 +26,7 @@ public final class StructureStart { - private final Structure structure; - private final PiecesContainer pieceContainer; - private final ChunkPos chunkPos; -- private int references; -+ private final java.util.concurrent.atomic.AtomicInteger references; // Folia - region threading - @Nullable - private volatile BoundingBox cachedBoundingBox; - -@@ -39,7 +39,7 @@ public final class StructureStart { - public StructureStart(Structure structure, ChunkPos chunkPos, int references, PiecesContainer pieceContainer) { - this.structure = structure; - this.chunkPos = chunkPos; -- this.references = references; -+ this.references = new java.util.concurrent.atomic.AtomicInteger(references); // Folia - region threading - this.pieceContainer = pieceContainer; - } - -@@ -126,7 +126,7 @@ public final class StructureStart { - compoundTag.putString("id", context.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(this.structure).toString()); - compoundTag.putInt("ChunkX", chunkPos.x); - compoundTag.putInt("ChunkZ", chunkPos.z); -- compoundTag.putInt("references", this.references); -+ compoundTag.putInt("references", this.references.get()); // Folia - region threading - compoundTag.put("Children", this.pieceContainer.save(context)); - return compoundTag; - } else { -@@ -144,15 +144,29 @@ public final class StructureStart { - } - - public boolean canBeReferenced() { -- return this.references < this.getMaxReferences(); -+ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading - } - -+ // Folia start - region threading -+ public boolean tryReference() { -+ for (int curr = this.references.get();;) { -+ if (curr >= this.getMaxReferences()) { -+ return false; -+ } -+ -+ if (curr == (curr = this.references.compareAndExchange(curr, curr + 1))) { -+ return true; -+ } // else: try again -+ } -+ } -+ // Folia end - region threading -+ - public void addReference() { -- this.references++; -+ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading - } - - public int getReferences() { -- return this.references; -+ return this.references.get(); // Folia - region threading - } - - protected int getMaxReferences() { -diff --git a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java -index 028eae2f9a459b60e92f3344091083aa93b54485..e7ea9df8f404a6176435204a91edeefab8070c89 100644 ---- a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java -+++ b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java -@@ -47,6 +47,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { - } - - private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates updates) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((net.minecraft.server.level.ServerLevel)this.level, pos, "Adding block without owning region"); // Folia - region threading - boolean flag = this.count > 0; - boolean flag1 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; - this.count++; -diff --git a/net/minecraft/world/level/saveddata/SavedData.java b/net/minecraft/world/level/saveddata/SavedData.java -index b681a5ca1c4215d5afcc988c169e22a84996a88d..3879127f6c4a7977176bcea7ccc21561210addc6 100644 ---- a/net/minecraft/world/level/saveddata/SavedData.java -+++ b/net/minecraft/world/level/saveddata/SavedData.java -@@ -8,7 +8,7 @@ import net.minecraft.nbt.NbtUtils; - import net.minecraft.util.datafix.DataFixTypes; - - public abstract class SavedData { -- private boolean dirty; -+ private volatile boolean dirty; // Folia - make map data thread-safe - - public abstract CompoundTag save(CompoundTag tag, HolderLookup.Provider registries); - -@@ -26,9 +26,10 @@ public abstract class SavedData { - - public CompoundTag save(HolderLookup.Provider registries) { - CompoundTag compoundTag = new CompoundTag(); -+ this.setDirty(false); // Folia - make map data thread-safe - move before save, so that any changes after are not lost - compoundTag.put("data", this.save(new CompoundTag(), registries)); - NbtUtils.addCurrentDataVersion(compoundTag); -- this.setDirty(false); -+ // Folia - make map data thread-safe - move before save, so that any changes after are not lost - return compoundTag; - } - -diff --git a/net/minecraft/world/level/saveddata/maps/MapIndex.java b/net/minecraft/world/level/saveddata/maps/MapIndex.java -index ffe604f8397a002800e6ecc2f878d0f6f1c98703..7ee324c32efe1e63d310120e468a2f0d8ca262b4 100644 ---- a/net/minecraft/world/level/saveddata/maps/MapIndex.java -+++ b/net/minecraft/world/level/saveddata/maps/MapIndex.java -@@ -34,17 +34,21 @@ public class MapIndex extends SavedData { - - @Override - public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { -+ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe - for (Entry entry : this.usedAuxIds.object2IntEntrySet()) { - tag.putInt(entry.getKey(), entry.getIntValue()); - } -+ } // Folia - make map data thread-safe - - return tag; - } - - public MapId getFreeAuxValueForMap() { -+ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe - int i = this.usedAuxIds.getInt("map") + 1; - this.usedAuxIds.put("map", i); - this.setDirty(); - return new MapId(i); -+ } // Folia - make map data thread-safe - } - } -diff --git a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java -index f5a131e870a4f1ad06ebfb1f360720cf19656fb5..cf827e018eec4910b8319cee7202d480fee573f6 100644 ---- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java -+++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java -@@ -201,7 +201,7 @@ public class MapItemSavedData extends SavedData { - } - - @Override -- public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { -+ public synchronized CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { // Folia - make map data thread-safe - ResourceLocation.CODEC - .encodeStart(NbtOps.INSTANCE, this.dimension.location()) - .resultOrPartial(LOGGER::error) -@@ -244,7 +244,7 @@ public class MapItemSavedData extends SavedData { - return tag; - } - -- public MapItemSavedData locked() { -+ public synchronized MapItemSavedData locked() { // Folia - make map data thread-safe - MapItemSavedData mapItemSavedData = new MapItemSavedData( - this.centerX, this.centerZ, this.scale, this.trackingPosition, this.unlimitedTracking, true, this.dimension - ); -@@ -255,7 +255,7 @@ public class MapItemSavedData extends SavedData { - return mapItemSavedData; - } - -- public MapItemSavedData scaled() { -+ public synchronized MapItemSavedData scaled() { // Folia - make map data thread-safe - return createFresh(this.centerX, this.centerZ, (byte)Mth.clamp(this.scale + 1, 0, 4), this.trackingPosition, this.unlimitedTracking, this.dimension); - } - -@@ -264,7 +264,8 @@ public class MapItemSavedData extends SavedData { - return itemStack -> itemStack == stack || itemStack.is(stack.getItem()) && Objects.equals(mapId, itemStack.get(DataComponents.MAP_ID)); - } - -- public void tickCarriedBy(Player player, ItemStack mapStack) { -+ public synchronized void tickCarriedBy(Player player, ItemStack mapStack) { // Folia - make map data thread-safe -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(player, "Ticking map player in incorrect region"); // Folia - region threading - if (!this.carriedByPlayers.containsKey(player)) { - MapItemSavedData.HoldingPlayer holdingPlayer = new MapItemSavedData.HoldingPlayer(player); - this.carriedByPlayers.put(player, holdingPlayer); -@@ -413,7 +414,7 @@ public class MapItemSavedData extends SavedData { - - private byte calculateRotation(@Nullable LevelAccessor level, double yRot) { - if (this.dimension == Level.NETHER && level != null) { -- int i = (int)(level.getLevelData().getDayTime() / 10L); -+ int i = (int)(level.dayTime() / 10L); // Folia - region threading - return (byte)(i * i * 34187121 + i * 121 >> 15 & 15); - } else { - double d = yRot < 0.0 ? yRot - 8.0 : yRot + 8.0; -@@ -447,25 +448,27 @@ public class MapItemSavedData extends SavedData { - } - - @Nullable -- public Packet getUpdatePacket(MapId mapId, Player player) { -+ public synchronized Packet getUpdatePacket(MapId mapId, Player player) { // Folia - make map data thread-safe - MapItemSavedData.HoldingPlayer holdingPlayer = this.carriedByPlayers.get(player); - return holdingPlayer == null ? null : holdingPlayer.nextUpdatePacket(mapId); - } - -- public void setColorsDirty(int x, int z) { -- this.setDirty(); -+ public synchronized void setColorsDirty(int x, int z) { // Folia - make map data thread-safe -+ //this.setDirty(); // Folia - make dirty only after updating data - moved down - - for (MapItemSavedData.HoldingPlayer holdingPlayer : this.carriedBy) { - holdingPlayer.markColorsDirty(x, z); - } -+ this.setDirty(); // Folia - make dirty only after updating data - moved from above - } - -- public void setDecorationsDirty() { -- this.setDirty(); -+ public synchronized void setDecorationsDirty() { // Folia - make map data thread-safe -+ //this.setDirty(); // Folia - make dirty only after updating data - moved down - this.carriedBy.forEach(MapItemSavedData.HoldingPlayer::markDecorationsDirty); -+ this.setDirty(); // Folia - make dirty only after updating data - moved from above - } - -- public MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { -+ public synchronized MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { // Folia - make map data thread-safe - MapItemSavedData.HoldingPlayer holdingPlayer = this.carriedByPlayers.get(player); - if (holdingPlayer == null) { - holdingPlayer = new MapItemSavedData.HoldingPlayer(player); -@@ -476,7 +479,7 @@ public class MapItemSavedData extends SavedData { - return holdingPlayer; - } - -- public boolean toggleBanner(LevelAccessor accessor, BlockPos pos) { -+ public synchronized boolean toggleBanner(LevelAccessor accessor, BlockPos pos) { // Folia - make map data thread-safe - double d = pos.getX() + 0.5; - double d1 = pos.getZ() + 0.5; - int i = 1 << this.scale; -@@ -484,7 +487,7 @@ public class MapItemSavedData extends SavedData { - double d3 = (d1 - this.centerZ) / i; - int i1 = 63; - if (d2 >= -63.0 && d3 >= -63.0 && d2 <= 63.0 && d3 <= 63.0) { -- MapBanner mapBanner = MapBanner.fromWorld(accessor, pos); -+ MapBanner mapBanner = accessor.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(accessor.getMinecraftWorld(), pos) ? null : MapBanner.fromWorld(accessor, pos); // Folia - make map data thread-safe - don't sync load or read data we do not own - if (mapBanner == null) { - return false; - } -@@ -504,7 +507,7 @@ public class MapItemSavedData extends SavedData { - return false; - } - -- public void checkBanners(BlockGetter reader, int x, int z) { -+ public synchronized void checkBanners(BlockGetter reader, int x, int z) { // Folia - make map data thread-safe - Iterator iterator = this.bannerMarkers.values().iterator(); - - while (iterator.hasNext()) { -@@ -523,13 +526,13 @@ public class MapItemSavedData extends SavedData { - return this.bannerMarkers.values(); - } - -- public void removedFromFrame(BlockPos pos, int entityId) { -+ public synchronized void removedFromFrame(BlockPos pos, int entityId) { // Folia - make map data thread-safe - this.removeDecoration(getFrameKey(entityId)); - this.frameMarkers.remove(MapFrame.frameId(pos)); - this.setDirty(); - } - -- public boolean updateColor(int x, int z, byte color) { -+ public synchronized boolean updateColor(int x, int z, byte color) { // Folia - make map data thread-safe - byte b = this.colors[x + z * 128]; - if (b != color) { - this.setColor(x, z, color); -@@ -539,12 +542,12 @@ public class MapItemSavedData extends SavedData { - } - } - -- public void setColor(int x, int z, byte color) { -+ public synchronized void setColor(int x, int z, byte color) { // Folia - make map data thread-safe - this.colors[x + z * 128] = color; - this.setColorsDirty(x, z); - } - -- public boolean isExplorationMap() { -+ public synchronized boolean isExplorationMap() { // Folia - make map data thread-safe - for (MapDecoration mapDecoration : this.decorations.values()) { - if (mapDecoration.type().value().explorationMapElement()) { - return true; -@@ -554,7 +557,7 @@ public class MapItemSavedData extends SavedData { - return false; - } - -- public void addClientSideDecorations(List decorations) { -+ public synchronized void addClientSideDecorations(List decorations) { // Folia - make map data thread-safe - this.decorations.clear(); - this.trackedDecorationCount = 0; - -@@ -571,7 +574,7 @@ public class MapItemSavedData extends SavedData { - return this.decorations.values(); - } - -- public boolean isTrackedCountOverLimit(int trackedCount) { -+ public synchronized boolean isTrackedCountOverLimit(int trackedCount) { // Folia - make map data thread-safe - return this.trackedDecorationCount >= trackedCount; - } - -@@ -726,11 +729,13 @@ public class MapItemSavedData extends SavedData { - } - - public void applyToMap(MapItemSavedData savedData) { -+ synchronized (savedData) { // Folia - make map data thread-safe - for (int i = 0; i < this.width; i++) { - for (int i1 = 0; i1 < this.height; i1++) { - savedData.setColor(this.startX + i, this.startY + i1, this.mapColors[i + i1 * this.width]); - } - } -+ } // Folia - make map data thread-safe - } - } - } -diff --git a/net/minecraft/world/level/storage/DimensionDataStorage.java b/net/minecraft/world/level/storage/DimensionDataStorage.java -index d9a3b5a2e6495b7e22c114506c2bd1e406f58f8f..ab572ac18fd02306210c87eb9ba5e5d4197ff997 100644 ---- a/net/minecraft/world/level/storage/DimensionDataStorage.java -+++ b/net/minecraft/world/level/storage/DimensionDataStorage.java -@@ -51,6 +51,7 @@ public class DimensionDataStorage implements AutoCloseable { - } - - public T computeIfAbsent(SavedData.Factory factory, String name) { -+ synchronized (this.cache) { // Folia - make map data thread-safe - T savedData = this.get(factory, name); - if (savedData != null) { - return savedData; -@@ -59,10 +60,12 @@ public class DimensionDataStorage implements AutoCloseable { - this.set(name, savedData1); - return savedData1; - } -+ } // Folia - make map data thread-safe - } - - @Nullable - public T get(SavedData.Factory factory, String name) { -+ synchronized (this.cache) { // Folia - make map data thread-safe - Optional optional = this.cache.get(name); - if (optional == null) { - optional = Optional.ofNullable(this.readSavedData(factory.deserializer(), factory.type(), name)); -@@ -70,6 +73,7 @@ public class DimensionDataStorage implements AutoCloseable { - } - - return (T)optional.orElse(null); -+ } // Folia - make map data thread-safe - } - - @Nullable -@@ -88,8 +92,10 @@ public class DimensionDataStorage implements AutoCloseable { - } - - public void set(String name, SavedData savedData) { -+ synchronized (this.cache) { // Folia - make map data thread-safe - this.cache.put(name, Optional.of(savedData)); - savedData.setDirty(); -+ } // Folia - make map data thread-safe - } - - public CompoundTag readTagFromDisk(String filename, DataFixTypes dataFixType, int version) throws IOException { -diff --git a/net/minecraft/world/ticks/LevelChunkTicks.java b/net/minecraft/world/ticks/LevelChunkTicks.java -index faf45ac459f7c25309d6ef6dce371d484a0dae7b..8a98064f2e44b27947c1af9c80ae0d7a397db7e4 100644 ---- a/net/minecraft/world/ticks/LevelChunkTicks.java -+++ b/net/minecraft/world/ticks/LevelChunkTicks.java -@@ -48,6 +48,21 @@ public class LevelChunkTicks implements SerializableTickContainer, TickCon - this.dirty = false; - } - // Paper end - rewrite chunk system -+ // Folia start - region threading -+ public void offsetTicks(final long offset) { -+ if (offset == 0 || this.tickQueue.isEmpty()) { -+ return; -+ } -+ final ScheduledTick[] queue = this.tickQueue.toArray(new ScheduledTick[0]); -+ this.tickQueue.clear(); -+ for (final ScheduledTick entry : queue) { -+ final ScheduledTick newEntry = new ScheduledTick<>( -+ entry.type(), entry.pos(), entry.triggerTick() + offset, entry.subTickOrder() -+ ); -+ this.tickQueue.add(newEntry); -+ } -+ } -+ // Folia end - region threading - - public LevelChunkTicks() { - } -diff --git a/net/minecraft/world/ticks/LevelTicks.java b/net/minecraft/world/ticks/LevelTicks.java -index 66abc2e7adee60fa98eed1ba36e018814fd02cad..2caedf1c12e5a388f7b14989310a2137bc1117c3 100644 ---- a/net/minecraft/world/ticks/LevelTicks.java -+++ b/net/minecraft/world/ticks/LevelTicks.java -@@ -39,12 +39,69 @@ public class LevelTicks implements LevelTickAccess { - private final List> alreadyRunThisTick = new ArrayList<>(); - private final Set> toRunThisTickSet = new ObjectOpenCustomHashSet<>(ScheduledTick.UNIQUE_TICK_HASH); - private final BiConsumer, ScheduledTick> chunkScheduleUpdater = (levelChunkTicks, scheduledTick) -> { -- if (scheduledTick.equals(levelChunkTicks.peek())) { -- this.updateContainerScheduling(scheduledTick); -+ if (scheduledTick.equals(levelChunkTicks.peek())) { // Folia - diff on change -+ this.updateContainerScheduling(scheduledTick); // Folia - diff on change - } - }; - -- public LevelTicks(LongPredicate tickCheck) { -+ // Folia start - region threading -+ public final net.minecraft.server.level.ServerLevel world; -+ public final boolean isBlock; -+ -+ public void merge(final LevelTicks into, final long tickOffset) { -+ // note: containersToTick, toRunThisTick, alreadyRunThisTick, toRunThisTickSet -+ // are all transient state, only ever non-empty during tick. But merging regions occurs while there -+ // is no tick happening, so we assume they are empty. -+ for (final java.util.Iterator>> iterator = -+ ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry> entry = iterator.next(); -+ final LevelChunkTicks tickContainer = entry.getValue(); -+ tickContainer.offsetTicks(tickOffset); -+ into.allContainers.put(entry.getLongKey(), tickContainer); -+ } -+ for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2LongMap.Entry entry = iterator.next(); -+ into.nextTickForContainer.put(entry.getLongKey(), entry.getLongValue() + tickOffset); -+ } -+ } -+ -+ public void split(final int chunkToRegionShift, -+ final it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap> regionToData) { -+ for (final java.util.Iterator>> iterator = -+ ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2ObjectMap.Entry> entry = iterator.next(); -+ -+ final long chunkKey = entry.getLongKey(); -+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey); -+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey); -+ -+ final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey( -+ chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift -+ ); -+ // Should always be non-null, since containers are removed on unload. -+ regionToData.get(regionSectionKey).allContainers.put(chunkKey, entry.getValue()); -+ } -+ for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); -+ iterator.hasNext();) { -+ final Long2LongMap.Entry entry = iterator.next(); -+ final long chunkKey = entry.getLongKey(); -+ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey); -+ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey); -+ -+ final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey( -+ chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift -+ ); -+ -+ // Should always be non-null, since containers are removed on unload. -+ regionToData.get(regionSectionKey).nextTickForContainer.put(chunkKey, entry.getLongValue()); -+ } -+ } -+ // Folia end - region threading -+ -+ public LevelTicks(LongPredicate tickCheck, net.minecraft.server.level.ServerLevel world, boolean isBlock) { this.world = world; this.isBlock = isBlock; // Folia - add world and isBlock - this.tickCheck = tickCheck; - } - -@@ -56,7 +113,17 @@ public class LevelTicks implements LevelTickAccess { - this.nextTickForContainer.put(packedChunkPos, scheduledTick.triggerTick()); - } - -- chunkTicks.setOnTickAdded(this.chunkScheduleUpdater); -+ // Folia start - region threading -+ final boolean isBlock = this.isBlock; -+ final net.minecraft.server.level.ServerLevel world = this.world; -+ // make sure the lambda contains no reference to this LevelTicks -+ chunkTicks.setOnTickAdded((LevelChunkTicks levelChunkTicks, ScheduledTick tick) -> { -+ if (tick.equals(levelChunkTicks.peek())) { -+ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); -+ ((LevelTicks)(isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks())).updateContainerScheduling(tick); -+ } -+ }); -+ // Folia end - region threading - } - - public void removeContainer(ChunkPos chunkPos) { -@@ -70,6 +137,7 @@ public class LevelTicks implements LevelTickAccess { - - @Override - public void schedule(ScheduledTick tick) { -+ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, tick.pos(), "Cannot schedule tick for another region!"); // Folia - region threading - long packedChunkPos = ChunkPos.asLong(tick.pos()); - LevelChunkTicks levelChunkTicks = this.allContainers.get(packedChunkPos); - if (levelChunkTicks == null) { diff --git a/folia-server/minecraft-patches/features/0003-Add-chunk-system-throughput-counters-to-tps.patch b/folia-server/minecraft-patches/features/0002-Add-chunk-system-throughput-counters-to-tps.patch similarity index 100% rename from folia-server/minecraft-patches/features/0003-Add-chunk-system-throughput-counters-to-tps.patch rename to folia-server/minecraft-patches/features/0002-Add-chunk-system-throughput-counters-to-tps.patch diff --git a/folia-server/minecraft-patches/features/0005-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch b/folia-server/minecraft-patches/features/0003-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch similarity index 100% rename from folia-server/minecraft-patches/features/0005-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch rename to folia-server/minecraft-patches/features/0003-Prevent-block-updates-in-non-loaded-or-non-owned-chu.patch diff --git a/folia-server/minecraft-patches/features/0006-Block-reading-in-world-tile-entities-on-worldgen-thr.patch b/folia-server/minecraft-patches/features/0004-Block-reading-in-world-tile-entities-on-worldgen-thr.patch similarity index 100% rename from folia-server/minecraft-patches/features/0006-Block-reading-in-world-tile-entities-on-worldgen-thr.patch rename to folia-server/minecraft-patches/features/0004-Block-reading-in-world-tile-entities-on-worldgen-thr.patch diff --git a/folia-server/minecraft-patches/features/0004-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch b/folia-server/minecraft-patches/features/0004-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch deleted file mode 100644 index 88bb656..0000000 --- a/folia-server/minecraft-patches/features/0004-Make-CraftEntity-getHandle-and-overrides-perform-thr.patch +++ /dev/null @@ -1,45 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 19 Mar 2023 14:35:46 -0700 -Subject: [PATCH] Make CraftEntity#getHandle and overrides perform thread - checks - -While these checks are painful, it should assist in debugging -threading issues for plugins. - -diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index dbe049c164657ae352f4dadfa673a07dbef2054d..7d5368330870aca9d14ff43296d64c8db6a3e89b 100644 ---- a/net/minecraft/world/entity/Entity.java -+++ b/net/minecraft/world/entity/Entity.java -@@ -3050,6 +3050,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - if (force || this.canRide(vehicle) && vehicle.canAddPassenger(this)) { -+ if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen - // CraftBukkit start - if (vehicle.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && this.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) { - org.bukkit.event.vehicle.VehicleEnterEvent event = new org.bukkit.event.vehicle.VehicleEnterEvent((org.bukkit.entity.Vehicle) vehicle.getBukkitEntity(), this.getBukkitEntity()); -@@ -3071,6 +3072,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return false; - } - // CraftBukkit end -+ } // Folia - region threading - suppress entire event logic during worldgen - if (this.isPassenger()) { - this.stopRiding(); - } -@@ -3152,6 +3154,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - throw new IllegalStateException("Use x.stopRiding(y), not y.removePassenger(x)"); - } else { - // CraftBukkit start -+ if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen - org.bukkit.craftbukkit.entity.CraftEntity craft = (org.bukkit.craftbukkit.entity.CraftEntity) passenger.getBukkitEntity().getVehicle(); - Entity orig = craft == null ? null : craft.getHandle(); - if (this.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && passenger.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) { -@@ -3179,6 +3182,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return false; - } - // CraftBukkit end -+ } // Folia - region threading - suppress entire event logic during worldgen - if (this.passengers.size() == 1 && this.passengers.get(0) == passenger) { - this.passengers = ImmutableList.of(); - } else { diff --git a/folia-server/minecraft-patches/features/0010-Sync-vehicle-position-to-player-position-on-player-d.patch b/folia-server/minecraft-patches/features/0005-Sync-vehicle-position-to-player-position-on-player-d.patch similarity index 100% rename from folia-server/minecraft-patches/features/0010-Sync-vehicle-position-to-player-position-on-player-d.patch rename to folia-server/minecraft-patches/features/0005-Sync-vehicle-position-to-player-position-on-player-d.patch diff --git a/folia-server/minecraft-patches/features/0011-Region-profiler.patch b/folia-server/minecraft-patches/features/0006-Region-profiler.patch similarity index 99% rename from folia-server/minecraft-patches/features/0011-Region-profiler.patch rename to folia-server/minecraft-patches/features/0006-Region-profiler.patch index b7d40e1..3a366fe 100644 --- a/folia-server/minecraft-patches/features/0011-Region-profiler.patch +++ b/folia-server/minecraft-patches/features/0006-Region-profiler.patch @@ -1704,7 +1704,7 @@ index c340d537749c49d83f50f6cec84ac75e1ace2bbd..089e694753e3c1b61902255442b26a3a } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index c09099070117483054f438b2bb77ff48a81610f0..a4aec91811cd986333cf6a818f70956d59bb3240 100644 +index 49a385261deef774575dfd7a5b259d8ed31ed91a..0cc5607080f79f9e9b65606a3e16fd4961368b02 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -729,6 +729,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @@ -1903,7 +1903,7 @@ index 49201d6664656ebe34c84c1c84b5ea4878729062..d9cc1d7e56c37d5ce92544edc10e89db } } diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java -index 7ad7b3b964939f5e389d968aa812d74ba96c9681..a7ca4fef8c97f19425fc0d05f5b32d357ceda5db 100644 +index e16a824488d2b43c430f12b8416fdeb590e66d28..5ea7fdf1e337da4c207dd6a53ca942480dd31922 100644 --- a/net/minecraft/world/level/Level.java +++ b/net/minecraft/world/level/Level.java @@ -200,6 +200,9 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl diff --git a/folia-server/minecraft-patches/features/0012-Add-watchdog-thread.patch b/folia-server/minecraft-patches/features/0007-Add-watchdog-thread.patch similarity index 100% rename from folia-server/minecraft-patches/features/0012-Add-watchdog-thread.patch rename to folia-server/minecraft-patches/features/0007-Add-watchdog-thread.patch diff --git a/folia-server/minecraft-patches/features/0007-Skip-worldstate-access-when-waking-players-up-during.patch b/folia-server/minecraft-patches/features/0007-Skip-worldstate-access-when-waking-players-up-during.patch deleted file mode 100644 index c42eb06..0000000 --- a/folia-server/minecraft-patches/features/0007-Skip-worldstate-access-when-waking-players-up-during.patch +++ /dev/null @@ -1,39 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sun, 23 Apr 2023 07:38:50 -0700 -Subject: [PATCH] Skip worldstate access when waking players up during data - deserialization - -In general, worldstate read/write is unacceptable during -data deserialization and is racey even in Vanilla. But in Folia, -some accesses may throw and as such we need to fix this directly. - -diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java -index ab85c5acc63abc07a55ff7c5e207527bb18d50b2..df0c10880c5aea110a4eade57d257d3a46e8c180 100644 ---- a/net/minecraft/server/level/ServerPlayer.java -+++ b/net/minecraft/server/level/ServerPlayer.java -@@ -674,7 +674,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc - this.getBukkitEntity().readExtraData(compound); // CraftBukkit - - if (this.isSleeping()) { -- this.stopSleeping(); -+ this.stopSleepingRaw(); // Folia - do not modify or read worldstate during data deserialization - } - - // CraftBukkit start -diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java -index 8f9e64590400039566ee5c9628d82a0eb9e56be1..5ba06cf6b26baa5acae9d64111ee3f61533e7867 100644 ---- a/net/minecraft/world/entity/LivingEntity.java -+++ b/net/minecraft/world/entity/LivingEntity.java -@@ -4333,6 +4333,11 @@ public abstract class LivingEntity extends Entity implements Attackable { - this.setXRot(0.0F); - } - }); -+ // Folia start - separate out -+ this.stopSleepingRaw(); -+ } -+ public void stopSleepingRaw() { -+ // Folia end - separate out - Vec3 vec3 = this.position(); - this.setPose(Pose.STANDING); - this.setPos(vec3.x, vec3.y, vec3.z); diff --git a/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/PaperHooks.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/PaperHooks.java.patch new file mode 100644 index 0000000..05cc4f7 --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/PaperHooks.java.patch @@ -0,0 +1,20 @@ +--- a/ca/spottedleaf/moonrise/paper/PaperHooks.java ++++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java +@@ -105,7 +_,7 @@ + } + + 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 +_,7 @@ + continue; + } + final T casted = (T)entityTypeTest.tryCast(part); +- if (casted != null && (predicate == null || predicate.test(casted))) { ++ if (casted != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(part) && (predicate == null || predicate.test(casted))) { // Folia - region threading + into.add(casted); + if (into.size() >= maxCount) { + break; diff --git a/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java.patch new file mode 100644 index 0000000..1a77151 --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java.patch @@ -0,0 +1,87 @@ +--- a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java ++++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java +@@ -80,18 +_,23 @@ + + @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 +_,13 @@ + + @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 +_,7 @@ + + @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 +_,18 @@ + + @Override + public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getTickingChunks().remove( +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() +- ); ++ chunk.getLevel().getCurrentWorldData().removeTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)(ServerLevel)chunk.getLevel()).moonrise$removeChunkForPlayerTicking(chunk); // Moonrise - chunk tick iteration + } + + @Override + public void onChunkEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().add( +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() +- ); ++ chunk.getLevel().getCurrentWorldData().addEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading + } + + @Override + public void onChunkNotEntityTicking(final LevelChunk chunk, final ChunkHolder holder) { +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)((ServerLevel)chunk.getLevel())).moonrise$getEntityTickingChunks().remove( +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder() +- ); ++ chunk.getLevel().getCurrentWorldData().removeEntityTickingChunk(chunk.moonrise$getChunkAndHolder()); // Folia - region threading + } + + @Override diff --git a/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java.patch new file mode 100644 index 0000000..5057fce --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java.patch @@ -0,0 +1,32 @@ +--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/EntityLookup.java +@@ -460,6 +_,19 @@ + 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 +_,9 @@ + 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/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java.patch new file mode 100644 index 0000000..cf96005 --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java.patch @@ -0,0 +1,36 @@ +--- 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 +_,7 @@ + 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 +_,7 @@ + if (entity instanceof ServerPlayer player) { + ((ChunkSystemServerLevel)this.serverWorld).moonrise$getNearbyPlayers().addPlayer(player); + } ++ this.world.getCurrentWorldData().addEntity(entity); // Folia - region threading + } + + @Override +@@ -87,14 +_,14 @@ + @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/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java.patch new file mode 100644 index 0000000..39a405b --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java.patch @@ -0,0 +1,20 @@ +--- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -216,7 +_,7 @@ + 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 +_,7 @@ + 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/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java.patch new file mode 100644 index 0000000..758bc23 --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java.patch @@ -0,0 +1,42 @@ +--- a/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/queue/ChunkUnloadQueue.java +@@ -29,6 +_,39 @@ + + 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<>(); + diff --git a/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java.patch new file mode 100644 index 0000000..bb7ea3d --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java.patch @@ -0,0 +1,391 @@ +--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -56,6 +_,14 @@ + 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 +_,83 @@ + private final ConcurrentLong2ReferenceChainedHashTable chunkHolders = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(16384, 0.25f); + private final ServerLevel world; + private final ChunkTaskScheduler taskScheduler; +- private long currentTick; +- +- 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); +- }); ++ // Folia start - region threading ++ public static final class HolderManagerRegionData { ++ private final ArrayDeque pendingFullLoadUpdate = new ArrayDeque<>(); ++ private final ObjectRBTreeSet autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> { ++ if (c1 == c2) { ++ return 0; ++ } ++ ++ final int saveTickCompare = Long.compare(c1.lastAutoSave, c2.lastAutoSave); ++ ++ if (saveTickCompare != 0) { ++ return saveTickCompare; ++ } ++ ++ final long coord1 = CoordinateUtils.getChunkKey(c1.chunkX, c1.chunkZ); ++ final long coord2 = CoordinateUtils.getChunkKey(c2.chunkX, c2.chunkZ); ++ ++ if (coord1 == coord2) { ++ throw new IllegalStateException("Duplicate chunkholder in auto save queue"); ++ } ++ ++ return Long.compare(coord1, coord2); ++ }); ++ ++ 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); ++ } ++ } ++ ++ public void split(final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, ++ final ReferenceOpenHashSet dataSet) { ++ for (final NewChunkHolder fullLoadUpdate : this.pendingFullLoadUpdate) { ++ final int regionCoordinateX = fullLoadUpdate.chunkX >> chunkToRegionShift; ++ final int regionCoordinateZ = fullLoadUpdate.chunkZ >> chunkToRegionShift; ++ ++ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); ++ if (data != null) { ++ data.pendingFullLoadUpdate.add(fullLoadUpdate); ++ } // else: fullLoadUpdate is an unloaded chunk holder ++ } ++ ++ for (final NewChunkHolder autoSave : this.autoSaveQueue) { ++ final int regionCoordinateX = autoSave.chunkX >> chunkToRegionShift; ++ final int regionCoordinateZ = autoSave.chunkZ >> chunkToRegionShift; ++ ++ final HolderManagerRegionData data = regionToData.get(CoordinateUtils.getChunkKey(regionCoordinateX, regionCoordinateZ)); ++ if (data != null) { ++ data.autoSaveQueue.add(autoSave); ++ } // else: autoSave is an unloaded chunk holder ++ } ++ } ++ } ++ ++ private ChunkHolderManager.HolderManagerRegionData getCurrentRegionData() { ++ 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().getHolderManagerRegionData(); ++ } ++ // Folia end - region threading ++ + + public ChunkHolderManager(final ServerLevel world, final ChunkTaskScheduler taskScheduler) { + this.world = world; +@@ -185,8 +_,13 @@ + } + + 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 +_,10 @@ + } + + 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 +_,35 @@ + } + + 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 +_,38 @@ + + 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(); +- +- if (logProgress) { ++ // 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 (first && logProgress) { // Folia - region threading + LOGGER.info("Saving all chunkholders for world '" + WorldUtil.getWorldName(this.world) + "'"); + } + +@@ -292,6 +_,12 @@ + } + 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 +_,7 @@ + } + } + } +- if (flush) { ++ if (last && flush) { // Folia - region threading + MoonriseRegionFileIO.flush(this.world); + try { + MoonriseRegionFileIO.flushRegionStorages(this.world); +@@ -732,7 +_,13 @@ + } + + 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 +_,7 @@ + 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 +_,56 @@ + 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 +_,7 @@ + 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 +_,13 @@ + + // 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 +_,7 @@ + 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/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java.patch new file mode 100644 index 0000000..1df42d8 --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java.patch @@ -0,0 +1,75 @@ +--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkTaskScheduler.java +@@ -122,7 +_,7 @@ + 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 +_,13 @@ + }; + + // 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 +_,7 @@ + */ + @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 +_,7 @@ + + 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,8 +_,26 @@ + + 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(); diff --git a/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java.patch new file mode 100644 index 0000000..3f3c55f --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java.patch @@ -0,0 +1,33 @@ +--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/NewChunkHolder.java +@@ -1359,10 +_,10 @@ + 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 +_,7 @@ + } + + // 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 +_,7 @@ + } + + // 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/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java.patch b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java.patch new file mode 100644 index 0000000..bf6db93 --- /dev/null +++ b/folia-server/minecraft-patches/sources/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java.patch @@ -0,0 +1,11 @@ +--- a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java ++++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +@@ -1940,7 +_,7 @@ + + for (int currChunkZ = minChunkZ; currChunkZ <= maxChunkZ; ++currChunkZ) { + for (int currChunkX = minChunkX; currChunkX <= maxChunkX; ++currChunkX) { +- final ChunkAccess chunk = chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); ++ final ChunkAccess chunk = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)world, currChunkX, currChunkZ) ? null : chunkSource.getChunk(currChunkX, currChunkZ, ChunkStatus.FULL, loadChunks); // Folia - region threading + + if (chunk == null) { + if ((collisionFlags & COLLISION_FLAG_COLLIDE_WITH_UNLOADED_CHUNKS) != 0) { diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch new file mode 100644 index 0000000..1d743d9 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch @@ -0,0 +1,174 @@ +--- a/io/papermc/paper/entity/activation/ActivationRange.java ++++ b/io/papermc/paper/entity/activation/ActivationRange.java +@@ -48,33 +_,34 @@ + + 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 +_,11 @@ + 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 +_,37 @@ + 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 +_,14 @@ + * + * @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 +_,7 @@ + */ + 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 +_,10 @@ + 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 +_,16 @@ + 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/folia-server/minecraft-patches/sources/io/papermc/paper/redstone/RedstoneWireTurbo.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/redstone/RedstoneWireTurbo.java.patch new file mode 100644 index 0000000..5f6c962 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/redstone/RedstoneWireTurbo.java.patch @@ -0,0 +1,19 @@ +--- a/io/papermc/paper/redstone/RedstoneWireTurbo.java ++++ b/io/papermc/paper/redstone/RedstoneWireTurbo.java +@@ -829,14 +_,14 @@ + 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/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionShutdownThread.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionShutdownThread.java.patch new file mode 100644 index 0000000..d75e4e1 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionShutdownThread.java.patch @@ -0,0 +1,229 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/RegionShutdownThread.java +@@ -1,0 +_,226 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.moonrise.common.util.WorldUtil; ++import com.mojang.logging.LogUtils; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.ServerPlayer; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.item.ItemStack; ++import net.minecraft.world.level.ChunkPos; ++import org.bukkit.event.inventory.InventoryCloseEvent; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.concurrent.TimeUnit; ++ ++public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.util.TickThread { ++ ++ private static final Logger LOGGER = LogUtils.getClassLogger(); ++ ++ ThreadedRegionizer.ThreadedRegion shuttingDown; ++ ++ public RegionShutdownThread(final String name) { ++ super(name); ++ this.setUncaughtExceptionHandler((thread, thr) -> { ++ LOGGER.error("Error shutting down server", thr); ++ }); ++ } ++ ++ static ThreadedRegionizer.ThreadedRegion getRegion() { ++ final Thread currentThread = Thread.currentThread(); ++ if (currentThread instanceof RegionShutdownThread shutdownThread) { ++ return shutdownThread.shuttingDown; ++ } ++ return null; ++ } ++ ++ ++ static RegionizedWorldData getWorldData() { ++ final Thread currentThread = Thread.currentThread(); ++ if (currentThread instanceof RegionShutdownThread shutdownThread) { ++ // no fast path for shutting down ++ if (shutdownThread.shuttingDown != null) { ++ return shutdownThread.shuttingDown.getData().world.worldRegionData.get(); ++ } ++ } ++ return null; ++ } ++ ++ // The region shutdown thread bypasses all tick thread checks, which will allow us to execute global saves ++ // it will not however let us perform arbitrary sync loads, arbitrary world state lookups simply because ++ // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not. ++ // Thus, the only operation that the shutdown thread will perform ++ ++ private void saveLevelData(final ServerLevel world) { ++ try { ++ world.saveLevelData(true); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save level data for " + world.getWorld().getName(), thr); ++ } ++ } ++ ++ private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region, ++ final ServerLevel world) { ++ try { ++ this.shuttingDown = region; ++ final List pendingTeleports = world.removeAllRegionTeleports(); ++ if (pendingTeleports.isEmpty()) { ++ return; ++ } ++ final ChunkPos center = region.getCenterChunk(); ++ LOGGER.info("Completing " + pendingTeleports.size() + " pending teleports in region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); ++ for (final ServerLevel.PendingTeleport pendingTeleport : pendingTeleports) { ++ LOGGER.info("Completing teleportation to target position " + pendingTeleport.to()); ++ ++ // first, add entities to entity chunk so that they will be saved ++ for (final Entity.EntityTreeNode node : pendingTeleport.rootVehicle().getFullTree()) { ++ // assume that world and position are set to destination here ++ node.root.setLevel(world); // in case the pending teleport is from a portal before it finds the exact destination ++ world.moonrise$getEntityLookup().addEntityForShutdownTeleportComplete(node.root); ++ } ++ ++ // then, rebuild the passenger tree so that when saving only the root vehicle will be written - and if ++ // there are any player passengers, that the later player saving will save the tree ++ pendingTeleport.rootVehicle().restore(); ++ ++ // now we are finished ++ LOGGER.info("Completed teleportation to target position " + pendingTeleport.to()); ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to complete pending teleports", thr); ++ } finally { ++ this.shuttingDown = null; ++ } ++ } ++ ++ private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region, ++ final boolean last) { ++ ChunkPos center = null; ++ try { ++ this.shuttingDown = region; ++ center = region.getCenterChunk(); ++ LOGGER.info("Saving chunks around region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'"); ++ region.regioniser.world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(true, true, false, last, false); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to save chunks for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); ++ } finally { ++ this.shuttingDown = null; ++ } ++ } ++ ++ private void haltChunkSystem(final ServerLevel world) { ++ try { ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(false, true, true, false, false); ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to halt chunk system for world '" + world.getWorld().getName() + "'", thr); ++ } ++ } ++ ++ private void closePlayerInventories(final ThreadedRegionizer.ThreadedRegion region) { ++ ChunkPos center = null; ++ try { ++ this.shuttingDown = region; ++ center = region.getCenterChunk(); ++ ++ final RegionizedWorldData worldData = region.regioniser.world.worldRegionData.get(); ++ ++ for (final ServerPlayer player : worldData.getLocalPlayers()) { ++ try { ++ // close inventory ++ if (player.containerMenu != player.inventoryMenu) { ++ player.closeContainer(InventoryCloseEvent.Reason.DISCONNECT); ++ } ++ ++ // drop carried item ++ if (!player.containerMenu.getCarried().isEmpty()) { ++ ItemStack carried = player.containerMenu.getCarried(); ++ player.containerMenu.setCarried(ItemStack.EMPTY); ++ player.drop(carried, false); ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to close player inventory for player: " + player, thr); ++ } ++ } ++ } catch (final Throwable thr) { ++ LOGGER.error("Failed to close player inventories for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); ++ } finally { ++ this.shuttingDown = null; ++ } ++ } ++ ++ @Override ++ public final void run() { ++ // await scheduler termination ++ LOGGER.info("Awaiting scheduler termination for 60s..."); ++ if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) { ++ LOGGER.info("Scheduler halted"); ++ } else { ++ LOGGER.warn("Scheduler did not terminate within 60s, proceeding with shutdown anyways"); ++ TickRegions.getScheduler().dumpAliveThreadTraces("Did not shut down in time"); ++ } ++ ++ MinecraftServer.getServer().stopServer(); // stop part 1: most logic, kicking players, plugins, etc ++ // halt all chunk systems first so that any in-progress chunk generation stops ++ LOGGER.info("Halting chunk systems..."); ++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { ++ try { ++ world.moonrise$getChunkTaskScheduler().halt(false, 0L); ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to soft halt chunk system for world '" + world.getWorld().getName() + "'", throwable); ++ } ++ } ++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { ++ this.haltChunkSystem(world); ++ } ++ LOGGER.info("Halted chunk systems"); ++ ++ LOGGER.info("Finishing pending teleports..."); ++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { ++ final List> ++ regions = new ArrayList<>(); ++ world.regioniser.computeForAllRegionsUnsynchronised(regions::add); ++ ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ this.finishTeleportations(regions.get(i), world); ++ } ++ } ++ LOGGER.info("Finished pending teleports"); ++ ++ LOGGER.info("Saving all worlds"); ++ for (final ServerLevel world : MinecraftServer.getServer().getAllLevels()) { ++ LOGGER.info("Saving world data for world '" + WorldUtil.getWorldName(world) + "'"); ++ ++ final List> ++ regions = new ArrayList<>(); ++ world.regioniser.computeForAllRegionsUnsynchronised(regions::add); ++ ++ LOGGER.info("Closing player inventories..."); ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ this.closePlayerInventories(regions.get(i)); ++ } ++ LOGGER.info("Closed player inventories"); ++ ++ LOGGER.info("Saving chunks..."); ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ this.saveRegionChunks(regions.get(i), (i + 1) == len); ++ } ++ LOGGER.info("Saved chunks"); ++ ++ LOGGER.info("Saving level data..."); ++ this.saveLevelData(world); ++ LOGGER.info("Saved level data"); ++ ++ LOGGER.info("Saved world data for world '" + WorldUtil.getWorldName(world) + "'"); ++ } ++ LOGGER.info("Saved all worlds"); ++ ++ // Note: only save after world data and pending teleportations ++ LOGGER.info("Saving all player data..."); ++ MinecraftServer.getServer().getPlayerList().saveAll(); ++ LOGGER.info("Saved all player data"); ++ ++ MinecraftServer.getServer().stopPart2(); // stop part 2: close other resources (io thread, etc) ++ // done, part 2 should call exit() ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedData.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedData.java.patch new file mode 100644 index 0000000..bac587f --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedData.java.patch @@ -0,0 +1,238 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/RegionizedData.java +@@ -1,0 +_,235 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.util.Validate; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.level.ServerLevel; ++import javax.annotation.Nullable; ++import java.util.function.Supplier; ++ ++/** ++ * Use to manage data that needs to be regionised. ++ *

    ++ * Note: that unlike {@link ThreadLocal}, regionised data is not deleted once the {@code RegionizedData} object is GC'd. ++ * The data is held in reference to the world it resides in. ++ *

    ++ *

    ++ * Note: Keep in mind that when regionised ticking is disabled, the entire server is considered a single region. ++ * That is, the data may or may not cross worlds. As such, the {@code RegionizedData} object must be instanced ++ * per world when appropriate, as it is no longer guaranteed that separate worlds contain separate regions. ++ * See below for more details on instancing per world. ++ *

    ++ *

    ++ * Regionised data may be world-checked. That is, {@link #get()} may throw an exception if the current ++ * region's world does not match the {@code RegionizedData}'s world. Consider the usages of {@code RegionizedData} below ++ * see why the behavior may or may not be desirable: ++ *

    ++ *         {@code
    ++ *         public class EntityTickList {
    ++ *             private final List entities = new ArrayList<>();
    ++ *
    ++ *             public void addEntity(Entity e) {
    ++ *                 this.entities.add(e);
    ++ *             }
    ++ *
    ++ *             public void removeEntity(Entity e) {
    ++ *                 this.entities.remove(e);
    ++ *             }
    ++ *         }
    ++ *
    ++ *         public class World {
    ++ *
    ++ *             // callback is left out of this example
    ++ *             // note: world != null here
    ++ *             public final RegionizedData entityTickLists =
    ++ *                 new RegionizedData<>(this, () -> new EntityTickList(), ...);
    ++ *
    ++ *             public void addTickingEntity(Entity e) {
    ++ *                 // What we expect here is that this world is the
    ++ *                 // current ticking region's world.
    ++ *                 // If that is true, then calling this.entityTickLists.get()
    ++ *                 // will retrieve the current region's EntityTickList
    ++ *                 // for this world, which is fine since the current
    ++ *                 // region is contained within this world.
    ++ *
    ++ *                 // But if the current region's world is not this world,
    ++ *                 // and if the world check is disabled, then we will actually
    ++ *                 // retrieve _this_ world's EntityTickList for the region,
    ++ *                 // and NOT the EntityTickList for the region's world.
    ++ *                 // This is because the RegionizedData object is instantiated
    ++ *                 // per world.
    ++ *                 this.entityTickLists.get().addEntity(e);
    ++ *             }
    ++ *         }
    ++ *
    ++ *         public class TickTimes {
    ++ *
    ++ *             private final List tickTimesNS = new ArrayList<>();
    ++ *
    ++ *             public void completeTick(long timeNS) {
    ++ *                 this.tickTimesNS.add(timeNS);
    ++ *             }
    ++ *
    ++ *             public double getAverageTickLengthMS() {
    ++ *                 double sum = 0.0;
    ++ *                 for (long time : tickTimesNS) {
    ++ *                     sum += (double)time;
    ++ *                 }
    ++ *                 return (sum / this.tickTimesNS.size()) / 1.0E6; // 1ms = 1 million ns
    ++ *             }
    ++ *         }
    ++ *
    ++ *         public class Server {
    ++ *             public final List worlds = ...;
    ++ *
    ++ *             // callback is left out of this example
    ++ *             // note: world == null here, because this RegionizedData object
    ++ *             // is not instantiated per world, but rather globally.
    ++ *             public final RegionizedData tickTimes =
    ++ *                  new RegionizedData<>(null, () -> new TickTimes(), ...);
    ++ *         }
    ++ *         }
    ++ *     
    ++ * In general, it is advised that if a RegionizedData object is instantiated per world, that world checking ++ * is enabled for it by passing the world to the constructor. ++ *

    ++ */ ++public final class RegionizedData { ++ ++ private final ServerLevel world; ++ private final Supplier initialValueSupplier; ++ private final RegioniserCallback callback; ++ ++ /** ++ * Creates a regionised data holder. The provided initial value supplier may not be null, and it must ++ * never produce {@code null} values. ++ *

    ++ * Note that the supplier or regioniser callback may be used while the region lock is held, so any blocking ++ * operations may deadlock the entire server and as such the function should be completely non-blocking ++ * and must complete in a timely manner. ++ *

    ++ *

    ++ * If the provided world is {@code null}, then the world checks are disabled. The world should only ever ++ * be {@code null} if the data is specifically not specific to worlds. For example, using {@code null} ++ * for an entity tick list is invalid since the entities are tied to a world and region, ++ * however using {@code null} for tasks to run at the end of a tick is valid since the tasks are tied to ++ * region only. ++ *

    ++ * @param world The world in which the region data resides. ++ * @param supplier Initial value supplier used to lazy initialise region data. ++ * @param callback Region callback to manage this regionised data. ++ */ ++ public RegionizedData(final ServerLevel world, final Supplier supplier, final RegioniserCallback callback) { ++ this.world = world; ++ this.initialValueSupplier = Validate.notNull(supplier, "Supplier may not be null."); ++ this.callback = Validate.notNull(callback, "Regioniser callback may not be null."); ++ } ++ ++ T createNewValue() { ++ return Validate.notNull(this.initialValueSupplier.get(), "Initial value supplier may not return null"); ++ } ++ ++ RegioniserCallback getCallback() { ++ return this.callback; ++ } ++ ++ /** ++ * Returns the current data type for the current ticking region. If there is no region, returns {@code null}. ++ * @return the current data type for the current ticking region. If there is no region, returns {@code null}. ++ * @throws IllegalStateException If the following are true: The server is in region ticking mode, ++ * this {@code RegionizedData}'s world is not {@code null}, ++ * and the current ticking region's world does not match this {@code RegionizedData}'s world. ++ */ ++ public @Nullable T get() { ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ ++ if (region == null) { ++ return null; ++ } ++ ++ if (this.world != null && this.world != region.getData().world) { ++ throw new IllegalStateException("World check failed: expected world: " + this.world.getWorld().getKey() + ", region world: " + region.getData().world.getWorld().getKey()); ++ } ++ ++ return region.getData().getOrCreateRegionizedData(this); ++ } ++ ++ /** ++ * Class responsible for handling merge / split requests from the regioniser. ++ *

    ++ * It is critical to note that each function is called while holding the region lock. ++ *

    ++ */ ++ public static interface RegioniserCallback { ++ ++ /** ++ * Completely merges the data in {@code from} to {@code into}. ++ *

    ++ * Calculating Tick Offsets: ++ * Sometimes data stores absolute tick deadlines, and since regions tick independently, absolute deadlines ++ * are not comparable across regions. Consider absolute deadlines {@code deadlineFrom, deadlineTo} in ++ * regions {@code from} and {@code into} respectively. We can calculate the relative deadline for the from ++ * region with {@code relFrom = deadlineFrom - currentTickFrom}. Then, we can use the same equation for ++ * computing the absolute deadline in region {@code into} that has the same relative deadline as {@code from} ++ * as {@code deadlineTo = relFrom + currentTickTo}. By substituting {@code relFrom} as {@code deadlineFrom - currentTickFrom}, ++ * we finally have that {@code deadlineTo = deadlineFrom + (currentTickTo - currentTickFrom)} and ++ * that we can use an offset {@code fromTickOffset = currentTickTo - currentTickFrom} to calculate ++ * {@code deadlineTo} as {@code deadlineTo = deadlineFrom + fromTickOffset}. ++ *

    ++ *

    ++ * Critical Notes: ++ *

  • ++ *
      ++ * This function is called while the region lock is held, so any blocking operations may ++ * deadlock the entire server and as such the function should be completely non-blocking and must complete ++ * in a timely manner. ++ *
    ++ *
      ++ * This function may not throw any exceptions, or the server will be left in an unrecoverable state. ++ *
    ++ *
  • ++ *

    ++ * ++ * @param from The data to merge from. ++ * @param into The data to merge into. ++ * @param fromTickOffset The addend to absolute tick deadlines stored in the {@code from} region to adjust to the into region. ++ */ ++ public void merge(final T from, final T into, final long fromTickOffset); ++ ++ /** ++ * Splits the data in {@code from} into {@code dataSet}. ++ *

    ++ * The chunk coordinate to region section coordinate bit shift amount is provided in {@code chunkToRegionShift}. ++ * To convert from chunk coordinates to region coordinates and keys, see the code below: ++ *

    ++         *         {@code
    ++         *         int chunkX = ...;
    ++         *         int chunkZ = ...;
    ++         *
    ++         *         int regionSectionX = chunkX >> chunkToRegionShift;
    ++         *         int regionSectionZ = chunkZ >> chunkToRegionShift;
    ++         *         long regionSectionKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(regionSectionX, regionSectionZ);
    ++         *         }
    ++         *     
    ++ *

    ++ *

    ++ * The {@code regionToData} hashtable provides a lookup from {@code regionSectionKey} (see above) to the ++ * data that is owned by the region which occupies the region section. ++ *

    ++ *

    ++ * Unlike {@link #merge(Object, Object, long)}, there is no absolute tick offset provided. This is because ++ * the new regions formed from the split will start at the same tick number, and so no adjustment is required. ++ *

    ++ * ++ * @param from The data to split from. ++ * @param chunkToRegionShift The signed right-shift value used to convert chunk coordinates into region section coordinates. ++ * @param regionToData Lookup hash table from region section key to . ++ * @param dataSet The data set to split into. ++ */ ++ public void split( ++ final T from, final int chunkToRegionShift, ++ final Long2ReferenceOpenHashMap regionToData, final ReferenceOpenHashSet dataSet ++ ); ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedServer.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedServer.java.patch new file mode 100644 index 0000000..013c080 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedServer.java.patch @@ -0,0 +1,458 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/RegionizedServer.java +@@ -1,0 +_,455 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler; ++import net.minecraft.CrashReport; ++import net.minecraft.ReportedException; ++import net.minecraft.network.Connection; ++import net.minecraft.network.PacketListener; ++import net.minecraft.network.PacketSendListener; ++import net.minecraft.network.chat.Component; ++import net.minecraft.network.chat.MutableComponent; ++import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.dedicated.DedicatedServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.network.ServerGamePacketListenerImpl; ++import net.minecraft.world.level.GameRules; ++import org.bukkit.Bukkit; ++import org.slf4j.Logger; ++import java.util.ArrayList; ++import java.util.Collections; ++import java.util.List; ++import java.util.concurrent.CopyOnWriteArrayList; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.function.BooleanSupplier; ++ ++public final class RegionizedServer { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final RegionizedServer INSTANCE = new RegionizedServer(); ++ ++ public final RegionizedTaskQueue taskQueue = new RegionizedTaskQueue(); ++ ++ private final CopyOnWriteArrayList worlds = new CopyOnWriteArrayList<>(); ++ private final CopyOnWriteArrayList connections = new CopyOnWriteArrayList<>(); ++ ++ private final MultiThreadedQueue globalTickQueue = new MultiThreadedQueue<>(); ++ ++ private final GlobalTickTickHandle tickHandle = new GlobalTickTickHandle(this); ++ ++ public static RegionizedServer getInstance() { ++ return INSTANCE; ++ } ++ ++ public void addConnection(final Connection conn) { ++ this.connections.add(conn); ++ } ++ ++ public boolean removeConnection(final Connection conn) { ++ return this.connections.remove(conn); ++ } ++ ++ public void addWorld(final ServerLevel world) { ++ this.worlds.add(world); ++ } ++ ++ public void init() { ++ // call init event _before_ scheduling anything ++ new RegionizedServerInitEvent().callEvent(); ++ ++ // now we can schedule ++ this.tickHandle.setInitialStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); ++ TickRegions.getScheduler().scheduleRegion(this.tickHandle); ++ TickRegions.getScheduler().init(); ++ } ++ ++ public void invalidateStatus() { ++ this.lastServerStatus = 0L; ++ } ++ ++ public void addTaskWithoutNotify(final Runnable run) { ++ this.globalTickQueue.add(run); ++ } ++ ++ public void addTask(final Runnable run) { ++ this.addTaskWithoutNotify(run); ++ TickRegions.getScheduler().setHasTasks(this.tickHandle); ++ } ++ ++ /** ++ * Returns the current tick of the region ticking. ++ * @throws IllegalStateException If there is no current region. ++ */ ++ public static long getCurrentTick() throws IllegalStateException { ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ if (TickThread.isShutdownThread()) { ++ return 0L; ++ } ++ throw new IllegalStateException("No currently ticking region"); ++ } ++ return region.getData().getCurrentTick(); ++ } ++ ++ public static boolean isGlobalTickThread() { ++ return INSTANCE.tickHandle == TickRegionScheduler.getCurrentTickingTask(); ++ } ++ ++ public static void ensureGlobalTickThread(final String reason) { ++ if (!isGlobalTickThread()) { ++ throw new IllegalStateException(reason); ++ } ++ } ++ ++ public static TickRegionScheduler.RegionScheduleHandle getGlobalTickData() { ++ return INSTANCE.tickHandle; ++ } ++ ++ private static final class GlobalTickTickHandle extends TickRegionScheduler.RegionScheduleHandle { ++ ++ private final RegionizedServer server; ++ ++ private final AtomicBoolean scheduled = new AtomicBoolean(); ++ private final AtomicBoolean ticking = new AtomicBoolean(); ++ ++ public GlobalTickTickHandle(final RegionizedServer server) { ++ super(null, SchedulerThreadPool.DEADLINE_NOT_SET); ++ this.server = server; ++ } ++ ++ /** ++ * Only valid to call BEFORE scheduled!!!! ++ */ ++ final void setInitialStart(final long start) { ++ if (this.scheduled.getAndSet(true)) { ++ throw new IllegalStateException("Double scheduling global tick"); ++ } ++ this.updateScheduledStart(start); ++ } ++ ++ @Override ++ protected boolean tryMarkTicking() { ++ return !this.ticking.getAndSet(true); ++ } ++ ++ @Override ++ protected boolean markNotTicking() { ++ return this.ticking.getAndSet(false); ++ } ++ ++ @Override ++ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { ++ this.drainTasks(); ++ this.server.globalTick(tickCount); ++ } ++ ++ private void drainTasks() { ++ while (this.runOneTask()); ++ } ++ ++ private boolean runOneTask() { ++ final Runnable run = this.server.globalTickQueue.poll(); ++ if (run == null) { ++ return false; ++ } ++ ++ // TODO try catch? ++ run.run(); ++ ++ return true; ++ } ++ ++ @Override ++ protected boolean runRegionTasks(final BooleanSupplier canContinue) { ++ do { ++ if (!this.runOneTask()) { ++ return false; ++ } ++ } while (canContinue.getAsBoolean()); ++ ++ return true; ++ } ++ ++ @Override ++ protected boolean hasIntermediateTasks() { ++ return !this.server.globalTickQueue.isEmpty(); ++ } ++ } ++ ++ private long lastServerStatus; ++ private long tickCount; ++ ++ /* ++ private final java.util.Random random = new java.util.Random(4L); ++ private final List> walkers = ++ new java.util.ArrayList<>(); ++ static final int PLAYERS = 500; ++ static final int RAD_BLOCKS = 1000; ++ static final int RAD = RAD_BLOCKS >> 4; ++ static final int RAD_BIG_BLOCKS = 100_000; ++ static final int RAD_BIG = RAD_BIG_BLOCKS >> 4; ++ static final int VD = 4 + 12; ++ static final int BIG_PLAYERS = 250; ++ static final double WALK_CHANCE = 0.3; ++ static final double TP_CHANCE = 0.2; ++ static final double TASK_CHANCE = 0.2; ++ ++ private ServerLevel getWorld() { ++ return this.worlds.get(0); ++ } ++ ++ private void init2() { ++ for (int i = 0; i < PLAYERS; ++i) { ++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; ++ int posX = this.random.nextInt(-rad, rad + 1); ++ int posZ = this.random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = new io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap<>(null) { ++ @Override ++ protected void addCallback(Void parameter, int chunkX, int chunkZ) { ++ ServerLevel world = RegionizedServer.this.getWorld(); ++ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) { ++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> { ++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {}); ++ }); ++ } ++ world.chunkTaskScheduler.chunkHolderManager.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ) ++ ); ++ } ++ ++ @Override ++ protected void removeCallback(Void parameter, int chunkX, int chunkZ) { ++ ServerLevel world = RegionizedServer.this.getWorld(); ++ if (RegionizedServer.this.random.nextDouble() <= TASK_CHANCE) { ++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> { ++ RegionizedServer.this.taskQueue.queueChunkTask(world, chunkX, chunkZ, () -> {}); ++ }); ++ } ++ world.chunkTaskScheduler.chunkHolderManager.removeTicketAtLevel( ++ net.minecraft.server.level.TicketType.PLAYER, chunkX, chunkZ, io.papermc.paper.chunk.system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, new net.minecraft.world.level.ChunkPos(posX, posZ) ++ ); ++ } ++ }; ++ ++ map.add(posX, posZ, VD); ++ ++ walkers.add(map); ++ } ++ } ++ ++ private void randomWalk() { ++ if (this.walkers.isEmpty()) { ++ this.init2(); ++ return; ++ } ++ ++ for (int i = 0; i < PLAYERS; ++i) { ++ if (this.random.nextDouble() > WALK_CHANCE) { ++ continue; ++ } ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = this.walkers.get(i); ++ ++ int updateX = this.random.nextInt(-1, 2); ++ int updateZ = this.random.nextInt(-1, 2); ++ ++ map.update(map.lastChunkX + updateX, map.lastChunkZ + updateZ, VD); ++ } ++ ++ for (int i = 0; i < PLAYERS; ++i) { ++ if (random.nextDouble() >= TP_CHANCE) { ++ continue; ++ } ++ ++ int rad = i < BIG_PLAYERS ? RAD_BIG : RAD; ++ int posX = random.nextInt(-rad, rad + 1); ++ int posZ = random.nextInt(-rad, rad + 1); ++ ++ io.papermc.paper.chunk.system.RegionizedPlayerChunkLoader.SingleUserAreaMap map = walkers.get(i); ++ ++ map.update(posX, posZ, VD); ++ } ++ } ++ */ ++ ++ private void globalTick(final int tickCount) { ++ /* ++ if (false) { ++ io.papermc.paper.threadedregions.ThreadedTicketLevelPropagator.main(null); ++ } ++ this.randomWalk(); ++ */ ++ ++this.tickCount; ++ // expire invalid click command callbacks ++ io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue((int)this.tickCount); ++ ++ // scheduler ++ ((FoliaGlobalRegionScheduler)Bukkit.getGlobalRegionScheduler()).tick(); ++ ++ // commands ++ ((DedicatedServer)MinecraftServer.getServer()).handleConsoleInputs(); ++ ++ // needs ++ // player ping sample ++ // world global tick ++ // connection tick ++ ++ // tick player ping sample ++ this.tickPlayerSample(); ++ ++ // tick worlds ++ for (final ServerLevel world : this.worlds) { ++ this.globalTick(world, tickCount); ++ } ++ ++ // tick connections ++ this.tickConnections(); ++ ++ // player list ++ MinecraftServer.getServer().getPlayerList().tick(); ++ } ++ ++ private void tickPlayerSample() { ++ final MinecraftServer mcServer = MinecraftServer.getServer(); ++ ++ final long currtime = System.nanoTime(); ++ ++ // player ping sample ++ // copied from MinecraftServer#tickServer ++ // note: we need to reorder setPlayers to be the last operation it does, rather than the first to avoid publishing ++ // an uncomplete status ++ if (currtime - this.lastServerStatus >= MinecraftServer.STATUS_EXPIRE_TIME_NANOS) { ++ this.lastServerStatus = currtime; ++ mcServer.rebuildServerStatus(); ++ } ++ } ++ ++ public static boolean isNotOwnedByGlobalRegion(final Connection conn) { ++ final PacketListener packetListener = conn.getPacketListener(); ++ ++ if (packetListener instanceof ServerGamePacketListenerImpl gamePacketListener) { ++ return !gamePacketListener.waitingForSwitchToConfig; ++ } ++ ++ if (conn.getPacketListener() instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { ++ return configurationPacketListener.switchToMain; ++ } ++ ++ return false; ++ } ++ ++ private void tickConnections() { ++ final List connections = new ArrayList<>(this.connections); ++ Collections.shuffle(connections); // shuffle to prevent people from "gaming" the server by re-logging ++ for (final Connection conn : connections) { ++ if (!conn.becomeActive()) { ++ continue; ++ } ++ ++ if (isNotOwnedByGlobalRegion(conn)) { ++ // we actually require that the owning regions remove the connection for us, as it is possible ++ // that ownership is transferred back to us ++ continue; ++ } ++ ++ if (!conn.isConnected()) { ++ this.removeConnection(conn); ++ conn.handleDisconnection(); ++ continue; ++ } ++ ++ try { ++ conn.tick(); ++ } catch (final Exception exception) { ++ if (conn.isMemoryConnection()) { ++ throw new ReportedException(CrashReport.forThrowable(exception, "Ticking memory connection")); ++ } ++ ++ LOGGER.warn("Failed to handle packet for {}", conn.getLoggableAddress(MinecraftServer.getServer().logIPs()), exception); ++ MutableComponent ichatmutablecomponent = Component.literal("Internal server error"); ++ ++ conn.send(new ClientboundDisconnectPacket(ichatmutablecomponent), PacketSendListener.thenRun(() -> { ++ conn.disconnect(ichatmutablecomponent); ++ })); ++ conn.setReadOnly(); ++ continue; ++ } ++ } ++ } ++ ++ // A global tick only updates things like weather / worldborder, basically anything in the world that is ++ // NOT tied to a specific region, but rather shared amongst all of them. ++ private void globalTick(final ServerLevel world, final int tickCount) { ++ // needs ++ // worldborder tick ++ // advancing the weather cycle ++ // sleep status thing ++ // updating sky brightness ++ // time ticking (game time + daylight), plus PrimayLevelDat#getScheduledEvents ticking ++ ++ // Typically, we expect there to be a running region to drain a world's global chunk tasks. However, ++ // this may not be the case - and thus, only the global tick thread can do anything. ++ world.taskQueueRegionData.drainGlobalChunkTasks(); ++ ++ // worldborder tick ++ this.tickWorldBorder(world); ++ ++ // weather cycle ++ this.advanceWeatherCycle(world); ++ ++ // sleep status ++ this.checkNightSkip(world); ++ ++ // update raids ++ this.updateRaids(world); ++ ++ // sky brightness ++ this.updateSkyBrightness(world); ++ ++ // time ticking (TODO API synchronisation?) ++ this.tickTime(world, tickCount); ++ ++ world.updateTickData(); ++ ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates ++ } ++ ++ private void updateRaids(final ServerLevel world) { ++ world.getRaids().globalTick(); ++ } ++ ++ private void checkNightSkip(final ServerLevel world) { ++ world.tickSleep(); ++ } ++ ++ private void advanceWeatherCycle(final ServerLevel world) { ++ world.advanceWeatherCycle(); ++ } ++ ++ private void updateSkyBrightness(final ServerLevel world) { ++ world.updateSkyBrightness(); ++ } ++ ++ private void tickWorldBorder(final ServerLevel world) { ++ world.getWorldBorder().tick(); ++ } ++ ++ private void tickTime(final ServerLevel world, final int tickCount) { ++ if (world.tickTime) { ++ if (world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ world.setDayTime(world.levelData.getDayTime() + (long)tickCount); ++ } ++ world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + (long)tickCount); ++ } ++ } ++ ++ public static final record WorldLevelData(ServerLevel world, long nonRedstoneGameTime, long dayTime) { ++ ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedTaskQueue.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedTaskQueue.java.patch new file mode 100644 index 0000000..169eda8 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedTaskQueue.java.patch @@ -0,0 +1,810 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java +@@ -1,0 +_,807 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; ++import ca.spottedleaf.concurrentutil.executor.PrioritisedExecutor; ++import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Priority; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.Unit; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayDeque; ++import java.util.Iterator; ++import java.util.concurrent.atomic.AtomicLong; ++ ++public final class RegionizedTaskQueue { ++ ++ private static final TicketType TASK_QUEUE_TICKET = TicketType.create("task_queue_ticket", (a, b) -> 0); ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.createChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, true, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.createTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask createTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ return new PrioritisedQueue.ChunkBasedPriorityTask(world.taskQueueRegionData, chunkX, chunkZ, false, run, priority); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.queueChunkTask(world, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueChunkTask(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createChunkTask(world, chunkX, chunkZ, run, priority); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run) { ++ return this.queueTickTaskQueue(world, chunkX, chunkZ, run, Priority.NORMAL); ++ } ++ ++ public PrioritisedExecutor.PrioritisedTask queueTickTaskQueue(final ServerLevel world, final int chunkX, final int chunkZ, ++ final Runnable run, final Priority priority) { ++ final PrioritisedExecutor.PrioritisedTask ret = this.createTickTaskQueue(world, chunkX, chunkZ, run, priority); ++ ++ ret.queue(); ++ ++ return ret; ++ } ++ ++ public static final class WorldRegionTaskData { ++ private final ServerLevel world; ++ private final MultiThreadedQueue globalChunkTask = new MultiThreadedQueue<>(); ++ private final ConcurrentLong2ReferenceChainedHashTable referenceCounters = new ConcurrentLong2ReferenceChainedHashTable<>(); ++ ++ public WorldRegionTaskData(final ServerLevel world) { ++ this.world = world; ++ } ++ ++ private boolean executeGlobalChunkTask() { ++ final Runnable run = this.globalChunkTask.poll(); ++ if (run != null) { ++ run.run(); ++ return true; ++ } ++ return false; ++ } ++ ++ public void drainGlobalChunkTasks() { ++ while (this.executeGlobalChunkTask()); ++ } ++ ++ public void pushGlobalChunkTask(final Runnable run) { ++ this.globalChunkTask.add(run); ++ } ++ ++ private PrioritisedQueue getQueue(final boolean synchronise, final int chunkX, final int chunkZ, final boolean isChunkTask) { ++ final ThreadedRegionizer regioniser = this.world.regioniser; ++ final ThreadedRegionizer.ThreadedRegion region ++ = synchronise ? regioniser.getRegionAtSynchronised(chunkX, chunkZ) : regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); ++ if (region == null) { ++ return null; ++ } ++ final RegionTaskQueueData taskQueueData = region.getData().getTaskQueueData(); ++ return (isChunkTask ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue); ++ } ++ ++ private void removeTicket(final long coord) { ++ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( ++ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void addTicket(final long coord) { ++ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( ++ TASK_QUEUE_TICKET, coord, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void processTicketUpdates(final long coord) { ++ this.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(CoordinateUtils.getChunkX(coord), CoordinateUtils.getChunkZ(coord)); ++ } ++ ++ // note: only call on acquired referenceCountData ++ private void ensureTicketAdded(final long coord, final ReferenceCountData referenceCountData) { ++ if (!referenceCountData.addedTicket) { ++ // fine if multiple threads do this, no removeTicket may be called for this coord due to reference count inc ++ this.addTicket(coord); ++ this.processTicketUpdates(coord); ++ referenceCountData.addedTicket = true; ++ } ++ } ++ ++ private void decrementReference(final ReferenceCountData referenceCountData, final long coord) { ++ if (!referenceCountData.decreaseReferenceCount()) { ++ return; ++ } // else: need to remove ticket ++ ++ // note: it is possible that another thread increments and then removes the reference before we can, so ++ // use ifPresent ++ this.referenceCounters.computeIfPresent(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> { ++ if (valueInMap.referenceCount.get() != 0L) { ++ return valueInMap; ++ } ++ ++ // note: valueInMap may not be referenceCountData ++ ++ // possible to invoke this outside of the compute call, but not required and requires additional logic ++ WorldRegionTaskData.this.removeTicket(keyInMap); ++ ++ return null; ++ }); ++ } ++ ++ private ReferenceCountData incrementReference(final long coord) { ++ ReferenceCountData referenceCountData = this.referenceCounters.get(coord); ++ ++ if (referenceCountData != null && referenceCountData.addCount()) { ++ this.ensureTicketAdded(coord, referenceCountData); ++ return referenceCountData; ++ } ++ ++ referenceCountData = this.referenceCounters.compute(coord, (final long keyInMap, final ReferenceCountData valueInMap) -> { ++ if (valueInMap == null) { ++ // sets reference count to 1 ++ return new ReferenceCountData(); ++ } ++ // OK if we add from 0, the remove call will use compute() and catch this race condition ++ valueInMap.referenceCount.getAndIncrement(); ++ ++ return valueInMap; ++ }); ++ ++ this.ensureTicketAdded(coord, referenceCountData); ++ ++ return referenceCountData; ++ } ++ } ++ ++ private static final class ReferenceCountData { ++ ++ public final AtomicLong referenceCount = new AtomicLong(1L); ++ public volatile boolean addedTicket; ++ ++ // returns false if reference count is 0, otherwise increments ref count ++ public boolean addCount() { ++ int failures = 0; ++ for (long curr = this.referenceCount.get();;) { ++ for (int i = 0; i < failures; ++i) { ++ Thread.onSpinWait(); ++ } ++ ++ if (curr == 0L) { ++ return false; ++ } ++ ++ if (curr == (curr = this.referenceCount.compareAndExchange(curr, curr + 1L))) { ++ return true; ++ } ++ ++ ++failures; ++ } ++ } ++ ++ // returns true if new reference count is 0 ++ public boolean decreaseReferenceCount() { ++ final long res = this.referenceCount.decrementAndGet(); ++ if (res >= 0L) { ++ return res == 0L; ++ } else { ++ throw new IllegalStateException("Negative reference count"); ++ } ++ } ++ } ++ ++ public static final class RegionTaskQueueData { ++ private final PrioritisedQueue tickTaskQueue = new PrioritisedQueue(); ++ private final PrioritisedQueue chunkQueue = new PrioritisedQueue(); ++ private final WorldRegionTaskData worldRegionTaskData; ++ ++ public RegionTaskQueueData(final WorldRegionTaskData worldRegionTaskData) { ++ this.worldRegionTaskData = worldRegionTaskData; ++ } ++ ++ void mergeInto(final RegionTaskQueueData into) { ++ this.tickTaskQueue.mergeInto(into.tickTaskQueue); ++ this.chunkQueue.mergeInto(into.chunkQueue); ++ } ++ ++ public boolean executeTickTask() { ++ return this.tickTaskQueue.executeTask(); ++ } ++ ++ public boolean executeChunkTask() { ++ return this.worldRegionTaskData.executeGlobalChunkTask() || this.chunkQueue.executeTask(); ++ } ++ ++ void split(final ThreadedRegionizer regioniser, ++ final Long2ReferenceOpenHashMap> into) { ++ this.tickTaskQueue.split( ++ false, regioniser, into ++ ); ++ this.chunkQueue.split( ++ true, regioniser, into ++ ); ++ } ++ ++ public void drainTasks() { ++ final PrioritisedQueue tickTaskQueue = this.tickTaskQueue; ++ final PrioritisedQueue chunkTaskQueue = this.chunkQueue; ++ ++ int allowedTickTasks = tickTaskQueue.getScheduledTasks(); ++ int allowedChunkTasks = chunkTaskQueue.getScheduledTasks(); ++ ++ boolean executeTickTasks = allowedTickTasks > 0; ++ boolean executeChunkTasks = allowedChunkTasks > 0; ++ boolean executeGlobalTasks = true; ++ ++ do { ++ executeTickTasks = executeTickTasks && allowedTickTasks-- > 0 && tickTaskQueue.executeTask(); ++ executeChunkTasks = executeChunkTasks && allowedChunkTasks-- > 0 && chunkTaskQueue.executeTask(); ++ executeGlobalTasks = executeGlobalTasks && this.worldRegionTaskData.executeGlobalChunkTask(); ++ } while (executeTickTasks | executeChunkTasks | executeGlobalTasks); ++ ++ if (allowedChunkTasks > 0) { ++ // if we executed chunk tasks, we should try to process ticket updates for full status changes ++ this.worldRegionTaskData.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); ++ } ++ } ++ ++ public boolean hasTasks() { ++ return !this.tickTaskQueue.isEmpty() || !this.chunkQueue.isEmpty(); ++ } ++ } ++ ++ static final class PrioritisedQueue { ++ private final ArrayDeque[] queues = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; { ++ for (int i = 0; i < Priority.TOTAL_SCHEDULABLE_PRIORITIES; ++i) { ++ this.queues[i] = new ArrayDeque<>(); ++ } ++ } ++ private boolean isDestroyed; ++ ++ public int getScheduledTasks() { ++ synchronized (this) { ++ int ret = 0; ++ ++ for (final ArrayDeque queue : this.queues) { ++ ret += queue.size(); ++ } ++ ++ return ret; ++ } ++ } ++ ++ public boolean isEmpty() { ++ final ArrayDeque[] queues = this.queues; ++ final int max = Priority.IDLE.priority; ++ synchronized (this) { ++ for (int i = 0; i <= max; ++i) { ++ if (!queues[i].isEmpty()) { ++ return false; ++ } ++ } ++ return true; ++ } ++ } ++ ++ public void mergeInto(final PrioritisedQueue target) { ++ synchronized (this) { ++ this.isDestroyed = true; ++ mergeInto(target, this.queues); ++ } ++ } ++ ++ private static void mergeInto(final PrioritisedQueue target, final ArrayDeque[] thisQueues) { ++ synchronized (target) { ++ final ArrayDeque[] otherQueues = target.queues; ++ for (int i = 0; i < thisQueues.length; ++i) { ++ final ArrayDeque fromQ = thisQueues[i]; ++ final ArrayDeque intoQ = otherQueues[i]; ++ ++ // it is possible for another thread to queue tasks into the target queue before we do ++ // since only the ticking region can poll, we don't have to worry about it when they are being queued - ++ // but when we are merging, we need to ensure order is maintained (notwithstanding priority changes) ++ // we can ensure order is maintained by adding all of the tasks from the fromQ into the intoQ at the ++ // front of the queue, but we need to use descending iterator to ensure we do not reverse ++ // the order of elements from fromQ ++ for (final Iterator iterator = fromQ.descendingIterator(); iterator.hasNext();) { ++ intoQ.addFirst(iterator.next()); ++ } ++ } ++ } ++ } ++ ++ // into is a map of section coordinate to region ++ public void split(final boolean isChunkData, ++ final ThreadedRegionizer regioniser, ++ final Long2ReferenceOpenHashMap> into) { ++ final Reference2ReferenceOpenHashMap, ArrayDeque[]> ++ split = new Reference2ReferenceOpenHashMap<>(); ++ final int shift = regioniser.sectionChunkShift; ++ synchronized (this) { ++ this.isDestroyed = true; ++ // like mergeTarget, we need to be careful about insertion order so we can maintain order when splitting ++ ++ // first, build the targets ++ final ArrayDeque[] thisQueues = this.queues; ++ for (int i = 0; i < thisQueues.length; ++i) { ++ final ArrayDeque fromQ = thisQueues[i]; ++ ++ for (final ChunkBasedPriorityTask task : fromQ) { ++ final int sectionX = task.chunkX >> shift; ++ final int sectionZ = task.chunkZ >> shift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ final ThreadedRegionizer.ThreadedRegion ++ region = into.get(sectionKey); ++ if (region == null) { ++ throw new IllegalStateException(); ++ } ++ ++ split.computeIfAbsent(region, (keyInMap) -> { ++ final ArrayDeque[] ret = new ArrayDeque[Priority.TOTAL_SCHEDULABLE_PRIORITIES]; ++ ++ for (int k = 0; k < ret.length; ++k) { ++ ret[k] = new ArrayDeque<>(); ++ } ++ ++ return ret; ++ })[i].add(task); ++ } ++ } ++ ++ // merge the targets into their queues ++ for (final Iterator, ArrayDeque[]>> ++ iterator = split.reference2ReferenceEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Reference2ReferenceMap.Entry, ArrayDeque[]> ++ entry = iterator.next(); ++ final RegionTaskQueueData taskQueueData = entry.getKey().getData().getTaskQueueData(); ++ mergeInto(isChunkData ? taskQueueData.chunkQueue : taskQueueData.tickTaskQueue, entry.getValue()); ++ } ++ } ++ } ++ ++ /** ++ * returns null if the task cannot be scheduled, returns false if this task queue is dead, and returns true ++ * if the task was added ++ */ ++ private Boolean tryPush(final ChunkBasedPriorityTask task) { ++ final ArrayDeque[] queues = this.queues; ++ synchronized (this) { ++ final Priority priority = task.getPriority(); ++ if (priority == Priority.COMPLETING) { ++ return null; ++ } ++ if (this.isDestroyed) { ++ return Boolean.FALSE; ++ } ++ queues[priority.priority].addLast(task); ++ return Boolean.TRUE; ++ } ++ } ++ ++ private boolean executeTask() { ++ final ArrayDeque[] queues = this.queues; ++ final int max = Priority.IDLE.priority; ++ ChunkBasedPriorityTask task = null; ++ ReferenceCountData referenceCounter = null; ++ synchronized (this) { ++ if (this.isDestroyed) { ++ throw new IllegalStateException("Attempting to poll from dead queue"); ++ } ++ ++ search_loop: ++ for (int i = 0; i <= max; ++i) { ++ final ArrayDeque queue = queues[i]; ++ while ((task = queue.pollFirst()) != null) { ++ if ((referenceCounter = task.trySetCompleting(i)) != null) { ++ break search_loop; ++ } ++ } ++ } ++ } ++ ++ if (task == null) { ++ return false; ++ } ++ ++ try { ++ task.executeInternal(); ++ } finally { ++ task.world.decrementReference(referenceCounter, task.sectionLowerLeftCoord); ++ } ++ ++ return true; ++ } ++ ++ private static final class ChunkBasedPriorityTask implements PrioritisedExecutor.PrioritisedTask { ++ ++ private static final ReferenceCountData REFERENCE_COUNTER_NOT_SET = new ReferenceCountData(); ++ static { ++ REFERENCE_COUNTER_NOT_SET.referenceCount.set((long)Integer.MIN_VALUE); ++ } ++ ++ private final WorldRegionTaskData world; ++ private final int chunkX; ++ private final int chunkZ; ++ private final long sectionLowerLeftCoord; // chunk coordinate ++ private final boolean isChunkTask; ++ ++ private volatile ReferenceCountData referenceCounter; ++ private static final VarHandle REFERENCE_COUNTER_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "referenceCounter", ReferenceCountData.class); ++ private Runnable run; ++ private volatile Priority priority; ++ private static final VarHandle PRIORITY_HANDLE = ConcurrentUtil.getVarHandle(ChunkBasedPriorityTask.class, "priority", Priority.class); ++ ++ ChunkBasedPriorityTask(final WorldRegionTaskData world, final int chunkX, final int chunkZ, final boolean isChunkTask, ++ final Runnable run, final Priority priority) { ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.isChunkTask = isChunkTask; ++ this.run = run; ++ this.setReferenceCounterPlain(REFERENCE_COUNTER_NOT_SET); ++ this.setPriorityPlain(priority); ++ ++ final int regionShift = world.world.regioniser.sectionChunkShift; ++ final int regionMask = (1 << regionShift) - 1; ++ ++ this.sectionLowerLeftCoord = CoordinateUtils.getChunkKey(chunkX & ~regionMask, chunkZ & ~regionMask); ++ } ++ ++ private Priority getPriorityVolatile() { ++ return (Priority)PRIORITY_HANDLE.getVolatile(this); ++ } ++ ++ private void setPriorityPlain(final Priority priority) { ++ PRIORITY_HANDLE.set(this, priority); ++ } ++ ++ private void setPriorityVolatile(final Priority priority) { ++ PRIORITY_HANDLE.setVolatile(this, priority); ++ } ++ ++ private Priority compareAndExchangePriority(final Priority expect, final Priority update) { ++ return (Priority)PRIORITY_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private void setReferenceCounterPlain(final ReferenceCountData value) { ++ REFERENCE_COUNTER_HANDLE.set(this, value); ++ } ++ ++ private ReferenceCountData getReferenceCounterVolatile() { ++ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.get(this); ++ } ++ ++ private ReferenceCountData compareAndExchangeReferenceCounter(final ReferenceCountData expect, final ReferenceCountData update) { ++ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private void executeInternal() { ++ try { ++ this.run.run(); ++ } finally { ++ this.run = null; ++ } ++ } ++ ++ private void cancelInternal() { ++ this.run = null; ++ } ++ ++ private boolean tryComplete(final boolean cancel) { ++ int failures = 0; ++ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) { ++ if (curr == null) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { ++ ++failures; ++ continue; ++ } ++ ++ // we have the reference count, we win no matter what. ++ this.setPriorityVolatile(Priority.COMPLETING); ++ ++ try { ++ if (cancel) { ++ this.cancelInternal(); ++ } else { ++ this.executeInternal(); ++ } ++ } finally { ++ if (curr != REFERENCE_COUNTER_NOT_SET) { ++ this.world.decrementReference(curr, this.sectionLowerLeftCoord); ++ } ++ } ++ ++ return true; ++ } ++ } ++ ++ @Override ++ public PrioritisedExecutor getExecutor() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean isQueued() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean queue() { ++ if (this.getReferenceCounterVolatile() != REFERENCE_COUNTER_NOT_SET) { ++ return false; ++ } ++ ++ final ReferenceCountData referenceCounter = this.world.incrementReference(this.sectionLowerLeftCoord); ++ if (this.compareAndExchangeReferenceCounter(REFERENCE_COUNTER_NOT_SET, referenceCounter) != REFERENCE_COUNTER_NOT_SET) { ++ // we don't expect race conditions here, so it is OK if we have to needlessly reference count ++ this.world.decrementReference(referenceCounter, this.sectionLowerLeftCoord); ++ return false; ++ } ++ ++ boolean synchronise = false; ++ for (;;) { ++ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve ++ // the same queue again, as the region lock will be given to us only when the merge/split operation ++ // is done ++ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); ++ ++ if (queue == null) { ++ if (!synchronise) { ++ // may be incorrectly null when unsynchronised ++ synchronise = true; ++ continue; ++ } ++ // may have been cancelled before we got to the queue ++ if (this.getReferenceCounterVolatile() != null) { ++ throw new IllegalStateException("Expected null ref count when queue does not exist"); ++ } ++ // the task never could be polled from the queue, so we return false ++ // don't decrement reference count, as we were certainly cancelled by another thread, which ++ // will decrement the reference count ++ return false; ++ } ++ ++ synchronise = true; ++ ++ final Boolean res = queue.tryPush(this); ++ if (res == null) { ++ // we were cancelled ++ // don't decrement reference count, as we were certainly cancelled by another thread, which ++ // will decrement the reference count ++ return false; ++ } ++ ++ if (!res.booleanValue()) { ++ // failed, try again ++ continue; ++ } ++ ++ // successfully queued ++ return true; ++ } ++ } ++ ++ private ReferenceCountData trySetCompleting(final int minPriority) { ++ // first, try to set priority to EXECUTING ++ for (Priority curr = this.getPriorityVolatile();;) { ++ if (curr.isLowerPriority(minPriority)) { ++ return null; ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, Priority.COMPLETING))) { ++ break; ++ } // else: continue ++ } ++ ++ for (ReferenceCountData curr = this.getReferenceCounterVolatile();;) { ++ if (curr == null) { ++ // something acquired before us ++ return null; ++ } ++ ++ if (curr == REFERENCE_COUNTER_NOT_SET) { ++ throw new IllegalStateException(); ++ } ++ ++ if (curr != (curr = this.compareAndExchangeReferenceCounter(curr, null))) { ++ continue; ++ } ++ ++ return curr; ++ } ++ } ++ ++ private void updatePriorityInQueue() { ++ boolean synchronise = false; ++ for (;;) { ++ final ReferenceCountData referenceCount = this.getReferenceCounterVolatile(); ++ if (referenceCount == REFERENCE_COUNTER_NOT_SET || referenceCount == null) { ++ // cancelled or not queued ++ return; ++ } ++ ++ if (this.getPriorityVolatile() == Priority.COMPLETING) { ++ // cancelled ++ return; ++ } ++ ++ // we need to synchronise for repeated operations so that we guarantee that we do not retrieve ++ // the same queue again, as the region lock will be given to us only when the merge/split operation ++ // is done ++ final PrioritisedQueue queue = this.world.getQueue(synchronise, this.chunkX, this.chunkZ, this.isChunkTask); ++ ++ if (queue == null) { ++ if (!synchronise) { ++ // may be incorrectly null when unsynchronised ++ synchronise = true; ++ continue; ++ } ++ // must have been removed ++ return; ++ } ++ ++ synchronise = true; ++ ++ final Boolean res = queue.tryPush(this); ++ if (res == null) { ++ // we were cancelled ++ return; ++ } ++ ++ if (!res.booleanValue()) { ++ // failed, try again ++ continue; ++ } ++ ++ // successfully queued ++ return; ++ } ++ } ++ ++ @Override ++ public Priority getPriority() { ++ return this.getPriorityVolatile(); ++ } ++ ++ @Override ++ public boolean lowerPriority(final Priority priority) { ++ int failures = 0; ++ for (Priority curr = this.getPriorityVolatile();;) { ++ if (curr == Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr.isLowerOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { ++ this.updatePriorityInQueue(); ++ return true; ++ } ++ ++failures; ++ } ++ } ++ ++ @Override ++ public long getSubOrder() { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean setSubOrder(final long subOrder) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean raiseSubOrder(final long subOrder) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean lowerSubOrder(final long subOrder) { ++ throw new UnsupportedOperationException(); ++ } ++ ++ @Override ++ public boolean setPriorityAndSubOrder(final Priority priority, final long subOrder) { ++ return this.setPriority(priority); ++ } ++ ++ @Override ++ public boolean setPriority(final Priority priority) { ++ int failures = 0; ++ for (Priority curr = this.getPriorityVolatile();;) { ++ if (curr == Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr == priority) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { ++ this.updatePriorityInQueue(); ++ return true; ++ } ++ ++failures; ++ } ++ } ++ ++ @Override ++ public boolean raisePriority(final Priority priority) { ++ int failures = 0; ++ for (Priority curr = this.getPriorityVolatile();;) { ++ if (curr == Priority.COMPLETING) { ++ return false; ++ } ++ ++ if (curr.isHigherOrEqualPriority(priority)) { ++ return false; ++ } ++ ++ for (int i = 0; i < failures; ++i) { ++ ConcurrentUtil.backoff(); ++ } ++ ++ if (curr == (curr = this.compareAndExchangePriority(curr, priority))) { ++ this.updatePriorityInQueue(); ++ return true; ++ } ++ ++failures; ++ } ++ } ++ ++ @Override ++ public boolean execute() { ++ return this.tryComplete(false); ++ } ++ ++ @Override ++ public boolean cancel() { ++ return this.tryComplete(true); ++ } ++ } ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedWorldData.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedWorldData.java.patch new file mode 100644 index 0000000..b17b64b --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/RegionizedWorldData.java.patch @@ -0,0 +1,773 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java +@@ -1,0 +_,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 blockEvents) { ++ for (final BlockEventData blockEventData : blockEvents) { ++ this.pushBlockEvent(blockEventData); ++ } ++ } ++ ++ public void removeIfBlockEvents(final Predicate predicate) { ++ for (final Iterator iterator = this.blockEvents.iterator(); iterator.hasNext();) { ++ final BlockEventData blockEventData = iterator.next(); ++ if (predicate.test(blockEventData)) { ++ iterator.remove(); ++ } ++ } ++ } ++ ++ public BlockEventData removeFirstBlockEvent() { ++ BlockEventData ret; ++ while (!this.blockEvents.isEmpty()) { ++ ret = this.blockEvents.removeFirst(); ++ if (TickThread.isTickThreadFor(this.world, ret.pos())) { ++ return ret; ++ } // else: chunk must have been unloaded ++ } ++ ++ return null; ++ } ++ ++ public LevelTicks getBlockLevelTicks() { ++ return this.blockLevelTicks; ++ } ++ ++ public LevelTicks getFluidLevelTicks() { ++ return this.fluidLevelTicks; ++ } ++ ++ // tile entity ticking ++ public void addBlockEntityTicker(final TickingBlockEntity ticker) { ++ TickThread.ensureTickThread(this.world, ticker.getPos(), "Tile entity must be owned by current region"); ++ ++ (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); ++ } ++ ++ public void seTtickingBlockEntities(final boolean to) { ++ this.tickingBlockEntities = true; ++ } ++ ++ public List getBlockEntityTickers() { ++ return this.blockEntityTickers; ++ } ++ ++ public void pushPendingTickingBlockEntities() { ++ if (!this.pendingBlockEntityTickers.isEmpty()) { ++ this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); ++ this.pendingBlockEntityTickers.clear(); ++ } ++ } ++ ++ // ticking chunks ++ public void addEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.entityTickingChunks.add(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public void removeEntityTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.entityTickingChunks.remove(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public ReferenceList getEntityTickingChunks() { ++ return this.entityTickingChunks; ++ } ++ ++ public void addTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.tickingChunks.add(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public void removeTickingChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.tickingChunks.remove(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public ReferenceList getTickingChunks() { ++ return this.tickingChunks; ++ } ++ ++ public void addChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.chunks.add(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public void removeChunk(final ServerChunkCache.ChunkAndHolder holder) { ++ this.chunks.remove(holder); ++ TickRegions.RegionStats.updateCurrentRegion(); ++ } ++ ++ public ReferenceList getChunks() { ++ return this.chunks; ++ } ++ ++ public int getEntityTickingChunkCount() { ++ return this.entityTickingChunks.size(); ++ } ++ ++ public int getChunkCount() { ++ return this.chunks.size(); ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/Schedule.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/Schedule.java.patch new file mode 100644 index 0000000..a0f3fc9 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/Schedule.java.patch @@ -0,0 +1,94 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/Schedule.java +@@ -1,0 +_,91 @@ ++package io.papermc.paper.threadedregions; ++ ++/** ++ * A Schedule is an object that can be used to maintain a periodic schedule for an event of interest. ++ */ ++public final class Schedule { ++ ++ private long lastPeriod; ++ ++ /** ++ * Initialises a schedule with the provided period. ++ * @param firstPeriod The last time an event of interest occurred. ++ * @see #setLastPeriod(long) ++ */ ++ public Schedule(final long firstPeriod) { ++ this.lastPeriod = firstPeriod; ++ } ++ ++ /** ++ * Updates the last period to the specified value. This call sets the last "time" the event ++ * of interest took place at. Thus, the value returned by {@link #getDeadline(long)} is ++ * the provided time plus the period length provided to {@code getDeadline}. ++ * @param value The value to set the last period to. ++ */ ++ public void setLastPeriod(final long value) { ++ this.lastPeriod = value; ++ } ++ ++ /** ++ * Returns the last time the event of interest should have taken place. ++ */ ++ public long getLastPeriod() { ++ return this.lastPeriod; ++ } ++ ++ /** ++ * Returns the number of times the event of interest should have taken place between the last ++ * period and the provided time given the period between each event. ++ * @param periodLength The length of the period between events in ns. ++ * @param time The provided time. ++ */ ++ public int getPeriodsAhead(final long periodLength, final long time) { ++ final long difference = time - this.lastPeriod; ++ final int ret = (int)(Math.abs(difference) / periodLength); ++ return difference >= 0 ? ret : -ret; ++ } ++ ++ /** ++ * Returns the next starting deadline for the event of interest to take place, ++ * given the provided period length. ++ * @param periodLength The provided period length. ++ */ ++ public long getDeadline(final long periodLength) { ++ return this.lastPeriod + periodLength; ++ } ++ ++ /** ++ * Adjusts the last period so that the next starting deadline returned is the next period specified, ++ * given the provided period length. ++ * @param nextPeriod The specified next starting deadline. ++ * @param periodLength The specified period length. ++ */ ++ public void setNextPeriod(final long nextPeriod, final long periodLength) { ++ this.lastPeriod = nextPeriod - periodLength; ++ } ++ ++ /** ++ * Increases the last period by the specified number of periods and period length. ++ * The specified number of periods may be < 0, in which case the last period ++ * will decrease. ++ * @param periods The specified number of periods. ++ * @param periodLength The specified period length. ++ */ ++ public void advanceBy(final int periods, final long periodLength) { ++ this.lastPeriod += (long)periods * periodLength; ++ } ++ ++ /** ++ * Sets the last period so that it is the specified number of periods ahead ++ * given the specified time and period length. ++ * @param periodsToBeAhead Specified number of periods to be ahead by. ++ * @param periodLength The specified period length. ++ * @param time The specified time. ++ */ ++ public void setPeriodsAhead(final int periodsToBeAhead, final long periodLength, final long time) { ++ final int periodsAhead = this.getPeriodsAhead(periodLength, time); ++ final int periodsToAdd = periodsToBeAhead - periodsAhead; ++ ++ this.lastPeriod -= (long)periodsToAdd * periodLength; ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TeleportUtils.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TeleportUtils.java.patch new file mode 100644 index 0000000..5546e86 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TeleportUtils.java.patch @@ -0,0 +1,73 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/TeleportUtils.java +@@ -1,0 +_,70 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.phys.Vec3; ++import org.bukkit.Location; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.event.player.PlayerTeleportEvent; ++import java.util.function.Consumer; ++ ++public final class TeleportUtils { ++ ++ public static void teleport(final Entity from, final boolean useFromRootVehicle, final Entity to, final Float yaw, final Float pitch, ++ final long teleportFlags, final PlayerTeleportEvent.TeleportCause cause, final Consumer onComplete) { ++ // retrieve coordinates ++ final CallbackCompletable positionCompletable = new CallbackCompletable<>(); ++ ++ positionCompletable.addWaiter( ++ (final Location loc, final Throwable thr) -> { ++ if (loc == null) { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ return; ++ } ++ final boolean scheduled = from.getBukkitEntity().taskScheduler.schedule( ++ (final Entity realFrom) -> { ++ final Vec3 pos = new Vec3( ++ loc.getX(), loc.getY(), loc.getZ() ++ ); ++ (useFromRootVehicle ? realFrom.getRootVehicle() : realFrom).teleportAsync( ++ ((CraftWorld)loc.getWorld()).getHandle(), pos, null, null, null, ++ cause, teleportFlags, onComplete ++ ); ++ }, ++ (final Entity retired) -> { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ }, ++ 1L ++ ); ++ if (!scheduled) { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ } ++ } ++ ); ++ ++ final boolean scheduled = to.getBukkitEntity().taskScheduler.schedule( ++ (final Entity target) -> { ++ positionCompletable.complete(target.getBukkitEntity().getLocation()); ++ }, ++ (final Entity retired) -> { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ }, ++ 1L ++ ); ++ if (!scheduled) { ++ if (onComplete != null) { ++ onComplete.accept(null); ++ } ++ } ++ } ++ ++ private TeleportUtils() {} ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/ThreadedRegionizer.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/ThreadedRegionizer.java.patch new file mode 100644 index 0000000..2eeb9b8 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/ThreadedRegionizer.java.patch @@ -0,0 +1,1408 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/ThreadedRegionizer.java +@@ -1,0 +_,1405 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.map.SWMRLong2ObjectHashTable; ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import com.destroystokyo.paper.util.SneakyThrow; ++import com.mojang.logging.LogUtils; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++import it.unimi.dsi.fastutil.longs.LongComparator; ++import it.unimi.dsi.fastutil.longs.LongIterator; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.core.BlockPos; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.entity.Entity; ++import net.minecraft.world.level.ChunkPos; ++import org.slf4j.Logger; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Iterator; ++import java.util.List; ++import java.util.Set; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.concurrent.locks.StampedLock; ++import java.util.function.BooleanSupplier; ++import java.util.function.Consumer; ++ ++public final class ThreadedRegionizer, S extends ThreadedRegionizer.ThreadedRegionSectionData> { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ ++ public final int regionSectionChunkSize; ++ public final int sectionChunkShift; ++ public final int minSectionRecalcCount; ++ public final int emptySectionCreateRadius; ++ public final int regionSectionMergeRadius; ++ public final double maxDeadRegionPercent; ++ public final ServerLevel world; ++ ++ private final SWMRLong2ObjectHashTable> sections = new SWMRLong2ObjectHashTable<>(); ++ private final SWMRLong2ObjectHashTable> regionsById = new SWMRLong2ObjectHashTable<>(); ++ private final RegionCallbacks callbacks; ++ private final StampedLock regionLock = new StampedLock(); ++ private Thread writeLockOwner; ++ ++ /* ++ static final record Operation(String type, int chunkX, int chunkZ) {} ++ private final MultiThreadedQueue ops = new MultiThreadedQueue<>(); ++ */ ++ ++ /* ++ * See REGION_LOGIC.md for complete details on what this class is doing ++ */ ++ ++ public ThreadedRegionizer(final int minSectionRecalcCount, final double maxDeadRegionPercent, ++ final int emptySectionCreateRadius, final int regionSectionMergeRadius, ++ final int regionSectionChunkShift, final ServerLevel world, ++ final RegionCallbacks callbacks) { ++ if (emptySectionCreateRadius <= 0) { ++ throw new IllegalStateException("Region section create radius must be > 0"); ++ } ++ if (regionSectionMergeRadius <= 0) { ++ throw new IllegalStateException("Region section merge radius must be > 0"); ++ } ++ this.regionSectionChunkSize = 1 << regionSectionChunkShift; ++ this.sectionChunkShift = regionSectionChunkShift; ++ this.minSectionRecalcCount = Math.max(2, minSectionRecalcCount); ++ this.maxDeadRegionPercent = maxDeadRegionPercent; ++ this.emptySectionCreateRadius = emptySectionCreateRadius; ++ this.regionSectionMergeRadius = regionSectionMergeRadius; ++ this.world = world; ++ this.callbacks = callbacks; ++ //this.loadTestData(); ++ } ++ ++ /* ++ private static String substr(String val, String prefix, int from) { ++ int idx = val.indexOf(prefix, from) + prefix.length(); ++ int idx2 = val.indexOf(',', idx); ++ if (idx2 == -1) { ++ idx2 = val.indexOf(']', idx); ++ } ++ return val.substring(idx, idx2); ++ } ++ ++ private void loadTestData() { ++ if (true) { ++ return; ++ } ++ try { ++ final JsonArray arr = JsonParser.parseReader(new FileReader("test.json")).getAsJsonArray(); ++ ++ List ops = new ArrayList<>(); ++ ++ for (JsonElement elem : arr) { ++ JsonObject obj = elem.getAsJsonObject(); ++ String val = obj.get("value").getAsString(); ++ ++ String type = substr(val, "type=", 0); ++ String x = substr(val, "chunkX=", 0); ++ String z = substr(val, "chunkZ=", 0); ++ ++ ops.add(new Operation(type, Integer.parseInt(x), Integer.parseInt(z))); ++ } ++ ++ for (Operation op : ops) { ++ switch (op.type) { ++ case "add": { ++ this.addChunk(op.chunkX, op.chunkZ); ++ break; ++ } ++ case "remove": { ++ this.removeChunk(op.chunkX, op.chunkZ); ++ break; ++ } ++ case "mark_ticking": { ++ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.tryMarkTicking(); ++ break; ++ } ++ case "rel_region": { ++ if (this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.state == ThreadedRegion.STATE_TICKING) { ++ this.sections.get(CoordinateUtils.getChunkKey(op.chunkX, op.chunkZ)).region.markNotTicking(); ++ } ++ break; ++ } ++ } ++ } ++ ++ } catch (final Exception ex) { ++ throw new IllegalStateException(ex); ++ } ++ } ++ */ ++ ++ public void acquireReadLock() { ++ this.regionLock.readLock(); ++ } ++ ++ public void releaseReadLock() { ++ this.regionLock.tryUnlockRead(); ++ } ++ ++ private void acquireWriteLock() { ++ final Thread currentThread = Thread.currentThread(); ++ if (this.writeLockOwner == currentThread) { ++ throw new IllegalStateException("Cannot recursively operate in the regioniser"); ++ } ++ this.regionLock.writeLock(); ++ this.writeLockOwner = currentThread; ++ } ++ ++ private void releaseWriteLock() { ++ this.writeLockOwner = null; ++ this.regionLock.tryUnlockWrite(); ++ } ++ ++ private void onRegionCreate(final ThreadedRegion region) { ++ final ThreadedRegion conflict; ++ if ((conflict = this.regionsById.putIfAbsent(region.id, region)) != null) { ++ throw new IllegalStateException("Region " + region + " is already mapped to " + conflict); ++ } ++ } ++ ++ private void onRegionDestroy(final ThreadedRegion region) { ++ final ThreadedRegion removed = this.regionsById.remove(region.id); ++ if (removed != region) { ++ throw new IllegalStateException("Expected to remove " + region + ", but removed " + removed); ++ } ++ } ++ ++ public int getSectionCoordinate(final int chunkCoordinate) { ++ return chunkCoordinate >> this.sectionChunkShift; ++ } ++ ++ public long getSectionKey(final BlockPos pos) { ++ return CoordinateUtils.getChunkKey((pos.getX() >> 4) >> this.sectionChunkShift, (pos.getZ() >> 4) >> this.sectionChunkShift); ++ } ++ ++ public long getSectionKey(final ChunkPos pos) { ++ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); ++ } ++ ++ public long getSectionKey(final Entity entity) { ++ final ChunkPos pos = entity.chunkPosition(); ++ return CoordinateUtils.getChunkKey(pos.x >> this.sectionChunkShift, pos.z >> this.sectionChunkShift); ++ } ++ ++ public void computeForAllRegions(final Consumer> consumer) { ++ this.regionLock.readLock(); ++ try { ++ this.regionsById.forEachValue(consumer); ++ } finally { ++ this.regionLock.tryUnlockRead(); ++ } ++ } ++ ++ public void computeForAllRegionsUnsynchronised(final Consumer> consumer) { ++ this.regionsById.forEachValue(consumer); ++ } ++ ++ public int computeForRegions(final int fromChunkX, final int fromChunkZ, final int toChunkX, final int toChunkZ, ++ final Consumer>> consumer) { ++ final int shift = this.sectionChunkShift; ++ final int fromSectionX = fromChunkX >> shift; ++ final int fromSectionZ = fromChunkZ >> shift; ++ final int toSectionX = toChunkX >> shift; ++ final int toSectionZ = toChunkZ >> shift; ++ this.acquireWriteLock(); ++ try { ++ final ReferenceOpenHashSet> set = new ReferenceOpenHashSet<>(); ++ ++ for (int currZ = fromSectionZ; currZ <= toSectionZ; ++currZ) { ++ for (int currX = fromSectionX; currX <= toSectionX; ++currX) { ++ final ThreadedRegionSection section = this.sections.get(CoordinateUtils.getChunkKey(currX, currZ)); ++ if (section != null) { ++ set.add(section.getRegionPlain()); ++ } ++ } ++ } ++ ++ consumer.accept(set); ++ ++ return set.size(); ++ } finally { ++ this.releaseWriteLock(); ++ } ++ } ++ ++ public ThreadedRegion getRegionAtUnsynchronised(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ final ThreadedRegionSection section = this.sections.get(sectionKey); ++ ++ return section == null ? null : section.getRegion(); ++ } ++ ++ public ThreadedRegion getRegionAtSynchronised(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ // try an optimistic read ++ { ++ final long readAttempt = this.regionLock.tryOptimisticRead(); ++ final ThreadedRegionSection optimisticSection = this.sections.get(sectionKey); ++ final ThreadedRegion optimisticRet = ++ optimisticSection == null ? null : optimisticSection.getRegionPlain(); ++ if (this.regionLock.validate(readAttempt)) { ++ return optimisticRet; ++ } ++ } ++ ++ // failed, fall back to acquiring the lock ++ this.regionLock.readLock(); ++ try { ++ final ThreadedRegionSection section = this.sections.get(sectionKey); ++ ++ return section == null ? null : section.getRegionPlain(); ++ } finally { ++ this.regionLock.tryUnlockRead(); ++ } ++ } ++ ++ /** ++ * Adds a chunk to the regioniser. Note that it is illegal to add a chunk unless ++ * addChunk has not been called for it or removeChunk has been previously called. ++ * ++ *

    ++ * Note that it is illegal to additionally call addChunk or removeChunk for the same ++ * region section in parallel. ++ *

    ++ */ ++ public void addChunk(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ // Given that for each section, no addChunk/removeChunk can occur in parallel, ++ // we can avoid the lock IF the section exists AND it has a non-zero chunk count. ++ { ++ final ThreadedRegionSection existing = this.sections.get(sectionKey); ++ if (existing != null && !existing.isEmpty()) { ++ existing.addChunk(chunkX, chunkZ); ++ return; ++ } // else: just acquire the write lock ++ } ++ ++ this.acquireWriteLock(); ++ try { ++ ThreadedRegionSection section = this.sections.get(sectionKey); ++ ++ List> newSections = new ArrayList<>(); ++ ++ if (section == null) { ++ // no section at all ++ section = new ThreadedRegionSection<>(sectionX, sectionZ, this, chunkX, chunkZ); ++ this.sections.put(sectionKey, section); ++ newSections.add(section); ++ } else { ++ section.addChunk(chunkX, chunkZ); ++ } ++ // due to the fast check from above, we know the section is empty whether we needed to create it or not ++ ++ // enforce the adjacency invariant by creating / updating neighbour sections ++ final int createRadius = this.emptySectionCreateRadius; ++ final int searchRadius = createRadius + this.regionSectionMergeRadius; ++ ReferenceOpenHashSet> nearbyRegions = null; ++ for (int dx = -searchRadius; dx <= searchRadius; ++dx) { ++ for (int dz = -searchRadius; dz <= searchRadius; ++dz) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ final int squareDistance = Math.max(Math.abs(dx), Math.abs(dz)); ++ final boolean inCreateRange = squareDistance <= createRadius; ++ ++ final int neighbourX = dx + sectionX; ++ final int neighbourZ = dz + sectionZ; ++ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); ++ ++ ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); ++ ++ if (neighbourSection != null) { ++ if (nearbyRegions == null) { ++ nearbyRegions = new ReferenceOpenHashSet<>(((searchRadius * 2 + 1) * (searchRadius * 2 + 1)) >> 1); ++ } ++ nearbyRegions.add(neighbourSection.getRegionPlain()); ++ } ++ ++ if (!inCreateRange) { ++ continue; ++ } ++ ++ // we need to ensure the section exists ++ if (neighbourSection != null) { ++ // nothing else to do ++ neighbourSection.incrementNonEmptyNeighbours(); ++ continue; ++ } ++ neighbourSection = new ThreadedRegionSection<>(neighbourX, neighbourZ, this, 1); ++ if (null != this.sections.put(neighbourKey, neighbourSection)) { ++ throw new IllegalStateException("Failed to insert new section"); ++ } ++ newSections.add(neighbourSection); ++ } ++ } ++ ++ if (newSections.isEmpty()) { ++ // if we didn't add any sections, then we don't need to merge any regions or create a region ++ return; ++ } ++ ++ final ThreadedRegion regionOfInterest; ++ final boolean regionOfInterestAlive; ++ if (nearbyRegions == null) { ++ // we can simply create a new region, don't have neighbours to worry about merging into ++ regionOfInterest = new ThreadedRegion<>(this); ++ regionOfInterestAlive = true; ++ ++ for (int i = 0, len = newSections.size(); i < len; ++i) { ++ regionOfInterest.addSection(newSections.get(i)); ++ } ++ ++ // only call create callback after adding sections ++ regionOfInterest.onCreate(); ++ } else { ++ // need to merge the regions ++ ThreadedRegion firstUnlockedRegion = null; ++ ++ for (final ThreadedRegion region : nearbyRegions) { ++ if (region.isTicking()) { ++ continue; ++ } ++ firstUnlockedRegion = region; ++ if (firstUnlockedRegion.state == ThreadedRegion.STATE_READY && (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty())) { ++ throw new IllegalStateException("Illegal state for unlocked region " + firstUnlockedRegion); ++ } ++ break; ++ } ++ ++ if (firstUnlockedRegion != null) { ++ regionOfInterest = firstUnlockedRegion; ++ } else { ++ regionOfInterest = new ThreadedRegion<>(this); ++ } ++ ++ for (int i = 0, len = newSections.size(); i < len; ++i) { ++ regionOfInterest.addSection(newSections.get(i)); ++ } ++ ++ // only call create callback after adding sections ++ if (firstUnlockedRegion == null) { ++ regionOfInterest.onCreate(); ++ } ++ ++ if (firstUnlockedRegion != null && nearbyRegions.size() == 1) { ++ // nothing to do further, no need to merge anything ++ return; ++ } ++ ++ // we need to now tell all the other regions to merge into the region we just created, ++ // and to merge all the ones we can immediately ++ ++ for (final ThreadedRegion region : nearbyRegions) { ++ if (region == regionOfInterest) { ++ continue; ++ } ++ ++ if (!region.killAndMergeInto(regionOfInterest)) { ++ // note: the region may already be a merge target ++ regionOfInterest.mergeIntoLater(region); ++ } ++ } ++ ++ if (firstUnlockedRegion != null && firstUnlockedRegion.state == ThreadedRegion.STATE_READY) { ++ // we need to retire this region if the merges added other pending merges ++ if (!firstUnlockedRegion.mergeIntoLater.isEmpty() || !firstUnlockedRegion.expectingMergeFrom.isEmpty()) { ++ firstUnlockedRegion.state = ThreadedRegion.STATE_TRANSIENT; ++ this.callbacks.onRegionInactive(firstUnlockedRegion); ++ } ++ } ++ ++ // need to set alive if we created it and there are no pending merges ++ regionOfInterestAlive = firstUnlockedRegion == null && regionOfInterest.mergeIntoLater.isEmpty() && regionOfInterest.expectingMergeFrom.isEmpty(); ++ } ++ ++ if (regionOfInterestAlive) { ++ regionOfInterest.state = ThreadedRegion.STATE_READY; ++ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Should not happen on region " + this); ++ } ++ this.callbacks.onRegionActive(regionOfInterest); ++ } ++ ++ if (regionOfInterest.state == ThreadedRegion.STATE_READY) { ++ if (!regionOfInterest.mergeIntoLater.isEmpty() || !regionOfInterest.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Should not happen on region " + this); ++ } ++ } ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); ++ SneakyThrow.sneaky(throwable); ++ return; // unreachable ++ } finally { ++ this.releaseWriteLock(); ++ } ++ } ++ ++ public void removeChunk(final int chunkX, final int chunkZ) { ++ final int sectionX = chunkX >> this.sectionChunkShift; ++ final int sectionZ = chunkZ >> this.sectionChunkShift; ++ final long sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ ++ // Given that for each section, no addChunk/removeChunk can occur in parallel, ++ // we can avoid the lock IF the section exists AND it has a chunk count > 1 ++ final ThreadedRegionSection section = this.sections.get(sectionKey); ++ if (section == null) { ++ throw new IllegalStateException("Chunk (" + chunkX + "," + chunkZ + ") has no section"); ++ } ++ if (!section.hasOnlyOneChunk()) { ++ // chunk will not go empty, so we don't need to acquire the lock ++ section.removeChunk(chunkX, chunkZ); ++ return; ++ } ++ ++ this.acquireWriteLock(); ++ try { ++ section.removeChunk(chunkX, chunkZ); ++ ++ final int searchRadius = this.emptySectionCreateRadius; ++ for (int dx = -searchRadius; dx <= searchRadius; ++dx) { ++ for (int dz = -searchRadius; dz <= searchRadius; ++dz) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ ++ final int neighbourX = dx + sectionX; ++ final int neighbourZ = dz + sectionZ; ++ final long neighbourKey = CoordinateUtils.getChunkKey(neighbourX, neighbourZ); ++ ++ final ThreadedRegionSection neighbourSection = this.sections.get(neighbourKey); ++ ++ // should be non-null here always ++ neighbourSection.decrementNonEmptyNeighbours(); ++ } ++ } ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to add chunk (" + chunkX + "," + chunkZ + ")", throwable); ++ SneakyThrow.sneaky(throwable); ++ return; // unreachable ++ } finally { ++ this.releaseWriteLock(); ++ } ++ } ++ ++ // must hold regionLock ++ private void onRegionRelease(final ThreadedRegion region) { ++ if (!region.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Region " + region + " should not have any regions to merge into!"); ++ } ++ ++ final boolean hasExpectingMerges = !region.expectingMergeFrom.isEmpty(); ++ ++ // is this region supposed to merge into any other region? ++ if (hasExpectingMerges) { ++ // merge the regions into this one ++ final ReferenceOpenHashSet> expectingMergeFrom = region.expectingMergeFrom.clone(); ++ for (final ThreadedRegion mergeFrom : expectingMergeFrom) { ++ if (!mergeFrom.killAndMergeInto(region)) { ++ throw new IllegalStateException("Merge from region " + mergeFrom + " should be killable! Trying to merge into " + region); ++ } ++ } ++ ++ if (!region.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Region " + region + " should no longer have merge requests after mering from " + expectingMergeFrom); ++ } ++ ++ if (!region.mergeIntoLater.isEmpty()) { ++ // There is another nearby ticking region that we need to merge into ++ region.state = ThreadedRegion.STATE_TRANSIENT; ++ this.callbacks.onRegionInactive(region); ++ // return to avoid removing dead sections or splitting, these actions will be performed ++ // by the region we merge into ++ return; ++ } ++ } ++ ++ // now check whether we need to recalculate regions ++ final boolean removeDeadSections = hasExpectingMerges || region.hasNoAliveSections() ++ || (region.sectionByKey.size() >= this.minSectionRecalcCount && region.getDeadSectionPercent() >= this.maxDeadRegionPercent); ++ final boolean removedDeadSections = removeDeadSections && !region.deadSections.isEmpty(); ++ if (removeDeadSections) { ++ // kill dead sections ++ for (final ThreadedRegionSection deadSection : region.deadSections) { ++ final long key = CoordinateUtils.getChunkKey(deadSection.sectionX, deadSection.sectionZ); ++ ++ if (!deadSection.isEmpty()) { ++ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has chunks!"); ++ } ++ if (deadSection.hasNonEmptyNeighbours()) { ++ throw new IllegalStateException("Dead section '" + deadSection.toStringWithRegion() + "' is marked dead but has non-empty neighbours!"); ++ } ++ if (!region.sectionByKey.remove(key, deadSection)) { ++ throw new IllegalStateException("Region " + region + " has inconsistent state, it should contain section " + deadSection); ++ } ++ if (this.sections.remove(key) != deadSection) { ++ throw new IllegalStateException("Cannot remove dead section '" + ++ deadSection.toStringWithRegion() + "' from section state! State at section coordinate: " + this.sections.get(key)); ++ } ++ } ++ region.deadSections.clear(); ++ } ++ ++ // if we removed dead sections, we should check if the region can be split into smaller ones ++ // otherwise, the region remains alive ++ if (!removedDeadSections) { ++ // didn't remove dead sections, don't check for split ++ region.state = ThreadedRegion.STATE_READY; ++ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Illegal state " + region); ++ } ++ return; ++ } ++ ++ // first, we need to build copy of coordinate->section map of all sections in recalculate ++ final Long2ReferenceOpenHashMap> recalculateSections = region.sectionByKey.clone(); ++ ++ if (recalculateSections.isEmpty()) { ++ // looks like the region's sections were all dead, and now there is no region at all ++ region.state = ThreadedRegion.STATE_DEAD; ++ region.onRemove(true); ++ return; ++ } ++ ++ // merge radius is max, since recalculateSections includes the dead or empty sections ++ final int mergeRadius = Math.max(this.regionSectionMergeRadius, this.emptySectionCreateRadius); ++ ++ final List>> newRegions = new ArrayList<>(); ++ while (!recalculateSections.isEmpty()) { ++ // select any section, then BFS around it to find all of its neighbours to form a region ++ // once no more neighbours are found, the region is complete ++ final List> currRegion = new ArrayList<>(); ++ final Iterator> firstIterator = recalculateSections.values().iterator(); ++ ++ currRegion.add(firstIterator.next()); ++ firstIterator.remove(); ++ search_loop: ++ for (int idx = 0; idx < currRegion.size(); ++idx) { ++ final ThreadedRegionSection curr = currRegion.get(idx); ++ final int centerX = curr.sectionX; ++ final int centerZ = curr.sectionZ; ++ ++ // find neighbours in radius ++ for (int dz = -mergeRadius; dz <= mergeRadius; ++dz) { ++ for (int dx = -mergeRadius; dx <= mergeRadius; ++dx) { ++ if ((dx | dz) == 0) { ++ continue; ++ } ++ ++ final ThreadedRegionSection section = recalculateSections.remove(CoordinateUtils.getChunkKey(dx + centerX, dz + centerZ)); ++ if (section == null) { ++ continue; ++ } ++ ++ currRegion.add(section); ++ ++ if (recalculateSections.isEmpty()) { ++ // no point in searching further ++ break search_loop; ++ } ++ } ++ } ++ } ++ ++ newRegions.add(currRegion); ++ } ++ ++ // now we have split the regions into separate parts, we can split recalculate ++ ++ if (newRegions.size() == 1) { ++ // no need to split anything, we're done here ++ region.state = ThreadedRegion.STATE_READY; ++ if (!region.expectingMergeFrom.isEmpty() || !region.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Illegal state " + region); ++ } ++ return; ++ } ++ ++ final List> newRegionObjects = new ArrayList<>(newRegions.size()); ++ for (int i = 0, len = newRegions.size(); i < len; ++i) { ++ newRegionObjects.add(new ThreadedRegion<>(this)); ++ } ++ ++ this.callbacks.preSplit(region, newRegionObjects); ++ ++ // need to split the region, so we need to kill the old one first ++ region.state = ThreadedRegion.STATE_DEAD; ++ region.onRemove(true); ++ ++ // create new regions ++ final Long2ReferenceOpenHashMap> newRegionsMap = new Long2ReferenceOpenHashMap<>(); ++ final ReferenceOpenHashSet> newRegionsSet = new ReferenceOpenHashSet<>(newRegionObjects); ++ ++ for (int i = 0, len = newRegions.size(); i < len; i++) { ++ final List> sections = newRegions.get(i); ++ final ThreadedRegion newRegion = newRegionObjects.get(i); ++ ++ for (final ThreadedRegionSection section : sections) { ++ section.setRegionRelease(null); ++ newRegion.addSection(section); ++ final ThreadedRegion curr = newRegionsMap.putIfAbsent(section.sectionKey, newRegion); ++ if (curr != null) { ++ throw new IllegalStateException("Expected no region at " + section + ", but got " + curr + ", should have put " + newRegion); ++ } ++ } ++ } ++ ++ region.split(newRegionsMap, newRegionsSet); ++ ++ // only after invoking data callbacks ++ ++ for (final ThreadedRegion newRegion : newRegionsSet) { ++ newRegion.state = ThreadedRegion.STATE_READY; ++ if (!newRegion.expectingMergeFrom.isEmpty() || !newRegion.mergeIntoLater.isEmpty()) { ++ throw new IllegalStateException("Illegal state " + newRegion); ++ } ++ newRegion.onCreate(); ++ this.callbacks.onRegionActive(newRegion); ++ } ++ } ++ ++ public static final class ThreadedRegion, S extends ThreadedRegionSectionData> { ++ ++ private static final AtomicLong REGION_ID_GENERATOR = new AtomicLong(); ++ ++ private static final int STATE_TRANSIENT = 0; ++ private static final int STATE_READY = 1; ++ private static final int STATE_TICKING = 2; ++ private static final int STATE_DEAD = 3; ++ ++ public final long id; ++ ++ private int state; ++ ++ private final Long2ReferenceOpenHashMap> sectionByKey = new Long2ReferenceOpenHashMap<>(); ++ private final ReferenceOpenHashSet> deadSections = new ReferenceOpenHashSet<>(); ++ ++ public final ThreadedRegionizer regioniser; ++ ++ private final R data; ++ ++ private final ReferenceOpenHashSet> mergeIntoLater = new ReferenceOpenHashSet<>(); ++ private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>(); ++ ++ public ThreadedRegion(final ThreadedRegionizer regioniser) { ++ this.regioniser = regioniser; ++ this.id = REGION_ID_GENERATOR.getAndIncrement(); ++ this.state = STATE_TRANSIENT; ++ this.data = regioniser.callbacks.createNewData(this); ++ } ++ ++ public LongArrayList getOwnedSections() { ++ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); ++ if (lock) { ++ this.regioniser.regionLock.readLock(); ++ } ++ try { ++ final LongArrayList ret = new LongArrayList(this.sectionByKey.size()); ++ ret.addAll(this.sectionByKey.keySet()); ++ ++ return ret; ++ } finally { ++ if (lock) { ++ this.regioniser.regionLock.tryUnlockRead(); ++ } ++ } ++ } ++ ++ /** ++ * returns an iterator directly over the sections map. This is only to be used by a thread which is _ticking_ ++ * 'this' region. ++ */ ++ public LongIterator getOwnedSectionsUnsynchronised() { ++ return this.sectionByKey.keySet().iterator(); ++ } ++ ++ public LongArrayList getOwnedChunks() { ++ final boolean lock = this.regioniser.writeLockOwner != Thread.currentThread(); ++ if (lock) { ++ this.regioniser.regionLock.readLock(); ++ } ++ try { ++ final LongArrayList ret = new LongArrayList(); ++ for (final ThreadedRegionSection section : this.sectionByKey.values()) { ++ ret.addAll(section.getChunks()); ++ } ++ ++ return ret; ++ } finally { ++ if (lock) { ++ this.regioniser.regionLock.tryUnlockRead(); ++ } ++ } ++ } ++ ++ public Long getCenterSection() { ++ final LongArrayList sections = this.getOwnedSections(); ++ ++ final LongComparator comparator = (final long k1, final long k2) -> { ++ final int x1 = CoordinateUtils.getChunkX(k1); ++ final int x2 = CoordinateUtils.getChunkX(k2); ++ ++ final int z1 = CoordinateUtils.getChunkZ(x1); ++ final int z2 = CoordinateUtils.getChunkZ(x2); ++ ++ final int zCompare = Integer.compare(z1, z2); ++ if (zCompare != 0) { ++ return zCompare; ++ } ++ ++ return Integer.compare(x1, x2); ++ }; ++ ++ // note: regions don't always have a chunk section at this point, because the region may have been killed ++ if (sections.isEmpty()) { ++ return null; ++ } ++ ++ sections.sort(comparator); ++ ++ return Long.valueOf(sections.getLong(sections.size() >> 1)); ++ } ++ ++ public ChunkPos getCenterChunk() { ++ final LongArrayList chunks = this.getOwnedChunks(); ++ ++ final LongComparator comparator = (final long k1, final long k2) -> { ++ final int x1 = CoordinateUtils.getChunkX(k1); ++ final int x2 = CoordinateUtils.getChunkX(k2); ++ ++ final int z1 = CoordinateUtils.getChunkZ(k1); ++ final int z2 = CoordinateUtils.getChunkZ(k2); ++ ++ final int zCompare = Integer.compare(z1, z2); ++ if (zCompare != 0) { ++ return zCompare; ++ } ++ ++ return Integer.compare(x1, x2); ++ }; ++ chunks.sort(comparator); ++ ++ // note: regions don't always have a chunk at this point, because the region may have been killed ++ if (chunks.isEmpty()) { ++ return null; ++ } ++ ++ final long middle = chunks.getLong(chunks.size() >> 1); ++ ++ return new ChunkPos(CoordinateUtils.getChunkX(middle), CoordinateUtils.getChunkZ(middle)); ++ } ++ ++ private void onCreate() { ++ this.regioniser.onRegionCreate(this); ++ this.regioniser.callbacks.onRegionCreate(this); ++ } ++ ++ private void onRemove(final boolean wasActive) { ++ if (wasActive) { ++ this.regioniser.callbacks.onRegionInactive(this); ++ } ++ this.regioniser.callbacks.onRegionDestroy(this); ++ this.regioniser.onRegionDestroy(this); ++ } ++ ++ private final boolean hasNoAliveSections() { ++ return this.deadSections.size() == this.sectionByKey.size(); ++ } ++ ++ private final double getDeadSectionPercent() { ++ return (double)this.deadSections.size() / (double)this.sectionByKey.size(); ++ } ++ ++ private void split(final Long2ReferenceOpenHashMap> into, final ReferenceOpenHashSet> regions) { ++ if (this.data != null) { ++ this.data.split(this.regioniser, into, regions); ++ } ++ } ++ ++ boolean killAndMergeInto(final ThreadedRegion mergeTarget) { ++ if (this.state == STATE_TICKING) { ++ return false; ++ } ++ ++ this.regioniser.callbacks.preMerge(this, mergeTarget); ++ ++ this.tryKill(); ++ ++ this.mergeInto(mergeTarget); ++ ++ return true; ++ } ++ ++ private void mergeInto(final ThreadedRegion mergeTarget) { ++ if (this == mergeTarget) { ++ throw new IllegalStateException("Cannot merge a region onto itself"); ++ } ++ if (!this.isDead()) { ++ throw new IllegalStateException("Source region is not dead! Source " + this + ", target " + mergeTarget); ++ } else if (mergeTarget.isDead()) { ++ throw new IllegalStateException("Target region is dead! Source " + this + ", target " + mergeTarget); ++ } ++ ++ for (final ThreadedRegionSection section : this.sectionByKey.values()) { ++ section.setRegionRelease(null); ++ mergeTarget.addSection(section); ++ } ++ for (final ThreadedRegionSection deadSection : this.deadSections) { ++ if (this.sectionByKey.get(deadSection.sectionKey) != deadSection) { ++ throw new IllegalStateException("Source region does not even contain its own dead sections! Missing " + deadSection + " from region " + this); ++ } ++ if (!mergeTarget.deadSections.add(deadSection)) { ++ throw new IllegalStateException("Merge target contains dead section from source! Has " + deadSection + " from region " + this); ++ } ++ } ++ ++ // forward merge expectations ++ for (final ThreadedRegion region : this.expectingMergeFrom) { ++ if (!region.mergeIntoLater.remove(this)) { ++ throw new IllegalStateException("Region " + region + " was not supposed to merge into " + this + "?"); ++ } ++ if (region != mergeTarget) { ++ region.mergeIntoLater(mergeTarget); ++ } ++ } ++ ++ // forward merge into ++ for (final ThreadedRegion region : this.mergeIntoLater) { ++ if (!region.expectingMergeFrom.remove(this)) { ++ throw new IllegalStateException("Region " + this + " was not supposed to merge into " + region + "?"); ++ } ++ if (region != mergeTarget) { ++ mergeTarget.mergeIntoLater(region); ++ } ++ } ++ ++ // finally, merge data ++ if (this.data != null) { ++ this.data.mergeInto(mergeTarget); ++ } ++ } ++ ++ private void mergeIntoLater(final ThreadedRegion region) { ++ if (region.isDead()) { ++ throw new IllegalStateException("Trying to merge later into a dead region: " + region); ++ } ++ final boolean add1, add2; ++ if ((add1 = this.mergeIntoLater.add(region)) != (add2 = region.expectingMergeFrom.add(this))) { ++ throw new IllegalStateException("Inconsistent state between target merge " + region + " and this " + this + ": add1,add2:" + add1 + "," + add2); ++ } ++ } ++ ++ private boolean tryKill() { ++ switch (this.state) { ++ case STATE_TRANSIENT: { ++ this.state = STATE_DEAD; ++ this.onRemove(false); ++ return true; ++ } ++ case STATE_READY: { ++ this.state = STATE_DEAD; ++ this.onRemove(true); ++ return true; ++ } ++ case STATE_TICKING: { ++ return false; ++ } ++ case STATE_DEAD: { ++ throw new IllegalStateException("Already dead"); ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + this.state); ++ } ++ } ++ } ++ ++ private boolean isDead() { ++ return this.state == STATE_DEAD; ++ } ++ ++ private boolean isTicking() { ++ return this.state == STATE_TICKING; ++ } ++ ++ private void removeDeadSection(final ThreadedRegionSection section) { ++ this.deadSections.remove(section); ++ } ++ ++ private void addDeadSection(final ThreadedRegionSection section) { ++ this.deadSections.add(section); ++ } ++ ++ private void addSection(final ThreadedRegionSection section) { ++ if (section.getRegionPlain() != null) { ++ throw new IllegalStateException("Section already has region"); ++ } ++ if (this.sectionByKey.putIfAbsent(section.sectionKey, section) != null) { ++ throw new IllegalStateException("Already have section " + section + ", mapped to " + this.sectionByKey.get(section.sectionKey)); ++ } ++ section.setRegionRelease(this); ++ } ++ ++ public R getData() { ++ return this.data; ++ } ++ ++ public boolean tryMarkTicking(final BooleanSupplier abort) { ++ this.regioniser.acquireWriteLock(); ++ try { ++ if (this.state != STATE_READY || abort.getAsBoolean()) { ++ return false; ++ } ++ ++ if (!this.mergeIntoLater.isEmpty() || !this.expectingMergeFrom.isEmpty()) { ++ throw new IllegalStateException("Region " + this + " should not be ready"); ++ } ++ ++ this.state = STATE_TICKING; ++ return true; ++ } finally { ++ this.regioniser.releaseWriteLock(); ++ } ++ } ++ ++ public boolean markNotTicking() { ++ this.regioniser.acquireWriteLock(); ++ try { ++ if (this.state != STATE_TICKING) { ++ throw new IllegalStateException("Attempting to release non-locked state"); ++ } ++ ++ this.regioniser.onRegionRelease(this); ++ ++ return this.state == STATE_READY; ++ } catch (final Throwable throwable) { ++ LOGGER.error("Failed to release region " + this, throwable); ++ SneakyThrow.sneaky(throwable); ++ return false; // unreachable ++ } finally { ++ this.regioniser.releaseWriteLock(); ++ } ++ } ++ ++ @Override ++ public String toString() { ++ final StringBuilder ret = new StringBuilder(128); ++ ++ ret.append("ThreadedRegion{"); ++ ret.append("state=").append(this.state).append(','); ++ // To avoid recursion in toString, maybe fix later? ++ //ret.append("mergeIntoLater=").append(this.mergeIntoLater).append(','); ++ //ret.append("expectingMergeFrom=").append(this.expectingMergeFrom).append(','); ++ ++ ret.append("sectionCount=").append(this.sectionByKey.size()).append(','); ++ ret.append("sections=["); ++ for (final Iterator> iterator = this.sectionByKey.values().iterator(); iterator.hasNext();) { ++ final ThreadedRegionSection section = iterator.next(); ++ ++ ret.append(section.toString()); ++ if (iterator.hasNext()) { ++ ret.append(','); ++ } ++ } ++ ret.append(']'); ++ ++ ret.append('}'); ++ return ret.toString(); ++ } ++ } ++ ++ public static final class ThreadedRegionSection, S extends ThreadedRegionSectionData> { ++ ++ public final int sectionX; ++ public final int sectionZ; ++ public final long sectionKey; ++ private final long[] chunksBitset; ++ private int chunkCount; ++ private int nonEmptyNeighbours; ++ ++ private ThreadedRegion region; ++ private static final VarHandle REGION_HANDLE = ConcurrentUtil.getVarHandle(ThreadedRegionSection.class, "region", ThreadedRegion.class); ++ ++ public final ThreadedRegionizer regioniser; ++ ++ private final int regionChunkShift; ++ private final int regionChunkMask; ++ ++ private final S data; ++ ++ private ThreadedRegion getRegionPlain() { ++ return (ThreadedRegion)REGION_HANDLE.get(this); ++ } ++ ++ private ThreadedRegion getRegionAcquire() { ++ return (ThreadedRegion)REGION_HANDLE.getAcquire(this); ++ } ++ ++ private void setRegionRelease(final ThreadedRegion value) { ++ REGION_HANDLE.setRelease(this, value); ++ } ++ ++ // creates an empty section with zero non-empty neighbours ++ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser) { ++ this.sectionX = sectionX; ++ this.sectionZ = sectionZ; ++ this.sectionKey = CoordinateUtils.getChunkKey(sectionX, sectionZ); ++ this.chunksBitset = new long[Math.max(1, regioniser.regionSectionChunkSize * regioniser.regionSectionChunkSize / Long.SIZE)]; ++ this.regioniser = regioniser; ++ this.regionChunkShift = regioniser.sectionChunkShift; ++ this.regionChunkMask = regioniser.regionSectionChunkSize - 1; ++ this.data = regioniser.callbacks ++ .createNewSectionData(sectionX, sectionZ, this.regionChunkShift); ++ } ++ ++ // creates a section with an initial chunk with zero non-empty neighbours ++ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, ++ final int chunkXInit, final int chunkZInit) { ++ this(sectionX, sectionZ, regioniser); ++ ++ final int initIndex = this.getChunkIndex(chunkXInit, chunkZInit); ++ this.chunkCount = 1; ++ this.chunksBitset[initIndex >>> 6] = 1L << (initIndex & (Long.SIZE - 1)); // index / Long.SIZE ++ } ++ ++ // creates an empty section with the specified number of non-empty neighbours ++ private ThreadedRegionSection(final int sectionX, final int sectionZ, final ThreadedRegionizer regioniser, ++ final int nonEmptyNeighbours) { ++ this(sectionX, sectionZ, regioniser); ++ ++ this.nonEmptyNeighbours = nonEmptyNeighbours; ++ } ++ ++ public LongArrayList getChunks() { ++ final LongArrayList ret = new LongArrayList(); ++ ++ if (this.chunkCount == 0) { ++ return ret; ++ } ++ ++ final int shift = this.regionChunkShift; ++ final int mask = this.regionChunkMask; ++ final int offsetX = this.sectionX << shift; ++ final int offsetZ = this.sectionZ << shift; ++ ++ final long[] bitset = this.chunksBitset; ++ for (int arrIdx = 0, arrLen = bitset.length; arrIdx < arrLen; ++arrIdx) { ++ long value = bitset[arrIdx]; ++ ++ for (int i = 0, bits = Long.bitCount(value); i < bits; ++i) { ++ final int valueIdx = Long.numberOfTrailingZeros(value); ++ value ^= ca.spottedleaf.concurrentutil.util.IntegerUtil.getTrailingBit(value); ++ ++ final int idx = valueIdx | (arrIdx << 6); ++ ++ final int localX = idx & mask; ++ final int localZ = (idx >>> shift) & mask; ++ ++ ret.add(CoordinateUtils.getChunkKey(localX | offsetX, localZ | offsetZ)); ++ } ++ } ++ ++ return ret; ++ } ++ ++ private boolean isEmpty() { ++ return this.chunkCount == 0; ++ } ++ ++ private boolean hasOnlyOneChunk() { ++ return this.chunkCount == 1; ++ } ++ ++ public boolean hasNonEmptyNeighbours() { ++ return this.nonEmptyNeighbours != 0; ++ } ++ ++ /** ++ * Returns the section data associated with this region section. May be {@code null}. ++ */ ++ public S getData() { ++ return this.data; ++ } ++ ++ /** ++ * Returns the region that owns this section. Unsynchronised access may produce outdateed or transient results. ++ */ ++ public ThreadedRegion getRegion() { ++ return this.getRegionAcquire(); ++ } ++ ++ private int getChunkIndex(final int chunkX, final int chunkZ) { ++ return (chunkX & this.regionChunkMask) | ((chunkZ & this.regionChunkMask) << this.regionChunkShift); ++ } ++ ++ private void markAlive() { ++ this.getRegionPlain().removeDeadSection(this); ++ } ++ ++ private void markDead() { ++ this.getRegionPlain().addDeadSection(this); ++ } ++ ++ private void incrementNonEmptyNeighbours() { ++ if (++this.nonEmptyNeighbours == 1 && this.chunkCount == 0) { ++ this.markAlive(); ++ } ++ final int createRadius = this.regioniser.emptySectionCreateRadius; ++ if (this.nonEmptyNeighbours >= ((createRadius * 2 + 1) * (createRadius * 2 + 1))) { ++ throw new IllegalStateException("Non empty neighbours exceeded max value for radius " + createRadius); ++ } ++ } ++ ++ private void decrementNonEmptyNeighbours() { ++ if (--this.nonEmptyNeighbours == 0 && this.chunkCount == 0) { ++ this.markDead(); ++ } ++ if (this.nonEmptyNeighbours < 0) { ++ throw new IllegalStateException("Non empty neighbours reached zero"); ++ } ++ } ++ ++ /** ++ * Returns whether the chunk was zero. Effectively returns whether the caller needs to create ++ * dead sections / increase non-empty neighbour count for neighbouring sections. ++ */ ++ private boolean addChunk(final int chunkX, final int chunkZ) { ++ final int index = this.getChunkIndex(chunkX, chunkZ); ++ final long bitset = this.chunksBitset[index >>> 6]; // index / Long.SIZE ++ final long after = this.chunksBitset[index >>> 6] = bitset | (1L << (index & (Long.SIZE - 1))); ++ if (after == bitset) { ++ throw new IllegalStateException("Cannot add a chunk to a section which already has the chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); ++ } ++ final boolean notEmpty = ++this.chunkCount == 1; ++ if (notEmpty && this.nonEmptyNeighbours == 0) { ++ this.markAlive(); ++ } ++ return notEmpty; ++ } ++ ++ /** ++ * Returns whether the chunk count is now zero. Effectively returns whether ++ * the caller needs to decrement the neighbour count for neighbouring sections. ++ */ ++ private boolean removeChunk(final int chunkX, final int chunkZ) { ++ final int index = this.getChunkIndex(chunkX, chunkZ); ++ final long before = this.chunksBitset[index >>> 6]; // index / Long.SIZE ++ final long bitset = this.chunksBitset[index >>> 6] = before & ~(1L << (index & (Long.SIZE - 1))); ++ if (before == bitset) { ++ throw new IllegalStateException("Cannot remove a chunk from a section which does not have that chunk! RegionSection: " + this + ", global chunk: " + new ChunkPos(chunkX, chunkZ).toString()); ++ } ++ final boolean empty = --this.chunkCount == 0; ++ if (empty && this.nonEmptyNeighbours == 0) { ++ this.markDead(); ++ } ++ return empty; ++ } ++ ++ @Override ++ public String toString() { ++ return "RegionSection{" + ++ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + ++ "chunkCount=" + this.chunkCount + "," + ++ "chunksBitset=" + toString(this.chunksBitset) + "," + ++ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + ++ "hash=" + this.hashCode() + ++ "}"; ++ } ++ ++ public String toStringWithRegion() { ++ return "RegionSection{" + ++ "sectionCoordinate=" + new ChunkPos(this.sectionX, this.sectionZ).toString() + "," + ++ "chunkCount=" + this.chunkCount + "," + ++ "chunksBitset=" + toString(this.chunksBitset) + "," + ++ "hash=" + this.hashCode() + "," + ++ "nonEmptyNeighbours=" + this.nonEmptyNeighbours + "," + ++ "region=" + this.getRegionAcquire() + ++ "}"; ++ } ++ ++ private static String toString(final long[] array) { ++ final StringBuilder ret = new StringBuilder(); ++ final char[] zeros = new char[Long.SIZE / 4]; ++ for (final long value : array) { ++ // zero pad the hex string ++ Arrays.fill(zeros, '0'); ++ final String string = Long.toHexString(value); ++ System.arraycopy(string.toCharArray(), 0, zeros, zeros.length - string.length(), string.length()); ++ ++ ret.append(zeros); ++ } ++ ++ return ret.toString(); ++ } ++ } ++ ++ public static interface ThreadedRegionData, S extends ThreadedRegionSectionData> { ++ ++ /** ++ * Splits this region data into the specified regions set. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param regioniser Regioniser for which the regions reside in. ++ * @param into A map of region section coordinate key to the region that owns the section. ++ * @param regions The set of regions to split into. ++ */ ++ public void split(final ThreadedRegionizer regioniser, final Long2ReferenceOpenHashMap> into, ++ final ReferenceOpenHashSet> regions); ++ ++ /** ++ * Callback to merge {@code this} region data into the specified region. The state of the region is undefined ++ * except that its region data is already created. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param into Specified region. ++ */ ++ public void mergeInto(final ThreadedRegion into); ++ } ++ ++ public static interface ThreadedRegionSectionData {} ++ ++ public static interface RegionCallbacks, S extends ThreadedRegionSectionData> { ++ ++ /** ++ * Creates new section data for the specified section x and section z. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param sectionX x coordinate of the section. ++ * @param sectionZ z coordinate of the section. ++ * @param sectionShift The signed right shift value that can be applied to any chunk coordinate that ++ * produces a section coordinate. ++ * @return New section data, may be {@code null}. ++ */ ++ public S createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift); ++ ++ /** ++ * Creates new region data for the specified region. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param forRegion The region to create the data for. ++ * @return New region data, may be {@code null}. ++ */ ++ public R createNewData(final ThreadedRegion forRegion); ++ ++ /** ++ * Callback for when a region is created. This is invoked after the region is completely set up, ++ * so its data and owned sections are reliable to inspect. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that was created. ++ */ ++ public void onRegionCreate(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region is destroyed. This is invoked before the region is actually destroyed; so ++ * its data and owned sections are reliable to inspect. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that is about to be destroyed. ++ */ ++ public void onRegionDestroy(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region is considered "active." An active region x is a non-destroyed region which ++ * is not scheduled to merge into another region y and there are no non-destroyed regions z which are ++ * scheduled to merge into the region x. Equivalently, an active region is not directly adjacent to any ++ * other region considering the regioniser's empty section radius. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that is now active. ++ */ ++ public void onRegionActive(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region transistions becomes inactive. An inactive region is non-destroyed, but ++ * has neighbouring adjacent regions considering the regioniser's empty section radius. Effectively, ++ * an inactive region may not tick and needs to be merged into its neighbouring regions. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param region The region that is now inactive. ++ */ ++ public void onRegionInactive(final ThreadedRegion region); ++ ++ /** ++ * Callback for when a region (from) is about to be merged into a target region (into). Note that ++ * {@code from} is still alive and is a distinct region. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param from The region that will be merged into the target. ++ * @param into The target of the merge. ++ */ ++ public void preMerge(final ThreadedRegion from, final ThreadedRegion into); ++ ++ /** ++ * Callback for when a region (from) is about to be split into a list of target region (into). Note that ++ * {@code from} is still alive, while the list of target regions are not initialised. ++ *

    ++ * Note: ++ *

    ++ *

    ++ * This function is always called while holding critical locks and as such should not attempt to block on anything, and ++ * should NOT retrieve or modify ANY world state. ++ *

    ++ * @param from The region that will be merged into the target. ++ * @param into The list of regions to split into. ++ */ ++ public void preSplit(final ThreadedRegion from, final List> into); ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickData.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickData.java.patch new file mode 100644 index 0000000..2daab49 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickData.java.patch @@ -0,0 +1,336 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/TickData.java +@@ -1,0 +_,333 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import io.papermc.paper.util.IntervalledCounter; ++import it.unimi.dsi.fastutil.longs.LongArrayList; ++ ++import java.util.ArrayDeque; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++ ++public final class TickData { ++ ++ private final long interval; // ns ++ ++ private final ArrayDeque timeData = new ArrayDeque<>(); ++ ++ public TickData(final long intervalNS) { ++ this.interval = intervalNS; ++ } ++ ++ public void addDataFrom(final TickRegionScheduler.TickTime time) { ++ final long start = time.tickStart(); ++ ++ TickRegionScheduler.TickTime first; ++ while ((first = this.timeData.peekFirst()) != null) { ++ // only remove data completely out of window ++ if ((start - first.tickEnd()) <= this.interval) { ++ break; ++ } ++ this.timeData.pollFirst(); ++ } ++ ++ this.timeData.add(time); ++ } ++ ++ // fromIndex inclusive, toIndex exclusive ++ // will throw if arr.length == 0 ++ private static double median(final long[] arr, final int fromIndex, final int toIndex) { ++ final int len = toIndex - fromIndex; ++ final int middle = fromIndex + (len >>> 1); ++ if ((len & 1) == 0) { ++ // even, average the two middle points ++ return (double)(arr[middle - 1] + arr[middle]) / 2.0; ++ } else { ++ // odd, just grab the middle ++ return (double)arr[middle]; ++ } ++ } ++ ++ // will throw if arr.length == 0 ++ private static SegmentData computeSegmentData(final long[] arr, final int fromIndex, final int toIndex, ++ final boolean inverse) { ++ final int len = toIndex - fromIndex; ++ long sum = 0L; ++ final double median = median(arr, fromIndex, toIndex); ++ long min = arr[0]; ++ long max = arr[0]; ++ ++ for (int i = fromIndex; i < toIndex; ++i) { ++ final long val = arr[i]; ++ sum += val; ++ if (val < min) { ++ min = val; ++ } ++ if (val > max) { ++ max = val; ++ } ++ } ++ ++ if (inverse) { ++ // for positive a,b we have that a >= b if and only if 1/a <= 1/b ++ return new SegmentData( ++ len, ++ (double)len / ((double)sum / 1.0E9), ++ 1.0E9 / median, ++ 1.0E9 / (double)max, ++ 1.0E9 / (double)min ++ ); ++ } else { ++ return new SegmentData( ++ len, ++ (double)sum / (double)len, ++ median, ++ (double)min, ++ (double)max ++ ); ++ } ++ } ++ ++ private static SegmentedAverage computeSegmentedAverage(final long[] data, final int allStart, final int allEnd, ++ final int percent99BestStart, final int percent99BestEnd, ++ final int percent95BestStart, final int percent95BestEnd, ++ final int percent1WorstStart, final int percent1WorstEnd, ++ final int percent5WorstStart, final int percent5WorstEnd, ++ final boolean inverse) { ++ return new SegmentedAverage( ++ computeSegmentData(data, allStart, allEnd, inverse), ++ computeSegmentData(data, percent99BestStart, percent99BestEnd, inverse), ++ computeSegmentData(data, percent95BestStart, percent95BestEnd, inverse), ++ computeSegmentData(data, percent1WorstStart, percent1WorstEnd, inverse), ++ computeSegmentData(data, percent5WorstStart, percent5WorstEnd, inverse) ++ ); ++ } ++ ++ private static record TickInformation( ++ long differenceFromLastTick, ++ long tickTime, ++ long tickTimeCPU ++ ) {} ++ ++ // rets null if there is no data ++ public TickReportData generateTickReport(final TickRegionScheduler.TickTime inProgress, final long endTime) { ++ if (this.timeData.isEmpty() && inProgress == null) { ++ return null; ++ } ++ ++ final List allData = new ArrayList<>(this.timeData); ++ if (inProgress != null) { ++ allData.add(inProgress); ++ } ++ ++ final long intervalStart = allData.get(0).tickStart(); ++ final long intervalEnd = allData.get(allData.size() - 1).tickEnd(); ++ ++ // to make utilisation accurate, we need to take the total time used over the last interval period - ++ // this means if a tick start before the measurement interval, but ends within the interval, then we ++ // only consider the time it spent ticking inside the interval ++ long totalTimeOverInterval = 0L; ++ long measureStart = endTime - this.interval; ++ ++ for (int i = 0, len = allData.size(); i < len; ++i) { ++ final TickRegionScheduler.TickTime time = allData.get(i); ++ if (TimeUtil.compareTimes(time.tickStart(), measureStart) < 0) { ++ final long diff = time.tickEnd() - measureStart; ++ if (diff > 0L) { ++ totalTimeOverInterval += diff; ++ } // else: the time is entirely out of interval ++ } else { ++ totalTimeOverInterval += time.tickLength(); ++ } ++ } ++ ++ // we only care about ticks, but because of inbetween tick task execution ++ // there will be data in allData that isn't ticks. But, that data cannot ++ // be ignored since it contributes to utilisation. ++ // So, we will "compact" the data by merging any inbetween tick times ++ // the next tick. ++ // If there is no "next tick", then we will create one. ++ final List collapsedData = new ArrayList<>(); ++ for (int i = 0, len = allData.size(); i < len; ++i) { ++ final List toCollapse = new ArrayList<>(); ++ TickRegionScheduler.TickTime lastTick = null; ++ for (;i < len; ++i) { ++ final TickRegionScheduler.TickTime time = allData.get(i); ++ if (!time.isTickExecution()) { ++ toCollapse.add(time); ++ continue; ++ } ++ lastTick = time; ++ break; ++ } ++ ++ if (toCollapse.isEmpty()) { ++ // nothing to collapse ++ final TickRegionScheduler.TickTime last = allData.get(i); ++ collapsedData.add( ++ new TickInformation( ++ last.differenceFromLastTick(), ++ last.tickLength(), ++ last.supportCPUTime() ? last.tickCpuTime() : 0L ++ ) ++ ); ++ } else { ++ long totalTickTime = 0L; ++ long totalCpuTime = 0L; ++ for (int k = 0, len2 = collapsedData.size(); k < len2; ++k) { ++ final TickRegionScheduler.TickTime time = toCollapse.get(k); ++ totalTickTime += time.tickLength(); ++ totalCpuTime += time.supportCPUTime() ? time.tickCpuTime() : 0L; ++ } ++ if (i < len) { ++ // we know there is a tick to collapse into ++ final TickRegionScheduler.TickTime last = allData.get(i); ++ collapsedData.add( ++ new TickInformation( ++ last.differenceFromLastTick(), ++ last.tickLength() + totalTickTime, ++ (last.supportCPUTime() ? last.tickCpuTime() : 0L) + totalCpuTime ++ ) ++ ); ++ } else { ++ // we do not have a tick to collapse into, so we must make one up ++ // we will assume that the tick is "starting now" and ongoing ++ ++ // compute difference between imaginary tick and last tick ++ final long differenceBetweenTicks; ++ if (lastTick != null) { ++ // we have a last tick, use it ++ differenceBetweenTicks = lastTick.tickStart(); ++ } else { ++ // we don't have a last tick, so we must make one up that makes sense ++ // if the current interval exceeds the max tick time, then use it ++ ++ // Otherwise use the interval length. ++ // This is how differenceFromLastTick() works on TickTime when there is no previous interval. ++ differenceBetweenTicks = Math.max( ++ TickRegionScheduler.TIME_BETWEEN_TICKS, totalTickTime ++ ); ++ } ++ ++ collapsedData.add( ++ new TickInformation( ++ differenceBetweenTicks, ++ totalTickTime, ++ totalCpuTime ++ ) ++ ); ++ } ++ } ++ } ++ ++ ++ final int collectedTicks = collapsedData.size(); ++ final long[] tickStartToStartDifferences = new long[collectedTicks]; ++ final long[] timePerTickDataRaw = new long[collectedTicks]; ++ final long[] missingCPUTimeDataRaw = new long[collectedTicks]; ++ ++ long totalTimeTicking = 0L; ++ ++ int i = 0; ++ for (final TickInformation time : collapsedData) { ++ tickStartToStartDifferences[i] = time.differenceFromLastTick(); ++ final long timePerTick = timePerTickDataRaw[i] = time.tickTime(); ++ missingCPUTimeDataRaw[i] = Math.max(0L, timePerTick - time.tickTimeCPU()); ++ ++ ++i; ++ ++ totalTimeTicking += timePerTick; ++ } ++ ++ Arrays.sort(tickStartToStartDifferences); ++ Arrays.sort(timePerTickDataRaw); ++ Arrays.sort(missingCPUTimeDataRaw); ++ ++ // Note: computeSegmentData cannot take start == end ++ final int allStart = 0; ++ final int allEnd = collectedTicks; ++ final int percent95BestStart = 0; ++ final int percent95BestEnd = collectedTicks == 1 ? 1 : (int)(0.95 * collectedTicks); ++ final int percent99BestStart = 0; ++ // (int)(0.99 * collectedTicks) == 0 if collectedTicks = 1, so we need to use 1 to avoid start == end ++ final int percent99BestEnd = collectedTicks == 1 ? 1 : (int)(0.99 * collectedTicks); ++ final int percent1WorstStart = (int)(0.99 * collectedTicks); ++ final int percent1WorstEnd = collectedTicks; ++ final int percent5WorstStart = (int)(0.95 * collectedTicks); ++ final int percent5WorstEnd = collectedTicks; ++ ++ final SegmentedAverage tpsData = computeSegmentedAverage( ++ tickStartToStartDifferences, ++ allStart, allEnd, ++ percent99BestStart, percent99BestEnd, ++ percent95BestStart, percent95BestEnd, ++ percent1WorstStart, percent1WorstEnd, ++ percent5WorstStart, percent5WorstEnd, ++ true ++ ); ++ ++ final SegmentedAverage timePerTickData = computeSegmentedAverage( ++ timePerTickDataRaw, ++ allStart, allEnd, ++ percent99BestStart, percent99BestEnd, ++ percent95BestStart, percent95BestEnd, ++ percent1WorstStart, percent1WorstEnd, ++ percent5WorstStart, percent5WorstEnd, ++ false ++ ); ++ ++ final SegmentedAverage missingCPUTimeData = computeSegmentedAverage( ++ missingCPUTimeDataRaw, ++ allStart, allEnd, ++ percent99BestStart, percent99BestEnd, ++ percent95BestStart, percent95BestEnd, ++ percent1WorstStart, percent1WorstEnd, ++ percent5WorstStart, percent5WorstEnd, ++ false ++ ); ++ ++ final double utilisation = (double)totalTimeOverInterval / (double)this.interval; ++ ++ return new TickReportData( ++ collectedTicks, ++ intervalStart, ++ intervalEnd, ++ totalTimeTicking, ++ utilisation, ++ ++ tpsData, ++ timePerTickData, ++ missingCPUTimeData ++ ); ++ } ++ ++ public static final record TickReportData( ++ int collectedTicks, ++ long collectedTickIntervalStart, ++ long collectedTickIntervalEnd, ++ long totalTimeTicking, ++ double utilisation, ++ ++ SegmentedAverage tpsData, ++ // in ns ++ SegmentedAverage timePerTickData, ++ // in ns ++ SegmentedAverage missingCPUTimeData ++ ) {} ++ ++ public static final record SegmentedAverage( ++ SegmentData segmentAll, ++ SegmentData segment99PercentBest, ++ SegmentData segment95PercentBest, ++ SegmentData segment5PercentWorst, ++ SegmentData segment1PercentWorst ++ ) {} ++ ++ public static final record SegmentData( ++ int count, ++ double average, ++ double median, ++ double least, ++ double greatest ++ ) {} ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegionScheduler.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegionScheduler.java.patch new file mode 100644 index 0000000..8d1f3f9 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegionScheduler.java.patch @@ -0,0 +1,567 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java +@@ -1,0 +_,564 @@ ++package io.papermc.paper.threadedregions; ++ ++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import ca.spottedleaf.moonrise.common.util.TickThread; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.util.TraceUtil; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import org.slf4j.Logger; ++import java.lang.management.ManagementFactory; ++import java.lang.management.ThreadMXBean; ++import java.util.concurrent.ThreadFactory; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicBoolean; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.BooleanSupplier; ++ ++public final class TickRegionScheduler { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static final ThreadMXBean THREAD_MX_BEAN = ManagementFactory.getThreadMXBean(); ++ private static final boolean MEASURE_CPU_TIME; ++ static { ++ MEASURE_CPU_TIME = THREAD_MX_BEAN.isThreadCpuTimeSupported(); ++ if (MEASURE_CPU_TIME) { ++ THREAD_MX_BEAN.setThreadCpuTimeEnabled(true); ++ } else { ++ LOGGER.warn("TickRegionScheduler CPU time measurement is not available"); ++ } ++ } ++ ++ public static final int TICK_RATE = 20; ++ public static final long TIME_BETWEEN_TICKS = 1_000_000_000L / TICK_RATE; // ns ++ ++ private final SchedulerThreadPool scheduler; ++ ++ public TickRegionScheduler(final int threads) { ++ this.scheduler = new SchedulerThreadPool(threads, new ThreadFactory() { ++ private final AtomicInteger idGenerator = new AtomicInteger(); ++ ++ @Override ++ public Thread newThread(final Runnable run) { ++ final Thread ret = new TickThreadRunner(run, "Region Scheduler Thread #" + this.idGenerator.getAndIncrement()); ++ ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException); ++ return ret; ++ } ++ }); ++ } ++ ++ public int getTotalThreadCount() { ++ return this.scheduler.getThreads().length; ++ } ++ ++ private static void setTickingRegion(final ThreadedRegionizer.ThreadedRegion region) { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ throw new IllegalStateException("Must be tick thread runner"); ++ } ++ if (region != null && tickThreadRunner.currentTickingRegion != null) { ++ throw new IllegalStateException("Trying to double set ticking region!"); ++ } ++ if (region == null && tickThreadRunner.currentTickingRegion == null) { ++ throw new IllegalStateException("Trying to double unset ticking region!"); ++ } ++ tickThreadRunner.currentTickingRegion = region; ++ if (region != null) { ++ tickThreadRunner.currentTickingWorldRegionizedData = region.regioniser.world.worldRegionData.get(); ++ } else { ++ tickThreadRunner.currentTickingWorldRegionizedData = null; ++ } ++ } ++ ++ private static void setTickTask(final SchedulerThreadPool.SchedulableTick task) { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ throw new IllegalStateException("Must be tick thread runner"); ++ } ++ if (task != null && tickThreadRunner.currentTickingTask != null) { ++ throw new IllegalStateException("Trying to double set ticking task!"); ++ } ++ if (task == null && tickThreadRunner.currentTickingTask == null) { ++ throw new IllegalStateException("Trying to double unset ticking task!"); ++ } ++ tickThreadRunner.currentTickingTask = task; ++ } ++ ++ /** ++ * Returns the current ticking region, or {@code null} if there is no ticking region. ++ * If this thread is not a TickThread, then returns {@code null}. ++ */ ++ public static ThreadedRegionizer.ThreadedRegion getCurrentRegion() { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ return RegionShutdownThread.getRegion(); ++ } ++ return tickThreadRunner.currentTickingRegion; ++ } ++ ++ /** ++ * Returns the current ticking region's world regionised data, or {@code null} if there is no ticking region. ++ * This is a faster alternative to calling the {@link RegionizedData#get()} method. ++ * If this thread is not a TickThread, then returns {@code null}. ++ */ ++ public static RegionizedWorldData getCurrentRegionizedWorldData() { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ return RegionShutdownThread.getWorldData(); ++ } ++ return tickThreadRunner.currentTickingWorldRegionizedData; ++ } ++ ++ /** ++ * Returns the current ticking task, or {@code null} if there is no ticking region. ++ * If this thread is not a TickThread, then returns {@code null}. ++ */ ++ public static SchedulerThreadPool.SchedulableTick getCurrentTickingTask() { ++ final Thread currThread = Thread.currentThread(); ++ if (!(currThread instanceof TickThreadRunner tickThreadRunner)) { ++ return null; ++ } ++ return tickThreadRunner.currentTickingTask; ++ } ++ ++ /** ++ * Schedules the given region ++ * @throws IllegalStateException If the region is already scheduled or is ticking ++ */ ++ public void scheduleRegion(final RegionScheduleHandle region) { ++ region.scheduler = this; ++ this.scheduler.schedule(region); ++ } ++ ++ /** ++ * Attempts to de-schedule the provided region. If the current region cannot be cancelled for its next tick or task ++ * execution, then it will be cancelled after. ++ */ ++ public void descheduleRegion(final RegionScheduleHandle region) { ++ // To avoid acquiring any of the locks the scheduler may be using, we ++ // simply cancel the next action. ++ region.markNonSchedulable(); ++ } ++ ++ /** ++ * Updates the tick start to the farthest into the future of its current scheduled time and the ++ * provided time. ++ * @return {@code false} if the region was not scheduled or is currently ticking or the specified time is less-than its ++ * current start time, {@code true} if the next tick start was adjusted. ++ */ ++ public boolean updateTickStartToMax(final RegionScheduleHandle region, final long newStart) { ++ return this.scheduler.updateTickStartToMax(region, newStart); ++ } ++ ++ public boolean halt(final boolean sync, final long maxWaitNS) { ++ return this.scheduler.halt(sync, maxWaitNS); ++ } ++ ++ void dumpAliveThreadTraces(final String reason) { ++ for (final Thread thread : this.scheduler.getThreads()) { ++ if (thread.isAlive()) { ++ TraceUtil.dumpTraceForThread(thread, reason); ++ } ++ } ++ } ++ ++ public void setHasTasks(final RegionScheduleHandle region) { ++ this.scheduler.notifyTasks(region); ++ } ++ ++ public void init() { ++ this.scheduler.start(); ++ } ++ ++ private void uncaughtException(final Thread thread, final Throwable thr) { ++ LOGGER.error("Uncaught exception in tick thread \"" + thread.getName() + "\"", thr); ++ ++ // prevent further ticks from occurring ++ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD ++ this.scheduler.halt(false, 0L); ++ ++ MinecraftServer.getServer().stopServer(); ++ } ++ ++ private void regionFailed(final RegionScheduleHandle handle, final boolean executingTasks, final Throwable thr) { ++ // when a region fails, we need to shut down the server gracefully ++ ++ // prevent further ticks from occurring ++ // we CANNOT sync, because WE ARE ON A SCHEDULER THREAD ++ this.scheduler.halt(false, 0L); ++ ++ final ChunkPos center = handle.region == null ? null : handle.region.region.getCenterChunk(); ++ final ServerLevel world = handle.region == null ? null : handle.region.world; ++ ++ LOGGER.error("Region #" + (handle.region == null ? -1L : handle.region.id) + " centered at chunk " + center + " in world '" + (world == null ? "null" : world.getWorld().getName()) + "' failed to " + (executingTasks ? "execute tasks" : "tick") + ":", thr); ++ ++ MinecraftServer.getServer().stopServer(); ++ } ++ ++ // By using our own thread object, we can use a field for the current region rather than a ThreadLocal. ++ // This is much faster than a thread local, since the thread local has to use a map lookup. ++ private static final class TickThreadRunner extends TickThread { ++ ++ private ThreadedRegionizer.ThreadedRegion currentTickingRegion; ++ private RegionizedWorldData currentTickingWorldRegionizedData; ++ private SchedulerThreadPool.SchedulableTick currentTickingTask; ++ ++ public TickThreadRunner(final Runnable run, final String name) { ++ super(run, name); ++ } ++ } ++ ++ public static abstract class RegionScheduleHandle extends SchedulerThreadPool.SchedulableTick { ++ ++ protected long currentTick; ++ protected long lastTickStart; ++ ++ protected final TickData tickTimes5s; ++ protected final TickData tickTimes15s; ++ protected final TickData tickTimes1m; ++ protected final TickData tickTimes5m; ++ protected final TickData tickTimes15m; ++ protected TickTime currentTickData; ++ protected Thread currentTickingThread; ++ ++ public final TickRegions.TickRegionData region; ++ private final AtomicBoolean cancelled = new AtomicBoolean(); ++ ++ protected final Schedule tickSchedule; ++ ++ private TickRegionScheduler scheduler; ++ ++ public RegionScheduleHandle(final TickRegions.TickRegionData region, final long firstStart) { ++ this.currentTick = 0L; ++ this.lastTickStart = SchedulerThreadPool.DEADLINE_NOT_SET; ++ this.tickTimes5s = new TickData(TimeUnit.SECONDS.toNanos(5L)); ++ this.tickTimes15s = new TickData(TimeUnit.SECONDS.toNanos(15L)); ++ this.tickTimes1m = new TickData(TimeUnit.MINUTES.toNanos(1L)); ++ this.tickTimes5m = new TickData(TimeUnit.MINUTES.toNanos(5L)); ++ this.tickTimes15m = new TickData(TimeUnit.MINUTES.toNanos(15L)); ++ this.region = region; ++ ++ this.setScheduledStart(firstStart); ++ this.tickSchedule = new Schedule(firstStart == SchedulerThreadPool.DEADLINE_NOT_SET ? firstStart : firstStart - TIME_BETWEEN_TICKS); ++ } ++ ++ /** ++ * Subclasses should call this instead of {@link ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool.SchedulableTick#setScheduledStart(long)} ++ * so that the tick schedule and scheduled start remain synchronised ++ */ ++ protected final void updateScheduledStart(final long to) { ++ this.setScheduledStart(to); ++ this.tickSchedule.setLastPeriod(to == SchedulerThreadPool.DEADLINE_NOT_SET ? to : to - TIME_BETWEEN_TICKS); ++ } ++ ++ public final void markNonSchedulable() { ++ this.cancelled.set(true); ++ } ++ ++ public final boolean isMarkedAsNonSchedulable() { ++ return this.cancelled.get(); ++ } ++ ++ protected abstract boolean tryMarkTicking(); ++ ++ protected abstract boolean markNotTicking(); ++ ++ protected abstract void tickRegion(final int tickCount, final long startTime, final long scheduledEnd); ++ ++ protected abstract boolean runRegionTasks(final BooleanSupplier canContinue); ++ ++ protected abstract boolean hasIntermediateTasks(); ++ ++ @Override ++ public final boolean hasTasks() { ++ return this.hasIntermediateTasks(); ++ } ++ ++ @Override ++ public final Boolean runTasks(final BooleanSupplier canContinue) { ++ if (this.cancelled.get()) { ++ return null; ++ } ++ ++ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ final long tickStart = System.nanoTime(); ++ ++ if (!this.tryMarkTicking()) { ++ if (!this.cancelled.get()) { ++ throw new IllegalStateException("Scheduled region should be acquirable"); ++ } ++ // region was killed ++ return null; ++ } ++ ++ TickRegionScheduler.setTickTask(this); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(this.region.region); ++ } ++ ++ synchronized (this) { ++ this.currentTickData = new TickTime( ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, tickStart, cpuStart, ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, ++ false ++ ); ++ this.currentTickingThread = Thread.currentThread(); ++ } ++ ++ final boolean ret; ++ try { ++ ret = this.runRegionTasks(() -> { ++ return !RegionScheduleHandle.this.cancelled.get() && canContinue.getAsBoolean(); ++ }); ++ } catch (final Throwable thr) { ++ this.scheduler.regionFailed(this, true, thr); ++ // don't release region for another tick ++ return null; ++ } finally { ++ final long tickEnd = System.nanoTime(); ++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ ++ final TickTime time = new TickTime( ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, ++ tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, false ++ ); ++ ++ this.addTickTime(time); ++ TickRegionScheduler.setTickTask(null); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(null); ++ } ++ } ++ ++ return !this.markNotTicking() || this.cancelled.get() ? null : Boolean.valueOf(ret); ++ } ++ ++ @Override ++ public final boolean runTick() { ++ // Remember, we are supposed use setScheduledStart if we return true here, otherwise ++ // the scheduler will try to schedule for the same time. ++ if (this.cancelled.get()) { ++ return false; ++ } ++ ++ final long cpuStart = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ final long tickStart = System.nanoTime(); ++ ++ // use max(), don't assume that tickStart >= scheduledStart ++ final int tickCount = Math.max(1, this.tickSchedule.getPeriodsAhead(TIME_BETWEEN_TICKS, tickStart)); ++ ++ if (!this.tryMarkTicking()) { ++ if (!this.cancelled.get()) { ++ throw new IllegalStateException("Scheduled region should be acquirable"); ++ } ++ // region was killed ++ return false; ++ } ++ if (this.cancelled.get()) { ++ this.markNotTicking(); ++ // region should be killed ++ return false; ++ } ++ ++ TickRegionScheduler.setTickTask(this); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(this.region.region); ++ } ++ this.incrementTickCount(); ++ final long lastTickStart = this.lastTickStart; ++ this.lastTickStart = tickStart; ++ ++ final long scheduledStart = this.getScheduledStart(); ++ final long scheduledEnd = scheduledStart + TIME_BETWEEN_TICKS; ++ ++ synchronized (this) { ++ this.currentTickData = new TickTime( ++ lastTickStart, scheduledStart, tickStart, cpuStart, ++ SchedulerThreadPool.DEADLINE_NOT_SET, SchedulerThreadPool.DEADLINE_NOT_SET, MEASURE_CPU_TIME, ++ true ++ ); ++ this.currentTickingThread = Thread.currentThread(); ++ } ++ ++ try { ++ // next start isn't updated until the end of this tick ++ this.tickRegion(tickCount, tickStart, scheduledEnd); ++ } catch (final Throwable thr) { ++ this.scheduler.regionFailed(this, false, thr); ++ // regionFailed will schedule a shutdown, so we should avoid letting this region tick further ++ return false; ++ } finally { ++ final long tickEnd = System.nanoTime(); ++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getCurrentThreadCpuTime() : 0L; ++ ++ // in order to ensure all regions get their chance at scheduling, we have to ensure that regions ++ // that exceed the max tick time are not always prioritised over everything else. Thus, we use the greatest ++ // of the current time and "ideal" next tick start. ++ this.tickSchedule.advanceBy(tickCount, TIME_BETWEEN_TICKS); ++ this.setScheduledStart(TimeUtil.getGreatestTime(tickEnd, this.tickSchedule.getDeadline(TIME_BETWEEN_TICKS))); ++ ++ final TickTime time = new TickTime( ++ lastTickStart, scheduledStart, tickStart, cpuStart, tickEnd, cpuEnd, MEASURE_CPU_TIME, true ++ ); ++ ++ this.addTickTime(time); ++ TickRegionScheduler.setTickTask(null); ++ if (this.region != null) { ++ TickRegionScheduler.setTickingRegion(null); ++ } ++ } ++ ++ // Only AFTER updating the tickStart ++ return this.markNotTicking() && !this.cancelled.get(); ++ } ++ ++ /** ++ * Only safe to call if this tick data matches the current ticking region. ++ */ ++ protected void addTickTime(final TickTime time) { ++ synchronized (this) { ++ this.currentTickData = null; ++ this.currentTickingThread = null; ++ this.tickTimes5s.addDataFrom(time); ++ this.tickTimes15s.addDataFrom(time); ++ this.tickTimes1m.addDataFrom(time); ++ this.tickTimes5m.addDataFrom(time); ++ this.tickTimes15m.addDataFrom(time); ++ } ++ } ++ ++ private TickTime adjustCurrentTickData(final long tickEnd) { ++ final TickTime currentTickData = this.currentTickData; ++ if (currentTickData == null) { ++ return null; ++ } ++ ++ final long cpuEnd = MEASURE_CPU_TIME ? THREAD_MX_BEAN.getThreadCpuTime(this.currentTickingThread.getId()) : 0L; ++ ++ return new TickTime( ++ currentTickData.previousTickStart(), currentTickData.scheduledTickStart(), ++ currentTickData.tickStart(), currentTickData.tickStartCPU(), ++ tickEnd, cpuEnd, ++ MEASURE_CPU_TIME, currentTickData.isTickExecution() ++ ); ++ } ++ ++ public final TickData.TickReportData getTickReport5s(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes5s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport15s(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes15s.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport1m(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes1m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport5m(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes5m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ public final TickData.TickReportData getTickReport15m(final long currTime) { ++ synchronized (this) { ++ return this.tickTimes15m.generateTickReport(this.adjustCurrentTickData(currTime), currTime); ++ } ++ } ++ ++ /** ++ * Only safe to call if this tick data matches the current ticking region. ++ */ ++ private void incrementTickCount() { ++ ++this.currentTick; ++ } ++ ++ /** ++ * Only safe to call if this tick data matches the current ticking region. ++ */ ++ public final long getCurrentTick() { ++ return this.currentTick; ++ } ++ ++ protected final void setCurrentTick(final long value) { ++ this.currentTick = value; ++ } ++ } ++ ++ // All time units are in nanoseconds. ++ public static final record TickTime( ++ long previousTickStart, ++ long scheduledTickStart, ++ long tickStart, ++ long tickStartCPU, ++ long tickEnd, ++ long tickEndCPU, ++ boolean supportCPUTime, ++ boolean isTickExecution ++ ) { ++ /** ++ * The difference between the start tick time and the scheduled start tick time. This value is ++ * < 0 if the tick started before the scheduled tick time. ++ * Only valid when {@link #isTickExecution()} is {@code true}. ++ */ ++ public final long startOvershoot() { ++ return this.tickStart - this.scheduledTickStart; ++ } ++ ++ /** ++ * The difference from the end tick time and the start tick time. Always >= 0 (unless nanoTime is just wrong). ++ */ ++ public final long tickLength() { ++ return this.tickEnd - this.tickStart; ++ } ++ ++ /** ++ * The total CPU time from the start tick time to the end tick time. Generally should be equal to the tickLength, ++ * unless there is CPU starvation or the tick thread was blocked by I/O or other tasks. Returns Long.MIN_VALUE ++ * if CPU time measurement is not supported. ++ */ ++ public final long tickCpuTime() { ++ if (!this.supportCPUTime()) { ++ return Long.MIN_VALUE; ++ } ++ return this.tickEndCPU - this.tickStartCPU; ++ } ++ ++ /** ++ * The difference in time from the start of the last tick to the start of the current tick. If there is no ++ * last tick, then this value is max(TIME_BETWEEN_TICKS, tickLength). ++ * Only valid when {@link #isTickExecution()} is {@code true}. ++ */ ++ public final long differenceFromLastTick() { ++ if (this.hasLastTick()) { ++ return this.tickStart - this.previousTickStart; ++ } ++ return Math.max(TIME_BETWEEN_TICKS, this.tickLength()); ++ } ++ ++ /** ++ * Returns whether there was a tick that occurred before this one. ++ * Only valid when {@link #isTickExecution()} is {@code true}. ++ */ ++ public boolean hasLastTick() { ++ return this.previousTickStart != SchedulerThreadPool.DEADLINE_NOT_SET; ++ } ++ ++ /* ++ * Remember, this is the expected behavior of the following: ++ * ++ * MSPT: Time per tick. This does not include overshoot time, just the tickLength(). ++ * ++ * TPS: The number of ticks per second. It should be ticks / (sum of differenceFromLastTick). ++ */ ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegions.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegions.java.patch new file mode 100644 index 0000000..a68ae79 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/TickRegions.java.patch @@ -0,0 +1,416 @@ +--- a/io/papermc/paper/threadedregions/TickRegions.java ++++ b/io/papermc/paper/threadedregions/TickRegions.java +@@ -1,10 +_,410 @@ + package io.papermc.paper.threadedregions; + +-// placeholder class for Folia +-public class TickRegions { ++import ca.spottedleaf.concurrentutil.scheduler.SchedulerThreadPool; ++import ca.spottedleaf.concurrentutil.util.TimeUtil; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; ++import com.mojang.logging.LogUtils; ++import io.papermc.paper.configuration.GlobalConfiguration; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; ++import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerLevel; ++import org.slf4j.Logger; ++import java.util.Iterator; ++import java.util.concurrent.TimeUnit; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.concurrent.atomic.AtomicLong; ++import java.util.function.BooleanSupplier; ++ ++public final class TickRegions implements ThreadedRegionizer.RegionCallbacks { ++ ++ private static final Logger LOGGER = LogUtils.getLogger(); ++ private static int regionShift = 31; + + public static int getRegionChunkShift() { +- return ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ThreadedTicketLevelPropagator.SECTION_SHIFT; ++ return regionShift; ++ } ++ ++ private static boolean initialised; ++ private static TickRegionScheduler scheduler; ++ ++ public static TickRegionScheduler getScheduler() { ++ return scheduler; ++ } ++ ++ public static void init(final GlobalConfiguration.ThreadedRegions config) { ++ if (initialised) { ++ return; ++ } ++ initialised = true; ++ int gridExponent = config.gridExponent; ++ gridExponent = Math.max(0, gridExponent); ++ gridExponent = Math.min(31, gridExponent); ++ regionShift = gridExponent; ++ ++ int tickThreads; ++ if (config.threads <= 0) { ++ tickThreads = Runtime.getRuntime().availableProcessors() / 2; ++ if (tickThreads <= 4) { ++ tickThreads = 1; ++ } else { ++ tickThreads = tickThreads / 4; ++ } ++ } else { ++ tickThreads = config.threads; ++ } ++ ++ scheduler = new TickRegionScheduler(tickThreads); ++ LOGGER.info("Regionised ticking is enabled with " + tickThreads + " tick threads"); ++ } ++ ++ @Override ++ public TickRegionData createNewData(final ThreadedRegionizer.ThreadedRegion region) { ++ return new TickRegionData(region); ++ } ++ ++ @Override ++ public TickRegionSectionData createNewSectionData(final int sectionX, final int sectionZ, final int sectionShift) { ++ return null; ++ } ++ ++ @Override ++ public void onRegionCreate(final ThreadedRegionizer.ThreadedRegion region) { ++ final TickRegionData data = region.getData(); ++ // post-region merge/split regioninfo update ++ data.getRegionStats().updateFrom(data.getOrCreateRegionizedData(data.world.worldRegionData)); ++ } ++ ++ @Override ++ public void onRegionDestroy(final ThreadedRegionizer.ThreadedRegion region) { ++ // nothing for now ++ } ++ ++ @Override ++ public void onRegionActive(final ThreadedRegionizer.ThreadedRegion region) { ++ final TickRegionData data = region.getData(); ++ ++ data.tickHandle.checkInitialSchedule(); ++ scheduler.scheduleRegion(data.tickHandle); ++ } ++ ++ @Override ++ public void onRegionInactive(final ThreadedRegionizer.ThreadedRegion region) { ++ final TickRegionData data = region.getData(); ++ ++ scheduler.descheduleRegion(data.tickHandle); ++ // old handle cannot be scheduled anymore, copy to a new handle ++ data.tickHandle = data.tickHandle.copy(); ++ } ++ ++ @Override ++ public void preMerge(final ThreadedRegionizer.ThreadedRegion from, ++ final ThreadedRegionizer.ThreadedRegion into) { ++ ++ } ++ ++ @Override ++ public void preSplit(final ThreadedRegionizer.ThreadedRegion from, ++ final java.util.List> into) { ++ ++ } ++ ++ public static final class TickRegionSectionData implements ThreadedRegionizer.ThreadedRegionSectionData {} ++ ++ public static final class RegionStats { ++ ++ private final AtomicInteger entityCount = new AtomicInteger(); ++ private final AtomicInteger playerCount = new AtomicInteger(); ++ private final AtomicInteger chunkCount = new AtomicInteger(); ++ ++ public int getEntityCount() { ++ return this.entityCount.get(); ++ } ++ ++ public int getPlayerCount() { ++ return this.playerCount.get(); ++ } ++ ++ public int getChunkCount() { ++ return this.chunkCount.get(); ++ } ++ ++ void updateFrom(final RegionizedWorldData data) { ++ this.entityCount.setRelease(data == null ? 0 : data.getEntityCount()); ++ this.playerCount.setRelease(data == null ? 0 : data.getPlayerCount()); ++ this.chunkCount.setRelease(data == null ? 0 : data.getChunkCount()); ++ } ++ ++ static void updateCurrentRegion() { ++ TickRegionScheduler.getCurrentRegion().getData().getRegionStats().updateFrom(TickRegionScheduler.getCurrentRegionizedWorldData()); ++ } ++ } ++ ++ public static final class TickRegionData implements ThreadedRegionizer.ThreadedRegionData { ++ ++ private static final AtomicLong ID_GENERATOR = new AtomicLong(); ++ /** Never 0L, since 0L is reserved for global region. */ ++ public final long id = ID_GENERATOR.incrementAndGet(); ++ ++ public final ThreadedRegionizer.ThreadedRegion region; ++ public final ServerLevel world; ++ ++ // generic regionised data ++ private final Reference2ReferenceOpenHashMap, Object> regionizedData = new Reference2ReferenceOpenHashMap<>(); ++ ++ // tick data ++ private ConcreteRegionTickHandle tickHandle = new ConcreteRegionTickHandle(this, SchedulerThreadPool.DEADLINE_NOT_SET); ++ ++ // queue data ++ private final RegionizedTaskQueue.RegionTaskQueueData taskQueueData; ++ ++ // chunk holder manager data ++ private final ChunkHolderManager.HolderManagerRegionData holderManagerRegionData = new ChunkHolderManager.HolderManagerRegionData(); ++ ++ // async-safe read-only region data ++ private final RegionStats regionStats; ++ ++ private TickRegionData(final ThreadedRegionizer.ThreadedRegion region) { ++ this.region = region; ++ this.world = region.regioniser.world; ++ this.taskQueueData = new RegionizedTaskQueue.RegionTaskQueueData(this.world.taskQueueRegionData); ++ this.regionStats = new RegionStats(); ++ } ++ ++ public RegionStats getRegionStats() { ++ return this.regionStats; ++ } ++ ++ public RegionizedTaskQueue.RegionTaskQueueData getTaskQueueData() { ++ return this.taskQueueData; ++ } ++ ++ // the value returned can be invalidated at any time, except when the caller ++ // is ticking this region ++ public TickRegionScheduler.RegionScheduleHandle getRegionSchedulingHandle() { ++ return this.tickHandle; ++ } ++ ++ public long getCurrentTick() { ++ return this.tickHandle.getCurrentTick(); ++ } ++ ++ public ChunkHolderManager.HolderManagerRegionData getHolderManagerRegionData() { ++ return this.holderManagerRegionData; ++ } ++ ++ T getRegionizedData(final RegionizedData regionizedData) { ++ return (T)this.regionizedData.get(regionizedData); ++ } ++ ++ T getOrCreateRegionizedData(final RegionizedData regionizedData) { ++ T ret = (T)this.regionizedData.get(regionizedData); ++ ++ if (ret != null) { ++ return ret; ++ } ++ ++ ret = regionizedData.createNewValue(); ++ this.regionizedData.put(regionizedData, ret); ++ ++ return ret; ++ } ++ ++ @Override ++ public void split(final ThreadedRegionizer regioniser, ++ final Long2ReferenceOpenHashMap> into, ++ final ReferenceOpenHashSet> regions) { ++ final int shift = regioniser.sectionChunkShift; ++ ++ // tick data ++ // note: here it is OK force us to access tick handle, as this region is owned (and thus not scheduled), ++ // and the other regions to split into are not scheduled yet. ++ for (final ThreadedRegionizer.ThreadedRegion region : regions) { ++ final TickRegionData data = region.getData(); ++ data.tickHandle.copyDeadlineAndTickCount(this.tickHandle); ++ } ++ ++ // generic regionised data ++ for (final Iterator, Object>> dataIterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); ++ dataIterator.hasNext();) { ++ final Reference2ReferenceMap.Entry, Object> regionDataEntry = dataIterator.next(); ++ final RegionizedData data = regionDataEntry.getKey(); ++ final Object from = regionDataEntry.getValue(); ++ ++ final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); ++ ++ for (final ThreadedRegionizer.ThreadedRegion region : regions) { ++ dataSet.add(region.getData().getOrCreateRegionizedData(data)); ++ } ++ ++ final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); ++ ++ for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); ++ regionIterator.hasNext();) { ++ final Long2ReferenceMap.Entry> entry = regionIterator.next(); ++ final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); ++ final Object to = region.getData().getOrCreateRegionizedData(data); ++ ++ regionToData.put(entry.getLongKey(), to); ++ } ++ ++ ((RegionizedData)data).getCallback().split(from, shift, regionToData, dataSet); ++ } ++ ++ // chunk holder manager data ++ { ++ final ReferenceOpenHashSet dataSet = new ReferenceOpenHashSet<>(regions.size(), 0.75f); ++ ++ for (final ThreadedRegionizer.ThreadedRegion region : regions) { ++ dataSet.add(region.getData().holderManagerRegionData); ++ } ++ ++ final Long2ReferenceOpenHashMap regionToData = new Long2ReferenceOpenHashMap<>(into.size(), 0.75f); ++ ++ for (final Iterator>> regionIterator = into.long2ReferenceEntrySet().fastIterator(); ++ regionIterator.hasNext();) { ++ final Long2ReferenceMap.Entry> entry = regionIterator.next(); ++ final ThreadedRegionizer.ThreadedRegion region = entry.getValue(); ++ final ChunkHolderManager.HolderManagerRegionData to = region.getData().holderManagerRegionData; ++ ++ regionToData.put(entry.getLongKey(), to); ++ } ++ ++ this.holderManagerRegionData.split(shift, regionToData, dataSet); ++ } ++ ++ // task queue ++ this.taskQueueData.split(regioniser, into); ++ } ++ ++ @Override ++ public void mergeInto(final ThreadedRegionizer.ThreadedRegion into) { ++ // Note: merge target is always a region being released from ticking ++ final TickRegionData data = into.getData(); ++ final long currentTickTo = data.getCurrentTick(); ++ final long currentTickFrom = this.getCurrentTick(); ++ ++ // here we can access tickHandle because the target (into) is the region being released, so it is ++ // not actually scheduled ++ // there's not really a great solution to the tick problem, no matter what it'll be messed up ++ // we will pick the greatest time delay so that tps will not exceed TICK_RATE ++ data.tickHandle.updateSchedulingToMax(this.tickHandle); ++ ++ // generic regionised data ++ final long fromTickOffset = currentTickTo - currentTickFrom; // see merge jd ++ for (final Iterator, Object>> iterator = this.regionizedData.reference2ReferenceEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Reference2ReferenceMap.Entry, Object> entry = iterator.next(); ++ final RegionizedData regionizedData = entry.getKey(); ++ final Object from = entry.getValue(); ++ final Object to = into.getData().getOrCreateRegionizedData(regionizedData); ++ ++ ((RegionizedData)regionizedData).getCallback().merge(from, to, fromTickOffset); ++ } ++ ++ // chunk holder manager data ++ this.holderManagerRegionData.merge(into.getData().holderManagerRegionData, fromTickOffset); ++ ++ // task queue ++ this.taskQueueData.mergeInto(data.taskQueueData); ++ } ++ } ++ ++ private static final class ConcreteRegionTickHandle extends TickRegionScheduler.RegionScheduleHandle { ++ ++ private final TickRegionData region; ++ ++ private ConcreteRegionTickHandle(final TickRegionData region, final long start) { ++ super(region, start); ++ this.region = region; ++ } ++ ++ private ConcreteRegionTickHandle copy() { ++ final ConcreteRegionTickHandle ret = new ConcreteRegionTickHandle(this.region, this.getScheduledStart()); ++ ++ ret.currentTick = this.currentTick; ++ ret.lastTickStart = this.lastTickStart; ++ ret.tickSchedule.setLastPeriod(this.tickSchedule.getLastPeriod()); ++ ++ return ret; ++ } ++ ++ private void updateSchedulingToMax(final ConcreteRegionTickHandle from) { ++ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ return; ++ } ++ ++ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ this.updateScheduledStart(from.getScheduledStart()); ++ return; ++ } ++ ++ this.updateScheduledStart(TimeUtil.getGreatestTime(from.getScheduledStart(), this.getScheduledStart())); ++ } ++ ++ private void copyDeadlineAndTickCount(final ConcreteRegionTickHandle from) { ++ this.currentTick = from.currentTick; ++ ++ if (from.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ return; ++ } ++ ++ this.tickSchedule.setLastPeriod(from.tickSchedule.getLastPeriod()); ++ this.setScheduledStart(from.getScheduledStart()); ++ } ++ ++ private void checkInitialSchedule() { ++ if (this.getScheduledStart() == SchedulerThreadPool.DEADLINE_NOT_SET) { ++ this.updateScheduledStart(System.nanoTime() + TickRegionScheduler.TIME_BETWEEN_TICKS); ++ } ++ } ++ ++ @Override ++ protected boolean tryMarkTicking() { ++ return this.region.region.tryMarkTicking(ConcreteRegionTickHandle.this::isMarkedAsNonSchedulable); ++ } ++ ++ @Override ++ protected boolean markNotTicking() { ++ return this.region.region.markNotTicking(); ++ } ++ ++ @Override ++ protected void tickRegion(final int tickCount, final long startTime, final long scheduledEnd) { ++ MinecraftServer.getServer().tickServer(startTime, scheduledEnd, TimeUnit.MILLISECONDS.toMillis(10L), this.region); ++ } ++ ++ @Override ++ protected boolean runRegionTasks(final BooleanSupplier canContinue) { ++ final RegionizedTaskQueue.RegionTaskQueueData queue = this.region.taskQueueData; ++ ++ boolean processedChunkTask = false; ++ ++ boolean executeChunkTask = true; ++ boolean executeTickTask = true; ++ do { ++ if (executeTickTask) { ++ executeTickTask = queue.executeTickTask(); ++ } ++ if (executeChunkTask) { ++ processedChunkTask |= (executeChunkTask = queue.executeChunkTask()); ++ } ++ } while ((executeChunkTask | executeTickTask) && canContinue.getAsBoolean()); ++ ++ if (processedChunkTask) { ++ // if we processed any chunk tasks, try to process ticket level updates for full status changes ++ this.region.world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); ++ } ++ return true; ++ } ++ ++ @Override ++ protected boolean hasIntermediateTasks() { ++ return this.region.taskQueueData.hasTasks(); ++ } + } + + } diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandServerHealth.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandServerHealth.java.patch new file mode 100644 index 0000000..6600144 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandServerHealth.java.patch @@ -0,0 +1,358 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/commands/CommandServerHealth.java +@@ -1,0 +_,355 @@ ++package io.papermc.paper.threadedregions.commands; ++ ++import io.papermc.paper.threadedregions.RegionizedServer; ++import io.papermc.paper.threadedregions.RegionizedWorldData; ++import io.papermc.paper.threadedregions.ThreadedRegionizer; ++import io.papermc.paper.threadedregions.TickData; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import io.papermc.paper.threadedregions.TickRegions; ++import it.unimi.dsi.fastutil.doubles.DoubleArrayList; ++import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair; ++import net.kyori.adventure.text.Component; ++import net.kyori.adventure.text.TextComponent; ++import net.kyori.adventure.text.event.ClickEvent; ++import net.kyori.adventure.text.event.HoverEvent; ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.text.format.TextDecoration; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.world.level.ChunkPos; ++import org.bukkit.Bukkit; ++import org.bukkit.World; ++import org.bukkit.command.Command; ++import org.bukkit.command.CommandSender; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.entity.Entity; ++import org.bukkit.entity.Player; ++import java.text.DecimalFormat; ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.List; ++import java.util.Locale; ++ ++public final class CommandServerHealth extends Command { ++ ++ private static final ThreadLocal TWO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { ++ return new DecimalFormat("#,##0.00"); ++ }); ++ private static final ThreadLocal ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { ++ return new DecimalFormat("#,##0.0"); ++ }); ++ private static final ThreadLocal NO_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { ++ return new DecimalFormat("#,##0"); ++ }); ++ ++ private static final TextColor HEADER = TextColor.color(79, 164, 240); ++ private static final TextColor PRIMARY = TextColor.color(48, 145, 237); ++ private static final TextColor SECONDARY = TextColor.color(104, 177, 240); ++ private static final TextColor INFORMATION = TextColor.color(145, 198, 243); ++ private static final TextColor LIST = TextColor.color(33, 97, 188); ++ ++ public CommandServerHealth() { ++ super("tps"); ++ this.setUsage("/ [server/region] [lowest regions to display]"); ++ this.setDescription("Reports information about server health."); ++ this.setPermission("bukkit.command.tps"); ++ } ++ ++ private static Component formatRegionInfo(final String prefix, final double util, final double mspt, final double tps, ++ final boolean newline) { ++ return Component.text() ++ .append(Component.text(prefix, PRIMARY, TextDecoration.BOLD)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) ++ .append(Component.text("% util at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) ++ .append(Component.text(" MSPT at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) ++ .append(Component.text(" TPS" + (newline ? "\n" : ""), PRIMARY)) ++ .build(); ++ } ++ ++ private static Component formatRegionStats(final TickRegions.RegionStats stats, final boolean newline) { ++ return Component.text() ++ .append(Component.text("Chunks: ", PRIMARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getChunkCount()), INFORMATION)) ++ .append(Component.text(" Players: ", PRIMARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getPlayerCount()), INFORMATION)) ++ .append(Component.text(" Entities: ", PRIMARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format((long)stats.getEntityCount()) + (newline ? "\n" : ""), INFORMATION)) ++ .build(); ++ } ++ ++ private static boolean executeRegion(final CommandSender sender, final String commandLabel, final String[] args) { ++ final ThreadedRegionizer.ThreadedRegion region = ++ TickRegionScheduler.getCurrentRegion(); ++ if (region == null) { ++ sender.sendMessage(Component.text("You are not in a region currently", NamedTextColor.RED)); ++ return true; ++ } ++ ++ final long currTime = System.nanoTime(); ++ ++ final TickData.TickReportData report15s = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); ++ final TickData.TickReportData report1m = region.getData().getRegionSchedulingHandle().getTickReport1m(currTime); ++ ++ final ServerLevel world = region.regioniser.world; ++ final ChunkPos chunkCenter = region.getCenterChunk(); ++ final int centerBlockX = ((chunkCenter.x << 4) | 7); ++ final int centerBlockZ = ((chunkCenter.z << 4) | 7); ++ ++ final double util15s = report15s.utilisation(); ++ final double tps15s = report15s.tpsData().segmentAll().average(); ++ final double mspt15s = report15s.timePerTickData().segmentAll().average() / 1.0E6; ++ ++ final double util1m = report1m.utilisation(); ++ final double tps1m = report1m.tpsData().segmentAll().average(); ++ final double mspt1m = report1m.timePerTickData().segmentAll().average() / 1.0E6; ++ ++ final int yLoc = 80; ++ final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; ++ ++ final Component line = Component.text() ++ .append(Component.text("Region around block ", PRIMARY)) ++ .append(Component.text(location, INFORMATION)) ++ .append(Component.text(":\n", PRIMARY)) ++ ++ .append( ++ formatRegionInfo("15s: ", util15s, mspt15s, tps15s, true) ++ ) ++ .append( ++ formatRegionInfo("1m: ", util1m, mspt1m, tps1m, true) ++ ) ++ .append( ++ formatRegionStats(region.getData().getRegionStats(), false) ++ ) ++ ++ .build(); ++ ++ sender.sendMessage(line); ++ ++ return true; ++ } ++ ++ private static boolean executeServer(final CommandSender sender, final String commandLabel, final String[] args) { ++ final int lowestRegionsCount; ++ if (args.length < 2) { ++ lowestRegionsCount = 3; ++ } else { ++ try { ++ lowestRegionsCount = Integer.parseInt(args[1]); ++ } catch (final NumberFormatException ex) { ++ sender.sendMessage(Component.text("Highest utilisation count '" + args[1] + "' must be an integer", NamedTextColor.RED)); ++ return true; ++ } ++ } ++ ++ final List> regions = ++ new ArrayList<>(); ++ ++ for (final World bukkitWorld : Bukkit.getWorlds()) { ++ final ServerLevel world = ((CraftWorld)bukkitWorld).getHandle(); ++ world.regioniser.computeForAllRegions(regions::add); ++ } ++ ++ final double minTps; ++ final double medianTps; ++ final double maxTps; ++ double totalUtil = 0.0; ++ ++ final DoubleArrayList tpsByRegion = new DoubleArrayList(); ++ final List reportsByRegion = new ArrayList<>(); ++ final int maxThreadCount = TickRegions.getScheduler().getTotalThreadCount(); ++ ++ final long currTime = System.nanoTime(); ++ final TickData.TickReportData globalTickReport = RegionizedServer.getGlobalTickData().getTickReport15s(currTime); ++ ++ for (final ThreadedRegionizer.ThreadedRegion region : regions) { ++ final TickData.TickReportData report = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); ++ tpsByRegion.add(report == null ? 20.0 : report.tpsData().segmentAll().average()); ++ reportsByRegion.add(report); ++ totalUtil += (report == null ? 0.0 : report.utilisation()); ++ } ++ ++ totalUtil += globalTickReport.utilisation(); ++ ++ tpsByRegion.sort(null); ++ if (!tpsByRegion.isEmpty()) { ++ minTps = tpsByRegion.getDouble(0); ++ maxTps = tpsByRegion.getDouble(tpsByRegion.size() - 1); ++ ++ final int middle = tpsByRegion.size() >> 1; ++ if ((tpsByRegion.size() & 1) == 0) { ++ // even, average the two middle points ++ medianTps = (tpsByRegion.getDouble(middle - 1) + tpsByRegion.getDouble(middle)) / 2.0; ++ } else { ++ // odd, can just grab middle ++ medianTps = tpsByRegion.getDouble(middle); ++ } ++ } else { ++ // no regions = green ++ minTps = medianTps = maxTps = 20.0; ++ } ++ ++ final List, TickData.TickReportData>> ++ regionsBelowThreshold = new ArrayList<>(); ++ ++ for (int i = 0, len = regions.size(); i < len; ++i) { ++ final TickData.TickReportData report = reportsByRegion.get(i); ++ ++ regionsBelowThreshold.add(new ObjectObjectImmutablePair<>(regions.get(i), report)); ++ } ++ ++ regionsBelowThreshold.sort((p1, p2) -> { ++ final TickData.TickReportData report1 = p1.right(); ++ final TickData.TickReportData report2 = p2.right(); ++ final double util1 = report1 == null ? 0.0 : report1.utilisation(); ++ final double util2 = report2 == null ? 0.0 : report2.utilisation(); ++ ++ // we want the largest first ++ return Double.compare(util2, util1); ++ }); ++ ++ final TextComponent.Builder lowestRegionsBuilder = Component.text(); ++ ++ if (sender instanceof Player) { ++ lowestRegionsBuilder.append(Component.text(" Click to teleport\n", SECONDARY)); ++ } ++ for (int i = 0, len = Math.min(lowestRegionsCount, regionsBelowThreshold.size()); i < len; ++i) { ++ final ObjectObjectImmutablePair, TickData.TickReportData> ++ pair = regionsBelowThreshold.get(i); ++ ++ final TickData.TickReportData report = pair.right(); ++ final ThreadedRegionizer.ThreadedRegion region = ++ pair.left(); ++ ++ if (report == null) { ++ // skip regions with no data ++ continue; ++ } ++ ++ final ServerLevel world = region.regioniser.world; ++ final ChunkPos chunkCenter = region.getCenterChunk(); ++ if (chunkCenter == null) { ++ // region does not exist anymore ++ continue; ++ } ++ final int centerBlockX = ((chunkCenter.x << 4) | 7); ++ final int centerBlockZ = ((chunkCenter.z << 4) | 7); ++ final double util = report.utilisation(); ++ final double tps = report.tpsData().segmentAll().average(); ++ final double mspt = report.timePerTickData().segmentAll().average() / 1.0E6; ++ ++ final int yLoc = 80; ++ final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; ++ final Component line = Component.text() ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Region around block ", PRIMARY)) ++ .append(Component.text(location, INFORMATION)) ++ .append(Component.text(":\n", PRIMARY)) ++ ++ .append(Component.text(" ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) ++ .append(Component.text("% util at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) ++ .append(Component.text(" MSPT at ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) ++ .append(Component.text(" TPS\n", PRIMARY)) ++ ++ .append(Component.text(" ", PRIMARY)) ++ .append(formatRegionStats(region.getData().getRegionStats(), (i + 1) != len)) ++ .build() ++ ++ .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey().toString() + " run tp " + centerBlockX + ".5 " + yLoc + " " + centerBlockZ + ".5")) ++ .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("Click to teleport to " + location, SECONDARY))); ++ ++ lowestRegionsBuilder.append(line); ++ } ++ ++ sender.sendMessage( ++ Component.text() ++ .append(Component.text("Server Health Report\n", HEADER, TextDecoration.BOLD)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Online Players: ", PRIMARY)) ++ .append(Component.text(Bukkit.getOnlinePlayers().size() + "\n", INFORMATION)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Total regions: ", PRIMARY)) ++ .append(Component.text(regions.size() + "\n", INFORMATION)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Utilisation: ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(totalUtil * 100.0), CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount))) ++ .append(Component.text("% / ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(maxThreadCount * 100.0), INFORMATION)) ++ .append(Component.text("%\n", PRIMARY)) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Lowest Region TPS: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(minTps) + "\n", CommandUtil.getColourForTPS(minTps))) ++ ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Median Region TPS: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(medianTps) + "\n", CommandUtil.getColourForTPS(medianTps))) ++ ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Highest Region TPS: ", PRIMARY)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(maxTps) + "\n", CommandUtil.getColourForTPS(maxTps))) ++ ++ .append(Component.text("Highest ", HEADER, TextDecoration.BOLD)) ++ .append(Component.text(Integer.toString(lowestRegionsCount), INFORMATION, TextDecoration.BOLD)) ++ .append(Component.text(" utilisation regions\n", HEADER, TextDecoration.BOLD)) ++ ++ .append(lowestRegionsBuilder.build()) ++ .build() ++ ); ++ ++ return true; ++ } ++ ++ @Override ++ public boolean execute(final CommandSender sender, final String commandLabel, final String[] args) { ++ final String type; ++ if (args.length < 1) { ++ type = "server"; ++ } else { ++ type = args[0]; ++ } ++ ++ switch (type.toLowerCase(Locale.ROOT)) { ++ case "server": { ++ return executeServer(sender, commandLabel, args); ++ } ++ case "region": { ++ if (!(sender instanceof Entity)) { ++ sender.sendMessage(Component.text("Cannot see current region information as console", NamedTextColor.RED)); ++ return true; ++ } ++ return executeRegion(sender, commandLabel, args); ++ } ++ default: { ++ sender.sendMessage(Component.text("Type '" + args[0] + "' must be one of: [server, region]", NamedTextColor.RED)); ++ return true; ++ } ++ } ++ } ++ ++ @Override ++ public List tabComplete(final CommandSender sender, final String alias, final String[] args) throws IllegalArgumentException { ++ if (args.length == 0) { ++ if (sender instanceof Entity) { ++ return CommandUtil.getSortedList(Arrays.asList("server", "region")); ++ } else { ++ return CommandUtil.getSortedList(Arrays.asList("server")); ++ } ++ } else if (args.length == 1) { ++ if (sender instanceof Entity) { ++ return CommandUtil.getSortedList(Arrays.asList("server", "region"), args[0]); ++ } else { ++ return CommandUtil.getSortedList(Arrays.asList("server"), args[0]); ++ } ++ } ++ return new ArrayList<>(); ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandUtil.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandUtil.java.patch new file mode 100644 index 0000000..2b3ac5f --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/commands/CommandUtil.java.patch @@ -0,0 +1,124 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/commands/CommandUtil.java +@@ -1,0 +_,121 @@ ++package io.papermc.paper.threadedregions.commands; ++ ++import net.kyori.adventure.text.format.NamedTextColor; ++import net.kyori.adventure.text.format.TextColor; ++import net.kyori.adventure.util.HSVLike; ++import net.minecraft.server.MinecraftServer; ++import net.minecraft.server.level.ServerPlayer; ++import java.util.ArrayList; ++import java.util.List; ++import java.util.function.Function; ++ ++public final class CommandUtil { ++ ++ public static List getSortedList(final Iterable iterable) { ++ final List ret = new ArrayList<>(); ++ for (final String val : iterable) { ++ ret.add(val); ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static List getSortedList(final Iterable iterable, final String prefix) { ++ final List ret = new ArrayList<>(); ++ for (final String val : iterable) { ++ if (val.regionMatches(0, prefix, 0, prefix.length())) { ++ ret.add(val); ++ } ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static List getSortedList(final Iterable iterable, final Function transform) { ++ final List ret = new ArrayList<>(); ++ for (final T val : iterable) { ++ final String transformed = transform.apply(val); ++ if (transformed != null) { ++ ret.add(transformed); ++ } ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static List getSortedList(final Iterable iterable, final Function transform, final String prefix) { ++ final List ret = new ArrayList<>(); ++ for (final T val : iterable) { ++ final String string = transform.apply(val); ++ if (string != null && string.regionMatches(0, prefix, 0, prefix.length())) { ++ ret.add(string); ++ } ++ } ++ ++ ret.sort(String.CASE_INSENSITIVE_ORDER); ++ ++ return ret; ++ } ++ ++ public static TextColor getColourForTPS(final double tps) { ++ final double difference = Math.min(Math.abs(20.0 - tps), 20.0); ++ final double coordinate; ++ if (difference <= 2.0) { ++ // >= 18 tps ++ coordinate = 70.0 + ((140.0 - 70.0)/(0.0 - 2.0)) * (difference - 2.0); ++ } else if (difference <= 5.0) { ++ // >= 15 tps ++ coordinate = 30.0 + ((70.0 - 30.0)/(2.0 - 5.0)) * (difference - 5.0); ++ } else if (difference <= 10.0) { ++ // >= 10 tps ++ coordinate = 10.0 + ((30.0 - 10.0)/(5.0 - 10.0)) * (difference - 10.0); ++ } else { ++ // >= 0.0 tps ++ coordinate = 0.0 + ((10.0 - 0.0)/(10.0 - 20.0)) * (difference - 20.0); ++ } ++ ++ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); ++ } ++ ++ public static TextColor getColourForMSPT(final double mspt) { ++ final double clamped = Math.min(Math.abs(mspt), 50.0); ++ final double coordinate; ++ if (clamped <= 15.0) { ++ coordinate = 130.0 + ((140.0 - 130.0)/(0.0 - 15.0)) * (clamped - 15.0); ++ } else if (clamped <= 25.0) { ++ coordinate = 90.0 + ((130.0 - 90.0)/(15.0 - 25.0)) * (clamped - 25.0); ++ } else if (clamped <= 35.0) { ++ coordinate = 30.0 + ((90.0 - 30.0)/(25.0 - 35.0)) * (clamped - 35.0); ++ } else if (clamped <= 40.0) { ++ coordinate = 15.0 + ((30.0 - 15.0)/(35.0 - 40.0)) * (clamped - 40.0); ++ } else { ++ coordinate = 0.0 + ((15.0 - 0.0)/(40.0 - 50.0)) * (clamped - 50.0); ++ } ++ ++ return TextColor.color(HSVLike.hsvLike((float)(coordinate / 360.0), 85.0f / 100.0f, 80.0f / 100.0f)); ++ } ++ ++ public static TextColor getUtilisationColourRegion(final double util) { ++ // TODO anything better? ++ // assume 20TPS ++ return getColourForMSPT(util * 50.0); ++ } ++ ++ public static ServerPlayer getPlayer(final String name) { ++ for (final ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) { ++ if (player.getGameProfile().getName().equalsIgnoreCase(name)) { ++ return player; ++ } ++ } ++ ++ return null; ++ } ++ ++ private CommandUtil() {} ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java.patch new file mode 100644 index 0000000..acfcb6c --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java.patch @@ -0,0 +1,427 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/scheduler/FoliaRegionScheduler.java +@@ -1,0 +_,424 @@ ++package io.papermc.paper.threadedregions.scheduler; ++ ++import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; ++import ca.spottedleaf.concurrentutil.util.Validate; ++import ca.spottedleaf.moonrise.common.util.CoordinateUtils; ++import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager; ++import io.papermc.paper.threadedregions.RegionizedData; ++import io.papermc.paper.threadedregions.RegionizedServer; ++import io.papermc.paper.threadedregions.TickRegionScheduler; ++import it.unimi.dsi.fastutil.longs.Long2ObjectMap; ++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; ++import net.minecraft.server.level.ServerLevel; ++import net.minecraft.server.level.TicketType; ++import net.minecraft.util.Unit; ++import org.bukkit.Bukkit; ++import org.bukkit.World; ++import org.bukkit.craftbukkit.CraftWorld; ++import org.bukkit.plugin.IllegalPluginAccessException; ++import org.bukkit.plugin.Plugin; ++import java.lang.invoke.VarHandle; ++import java.util.ArrayList; ++import java.util.Iterator; ++import java.util.List; ++import java.util.function.Consumer; ++import java.util.logging.Level; ++ ++public final class FoliaRegionScheduler implements RegionScheduler { ++ ++ private static Runnable wrap(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { ++ return () -> { ++ try { ++ run.run(); ++ } catch (final Throwable throwable) { ++ plugin.getLogger().log(Level.WARNING, "Location task for " + plugin.getDescription().getFullName() ++ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); ++ } ++ }; ++ } ++ ++ private static final RegionizedData SCHEDULER_DATA = new RegionizedData<>(null, Scheduler::new, Scheduler.REGIONISER_CALLBACK); ++ ++ private static void scheduleInternalOnRegion(final LocationScheduledTask task, final long delay) { ++ SCHEDULER_DATA.get().queueTask(task, delay); ++ } ++ ++ private static void scheduleInternalOffRegion(final LocationScheduledTask task, final long delay) { ++ final World world = task.world; ++ if (world == null) { ++ // cancelled ++ return; ++ } ++ ++ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ ((CraftWorld) world).getHandle(), task.chunkX, task.chunkZ, () -> { ++ scheduleInternalOnRegion(task, delay); ++ } ++ ); ++ } ++ ++ @Override ++ public void execute(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Runnable run) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(world, "World may not be null"); ++ Validate.notNull(run, "Runnable may not be null"); ++ ++ RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ ((CraftWorld) world).getHandle(), chunkX, chunkZ, wrap(plugin, world, chunkX, chunkZ, run) ++ ); ++ } ++ ++ @Override ++ public ScheduledTask run(final Plugin plugin, final World world, final int chunkX, final int chunkZ, final Consumer task) { ++ return this.runDelayed(plugin, world, chunkX, chunkZ, task, 1); ++ } ++ ++ @Override ++ public ScheduledTask runDelayed(final Plugin plugin, final World world, final int chunkX, final int chunkZ, ++ final Consumer task, final long delayTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(world, "World may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (delayTicks <= 0) { ++ throw new IllegalArgumentException("Delay ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, -1, task); ++ ++ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { ++ scheduleInternalOnRegion(ret, delayTicks); ++ } else { ++ scheduleInternalOffRegion(ret, delayTicks); ++ } ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ @Override ++ public ScheduledTask runAtFixedRate(final Plugin plugin, final World world, final int chunkX, final int chunkZ, ++ final Consumer task, final long initialDelayTicks, final long periodTicks) { ++ Validate.notNull(plugin, "Plugin may not be null"); ++ Validate.notNull(world, "World may not be null"); ++ Validate.notNull(task, "Task may not be null"); ++ if (initialDelayTicks <= 0) { ++ throw new IllegalArgumentException("Initial delay ticks may not be <= 0"); ++ } ++ if (periodTicks <= 0) { ++ throw new IllegalArgumentException("Period ticks may not be <= 0"); ++ } ++ ++ if (!plugin.isEnabled()) { ++ throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); ++ } ++ ++ final LocationScheduledTask ret = new LocationScheduledTask(plugin, world, chunkX, chunkZ, periodTicks, task); ++ ++ if (Bukkit.isOwnedByCurrentRegion(world, chunkX, chunkZ)) { ++ scheduleInternalOnRegion(ret, initialDelayTicks); ++ } else { ++ scheduleInternalOffRegion(ret, initialDelayTicks); ++ } ++ ++ if (!plugin.isEnabled()) { ++ // handle race condition where plugin is disabled asynchronously ++ ret.cancel(); ++ } ++ ++ return ret; ++ } ++ ++ public void tick() { ++ SCHEDULER_DATA.get().tick(); ++ } ++ ++ private static final class Scheduler { ++ private static final RegionizedData.RegioniserCallback REGIONISER_CALLBACK = new RegionizedData.RegioniserCallback<>() { ++ @Override ++ public void merge(final Scheduler from, final Scheduler into, final long fromTickOffset) { ++ for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); ++ sectionIterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final Long2ObjectOpenHashMap> section = entry.getValue(); ++ ++ final Long2ObjectOpenHashMap> sectionAdjusted = new Long2ObjectOpenHashMap<>(section.size()); ++ ++ for (final Iterator>> iterator = section.long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> e = iterator.next(); ++ final long newTick = e.getLongKey() + fromTickOffset; ++ final List tasks = e.getValue(); ++ ++ sectionAdjusted.put(newTick, tasks); ++ } ++ ++ into.tasksByDeadlineBySection.put(sectionKey, sectionAdjusted); ++ } ++ } ++ ++ @Override ++ public void split(final Scheduler from, final int chunkToRegionShift, final Long2ReferenceOpenHashMap regionToData, ++ final ReferenceOpenHashSet dataSet) { ++ for (final Scheduler into : dataSet) { ++ into.tickCount = from.tickCount; ++ } ++ ++ for (final Iterator>>> sectionIterator = from.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); ++ sectionIterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final Long2ObjectOpenHashMap> section = entry.getValue(); ++ ++ final Scheduler into = regionToData.get(sectionKey); ++ ++ into.tasksByDeadlineBySection.put(sectionKey, section); ++ } ++ } ++ }; ++ ++ private long tickCount = 0L; ++ // map of region section -> map of deadline -> list of tasks ++ private final Long2ObjectOpenHashMap>> tasksByDeadlineBySection = new Long2ObjectOpenHashMap<>(); ++ ++ private void addTicket(final int sectionX, final int sectionZ) { ++ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; ++ final int shift = world.moonrise$getRegionChunkShift(); ++ final int chunkX = sectionX << shift; ++ final int chunkZ = sectionZ << shift; ++ ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( ++ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void removeTicket(final long sectionKey) { ++ final ServerLevel world = TickRegionScheduler.getCurrentRegionizedWorldData().world; ++ final int shift = world.moonrise$getRegionChunkShift(); ++ final int chunkX = CoordinateUtils.getChunkX(sectionKey) << shift; ++ final int chunkZ = CoordinateUtils.getChunkZ(sectionKey) << shift; ++ ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( ++ TicketType.REGION_SCHEDULER_API_HOLD, chunkX, chunkZ, ChunkHolderManager.MAX_TICKET_LEVEL, Unit.INSTANCE ++ ); ++ } ++ ++ private void queueTask(final LocationScheduledTask task, final long delay) { ++ // note: must be on the thread that owns this scheduler ++ // note: delay > 0 ++ ++ final World world = task.world; ++ if (world == null) { ++ // cancelled ++ return; ++ } ++ ++ final int shift = ((CraftWorld)world).getHandle().moonrise$getRegionChunkShift(); ++ final int sectionX = task.chunkX >> shift; ++ final int sectionZ = task.chunkZ >> shift; ++ ++ final Long2ObjectOpenHashMap> section = ++ this.tasksByDeadlineBySection.computeIfAbsent(CoordinateUtils.getChunkKey(sectionX, sectionZ), (final long keyInMap) -> { ++ return new Long2ObjectOpenHashMap<>(); ++ } ++ ); ++ ++ if (section.isEmpty()) { ++ // need to keep the scheduler loaded for this location in order for tick() to be called... ++ this.addTicket(sectionX, sectionZ); ++ } ++ ++ section.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> { ++ return new ArrayList<>(); ++ }).add(task); ++ } ++ ++ public void tick() { ++ ++this.tickCount; ++ ++ final List run = new ArrayList<>(); ++ ++ for (final Iterator>>> sectionIterator = this.tasksByDeadlineBySection.long2ObjectEntrySet().fastIterator(); ++ sectionIterator.hasNext();) { ++ final Long2ObjectMap.Entry>> entry = sectionIterator.next(); ++ final long sectionKey = entry.getLongKey(); ++ final Long2ObjectOpenHashMap> section = entry.getValue(); ++ ++ final List tasks = section.remove(this.tickCount); ++ ++ if (tasks == null) { ++ continue; ++ } ++ ++ run.addAll(tasks); ++ ++ if (section.isEmpty()) { ++ this.removeTicket(sectionKey); ++ sectionIterator.remove(); ++ } ++ } ++ ++ for (int i = 0, len = run.size(); i < len; ++i) { ++ run.get(i).run(); ++ } ++ } ++ } ++ ++ private static final class LocationScheduledTask implements ScheduledTask, Runnable { ++ ++ private static final int STATE_IDLE = 0; ++ private static final int STATE_EXECUTING = 1; ++ private static final int STATE_EXECUTING_CANCELLED = 2; ++ private static final int STATE_FINISHED = 3; ++ private static final int STATE_CANCELLED = 4; ++ ++ private final Plugin plugin; ++ private final int chunkX; ++ private final int chunkZ; ++ private final long repeatDelay; // in ticks ++ private World world; ++ private Consumer run; ++ ++ private volatile int state; ++ private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(LocationScheduledTask.class, "state", int.class); ++ ++ private LocationScheduledTask(final Plugin plugin, final World world, final int chunkX, final int chunkZ, ++ final long repeatDelay, final Consumer run) { ++ this.plugin = plugin; ++ this.world = world; ++ this.chunkX = chunkX; ++ this.chunkZ = chunkZ; ++ this.repeatDelay = repeatDelay; ++ this.run = run; ++ } ++ ++ private final int getStateVolatile() { ++ return (int)STATE_HANDLE.get(this); ++ } ++ ++ private final int compareAndExchangeStateVolatile(final int expect, final int update) { ++ return (int)STATE_HANDLE.compareAndExchange(this, expect, update); ++ } ++ ++ private final void setStateVolatile(final int value) { ++ STATE_HANDLE.setVolatile(this, value); ++ } ++ ++ @Override ++ public void run() { ++ if (!this.plugin.isEnabled()) { ++ // don't execute if the plugin is disabled ++ return; ++ } ++ ++ final boolean repeating = this.isRepeatingTask(); ++ if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { ++ // cancelled ++ return; ++ } ++ ++ try { ++ this.run.accept(this); ++ } catch (final Throwable throwable) { ++ this.plugin.getLogger().log(Level.WARNING, "Location task for " + this.plugin.getDescription().getFullName() ++ + " in world " + world + " at " + chunkX + ", " + chunkZ + " generated an exception", throwable); ++ } finally { ++ boolean reschedule = false; ++ if (!repeating) { ++ this.setStateVolatile(STATE_FINISHED); ++ } else if (!this.plugin.isEnabled()) { ++ this.setStateVolatile(STATE_CANCELLED); ++ } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { ++ reschedule = true; ++ } // else: cancelled repeating task ++ ++ if (!reschedule) { ++ this.run = null; ++ this.world = null; ++ } else { ++ FoliaRegionScheduler.scheduleInternalOnRegion(this, this.repeatDelay); ++ } ++ } ++ } ++ ++ @Override ++ public Plugin getOwningPlugin() { ++ return this.plugin; ++ } ++ ++ @Override ++ public boolean isRepeatingTask() { ++ return this.repeatDelay > 0; ++ } ++ ++ @Override ++ public CancelledState cancel() { ++ for (int curr = this.getStateVolatile();;) { ++ switch (curr) { ++ case STATE_IDLE: { ++ if (STATE_IDLE == (curr = this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_CANCELLED))) { ++ this.state = STATE_CANCELLED; ++ this.run = null; ++ this.world = null; ++ return CancelledState.CANCELLED_BY_CALLER; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING: { ++ if (!this.isRepeatingTask()) { ++ return CancelledState.RUNNING; ++ } ++ if (STATE_EXECUTING == (curr = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_EXECUTING_CANCELLED))) { ++ return CancelledState.NEXT_RUNS_CANCELLED; ++ } ++ // try again ++ continue; ++ } ++ case STATE_EXECUTING_CANCELLED: { ++ return CancelledState.NEXT_RUNS_CANCELLED_ALREADY; ++ } ++ case STATE_FINISHED: { ++ return CancelledState.ALREADY_EXECUTED; ++ } ++ case STATE_CANCELLED: { ++ return CancelledState.CANCELLED_ALREADY; ++ } ++ default: { ++ throw new IllegalStateException("Unknown state: " + curr); ++ } ++ } ++ } ++ } ++ ++ @Override ++ public ExecutionState getExecutionState() { ++ final int state = this.getStateVolatile(); ++ switch (state) { ++ case STATE_IDLE: ++ return ExecutionState.IDLE; ++ case STATE_EXECUTING: ++ return ExecutionState.RUNNING; ++ case STATE_EXECUTING_CANCELLED: ++ return ExecutionState.CANCELLED_RUNNING; ++ case STATE_FINISHED: ++ return ExecutionState.FINISHED; ++ case STATE_CANCELLED: ++ return ExecutionState.CANCELLED; ++ default: { ++ throw new IllegalStateException("Unknown state: " + state); ++ } ++ } ++ } ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java.patch new file mode 100644 index 0000000..e813649 --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java.patch @@ -0,0 +1,82 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/util/SimpleThreadLocalRandomSource.java +@@ -1,0 +_,79 @@ ++package io.papermc.paper.threadedregions.util; ++ ++import net.minecraft.util.RandomSource; ++import net.minecraft.world.level.levelgen.BitRandomSource; ++import net.minecraft.world.level.levelgen.PositionalRandomFactory; ++import java.util.concurrent.ThreadLocalRandom; ++ ++public final class SimpleThreadLocalRandomSource implements BitRandomSource { ++ ++ public static final SimpleThreadLocalRandomSource INSTANCE = new SimpleThreadLocalRandomSource(); ++ ++ private final PositionalRandomFactory positionalRandomFactory = new SimpleThreadLocalRandomSource.SimpleThreadLocalRandomPositionalRandomFactory(); ++ ++ private SimpleThreadLocalRandomSource() {} ++ ++ @Override ++ public int next(final int bits) { ++ return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits); ++ } ++ ++ @Override ++ public int nextInt() { ++ return ThreadLocalRandom.current().nextInt(); ++ } ++ ++ @Override ++ public int nextInt(final int bound) { ++ if (bound <= 0) { ++ throw new IllegalArgumentException(); ++ } ++ ++ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ ++ final long value = (long)this.nextInt() & 0xFFFFFFFFL; ++ return (int)((value * (long)bound) >>> Integer.SIZE); ++ } ++ ++ @Override ++ public void setSeed(final long seed) { ++ // no-op ++ } ++ ++ @Override ++ public double nextGaussian() { ++ return ThreadLocalRandom.current().nextGaussian(); ++ } ++ ++ @Override ++ public RandomSource fork() { ++ return this; ++ } ++ ++ @Override ++ public PositionalRandomFactory forkPositional() { ++ return this.positionalRandomFactory; ++ } ++ ++ private static final class SimpleThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory { ++ ++ @Override ++ public RandomSource fromHashOf(final String seed) { ++ return SimpleThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public RandomSource fromSeed(final long seed) { ++ return SimpleThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public RandomSource at(final int x, final int y, final int z) { ++ return SimpleThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public void parityConfigString(final StringBuilder info) { ++ info.append("SimpleThreadLocalRandomPositionalRandomFactory{}"); ++ } ++ } ++} diff --git a/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java.patch b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java.patch new file mode 100644 index 0000000..2c5a7fd --- /dev/null +++ b/folia-server/minecraft-patches/sources/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java.patch @@ -0,0 +1,76 @@ +--- /dev/null ++++ b/io/papermc/paper/threadedregions/util/ThreadLocalRandomSource.java +@@ -1,0 +_,73 @@ ++package io.papermc.paper.threadedregions.util; ++ ++import net.minecraft.util.RandomSource; ++import net.minecraft.world.level.levelgen.BitRandomSource; ++import net.minecraft.world.level.levelgen.PositionalRandomFactory; ++import java.util.concurrent.ThreadLocalRandom; ++ ++public final class ThreadLocalRandomSource implements BitRandomSource { ++ ++ public static final ThreadLocalRandomSource INSTANCE = new ThreadLocalRandomSource(); ++ ++ private final PositionalRandomFactory positionalRandomFactory = new ThreadLocalRandomPositionalRandomFactory(); ++ ++ private ThreadLocalRandomSource() {} ++ ++ @Override ++ public int next(final int bits) { ++ return ThreadLocalRandom.current().nextInt() >>> (Integer.SIZE - bits); ++ } ++ ++ @Override ++ public int nextInt() { ++ return ThreadLocalRandom.current().nextInt(); ++ } ++ ++ @Override ++ public int nextInt(final int bound) { ++ return ThreadLocalRandom.current().nextInt(bound); ++ } ++ ++ @Override ++ public void setSeed(final long seed) { ++ // no-op ++ } ++ ++ @Override ++ public double nextGaussian() { ++ return ThreadLocalRandom.current().nextGaussian(); ++ } ++ ++ @Override ++ public RandomSource fork() { ++ return this; ++ } ++ ++ @Override ++ public PositionalRandomFactory forkPositional() { ++ return this.positionalRandomFactory; ++ } ++ ++ private static final class ThreadLocalRandomPositionalRandomFactory implements PositionalRandomFactory { ++ ++ @Override ++ public RandomSource fromHashOf(final String seed) { ++ return ThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public RandomSource fromSeed(final long seed) { ++ return ThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public RandomSource at(final int x, final int y, final int z) { ++ return ThreadLocalRandomSource.INSTANCE; ++ } ++ ++ @Override ++ public void parityConfigString(final StringBuilder info) { ++ info.append("ThreadLocalRandomPositionalRandomFactory{}"); ++ } ++ } ++} diff --git a/folia-server/minecraft-patches/sources/net/minecraft/commands/CommandSourceStack.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/commands/CommandSourceStack.java.patch new file mode 100644 index 0000000..ccc2983 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/commands/CommandSourceStack.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/commands/CommandSourceStack.java ++++ b/net/minecraft/commands/CommandSourceStack.java +@@ -91,7 +_,7 @@ + CommandResultCallback.EMPTY, + EntityAnchorArgument.Anchor.FEET, + CommandSigningContext.ANONYMOUS, +- TaskChainer.immediate(server) ++ TaskChainer.immediate((Runnable run) -> { io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run);}) // Folia - region threading + ); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/commands/Commands.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/commands/Commands.java.patch new file mode 100644 index 0000000..7c49aba --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/commands/Commands.java.patch @@ -0,0 +1,112 @@ +--- a/net/minecraft/commands/Commands.java ++++ b/net/minecraft/commands/Commands.java +@@ -153,13 +_,13 @@ + AdvancementCommands.register(this.dispatcher); + AttributeCommand.register(this.dispatcher, context); + ExecuteCommand.register(this.dispatcher, context); +- BossBarCommands.register(this.dispatcher, context); ++ //BossBarCommands.register(this.dispatcher, context); // Folia - region threading - TODO + ClearInventoryCommands.register(this.dispatcher, context); +- CloneCommands.register(this.dispatcher, context); ++ //CloneCommands.register(this.dispatcher, context); // Folia - region threading - TODO + DamageCommand.register(this.dispatcher, context); +- DataCommands.register(this.dispatcher); +- DataPackCommand.register(this.dispatcher); +- DebugCommand.register(this.dispatcher); ++ //DataCommands.register(this.dispatcher); // Folia - region threading - TODO ++ //DataPackCommand.register(this.dispatcher); // Folia - region threading - TODO ++ //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO + DefaultGameModeCommands.register(this.dispatcher); + DifficultyCommand.register(this.dispatcher); + EffectCommands.register(this.dispatcher, context); +@@ -169,47 +_,47 @@ + FillCommand.register(this.dispatcher, context); + FillBiomeCommand.register(this.dispatcher, context); + ForceLoadCommand.register(this.dispatcher); +- FunctionCommand.register(this.dispatcher); ++ //FunctionCommand.register(this.dispatcher); // Folia - region threading - TODO + GameModeCommand.register(this.dispatcher); + GameRuleCommand.register(this.dispatcher, context); + GiveCommand.register(this.dispatcher, context); + HelpCommand.register(this.dispatcher); +- ItemCommands.register(this.dispatcher, context); ++ //ItemCommands.register(this.dispatcher, context); // Folia - region threading - TODO later + KickCommand.register(this.dispatcher); + KillCommand.register(this.dispatcher); + ListPlayersCommand.register(this.dispatcher); + LocateCommand.register(this.dispatcher, context); +- LootCommand.register(this.dispatcher, context); ++ //LootCommand.register(this.dispatcher, context); // Folia - region threading - TODO later + MsgCommand.register(this.dispatcher); + ParticleCommand.register(this.dispatcher, context); + PlaceCommand.register(this.dispatcher); + PlaySoundCommand.register(this.dispatcher); + RandomCommand.register(this.dispatcher); +- ReloadCommand.register(this.dispatcher); ++ //ReloadCommand.register(this.dispatcher); // Folia - region threading + RecipeCommand.register(this.dispatcher); +- ReturnCommand.register(this.dispatcher); +- RideCommand.register(this.dispatcher); +- RotateCommand.register(this.dispatcher); ++ //ReturnCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //RideCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //RotateCommand.register(this.dispatcher); // Folia - region threading - TODO later + SayCommand.register(this.dispatcher); +- ScheduleCommand.register(this.dispatcher); +- ScoreboardCommand.register(this.dispatcher, context); ++ //ScheduleCommand.register(this.dispatcher); // Folia - region threading ++ //ScoreboardCommand.register(this.dispatcher, context); // Folia - region threading + SeedCommand.register(this.dispatcher, selection != Commands.CommandSelection.INTEGRATED); + SetBlockCommand.register(this.dispatcher, context); + SetSpawnCommand.register(this.dispatcher); + SetWorldSpawnCommand.register(this.dispatcher); +- SpectateCommand.register(this.dispatcher); +- SpreadPlayersCommand.register(this.dispatcher); ++ //SpectateCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //SpreadPlayersCommand.register(this.dispatcher); // Folia - region threading - TODO later + StopSoundCommand.register(this.dispatcher); + SummonCommand.register(this.dispatcher, context); +- TagCommand.register(this.dispatcher); +- TeamCommand.register(this.dispatcher, context); +- TeamMsgCommand.register(this.dispatcher); ++ //TagCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //TeamCommand.register(this.dispatcher, context); // Folia - region threading - TODO later ++ //TeamMsgCommand.register(this.dispatcher); // Folia - region threading - TODO later + TeleportCommand.register(this.dispatcher); + TellRawCommand.register(this.dispatcher, context); +- TickCommand.register(this.dispatcher); ++ //TickCommand.register(this.dispatcher); // Folia - region threading - TODO later + TimeCommand.register(this.dispatcher); + TitleCommand.register(this.dispatcher, context); +- TriggerCommand.register(this.dispatcher); ++ //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later + WeatherCommand.register(this.dispatcher); + WorldBorderCommand.register(this.dispatcher); + if (JvmProfiler.INSTANCE.isAvailable()) { +@@ -237,8 +_,8 @@ + OpCommand.register(this.dispatcher); + PardonCommand.register(this.dispatcher); + PardonIpCommand.register(this.dispatcher); +- PerfCommand.register(this.dispatcher); +- SaveAllCommand.register(this.dispatcher); ++ //PerfCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ //SaveAllCommand.register(this.dispatcher); // Folia - region threading - TODO later + SaveOffCommand.register(this.dispatcher); + SaveOnCommand.register(this.dispatcher); + SetPlayerIdleTimeoutCommand.register(this.dispatcher); +@@ -480,9 +_,12 @@ + } + // Paper start - Perf: Async command map building + new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent(player.getBukkitEntity(), (RootCommandNode) rootCommandNode, false).callEvent(); // Paper - Brigadier API +- net.minecraft.server.MinecraftServer.getServer().execute(() -> { +- runSync(player, bukkit, rootCommandNode); +- }); ++ // Folia start - region threading ++ // ignore if retired ++ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer updatedPlayer) -> { ++ runSync((ServerPlayer)updatedPlayer, bukkit, rootCommandNode); ++ }, null, 1L); ++ // Folia end - region threading + } + + private void runSync(ServerPlayer player, java.util.Collection bukkit, RootCommandNode rootCommandNode) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch new file mode 100644 index 0000000..074f14b --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java +@@ -46,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(d1, d2 + d4, d3)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + serverLevel.getCraftServer().getPluginManager().callEvent(event); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch new file mode 100644 index 0000000..4792b27 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java +@@ -78,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(stack); + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(itemEntity.getDeltaMovement())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + level.getCraftServer().getPluginManager().callEvent(event); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch new file mode 100644 index 0000000..e326818 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch @@ -0,0 +1,149 @@ +--- a/net/minecraft/core/dispenser/DispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/DispenseItemBehavior.java +@@ -89,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + serverLevel.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -147,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + serverLevel.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -201,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); + + org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -251,7 +_,7 @@ + org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockSource.pos()); + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleCopy); + org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), abstractChestedHorse.getBukkitLivingEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -329,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(x, y, z)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + level.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -389,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + levelAccessor.getMinecraftWorld().getCraftServer().getPluginManager().callEvent(event); + } + +@@ -425,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + serverLevel.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -482,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + level.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -500,7 +_,8 @@ + } + } + +- level.captureTreeGeneration = true; ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ++ worldData.captureTreeGeneration = true; // Folia - region threading + // CraftBukkit end + if (!BoneMealItem.growCrop(item, level, blockPos) && !BoneMealItem.growWaterPlant(item, level, blockPos, null)) { + this.setSuccess(false); +@@ -508,13 +_,13 @@ + level.levelEvent(1505, blockPos, 15); + } + // CraftBukkit start +- level.captureTreeGeneration = false; +- if (level.capturedBlockStates.size() > 0) { +- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; +- net.minecraft.world.level.block.SaplingBlock.treeType = null; ++ worldData.captureTreeGeneration = false; // Folia - region threading ++ if (worldData.capturedBlockStates.size() > 0) { // Folia - region threading ++ org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // Folia - region threading ++ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); // Folia - region threading + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(blockPos, level.getWorld()); +- List blocks = new java.util.ArrayList<>(level.capturedBlockStates.values()); +- level.capturedBlockStates.clear(); ++ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + org.bukkit.event.world.StructureGrowEvent structureEvent = null; + if (treeType != null) { + structureEvent = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks); +@@ -548,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(singleItemStack); + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) blockPos.getX() + 0.5D, (double) blockPos.getY(), (double) blockPos.getZ() + 0.5D)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + level.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -591,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + level.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -644,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + level.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -702,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - only single item in event + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(blockPos)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + serverLevel.getCraftServer().getPluginManager().callEvent(event); + } + +@@ -783,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), entitiesOfClass.get(0).getBukkitLivingEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + serverLevel.getCraftServer().getPluginManager().callEvent(event); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch new file mode 100644 index 0000000..b4c365c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java +@@ -39,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemStack); + + org.bukkit.event.block.BlockDispenseArmorEvent event = new org.bukkit.event.block.BlockDispenseArmorEvent(block, craftItem.clone(), (org.bukkit.craftbukkit.entity.CraftLivingEntity) livingEntity.getBukkitEntity()); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + world.getCraftServer().getPluginManager().callEvent(event); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch new file mode 100644 index 0000000..c6c876e --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java +@@ -62,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1); + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block2, craftItem.clone(), new org.bukkit.util.Vector(vec31.x, vec31.y, vec31.z)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + serverLevel.getCraftServer().getPluginManager().callEvent(event); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch new file mode 100644 index 0000000..3b5c203 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java ++++ b/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java +@@ -32,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack1); + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(block, craftItem.clone(), new org.bukkit.util.Vector((double) direction.getStepX(), (double) direction.getStepY(), (double) direction.getStepZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + serverLevel.getCraftServer().getPluginManager().callEvent(event); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch new file mode 100644 index 0000000..d38a648 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -25,7 +_,7 @@ + org.bukkit.block.Block bukkitBlock = org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, blockSource.pos()); + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item); // Paper - ignore stack size on damageable items + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(0, 0, 0)); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + serverLevel.getCraftServer().getPluginManager().callEvent(event); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch new file mode 100644 index 0000000..7250354 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java ++++ b/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java +@@ -27,7 +_,7 @@ + org.bukkit.craftbukkit.inventory.CraftItemStack craftItem = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(item.copyWithCount(1)); // Paper - single item in event + + org.bukkit.event.block.BlockDispenseEvent event = new org.bukkit.event.block.BlockDispenseEvent(bukkitBlock, craftItem.clone(), new org.bukkit.util.Vector(blockPos.getX(), blockPos.getY(), blockPos.getZ())); +- if (!DispenserBlock.eventFired) { ++ if (!DispenserBlock.eventFired.get().booleanValue()) { // Folia - region threading + blockSource.level().getCraftServer().getPluginManager().callEvent(event); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestHelper.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestHelper.java.patch new file mode 100644 index 0000000..bdb7f73 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestHelper.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/gametest/framework/GameTestHelper.java ++++ b/net/minecraft/gametest/framework/GameTestHelper.java +@@ -306,7 +_,7 @@ + }; + Connection connection = new Connection(PacketFlow.SERVERBOUND); + new EmbeddedChannel(connection); +- this.getLevel().getServer().getPlayerList().placeNewPlayer(connection, serverPlayer, commonListenerCookie); ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + return serverPlayer; + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestServer.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestServer.java.patch new file mode 100644 index 0000000..c98f1d5 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/gametest/framework/GameTestServer.java.patch @@ -0,0 +1,17 @@ +--- a/net/minecraft/gametest/framework/GameTestServer.java ++++ b/net/minecraft/gametest/framework/GameTestServer.java +@@ -175,8 +_,12 @@ + } + + @Override +- public void tickServer(BooleanSupplier hasTimeLeft) { +- super.tickServer(hasTimeLeft); ++ // Folia start - region threading ++ public void tickServer(long startTime, long scheduledEnd, long targetBuffer, ++ io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { ++ if (true) throw new UnsupportedOperationException(); ++ super.tickServer(startTime, scheduledEnd, targetBuffer, region); ++ // Folia end - region threading + ServerLevel serverLevel = this.overworld(); + if (!this.haveTestsStarted()) { + this.startTests(serverLevel); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch new file mode 100644 index 0000000..4f35b55 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/network/Connection.java.patch @@ -0,0 +1,336 @@ +--- a/net/minecraft/network/Connection.java ++++ b/net/minecraft/network/Connection.java +@@ -85,7 +_,7 @@ + private static final ProtocolInfo INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND; + private final PacketFlow receiving; + private volatile boolean sendLoginDisconnect = true; +- private final Queue pendingActions = Queues.newConcurrentLinkedQueue(); // Paper - Optimize network ++ private final Queue pendingActions = new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); // Paper - Optimize network // Folia - region threading - connection fixes + public Channel channel; + public SocketAddress address; + // Spigot start +@@ -100,7 +_,7 @@ + @Nullable + private DisconnectionDetails disconnectionDetails; + private boolean encrypted; +- private boolean disconnectionHandled; ++ private final java.util.concurrent.atomic.AtomicBoolean disconnectionHandled = new java.util.concurrent.atomic.AtomicBoolean(false); // Folia - region threading - may be called concurrently during configuration stage + private int receivedPackets; + private int sentPackets; + private float averageReceivedPackets; +@@ -154,6 +_,41 @@ + this.receiving = receiving; + } + ++ // Folia start - region threading ++ private volatile boolean becomeActive; ++ ++ public boolean becomeActive() { ++ return this.becomeActive; ++ } ++ ++ private static record DisconnectReq(DisconnectionDetails disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {} ++ ++ private final ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue disconnectReqs = ++ new ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue<>(); ++ ++ /** ++ * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the ++ * same thread that could disconnect. ++ */ ++ public final void disconnectSafely(DisconnectionDetails disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { ++ this.disconnectReqs.add(new DisconnectReq(disconnectReason, cause)); ++ // We can't halt packet processing here because a plugin could cancel a kick request. ++ } ++ ++ /** ++ * Safely disconnects the connection while possibly on another thread. Note: This call will not block, even if on the ++ * same thread that could disconnect. ++ */ ++ public final void disconnectSafely(Component disconnectReason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { ++ this.disconnectReqs.add(new DisconnectReq(new DisconnectionDetails(disconnectReason), cause)); ++ // We can't halt packet processing here because a plugin could cancel a kick request. ++ } ++ ++ public final boolean isPlayerConnected() { ++ return this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl; ++ } ++ // Folia end - region threading ++ + @Override + public void channelActive(ChannelHandlerContext context) throws Exception { + super.channelActive(context); +@@ -163,6 +_,7 @@ + if (this.delayedDisconnect != null) { + this.disconnect(this.delayedDisconnect); + } ++ this.becomeActive = true; // Folia - region threading + } + + @Override +@@ -434,7 +_,7 @@ + } + + packet.onPacketDispatch(this.getPlayer()); +- if (connected && (InnerUtil.canSendImmediate(this, packet) ++ if (false && connected && (InnerUtil.canSendImmediate(this, packet) // Folia - region threading - connection fixes + || (io.papermc.paper.util.MCUtil.isMainThread() && packet.isReady() && this.pendingActions.isEmpty() + && (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())))) { + this.sendPacket(packet, listener, flush); +@@ -463,11 +_,12 @@ + } + + public void runOnceConnected(Consumer action) { +- if (this.isConnected()) { ++ if (false && this.isConnected()) { // Folia - region threading - connection fixes + this.flushQueue(); + action.accept(this); + } else { + this.pendingActions.add(new WrappedConsumer(action)); // Paper - Optimize network ++ this.flushQueue(); // Folia - region threading - connection fixes + } + } + +@@ -518,10 +_,11 @@ + } + + public void flushChannel() { +- if (this.isConnected()) { ++ if (false && this.isConnected()) { // Folia - region threading - connection fixes + this.flush(); + } else { + this.pendingActions.add(new WrappedConsumer(Connection::flush)); // Paper - Optimize network ++ this.flushQueue(); // Folia - region threading - connection fixes + } + } + +@@ -535,53 +_,61 @@ + + // Paper start - Optimize network: Rewrite this to be safer if ran off main thread + private boolean flushQueue() { +- if (!this.isConnected()) { +- return true; +- } +- if (io.papermc.paper.util.MCUtil.isMainThread()) { +- return this.processQueue(); +- } else if (this.isPending) { +- // Should only happen during login/status stages +- synchronized (this.pendingActions) { +- return this.processQueue(); +- } +- } +- return false; +- } ++ return this.processQueue(); // Folia - region threading - connection fixes ++ } ++ ++ // Folia start - region threading - connection fixes ++ // allow only one thread to be flushing the queue at once to ensure packets are written in the order they are sent ++ // into the queue ++ private final java.util.concurrent.atomic.AtomicBoolean flushingQueue = new java.util.concurrent.atomic.AtomicBoolean(); ++ ++ private static boolean canWrite(WrappedConsumer queued) { ++ return queued != null && (!(queued instanceof PacketSendAction packet) || packet.packet.isReady()); ++ } ++ ++ private boolean canWritePackets() { ++ return canWrite(this.pendingActions.peek()); ++ } ++ // Folia end - region threading - connection fixes + + private boolean processQueue() { +- if (this.pendingActions.isEmpty()) { ++ // Folia start - region threading - connection fixes ++ if (!this.isConnected()) { + return true; + } + +- // If we are on main, we are safe here in that nothing else should be processing queue off main anymore +- // But if we are not on main due to login/status, the parent is synchronized on packetQueue +- final java.util.Iterator iterator = this.pendingActions.iterator(); +- while (iterator.hasNext()) { +- final WrappedConsumer queued = iterator.next(); // poll -> peek +- +- // Fix NPE (Spigot bug caused by handleDisconnection()) +- if (queued == null) { +- return true; +- } +- +- if (queued.isConsumed()) { +- continue; +- } +- +- if (queued instanceof PacketSendAction packetSendAction) { +- final Packet packet = packetSendAction.packet; +- if (!packet.isReady()) { ++ while (this.canWritePackets()) { ++ final boolean set = this.flushingQueue.getAndSet(true); ++ try { ++ if (set) { ++ // we didn't acquire the lock, break + return false; + } +- } +- +- iterator.remove(); +- if (queued.tryMarkConsumed()) { +- queued.accept(this); ++ ++ ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue queue = ++ (ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue)this.pendingActions; ++ WrappedConsumer holder; ++ for (;;) { ++ // synchronise so that queue clears appear atomic ++ synchronized (queue) { ++ holder = queue.pollIf(Connection::canWrite); ++ } ++ if (holder == null) { ++ break; ++ } ++ ++ holder.accept(this); ++ } ++ ++ } finally { ++ if (!set) { ++ this.flushingQueue.set(false); ++ } + } + } ++ + return true; ++ // Folia end - region threading - connection fixes + } + // Paper end - Optimize network + +@@ -590,17 +_,37 @@ + private static int currTick; // Paper - Buffer joins to world + public void tick() { + this.flushQueue(); +- // Paper start - Buffer joins to world +- if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) { +- Connection.currTick = net.minecraft.server.MinecraftServer.currentTick; +- Connection.joinAttemptsThisTick = 0; +- } +- // Paper end - Buffer joins to world ++ // Folia - this is broken ++ // Folia start - region threading ++ // handle disconnect requests, but only after flushQueue() ++ DisconnectReq disconnectReq; ++ while ((disconnectReq = this.disconnectReqs.poll()) != null) { ++ PacketListener packetlistener = this.packetListener; ++ ++ if (packetlistener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { ++ loginPacketListener.disconnect(disconnectReq.disconnectReason.reason()); ++ // this doesn't fail, so abort any further attempts ++ return; ++ } else if (packetlistener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { ++ commonPacketListener.disconnect(disconnectReq.disconnectReason, disconnectReq.cause); ++ // may be cancelled by a plugin, if not cancelled then any further calls do nothing ++ continue; ++ } else { ++ // no idea what packet to send ++ this.disconnect(disconnectReq.disconnectReason); ++ this.setReadOnly(); ++ return; ++ } ++ } ++ if (!this.isConnected()) { ++ // disconnected from above ++ this.handleDisconnection(); ++ return; ++ } ++ // Folia end - region threading + if (this.packetListener instanceof TickablePacketListener tickablePacketListener) { + // Paper start - Buffer joins to world +- if (!(this.packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) +- || loginPacketListener.state != net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING +- || Connection.joinAttemptsThisTick++ < MAX_PER_TICK) { ++ if (true) { // Folia - region threading + // Paper start - detailed watchdog information + net.minecraft.network.protocol.PacketUtils.packetProcessing.push(this.packetListener); + try { +@@ -611,7 +_,7 @@ + } // Paper end - Buffer joins to world + } + +- if (!this.isConnected() && !this.disconnectionHandled) { ++ if (!this.isConnected()) {// Folia - region threading - it's fine to call if it is already handled, as it no longer logs + this.handleDisconnection(); + } + +@@ -662,6 +_,7 @@ + this.channel.close(); // We can't wait as this may be called from an event loop. + this.disconnectionDetails = disconnectionDetails; + } ++ this.becomeActive = true; // Folia - region threading + } + + public boolean isMemoryConnection() { +@@ -853,10 +_,10 @@ + + public void handleDisconnection() { + if (this.channel != null && !this.channel.isOpen()) { +- if (this.disconnectionHandled) { ++ if (!this.disconnectionHandled.compareAndSet(false, true)) { // Folia - region threading - may be called concurrently during configuration stage + // LOGGER.warn("handleDisconnection() called twice"); // Paper - Don't log useless message + } else { +- this.disconnectionHandled = true; ++ //this.disconnectionHandled = true; // Folia - region threading - may be called concurrently during configuration stage - set above + PacketListener packetListener = this.getPacketListener(); + PacketListener packetListener1 = packetListener != null ? packetListener : this.disconnectListener; + if (packetListener1 != null) { +@@ -885,6 +_,21 @@ + } + } + // Paper end - Add PlayerConnectionCloseEvent ++ // Folia start - region threading ++ if (packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl commonPacketListener) { ++ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( ++ commonPacketListener.getOwner().getName(), ++ commonPacketListener.getOwner().getId(), this ++ ); ++ } else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { ++ if (loginPacketListener.state.ordinal() >= net.minecraft.server.network.ServerLoginPacketListenerImpl.State.VERIFYING.ordinal()) { ++ net.minecraft.server.MinecraftServer.getServer().getPlayerList().removeConnection( ++ loginPacketListener.authenticatedProfile.getName(), ++ loginPacketListener.authenticatedProfile.getId(), this ++ ); ++ } ++ } ++ // Folia end - region threading + } + } + } +@@ -904,15 +_,25 @@ + // Paper start - Optimize network + public void clearPacketQueue() { + final net.minecraft.server.level.ServerPlayer player = getPlayer(); +- for (final Consumer queuedAction : this.pendingActions) { +- if (queuedAction instanceof PacketSendAction packetSendAction) { +- final Packet packet = packetSendAction.packet; +- if (packet.hasFinishListener()) { +- packet.onPacketDispatchFinish(player, null); ++ // Folia start - region threading - connection fixes ++ java.util.List queuedPackets = new java.util.ArrayList<>(); ++ // synchronise so that flushQueue does not poll values while the queue is being cleared ++ synchronized (this.pendingActions) { ++ Connection.WrappedConsumer consumer; ++ while ((consumer = this.pendingActions.poll()) != null) { ++ if (consumer instanceof Connection.PacketSendAction packetHolder) { ++ queuedPackets.add(packetHolder); + } + } + } +- this.pendingActions.clear(); ++ ++ for (Connection.PacketSendAction queuedPacket : queuedPackets) { ++ Packet packet = queuedPacket.packet; ++ if (packet.hasFinishListener()) { ++ packet.onPacketDispatchFinish(player, null); ++ } ++ } ++ // Folia end - region threading - connection fixes + } + + private static class InnerUtil { // Attempt to hide these methods from ProtocolLib, so it doesn't accidently pick them up. diff --git a/folia-server/minecraft-patches/sources/net/minecraft/network/protocol/PacketUtils.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/network/protocol/PacketUtils.java.patch new file mode 100644 index 0000000..e051187 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/network/protocol/PacketUtils.java.patch @@ -0,0 +1,37 @@ +--- a/net/minecraft/network/protocol/PacketUtils.java ++++ b/net/minecraft/network/protocol/PacketUtils.java +@@ -20,7 +_,7 @@ + + public static void ensureRunningOnSameThread(Packet packet, T processor, BlockableEventLoop executor) throws RunningOnDifferentThreadException { + if (!executor.isSameThread()) { +- executor.executeIfPossible(() -> { ++ Runnable run = () -> { // Folia - region threading + packetProcessing.push(processor); // Paper - detailed watchdog information + try { // Paper - detailed watchdog information + if (processor instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl serverCommonPacketListener && serverCommonPacketListener.processedDisconnect) return; // Paper - Don't handle sync packets for kicked players +@@ -43,7 +_,24 @@ + packetProcessing.pop(); + } + // Paper end - detailed watchdog information +- }); ++ // Folia start - region threading ++ }; ++ // ignore retired state, if removed then we don't want the packet to be handled ++ if (processor instanceof net.minecraft.server.network.ServerGamePacketListenerImpl gamePacketListener) { ++ gamePacketListener.player.getBukkitEntity().taskScheduler.schedule( ++ (net.minecraft.server.level.ServerPlayer player) -> { ++ run.run(); ++ }, ++ null, 1L ++ ); ++ } else if (processor instanceof net.minecraft.server.network.ServerConfigurationPacketListenerImpl configurationPacketListener) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); ++ } else if (processor instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginPacketListener) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(run); ++ } else { ++ throw new UnsupportedOperationException("Unknown listener: " + processor); ++ } ++ // Folia end - region threading + throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch new file mode 100644 index 0000000..d631317 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/MinecraftServer.java.patch @@ -0,0 +1,779 @@ +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -184,7 +_,7 @@ + private static final int OVERLOADED_TICKS_THRESHOLD = 20; + private static final long OVERLOADED_WARNING_INTERVAL_NANOS = 10L * TimeUtil.NANOSECONDS_PER_SECOND; + private static final int OVERLOADED_TICKS_WARNING_INTERVAL = 100; +- private static final long STATUS_EXPIRE_TIME_NANOS = 5L * TimeUtil.NANOSECONDS_PER_SECOND; ++ public static final long STATUS_EXPIRE_TIME_NANOS = 5L * TimeUtil.NANOSECONDS_PER_SECOND; // Folia - region threading - public + private static final long PREPARE_LEVELS_DEFAULT_DELAY_NANOS = 10L * TimeUtil.NANOSECONDS_PER_MILLISECOND; + private static final int MAX_STATUS_PLAYER_SAMPLE = 12; + private static final int SPAWN_POSITION_SEARCH_RADIUS = 5; +@@ -222,8 +_,7 @@ + private volatile boolean running = true; + private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart + private boolean stopped; +- private int tickCount; +- private int ticksUntilAutosave = 6000; ++ // Folia - region threading + protected final Proxy proxy; + private boolean onlineMode; + private boolean preventProxyConnections; +@@ -283,7 +_,7 @@ + public org.bukkit.craftbukkit.CraftServer server; + public joptsimple.OptionSet options; + public org.bukkit.command.ConsoleCommandSender console; +- public static int currentTick; // Paper - improve tick loop ++ //public static int currentTick; // Paper - improve tick loop // Folia - region threading + public java.util.Queue processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; + // Paper - don't store the vanilla dispatcher +@@ -304,6 +_,50 @@ + private final Set pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping + public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation + ++ // Folia start - regionised ticking ++ public final io.papermc.paper.threadedregions.RegionizedServer regionizedServer = new io.papermc.paper.threadedregions.RegionizedServer(); ++ ++ @Override ++ public CompletableFuture submit(java.util.function.Supplier task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ return super.submit(task); ++ } ++ ++ @Override ++ public CompletableFuture submit(Runnable task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ return super.submit(task); ++ } ++ ++ @Override ++ public void schedule(TickTask task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.schedule(task); ++ } ++ ++ @Override ++ public void executeBlocking(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.executeBlocking(runnable); ++ } ++ ++ @Override ++ public void execute(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.execute(runnable); ++ } ++ // Folia end - regionised ticking ++ + public static S spin(Function threadFunction) { + ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system + AtomicReference atomicReference = new AtomicReference<>(); +@@ -332,46 +_,30 @@ + private static final long MAX_CHUNK_EXEC_TIME = 1000L; // 1us + private static final long TASK_EXECUTION_FAILURE_BACKOFF = 5L * 1000L; // 5us + +- private long lastMidTickExecute; +- private long lastMidTickExecuteFailure; +- +- private boolean tickMidTickTasks() { +- // give all worlds a fair chance at by targeting them all. +- // if we execute too many tasks, that's fine - we have logic to correctly handle overuse of allocated time. +- boolean executed = false; +- for (final ServerLevel world : this.getAllLevels()) { +- long currTime = System.nanoTime(); +- if (currTime - ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$getLastMidTickFailure() <= TASK_EXECUTION_FAILURE_BACKOFF) { +- continue; +- } +- if (!world.getChunkSource().pollTask()) { +- // we need to back off if this fails +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)world).moonrise$setLastMidTickFailure(currTime); +- } else { +- executed = true; +- } +- } +- +- return executed; ++ // Folia - region threading - moved to regionized data ++ ++ private boolean tickMidTickTasks(final io.papermc.paper.threadedregions.RegionizedWorldData worldData) { // Folia - region threading ++ return worldData.world.getChunkSource().pollTask(); // Folia - region threading + } + + @Override + public final void moonrise$executeMidTickTasks() { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading + final long startTime = System.nanoTime(); +- if ((startTime - this.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - this.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) { ++ if ((startTime - worldData.lastMidTickExecute) <= CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME || (startTime - worldData.lastMidTickExecuteFailure) <= TASK_EXECUTION_FAILURE_BACKOFF) { // Folia - region threading + // it's shown to be bad to constantly hit the queue (chunk loads slow to a crawl), even if no tasks are executed. + // so, backoff to prevent this + return; + } + + for (;;) { +- final boolean moreTasks = this.tickMidTickTasks(); ++ final boolean moreTasks = this.tickMidTickTasks(worldData); // Folia - region threading + final long currTime = System.nanoTime(); + final long diff = currTime - startTime; + + if (!moreTasks || diff >= MAX_CHUNK_EXEC_TIME) { + if (!moreTasks) { +- this.lastMidTickExecuteFailure = currTime; ++ worldData.lastMidTickExecuteFailure = currTime; // Folia - region threading + } + + // note: negative values reduce the time +@@ -384,7 +_,7 @@ + final double overuseCount = (double)overuse/(double)MAX_CHUNK_EXEC_TIME; + final long extraSleep = (long)Math.round(overuseCount*CHUNK_TASK_QUEUE_BACKOFF_MIN_TIME); + +- this.lastMidTickExecute = currTime + extraSleep; ++ worldData.lastMidTickExecute = currTime + extraSleep; // Folia - region threading + return; + } + } +@@ -710,7 +_,21 @@ + // Back to the createLevels method without crazy modifications + primaryLevelData.setModdedInfo(this.getServerModName(), this.getModdedStatus().shouldReportAsModified()); + this.addLevel(serverLevel); // Paper - Put world into worldlist before initing the world; move up +- this.initWorld(serverLevel, primaryLevelData, this.worldData, worldOptions); ++ // Folia start - region threading ++ // the spawn should be within ~1024 blocks, so we force add ticket levels to ensure the first thread ++ // to init spawn will not run into any ownership issues ++ // move init to start of tickServer ++ int loadRegionRadius = 1024 >> 4; ++ serverLevel.randomSpawnSelection = new ChunkPos(serverLevel.getChunkSource().randomState().sampler().findSpawnPosition()); ++ for (int currX = -loadRegionRadius; currX <= loadRegionRadius; ++currX) { ++ for (int currZ = -loadRegionRadius; currZ <= loadRegionRadius; ++currZ) { ++ ChunkPos pos = new ChunkPos(currX, currZ); ++ serverLevel.chunkSource.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.UNKNOWN, pos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, pos ++ ); ++ } ++ } ++ // Folia end - region threading + + // Paper - Put world into worldlist before initing the world; move up + this.getPlayerList().addWorldborderListener(serverLevel); +@@ -723,6 +_,7 @@ + for (ServerLevel serverLevel : this.getAllLevels()) { + this.prepareLevels(serverLevel.getChunkSource().chunkMap.progressListener, serverLevel); + // Paper - rewrite chunk system ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addWorld(serverLevel); // Folia - region threading + this.server.getPluginManager().callEvent(new org.bukkit.event.world.WorldLoadEvent(serverLevel.getWorld())); + } + +@@ -804,10 +_,11 @@ + } + } + // CraftBukkit end +- ChunkPos chunkPos = new ChunkPos(chunkSource.randomState().sampler().findSpawnPosition()); // Paper - Only attempt to find spawn position if there isn't a fixed spawn position set ++ ChunkPos chunkPos = level.randomSpawnSelection; // Paper - Only attempt to find spawn position if there isn't a fixed spawn position set // Folia - region threading + int spawnHeight = chunkSource.getGenerator().getSpawnHeight(level); + if (spawnHeight < level.getMinY()) { + BlockPos worldPosition = chunkPos.getWorldPosition(); ++ level.getChunk(worldPosition.offset(8, 0, 8)); // Folia - region threading - sync load first + spawnHeight = level.getHeight(Heightmap.Types.WORLD_SURFACE, worldPosition.getX() + 8, worldPosition.getZ() + 8); + } + +@@ -869,14 +_,10 @@ + int _int = serverLevel.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS); // CraftBukkit - per-world + int i = _int > 0 ? Mth.square(ChunkProgressListener.calculateDiameter(_int)) : 0; + +- while (chunkSource.getTickingGenerated() < i) { +- // CraftBukkit start +- // this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS; +- this.executeModerately(); +- } ++ // Folia - region threading + + // this.nextTickTimeNanos = Util.getNanos() + PREPARE_LEVELS_DEFAULT_DELAY_NANOS; +- this.executeModerately(); ++ //this.executeModerately(); // Folia - region threading + + if (true) { + ServerLevel serverLevel1 = serverLevel; +@@ -895,7 +_,7 @@ + + // CraftBukkit start + // this.nextTickTimeNanos = SystemUtils.getNanos() + MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS; +- this.executeModerately(); ++ //this.executeModerately(); // Folia - region threading + // CraftBukkit end + listener.stop(); + // CraftBukkit start +@@ -985,7 +_,37 @@ + } + // CraftBukkit end + ++ // Folia start - region threading ++ private final java.util.concurrent.atomic.AtomicBoolean hasStartedShutdownThread = new java.util.concurrent.atomic.AtomicBoolean(); ++ ++ private void haltServerRegionThreading() { ++ if (this.hasStartedShutdownThread.getAndSet(true)) { ++ // already started shutdown ++ return; ++ } ++ new io.papermc.paper.threadedregions.RegionShutdownThread("Region shutdown thread").start(); ++ } ++ ++ public void haltCurrentRegion() { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isShutdownThread()) { ++ throw new IllegalStateException(); ++ } ++ } ++ // Folia end - region threading ++ + public void stopServer() { ++ // Folia start - region threading ++ // halt scheduler ++ // don't wait, we may be on a scheduler thread ++ io.papermc.paper.threadedregions.TickRegions.getScheduler().halt(false, 0L); ++ // cannot run shutdown logic on this thread, as it may be a scheduler ++ if (true) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isShutdownThread()) { ++ this.haltServerRegionThreading(); ++ return; ++ } // else: fall through to regular stop logic ++ } ++ // Folia end - region threading + // CraftBukkit start - prevent double stopping on multiple threads + synchronized(this.stopLock) { + if (this.hasStopped) return; +@@ -1012,12 +_,19 @@ + this.getConnection().stop(); + this.isSaving = true; + if (this.playerList != null) { +- LOGGER.info("Saving players"); +- this.playerList.saveAll(); ++ //LOGGER.info("Saving players"); // Folia - move to shutdown thread logic ++ //this.playerList.saveAll(); // Folia - move to shutdown thread logic + this.playerList.removeAll(this.isRestarting); // Paper + try { Thread.sleep(100); } catch (InterruptedException ex) {} // CraftBukkit - SPIGOT-625 - give server at least a chance to send packets + } + ++ // Folia start - region threading ++ if (true) { ++ // the rest till part 2 is handled by the region shutdown thread ++ return; ++ } ++ // Folia end - region threading ++ + LOGGER.info("Saving worlds"); + + for (ServerLevel serverLevel : this.getAllLevels()) { +@@ -1040,6 +_,11 @@ + this.saveAllChunks(false, true, false, true); // Paper - rewrite chunk system + + this.isSaving = false; ++ // Folia start - region threading ++ this.stopPart2(); ++ } ++ public void stopPart2() { ++ // Folia end - region threading + this.resources.close(); + + try { +@@ -1098,6 +_,7 @@ + if (isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper - Debugging + // Paper end + this.running = false; ++ this.stopServer(); // Folia - region threading + if (waitForServer) { + try { + this.serverThread.join(); +@@ -1169,6 +_,18 @@ + this.status = this.buildServerStatus(); + + this.server.spark.enableBeforePlugins(); // Paper - spark ++ // Folia start - region threading ++ if (true) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().init(); // Folia - region threading - only after loading worlds ++ final long actualDoneTimeMs = System.currentTimeMillis() - org.bukkit.craftbukkit.Main.BOOT_TIME.toEpochMilli(); // Paper - Improve startup message ++ LOGGER.info("Done ({})! For help, type \"help\"", String.format(java.util.Locale.ROOT, "%.3fs", actualDoneTimeMs / 1000.00D)); // Paper - Improve startup message ++ for (;;) { ++ try { ++ Thread.sleep(Long.MAX_VALUE); ++ } catch (final InterruptedException ex) {} ++ } ++ } ++ // Folia end - region threading + // Spigot start + // Paper start + LOGGER.info("Running delayed init tasks"); +@@ -1218,7 +_,7 @@ + // Spigot start + // Paper start - further improve server tick loop + currentTime = Util.getNanos(); +- if (++MinecraftServer.currentTick % MinecraftServer.SAMPLE_INTERVAL == 0) { ++ if (false) { // Folia - region threading + final long diff = currentTime - tickSection; + final java.math.BigDecimal currentTps = TPS_BASE.divide(new java.math.BigDecimal(diff), 30, java.math.RoundingMode.HALF_UP); + tps1.add(currentTps, diff); +@@ -1237,7 +_,7 @@ + boolean flag = l == 0L; + if (this.debugCommandProfilerDelayStart) { + this.debugCommandProfilerDelayStart = false; +- this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount); ++ //this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount); // Folia - region threading + } + + //MinecraftServer.currentTick = (int) (System.currentTimeMillis() / 50); // CraftBukkit // Paper - don't overwrite current tick time +@@ -1248,7 +_,7 @@ + ProfilerFiller profilerFiller = Profiler.get(); + profilerFiller.push("tick"); + this.tickFrame.start(); +- this.tickServer(flag ? () -> false : this::haveTime); ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + // Paper start - rewrite chunk system + final Throwable crash = this.chunkSystemCrash; + if (crash != null) { +@@ -1403,28 +_,24 @@ + + @Override + public TickTask wrapRunnable(Runnable runnable) { +- // Paper start - anything that does try to post to main during watchdog crash, run on watchdog +- if (this.hasStopped && Thread.currentThread().equals(shutdownThread)) { +- runnable.run(); +- runnable = () -> {}; +- } +- // Paper end +- return new TickTask(this.tickCount, runnable); ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + @Override + protected boolean shouldRun(TickTask runnable) { +- return runnable.getTick() + 3 < this.tickCount || this.haveTime(); ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + @Override + public boolean pollTask() { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + boolean flag = this.pollTaskInternal(); + this.mayHaveDelayedTasks = flag; + return flag; + } + + private boolean pollTaskInternal() { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + if (super.pollTask()) { + this.moonrise$executeMidTickTasks(); // Paper - rewrite chunk system + return true; +@@ -1444,6 +_,7 @@ + + @Override + public void doRunTask(TickTask task) { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + Profiler.get().incrementCounter("runTask"); + super.doRunTask(task); + } +@@ -1485,12 +_,15 @@ + return false; + } + +- public void tickServer(BooleanSupplier hasTimeLeft) { ++ // Folia start - region threading ++ public void tickServer(long startTime, long scheduledEnd, long targetBuffer, ++ io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { ++ // Folia end - region threading + org.spigotmc.WatchdogThread.tick(); // Spigot +- long nanos = Util.getNanos(); ++ long nanos = startTime; // Folia - region threading + int i = this.pauseWhileEmptySeconds() * 20; + this.removeDisabledPluginsBlockingSleep(); // Paper - API to allow/disallow tick sleeping +- if (i > 0) { ++ if (false && i > 0) { // Folia - region threading - this is complicated to implement, and even if done correctly is messy + if (this.playerList.getPlayerCount() == 0 && !this.tickRateManager.isSprinting() && this.pluginsBlockingSleep.isEmpty()) { // Paper - API to allow/disallow tick sleeping + this.emptyTicks++; + } else { +@@ -1515,24 +_,58 @@ + level.getChunkSource().tick(() -> true, false); + } + // Paper end - avoid issues with certain tasks not processing during sleep +- this.server.spark.executeMainThreadTasks(); // Paper - spark ++ //this.server.spark.executeMainThreadTasks(); // Paper - spark // Folia - region threading + this.tickConnection(); + this.server.spark.tickEnd(((double)(System.nanoTime() - lastTick) / 1000000D)); // Paper - spark + return; + } + } + ++ // Folia start - region threading ++ region.world.getCurrentWorldData().updateTickData(); ++ if (region.world.checkInitialised.get() != ServerLevel.WORLD_INIT_CHECKED) { ++ synchronized (region.world.checkInitialised) { ++ if (region.world.checkInitialised.compareAndSet(ServerLevel.WORLD_INIT_NOT_CHECKED, ServerLevel.WORLD_INIT_CHECKING)) { ++ LOGGER.info("Initialising world '" + region.world.getWorld().getName() + "' before it can be ticked..."); ++ this.initWorld(region.world, region.world.serverLevelData, worldData, region.world.serverLevelData.worldGenOptions()); // Folia - delayed until first tick of world ++ region.world.checkInitialised.set(ServerLevel.WORLD_INIT_CHECKED); ++ LOGGER.info("Initialised world '" + region.world.getWorld().getName() + "'"); ++ } // else: must be checked ++ } ++ } ++ BooleanSupplier hasTimeLeft = () -> { ++ return scheduledEnd - System.nanoTime() > targetBuffer; ++ }; ++ // Folia end - region threading ++ + this.server.spark.tickStart(); // Paper - spark +- new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events +- this.tickCount++; +- this.tickRateManager.tick(); +- this.tickChildren(hasTimeLeft); +- if (nanos - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) { ++ new com.destroystokyo.paper.event.server.ServerTickStartEvent((int)region.getCurrentTick()).callEvent(); // Paper - Server Tick Events // Folia - region threading ++ // Folia start - region threading ++ if (region != null) { ++ region.getTaskQueueData().drainTasks(); ++ ((io.papermc.paper.threadedregions.scheduler.FoliaRegionScheduler)org.bukkit.Bukkit.getRegionScheduler()).tick(); ++ // now run all the entity schedulers ++ // TODO there has got to be a more efficient variant of this crap ++ for (net.minecraft.world.entity.Entity entity : region.world.getCurrentWorldData().getLocalEntitiesCopy()) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) || entity.isRemoved()) { ++ continue; ++ } ++ org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); ++ if (bukkit != null) { ++ bukkit.taskScheduler.executeTick(); ++ } ++ } ++ } ++ // Folia end - region threading ++ //this.tickCount++; // Folia - region threading ++ //this.tickRateManager.tick(); // Folia - region threading ++ this.tickChildren(hasTimeLeft, region); // Folia - region threading ++ if (false && nanos - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) { // Folia - region threading + this.lastServerStatus = nanos; + this.status = this.buildServerStatus(); + } + +- this.ticksUntilAutosave--; ++ //this.ticksUntilAutosave--; // Folia - region threading + // Paper start - Incremental chunk and player saving + final ProfilerFiller profiler = Profiler.get(); + int playerSaveInterval = io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.rate; +@@ -1540,15 +_,15 @@ + playerSaveInterval = autosavePeriod; + } + profiler.push("save"); +- final boolean fullSave = autosavePeriod > 0 && this.tickCount % autosavePeriod == 0; ++ final boolean fullSave = autosavePeriod > 0 && io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() % autosavePeriod == 0; // Folia - region threading + try { + this.isSaving = true; + if (playerSaveInterval > 0) { + this.playerList.saveAll(playerSaveInterval); + } +- for (final ServerLevel level : this.getAllLevels()) { ++ for (final ServerLevel level : (region == null ? this.getAllLevels() : Arrays.asList(region.world))) { // Folia - region threading + if (level.paperConfig().chunks.autoSaveInterval.value() > 0) { +- level.saveIncrementally(fullSave); ++ level.saveIncrementally(region == null && fullSave); // Folia - region threading - don't save level.dat + } + } + } finally { +@@ -1558,32 +_,19 @@ + // Paper end - Incremental chunk and player saving + + ProfilerFiller profilerFiller = Profiler.get(); +- this.runAllTasks(); // Paper - move runAllTasks() into full server tick (previously for timings) +- this.server.spark.executeMainThreadTasks(); // Paper - spark ++ //this.runAllTasks(); // Paper - move runAllTasks() into full server tick (previously for timings) // Folia - region threading ++ //this.server.spark.executeMainThreadTasks(); // Paper - spark // Folia - region threading + // Paper start - Server Tick Events + long endTime = System.nanoTime(); +- long remaining = (TICK_TIME - (endTime - lastTick)) - catchupTime; +- new com.destroystokyo.paper.event.server.ServerTickEndEvent(this.tickCount, ((double)(endTime - lastTick) / 1000000D), remaining).callEvent(); ++ long remaining = scheduledEnd - endTime; // Folia - region ticking ++ new com.destroystokyo.paper.event.server.ServerTickEndEvent((int)io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(), ((double)(endTime - startTime) / 1000000D), remaining).callEvent(); // Folia - region ticking + // Paper end - Server Tick Events +- this.server.spark.tickEnd(((double)(endTime - lastTick) / 1000000D)); // Paper - spark +- profilerFiller.push("tallying"); +- long l = Util.getNanos() - nanos; +- int i1 = this.tickCount % 100; +- this.aggregatedTickTimesNanos = this.aggregatedTickTimesNanos - this.tickTimesNanos[i1]; +- this.aggregatedTickTimesNanos += l; +- this.tickTimesNanos[i1] = l; +- this.smoothedTickTimeMillis = this.smoothedTickTimeMillis * 0.8F + (float)l / (float)TimeUtil.NANOSECONDS_PER_MILLISECOND * 0.19999999F; +- // Paper start - Add tick times API and /mspt command +- this.tickTimes5s.add(this.tickCount, l); +- this.tickTimes10s.add(this.tickCount, l); +- this.tickTimes60s.add(this.tickCount, l); +- // Paper end - Add tick times API and /mspt command +- this.logTickMethodTime(nanos); +- profilerFiller.pop(); ++ this.server.spark.tickEnd(((double)(endTime - startTime) / 1000000D)); // Paper - spark // Folia - region threading ++ // Folia - region threading + } + + private void autoSave() { +- this.ticksUntilAutosave = this.autosavePeriod; // CraftBukkit ++ //this.ticksUntilAutosave = this.autosavePeriod; // CraftBukkit // Folia - region threading + LOGGER.debug("Autosave started"); + ProfilerFiller profilerFiller = Profiler.get(); + profilerFiller.push("save"); +@@ -1598,30 +_,22 @@ + } + } + +- private int computeNextAutosaveInterval() { +- float f; +- if (this.tickRateManager.isSprinting()) { +- long l = this.getAverageTickTimeNanos() + 1L; +- f = (float)TimeUtil.NANOSECONDS_PER_SECOND / (float)l; +- } else { +- f = this.tickRateManager.tickrate(); +- } +- +- int i = 300; +- return Math.max(100, (int)(f * 300.0F)); +- } ++ // Folia - region threading - use absolute time instead of this + + public void onTickRateChanged() { +- int i = this.computeNextAutosaveInterval(); +- if (i < this.ticksUntilAutosave) { +- this.ticksUntilAutosave = i; +- } ++ // Folia - region threading - use absolute time instead of this + } + + protected abstract SampleLogger getTickTimeLogger(); + + public abstract boolean isTickTimeLoggingEnabled(); + ++ // Folia start - region threading ++ public void rebuildServerStatus() { ++ this.status = this.buildServerStatus(); ++ } ++ // Folia end - region threading ++ + private ServerStatus buildServerStatus() { + ServerStatus.Players players = this.buildPlayerStatus(); + return new ServerStatus( +@@ -1634,7 +_,7 @@ + } + + private ServerStatus.Players buildPlayerStatus() { +- List players = this.playerList.getPlayers(); ++ List players = new java.util.ArrayList<>(this.playerList.getPlayers()); // Folia - region threading + int maxPlayers = this.getMaxPlayers(); + if (this.hidesOnlinePlayers()) { + return new ServerStatus.Players(maxPlayers, players.size(), List.of()); +@@ -1653,44 +_,34 @@ + } + } + +- protected void tickChildren(BooleanSupplier hasTimeLeft) { ++ protected void tickChildren(BooleanSupplier hasTimeLeft, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - region threading ++ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - regionised ticking + ProfilerFiller profilerFiller = Profiler.get(); +- this.getPlayerList().getPlayers().forEach(serverPlayer1 -> serverPlayer1.connection.suspendFlushing()); +- this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit ++ //this.getPlayerList().getPlayers().forEach(serverPlayer1 -> serverPlayer1.connection.suspendFlushing()); // Folia - region threading ++ //this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit // Folia - region threading + // Paper start - Folia scheduler API +- ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) org.bukkit.Bukkit.getGlobalRegionScheduler()).tick(); +- getAllLevels().forEach(level -> { +- for (final net.minecraft.world.entity.Entity entity : level.getEntities().getAll()) { +- if (entity.isRemoved()) { +- continue; +- } +- final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); +- if (bukkit != null) { +- bukkit.taskScheduler.executeTick(); +- } +- } +- }); ++ // Folia - region threading - moved to global tick - and moved entity scheduler to tickRegion + // Paper end - Folia scheduler API +- io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper ++ //io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper // Folia - region threading - moved to global tick + profilerFiller.push("commandFunctions"); +- this.getFunctions().tick(); ++ //this.getFunctions().tick(); // Folia - region threading - TODO Purge functions + profilerFiller.popPush("levels"); + + // CraftBukkit start + // Run tasks that are waiting on processing +- while (!this.processQueue.isEmpty()) { ++ if (false) while (!this.processQueue.isEmpty()) { // Folia - region threading + this.processQueue.remove().run(); + } + + // Send time updates to everyone, it will get the right time from the world the player is in. + // Paper start - Perf: Optimize time updates +- for (final ServerLevel level : this.getAllLevels()) { ++ for (final ServerLevel level : Arrays.asList(region.world)) { // Folia - region threading + final boolean doDaylight = level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT); + final long dayTime = level.getDayTime(); + long worldTime = level.getGameTime(); + final ClientboundSetTimePacket worldPacket = new ClientboundSetTimePacket(worldTime, dayTime, doDaylight); +- for (Player entityhuman : level.players()) { +- if (!(entityhuman instanceof ServerPlayer) || (tickCount + entityhuman.getId()) % 20 != 0) { ++ for (Player entityhuman : level.getLocalPlayers()) { // Folia - region threading ++ if (!(entityhuman instanceof ServerPlayer) || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + entityhuman.getId()) % 20 != 0) { // Folia - region threading + continue; + } + ServerPlayer entityplayer = (ServerPlayer) entityhuman; +@@ -1703,12 +_,9 @@ + } + } + +- this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked +- for (ServerLevel serverLevel : this.getAllLevels()) { +- serverLevel.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent +- serverLevel.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent +- serverLevel.updateLagCompensationTick(); // Paper - lag compensation +- net.minecraft.world.level.block.entity.HopperBlockEntity.skipHopperEvents = serverLevel.paperConfig().hopper.disableMoveEvent || org.bukkit.event.inventory.InventoryMoveItemEvent.getHandlerList().getRegisteredListeners().length == 0; // Paper - Perf: Optimize Hoppers ++ //this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked // Folia - region threading ++ for (ServerLevel serverLevel : Arrays.asList(region.world)) { // Folia - region threading ++ // Folia - region threading + profilerFiller.push(() -> serverLevel + " " + serverLevel.dimension().location()); + /* Drop global time updates + if (this.tickCount % 20 == 0) { +@@ -1721,7 +_,7 @@ + profilerFiller.push("tick"); + + try { +- serverLevel.tick(hasTimeLeft); ++ serverLevel.tick(hasTimeLeft, region); // Folia - region threading + } catch (Throwable var7) { + CrashReport crashReport = CrashReport.forThrowable(var7, "Exception ticking world"); + serverLevel.fillReportDetails(crashReport); +@@ -1730,27 +_,27 @@ + + profilerFiller.pop(); + profilerFiller.pop(); +- serverLevel.explosionDensityCache.clear(); // Paper - Optimize explosions ++ regionizedWorldData.explosionDensityCache.clear(); // Paper - Optimize explosions // Folia - region threading + } +- this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked ++ //this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked // Folia - region threading + + profilerFiller.popPush("connection"); +- this.tickConnection(); ++ regionizedWorldData.tickConnections(); // Folia - region threading + profilerFiller.popPush("players"); +- this.playerList.tick(); ++ //this.playerList.tick(); // Folia - region threading + if (SharedConstants.IS_RUNNING_IN_IDE && this.tickRateManager.runsNormally()) { + GameTestTicker.SINGLETON.tick(); + } + + profilerFiller.popPush("server gui refresh"); + +- for (int i = 0; i < this.tickables.size(); i++) { ++ if (false) for (int i = 0; i < this.tickables.size(); i++) { // Folia - region threading - TODO WTF is this? + this.tickables.get(i).run(); + } + + profilerFiller.popPush("send chunks"); + +- for (ServerPlayer serverPlayer : this.playerList.getPlayers()) { ++ if (false) for (ServerPlayer serverPlayer : this.playerList.getPlayers()) { // Folia - region threading + serverPlayer.connection.chunkSender.sendNextChunks(serverPlayer); + serverPlayer.connection.resumeFlushing(); + } +@@ -2073,7 +_,7 @@ + } + + public int getTickCount() { +- return this.tickCount; ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + public int getSpawnProtectionRadius() { +@@ -2128,6 +_,15 @@ + } + + public void invalidateStatus() { ++ // Folia start - region threading ++ if (true) { ++ // we don't need this to notify the global tick region ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTaskWithoutNotify(() -> { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().invalidateStatus(); ++ }); ++ return; ++ } ++ // Folia end - region threading + this.lastServerStatus = 0L; + } + +@@ -2142,6 +_,7 @@ + + @Override + public void executeIfPossible(Runnable task) { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + if (this.isStopped()) { + throw new io.papermc.paper.util.ServerStopRejectedExecutionException("Server already shutting down"); // Paper - do not prematurely disconnect players on stop + } else { +@@ -2455,7 +_,12 @@ + } + + public long getAverageTickTimeNanos() { +- return this.aggregatedTickTimesNanos / Math.min(100, Math.max(this.tickCount, 1)); ++ // Folia start - region threading ++ if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentTickingTask() instanceof io.papermc.paper.threadedregions.TickRegionScheduler.RegionScheduleHandle handle) { ++ return (long)Math.ceil(handle.getTickReport5s(System.nanoTime()).timePerTickData().segmentAll().average()); ++ } ++ return 0L; ++ // Folia end - region threading + } + + public long[] getTickTimesNanos() { +@@ -2705,13 +_,7 @@ + } + + public ProfileResults stopTimeProfiler() { +- if (this.debugCommandProfiler == null) { +- return EmptyProfileResults.EMPTY; +- } else { +- ProfileResults profileResults = this.debugCommandProfiler.stop(Util.getNanos(), this.tickCount); +- this.debugCommandProfiler = null; +- return profileResults; +- } ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + public int getMaxChainedNeighborUpdates() { +@@ -2896,24 +_,15 @@ + + // Paper start - API to check if the server is sleeping + public boolean isTickPaused() { +- return this.emptyTicks > 0 && this.emptyTicks >= this.pauseWhileEmptySeconds() * 20; ++ return false; // Folia - region threading + } + + public void addPluginAllowingSleep(final String pluginName, final boolean value) { +- if (!value) { +- this.pluginsBlockingSleep.add(pluginName); +- } else { +- this.pluginsBlockingSleep.remove(pluginName); +- } ++ // Folia - region threading + } + + private void removeDisabledPluginsBlockingSleep() { +- if (this.pluginsBlockingSleep.isEmpty()) { +- return; +- } +- this.pluginsBlockingSleep.removeIf(plugin -> ( +- !io.papermc.paper.plugin.manager.PaperPluginManagerImpl.getInstance().isPluginEnabled(plugin) +- )); ++ // Folia - region threading + } + // Paper end - API to check if the server is sleeping + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/AdvancementCommands.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/AdvancementCommands.java.patch new file mode 100644 index 0000000..9ab05d2 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/AdvancementCommands.java.patch @@ -0,0 +1,32 @@ +--- a/net/minecraft/server/commands/AdvancementCommands.java ++++ b/net/minecraft/server/commands/AdvancementCommands.java +@@ -246,7 +_,12 @@ + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +- i += action.perform(serverPlayer, advancements); ++ // Folia start - region threading ++ i += 1; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ action.perform(player, advancements); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { +@@ -310,9 +_,12 @@ + throw ERROR_CRITERION_NOT_FOUND.create(Advancement.name(advancement), criterionName); + } else { + for (ServerPlayer serverPlayer : targets) { +- if (action.performCriterion(serverPlayer, advancement, criterionName)) { +- i++; +- } ++ // Folia start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ action.performCriterion(player, advancement, criterionName); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/AttributeCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/AttributeCommand.java.patch new file mode 100644 index 0000000..6795ac5 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/AttributeCommand.java.patch @@ -0,0 +1,188 @@ +--- a/net/minecraft/server/commands/AttributeCommand.java ++++ b/net/minecraft/server/commands/AttributeCommand.java +@@ -266,30 +_,62 @@ + } + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int getAttributeValue(CommandSourceStack source, Entity entity, Holder attribute, double scale) throws CommandSyntaxException { +- LivingEntity entityWithAttribute = getEntityWithAttribute(entity, attribute); ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ LivingEntity entityWithAttribute = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading + double attributeValue = entityWithAttribute.getAttributeValue(attribute); + source.sendSuccess( +- () -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), entity.getName(), attributeValue), false ++ () -> Component.translatable("commands.attribute.value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), attributeValue), false // Folia - region threading + ); +- return (int)(attributeValue * scale); ++ return; // Folia - region threading ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static int getAttributeBase(CommandSourceStack source, Entity entity, Holder attribute, double scale) throws CommandSyntaxException { +- LivingEntity entityWithAttribute = getEntityWithAttribute(entity, attribute); ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ LivingEntity entityWithAttribute = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading + double attributeBaseValue = entityWithAttribute.getAttributeBaseValue(attribute); + source.sendSuccess( +- () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), entity.getName(), attributeBaseValue), ++ () -> Component.translatable("commands.attribute.base_value.get.success", getAttributeDescription(attribute), nmsEntity.getName(), attributeBaseValue), // Folia - region threading + false + ); +- return (int)(attributeBaseValue * scale); ++ return; // Folia - region threading ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static int getAttributeModifier(CommandSourceStack source, Entity entity, Holder attribute, ResourceLocation id, double scale) throws CommandSyntaxException { +- LivingEntity entityWithAttribute = getEntityWithAttribute(entity, attribute); ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ LivingEntity entityWithAttribute = getEntityWithAttribute(nmsEntity, attribute); // Folia - region threading + AttributeMap attributes = entityWithAttribute.getAttributes(); + if (!attributes.hasModifier(attribute, id)) { +- throw ERROR_NO_SUCH_MODIFIER.create(entity.getName(), getAttributeDescription(attribute), id); ++ throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading + } else { + double modifierValue = attributes.getModifierValue(attribute, id); + source.sendSuccess( +@@ -297,13 +_,20 @@ + "commands.attribute.modifier.value.get.success", + Component.translationArg(id), + getAttributeDescription(attribute), +- entity.getName(), ++ nmsEntity.getName(), // Folia - region threading + modifierValue + ), + false + ); +- return (int)(modifierValue * scale); ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static Stream getAttributeModifiers(Entity entity, Holder attribute) throws CommandSyntaxException { +@@ -312,11 +_,22 @@ + } + + private static int setAttributeBase(CommandSourceStack source, Entity entity, Holder attribute, double value) throws CommandSyntaxException { +- getAttributeInstance(entity, attribute).setBaseValue(value); ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ getAttributeInstance(nmsEntity, attribute).setBaseValue(value); // Folia - region threading + source.sendSuccess( +- () -> Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), entity.getName(), value), false ++ () -> Component.translatable("commands.attribute.base_value.set.success", getAttributeDescription(attribute), nmsEntity.getName(), value), false // Folia - region threading + ); +- return 1; ++ return; // Folia - region threading ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static int resetAttributeBase(CommandSourceStack source, Entity entity, Holder attribute) throws CommandSyntaxException { +@@ -338,35 +_,57 @@ + private static int addModifier( + CommandSourceStack source, Entity entity, Holder attribute, ResourceLocation id, double amount, AttributeModifier.Operation operation + ) throws CommandSyntaxException { +- AttributeInstance attributeInstance = getAttributeInstance(entity, attribute); ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); // Folia - region threading + AttributeModifier attributeModifier = new AttributeModifier(id, amount, operation); + if (attributeInstance.hasModifier(id)) { +- throw ERROR_MODIFIER_ALREADY_PRESENT.create(entity.getName(), getAttributeDescription(attribute), id); ++ throw ERROR_MODIFIER_ALREADY_PRESENT.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading + } else { + attributeInstance.addPermanentModifier(attributeModifier); + source.sendSuccess( + () -> Component.translatable( +- "commands.attribute.modifier.add.success", Component.translationArg(id), getAttributeDescription(attribute), entity.getName() ++ "commands.attribute.modifier.add.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName() // Folia - region threading + ), + false + ); +- return 1; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static int removeModifier(CommandSourceStack source, Entity entity, Holder attribute, ResourceLocation id) throws CommandSyntaxException { +- AttributeInstance attributeInstance = getAttributeInstance(entity, attribute); ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ AttributeInstance attributeInstance = getAttributeInstance(nmsEntity, attribute); // Folia - region threading + if (attributeInstance.removeModifier(id)) { + source.sendSuccess( + () -> Component.translatable( +- "commands.attribute.modifier.remove.success", Component.translationArg(id), getAttributeDescription(attribute), entity.getName() ++ "commands.attribute.modifier.remove.success", Component.translationArg(id), getAttributeDescription(attribute), nmsEntity.getName() // Folia - region threading + ), + false + ); +- return 1; ++ return; // Folia - region threading + } else { +- throw ERROR_NO_SUCH_MODIFIER.create(entity.getName(), getAttributeDescription(attribute), id); ++ throw ERROR_NO_SUCH_MODIFIER.create(nmsEntity.getName(), getAttributeDescription(attribute), id); // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + + private static Component getAttributeDescription(Holder attribute) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/ClearInventoryCommands.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/ClearInventoryCommands.java.patch new file mode 100644 index 0000000..6612476 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/ClearInventoryCommands.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/server/commands/ClearInventoryCommands.java ++++ b/net/minecraft/server/commands/ClearInventoryCommands.java +@@ -65,9 +_,14 @@ + int i = 0; + + for (ServerPlayer serverPlayer : targetPlayers) { +- i += serverPlayer.getInventory().clearOrCountMatchingItems(itemPredicate, maxCount, serverPlayer.inventoryMenu.getCraftSlots()); +- serverPlayer.containerMenu.broadcastChanges(); +- serverPlayer.inventoryMenu.slotsChanged(serverPlayer.getInventory()); ++ // Folia start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ player.getInventory().clearOrCountMatchingItems(itemPredicate, maxCount, player.inventoryMenu.getCraftSlots()); ++ player.containerMenu.broadcastChanges(); ++ player.inventoryMenu.slotsChanged(player.getInventory()); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/DamageCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/DamageCommand.java.patch new file mode 100644 index 0000000..8eae6c4 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/DamageCommand.java.patch @@ -0,0 +1,35 @@ +--- a/net/minecraft/server/commands/DamageCommand.java ++++ b/net/minecraft/server/commands/DamageCommand.java +@@ -102,12 +_,29 @@ + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int damage(CommandSourceStack source, Entity target, float amount, DamageSource damageType) throws CommandSyntaxException { +- if (target.hurtServer(source.getLevel(), damageType, amount)) { +- source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, target.getDisplayName()), true); +- return 1; ++ // Folia start - region threading ++ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ try { ++ // Folia end - region threading ++ if (nmsEntity.hurtServer(source.getLevel(), damageType, amount)) { // Folia - region threading ++ source.sendSuccess(() -> Component.translatable("commands.damage.success", amount, nmsEntity.getDisplayName()), true); // Folia - region threading ++ return; // Folia - region threading + } else { + throw ERROR_INVULNERABLE.create(); + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }, null, 1L); ++ return 0; ++ // Folia end - region threading + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/DefaultGameModeCommands.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/DefaultGameModeCommands.java.patch new file mode 100644 index 0000000..0ad0a21 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/DefaultGameModeCommands.java.patch @@ -0,0 +1,18 @@ +--- a/net/minecraft/server/commands/DefaultGameModeCommands.java ++++ b/net/minecraft/server/commands/DefaultGameModeCommands.java +@@ -28,12 +_,14 @@ + GameType forcedGameType = server.getForcedGameType(); + if (forcedGameType != null) { + for (ServerPlayer serverPlayer : server.getPlayerList().getPlayers()) { ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading + // Paper start - Expand PlayerGameModeChangeEvent +- org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); ++ org.bukkit.event.player.PlayerGameModeChangeEvent event = player.setGameMode(gamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.empty()); // Folia - region threading + if (event != null && event.isCancelled()) { + commandSource.sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false); + } + // Paper end - Expand PlayerGameModeChangeEvent ++ }, null, 1L); // Folia - region threading + i++; + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/EffectCommands.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/EffectCommands.java.patch new file mode 100644 index 0000000..5297213 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/EffectCommands.java.patch @@ -0,0 +1,44 @@ +--- a/net/minecraft/server/commands/EffectCommands.java ++++ b/net/minecraft/server/commands/EffectCommands.java +@@ -180,7 +_,12 @@ + for (Entity entity : targets) { + if (entity instanceof LivingEntity) { + MobEffectInstance mobEffectInstance = new MobEffectInstance(effect, i1, amplifier, false, showParticles); +- if (((LivingEntity)entity).addEffect(mobEffectInstance, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ ((LivingEntity)nmsEntity).addEffect(mobEffectInstance, source.getEntity(), org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Folia end - region threading ++ if (true) { // CraftBukkit // Folia - region threading + i++; + } + } +@@ -210,7 +_,12 @@ + int i = 0; + + for (Entity entity : targets) { +- if (entity instanceof LivingEntity && ((LivingEntity)entity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit ++ if (entity instanceof LivingEntity && true) { // CraftBukkit // Folia - region threading ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ ((LivingEntity)nmsEntity).removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Folia end - region threading + i++; + } + } +@@ -235,7 +_,12 @@ + int i = 0; + + for (Entity entity : targets) { +- if (entity instanceof LivingEntity && ((LivingEntity)entity).removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND)) { // CraftBukkit ++ if (entity instanceof LivingEntity && true) { // CraftBukkit // Folia - region threading ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ ((LivingEntity)nmsEntity).removeEffect(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.COMMAND); ++ }, null, 1L); ++ // Folia end - region threading + i++; + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/EnchantCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/EnchantCommand.java.patch new file mode 100644 index 0000000..449d1e4 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/EnchantCommand.java.patch @@ -0,0 +1,100 @@ +--- a/net/minecraft/server/commands/EnchantCommand.java ++++ b/net/minecraft/server/commands/EnchantCommand.java +@@ -68,51 +_,78 @@ + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int enchant(CommandSourceStack source, Collection targets, Holder enchantment, int level) throws CommandSyntaxException { + Enchantment enchantment1 = enchantment.value(); + if (level > enchantment1.getMaxLevel()) { + throw ERROR_LEVEL_TOO_HIGH.create(level, enchantment1.getMaxLevel()); + } else { +- int i = 0; ++ final java.util.concurrent.atomic.AtomicInteger changed = new java.util.concurrent.atomic.AtomicInteger(0); // Folia - region threading ++ final java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(targets.size()); // Folia - region threading ++ final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName = new java.util.concurrent.atomic.AtomicReference<>(); // Folia - region threading + + for (Entity entity : targets) { + if (entity instanceof LivingEntity) { +- LivingEntity livingEntity = (LivingEntity)entity; +- ItemStack mainHandItem = livingEntity.getMainHandItem(); +- if (!mainHandItem.isEmpty()) { +- if (enchantment1.canEnchant(mainHandItem) +- && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(mainHandItem).keySet(), enchantment)) { +- mainHandItem.enchant(enchantment, level); +- i++; +- } else if (targets.size() == 1) { +- throw ERROR_INCOMPATIBLE.create(mainHandItem.getHoverName().getString()); ++ // Folia start - region threading ++ entity.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { ++ try { ++ LivingEntity livingEntity = (LivingEntity)nmsEntity; ++ ItemStack mainHandItem = livingEntity.getMainHandItem(); ++ if (!mainHandItem.isEmpty()) { ++ if (enchantment1.canEnchant(mainHandItem) ++ && EnchantmentHelper.isEnchantmentCompatible(EnchantmentHelper.getEnchantmentsForCrafting(mainHandItem).keySet(), enchantment)) { ++ mainHandItem.enchant(enchantment, level); ++ possibleSingleDisplayName.set(livingEntity.getDisplayName()); ++ changed.incrementAndGet(); ++ } else if (targets.size() == 1) { ++ throw ERROR_INCOMPATIBLE.create(mainHandItem.getHoverName().getString()); ++ } ++ } else if (targets.size() == 1) { ++ throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); ++ } ++ } catch (final CommandSyntaxException exception) { ++ sendMessage(source, exception); ++ return; // don't send feedback twice + } +- } else if (targets.size() == 1) { +- throw ERROR_NO_ITEM.create(livingEntity.getName().getString()); +- } ++ sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed); ++ }, ignored -> sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed), 1L); + } else if (targets.size() == 1) { + throw ERROR_NOT_LIVING_ENTITY.create(entity.getName().getString()); ++ } else { ++ sendFeedback(source, enchantment, level, possibleSingleDisplayName, count, changed); ++ // Folia end - region threading + } + } ++ return targets.size(); // Folia - region threading ++ } ++ } + ++ // Folia start - region threading ++ private static void sendFeedback(final CommandSourceStack source, final Holder enchantment, final int level, final java.util.concurrent.atomic.AtomicReference possibleSingleDisplayName, final java.util.concurrent.atomic.AtomicInteger count, final java.util.concurrent.atomic.AtomicInteger changed) { ++ if (count.decrementAndGet() == 0) { ++ final int i = changed.get(); + if (i == 0) { +- throw ERROR_NOTHING_HAPPENED.create(); ++ sendMessage(source, ERROR_NOTHING_HAPPENED.create()); + } else { +- if (targets.size() == 1) { ++ if (i == 1) { + source.sendSuccess( + () -> Component.translatable( +- "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), targets.iterator().next().getDisplayName() ++ "commands.enchant.success.single", Enchantment.getFullname(enchantment, level), possibleSingleDisplayName.get() + ), + true + ); + } else { + source.sendSuccess( +- () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), targets.size()), true ++ () -> Component.translatable("commands.enchant.success.multiple", Enchantment.getFullname(enchantment, level), i), true + ); + } +- +- return i; + } + } + } ++ // Folia end - region threading + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/ExperienceCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/ExperienceCommand.java.patch new file mode 100644 index 0000000..fce632c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/ExperienceCommand.java.patch @@ -0,0 +1,39 @@ +--- a/net/minecraft/server/commands/ExperienceCommand.java ++++ b/net/minecraft/server/commands/ExperienceCommand.java +@@ -131,14 +_,18 @@ + } + + private static int queryExperience(CommandSourceStack source, ServerPlayer player, ExperienceCommand.Type type) { +- int i = type.query.applyAsInt(player); +- source.sendSuccess(() -> Component.translatable("commands.experience.query." + type.name, player.getDisplayName(), i), false); +- return i; ++ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading ++ int i = type.query.applyAsInt(serverPlayer); // Folia - region threading ++ source.sendSuccess(() -> Component.translatable("commands.experience.query." + type.name, serverPlayer.getDisplayName(), i), false); // Folia - region threading ++ }, null, 1L); // Folia - region threading ++ return 0; // Folia - region threading + } + + private static int addExperience(CommandSourceStack source, Collection targets, int amount, ExperienceCommand.Type type) { + for (ServerPlayer serverPlayer : targets) { +- type.add.accept(serverPlayer, amount); ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading ++ type.add.accept(player, amount); ++ }, null, 1L); // Folia - region threading + } + + if (targets.size() == 1) { +@@ -157,9 +_,11 @@ + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +- if (type.set.test(serverPlayer, amount)) { +- i++; ++ i++; serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { // Folia - region threading ++ if (type.set.test(player, amount)) { // Folia - region threading ++ //i++; // Folia - region threading + } ++ }, null, 1L); // Folia - region threading + } + + if (i == 0) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillBiomeCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillBiomeCommand.java.patch new file mode 100644 index 0000000..65c8421 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillBiomeCommand.java.patch @@ -0,0 +1,49 @@ +--- a/net/minecraft/server/commands/FillBiomeCommand.java ++++ b/net/minecraft/server/commands/FillBiomeCommand.java +@@ -107,6 +_,16 @@ + return fill(level, from, to, biome, biome1 -> true, message -> {}); + } + ++ // Folia start - region threading ++ private static void sendMessage(Consumer> src, Supplier> supplier) { ++ Either either = supplier.get(); ++ CommandSyntaxException ex = either == null ? null : either.right().orElse(null); ++ if (ex != null) { ++ src.accept(() -> (Component)ex.getRawMessage()); ++ } ++ } ++ // Folia end - region threading ++ + public static Either fill( + ServerLevel level, BlockPos from, BlockPos to, Holder biome, Predicate> filter, Consumer> messageOutput + ) { +@@ -118,6 +_,17 @@ + if (i > _int) { + return Either.right(ERROR_VOLUME_TOO_LARGE.create(_int, i)); + } else { ++ // Folia start - region threading ++ int buffer = 0; // no buffer, we do not touch neighbours ++ level.moonrise$loadChunksAsync( ++ (boundingBox.minX() - buffer) >> 4, ++ (boundingBox.maxX() + buffer) >> 4, ++ (boundingBox.minZ() - buffer) >> 4, ++ (boundingBox.maxZ() + buffer) >> 4, ++ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ sendMessage(messageOutput, () -> { + List list = new ArrayList<>(); + + for (int sectionPosMinZ = SectionPos.blockToSectionCoord(boundingBox.minZ()); +@@ -158,6 +_,11 @@ + ) + ); + return Either.left(mutableInt.getValue()); ++ // Folia start - region threading ++ }); // sendMessage ++ }); // loadChunksASync ++ return Either.left(Integer.valueOf(0)); ++ // Folia end - region threading + } + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillCommand.java.patch new file mode 100644 index 0000000..7628495 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/FillCommand.java.patch @@ -0,0 +1,49 @@ +--- a/net/minecraft/server/commands/FillCommand.java ++++ b/net/minecraft/server/commands/FillCommand.java +@@ -151,6 +_,12 @@ + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int fillBlocks( + CommandSourceStack source, BoundingBox area, BlockInput newBlock, FillCommand.Mode mode, @Nullable Predicate replacingPredicate + ) throws CommandSyntaxException { +@@ -161,6 +_,18 @@ + } else { + List list = Lists.newArrayList(); + ServerLevel level = source.getLevel(); ++ // Folia start - region threading ++ int buffer = 32; ++ // physics may spill into neighbour chunks, so use a buffer ++ level.moonrise$loadChunksAsync( ++ (area.minX() - buffer) >> 4, ++ (area.maxX() + buffer) >> 4, ++ (area.minZ() - buffer) >> 4, ++ (area.maxZ() + buffer) >> 4, ++ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { // Folia end - region threading + int i1 = 0; + + for (BlockPos blockPos : BlockPos.betweenClosed(area.minX(), area.minY(), area.minZ(), area.maxX(), area.maxY(), area.maxZ())) { +@@ -187,8 +_,13 @@ + } else { + int i2 = i1; + source.sendSuccess(() -> Component.translatable("commands.fill.success", i2), true); +- return i1; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); return 0; // Folia end - region threading + } + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/ForceLoadCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/ForceLoadCommand.java.patch new file mode 100644 index 0000000..3add8c1 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/ForceLoadCommand.java.patch @@ -0,0 +1,93 @@ +--- a/net/minecraft/server/commands/ForceLoadCommand.java ++++ b/net/minecraft/server/commands/ForceLoadCommand.java +@@ -97,7 +_,17 @@ + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int queryForceLoad(CommandSourceStack source, ColumnPos pos) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + ChunkPos chunkPos = pos.toChunkPos(); + ServerLevel level = source.getLevel(); + ResourceKey resourceKey = level.dimension(); +@@ -109,14 +_,22 @@ + ), + false + ); +- return 1; ++ return; // Folia - region threading + } else { + throw ERROR_NOT_TICKING.create(chunkPos, resourceKey.location()); + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int listForceLoad(CommandSourceStack source) { + ServerLevel level = source.getLevel(); ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + ResourceKey resourceKey = level.dimension(); + LongSet forcedChunks = level.getForcedChunks(); + int size = forcedChunks.size(); +@@ -134,20 +_,27 @@ + } else { + source.sendFailure(Component.translatable("commands.forceload.added.none", Component.translationArg(resourceKey.location()))); + } ++ }); // Folia - region threading + +- return size; ++ return 1; // Folia - region threading + } + + private static int removeAll(CommandSourceStack source) { + ServerLevel level = source.getLevel(); + ResourceKey resourceKey = level.dimension(); ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + LongSet forcedChunks = level.getForcedChunks(); + forcedChunks.forEach(packedChunkPos -> level.setChunkForced(ChunkPos.getX(packedChunkPos), ChunkPos.getZ(packedChunkPos), false)); + source.sendSuccess(() -> Component.translatable("commands.forceload.removed.all", Component.translationArg(resourceKey.location())), true); ++ }); // Folia - region threading + return 0; + } + + private static int changeForceLoad(CommandSourceStack source, ColumnPos from, ColumnPos to, boolean add) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + int min = Math.min(from.x(), to.x()); + int min1 = Math.min(from.z(), to.z()); + int max = Math.max(from.x(), to.x()); +@@ -207,11 +_,18 @@ + ); + } + +- return i2x; ++ return; // Folia - region threading + } + } + } else { + throw BlockPosArgument.ERROR_OUT_OF_WORLD.create(); + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/GameModeCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/GameModeCommand.java.patch new file mode 100644 index 0000000..d9b512a --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/GameModeCommand.java.patch @@ -0,0 +1,24 @@ +--- a/net/minecraft/server/commands/GameModeCommand.java ++++ b/net/minecraft/server/commands/GameModeCommand.java +@@ -54,15 +_,18 @@ + int i = 0; + + for (ServerPlayer serverPlayer : players) { ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer nmsEntity) -> { // Folia - region threading + // Paper start - Expand PlayerGameModeChangeEvent +- org.bukkit.event.player.PlayerGameModeChangeEvent event = serverPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); ++ org.bukkit.event.player.PlayerGameModeChangeEvent event = nmsEntity.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.empty()); // Folia - region threading + if (event != null && !event.isCancelled()) { +- logGamemodeChange(source.getSource(), serverPlayer, gameType); +- i++; ++ logGamemodeChange(source.getSource(), nmsEntity, gameType); // Folia - region threading ++ //i++; // Folia - region threading + } else if (event != null && event.cancelMessage() != null) { + source.getSource().sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true); + // Paper end - Expand PlayerGameModeChangeEvent + } ++ }, null, 1L); // Folia - region threading ++ ++i; // Folia - region threading + } + + return i; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/GiveCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/GiveCommand.java.patch new file mode 100644 index 0000000..6765546 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/GiveCommand.java.patch @@ -0,0 +1,47 @@ +--- a/net/minecraft/server/commands/GiveCommand.java ++++ b/net/minecraft/server/commands/GiveCommand.java +@@ -65,32 +_,34 @@ + int min = Math.min(maxStackSize, i1); + i1 -= min; + ItemStack itemStack1 = item.createItemStack(min, false); +- boolean flag = serverPlayer.getInventory().add(itemStack1); ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer nmsEntity) -> { // Folia - region threading ++ boolean flag = nmsEntity.getInventory().add(itemStack1); // Folia - region threading + if (flag && itemStack1.isEmpty()) { +- ItemEntity itemEntity = serverPlayer.drop(itemStack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event ++ ItemEntity itemEntity = nmsEntity.drop(itemStack, false, false, false); // CraftBukkit - SPIGOT-2942: Add boolean to call event // Folia - region threading + if (itemEntity != null) { + itemEntity.makeFakeItem(); + } + +- serverPlayer.level() ++ nmsEntity.level() // Folia - region threading + .playSound( + null, +- serverPlayer.getX(), +- serverPlayer.getY(), +- serverPlayer.getZ(), ++ nmsEntity.getX(), // Folia - region threading ++ nmsEntity.getY(), // Folia - region threading ++ nmsEntity.getZ(), // Folia - region threading + SoundEvents.ITEM_PICKUP, + SoundSource.PLAYERS, + 0.2F, +- ((serverPlayer.getRandom().nextFloat() - serverPlayer.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F ++ ((nmsEntity.getRandom().nextFloat() - nmsEntity.getRandom().nextFloat()) * 0.7F + 1.0F) * 2.0F // Folia - region threading + ); +- serverPlayer.containerMenu.broadcastChanges(); ++ nmsEntity.containerMenu.broadcastChanges(); // Folia - region threading + } else { +- ItemEntity itemEntity = serverPlayer.drop(itemStack1, false); ++ ItemEntity itemEntity = nmsEntity.drop(itemStack1, false); // Folia - region threading + if (itemEntity != null) { + itemEntity.setNoPickUpDelay(); +- itemEntity.setTarget(serverPlayer.getUUID()); ++ itemEntity.setTarget(nmsEntity.getUUID()); // Folia - region threading + } + } ++ }, null, 1L); // Folia - region threading + } + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/KillCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/KillCommand.java.patch new file mode 100644 index 0000000..37de3a2 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/KillCommand.java.patch @@ -0,0 +1,13 @@ +--- a/net/minecraft/server/commands/KillCommand.java ++++ b/net/minecraft/server/commands/KillCommand.java +@@ -24,7 +_,9 @@ + + private static int kill(CommandSourceStack source, Collection targets) { + for (Entity entity : targets) { +- entity.kill(source.getLevel()); ++ entity.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { // Folia - region threading ++ nmsEntity.kill((net.minecraft.server.level.ServerLevel)nmsEntity.level()); // Folia - region threading ++ }, null, 1L); // Folia - region threading + } + + if (targets.size() == 1) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/PlaceCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/PlaceCommand.java.patch new file mode 100644 index 0000000..5157b63 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/PlaceCommand.java.patch @@ -0,0 +1,134 @@ +--- a/net/minecraft/server/commands/PlaceCommand.java ++++ b/net/minecraft/server/commands/PlaceCommand.java +@@ -233,36 +_,79 @@ + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + public static int placeFeature(CommandSourceStack source, Holder.Reference> feature, BlockPos pos) throws CommandSyntaxException { + ServerLevel level = source.getLevel(); + ConfiguredFeature configuredFeature = feature.value(); + ChunkPos chunkPos = new ChunkPos(pos); + checkLoaded(level, new ChunkPos(chunkPos.x - 1, chunkPos.z - 1), new ChunkPos(chunkPos.x + 1, chunkPos.z + 1)); ++ // Folia start - region threading ++ level.moonrise$loadChunksAsync( ++ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { ++ // Folia end - region threading + if (!configuredFeature.place(level, level.getChunkSource().getGenerator(), level.getRandom(), pos)) { + throw ERROR_FEATURE_FAILED.create(); + } else { + String string = feature.key().location().toString(); + source.sendSuccess(() -> Component.translatable("commands.place.feature.success", string, pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Folia end - region threading + } + + public static int placeJigsaw(CommandSourceStack source, Holder templatePool, ResourceLocation target, int maxDepth, BlockPos pos) throws CommandSyntaxException { + ServerLevel level = source.getLevel(); + ChunkPos chunkPos = new ChunkPos(pos); + checkLoaded(level, chunkPos, chunkPos); ++ // Folia start - region threading ++ level.moonrise$loadChunksAsync( ++ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { ++ // Folia end - region threading + if (!JigsawPlacement.generateJigsaw(level, templatePool, target, maxDepth, pos, false)) { + throw ERROR_JIGSAW_FAILED.create(); + } else { + source.sendSuccess(() -> Component.translatable("commands.place.jigsaw.success", pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Folia end - region threading + } + + public static int placeStructure(CommandSourceStack source, Holder.Reference structure, BlockPos pos) throws CommandSyntaxException { + ServerLevel level = source.getLevel(); + Structure structure1 = structure.value(); + ChunkGenerator generator = level.getChunkSource().getGenerator(); ++ // Folia start - region threading ++ level.moonrise$loadChunksAsync( ++ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { ++ // Folia end - region threading + StructureStart structureStart = structure1.generate( + structure, + level.dimension(), +@@ -305,14 +_,29 @@ + ); + String string = structure.key().location().toString(); + source.sendSuccess(() -> Component.translatable("commands.place.structure.success", string, pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Folia end - region threading + } + + public static int placeTemplate( + CommandSourceStack source, ResourceLocation template, BlockPos pos, Rotation rotation, Mirror mirror, float integrity, int seed + ) throws CommandSyntaxException { + ServerLevel level = source.getLevel(); ++ // Folia start - region threading ++ level.moonrise$loadChunksAsync( ++ pos, 16, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunks) -> { ++ try { ++ // Folia end - region threading + StructureTemplateManager structureManager = level.getStructureManager(); + + Optional optional; +@@ -340,9 +_,17 @@ + () -> Component.translatable("commands.place.template.success", Component.translationArg(template), pos.getX(), pos.getY(), pos.getZ()), + true + ); +- return 1; ++ return; // Folia - region threading + } + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ } ++ ); ++ return 1; ++ // Folia end - region threading + } + + private static void checkLoaded(ServerLevel level, ChunkPos start, ChunkPos end) throws CommandSyntaxException { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/RecipeCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/RecipeCommand.java.patch new file mode 100644 index 0000000..5d2f509 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/RecipeCommand.java.patch @@ -0,0 +1,30 @@ +--- a/net/minecraft/server/commands/RecipeCommand.java ++++ b/net/minecraft/server/commands/RecipeCommand.java +@@ -81,7 +_,12 @@ + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +- i += serverPlayer.awardRecipes(recipes); ++ // Folia start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ player.awardRecipes(recipes); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { +@@ -103,7 +_,12 @@ + int i = 0; + + for (ServerPlayer serverPlayer : targets) { +- i += serverPlayer.resetRecipes(recipes); ++ // Folia start - region threading ++ ++i; ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ player.resetRecipes(recipes); ++ }, null, 1L); ++ // Folia end - region threading + } + + if (i == 0) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetBlockCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetBlockCommand.java.patch new file mode 100644 index 0000000..3169fa2 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetBlockCommand.java.patch @@ -0,0 +1,42 @@ +--- a/net/minecraft/server/commands/SetBlockCommand.java ++++ b/net/minecraft/server/commands/SetBlockCommand.java +@@ -80,10 +_,21 @@ + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int setBlock( + CommandSourceStack source, BlockPos pos, BlockInput state, SetBlockCommand.Mode mode, @Nullable Predicate predicate + ) throws CommandSyntaxException { + ServerLevel level = source.getLevel(); ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ level, pos.getX() >> 4, pos.getZ() >> 4, () -> { ++ try { ++ // Folia end - region threading + if (predicate != null && !predicate.test(new BlockInWorld(level, pos, true))) { + throw ERROR_FAILED.create(); + } else { +@@ -102,9 +_,16 @@ + } else { + level.blockUpdated(pos, state.getState().getBlock()); + source.sendSuccess(() -> Component.translatable("commands.setblock.success", pos.getX(), pos.getY(), pos.getZ()), true); +- return 1; ++ return; // Folia - region threading + } + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + public interface Filter { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetSpawnCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetSpawnCommand.java.patch new file mode 100644 index 0000000..d69276a --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/SetSpawnCommand.java.patch @@ -0,0 +1,15 @@ +--- a/net/minecraft/server/commands/SetSpawnCommand.java ++++ b/net/minecraft/server/commands/SetSpawnCommand.java +@@ -69,7 +_,11 @@ + final Collection actualTargets = new java.util.ArrayList<>(); // Paper - Add PlayerSetSpawnEvent + for (ServerPlayer serverPlayer : targets) { + // Paper start - Add PlayerSetSpawnEvent +- if (serverPlayer.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND)) { ++ // Folia start - region threading ++ serverPlayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ player.setRespawnPosition(resourceKey, pos, angle, true, false, com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.COMMAND); ++ }, null, 1L); ++ if (true) { // Folia end - region threading + actualTargets.add(serverPlayer); + } + // Paper end - Add PlayerSetSpawnEvent diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/SummonCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/SummonCommand.java.patch new file mode 100644 index 0000000..cfe4a7c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/SummonCommand.java.patch @@ -0,0 +1,26 @@ +--- a/net/minecraft/server/commands/SummonCommand.java ++++ b/net/minecraft/server/commands/SummonCommand.java +@@ -88,12 +_,18 @@ + if (entity == null) { + throw ERROR_FAILED.create(); + } else { +- if (randomizeProperties && entity instanceof Mob) { +- ((Mob)entity) +- .finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, null); +- } ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ level, entity.chunkPosition().x, entity.chunkPosition().z, () -> { ++ if (randomizeProperties && entity instanceof Mob) { ++ ((Mob)entity) ++ .finalizeSpawn(source.getLevel(), source.getLevel().getCurrentDifficultyAt(entity.blockPosition()), EntitySpawnReason.COMMAND, null); ++ } ++ level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND); ++ }); ++ // Folia end - region threading + +- if (!level.tryAddFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.COMMAND)) { // CraftBukkit - pass a spawn reason of "COMMAND" ++ if (false) { // CraftBukkit - pass a spawn reason of "COMMAND" // Folia - region threading + throw ERROR_DUPLICATE_UUID.create(); + } else { + return entity; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/TeleportCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/TeleportCommand.java.patch new file mode 100644 index 0000000..d3f4350 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/TeleportCommand.java.patch @@ -0,0 +1,47 @@ +--- a/net/minecraft/server/commands/TeleportCommand.java ++++ b/net/minecraft/server/commands/TeleportCommand.java +@@ -154,18 +_,7 @@ + + private static int teleportToEntity(CommandSourceStack source, Collection targets, Entity destination) throws CommandSyntaxException { + for (Entity entity : targets) { +- performTeleport( +- source, +- entity, +- (ServerLevel)destination.level(), +- destination.getX(), +- destination.getY(), +- destination.getZ(), +- EnumSet.noneOf(Relative.class), +- destination.getYRot(), +- destination.getXRot(), +- null +- ); ++ io.papermc.paper.threadedregions.TeleportUtils.teleport(entity, false, destination, Float.valueOf(destination.getYRot()), Float.valueOf(destination.getXRot()), Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, null); // Folia - region threading + } + + if (targets.size() == 1) { +@@ -290,6 +_,24 @@ + float f1 = relatives.contains(Relative.X_ROT) ? xRot - target.getXRot() : xRot; + float f2 = Mth.wrapDegrees(f); + float f3 = Mth.wrapDegrees(f1); ++ // Folia start - region threading ++ if (true) { ++ ServerLevel worldFinal = level; ++ Vec3 posFinal = new Vec3(x, y, z); ++ Float yawFinal = Float.valueOf(f); ++ Float pitchFinal = Float.valueOf(f1); ++ target.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { ++ nmsEntity.unRide(); ++ nmsEntity.teleportAsync( ++ worldFinal, posFinal, yawFinal, pitchFinal, Vec3.ZERO, ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.COMMAND, ++ Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ null ++ ); ++ }, null, 1L); ++ return; ++ } ++ // Folia end - region threading + // CraftBukkit start - Teleport event + boolean result; + if (target instanceof final net.minecraft.server.level.ServerPlayer player) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/TimeCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/TimeCommand.java.patch new file mode 100644 index 0000000..9be48a8 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/TimeCommand.java.patch @@ -0,0 +1,33 @@ +--- a/net/minecraft/server/commands/TimeCommand.java ++++ b/net/minecraft/server/commands/TimeCommand.java +@@ -56,6 +_,7 @@ + } + + public static int setTime(CommandSourceStack source, int time) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change + // serverLevel.setDayTime(time); + // CraftBukkit start +@@ -69,10 +_,12 @@ + + source.getServer().forceTimeSynchronization(); + source.sendSuccess(() -> Component.translatable("commands.time.set", time), true); +- return getDayTime(source.getLevel()); ++ }); // Folia - region threading ++ return 0; // Folia - region threading + } + + public static int addTime(CommandSourceStack source, int amount) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + for (ServerLevel serverLevel : io.papermc.paper.configuration.GlobalConfiguration.get().commands.timeCommandAffectsAllWorlds ? source.getServer().getAllLevels() : java.util.List.of(source.getLevel())) { // CraftBukkit - SPIGOT-6496: Only set the time for the world the command originates in // Paper - add config option for spigot's change + // CraftBukkit start + org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent(serverLevel.getWorld(), org.bukkit.event.world.TimeSkipEvent.SkipReason.COMMAND, amount); +@@ -86,6 +_,7 @@ + source.getServer().forceTimeSynchronization(); + int dayTime = getDayTime(source.getLevel()); + source.sendSuccess(() -> Component.translatable("commands.time.set", dayTime), true); +- return dayTime; ++ }); // Folia - region threading ++ return 0; // Folia - region threading + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/WeatherCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/WeatherCommand.java.patch new file mode 100644 index 0000000..62942b7 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/WeatherCommand.java.patch @@ -0,0 +1,29 @@ +--- a/net/minecraft/server/commands/WeatherCommand.java ++++ b/net/minecraft/server/commands/WeatherCommand.java +@@ -48,20 +_,26 @@ + } + + private static int setClear(CommandSourceStack source, int time) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + source.getLevel().setWeatherParameters(getDuration(source, time, ServerLevel.RAIN_DELAY), 0, false, false); // CraftBukkit - SPIGOT-7680: per-world + source.sendSuccess(() -> Component.translatable("commands.weather.set.clear"), true); ++ }); // Folia - region threading + return time; + } + + private static int setRain(CommandSourceStack source, int time) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.RAIN_DURATION), true, false); // CraftBukkit - SPIGOT-7680: per-world + source.sendSuccess(() -> Component.translatable("commands.weather.set.rain"), true); ++ }); // Folia - region threading + return time; + } + + private static int setThunder(CommandSourceStack source, int time) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Folia - region threading + source.getLevel().setWeatherParameters(0, getDuration(source, time, ServerLevel.THUNDER_DURATION), true, true); // CraftBukkit - SPIGOT-7680: per-world + source.sendSuccess(() -> Component.translatable("commands.weather.set.thunder"), true); ++ }); // Folia - region threading + return time; + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/commands/WorldBorderCommand.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/WorldBorderCommand.java.patch new file mode 100644 index 0000000..47f4079 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/commands/WorldBorderCommand.java.patch @@ -0,0 +1,169 @@ +--- a/net/minecraft/server/commands/WorldBorderCommand.java ++++ b/net/minecraft/server/commands/WorldBorderCommand.java +@@ -134,18 +_,39 @@ + ); + } + ++ // Folia start - region threading ++ private static void sendMessage(CommandSourceStack src, CommandSyntaxException ex) { ++ src.sendFailure((Component)ex.getRawMessage()); ++ } ++ // Folia end - region threading ++ + private static int setDamageBuffer(CommandSourceStack source, float distance) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit + if (worldBorder.getDamageSafeZone() == distance) { + throw ERROR_SAME_DAMAGE_BUFFER.create(); + } else { + worldBorder.setDamageSafeZone(distance); + source.sendSuccess(() -> Component.translatable("commands.worldborder.damage.buffer.success", String.format(Locale.ROOT, "%.2f", distance)), true); +- return (int)distance; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setDamageAmount(CommandSourceStack source, float damagePerBlock) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit + if (worldBorder.getDamagePerBlock() == damagePerBlock) { + throw ERROR_SAME_DAMAGE_AMOUNT.create(); +@@ -154,39 +_,79 @@ + source.sendSuccess( + () -> Component.translatable("commands.worldborder.damage.amount.success", String.format(Locale.ROOT, "%.2f", damagePerBlock)), true + ); +- return (int)damagePerBlock; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setWarningTime(CommandSourceStack source, int time) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit + if (worldBorder.getWarningTime() == time) { + throw ERROR_SAME_WARNING_TIME.create(); + } else { + worldBorder.setWarningTime(time); + source.sendSuccess(() -> Component.translatable("commands.worldborder.warning.time.success", time), true); +- return time; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setWarningDistance(CommandSourceStack source, int distance) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit + if (worldBorder.getWarningBlocks() == distance) { + throw ERROR_SAME_WARNING_DISTANCE.create(); + } else { + worldBorder.setWarningBlocks(distance); + source.sendSuccess(() -> Component.translatable("commands.worldborder.warning.distance.success", distance), true); +- return distance; ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int getSize(CommandSourceStack source) { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ // Folia end - region threading + double size = source.getLevel().getWorldBorder().getSize(); // CraftBukkit + source.sendSuccess(() -> Component.translatable("commands.worldborder.get", String.format(Locale.ROOT, "%.0f", size)), false); +- return Mth.floor(size + 0.5); ++ return; // Folia - region threading ++ // Folia start - region threading ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setCenter(CommandSourceStack source, Vec2 pos) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit + if (worldBorder.getCenterX() == pos.x && worldBorder.getCenterZ() == pos.y) { + throw ERROR_SAME_CENTER.create(); +@@ -198,13 +_,24 @@ + ), + true + ); +- return 0; ++ return; // Folia - region threading + } else { + throw ERROR_TOO_FAR_OUT.create(); + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + + private static int setSize(CommandSourceStack source, double newSize, long time) throws CommandSyntaxException { ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ try { ++ // Folia end - region threading + WorldBorder worldBorder = source.getLevel().getWorldBorder(); // CraftBukkit + double size = worldBorder.getSize(); + if (size == newSize) { +@@ -234,7 +_,14 @@ + source.sendSuccess(() -> Component.translatable("commands.worldborder.set.immediate", String.format(Locale.ROOT, "%.1f", newSize)), true); + } + +- return (int)(newSize - size); ++ return; // Folia - region threading + } ++ // Folia start - region threading ++ } catch (CommandSyntaxException ex) { ++ sendMessage(source, ex); ++ } ++ }); ++ return 1; ++ // Folia end - region threading + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch new file mode 100644 index 0000000..ca927db --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch @@ -0,0 +1,39 @@ +--- a/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/net/minecraft/server/dedicated/DedicatedServer.java +@@ -425,7 +_,7 @@ + @Override + public void tickConnection() { + super.tickConnection(); +- this.handleConsoleInputs(); ++ // Folia - region threading + } + + @Override +@@ -732,7 +_,8 @@ + + public String runCommand(RconConsoleSource rconConsoleSource, String s) { + rconConsoleSource.prepareForCommand(); +- this.executeBlocking(() -> { ++ final java.util.concurrent.atomic.AtomicReference command = new java.util.concurrent.atomic.AtomicReference<>(s); // Folia start - region threading ++ Runnable sync = () -> { // Folia - region threading + CommandSourceStack wrapper = rconConsoleSource.createCommandSourceStack(); + org.bukkit.event.server.RemoteServerCommandEvent event = new org.bukkit.event.server.RemoteServerCommandEvent(rconConsoleSource.getBukkitSender(wrapper), s); + this.server.getPluginManager().callEvent(event); +@@ -741,7 +_,16 @@ + } + ConsoleInput serverCommand = new ConsoleInput(event.getCommand(), wrapper); + this.server.dispatchServerCommand(event.getSender(), serverCommand); +- }); ++ }; // Folia start - region threading ++ java.util.concurrent.CompletableFuture ++ .runAsync(sync, io.papermc.paper.threadedregions.RegionizedServer.getInstance()::addTask) ++ .whenComplete((Void r, Throwable t) -> { ++ if (t != null) { ++ LOGGER.error("Error handling command for rcon: " + s, t); ++ } ++ }) ++ .join(); ++ // Folia end - region threading + return rconConsoleSource.getCommandResponse(); + // CraftBukkit end + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/level/ChunkMap.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ChunkMap.java.patch new file mode 100644 index 0000000..467aa07 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ChunkMap.java.patch @@ -0,0 +1,217 @@ +--- a/net/minecraft/server/level/ChunkMap.java ++++ b/net/minecraft/server/level/ChunkMap.java +@@ -128,8 +_,8 @@ + public final ChunkMap.DistanceManager distanceManager; + public final AtomicInteger tickingGenerated = new AtomicInteger(); // Paper - public + private final String storageName; +- private final PlayerMap playerMap = new PlayerMap(); +- public final Int2ObjectMap entityMap = new Int2ObjectOpenHashMap<>(); ++ //private final PlayerMap playerMap = new PlayerMap(); // Folia - region threading ++ //public final Int2ObjectMap entityMap = new Int2ObjectOpenHashMap<>(); // Folia - region threading + private final Long2ByteMap chunkTypeCache = new Long2ByteOpenHashMap(); + // Paper - rewrite chunk system + public int serverViewDistance; +@@ -797,12 +_,12 @@ + + void updatePlayerStatus(ServerPlayer player, boolean track) { + boolean flag = this.skipPlayer(player); +- boolean flag1 = this.playerMap.ignoredOrUnknown(player); ++ //boolean flag1 = this.playerMap.ignoredOrUnknown(player); // Folia - region threading + if (track) { +- this.playerMap.addPlayer(player, flag); ++ //this.playerMap.addPlayer(player, flag); // Folia - region threading + this.updatePlayerPos(player); + if (!flag) { +- this.distanceManager.addPlayer(SectionPos.of(player), player); ++ //this.distanceManager.addPlayer(SectionPos.of(player), player); // Folia - region threading + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$addPlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation + } + +@@ -810,9 +_,9 @@ + ca.spottedleaf.moonrise.common.PlatformHooks.get().addPlayerToDistanceMaps(this.level, player); // Paper - rewrite chunk system + } else { + SectionPos lastSectionPos = player.getLastSectionPos(); +- this.playerMap.removePlayer(player); +- if (!flag1) { +- this.distanceManager.removePlayer(lastSectionPos, player); ++ //this.playerMap.removePlayer(player); // Folia - region threading ++ if (true) { // Folia - region threading ++ //this.distanceManager.removePlayer(lastSectionPos, player); // Folia - region threading + ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$removePlayer(player, SectionPos.of(player)); // Paper - chunk tick iteration optimisation + } + +@@ -830,27 +_,13 @@ + + SectionPos lastSectionPos = player.getLastSectionPos(); + SectionPos sectionPos = SectionPos.of(player); +- boolean flag = this.playerMap.ignored(player); ++ //boolean flag = this.playerMap.ignored(player); // Folia - region threading + boolean flag1 = this.skipPlayer(player); +- boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong(); +- if (flag2 || flag != flag1) { ++ //boolean flag2 = lastSectionPos.asLong() != sectionPos.asLong(); // Folia - region threading ++ if (true) { // Folia - region threading + this.updatePlayerPos(player); +- ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, lastSectionPos, sectionPos, flag, flag1); // Paper - chunk tick iteration optimisation +- if (!flag) { +- this.distanceManager.removePlayer(lastSectionPos, player); +- } +- +- if (!flag1) { +- this.distanceManager.addPlayer(sectionPos, player); +- } +- +- if (!flag && flag1) { +- this.playerMap.ignorePlayer(player); +- } +- +- if (flag && !flag1) { +- this.playerMap.unIgnorePlayer(player); +- } ++ ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickDistanceManager)this.distanceManager).moonrise$updatePlayer(player, lastSectionPos, sectionPos, false, flag1); // Paper - chunk tick iteration optimisation // Folia - region threading ++ // Folia - region threading + + // Paper - rewrite chunk system + } +@@ -880,9 +_,9 @@ + public void addEntity(Entity entity) { + org.spigotmc.AsyncCatcher.catchOp("entity track"); // Spigot + // Paper start - ignore and warn about illegal addEntity calls instead of crashing server +- if (!entity.valid || entity.level() != this.level || this.entityMap.containsKey(entity.getId())) { ++ if (!entity.valid || entity.level() != this.level || entity.moonrise$getTrackedEntity() != null) { // Folia - region threading + LOGGER.error("Illegal ChunkMap::addEntity for world " + this.level.getWorld().getName() +- + ": " + entity + (this.entityMap.containsKey(entity.getId()) ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); ++ + ": " + entity + (entity.moonrise$getTrackedEntity() != null ? " ALREADY CONTAINED (This would have crashed your server)" : ""), new Throwable()); // Folia - region threading + return; + } + // Paper end - ignore and warn about illegal addEntity calls instead of crashing server +@@ -893,22 +_,28 @@ + i = org.spigotmc.TrackingRange.getEntityTrackingRange(entity, i); // Spigot + if (i != 0) { + int updateInterval = type.updateInterval(); +- if (this.entityMap.containsKey(entity.getId())) { ++ if (entity.moonrise$getTrackedEntity() != null) { // Folia - region threading + throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("Entity is already tracked!")); + } else { + ChunkMap.TrackedEntity trackedEntity = new ChunkMap.TrackedEntity(entity, i, updateInterval, type.trackDeltas()); +- this.entityMap.put(entity.getId(), trackedEntity); ++ //this.entityMap.put(entity.getId(), trackedEntity); // Folia - region threading + // Paper start - optimise entity tracker + if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$getTrackedEntity() != null) { + throw new IllegalStateException("Entity is already tracked"); + } + ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(trackedEntity); + // Paper end - optimise entity tracker +- trackedEntity.updatePlayers(this.level.players()); ++ trackedEntity.updatePlayers(this.level.getLocalPlayers()); // Folia - region threading + if (entity instanceof ServerPlayer serverPlayer) { + this.updatePlayerStatus(serverPlayer, true); + +- for (ChunkMap.TrackedEntity trackedEntity1 : this.entityMap.values()) { ++ // Folia start - region threading ++ for (Entity possible : this.level.getCurrentWorldData().trackerEntities) { ++ ChunkMap.TrackedEntity trackedEntity1 = possible.moonrise$getTrackedEntity(); ++ if (trackedEntity == null) { ++ continue; ++ } ++ // Folia end - region threading + if (trackedEntity1.entity != serverPlayer) { + trackedEntity1.updatePlayer(serverPlayer); + } +@@ -924,12 +_,19 @@ + if (entity instanceof ServerPlayer serverPlayer) { + this.updatePlayerStatus(serverPlayer, false); + +- for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { ++ // Folia start - region threading ++ for (Entity possible : this.level.getCurrentWorldData().getLocalEntities()) { ++ ChunkMap.TrackedEntity trackedEntity = possible.moonrise$getTrackedEntity(); ++ if (trackedEntity == null) { ++ continue; ++ } ++ // Folia end - region threading + trackedEntity.removePlayer(serverPlayer); + } ++ // Folia end - region threading + } + +- ChunkMap.TrackedEntity trackedEntity1 = this.entityMap.remove(entity.getId()); ++ ChunkMap.TrackedEntity trackedEntity1 = entity.moonrise$getTrackedEntity(); // Folia - region threading + if (trackedEntity1 != null) { + trackedEntity1.broadcastRemoved(); + } +@@ -938,9 +_,10 @@ + + // Paper start - optimise entity tracker + private void newTrackerTick() { ++ final io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading + final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();; + +- final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = entityLookup.trackerEntities; ++ final ca.spottedleaf.moonrise.common.list.ReferenceList trackerEntities = worldData.trackerEntities; // Folia - region threading + final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked(); + for (int i = 0, len = trackerEntities.size(); i < len; ++i) { + final Entity entity = trackerEntitiesRaw[i]; +@@ -966,44 +_,18 @@ + // Paper end - optimise entity tracker + // Paper - rewrite chunk system + +- List list = Lists.newArrayList(); +- List list1 = this.level.players(); +- +- for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { +- SectionPos sectionPos = trackedEntity.lastSectionPos; +- SectionPos sectionPos1 = SectionPos.of(trackedEntity.entity); +- boolean flag = !Objects.equals(sectionPos, sectionPos1); +- if (flag) { +- trackedEntity.updatePlayers(list1); +- Entity entity = trackedEntity.entity; +- if (entity instanceof ServerPlayer) { +- list.add((ServerPlayer)entity); +- } +- +- trackedEntity.lastSectionPos = sectionPos1; +- } +- +- if (flag || this.distanceManager.inEntityTickingRange(sectionPos1.chunk().toLong())) { +- trackedEntity.serverEntity.sendChanges(); +- } +- } +- +- if (!list.isEmpty()) { +- for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) { +- trackedEntity.updatePlayers(list); +- } +- } ++ // Folia - region threading + } + + public void broadcast(Entity entity, Packet packet) { +- ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId()); ++ ChunkMap.TrackedEntity trackedEntity = entity.moonrise$getTrackedEntity(); // Folia - region threading + if (trackedEntity != null) { + trackedEntity.broadcast(packet); + } + } + + protected void broadcastAndSend(Entity entity, Packet packet) { +- ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId()); ++ ChunkMap.TrackedEntity trackedEntity = entity.moonrise$getTrackedEntity(); // Folia - region threading + if (trackedEntity != null) { + trackedEntity.broadcastAndSend(packet); + } +@@ -1231,8 +_,13 @@ + } + flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z); + // Paper end - Configurable entity tracking range by Y ++ // Folia start - region threading ++ if (flag && (this.entity instanceof ServerPlayer thisEntity) && thisEntity.broadcastedDeath) { ++ flag = false; ++ } ++ // Folia end - region threading + // CraftBukkit start - respect vanish API +- if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits ++ if (flag && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !player.getBukkitEntity().canSee(this.entity.getBukkitEntity()))) { // Paper - only consider hits // Folia - region threading + flag = false; + } + // CraftBukkit end diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/level/DistanceManager.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/level/DistanceManager.java.patch new file mode 100644 index 0000000..5be09f6 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/level/DistanceManager.java.patch @@ -0,0 +1,53 @@ +--- a/net/minecraft/server/level/DistanceManager.java ++++ b/net/minecraft/server/level/DistanceManager.java +@@ -57,16 +_,16 @@ + } + // Paper end - rewrite chunk system + // Paper start - chunk tick iteration optimisation +- private final ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap spawnChunkTracker = new ca.spottedleaf.moonrise.common.misc.PositionCountingAreaMap<>(); ++ // Folia - move to regionized world data + + @Override + public final void moonrise$addPlayer(final ServerPlayer player, final SectionPos pos) { +- this.spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); ++ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.add(player, pos.x(), pos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading + } + + @Override + public final void moonrise$removePlayer(final ServerPlayer player, final SectionPos pos) { +- this.spawnChunkTracker.remove(player); ++ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading + } + + @Override +@@ -74,9 +_,9 @@ + final SectionPos oldPos, final SectionPos newPos, + final boolean oldIgnore, final boolean newIgnore) { + if (newIgnore) { +- this.spawnChunkTracker.remove(player); ++ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.remove(player); // Folia - region threading + } else { +- this.spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); ++ this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.addOrUpdate(player, newPos.x(), newPos.z(), ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickConstants.PLAYER_SPAWN_TRACK_RANGE); // Folia - region threading + } + } + // Paper end - chunk tick iteration optimisation +@@ -208,15 +_,15 @@ + } + + public int getNaturalSpawnChunkCount() { +- return this.spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation ++ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getTotalPositions(); // Paper - chunk tick iteration optimisation // Folia - region threading + } + + public boolean hasPlayersNearby(long chunkPos) { +- return this.spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation ++ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.hasObjectsNear(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)); // Paper - chunk tick iteration optimisation // Folia - region threading + } + + public LongIterator getSpawnCandidateChunks() { +- return this.spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation ++ return this.moonrise$getChunkMap().level.getCurrentWorldData().spawnChunkTracker.getPositions().iterator(); // Paper - chunk tick iteration optimisation // Folia - region threading + } + + public String getDebugStatus() { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch new file mode 100644 index 0000000..450a7a8 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerChunkCache.java.patch @@ -0,0 +1,265 @@ +--- a/net/minecraft/server/level/ServerChunkCache.java ++++ b/net/minecraft/server/level/ServerChunkCache.java +@@ -61,18 +_,14 @@ + public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; + public final ChunkMap chunkMap; + private final DimensionDataStorage dataStorage; +- private long lastInhabitedUpdate; ++ //private long lastInhabitedUpdate; // Folia - region threading + public boolean spawnEnemies = true; + public boolean spawnFriendlies = true; + private static final int CACHE_SIZE = 4; + private final long[] lastChunkPos = new long[4]; + private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4]; + private final ChunkAccess[] lastChunk = new ChunkAccess[4]; +- private final List tickingChunks = new ArrayList<>(); +- private final Set chunkHoldersToBroadcast = new ReferenceOpenHashSet<>(); +- @Nullable +- @VisibleForDebug +- private NaturalSpawner.SpawnState lastSpawnState; ++ // Folia - moved to regionised world data + // Paper start + private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); + public int getFullChunksCount() { +@@ -98,6 +_,11 @@ + } + + private ChunkAccess syncLoad(final int chunkX, final int chunkZ, final ChunkStatus toStatus) { ++ // Folia start - region threading ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, chunkX, chunkZ, "Cannot asynchronously load chunks"); ++ } ++ // Folia end - region threading + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getChunkTaskScheduler(); + final CompletableFuture completable = new CompletableFuture<>(); + chunkTaskScheduler.scheduleChunkLoad( +@@ -355,6 +_,7 @@ + } + + public CompletableFuture> getChunkFuture(int x, int z, ChunkStatus chunkStatus, boolean requireChunk) { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + boolean flag = Thread.currentThread() == this.mainThread; + CompletableFuture> chunkFutureMainThread; + if (flag) { +@@ -502,14 +_,15 @@ + } + + private void tickChunks() { +- long gameTime = this.level.getGameTime(); +- long l = gameTime - this.lastInhabitedUpdate; +- this.lastInhabitedUpdate = gameTime; ++ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading ++ //long gameTime = this.level.getGameTime(); // Folia - region threading ++ long l = 1L; // Folia - region threading ++ //this.lastInhabitedUpdate = gameTime; // Folia - region threading + if (!this.level.isDebug()) { + ProfilerFiller profilerFiller = Profiler.get(); + profilerFiller.push("pollingChunks"); + if (this.level.tickRateManager().runsNormally()) { +- List list = this.tickingChunks; ++ List list = regionizedWorldData.temporaryChunkTickList; // Folia - region threading + + try { + profilerFiller.push("filteringTickingChunks"); +@@ -532,23 +_,24 @@ + } + + private void broadcastChangedChunks(ProfilerFiller profiler) { ++ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading + profiler.push("broadcast"); + +- for (ChunkHolder chunkHolder : this.chunkHoldersToBroadcast) { ++ for (ChunkHolder chunkHolder : regionizedWorldData.chunkHoldersToBroadcast) { // Folia - region threading - note: do not need to thread check, as getChunkToSend is only non-null when the chunkholder is loaded + LevelChunk tickingChunk = chunkHolder.getChunkToSend(); // Paper - rewrite chunk system + if (tickingChunk != null) { + chunkHolder.broadcastChanges(tickingChunk); + } + } + +- this.chunkHoldersToBroadcast.clear(); ++ regionizedWorldData.chunkHoldersToBroadcast.clear(); // Folia - region threading + profiler.pop(); + } + + private void collectTickingChunks(List output) { + // Paper start - chunk tick iteration optimisation + final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = +- ((ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel)this.level).moonrise$getPlayerTickingChunks(); ++ this.level.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading + + final ServerChunkCache.ChunkAndHolder[] raw = tickingChunks.getRawDataUnchecked(); + final int size = tickingChunks.size(); +@@ -569,13 +_,14 @@ + } + + private void tickChunks(ProfilerFiller profiler, long timeInhabited, List chunks) { ++ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.level.getCurrentWorldData(); // Folia - region threading + profiler.popPush("naturalSpawnCount"); + int naturalSpawnChunkCount = this.distanceManager.getNaturalSpawnChunkCount(); + // Paper start - Optional per player mob spawns + NaturalSpawner.SpawnState spawnState; + if ((this.spawnFriendlies || this.spawnEnemies) && this.level.paperConfig().entities.spawning.perPlayerMobSpawns) { // don't count mobs when animals and monsters are disabled + // re-set mob counts +- for (ServerPlayer player : this.level.players) { ++ for (ServerPlayer player : this.level.getLocalPlayers()) { // Folia - region threading + // Paper start - per player mob spawning backoff + for (int ii = 0; ii < ServerPlayer.MOBCATEGORY_TOTAL_ENUMS; ii++) { + player.mobCounts[ii] = 0; +@@ -588,26 +_,26 @@ + } + // Paper end - per player mob spawning backoff + } +- spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, null, true); ++ spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, null, true); // Folia - region threading - note: function only cares about loaded entities, doesn't need all + } else { +- spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, this.level.getAllEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); ++ spawnState = NaturalSpawner.createState(naturalSpawnChunkCount, regionizedWorldData.getLoadedEntities(), this::getFullChunk, !this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new LocalMobCapCalculator(this.chunkMap) : null, false); // Folia - region threading - note: function only cares about loaded entities, doesn't need all + } + // Paper end - Optional per player mob spawns +- this.lastSpawnState = spawnState; ++ regionizedWorldData.lastSpawnState = spawnState; // Folia - region threading + profiler.popPush("spawnAndTick"); +- boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit ++ boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.getLocalPlayers().isEmpty(); // CraftBukkit // Folia - region threading + int _int = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); + List filteredSpawningCategories; + if (_boolean && (this.spawnEnemies || this.spawnFriendlies)) { + // Paper start - PlayerNaturallySpawnCreaturesEvent +- for (ServerPlayer entityPlayer : this.level.players()) { ++ for (ServerPlayer entityPlayer : this.level.getLocalPlayers()) { // Folia - region threading + int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, entityPlayer.getBukkitEntity().getViewDistance()); + chunkRange = Math.min(chunkRange, 8); + entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange); + entityPlayer.playerNaturallySpawnedEvent.callEvent(); + } + // Paper end - PlayerNaturallySpawnCreaturesEvent +- boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit ++ boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getRedstoneGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit // Folia - region threading + filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag, this.level); // CraftBukkit + } else { + filteredSpawningCategories = List.of(); +@@ -673,18 +_,23 @@ + int sectionPosZ = SectionPos.blockToSectionCoord(pos.getZ()); + ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(ChunkPos.asLong(sectionPosX, sectionPosZ)); + if (visibleChunkIfPresent != null && visibleChunkIfPresent.blockChanged(pos)) { +- this.chunkHoldersToBroadcast.add(visibleChunkIfPresent); ++ this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(visibleChunkIfPresent); // Folia - region threading + } + } + + @Override + public void onLightUpdate(LightLayer type, SectionPos pos) { +- this.mainThreadProcessor.execute(() -> { ++ Runnable run = () -> { // Folia - region threading + ChunkHolder visibleChunkIfPresent = this.getVisibleChunkIfPresent(pos.chunk().toLong()); + if (visibleChunkIfPresent != null && visibleChunkIfPresent.sectionLightChanged(type, pos.y())) { +- this.chunkHoldersToBroadcast.add(visibleChunkIfPresent); ++ this.level.getCurrentWorldData().chunkHoldersToBroadcast.add(visibleChunkIfPresent); // Folia - region threading + } +- }); ++ }; // Folia - region threading ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( ++ this.level, pos.getX(), pos.getZ(), run ++ ); ++ // Folia end - region threading + } + + public void addRegionTicket(TicketType type, ChunkPos pos, int distance, T value) { +@@ -766,7 +_,8 @@ + @Nullable + @VisibleForDebug + public NaturalSpawner.SpawnState getLastSpawnState() { +- return this.lastSpawnState; ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading ++ return worldData == null ? null : worldData.lastSpawnState; // Folia - region threading + } + + public void removeTicketsOnClosing() { +@@ -775,7 +_,7 @@ + + public void onChunkReadyToSend(ChunkHolder chunkHolder) { + if (chunkHolder.hasChangesToBroadcast()) { +- this.chunkHoldersToBroadcast.add(chunkHolder); ++ throw new UnsupportedOperationException(); // Folia - region threading + } + } + +@@ -812,20 +_,76 @@ + return ServerChunkCache.this.mainThread; + } + ++ // Folia start - region threading ++ @Override ++ public CompletableFuture submit(Supplier task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ return super.submit(task); ++ } ++ ++ @Override ++ public CompletableFuture submit(Runnable task) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ return super.submit(task); ++ } ++ ++ @Override ++ public void schedule(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.schedule(runnable); ++ } ++ ++ @Override ++ public void executeBlocking(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.executeBlocking(runnable); ++ } ++ ++ @Override ++ public void execute(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.execute(runnable); ++ } ++ ++ @Override ++ public void executeIfPossible(Runnable runnable) { ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ super.executeIfPossible(runnable); ++ } ++ // Folia end - region threading ++ + @Override + protected void doRunTask(Runnable task) { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + Profiler.get().incrementCounter("runTask"); + super.doRunTask(task); + } + + @Override + public boolean pollTask() { ++ // Folia start - region threading ++ if (ServerChunkCache.this.level != io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().world) { ++ throw new IllegalStateException("Polling tasks from non-owned region"); ++ } ++ // Folia end - region threading + // Paper start - rewrite chunk system + final ServerChunkCache serverChunkCache = ServerChunkCache.this; + if (serverChunkCache.runDistanceManagerUpdates()) { + return true; + } else { +- return super.pollTask() | ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)serverChunkCache.level).moonrise$getChunkTaskScheduler().executeMainThreadTask(); ++ return io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegion().getData().getTaskQueueData().executeChunkTask(); // Folia - region threading + } + // Paper end - rewrite chunk system + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerEntityGetter.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerEntityGetter.java.patch new file mode 100644 index 0000000..d375195 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerEntityGetter.java.patch @@ -0,0 +1,32 @@ +--- a/net/minecraft/server/level/ServerEntityGetter.java ++++ b/net/minecraft/server/level/ServerEntityGetter.java +@@ -14,17 +_,17 @@ + + @Nullable + default Player getNearestPlayer(TargetingConditions targetingConditions, LivingEntity source) { +- return this.getNearestEntity(this.players(), targetingConditions, source, source.getX(), source.getY(), source.getZ()); ++ return this.getNearestEntity(this.getLocalPlayers(), targetingConditions, source, source.getX(), source.getY(), source.getZ()); // Folia - region threading + } + + @Nullable + default Player getNearestPlayer(TargetingConditions targetingConditions, LivingEntity source, double x, double y, double z) { +- return this.getNearestEntity(this.players(), targetingConditions, source, x, y, z); ++ return this.getNearestEntity(this.getLocalPlayers(), targetingConditions, source, x, y, z); // Folia - region threading + } + + @Nullable + default Player getNearestPlayer(TargetingConditions targetingConditions, double x, double y, double z) { +- return this.getNearestEntity(this.players(), targetingConditions, null, x, y, z); ++ return this.getNearestEntity(this.getLocalPlayers(), targetingConditions, null, x, y, z); // Folia - region threading + } + + @Nullable +@@ -57,7 +_,7 @@ + default List getNearbyPlayers(TargetingConditions targetingConditions, LivingEntity source, AABB area) { + List list = new ArrayList<>(); + +- for (Player player : this.players()) { ++ for (Player player : this.getLocalPlayers()) { // Folia - region threading + if (area.contains(player.getX(), player.getY(), player.getZ()) && targetingConditions.test(this.getLevel(), source, player)) { + list.add(player); + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch new file mode 100644 index 0000000..84004b2 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch @@ -0,0 +1,1133 @@ +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -179,42 +_,40 @@ + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int EMPTY_TIME_NO_TICK = 300; + private static final int MAX_SCHEDULED_TICKS_PER_TICK = 65536; +- final List players = Lists.newArrayList(); ++ final List players = new java.util.concurrent.CopyOnWriteArrayList<>(); // Folia - region threading + public final ServerChunkCache chunkSource; + private final MinecraftServer server; + public final net.minecraft.world.level.storage.PrimaryLevelData serverLevelData; // CraftBukkit - type + private int lastSpawnChunkRadius; +- final EntityTickList entityTickList = new EntityTickList(); ++ //final EntityTickList entityTickList = new EntityTickList(); // Folia - region threading + // Paper - rewrite chunk system + private final GameEventDispatcher gameEventDispatcher; + public boolean noSave; + private final SleepStatus sleepStatus; + private int emptyTime; + private final PortalForcer portalForcer; +- private final LevelTicks blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); +- private final LevelTicks fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); +- private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); +- final Set navigatingMobs = new ObjectOpenHashSet<>(); ++ //private final LevelTicks blockTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading ++ //private final LevelTicks fluidTicks = new LevelTicks<>(this::isPositionTickingWithEntitiesLoaded); // Folia - region threading ++ //private final PathTypeCache pathTypesByPosCache = new PathTypeCache(); // Folia - region threading ++ //final Set navigatingMobs = new ObjectOpenHashSet<>(); // Folia - region threading + volatile boolean isUpdatingNavigations; + protected final Raids raids; +- private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); +- private final List blockEventsToReschedule = new ArrayList<>(64); +- private boolean handlingTick; ++ //private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); // Folia - region threading ++ //private final List blockEventsToReschedule = new ArrayList<>(64); // Folia - region threading ++ //private boolean handlingTick; // Folia - region threading + private final List customSpawners; + @Nullable + private EndDragonFight dragonFight; +- final Int2ObjectMap dragonParts = new Int2ObjectOpenHashMap<>(); ++ final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable dragonParts = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>(); // Folia - region threading + private final StructureManager structureManager; + private final StructureCheck structureCheck; +- private final boolean tickTime; ++ public final boolean tickTime; // Folia - region threading + private final RandomSequences randomSequences; + + // CraftBukkit start + public final LevelStorageSource.LevelStorageAccess levelStorageAccess; + public final UUID uuid; +- public boolean hasPhysicsEvent = true; // Paper - BlockPhysicsEvent +- public boolean hasEntityMoveEvent; // Paper - Add EntityMoveEvent +- private final alternate.current.wire.WireHandler wireHandler = new alternate.current.wire.WireHandler(this); // Paper - optimize redstone (Alternate Current) ++ // Folia - region threading - move to regionised world data + + public LevelChunk getChunkIfLoaded(int x, int z) { + return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately +@@ -242,6 +_,13 @@ + int minChunkZ = minBlockZ >> 4; + int maxChunkZ = maxBlockZ >> 4; + ++ // Folia start - region threading ++ // don't let players move into regions not owned ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, minChunkX, minChunkZ, maxChunkX, maxChunkZ)) { ++ return false; ++ } ++ // Folia end - region threading ++ + ServerChunkCache chunkProvider = this.getChunkSource(); + + for (int cx = minChunkX; cx <= maxChunkX; ++cx) { +@@ -297,11 +_,7 @@ + private final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler chunkTaskScheduler; + private long lastMidTickFailure; + private long tickedBlocksOrFluids; +- private final ca.spottedleaf.moonrise.common.misc.NearbyPlayers nearbyPlayers = new ca.spottedleaf.moonrise.common.misc.NearbyPlayers((ServerLevel)(Object)this); +- private static final ServerChunkCache.ChunkAndHolder[] EMPTY_CHUNK_AND_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; +- private final ca.spottedleaf.moonrise.common.list.ReferenceList loadedChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); +- private final ca.spottedleaf.moonrise.common.list.ReferenceList tickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); +- private final ca.spottedleaf.moonrise.common.list.ReferenceList entityTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_CHUNK_AND_HOLDERS); ++ // Folia - region threading - move to regionized data + + @Override + public final LevelChunk moonrise$getFullChunkIfLoaded(final int chunkX, final int chunkZ) { +@@ -359,7 +_,7 @@ + + @Override + public final int moonrise$getRegionChunkShift() { +- return io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(); ++ return this.regioniser.sectionChunkShift; // Folia - region threading + } + + @Override +@@ -460,22 +_,22 @@ + + @Override + public final ca.spottedleaf.moonrise.common.misc.NearbyPlayers moonrise$getNearbyPlayers() { +- return this.nearbyPlayers; ++ return this.getCurrentWorldData().getNearbyPlayers(); // Folia - region threading + } + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getLoadedChunks() { +- return this.loadedChunks; ++ return this.getCurrentWorldData().getChunks(); // Folia - region threading + } + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getTickingChunks() { +- return this.tickingChunks; ++ return this.getCurrentWorldData().getTickingChunks(); // Folia - region threading + } + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getEntityTickingChunks() { +- return this.entityTickingChunks; ++ return this.getCurrentWorldData().getEntityTickingChunks(); // Folia - region threading + } + + @Override +@@ -495,80 +_,85 @@ + // Paper end - rewrite chunk system + // Paper start - chunk tick iteration + private static final ServerChunkCache.ChunkAndHolder[] EMPTY_PLAYER_CHUNK_HOLDERS = new ServerChunkCache.ChunkAndHolder[0]; +- private final ca.spottedleaf.moonrise.common.list.ReferenceList playerTickingChunks = new ca.spottedleaf.moonrise.common.list.ReferenceList<>(EMPTY_PLAYER_CHUNK_HOLDERS); +- private final it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap playerTickingRequests = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); ++ // Folia - region threading + + @Override + public final ca.spottedleaf.moonrise.common.list.ReferenceList moonrise$getPlayerTickingChunks() { +- return this.playerTickingChunks; ++ throw new UnsupportedOperationException(); // Folia - region threading + } + + @Override + public final void moonrise$markChunkForPlayerTicking(final LevelChunk chunk) { +- final ChunkPos pos = chunk.getPos(); +- if (!this.playerTickingRequests.containsKey(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(pos))) { +- return; +- } +- +- this.playerTickingChunks.add(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); ++ // Folia - region threading + } + + @Override + public final void moonrise$removeChunkForPlayerTicking(final LevelChunk chunk) { +- this.playerTickingChunks.remove(((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)chunk).moonrise$getChunkAndHolder()); ++ // Folia - region threading + } + + @Override + public final void moonrise$addPlayerTickingRequest(final int chunkX, final int chunkZ) { +- ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot add ticking request async"); +- +- final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); +- +- if (this.playerTickingRequests.addTo(chunkKey, 1) != 0) { +- // already added +- return; +- } +- +- final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() +- .chunkHolderManager.getChunkHolder(chunkKey); +- +- if (chunkHolder == null || !chunkHolder.isTickingReady()) { +- return; +- } +- +- this.playerTickingChunks.add( +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() +- ); ++ // Folia - region threading + } + + @Override + public final void moonrise$removePlayerTickingRequest(final int chunkX, final int chunkZ) { +- ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)(Object)this, chunkX, chunkZ, "Cannot remove ticking request async"); +- +- final long chunkKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey(chunkX, chunkZ); +- final int val = this.playerTickingRequests.addTo(chunkKey, -1); +- +- if (val <= 0) { +- throw new IllegalStateException("Negative counter"); +- } +- +- if (val != 1) { +- // still has at least one request +- return; +- } +- +- final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)(ServerLevel)(Object)this).moonrise$getChunkTaskScheduler() +- .chunkHolderManager.getChunkHolder(chunkKey); +- +- if (chunkHolder == null || !chunkHolder.isTickingReady()) { +- return; +- } +- +- this.playerTickingChunks.remove( +- ((ca.spottedleaf.moonrise.patches.chunk_system.level.chunk.ChunkSystemLevelChunk)(LevelChunk)chunkHolder.getCurrentChunk()).moonrise$getChunkAndHolder() +- ); ++ // Folia - region threading + } + // Paper end - chunk tick iteration ++ // Folia start - region threading ++ public final io.papermc.paper.threadedregions.TickRegions tickRegions = new io.papermc.paper.threadedregions.TickRegions(); ++ public final io.papermc.paper.threadedregions.ThreadedRegionizer regioniser; ++ { ++ this.regioniser = new io.papermc.paper.threadedregions.ThreadedRegionizer<>( ++ (int)Math.max(1L, (8L * 16L * 16L) / (1L << (2 * (io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())))), ++ (1.0 / 6.0), ++ Math.max(1, 8 / (1 << io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift())), ++ 1, ++ io.papermc.paper.threadedregions.TickRegions.getRegionChunkShift(), ++ this, ++ this.tickRegions ++ ); ++ } ++ public final io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData taskQueueRegionData = new io.papermc.paper.threadedregions.RegionizedTaskQueue.WorldRegionTaskData(this); ++ public static final int WORLD_INIT_NOT_CHECKED = 0; ++ public static final int WORLD_INIT_CHECKING = 1; ++ public static final int WORLD_INIT_CHECKED = 2; ++ public final java.util.concurrent.atomic.AtomicInteger checkInitialised = new java.util.concurrent.atomic.AtomicInteger(WORLD_INIT_NOT_CHECKED); ++ public ChunkPos randomSpawnSelection; ++ ++ public static final record PendingTeleport(Entity.EntityTreeNode rootVehicle, Vec3 to) {} ++ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet pendingTeleports = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); ++ ++ public void pushPendingTeleport(final PendingTeleport teleport) { ++ synchronized (this.pendingTeleports) { ++ this.pendingTeleports.add(teleport); ++ } ++ } ++ ++ public boolean removePendingTeleport(final PendingTeleport teleport) { ++ synchronized (this.pendingTeleports) { ++ return this.pendingTeleports.remove(teleport); ++ } ++ } ++ ++ public List removeAllRegionTeleports() { ++ final List ret = new ArrayList<>(); ++ ++ synchronized (this.pendingTeleports) { ++ for (final java.util.Iterator iterator = this.pendingTeleports.iterator(); iterator.hasNext(); ) { ++ final PendingTeleport pendingTeleport = iterator.next(); ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, pendingTeleport.to())) { ++ ret.add(pendingTeleport); ++ iterator.remove(); ++ } ++ } ++ } ++ ++ return ret; ++ } ++ // Folia end - region threading + + public ServerLevel( + MinecraftServer server, +@@ -633,7 +_,7 @@ + ); + this.chunkSource.getGeneratorState().ensureStructuresGenerated(); + this.portalForcer = new PortalForcer(this); +- this.updateSkyBrightness(); ++ //this.updateSkyBrightness(); // Folia - region threading - delay until first tick + this.prepareWeather(); + this.getWorldBorder().setAbsoluteMaxSize(server.getAbsoluteMaxWorldSize()); + this.raids = this.getDataStorage().computeIfAbsent(Raids.factory(this), Raids.getFileId(this.dimensionTypeRegistration())); +@@ -681,7 +_,14 @@ + this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); + // Paper end - rewrite chunk system + this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit +- } ++ this.updateTickData(); // Folia - region threading - make sure it is initialised before ticked ++ } ++ ++ // Folia start - region threading ++ public void updateTickData() { ++ this.tickData = new io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData(this, this.serverLevelData.getGameTime(), this.serverLevelData.getDayTime()); ++ } ++ // Folia end - region threading + + // Paper start + @Override +@@ -709,61 +_,39 @@ + return this.getChunkSource().getGenerator().getBiomeSource().getNoiseBiome(x, y, z, this.getChunkSource().randomState().sampler()); + } + ++ @Override // Folia - region threading + public StructureManager structureManager() { + return this.structureManager; + } + +- public void tick(BooleanSupplier hasTimeLeft) { ++ public void tick(BooleanSupplier hasTimeLeft, io.papermc.paper.threadedregions.TickRegions.TickRegionData region) { // Folia - regionised ticking ++ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking + ProfilerFiller profilerFiller = Profiler.get(); +- this.handlingTick = true; ++ regionizedWorldData.setHandlingTick(true); // Folia - regionised ticking + TickRateManager tickRateManager = this.tickRateManager(); + boolean runsNormally = tickRateManager.runsNormally(); + if (runsNormally) { + profilerFiller.push("world border"); +- this.getWorldBorder().tick(); ++ //this.getWorldBorder().tick(); // Folia - regionised ticking + profilerFiller.popPush("weather"); +- this.advanceWeatherCycle(); ++ //this.advanceWeatherCycle(); // Folia - regionised ticking + profilerFiller.pop(); + } + +- int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); +- if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) { +- // Paper start - create time skip event - move up calculations +- final long newDayTime = this.levelData.getDayTime() + 24000L; +- org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent( +- this.getWorld(), +- org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP, +- (newDayTime - newDayTime % 24000L) - this.getDayTime() +- ); +- // Paper end - create time skip event - move up calculations +- if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { +- // Paper start - call time skip event if gamerule is enabled +- // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime +- // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param +- if (event.callEvent()) { +- this.setDayTime(this.getDayTime() + event.getSkipAmount()); +- } +- // Paper end - call time skip event if gamerule is enabled +- } +- +- if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled +- if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { +- this.resetWeatherCycle(); +- } +- } +- +- this.updateSkyBrightness(); ++ this.tickSleep(); // Folia - region threading - move into tickSleep ++ ++ //this.updateSkyBrightness(); // Folia - region threading + if (runsNormally) { + this.tickTime(); + } + + profilerFiller.push("tickPending"); + if (!this.isDebug() && runsNormally) { +- long l = this.getGameTime(); ++ long l = regionizedWorldData.getRedstoneGameTime(); // Folia - region threading + profilerFiller.push("blockTicks"); +- this.blockTicks.tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks ++ regionizedWorldData.getBlockLevelTicks().tick(l, paperConfig().environment.maxBlockTicks, this::tickBlock); // Paper - configurable max block ticks // Folia - region ticking + profilerFiller.popPush("fluidTicks"); +- this.fluidTicks.tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks ++ regionizedWorldData.getFluidLevelTicks().tick(l, paperConfig().environment.maxFluidTicks, this::tickFluid); // Paper - configurable max fluid ticks // Folia - region ticking + profilerFiller.pop(); + } + +@@ -779,9 +_,9 @@ + this.runBlockEvents(); + } + +- this.handlingTick = false; ++ regionizedWorldData.setHandlingTick(false); // Folia - regionised ticking + profilerFiller.pop(); +- boolean flag = !paperConfig().unsupportedSettings.disableWorldTickingWhenEmpty || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this ++ boolean flag = true || !this.players.isEmpty() || !this.getForcedChunks().isEmpty(); // CraftBukkit - this prevents entity cleanup, other issues on servers with no players // Paper - restore this // Folia - unrestore this, we always need to tick empty worlds + if (flag) { + this.resetEmptyTime(); + } +@@ -789,19 +_,29 @@ + if (flag || this.emptyTime++ < 300) { + profilerFiller.push("entities"); + if (this.dragonFight != null && runsNormally) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, this.dragonFight.origin)) { // Folia - region threading + profilerFiller.push("dragonFight"); + this.dragonFight.tick(); + profilerFiller.pop(); ++ } else { // Folia start - region threading ++ // try to load dragon fight ++ ChunkPos fightCenter = new ChunkPos(this.dragonFight.origin); ++ this.chunkSource.addTicketAtLevel( ++ TicketType.UNKNOWN, fightCenter, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ fightCenter ++ ); ++ } // Folia end - region threading + } + + io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR +- this.entityTickList +- .forEach( ++ regionizedWorldData // Folia - regionised ticking ++ .forEachTickingEntity( // Folia - regionised ticking + entity -> { + if (!entity.isRemoved()) { + if (!tickRateManager.isEntityFrozen(entity)) { + profilerFiller.push("checkDespawn"); + entity.checkDespawn(); ++ if (entity.isRemoved()) return; // Folia - region threading - if we despawned, DON'T TICK IT! + profilerFiller.pop(); + if (true) { // Paper - rewrite chunk system + Entity vehicle = entity.getVehicle(); +@@ -830,6 +_,36 @@ + profilerFiller.pop(); + } + ++ // Folia start - region threading ++ public void tickSleep() { ++ int _int = this.getGameRules().getInt(GameRules.RULE_PLAYERS_SLEEPING_PERCENTAGE); ++ if (this.sleepStatus.areEnoughSleeping(_int) && this.sleepStatus.areEnoughDeepSleeping(_int, this.players)) { ++ // Paper start - create time skip event - move up calculations ++ final long newDayTime = this.levelData.getDayTime() + 24000L; ++ org.bukkit.event.world.TimeSkipEvent event = new org.bukkit.event.world.TimeSkipEvent( ++ this.getWorld(), ++ org.bukkit.event.world.TimeSkipEvent.SkipReason.NIGHT_SKIP, ++ (newDayTime - newDayTime % 24000L) - this.getDayTime() ++ ); ++ // Paper end - create time skip event - move up calculations ++ if (this.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ // Paper start - call time skip event if gamerule is enabled ++ // long l = this.levelData.getDayTime() + 24000L; // Paper - diff on change to above - newDayTime ++ // this.setDayTime(l - l % 24000L); // Paper - diff on change to above - event param ++ if (event.callEvent()) { ++ this.setDayTime(this.getDayTime() + event.getSkipAmount()); ++ } ++ // Paper end - call time skip event if gamerule is enabled ++ } ++ ++ if (!event.isCancelled()) this.wakeUpAllPlayers(); // Paper - only wake up players if time skip event is not cancelled ++ if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE) && this.isRaining()) { ++ this.resetWeatherCycle(); ++ } ++ } ++ } ++ // Folia end - region threading ++ + @Override + public boolean shouldTickBlocksAt(long chunkPos) { + // Paper start - rewrite chunk system +@@ -840,12 +_,13 @@ + + protected void tickTime() { + if (this.tickTime) { +- long l = this.levelData.getGameTime() + 1L; +- this.serverLevelData.setGameTime(l); ++ io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading ++ long l = regionizedWorldData.getRedstoneGameTime() + 1L; // Folia - region threading ++ regionizedWorldData.setRedstoneGameTime(l); // Folia - region threading + Profiler.get().push("scheduledFunctions"); +- this.serverLevelData.getScheduledEvents().tick(this.server, l); ++ //this.serverLevelData.getScheduledEvents().tick(this.server, l); // Folia - region threading - TODO any way to bring this in? + Profiler.get().pop(); +- if (this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { ++ if (false && this.serverLevelData.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT)) { // Folia - region threading + this.setDayTime(this.levelData.getDayTime() + 1L); + } + } +@@ -863,16 +_,27 @@ + + private void wakeUpAllPlayers() { + this.sleepStatus.removeAllSleepers(); +- this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach(player -> player.stopSleepInBed(false, false)); ++ // Folia start - region threading ++ this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach((ServerPlayer entityplayer) -> { ++ // Folia start - region threading ++ entityplayer.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> { ++ if (player.level() != ServerLevel.this || !player.isSleeping()) { ++ return; ++ } ++ player.stopSleepInBed(false, false); ++ }, null, 1L); ++ } ++ ); ++ // Folia end - region threading + } + + // Paper start - optimise random ticking +- private final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = new ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); ++ private final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource.INSTANCE; // Folia - region threading + + private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) { + final LevelChunkSection[] sections = chunk.getSections(); + final int minSection = ca.spottedleaf.moonrise.common.util.WorldUtil.getMinSection((ServerLevel)(Object)this); +- final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; ++ final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Folia - region threading + final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294(); + + final ChunkPos cpos = chunk.getPos(); +@@ -919,7 +_,7 @@ + // Paper end - optimise random ticking + + public void tickChunk(LevelChunk chunk, int randomTickSpeed) { +- final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking ++ final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = this.simpleRandom; // Paper - optimise random ticking // Folia - region threading + ChunkPos pos = chunk.getPos(); + boolean isRaining = this.isRaining(); + int minBlockX = pos.getMinBlockX(); +@@ -1044,7 +_,7 @@ + } + + public boolean isHandlingTick() { +- return this.handlingTick; ++ return this.getCurrentWorldData().isHandlingTick(); // Folia - regionised ticking + } + + public boolean canSleepThroughNights() { +@@ -1070,6 +_,14 @@ + } + + public void updateSleepingPlayerList() { ++ // Folia start - region threading ++ if (!io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ ServerLevel.this.updateSleepingPlayerList(); ++ }); ++ return; ++ } ++ // Folia end - region threading + if (!this.players.isEmpty() && this.sleepStatus.update(this.players)) { + this.announceSleepStatus(); + } +@@ -1080,7 +_,7 @@ + return this.server.getScoreboard(); + } + +- private void advanceWeatherCycle() { ++ public void advanceWeatherCycle() { // Folia - region threading - public + boolean isRaining = this.isRaining(); + if (this.dimensionType().hasSkyLight()) { + if (this.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE)) { +@@ -1166,7 +_,8 @@ + this.server.getPlayerList().broadcastAll(new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, this.thunderLevel)); + } + */ +- for (ServerPlayer player : this.players) { ++ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading ++ for (ServerPlayer player : players) { // Folia - region threading + if (player.level() == this) { + player.tickWeather(); + } +@@ -1174,13 +_,13 @@ + + if (isRaining != this.isRaining()) { + // Only send weather packets to those affected +- for (ServerPlayer player : this.players) { ++ for (ServerPlayer player : players) { // Folia - region threading + if (player.level() == this) { + player.setPlayerWeather((!isRaining ? org.bukkit.WeatherType.DOWNFALL : org.bukkit.WeatherType.CLEAR), false); + } + } + } +- for (ServerPlayer player : this.players) { ++ for (ServerPlayer player : players) { // Folia - region threading + if (player.level() == this) { + player.updateWeather(this.oRainLevel, this.rainLevel, this.oThunderLevel, this.thunderLevel); + } +@@ -1241,13 +_,10 @@ + + // Paper start - log detailed entity tick information + // TODO replace with varhandle +- static final java.util.concurrent.atomic.AtomicReference currentlyTickingEntity = new java.util.concurrent.atomic.AtomicReference<>(); ++ // Folia - region threading + + public static List getCurrentlyTickingEntities() { +- Entity ticking = currentlyTickingEntity.get(); +- List ret = java.util.Arrays.asList(ticking == null ? new Entity[0] : new Entity[] { ticking }); +- +- return ret; ++ throw new UnsupportedOperationException(); // Folia - region threading + } + // Paper end - log detailed entity tick information + +@@ -1255,9 +_,7 @@ + // Paper start - log detailed entity tick information + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); + try { +- if (currentlyTickingEntity.get() == null) { +- currentlyTickingEntity.lazySet(entity); +- } ++ // Folia - region threading + // Paper end - log detailed entity tick information + entity.setOldPosAndRot(); + ProfilerFiller profilerFiller = Profiler.get(); +@@ -1267,7 +_,16 @@ + final boolean isActive = io.papermc.paper.entity.activation.ActivationRange.checkIfActive(entity); // Paper - EAR 2 + if (isActive) { // Paper - EAR 2 + entity.tick(); +- entity.postTick(); // CraftBukkit ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { ++ // removed from region while ticking ++ return; ++ } ++ if (entity.handlePortal()) { ++ // portalled ++ return; ++ } ++ // Folia end - region threading + } else {entity.inactiveTick();} // Paper - EAR 2 + profilerFiller.pop(); + +@@ -1276,9 +_,7 @@ + } + // Paper start - log detailed entity tick information + } finally { +- if (currentlyTickingEntity.get() == entity) { +- currentlyTickingEntity.lazySet(null); +- } ++ // Folia - region threading + } + // Paper end - log detailed entity tick information + } +@@ -1286,7 +_,7 @@ + private void tickPassenger(Entity ridingEntity, Entity passengerEntity, final boolean isActive) { // Paper - EAR 2 + if (passengerEntity.isRemoved() || passengerEntity.getVehicle() != ridingEntity) { + passengerEntity.stopRiding(); +- } else if (passengerEntity instanceof Player || this.entityTickList.contains(passengerEntity)) { ++ } else if (passengerEntity instanceof Player || this.getCurrentWorldData().hasEntityTickingEntity(passengerEntity)) { // Folia - region threading + passengerEntity.setOldPosAndRot(); + passengerEntity.tickCount++; + ProfilerFiller profilerFiller = Profiler.get(); +@@ -1295,7 +_,16 @@ + // Paper start - EAR 2 + if (isActive) { + passengerEntity.rideTick(); +- passengerEntity.postTick(); // CraftBukkit ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(passengerEntity)) { ++ // removed from region while ticking ++ return; ++ } ++ if (passengerEntity.handlePortal()) { ++ // portalled ++ return; ++ } ++ // Folia end - region threading + } else { + passengerEntity.setDeltaMovement(Vec3.ZERO); + passengerEntity.inactiveTick(); +@@ -1369,19 +_,20 @@ + } + // Paper end - add close param + +- // CraftBukkit start - moved from MinecraftServer.saveChunks ++ // Folia - move into saveLevelData ++ } ++ ++ public void saveLevelData(boolean join) { // Folia - public ++ if (this.dragonFight != null) { ++ this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit ++ } ++ // Folia start - moved into saveLevelData + ServerLevel worldserver1 = this; + + this.serverLevelData.setWorldBorder(worldserver1.getWorldBorder().createSettings()); + this.serverLevelData.setCustomBossEvents(this.server.getCustomBossEvents().save(this.registryAccess())); + this.levelStorageAccess.saveDataTag(this.server.registryAccess(), this.serverLevelData, this.server.getPlayerList().getSingleplayerData()); +- // CraftBukkit end +- } +- +- private void saveLevelData(boolean join) { +- if (this.dragonFight != null) { +- this.serverLevelData.setEndDragonFightData(this.dragonFight.saveData()); // CraftBukkit +- } ++ // Folia end - moved into saveLevelData + + DimensionDataStorage dataStorage = this.getChunkSource().getDataStorage(); + if (join) { +@@ -1437,6 +_,19 @@ + return list; + } + ++ // Folia start - region threading ++ @Nullable ++ public ServerPlayer getRandomLocalPlayer() { ++ List list = this.getLocalPlayers(); ++ list = new java.util.ArrayList<>(list); ++ list.removeIf((ServerPlayer player) -> { ++ return !player.isAlive(); ++ }); ++ ++ return list.isEmpty() ? null : (ServerPlayer) list.get(this.random.nextInt(list.size())); ++ } ++ // Folia end - region threading ++ + @Nullable + public ServerPlayer getRandomPlayer() { + List players = this.getPlayers(LivingEntity::isAlive); +@@ -1518,8 +_,8 @@ + } else { + if (entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity && itemEntity.getItem().isEmpty()) return false; // Paper - Prevent empty items from being added + // Paper start - capture all item additions to the world +- if (captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { +- captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); ++ if (this.getCurrentWorldData().captureDrops != null && entity instanceof net.minecraft.world.entity.item.ItemEntity) { // Folia - region threading ++ this.getCurrentWorldData().captureDrops.add((net.minecraft.world.entity.item.ItemEntity) entity); // Folia - region threading + return true; + } + // Paper end - capture all item additions to the world +@@ -1694,13 +_,14 @@ + + @Override + public void sendBlockUpdated(BlockPos pos, BlockState oldState, BlockState newState, int flags) { +- if (this.isUpdatingNavigations) { ++ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - region threading ++ if (false && this.isUpdatingNavigations) { // Folia - region threading + String string = "recursive call to sendBlockUpdated"; + Util.logAndPauseIfInIde("recursive call to sendBlockUpdated", new IllegalStateException("recursive call to sendBlockUpdated")); + } + + this.getChunkSource().blockChanged(pos); +- this.pathTypesByPosCache.invalidate(pos); ++ regionizedWorldData.pathTypesByPosCache.invalidate(pos); // Folia - region threading + if (this.paperConfig().misc.updatePathfindingOnBlockUpdate) { // Paper - option to disable pathfinding updates + VoxelShape collisionShape = oldState.getCollisionShape(this, pos); + VoxelShape collisionShape1 = newState.getCollisionShape(this, pos); +@@ -1708,7 +_,8 @@ + List list = new ObjectArrayList<>(); + + try { // Paper - catch CME see below why +- for (Mob mob : this.navigatingMobs) { ++ for (java.util.Iterator iterator = regionizedWorldData.getNavigatingMobs(); iterator.hasNext();) { // Folia - region threading ++ Mob mob = iterator.next(); // Folia - region threading + PathNavigation navigation = mob.getNavigation(); + if (navigation.shouldRecomputePath(pos)) { + list.add(navigation); +@@ -1725,13 +_,13 @@ + // Paper end - catch CME see below why + + try { +- this.isUpdatingNavigations = true; ++ //this.isUpdatingNavigations = true; // Folia - region threading + + for (PathNavigation pathNavigation : list) { + pathNavigation.recomputePath(); + } + } finally { +- this.isUpdatingNavigations = false; ++ //this.isUpdatingNavigations = false; // Folia - region threading + } + } + } // Paper - option to disable pathfinding updates +@@ -1739,29 +_,29 @@ + + @Override + public void updateNeighborsAt(BlockPos pos, Block block) { +- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement ++ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading + this.updateNeighborsAt(pos, block, ExperimentalRedstoneUtils.initialOrientation(this, null, null)); + } + + @Override + public void updateNeighborsAt(BlockPos pos, Block block, @Nullable Orientation orientation) { +- if (captureBlockStates) { return; } // Paper - Cancel all physics during placement +- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation); ++ if (this.getCurrentWorldData().captureBlockStates) { return; } // Paper - Cancel all physics during placement // Folia - region threading ++ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, null, orientation); // Folia - region threading + } + + @Override + public void updateNeighborsAtExceptFromFacing(BlockPos pos, Block block, Direction facing, @Nullable Orientation orientation) { +- this.neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, facing, orientation); ++ this.getCurrentWorldData().neighborUpdater.updateNeighborsAtExceptFromFacing(pos, block, facing, orientation); // Folia - region threading + } + + @Override + public void neighborChanged(BlockPos pos, Block block, @Nullable Orientation orientation) { +- this.neighborUpdater.neighborChanged(pos, block, orientation); ++ this.getCurrentWorldData().neighborUpdater.neighborChanged(pos, block, orientation); // Folia - region threading + } + + @Override + public void neighborChanged(BlockState state, BlockPos pos, Block block, @Nullable Orientation orientation, boolean movedByPiston) { +- this.neighborUpdater.neighborChanged(state, pos, block, orientation, movedByPiston); ++ this.getCurrentWorldData().neighborUpdater.neighborChanged(state, pos, block, orientation, movedByPiston); // Folia - region threading + } + + @Override +@@ -1851,7 +_,7 @@ + // CraftBukkit end + ParticleOptions particleOptions = serverExplosion.isSmall() ? smallExplosionParticles : largeExplosionParticles; + +- for (ServerPlayer serverPlayer : this.players) { ++ for (ServerPlayer serverPlayer : this.getLocalPlayers()) { // Folia - region thraeding + if (serverPlayer.distanceToSqr(vec3) < 4096.0) { + Optional optional = Optional.ofNullable(serverExplosion.getHitPlayers().get(serverPlayer)); + serverPlayer.connection.send(new ClientboundExplodePacket(vec3, optional, particleOptions, explosionSound)); +@@ -1867,14 +_,17 @@ + + @Override + public void blockEvent(BlockPos pos, Block block, int eventID, int eventParam) { +- this.blockEvents.add(new BlockEventData(pos, block, eventID, eventParam)); ++ this.getCurrentWorldData().pushBlockEvent(new BlockEventData(pos, block, eventID, eventParam)); // Folia - regionised ticking + } + + private void runBlockEvents() { +- this.blockEventsToReschedule.clear(); ++ List blockEventsToReschedule = new ArrayList<>(64); // Folia - regionised ticking + +- while (!this.blockEvents.isEmpty()) { +- BlockEventData blockEventData = this.blockEvents.removeFirst(); ++ // Folia start - regionised ticking ++ io.papermc.paper.threadedregions.RegionizedWorldData worldRegionData = this.getCurrentWorldData(); ++ BlockEventData blockEventData; ++ while ((blockEventData = worldRegionData.removeFirstBlockEvent()) != null) { ++ // Folia end - regionised ticking + if (this.shouldTickBlocksAt(blockEventData.pos())) { + if (this.doBlockEvent(blockEventData)) { + this.server +@@ -1890,11 +_,11 @@ + ); + } + } else { +- this.blockEventsToReschedule.add(blockEventData); ++ blockEventsToReschedule.add(blockEventData); // Folia - regionised ticking + } + } + +- this.blockEvents.addAll(this.blockEventsToReschedule); ++ worldRegionData.pushBlockEvents(blockEventsToReschedule); // Folia - regionised ticking + } + + private boolean doBlockEvent(BlockEventData event) { +@@ -1904,12 +_,12 @@ + + @Override + public LevelTicks getBlockTicks() { +- return this.blockTicks; ++ return this.getCurrentWorldData().getBlockLevelTicks(); // Folia - region ticking + } + + @Override + public LevelTicks getFluidTicks() { +- return this.fluidTicks; ++ return this.getCurrentWorldData().getFluidLevelTicks(); // Folia - region ticking + } + + @Nonnull +@@ -1962,7 +_,7 @@ + double zOffset, + double speed + ) { +- return sendParticlesSource(this.players, sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed); ++ return sendParticlesSource(this.getLocalPlayers(), sender, type, overrideLimiter, alwaysShow, posX, posY, posZ, particleCount, xOffset, yOffset, zOffset, speed); // Folia - region threading + } + public int sendParticlesSource( + List receivers, +@@ -2045,12 +_,12 @@ + @Nullable + public Entity getEntityOrPart(int id) { + Entity entity = this.getEntities().get(id); +- return entity != null ? entity : this.dragonParts.get(id); ++ return entity != null ? entity : this.dragonParts.get((long)id); // Folia - diff on change + } + + @Override + public Collection dragonParts() { +- return this.dragonParts.values(); ++ return this.dragonParts.values(); // Folia - diff on change + } + + @Nullable +@@ -2105,6 +_,7 @@ + // Paper start - Call missing map initialize event and set id + final DimensionDataStorage storage = this.getServer().overworld().getDataStorage(); + ++ synchronized (storage.cache) { // Folia - region threading + final Optional cacheEntry = storage.cache.get(mapId.key()); + if (cacheEntry == null) { // Cache did not contain, try to load and may init + final MapItemSavedData mapData = storage.get(MapItemSavedData.factory(), mapId.key()); // get populates the cache +@@ -2124,6 +_,7 @@ + } + + return null; ++ } // Folia - region threading + // Paper end - Call missing map initialize event and set id + } + +@@ -2178,6 +_,7 @@ + } + + public boolean setChunkForced(int chunkX, int chunkZ, boolean add) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot modify force loaded chunks off of the global region"); // Folia - region threading + ForcedChunksSavedData forcedChunksSavedData = this.getDataStorage().computeIfAbsent(ForcedChunksSavedData.factory(), "chunks"); + ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); + long packedChunkPos = chunkPos.toLong(); +@@ -2185,7 +_,7 @@ + if (add) { + flag = forcedChunksSavedData.getChunks().add(packedChunkPos); + if (flag) { +- this.getChunk(chunkX, chunkZ); ++ //this.getChunk(chunkX, chunkZ); // Folia - region threading - we must let the chunk load asynchronously + } + } else { + flag = forcedChunksSavedData.getChunks().remove(packedChunkPos); +@@ -2210,11 +_,24 @@ + Optional> optional1 = PoiTypes.forState(newState); + if (!Objects.equals(optional, optional1)) { + BlockPos blockPos = pos.immutable(); +- optional.ifPresent(poiType -> this.getServer().execute(() -> { ++ // Folia start - region threading ++ optional.ifPresent(poiType -> { ++ Runnable run = () -> { ++ // Folia end - region threading + this.getPoiManager().remove(blockPos); + DebugPackets.sendPoiRemovedPacket(this, blockPos); +- })); +- optional1.ifPresent(poiType -> this.getServer().execute(() -> { ++ // Folia start - region threading ++ }; ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( ++ this, blockPos.getX() >> 4, blockPos.getZ() >> 4, run ++ ); ++ }); ++ // Folia end - region threading ++ // Folia start - region threading ++ optional1.ifPresent(poiType -> { ++ Runnable run = () -> { ++ // Folia end - region threading + // Paper start - Remove stale POIs + if (optional.isEmpty() && this.getPoiManager().exists(blockPos, ignored -> true)) { + this.getPoiManager().remove(blockPos); +@@ -2222,7 +_,15 @@ + // Paper end - Remove stale POIs + this.getPoiManager().add(blockPos, (Holder)poiType); + DebugPackets.sendPoiAddedPacket(this, blockPos); +- })); ++ // Folia start - region threading ++ }; ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueChunkTask( ++ this, blockPos.getX() >> 4, blockPos.getZ() >> 4, run ++ ); ++ // Folia end - region threading ++ }); ++ // Folia end - region threading + } + } + +@@ -2276,7 +_,7 @@ + } + + bufferedWriter.write(String.format(Locale.ROOT, "entities: %s\n", this.moonrise$getEntityLookup().getDebugInfo())); // Paper - rewrite chunk system +- bufferedWriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); ++ //bufferedWriter.write(String.format(Locale.ROOT, "block_entity_tickers: %d\n", this.blockEntityTickers.size())); // Folia - region threading + bufferedWriter.write(String.format(Locale.ROOT, "block_ticks: %d\n", this.getBlockTicks().count())); + bufferedWriter.write(String.format(Locale.ROOT, "fluid_ticks: %d\n", this.getFluidTicks().count())); + bufferedWriter.write("distance_manager: " + chunkMap.getDistanceManager().getDebugStatus() + "\n"); +@@ -2346,7 +_,7 @@ + private void dumpBlockEntityTickers(Writer output) throws IOException { + CsvOutput csvOutput = CsvOutput.builder().addColumn("x").addColumn("y").addColumn("z").addColumn("type").build(output); + +- for (TickingBlockEntity tickingBlockEntity : this.blockEntityTickers) { ++ for (TickingBlockEntity tickingBlockEntity : (Iterable)null) { // Folia - region threading + BlockPos pos = tickingBlockEntity.getPos(); + csvOutput.writeRow(pos.getX(), pos.getY(), pos.getZ(), tickingBlockEntity.getType()); + } +@@ -2354,14 +_,14 @@ + + @VisibleForTesting + public void clearBlockEvents(BoundingBox boundingBox) { +- this.blockEvents.removeIf(blockEventData -> boundingBox.isInside(blockEventData.pos())); ++ this.getCurrentWorldData().removeIfBlockEvents(blockEventData -> boundingBox.isInside(blockEventData.pos())); // Folia - regionised ticking + } + + @Override + public void blockUpdated(BlockPos pos, Block block) { + if (!this.isDebug()) { + // CraftBukkit start +- if (this.populating) { ++ if (this.getCurrentWorldData().populating) { // Folia - region threading + return; + } + // CraftBukkit end +@@ -2410,8 +_,8 @@ + this.players.size(), + this.moonrise$getEntityLookup().getDebugInfo(), // Paper - rewrite chunk system + getTypeCount(this.moonrise$getEntityLookup().getAll(), entity -> BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString()), // Paper - rewrite chunk system +- this.blockEntityTickers.size(), +- getTypeCount(this.blockEntityTickers, TickingBlockEntity::getType), ++ 0, // Folia - region threading ++ "null", // Folia - region threading + this.getBlockTicks().count(), + this.getFluidTicks().count(), + this.gatherChunkSourceStats() +@@ -2463,15 +_,15 @@ + } + + public void startTickingChunk(LevelChunk chunk) { +- chunk.unpackTicks(this.getLevelData().getGameTime()); ++ chunk.unpackTicks(this.getRedstoneGameTime()); // Folia - region threading + } + + public void onStructureStartsAvailable(ChunkAccess chunk) { +- this.server.execute(() -> this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts())); ++ this.structureCheck.onStructureLoad(chunk.getPos(), chunk.getAllStarts()); // Folia - region threading + } + + public PathTypeCache getPathTypeCache() { +- return this.pathTypesByPosCache; ++ return this.getCurrentWorldData().pathTypesByPosCache; // Folia - region threading + } + + @Override +@@ -2489,7 +_,7 @@ + return this.moonrise$getAnyChunkIfLoaded(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkPos)) != null; // Paper - rewrite chunk system + } + +- private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { ++ public boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { // Folia - region threaded - make public + // Paper start - rewrite chunk system + final ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkHolder = this.moonrise$getChunkTaskScheduler().chunkHolderManager.getChunkHolder(chunkPos); + // isTicking implies the chunk is loaded, and the chunk is loaded now implies the entities are loaded +@@ -2581,7 +_,7 @@ + // Paper start - optimize redstone (Alternate Current) + @Override + public alternate.current.wire.WireHandler getWireHandler() { +- return wireHandler; ++ return this.getCurrentWorldData().wireHandler; // Folia - region threading + } + // Paper end - optimize redstone (Alternate Current) + +@@ -2592,18 +_,18 @@ + + @Override + public void onDestroyed(Entity entity) { +- ServerLevel.this.getScoreboard().entityRemoved(entity); ++ // ServerLevel.this.getScoreboard().entityRemoved(entity); // Folia - region threading + } + + @Override + public void onTickingStart(Entity entity) { + if (entity instanceof net.minecraft.world.entity.Marker && !paperConfig().entities.markers.tick) return; // Paper - Configurable marker ticking +- ServerLevel.this.entityTickList.add(entity); ++ ServerLevel.this.getCurrentWorldData().addEntityTickingEntity(entity); // Folia - region threading + } + + @Override + public void onTickingEnd(Entity entity) { +- ServerLevel.this.entityTickList.remove(entity); ++ ServerLevel.this.getCurrentWorldData().removeEntityTickingEntity(entity); // Folia - region threading + // Paper start - Reset pearls when they stop being ticked + if (ServerLevel.this.paperConfig().fixes.disableUnloadedChunkEnderpearlExploit && ServerLevel.this.paperConfig().misc.legacyEnderPearlBehavior && entity instanceof net.minecraft.world.entity.projectile.ThrownEnderpearl pearl) { + pearl.cachedOwner = null; +@@ -2615,6 +_,7 @@ + @Override + public void onTrackingStart(Entity entity) { + org.spigotmc.AsyncCatcher.catchOp("entity register"); // Spigot ++ ServerLevel.this.getCurrentWorldData().addLoadedEntity(entity); // Folia - region threading + // ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server; moved down below valid=true + if (entity instanceof ServerPlayer serverPlayer) { + ServerLevel.this.players.add(serverPlayer); +@@ -2629,12 +_,12 @@ + ); + } + +- ServerLevel.this.navigatingMobs.add(mob); ++ ServerLevel.this.getCurrentWorldData().addNavigatingMob(mob); // Folia - region threading + } + + if (entity instanceof EnderDragon enderDragon) { + for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) { +- ServerLevel.this.dragonParts.put(enderDragonPart.getId(), enderDragonPart); ++ ServerLevel.this.dragonParts.put((long)enderDragonPart.getId(), enderDragonPart); // Folia - diff on change + } + } + +@@ -2657,18 +_,27 @@ + @Override + public void onTrackingEnd(Entity entity) { + org.spigotmc.AsyncCatcher.catchOp("entity unregister"); // Spigot ++ ServerLevel.this.getCurrentWorldData().removeLoadedEntity(entity); // Folia - region threading + // Spigot start // TODO I don't think this is needed anymore + if (entity instanceof Player player) { + for (final ServerLevel level : ServerLevel.this.getServer().getAllLevels()) { +- for (final Optional savedData : level.getDataStorage().cache.values()) { ++ // Folia start - make map data thread-safe ++ List> worldDataCache; ++ synchronized (level.getDataStorage().cache) { ++ worldDataCache = new java.util.ArrayList<>(level.getDataStorage().cache.values()); ++ } ++ for (final Optional savedData : worldDataCache) { ++ // Folia end - make map data thread-safe + if (savedData.isEmpty() || !(savedData.get() instanceof MapItemSavedData map)) { + continue; + } + ++ synchronized (map) { // Folia - make map data thread-safe + map.carriedByPlayers.remove(player); + if (map.carriedBy.removeIf(holdingPlayer -> holdingPlayer.player == player)) { + map.decorations.remove(player.getName().getString()); + } ++ } // Folia - make map data thread-safe + } + } + } +@@ -2699,18 +_,19 @@ + ); + } + +- ServerLevel.this.navigatingMobs.remove(mob); ++ ServerLevel.this.getCurrentWorldData().removeNavigatingMob(mob); // Folia - region threading + } + + if (entity instanceof EnderDragon enderDragon) { + for (EnderDragonPart enderDragonPart : enderDragon.getSubEntities()) { +- ServerLevel.this.dragonParts.remove(enderDragonPart.getId()); ++ ServerLevel.this.dragonParts.remove((long)enderDragonPart.getId()); // Folia - diff on change + } + } + + entity.updateDynamicGameEventListener(DynamicGameEventListener::remove); + // CraftBukkit start + entity.valid = false; ++ // Folia - region threading - TODO THIS SHIT + if (!(entity instanceof ServerPlayer)) { + for (ServerPlayer player : ServerLevel.this.server.getPlayerList().players) { // Paper - call onEntityRemove for all online players + player.getBukkitEntity().onEntityRemove(entity); +@@ -2738,11 +_,11 @@ + private long lagCompensationTick = MinecraftServer.SERVER_INIT; + + public long getLagCompensationTick() { +- return this.lagCompensationTick; ++ return this.getCurrentWorldData().getLagCompensationTick(); // Folia - region threading + } + + public void updateLagCompensationTick() { +- this.lagCompensationTick = (System.nanoTime() - MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); ++ throw new UnsupportedOperationException(); // Folia - region threading + } + // Paper end - lag compensation + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch new file mode 100644 index 0000000..ee3e8de --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch @@ -0,0 +1,550 @@ +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -180,7 +_,7 @@ + + public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system + private static final Logger LOGGER = LogUtils.getLogger(); +- public long lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving ++ public static final long LAST_SAVE_ABSENT = Long.MIN_VALUE; public long lastSave = LAST_SAVE_ABSENT; // Paper // Folia - threaded regions - changed to nanoTime + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; + private static final int FLY_STAT_RECORDING_SPEED = 25; +@@ -443,8 +_,149 @@ + this.maxHealthCache = this.getMaxHealth(); + } + ++ // Folia start - region threading ++ private static final int SPAWN_RADIUS_SELECTION_SEARCH = 5; ++ ++ private static BlockPos getRandomSpawn(ServerLevel world, RandomSource random) { ++ BlockPos spawn = world.getSharedSpawnPos(); ++ double radius = (double)Math.max(0, world.getGameRules().getInt(GameRules.RULE_SPAWN_RADIUS)); ++ ++ double spawnX = (double)spawn.getX() + 0.5; ++ double spawnZ = (double)spawn.getZ() + 0.5; ++ ++ net.minecraft.world.level.border.WorldBorder worldBorder = world.getWorldBorder(); ++ ++ double selectMinX = Math.max(worldBorder.getMinX() + 1.0, spawnX - radius); ++ double selectMinZ = Math.max(worldBorder.getMinZ() + 1.0, spawnZ - radius); ++ double selectMaxX = Math.min(worldBorder.getMaxX() - 1.0, spawnX + radius); ++ double selectMaxZ = Math.min(worldBorder.getMaxZ() - 1.0, spawnZ + radius); ++ ++ double amountX = selectMaxX - selectMinX; ++ double amountZ = selectMaxZ - selectMinZ; ++ ++ int selectX = amountX < 1.0 ? Mth.floor(worldBorder.getCenterX()) : (int)Mth.floor((amountX + 1.0) * random.nextDouble() + selectMinX); ++ int selectZ = amountZ < 1.0 ? Mth.floor(worldBorder.getCenterZ()) : (int)Mth.floor((amountZ + 1.0) * random.nextDouble() + selectMinZ); ++ ++ return new BlockPos(selectX, 0, selectZ); ++ } ++ ++ private static void completeSpawn(ServerLevel world, BlockPos selected, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(selected), world.levelData.getSpawnAngle(), 0.0f)); ++ } ++ ++ private static BlockPos findSpawnAround(ServerLevel world, ServerPlayer player, BlockPos selected) { ++ // try hard to find, so that we don't attempt another chunk load ++ for (int dz = -SPAWN_RADIUS_SELECTION_SEARCH; dz <= SPAWN_RADIUS_SELECTION_SEARCH; ++dz) { ++ for (int dx = -SPAWN_RADIUS_SELECTION_SEARCH; dx <= SPAWN_RADIUS_SELECTION_SEARCH; ++dx) { ++ BlockPos inChunk = PlayerRespawnLogic.getOverworldRespawnPos(world, selected.getX() + dx, selected.getZ() + dz); ++ if (inChunk == null) { ++ continue; ++ } ++ ++ AABB checkVolume = player.getBoundingBoxAt((double)inChunk.getX() + 0.5, (double)inChunk.getY(), (double)inChunk.getZ() + 0.5); ++ ++ if (!player.noCollisionNoLiquid(world, checkVolume)) { ++ continue; ++ } ++ ++ return inChunk; ++ } ++ } ++ ++ return null; ++ } ++ ++ // rets false when another attempt is required ++ private static boolean trySpawnOrSchedule(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { ++ ++attemptCount[0]; ++ ++ BlockPos rough = getRandomSpawn(world, random); ++ ++ // add 2 to ensure that the chunks are loaded for collision checks ++ int minX = (rough.getX() - (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; ++ int minZ = (rough.getZ() - (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; ++ int maxX = (rough.getX() + (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; ++ int maxZ = (rough.getZ() + (SPAWN_RADIUS_SELECTION_SEARCH + 2)) >> 4; ++ ++ // we could short circuit this check, but it would possibly recurse. Then, it could end up causing a stack overflow ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, minX, minZ, maxX, maxZ) || !world.moonrise$areChunksLoaded(minX, minZ, maxX, maxZ)) { ++ world.moonrise$loadChunksAsync(minX, maxX, minZ, maxZ, ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (unused) -> { ++ BlockPos selected = findSpawnAround(world, player, rough); ++ if (selected == null) { ++ // run more spawn attempts ++ selectSpawn(world, player, random, attemptCount, maxAttempts, toComplete); ++ return; ++ } ++ ++ completeSpawn(world, selected, toComplete); ++ return; ++ } ++ ); ++ return true; ++ } ++ ++ BlockPos selected = findSpawnAround(world, player, rough); ++ if (selected == null) { ++ return false; ++ } ++ ++ completeSpawn(world, selected, toComplete); ++ return true; ++ } ++ ++ private static void selectSpawn(ServerLevel world, ServerPlayer player, RandomSource random, int[] attemptCount, int maxAttempts, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { ++ do { ++ if (attemptCount[0] >= maxAttempts) { ++ BlockPos sharedSpawn = world.getSharedSpawnPos(); ++ ++ LOGGER.warn("Found no spawn in radius for player '" + player.getName() + "', ignoring radius"); ++ ++ selectSpawnWithoutRadius(world, player, sharedSpawn, toComplete); ++ return; ++ } ++ } while (!trySpawnOrSchedule(world, player, random, attemptCount, maxAttempts, toComplete)); ++ } ++ ++ ++ private static void selectSpawnWithoutRadius(ServerLevel world, ServerPlayer player, BlockPos spawn, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { ++ world.loadChunksForMoveAsync(player.getBoundingBoxAt(spawn.getX() + 0.5, spawn.getY(), spawn.getZ() + 0.5), ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (c) -> { ++ BlockPos ret = spawn; ++ while (!player.noCollisionNoLiquid(world, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY(), ret.getZ() + 0.5)) && ret.getY() < (double)world.getMaxY()) { ++ ret = ret.above(); ++ } ++ while (player.noCollisionNoLiquid(world, player.getBoundingBoxAt(ret.getX() + 0.5, ret.getY() - 1, ret.getZ() + 0.5)) && ret.getY() > (double)(world.getMinY() + 1)) { ++ ret = ret.below(); ++ } ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(world, Vec3.atBottomCenterOf(ret), world.levelData.getSpawnAngle(), 0.0f)); ++ } ++ ); ++ } ++ ++ public static void fudgeSpawnLocation(ServerLevel world, ServerPlayer player, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { // Folia - region threading ++ BlockPos blockposition = world.getSharedSpawnPos(); ++ ++ if (world.dimensionType().hasSkyLight() && world.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit ++ selectSpawn(world, player, player.random, new int[1], 500, toComplete); ++ } else { ++ selectSpawnWithoutRadius(world, player, blockposition, toComplete); ++ } ++ ++ } ++ // Folia end - region threading ++ + @Override + public BlockPos adjustSpawnLocation(ServerLevel level, BlockPos pos) { ++ // Folia start - region threading ++ if (true) { ++ throw new UnsupportedOperationException(); ++ } ++ // Folia end - region threading + AABB aabb = this.getDimensions(Pose.STANDING).makeBoundingBox(Vec3.ZERO); + BlockPos blockPos = pos; + if (level.dimensionType().hasSkyLight() && level.serverLevelData.getGameType() != GameType.ADVENTURE) { // CraftBukkit +@@ -533,7 +_,7 @@ + this.getBukkitEntity().readExtraData(compound); // CraftBukkit + + if (this.isSleeping()) { +- this.stopSleeping(); ++ this.stopSleepingRaw(); // Folia - do not modify or read worldstate during data deserialization + } + + // CraftBukkit start +@@ -709,10 +_,17 @@ + ServerLevel level = this.level().getServer().getLevel(optional.get()); + if (level != null) { + Entity entity = EntityType.loadEntityRecursive( +- compoundTag, level, EntitySpawnReason.LOAD, entity1 -> !level.addWithUUID(entity1) ? null : entity1 ++ compoundTag, level, EntitySpawnReason.LOAD, entity1 -> entity1 // Folia - region threading - delay world add + ); + if (entity != null) { +- placeEnderPearlTicket(level, entity.chunkPosition()); ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ level, entity.chunkPosition().x, entity.chunkPosition().z, () -> { ++ level.addFreshEntityWithPassengers(entity); ++ ServerPlayer.placeEnderPearlTicket(level, entity.chunkPosition()); ++ } ++ ); ++ // Folia end - region threading + } else { + LOGGER.warn("Failed to spawn player ender pearl in level ({}), skipping", optional.get()); + } +@@ -1357,6 +_,324 @@ + } + } + ++ // Folia start - region threading ++ /** ++ * Teleport flag indicating that the player is to be respawned, expected to only be used ++ * internally for {@link #respawn(java.util.function.Consumer, PlayerRespawnEvent.RespawnReason)} ++ */ ++ public static final long TELEPORT_FLAGS_PLAYER_RESPAWN = Long.MIN_VALUE >>> 0; ++ ++ public void exitEndCredits() { ++ if (!this.wonGame) { ++ // not in the end credits anymore ++ return; ++ } ++ this.wonGame = false; ++ ++ this.respawn((player) -> { ++ CriteriaTriggers.CHANGED_DIMENSION.trigger(player, Level.END, Level.OVERWORLD); ++ }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.END_PORTAL, true); ++ } ++ ++ public void respawn(java.util.function.Consumer respawnComplete, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason reason) { ++ this.respawn(respawnComplete, reason, false); ++ } ++ ++ private void respawn(java.util.function.Consumer respawnComplete, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason reason, boolean alive) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot respawn entity async"); ++ ++ this.getBukkitEntity(); // force bukkit entity to be created before TPing ++ ++ if (alive != this.isAlive()) { ++ throw new IllegalStateException("isAlive expected = " + alive); ++ } ++ ++ if (!this.hasNullCallback()) { ++ this.unRide(); ++ } ++ ++ if (this.isVehicle() || this.isPassenger()) { ++ throw new IllegalStateException("Dead player should not be a vehicle or passenger"); ++ } ++ ++ ServerLevel origin = this.serverLevel(); ++ ServerLevel respawnWorld = this.server.getLevel(this.getRespawnDimension()); ++ ++ // modified based off PlayerList#respawn ++ ++ EntityTreeNode passengerTree = this.makePassengerTree(); ++ ++ this.isChangingDimension = true; ++ origin.removePlayerImmediately(this, RemovalReason.CHANGED_DIMENSION); ++ // reset player if needed, only after removal from world ++ if (!alive) { ++ ServerPlayer.this.reset(); ++ } ++ // must be manually removed from connections, delay until after reset() so that we do not trip any thread checks ++ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection); ++ ++ BlockPos respawnPos = this.getRespawnPosition(); ++ float respawnAngle = this.getRespawnAngle(); ++ boolean isRespawnForced = this.isRespawnForced(); ++ ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable spawnPosComplete = ++ new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ boolean[] usedRespawnAnchor = new boolean[1]; ++ ++ // set up post spawn location logic ++ spawnPosComplete.addWaiter((spawnLoc, throwable) -> { ++ // update pos and velocity ++ ServerPlayer.this.setPosRaw(spawnLoc.getX(), spawnLoc.getY(), spawnLoc.getZ()); ++ ServerPlayer.this.setYRot(spawnLoc.getYaw()); ++ ServerPlayer.this.setYHeadRot(spawnLoc.getYaw()); ++ ServerPlayer.this.setXRot(spawnLoc.getPitch()); ++ ServerPlayer.this.setDeltaMovement(Vec3.ZERO); ++ // placeInAsync will update the world ++ ++ this.placeInAsync( ++ origin, ++ // use the load chunk flag just in case the spawn loc isn't loaded, and to ensure the chunks ++ // stay loaded for a bit with the teleport ticket ++ ((org.bukkit.craftbukkit.CraftWorld)spawnLoc.getWorld()).getHandle(), ++ TELEPORT_FLAG_LOAD_CHUNK | TELEPORT_FLAGS_PLAYER_RESPAWN, ++ passengerTree, // note: we expect this to just be the player, no passengers ++ (entity) -> { ++ // now the player is in the world, and can receive sound ++ if (usedRespawnAnchor[0]) { ++ ServerPlayer.this.connection.send( ++ new ClientboundSoundPacket( ++ net.minecraft.sounds.SoundEvents.RESPAWN_ANCHOR_DEPLETE, SoundSource.BLOCKS, ++ ServerPlayer.this.getX(), ServerPlayer.this.getY(), ServerPlayer.this.getZ(), ++ 1.0F, 1.0F, ServerPlayer.this.serverLevel().getRandom().nextLong() ++ ) ++ ); ++ } ++ // now the respawn logic is complete ++ ++ // last, call the function callback ++ if (respawnComplete != null) { ++ respawnComplete.accept(ServerPlayer.this); ++ } ++ } ++ ); ++ }); ++ ++ // find and modify respawn block state ++ if (respawnWorld == null || respawnPos == null) { ++ // default to regular spawn ++ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); ++ } else { ++ // load chunk for block ++ // give at least 1 radius of loaded chunks so that we do not sync load anything ++ int radiusBlocks = 16; ++ respawnWorld.moonrise$loadChunksAsync(respawnPos, radiusBlocks, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (chunks) -> { ++ ServerPlayer.RespawnPosAngle spawnPos = ServerPlayer.findRespawnAndUseSpawnBlock( ++ respawnWorld, respawnPos, respawnAngle, isRespawnForced, !alive ++ ).orElse(null); ++ if (spawnPos == null) { ++ // no spawn ++ ServerPlayer.this.connection.send( ++ new ClientboundGameEventPacket(ClientboundGameEventPacket.NO_RESPAWN_BLOCK_AVAILABLE, 0.0F) ++ ); ++ ServerPlayer.this.setRespawnPosition( ++ null, null, 0f, false, false, ++ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN ++ ); ++ // default to regular spawn ++ fudgeSpawnLocation(this.server.getLevel(Level.OVERWORLD), this, spawnPosComplete); ++ return; ++ } ++ ++ boolean isRespawnAnchor = respawnWorld.getBlockState(respawnPos).is(net.minecraft.world.level.block.Blocks.RESPAWN_ANCHOR); ++ boolean isBed = respawnWorld.getBlockState(respawnPos).is(net.minecraft.tags.BlockTags.BEDS); ++ usedRespawnAnchor[0] = !alive && isRespawnAnchor; ++ ++ ServerPlayer.this.setRespawnPosition( ++ respawnWorld.dimension(), respawnPos, respawnAngle, isRespawnForced, false, ++ com.destroystokyo.paper.event.player.PlayerSetSpawnEvent.Cause.PLAYER_RESPAWN ++ ); ++ ++ // finished now, pass the location on ++ spawnPosComplete.complete( ++ io.papermc.paper.util.MCUtil.toLocation(respawnWorld, spawnPos.position(), spawnPos.yaw(), 0.0f) ++ ); ++ return; ++ } ++ ); ++ } ++ } ++ ++ @Override ++ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ if (yaw != null) { ++ this.setYRot(yaw.floatValue()); ++ this.setYHeadRot(yaw.floatValue()); ++ } ++ if (pitch != null) { ++ this.setXRot(pitch.floatValue()); ++ } ++ if (velocity != null) { ++ this.setDeltaMovement(velocity); ++ } ++ this.connection.internalTeleport( ++ new net.minecraft.world.entity.PositionMoveRotation( ++ pos, this.getDeltaMovement(), this.getYRot(), this.getXRot() ++ ), ++ java.util.Collections.emptySet() ++ ); ++ this.connection.resetPosition(); ++ this.setOldPosAndRot(); ++ this.resetStoredPositions(); ++ } ++ ++ @Override ++ protected ServerPlayer transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ // must be manually removed from connections ++ this.serverLevel().getCurrentWorldData().connections.remove(this.connection.connection); ++ this.serverLevel().removePlayerImmediately(this, Entity.RemovalReason.CHANGED_DIMENSION); ++ ++ this.spawnIn(destination); ++ this.transform(pos, yaw, pitch, velocity); ++ ++ return this; ++ } ++ ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ this.stopUsingItem(); ++ } ++ ++ @Override ++ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { ++ if (destination == originWorld && (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L) { ++ this.unsetRemoved(); ++ destination.addDuringTeleport(this); ++ ++ // must be manually added to connections ++ this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection); ++ ++ // required to set up the pending teleport stuff to the client, and to actually update ++ // the player's position clientside ++ this.connection.internalTeleport( ++ new net.minecraft.world.entity.PositionMoveRotation( ++ this.position(), this.getDeltaMovement(), this.getYRot(), this.getXRot() ++ ), ++ java.util.Collections.emptySet() ++ ); ++ this.connection.resetPosition(); ++ ++ this.postChangeDimension(); ++ } else { ++ // Modelled after PlayerList#respawn ++ ++ // We avoid checking for disconnection here, which means we do not have to add/remove from ++ // the player list here. We can let this be properly handled by the connection handler ++ ++ // pre-add logic ++ PlayerList playerlist = this.server.getPlayerList(); ++ net.minecraft.world.level.storage.LevelData worlddata = destination.getLevelData(); ++ this.connection.send( ++ new ClientboundRespawnPacket( ++ this.createCommonSpawnInfo(destination), ++ (teleportFlags & TELEPORT_FLAGS_PLAYER_RESPAWN) == 0L ? (byte)1 : (byte)0 ++ ) ++ ); ++ // don't bother with the chunk cache radius and simulation distance packets, they are handled ++ // by the chunk loader ++ this.spawnIn(destination); // important that destination != null ++ // we can delay teleport until later, the player position is already set up at the target ++ this.setShiftKeyDown(false); ++ ++ this.connection.send(new net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket( ++ destination.getSharedSpawnPos(), destination.getSharedSpawnAngle() ++ )); ++ this.connection.send(new ClientboundChangeDifficultyPacket( ++ worlddata.getDifficulty(), worlddata.isDifficultyLocked() ++ )); ++ this.connection.send(new ClientboundSetExperiencePacket( ++ this.experienceProgress, this.totalExperience, this.experienceLevel ++ )); ++ ++ playerlist.sendActivePlayerEffects(this); ++ playerlist.sendLevelInfo(this, destination); ++ playerlist.sendPlayerPermissionLevel(this); ++ ++ // regular world add logic ++ this.unsetRemoved(); ++ destination.addDuringTeleport(this); ++ ++ // must be manually added to connections ++ this.serverLevel().getCurrentWorldData().connections.add(this.connection.connection); ++ ++ // required to set up the pending teleport stuff to the client, and to actually update ++ // the player's position clientside ++ this.connection.internalTeleport( ++ new net.minecraft.world.entity.PositionMoveRotation( ++ this.position(), this.getDeltaMovement(), this.getYRot(), this.getXRot() ++ ), ++ java.util.Collections.emptySet() ++ ); ++ this.connection.resetPosition(); ++ ++ // delay callback until after post add logic ++ ++ // post add logic ++ ++ // "Added from changeDimension" ++ this.setHealth(this.getHealth()); ++ playerlist.sendAllPlayerInfo(this); ++ this.onUpdateAbilities(); ++ /*for (MobEffectInstance mobEffect : this.getActiveEffects()) { ++ this.connection.send(new ClientboundUpdateMobEffectPacket(this.getId(), mobEffect, false)); ++ }*/ // handled by sendActivePlayerEffects ++ ++ // Paper start - Reset shield blocking on dimension change ++ if (this.isBlocking()) { ++ this.stopUsingItem(); ++ } ++ // Paper end - Reset shield blocking on dimension change ++ ++ this.triggerDimensionChangeTriggers(originWorld); ++ ++ // finished ++ ++ this.postChangeDimension(); ++ } ++ } ++ ++ @Override ++ public boolean endPortalLogicAsync(BlockPos portalPos) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ if (this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { ++ if (!this.canPortalAsync(null, false)) { ++ return false; ++ } ++ this.wonGame = true; ++ // TODO is there a better solution to this that DOESN'T skip the credits? ++ this.seenCredits = true; ++ if (!this.seenCredits) { ++ this.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.WIN_GAME, 0.0F)); ++ } ++ this.exitEndCredits(); ++ return true; ++ } else { ++ return super.endPortalLogicAsync(portalPos); ++ } ++ } ++ ++ @Override ++ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { ++ super.prePortalLogic(origin, destination, type); ++ if (origin.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.OVERWORLD && destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER) { ++ this.enteredNetherPosition = this.position(); ++ } ++ } ++ // Folia end - region threading ++ + @Nullable + @Override + public ServerPlayer teleport(TeleportTransition teleportTransition) { +@@ -2398,6 +_,11 @@ + } + + public void setCamera(@Nullable Entity entityToSpectate) { ++ // Folia start - region threading ++ if (entityToSpectate != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entityToSpectate)) { ++ return; ++ } ++ // Folia end - region threading + Entity camera = this.getCamera(); + this.camera = (Entity)(entityToSpectate == null ? this : entityToSpectate); + if (camera != this.camera) { +@@ -2896,11 +_,11 @@ + } + + public void registerEnderPearl(ThrownEnderpearl enderPearl) { +- this.enderPearls.add(enderPearl); ++ //this.enderPearls.add(enderPearl); // Folia - region threading - do not track ender pearls + } + + public void deregisterEnderPearl(ThrownEnderpearl enderPearl) { +- this.enderPearls.remove(enderPearl); ++ //this.enderPearls.remove(enderPearl); // Folia - region threading - do not track ender pearls + } + + public Set getEnderPearls() { +@@ -3054,7 +_,7 @@ + this.experienceLevel = this.newLevel; + this.totalExperience = this.newTotalExp; + this.experienceProgress = 0; +- this.deathTime = 0; ++ this.deathTime = 0; this.broadcastedDeath = false; // Folia - region threading + this.setArrowCount(0, true); // CraftBukkit - ArrowBodyCountChangeEvent + this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH); + this.effectsDirty = true; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayerGameMode.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayerGameMode.java.patch new file mode 100644 index 0000000..02cda7c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayerGameMode.java.patch @@ -0,0 +1,40 @@ +--- a/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -114,7 +_,7 @@ + // this.gameTicks = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit + this.gameTicks = (int) this.level.getLagCompensationTick(); // Paper - lag compensate eating + if (this.hasDelayedDestroy) { +- BlockState blockState = this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks ++ BlockState blockState = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.delayedDestroyPos) ? null : this.level.getBlockStateIfLoaded(this.delayedDestroyPos); // Paper - Don't allow digging into unloaded chunks // Folia - region threading - don't destroy blocks not owned + if (blockState == null || blockState.isAir()) { // Paper - Don't allow digging into unloaded chunks + this.hasDelayedDestroy = false; + } else { +@@ -126,7 +_,7 @@ + } + } else if (this.isDestroyingBlock) { + // Paper start - Don't allow digging into unloaded chunks; don't want to do same logic as above, return instead +- BlockState blockState = this.level.getBlockStateIfLoaded(this.destroyPos); ++ BlockState blockState = !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.destroyPos) ? null : this.level.getBlockStateIfLoaded(this.destroyPos); // Folia - region threading - don't destroy blocks not owned + if (blockState == null) { + this.isDestroyingBlock = false; + return; +@@ -369,7 +_,7 @@ + } else { + // CraftBukkit start + org.bukkit.block.BlockState state = bblock.getState(); +- this.level.captureDrops = new java.util.ArrayList<>(); ++ this.level.getCurrentWorldData().captureDrops = new java.util.ArrayList<>(); // Folia - region threading + // CraftBukkit end + BlockState blockState1 = block.playerWillDestroy(this.level, pos, blockState, this.player); + boolean flag = this.level.removeBlock(pos, false); +@@ -395,8 +_,8 @@ + // return true; // CraftBukkit + } + // CraftBukkit start +- java.util.List itemsToDrop = this.level.captureDrops; // Paper - capture all item additions to the world +- this.level.captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff ++ java.util.List itemsToDrop = this.level.getCurrentWorldData().captureDrops; // Paper - capture all item additions to the world // Folia - region threading ++ this.level.getCurrentWorldData().captureDrops = null; // Paper - capture all item additions to the world; Remove this earlier so that we can actually drop stuff // Folia - region threading + if (event.isDropItems()) { + org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockDropItemEvent(bblock, state, this.player, itemsToDrop); // Paper - capture all item additions to the world + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/level/TicketType.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/level/TicketType.java.patch new file mode 100644 index 0000000..99262e2 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/level/TicketType.java.patch @@ -0,0 +1,22 @@ +--- a/net/minecraft/server/level/TicketType.java ++++ b/net/minecraft/server/level/TicketType.java +@@ -17,10 +_,18 @@ + public static final TicketType FORCED = create("forced", Comparator.comparingLong(ChunkPos::toLong)); + public static final TicketType PORTAL = create("portal", Vec3i::compareTo, 300); + public static final TicketType ENDER_PEARL = create("ender_pearl", Comparator.comparingLong(ChunkPos::toLong), 40); +- public static final TicketType UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 1); ++ public static final TicketType UNKNOWN = create("unknown", Comparator.comparingLong(ChunkPos::toLong), 5); // Folia - region threading + public static final TicketType PLUGIN = TicketType.create("plugin", (a, b) -> 0); // CraftBukkit + public static final TicketType PLUGIN_TICKET = TicketType.create("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit + public static final TicketType POST_TELEPORT = TicketType.create("post_teleport", Integer::compare, 5); // Paper - post teleport ticket type ++ // Folia start - region threading ++ public static final TicketType LOGIN = create("folia:login", (u1, u2) -> 0, 20); ++ public static final TicketType DELAYED = create("folia:delay", (u1, u2) -> 0, 5); ++ public static final TicketType END_GATEWAY_EXIT_SEARCH = create("folia:end_gateway_exit_search", Long::compareTo); ++ public static final TicketType NETHER_PORTAL_DOUBLE_CHECK = create("folia:nether_portal_double_check", Long::compareTo); ++ public static final TicketType TELEPORT_HOLD_TICKET = create("folia:teleport_hold_ticket", Long::compareTo); ++ public static final TicketType REGION_SCHEDULER_API_HOLD = create("folia:region_scheduler_api_hold", (a, b) -> 0); ++ // Folia end - region threading + + public static TicketType create(String name, Comparator comparator) { + return new TicketType<>(name, comparator, 0L); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/level/WorldGenRegion.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/level/WorldGenRegion.java.patch new file mode 100644 index 0000000..0f75543 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/level/WorldGenRegion.java.patch @@ -0,0 +1,24 @@ +--- a/net/minecraft/server/level/WorldGenRegion.java ++++ b/net/minecraft/server/level/WorldGenRegion.java +@@ -107,6 +_,13 @@ + return this.getLightEngine().getRawBrightness(blockPos, subtract); + } + // Paper end - rewrite chunk system ++ // Folia start - region threading ++ private final net.minecraft.world.level.StructureManager structureManager; ++ @Override ++ public net.minecraft.world.level.StructureManager structureManager() { ++ return this.structureManager; ++ } ++ // Folia end - region threading + + public WorldGenRegion(ServerLevel level, StaticCache2D cache, ChunkStep generatingStep, ChunkAccess center) { + this.generatingStep = generatingStep; +@@ -118,6 +_,7 @@ + this.random = level.getChunkSource().randomState().getOrCreateRandomFactory(WORLDGEN_REGION_RANDOM).at(this.center.getPos().getWorldPosition()); + this.dimensionType = level.dimensionType(); + this.biomeManager = new BiomeManager(this, BiomeManager.obfuscateSeed(this.seed)); ++ this.structureManager = level.structureManager().forWorldGenRegion(this); // Folia - region threading + } + + public boolean isOldChunkAround(ChunkPos pos, int radius) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch new file mode 100644 index 0000000..d550f05 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch @@ -0,0 +1,89 @@ +--- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +@@ -96,6 +_,10 @@ + } + } + ++ // Folia start - region threading ++ private boolean handledDisconnect = false; ++ // Folia end - region threading ++ + @Override + public void onDisconnect(DisconnectionDetails details) { + // Paper start - Fix kick event leave message not being sent +@@ -104,10 +_,18 @@ + + public void onDisconnect(DisconnectionDetails info, @Nullable net.kyori.adventure.text.Component quitMessage) { + // Paper end - Fix kick event leave message not being sent ++ // Folia start - region threading ++ if (this.handledDisconnect) { ++ // avoid retiring scheduler twice ++ return; ++ } ++ this.handledDisconnect = true; ++ // Folia end - region threading + if (this.isSingleplayerOwner()) { + LOGGER.info("Stopping singleplayer server as player logged out"); + this.server.halt(false); + } ++ this.player.getBukkitEntity().taskScheduler.retire(); // Folia - region threading + } + + @Override +@@ -330,24 +_,8 @@ + if (this.processedDisconnect) { + return; + } +- if (!this.cserver.isPrimaryThread()) { +- org.bukkit.craftbukkit.util.Waitable waitable = new org.bukkit.craftbukkit.util.Waitable() { +- @Override +- protected Object evaluate() { +- ServerCommonPacketListenerImpl.this.disconnect(disconnectionDetails, cause); // Paper - kick event causes +- return null; +- } +- }; +- +- this.server.processQueue.add(waitable); +- +- try { +- waitable.get(); +- } catch (InterruptedException e) { +- Thread.currentThread().interrupt(); +- } catch (java.util.concurrent.ExecutionException e) { +- throw new RuntimeException(e); +- } ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player)) { // Folia - region threading ++ this.connection.disconnectSafely(disconnectionDetails, cause); // Folia - region threading - it HAS to be delayed/async to avoid deadlock if we try to wait for another region + return; + } + +@@ -378,7 +_,7 @@ + this.onDisconnect(disconnectionDetails, leaveMessage); // CraftBukkit - fire quit instantly // Paper - use kick event leave message + this.connection.setReadOnly(); + // CraftBukkit - Don't wait +- this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper ++ //this.server.scheduleOnMain(this.connection::handleDisconnection); // Paper // Folia - region threading + } + + // Paper start - add proper async disconnect +@@ -391,19 +_,7 @@ + } + + public void disconnectAsync(DisconnectionDetails disconnectionInfo, org.bukkit.event.player.PlayerKickEvent.Cause cause) { +- if (this.cserver.isPrimaryThread()) { +- this.disconnect(disconnectionInfo, cause); +- return; +- } +- +- this.connection.setReadOnly(); +- this.server.scheduleOnMain(() -> { +- ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause); +- if (ServerCommonPacketListenerImpl.this.player.quitReason == null) { +- // cancelled +- ServerCommonPacketListenerImpl.this.connection.enableAutoRead(); +- } +- }); ++ this.disconnect(disconnectionInfo, cause); // Folia - threaded regions + } + // Paper end - add proper async disconnect + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch new file mode 100644 index 0000000..d16a026 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch @@ -0,0 +1,70 @@ +--- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +@@ -47,6 +_,7 @@ + private ClientInformation clientInformation; + @Nullable + private SynchronizeRegistriesTask synchronizeRegistriesTask; ++ public boolean switchToMain = false; // Folia - region threading - rewrite login process + + // CraftBukkit start + public ServerConfigurationPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie, ServerPlayer player) { +@@ -160,7 +_,58 @@ + } + + ServerPlayer playerForLogin = playerList.getPlayerForLogin(this.gameProfile, this.clientInformation, this.player); // CraftBukkit +- playerList.placeNewPlayer(this.connection, playerForLogin, this.createCookie(this.clientInformation)); ++ // Folia start - region threading - rewrite login process ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Cannot handle player login off global tick thread"); ++ CommonListenerCookie clientData = this.createCookie(this.clientInformation); ++ org.apache.commons.lang3.mutable.MutableObject data = new org.apache.commons.lang3.mutable.MutableObject<>(); ++ org.apache.commons.lang3.mutable.MutableObject lastKnownName = new org.apache.commons.lang3.mutable.MutableObject<>(); ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ // note: need to call addWaiter before completion to ensure the callback is invoked synchronously ++ // the loadSpawnForNewPlayer function always completes the completable once the chunks were loaded, ++ // on the load callback for those chunks (so on the same region) ++ // this guarantees the chunk cannot unload under our feet ++ toComplete.addWaiter((org.bukkit.Location loc, Throwable t) -> { ++ int chunkX = net.minecraft.util.Mth.floor(loc.getX()) >> 4; ++ int chunkZ = net.minecraft.util.Mth.floor(loc.getZ()) >> 4; ++ ++ net.minecraft.server.level.ServerLevel world = ((org.bukkit.craftbukkit.CraftWorld)loc.getWorld()).getHandle(); ++ // we just need to hold the chunks at loaded until the next tick ++ // so we do not need to care about unique IDs for the ticket ++ world.getChunkSource().addTicketAtLevel( ++ net.minecraft.server.level.TicketType.LOGIN, ++ new net.minecraft.world.level.ChunkPos(chunkX, chunkZ), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, ++ net.minecraft.util.Unit.INSTANCE ++ ); ++ ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, chunkX, chunkZ, ++ () -> { ++ // once switchToMain is set, the current ticking region now owns the connection and is responsible ++ // for cleaning it up ++ playerList.placeNewPlayer( ++ ServerConfigurationPacketListenerImpl.this.connection, ++ playerForLogin, ++ clientData, ++ java.util.Optional.ofNullable(data.getValue()), ++ lastKnownName.getValue(), ++ loc ++ ); ++ }, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER ++ ); ++ }); ++ this.switchToMain = true; ++ try { ++ // now the connection responsibility is transferred on the region ++ playerList.loadSpawnForNewPlayer(this.connection, playerForLogin, clientData, data, lastKnownName, toComplete); ++ } catch (final Throwable throwable) { ++ // assume toComplete will not be invoked ++ // ensure global tick thread owns the connection again, to properly disconnect it ++ this.switchToMain = false; ++ throw new RuntimeException(throwable); ++ } ++ // Folia end - region threading - rewrite login process + } catch (Exception var5) { + LOGGER.error("Couldn't place player in world", (Throwable)var5); + // Paper start - Debugging diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch new file mode 100644 index 0000000..284192d --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerConnectionListener.java.patch @@ -0,0 +1,28 @@ +--- a/net/minecraft/server/network/ServerConnectionListener.java ++++ b/net/minecraft/server/network/ServerConnectionListener.java +@@ -167,12 +_,15 @@ + } + // Paper end - Add support for proxy protocol + // ServerConnectionListener.this.connections.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking +- ServerConnectionListener.this.pending.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking ++ //ServerConnectionListener.this.pending.add(connection); // Paper - prevent blocking on adding a new connection while the server is ticking // Folia - connection fixes - move down + connection.configurePacketHandler(channelPipeline); + connection.setListenerForServerboundHandshake( + new ServerHandshakePacketListenerImpl(ServerConnectionListener.this.server, connection) + ); + io.papermc.paper.network.ChannelInitializeListenerHolder.callListeners(channel); // Paper - Add Channel initialization listeners ++ // Folia start - regionised threading ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addConnection(connection); ++ // Folia end - regionised threading + } + } + ) +@@ -242,7 +_,7 @@ + // Spigot start + this.addPending(); // Paper - prevent blocking on adding a new connection while the server is ticking + // This prevents players from 'gaming' the server, and strategically relogging to increase their position in the tick order +- if (org.spigotmc.SpigotConfig.playerShuffle > 0 && MinecraftServer.currentTick % org.spigotmc.SpigotConfig.playerShuffle == 0) { ++ if (org.spigotmc.SpigotConfig.playerShuffle > 0 && 0 % org.spigotmc.SpigotConfig.playerShuffle == 0) { // Folia - region threading + Collections.shuffle(this.connections); + } + // Spigot end diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch new file mode 100644 index 0000000..64456e6 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch @@ -0,0 +1,396 @@ +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -292,10 +_,10 @@ + private int knownMovePacketCount; + private boolean receivedMovementThisTick; + // CraftBukkit start - add fields +- private int lastTick = MinecraftServer.currentTick; ++ private long lastTick = Util.getMillis() / 50L; // Folia - region threading + private int allowedPlayerTicks = 1; +- private int lastDropTick = MinecraftServer.currentTick; +- private int lastBookTick = MinecraftServer.currentTick; ++ private long lastDropTick = Util.getMillis() / 50L; // Folia - region threading ++ private long lastBookTick = Util.getMillis() / 50L; // Folia - region threading + private int dropCount = 0; + + private boolean hasMoved = false; +@@ -313,9 +_,16 @@ + private final LastSeenMessagesValidator lastSeenMessages = new LastSeenMessagesValidator(20); + private final MessageSignatureCache messageSignatureCache = MessageSignatureCache.createDefault(); + private final FutureChain chatMessageChain; +- private boolean waitingForSwitchToConfig; ++ public volatile boolean waitingForSwitchToConfig; // Folia - rewrite login process - fix bad ordering of this field write + public + private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length + ++ // Folia start - region threading ++ public net.minecraft.world.level.ChunkPos disconnectPos; ++ private static final java.util.concurrent.atomic.AtomicLong DISCONNECT_TICKET_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); ++ public static final net.minecraft.server.level.TicketType DISCONNECT_TICKET = net.minecraft.server.level.TicketType.create("disconnect_ticket", Long::compareTo); ++ public final Long disconnectTicketId = Long.valueOf(DISCONNECT_TICKET_ID_GENERATOR.getAndIncrement()); ++ // Folia end - region threading ++ + public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie cookie) { + super(server, connection, cookie, player); // CraftBukkit + this.chunkSender = new PlayerChunkSender(connection.isMemoryConnection()); +@@ -328,6 +_,12 @@ + + @Override + public void tick() { ++ // Folia start - region threading ++ this.keepConnectionAlive(); ++ if (this.processedDisconnect || this.player.wonGame) { ++ return; ++ } ++ // Folia end - region threading + if (this.ackBlockChangesUpTo > -1) { + this.send(new ClientboundBlockChangedAckPacket(this.ackBlockChangesUpTo)); + this.ackBlockChangesUpTo = -1; +@@ -376,7 +_,7 @@ + this.aboveGroundVehicleTickCount = 0; + } + +- this.keepConnectionAlive(); ++ // Folia - region threading - moved to beginning of method + this.chatSpamThrottler.tick(); + this.dropSpamThrottler.tick(); + this.tabSpamThrottler.tick(); // Paper - configurable tab spam limits +@@ -412,6 +_,19 @@ + this.lastGoodX = this.player.getX(); + this.lastGoodY = this.player.getY(); + this.lastGoodZ = this.player.getZ(); ++ // Folia start - support vehicle teleportations ++ this.lastVehicle = this.player.getRootVehicle(); ++ if (this.lastVehicle != this.player && this.lastVehicle.getControllingPassenger() == this.player) { ++ this.vehicleFirstGoodX = this.lastVehicle.getX(); ++ this.vehicleFirstGoodY = this.lastVehicle.getY(); ++ this.vehicleFirstGoodZ = this.lastVehicle.getZ(); ++ this.vehicleLastGoodX = this.lastVehicle.getX(); ++ this.vehicleLastGoodY = this.lastVehicle.getY(); ++ this.vehicleLastGoodZ = this.lastVehicle.getZ(); ++ } else { ++ this.lastVehicle = null; ++ } ++ // Folia end - support vehicle teleportations + } + + @Override +@@ -519,9 +_,10 @@ + // Paper end - fix large move vectors killing the server + + // CraftBukkit start - handle custom speeds and skipped ticks +- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; ++ int currTick = (int)(Util.getMillis() / 50); // Folia - region threading ++ this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading + this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); +- this.lastTick = (int) (System.currentTimeMillis() / 50); ++ this.lastTick = (int) currTick; // Folia - region threading + + ++this.receivedMovePacketCount; + int i = this.receivedMovePacketCount - this.knownMovePacketCount; +@@ -588,7 +_,7 @@ + } + + rootVehicle.absMoveTo(d, d1, d2, f, f1); +- this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit ++ //this.player.absMoveTo(d, d1, d2, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - move to repositionAllPassengers + // Paper start - optimise out extra getCubes + boolean teleportBack = flag2; // violating this is always a fail + if (!teleportBack) { +@@ -600,10 +_,18 @@ + } + if (teleportBack) { // Paper end - optimise out extra getCubes + rootVehicle.absMoveTo(x, y, z, f, f1); +- this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit ++ //this.player.absMoveTo(x, y, z, this.player.getYRot(), this.player.getXRot()); // CraftBukkit // Folia - not needed, the player is no longer updated + this.send(ClientboundMoveVehiclePacket.fromEntity(rootVehicle)); + return; + } ++ ++ // Folia start - move to positionRider ++ // this correction is required on folia since we move the connection tick to the beginning of the server ++ // tick, which would make any desync here visible ++ // this will correctly update the passenger positions for all mounted entities ++ // this prevents desync and ensures that all passengers have the correct rider-adjusted position ++ rootVehicle.repositionAllPassengers(false); ++ // Folia end - move to positionRider + + // CraftBukkit start - fire PlayerMoveEvent + org.bukkit.entity.Player player = this.getCraftPlayer(); +@@ -635,7 +_,7 @@ + + // If the event is cancelled we move the player back to their old location. + if (event.isCancelled()) { +- this.teleport(from); ++ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading + return; + } + +@@ -643,7 +_,7 @@ + // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. + // We only do this if the Event was not cancelled. + if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { +- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); ++ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading + return; + } + +@@ -817,7 +_,7 @@ + } + + // This needs to be on main +- this.server.scheduleOnMain(() -> this.sendServerSuggestions(packet, stringReader)); ++ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> this.sendServerSuggestions(packet, stringReader), null, 1L); // Folia - region threading + } else if (!completions.isEmpty()) { + final com.mojang.brigadier.suggestion.SuggestionsBuilder builder0 = new com.mojang.brigadier.suggestion.SuggestionsBuilder(packet.getCommand(), stringReader.getTotalLength()); + final com.mojang.brigadier.suggestion.SuggestionsBuilder builder = builder0.createOffset(builder0.getInput().lastIndexOf(' ') + 1); +@@ -1200,11 +_,11 @@ + } + // Paper end - Book size limits + // CraftBukkit start +- if (this.lastBookTick + 20 > MinecraftServer.currentTick) { ++ if (this.lastBookTick + 20 > this.lastTick) { // Folia - region threading + this.disconnectAsync(Component.literal("Book edited too quickly!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - add proper async disconnect + return; + } +- this.lastBookTick = MinecraftServer.currentTick; ++ this.lastBookTick = this.lastTick; // Folia - region threading + // CraftBukkit end + int slot = packet.slot(); + if (Inventory.isHotbarSlot(slot) || slot == 40) { +@@ -1215,7 +_,22 @@ + Consumer> consumer = optional.isPresent() + ? texts -> this.signBook(texts.get(0), texts.subList(1, texts.size()), slot) + : texts -> this.updateBookContents(texts, slot); +- this.filterTextPacket(list).thenAcceptAsync(consumer, this.server); ++ // Folia start - region threading ++ this.filterTextPacket(list).thenAcceptAsync( ++ consumer, ++ (Runnable run) -> { ++ this.player.getBukkitEntity().taskScheduler.schedule( ++ (player) -> { ++ run.run(); ++ }, ++ null, 1L); ++ } ++ ).whenComplete((Object res, Throwable thr) -> { ++ if (thr != null) { ++ LOGGER.error("Failed to handle book update packet", thr); ++ } ++ }); ++ // Folia end - region threading + } + } + +@@ -1341,9 +_,10 @@ + int i = this.receivedMovePacketCount - this.knownMovePacketCount; + + // CraftBukkit start - handle custom speeds and skipped ticks +- this.allowedPlayerTicks += (System.currentTimeMillis() / 50) - this.lastTick; ++ int currTick = (int)(Util.getMillis() / 50); // Folia - region threading ++ this.allowedPlayerTicks += currTick - this.lastTick; // Folia - region threading + this.allowedPlayerTicks = Math.max(this.allowedPlayerTicks, 1); +- this.lastTick = (int) (System.currentTimeMillis() / 50); ++ this.lastTick = (int) currTick; // Folia - region threading + + if (i > Math.max(this.allowedPlayerTicks, 5)) { + LOGGER.debug("{} is sending move packets too frequently ({} packets since last tick)", this.player.getName().getString(), i); +@@ -1532,7 +_,7 @@ + + // If the event is cancelled we move the player back to their old location. + if (event.isCancelled()) { +- this.teleport(from); ++ this.player.getBukkitEntity().teleportAsync(from, PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading + return; + } + +@@ -1540,7 +_,7 @@ + // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors. + // We only do this if the Event was not cancelled. + if (!oldTo.equals(event.getTo()) && !event.isCancelled()) { +- this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); ++ this.player.getBukkitEntity().teleportAsync(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN); // Folia - region threading + return; + } + +@@ -1799,9 +_,9 @@ + if (!this.player.isSpectator()) { + // limit how quickly items can be dropped + // If the ticks aren't the same then the count starts from 0 and we update the lastDropTick. +- if (this.lastDropTick != MinecraftServer.currentTick) { ++ if (this.lastDropTick != io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick()) { // Folia - region threading + this.dropCount = 0; +- this.lastDropTick = MinecraftServer.currentTick; ++ this.lastDropTick = io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick(); // Folia - region threading + } else { + // Else we increment the drop count and check the amount. + this.dropCount++; +@@ -1829,7 +_,7 @@ + case ABORT_DESTROY_BLOCK: + case STOP_DESTROY_BLOCK: + // Paper start - Don't allow digging into unloaded chunks +- if (this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), pos.getX() >> 4, pos.getZ() >> 4, 8) || this.player.level().getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null) { // Folia - region threading - don't destroy blocks not owned + this.player.connection.ackBlockChangesUpTo(packet.getSequence()); + return; + } +@@ -1911,7 +_,7 @@ + } + // Paper end - improve distance check + BlockPos blockPos = hitResult.getBlockPos(); +- if (this.player.canInteractWithBlock(blockPos, 1.0)) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.serverLevel(), blockPos.getX() >> 4, blockPos.getZ() >> 4, 8) && this.player.canInteractWithBlock(blockPos, 1.0)) { // Folia - do not allow players to interact with blocks outside the current region + Vec3 vec3 = location.subtract(Vec3.atCenterOf(blockPos)); + double d = 1.0000001; + if (Math.abs(vec3.x()) < 1.0000001 && Math.abs(vec3.y()) < 1.0000001 && Math.abs(vec3.z()) < 1.0000001) { +@@ -2032,7 +_,7 @@ + for (ServerLevel serverLevel : this.server.getAllLevels()) { + Entity entity = packet.getEntity(serverLevel); + if (entity != null) { +- this.player.teleportTo(serverLevel, entity.getX(), entity.getY(), entity.getZ(), Set.of(), entity.getYRot(), entity.getXRot(), true, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE); // CraftBukkit ++ io.papermc.paper.threadedregions.TeleportUtils.teleport(this.player, false, entity, null, null, Entity.TELEPORT_FLAG_LOAD_CHUNK, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.SPECTATE, null); // Folia - region threading + return; + } + } +@@ -2064,7 +_,7 @@ + } + // CraftBukkit end + LOGGER.info("{} lost connection: {}", this.player.getName().getString(), details.reason().getString()); +- this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent ++ if (!this.waitingForSwitchToConfig) this.removePlayerFromWorld(quitMessage); // Paper - Fix kick event leave message not being sent // Folia - region threading + super.onDisconnect(details, quitMessage); // Paper - Fix kick event leave message not being sent + } + +@@ -2073,6 +_,8 @@ + this.removePlayerFromWorld(null); + } + ++ public boolean hackSwitchingConfig; // Folia - rewrite login process ++ + private void removePlayerFromWorld(@Nullable net.kyori.adventure.text.Component quitMessage) { + // Paper end - Fix kick event leave message not being sent + this.chatMessageChain.close(); +@@ -2086,6 +_,8 @@ + this.player.disconnect(); + // Paper start - Adventure + quitMessage = quitMessage == null ? this.server.getPlayerList().remove(this.player) : this.server.getPlayerList().remove(this.player, quitMessage); // Paper - pass in quitMessage to fix kick message not being used ++ if (!this.hackSwitchingConfig) this.disconnectPos = this.player.chunkPosition(); // Folia - region threading - note: only set after removing, since it can tick the player ++ if (!this.hackSwitchingConfig) this.player.serverLevel().chunkSource.addTicketAtLevel(DISCONNECT_TICKET, this.disconnectPos, ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, this.disconnectTicketId); // Folia - region threading - force chunk to be loaded so that the region is not lost + if ((quitMessage != null) && !quitMessage.equals(net.kyori.adventure.text.Component.empty())) { + this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(quitMessage), false); + // Paper end - Adventure +@@ -2324,7 +_,7 @@ + this.player.resetLastActionTime(); + // CraftBukkit start + if (sync) { +- this.server.execute(handler); ++ this.player.getBukkitEntity().taskScheduler.schedule((ServerPlayer player) -> handler.run(), null, 1L); // Folia - region threading + } else { + handler.run(); + } +@@ -2379,7 +_,7 @@ + String originalFormat = event.getFormat(), originalMessage = event.getMessage(); + this.cserver.getPluginManager().callEvent(event); + +- if (PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { ++ if (false && PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading + // Evil plugins still listening to deprecated event + final PlayerChatEvent queueEvent = new PlayerChatEvent(player, event.getMessage(), event.getFormat(), event.getRecipients()); + queueEvent.setCancelled(event.isCancelled()); +@@ -2476,6 +_,7 @@ + if (rawMessage.isEmpty()) { + LOGGER.warn("{} tried to send an empty message", this.player.getScoreboardName()); + } else if (this.getCraftPlayer().isConversing()) { ++ if (true) throw new UnsupportedOperationException(); // Folia - region threading + final String conversationInput = rawMessage; + this.server.processQueue.add(() -> ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput)); + } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { // Re-add "Command Only" flag check +@@ -2701,8 +_,25 @@ + // Spigot end + + public void switchToConfig() { +- this.waitingForSwitchToConfig = true; ++ // Folia start - rewrite login process ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.player, "Cannot switch config off-main"); ++ if (io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { ++ throw new IllegalStateException("Cannot switch config while on global tick thread"); ++ } ++ // Folia end - rewrite login process ++ // Folia start - rewrite login process - fix bad ordering of this field write - move after removed from world ++ // the field write ordering is bad as it allows the client to send the response packet before the player is ++ // removed from the world ++ // Folia end - rewrite login process - fix bad ordering of this field write - move after removed from world ++ try { // Folia - rewrite login process - move connection ownership to global region ++ this.hackSwitchingConfig = true; // Folia - rewrite login process - avoid adding logout ticket here and retiring scheduler + this.removePlayerFromWorld(); ++ } finally { // Folia start - rewrite login process - move connection ownership to global region ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.player.serverLevel().getCurrentWorldData(); ++ worldData.connections.remove(this.connection); ++ // once waitingForSwitchToConfig is set, the global tick thread will own the connection ++ } // Folia end - rewrite login process - move connection ownership to global region ++ this.waitingForSwitchToConfig = true; // Folia - rewrite login process - fix bad ordering of this field write - moved down + this.send(ClientboundStartConfigurationPacket.INSTANCE); + this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); + } +@@ -2727,7 +_,7 @@ + // Spigot end + this.player.resetLastActionTime(); + this.player.setShiftKeyDown(packet.isUsingSecondaryAction()); +- if (target != null) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(target) && target != null) { // Folia - region threading - do not allow interaction of entities outside the current region + if (!serverLevel.getWorldBorder().isWithinBounds(target.blockPosition())) { + return; + } +@@ -2859,6 +_,12 @@ + switch (action) { + case PERFORM_RESPAWN: + if (this.player.wonGame) { ++ // Folia start - region threading ++ if (true) { ++ this.player.exitEndCredits(); ++ return; ++ } ++ // Folia end - region threading + this.player.wonGame = false; + this.player = this.server.getPlayerList().respawn(this.player, true, Entity.RemovalReason.CHANGED_DIMENSION, RespawnReason.END_PORTAL); // CraftBukkit + this.resetPosition(); +@@ -2868,6 +_,17 @@ + return; + } + ++ // Folia start - region threading ++ if (true) { ++ this.player.respawn((ServerPlayer player) -> { ++ if (ServerGamePacketListenerImpl.this.server.isHardcore()) { ++ ServerGamePacketListenerImpl.this.player.setGameMode(GameType.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper - Expand PlayerGameModeChangeEvent ++ ((GameRules.BooleanValue) ServerGamePacketListenerImpl.this.player.serverLevel().getGameRules().getRule(GameRules.RULE_SPECTATORSGENERATECHUNKS)).set(false, ServerGamePacketListenerImpl.this.player.serverLevel()); // CraftBukkit - per-world ++ } ++ }, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason.DEATH); ++ return; ++ } ++ // Folia end - region threading + this.player = this.server.getPlayerList().respawn(this.player, false, Entity.RemovalReason.KILLED, RespawnReason.DEATH); // CraftBukkit + this.resetPosition(); + if (this.server.isHardcore()) { +@@ -3413,7 +_,21 @@ + } + List list = Stream.of(lines).map(ChatFormatting::stripFormatting).collect(Collectors.toList()); + // Paper end - Limit client sign length +- this.filterTextPacket(list).thenAcceptAsync(list1 -> this.updateSignText(packet, (List)list1), this.server); ++ // Folia start - region threading ++ this.filterTextPacket(list).thenAcceptAsync((list1) -> { ++ this.updateSignText(packet, (List)list1); ++ }, (Runnable run) -> { ++ this.player.getBukkitEntity().taskScheduler.schedule( ++ (player) -> { ++ run.run(); ++ }, ++ null, 1L); ++ }).whenComplete((Object res, Throwable thr) -> { ++ if (thr != null) { ++ LOGGER.error("Failed to handle sign update packet", thr); ++ } ++ }); ++ // Folia end - region threading + } + + private void updateSignText(ServerboundSignUpdatePacket packet, List filteredText) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch new file mode 100644 index 0000000..b05b4e1 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch @@ -0,0 +1,33 @@ +--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -111,7 +_,11 @@ + // Paper end - Do not allow logins while the server is shutting down + + if (this.state == ServerLoginPacketListenerImpl.State.VERIFYING) { +- if (this.connection.isConnected()) { // Paper - prevent logins to be processed even though disconnect was called ++ // Folia start - region threading - rewrite login process ++ String name = this.authenticatedProfile.getName(); ++ java.util.UUID uniqueId = this.authenticatedProfile.getId(); ++ if (this.server.getPlayerList().pushPendingJoin(name, uniqueId, this.connection)) { ++ // Folia end - region threading - rewrite login process + this.verifyLoginAndFinishConnectionSetup(Objects.requireNonNull(this.authenticatedProfile)); + } // Paper - prevent logins to be processed even though disconnect was called + } +@@ -250,7 +_,7 @@ + ); + } + +- boolean flag = playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference ++ boolean flag = false && playerList.disconnectAllPlayersWithProfile(profile, this.player); // CraftBukkit - add player reference // Folia - rewrite login process - always false here + if (flag) { + this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT; + } else { +@@ -362,7 +_,7 @@ + uniqueId = gameprofile.getId(); + // Paper end - Add more fields to AsyncPlayerPreLoginEvent + +- if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { ++ if (false && PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) { // Folia - region threading + final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId); + if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) { + event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/players/BanListEntry.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/players/BanListEntry.java.patch new file mode 100644 index 0000000..d7018ee --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/players/BanListEntry.java.patch @@ -0,0 +1,50 @@ +--- a/net/minecraft/server/players/BanListEntry.java ++++ b/net/minecraft/server/players/BanListEntry.java +@@ -9,7 +_,7 @@ + import net.minecraft.network.chat.Component; + + public abstract class BanListEntry extends StoredUserEntry { +- public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT); ++ public static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT)); // Folia - region threading - SDF is not thread-safe + public static final String EXPIRES_NEVER = "forever"; + protected final Date created; + protected final String source; +@@ -30,7 +_,7 @@ + + Date date; + try { +- date = entryData.has("created") ? DATE_FORMAT.parse(entryData.get("created").getAsString()) : new Date(); ++ date = entryData.has("created") ? DATE_FORMAT.get().parse(entryData.get("created").getAsString()) : new Date(); // Folia - region threading - SDF is not thread-safe + } catch (ParseException var7) { + date = new Date(); + } +@@ -40,7 +_,7 @@ + + Date date1; + try { +- date1 = entryData.has("expires") ? DATE_FORMAT.parse(entryData.get("expires").getAsString()) : null; ++ date1 = entryData.has("expires") ? DATE_FORMAT.get().parse(entryData.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe + } catch (ParseException var6) { + date1 = null; + } +@@ -75,9 +_,9 @@ + + @Override + protected void serialize(JsonObject data) { +- data.addProperty("created", DATE_FORMAT.format(this.created)); ++ data.addProperty("created", DATE_FORMAT.get().format(this.created)); // Folia - region threading - SDF is not thread-safe + data.addProperty("source", this.source); +- data.addProperty("expires", this.expires == null ? "forever" : DATE_FORMAT.format(this.expires)); ++ data.addProperty("expires", this.expires == null ? "forever" : DATE_FORMAT.get().format(this.expires)); // Folia - region threading - SDF is not thread-safe + data.addProperty("reason", this.reason); + } + +@@ -86,7 +_,7 @@ + Date expires = null; + + try { +- expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.parse(jsonobject.get("expires").getAsString()) : null; ++ expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.get().parse(jsonobject.get("expires").getAsString()) : null; // Folia - region threading - SDF is not thread-safe + } catch (ParseException ex) { + // Guess we don't have a date + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch new file mode 100644 index 0000000..cb2ee52 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/players/OldUsersConverter.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/server/players/OldUsersConverter.java ++++ b/net/minecraft/server/players/OldUsersConverter.java +@@ -469,7 +_,7 @@ + static Date parseDate(String input, Date defaultValue) { + Date date; + try { +- date = BanListEntry.DATE_FORMAT.parse(input); ++ date = BanListEntry.DATE_FORMAT.get().parse(input); // Folia - region threading - SDF is not thread-safe + } catch (ParseException var4) { + date = defaultValue; + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/players/PlayerList.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/players/PlayerList.java.patch new file mode 100644 index 0000000..e9c0812 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/players/PlayerList.java.patch @@ -0,0 +1,419 @@ +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -110,10 +_,10 @@ + public static final Component DUPLICATE_LOGIN_DISCONNECT_MESSAGE = Component.translatable("multiplayer.disconnect.duplicate_login"); + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int SEND_PLAYER_INFO_INTERVAL = 600; +- private static final SimpleDateFormat BAN_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z"); ++ private static final ThreadLocal BAN_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z")); // Folia - region threading - SDF is not thread-safe + private final MinecraftServer server; + public final List players = new java.util.concurrent.CopyOnWriteArrayList(); // CraftBukkit - ArrayList -> CopyOnWriteArrayList: Iterator safety +- private final Map playersByUUID = Maps.newHashMap(); ++ private final Map playersByUUID = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading - change to CHM - Note: we do NOT expect concurrency PER KEY! + private final UserBanList bans = new UserBanList(USERBANLIST_FILE); + private final IpBanList ipBans = new IpBanList(IPBANLIST_FILE); + private final ServerOpList ops = new ServerOpList(OPLIST_FILE); +@@ -137,6 +_,60 @@ + private final Map playersByName = new java.util.HashMap<>(); + public @Nullable String collideRuleTeamName; // Paper - Configurable player collision + ++ // Folia start - region threading ++ private final Object connectionsStateLock = new Object(); ++ private final Map connectionByName = new java.util.HashMap<>(); ++ private final Map connectionById = new java.util.HashMap<>(); ++ private final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet usersCountedAgainstLimit = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); ++ ++ public boolean pushPendingJoin(String userName, UUID byId, Connection conn) { ++ userName = userName.toLowerCase(java.util.Locale.ROOT); ++ Connection conflictingName, conflictingId; ++ synchronized (this.connectionsStateLock) { ++ conflictingName = this.connectionByName.get(userName); ++ conflictingId = this.connectionById.get(byId); ++ ++ if (conflictingName == null && conflictingId == null) { ++ this.connectionByName.put(userName, conn); ++ this.connectionById.put(byId, conn); ++ } ++ } ++ ++ Component message = Component.translatable("multiplayer.disconnect.duplicate_login", new Object[0]); ++ ++ if (conflictingId != null || conflictingName != null) { ++ if (conflictingName != null && conflictingName.isPlayerConnected()) { ++ conflictingName.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); ++ } ++ if (conflictingName != conflictingId && conflictingId != null && conflictingId.isPlayerConnected()) { ++ conflictingId.disconnectSafely(message, org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); ++ } ++ } ++ ++ return conflictingName == null && conflictingId == null; ++ } ++ ++ public void removeConnection(String userName, UUID byId, Connection conn) { ++ userName = userName.toLowerCase(java.util.Locale.ROOT); ++ synchronized (this.connectionsStateLock) { ++ this.connectionByName.remove(userName, conn); ++ this.connectionById.remove(byId, conn); ++ this.usersCountedAgainstLimit.remove(conn); ++ } ++ } ++ ++ private boolean countConnection(Connection conn, int limit) { ++ synchronized (this.connectionsStateLock) { ++ int count = this.usersCountedAgainstLimit.size(); ++ if (count >= limit) { ++ return false; ++ } ++ this.usersCountedAgainstLimit.add(conn); ++ return true; ++ } ++ } ++ // Folia end - region threading ++ + public PlayerList(MinecraftServer server, LayeredRegistryAccess registries, PlayerDataStorage playerIo, int maxPlayers) { + this.cserver = server.server = new org.bukkit.craftbukkit.CraftServer((net.minecraft.server.dedicated.DedicatedServer) server, this); + server.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper +@@ -149,7 +_,7 @@ + + abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor + +- public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie) { ++ public void loadSpawnForNewPlayer(final Connection connection, final ServerPlayer player, final CommonListenerCookie cookie, org.apache.commons.lang3.mutable.MutableObject data, org.apache.commons.lang3.mutable.MutableObject lastKnownName, ca.spottedleaf.concurrentutil.completable.CallbackCompletable toComplete) { // Folia - region threading - rewrite login process + player.isRealPlayer = true; // Paper + player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed + GameProfile gameProfile = player.getGameProfile(); +@@ -221,17 +_,41 @@ + player.spawnReason = org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT; // set Player SpawnReason to DEFAULT on first login + // Paper start - reset to main world spawn if first spawn or invalid world + } ++ // Folia start - region threading - rewrite login process ++ // must write to these before toComplete is invoked ++ data.setValue(optional.orElse(null)); ++ lastKnownName.setValue(string); ++ // Folia end - region threading - rewrite login process + if (optional.isEmpty() || invalidPlayerWorld[0]) { + // Paper end - reset to main world spawn if first spawn or invalid world +- player.moveTo(player.adjustSpawnLocation(serverLevel, serverLevel.getSharedSpawnPos()).getBottomCenter(), serverLevel.getSharedSpawnAngle(), 0.0F); // Paper - MC-200092 - fix first spawn pos yaw being ignored ++ ServerPlayer.fudgeSpawnLocation(serverLevel, player, toComplete); // Paper - MC-200092 - fix first spawn pos yaw being ignored // Folia - region threading ++ } else { ++ serverLevel.loadChunksForMoveAsync( ++ player.getBoundingBox(), ++ ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (c) -> { ++ toComplete.complete(io.papermc.paper.util.MCUtil.toLocation(serverLevel, player.position())); ++ } ++ ); + } ++ // Folia end - region threading - rewrite login process + // Paper end - Entity#getEntitySpawnReason ++ // Folia start - region threading - rewrite login process ++ return; ++ } ++ // optional -> player data ++ // s -> last known name ++ public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie, Optional optional, String string, org.bukkit.Location selectedSpawn) { ++ ServerLevel serverLevel = ((org.bukkit.craftbukkit.CraftWorld)selectedSpawn.getWorld()).getHandle(); ++ player.setPosRaw(selectedSpawn.getX(), selectedSpawn.getY(), selectedSpawn.getZ()); ++ player.lastSave = System.nanoTime(); // changed to nanoTime ++ // Folia end - region threading - rewrite login process + player.setServerLevel(serverLevel); + String loggableAddress = connection.getLoggableAddress(this.server.logIPs()); + // Spigot start - spawn location event + org.bukkit.entity.Player spawnPlayer = player.getBukkitEntity(); + org.spigotmc.event.player.PlayerSpawnLocationEvent ev = new org.spigotmc.event.player.PlayerSpawnLocationEvent(spawnPlayer, spawnPlayer.getLocation()); +- this.cserver.getPluginManager().callEvent(ev); ++ //this.cserver.getPluginManager().callEvent(ev); // Folia - region threading - TODO WTF TO DO WITH THIS EVENT? + + org.bukkit.Location loc = ev.getSpawnLocation(); + serverLevel = ((org.bukkit.craftbukkit.CraftWorld) loc.getWorld()).getHandle(); +@@ -254,6 +_,11 @@ + LevelData levelData = serverLevel.getLevelData(); + player.loadGameTypes(optional.orElse(null)); + ServerGamePacketListenerImpl serverGamePacketListenerImpl = new ServerGamePacketListenerImpl(this.server, connection, player, cookie); ++ // Folia start - rewrite login process ++ // only after setting the connection listener to game type, add the connection to this regions list ++ serverLevel.getCurrentWorldData().connections.add(connection); ++ // Folia end - rewrite login process ++ + connection.setupInboundProtocol( + GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), serverGamePacketListenerImpl + ); +@@ -287,7 +_,7 @@ + this.sendPlayerPermissionLevel(player); + player.getStats().markAllDirty(); + player.getRecipeBook().sendInitialRecipeBook(player); +- this.updateEntireScoreboard(serverLevel.getScoreboard(), player); ++ //this.updateEntireScoreboard(serverLevel.getScoreboard(), player); // Folia - region threading + this.server.invalidateStatus(); + MutableComponent mutableComponent; + if (player.getGameProfile().getName().equalsIgnoreCase(string)) { +@@ -327,7 +_,7 @@ + this.cserver.getPluginManager().callEvent(playerJoinEvent); + + if (!player.connection.isAcceptingMessages()) { +- return; ++ //return; // Folia - region threading - must still allow the player to connect, as we must add to chunk map before handling disconnect + } + + final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); +@@ -342,8 +_,7 @@ + ClientboundPlayerInfoUpdatePacket packet = ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player)); // Paper - Add Listing API for Player + + final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); // Paper - Use single player info update packet on join +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer1 = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer entityplayer1 : this.players) { // Folia - region threading + + if (entityplayer1.getBukkitEntity().canSee(bukkitPlayer)) { + // Paper start - Add Listing API for Player +@@ -392,7 +_,7 @@ + // Paper start - Configurable player collision; Add to collideRule team if needed + final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); + final PlayerTeam collideRuleTeam = scoreboard.getPlayerTeam(this.collideRuleTeamName); +- if (this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { ++ if (false && this.collideRuleTeamName != null && collideRuleTeam != null && player.getTeam() == null) { // Folia - region threading + scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam); + } + // Paper end - Configurable player collision +@@ -482,7 +_,7 @@ + + protected void save(ServerPlayer player) { + if (!player.getBukkitEntity().isPersistent()) return; // CraftBukkit +- player.lastSave = MinecraftServer.currentTick; // Paper - Incremental chunk and player saving ++ player.lastSave = System.nanoTime(); // Folia - region threading - changed to nanoTime tracking + this.playerIo.save(player); + ServerStatsCounter serverStatsCounter = player.getStats(); // CraftBukkit + if (serverStatsCounter != null) { +@@ -517,7 +_,7 @@ + // CraftBukkit end + + // Paper start - Configurable player collision; Remove from collideRule team if needed +- if (this.collideRuleTeamName != null) { ++ if (false && this.collideRuleTeamName != null) { // Folia - region threading + final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); + final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); + if (player.getTeam() == team && team != null) { +@@ -566,7 +_,7 @@ + } + + serverLevel.removePlayerImmediately(player, Entity.RemovalReason.UNLOADED_WITH_PLAYER); +- player.retireScheduler(); // Paper - Folia schedulers ++ //player.retireScheduler(); // Paper - Folia schedulers // Folia - region threading - move to onDisconnect of common packet listener + player.getAdvancements().stopListening(); + this.players.remove(player); + this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot +@@ -584,8 +_,7 @@ + // CraftBukkit start + // this.broadcastAll(new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID()))); + ClientboundPlayerInfoRemovePacket packet = new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())); +- for (int i = 0; i < this.players.size(); i++) { +- ServerPlayer otherPlayer = (ServerPlayer) this.players.get(i); ++ for (ServerPlayer otherPlayer : this.players) { // Folia - region threading + + if (otherPlayer.getBukkitEntity().canSee(player.getBukkitEntity())) { + otherPlayer.connection.send(packet); +@@ -609,19 +_,12 @@ + + ServerPlayer entityplayer; + +- for (int i = 0; i < this.players.size(); ++i) { +- entityplayer = (ServerPlayer) this.players.get(i); +- if (entityplayer.getUUID().equals(uuid) || (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() && entityplayer.getGameProfile().getName().equalsIgnoreCase(gameProfile.getName()))) { // Paper - validate usernames +- list.add(entityplayer); +- } +- } ++ // Folia - region threading - rewrite login process - moved to pushPendingJoin + + java.util.Iterator iterator = list.iterator(); + + while (iterator.hasNext()) { +- entityplayer = (ServerPlayer) iterator.next(); +- this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved +- entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause ++ // Folia - moved to pushPendingJoin + } + + // Instead of kicking then returning, we need to store the kick reason +@@ -641,7 +_,7 @@ + MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned.reason", userBanListEntry.getReason()); + if (userBanListEntry.getExpires() != null) { + mutableComponent.append( +- Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.format(userBanListEntry.getExpires())) ++ Component.translatable("multiplayer.disconnect.banned.expiration", BAN_DATE_FORMAT.get().format(userBanListEntry.getExpires())) // Folia - region threading - SDF is not thread-safe + ); + } + +@@ -655,7 +_,7 @@ + MutableComponent mutableComponent = Component.translatable("multiplayer.disconnect.banned_ip.reason", ipBanListEntry.getReason()); + if (ipBanListEntry.getExpires() != null) { + mutableComponent.append( +- Component.translatable("multiplayer.disconnect.banned_ip.expiration", BAN_DATE_FORMAT.format(ipBanListEntry.getExpires())) ++ Component.translatable("multiplayer.disconnect.banned_ip.expiration", BAN_DATE_FORMAT.get().format(ipBanListEntry.getExpires())) // Folia - region threading - SDF is not thread-safe + ); + } + +@@ -665,7 +_,7 @@ + // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile) + // ? Component.translatable("multiplayer.disconnect.server_full") + // : null; +- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)) { ++ if (!this.countConnection(loginlistener.connection, this.maxPlayers) && !this.canBypassPlayerLimit(gameProfile)) { // Folia - region threading - we control connection state here now async, not player list size + event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure + } + } +@@ -714,6 +_,11 @@ + return this.respawn(player, keepInventory, reason, eventReason, null); + } + public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, org.bukkit.Location location) { ++ // Folia start - region threading ++ if (true) { ++ throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); ++ } ++ // Folia end - region threading + player.stopRiding(); // CraftBukkit + this.players.remove(player); + this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot +@@ -884,10 +_,10 @@ + public void tick() { + if (++this.sendAllPlayerInfoIn > 600) { + // CraftBukkit start +- for (int i = 0; i < this.players.size(); ++i) { +- final ServerPlayer target = this.players.get(i); ++ ServerPlayer[] players = this.players.toArray(new ServerPlayer[0]); // Folia - region threading ++ for (final ServerPlayer target : players) { // Folia - region threading + +- target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), com.google.common.collect.Collections2.filter(this.players, t -> target.getBukkitEntity().canSee(t.getBukkitEntity())))); ++ target.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY), com.google.common.collect.Collections2.filter(java.util.Arrays.asList(players),t -> target.getBukkitEntity().canSee(t.getBukkitEntity())))); // Folia - region threading + } + // CraftBukkit end + this.sendAllPlayerInfoIn = 0; +@@ -896,18 +_,17 @@ + + // CraftBukkit start - add a world/entity limited version + public void broadcastAll(Packet packet, net.minecraft.world.entity.player.Player entityhuman) { +- for (int i = 0; i < this.players.size(); ++i) { +- ServerPlayer entityplayer = this.players.get(i); ++ for (ServerPlayer entityplayer : this.players) { // Folia - region threading + if (entityhuman != null && !entityplayer.getBukkitEntity().canSee(entityhuman.getBukkitEntity())) { + continue; + } +- ((ServerPlayer) this.players.get(i)).connection.send(packet); ++ entityplayer.connection.send(packet); // Folia - region threading + } + } + + public void broadcastAll(Packet packet, Level world) { +- for (int i = 0; i < world.players().size(); ++i) { +- ((ServerPlayer) world.players().get(i)).connection.send(packet); ++ for (net.minecraft.world.entity.player.Player player : world.players()) { // Folia - region threading ++ ((ServerPlayer) player).connection.send(packet); // Folia - region threading + } + + } +@@ -944,8 +_,7 @@ + if (team == null) { + this.broadcastSystemMessage(message, false); + } else { +- for (int i = 0; i < this.players.size(); i++) { +- ServerPlayer serverPlayer = this.players.get(i); ++ for (ServerPlayer serverPlayer : this.players) { // Folia - region threading + if (serverPlayer.getTeam() != team) { + serverPlayer.sendSystemMessage(message); + } +@@ -954,10 +_,11 @@ + } + + public String[] getPlayerNamesArray() { ++ List players = new java.util.ArrayList<>(this.players); // Folia - region threading + String[] strings = new String[this.players.size()]; + +- for (int i = 0; i < this.players.size(); i++) { +- strings[i] = this.players.get(i).getGameProfile().getName(); ++ for (int i = 0; i < players.size(); i++) { // Folia - region threading ++ strings[i] = players.get(i).getGameProfile().getName(); // Folia - region threading + } + + return strings; +@@ -975,7 +_,9 @@ + this.ops.add(new ServerOpListEntry(profile, this.server.getOperatorUserPermissionLevel(), this.ops.canBypassPlayerLimit(profile))); + ServerPlayer player = this.getPlayer(profile.getId()); + if (player != null) { +- this.sendPlayerPermissionLevel(player); ++ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading ++ this.sendPlayerPermissionLevel(serverPlayer); // Folia - region threading ++ }, null, 1L); // Folia - region threading + } + } + +@@ -983,7 +_,9 @@ + this.ops.remove(profile); + ServerPlayer player = this.getPlayer(profile.getId()); + if (player != null) { +- this.sendPlayerPermissionLevel(player); ++ player.getBukkitEntity().taskScheduler.schedule((ServerPlayer serverPlayer) -> { // Folia - region threading ++ this.sendPlayerPermissionLevel(serverPlayer); // Folia - region threading ++ }, null, 1L); // Folia - region threading + } + } + +@@ -1046,8 +_,7 @@ + } + + public void broadcast(@Nullable Player except, double x, double y, double z, double radius, ResourceKey dimension, Packet packet) { +- for (int i = 0; i < this.players.size(); i++) { +- ServerPlayer serverPlayer = this.players.get(i); ++ for (ServerPlayer serverPlayer : this.players) { // Folia - region threading + // CraftBukkit start - Test if player receiving packet can see the source of the packet + if (except != null && !serverPlayer.getBukkitEntity().canSee(except.getBukkitEntity())) { + continue; +@@ -1072,10 +_,15 @@ + public void saveAll(final int interval) { + io.papermc.paper.util.MCUtil.ensureMain("Save Players" , () -> { // Paper - Ensure main + int numSaved = 0; +- final long now = MinecraftServer.currentTick; +- for (int i = 0; i < this.players.size(); i++) { +- final ServerPlayer player = this.players.get(i); +- if (interval == -1 || now - player.lastSave >= interval) { ++ final long now = System.nanoTime(); // Folia - region threading ++ long timeInterval = (long)interval * io.papermc.paper.threadedregions.TickRegionScheduler.TIME_BETWEEN_TICKS; // Folia - region threading ++ for (final ServerPlayer player : this.players) { // Folia - region threading ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { ++ continue; ++ } ++ // Folia end - region threading ++ if (interval == -1 || now - player.lastSave >= timeInterval) { // Folia - region threading + this.save(player); + if (interval != -1 && ++numSaved >= io.papermc.paper.configuration.GlobalConfiguration.get().playerAutoSave.maxPerTick()) { + break; +@@ -1194,6 +_,20 @@ + } + + public void removeAll(boolean isRestarting) { ++ // Folia start - region threading ++ // just send disconnect packet, don't modify state ++ for (ServerPlayer player : this.players) { ++ final Component shutdownMessage = io.papermc.paper.adventure.PaperAdventure.asVanilla(this.server.server.shutdownMessage()); // Paper - Adventure ++ // CraftBukkit end ++ ++ player.connection.send(new net.minecraft.network.protocol.common.ClientboundDisconnectPacket(shutdownMessage), net.minecraft.network.PacketSendListener.thenRun(() -> { ++ player.connection.connection.disconnect(shutdownMessage); ++ })); ++ } ++ if (true) { ++ return; ++ } ++ // Folia end - region threading + // Paper end + // CraftBukkit start - disconnect safely + for (ServerPlayer player : this.players) { +@@ -1203,7 +_,7 @@ + // CraftBukkit end + + // Paper start - Configurable player collision; Remove collideRule team if it exists +- if (this.collideRuleTeamName != null) { ++ if (false && this.collideRuleTeamName != null) { // Folia - region threading + final net.minecraft.world.scores.Scoreboard scoreboard = this.getServer().getLevel(Level.OVERWORLD).getScoreboard(); + final PlayerTeam team = scoreboard.getPlayersTeam(this.collideRuleTeamName); + if (team != null) scoreboard.removePlayerTeam(team); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/server/players/StoredUserList.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/server/players/StoredUserList.java.patch new file mode 100644 index 0000000..8565367 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/server/players/StoredUserList.java.patch @@ -0,0 +1,29 @@ +--- a/net/minecraft/server/players/StoredUserList.java ++++ b/net/minecraft/server/players/StoredUserList.java +@@ -97,6 +_,7 @@ + } + + public void save() throws IOException { ++ synchronized (this) { // Folia - region threading + this.removeExpired(); // Paper - remove expired values before saving + JsonArray jsonArray = new JsonArray(); + this.map.values().stream().map(storedEntry -> Util.make(new JsonObject(), storedEntry::serialize)).forEach(jsonArray::add); +@@ -104,9 +_,11 @@ + try (BufferedWriter writer = Files.newWriter(this.file, StandardCharsets.UTF_8)) { + GSON.toJson(jsonArray, GSON.newJsonWriter(writer)); + } ++ } // Folia - region threading + } + + public void load() throws IOException { ++ synchronized (this) { // Folia - region threading + if (this.file.exists()) { + try (BufferedReader reader = Files.newReader(this.file, StandardCharsets.UTF_8)) { + this.map.clear(); +@@ -131,5 +_,6 @@ + } + // Spigot end + } ++ } // Folia - region threading + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/util/SpawnUtil.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/util/SpawnUtil.java.patch new file mode 100644 index 0000000..7d597d0 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/util/SpawnUtil.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/util/SpawnUtil.java ++++ b/net/minecraft/util/SpawnUtil.java +@@ -83,7 +_,7 @@ + return Optional.of(mob); + } + +- mob.discard(null); // CraftBukkit - add Bukkit remove cause ++ //mob.discard(null); // CraftBukkit - add Bukkit remove cause // Folia - region threading + } + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/RandomSequences.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/RandomSequences.java.patch new file mode 100644 index 0000000..0695ffb --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/RandomSequences.java.patch @@ -0,0 +1,83 @@ +--- a/net/minecraft/world/RandomSequences.java ++++ b/net/minecraft/world/RandomSequences.java +@@ -21,7 +_,7 @@ + private int salt; + private boolean includeWorldSeed = true; + private boolean includeSequenceId = true; +- private final Map sequences = new Object2ObjectOpenHashMap<>(); ++ private final Map sequences = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading + + public static SavedData.Factory factory(long seed) { + return new SavedData.Factory<>( +@@ -120,61 +_,61 @@ + @Override + public RandomSource fork() { + RandomSequences.this.setDirty(); +- return this.random.fork(); ++ synchronized (this.random) { return this.random.fork(); } // Folia - region threading + } + + @Override + public PositionalRandomFactory forkPositional() { + RandomSequences.this.setDirty(); +- return this.random.forkPositional(); ++ synchronized (this.random) { return this.random.forkPositional(); } // Folia - region threading + } + + @Override + public void setSeed(long seed) { + RandomSequences.this.setDirty(); +- this.random.setSeed(seed); ++ synchronized (this.random) { this.random.setSeed(seed); } // Folia - region threading + } + + @Override + public int nextInt() { + RandomSequences.this.setDirty(); +- return this.random.nextInt(); ++ synchronized (this.random) { return this.random.nextInt(); } // Folia - region threading + } + + @Override + public int nextInt(int bound) { + RandomSequences.this.setDirty(); +- return this.random.nextInt(bound); ++ synchronized (this.random) { return this.random.nextInt(bound); } // Folia - region threading + } + + @Override + public long nextLong() { + RandomSequences.this.setDirty(); +- return this.random.nextLong(); ++ synchronized (this.random) { return this.random.nextLong(); } // Folia - region threading + } + + @Override + public boolean nextBoolean() { + RandomSequences.this.setDirty(); +- return this.random.nextBoolean(); ++ synchronized (this.random) { return this.random.nextBoolean(); } // Folia - region threading + } + + @Override + public float nextFloat() { + RandomSequences.this.setDirty(); +- return this.random.nextFloat(); ++ synchronized (this.random) { return this.random.nextFloat(); } // Folia - region threading + } + + @Override + public double nextDouble() { + RandomSequences.this.setDirty(); +- return this.random.nextDouble(); ++ synchronized (this.random) { return this.random.nextDouble(); } // Folia - region threading + } + + @Override + public double nextGaussian() { + RandomSequences.this.setDirty(); +- return this.random.nextGaussian(); ++ synchronized (this.random) { return this.random.nextGaussian(); } // Folia - region threading + } + + @Override diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/CombatTracker.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/CombatTracker.java.patch new file mode 100644 index 0000000..4435c97 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/CombatTracker.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/world/damagesource/CombatTracker.java ++++ b/net/minecraft/world/damagesource/CombatTracker.java +@@ -53,7 +_,7 @@ + } + + private Component getMessageForAssistedFall(Entity entity, Component entityDisplayName, String hasWeaponTranslationKey, String noWeaponTranslationKey) { +- ItemStack itemStack = entity instanceof LivingEntity livingEntity ? livingEntity.getMainHandItem() : ItemStack.EMPTY; ++ ItemStack itemStack = entity instanceof LivingEntity livingEntity && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity) ? livingEntity.getMainHandItem() : ItemStack.EMPTY; // Folia - region threading + return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME) + ? Component.translatable(hasWeaponTranslationKey, this.mob.getDisplayName(), entityDisplayName, itemStack.getDisplayName()) + : Component.translatable(noWeaponTranslationKey, this.mob.getDisplayName(), entityDisplayName); +@@ -80,7 +_,7 @@ + + @Nullable + private static Component getDisplayName(@Nullable Entity entity) { +- return entity == null ? null : entity.getDisplayName(); ++ return entity == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity) ? null : entity.getDisplayName(); // Folia - region threading + } + + public Component getDeathMessage() { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/DamageSource.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/DamageSource.java.patch new file mode 100644 index 0000000..e62931c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/DamageSource.java.patch @@ -0,0 +1,17 @@ +--- a/net/minecraft/world/damagesource/DamageSource.java ++++ b/net/minecraft/world/damagesource/DamageSource.java +@@ -178,12 +_,12 @@ + if (this.causingEntity == null && this.directEntity == null) { + LivingEntity killCredit = livingEntity.getKillCredit(); + String string1 = string + ".player"; +- return killCredit != null ++ return killCredit != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(killCredit) + ? Component.translatable(string1, livingEntity.getDisplayName(), killCredit.getDisplayName()) + : Component.translatable(string, livingEntity.getDisplayName()); + } else { + Component component = this.causingEntity == null ? this.directEntity.getDisplayName() : this.causingEntity.getDisplayName(); +- ItemStack itemStack = this.causingEntity instanceof LivingEntity livingEntity1 ? livingEntity1.getMainHandItem() : ItemStack.EMPTY; ++ ItemStack itemStack = this.causingEntity instanceof LivingEntity livingEntity1 && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(livingEntity1) ? livingEntity1.getMainHandItem() : ItemStack.EMPTY; // Folia - region threading + return !itemStack.isEmpty() && itemStack.has(DataComponents.CUSTOM_NAME) + ? Component.translatable(string + ".item", livingEntity.getDisplayName(), component, itemStack.getDisplayName()) + : Component.translatable(string, livingEntity.getDisplayName(), component); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/FallLocation.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/FallLocation.java.patch new file mode 100644 index 0000000..42cd8c4 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/damagesource/FallLocation.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/damagesource/FallLocation.java ++++ b/net/minecraft/world/damagesource/FallLocation.java +@@ -35,7 +_,7 @@ + @Nullable + public static FallLocation getCurrentFallLocation(LivingEntity entity) { + Optional lastClimbablePos = entity.getLastClimbablePos(); +- if (lastClimbablePos.isPresent()) { ++ if (lastClimbablePos.isPresent() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)entity.level(), lastClimbablePos.get())) { // Folia - region threading + BlockState blockState = entity.level().getBlockState(lastClimbablePos.get()); + return blockToFallLocation(blockState); + } else { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch new file mode 100644 index 0000000..b4f4b03 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch @@ -0,0 +1,1047 @@ +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -145,7 +_,7 @@ + } + + // Paper start - Share random for entities to make them more random +- public static RandomSource SHARED_RANDOM = new RandomRandomSource(); ++ public static RandomSource SHARED_RANDOM = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading + // Paper start - replace random + private static final class RandomRandomSource extends ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom { + public RandomRandomSource() { +@@ -175,7 +_,7 @@ + public org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason; // Paper - Entity#getEntitySpawnReason + + public boolean collisionLoadChunks = false; // Paper +- private org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity; ++ private volatile org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity; // Folia - region threading + + public org.bukkit.craftbukkit.entity.CraftEntity getBukkitEntity() { + if (this.bukkitEntity == null) { +@@ -294,7 +_,7 @@ + private boolean hasGlowingTag; + private final Set tags = new io.papermc.paper.util.SizeLimitedSet<>(new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(), MAX_ENTITY_TAG_COUNT); // Paper - fully limit tag size - replace set impl + private final double[] pistonDeltas = new double[]{0.0, 0.0, 0.0}; +- private long pistonDeltasGameTime; ++ private long pistonDeltasGameTime = Long.MIN_VALUE; // Folia - region threading + private EntityDimensions dimensions; + private float eyeHeight; + public boolean isInPowderSnow; +@@ -525,6 +_,19 @@ + } + } + // Paper end - optimise entity tracker ++ // Folia start - region ticking ++ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { ++ if (this.activatedTick != Integer.MIN_VALUE) { ++ this.activatedTick += fromTickOffset; ++ } ++ if (this.activatedImmunityTick != Integer.MIN_VALUE) { ++ this.activatedImmunityTick += fromTickOffset; ++ } ++ if (this.pistonDeltasGameTime != Long.MIN_VALUE) { ++ this.pistonDeltasGameTime += fromRedstoneTimeOffset; ++ } ++ } ++ // Folia end - region ticking + + public Entity(EntityType entityType, Level level) { + this.type = entityType; +@@ -655,8 +_,7 @@ + // due to interactions on the client. + public void resendPossiblyDesyncedEntityData(net.minecraft.server.level.ServerPlayer player) { + if (player.getBukkitEntity().canSee(this.getBukkitEntity())) { +- ServerLevel world = (net.minecraft.server.level.ServerLevel)this.level(); +- net.minecraft.server.level.ChunkMap.TrackedEntity tracker = world == null ? null : world.getChunkSource().chunkMap.entityMap.get(this.getId()); ++ net.minecraft.server.level.ChunkMap.TrackedEntity tracker = this.moonrise$getTrackedEntity(); // Folia - region threading + if (tracker == null) { + return; + } +@@ -823,7 +_,7 @@ + public void postTick() { + // No clean way to break out of ticking once the entity has been copied to a new world, so instead we move the portalling later in the tick cycle + if (!(this instanceof ServerPlayer) && this.isAlive()) { // Paper - don't attempt to teleport dead entities +- this.handlePortal(); ++ //this.handlePortal(); // Folia - region threading + } + } + // CraftBukkit end +@@ -841,7 +_,7 @@ + this.boardingCooldown--; + } + +- if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick ++ //if (this instanceof ServerPlayer) this.handlePortal(); // CraftBukkit - // Moved up to postTick // Folia - region threading - ONLY allow in postTick() + if (this.canSpawnSprintParticle()) { + this.spawnSprintParticle(); + } +@@ -1104,8 +_,8 @@ + } else { + this.wasOnFire = this.isOnFire(); + if (type == MoverType.PISTON) { +- this.activatedTick = Math.max(this.activatedTick, MinecraftServer.currentTick + 20); // Paper - EAR 2 +- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, MinecraftServer.currentTick + 20); // Paper - EAR 2 ++ this.activatedTick = Math.max(this.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading ++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); // Paper - EAR 2 // Folia - region threading + movement = this.limitPistonMovement(movement); + if (movement.equals(Vec3.ZERO)) { + return; +@@ -1404,7 +_,7 @@ + if (pos.lengthSqr() <= 1.0E-7) { + return pos; + } else { +- long gameTime = this.level().getGameTime(); ++ long gameTime = this.level().getRedstoneGameTime(); // Folia - region threading + if (gameTime != this.pistonDeltasGameTime) { + Arrays.fill(this.pistonDeltas, 0.0); + this.pistonDeltasGameTime = gameTime; +@@ -3038,6 +_,7 @@ + } + + if (force || this.canRide(vehicle) && vehicle.canAddPassenger(this)) { ++ if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen + // CraftBukkit start + if (vehicle.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && this.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) { + org.bukkit.event.vehicle.VehicleEnterEvent event = new org.bukkit.event.vehicle.VehicleEnterEvent((org.bukkit.entity.Vehicle) vehicle.getBukkitEntity(), this.getBukkitEntity()); +@@ -3059,6 +_,7 @@ + return false; + } + // CraftBukkit end ++ } // Folia - region threading - suppress entire event logic during worldgen + if (this.isPassenger()) { + this.stopRiding(); + } +@@ -3126,7 +_,7 @@ + this.passengers = ImmutableList.copyOf(list); + } + +- this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); ++ if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_MOUNT, passenger); // Folia - region threading - do not fire game events for entities not added + } + } + +@@ -3140,6 +_,7 @@ + throw new IllegalStateException("Use x.stopRiding(y), not y.removePassenger(x)"); + } else { + // CraftBukkit start ++ if (this.valid) { // Folia - region threading - suppress entire event logic during worldgen + org.bukkit.craftbukkit.entity.CraftEntity craft = (org.bukkit.craftbukkit.entity.CraftEntity) passenger.getBukkitEntity().getVehicle(); + Entity orig = craft == null ? null : craft.getHandle(); + if (this.getBukkitEntity() instanceof org.bukkit.entity.Vehicle && passenger.getBukkitEntity() instanceof org.bukkit.entity.LivingEntity) { +@@ -3167,6 +_,7 @@ + return false; + } + // CraftBukkit end ++ } // Folia - region threading - suppress entire event logic during worldgen + if (this.passengers.size() == 1 && this.passengers.get(0) == passenger) { + this.passengers = ImmutableList.of(); + } else { +@@ -3174,7 +_,7 @@ + } + + passenger.boardingCooldown = 60; +- this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger); ++ if (!passenger.hasNullCallback()) this.gameEvent(GameEvent.ENTITY_DISMOUNT, passenger); // Folia - region threading - do not fire game events for entities not added + } + return true; // CraftBukkit + } +@@ -3258,7 +_,7 @@ + } + } + +- protected void handlePortal() { ++ public boolean handlePortal() { // Folia - region threading - public, ret type -> boolean + if (this.level() instanceof ServerLevel serverLevel) { + this.processPortalCooldown(); + if (this.portalProcess != null) { +@@ -3266,21 +_,20 @@ + ProfilerFiller profilerFiller = Profiler.get(); + profilerFiller.push("portal"); + this.setPortalCooldown(); +- TeleportTransition portalDestination = this.portalProcess.getPortalDestination(serverLevel, this); +- if (portalDestination != null) { +- ServerLevel level = portalDestination.newLevel(); +- if (this instanceof ServerPlayer // CraftBukkit - always call event for players +- || (level != null && (level.dimension() == serverLevel.dimension() || this.canTeleport(serverLevel, level)))) { // CraftBukkit +- this.teleport(portalDestination); +- } ++ // Folia start - region threading ++ try { ++ return this.portalProcess.portalAsync(serverLevel, this); ++ } finally { ++ profilerFiller.pop(); + } +- +- profilerFiller.pop(); ++ // Folia end - region threading + } else if (this.portalProcess.hasExpired()) { + this.portalProcess = null; + } + } + } ++ ++ return false; // Folia - region threading + } + + public int getDimensionChangingDelay() { +@@ -3420,6 +_,11 @@ + + @Nullable + public PlayerTeam getTeam() { ++ // Folia start - region threading ++ if (true) { ++ return null; ++ } ++ // Folia end - region threading + if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof Player)) { return null; } // Paper - Perf: Disable Scoreboards for non players by default + return this.level().getScoreboard().getPlayersTeam(this.getScoreboardName()); + } +@@ -3726,8 +_,782 @@ + this.portalProcess = entity.portalProcess; + } + ++ // Folia start - region threading ++ public static class EntityTreeNode { ++ @Nullable ++ public EntityTreeNode parent; ++ public Entity root; ++ @Nullable ++ public EntityTreeNode[] passengers; ++ ++ public EntityTreeNode(EntityTreeNode parent, Entity root) { ++ this.parent = parent; ++ this.root = root; ++ } ++ ++ public EntityTreeNode(EntityTreeNode parent, Entity root, EntityTreeNode[] passengers) { ++ this.parent = parent; ++ this.root = root; ++ this.passengers = passengers; ++ } ++ ++ public List getFullTree() { ++ List ret = new java.util.ArrayList<>(); ++ ret.add(this); ++ ++ // this is just a BFS except we don't remove from head, we just advance down the list ++ for (int i = 0; i < ret.size(); ++i) { ++ EntityTreeNode node = ret.get(i); ++ ++ EntityTreeNode[] passengers = node.passengers; ++ if (passengers == null) { ++ continue; ++ } ++ for (EntityTreeNode passenger : passengers) { ++ ret.add(passenger); ++ } ++ } ++ ++ return ret; ++ } ++ ++ public void restore() { ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(this); ++ ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ EntityTreeNode[] passengers = curr.passengers; ++ if (passengers == null) { ++ continue; ++ } ++ ++ List newPassengers = new java.util.ArrayList<>(); ++ for (EntityTreeNode passenger : passengers) { ++ newPassengers.add(passenger.root); ++ passenger.root.vehicle = curr.root; ++ } ++ ++ curr.root.passengers = ImmutableList.copyOf(newPassengers); ++ } ++ } ++ ++ public void addTracker() { ++ for (final EntityTreeNode node : this.getFullTree()) { ++ if (node.root.moonrise$getTrackedEntity() != null) { ++ for (final ServerPlayer player : node.root.level.getLocalPlayers()) { ++ node.root.moonrise$getTrackedEntity().updatePlayer(player); ++ } ++ } ++ } ++ } ++ ++ public void clearTracker() { ++ for (final EntityTreeNode node : this.getFullTree()) { ++ if (node.root.moonrise$getTrackedEntity() != null) { ++ node.root.moonrise$getTrackedEntity().moonrise$removeNonTickThreadPlayers(); ++ for (final ServerPlayer player : node.root.level.getLocalPlayers()) { ++ node.root.moonrise$getTrackedEntity().removePlayer(player); ++ } ++ } ++ } ++ } ++ ++ public void adjustRiders(boolean teleport) { ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(this); ++ ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ EntityTreeNode[] passengers = curr.passengers; ++ if (passengers == null) { ++ continue; ++ } ++ ++ for (EntityTreeNode passenger : passengers) { ++ curr.root.positionRider(passenger.root, teleport ? Entity::moveTo : Entity::setPos); ++ } ++ } ++ } ++ } ++ ++ public void repositionAllPassengers(boolean teleport) { ++ this.makePassengerTree().adjustRiders(teleport); ++ } ++ ++ protected EntityTreeNode makePassengerTree() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot read passengers off of the main thread"); ++ ++ EntityTreeNode root = new EntityTreeNode(null, this); ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(root); ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ Entity vehicle = curr.root; ++ List passengers = vehicle.passengers; ++ if (passengers.isEmpty()) { ++ continue; ++ } ++ ++ EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; ++ curr.passengers = treePassengers; ++ ++ for (int i = 0; i < passengers.size(); ++i) { ++ Entity passenger = passengers.get(i); ++ queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); ++ } ++ } ++ ++ return root; ++ } ++ ++ protected EntityTreeNode detachPassengers() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot adjust passengers/vehicle off of the main thread"); ++ ++ EntityTreeNode root = new EntityTreeNode(null, this); ++ java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); ++ queue.add(root); ++ EntityTreeNode curr; ++ while ((curr = queue.pollFirst()) != null) { ++ Entity vehicle = curr.root; ++ List passengers = vehicle.passengers; ++ if (passengers.isEmpty()) { ++ continue; ++ } ++ ++ vehicle.passengers = ImmutableList.of(); ++ ++ EntityTreeNode[] treePassengers = new EntityTreeNode[passengers.size()]; ++ curr.passengers = treePassengers; ++ ++ for (int i = 0; i < passengers.size(); ++i) { ++ Entity passenger = passengers.get(i); ++ passenger.vehicle = null; ++ queue.addLast(treePassengers[i] = new EntityTreeNode(curr, passenger)); ++ } ++ } ++ ++ return root; ++ } ++ ++ /** ++ * This flag will perform an async load on the chunks determined by ++ * the entity's bounding box before teleporting the entity. ++ */ ++ public static final long TELEPORT_FLAG_LOAD_CHUNK = 1L << 0; ++ /** ++ * This flag requires the entity being teleported to be a root vehicle. ++ * Thus, if you want to teleport a non-root vehicle, you must dismount ++ * the target entity before calling teleport, otherwise the ++ * teleport will be refused. ++ */ ++ public static final long TELEPORT_FLAG_TELEPORT_PASSENGERS = 1L << 1; ++ /** ++ * The flag will dismount any passengers and dismout from the current vehicle ++ * to teleport if and only if dismounting would result in the teleport being allowed. ++ */ ++ public static final long TELEPORT_FLAG_UNMOUNT = 1L << 2; ++ ++ protected void placeSingleSync(ServerLevel originWorld, ServerLevel destination, EntityTreeNode treeNode, long teleportFlags) { ++ destination.addDuringTeleport(this); ++ } ++ ++ protected final void placeInAsync(ServerLevel originWorld, ServerLevel destination, long teleportFlags, ++ EntityTreeNode passengerTree, java.util.function.Consumer teleportComplete) { ++ Vec3 pos = this.position(); ++ ChunkPos posChunk = new ChunkPos( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos) ++ ); ++ ++ // ensure the region is always ticking in case of a shutdown ++ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region ++ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); ++ originWorld.chunkSource.addTicketAtLevel( ++ TicketType.TELEPORT_HOLD_TICKET, posChunk, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ teleportHoldId ++ ); ++ final ServerLevel.PendingTeleport pendingTeleport = new ServerLevel.PendingTeleport(passengerTree, pos); ++ destination.pushPendingTeleport(pendingTeleport); ++ ++ Runnable scheduleEntityJoin = () -> { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ destination, ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos), ++ () -> { ++ if (!destination.removePendingTeleport(pendingTeleport)) { ++ // shutdown logic placed the entity already, and we are shutting down - do nothing to ensure ++ // we do not produce any errors here ++ return; ++ } ++ originWorld.chunkSource.removeTicketAtLevel( ++ TicketType.TELEPORT_HOLD_TICKET, posChunk, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ teleportHoldId ++ ); ++ List fullTree = passengerTree.getFullTree(); ++ for (EntityTreeNode node : fullTree) { ++ node.root.placeSingleSync(originWorld, destination, node, teleportFlags); ++ } ++ ++ // restore passenger tree ++ passengerTree.restore(); ++ passengerTree.adjustRiders(true); ++ ++ // invoke post dimension change now ++ for (EntityTreeNode node : fullTree) { ++ node.root.postChangeDimension(); ++ } ++ ++ if (teleportComplete != null) { ++ teleportComplete.accept(Entity.this); ++ } ++ } ++ ); ++ }; ++ ++ if ((teleportFlags & TELEPORT_FLAG_LOAD_CHUNK) != 0L) { ++ destination.loadChunksForMoveAsync( ++ this.getBoundingBox(), ca.spottedleaf.concurrentutil.util.Priority.HIGHER, ++ (chunkList) -> { ++ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunkList) { ++ destination.chunkSource.addTicketAtLevel( ++ TicketType.POST_TELEPORT, chunk.getPos(), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, ++ Integer.valueOf(Entity.this.getId()) ++ ); ++ } ++ scheduleEntityJoin.run(); ++ } ++ ); ++ } else { ++ scheduleEntityJoin.run(); ++ } ++ } ++ ++ protected boolean canTeleportAsync() { ++ return !this.hasNullCallback() && !this.isRemoved() && this.isAlive() && (!(this instanceof net.minecraft.world.entity.LivingEntity livingEntity) || !livingEntity.isSleeping()); ++ } ++ ++ // Mojang for whatever reason has started storing positions to cache certain physics properties that entities collide with ++ // As usual though, they don't properly do anything to prevent serious desync with respect to the current entity position ++ // We add additional logic to reset these before teleporting to prevent issues with them possibly tripping thread checks. ++ protected void resetStoredPositions() { ++ this.mainSupportingBlockPos = Optional.empty(); ++ } ++ ++ protected void teleportSyncSameRegion(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ if (yaw != null) { ++ this.setYRot(yaw.floatValue()); ++ this.setYHeadRot(yaw.floatValue()); ++ } ++ if (pitch != null) { ++ this.setXRot(pitch.floatValue()); ++ } ++ if (velocity != null) { ++ this.setDeltaMovement(velocity); ++ } ++ this.moveTo(pos.x, pos.y, pos.z); ++ this.setOldPosAndRot(); ++ this.resetStoredPositions(); ++ } ++ ++ protected final void transform(TeleportTransition telpeort) { ++ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute( ++ PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives() ++ ); ++ this.transform( ++ move.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement() ++ ); ++ } ++ ++ protected void transform(Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ if (yaw != null) { ++ this.setYRot(yaw.floatValue()); ++ this.setYHeadRot(yaw.floatValue()); ++ } ++ if (pitch != null) { ++ this.setXRot(pitch.floatValue()); ++ } ++ if (velocity != null) { ++ this.setDeltaMovement(velocity); ++ } ++ if (pos != null) { ++ this.setPosRaw(pos.x, pos.y, pos.z); ++ } ++ this.setOldPosAndRot(); ++ } ++ ++ protected final Entity transformForAsyncTeleport(TeleportTransition telpeort) { ++ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute( ++ PositionMoveRotation.of(this), PositionMoveRotation.of(telpeort), telpeort.relatives() ++ ); ++ return this.transformForAsyncTeleport( ++ telpeort.newLevel(), telpeort.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement() ++ ); ++ } ++ ++ protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ this.removeAfterChangingDimensions(); // remove before so that any CBEntity#getHandle call affects this entity before copying ++ ++ Entity copy = this.getType().create(destination, EntitySpawnReason.DIMENSION_TRAVEL); ++ copy.restoreFrom(this); ++ copy.transform(pos, yaw, pitch, velocity); ++ // vanilla code used to call remove _after_ copying, and some stuff is required to be after copy - so add hook here ++ // for example, clearing of inventory after switching dimensions ++ this.postRemoveAfterChangingDimensions(); ++ ++ return copy; ++ } ++ ++ public final boolean teleportAsync(TeleportTransition teleportTarget, long teleportFlags, ++ java.util.function.Consumer teleportComplete) { ++ PositionMoveRotation move = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this), PositionMoveRotation.of(teleportTarget), teleportTarget.relatives()); ++ ++ return this.teleportAsync( ++ teleportTarget.newLevel(), move.position(), Float.valueOf(move.yRot()), Float.valueOf(move.xRot()), move.deltaMovement(), ++ teleportTarget.cause(), teleportFlags, teleportComplete ++ ); ++ } ++ ++ public final boolean teleportAsync(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity, ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, long teleportFlags, ++ java.util.function.Consumer teleportComplete) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot teleport entity async"); ++ ++ if (!ServerLevel.isInSpawnableBounds(new BlockPos(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockY(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockZ(pos)))) { ++ return false; ++ } ++ ++ if (!this.canTeleportAsync()) { ++ return false; ++ } ++ this.getBukkitEntity(); // force bukkit entity to be created before TPing ++ if ((teleportFlags & TELEPORT_FLAG_UNMOUNT) == 0L) { ++ for (Entity entity : this.getIndirectPassengers()) { ++ if (!entity.canTeleportAsync()) { ++ return false; ++ } ++ entity.getBukkitEntity(); // force bukkit entity to be created before TPing ++ } ++ } else { ++ this.unRide(); ++ } ++ ++ if ((teleportFlags & TELEPORT_FLAG_TELEPORT_PASSENGERS) != 0L) { ++ if (this.isPassenger()) { ++ return false; ++ } ++ } else { ++ if (this.isVehicle() || this.isPassenger()) { ++ return false; ++ } ++ } ++ ++ // TODO any events that can modify go HERE ++ ++ // check for same region ++ if (destination == this.level()) { ++ Vec3 currPos = this.position(); ++ if ( ++ destination.regioniser.getRegionAtUnsynchronised( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(currPos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(currPos) ++ ) == destination.regioniser.getRegionAtUnsynchronised( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(pos) ++ ) ++ ) { ++ EntityTreeNode passengerTree = this.detachPassengers(); ++ // Note: The client does not accept position updates for controlled entities. So, we must ++ // perform a lot of tracker updates here to make it all work out. ++ ++ // first, clear the tracker ++ passengerTree.clearTracker(); ++ for (EntityTreeNode entity : passengerTree.getFullTree()) { ++ entity.root.teleportSyncSameRegion(pos, yaw, pitch, velocity); ++ } ++ ++ passengerTree.restore(); ++ // re-add to the tracker once the tree is restored ++ passengerTree.addTracker(); ++ ++ // adjust entities to final position ++ passengerTree.adjustRiders(true); ++ ++ // the tracker clear/add logic is only used in the same region, as the other logic ++ // performs add/remove from world logic which will also perform add/remove tracker logic ++ ++ if (teleportComplete != null) { ++ teleportComplete.accept(this); ++ } ++ return true; ++ } ++ } ++ ++ EntityTreeNode passengerTree = this.detachPassengers(); ++ List fullPassengerTree = passengerTree.getFullTree(); ++ ServerLevel originWorld = (ServerLevel)this.level; ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root.preChangeDimension(); ++ } ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root = node.root.transformForAsyncTeleport(destination, pos, yaw, pitch, velocity); ++ } ++ ++ passengerTree.root.placeInAsync(originWorld, destination, teleportFlags, passengerTree, teleportComplete); ++ ++ return true; ++ } ++ ++ public void preChangeDimension() { ++ if (this instanceof Leashable leashable) { ++ leashable.dropLeash(); ++ } ++ } ++ ++ public void postChangeDimension() { ++ this.resetStoredPositions(); ++ } ++ ++ protected static enum PortalType { ++ NETHER, END; ++ } ++ ++ public boolean endPortalLogicAsync(BlockPos portalPos) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ ServerLevel destination = this.getServer().getLevel(this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END ? Level.OVERWORLD : Level.END); ++ if (destination == null) { ++ // wat ++ return false; ++ } ++ ++ return this.portalToAsync(destination, portalPos, true, PortalType.END, null); ++ } ++ ++ public boolean netherPortalLogicAsync(BlockPos portalPos) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ ++ ServerLevel destination = this.getServer().getLevel(this.level().getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER ? Level.OVERWORLD : Level.NETHER); ++ if (destination == null) { ++ // wat ++ return false; ++ } ++ ++ return this.portalToAsync(destination, portalPos, true, PortalType.NETHER, null); ++ } ++ ++ private static final java.util.concurrent.atomic.AtomicLong CREATE_PORTAL_DOUBLE_CHECK = new java.util.concurrent.atomic.AtomicLong(); ++ private static final java.util.concurrent.atomic.AtomicLong TELEPORT_HOLD_TICKET_GEN = new java.util.concurrent.atomic.AtomicLong(); ++ ++ // To simplify portal logic, in region threading both players ++ // and non-player entities will create portals. By guaranteeing ++ // that the teleportation can take place, we can simply ++ // remove the entity, find/create the portal, and place async. ++ // If we have to worry about whether the entity may not teleport, ++ // we need to first search, then report back, ... ++ protected void findOrCreatePortalAsync(ServerLevel origin, BlockPos originPortal, ServerLevel destination, PortalType type, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalInfoCompletable) { ++ switch (type) { ++ // end portal logic is quite simple, the spawn in the end is fixed and when returning to the overworld ++ // we just select the spawn position ++ case END: { ++ if (destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { ++ BlockPos targetPos = ServerLevel.END_SPAWN_POINT; ++ // need to load chunks so we can create the platform ++ destination.moonrise$loadChunksAsync( ++ targetPos, 16, // load 16 blocks to be safe from block physics ++ ca.spottedleaf.concurrentutil.util.Priority.HIGH, ++ (chunks) -> { ++ net.minecraft.world.level.levelgen.feature.EndPlatformFeature.createEndPlatform(destination, targetPos.below(), true, null); ++ ++ // the portal obsidian is placed at targetPos.y - 2, so if we want to place the entity ++ // on the obsidian, we need to spawn at targetPos.y - 1 ++ portalInfoCompletable.complete( ++ new net.minecraft.world.level.portal.TeleportTransition( ++ destination, Vec3.atBottomCenterOf(targetPos.below()), Vec3.ZERO, 90.0f, 0.0f, ++ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL ++ ) ++ ); ++ } ++ ); ++ } else { ++ BlockPos spawnPos = destination.getSharedSpawnPos(); ++ // need to load chunk for heightmap ++ destination.moonrise$loadChunksAsync( ++ spawnPos, 0, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGH, ++ (chunks) -> { ++ BlockPos adjustedSpawn = destination.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, spawnPos); ++ ++ // done ++ portalInfoCompletable.complete( ++ new net.minecraft.world.level.portal.TeleportTransition( ++ destination, Vec3.atBottomCenterOf(adjustedSpawn), Vec3.ZERO, 90.0f, 0.0f, ++ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL ++ ) ++ ); ++ } ++ ); ++ } ++ ++ break; ++ } ++ // for the nether logic, we need to first load the chunks in radius to empty (so that POI is created) ++ // then we can search for an existing portal using the POI routines ++ // if we don't find a portal, then we bring the chunks in the create radius to full and ++ // create it ++ case NETHER: { ++ // hoisted from the create fallback, so that we can avoid the sync load later if we need it ++ BlockState originalPortalBlock = origin.getBlockStateIfLoaded(originPortal); ++ Direction.Axis originalPortalDirection = originalPortalBlock == null ? Direction.Axis.X : ++ originalPortalBlock.getOptionalValue(net.minecraft.world.level.block.NetherPortalBlock.AXIS).orElse(Direction.Axis.X); ++ BlockUtil.FoundRectangle originalPortalRectangle = ++ originalPortalBlock == null || !originalPortalBlock.hasProperty(net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_AXIS) ++ ? null ++ : BlockUtil.getLargestRectangleAround( ++ originPortal, originalPortalDirection, 21, Direction.Axis.Y, 21, ++ (blockpos) -> { ++ return origin.getBlockStateFromEmptyChunkIfLoaded(blockpos) == originalPortalBlock; ++ } ++ ); ++ ++ boolean destinationIsNether = destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER; ++ ++ int portalSearchRadius = origin.paperConfig().environment.portalSearchVanillaDimensionScaling && destinationIsNether ? ++ (int)(destination.paperConfig().environment.portalSearchRadius / destination.dimensionType().coordinateScale()) : ++ destination.paperConfig().environment.portalSearchRadius; ++ int portalCreateRadius = destination.paperConfig().environment.portalCreateRadius; ++ ++ WorldBorder destinationBorder = destination.getWorldBorder(); ++ double dimensionScale = net.minecraft.world.level.dimension.DimensionType.getTeleportationScale(origin.dimensionType(), destination.dimensionType()); ++ BlockPos targetPos = destination.getWorldBorder().clampToBounds(this.getX() * dimensionScale, this.getY(), this.getZ() * dimensionScale); ++ ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalFound ++ = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ ++ // post portal find/create logic ++ portalFound.addWaiter( ++ (BlockUtil.FoundRectangle portal, Throwable thr) -> { ++ // no portal could be created ++ if (portal == null) { ++ portalInfoCompletable.complete( ++ new TeleportTransition(destination, Vec3.atCenterOf(targetPos), Vec3.ZERO, ++ 90.0f, 0.0f, ++ TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET)) ++ ); ++ return; ++ } ++ ++ Vec3 relativePos = originalPortalRectangle == null ? ++ new Vec3(0.5, 0.0, 0.0) : ++ Entity.this.getRelativePortalPosition(originalPortalDirection, originalPortalRectangle); ++ ++ portalInfoCompletable.complete( ++ net.minecraft.world.level.block.NetherPortalBlock.createDimensionTransition( ++ destination, portal, originalPortalDirection, relativePos, ++ Entity.this, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET) ++ ) ++ ); ++ } ++ ); ++ ++ // kick off search for existing portal or creation ++ destination.moonrise$loadChunksAsync( ++ // add 32 so that the final search for a portal frame doesn't load any chunks ++ targetPos, portalSearchRadius + 32, ++ net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGH, ++ (chunks) -> { ++ BlockUtil.FoundRectangle portal = ++ net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius); ++ if (portal != null) { ++ portalFound.complete(portal); ++ return; ++ } ++ ++ // add tickets so that we can re-search for a portal once the chunks are loaded ++ Long ticketId = Long.valueOf(CREATE_PORTAL_DOUBLE_CHECK.getAndIncrement()); ++ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { ++ destination.chunkSource.addTicketAtLevel( ++ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ ticketId ++ ); ++ } ++ ++ // no portal found - create one ++ destination.moonrise$loadChunksAsync( ++ targetPos, portalCreateRadius + 32, ++ ca.spottedleaf.concurrentutil.util.Priority.HIGH, ++ (chunks2) -> { ++ // don't need the tickets anymore ++ // note: we expect removeTicketsAtLevel to add an unknown ticket for us automatically ++ // if the ticket level were to decrease ++ for (net.minecraft.world.level.chunk.ChunkAccess chunk : chunks) { ++ destination.chunkSource.removeTicketAtLevel( ++ TicketType.NETHER_PORTAL_DOUBLE_CHECK, chunk.getPos(), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ ticketId ++ ); ++ } ++ ++ // when two entities portal at the same time, it is possible that both entities reach this ++ // part of the code - and create a double portal ++ // to fix this, we just issue another search to try and see if another entity created ++ // a portal nearby ++ BlockUtil.FoundRectangle existingTryAgain = ++ net.minecraft.world.level.block.NetherPortalBlock.findPortalAround(destination, targetPos, destinationBorder, portalSearchRadius); ++ if (existingTryAgain != null) { ++ portalFound.complete(existingTryAgain); ++ return; ++ } ++ ++ // we do not have the correct entity reference here ++ BlockUtil.FoundRectangle createdPortal = ++ destination.getPortalForcer().createPortal(targetPos, originalPortalDirection, null, portalCreateRadius).orElse(null); ++ // if it wasn't created, passing null is expected here ++ portalFound.complete(createdPortal); ++ } ++ ); ++ } ++ ); ++ break; ++ } ++ default: { ++ throw new IllegalStateException("Unknown portal type " + type); ++ } ++ } ++ } ++ ++ public boolean canPortalAsync(ServerLevel to, boolean considerPassengers) { ++ return this.canPortalAsync(to, considerPassengers, false); ++ } ++ ++ protected boolean canPortalAsync(ServerLevel to, boolean considerPassengers, boolean skipPassengerCheck) { ++ if (considerPassengers) { ++ if (!skipPassengerCheck && this.isPassenger()) { ++ return false; ++ } ++ } else { ++ if (this.isVehicle() || (!skipPassengerCheck && this.isPassenger())) { ++ return false; ++ } ++ } ++ this.getBukkitEntity(); // force bukkit entity to be created before TPing ++ if (!this.canTeleportAsync()) { ++ return false; ++ } ++ if (considerPassengers) { ++ for (Entity entity : this.passengers) { ++ if (!entity.canPortalAsync(to, true, true)) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ ++ protected void prePortalLogic(ServerLevel origin, ServerLevel destination, PortalType type) { ++ ++ } ++ ++ protected boolean portalToAsync(ServerLevel destination, BlockPos portalPos, boolean takePassengers, ++ PortalType type, java.util.function.Consumer teleportComplete) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot portal entity async"); ++ if (!this.canPortalAsync(destination, takePassengers)) { ++ return false; ++ } ++ ++ Vec3 initialPosition = this.position(); ++ ChunkPos initialPositionChunk = new ChunkPos( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(initialPosition), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(initialPosition) ++ ); ++ ++ // first, remove entity/passengers from world ++ EntityTreeNode passengerTree = this.detachPassengers(); ++ List fullPassengerTree = passengerTree.getFullTree(); ++ ServerLevel originWorld = (ServerLevel)this.level; ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root.preChangeDimension(); ++ node.root.prePortalLogic(originWorld, destination, type); ++ } ++ ++ for (EntityTreeNode node : fullPassengerTree) { ++ // we will update pos/rot/speed later ++ node.root = node.root.transformForAsyncTeleport(destination, null, null, null, null); ++ // set portal cooldown ++ node.root.setPortalCooldown(); ++ } ++ ++ // ensure the region is always ticking in case of a shutdown ++ // otherwise, the shutdown will not be able to complete the shutdown as it requires a ticking region ++ Long teleportHoldId = Long.valueOf(TELEPORT_HOLD_TICKET_GEN.getAndIncrement()); ++ originWorld.chunkSource.addTicketAtLevel( ++ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ teleportHoldId ++ ); ++ ++ ServerLevel.PendingTeleport beforeFindDestination = new ServerLevel.PendingTeleport(passengerTree, initialPosition); ++ originWorld.pushPendingTeleport(beforeFindDestination); ++ ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalInfoCompletable ++ = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ ++ portalInfoCompletable.addWaiter((TeleportTransition info, Throwable throwable) -> { ++ if (!originWorld.removePendingTeleport(beforeFindDestination)) { ++ // the shutdown thread has placed us back into the origin world at the original position ++ // we just have to abandon this teleport to prevent duplication ++ return; ++ } ++ originWorld.chunkSource.removeTicketAtLevel( ++ TicketType.TELEPORT_HOLD_TICKET, initialPositionChunk, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.MAX_TICKET_LEVEL, ++ teleportHoldId ++ ); ++ // adjust passenger tree to final pos/rot/speed ++ for (EntityTreeNode node : fullPassengerTree) { ++ node.root.transform(info); ++ } ++ ++ // place ++ passengerTree.root.placeInAsync( ++ originWorld, destination, Entity.TELEPORT_FLAG_LOAD_CHUNK | (takePassengers ? Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS : 0L), ++ passengerTree, ++ (Entity teleported) -> { ++ if (info.postTeleportTransition() != null) { ++ info.postTeleportTransition().onTransition(teleported); ++ } ++ ++ if (teleportComplete != null) { ++ teleportComplete.accept(teleported); ++ } ++ } ++ ); ++ }); ++ ++ ++ passengerTree.root.findOrCreatePortalAsync(originWorld, portalPos, destination, type, portalInfoCompletable); ++ ++ return true; ++ } ++ // Folia end - region threading ++ + @Nullable + public Entity teleport(TeleportTransition teleportTransition) { ++ // Folia start - region threading ++ if (true) { ++ throw new UnsupportedOperationException("Must use teleportAsync while in region threading"); ++ } ++ // Folia end - region threading + // Paper start - Fix item duplication and teleport issues + if ((!this.isAlive() || !this.valid) && (teleportTransition.newLevel() != this.level)) { + LOGGER.warn("Illegal Entity Teleport " + this + " to " + teleportTransition.newLevel() + ":" + teleportTransition.position(), new Throwable()); +@@ -3911,6 +_,12 @@ + } + } + ++ // Folia start - region threading - move inventory clearing until after the dimension change ++ protected void postRemoveAfterChangingDimensions() { ++ ++ } ++ // Folia end - region threading - move inventory clearing until after the dimension change ++ + protected void removeAfterChangingDimensions() { + this.setRemoved(Entity.RemovalReason.CHANGED_DIMENSION, null); // CraftBukkit - add Bukkit remove cause + if (this instanceof Leashable leashable && leashable.isLeashed()) { // Paper - only call if it is leashed +@@ -4790,7 +_,8 @@ + } + } + // Paper end - Fix MC-4 +- if (this.position.x != x || this.position.y != y || this.position.z != z) { ++ boolean posChanged = this.position.x != x || this.position.y != y || this.position.z != z; ++ if (posChanged) { // Folia - region threading + synchronized (this.posLock) { // Paper - detailed watchdog information + this.position = new Vec3(x, y, z); + } // Paper - detailed watchdog information +@@ -4809,7 +_,7 @@ + } + // Paper start - Block invalid positions and bounding box; don't allow desync of pos and AABB + // hanging has its own special logic +- if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || this.position.x != x || this.position.y != y || this.position.z != z)) { ++ if (!(this instanceof net.minecraft.world.entity.decoration.HangingEntity) && (forceBoundingBoxUpdate || posChanged)) { + this.setBoundingBox(this.makeBoundingBox()); + } + // Paper end - Block invalid positions and bounding box +@@ -4893,6 +_,12 @@ + return this.removalReason != null; + } + ++ // Folia start - region threading ++ public final boolean hasNullCallback() { ++ return this.levelCallback == EntityInLevelCallback.NULL; ++ } ++ // Folia end - region threading ++ + @Nullable + public Entity.RemovalReason getRemovalReason() { + return this.removalReason; +@@ -4915,6 +_,9 @@ + org.bukkit.craftbukkit.event.CraftEventFactory.callEntityRemoveEvent(this, cause); + // CraftBukkit end + final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers ++ // Folia start - region threading ++ this.preRemove(removalReason); ++ // Folia end - region threading + if (this.removalReason == null) { + this.removalReason = removalReason; + } +@@ -4937,6 +_,10 @@ + public void unsetRemoved() { + this.removalReason = null; + } ++ ++ // Folia start - region threading ++ protected void preRemove(Entity.RemovalReason reason) {} ++ // Folia end - region threading + + // Paper start - Folia schedulers + /** diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/LivingEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/LivingEntity.java.patch new file mode 100644 index 0000000..c93d7ec --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/LivingEntity.java.patch @@ -0,0 +1,148 @@ +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -278,7 +_,7 @@ + private Optional lastClimbablePos = Optional.empty(); + @Nullable + private DamageSource lastDamageSource; +- private long lastDamageStamp; ++ private long lastDamageStamp = Long.MIN_VALUE; // Folia - region threading + protected int autoSpinAttackTicks; + protected float autoSpinAttackDmg; + @Nullable +@@ -307,6 +_,21 @@ + return this.getYHeadRot(); + } + // CraftBukkit end ++ // Folia start - region threading ++ @Override ++ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { ++ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ if (this.lastDamageStamp != Long.MIN_VALUE) { ++ this.lastDamageStamp += fromRedstoneTimeOffset; ++ } ++ } ++ ++ @Override ++ protected void resetStoredPositions() { ++ super.resetStoredPositions(); ++ this.lastClimbablePos = Optional.empty(); ++ } ++ // Folia end - region threading + + protected LivingEntity(EntityType entityType, Level level) { + super(entityType, level); +@@ -528,7 +_,7 @@ + + if (this.isDeadOrDying() && this.level().shouldTickDeath(this)) { + this.tickDeath(); +- } ++ } else { this.broadcastedDeath = false; } // Folia - region threading + + if (this.lastHurtByPlayerTime > 0) { + this.lastHurtByPlayerTime--; +@@ -611,11 +_,14 @@ + return true; + } + ++ public boolean broadcastedDeath = false; // Folia - region threading + protected void tickDeath() { + this.deathTime++; + if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved()) { + this.level().broadcastEntityEvent(this, (byte)60); +- this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause ++ this.broadcastedDeath = true; // Folia - region threading - death has been broadcasted ++ if (!(this instanceof ServerPlayer)) this.remove(Entity.RemovalReason.KILLED, EntityRemoveEvent.Cause.DEATH); // CraftBukkit - add Bukkit remove cause // Folia - region threading - don't remove, we want the tick scheduler to be running ++ if ((this instanceof ServerPlayer)) this.unRide(); // Folia - region threading - unmount player when dead + } + } + +@@ -851,9 +_,9 @@ + } + + this.hurtTime = compound.getShort("HurtTime"); +- this.deathTime = compound.getShort("DeathTime"); ++ this.deathTime = compound.getShort("DeathTime"); this.broadcastedDeath = false; // Folia - region threading + this.lastHurtByMobTimestamp = compound.getInt("HurtByTimestamp"); +- if (compound.contains("Team", 8)) { ++ if (false && compound.contains("Team", 8)) { // Folia start - region threading + String string = compound.getString("Team"); + Scoreboard scoreboard = this.level().getScoreboard(); + PlayerTeam playerTeam = scoreboard.getPlayerTeam(string); +@@ -1115,6 +_,7 @@ + public boolean addEffect(MobEffectInstance effectInstance, @Nullable Entity entity, EntityPotionEffectEvent.Cause cause, boolean fireEvent) { + // Paper end - Don't fire sync event during generation + // org.spigotmc.AsyncCatcher.catchOp("effect add"); // Spigot // Paper - move to API ++ if (!this.hasNullCallback()) ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot add effects to entities asynchronously"); // Folia - region threading + if (this.isTickingEffects) { + this.effectsToProcess.add(new ProcessableEffect(effectInstance, cause)); + return true; +@@ -1502,7 +_,7 @@ + boolean flag2 = !flag; // CraftBukkit - Ensure to return false if damage is blocked + if (flag2) { + this.lastDamageSource = damageSource; +- this.lastDamageStamp = this.level().getGameTime(); ++ this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading + + for (MobEffectInstance mobEffectInstance : this.getActiveEffects()) { + mobEffectInstance.onMobHurt(level, this, damageSource, amount); +@@ -1629,7 +_,7 @@ + + @Nullable + public DamageSource getLastDamageSource() { +- if (this.level().getGameTime() - this.lastDamageStamp > 40L) { ++ if (this.level().getRedstoneGameTime() - this.lastDamageStamp > 40L || this.lastDamageStamp == Long.MIN_VALUE) { // Folia - region threading + this.lastDamageSource = null; + } + +@@ -2420,10 +_,10 @@ + + @Nullable + public LivingEntity getKillCredit() { +- if (this.lastHurtByPlayer != null) { ++ if (this.lastHurtByPlayer != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByPlayer)) { // Folia - region threading + return this.lastHurtByPlayer; + } else { +- return this.lastHurtByMob != null ? this.lastHurtByMob : null; ++ return this.lastHurtByMob != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.lastHurtByMob) ? this.lastHurtByMob : null; // Folia - region threading + } + } + +@@ -2502,7 +_,7 @@ + } + + this.lastDamageSource = damageSource; +- this.lastDamageStamp = this.level().getGameTime(); ++ this.lastDamageStamp = this.level().getRedstoneGameTime(); // Folia - region threading + } + + @Override +@@ -3479,7 +_,7 @@ + this.pushEntities(); + profilerFiller.pop(); + // Paper start - Add EntityMoveEvent +- if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof Player)) { ++ if (((ServerLevel) this.level()).getCurrentWorldData().hasEntityMoveEvent && !(this instanceof Player)) { // Folia - region threading + if (this.xo != this.getX() || this.yo != this.getY() || this.zo != this.getZ() || this.yRotO != this.getYRot() || this.xRotO != this.getXRot()) { + Location from = new Location(this.level().getWorld(), this.xo, this.yo, this.zo, this.yRotO, this.xRotO); + Location to = new Location(this.level().getWorld(), this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); +@@ -4152,7 +_,7 @@ + boolean flag = false; + BlockPos blockPos = BlockPos.containing(x, y, z); + Level level = this.level(); +- if (level.hasChunkAt(blockPos)) { ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((ServerLevel)level, blockPos) && level.hasChunkAt(blockPos)) { // Folia - region threading + boolean flag1 = false; + + while (!flag1 && blockPos.getY() > level.getMinY()) { +@@ -4314,6 +_,11 @@ + this.setXRot(0.0F); + } + }); ++ // Folia start - separate out ++ this.stopSleepingRaw(); ++ } ++ public void stopSleepingRaw() { ++ // Folia end - separate out + Vec3 vec3 = this.position(); + this.setPose(Pose.STANDING); + this.setPos(vec3.x, vec3.y, vec3.z); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/Mob.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/Mob.java.patch new file mode 100644 index 0000000..a527d22 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/Mob.java.patch @@ -0,0 +1,61 @@ +--- a/net/minecraft/world/entity/Mob.java ++++ b/net/minecraft/world/entity/Mob.java +@@ -254,8 +_,20 @@ + @Nullable + @Override + public LivingEntity getTarget() { +- return this.target; +- } ++ // Folia start - region threading ++ if (this.target != null && (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.target) || this.target.isRemoved())) { ++ this.target = null; ++ return null; ++ } ++ // Folia end - region threading ++ return this.target; ++ } ++ ++ // Folia start - region threading ++ public LivingEntity getTargetRaw() { ++ return this.target; ++ } ++ // Folia end - region threading + + @Nullable + protected final LivingEntity getTargetFromBrain() { +@@ -268,7 +_,7 @@ + } + + public boolean setTarget(LivingEntity target, EntityTargetEvent.TargetReason reason, boolean fireEvent) { +- if (this.getTarget() == target) { ++ if (this.getTargetRaw() == target) { // Folia - region threading + return false; + } + if (fireEvent) { +@@ -1663,12 +_,26 @@ + @Override + protected void removeAfterChangingDimensions() { + super.removeAfterChangingDimensions(); ++ // Folia start - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions ++// this.getAllSlots().forEach(itemStack -> { ++// if (!itemStack.isEmpty()) { ++// itemStack.setCount(0); ++// } ++// }); ++ // Folia end - region threading - move inventory clearing until after the dimension change - move into postRemoveAfterChangingDimensions ++ } ++ ++ // Folia start - region threading ++ @Override ++ protected void postRemoveAfterChangingDimensions() { ++ super.postRemoveAfterChangingDimensions(); + this.getAllSlots().forEach(itemStack -> { + if (!itemStack.isEmpty()) { + itemStack.setCount(0); + } + }); + } ++ // Folia end - region threading + + @Nullable + @Override diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/PortalProcessor.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/PortalProcessor.java.patch new file mode 100644 index 0000000..7dd8cd8 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/PortalProcessor.java.patch @@ -0,0 +1,15 @@ +--- a/net/minecraft/world/entity/PortalProcessor.java ++++ b/net/minecraft/world/entity/PortalProcessor.java +@@ -33,6 +_,12 @@ + return this.portal.getPortalDestination(level, entity, this.entryPosition); + } + ++ // Folia start - region threading ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget) { ++ return this.portal.portalAsync(sourceWorld, portalTarget, this.entryPosition); ++ } ++ // Folia end - region threading ++ + public Portal.Transition getPortalLocalTransition() { + return this.portal.getLocalTransition(); + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/TamableAnimal.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/TamableAnimal.java.patch new file mode 100644 index 0000000..fcf4085 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/TamableAnimal.java.patch @@ -0,0 +1,38 @@ +--- a/net/minecraft/world/entity/TamableAnimal.java ++++ b/net/minecraft/world/entity/TamableAnimal.java +@@ -263,6 +_,11 @@ + public void tryToTeleportToOwner() { + LivingEntity owner = this.getOwner(); + if (owner != null) { ++ // Folia start - region threading ++ if (owner.isRemoved() || owner.level() != this.level()) { ++ return; ++ } ++ // Folia end - region threading + this.teleportToAroundBlockPos(owner.blockPosition()); + } + } +@@ -295,7 +_,22 @@ + return false; + } + org.bukkit.Location to = event.getTo(); +- this.moveTo(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch()); ++ // Folia start - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick ++ // also, use teleportAsync so that crossing region boundaries will not blow up ++ org.bukkit.Location finalTo = to; ++ Level sourceWorld = this.level(); ++ this.getBukkitEntity().taskScheduler.schedule((TamableAnimal nmsEntity) -> { ++ if (nmsEntity.level() == sourceWorld) { ++ nmsEntity.teleportAsync( ++ (net.minecraft.server.level.ServerLevel)nmsEntity.level(), ++ new net.minecraft.world.phys.Vec3(finalTo.getX(), finalTo.getY(), finalTo.getZ()), ++ Float.valueOf(finalTo.getYaw()), Float.valueOf(finalTo.getPitch()), ++ net.minecraft.world.phys.Vec3.ZERO, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN, Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ null ++ ); ++ } ++ }, null, 1L); ++ // Folia end - region threading - can't teleport here, we may be removed by teleport logic - delay until next tick + // CraftBukkit end + this.navigation.stop(); + return true; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/Brain.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/Brain.java.patch new file mode 100644 index 0000000..a92a131 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/Brain.java.patch @@ -0,0 +1,21 @@ +--- a/net/minecraft/world/entity/ai/Brain.java ++++ b/net/minecraft/world/entity/ai/Brain.java +@@ -425,9 +_,17 @@ + } + + public void stopAll(ServerLevel level, E owner) { ++ // Folia start - region threading ++ List> behaviors = this.getRunningBehaviors(); ++ if (behaviors.isEmpty()) { ++ // avoid calling getGameTime, as this may be called while portalling an entity - which will cause ++ // the world data retrieval to fail ++ return; ++ } ++ // Folia end - region threading + long gameTime = owner.level().getGameTime(); + +- for (BehaviorControl behaviorControl : this.getRunningBehaviors()) { ++ for (BehaviorControl behaviorControl : behaviors) { // Folia - region threading + behaviorControl.doStop(level, owner, gameTime); + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java.patch new file mode 100644 index 0000000..8522d57 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java ++++ b/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java +@@ -19,6 +_,11 @@ + instance, + (jobSite, nearestLivingEntities) -> (level, villager, gameTime) -> { + GlobalPos globalPos = instance.get(jobSite); ++ // Folia start - region threading ++ if (globalPos.dimension() != level.dimension() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, globalPos.pos())) { ++ return true; ++ } ++ // Folia end - region threading + level.getPoiManager() + .getType(globalPos.pos()) + .ifPresent( diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch new file mode 100644 index 0000000..8a9e6bc --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java ++++ b/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java +@@ -51,7 +_,7 @@ + public boolean canContinueToUse() { + return !this.navigation.isDone() + && !this.tamable.unableToMoveToOwner() +- && !(this.tamable.distanceToSqr(this.owner) <= this.stopDistance * this.stopDistance); ++ && !(this.owner.level() == this.tamable.level() && this.tamable.distanceToSqr(this.owner) <= this.stopDistance * this.stopDistance); // Folia - region threading - check level + } + + @Override diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch new file mode 100644 index 0000000..62f0121 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java ++++ b/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java +@@ -42,6 +_,11 @@ + + @Override + public Path createPath(BlockPos pos, @javax.annotation.Nullable Entity entity, int accuracy) { // Paper - EntityPathfindEvent ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level, pos)) { ++ return null; ++ } ++ // Folia end - region threading + LevelChunk chunkNow = this.level.getChunkSource().getChunkNow(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ())); + if (chunkNow == null) { + return null; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch new file mode 100644 index 0000000..a992f24 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch @@ -0,0 +1,34 @@ +--- a/net/minecraft/world/entity/ai/navigation/PathNavigation.java ++++ b/net/minecraft/world/entity/ai/navigation/PathNavigation.java +@@ -96,11 +_,11 @@ + } + + public void recomputePath() { +- if (this.level.getGameTime() - this.timeLastRecompute > 20L) { ++ if (this.tick - this.timeLastRecompute > 20L) { // Folia - region threading + if (this.targetPos != null) { + this.path = null; + this.path = this.createPath(this.targetPos, this.reachRange); +- this.timeLastRecompute = this.level.getGameTime(); ++ this.timeLastRecompute = this.tick; // Folia - region threading + this.hasDelayedRecomputation = false; + } + } else { +@@ -221,7 +_,7 @@ + + public boolean moveTo(Entity entity, double speed) { + // Paper start - Perf: Optimise pathfinding +- if (this.pathfindFailures > 10 && this.path == null && net.minecraft.server.MinecraftServer.currentTick < this.lastFailure + 40) { ++ if (this.pathfindFailures > 10 && this.path == null && this.tick < this.lastFailure + 40) { // Folia - region threading + return false; + } + // Paper end - Perf: Optimise pathfinding +@@ -233,7 +_,7 @@ + return true; + } else { + this.pathfindFailures++; +- this.lastFailure = net.minecraft.server.MinecraftServer.currentTick; ++ this.lastFailure = this.tick; // Folia - region threading + return false; + } + // Paper end - Perf: Optimise pathfinding diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/PlayerSensor.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/PlayerSensor.java.patch new file mode 100644 index 0000000..09962a2 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/PlayerSensor.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/ai/sensing/PlayerSensor.java ++++ b/net/minecraft/world/entity/ai/sensing/PlayerSensor.java +@@ -22,7 +_,7 @@ + + @Override + protected void doTick(ServerLevel level, LivingEntity entity) { +- List list = level.players() ++ List list = level.getLocalPlayers() // Folia - region threading + .stream() + .filter(EntitySelector.NO_SPECTATORS) + .filter(serverPlayer -> entity.closerThan(serverPlayer, this.getFollowDistance(entity))) diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch new file mode 100644 index 0000000..8058123 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/ai/sensing/TemptingSensor.java ++++ b/net/minecraft/world/entity/ai/sensing/TemptingSensor.java +@@ -36,7 +_,7 @@ + protected void doTick(ServerLevel level, PathfinderMob entity) { + Brain brain = entity.getBrain(); + TargetingConditions targetingConditions = TEMPT_TARGETING.copy().range((float)entity.getAttributeValue(Attributes.TEMPT_RANGE)); +- List list = level.players() ++ List list = level.getLocalPlayers() // Folia - region threading + .stream() + .filter(EntitySelector.NO_SPECTATORS) + .filter(serverPlayer -> targetingConditions.test(level, entity, serverPlayer)) diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/VillageSiege.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/VillageSiege.java.patch new file mode 100644 index 0000000..98576b1 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/VillageSiege.java.patch @@ -0,0 +1,134 @@ +--- a/net/minecraft/world/entity/ai/village/VillageSiege.java ++++ b/net/minecraft/world/entity/ai/village/VillageSiege.java +@@ -18,68 +_,72 @@ + + public class VillageSiege implements CustomSpawner { + private static final Logger LOGGER = LogUtils.getLogger(); +- private boolean hasSetupSiege; +- private VillageSiege.State siegeState = VillageSiege.State.SIEGE_DONE; +- private int zombiesToSpawn; +- private int nextSpawnTime; +- private int spawnX; +- private int spawnY; +- private int spawnZ; ++ // Folia - region threading + + @Override + public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ++ // Folia start - region threading ++ // check if the spawn pos is no longer owned by this region ++ if (worldData.villageSiegeState.siegeState != State.SIEGE_DONE ++ && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, worldData.villageSiegeState.spawnX >> 4, worldData.villageSiegeState.spawnZ >> 4, 8)) { ++ // can't spawn here, just re-set ++ worldData.villageSiegeState = new io.papermc.paper.threadedregions.RegionizedWorldData.VillageSiegeState(); ++ } ++ // Folia end - region threading + if (!level.isDay() && spawnHostiles) { + float timeOfDay = level.getTimeOfDay(0.0F); + if (timeOfDay == 0.5) { +- this.siegeState = level.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; ++ worldData.villageSiegeState.siegeState = level.random.nextInt(10) == 0 ? VillageSiege.State.SIEGE_TONIGHT : VillageSiege.State.SIEGE_DONE; // Folia - region threading + } + +- if (this.siegeState == VillageSiege.State.SIEGE_DONE) { ++ if (worldData.villageSiegeState.siegeState == VillageSiege.State.SIEGE_DONE) { // Folia - region threading + return 0; + } else { +- if (!this.hasSetupSiege) { ++ if (!worldData.villageSiegeState.hasSetupSiege) { // Folia - region threading + if (!this.tryToSetupSiege(level)) { + return 0; + } + +- this.hasSetupSiege = true; ++ worldData.villageSiegeState.hasSetupSiege = true; // Folia - region threading + } + +- if (this.nextSpawnTime > 0) { +- this.nextSpawnTime--; ++ if (worldData.villageSiegeState.nextSpawnTime > 0) { // Folia - region threading ++ worldData.villageSiegeState.nextSpawnTime--; // Folia - region threading + return 0; + } else { +- this.nextSpawnTime = 2; +- if (this.zombiesToSpawn > 0) { ++ worldData.villageSiegeState.nextSpawnTime = 2; // Folia - region threading ++ if (worldData.villageSiegeState.zombiesToSpawn > 0) { // Folia - region threading + this.trySpawn(level); +- this.zombiesToSpawn--; ++ worldData.villageSiegeState.zombiesToSpawn--; // Folia - region threading + } else { +- this.siegeState = VillageSiege.State.SIEGE_DONE; ++ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading + } + + return 1; + } + } + } else { +- this.siegeState = VillageSiege.State.SIEGE_DONE; +- this.hasSetupSiege = false; ++ worldData.villageSiegeState.siegeState = VillageSiege.State.SIEGE_DONE; // Folia - region threading ++ worldData.villageSiegeState.hasSetupSiege = false; // Folia - region threading + return 0; + } + } + + private boolean tryToSetupSiege(ServerLevel level) { +- for (Player player : level.players()) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ++ for (Player player : level.getLocalPlayers()) { // Folia - region threading + if (!player.isSpectator()) { + BlockPos blockPos = player.blockPosition(); + if (level.isVillage(blockPos) && !level.getBiome(blockPos).is(BiomeTags.WITHOUT_ZOMBIE_SIEGES)) { + for (int i = 0; i < 10; i++) { + float f = level.random.nextFloat() * (float) (Math.PI * 2); +- this.spawnX = blockPos.getX() + Mth.floor(Mth.cos(f) * 32.0F); +- this.spawnY = blockPos.getY(); +- this.spawnZ = blockPos.getZ() + Mth.floor(Mth.sin(f) * 32.0F); +- if (this.findRandomSpawnPos(level, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)) != null) { +- this.nextSpawnTime = 0; +- this.zombiesToSpawn = 20; ++ worldData.villageSiegeState.spawnX = blockPos.getX() + Mth.floor(Mth.cos(f) * 32.0F); // Folia - region threading ++ worldData.villageSiegeState.spawnY = blockPos.getY(); // Folia - region threading ++ worldData.villageSiegeState.spawnZ = blockPos.getZ() + Mth.floor(Mth.sin(f) * 32.0F); // Folia - region threading ++ if (this.findRandomSpawnPos(level, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)) != null) { // Folia - region threading ++ worldData.villageSiegeState.nextSpawnTime = 0; // Folia - region threading ++ worldData.villageSiegeState.zombiesToSpawn = 20; // Folia - region threading + break; + } + } +@@ -93,11 +_,13 @@ + } + + private void trySpawn(ServerLevel level) { +- Vec3 vec3 = this.findRandomSpawnPos(level, new BlockPos(this.spawnX, this.spawnY, this.spawnZ)); ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ++ Vec3 vec3 = this.findRandomSpawnPos(level, new BlockPos(worldData.villageSiegeState.spawnX, worldData.villageSiegeState.spawnY, worldData.villageSiegeState.spawnZ)); // Folia - region threading + if (vec3 != null) { + Zombie zombie; + try { + zombie = new Zombie(level); ++ zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up + zombie.finalizeSpawn(level, level.getCurrentDifficultyAt(zombie.blockPosition()), EntitySpawnReason.EVENT, null); + } catch (Exception var5) { + LOGGER.warn("Failed to create zombie for village siege at {}", vec3, var5); +@@ -105,7 +_,7 @@ + return; + } + +- zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); ++ //zombie.moveTo(vec3.x, vec3.y, vec3.z, level.random.nextFloat() * 360.0F, 0.0F); // Folia - region threading - move up + level.addFreshEntityWithPassengers(zombie, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.VILLAGE_INVASION); // CraftBukkit + } + } +@@ -125,7 +_,7 @@ + return null; + } + +- static enum State { ++ public static enum State { // Folia - region threading + SIEGE_CAN_ACTIVATE, + SIEGE_TONIGHT, + SIEGE_DONE; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/poi/PoiManager.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/poi/PoiManager.java.patch new file mode 100644 index 0000000..eb00cef --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/ai/village/poi/PoiManager.java.patch @@ -0,0 +1,39 @@ +--- a/net/minecraft/world/entity/ai/village/poi/PoiManager.java ++++ b/net/minecraft/world/entity/ai/village/poi/PoiManager.java +@@ -58,11 +_,13 @@ + } + + private void updateDistanceTracking(long section) { ++ synchronized (this.villageDistanceTracker) { // Folia - region threading + if (this.isVillageCenter(section)) { + this.villageDistanceTracker.setSource(section, POI_DATA_SOURCE); + } else { + this.villageDistanceTracker.removeSource(section); + } ++ } // Folia - region threading + } + + @Override +@@ -347,10 +_,12 @@ + } + + public int sectionsToVillage(SectionPos sectionPos) { ++ synchronized (this.villageDistanceTracker) { // Folia - region threading + // Paper start - rewrite chunk system + this.villageDistanceTracker.propagateUpdates(); + return convertBetweenLevels(this.villageDistanceTracker.getLevel(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(sectionPos))); + // Paper end - rewrite chunk system ++ } // Folia - region threading + } + + boolean isVillageCenter(long chunkPos) { +@@ -364,7 +_,9 @@ + + @Override + public void tick(BooleanSupplier aheadOfTime) { ++ synchronized (this.villageDistanceTracker) { // Folia - region threading + this.villageDistanceTracker.propagateUpdates(); // Paper - rewrite chunk system ++ } // Folia - region threading + } + + @Override diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Bee.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Bee.java.patch new file mode 100644 index 0000000..b1317e2 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Bee.java.patch @@ -0,0 +1,26 @@ +--- a/net/minecraft/world/entity/animal/Bee.java ++++ b/net/minecraft/world/entity/animal/Bee.java +@@ -815,6 +_,11 @@ + + @Override + public boolean canBeeUse() { ++ // Folia start - region threading ++ if (Bee.this.hivePos != null && Bee.this.isTooFarAway(Bee.this.hivePos)) { ++ Bee.this.hivePos = null; ++ } ++ // Folia end - region threading + return Bee.this.hivePos != null + && !Bee.this.isTooFarAway(Bee.this.hivePos) + && !Bee.this.hasRestriction() +@@ -925,6 +_,11 @@ + + @Override + public boolean canBeeUse() { ++ // Folia start - region threading ++ if (Bee.this.savedFlowerPos != null && Bee.this.isTooFarAway(Bee.this.savedFlowerPos)) { ++ Bee.this.savedFlowerPos = null; ++ } ++ // Folia end - region threading + return Bee.this.savedFlowerPos != null + && !Bee.this.hasRestriction() + && this.wantsToGoToKnownFlower() diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Cat.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Cat.java.patch new file mode 100644 index 0000000..c271e63 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/animal/Cat.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/animal/Cat.java ++++ b/net/minecraft/world/entity/animal/Cat.java +@@ -342,7 +_,7 @@ + TagKey tagKey = flag ? CatVariantTags.FULL_MOON_SPAWNS : CatVariantTags.DEFAULT_SPAWNS; + BuiltInRegistries.CAT_VARIANT.getRandomElementOf(tagKey, level.getRandom()).ifPresent(this::setVariant); + ServerLevel level1 = level.getLevel(); +- if (level1.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK, level).isValid()) { // Paper - Fix swamp hut cat generation deadlock ++ if (level.structureManager().getStructureWithPieceAt(this.blockPosition(), StructureTags.CATS_SPAWN_AS_BLACK).isValid()) { // Paper - Fix swamp hut cat generation deadlock // Folia - region threading - properly fix this + this.setVariant(BuiltInRegistries.CAT_VARIANT.getOrThrow(CatVariant.ALL_BLACK)); + this.setPersistenceRequired(); + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch new file mode 100644 index 0000000..b392695 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java ++++ b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java +@@ -53,7 +_,7 @@ + public void tick() { + this.time++; + this.applyEffectsFromBlocks(); +- this.handlePortal(); ++ //this.handlePortal(); // Folia - region threading + if (this.level() instanceof ServerLevel) { + BlockPos blockPos = this.blockPosition(); + if (((ServerLevel)this.level()).getDragonFight() != null && this.level().getBlockState(blockPos).isAir()) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/decoration/ItemFrame.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/decoration/ItemFrame.java.patch new file mode 100644 index 0000000..bb3cd5a --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/decoration/ItemFrame.java.patch @@ -0,0 +1,12 @@ +--- a/net/minecraft/world/entity/decoration/ItemFrame.java ++++ b/net/minecraft/world/entity/decoration/ItemFrame.java +@@ -242,7 +_,9 @@ + if (framedMapId != null) { + MapItemSavedData savedData = MapItem.getSavedData(framedMapId, this.level()); + if (savedData != null) { ++ synchronized (savedData) { // Folia - make map data thread-safe + savedData.removedFromFrame(this.pos, this.getId()); ++ } // Folia - make map data thread-safe + } + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/FallingBlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/FallingBlockEntity.java.patch new file mode 100644 index 0000000..f193a8f --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/FallingBlockEntity.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -162,7 +_,7 @@ + return; + } + // Paper end - Configurable falling blocks height nerf +- this.handlePortal(); ++ //this.handlePortal(); // Folia - region threading + if (this.level() instanceof ServerLevel serverLevel && (this.isAlive() || this.forceTickAfterTeleportToDuplicate)) { + BlockPos blockPos = this.blockPosition(); + boolean flag = this.blockState.getBlock() instanceof ConcretePowderBlock; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/ItemEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/ItemEntity.java.patch new file mode 100644 index 0000000..bcbafd0 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/ItemEntity.java.patch @@ -0,0 +1,27 @@ +--- a/net/minecraft/world/entity/item/ItemEntity.java ++++ b/net/minecraft/world/entity/item/ItemEntity.java +@@ -521,13 +_,21 @@ + return false; + } + ++ // Folia start - region threading ++ @Override ++ public void postChangeDimension() { ++ super.postChangeDimension(); ++ if (!this.level().isClientSide) { ++ this.mergeWithNeighbours(); ++ } ++ } ++ // Folia end - region threading ++ + @Nullable + @Override + public Entity teleport(TeleportTransition teleportTransition) { + Entity entity = super.teleport(teleportTransition); +- if (!this.level().isClientSide && entity instanceof ItemEntity itemEntity) { +- itemEntity.mergeWithNeighbours(); +- } ++ if (entity != null) entity.postChangeDimension(); // Folia - region threading - move to post change + + return entity; + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/PrimedTnt.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/PrimedTnt.java.patch new file mode 100644 index 0000000..8e5206c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/item/PrimedTnt.java.patch @@ -0,0 +1,22 @@ +--- a/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/net/minecraft/world/entity/item/PrimedTnt.java +@@ -98,8 +_,8 @@ + + @Override + public void tick() { +- if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot +- this.handlePortal(); ++ if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().getCurrentWorldData().currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot // Folia - region threading ++ //this.handlePortal(); // Folia - region threading + this.applyGravity(); + this.move(MoverType.SELF, this.getDeltaMovement()); + this.applyEffectsFromBlocks(); +@@ -137,7 +_,7 @@ + */ + // Send position and velocity updates to nearby players on every tick while the TNT is in water. + // This does pretty well at keeping their clients in sync with the server. +- net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel) this.level()).getChunkSource().chunkMap.entityMap.get(this.getId()); ++ net.minecraft.server.level.ChunkMap.TrackedEntity ete = this.moonrise$getTrackedEntity(); // Folia - region threading + if (ete != null) { + net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this); + net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket.teleport(this.getId(), net.minecraft.world.entity.PositionMoveRotation.of(this), java.util.Set.of(), this.onGround); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/Vex.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/Vex.java.patch new file mode 100644 index 0000000..94ff457 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/Vex.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/monster/Vex.java ++++ b/net/minecraft/world/entity/monster/Vex.java +@@ -349,7 +_,7 @@ + @Override + public void tick() { + BlockPos boundOrigin = Vex.this.getBoundOrigin(); +- if (boundOrigin == null) { ++ if (boundOrigin == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)Vex.this.level(), boundOrigin)) { // Folia - region threading + boundOrigin = Vex.this.blockPosition(); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/ZombieVillager.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/ZombieVillager.java.patch new file mode 100644 index 0000000..c5f8ba6 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/monster/ZombieVillager.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/world/entity/monster/ZombieVillager.java ++++ b/net/minecraft/world/entity/monster/ZombieVillager.java +@@ -69,7 +_,7 @@ + @Nullable + private MerchantOffers tradeOffers; + private int villagerXp; +- private int lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - add field ++ //private int lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit - add field // Folia - region threading - restore original timers + + public ZombieVillager(EntityType entityType, Level level) { + super(entityType, level); +@@ -149,7 +_,7 @@ + } + + super.tick(); +- this.lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit ++ //this.lastTick = net.minecraft.server.MinecraftServer.currentTick; // CraftBukkit // Folia - region threading - restore original timers + } + + @Override diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/AbstractVillager.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/AbstractVillager.java.patch new file mode 100644 index 0000000..ad8697c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/AbstractVillager.java.patch @@ -0,0 +1,22 @@ +--- a/net/minecraft/world/entity/npc/AbstractVillager.java ++++ b/net/minecraft/world/entity/npc/AbstractVillager.java +@@ -218,10 +_,18 @@ + this.readInventoryFromTag(compound, this.registryAccess()); + } + ++ // Folia start - region threading ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ this.stopTrading(); ++ } ++ // Folia end - region threading ++ + @Nullable + @Override + public Entity teleport(TeleportTransition teleportTransition) { +- this.stopTrading(); ++ this.preChangeDimension(); // Folia - region threading - move into preChangeDimension + return super.teleport(teleportTransition); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/CatSpawner.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/CatSpawner.java.patch new file mode 100644 index 0000000..6d84983 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/CatSpawner.java.patch @@ -0,0 +1,26 @@ +--- a/net/minecraft/world/entity/npc/CatSpawner.java ++++ b/net/minecraft/world/entity/npc/CatSpawner.java +@@ -18,17 +_,18 @@ + + public class CatSpawner implements CustomSpawner { + private static final int TICK_DELAY = 1200; +- private int nextTick; ++ //private int nextTick; // Folia - region threading + + @Override + public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) { + if (spawnPassives && level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { +- this.nextTick--; +- if (this.nextTick > 0) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ++ worldData.catSpawnerNextTick--; // Folia - region threading ++ if (worldData.catSpawnerNextTick > 0) { // Folia - region threading + return 0; + } else { +- this.nextTick = 1200; +- Player randomPlayer = level.getRandomPlayer(); ++ worldData.catSpawnerNextTick = 1200; // Folia - region threading ++ Player randomPlayer = level.getRandomLocalPlayer(); // Folia - region threading + if (randomPlayer == null) { + return 0; + } else { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/Villager.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/Villager.java.patch new file mode 100644 index 0000000..a0ac6d7 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/Villager.java.patch @@ -0,0 +1,28 @@ +--- a/net/minecraft/world/entity/npc/Villager.java ++++ b/net/minecraft/world/entity/npc/Villager.java +@@ -246,7 +_,7 @@ + villagerBrain.setCoreActivities(ImmutableSet.of(Activity.CORE)); + villagerBrain.setDefaultActivity(Activity.IDLE); + villagerBrain.setActiveActivityIfPossible(Activity.IDLE); +- villagerBrain.updateActivityFromSchedule(this.level().getDayTime(), this.level().getGameTime()); ++ villagerBrain.updateActivityFromSchedule(this.level().getLevelData().getDayTime(), this.level().getLevelData().getGameTime()); // Folia - region threading - not in the world yet + } + + @Override +@@ -693,6 +_,8 @@ + this.brain.getMemory(moduleType).ifPresent(globalPos -> { + ServerLevel level = server.getLevel(globalPos.dimension()); + if (level != null) { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( // Folia - region threading ++ level, globalPos.pos().getX() >> 4, globalPos.pos().getZ() >> 4, () -> { // Folia - region threading + PoiManager poiManager = level.getPoiManager(); + Optional> type = poiManager.getType(globalPos.pos()); + BiPredicate> biPredicate = POI_MEMORIES.get(moduleType); +@@ -700,6 +_,7 @@ + poiManager.release(globalPos.pos()); + DebugPackets.sendPoiTicketCountPacket(level, globalPos.pos()); + } ++ }); // Folia - region threading + } + }); + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch new file mode 100644 index 0000000..498eb81 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch @@ -0,0 +1,90 @@ +--- a/net/minecraft/world/entity/npc/WanderingTraderSpawner.java ++++ b/net/minecraft/world/entity/npc/WanderingTraderSpawner.java +@@ -30,16 +_,14 @@ + private static final int SPAWN_CHANCE_INCREASE = 25; + private static final int SPAWN_ONE_IN_X_CHANCE = 10; + private static final int NUMBER_OF_SPAWN_ATTEMPTS = 10; +- private final RandomSource random = RandomSource.create(); ++ private final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Folia - region threading + private final ServerLevelData serverLevelData; +- private int tickDelay; +- private int spawnDelay; +- private int spawnChance; ++ // Folia - region threading + + public WanderingTraderSpawner(ServerLevelData serverLevelData) { + this.serverLevelData = serverLevelData; + // Paper start - Add Wandering Trader spawn rate config options +- this.tickDelay = Integer.MIN_VALUE; ++ //this.tickDelay = Integer.MIN_VALUE; // Folia - region threading - moved to regionisedworlddata + // this.spawnDelay = serverLevelData.getWanderingTraderSpawnDelay(); + // this.spawnChance = serverLevelData.getWanderingTraderSpawnChance(); + // if (this.spawnDelay == 0 && this.spawnChance == 0) { +@@ -53,35 +_,36 @@ + + @Override + public int tick(ServerLevel level, boolean spawnHostiles, boolean spawnPassives) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + // Paper start - Add Wandering Trader spawn rate config options +- if (this.tickDelay == Integer.MIN_VALUE) { +- this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; +- this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; +- this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; ++ if (worldData.wanderingTraderTickDelay == Integer.MIN_VALUE) { // Folia - region threading ++ worldData.wanderingTraderTickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading ++ worldData.wanderingTraderSpawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading ++ worldData.wanderingTraderSpawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading + } + if (!level.getGameRules().getBoolean(GameRules.RULE_DO_TRADER_SPAWNING)) { + return 0; +- } else if (--this.tickDelay - 1 > 0) { +- this.tickDelay = this.tickDelay - 1; ++ } else if (--worldData.wanderingTraderTickDelay - 1 > 0) { // Folia - region threading ++ worldData.wanderingTraderTickDelay = worldData.wanderingTraderTickDelay - 1; // Folia - region threading + return 0; + } else { +- this.tickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; +- this.spawnDelay = this.spawnDelay - level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; ++ worldData.wanderingTraderTickDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading ++ worldData.wanderingTraderSpawnDelay = worldData.wanderingTraderSpawnDelay - level.paperConfig().entities.spawning.wanderingTrader.spawnMinuteLength; // Folia - region threading + //this.serverLevelData.setWanderingTraderSpawnDelay(this.spawnDelay); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways +- if (this.spawnDelay > 0) { ++ if (worldData.wanderingTraderSpawnDelay > 0) { // Folia - region threading + return 0; + } else { +- this.spawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; ++ worldData.wanderingTraderSpawnDelay = level.paperConfig().entities.spawning.wanderingTrader.spawnDayLength; // Folia - region threading + if (!level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { + return 0; + } else { +- int i = this.spawnChance; +- this.spawnChance = Mth.clamp(this.spawnChance + level.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); ++ int i = worldData.wanderingTraderSpawnChance; // Folia - region threading ++ worldData.wanderingTraderSpawnChance = Mth.clamp(worldData.wanderingTraderSpawnChance + level.paperConfig().entities.spawning.wanderingTrader.spawnChanceFailureIncrement, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin, level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMax); // Folia - region threading + //this.serverLevelData.setWanderingTraderSpawnChance(this.spawnChance); // Paper - We don't need to save this value to disk if it gets set back to a hardcoded value anyways + if (this.random.nextInt(100) > i) { + return 0; + } else if (this.spawn(level)) { +- this.spawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; ++ worldData.wanderingTraderSpawnChance = level.paperConfig().entities.spawning.wanderingTrader.spawnChanceMin; // Folia - region threading + // Paper end - Add Wandering Trader spawn rate config options + return 1; + } else { +@@ -93,7 +_,7 @@ + } + + private boolean spawn(ServerLevel serverLevel) { +- Player randomPlayer = serverLevel.getRandomPlayer(); ++ Player randomPlayer = serverLevel.getRandomLocalPlayer(); // Folia - region threading + if (randomPlayer == null) { + return true; + } else if (this.random.nextInt(10) != 0) { +@@ -116,7 +_,7 @@ + this.tryToSpawnLlamaFor(serverLevel, wanderingTrader, 4); + } + +- this.serverLevelData.setWanderingTraderId(wanderingTrader.getUUID()); ++ //this.serverLevelData.setWanderingTraderId(wanderingTrader.getUUID()); // Folia - region threading - doesn't appear to be used anywhere, so avoid the race condition here... + // wanderingTrader.setDespawnDelay(48000); // Paper - moved above, modifiable by plugins on CreatureSpawnEvent + wanderingTrader.setWanderTarget(blockPos1); + wanderingTrader.restrictTo(blockPos1, 16); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch new file mode 100644 index 0000000..12553ae --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch @@ -0,0 +1,17 @@ +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -1504,6 +_,14 @@ + } + } + ++ // Folia start - region threading ++ @Override ++ protected void preRemove(RemovalReason reason) { ++ super.preRemove(reason); ++ this.fishing = null; ++ } ++ // Folia end - region threading ++ + public boolean isLocalPlayer() { + return false; + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractArrow.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractArrow.java.patch new file mode 100644 index 0000000..6ec6a75 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractArrow.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/entity/projectile/AbstractArrow.java ++++ b/net/minecraft/world/entity/projectile/AbstractArrow.java +@@ -176,6 +_,11 @@ + + @Override + public void tick() { ++ // Folia start - region threading - make sure entities do not move into regions they do not own ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { ++ return; ++ } ++ // Folia end - region threading - make sure entities do not move into regions they do not own + boolean flag = !this.isNoPhysics(); + Vec3 deltaMovement = this.getDeltaMovement(); + BlockPos blockPos = this.blockPosition(); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch new file mode 100644 index 0000000..bf3ab2b --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java ++++ b/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java +@@ -80,6 +_,11 @@ + this.setPos(location); + this.applyEffectsFromBlocks(); + super.tick(); ++ // Folia start - region threading - make sure entities do not move into regions they do not own ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { ++ return; ++ } ++ // Folia end - region threading - make sure entities do not move into regions they do not own + if (this.shouldBurn()) { + this.igniteForSeconds(1.0F); + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch new file mode 100644 index 0000000..e46ad4b --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/entity/projectile/FireworkRocketEntity.java ++++ b/net/minecraft/world/entity/projectile/FireworkRocketEntity.java +@@ -130,6 +_,11 @@ + } + }); + } ++ // Folia start - region threading ++ if (this.attachedToEntity != null && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.attachedToEntity)) { ++ this.attachedToEntity = null; ++ } ++ // Folia end - region threading + + if (this.attachedToEntity != null) { + Vec3 handHoldingItemAngle; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FishingHook.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FishingHook.java.patch new file mode 100644 index 0000000..3baf552 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/FishingHook.java.patch @@ -0,0 +1,50 @@ +--- a/net/minecraft/world/entity/projectile/FishingHook.java ++++ b/net/minecraft/world/entity/projectile/FishingHook.java +@@ -94,7 +_,7 @@ + + public FishingHook(Player player, Level level, int luck, int lureSpeed) { + this(EntityType.FISHING_BOBBER, level, luck, lureSpeed); +- this.setOwner(player); ++ //this.setOwner(player); // Folia - region threading - move this down after position so that thread-checks do not fail + float xRot = player.getXRot(); + float yRot = player.getYRot(); + float cos = Mth.cos(-yRot * (float) (Math.PI / 180.0) - (float) Math.PI); +@@ -105,6 +_,7 @@ + double eyeY = player.getEyeY(); + double d1 = player.getZ() - cos * 0.3; + this.moveTo(d, eyeY, d1, yRot, xRot); ++ this.setOwner(player); // Folia - region threading - move this down after position so that thread-checks do not fail + Vec3 vec3 = new Vec3(-sin, Mth.clamp(-(sin1 / f), -5.0F, 5.0F), -cos); + double len = vec3.length(); + vec3 = vec3.multiply( +@@ -260,6 +_,11 @@ + } + + private boolean shouldStopFishing(Player player) { ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player)) { ++ return true; ++ } ++ // Folia end - region threading + ItemStack mainHandItem = player.getMainHandItem(); + ItemStack offhandItem = player.getOffhandItem(); + boolean isFishingRod = mainHandItem.is(Items.FISHING_ROD); +@@ -623,9 +_,17 @@ + @Override + public void remove(Entity.RemovalReason reason, org.bukkit.event.entity.EntityRemoveEvent.Cause cause) { + // CraftBukkit end +- this.updateOwnerInfo(null); ++ //this.updateOwnerInfo(null); // Folia - region threading - move into preRemove + super.remove(reason, cause); // CraftBukkit - add Bukkit remove cause + } ++ ++ // Folia start - region threading ++ @Override ++ protected void preRemove(RemovalReason reason) { ++ super.preRemove(reason); ++ this.updateOwnerInfo(null); ++ } ++ // Folia end - region threading + + @Override + public void onClientRemoval() { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/LlamaSpit.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/LlamaSpit.java.patch new file mode 100644 index 0000000..738b678 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/LlamaSpit.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/entity/projectile/LlamaSpit.java ++++ b/net/minecraft/world/entity/projectile/LlamaSpit.java +@@ -41,6 +_,11 @@ + @Override + public void tick() { + super.tick(); ++ // Folia start - region threading - make sure entities do not move into regions they do not own ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { ++ return; ++ } ++ // Folia end - region threading - make sure entities do not move into regions they do not own + Vec3 deltaMovement = this.getDeltaMovement(); + HitResult hitResultOnMoveVector = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity); + this.preHitTargetOrDeflectSelf(hitResultOnMoveVector); // CraftBukkit - projectile hit event diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/Projectile.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/Projectile.java.patch new file mode 100644 index 0000000..bb47f36 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/Projectile.java.patch @@ -0,0 +1,87 @@ +--- a/net/minecraft/world/entity/projectile/Projectile.java ++++ b/net/minecraft/world/entity/projectile/Projectile.java +@@ -38,7 +_,7 @@ + @Nullable + public UUID ownerUUID; + @Nullable +- public Entity cachedOwner; ++ public org.bukkit.craftbukkit.entity.CraftEntity cachedOwner; // Folia - region threading - replace with CraftEntity + public boolean leftOwner; + public boolean hasBeenShot; + @Nullable +@@ -52,7 +_,7 @@ + public void setOwner(@Nullable Entity owner) { + if (owner != null) { + this.ownerUUID = owner.getUUID(); +- this.cachedOwner = owner; ++ this.cachedOwner = owner.getBukkitEntity(); // Folia - region threading + } + // Paper start - Refresh ProjectileSource for projectiles + else { +@@ -69,22 +_,38 @@ + if (fillCache) { + this.getOwner(); + } +- if (this.cachedOwner != null && !this.cachedOwner.isRemoved() && this.projectileSource == null && this.cachedOwner.getBukkitEntity() instanceof org.bukkit.projectiles.ProjectileSource projSource) { ++ if (this.cachedOwner != null && !this.cachedOwner.getHandleRaw().isRemoved() && this.projectileSource == null && this.cachedOwner instanceof org.bukkit.projectiles.ProjectileSource projSource) { // Folia - region threading + this.projectileSource = projSource; + } + } + // Paper end - Refresh ProjectileSource for projectiles + ++ // Folia start - region threading ++ // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner. ++ // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior. + @Nullable + @Override + public Entity getOwner() { +- if (this.cachedOwner != null && !this.cachedOwner.isRemoved()) { ++ Entity ret = this.getOwnerRaw(); ++ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(ret) && (ret == null || !ret.isRemoved()) ? ret : null; ++ } ++ // Folia end - region threading ++ ++ @Nullable ++ public Entity getOwnerRaw() { // Folia - region threading ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot update owner state asynchronously"); // Folia - region threading ++ if (this.cachedOwner != null && !this.cachedOwner.isPurged()) { // Folia - region threading + this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles +- return this.cachedOwner; ++ return this.cachedOwner.getHandleRaw(); // Folia - region threading + } else if (this.ownerUUID != null) { +- this.cachedOwner = this.findOwner(this.ownerUUID); ++ // Folia start - region threading ++ Entity ret = this.findOwner(this.ownerUUID); ++ if (ret != null) { ++ this.cachedOwner = ret.getBukkitEntity(); ++ } ++ // Folia end - region threading + this.refreshProjectileSource(false); // Paper - Refresh ProjectileSource for projectiles +- return this.cachedOwner; ++ return ret; // Folia - region threading + } else { + return null; + } +@@ -130,7 +_,12 @@ + protected void setOwnerThroughUUID(UUID uuid) { + if (this.ownerUUID != uuid) { + this.ownerUUID = uuid; +- this.cachedOwner = this.findOwner(uuid); ++ // Folia start - region threading ++ Entity cachedOwner = this.findOwner(this.ownerUUID); ++ if (cachedOwner != null) { ++ this.cachedOwner = cachedOwner.getBukkitEntity(); ++ } ++ // Folia end - region threading + } + } + +@@ -451,7 +_,7 @@ + @Override + public boolean mayInteract(ServerLevel level, BlockPos pos) { + Entity owner = this.getOwner(); +- return owner instanceof Player ? owner.mayInteract(level, pos) : owner == null || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); ++ return owner instanceof Player && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(owner) ? owner.mayInteract(level, pos) : owner == null || level.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); // Folia - region threading + } + + public boolean mayBreak(ServerLevel level) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/SmallFireball.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/SmallFireball.java.patch new file mode 100644 index 0000000..ac75249 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/SmallFireball.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/projectile/SmallFireball.java ++++ b/net/minecraft/world/entity/projectile/SmallFireball.java +@@ -24,7 +_,7 @@ + public SmallFireball(Level level, LivingEntity owner, Vec3 movement) { + super(EntityType.SMALL_FIREBALL, owner, movement, level); + // CraftBukkit start +- if (this.getOwner() != null && this.getOwner() instanceof Mob) { ++ if (owner != null && this.getOwner() != null && this.getOwner() instanceof Mob) { // Folia - region threading + this.isIncendiary = (level instanceof ServerLevel serverLevel) && serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING); + } + // CraftBukkit end diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch new file mode 100644 index 0000000..cf0996c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/entity/projectile/ThrowableProjectile.java ++++ b/net/minecraft/world/entity/projectile/ThrowableProjectile.java +@@ -43,6 +_,11 @@ + + @Override + public void tick() { ++ // Folia start - region threading - make sure entities do not move into regions they do not own ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor((net.minecraft.server.level.ServerLevel)this.level(), this.position(), this.getDeltaMovement(), 1)) { ++ return; ++ } ++ // Folia end - region threading - make sure entities do not move into regions they do not own + this.handleFirstTickBubbleColumn(); + this.applyGravity(); + this.applyInertia(); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch new file mode 100644 index 0000000..4ece4e7 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch @@ -0,0 +1,122 @@ +--- a/net/minecraft/world/entity/projectile/ThrownEnderpearl.java ++++ b/net/minecraft/world/entity/projectile/ThrownEnderpearl.java +@@ -99,6 +_,81 @@ + result.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F); + } + ++ // Folia start - region threading ++ private static void attemptTeleport(Entity source, ServerLevel checkWorld, net.minecraft.world.phys.Vec3 to) { ++ final boolean onPortalCooldown = source.isOnPortalCooldown(); ++ // ignore retired callback, in those cases we do not want to teleport ++ source.getBukkitEntity().taskScheduler.schedule( ++ (Entity entity) -> { ++ if (!isAllowedToTeleportOwner(entity, checkWorld)) { ++ return; ++ } ++ // source is now an invalid reference, do not use it, use the entity parameter ++ net.minecraft.world.phys.Vec3 endermitePos = entity.position(); ++ ++ // dismount from any vehicles, so we can teleport and to prevent desync ++ if (entity.isPassenger()) { ++ entity.unRide(); ++ } ++ ++ if (onPortalCooldown) { ++ entity.setPortalCooldown(); ++ } ++ ++ entity.teleportAsync( ++ checkWorld, to, null, null, null, ++ org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.ENDER_PEARL, ++ // chunk could have been unloaded ++ Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS | Entity.TELEPORT_FLAG_LOAD_CHUNK, ++ (Entity teleported) -> { ++ // entity is now an invalid reference, do not use it, instead use teleported ++ if (teleported instanceof ServerPlayer player) { ++ // connection teleport is already done ++ ServerLevel world = player.serverLevel(); ++ ++ // endermite spawn chance ++ if (world.random.nextFloat() < 0.05F && world.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING)) { ++ Endermite entityendermite = (Endermite) EntityType.ENDERMITE.create(world, EntitySpawnReason.TRIGGERED); ++ ++ if (entityendermite != null) { ++ float yRot = teleported.getYRot(); ++ float xRot = teleported.getXRot(); ++ Runnable spawn = () -> { ++ entityendermite.moveTo(endermitePos.x, endermitePos.y, endermitePos.z, yRot, xRot); ++ world.addFreshEntity(entityendermite, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.ENDER_PEARL); ++ }; ++ ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, endermitePos, net.minecraft.world.phys.Vec3.ZERO, 1)) { ++ spawn.run(); ++ } else { ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.x), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkCoordinate(endermitePos.z), ++ spawn ++ ); ++ } ++ } ++ } ++ ++ // damage player ++ teleported.resetFallDistance(); ++ player.resetCurrentImpulseContext(); ++ player.hurtServer(player.serverLevel(), player.damageSources().enderPearl().customEventDamager(player), 5.0F); // CraftBukkit // Paper - fix DamageSource API ++ playSound(teleported.level(), to); ++ } else { ++ // reset fall damage so that if the entity was falling they do not instantly die ++ teleported.resetFallDistance(); ++ playSound(teleported.level(), to); ++ } ++ } ++ ); ++ }, ++ null, 1L ++ ); ++ } ++ // Folia end - region threading ++ + @Override + protected void onHit(HitResult result) { + super.onHit(result); +@@ -117,6 +_,20 @@ + } + + if (this.level() instanceof ServerLevel serverLevel && !this.isRemoved()) { ++ // Folia start - region threading ++ if (true) { ++ // we can't fire events, because we do not actually know where the other entity is located ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this)) { ++ throw new IllegalStateException("Must be on tick thread for ticking entity: " + this); ++ } ++ Entity entity = this.getOwnerRaw(); ++ if (entity != null) { ++ attemptTeleport(entity, (ServerLevel)this.level(), this.position()); ++ } ++ this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.HIT); ++ return; ++ } ++ // Folia end - region threading + Entity owner = this.getOwner(); + if (owner != null && isAllowedToTeleportOwner(owner, serverLevel)) { + if (owner.isPassenger()) { +@@ -212,7 +_,15 @@ + } + } + +- private void playSound(Level level, Vec3 pos) { ++ // Folia start - region threading ++ @Override ++ public void preChangeDimension() { ++ super.preChangeDimension(); ++ // Don't change the owner here, since the tick logic will consider it anyways. ++ } ++ // Folia end - region threading ++ ++ private static void playSound(Level level, Vec3 pos) { // Folia - region threading - static + level.playSound(null, pos.x, pos.y, pos.z, SoundEvents.PLAYER_TELEPORT, SoundSource.PLAYERS); + } + diff --git a/folia-server/minecraft-patches/features/0009-Fix-off-region-raid-heroes.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raid.java.patch similarity index 58% rename from folia-server/minecraft-patches/features/0009-Fix-off-region-raid-heroes.patch rename to folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raid.java.patch index 0ad98a8..aadaf34 100644 --- a/folia-server/minecraft-patches/features/0009-Fix-off-region-raid-heroes.patch +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raid.java.patch @@ -1,18 +1,29 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: WillQi -Date: Mon, 15 May 2023 23:45:09 -0600 -Subject: [PATCH] Fix off region raid heroes - -This patch aims to solve a potential incorrect thread call when completing a raid. -If a player is a hero of the village but proceeds to leave the region of the -raid before it's completion, it would throw an exception due to not being on the -same region thread anymore. - -diff --git a/net/minecraft/world/entity/raid/Raid.java b/net/minecraft/world/entity/raid/Raid.java -index b7045b2d4665c72d0c4849c711be4e44f7d17ad3..58ea6738dd9a04831197b850850720d5a775a131 100644 --- a/net/minecraft/world/entity/raid/Raid.java +++ b/net/minecraft/world/entity/raid/Raid.java -@@ -391,14 +391,21 @@ public class Raid { +@@ -110,6 +_,13 @@ + public final org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(PDC_TYPE_REGISTRY); + // Paper end + ++ // Folia start - make raids thread-safe ++ public boolean ownsRaid() { ++ BlockPos center = this.getCenter(); ++ return center != null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, center.getX() >> 4, center.getZ() >> 4, 8); ++ } ++ // Folia end - make raids thread-safe ++ + public Raid(int id, ServerLevel level, BlockPos center) { + this.id = id; + this.level = level; +@@ -207,7 +_,7 @@ + private Predicate validPlayer() { + return player -> { + BlockPos blockPos = player.blockPosition(); +- return player.isAlive() && this.level.getRaidAt(blockPos) == this; ++ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) && player.isAlive() && this.level.getRaidAt(blockPos) == this; // Folia - make raids thread-safe + }; + } + +@@ -384,14 +_,21 @@ if (entity instanceof LivingEntity) { LivingEntity livingEntity = (LivingEntity)entity; if (!entity.isSpectator()) { @@ -39,3 +50,12 @@ index b7045b2d4665c72d0c4849c711be4e44f7d17ad3..58ea6738dd9a04831197b850850720d5 } } } +@@ -496,7 +_,7 @@ + Collection players = this.raidEvent.getPlayers(); + long randomLong = this.random.nextLong(); + +- for (ServerPlayer serverPlayer : this.level.players()) { ++ for (ServerPlayer serverPlayer : this.level.getLocalPlayers()) { // Folia - region threading + Vec3 vec3 = serverPlayer.position(); + Vec3 vec31 = Vec3.atCenterOf(pos); + double squareRoot = Math.sqrt((vec31.x - vec3.x) * (vec31.x - vec3.x) + (vec31.z - vec3.z) * (vec31.z - vec3.z)); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raider.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raider.java.patch new file mode 100644 index 0000000..e766fc7 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raider.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/raid/Raider.java ++++ b/net/minecraft/world/entity/raid/Raider.java +@@ -86,7 +_,7 @@ + Raid currentRaid = this.getCurrentRaid(); + if (this.canJoinRaid()) { + if (currentRaid == null) { +- if (this.level().getGameTime() % 20L == 0L) { ++ if (this.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading + Raid raidAt = ((ServerLevel)this.level()).getRaidAt(this.blockPosition()); + if (raidAt != null && Raids.canJoinRaid(this, raidAt)) { + raidAt.joinRaid(raidAt.getGroupsSpawned(), this, null, true); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raids.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raids.java.patch new file mode 100644 index 0000000..b58e170 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/raid/Raids.java.patch @@ -0,0 +1,119 @@ +--- a/net/minecraft/world/entity/raid/Raids.java ++++ b/net/minecraft/world/entity/raid/Raids.java +@@ -25,9 +_,9 @@ + + public class Raids extends SavedData { + private static final String RAID_FILE_ID = "raids"; +- public final Map raidMap = Maps.newHashMap(); ++ public final Map raidMap = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - make raids thread-safe + private final ServerLevel level; +- private int nextAvailableID; ++ private final java.util.concurrent.atomic.AtomicInteger nextAvailableID = new java.util.concurrent.atomic.AtomicInteger(); // Folia - make raids thread-safe + private int tick; + + public static SavedData.Factory factory(ServerLevel level) { +@@ -36,7 +_,7 @@ + + public Raids(ServerLevel level) { + this.level = level; +- this.nextAvailableID = 1; ++ this.nextAvailableID.set(1); // Folia - make raids thread-safe + this.setDirty(); + } + +@@ -44,12 +_,25 @@ + return this.raidMap.get(id); + } + ++ // Folia start - make raids thread-safe ++ public void globalTick() { ++ ++this.tick; ++ if (this.tick % 200 == 0) { ++ this.setDirty(); ++ } ++ } ++ + public void tick() { +- this.tick++; ++ // Folia end - make raids thread-safe + Iterator iterator = this.raidMap.values().iterator(); + + while (iterator.hasNext()) { + Raid raid = iterator.next(); ++ // Folia start - make raids thread-safe ++ if (!raid.ownsRaid()) { ++ continue; ++ } ++ // Folia end - make raids thread-safe + if (this.level.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS)) { + raid.stop(); + } +@@ -62,14 +_,17 @@ + } + } + +- if (this.tick % 200 == 0) { +- this.setDirty(); +- } ++ // Folia - make raids thread-safe - move to globalTick() + + DebugPackets.sendRaids(this.level, this.raidMap.values()); + } + + public static boolean canJoinRaid(Raider raider, Raid raid) { ++ // Folia start - make raids thread-safe ++ if (!raid.ownsRaid()) { ++ return false; ++ } ++ // Folia end - make raids thread-safe + return raider != null + && raid != null + && raid.getLevel() != null +@@ -87,7 +_,7 @@ + return null; + } else { + DimensionType dimensionType = player.level().dimensionType(); +- if (!dimensionType.hasRaids()) { ++ if (!dimensionType.hasRaids() || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(player) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, pos.getX() >> 4, pos.getZ() >> 4, 8) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, player.chunkPosition().x, player.chunkPosition().z, 8)) { // Folia - region threading + return null; + } else { + List list = this.level +@@ -145,7 +_,7 @@ + + public static Raids load(ServerLevel level, CompoundTag tag) { + Raids raids = new Raids(level); +- raids.nextAvailableID = tag.getInt("NextAvailableID"); ++ raids.nextAvailableID.set(tag.getInt("NextAvailableID")); // Folia - make raids thread-safe + raids.tick = tag.getInt("Tick"); + ListTag list = tag.getList("Raids", 10); + +@@ -160,7 +_,7 @@ + + @Override + public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { +- tag.putInt("NextAvailableID", this.nextAvailableID); ++ tag.putInt("NextAvailableID", this.nextAvailableID.get()); // Folia - make raids thread-safe + tag.putInt("Tick", this.tick); + ListTag listTag = new ListTag(); + +@@ -179,7 +_,7 @@ + } + + private int getUniqueId() { +- return ++this.nextAvailableID; ++ return this.nextAvailableID.incrementAndGet(); // Folia - make raids thread-safe + } + + @Nullable +@@ -188,6 +_,11 @@ + double d = distance; + + for (Raid raid1 : this.raidMap.values()) { ++ // Folia start - make raids thread-safe ++ if (!raid1.ownsRaid()) { ++ continue; ++ } ++ // Folia end - make raids thread-safe + double d1 = raid1.getCenter().distSqr(pos); + if (raid1.isActive() && d1 < d) { + raid = raid1; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch new file mode 100644 index 0000000..7988197 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java ++++ b/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java +@@ -145,5 +_,11 @@ + return net.minecraft.world.entity.vehicle.MinecartCommandBlock.this.getBukkitEntity(); + } + // CraftBukkit end ++ // Folia start ++ @Override ++ public void threadCheck() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(MinecartCommandBlock.this, "Asynchronous sendSystemMessage to a command block"); ++ } ++ // Folia end + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartHopper.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartHopper.java.patch new file mode 100644 index 0000000..148301c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/entity/vehicle/MinecartHopper.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/vehicle/MinecartHopper.java ++++ b/net/minecraft/world/entity/vehicle/MinecartHopper.java +@@ -145,7 +_,7 @@ + + // Paper start + public void immunize() { +- this.activatedImmunityTick = Math.max(this.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 20); ++ this.activatedImmunityTick = Math.max(this.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 20); + } + // Paper end + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/item/ItemStack.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/item/ItemStack.java.patch new file mode 100644 index 0000000..12c7a2b --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/item/ItemStack.java.patch @@ -0,0 +1,128 @@ +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -386,31 +_,32 @@ + DataComponentPatch previousPatch = this.components.asPatch(); + int oldCount = this.getCount(); + ServerLevel serverLevel = (ServerLevel) context.getLevel(); ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = serverLevel.getCurrentWorldData(); // Folia - region threading + + if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement +- serverLevel.captureBlockStates = true; ++ worldData.captureBlockStates = true; // Folia - region threading + // special case bonemeal + if (item == Items.BONE_MEAL) { +- serverLevel.captureTreeGeneration = true; ++ worldData.captureTreeGeneration = true; // Folia - region threading + } + } + InteractionResult interactionResult; + try { + interactionResult = item.useOn(context); + } finally { +- serverLevel.captureBlockStates = false; ++ worldData.captureBlockStates = false; // Folia - region threading + } + DataComponentPatch newPatch = this.components.asPatch(); + int newCount = this.getCount(); + this.setCount(oldCount); + this.restorePatch(previousPatch); +- if (interactionResult.consumesAction() && serverLevel.captureTreeGeneration && !serverLevel.capturedBlockStates.isEmpty()) { +- serverLevel.captureTreeGeneration = false; ++ if (interactionResult.consumesAction() && worldData.captureTreeGeneration && !worldData.capturedBlockStates.isEmpty()) { // Folia - region threading ++ worldData.captureTreeGeneration = false; // Folia - region threading + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(clickedPos, serverLevel.getWorld()); +- org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; +- net.minecraft.world.level.block.SaplingBlock.treeType = null; +- List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); +- serverLevel.capturedBlockStates.clear(); ++ org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeTypeRT.get(); // Folia - region threading ++ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(null); // Folia - region threading ++ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + org.bukkit.event.world.StructureGrowEvent structureEvent = null; + if (treeType != null) { + boolean isBonemeal = this.getItem() == Items.BONE_MEAL; +@@ -436,15 +_,15 @@ + player.awardStat(Stats.ITEM_USED.get(item)); // SPIGOT-7236 - award stat + } + +- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return ++ SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading + return interactionResult; + } +- serverLevel.captureTreeGeneration = false; ++ worldData.captureTreeGeneration = false; // Folia - region threading + if (player != null && interactionResult instanceof InteractionResult.Success success && success.wasItemInteraction()) { + InteractionHand hand = context.getHand(); + org.bukkit.event.block.BlockPlaceEvent placeEvent = null; +- List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); +- serverLevel.capturedBlockStates.clear(); ++ List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + if (blocks.size() > 1) { + placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(serverLevel, player, hand, blocks, clickedPos.getX(), clickedPos.getY(), clickedPos.getZ()); + } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement +@@ -455,17 +_,17 @@ + interactionResult = InteractionResult.FAIL; // cancel placement + // PAIL: Remove this when MC-99075 fixed + placeEvent.getPlayer().updateInventory(); +- serverLevel.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot ++ worldData.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot // Folia - region threading + // revert back all captured blocks +- serverLevel.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 +- serverLevel.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent ++ worldData.preventPoiUpdated = true; // CraftBukkit - SPIGOT-5710 // Folia - region threading ++ worldData.isBlockPlaceCancelled = true; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading + for (org.bukkit.block.BlockState blockstate : blocks) { + blockstate.update(true, false); + } +- serverLevel.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent +- serverLevel.preventPoiUpdated = false; ++ worldData.isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading ++ worldData.preventPoiUpdated = false; // Folia - region threading + +- SignItem.openSign = null; // SPIGOT-6758 - Reset on early return ++ SignItem.openSign.set(null); // SPIGOT-6758 - Reset on early return // Folia - region threading + } else { + // Change the stack to its new contents if it hasn't been tampered with. + if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), previousPatch)) { +@@ -473,7 +_,7 @@ + this.setCount(newCount); + } + +- for (java.util.Map.Entry e : serverLevel.capturedTileEntities.entrySet()) { ++ for (java.util.Map.Entry e : worldData.capturedTileEntities.entrySet()) { // Folia - region threading + serverLevel.setBlockEntity(e.getValue()); + } + +@@ -508,15 +_,15 @@ + } + + // SPIGOT-4678 +- if (this.item instanceof SignItem && SignItem.openSign != null) { ++ if (this.item instanceof SignItem && SignItem.openSign.get() != null) { // Folia - region threading + try { +- if (serverLevel.getBlockEntity(SignItem.openSign) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) { +- if (serverLevel.getBlockState(SignItem.openSign).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) { ++ if (serverLevel.getBlockEntity(SignItem.openSign.get()) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) { // Folia - region threading ++ if (serverLevel.getBlockState(SignItem.openSign.get()).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) { // Folia - region threading + signBlock.openTextEdit(player, blockEntity, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // CraftBukkit // Paper - Add PlayerOpenSignEvent + } + } + } finally { +- SignItem.openSign = null; ++ SignItem.openSign.set(null); + } + } + +@@ -544,8 +_,8 @@ + player.awardStat(Stats.ITEM_USED.get(item)); + } + } +- serverLevel.capturedTileEntities.clear(); +- serverLevel.capturedBlockStates.clear(); ++ worldData.capturedTileEntities.clear(); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + // CraftBukkit end + + return interactionResult; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/item/MapItem.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/item/MapItem.java.patch new file mode 100644 index 0000000..61acbd4 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/item/MapItem.java.patch @@ -0,0 +1,61 @@ +--- a/net/minecraft/world/item/MapItem.java ++++ b/net/minecraft/world/item/MapItem.java +@@ -70,6 +_,7 @@ + } + + public void update(Level level, Entity viewer, MapItemSavedData data) { ++ synchronized (data) { // Folia - make map data thread-safe + if (level.dimension() == data.dimension && viewer instanceof Player) { + int i = 1 << data.scale; + int i1 = data.centerX; +@@ -99,8 +_,8 @@ + int i9 = (i1 / i + i6 - 64) * i; + int i10 = (i2 / i + i7 - 64) * i; + Multiset multiset = LinkedHashMultiset.create(); +- LevelChunk chunk = level.getChunkIfLoaded(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10)); // Paper - Maps shouldn't load chunks +- if (chunk != null && !chunk.isEmpty()) { // Paper - Maps shouldn't load chunks ++ LevelChunk chunk = level.getChunkIfLoaded(SectionPos.blockToSectionCoord(i9), SectionPos.blockToSectionCoord(i10)); // Paper - Maps shouldn't load chunks // Folia - super important that it uses getChunkIfLoaded ++ if (chunk != null && !chunk.isEmpty() && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, chunk.getPos())) { // Paper - Maps shouldn't load chunks // Folia - make sure chunk is owned + int i11 = 0; + double d1 = 0.0; + if (level.dimensionType().hasCeiling()) { +@@ -182,6 +_,7 @@ + } + } + } ++ } // Folia - make map data thread-safe + } + + private BlockState getCorrectStateForFluidBlock(Level level, BlockState state, BlockPos pos) { +@@ -196,6 +_,7 @@ + public static void renderBiomePreviewMap(ServerLevel serverLevel, ItemStack stack) { + MapItemSavedData savedData = getSavedData(stack, serverLevel); + if (savedData != null) { ++ synchronized (savedData) { // Folia - make map data thread-safe + if (serverLevel.dimension() == savedData.dimension) { + int i = 1 << savedData.scale; + int i1 = savedData.centerX; +@@ -265,6 +_,7 @@ + } + } + } ++ } // Folia - make map data thread-safe + } + } + +@@ -273,6 +_,7 @@ + if (!level.isClientSide) { + MapItemSavedData savedData = getSavedData(stack, level); + if (savedData != null) { ++ synchronized (savedData) { // Folia - region threading + if (entity instanceof Player player) { + savedData.tickCarriedBy(player, stack); + } +@@ -280,6 +_,7 @@ + if (!savedData.locked && (isSelected || entity instanceof Player && ((Player)entity).getOffhandItem() == stack)) { + this.update(level, entity, savedData); + } ++ } // Folia - region threading + } + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/item/SignItem.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/item/SignItem.java.patch new file mode 100644 index 0000000..c165b42 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/item/SignItem.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/world/item/SignItem.java ++++ b/net/minecraft/world/item/SignItem.java +@@ -11,7 +_,7 @@ + import net.minecraft.world.level.block.state.BlockState; + + public class SignItem extends StandingAndWallBlockItem { +- public static BlockPos openSign; // CraftBukkit ++ public static final ThreadLocal openSign = new ThreadLocal<>(); // CraftBukkit // Folia - region threading + public SignItem(Block standingBlock, Block wallBlock, Item.Properties properties) { + super(standingBlock, wallBlock, Direction.DOWN, properties); + } +@@ -30,7 +_,7 @@ + && level.getBlockState(pos).getBlock() instanceof SignBlock signBlock) { + // CraftBukkit start - SPIGOT-4678 + // signBlock.openTextEdit(player, signBlockEntity, true); +- SignItem.openSign = pos; ++ SignItem.openSign.set(pos); // Folia - region threading + // CraftBukkit end + } + diff --git a/folia-server/minecraft-patches/features/0008-Do-not-access-POI-data-for-lodestone-compass.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/item/component/LodestoneTracker.java.patch similarity index 57% rename from folia-server/minecraft-patches/features/0008-Do-not-access-POI-data-for-lodestone-compass.patch rename to folia-server/minecraft-patches/sources/net/minecraft/world/item/component/LodestoneTracker.java.patch index 6c13513..32f635e 100644 --- a/folia-server/minecraft-patches/features/0008-Do-not-access-POI-data-for-lodestone-compass.patch +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/item/component/LodestoneTracker.java.patch @@ -1,18 +1,6 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Spottedleaf -Date: Sat, 13 May 2023 17:13:40 -0700 -Subject: [PATCH] Do not access POI data for lodestone compass - -Instead, we can just check the loaded chunk's block position for -the lodestone block, as that is at least safe enough for the light -engine compared to the POI access. This should make it safe for -off-region access. - -diff --git a/net/minecraft/world/item/component/LodestoneTracker.java b/net/minecraft/world/item/component/LodestoneTracker.java -index 0c00c23743a4978e8dceed5bbee8ca44b0e0c8d6..b6de5d017bed5c71125f26881b95383386aa1a79 100644 --- a/net/minecraft/world/item/component/LodestoneTracker.java +++ b/net/minecraft/world/item/component/LodestoneTracker.java -@@ -29,7 +29,10 @@ public record LodestoneTracker(Optional target, boolean tracked) { +@@ -29,7 +_,10 @@ return this; } else { BlockPos blockPos = this.target.get().pos(); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/BaseCommandBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/BaseCommandBlock.java.patch new file mode 100644 index 0000000..dca7751 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/BaseCommandBlock.java.patch @@ -0,0 +1,35 @@ +--- a/net/minecraft/world/level/BaseCommandBlock.java ++++ b/net/minecraft/world/level/BaseCommandBlock.java +@@ -21,7 +_,7 @@ + import net.minecraft.world.phys.Vec3; + + public abstract class BaseCommandBlock implements CommandSource { +- private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); ++ private static final ThreadLocal TIME_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss")); // Folia - region threading - SDF is not thread-safe + private static final Component DEFAULT_NAME = Component.literal("@"); + private long lastExecution = -1L; + private boolean updateLastExecution = true; +@@ -114,6 +_,7 @@ + } + + public boolean performCommand(Level level) { ++ if (true) return false; // Folia - region threading + if (level.isClientSide || level.getGameTime() == this.lastExecution) { + return false; + } else if ("Searge".equalsIgnoreCase(this.command)) { +@@ -164,11 +_,14 @@ + this.customName = customName; + } + ++ public void threadCheck() {} // Folia ++ + @Override + public void sendSystemMessage(Component component) { + if (this.trackOutput) { + org.spigotmc.AsyncCatcher.catchOp("sendSystemMessage to a command block"); // Paper - Don't broadcast messages to command blocks +- this.lastOutput = Component.literal("[" + TIME_FORMAT.format(new Date()) + "] ").append(component); ++ this.threadCheck(); // Folia ++ this.lastOutput = Component.literal("[" + TIME_FORMAT.get().format(new Date()) + "] ").append(component); // Folia - region threading - SDF is not thread-safe + this.onUpdated(); + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch new file mode 100644 index 0000000..1d68066 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch @@ -0,0 +1,61 @@ +--- a/net/minecraft/world/level/EntityGetter.java ++++ b/net/minecraft/world/level/EntityGetter.java +@@ -24,6 +_,12 @@ + return this.getEntities(EntityTypeTest.forClass(entityClass), area, filter); + } + ++ // Folia start - region threading ++ default List getLocalPlayers() { ++ return java.util.Collections.emptyList(); ++ } ++ // Folia end - region threading ++ + List players(); + + default List getEntities(@Nullable Entity entity, AABB area) { +@@ -123,7 +_,7 @@ + double d = -1.0; + Player player = null; + +- for (Player player1 : this.players()) { ++ for (Player player1 : this.getLocalPlayers()) { // Folia - region threading + if (predicate == null || predicate.test(player1)) { + double d1 = player1.distanceToSqr(x, y, z); + if ((distance < 0.0 || d1 < distance * distance) && (d == -1.0 || d1 < d)) { +@@ -144,7 +_,7 @@ + default List findNearbyBukkitPlayers(double x, double y, double z, double radius, @Nullable Predicate predicate) { + com.google.common.collect.ImmutableList.Builder builder = com.google.common.collect.ImmutableList.builder(); + +- for (Player human : this.players()) { ++ for (Player human : this.getLocalPlayers()) { // Folia - region threading + if (predicate == null || predicate.test(human)) { + double distanceSquared = human.distanceToSqr(x, y, z); + +@@ -171,7 +_,7 @@ + + // Paper start - Affects Spawning API + default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) { +- for (Player player : this.players()) { ++ for (Player player : this.getLocalPlayers()) { // Folia - region threading + if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check + double distanceSqr = player.distanceToSqr(x, y, z); + if (range < 0.0D || distanceSqr < range * range) { +@@ -184,7 +_,7 @@ + // Paper end - Affects Spawning API + + default boolean hasNearbyAlivePlayer(double x, double y, double z, double distance) { +- for (Player player : this.players()) { ++ for (Player player : this.getLocalPlayers()) { // Folia - region threading + if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { + double d = player.distanceToSqr(x, y, z); + if (distance < 0.0 || d < distance * distance) { +@@ -198,8 +_,7 @@ + + @Nullable + default Player getPlayerByUUID(UUID uniqueId) { +- for (int i = 0; i < this.players().size(); i++) { +- Player player = this.players().get(i); ++ for (Player player : this.getLocalPlayers()) { // Folia - region threading + if (uniqueId.equals(player.getUUID())) { + return player; + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch new file mode 100644 index 0000000..3b5dbac --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch @@ -0,0 +1,404 @@ +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -115,10 +_,10 @@ + public static final int TICKS_PER_DAY = 24000; + public static final int MAX_ENTITY_SPAWN_Y = 20000000; + public static final int MIN_ENTITY_SPAWN_Y = -20000000; +- public final List blockEntityTickers = Lists.newArrayList(); // Paper - public +- protected final NeighborUpdater neighborUpdater; +- private final List pendingBlockEntityTickers = Lists.newArrayList(); +- private boolean tickingBlockEntities; ++ //public final List blockEntityTickers = Lists.newArrayList(); // Paper - public // Folia - region threading ++ public final int neighbourUpdateMax; //protected final NeighborUpdater neighborUpdater; // Folia - region threading ++ //private final List pendingBlockEntityTickers = Lists.newArrayList(); // Folia - region threading ++ //private boolean tickingBlockEntities; // Folia - region threading + public final Thread thread; + private final boolean isDebug; + private int skyDarken; +@@ -128,7 +_,7 @@ + public float rainLevel; + protected float oThunderLevel; + public float thunderLevel; +- public final RandomSource random = new ca.spottedleaf.moonrise.common.util.ThreadUnsafeRandom(net.minecraft.world.level.levelgen.RandomSupport.generateUniqueSeed()); // Paper - replace random ++ public final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Paper - replace random // Folia - region threading + @Deprecated + private final RandomSource threadSafeRandom = RandomSource.createThreadSafe(); + private final Holder dimensionTypeRegistration; +@@ -139,28 +_,17 @@ + private final ResourceKey dimension; + private final RegistryAccess registryAccess; + private final DamageSources damageSources; +- private long subTickCount; ++ private final java.util.concurrent.atomic.AtomicLong subTickCount = new java.util.concurrent.atomic.AtomicLong(); //private long subTickCount; // Folia - region threading + + // CraftBukkit start Added the following + private final CraftWorld world; + public boolean pvpMode; + public org.bukkit.generator.ChunkGenerator generator; + +- public boolean preventPoiUpdated = false; // CraftBukkit - SPIGOT-5710 +- public boolean captureBlockStates = false; +- public boolean captureTreeGeneration = false; +- public boolean isBlockPlaceCancelled = false; // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent +- public Map capturedBlockStates = new java.util.LinkedHashMap<>(); // Paper +- public Map capturedTileEntities = new java.util.LinkedHashMap<>(); // Paper - Retain block place order when capturing blockstates +- public List captureDrops; ++ // Folia - region threading - moved to regionised data + public final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>(); +- // Paper start +- public int wakeupInactiveRemainingAnimals; +- public int wakeupInactiveRemainingFlying; +- public int wakeupInactiveRemainingMonsters; +- public int wakeupInactiveRemainingVillagers; +- // Paper end +- public boolean populating; ++ // Folia - region threading - moved to regionised data ++ // Folia - region threading + public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot + // Paper start - add paper world config + private final io.papermc.paper.configuration.WorldConfiguration paperConfig; +@@ -173,9 +_,9 @@ + public static BlockPos lastPhysicsProblem; // Spigot + private org.spigotmc.TickLimiter entityLimiter; + private org.spigotmc.TickLimiter tileLimiter; +- private int tileTickPosition; +- public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions +- public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here ++ //private int tileTickPosition; // Folia - region threading ++ //public final Map explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions // Folia - region threading ++ //public java.util.ArrayDeque redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here // Folia - region threading + + public CraftWorld getWorld() { + return this.world; +@@ -825,6 +_,32 @@ + return chunk != null ? chunk.getNoiseBiome(x, y, z) : this.getUncachedNoiseBiome(x, y, z); + } + // Paper end - optimise random ticking ++ // Folia start - region ticking ++ public final io.papermc.paper.threadedregions.RegionizedData worldRegionData ++ = new io.papermc.paper.threadedregions.RegionizedData<>( ++ (ServerLevel)this, () -> new io.papermc.paper.threadedregions.RegionizedWorldData((ServerLevel)Level.this), ++ io.papermc.paper.threadedregions.RegionizedWorldData.REGION_CALLBACK ++ ); ++ public volatile io.papermc.paper.threadedregions.RegionizedServer.WorldLevelData tickData; ++ public final java.util.concurrent.ConcurrentHashMap.KeySetView needsChangeBroadcasting = java.util.concurrent.ConcurrentHashMap.newKeySet(); ++ ++ public io.papermc.paper.threadedregions.RegionizedWorldData getCurrentWorldData() { ++ final io.papermc.paper.threadedregions.RegionizedWorldData ret = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); ++ if (ret == null) { ++ return ret; ++ } ++ Level world = ret.world; ++ if (world != this) { ++ throw new IllegalStateException("World mismatch: expected " + this.getWorld().getName() + " but got " + world.getWorld().getName()); ++ } ++ return ret; ++ } ++ ++ @Override ++ public List getLocalPlayers() { ++ return this.getCurrentWorldData().getLocalPlayers(); ++ } ++ // Folia end - region ticking + + protected Level( + WritableLevelData levelData, +@@ -888,7 +_,7 @@ + this.thread = Thread.currentThread(); + this.biomeManager = new BiomeManager(this, biomeZoomSeed); + this.isDebug = isDebug; +- this.neighborUpdater = new CollectingNeighborUpdater(this, maxChainedNeighborUpdates); ++ this.neighbourUpdateMax = maxChainedNeighborUpdates; // Folia - region threading + this.registryAccess = registryAccess; + this.damageSources = new DamageSources(registryAccess); + +@@ -1035,8 +_,8 @@ + @Nullable + public final BlockState getBlockStateIfLoaded(BlockPos pos) { + // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { +- CraftBlockState previous = this.capturedBlockStates.get(pos); ++ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading ++ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Folia - region threading + if (previous != null) { + return previous.getHandle(); + } +@@ -1098,16 +_,18 @@ + + @Override + public boolean setBlock(BlockPos pos, BlockState state, int flags, int recursionLeft) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, pos, "Updating block asynchronously"); // Folia - region threading ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); // Folia - region threading + // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { ++ if (worldData.captureTreeGeneration) { // Folia - region threading + // Paper start - Protect Bedrock and End Portal/Frames from being destroyed + BlockState type = getBlockState(pos); + if (!type.isDestroyable()) return false; + // Paper end - Protect Bedrock and End Portal/Frames from being destroyed +- CraftBlockState blockstate = this.capturedBlockStates.get(pos); ++ CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Folia - region threading + if (blockstate == null) { + blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags); +- this.capturedBlockStates.put(pos.immutable(), blockstate); ++ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading + } + blockstate.setData(state); + blockstate.setFlag(flags); +@@ -1123,10 +_,10 @@ + Block block = state.getBlock(); + // CraftBukkit start - capture blockstates + boolean captured = false; +- if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) { ++ if (worldData.captureBlockStates && !worldData.capturedBlockStates.containsKey(pos)) { // Folia - region threading + CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot + blockstate.setFlag(flags); // Paper - set flag +- this.capturedBlockStates.put(pos.immutable(), blockstate); ++ worldData.capturedBlockStates.put(pos.immutable(), blockstate); // Folia - region threading + captured = true; + } + // CraftBukkit end +@@ -1136,8 +_,8 @@ + + if (blockState == null) { + // CraftBukkit start - remove blockstate if failed (or the same) +- if (this.captureBlockStates && captured) { +- this.capturedBlockStates.remove(pos); ++ if (worldData.captureBlockStates && captured) { // Folia - region threading ++ worldData.capturedBlockStates.remove(pos); // Folia - region threading + } + // CraftBukkit end + return false; +@@ -1174,7 +_,7 @@ + */ + + // CraftBukkit start +- if (!this.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates ++ if (!worldData.captureBlockStates) { // Don't notify clients or update physics while capturing blockstates // Folia - region threading + // Modularize client and physic updates + // Spigot start + try { +@@ -1219,7 +_,7 @@ + iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam + CraftWorld world = ((ServerLevel) this).getWorld(); + boolean cancelledUpdates = false; // Paper - Fix block place logic +- if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent ++ if (world != null && ((ServerLevel)this).getCurrentWorldData().hasPhysicsEvent) { // Paper - BlockPhysicsEvent // Folia - region threading + BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata)); + this.getCraftServer().getPluginManager().callEvent(event); + +@@ -1233,7 +_,7 @@ + } + + // CraftBukkit start - SPIGOT-5710 +- if (!this.preventPoiUpdated) { ++ if (!this.getCurrentWorldData().preventPoiUpdated) { // Folia - region threading + this.onBlockStateChange(blockposition, iblockdata1, iblockdata2); + } + // CraftBukkit end +@@ -1322,7 +_,7 @@ + + @Override + public void neighborShapeChanged(Direction direction, BlockPos pos, BlockPos neighborPos, BlockState neighborState, int flags, int recursionLeft) { +- this.neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, recursionLeft); ++ this.getCurrentWorldData().neighborUpdater.shapeUpdate(direction, neighborState, pos, neighborPos, flags, recursionLeft); // Folia - region threading + } + + @Override +@@ -1346,11 +_,34 @@ + return this.getChunkSource().getLightEngine(); + } + ++ // Folia start - region threading ++ @Nullable ++ public BlockState getBlockStateFromEmptyChunkIfLoaded(BlockPos pos) { ++ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); ++ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); ++ if (chunk != null) { ++ return chunk.getBlockState(pos); ++ } ++ return null; ++ } ++ ++ @Nullable ++ public BlockState getBlockStateFromEmptyChunk(BlockPos pos) { ++ net.minecraft.server.level.ServerChunkCache chunkProvider = (net.minecraft.server.level.ServerChunkCache)this.getChunkSource(); ++ ChunkAccess chunk = chunkProvider.getChunkAtImmediately(pos.getX() >> 4, pos.getZ() >> 4); ++ if (chunk != null) { ++ return chunk.getBlockState(pos); ++ } ++ chunk = chunkProvider.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.EMPTY, true); ++ return chunk.getBlockState(pos); ++ } ++ // Folia end - region threading ++ + @Override + public BlockState getBlockState(BlockPos pos) { + // CraftBukkit start - tree generation +- if (this.captureTreeGeneration) { +- CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper ++ if (this.getCurrentWorldData().captureTreeGeneration) { // Folia - region threading ++ CraftBlockState previous = this.getCurrentWorldData().capturedBlockStates.get(pos); // Paper // Folia - region threading + if (previous != null) { + return previous.getHandle(); + } +@@ -1454,17 +_,16 @@ + } + + public void addBlockEntityTicker(TickingBlockEntity ticker) { +- (this.tickingBlockEntities ? this.pendingBlockEntityTickers : this.blockEntityTickers).add(ticker); ++ ((ServerLevel)this).getCurrentWorldData().addBlockEntityTicker(ticker); // Folia - regionised ticking + } + + protected void tickBlockEntities() { + ProfilerFiller profilerFiller = Profiler.get(); + profilerFiller.push("blockEntities"); +- this.tickingBlockEntities = true; +- if (!this.pendingBlockEntityTickers.isEmpty()) { +- this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); +- this.pendingBlockEntityTickers.clear(); +- } ++ final io.papermc.paper.threadedregions.RegionizedWorldData regionizedWorldData = this.getCurrentWorldData(); // Folia - regionised ticking ++ regionizedWorldData.seTtickingBlockEntities(true); // Folia - regionised ticking ++ regionizedWorldData.pushPendingTickingBlockEntities(); // Folia - regionised ticking ++ List blockEntityTickers = regionizedWorldData.getBlockEntityTickers(); // Folia - regionised ticking + + // Spigot start + boolean runsNormally = this.tickRateManager().runsNormally(); +@@ -1472,9 +_,8 @@ + int tickedEntities = 0; // Paper - rewrite chunk system + var toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet(); // Paper - Fix MC-117075; use removeAll + toRemove.add(null); // Paper - Fix MC-117075 +- for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters +- this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0; +- TickingBlockEntity tickingBlockEntity = this.blockEntityTickers.get(this.tileTickPosition); ++ for (int i = 0; i < blockEntityTickers.size(); i++) { // Paper - Disable tick limiters // Folia - regionised ticking ++ TickingBlockEntity tickingBlockEntity = blockEntityTickers.get(i); // Folia - regionised ticking + // Spigot end + if (tickingBlockEntity.isRemoved()) { + toRemove.add(tickingBlockEntity); // Paper - Fix MC-117075; use removeAll +@@ -1487,11 +_,11 @@ + // Paper end - rewrite chunk system + } + } +- this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 ++ blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 // Folia - regionised ticking + +- this.tickingBlockEntities = false; ++ regionizedWorldData.seTtickingBlockEntities(false); // Folia - regionised ticking + profilerFiller.pop(); +- this.spigotConfig.currentPrimedTnt = 0; // Spigot ++ regionizedWorldData.currentPrimedTnt = 0; // Spigot // Folia - region threading + } + + public void guardEntityTick(Consumer consumerEntity, T entity) { +@@ -1502,7 +_,8 @@ + final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); + MinecraftServer.LOGGER.error(msg, var6); + getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, var6))); // Paper - ServerExceptionEvent +- entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ if (!(entity instanceof net.minecraft.server.level.ServerPlayer)) entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); // Folia - properly disconnect players ++ if (entity instanceof net.minecraft.server.level.ServerPlayer player) player.connection.disconnect(net.minecraft.network.chat.Component.translatable("multiplayer.disconnect.generic"), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Folia - properly disconnect players + // Paper end - Prevent block entity and entity crashes + } + this.moonrise$midTickTasks(); // Paper - rewrite chunk system +@@ -1648,9 +_,14 @@ + + @Nullable + public BlockEntity getBlockEntity(BlockPos pos, boolean validate) { ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { ++ return null; ++ } ++ // Folia end - region threading + // Paper start - Perf: Optimize capturedTileEntities lookup + net.minecraft.world.level.block.entity.BlockEntity blockEntity; +- if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(pos)) != null) { ++ if (!this.getCurrentWorldData().capturedTileEntities.isEmpty() && (blockEntity = this.getCurrentWorldData().capturedTileEntities.get(pos)) != null) { // Folia - region threading + return blockEntity; + } + // Paper end - Perf: Optimize capturedTileEntities lookup +@@ -1668,8 +_,8 @@ + BlockPos blockPos = blockEntity.getBlockPos(); + if (!this.isOutsideBuildHeight(blockPos)) { + // CraftBukkit start +- if (this.captureBlockStates) { +- this.capturedTileEntities.put(blockPos.immutable(), blockEntity); ++ if (this.getCurrentWorldData().captureBlockStates) { // Folia - region threading ++ this.getCurrentWorldData().capturedTileEntities.put(blockPos.immutable(), blockEntity); // Folia - region threading + return; + } + // CraftBukkit end +@@ -1749,6 +_,7 @@ + + @Override + public List getEntities(@Nullable Entity entity, AABB boundingBox, Predicate predicate) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel)this, boundingBox, "Cannot getEntities asynchronously"); // Folia - region threading + Profiler.get().incrementCounter("getEntities"); + List list = Lists.newArrayList(); + +@@ -1778,6 +_,7 @@ + public void getEntities(final EntityTypeTest entityTypeTest, + final AABB boundingBox, final Predicate predicate, + final List into, final int maxCount) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, boundingBox, "Cannot getEntities asynchronously"); // Folia - region threading + Profiler.get().incrementCounter("getEntities"); + + if (entityTypeTest instanceof net.minecraft.world.entity.EntityType byType) { +@@ -1877,13 +_,34 @@ + public void disconnect() { + } + ++ @Override // Folia - region threading + public long getGameTime() { +- return this.levelData.getGameTime(); ++ // Folia start - region threading ++ // Dumb world gen thread calls this for some reason. So, check for null. ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); ++ return worldData == null ? this.getLevelData().getGameTime() : worldData.getTickData().nonRedstoneGameTime(); ++ // Folia end - region threading + } + + public long getDayTime() { +- return this.levelData.getDayTime(); +- } ++ // Folia start - region threading ++ // Dumb world gen thread calls this for some reason. So, check for null. ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.getCurrentWorldData(); ++ return worldData == null ? this.getLevelData().getDayTime() : worldData.getTickData().dayTime(); ++ // Folia end - region threading ++ } ++ ++ // Folia start - region threading ++ @Override ++ public long dayTime() { ++ return this.getDayTime(); ++ } ++ ++ @Override ++ public long getRedstoneGameTime() { ++ return this.getCurrentWorldData().getRedstoneGameTime(); ++ } ++ // Folia end - region threading + + public boolean mayInteract(Player player, BlockPos pos) { + return true; +@@ -2061,8 +_,7 @@ + public abstract RecipeAccess recipeAccess(); + + public BlockPos getBlockRandomPos(int x, int y, int z, int yMask) { +- this.randValue = this.randValue * 3 + 1013904223; +- int i = this.randValue >> 2; ++ int i = this.random.nextInt() >> 2; // Folia - region threading + return new BlockPos(x + (i & 15), y + (i >> 16 & yMask), z + (i >> 8 & 15)); + } + +@@ -2083,7 +_,7 @@ + + @Override + public long nextSubTickCount() { +- return this.subTickCount++; ++ return this.subTickCount.getAndIncrement(); // Folia - region threading + } + + @Override diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelAccessor.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelAccessor.java.patch new file mode 100644 index 0000000..a0ecb20 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelAccessor.java.patch @@ -0,0 +1,29 @@ +--- a/net/minecraft/world/level/LevelAccessor.java ++++ b/net/minecraft/world/level/LevelAccessor.java +@@ -33,14 +_,24 @@ + + long nextSubTickCount(); + ++ // Folia start - region threading ++ default long getGameTime() { ++ return this.getLevelData().getGameTime(); ++ } ++ ++ default long getRedstoneGameTime() { ++ return this.getLevelData().getGameTime(); ++ } ++ // Folia end - region threading ++ + @Override + default ScheduledTick createTick(BlockPos pos, T type, int delay, TickPriority priority) { +- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + delay, priority, this.nextSubTickCount()); ++ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + delay, priority, this.nextSubTickCount()); // Folia - region threading + } + + @Override + default ScheduledTick createTick(BlockPos pos, T type, int delay) { +- return new ScheduledTick<>(type, pos, this.getLevelData().getGameTime() + delay, this.nextSubTickCount()); ++ return new ScheduledTick<>(type, pos, this.getRedstoneGameTime() + delay, this.nextSubTickCount()); // Folia - region threading + } + + LevelData getLevelData(); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelReader.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelReader.java.patch new file mode 100644 index 0000000..79def9c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/LevelReader.java.patch @@ -0,0 +1,28 @@ +--- a/net/minecraft/world/level/LevelReader.java ++++ b/net/minecraft/world/level/LevelReader.java +@@ -204,6 +_,25 @@ + return toY >= this.getMinY() && fromY <= this.getMaxY() && this.hasChunksAt(fromX, fromZ, toX, toZ); + } + ++ // Folia start - region threading ++ default boolean hasAndOwnsChunksAt(int minX, int minZ, int maxX, int maxZ) { ++ int i = SectionPos.blockToSectionCoord(minX); ++ int j = SectionPos.blockToSectionCoord(maxX); ++ int k = SectionPos.blockToSectionCoord(minZ); ++ int l = SectionPos.blockToSectionCoord(maxZ); ++ ++ for(int m = i; m <= j; ++m) { ++ for(int n = k; n <= l; ++n) { ++ if (!this.hasChunk(m, n) || (this instanceof net.minecraft.server.level.ServerLevel world && !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(world, m, n))) { ++ return false; ++ } ++ } ++ } ++ ++ return true; ++ } ++ // Folia end - region threading ++ + @Deprecated + default boolean hasChunksAt(int fromX, int fromZ, int toX, int toZ) { + int sectionPosCoord = SectionPos.blockToSectionCoord(fromX); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/NaturalSpawner.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/NaturalSpawner.java.patch new file mode 100644 index 0000000..165c607 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/NaturalSpawner.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/NaturalSpawner.java ++++ b/net/minecraft/world/level/NaturalSpawner.java +@@ -137,7 +_,7 @@ + int limit = mobCategory.getMaxInstancesPerChunk(); + SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(mobCategory); + if (CraftSpawnCategory.isValidForLimits(spawnCategory)) { +- spawnThisTick = level.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % level.ticksPerSpawnCategory.getLong(spawnCategory) == 0; ++ spawnThisTick = level.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && level.getRedstoneGameTime() % level.ticksPerSpawnCategory.getLong(spawnCategory) == 0; // Folia - region threading + limit = level.getWorld().getSpawnLimit(spawnCategory); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerExplosion.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerExplosion.java.patch new file mode 100644 index 0000000..d2ade28 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerExplosion.java.patch @@ -0,0 +1,24 @@ +--- a/net/minecraft/world/level/ServerExplosion.java ++++ b/net/minecraft/world/level/ServerExplosion.java +@@ -773,17 +_,18 @@ + if (!this.level.paperConfig().environment.optimizeExplosions) { + return this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations + } ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = this.level.getCurrentWorldData(); // Folia - region threading + CacheKey key = new CacheKey(this, entity.getBoundingBox()); +- Float blockDensity = this.level.explosionDensityCache.get(key); ++ Float blockDensity = worldData.explosionDensityCache.get(key); // Folia - region threading + if (blockDensity == null) { + blockDensity = this.getSeenFraction(vec3d, entity, this.directMappedBlockCache, this.mutablePos); // Paper - collision optimisations +- this.level.explosionDensityCache.put(key, blockDensity); ++ worldData.explosionDensityCache.put(key, blockDensity); // Folia - region threading + } + + return blockDensity; + } + +- static class CacheKey { ++ public static class CacheKey { // Folia - region threading - public + private final Level world; + private final double posX, posY, posZ; + private final double minX, minY, minZ; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerLevelAccessor.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerLevelAccessor.java.patch new file mode 100644 index 0000000..baae667 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/ServerLevelAccessor.java.patch @@ -0,0 +1,15 @@ +--- a/net/minecraft/world/level/ServerLevelAccessor.java ++++ b/net/minecraft/world/level/ServerLevelAccessor.java +@@ -6,6 +_,12 @@ + public interface ServerLevelAccessor extends LevelAccessor { + ServerLevel getLevel(); + ++ // Folia start - region threading ++ default public StructureManager structureManager() { ++ throw new UnsupportedOperationException(); ++ } ++ // Folia end - region threading ++ + default void addFreshEntityWithPassengers(Entity entity) { + // CraftBukkit start + this.addFreshEntityWithPassengers(entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.DEFAULT); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/StructureManager.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/StructureManager.java.patch new file mode 100644 index 0000000..890ce2e --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/StructureManager.java.patch @@ -0,0 +1,48 @@ +--- a/net/minecraft/world/level/StructureManager.java ++++ b/net/minecraft/world/level/StructureManager.java +@@ -48,12 +_,7 @@ + } + + public List startsForStructure(ChunkPos chunkPos, Predicate structurePredicate) { +- // Paper start - Fix swamp hut cat generation deadlock +- return this.startsForStructure(chunkPos, structurePredicate, null); +- } +- +- public List startsForStructure(ChunkPos chunkPos, Predicate structurePredicate, @Nullable ServerLevelAccessor levelAccessor) { +- Map allReferences = (levelAccessor == null ? this.level : levelAccessor).getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); ++ Map allReferences = this.level.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences(); // Folia - region threading + // Paper end - Fix swamp hut cat generation deadlock + Builder builder = ImmutableList.builder(); + +@@ -124,20 +_,12 @@ + } + + public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate> predicate) { +- // Paper start - Fix swamp hut cat generation deadlock +- return this.getStructureWithPieceAt(pos, predicate, null); +- } +- +- public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey tag, @Nullable ServerLevelAccessor levelAccessor) { +- return this.getStructureWithPieceAt(pos, structure -> structure.is(tag), levelAccessor); +- } +- +- public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate> predicate, @Nullable ServerLevelAccessor levelAccessor) { ++ // Folia - region threading + // Paper end - Fix swamp hut cat generation deadlock + Registry registry = this.registryAccess().lookupOrThrow(Registries.STRUCTURE); + + for (StructureStart structureStart : this.startsForStructure( +- new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false), levelAccessor // Paper - Fix swamp hut cat generation deadlock ++ new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false) // Paper - Fix swamp hut cat generation deadlock // Folia - region threading + )) { + if (this.structureHasPieceAt(pos, structureStart)) { + return structureStart; +@@ -182,7 +_,7 @@ + } + + public void addReference(StructureStart structureStart) { +- structureStart.addReference(); ++ //structureStart.addReference(); // Folia - region threading - move to caller + this.structureCheck.incrementReference(structureStart.getChunkPos(), structureStart.getStructure()); + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BedBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BedBlock.java.patch new file mode 100644 index 0000000..4f3477c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BedBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/BedBlock.java ++++ b/net/minecraft/world/level/block/BedBlock.java +@@ -346,7 +_,7 @@ + BlockPos blockPos = pos.relative(state.getValue(FACING)); + level.setBlock(blockPos, state.setValue(PART, BedPart.HEAD), 3); + // CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states +- if (level.captureBlockStates) { ++ if (level.getCurrentWorldData().captureBlockStates) { // Folia - region threading + return; + } + // CraftBukkit end diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Block.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Block.java.patch new file mode 100644 index 0000000..c72a18a --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Block.java.patch @@ -0,0 +1,13 @@ +--- a/net/minecraft/world/level/block/Block.java ++++ b/net/minecraft/world/level/block/Block.java +@@ -362,8 +_,8 @@ + ItemEntity itemEntity = itemEntitySupplier.get(); + itemEntity.setDefaultPickUpDelay(); + // CraftBukkit start +- if (level.captureDrops != null) { +- level.captureDrops.add(itemEntity); ++ if (level.getCurrentWorldData().captureDrops != null) { // Folia - region threading ++ level.getCurrentWorldData().captureDrops.add(itemEntity); // Folia - region threading + } else { + level.addFreshEntity(itemEntity); + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BushBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BushBlock.java.patch new file mode 100644 index 0000000..64b836c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/BushBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/BushBlock.java ++++ b/net/minecraft/world/level/block/BushBlock.java +@@ -38,7 +_,7 @@ + // CraftBukkit start + if (!state.canSurvive(level, pos)) { + // Suppress during worldgen +- if (!(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(serverLevel, pos).isCancelled()) { // Paper ++ if (!(level instanceof net.minecraft.server.level.ServerLevel serverLevel && serverLevel.getCurrentWorldData().hasPhysicsEvent) || !org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(serverLevel, pos).isCancelled()) { // Paper // Folia - region threading + return Blocks.AIR.defaultBlockState(); + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch new file mode 100644 index 0000000..da57a25 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/DaylightDetectorBlock.java ++++ b/net/minecraft/world/level/block/DaylightDetectorBlock.java +@@ -110,7 +_,7 @@ + } + + private static void tickEntity(Level level, BlockPos pos, BlockState state, DaylightDetectorBlockEntity blockEntity) { +- if (level.getGameTime() % 20L == 0L) { ++ if (level.getRedstoneGameTime() % 20L == 0L) { // Folia - region threading + updateSignalStrength(state, level, pos); + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DispenserBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DispenserBlock.java.patch new file mode 100644 index 0000000..e3118c6 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DispenserBlock.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/world/level/block/DispenserBlock.java ++++ b/net/minecraft/world/level/block/DispenserBlock.java +@@ -50,7 +_,7 @@ + private static final DefaultDispenseItemBehavior DEFAULT_BEHAVIOR = new DefaultDispenseItemBehavior(); + public static final Map DISPENSER_REGISTRY = new IdentityHashMap<>(); + private static final int TRIGGER_DURATION = 4; +- public static boolean eventFired = false; // CraftBukkit ++ public static ThreadLocal eventFired = ThreadLocal.withInitial(() -> Boolean.FALSE); // CraftBukkit // Folia - region threading + + @Override + public MapCodec codec() { +@@ -96,7 +_,7 @@ + DispenseItemBehavior dispenseMethod = this.getDispenseMethod(level, item); + if (dispenseMethod != DispenseItemBehavior.NOOP) { + if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockPreDispenseEvent(level, pos, item, randomSlot)) return; // Paper - Add BlockPreDispenseEvent +- DispenserBlock.eventFired = false; // CraftBukkit - reset event status ++ DispenserBlock.eventFired.set(Boolean.FALSE); // CraftBukkit - reset event status // Folia - region threading + dispenserBlockEntity.setItem(randomSlot, dispenseMethod.dispense(blockSource, item)); + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DoublePlantBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DoublePlantBlock.java.patch new file mode 100644 index 0000000..43342d5 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/DoublePlantBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/DoublePlantBlock.java ++++ b/net/minecraft/world/level/block/DoublePlantBlock.java +@@ -118,7 +_,7 @@ + + protected static void preventDropFromBottomPart(Level level, BlockPos pos, BlockState state, Player player) { + // CraftBukkit start +- if (((net.minecraft.server.level.ServerLevel)level).hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(level, pos).isCancelled()) { // Paper ++ if (((net.minecraft.server.level.ServerLevel)level).getCurrentWorldData().hasPhysicsEvent && org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPhysicsEvent(level, pos).isCancelled()) { // Paper // Folia - region threading + return; + } + // CraftBukkit end diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndGatewayBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndGatewayBlock.java.patch new file mode 100644 index 0000000..0d623f8 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndGatewayBlock.java.patch @@ -0,0 +1,50 @@ +--- a/net/minecraft/world/level/block/EndGatewayBlock.java ++++ b/net/minecraft/world/level/block/EndGatewayBlock.java +@@ -111,16 +_,42 @@ + if (portalPosition == null) { + return null; + } else { +- return entity instanceof ThrownEnderpearl +- ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit +- : new TeleportTransition( +- level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit +- ); ++ return getTeleportTransition(level, entity, portalPosition); // Folia - region threading + } + } else { + return null; + } + } ++ ++ // Folia start - region threading ++ public static TeleportTransition getTeleportTransition(ServerLevel level, Entity entity, Vec3 portalPosition) { ++ return entity instanceof ThrownEnderpearl ++ ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit ++ : new TeleportTransition( ++ level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit ++ ); ++ } ++ ++ @Override ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { ++ return false; ++ } ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { ++ return false; ++ } ++ ++ BlockEntity tile = sourceWorld.getBlockEntity(portalPos); ++ ++ if (!(tile instanceof TheEndGatewayBlockEntity endGateway)) { ++ return false; ++ } ++ ++ return TheEndGatewayBlockEntity.teleportRegionThreading( ++ sourceWorld, portalPos, portalTarget, endGateway, TeleportTransition.PLACE_PORTAL_TICKET ++ ); ++ } ++ // Folia end - region threading + + @Override + protected RenderShape getRenderShape(BlockState state) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndPortalBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndPortalBlock.java.patch new file mode 100644 index 0000000..2881691 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/EndPortalBlock.java.patch @@ -0,0 +1,32 @@ +--- a/net/minecraft/world/level/block/EndPortalBlock.java ++++ b/net/minecraft/world/level/block/EndPortalBlock.java +@@ -63,7 +_,7 @@ + level.getCraftServer().getPluginManager().callEvent(event); + if (event.isCancelled()) return; // Paper - make cancellable + // CraftBukkit end +- if (!level.isClientSide && level.dimension() == Level.END && entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) { ++ if (false && !level.isClientSide && level.dimension() == Level.END && entity instanceof ServerPlayer serverPlayer && !serverPlayer.seenCredits) { // Folia - region threading - do not show credits + if (level.paperConfig().misc.disableEndCredits) {serverPlayer.seenCredits = true; return;} // Paper - Option to disable end credits + serverPlayer.showEndCredits(); + } else { +@@ -112,6 +_,20 @@ + // CraftBukkit end + } + } ++ ++ // Folia start - region threading ++ @Override ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { ++ return false; ++ } ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { ++ return false; ++ } ++ ++ return portalTarget.endPortalLogicAsync(portalPos); ++ } ++ // Folia end - region threading + + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FarmBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FarmBlock.java.patch new file mode 100644 index 0000000..121cab6 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FarmBlock.java.patch @@ -0,0 +1,13 @@ +--- a/net/minecraft/world/level/block/FarmBlock.java ++++ b/net/minecraft/world/level/block/FarmBlock.java +@@ -95,8 +_,8 @@ + @Override + protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + int moistureValue = state.getValue(MOISTURE); +- if (moistureValue > 0 && level.paperConfig().tickRates.wetFarmland != 1 && (level.paperConfig().tickRates.wetFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks +- if (moistureValue == 0 && level.paperConfig().tickRates.dryFarmland != 1 && (level.paperConfig().tickRates.dryFarmland < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks ++ if (moistureValue > 0 && level.paperConfig().tickRates.wetFarmland != 1 && (level.paperConfig().tickRates.wetFarmland < 1 || (level.getRedstoneGameTime() + pos.hashCode()) % level.paperConfig().tickRates.wetFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading ++ if (moistureValue == 0 && level.paperConfig().tickRates.dryFarmland != 1 && (level.paperConfig().tickRates.dryFarmland < 1 || (level.getRedstoneGameTime() + pos.hashCode()) % level.paperConfig().tickRates.dryFarmland != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - region threading + if (!isNearWater(level, pos) && !level.isRainingAt(pos.above())) { + if (moistureValue > 0) { + org.bukkit.craftbukkit.event.CraftEventFactory.handleMoistureChangeEvent(level, pos, state.setValue(FarmBlock.MOISTURE, moistureValue - 1), 2); // CraftBukkit diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FungusBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FungusBlock.java.patch new file mode 100644 index 0000000..d30d181 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/FungusBlock.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/level/block/FungusBlock.java ++++ b/net/minecraft/world/level/block/FungusBlock.java +@@ -76,9 +_,9 @@ + // CraftBukkit start + .map((value) -> { + if (this == Blocks.WARPED_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.WARPED_FUNGUS; ++ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.WARPED_FUNGUS); // Folia - region threading + } else if (this == Blocks.CRIMSON_FUNGUS) { +- SaplingBlock.treeType = org.bukkit.TreeType.CRIMSON_FUNGUS; ++ SaplingBlock.treeTypeRT.set(org.bukkit.TreeType.CRIMSON_FUNGUS); // Folia - region threading + } + return value; + }) diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/HoneyBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/HoneyBlock.java.patch new file mode 100644 index 0000000..3a4551f --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/HoneyBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/HoneyBlock.java ++++ b/net/minecraft/world/level/block/HoneyBlock.java +@@ -94,7 +_,7 @@ + } + + private void maybeDoSlideAchievement(Entity entity, BlockPos pos) { +- if (entity instanceof ServerPlayer && entity.level().getGameTime() % 20L == 0L) { ++ if (entity instanceof ServerPlayer && entity.level().getRedstoneGameTime() % 20L == 0L) { // Folia - region threading + CriteriaTriggers.HONEY_BLOCK_SLIDE.trigger((ServerPlayer)entity, entity.level().getBlockState(pos)); + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/LightningRodBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/LightningRodBlock.java.patch new file mode 100644 index 0000000..1401539 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/LightningRodBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/LightningRodBlock.java ++++ b/net/minecraft/world/level/block/LightningRodBlock.java +@@ -116,7 +_,7 @@ + @Override + public void animateTick(BlockState state, Level level, BlockPos pos, RandomSource random) { + if (level.isThundering() +- && level.random.nextInt(200) <= level.getGameTime() % 200L ++ && level.random.nextInt(200) <= level.getRedstoneGameTime() % 200L // Folia - region threading + && pos.getY() == level.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ()) - 1) { + ParticleUtils.spawnParticlesAlongAxis(state.getValue(FACING).getAxis(), level, pos, 0.125, ParticleTypes.ELECTRIC_SPARK, UniformInt.of(1, 2)); + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/MushroomBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/MushroomBlock.java.patch new file mode 100644 index 0000000..0efed8b --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/MushroomBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/MushroomBlock.java ++++ b/net/minecraft/world/level/block/MushroomBlock.java +@@ -94,7 +_,7 @@ + return false; + } else { + level.removeBlock(pos, false); +- SaplingBlock.treeType = (this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM; // CraftBukkit ++ SaplingBlock.treeTypeRT.set((this == Blocks.BROWN_MUSHROOM) ? org.bukkit.TreeType.BROWN_MUSHROOM : org.bukkit.TreeType.RED_MUSHROOM); // CraftBukkit // Folia - region threading + if (optional.get().value().place(level, level.getChunkSource().getGenerator(), random, pos)) { + return true; + } else { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/NetherPortalBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/NetherPortalBlock.java.patch new file mode 100644 index 0000000..e5a5809 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/NetherPortalBlock.java.patch @@ -0,0 +1,62 @@ +--- a/net/minecraft/world/level/block/NetherPortalBlock.java ++++ b/net/minecraft/world/level/block/NetherPortalBlock.java +@@ -181,6 +_,33 @@ + } + } + ++ // Folia start - region threading ++ @Override ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(portalTarget)) { ++ return false; ++ } ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(sourceWorld, portalPos)) { ++ return false; ++ } ++ ++ return portalTarget.netherPortalLogicAsync(portalPos); ++ } ++ ++ public static BlockUtil.FoundRectangle findPortalAround(ServerLevel world, BlockPos rough, WorldBorder worldBorder, int searchRadius) { ++ BlockPos found = world.getPortalForcer().findClosestPortalPosition(rough, worldBorder, searchRadius).orElse(null); ++ if (found == null) { ++ return null; ++ } ++ ++ BlockState portalState = world.getBlockStateFromEmptyChunk(found); ++ ++ return BlockUtil.getLargestRectangleAround(found, portalState.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (pos) -> { ++ return world.getBlockStateFromEmptyChunk(pos) == portalState; ++ }); ++ } ++ // Folia end - region threading ++ + @Nullable + private TeleportTransition getExitPortal(ServerLevel level, Entity entity, BlockPos pos, BlockPos exitPos, boolean isNether, WorldBorder worldBorder, int searchRadius, boolean canCreatePortal, int createRadius) { // CraftBukkit + Optional optional = level.getPortalForcer().findClosestPortalPosition(exitPos, worldBorder, searchRadius); // CraftBukkit +@@ -188,14 +_,14 @@ + TeleportTransition.PostTeleportTransition postTeleportTransition; + if (optional.isPresent()) { + BlockPos blockPos = optional.get(); +- BlockState blockState = level.getBlockState(blockPos); ++ BlockState blockState = level.getBlockStateFromEmptyChunk(blockPos); // Folia - region threading + largestRectangleAround = BlockUtil.getLargestRectangleAround( + blockPos, + blockState.getValue(BlockStateProperties.HORIZONTAL_AXIS), + 21, + Direction.Axis.Y, + 21, +- blockPos1 -> level.getBlockState(blockPos1) == blockState ++ blockPos1 -> level.getBlockStateFromEmptyChunk(blockPos1) == blockState // Folia - region threading + ); + postTeleportTransition = TeleportTransition.PLAY_PORTAL_SOUND.then(entity1 -> entity1.placePortalTicket(blockPos)); + } else if (canCreatePortal) { // CraftBukkit +@@ -238,7 +_,7 @@ + return createDimensionTransition(level, rectangle, axis, relativePortalPosition, entity, postTeleportTransition); + } + +- private static TeleportTransition createDimensionTransition( ++ public static TeleportTransition createDimensionTransition( // Folia - region threading - public + ServerLevel level, + BlockUtil.FoundRectangle rectangle, + Direction.Axis axis, diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Portal.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Portal.java.patch new file mode 100644 index 0000000..a64d6b4 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/Portal.java.patch @@ -0,0 +1,13 @@ +--- a/net/minecraft/world/level/block/Portal.java ++++ b/net/minecraft/world/level/block/Portal.java +@@ -14,6 +_,10 @@ + @Nullable + TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos); + ++ // Folia start - region threading ++ public boolean portalAsync(ServerLevel sourceWorld, Entity portalTarget, BlockPos portalPos); ++ // Folia end - region threading ++ + default Portal.Transition getLocalTransition() { + return Portal.Transition.NONE; + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedStoneWireBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedStoneWireBlock.java.patch new file mode 100644 index 0000000..f6bc0b3 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedStoneWireBlock.java.patch @@ -0,0 +1,80 @@ +--- a/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -91,7 +_,7 @@ + private static final float PARTICLE_DENSITY = 0.2F; + private final BlockState crossState; + private final RedstoneWireEvaluator evaluator = new DefaultRedstoneWireEvaluator(this); +- public boolean shouldSignal = true; ++ //public boolean shouldSignal = true; // Folia - region threading - move to regionised world data + + @Override + public MapCodec codec() { +@@ -293,6 +_,11 @@ + // Paper start - Optimize redstone (Eigencraft) + // The bulk of the new functionality is found in RedstoneWireTurbo.java + io.papermc.paper.redstone.RedstoneWireTurbo turbo = new io.papermc.paper.redstone.RedstoneWireTurbo(this); ++ // Folia start - region threading ++ private io.papermc.paper.redstone.RedstoneWireTurbo getTurbo(Level world) { ++ return world.getCurrentWorldData().turbo; ++ } ++ // Folia end - region threading + + /* + * Modified version of pre-existing updateSurroundingRedstone, which is called from +@@ -308,7 +_,7 @@ + if (orientation != null) { + source = pos.relative(orientation.getFront().getOpposite()); + } +- turbo.updateSurroundingRedstone(worldIn, pos, state, source); ++ getTurbo(worldIn).updateSurroundingRedstone(worldIn, pos, state, source); // Folia - region threading + return; + } + updatePowerStrength(worldIn, pos, state, orientation, blockAdded); +@@ -336,7 +_,7 @@ + // [Space Walker] suppress shape updates and emit those manually to + // bypass the new neighbor update stack. + if (level.setBlock(pos, state, Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_CLIENTS)) { +- turbo.updateNeighborShapes(level, pos, state); ++ this.getTurbo(level).updateNeighborShapes(level, pos, state); // Folia - region threading + } + } + } +@@ -353,9 +_,9 @@ + } + + public int getBlockSignal(Level level, BlockPos pos) { +- this.shouldSignal = false; ++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = false; // Folia - region threading + int bestNeighborSignal = level.getBestNeighborSignal(pos); +- this.shouldSignal = true; ++ io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal = true; // Folia - region threading + return bestNeighborSignal; + } + +@@ -450,12 +_,12 @@ + + @Override + protected int getDirectSignal(BlockState blockState, BlockGetter blockAccess, BlockPos pos, Direction side) { +- return !this.shouldSignal ? 0 : blockState.getSignal(blockAccess, pos, side); ++ return !io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal ? 0 : blockState.getSignal(blockAccess, pos, side); // Folia - region threading + } + + @Override + protected int getSignal(BlockState blockState, BlockGetter blockAccess, BlockPos pos, Direction side) { +- if (this.shouldSignal && side != Direction.DOWN) { ++ if (io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData().shouldSignal && side != Direction.DOWN) { // Folia - region threading + int powerValue = blockState.getValue(POWER); + if (powerValue == 0) { + return 0; +@@ -487,7 +_,10 @@ + + @Override + protected boolean isSignalSource(BlockState state) { +- return this.shouldSignal; ++ // Folia start - region threading ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); ++ return worldData == null || worldData.shouldSignal; ++ // Folia end - region threading + } + + public static int getColorForPower(int power) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch new file mode 100644 index 0000000..5a3c83c --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch @@ -0,0 +1,53 @@ +--- a/net/minecraft/world/level/block/RedstoneTorchBlock.java ++++ b/net/minecraft/world/level/block/RedstoneTorchBlock.java +@@ -73,10 +_,10 @@ + protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + boolean hasNeighborSignal = this.hasNeighborSignal(level, pos, state); + // Paper start - Faster redstone torch rapid clock removal +- java.util.ArrayDeque redstoneUpdateInfos = level.redstoneUpdateInfos; ++ java.util.ArrayDeque redstoneUpdateInfos = level.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading + if (redstoneUpdateInfos != null) { + RedstoneTorchBlock.Toggle curr; +- while ((curr = redstoneUpdateInfos.peek()) != null && level.getGameTime() - curr.when > 60L) { ++ while ((curr = redstoneUpdateInfos.peek()) != null && level.getRedstoneGameTime() - curr.when > 60L) { // Folia - region threading + redstoneUpdateInfos.poll(); + } + } +@@ -154,13 +_,13 @@ + + private static boolean isToggledTooFrequently(Level level, BlockPos pos, boolean logToggle) { + // Paper start - Faster redstone torch rapid clock removal +- java.util.ArrayDeque list = level.redstoneUpdateInfos; ++ java.util.ArrayDeque list = level.getCurrentWorldData().redstoneUpdateInfos; // Folia - region threading + if (list == null) { +- list = level.redstoneUpdateInfos = new java.util.ArrayDeque<>(); ++ list = level.getCurrentWorldData().redstoneUpdateInfos = new java.util.ArrayDeque<>(); // Folia - region threading + } + // Paper end - Faster redstone torch rapid clock removal + if (logToggle) { +- list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), level.getGameTime())); ++ list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), level.getRedstoneGameTime())); // Folia - region threading + } + + int i = 0; +@@ -182,12 +_,18 @@ + } + + public static class Toggle { +- final BlockPos pos; +- final long when; ++ public final BlockPos pos; // Folia - region threading ++ long when; // Folia - region threading + + public Toggle(BlockPos pos, long when) { + this.pos = pos; + this.when = when; + } ++ ++ // Folia start - region ticking ++ public void offsetTime(long offset) { ++ this.when += offset; ++ } ++ // Folia end - region ticking + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SaplingBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SaplingBlock.java.patch new file mode 100644 index 0000000..8a11256 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SaplingBlock.java.patch @@ -0,0 +1,39 @@ +--- a/net/minecraft/world/level/block/SaplingBlock.java ++++ b/net/minecraft/world/level/block/SaplingBlock.java +@@ -26,7 +_,7 @@ + protected static final float AABB_OFFSET = 6.0F; + protected static final VoxelShape SHAPE = Block.box(2.0, 0.0, 2.0, 14.0, 12.0, 14.0); + protected final TreeGrower treeGrower; +- public static org.bukkit.TreeType treeType; // CraftBukkit ++ public static final ThreadLocal treeTypeRT = new ThreadLocal<>(); // CraftBukkit // Folia - region threading + + @Override + public MapCodec codec() { +@@ -56,18 +_,19 @@ + level.setBlock(pos, state.cycle(STAGE), 4); + } else { + // CraftBukkit start +- if (level.captureTreeGeneration) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ++ if (worldData.captureTreeGeneration) { // Folia - region threading + this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); + } else { +- level.captureTreeGeneration = true; ++ worldData.captureTreeGeneration = true; // Folia - region threading + this.treeGrower.growTree(level, level.getChunkSource().getGenerator(), pos, state, random); +- level.captureTreeGeneration = false; +- if (!level.capturedBlockStates.isEmpty()) { +- org.bukkit.TreeType treeType = SaplingBlock.treeType; +- SaplingBlock.treeType = null; ++ worldData.captureTreeGeneration = false; // Folia - region threading ++ if (!worldData.capturedBlockStates.isEmpty()) { // Folia - region threading ++ org.bukkit.TreeType treeType = SaplingBlock.treeTypeRT.get(); // Folia - region threading ++ SaplingBlock.treeTypeRT.set(null); // Folia - region threading + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level.getWorld()); +- java.util.List blocks = new java.util.ArrayList<>(level.capturedBlockStates.values()); +- level.capturedBlockStates.clear(); ++ java.util.List blocks = new java.util.ArrayList<>(worldData.capturedBlockStates.values()); // Folia - region threading ++ worldData.capturedBlockStates.clear(); // Folia - region threading + org.bukkit.event.world.StructureGrowEvent event = null; + if (treeType != null) { + event = new org.bukkit.event.world.StructureGrowEvent(location, treeType, false, null, blocks); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch new file mode 100644 index 0000000..d0537b4 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java ++++ b/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java +@@ -50,7 +_,7 @@ + + @Override + protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { +- if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (net.minecraft.server.MinecraftServer.currentTick + pos.hashCode()) % level.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks ++ if (this instanceof GrassBlock && level.paperConfig().tickRates.grassSpread != 1 && (level.paperConfig().tickRates.grassSpread < 1 || (io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + pos.hashCode()) % level.paperConfig().tickRates.grassSpread != 0)) { return; } // Paper - Configurable random tick rates for blocks // Folia - regionised ticking + // Paper start - Perf: optimize dirt and snow spreading + final net.minecraft.world.level.chunk.ChunkAccess cachedBlockChunk = level.getChunkIfLoaded(pos); + if (cachedBlockChunk == null) { // Is this needed? diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch new file mode 100644 index 0000000..e953dea --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/WitherSkullBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/WitherSkullBlock.java ++++ b/net/minecraft/world/level/block/WitherSkullBlock.java +@@ -51,7 +_,7 @@ + } + + public static void checkSpawn(Level level, BlockPos pos, SkullBlockEntity blockEntity) { +- if (level.captureBlockStates) return; // CraftBukkit ++ if (level.getCurrentWorldData().captureBlockStates) return; // CraftBukkit // Folia - region threading + if (!level.isClientSide) { + BlockState blockState = blockEntity.getBlockState(); + boolean flag = blockState.is(Blocks.WITHER_SKELETON_SKULL) || blockState.is(Blocks.WITHER_SKELETON_WALL_SKULL); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch new file mode 100644 index 0000000..bad97de --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/world/level/block/entity/BeaconBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BeaconBlockEntity.java +@@ -211,7 +_,7 @@ + } + + int i = blockEntity.levels; final int originalLevels = i; // Paper - OBFHELPER +- if (level.getGameTime() % 80L == 0L) { ++ if (level.getRedstoneGameTime() % 80L == 0L) { // Folia - region threading + if (!blockEntity.beamSections.isEmpty()) { + blockEntity.levels = updateBase(level, x, y, z); + } +@@ -345,7 +_,7 @@ + list = level.getEntitiesOfClass(Player.class, aabb); // Diff from applyEffect + } else { + list = new java.util.ArrayList<>(); +- for (final Player player : level.players()) { ++ for (final Player player : level.getLocalPlayers()) { // Folia - region threading + if (!net.minecraft.world.entity.EntitySelector.NO_SPECTATORS.test(player)) continue; + if (player.getBoundingBox().intersects(aabb)) { + list.add(player); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BlockEntity.java.patch new file mode 100644 index 0000000..370e8e9 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BlockEntity.java.patch @@ -0,0 +1,33 @@ +--- a/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -26,7 +_,7 @@ + import org.slf4j.Logger; + + public abstract class BlockEntity { +- static boolean ignoreBlockEntityUpdates; // Paper - Perf: Optimize Hoppers ++ static final ThreadLocal IGNORE_TILE_UPDATES = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading + // CraftBukkit start - data containers + private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); + public org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer persistentDataContainer; +@@ -40,6 +_,12 @@ + private BlockState blockState; + private DataComponentMap components = DataComponentMap.EMPTY; + ++ // Folia start - region ticking ++ public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { ++ ++ } ++ // Folia end - region ticking ++ + public BlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + this.type = type; + this.worldPosition = pos.immutable(); +@@ -197,7 +_,7 @@ + + public void setChanged() { + if (this.level != null) { +- if (ignoreBlockEntityUpdates) return; // Paper - Perf: Optimize Hoppers ++ if (IGNORE_TILE_UPDATES.get().booleanValue()) return; // Paper - Perf: Optimize Hoppers // Folia - region threading + setChanged(this.level, this.worldPosition, this.blockState); + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch new file mode 100644 index 0000000..546e29e --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch @@ -0,0 +1,16 @@ +--- a/net/minecraft/world/level/block/entity/CommandBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/CommandBlockEntity.java +@@ -66,6 +_,13 @@ + ); + } + ++ // Folia start ++ @Override ++ public void threadCheck() { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((ServerLevel) CommandBlockEntity.this.level, CommandBlockEntity.this.worldPosition, "Asynchronous sendSystemMessage to a command block"); ++ } ++ // Folia end ++ + @Override + public boolean isValid() { + return !CommandBlockEntity.this.isRemoved(); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch new file mode 100644 index 0000000..66ccbdd --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/world/level/block/entity/ConduitBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ConduitBlockEntity.java +@@ -81,7 +_,7 @@ + + public static void clientTick(Level level, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { + blockEntity.tickCount++; +- long gameTime = level.getGameTime(); ++ long gameTime = level.getRedstoneGameTime(); // Folia - region threading + List list = blockEntity.effectBlocks; + if (gameTime % 40L == 0L) { + blockEntity.isActive = updateShape(level, pos, list); +@@ -97,7 +_,7 @@ + + public static void serverTick(Level level, BlockPos pos, BlockState state, ConduitBlockEntity blockEntity) { + blockEntity.tickCount++; +- long gameTime = level.getGameTime(); ++ long gameTime = level.getRedstoneGameTime(); // Folia - region threading + List list = blockEntity.effectBlocks; + if (gameTime % 40L == 0L) { + boolean flag = updateShape(level, pos, list); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch new file mode 100644 index 0000000..1786d77 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch @@ -0,0 +1,150 @@ +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -34,7 +_,7 @@ + private static final int[][] CACHED_SLOTS = new int[54][]; + private NonNullList items = NonNullList.withSize(5, ItemStack.EMPTY); + public int cooldownTime = -1; +- private long tickedGameTime; ++ private long tickedGameTime = Long.MIN_VALUE; // Folia - region threading + private Direction facing; + + // CraftBukkit start - add fields and methods +@@ -67,6 +_,15 @@ + } + // CraftBukkit end + ++ // Folia start - region threading ++ @Override ++ public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { ++ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ if (this.tickedGameTime != Long.MIN_VALUE) { ++ this.tickedGameTime += fromRedstoneTimeOffset; ++ } ++ } ++ // Folia end - region threading + + public HopperBlockEntity(BlockPos pos, BlockState blockState) { + super(BlockEntityType.HOPPER, pos, blockState); +@@ -125,7 +_,7 @@ + + public static void pushItemsTick(Level level, BlockPos pos, BlockState state, HopperBlockEntity blockEntity) { + blockEntity.cooldownTime--; +- blockEntity.tickedGameTime = level.getGameTime(); ++ blockEntity.tickedGameTime = level.getRedstoneGameTime(); // Folia - region threading + if (!blockEntity.isOnCooldown()) { + blockEntity.setCooldown(0); + // Spigot start +@@ -213,12 +_,11 @@ + } + + // Paper start - Perf: Optimize Hoppers +- public static boolean skipHopperEvents; +- private static boolean skipPullModeEventFire; +- private static boolean skipPushModeEventFire; ++ // Folia - region threading - moved to RegionizedWorldData + + private static boolean hopperPush(final Level level, final Container destination, final Direction direction, final HopperBlockEntity hopper) { +- skipPushModeEventFire = skipHopperEvents; ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ++ worldData.skipPushModeEventFire = worldData.skipHopperEvents; // Folia - region threading + boolean foundItem = false; + for (int i = 0; i < hopper.getContainerSize(); ++i) { + final ItemStack item = hopper.getItem(i); +@@ -233,7 +_,7 @@ + + // We only need to fire the event once to give protection plugins a chance to cancel this event + // Because nothing uses getItem, every event call should end up the same result. +- if (!skipPushModeEventFire) { ++ if (!worldData.skipPushModeEventFire) { // Folia - region threading + movedItem = callPushMoveEvent(destination, movedItem, hopper); + if (movedItem == null) { // cancelled + origItemStack.setCount(originalItemCount); +@@ -263,13 +_,14 @@ + } + + private static boolean hopperPull(final Level level, final Hopper hopper, final Container container, ItemStack origItemStack, final int i) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + ItemStack movedItem = origItemStack; + final int originalItemCount = origItemStack.getCount(); + final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); + container.setChanged(); // original logic always marks source inv as changed even if no move happens. + movedItem.setCount(movedItemCount); + +- if (!skipPullModeEventFire) { ++ if (!worldData.skipPullModeEventFire) { // Folia - region threading + movedItem = callPullMoveEvent(hopper, container, movedItem); + if (movedItem == null) { // cancelled + origItemStack.setCount(originalItemCount); +@@ -289,9 +_,9 @@ + origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); + } + +- ignoreBlockEntityUpdates = true; ++ IGNORE_TILE_UPDATES.set(true); // Folia - region threading + container.setItem(i, origItemStack); +- ignoreBlockEntityUpdates = false; ++ IGNORE_TILE_UPDATES.set(false); // Folia - region threading + container.setChanged(); + return true; + } +@@ -306,6 +_,7 @@ + + @Nullable + private static ItemStack callPushMoveEvent(Container destination, ItemStack itemStack, HopperBlockEntity hopper) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading + final org.bukkit.inventory.Inventory destinationInventory = getInventory(destination); + final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent( + hopper.getOwner(false).getInventory(), +@@ -315,7 +_,7 @@ + ); + final boolean result = event.callEvent(); + if (!event.calledGetItem && !event.calledSetItem) { +- skipPushModeEventFire = true; ++ worldData.skipPushModeEventFire = true; // Folia - region threading + } + if (!result) { + applyCooldown(hopper); +@@ -331,6 +_,7 @@ + + @Nullable + private static ItemStack callPullMoveEvent(final Hopper hopper, final Container container, final ItemStack itemstack) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading + final org.bukkit.inventory.Inventory sourceInventory = getInventory(container); + final org.bukkit.inventory.Inventory destination = getInventory(hopper); + +@@ -338,7 +_,7 @@ + final io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent event = new io.papermc.paper.event.inventory.PaperInventoryMoveItemEvent(sourceInventory, org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(itemstack), destination, false); + final boolean result = event.callEvent(); + if (!event.calledGetItem && !event.calledSetItem) { +- skipPullModeEventFire = true; ++ worldData.skipPullModeEventFire = true; // Folia - region threading + } + if (!result) { + applyCooldown(hopper); +@@ -524,12 +_,13 @@ + } + + public static boolean suckInItems(Level level, Hopper hopper) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading + BlockPos blockPos = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ()); + BlockState blockState = level.getBlockState(blockPos); + Container sourceContainer = getSourceContainer(level, hopper, blockPos, blockState); + if (sourceContainer != null) { + Direction direction = Direction.DOWN; +- skipPullModeEventFire = skipHopperEvents; // Paper - Perf: Optimize Hoppers ++ worldData.skipPullModeEventFire = worldData.skipHopperEvents; // Paper - Perf: Optimize Hoppers // Folia - region threading + + for (int i : getSlots(sourceContainer, direction)) { + if (tryTakeInItemFromSlot(hopper, sourceContainer, i, direction, level)) { // Spigot +@@ -678,9 +_,9 @@ + stack = stack.split(destination.getMaxStackSize()); + } + // Spigot end +- ignoreBlockEntityUpdates = true; // Paper - Perf: Optimize Hoppers ++ IGNORE_TILE_UPDATES.set(Boolean.TRUE); // Paper - Perf: Optimize Hoppers // Folia - region threading + destination.setItem(slot, stack); +- ignoreBlockEntityUpdates = false; // Paper - Perf: Optimize Hoppers ++ IGNORE_TILE_UPDATES.set(Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading + stack = leftover; // Paper - Make hoppers respect inventory max stack size + flag = true; + } else if (canMergeItems(item, stack)) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch new file mode 100644 index 0000000..f008c88 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch @@ -0,0 +1,14 @@ +--- a/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java +@@ -43,9 +_,9 @@ + // Paper end - Fix NPE in SculkBloomEvent world access + + public static void serverTick(Level level, BlockPos pos, BlockState state, SculkCatalystBlockEntity sculkCatalyst) { +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = sculkCatalyst.getBlockPos(); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. ++ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(sculkCatalyst.getBlockPos()); // CraftBukkit - SPIGOT-7068: Add source block override, not the most elegant way but better than passing down a BlockPosition up to five methods deep. // Folia - region threading + sculkCatalyst.catalystListener.getSculkSpreader().updateCursors(level, pos, level.getRandom(), true); +- org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverride = null; // CraftBukkit ++ org.bukkit.craftbukkit.event.CraftEventFactory.sourceBlockOverrideRT.set(null); // CraftBukkit // Folia - region threading + } + + @Override diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch new file mode 100644 index 0000000..64a6402 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch @@ -0,0 +1,246 @@ +--- a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +@@ -35,9 +_,12 @@ + public long age; + private int teleportCooldown; + @Nullable +- public BlockPos exitPortal; ++ public volatile BlockPos exitPortal; // Folia - region threading - volatile + public boolean exactTeleport; + ++ private static final java.util.concurrent.atomic.AtomicLong SEARCHING_FOR_EXIT_ID_GENERATOR = new java.util.concurrent.atomic.AtomicLong(); // Folia - region threading ++ private Long searchingForExitId; // Folia - region threading ++ + public TheEndGatewayBlockEntity(BlockPos pos, BlockState blockState) { + super(BlockEntityType.END_GATEWAY, pos, blockState); + } +@@ -129,6 +_,104 @@ + } + } + ++ // Folia start - region threading ++ private void trySearchForExit(ServerLevel world, BlockPos fromPos) { ++ if (this.searchingForExitId != null) { ++ return; ++ } ++ this.searchingForExitId = Long.valueOf(SEARCHING_FOR_EXIT_ID_GENERATOR.getAndIncrement()); ++ int chunkX = fromPos.getX() >> 4; ++ int chunkZ = fromPos.getZ() >> 4; ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, ++ chunkX, chunkZ, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, ++ this.searchingForExitId ++ ); ++ ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ ++ complete.addWaiter((tpLoc, throwable) -> { ++ // create the exit portal ++ TheEndGatewayBlockEntity.LOGGER.debug("Creating portal at {}", tpLoc); ++ TheEndGatewayBlockEntity.spawnGatewayPortal(world, tpLoc, EndGatewayConfiguration.knownExit(fromPos, false)); ++ ++ // need to go onto the tick thread to avoid saving issues ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, chunkX, chunkZ, ++ () -> { ++ // update the exit portal location ++ TheEndGatewayBlockEntity.this.exitPortal = tpLoc; ++ ++ // remove ticket keeping the gateway loaded ++ world.moonrise$getChunkTaskScheduler().chunkHolderManager.removeTicketAtLevel( ++ net.minecraft.server.level.TicketType.END_GATEWAY_EXIT_SEARCH, ++ chunkX, chunkZ, ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.BLOCK_TICKING_TICKET_LEVEL, ++ this.searchingForExitId ++ ); ++ TheEndGatewayBlockEntity.this.searchingForExitId = null; ++ } ++ ); ++ }); ++ ++ findOrCreateValidTeleportPosRegionThreading(world, fromPos, complete); ++ } ++ ++ public static boolean teleportRegionThreading(ServerLevel portalWorld, BlockPos portalPos, ++ net.minecraft.world.entity.Entity toTeleport, ++ TheEndGatewayBlockEntity portalTile, ++ net.minecraft.world.level.portal.TeleportTransition.PostTeleportTransition post) { ++ // can we even teleport in this dimension? ++ if (portalTile.exitPortal == null && portalWorld.getTypeKey() != net.minecraft.world.level.dimension.LevelStem.END) { ++ return false; ++ } ++ ++ // First, find the position we are trying to teleport to ++ BlockPos teleportPos = portalTile.exitPortal; ++ boolean isExactTeleport = portalTile.exactTeleport; ++ ++ if (teleportPos == null) { ++ portalTile.trySearchForExit(portalWorld, portalPos); ++ return false; ++ } ++ ++ // note: we handle the position from the TeleportTransition ++ net.minecraft.world.level.portal.TeleportTransition teleport = net.minecraft.world.level.block.EndGatewayBlock.getTeleportTransition( ++ portalWorld, toTeleport, Vec3.atCenterOf(teleportPos) ++ ); ++ ++ ++ if (isExactTeleport) { ++ // blind teleport ++ return toTeleport.teleportAsync( ++ teleport, net.minecraft.world.entity.Entity.TELEPORT_FLAG_LOAD_CHUNK | net.minecraft.world.entity.Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, ++ post == null ? null : (net.minecraft.world.entity.Entity teleportedEntity) -> { ++ post.onTransition(teleportedEntity); ++ } ++ ); ++ } else { ++ // we could hack around by first loading the chunks, then calling back to here and checking if the entity ++ // should be teleported, something something else... ++ // however, we know the target location cannot differ by one region section: so we can ++ // just teleport and adjust the position after ++ return toTeleport.teleportAsync( ++ teleport, net.minecraft.world.entity.Entity.TELEPORT_FLAG_LOAD_CHUNK | net.minecraft.world.entity.Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, ++ (net.minecraft.world.entity.Entity teleportedEntity) -> { ++ // adjust to the final exit position ++ Vec3 adjusted = Vec3.atCenterOf(TheEndGatewayBlockEntity.findExitPosition(portalWorld, teleportPos)); ++ // teleportTo will adjust rider positions ++ teleportedEntity.teleportTo(adjusted.x, adjusted.y, adjusted.z); ++ ++ if (post != null) { ++ post.onTransition(teleportedEntity); ++ } ++ } ++ ); ++ } ++ } ++ // Folia end - region threading ++ + @Nullable + public Vec3 getPortalPosition(ServerLevel level, BlockPos pos) { + if (this.exitPortal == null && level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { // CraftBukkit - work in alternate worlds +@@ -173,6 +_,124 @@ + + return findTallestBlock(level, blockPos, 16, true); + } ++ ++ // Folia start - region threading ++ private static void findOrCreateValidTeleportPosRegionThreading(ServerLevel world, BlockPos pos, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete) { ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable tentativeSelection = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); ++ ++ tentativeSelection.addWaiter((vec3d, throwable) -> { ++ LevelChunk chunk = TheEndGatewayBlockEntity.getChunk(world, vec3d); ++ BlockPos blockposition1 = TheEndGatewayBlockEntity.findValidSpawnInChunk(chunk); ++ if (blockposition1 == null) { ++ BlockPos blockposition2 = BlockPos.containing(vec3d.x + 0.5D, 75.0D, vec3d.z + 0.5D); ++ ++ TheEndGatewayBlockEntity.LOGGER.debug("Failed to find a suitable block to teleport to, spawning an island on {}", blockposition2); ++ world.registryAccess().lookup(Registries.CONFIGURED_FEATURE).flatMap((iregistry) -> { ++ return iregistry.get(EndFeatures.END_ISLAND); ++ }).ifPresent((holder_c) -> { ++ ((net.minecraft.world.level.levelgen.feature.ConfiguredFeature) holder_c.value()).place(world, world.getChunkSource().getGenerator(), RandomSource.create(blockposition2.asLong()), blockposition2); ++ }); ++ blockposition1 = blockposition2; ++ } else { ++ TheEndGatewayBlockEntity.LOGGER.debug("Found suitable block to teleport to: {}", blockposition1); ++ } ++ ++ // Here, there is no guarantee the chunks in 1 radius are in this region due to the fact that we just chained ++ // possibly 16x chunk loads along an axis (findExitPortalXZPosTentativeRegionThreading) using the chunk queue ++ // (regioniser only guarantees at least 8 chunks along a single axis) ++ // so, we need to schedule for the next tick ++ int posX = blockposition1.getX(); ++ int posZ = blockposition1.getZ(); ++ int radius = 16; ++ ++ BlockPos finalBlockPosition1 = blockposition1; ++ world.moonrise$loadChunksAsync(blockposition1, radius, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (java.util.List chunks) -> { ++ // make sure chunks are kept loaded ++ for (net.minecraft.world.level.chunk.ChunkAccess access : chunks) { ++ world.chunkSource.addTicketAtLevel( ++ net.minecraft.server.level.TicketType.DELAYED, access.getPos(), ++ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager.FULL_LOADED_TICKET_LEVEL, ++ net.minecraft.util.Unit.INSTANCE ++ ); ++ } ++ // now after the chunks are loaded, we can delay by one tick ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( ++ world, posX >> 4, posZ >> 4, () -> { ++ // find final location ++ BlockPos tpLoc = TheEndGatewayBlockEntity.findTallestBlock(world, finalBlockPosition1, radius, true).above(GATEWAY_HEIGHT_ABOVE_SURFACE); ++ ++ // done ++ complete.complete(tpLoc); ++ } ++ ); ++ } ++ ); ++ }); ++ ++ // fire off chain ++ findExitPortalXZPosTentativeRegionThreading(world, pos, tentativeSelection); ++ } ++ ++ private static void findExitPortalXZPosTentativeRegionThreading(ServerLevel world, BlockPos pos, ++ ca.spottedleaf.concurrentutil.completable.CallbackCompletable complete) { ++ Vec3 posDirFromOrigin = new Vec3(pos.getX(), 0.0D, pos.getZ()).normalize(); ++ Vec3 posDirExtruded = posDirFromOrigin.scale(1024.0D); ++ ++ class Vars { ++ int i = 16; ++ boolean mode = false; ++ Vec3 currPos = posDirExtruded; ++ } ++ Vars vars = new Vars(); ++ ++ Runnable handle = new Runnable() { ++ @Override ++ public void run() { ++ if (vars.mode != TheEndGatewayBlockEntity.isChunkEmpty(world, vars.currPos)) { ++ vars.i = 0; // fall back to completing ++ } ++ ++ // try to load next chunk ++ if (vars.i-- <= 0) { ++ if (vars.mode) { ++ complete.complete(vars.currPos); ++ return; ++ } ++ vars.mode = true; ++ vars.i = 16; ++ } ++ ++ vars.currPos = vars.currPos.add(posDirFromOrigin.scale(vars.mode ? 16.0 : -16.0)); ++ // schedule next iteration ++ world.moonrise$getChunkTaskScheduler().scheduleChunkLoad( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(vars.currPos), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(vars.currPos), ++ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ true, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunk) -> { ++ this.run(); ++ } ++ ); ++ } ++ }; ++ ++ // kick off first chunk load ++ world.moonrise$getChunkTaskScheduler().scheduleChunkLoad( ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(posDirExtruded), ++ ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(posDirExtruded), ++ net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ++ true, ++ ca.spottedleaf.concurrentutil.util.Priority.NORMAL, ++ (chunk) -> { ++ handle.run(); ++ } ++ ); ++ } ++ // Folia end - region threading + + private static Vec3 findExitPortalXZPosTentative(ServerLevel level, BlockPos pos) { + Vec3 vec3 = new Vec3(pos.getX(), 0.0, pos.getZ()).normalize(); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TickingBlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TickingBlockEntity.java.patch new file mode 100644 index 0000000..47b43a6 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/TickingBlockEntity.java.patch @@ -0,0 +1,9 @@ +--- a/net/minecraft/world/level/block/entity/TickingBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/TickingBlockEntity.java +@@ -10,4 +_,6 @@ + BlockPos getPos(); + + String getType(); ++ ++ BlockEntity getTileEntity(); // Folia - region threading + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch new file mode 100644 index 0000000..fccd549 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/grower/TreeGrower.java.patch @@ -0,0 +1,84 @@ +--- a/net/minecraft/world/level/block/grower/TreeGrower.java ++++ b/net/minecraft/world/level/block/grower/TreeGrower.java +@@ -203,56 +_,58 @@ + + // CraftBukkit start + private void setTreeType(Holder> holder) { ++ org.bukkit.TreeType treeType; // Folia - region threading + ResourceKey> treeFeature = holder.unwrapKey().get(); + if (treeFeature == TreeFeatures.OAK || treeFeature == TreeFeatures.OAK_BEES_005) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TREE; ++ treeType = org.bukkit.TreeType.TREE; // Folia - region threading + } else if (treeFeature == TreeFeatures.HUGE_RED_MUSHROOM) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_MUSHROOM; ++ treeType = org.bukkit.TreeType.RED_MUSHROOM; // Folia - region threading + } else if (treeFeature == TreeFeatures.HUGE_BROWN_MUSHROOM) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BROWN_MUSHROOM; ++ treeType = org.bukkit.TreeType.BROWN_MUSHROOM; // Folia - region threading + } else if (treeFeature == TreeFeatures.JUNGLE_TREE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.COCOA_TREE; ++ treeType = org.bukkit.TreeType.COCOA_TREE; // Folia - region threading + } else if (treeFeature == TreeFeatures.JUNGLE_TREE_NO_VINE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SMALL_JUNGLE; ++ treeType = org.bukkit.TreeType.SMALL_JUNGLE; // Folia - region threading + } else if (treeFeature == TreeFeatures.PINE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_REDWOOD; ++ treeType = org.bukkit.TreeType.TALL_REDWOOD; // Folia - region threading + } else if (treeFeature == TreeFeatures.SPRUCE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.REDWOOD; ++ treeType = org.bukkit.TreeType.REDWOOD; // Folia - region threading + } else if (treeFeature == TreeFeatures.ACACIA) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ACACIA; ++ treeType = org.bukkit.TreeType.ACACIA; // Folia - region threading + } else if (treeFeature == TreeFeatures.BIRCH || treeFeature == TreeFeatures.BIRCH_BEES_005) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIRCH; ++ treeType = org.bukkit.TreeType.BIRCH; // Folia - region threading + } else if (treeFeature == TreeFeatures.SUPER_BIRCH_BEES_0002) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_BIRCH; ++ treeType = org.bukkit.TreeType.TALL_BIRCH; // Folia - region threading + } else if (treeFeature == TreeFeatures.SWAMP_OAK) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.SWAMP; ++ treeType = org.bukkit.TreeType.SWAMP; // Folia - region threading + } else if (treeFeature == TreeFeatures.FANCY_OAK || treeFeature == TreeFeatures.FANCY_OAK_BEES_005) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.BIG_TREE; ++ treeType = org.bukkit.TreeType.BIG_TREE; // Folia - region threading + } else if (treeFeature == TreeFeatures.JUNGLE_BUSH) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE_BUSH; ++ treeType = org.bukkit.TreeType.JUNGLE_BUSH; // Folia - region threading + } else if (treeFeature == TreeFeatures.DARK_OAK) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.DARK_OAK; ++ treeType = org.bukkit.TreeType.DARK_OAK; // Folia - region threading + } else if (treeFeature == TreeFeatures.MEGA_SPRUCE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_REDWOOD; ++ treeType = org.bukkit.TreeType.MEGA_REDWOOD; // Folia - region threading + } else if (treeFeature == TreeFeatures.MEGA_PINE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MEGA_PINE; ++ treeType = org.bukkit.TreeType.MEGA_PINE; // Folia - region threading + } else if (treeFeature == TreeFeatures.MEGA_JUNGLE_TREE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.JUNGLE; ++ treeType = org.bukkit.TreeType.JUNGLE; // Folia - region threading + } else if (treeFeature == TreeFeatures.AZALEA_TREE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.AZALEA; ++ treeType = org.bukkit.TreeType.AZALEA; // Folia - region threading + } else if (treeFeature == TreeFeatures.MANGROVE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.MANGROVE; ++ treeType = org.bukkit.TreeType.MANGROVE; // Folia - region threading + } else if (treeFeature == TreeFeatures.TALL_MANGROVE) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.TALL_MANGROVE; ++ treeType = org.bukkit.TreeType.TALL_MANGROVE; // Folia - region threading + } else if (treeFeature == TreeFeatures.CHERRY || treeFeature == TreeFeatures.CHERRY_BEES_005) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.CHERRY; ++ treeType = org.bukkit.TreeType.CHERRY; // Folia - region threading + } else if (treeFeature == TreeFeatures.PALE_OAK || treeFeature == TreeFeatures.PALE_OAK_BONEMEAL) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK; ++ treeType = org.bukkit.TreeType.PALE_OAK; // Folia - region threading + } else if (treeFeature == TreeFeatures.PALE_OAK_CREAKING) { +- net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; ++ treeType = org.bukkit.TreeType.PALE_OAK_CREAKING; // Folia - region threading + } else { + throw new IllegalArgumentException("Unknown tree generator " + treeFeature); + } ++ net.minecraft.world.level.block.SaplingBlock.treeTypeRT.set(treeType); // Folia - region threading + } + // CraftBukkit end + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch new file mode 100644 index 0000000..2810e1b --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/block/piston/PistonBaseBlock.java ++++ b/net/minecraft/world/level/block/piston/PistonBaseBlock.java +@@ -139,7 +_,7 @@ + && pistonMovingBlockEntity.isExtending() + && ( + pistonMovingBlockEntity.getProgress(0.0F) < 0.5F +- || level.getGameTime() == pistonMovingBlockEntity.getLastTicked() ++ || level.getRedstoneGameTime() == pistonMovingBlockEntity.getLastTicked() // Folia - region threading + || ((ServerLevel)level).isHandlingTick() + )) { + i = 2; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch new file mode 100644 index 0000000..cf6a175 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch @@ -0,0 +1,43 @@ +--- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java ++++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +@@ -41,9 +_,19 @@ + private static final ThreadLocal NOCLIP = ThreadLocal.withInitial(() -> null); + private float progress; + private float progressO; +- private long lastTicked; ++ private long lastTicked = Long.MIN_VALUE; // Folia - region threading + private int deathTicks; + ++ // Folia start - region threading ++ @Override ++ public void updateTicks(long fromTickOffset, long fromRedstoneTimeOffset) { ++ super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); ++ if (this.lastTicked != Long.MIN_VALUE) { ++ this.lastTicked += fromRedstoneTimeOffset; ++ } ++ } ++ // Folia end - region threading ++ + public PistonMovingBlockEntity(BlockPos pos, BlockState blockState) { + super(BlockEntityType.PISTON, pos, blockState); + } +@@ -150,8 +_,8 @@ + + entity.setDeltaMovement(d1, d2, d3); + // Paper - EAR items stuck in slime pushed by a piston +- entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10); +- entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10); ++ entity.activatedTick = Math.max(entity.activatedTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading ++ entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() + 10); // Folia - region threading + // Paper end + break; + } +@@ -292,7 +_,7 @@ + } + + public static void tick(Level level, BlockPos pos, BlockState state, PistonMovingBlockEntity blockEntity) { +- blockEntity.lastTicked = level.getGameTime(); ++ blockEntity.lastTicked = level.getRedstoneGameTime(); // Folia - region threading + blockEntity.progressO = blockEntity.progress; + if (blockEntity.progressO >= 1.0F) { + if (level.isClientSide && blockEntity.deathTicks < 5) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/border/WorldBorder.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/border/WorldBorder.java.patch new file mode 100644 index 0000000..8c37a6f --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/border/WorldBorder.java.patch @@ -0,0 +1,31 @@ +--- a/net/minecraft/world/level/border/WorldBorder.java ++++ b/net/minecraft/world/level/border/WorldBorder.java +@@ -30,6 +_,8 @@ + public static final WorldBorder.Settings DEFAULT_SETTINGS = new WorldBorder.Settings(0.0, 0.0, 0.2, 5.0, 5, 15, 5.999997E7F, 0L, 0.0); + public net.minecraft.server.level.ServerLevel world; // CraftBukkit + ++ // Folia - region threading - TODO make this shit thread-safe ++ + public boolean isWithinBounds(BlockPos pos) { + return this.isWithinBounds(pos.getX(), pos.getZ()); + } +@@ -43,16 +_,14 @@ + } + + // Paper start - Bound treasure maps to world border +- private final BlockPos.MutableBlockPos mutPos = new BlockPos.MutableBlockPos(); ++ private static final ThreadLocal mutPos = ThreadLocal.withInitial(() -> new BlockPos.MutableBlockPos()); // Folia - region threading + + public boolean isBlockInBounds(int chunkX, int chunkZ) { +- this.mutPos.set(chunkX, 64, chunkZ); +- return this.isWithinBounds(this.mutPos); ++ return this.isWithinBounds(mutPos.get().set(chunkX, 64, chunkZ)); // Folia - region threading + } + + public boolean isChunkInBounds(int chunkX, int chunkZ) { +- this.mutPos.set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15); +- return this.isWithinBounds(this.mutPos); ++ return this.isWithinBounds(mutPos.get().set(((chunkX << 4) + 15), 64, (chunkZ << 4) + 15)); // Folia - region threading + } + // Paper end - Bound treasure maps to world border + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/ChunkGenerator.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/ChunkGenerator.java.patch new file mode 100644 index 0000000..8ee3c72 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/ChunkGenerator.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -327,7 +_,7 @@ + } + + private static boolean tryAddReference(StructureManager structureManager, StructureStart structureStart) { +- if (structureStart.canBeReferenced()) { ++ if (structureStart.tryReference()) { // Folia - region threading + structureManager.addReference(structureStart); + return true; + } else { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/LevelChunk.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/LevelChunk.java.patch new file mode 100644 index 0000000..9e07081 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/LevelChunk.java.patch @@ -0,0 +1,117 @@ +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -59,6 +_,13 @@ + public void tick() { + } + ++ // Folia start - region threading ++ @Override ++ public BlockEntity getTileEntity() { ++ return null; ++ } ++ // Folia end - region threading ++ + @Override + public boolean isRemoved() { + return true; +@@ -230,11 +_,7 @@ + + @Override + public void markUnsaved() { +- boolean isUnsaved = this.isUnsaved(); +- super.markUnsaved(); +- if (!isUnsaved) { +- this.unsavedListener.setUnsaved(this.chunkPos); +- } ++ super.markUnsaved(); // Folia - region threading - unsavedListener is not really use + } + + @Override +@@ -360,6 +_,7 @@ + + @Nullable + public BlockState setBlockState(BlockPos pos, BlockState state, boolean isMoving, boolean doPlace) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.level, pos, "Updating block asynchronously"); // Folia - region threading + // CraftBukkit end + int y = pos.getY(); + LevelChunkSection section = this.getSection(this.getSectionIndex(y)); +@@ -395,7 +_,7 @@ + } + + boolean hasBlockEntity = blockState.hasBlockEntity(); +- if (!this.level.isClientSide && !this.level.isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent ++ if (!this.level.isClientSide && !this.level.getCurrentWorldData().isBlockPlaceCancelled) { // Paper - prevent calling cleanup logic when undoing a block place upon a cancelled BlockPlaceEvent // Folia - region threading + blockState.onRemove(this.level, pos, state, isMoving); + } else if (!blockState.is(block) && hasBlockEntity) { + this.removeBlockEntity(pos); +@@ -404,7 +_,7 @@ + if (!section.getBlockState(i, i1, i2).is(block)) { + return null; + } else { +- if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. ++ if (!this.level.isClientSide && doPlace && (!this.level.getCurrentWorldData().captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. // Folia - region threading + state.onPlace(this.level, pos, blockState, isMoving); + } + +@@ -459,7 +_,7 @@ + @Nullable + public BlockEntity getBlockEntity(BlockPos pos, LevelChunk.EntityCreationType creationType) { + // CraftBukkit start +- BlockEntity blockEntity = this.level.capturedTileEntities.get(pos); ++ BlockEntity blockEntity = this.level.getCurrentWorldData().capturedTileEntities.get(pos); // Folia - region threading + if (blockEntity == null) { + blockEntity = this.blockEntities.get(pos); + } +@@ -646,13 +_,13 @@ + + org.bukkit.World world = this.level.getWorld(); + if (world != null) { +- this.level.populating = true; ++ this.level.getCurrentWorldData().populating = true; // Folia - region threading + try { + for (org.bukkit.generator.BlockPopulator populator : world.getPopulators()) { + populator.populate(world, random, bukkitChunk); + } + } finally { +- this.level.populating = false; ++ this.level.getCurrentWorldData().populating = false; // Folia - region threading + } + } + server.getPluginManager().callEvent(new org.bukkit.event.world.ChunkPopulateEvent(bukkitChunk)); +@@ -678,7 +_,7 @@ + @Override + public boolean isUnsaved() { + // Paper start - rewrite chunk system +- final long gameTime = this.level.getGameTime(); ++ final long gameTime = this.level.getRedstoneGameTime(); // Folia - region threading + if (((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.blockTicks).moonrise$isDirty(gameTime) + || ((ca.spottedleaf.moonrise.patches.chunk_system.ticks.ChunkSystemLevelChunkTicks)this.fluidTicks).moonrise$isDirty(gameTime)) { + return true; +@@ -905,6 +_,13 @@ + this.ticker = ticker; + } + ++ // Folia start - region threading ++ @Override ++ public BlockEntity getTileEntity() { ++ return this.blockEntity; ++ } ++ // Folia end - region threading ++ + @Override + public void tick() { + if (!this.blockEntity.isRemoved() && this.blockEntity.hasLevel()) { +@@ -982,6 +_,13 @@ + void rebind(TickingBlockEntity ticker) { + this.ticker = ticker; + } ++ ++ // Folia start - region threading ++ @Override ++ public BlockEntity getTileEntity() { ++ return this.ticker == null ? null : this.ticker.getTileEntity(); ++ } ++ // Folia end - region threading + + @Override + public void tick() { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch new file mode 100644 index 0000000..6ea9c0a --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java ++++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java +@@ -574,7 +_,7 @@ + } + } + +- ChunkAccess.PackedTicks ticksForSerialization = chunk.getTicksForSerialization(level.getGameTime()); ++ ChunkAccess.PackedTicks ticksForSerialization = chunk.getTicksForSerialization(level.getRedstoneGameTime()); // Folia - region threading + ShortList[] lists = Arrays.stream(chunk.getPostProcessing()) + .map(list3 -> list3 != null ? new ShortArrayList(list3) : null) + .toArray(ShortList[]::new); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch new file mode 100644 index 0000000..0b59d9a --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch @@ -0,0 +1,65 @@ +--- a/net/minecraft/world/level/dimension/end/EndDragonFight.java ++++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java +@@ -77,7 +_,7 @@ + .setPlayBossMusic(true) + .setCreateWorldFog(true); + public final ServerLevel level; +- private final BlockPos origin; ++ public final BlockPos origin; // Folia - region threading + public final ObjectArrayList gateways = new ObjectArrayList<>(); + private final BlockPattern exitPortalPattern; + private int ticksSinceDragonSeen; +@@ -162,7 +_,7 @@ + + if (!this.dragonEvent.getPlayers().isEmpty()) { + this.level.getChunkSource().addRegionTicket(TicketType.DRAGON, new ChunkPos(0, 0), 9, Unit.INSTANCE); +- boolean isArenaLoaded = this.isArenaLoaded(); ++ boolean isArenaLoaded = this.isArenaLoaded(); if (!isArenaLoaded) { return; } // Folia - region threading - don't tick if we don't own the entire region + if (this.needsStateScanning && isArenaLoaded) { + this.scanState(); + this.needsStateScanning = false; +@@ -208,6 +_,12 @@ + } + + List dragons = this.level.getDragons(); ++ // Folia start - region threading ++ // we do not want to deal with any dragons NOT nearby ++ dragons.removeIf((EnderDragon dragon) -> { ++ return !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(dragon); ++ }); ++ // Folia end - region threading + if (dragons.isEmpty()) { + this.dragonKilled = true; + } else { +@@ -323,8 +_,8 @@ + + for (int i = -8 + chunkPos.x; i <= 8 + chunkPos.x; i++) { + for (int i1 = 8 + chunkPos.z; i1 <= 8 + chunkPos.z; i1++) { +- ChunkAccess chunk = this.level.getChunk(i, i1, ChunkStatus.FULL, false); +- if (!(chunk instanceof LevelChunk)) { ++ ChunkAccess chunk = this.level.getChunkIfLoaded(i, i1); // Folia - region threading ++ if (!(chunk instanceof LevelChunk) || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, i, i1, this.level.regioniser.regionSectionChunkSize)) { + return false; + } + +@@ -496,6 +_,11 @@ + } + + public void onCrystalDestroyed(EndCrystal crystal, DamageSource dmgSrc) { ++ // Folia start - region threading ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) { ++ return; ++ } ++ // Folia end - region threading + if (this.respawnStage != null && this.respawnCrystals.contains(crystal)) { + LOGGER.debug("Aborting respawn sequence"); + this.respawnStage = null; +@@ -521,7 +_,7 @@ + + public boolean tryRespawn(@Nullable BlockPos placedEndCrystalPos) { // placedEndCrystalPos is null if the tryRespawn() call was not caused by a placed end crystal + // Paper end - Perf: Do crystal-portal proximity check before entity lookup +- if (this.dragonKilled && this.respawnStage == null) { ++ if (this.dragonKilled && this.respawnStage == null && ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level, this.origin)) { // Folia - region threading + BlockPos blockPos = this.portalLocation; + if (blockPos == null) { + LOGGER.debug("Tried to respawn, but need to find the portal first."); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch new file mode 100644 index 0000000..817bc3f --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch @@ -0,0 +1,54 @@ +--- a/net/minecraft/world/level/levelgen/PatrolSpawner.java ++++ b/net/minecraft/world/level/levelgen/PatrolSpawner.java +@@ -16,7 +_,7 @@ + import net.minecraft.world.level.block.state.BlockState; + + public class PatrolSpawner implements CustomSpawner { +- private int nextTick; ++ //private int nextTick; // Folia - region threading + + @Override + public int tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { +@@ -27,6 +_,7 @@ + return 0; + } else { + RandomSource randomSource = level.random; ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading + // this.nextTick--; + // if (this.nextTick > 0) { + // return 0; +@@ -38,12 +_,12 @@ + // } else if (randomSource.nextInt(5) != 0) { + // Paper start - Pillager patrol spawn settings and per player options + // Random player selection moved up for per player spawning and configuration +- int size = level.players().size(); ++ int size = level.getLocalPlayers().size(); + if (size < 1) { + return 0; + } + +- net.minecraft.server.level.ServerPlayer player = level.players().get(randomSource.nextInt(size)); ++ net.minecraft.server.level.ServerPlayer player = level.getLocalPlayers().get(randomSource.nextInt(size)); // Folia - region threading + if (player.isSpectator()) { + return 0; + } +@@ -53,8 +_,8 @@ + --player.patrolSpawnDelay; + patrolSpawnDelay = player.patrolSpawnDelay; + } else { +- this.nextTick--; +- patrolSpawnDelay = this.nextTick; ++ worldData.patrolSpawnerNextTick--; // Folia - region threading ++ patrolSpawnDelay = worldData.patrolSpawnerNextTick; // Folia - region threading + } + if (patrolSpawnDelay > 0) { + return 0; +@@ -68,7 +_,7 @@ + if (level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.perPlayer) { + player.patrolSpawnDelay += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); + } else { +- this.nextTick += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); ++ worldData.patrolSpawnerNextTick += level.paperConfig().entities.behavior.pillagerPatrols.spawnDelay.ticks + randomSource.nextInt(1200); // Folia - region threading + } + + if (days < level.paperConfig().entities.behavior.pillagerPatrols.start.day || !level.isDay()) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch new file mode 100644 index 0000000..66bce0e --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch @@ -0,0 +1,38 @@ +--- a/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -19,7 +_,7 @@ + import net.minecraft.world.level.material.FluidState; + + public class PhantomSpawner implements CustomSpawner { +- private int nextTick; ++ //private int nextTick; // Folia - region threading + + @Override + public int tick(ServerLevel level, boolean spawnEnemies, boolean spawnFriendlies) { +@@ -34,21 +_,22 @@ + } + // Paper end - Ability to control player's insomnia and phantoms + RandomSource randomSource = level.random; +- this.nextTick--; +- if (this.nextTick > 0) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = level.getCurrentWorldData(); // Folia - region threading ++ worldData.phantomSpawnerNextTick--; // Folia - region threading ++ if (worldData.phantomSpawnerNextTick > 0) { // Folia - region threading + return 0; + } else { + // Paper start - Ability to control player's insomnia and phantoms + int spawnAttemptMinSeconds = level.paperConfig().entities.behavior.phantomsSpawnAttemptMinSeconds; + int spawnAttemptMaxSeconds = level.paperConfig().entities.behavior.phantomsSpawnAttemptMaxSeconds; +- this.nextTick += (spawnAttemptMinSeconds + randomSource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; ++ worldData.phantomSpawnerNextTick += (spawnAttemptMinSeconds + randomSource.nextInt(spawnAttemptMaxSeconds - spawnAttemptMinSeconds + 1)) * 20; // Folia - region threading + // Paper end - Ability to control player's insomnia and phantoms + if (level.getSkyDarken() < 5 && level.dimensionType().hasSkyLight()) { + return 0; + } else { + int i = 0; + +- for (ServerPlayer serverPlayer : level.players()) { ++ for (ServerPlayer serverPlayer : level.getLocalPlayers()) { // Folia - region threading + if (!serverPlayer.isSpectator() && (!level.paperConfig().entities.behavior.phantomsDoNotSpawnOnCreativePlayers || !serverPlayer.isCreative())) { // Paper - Add phantom creative and insomniac controls + BlockPos blockPos = serverPlayer.blockPosition(); + if (!level.dimensionType().hasSkyLight() || blockPos.getY() >= level.getSeaLevel() && level.canSeeSky(blockPos)) { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch new file mode 100644 index 0000000..ca02d11 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java ++++ b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java +@@ -47,7 +_,7 @@ + + // CraftBukkit start + // SPIGOT-7746: Entity will only be null during world generation, which is async, so just generate without event +- if (entity != null) { ++ if (false) { // Folia - region threading + org.bukkit.World bworld = level.getLevel().getWorld(); + org.bukkit.event.world.PortalCreateEvent portalEvent = new org.bukkit.event.world.PortalCreateEvent((java.util.List) (java.util.List) blockList.getList(), bworld, entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.END_PLATFORM); + level.getLevel().getCraftServer().getPluginManager().callEvent(portalEvent); diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch new file mode 100644 index 0000000..b124774 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch @@ -0,0 +1,63 @@ +--- a/net/minecraft/world/level/levelgen/structure/StructureStart.java ++++ b/net/minecraft/world/level/levelgen/structure/StructureStart.java +@@ -26,7 +_,7 @@ + private final Structure structure; + private final PiecesContainer pieceContainer; + private final ChunkPos chunkPos; +- private int references; ++ private final java.util.concurrent.atomic.AtomicInteger references; // Folia - region threading + @Nullable + private volatile BoundingBox cachedBoundingBox; + +@@ -39,7 +_,7 @@ + public StructureStart(Structure structure, ChunkPos chunkPos, int references, PiecesContainer pieceContainer) { + this.structure = structure; + this.chunkPos = chunkPos; +- this.references = references; ++ this.references = new java.util.concurrent.atomic.AtomicInteger(references); // Folia - region threading + this.pieceContainer = pieceContainer; + } + +@@ -126,7 +_,7 @@ + compoundTag.putString("id", context.registryAccess().lookupOrThrow(Registries.STRUCTURE).getKey(this.structure).toString()); + compoundTag.putInt("ChunkX", chunkPos.x); + compoundTag.putInt("ChunkZ", chunkPos.z); +- compoundTag.putInt("references", this.references); ++ compoundTag.putInt("references", this.references.get()); // Folia - region threading + compoundTag.put("Children", this.pieceContainer.save(context)); + return compoundTag; + } else { +@@ -144,15 +_,29 @@ + } + + public boolean canBeReferenced() { +- return this.references < this.getMaxReferences(); +- } ++ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading ++ } ++ ++ // Folia start - region threading ++ public boolean tryReference() { ++ for (int curr = this.references.get();;) { ++ if (curr >= this.getMaxReferences()) { ++ return false; ++ } ++ ++ if (curr == (curr = this.references.compareAndExchange(curr, curr + 1))) { ++ return true; ++ } // else: try again ++ } ++ } ++ // Folia end - region threading + + public void addReference() { +- this.references++; ++ throw new UnsupportedOperationException("Use tryReference()"); // Folia - region threading + } + + public int getReferences() { +- return this.references; ++ return this.references.get(); // Folia - region threading + } + + protected int getMaxReferences() { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch new file mode 100644 index 0000000..3acbf63 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch @@ -0,0 +1,10 @@ +--- a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java ++++ b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +@@ -47,6 +_,7 @@ + } + + private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates updates) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread((net.minecraft.server.level.ServerLevel)this.level, pos, "Adding block without owning region"); // Folia - region threading + boolean flag = this.count > 0; + boolean flag1 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; + this.count++; diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/SavedData.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/SavedData.java.patch new file mode 100644 index 0000000..bae465b --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/SavedData.java.patch @@ -0,0 +1,23 @@ +--- a/net/minecraft/world/level/saveddata/SavedData.java ++++ b/net/minecraft/world/level/saveddata/SavedData.java +@@ -8,7 +_,7 @@ + import net.minecraft.util.datafix.DataFixTypes; + + public abstract class SavedData { +- private boolean dirty; ++ private volatile boolean dirty; // Folia - make map data thread-safe + + public abstract CompoundTag save(CompoundTag tag, HolderLookup.Provider registries); + +@@ -26,9 +_,10 @@ + + public CompoundTag save(HolderLookup.Provider registries) { + CompoundTag compoundTag = new CompoundTag(); ++ this.setDirty(false); // Folia - make map data thread-safe - move before save, so that any changes after are not lost + compoundTag.put("data", this.save(new CompoundTag(), registries)); + NbtUtils.addCurrentDataVersion(compoundTag); +- this.setDirty(false); ++ // Folia - make map data thread-safe - move before save, so that any changes after are not lost + return compoundTag; + } + diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapIndex.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapIndex.java.patch new file mode 100644 index 0000000..bb7c8c7 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapIndex.java.patch @@ -0,0 +1,24 @@ +--- a/net/minecraft/world/level/saveddata/maps/MapIndex.java ++++ b/net/minecraft/world/level/saveddata/maps/MapIndex.java +@@ -34,17 +_,21 @@ + + @Override + public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { ++ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe + for (Entry entry : this.usedAuxIds.object2IntEntrySet()) { + tag.putInt(entry.getKey(), entry.getIntValue()); + } ++ } // Folia - make map data thread-safe + + return tag; + } + + public MapId getFreeAuxValueForMap() { ++ synchronized (this.usedAuxIds) { // Folia - make map data thread-safe + int i = this.usedAuxIds.getInt("map") + 1; + this.usedAuxIds.put("map", i); + this.setDirty(); + return new MapId(i); ++ } // Folia - make map data thread-safe + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch new file mode 100644 index 0000000..5bbb012 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch @@ -0,0 +1,172 @@ +--- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java ++++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -201,7 +_,7 @@ + } + + @Override +- public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { ++ public synchronized CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) { // Folia - make map data thread-safe + ResourceLocation.CODEC + .encodeStart(NbtOps.INSTANCE, this.dimension.location()) + .resultOrPartial(LOGGER::error) +@@ -244,7 +_,7 @@ + return tag; + } + +- public MapItemSavedData locked() { ++ public synchronized MapItemSavedData locked() { // Folia - make map data thread-safe + MapItemSavedData mapItemSavedData = new MapItemSavedData( + this.centerX, this.centerZ, this.scale, this.trackingPosition, this.unlimitedTracking, true, this.dimension + ); +@@ -255,7 +_,7 @@ + return mapItemSavedData; + } + +- public MapItemSavedData scaled() { ++ public synchronized MapItemSavedData scaled() { // Folia - make map data thread-safe + return createFresh(this.centerX, this.centerZ, (byte)Mth.clamp(this.scale + 1, 0, 4), this.trackingPosition, this.unlimitedTracking, this.dimension); + } + +@@ -264,7 +_,8 @@ + return itemStack -> itemStack == stack || itemStack.is(stack.getItem()) && Objects.equals(mapId, itemStack.get(DataComponents.MAP_ID)); + } + +- public void tickCarriedBy(Player player, ItemStack mapStack) { ++ public synchronized void tickCarriedBy(Player player, ItemStack mapStack) { // Folia - make map data thread-safe ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(player, "Ticking map player in incorrect region"); // Folia - region threading + if (!this.carriedByPlayers.containsKey(player)) { + MapItemSavedData.HoldingPlayer holdingPlayer = new MapItemSavedData.HoldingPlayer(player); + this.carriedByPlayers.put(player, holdingPlayer); +@@ -413,7 +_,7 @@ + + private byte calculateRotation(@Nullable LevelAccessor level, double yRot) { + if (this.dimension == Level.NETHER && level != null) { +- int i = (int)(level.getLevelData().getDayTime() / 10L); ++ int i = (int)(level.dayTime() / 10L); // Folia - region threading + return (byte)(i * i * 34187121 + i * 121 >> 15 & 15); + } else { + double d = yRot < 0.0 ? yRot - 8.0 : yRot + 8.0; +@@ -447,25 +_,27 @@ + } + + @Nullable +- public Packet getUpdatePacket(MapId mapId, Player player) { ++ public synchronized Packet getUpdatePacket(MapId mapId, Player player) { // Folia - make map data thread-safe + MapItemSavedData.HoldingPlayer holdingPlayer = this.carriedByPlayers.get(player); + return holdingPlayer == null ? null : holdingPlayer.nextUpdatePacket(mapId); + } + +- public void setColorsDirty(int x, int z) { +- this.setDirty(); ++ public synchronized void setColorsDirty(int x, int z) { // Folia - make map data thread-safe ++ //this.setDirty(); // Folia - make dirty only after updating data - moved down + + for (MapItemSavedData.HoldingPlayer holdingPlayer : this.carriedBy) { + holdingPlayer.markColorsDirty(x, z); + } ++ this.setDirty(); // Folia - make dirty only after updating data - moved from above + } + +- public void setDecorationsDirty() { +- this.setDirty(); ++ public synchronized void setDecorationsDirty() { // Folia - make map data thread-safe ++ //this.setDirty(); // Folia - make dirty only after updating data - moved down + this.carriedBy.forEach(MapItemSavedData.HoldingPlayer::markDecorationsDirty); ++ this.setDirty(); // Folia - make dirty only after updating data - moved from above + } + +- public MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { ++ public synchronized MapItemSavedData.HoldingPlayer getHoldingPlayer(Player player) { // Folia - make map data thread-safe + MapItemSavedData.HoldingPlayer holdingPlayer = this.carriedByPlayers.get(player); + if (holdingPlayer == null) { + holdingPlayer = new MapItemSavedData.HoldingPlayer(player); +@@ -476,7 +_,7 @@ + return holdingPlayer; + } + +- public boolean toggleBanner(LevelAccessor accessor, BlockPos pos) { ++ public synchronized boolean toggleBanner(LevelAccessor accessor, BlockPos pos) { // Folia - make map data thread-safe + double d = pos.getX() + 0.5; + double d1 = pos.getZ() + 0.5; + int i = 1 << this.scale; +@@ -484,7 +_,7 @@ + double d3 = (d1 - this.centerZ) / i; + int i1 = 63; + if (d2 >= -63.0 && d3 >= -63.0 && d2 <= 63.0 && d3 <= 63.0) { +- MapBanner mapBanner = MapBanner.fromWorld(accessor, pos); ++ MapBanner mapBanner = accessor.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4) == null || !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(accessor.getMinecraftWorld(), pos) ? null : MapBanner.fromWorld(accessor, pos); // Folia - make map data thread-safe - don't sync load or read data we do not own + if (mapBanner == null) { + return false; + } +@@ -504,7 +_,7 @@ + return false; + } + +- public void checkBanners(BlockGetter reader, int x, int z) { ++ public synchronized void checkBanners(BlockGetter reader, int x, int z) { // Folia - make map data thread-safe + Iterator iterator = this.bannerMarkers.values().iterator(); + + while (iterator.hasNext()) { +@@ -523,13 +_,13 @@ + return this.bannerMarkers.values(); + } + +- public void removedFromFrame(BlockPos pos, int entityId) { ++ public synchronized void removedFromFrame(BlockPos pos, int entityId) { // Folia - make map data thread-safe + this.removeDecoration(getFrameKey(entityId)); + this.frameMarkers.remove(MapFrame.frameId(pos)); + this.setDirty(); + } + +- public boolean updateColor(int x, int z, byte color) { ++ public synchronized boolean updateColor(int x, int z, byte color) { // Folia - make map data thread-safe + byte b = this.colors[x + z * 128]; + if (b != color) { + this.setColor(x, z, color); +@@ -539,12 +_,12 @@ + } + } + +- public void setColor(int x, int z, byte color) { ++ public synchronized void setColor(int x, int z, byte color) { // Folia - make map data thread-safe + this.colors[x + z * 128] = color; + this.setColorsDirty(x, z); + } + +- public boolean isExplorationMap() { ++ public synchronized boolean isExplorationMap() { // Folia - make map data thread-safe + for (MapDecoration mapDecoration : this.decorations.values()) { + if (mapDecoration.type().value().explorationMapElement()) { + return true; +@@ -554,7 +_,7 @@ + return false; + } + +- public void addClientSideDecorations(List decorations) { ++ public synchronized void addClientSideDecorations(List decorations) { // Folia - make map data thread-safe + this.decorations.clear(); + this.trackedDecorationCount = 0; + +@@ -571,7 +_,7 @@ + return this.decorations.values(); + } + +- public boolean isTrackedCountOverLimit(int trackedCount) { ++ public synchronized boolean isTrackedCountOverLimit(int trackedCount) { // Folia - make map data thread-safe + return this.trackedDecorationCount >= trackedCount; + } + +@@ -726,11 +_,13 @@ + } + + public void applyToMap(MapItemSavedData savedData) { ++ synchronized (savedData) { // Folia - make map data thread-safe + for (int i = 0; i < this.width; i++) { + for (int i1 = 0; i1 < this.height; i1++) { + savedData.setColor(this.startX + i, this.startY + i1, this.mapColors[i + i1 * this.width]); + } + } ++ } // Folia - make map data thread-safe + } + } + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/level/storage/DimensionDataStorage.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/level/storage/DimensionDataStorage.java.patch new file mode 100644 index 0000000..40a48a1 --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/level/storage/DimensionDataStorage.java.patch @@ -0,0 +1,42 @@ +--- a/net/minecraft/world/level/storage/DimensionDataStorage.java ++++ b/net/minecraft/world/level/storage/DimensionDataStorage.java +@@ -51,6 +_,7 @@ + } + + public T computeIfAbsent(SavedData.Factory factory, String name) { ++ synchronized (this.cache) { // Folia - make map data thread-safe + T savedData = this.get(factory, name); + if (savedData != null) { + return savedData; +@@ -59,10 +_,12 @@ + this.set(name, savedData1); + return savedData1; + } ++ } // Folia - make map data thread-safe + } + + @Nullable + public T get(SavedData.Factory factory, String name) { ++ synchronized (this.cache) { // Folia - make map data thread-safe + Optional optional = this.cache.get(name); + if (optional == null) { + optional = Optional.ofNullable(this.readSavedData(factory.deserializer(), factory.type(), name)); +@@ -70,6 +_,7 @@ + } + + return (T)optional.orElse(null); ++ } // Folia - make map data thread-safe + } + + @Nullable +@@ -88,8 +_,10 @@ + } + + public void set(String name, SavedData savedData) { ++ synchronized (this.cache) { // Folia - make map data thread-safe + this.cache.put(name, Optional.of(savedData)); + savedData.setDirty(); ++ } // Folia - make map data thread-safe + } + + public CompoundTag readTagFromDisk(String filename, DataFixTypes dataFixType, int version) throws IOException { diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelChunkTicks.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelChunkTicks.java.patch new file mode 100644 index 0000000..aaa148a --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelChunkTicks.java.patch @@ -0,0 +1,24 @@ +--- a/net/minecraft/world/ticks/LevelChunkTicks.java ++++ b/net/minecraft/world/ticks/LevelChunkTicks.java +@@ -48,6 +_,21 @@ + this.dirty = false; + } + // Paper end - rewrite chunk system ++ // Folia start - region threading ++ public void offsetTicks(final long offset) { ++ if (offset == 0 || this.tickQueue.isEmpty()) { ++ return; ++ } ++ final ScheduledTick[] queue = this.tickQueue.toArray(new ScheduledTick[0]); ++ this.tickQueue.clear(); ++ for (final ScheduledTick entry : queue) { ++ final ScheduledTick newEntry = new ScheduledTick<>( ++ entry.type(), entry.pos(), entry.triggerTick() + offset, entry.subTickOrder() ++ ); ++ this.tickQueue.add(newEntry); ++ } ++ } ++ // Folia end - region threading + + public LevelChunkTicks() { + } diff --git a/folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelTicks.java.patch b/folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelTicks.java.patch new file mode 100644 index 0000000..19d94eb --- /dev/null +++ b/folia-server/minecraft-patches/sources/net/minecraft/world/ticks/LevelTicks.java.patch @@ -0,0 +1,102 @@ +--- a/net/minecraft/world/ticks/LevelTicks.java ++++ b/net/minecraft/world/ticks/LevelTicks.java +@@ -39,12 +_,69 @@ + private final List> alreadyRunThisTick = new ArrayList<>(); + private final Set> toRunThisTickSet = new ObjectOpenCustomHashSet<>(ScheduledTick.UNIQUE_TICK_HASH); + private final BiConsumer, ScheduledTick> chunkScheduleUpdater = (levelChunkTicks, scheduledTick) -> { +- if (scheduledTick.equals(levelChunkTicks.peek())) { +- this.updateContainerScheduling(scheduledTick); ++ if (scheduledTick.equals(levelChunkTicks.peek())) { // Folia - diff on change ++ this.updateContainerScheduling(scheduledTick); // Folia - diff on change + } + }; + +- public LevelTicks(LongPredicate tickCheck) { ++ // Folia start - region threading ++ public final net.minecraft.server.level.ServerLevel world; ++ public final boolean isBlock; ++ ++ public void merge(final LevelTicks into, final long tickOffset) { ++ // note: containersToTick, toRunThisTick, alreadyRunThisTick, toRunThisTickSet ++ // are all transient state, only ever non-empty during tick. But merging regions occurs while there ++ // is no tick happening, so we assume they are empty. ++ for (final java.util.Iterator>> iterator = ++ ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ final LevelChunkTicks tickContainer = entry.getValue(); ++ tickContainer.offsetTicks(tickOffset); ++ into.allContainers.put(entry.getLongKey(), tickContainer); ++ } ++ for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2LongMap.Entry entry = iterator.next(); ++ into.nextTickForContainer.put(entry.getLongKey(), entry.getLongValue() + tickOffset); ++ } ++ } ++ ++ public void split(final int chunkToRegionShift, ++ final it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap> regionToData) { ++ for (final java.util.Iterator>> iterator = ++ ((Long2ObjectOpenHashMap>)this.allContainers).long2ObjectEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2ObjectMap.Entry> entry = iterator.next(); ++ ++ final long chunkKey = entry.getLongKey(); ++ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey); ++ ++ final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey( ++ chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift ++ ); ++ // Should always be non-null, since containers are removed on unload. ++ regionToData.get(regionSectionKey).allContainers.put(chunkKey, entry.getValue()); ++ } ++ for (final java.util.Iterator iterator = ((Long2LongOpenHashMap)this.nextTickForContainer).long2LongEntrySet().fastIterator(); ++ iterator.hasNext();) { ++ final Long2LongMap.Entry entry = iterator.next(); ++ final long chunkKey = entry.getLongKey(); ++ final int chunkX = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(chunkKey); ++ final int chunkZ = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(chunkKey); ++ ++ final long regionSectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkKey( ++ chunkX >> chunkToRegionShift, chunkZ >> chunkToRegionShift ++ ); ++ ++ // Should always be non-null, since containers are removed on unload. ++ regionToData.get(regionSectionKey).nextTickForContainer.put(chunkKey, entry.getLongValue()); ++ } ++ } ++ // Folia end - region threading ++ ++ public LevelTicks(LongPredicate tickCheck, net.minecraft.server.level.ServerLevel world, boolean isBlock) { this.world = world; this.isBlock = isBlock; // Folia - add world and isBlock + this.tickCheck = tickCheck; + } + +@@ -56,7 +_,17 @@ + this.nextTickForContainer.put(packedChunkPos, scheduledTick.triggerTick()); + } + +- chunkTicks.setOnTickAdded(this.chunkScheduleUpdater); ++ // Folia start - region threading ++ final boolean isBlock = this.isBlock; ++ final net.minecraft.server.level.ServerLevel world = this.world; ++ // make sure the lambda contains no reference to this LevelTicks ++ chunkTicks.setOnTickAdded((LevelChunkTicks levelChunkTicks, ScheduledTick tick) -> { ++ if (tick.equals(levelChunkTicks.peek())) { ++ io.papermc.paper.threadedregions.RegionizedWorldData worldData = world.getCurrentWorldData(); ++ ((LevelTicks)(isBlock ? worldData.getBlockLevelTicks() : worldData.getFluidLevelTicks())).updateContainerScheduling(tick); ++ } ++ }); ++ // Folia end - region threading + } + + public void removeContainer(ChunkPos chunkPos) { +@@ -70,6 +_,7 @@ + + @Override + public void schedule(ScheduledTick tick) { ++ ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this.world, tick.pos(), "Cannot schedule tick for another region!"); // Folia - region threading + long packedChunkPos = ChunkPos.asLong(tick.pos()); + LevelChunkTicks levelChunkTicks = this.allContainers.get(packedChunkPos); + if (levelChunkTicks == null) {