diff --git a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch index 3de10a1ff1..a79638003a 100644 --- a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch @@ -927,24 +927,22 @@ this.onServerExit(); } -@@ -889,9 +1336,16 @@ +@@ -889,7 +1336,14 @@ } private boolean haveTime() { - return this.runningTask() || Util.getNanos() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTimeNanos : this.nextTickTimeNanos); + // CraftBukkit start + return this.forceTicks || this.runningTask() || Util.getNanos() < (this.mayHaveDelayedTasks ? this.delayedTasksMaxNextTickTimeNanos : this.nextTickTimeNanos); - } - ++ } ++ + private void executeModerately() { + this.runAllTasks(); + java.util.concurrent.locks.LockSupport.parkNanos("executing tasks", 1000L); + // CraftBukkit end -+ } -+ - public static boolean throwIfFatalException() { - RuntimeException runtimeexception = (RuntimeException) MinecraftServer.fatalException.get(); + } + public static boolean throwIfFatalException() { @@ -903,7 +1357,7 @@ } @@ -1061,11 +1059,25 @@ ObjectArrayList objectarraylist = new ObjectArrayList(j); int k = Mth.nextInt(this.random, 0, list.size() - j); -@@ -1154,24 +1623,58 @@ +@@ -1154,24 +1623,72 @@ this.getPlayerList().getPlayers().forEach((entityplayer) -> { entityplayer.connection.suspendFlushing(); }); + this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit ++ // Paper start - Folia scheduler API ++ ((io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler) Bukkit.getGlobalRegionScheduler()).tick(); ++ getAllLevels().forEach(level -> { ++ for (final Entity entity : level.getEntities().getAll()) { ++ if (entity.isRemoved()) { ++ continue; ++ } ++ final org.bukkit.craftbukkit.entity.CraftEntity bukkit = entity.getBukkitEntityRaw(); ++ if (bukkit != null) { ++ bukkit.taskScheduler.executeTick(); ++ } ++ } ++ }); ++ // Paper end - Folia scheduler API + io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.CALLBACK_MANAGER.handleQueue(this.tickCount); // Paper gameprofilerfiller.push("commandFunctions"); this.getFunctions().tick(); @@ -1121,7 +1133,7 @@ gameprofilerfiller.push("tick"); -@@ -1186,7 +1689,9 @@ +@@ -1186,7 +1703,9 @@ gameprofilerfiller.pop(); gameprofilerfiller.pop(); @@ -1131,7 +1143,7 @@ gameprofilerfiller.popPush("connection"); this.tickConnection(); -@@ -1267,6 +1772,22 @@ +@@ -1267,6 +1786,22 @@ return (ServerLevel) this.levels.get(key); } @@ -1154,7 +1166,7 @@ public Set> levelKeys() { return this.levels.keySet(); } -@@ -1296,7 +1817,7 @@ +@@ -1296,7 +1831,7 @@ @DontObfuscate public String getServerModName() { @@ -1163,7 +1175,7 @@ } public SystemReport fillSystemReport(SystemReport details) { -@@ -1347,7 +1868,7 @@ +@@ -1347,7 +1882,7 @@ @Override public void sendSystemMessage(Component message) { @@ -1172,7 +1184,7 @@ } public KeyPair getKeyPair() { -@@ -1385,11 +1906,14 @@ +@@ -1385,11 +1920,14 @@ } } @@ -1192,7 +1204,7 @@ } } -@@ -1403,7 +1927,7 @@ +@@ -1403,7 +1941,7 @@ while (iterator.hasNext()) { ServerLevel worldserver = (ServerLevel) iterator.next(); @@ -1201,7 +1213,7 @@ } } -@@ -1481,10 +2005,20 @@ +@@ -1481,10 +2019,20 @@ @Override public String getMotd() { @@ -1223,7 +1235,7 @@ this.motd = motd; } -@@ -1507,7 +2041,7 @@ +@@ -1507,7 +2055,7 @@ } public ServerConnectionListener getConnection() { @@ -1232,7 +1244,7 @@ } public boolean isReady() { -@@ -1593,7 +2127,7 @@ +@@ -1593,7 +2141,7 @@ @Override public void executeIfPossible(Runnable runnable) { if (this.isStopped()) { @@ -1241,7 +1253,7 @@ } else { super.executeIfPossible(runnable); } -@@ -1632,13 +2166,19 @@ +@@ -1632,13 +2180,19 @@ return this.functionManager; } @@ -1263,7 +1275,7 @@ }, this).thenCompose((immutablelist) -> { MultiPackResourceManager resourcemanager = new MultiPackResourceManager(PackType.SERVER_DATA, immutablelist); List> list = TagLoader.loadTagsForExistingRegistries(resourcemanager, this.registries.compositeAccess()); -@@ -1654,17 +2194,21 @@ +@@ -1654,17 +2208,21 @@ }).thenAcceptAsync((minecraftserver_reloadableresources) -> { this.resources.close(); this.resources = minecraftserver_reloadableresources; @@ -1285,7 +1297,7 @@ }, this); if (this.isSameThread()) { -@@ -1789,14 +2333,15 @@ +@@ -1789,14 +2347,15 @@ if (this.isEnforceWhitelist()) { PlayerList playerlist = source.getServer().getPlayerList(); UserWhiteList whitelist = playerlist.getWhiteList(); @@ -1303,7 +1315,7 @@ } } -@@ -1952,7 +2497,7 @@ +@@ -1952,7 +2511,7 @@ final List list = Lists.newArrayList(); final GameRules gamerules = this.getGameRules(); @@ -1312,7 +1324,7 @@ @Override public > void visit(GameRules.Key key, GameRules.Type type) { list.add(String.format(Locale.ROOT, "%s=%s\n", key.getId(), gamerules.getRule(key))); -@@ -2058,7 +2603,7 @@ +@@ -2058,7 +2617,7 @@ try { label51: { @@ -1321,19 +1333,22 @@ try { arraylist = Lists.newArrayList(NativeModuleLister.listModules()); -@@ -2108,6 +2653,21 @@ - - } - +@@ -2105,9 +2664,24 @@ + if (bufferedwriter != null) { + bufferedwriter.close(); + } ++ ++ } ++ + // CraftBukkit start + public boolean isDebugging() { + return false; + } -+ + + public static MinecraftServer getServer() { + return SERVER; // Paper -+ } -+ + } + + @Deprecated + public static RegistryAccess getDefaultRegistryAccess() { + return CraftRegistry.getMinecraftRegistry(); @@ -1343,7 +1358,7 @@ private ProfilerFiller createProfiler() { if (this.willStartRecordingMetrics) { this.metricsRecorder = ActiveMetricsRecorder.createStarted(new ServerMetricsSamplersProvider(Util.timeSource, this.isDedicatedServer()), Util.timeSource, Util.ioPool(), new MetricsPersister("server"), this.onMetricsRecordingStopped, (path) -> { -@@ -2225,18 +2785,24 @@ +@@ -2225,18 +2799,24 @@ } public void logChatMessage(Component message, ChatType.Bound params, @Nullable String prefix) { @@ -1372,7 +1387,7 @@ } public boolean logIPs() { -@@ -2377,6 +2943,32 @@ +@@ -2377,6 +2957,32 @@ } public static record ServerResourcePackInfo(UUID id, String url, String hash, boolean isRequired, @Nullable Component prompt) { diff --git a/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch index 24a5526970..5f4d47ba53 100644 --- a/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/players/PlayerList.java.patch @@ -397,7 +397,7 @@ if (advancementdataplayer != null) { advancementdataplayer.save(); -@@ -334,95 +525,209 @@ +@@ -334,95 +525,210 @@ } @@ -489,6 +489,7 @@ - this.server.getCustomBossEvents().onPlayerDisconnect(player); - UUID uuid = player.getUUID(); + worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); ++ entityplayer.retireScheduler(); // Paper - Folia schedulers + entityplayer.getAdvancements().stopListening(); + this.players.remove(entityplayer); + this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot @@ -645,7 +646,7 @@ if (entityplayer1 != null) { set.add(entityplayer1); -@@ -431,72 +736,160 @@ +@@ -431,72 +737,160 @@ Iterator iterator1 = set.iterator(); while (iterator1.hasNext()) { @@ -827,7 +828,7 @@ return entityplayer1; } -@@ -516,15 +909,32 @@ +@@ -516,15 +910,32 @@ } public void sendPlayerPermissionLevel(ServerPlayer player) { @@ -862,7 +863,7 @@ this.sendAllPlayerInfoIn = 0; } -@@ -541,6 +951,25 @@ +@@ -541,6 +952,25 @@ } @@ -888,7 +889,7 @@ public void broadcastAll(Packet packet, ResourceKey dimension) { Iterator iterator = this.players.iterator(); -@@ -554,7 +983,7 @@ +@@ -554,7 +984,7 @@ } @@ -897,7 +898,7 @@ PlayerTeam scoreboardteam = source.getTeam(); if (scoreboardteam != null) { -@@ -573,7 +1002,7 @@ +@@ -573,7 +1003,7 @@ } } @@ -906,7 +907,7 @@ PlayerTeam scoreboardteam = source.getTeam(); if (scoreboardteam == null) { -@@ -619,7 +1048,7 @@ +@@ -619,7 +1049,7 @@ } public void deop(GameProfile profile) { @@ -915,7 +916,7 @@ ServerPlayer entityplayer = this.getPlayer(profile.getId()); if (entityplayer != null) { -@@ -629,6 +1058,11 @@ +@@ -629,6 +1059,11 @@ } private void sendPlayerPermissionLevel(ServerPlayer player, int permissionLevel) { @@ -927,7 +928,7 @@ if (player.connection != null) { byte b0; -@@ -643,36 +1077,53 @@ +@@ -643,36 +1078,53 @@ player.connection.send(new ClientboundEntityEventPacket(player, b0)); } @@ -994,7 +995,7 @@ if (entityplayer != player && entityplayer.level().dimension() == worldKey) { double d4 = x - entityplayer.getX(); double d5 = y - entityplayer.getY(); -@@ -687,10 +1138,12 @@ +@@ -687,10 +1139,12 @@ } public void saveAll() { @@ -1007,7 +1008,7 @@ } public UserWhiteList getWhiteList() { -@@ -712,15 +1165,19 @@ +@@ -712,15 +1166,19 @@ public void reloadWhiteList() {} public void sendLevelInfo(ServerPlayer player, ServerLevel world) { @@ -1031,7 +1032,7 @@ } player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.LEVEL_CHUNKS_LOAD_START, 0.0F)); -@@ -729,8 +1186,16 @@ +@@ -729,8 +1187,16 @@ public void sendAllPlayerInfo(ServerPlayer player) { player.inventoryMenu.sendAllDataToRemote(); @@ -1049,7 +1050,7 @@ } public int getPlayerCount() { -@@ -746,6 +1211,7 @@ +@@ -746,6 +1212,7 @@ } public void setUsingWhiteList(boolean whitelistEnabled) { @@ -1057,7 +1058,7 @@ this.doWhiteList = whitelistEnabled; } -@@ -786,12 +1252,36 @@ +@@ -786,12 +1253,36 @@ } public void removeAll() { @@ -1096,7 +1097,7 @@ public void broadcastSystemMessage(Component message, boolean overlay) { this.broadcastSystemMessage(message, (entityplayer) -> { return message; -@@ -819,24 +1309,43 @@ +@@ -819,24 +1310,43 @@ } public void broadcastChatMessage(PlayerChatMessage message, ServerPlayer sender, ChatType.Bound params) { @@ -1143,7 +1144,7 @@ } if (flag1 && sender != null) { -@@ -845,20 +1354,27 @@ +@@ -845,20 +1355,27 @@ } @@ -1176,7 +1177,7 @@ Path path = file2.toPath(); if (FileUtil.isPathNormalized(path) && FileUtil.isPathPortable(path) && path.startsWith(file.getPath()) && file2.isFile()) { -@@ -867,7 +1383,7 @@ +@@ -867,7 +1384,7 @@ } serverstatisticmanager = new ServerStatsCounter(this.server, file1); @@ -1185,7 +1186,7 @@ } return serverstatisticmanager; -@@ -875,13 +1391,13 @@ +@@ -875,13 +1392,13 @@ public PlayerAdvancements getPlayerAdvancements(ServerPlayer player) { UUID uuid = player.getUUID(); @@ -1201,7 +1202,7 @@ } advancementdataplayer.setPlayer(player); -@@ -932,15 +1448,28 @@ +@@ -932,15 +1449,28 @@ } public void reloadResources() { diff --git a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch index 72c0028352..5fb0fc68d5 100644 --- a/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch +++ b/paper-server/patches/sources/net/minecraft/world/entity/Entity.java.patch @@ -18,7 +18,7 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.FenceGateBlock; -@@ -138,9 +138,142 @@ +@@ -138,9 +138,153 @@ import net.minecraft.world.scores.ScoreHolder; import net.minecraft.world.scores.Team; import org.slf4j.Logger; @@ -61,7 +61,7 @@ +// CraftBukkit end public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess, ScoreHolder { -+ + + // CraftBukkit start + private static final int CURRENT_LEVEL = 2; + public boolean preserveMotion = true; // Paper - Fix Entity Teleportation and cancel velocity if teleported; keep initial motion on first setPositionRotation @@ -93,7 +93,7 @@ + public net.minecraft.world.level.levelgen.PositionalRandomFactory forkPositional() { + return new net.minecraft.world.level.levelgen.LegacyRandomSource.LegacyPositionalRandomFactory(this.nextLong()); + } - ++ + // these below are added to fix reobf issues that I don't wanna deal with right now + @Override + public int next(int bits) { @@ -147,10 +147,21 @@ + + public CraftEntity getBukkitEntity() { + if (this.bukkitEntity == null) { -+ this.bukkitEntity = CraftEntity.getEntity(this.level.getCraftServer(), this); ++ // Paper start - Folia schedulers ++ synchronized (this) { ++ if (this.bukkitEntity == null) { ++ return this.bukkitEntity = CraftEntity.getEntity(this.level.getCraftServer(), this); ++ } ++ } ++ // Paper end - Folia schedulers + } + return this.bukkitEntity; + } ++ // Paper start ++ public CraftEntity getBukkitEntityRaw() { ++ return this.bukkitEntity; ++ } ++ // Paper end + + // CraftBukkit - SPIGOT-6907: re-implement LivingEntity#setMaximumAir() + public int getDefaultMaxAirSupply() { @@ -161,7 +172,7 @@ private static final Logger LOGGER = LogUtils.getLogger(); public static final String ID_TAG = "id"; public static final String PASSENGERS_TAG = "Passengers"; -@@ -224,7 +357,7 @@ +@@ -224,7 +368,7 @@ private static final EntityDataAccessor DATA_CUSTOM_NAME_VISIBLE = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor DATA_SILENT = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor DATA_NO_GRAVITY = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.BOOLEAN); @@ -170,7 +181,7 @@ private static final EntityDataAccessor DATA_TICKS_FROZEN = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.INT); private EntityInLevelCallback levelCallback; private final VecDeltaCodec packetPositionCodec; -@@ -253,7 +386,67 @@ +@@ -253,7 +397,67 @@ private final List movementThisTick; private final Set blocksInside; private final LongSet visitedBlocks; @@ -238,7 +249,7 @@ public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); this.passengers = ImmutableList.of(); -@@ -261,7 +454,7 @@ +@@ -261,7 +465,7 @@ this.bb = Entity.INITIAL_AABB; this.stuckSpeedMultiplier = Vec3.ZERO; this.nextStep = 1.0F; @@ -247,7 +258,7 @@ this.remainingFireTicks = -this.getFireImmuneTicks(); this.fluidHeight = new Object2DoubleArrayMap(2); this.fluidOnEyes = new HashSet(); -@@ -284,6 +477,13 @@ +@@ -284,6 +488,13 @@ this.position = Vec3.ZERO; this.blockPosition = BlockPos.ZERO; this.chunkPosition = ChunkPos.ZERO; @@ -261,7 +272,7 @@ SynchedEntityData.Builder datawatcher_a = new SynchedEntityData.Builder(this); datawatcher_a.define(Entity.DATA_SHARED_FLAGS_ID, (byte) 0); -@@ -292,7 +492,7 @@ +@@ -292,7 +503,7 @@ datawatcher_a.define(Entity.DATA_CUSTOM_NAME, Optional.empty()); datawatcher_a.define(Entity.DATA_SILENT, false); datawatcher_a.define(Entity.DATA_NO_GRAVITY, false); @@ -270,7 +281,7 @@ datawatcher_a.define(Entity.DATA_TICKS_FROZEN, 0); this.defineSynchedData(datawatcher_a); this.entityData = datawatcher_a.build(); -@@ -362,20 +562,36 @@ +@@ -362,20 +573,36 @@ } public void kill(ServerLevel world) { @@ -309,7 +320,7 @@ public boolean equals(Object object) { return object instanceof Entity ? ((Entity) object).id == this.id : false; } -@@ -385,22 +601,34 @@ +@@ -385,22 +612,34 @@ } public void remove(Entity.RemovalReason reason) { @@ -349,7 +360,7 @@ return this.getPose() == pose; } -@@ -417,6 +645,33 @@ +@@ -417,6 +656,33 @@ } public void setRot(float yaw, float pitch) { @@ -383,7 +394,7 @@ this.setYRot(yaw % 360.0F); this.setXRot(pitch % 360.0F); } -@@ -426,8 +681,8 @@ +@@ -426,8 +692,8 @@ } public void setPos(double x, double y, double z) { @@ -394,7 +405,7 @@ } protected final AABB makeBoundingBox() { -@@ -460,12 +715,22 @@ +@@ -460,12 +726,22 @@ public void tick() { this.baseTick(); @@ -417,7 +428,7 @@ this.inBlockState = null; if (this.isPassenger() && this.getVehicle().isRemoved()) { this.stopRiding(); -@@ -475,7 +740,7 @@ +@@ -475,7 +751,7 @@ --this.boardingCooldown; } @@ -426,7 +437,7 @@ if (this.canSpawnSprintParticle()) { this.spawnSprintParticle(); } -@@ -502,7 +767,7 @@ +@@ -502,7 +778,7 @@ this.setRemainingFireTicks(this.remainingFireTicks - 1); } @@ -435,7 +446,7 @@ this.setTicksFrozen(0); this.level().levelEvent((Player) null, 1009, this.blockPosition, 1); } -@@ -514,6 +779,10 @@ +@@ -514,6 +790,10 @@ if (this.isInLava()) { this.lavaHurt(); this.fallDistance *= 0.5F; @@ -446,7 +457,7 @@ } this.checkBelowWorld(); -@@ -525,7 +794,7 @@ +@@ -525,7 +805,7 @@ world = this.level(); if (world instanceof ServerLevel worldserver) { if (this instanceof Leashable) { @@ -455,7 +466,7 @@ } } -@@ -537,7 +806,11 @@ +@@ -537,7 +817,11 @@ } public void checkBelowWorld() { @@ -468,7 +479,7 @@ this.onBelowWorld(); } -@@ -568,15 +841,32 @@ +@@ -568,15 +852,32 @@ public void lavaHurt() { if (!this.fireImmune()) { @@ -503,7 +514,7 @@ } } -@@ -587,9 +877,25 @@ +@@ -587,9 +888,25 @@ } public final void igniteForSeconds(float seconds) { @@ -530,7 +541,7 @@ public void igniteForTicks(int ticks) { if (this.remainingFireTicks < ticks) { this.setRemainingFireTicks(ticks); -@@ -610,7 +916,7 @@ +@@ -610,7 +927,7 @@ } protected void onBelowWorld() { @@ -539,7 +550,7 @@ } public boolean isFree(double offsetX, double offsetY, double offsetZ) { -@@ -672,6 +978,7 @@ +@@ -672,6 +989,7 @@ } public void move(MoverType type, Vec3 movement) { @@ -547,7 +558,7 @@ if (this.noPhysics) { this.setPos(this.getX() + movement.x, this.getY() + movement.y, this.getZ() + movement.z); } else { -@@ -750,6 +1057,28 @@ +@@ -750,6 +1068,28 @@ } } @@ -576,7 +587,7 @@ if (!this.level().isClientSide() || this.isControlledByLocalInstance()) { Entity.MovementEmission entity_movementemission = this.getMovementEmission(); -@@ -913,7 +1242,7 @@ +@@ -913,7 +1253,7 @@ } protected BlockPos getOnPos(float offset) { @@ -585,15 +596,17 @@ BlockPos blockposition = (BlockPos) this.mainSupportingBlockPos.get(); if (offset <= 1.0E-5F) { -@@ -1133,6 +1462,20 @@ - return SoundEvents.GENERIC_SPLASH; - } +@@ -1131,8 +1471,22 @@ + protected SoundEvent getSwimHighSpeedSplashSound() { + return SoundEvents.GENERIC_SPLASH; ++ } ++ + // CraftBukkit start - Add delegate methods + public SoundEvent getSwimSound0() { + return this.getSwimSound(); -+ } -+ + } + + public SoundEvent getSwimSplashSound0() { + return this.getSwimSplashSound(); + } @@ -606,7 +619,7 @@ public void recordMovementThroughBlocks(Vec3 oldPos, Vec3 newPos) { this.movementThisTick.add(new Entity.Movement(oldPos, newPos)); } -@@ -1599,6 +1942,7 @@ +@@ -1599,6 +1953,7 @@ this.setXRot(Mth.clamp(pitch, -90.0F, 90.0F) % 360.0F); this.yRotO = this.getYRot(); this.xRotO = this.getXRot(); @@ -614,7 +627,7 @@ } public void absMoveTo(double x, double y, double z) { -@@ -1609,6 +1953,7 @@ +@@ -1609,6 +1964,7 @@ this.yo = y; this.zo = d4; this.setPos(d3, y, d4); @@ -622,7 +635,7 @@ } public void moveTo(Vec3 pos) { -@@ -1628,11 +1973,19 @@ +@@ -1628,11 +1984,19 @@ } public void moveTo(double x, double y, double z, float yaw, float pitch) { @@ -642,7 +655,7 @@ } public final void setOldPosAndRot() { -@@ -1701,6 +2054,7 @@ +@@ -1701,6 +2065,7 @@ public void push(Entity entity) { if (!this.isPassengerOfSameVehicle(entity)) { if (!entity.noPhysics && !this.noPhysics) { @@ -650,7 +663,7 @@ double d0 = entity.getX() - this.getX(); double d1 = entity.getZ() - this.getZ(); double d2 = Mth.absMax(d0, d1); -@@ -1737,7 +2091,21 @@ +@@ -1737,7 +2102,21 @@ } public void push(double deltaX, double deltaY, double deltaZ) { @@ -673,7 +686,7 @@ this.hasImpulse = true; } -@@ -1858,9 +2226,21 @@ +@@ -1858,9 +2237,21 @@ } public boolean isPushable() { @@ -695,7 +708,7 @@ public void awardKillScore(Entity entityKilled, DamageSource damageSource) { if (entityKilled instanceof ServerPlayer) { CriteriaTriggers.ENTITY_KILLED_PLAYER.trigger((ServerPlayer) entityKilled, this, damageSource); -@@ -1889,74 +2269,133 @@ +@@ -1889,74 +2280,133 @@ } public boolean saveAsPassenger(CompoundTag nbt) { @@ -852,7 +865,7 @@ } ListTag nbttaglist; -@@ -1972,10 +2411,10 @@ +@@ -1972,10 +2422,10 @@ nbttaglist.add(StringTag.valueOf(s)); } @@ -865,7 +878,7 @@ if (this.isVehicle()) { nbttaglist = new ListTag(); iterator = this.getPassengers().iterator(); -@@ -1984,17 +2423,44 @@ +@@ -1984,17 +2434,44 @@ Entity entity = (Entity) iterator.next(); CompoundTag nbttagcompound1 = new CompoundTag(); @@ -913,7 +926,7 @@ } catch (Throwable throwable) { CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT"); CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being saved"); -@@ -2080,6 +2546,71 @@ +@@ -2080,6 +2557,71 @@ } else { throw new IllegalStateException("Entity has invalid position"); } @@ -985,7 +998,7 @@ } catch (Throwable throwable) { CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT"); CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Entity being loaded"); -@@ -2101,6 +2632,12 @@ +@@ -2101,6 +2643,12 @@ return entitytypes.canSerialize() && minecraftkey != null ? minecraftkey.toString() : null; } @@ -998,7 +1011,7 @@ protected abstract void readAdditionalSaveData(CompoundTag nbt); protected abstract void addAdditionalSaveData(CompoundTag nbt); -@@ -2153,9 +2690,31 @@ +@@ -2153,9 +2701,31 @@ if (stack.isEmpty()) { return null; } else { @@ -1031,7 +1044,7 @@ world.addFreshEntity(entityitem); return entityitem; } -@@ -2184,7 +2743,16 @@ +@@ -2184,7 +2754,16 @@ if (this.isAlive() && this instanceof Leashable leashable) { if (leashable.getLeashHolder() == player) { if (!this.level().isClientSide()) { @@ -1049,7 +1062,7 @@ leashable.removeLeash(); } else { leashable.dropLeash(); -@@ -2200,6 +2768,13 @@ +@@ -2200,6 +2779,13 @@ if (itemstack.is(Items.LEAD) && leashable.canHaveALeashAttachedToIt()) { if (!this.level().isClientSide()) { @@ -1063,7 +1076,7 @@ leashable.setLeashedTo(player, true); } -@@ -2265,15 +2840,15 @@ +@@ -2265,15 +2851,15 @@ } public boolean showVehicleHealth() { @@ -1082,7 +1095,7 @@ return false; } else { for (Entity entity1 = entity; entity1.vehicle != null; entity1 = entity1.vehicle) { -@@ -2285,11 +2860,32 @@ +@@ -2285,11 +2871,32 @@ if (!force && (!this.canRide(entity) || !entity.canAddPassenger(this))) { return false; } else { @@ -1116,7 +1129,7 @@ this.vehicle = entity; this.vehicle.addPassenger(this); entity.getIndirectPassengersStream().filter((entity2) -> { -@@ -2314,19 +2910,30 @@ +@@ -2314,19 +2921,30 @@ } public void removeVehicle() { @@ -1149,7 +1162,7 @@ protected void addPassenger(Entity passenger) { if (passenger.getVehicle() != this) { throw new IllegalStateException("Use x.startRiding(y), not y.addPassenger(x)"); -@@ -2349,21 +2956,53 @@ +@@ -2349,21 +2967,53 @@ } } @@ -1209,7 +1222,7 @@ } protected boolean canAddPassenger(Entity passenger) { -@@ -2464,7 +3103,7 @@ +@@ -2464,7 +3114,7 @@ if (teleporttransition != null) { ServerLevel worldserver1 = teleporttransition.newLevel(); @@ -1218,7 +1231,7 @@ this.teleport(teleporttransition); } } -@@ -2547,7 +3186,7 @@ +@@ -2547,7 +3197,7 @@ } public boolean isCrouching() { @@ -1227,7 +1240,7 @@ } public boolean isSprinting() { -@@ -2563,7 +3202,7 @@ +@@ -2563,7 +3213,7 @@ } public boolean isVisuallySwimming() { @@ -1236,7 +1249,7 @@ } public boolean isVisuallyCrawling() { -@@ -2571,6 +3210,13 @@ +@@ -2571,6 +3221,13 @@ } public void setSwimming(boolean swimming) { @@ -1250,7 +1263,7 @@ this.setSharedFlag(4, swimming); } -@@ -2609,6 +3255,7 @@ +@@ -2609,6 +3266,7 @@ @Nullable public PlayerTeam getTeam() { @@ -1258,7 +1271,7 @@ return this.level().getScoreboard().getPlayersTeam(this.getScoreboardName()); } -@@ -2624,8 +3271,12 @@ +@@ -2624,8 +3282,12 @@ return this.getTeam() != null ? this.getTeam().isAlliedTo(team) : false; } @@ -1272,7 +1285,7 @@ } public boolean getSharedFlag(int index) { -@@ -2644,7 +3295,7 @@ +@@ -2644,7 +3306,7 @@ } public int getMaxAirSupply() { @@ -1281,7 +1294,7 @@ } public int getAirSupply() { -@@ -2652,7 +3303,18 @@ +@@ -2652,7 +3314,18 @@ } public void setAirSupply(int air) { @@ -1301,7 +1314,7 @@ } public int getTicksFrozen() { -@@ -2679,11 +3341,44 @@ +@@ -2679,11 +3352,44 @@ public void thunderHit(ServerLevel world, LightningBolt lightning) { this.setRemainingFireTicks(this.remainingFireTicks + 1); @@ -1348,7 +1361,7 @@ } public void onAboveBubbleCol(boolean drag) { -@@ -2713,7 +3408,7 @@ +@@ -2713,7 +3419,7 @@ this.resetFallDistance(); } @@ -1357,7 +1370,7 @@ return true; } -@@ -2818,7 +3513,7 @@ +@@ -2818,7 +3524,7 @@ public String toString() { String s = this.level() == null ? "~NULL~" : this.level().toString(); @@ -1366,7 +1379,7 @@ } public final boolean isInvulnerableToBase(DamageSource damageSource) { -@@ -2838,6 +3533,13 @@ +@@ -2838,6 +3544,13 @@ } public void restoreFrom(Entity original) { @@ -1380,7 +1393,7 @@ CompoundTag nbttagcompound = original.saveWithoutId(new CompoundTag()); nbttagcompound.remove("Dimension"); -@@ -2850,8 +3552,57 @@ +@@ -2850,8 +3563,57 @@ public Entity teleport(TeleportTransition teleportTarget) { Level world = this.level(); @@ -1438,7 +1451,7 @@ ServerLevel worldserver1 = teleportTarget.newLevel(); boolean flag = worldserver1.dimension() != worldserver.dimension(); -@@ -2918,10 +3669,19 @@ +@@ -2918,10 +3680,19 @@ gameprofilerfiller.pop(); return null; } else { @@ -1459,7 +1472,7 @@ Iterator iterator1 = list1.iterator(); while (iterator1.hasNext()) { -@@ -2947,7 +3707,7 @@ +@@ -2947,7 +3718,7 @@ } private void sendTeleportTransitionToRidingPlayers(TeleportTransition teleportTarget) { @@ -1468,7 +1481,7 @@ Iterator iterator = this.getIndirectPassengers().iterator(); while (iterator.hasNext()) { -@@ -2995,9 +3755,17 @@ +@@ -2995,9 +3766,17 @@ } protected void removeAfterChangingDimensions() { @@ -1489,7 +1502,7 @@ } } -@@ -3006,11 +3774,34 @@ +@@ -3006,11 +3785,34 @@ return PortalShape.getRelativePosition(portalRect, portalAxis, this.position(), this.getDimensions(this.getPose())); } @@ -1524,7 +1537,7 @@ if (from.dimension() == Level.END && to.dimension() == Level.OVERWORLD) { Iterator iterator = this.getPassengers().iterator(); -@@ -3134,10 +3925,16 @@ +@@ -3134,10 +3936,16 @@ return (Boolean) this.entityData.get(Entity.DATA_CUSTOM_NAME_VISIBLE); } @@ -1544,7 +1557,7 @@ return entity != null; } -@@ -3187,7 +3984,7 @@ +@@ -3187,7 +3995,7 @@ /** @deprecated */ @Deprecated protected void fixupDimensions() { @@ -1553,7 +1566,7 @@ EntityDimensions entitysize = this.getDimensions(entitypose); this.dimensions = entitysize; -@@ -3196,7 +3993,7 @@ +@@ -3196,7 +4004,7 @@ public void refreshDimensions() { EntityDimensions entitysize = this.dimensions; @@ -1562,7 +1575,7 @@ EntityDimensions entitysize1 = this.getDimensions(entitypose); this.dimensions = entitysize1; -@@ -3258,10 +4055,29 @@ +@@ -3258,10 +4066,29 @@ } public final void setBoundingBox(AABB boundingBox) { @@ -1594,7 +1607,7 @@ return this.getDimensions(pose).eyeHeight(); } -@@ -3300,7 +4116,14 @@ +@@ -3300,7 +4127,14 @@ public void startSeenByPlayer(ServerPlayer player) {} @@ -1610,7 +1623,7 @@ public float rotate(Rotation rotation) { float f = Mth.wrapDegrees(this.getYRot()); -@@ -3335,7 +4158,7 @@ +@@ -3335,7 +4169,7 @@ } @Nullable @@ -1619,7 +1632,7 @@ return null; } -@@ -3373,20 +4196,34 @@ +@@ -3373,20 +4207,34 @@ } private Stream getIndirectPassengersStream() { @@ -1654,7 +1667,7 @@ return () -> { return this.getIndirectPassengersStream().iterator(); }; -@@ -3399,6 +4236,7 @@ +@@ -3399,6 +4247,7 @@ } public boolean hasExactlyOnePlayerPassenger() { @@ -1662,7 +1675,7 @@ return this.countPlayerPassengers() == 1; } -@@ -3435,7 +4273,7 @@ +@@ -3435,7 +4284,7 @@ } public boolean isControlledByLocalInstance() { @@ -1671,7 +1684,7 @@ if (entityliving instanceof Player entityhuman) { return entityhuman.isLocalPlayer(); -@@ -3445,7 +4283,7 @@ +@@ -3445,7 +4294,7 @@ } public boolean isControlledByClient() { @@ -1680,7 +1693,7 @@ return entityliving != null && entityliving.isControlledByClient(); } -@@ -3463,7 +4301,7 @@ +@@ -3463,7 +4312,7 @@ return new Vec3((double) f1 * d2 / (double) f3, 0.0D, (double) f2 * d2 / (double) f3); } @@ -1689,7 +1702,7 @@ return new Vec3(this.getX(), this.getBoundingBox().maxY, this.getZ()); } -@@ -3489,8 +4327,37 @@ +@@ -3489,8 +4338,37 @@ return 1; } @@ -1728,7 +1741,7 @@ } public void lookAt(EntityAnchorArgument.Anchor anchorPoint, Vec3 target) { -@@ -3551,6 +4418,11 @@ +@@ -3551,6 +4429,11 @@ vec3d = vec3d.add(vec3d1); ++k1; } @@ -1740,7 +1753,7 @@ } } } -@@ -3613,7 +4485,7 @@ +@@ -3613,7 +4496,7 @@ return new ClientboundAddEntityPacket(this, entityTrackerEntry); } @@ -1749,7 +1762,7 @@ return this.type.getDimensions(); } -@@ -3714,7 +4586,39 @@ +@@ -3714,7 +4597,39 @@ return this.getZ((2.0D * this.random.nextDouble() - 1.0D) * widthScale); } @@ -1789,7 +1802,7 @@ if (this.position.x != x || this.position.y != y || this.position.z != z) { this.position = new Vec3(x, y, z); int i = Mth.floor(x); -@@ -3732,6 +4636,12 @@ +@@ -3732,6 +4647,12 @@ this.levelCallback.onMove(); } @@ -1802,7 +1815,7 @@ } public void checkDespawn() {} -@@ -3818,8 +4728,16 @@ +@@ -3818,8 +4739,17 @@ @Override public final void setRemoved(Entity.RemovalReason reason) { @@ -1814,13 +1827,14 @@ + public final void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) { + CraftEventFactory.callEntityRemoveEvent(this, cause); + // CraftBukkit end ++ final boolean alreadyRemoved = this.removalReason != null; // Paper - Folia schedulers if (this.removalReason == null) { - this.removalReason = reason; + this.removalReason = entity_removalreason; } if (this.removalReason.shouldDestroy()) { -@@ -3827,8 +4745,8 @@ +@@ -3827,14 +4757,30 @@ } this.getPassengers().forEach(Entity::stopRiding); @@ -1828,10 +1842,32 @@ - this.onRemoval(reason); + this.levelCallback.onRemove(entity_removalreason); + this.onRemoval(entity_removalreason); ++ // Paper start - Folia schedulers ++ if (!(this instanceof ServerPlayer) && entity_removalreason != RemovalReason.CHANGED_DIMENSION && !alreadyRemoved) { ++ // Players need to be special cased, because they are regularly removed from the world ++ this.retireScheduler(); ++ } ++ // Paper end - Folia schedulers } public void unsetRemoved() { -@@ -3887,7 +4805,7 @@ + this.removalReason = null; + } + ++ // Paper start - Folia schedulers ++ /** ++ * Invoked only when the entity is truly removed from the server, never to be added to any world. ++ */ ++ public final void retireScheduler() { ++ // we need to force create the bukkit entity so that the scheduler can be retired... ++ this.getBukkitEntity().taskScheduler.retire(); ++ } ++ // Paper end - Folia schedulers ++ + @Override + public void setLevelCallback(EntityInLevelCallback changeListener) { + this.levelCallback = changeListener; +@@ -3887,7 +4833,7 @@ } public Vec3 getKnownMovement() { @@ -1840,7 +1876,7 @@ if (entityliving instanceof Player entityhuman) { if (this.isAlive()) { -@@ -3962,4 +4880,14 @@ +@@ -3962,4 +4908,14 @@ void accept(Entity entity, double x, double y, double z); } diff --git a/paper-server/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/paper-server/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java index d2dee700f2..834b85f24d 100644 --- a/paper-server/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java +++ b/paper-server/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java @@ -263,6 +263,22 @@ class PaperPluginInstanceManager { + pluginName + " (Is it up to date?)", ex, plugin); // Paper } + // Paper start - Folia schedulers + try { + this.server.getGlobalRegionScheduler().cancelTasks(plugin); + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while cancelling global tasks for " + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + + try { + this.server.getAsyncScheduler().cancelTasks(plugin); + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while cancelling async tasks for " + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + // Paper end - Folia schedulers + try { this.server.getServicesManager().unregisterAll(plugin); } catch (Throwable ex) { diff --git a/paper-server/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java b/paper-server/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java new file mode 100644 index 0000000000..c03608fec9 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/threadedregions/EntityScheduler.java @@ -0,0 +1,181 @@ +package io.papermc.paper.threadedregions; + +import ca.spottedleaf.concurrentutil.util.Validate; +import ca.spottedleaf.moonrise.common.util.TickThread; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.world.entity.Entity; +import org.bukkit.craftbukkit.entity.CraftEntity; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * An entity can move between worlds with an arbitrary tick delay, be temporarily removed + * for players (i.e end credits), be partially removed from world state (i.e inactive but not removed), + * teleport between ticking regions, teleport between worlds (which will change the underlying Entity object + * for non-players), and even be removed entirely from the server. The uncertainty of an entity's state can make + * it difficult to schedule tasks without worrying about undefined behaviors resulting from any of the states listed + * previously. + * + *

+ * This class is designed to eliminate those states by providing an interface to run tasks only when an entity + * is contained in a world, on the owning thread for the region, and by providing the current Entity object. + * The scheduler also allows a task to provide a callback, the "retired" callback, that will be invoked + * if the entity is removed before a task that was scheduled could be executed. The scheduler is also + * completely thread-safe, allowing tasks to be scheduled from any thread context. The scheduler also indicates + * properly whether a task was scheduled successfully (i.e scheduler not retired), thus the code scheduling any task + * knows whether the given callbacks will be invoked eventually or not - which may be critical for off-thread + * contexts. + *

+ */ +public final class EntityScheduler { + + /** + * The Entity. Note that it is the CraftEntity, since only that class properly tracks world transfers. + */ + public final CraftEntity entity; + + private static final record ScheduledTask(Consumer run, Consumer retired) {} + + private long tickCount = 0L; + private static final long RETIRED_TICK_COUNT = -1L; + private final Object stateLock = new Object(); + private final Long2ObjectOpenHashMap> oneTimeDelayed = new Long2ObjectOpenHashMap<>(); + + private final ArrayDeque currentlyExecuting = new ArrayDeque<>(); + + public EntityScheduler(final CraftEntity entity) { + this.entity = Validate.notNull(entity); + } + + /** + * Retires the scheduler, preventing new tasks from being scheduled and invoking the retired callback + * on all currently scheduled tasks. + * + *

+ * Note: This should only be invoked after synchronously removing the entity from the world. + *

+ * + * @throws IllegalStateException If the scheduler is already retired. + */ + public void retire() { + synchronized (this.stateLock) { + if (this.tickCount == RETIRED_TICK_COUNT) { + throw new IllegalStateException("Already retired"); + } + this.tickCount = RETIRED_TICK_COUNT; + } + + final Entity thisEntity = this.entity.getHandleRaw(); + + // correctly handle and order retiring while running executeTick + for (int i = 0, len = this.currentlyExecuting.size(); i < len; ++i) { + final ScheduledTask task = this.currentlyExecuting.pollFirst(); + final Consumer retireTask = (Consumer)task.retired; + if (retireTask == null) { + continue; + } + + retireTask.accept(thisEntity); + } + + for (final List tasks : this.oneTimeDelayed.values()) { + for (int i = 0, len = tasks.size(); i < len; ++i) { + final ScheduledTask task = tasks.get(i); + final Consumer retireTask = (Consumer)task.retired; + if (retireTask == null) { + continue; + } + + retireTask.accept(thisEntity); + } + } + } + + /** + * Schedules a task with the given delay. If the task failed to schedule because the scheduler is retired (entity + * removed), then returns {@code false}. Otherwise, either the run callback will be invoked after the specified delay, + * or the retired callback will be invoked if the scheduler is retired. + * Note that the retired callback is invoked in critical code, so it should not attempt to remove the entity, remove + * other entities, load chunks, load worlds, modify ticket levels, etc. + * + *

+ * It is guaranteed that the run and retired callback are invoked on the region which owns the entity. + *

+ *

+ * The run and retired callback take an Entity parameter representing the current object entity that the scheduler + * is tied to. Since the scheduler is transferred when an entity changes dimensions, it is possible the entity parameter + * is not the same when the task was first scheduled. Thus, only the parameter provided should be used. + *

+ * @param run The callback to run after the specified delay, may not be null. + * @param retired Retire callback to run if the entity is retired before the run callback can be invoked, may be null. + * @param delay The delay in ticks before the run callback is invoked. Any value less-than 1 is treated as 1. + * @return {@code true} if the task was scheduled, which means that either the run function or the retired function + * will be invoked (but never both), or {@code false} indicating neither the run nor retired function will be invoked + * since the scheduler has been retired. + */ + public boolean schedule(final Consumer run, final Consumer retired, final long delay) { + Validate.notNull(run, "Run task may not be null"); + + final ScheduledTask task = new ScheduledTask(run, retired); + synchronized (this.stateLock) { + if (this.tickCount == RETIRED_TICK_COUNT) { + return false; + } + this.oneTimeDelayed.computeIfAbsent(this.tickCount + Math.max(1L, delay), (final long keyInMap) -> { + return new ArrayList<>(); + }).add(task); + } + + return true; + } + + /** + * Executes a tick for the scheduler. + * + * @throws IllegalStateException If the scheduler is retired. + */ + public void executeTick() { + final Entity thisEntity = this.entity.getHandleRaw(); + + TickThread.ensureTickThread(thisEntity, "May not tick entity scheduler asynchronously"); + final List toRun; + synchronized (this.stateLock) { + if (this.tickCount == RETIRED_TICK_COUNT) { + throw new IllegalStateException("Ticking retired scheduler"); + } + ++this.tickCount; + if (this.oneTimeDelayed.isEmpty()) { + toRun = null; + } else { + toRun = this.oneTimeDelayed.remove(this.tickCount); + } + } + + if (toRun != null) { + for (int i = 0, len = toRun.size(); i < len; ++i) { + this.currentlyExecuting.addLast(toRun.get(i)); + } + } + + // Note: It is allowed for the tasks executed to retire the entity in a given task. + for (int i = 0, len = this.currentlyExecuting.size(); i < len; ++i) { + if (!TickThread.isTickThreadFor(thisEntity)) { + // tp has been queued sync by one of the tasks + // in this case, we need to delay the tasks for next tick + break; + } + final ScheduledTask task = this.currentlyExecuting.pollFirst(); + + if (this.tickCount != RETIRED_TICK_COUNT) { + ((Consumer)task.run).accept(thisEntity); + } else { + // retired synchronously + // note: here task is null + break; + } + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FallbackRegionScheduler.java b/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FallbackRegionScheduler.java new file mode 100644 index 0000000000..94056d61a3 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FallbackRegionScheduler.java @@ -0,0 +1,30 @@ +package io.papermc.paper.threadedregions.scheduler; + +import org.bukkit.World; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +public final class FallbackRegionScheduler implements RegionScheduler { + + @Override + public void execute(@NotNull final Plugin plugin, @NotNull final World world, final int chunkX, final int chunkZ, @NotNull final Runnable run) { + plugin.getServer().getGlobalRegionScheduler().execute(plugin, run); + } + + @Override + public @NotNull ScheduledTask run(@NotNull final Plugin plugin, @NotNull final World world, final int chunkX, final int chunkZ, @NotNull final Consumer task) { + return plugin.getServer().getGlobalRegionScheduler().run(plugin, task); + } + + @Override + public @NotNull ScheduledTask runDelayed(@NotNull final Plugin plugin, @NotNull final World world, final int chunkX, final int chunkZ, @NotNull final Consumer task, final long delayTicks) { + return plugin.getServer().getGlobalRegionScheduler().runDelayed(plugin, task, delayTicks); + } + + @Override + public @NotNull ScheduledTask runAtFixedRate(@NotNull final Plugin plugin, @NotNull final World world, final int chunkX, final int chunkZ, @NotNull final Consumer task, final long initialDelayTicks, final long periodTicks) { + return plugin.getServer().getGlobalRegionScheduler().runAtFixedRate(plugin, task, initialDelayTicks, periodTicks); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java b/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java new file mode 100644 index 0000000000..374abffb9f --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaAsyncScheduler.java @@ -0,0 +1,328 @@ +package io.papermc.paper.threadedregions.scheduler; + +import ca.spottedleaf.concurrentutil.util.Validate; +import com.mojang.logging.LogUtils; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; +import org.slf4j.Logger; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.logging.Level; + +public final class FoliaAsyncScheduler implements AsyncScheduler { + + private static final Logger LOGGER = LogUtils.getClassLogger(); + + private final Executor executors = new ThreadPoolExecutor(Math.max(4, Runtime.getRuntime().availableProcessors() / 2), Integer.MAX_VALUE, + 30L, TimeUnit.SECONDS, new SynchronousQueue<>(), + new ThreadFactory() { + private final AtomicInteger idGenerator = new AtomicInteger(); + + @Override + public Thread newThread(final Runnable run) { + final Thread ret = new Thread(run); + + ret.setName("Folia Async Scheduler Thread #" + this.idGenerator.getAndIncrement()); + ret.setPriority(Thread.NORM_PRIORITY - 1); + ret.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { + LOGGER.error("Uncaught exception in thread: " + thread.getName(), thr); + }); + + return ret; + } + } + ); + + private final ScheduledExecutorService timerThread = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(final Runnable run) { + final Thread ret = new Thread(run); + + ret.setName("Folia Async Scheduler Thread Timer"); + ret.setPriority(Thread.NORM_PRIORITY + 1); + ret.setUncaughtExceptionHandler((final Thread thread, final Throwable thr) -> { + LOGGER.error("Uncaught exception in thread: " + thread.getName(), thr); + }); + + return ret; + } + }); + + private final Set tasks = ConcurrentHashMap.newKeySet(); + + @Override + public ScheduledTask runNow(final Plugin plugin, final Consumer task) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + final AsyncScheduledTask ret = new AsyncScheduledTask(plugin, -1L, task, null, -1L); + + this.tasks.add(ret); + this.executors.execute(ret); + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public ScheduledTask runDelayed(final Plugin plugin, final Consumer task, final long delay, + final TimeUnit unit) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + Validate.notNull(unit, "Time unit may not be null"); + if (delay < 0L) { + throw new IllegalArgumentException("Delay may not be < 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + return this.scheduleTimerTask(plugin, task, delay, -1L, unit); + } + + @Override + public ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer task, final long initialDelay, + final long period, final TimeUnit unit) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(task, "Task may not be null"); + Validate.notNull(unit, "Time unit may not be null"); + if (initialDelay < 0L) { + throw new IllegalArgumentException("Initial delay may not be < 0"); + } + if (period <= 0L) { + throw new IllegalArgumentException("Period may not be <= 0"); + } + + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register task while disabled"); + } + + return this.scheduleTimerTask(plugin, task, initialDelay, period, unit); + } + + private AsyncScheduledTask scheduleTimerTask(final Plugin plugin, final Consumer task, final long initialDelay, + final long period, final TimeUnit unit) { + final AsyncScheduledTask ret = new AsyncScheduledTask( + plugin, period <= 0 ? period : unit.toNanos(period), task, null, + System.nanoTime() + unit.toNanos(initialDelay) + ); + + synchronized (ret) { + // even though ret is not published, we need to synchronise while scheduling to avoid a race condition + // for when a scheduled task immediately executes before we update the delay field and state field + ret.setDelay(this.timerThread.schedule(ret, initialDelay, unit)); + this.tasks.add(ret); + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public void cancelTasks(final Plugin plugin) { + Validate.notNull(plugin, "Plugin may not be null"); + + for (final AsyncScheduledTask task : this.tasks) { + if (task.plugin == plugin) { + task.cancel(); + } + } + } + + private final class AsyncScheduledTask implements ScheduledTask, Runnable { + + private static final int STATE_ON_TIMER = 0; + private static final int STATE_SCHEDULED_EXECUTOR = 1; + private static final int STATE_EXECUTING = 2; + private static final int STATE_EXECUTING_CANCELLED = 3; + private static final int STATE_FINISHED = 4; + private static final int STATE_CANCELLED = 5; + + private final Plugin plugin; + private final long repeatDelay; // in ns + private Consumer run; + private ScheduledFuture delay; + private int state; + private long scheduleTarget; + + public AsyncScheduledTask(final Plugin plugin, final long repeatDelay, final Consumer run, + final ScheduledFuture delay, final long firstTarget) { + this.plugin = plugin; + this.repeatDelay = repeatDelay; + this.run = run; + this.delay = delay; + this.state = delay == null ? STATE_SCHEDULED_EXECUTOR : STATE_ON_TIMER; + this.scheduleTarget = firstTarget; + } + + private void setDelay(final ScheduledFuture delay) { + this.delay = delay; + this.state = STATE_SCHEDULED_EXECUTOR; + } + + @Override + public void run() { + final boolean repeating = this.isRepeatingTask(); + // try to advance state + final boolean timer; + synchronized (this) { + if (this.state == STATE_ON_TIMER) { + timer = true; + this.delay = null; + this.state = STATE_SCHEDULED_EXECUTOR; + } else if (this.state != STATE_SCHEDULED_EXECUTOR) { + // cancelled + if (this.state != STATE_CANCELLED) { + throw new IllegalStateException("Wrong state: " + this.state); + } + return; + } else { + timer = false; + this.state = STATE_EXECUTING; + } + } + + if (timer) { + // the scheduled executor is single thread, and unfortunately not expandable with threads + // so we just schedule onto the executor + FoliaAsyncScheduler.this.executors.execute(this); + return; + } + + try { + this.run.accept(this); + } catch (final Throwable throwable) { + this.plugin.getLogger().log(Level.WARNING, "Async task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable); + } finally { + boolean removeFromTasks = false; + synchronized (this) { + if (!repeating) { + // only want to execute once, so we're done + removeFromTasks = true; + this.state = STATE_FINISHED; + } else if (this.state != STATE_EXECUTING_CANCELLED) { + this.state = STATE_ON_TIMER; + // account for any delays, whether it be by task exec. or scheduler issues so that we keep + // the fixed schedule + final long currTime = System.nanoTime(); + final long delay = Math.max(0L, this.scheduleTarget + this.repeatDelay - currTime); + this.scheduleTarget = currTime + delay; + this.delay = FoliaAsyncScheduler.this.timerThread.schedule(this, delay, TimeUnit.NANOSECONDS); + } else { + // cancelled repeating task + removeFromTasks = true; + } + } + + if (removeFromTasks) { + this.run = null; + FoliaAsyncScheduler.this.tasks.remove(this); + } + } + } + + @Override + public Plugin getOwningPlugin() { + return this.plugin; + } + + @Override + public boolean isRepeatingTask() { + return this.repeatDelay > 0L; + } + + @Override + public CancelledState cancel() { + ScheduledFuture delay = null; + CancelledState ret; + synchronized (this) { + switch (this.state) { + case STATE_ON_TIMER: { + delay = this.delay; + this.delay = null; + this.state = STATE_CANCELLED; + ret = CancelledState.CANCELLED_BY_CALLER; + break; + } + case STATE_SCHEDULED_EXECUTOR: { + this.state = STATE_CANCELLED; + ret = CancelledState.CANCELLED_BY_CALLER; + break; + } + case STATE_EXECUTING: { + if (!this.isRepeatingTask()) { + return CancelledState.RUNNING; + } + this.state = STATE_EXECUTING_CANCELLED; + return CancelledState.NEXT_RUNS_CANCELLED; + } + 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: " + this.state); + } + } + } + + if (delay != null) { + delay.cancel(false); + } + this.run = null; + FoliaAsyncScheduler.this.tasks.remove(this); + return ret; + } + + @Override + public ExecutionState getExecutionState() { + synchronized (this) { + switch (this.state) { + case STATE_ON_TIMER: + case STATE_SCHEDULED_EXECUTOR: + 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: " + this.state); + } + } + } + } + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java b/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java new file mode 100644 index 0000000000..0117549628 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaEntityScheduler.java @@ -0,0 +1,268 @@ +package io.papermc.paper.threadedregions.scheduler; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import net.minecraft.world.entity.Entity; +import org.bukkit.craftbukkit.entity.CraftEntity; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.VarHandle; +import java.util.function.Consumer; +import java.util.logging.Level; + +public final class FoliaEntityScheduler implements EntityScheduler { + + private final CraftEntity entity; + + public FoliaEntityScheduler(final CraftEntity entity) { + this.entity = entity; + } + + private static Consumer wrap(final Plugin plugin, final Runnable runnable) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(runnable, "Runnable may not be null"); + + return (final Entity nmsEntity) -> { + if (!plugin.isEnabled()) { + // don't execute if the plugin is disabled + return; + } + try { + runnable.run(); + } catch (final Throwable throwable) { + plugin.getLogger().log(Level.WARNING, "Entity task for " + plugin.getDescription().getFullName() + " generated an exception", throwable); + } + }; + } + + @Override + public boolean execute(final Plugin plugin, final Runnable run, final Runnable retired, + final long delay) { + final Consumer runNMS = wrap(plugin, run); + final Consumer runRetired = retired == null ? null : wrap(plugin, retired); + + return this.entity.taskScheduler.schedule(runNMS, runRetired, delay); + } + + @Override + public @Nullable ScheduledTask run(final Plugin plugin, final Consumer task, final Runnable retired) { + return this.runDelayed(plugin, task, retired, 1); + } + + @Override + public @Nullable ScheduledTask runDelayed(final Plugin plugin, final Consumer task, final Runnable retired, + final long delayTicks) { + Validate.notNull(plugin, "Plugin 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 EntityScheduledTask ret = new EntityScheduledTask(plugin, -1, task, retired); + + if (!this.scheduleInternal(ret, delayTicks)) { + return null; + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public @Nullable ScheduledTask runAtFixedRate(final Plugin plugin, final Consumer task, + final Runnable retired, final long initialDelayTicks, final long periodTicks) { + Validate.notNull(plugin, "Plugin 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 EntityScheduledTask ret = new EntityScheduledTask(plugin, periodTicks, task, retired); + + if (!this.scheduleInternal(ret, initialDelayTicks)) { + return null; + } + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + private boolean scheduleInternal(final EntityScheduledTask ret, final long delay) { + return this.entity.taskScheduler.schedule(ret, ret, delay); + } + + private final class EntityScheduledTask implements ScheduledTask, Consumer { + + 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 long repeatDelay; // in ticks + private Consumer run; + private Runnable retired; + private volatile int state; + + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(EntityScheduledTask.class, "state", int.class); + + private EntityScheduledTask(final Plugin plugin, final long repeatDelay, final Consumer run, final Runnable retired) { + this.plugin = plugin; + this.repeatDelay = repeatDelay; + this.run = run; + this.retired = retired; + } + + 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 accept(final Entity entity) { + if (!this.plugin.isEnabled()) { + // don't execute if the plugin is disabled + this.setStateVolatile(STATE_CANCELLED); + return; + } + + final boolean repeating = this.isRepeatingTask(); + if (STATE_IDLE != this.compareAndExchangeStateVolatile(STATE_IDLE, STATE_EXECUTING)) { + // cancelled + return; + } + + final boolean retired = entity.isRemoved(); + + try { + if (!retired) { + this.run.accept(this); + } else { + if (this.retired != null) { + this.retired.run(); + } + } + } catch (final Throwable throwable) { + this.plugin.getLogger().log(Level.WARNING, "Entity task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable); + } finally { + boolean reschedule = false; + if (!repeating && !retired) { + this.setStateVolatile(STATE_FINISHED); + } else if (retired || !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.retired = null; + } else { + if (!FoliaEntityScheduler.this.scheduleInternal(this, this.repeatDelay)) { + // the task itself must have removed the entity, so in this case we need to mark as cancelled + this.setStateVolatile(STATE_CANCELLED); + } + } + } + } + + @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.retired = 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/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java b/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java new file mode 100644 index 0000000000..d306f91175 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/threadedregions/scheduler/FoliaGlobalRegionScheduler.java @@ -0,0 +1,267 @@ +package io.papermc.paper.threadedregions.scheduler; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.concurrentutil.util.Validate; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; + +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Level; + +public class FoliaGlobalRegionScheduler implements GlobalRegionScheduler { + + private long tickCount = 0L; + private final Object stateLock = new Object(); + private final Long2ObjectOpenHashMap> tasksByDeadline = new Long2ObjectOpenHashMap<>(); + + public void tick() { + final List run; + synchronized (this.stateLock) { + ++this.tickCount; + if (this.tasksByDeadline.isEmpty()) { + run = null; + } else { + run = this.tasksByDeadline.remove(this.tickCount); + } + } + + if (run == null) { + return; + } + + for (int i = 0, len = run.size(); i < len; ++i) { + run.get(i).run(); + } + } + + @Override + public void execute(final Plugin plugin, final Runnable run) { + Validate.notNull(plugin, "Plugin may not be null"); + Validate.notNull(run, "Runnable may not be null"); + + this.run(plugin, (final ScheduledTask task) -> { + run.run(); + }); + } + + @Override + public ScheduledTask run(final Plugin plugin, final Consumer task) { + return this.runDelayed(plugin, task, 1); + } + + @Override + public ScheduledTask runDelayed(final Plugin plugin, final Consumer task, final long delayTicks) { + Validate.notNull(plugin, "Plugin 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 GlobalScheduledTask ret = new GlobalScheduledTask(plugin, -1, task); + + this.scheduleInternal(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 Consumer task, final long initialDelayTicks, final long periodTicks) { + Validate.notNull(plugin, "Plugin 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 GlobalScheduledTask ret = new GlobalScheduledTask(plugin, periodTicks, task); + + this.scheduleInternal(ret, initialDelayTicks); + + if (!plugin.isEnabled()) { + // handle race condition where plugin is disabled asynchronously + ret.cancel(); + } + + return ret; + } + + @Override + public void cancelTasks(final Plugin plugin) { + Validate.notNull(plugin, "Plugin may not be null"); + + final List toCancel = new ArrayList<>(); + synchronized (this.stateLock) { + for (final List tasks : this.tasksByDeadline.values()) { + for (int i = 0, len = tasks.size(); i < len; ++i) { + final GlobalScheduledTask task = tasks.get(i); + if (task.plugin == plugin) { + toCancel.add(task); + } + } + } + } + + for (int i = 0, len = toCancel.size(); i < len; ++i) { + toCancel.get(i).cancel(); + } + } + + private void scheduleInternal(final GlobalScheduledTask task, final long delay) { + // note: delay > 0 + synchronized (this.stateLock) { + this.tasksByDeadline.computeIfAbsent(this.tickCount + delay, (final long keyInMap) -> { + return new ArrayList<>(); + }).add(task); + } + } + + private final class GlobalScheduledTask 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 long repeatDelay; // in ticks + private Consumer run; + private volatile int state; + + private static final VarHandle STATE_HANDLE = ConcurrentUtil.getVarHandle(GlobalScheduledTask.class, "state", int.class); + + private GlobalScheduledTask(final Plugin plugin, final long repeatDelay, final Consumer run) { + this.plugin = plugin; + 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() { + 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, "Global task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable); + } finally { + boolean reschedule = false; + if (!repeating) { + this.setStateVolatile(STATE_FINISHED); + } else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) { + reschedule = true; + } // else: cancelled repeating task + + if (!reschedule) { + this.run = null; + } else { + FoliaGlobalRegionScheduler.this.scheduleInternal(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; + 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/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java index 5c907eca23..4c4fa7bbaf 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -313,6 +313,88 @@ public final class CraftServer implements Server { private final io.papermc.paper.logging.SysoutCatcher sysoutCatcher = new io.papermc.paper.logging.SysoutCatcher(); // Paper private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; // Paper - Custom Potion Mixes + // Paper start - Folia region threading API + private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler(); + private final io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler asyncScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaAsyncScheduler(); + private final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler globalRegionScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler(); + + @Override + public final io.papermc.paper.threadedregions.scheduler.RegionScheduler getRegionScheduler() { + return this.regionizedScheduler; + } + + @Override + public final io.papermc.paper.threadedregions.scheduler.AsyncScheduler getAsyncScheduler() { + return this.asyncScheduler; + } + + @Override + public final io.papermc.paper.threadedregions.scheduler.FoliaGlobalRegionScheduler getGlobalRegionScheduler() { + return this.globalRegionScheduler; + } + + @Override + public final boolean isOwnedByCurrentRegion(World world, io.papermc.paper.math.Position position) { + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), position.blockX() >> 4, position.blockZ() >> 4 + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(World world, io.papermc.paper.math.Position position, int squareRadiusChunks) { + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), position.blockX() >> 4, position.blockZ() >> 4, squareRadiusChunks + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(Location location) { + World world = location.getWorld(); + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), location.getBlockX() >> 4, location.getBlockZ() >> 4 + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(Location location, int squareRadiusChunks) { + World world = location.getWorld(); + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), location.getBlockX() >> 4, location.getBlockZ() >> 4, squareRadiusChunks + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ) { + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), chunkX, chunkZ + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ, int squareRadiusChunks) { + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), chunkX, chunkZ, squareRadiusChunks + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(World world, int minChunkX, int minChunkZ, int maxChunkX, int maxChunkZ) { + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor( + ((CraftWorld) world).getHandle(), minChunkX, minChunkZ, maxChunkX, maxChunkZ + ); + } + + @Override + public final boolean isOwnedByCurrentRegion(Entity entity) { + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(((org.bukkit.craftbukkit.entity.CraftEntity) entity).getHandleRaw()); + } + + @Override + public final boolean isGlobalTickThread() { + return ca.spottedleaf.moonrise.common.util.TickThread.isTickThread(); + } + // Paper end - Folia reagion threading API + static { ConfigurationSerialization.registerClass(CraftOfflinePlayer.class); ConfigurationSerialization.registerClass(CraftPlayerProfile.class); diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java index f1992713a9..d078f21456 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java @@ -71,6 +71,15 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { private EntityDamageEvent lastDamageEvent; private final CraftPersistentDataContainer persistentDataContainer = new CraftPersistentDataContainer(CraftEntity.DATA_TYPE_REGISTRY); protected net.kyori.adventure.pointer.Pointers adventure$pointers; // Paper - implement pointers + // Paper start - Folia shedulers + public final io.papermc.paper.threadedregions.EntityScheduler taskScheduler = new io.papermc.paper.threadedregions.EntityScheduler(this); + private final io.papermc.paper.threadedregions.scheduler.FoliaEntityScheduler apiScheduler = new io.papermc.paper.threadedregions.scheduler.FoliaEntityScheduler(this); + + @Override + public final io.papermc.paper.threadedregions.scheduler.EntityScheduler getScheduler() { + return this.apiScheduler; + }; + // Paper end - Folia schedulers public CraftEntity(final CraftServer server, final Entity entity) { this.server = server; @@ -487,6 +496,12 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { return this.entity; } + // Paper start + public Entity getHandleRaw() { + return this.entity; + } + // Paper end + @Override public final EntityType getType() { return this.entityType;