From fb0d9a1cd6e9865d0fcf5d8cf58c309457a1b23d Mon Sep 17 00:00:00 2001 From: William Blake Galbreath Date: Sat, 4 Jan 2025 19:40:44 -0800 Subject: [PATCH] AFK API --- patches/api/0009-AFK-API.patch | 113 ------ patches/server/0011-AFK-API.patch | 331 ------------------ .../java/org/bukkit/entity/Player.java.patch | 22 +- .../purpurmc/purpur/event/PlayerAFKEvent.java | 71 ++++ .../features/0001-Ridables.patch | 10 +- .../activation/ActivationRange.java.patch | 11 + .../server/level/ServerPlayer.java.patch | 77 +++- .../ServerGamePacketListenerImpl.java.patch | 63 ++++ .../server/players/SleepStatus.java.patch | 20 ++ .../world/entity/EntitySelector.java.patch | 11 + .../targeting/TargetingConditions.java.patch | 13 + .../world/entity/player/Player.java.patch | 16 + .../world/level/EntityGetter.java.patch | 11 + .../craftbukkit/entity/CraftPlayer.java.patch | 35 +- .../org/purpurmc/purpur/PurpurConfig.java | 11 + .../purpurmc/purpur/PurpurWorldConfig.java | 18 + 16 files changed, 378 insertions(+), 455 deletions(-) delete mode 100644 patches/api/0009-AFK-API.patch delete mode 100644 patches/server/0011-AFK-API.patch create mode 100644 purpur-api/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java create mode 100644 purpur-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch create mode 100644 purpur-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch create mode 100644 purpur-server/minecraft-patches/sources/net/minecraft/server/players/SleepStatus.java.patch create mode 100644 purpur-server/minecraft-patches/sources/net/minecraft/world/entity/EntitySelector.java.patch create mode 100644 purpur-server/minecraft-patches/sources/net/minecraft/world/entity/ai/targeting/TargetingConditions.java.patch create mode 100644 purpur-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch create mode 100644 purpur-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch diff --git a/patches/api/0009-AFK-API.patch b/patches/api/0009-AFK-API.patch deleted file mode 100644 index 1c3bac8d5..000000000 --- a/patches/api/0009-AFK-API.patch +++ /dev/null @@ -1,113 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: William Blake Galbreath -Date: Sat, 10 Aug 2019 22:19:56 -0500 -Subject: [PATCH] AFK API - - -diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java -index c8365c38c91b3e6c4f721074f0646fe5adffbdf6..ed0f892ed987419809fe1a0390b6278c99659919 100644 ---- a/src/main/java/org/bukkit/entity/Player.java -+++ b/src/main/java/org/bukkit/entity/Player.java -@@ -3919,5 +3919,25 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM - * @return True if Player uses Purpur Client - */ - public boolean usesPurpurClient(); -+ -+ /** -+ * Check if player is AFK -+ * -+ * @return True if AFK -+ */ -+ boolean isAfk(); -+ -+ /** -+ * Set player as AFK -+ * -+ * @param setAfk Whether to set AFK or not -+ */ -+ void setAfk(boolean setAfk); -+ -+ /** -+ * Reset the idle timer back to 0 -+ * @deprecated Use {@link #resetIdleDuration()} instead -+ */ -+ void resetIdleTimer(); - // Purpur end - } -diff --git a/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java b/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e9637b82014fe3f4f4671b24d18f77f3d5e4b8ad ---- /dev/null -+++ b/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java -@@ -0,0 +1,71 @@ -+package org.purpurmc.purpur.event; -+ -+import org.bukkit.entity.Player; -+import org.bukkit.event.Cancellable; -+import org.bukkit.event.HandlerList; -+import org.bukkit.event.player.PlayerEvent; -+import org.jetbrains.annotations.ApiStatus; -+import org.jspecify.annotations.NullMarked; -+import org.jspecify.annotations.Nullable; -+ -+@NullMarked -+public class PlayerAFKEvent extends PlayerEvent implements Cancellable { -+ private static final HandlerList handlers = new HandlerList(); -+ private final boolean setAfk; -+ private boolean shouldKick; -+ private @Nullable String broadcast; -+ private boolean cancel; -+ -+ @ApiStatus.Internal -+ public PlayerAFKEvent(Player player, boolean setAfk, boolean shouldKick, @Nullable String broadcast, boolean async) { -+ super(player, async); -+ this.setAfk = setAfk; -+ this.shouldKick = shouldKick; -+ this.broadcast = broadcast; -+ } -+ -+ /** -+ * Whether player is going afk or coming back -+ * -+ * @return True if going afk. False is coming back -+ */ -+ public boolean isGoingAfk() { -+ return setAfk; -+ } -+ -+ public boolean shouldKick() { -+ return shouldKick; -+ } -+ -+ public void setShouldKick(boolean shouldKick) { -+ this.shouldKick = shouldKick; -+ } -+ -+ @Nullable -+ public String getBroadcastMsg() { -+ return broadcast; -+ } -+ -+ public void setBroadcastMsg(@Nullable String broadcast) { -+ this.broadcast = broadcast; -+ } -+ -+ @Override -+ public boolean isCancelled() { -+ return cancel; -+ } -+ -+ @Override -+ public void setCancelled(boolean cancel) { -+ this.cancel = cancel; -+ } -+ -+ @Override -+ public HandlerList getHandlers() { -+ return handlers; -+ } -+ -+ public static HandlerList getHandlerList() { -+ return handlers; -+ } -+} diff --git a/patches/server/0011-AFK-API.patch b/patches/server/0011-AFK-API.patch deleted file mode 100644 index 59a62d54b..000000000 --- a/patches/server/0011-AFK-API.patch +++ /dev/null @@ -1,331 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: William Blake Galbreath -Date: Thu, 8 Aug 2019 15:29:15 -0500 -Subject: [PATCH] AFK API - - -diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java -index 8207208d6fb3f982e9909add9e74a0dda69e8120..5e8a5e8e4120420a170c4733ae217e7948cef46c 100644 ---- a/net/minecraft/server/level/ServerPlayer.java -+++ b/net/minecraft/server/level/ServerPlayer.java -@@ -2621,8 +2621,68 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - - public void resetLastActionTime() { - this.lastActionTime = Util.getMillis(); -+ this.setAfk(false); // Purpur - } - -+ // Purpur start - AFK API -+ private boolean isAfk = false; -+ -+ @Override -+ public void setAfk(boolean afk) { -+ if (this.isAfk == afk) { -+ return; -+ } -+ -+ String msg = afk ? org.purpurmc.purpur.PurpurConfig.afkBroadcastAway : org.purpurmc.purpur.PurpurConfig.afkBroadcastBack; -+ -+ org.purpurmc.purpur.event.PlayerAFKEvent event = new org.purpurmc.purpur.event.PlayerAFKEvent(this.getBukkitEntity(), afk, this.level().purpurConfig.idleTimeoutKick, msg, !Bukkit.isPrimaryThread()); -+ if (!event.callEvent() || event.shouldKick()) { -+ return; -+ } -+ -+ this.isAfk = afk; -+ -+ if (!afk) { -+ resetLastActionTime(); -+ } -+ -+ msg = event.getBroadcastMsg(); -+ if (msg != null && !msg.isEmpty()) { -+ String playerName = this.getGameProfile().getName(); -+ if (org.purpurmc.purpur.PurpurConfig.afkBroadcastUseDisplayName) { -+ net.kyori.adventure.text.Component playerDisplayNameComponent = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(this.getBukkitEntity().getDisplayName()); -+ playerName = net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(playerDisplayNameComponent); -+ } -+ server.getPlayerList().broadcastMiniMessage(String.format(msg, playerName), false); -+ } -+ -+ if (this.level().purpurConfig.idleTimeoutUpdateTabList) { -+ String scoreboardName = getScoreboardName(); -+ String playerListName = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().serialize(getBukkitEntity().playerListName()); -+ String[] split = playerListName.split(scoreboardName); -+ String prefix = (split.length > 0 ? split[0] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix, ""); -+ String suffix = (split.length > 1 ? split[1] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, ""); -+ if (afk) { -+ getBukkitEntity().setPlayerListName(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix + prefix + scoreboardName + suffix + org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, true); -+ } else { -+ getBukkitEntity().setPlayerListName(prefix + scoreboardName + suffix, true); -+ } -+ } -+ -+ ((ServerLevel) this.level()).updateSleepingPlayerList(); -+ } -+ -+ @Override -+ public boolean isAfk() { -+ return this.isAfk; -+ } -+ -+ @Override -+ public boolean canBeCollidedWith() { -+ return !this.isAfk() && super.canBeCollidedWith(); -+ } -+ // Purpur end - AFK API -+ - public ServerStatsCounter getStats() { - return this.stats; - } -diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index fbc59503316d566e88b037851afd74e5469c281b..830e0705f3a3d45fcf0eab6b8eea403c3023f3f9 100644 ---- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -344,6 +344,20 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - private boolean justTeleported = false; - // CraftBukkit end - -+ // Purpur start - AFK API -+ private final com.google.common.cache.LoadingCache kickPermissionCache = com.google.common.cache.CacheBuilder.newBuilder() -+ .maximumSize(1000) -+ .expireAfterWrite(1, java.util.concurrent.TimeUnit.MINUTES) -+ .build( -+ new com.google.common.cache.CacheLoader<>() { -+ @Override -+ public Boolean load(CraftPlayer player) { -+ return player.hasPermission("purpur.bypassIdleKick"); -+ } -+ } -+ ); -+ // Purpur end - AFK API -+ - @Override - public void tick() { - if (this.ackBlockChangesUpTo > -1) { -@@ -400,6 +414,12 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - this.recipeSpamPackets.tick(); // Paper - auto recipe limit - this.dropSpamThrottler.tick(); - if (this.player.getLastActionTime() > 0L && this.server.getPlayerIdleTimeout() > 0 && Util.getMillis() - this.player.getLastActionTime() > (long) this.server.getPlayerIdleTimeout() * 1000L * 60L && !this.player.wonGame) { // Paper - Prevent AFK kick while watching end credits -+ // Purpur start - AFK API -+ this.player.setAfk(true); -+ if (!this.player.level().purpurConfig.idleTimeoutKick || (!Boolean.parseBoolean(System.getenv("PURPUR_FORCE_IDLE_KICK")) && kickPermissionCache.getUnchecked(this.player.getBukkitEntity()))) { -+ return; -+ } -+ // Purpur end - AFK API - this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854 - this.disconnect((Component) Component.translatable("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause - } -@@ -656,6 +676,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - this.lastYaw = to.getYaw(); - this.lastPitch = to.getPitch(); - -+ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur - AFK API -+ - Location oldTo = to.clone(); - PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); - this.cserver.getPluginManager().callEvent(event); -@@ -1554,7 +1576,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - movedWrongly = true; - if (event.getLogWarning()) - // Paper end -- ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString()); -+ ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!, ({})", this.player.getName().getString(), d11); // Purpur - AFK API - } // Paper - } - -@@ -1622,6 +1644,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - this.lastYaw = to.getYaw(); - this.lastPitch = to.getPitch(); - -+ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur - AFK API -+ - Location oldTo = to.clone(); - PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); - this.cserver.getPluginManager().callEvent(event); -diff --git a/net/minecraft/server/players/SleepStatus.java b/net/minecraft/server/players/SleepStatus.java -index 823efad652d8ff9e96b99375b102fef6f017716e..bdf0240840d92bf95f94c6fb1125eeaa105e303b 100644 ---- a/net/minecraft/server/players/SleepStatus.java -+++ b/net/minecraft/server/players/SleepStatus.java -@@ -19,7 +19,7 @@ public class SleepStatus { - - public boolean areEnoughDeepSleeping(int percentage, List players) { - // CraftBukkit start -- int j = (int) players.stream().filter((eh) -> { return eh.isSleepingLongEnough() || eh.fauxSleeping; }).count(); -+ int j = (int) players.stream().filter((eh) -> { return eh.isSleepingLongEnough() || eh.fauxSleeping || (eh.level().purpurConfig.idleTimeoutCountAsSleeping && eh.isAfk()); }).count(); // Purpur - AFK API - boolean anyDeepSleep = players.stream().anyMatch(Player::isSleepingLongEnough); - - return anyDeepSleep && j >= this.sleepersNeeded(percentage); -@@ -52,7 +52,7 @@ public class SleepStatus { - - if (!entityplayer.isSpectator()) { - ++this.activePlayers; -- if (entityplayer.isSleeping() || entityplayer.fauxSleeping) { // CraftBukkit -+ if ((entityplayer.isSleeping() || entityplayer.fauxSleeping) || (entityplayer.level().purpurConfig.idleTimeoutCountAsSleeping && entityplayer.isAfk())) { // CraftBukkit // Purpur - AFK API - ++this.sleepingPlayers; - } - // CraftBukkit start -diff --git a/net/minecraft/world/entity/EntitySelector.java b/net/minecraft/world/entity/EntitySelector.java -index 6bf691fcc6486bde73bae30eff09142802c29eda..d99b223be90f0c04bb9274228ad323a7c7f218b2 100644 ---- a/net/minecraft/world/entity/EntitySelector.java -+++ b/net/minecraft/world/entity/EntitySelector.java -@@ -39,6 +39,7 @@ public final class EntitySelector { - return net.minecraft.util.Mth.clamp(serverPlayer.getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE) >= playerInsomniaTicks; - }; - // Paper end - Ability to control player's insomnia and phantoms -+ public static Predicate notAfk = (player) -> !player.isAfk(); // Purpur - AFK API - - private EntitySelector() {} - // Paper start - Affects Spawning API -diff --git a/net/minecraft/world/entity/ai/targeting/TargetingConditions.java b/net/minecraft/world/entity/ai/targeting/TargetingConditions.java -index 52982c1e6a4da36392569c791853279f5f9ac31a..ebb827e213a3ba5eeb2fe5b78f5dee99403097b6 100644 ---- a/net/minecraft/world/entity/ai/targeting/TargetingConditions.java -+++ b/net/minecraft/world/entity/ai/targeting/TargetingConditions.java -@@ -64,6 +64,10 @@ public class TargetingConditions { - return false; - } else if (this.selector != null && !this.selector.test(target, world)) { - return false; -+ // Purpur start - AFK API -+ } else if (!world.purpurConfig.idleTimeoutTargetPlayer && target instanceof net.minecraft.server.level.ServerPlayer player && player.isAfk()) { -+ return false; -+ // Purpur end - AFK API - } else { - if (tester == null) { - if (this.isCombat && (!target.canBeSeenAsEnemy() || world.getDifficulty() == Difficulty.PEACEFUL)) { -diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java -index 87c6378104ff47549c751e09afb6cfcd9b8dcf5d..2f69a511db8d43fbd3a17387cded1d3573579fce 100644 ---- a/net/minecraft/world/entity/player/Player.java -+++ b/net/minecraft/world/entity/player/Player.java -@@ -206,6 +206,13 @@ public abstract class Player extends LivingEntity { - public boolean fauxSleeping; - public int oldLevel = -1; - -+ // Purpur start - AFK API -+ public abstract void setAfk(boolean afk); -+ -+ public boolean isAfk() { -+ return false; -+ } -+ // Purpur end - AFK API - @Override - public CraftHumanEntity getBukkitEntity() { - return (CraftHumanEntity) super.getBukkitEntity(); -diff --git a/net/minecraft/world/level/EntityGetter.java b/net/minecraft/world/level/EntityGetter.java -index 5d7a6e4b73f032db356e7ec369b150013e940ee6..e164833de0c29eed9025dd4af3f2bb74c92d2250 100644 ---- a/net/minecraft/world/level/EntityGetter.java -+++ b/net/minecraft/world/level/EntityGetter.java -@@ -184,7 +184,7 @@ public interface EntityGetter extends ca.spottedleaf.moonrise.patches.chunk_syst - - default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) { - for (Player player : this.players()) { -- if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { -+ if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player) && EntitySelector.notAfk.test(player)) { // Purpur - AFK API - double d = player.distanceToSqr(x, y, z); - if (range < 0.0 || d < range * range) { - return true; -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index c5bd2a45b32e8dff83c148379544db125684622a..16671636da1b6da62a0bdf0daab745fcd89143b7 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -584,10 +584,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - - @Override - public void setPlayerListName(String name) { -+ // Purpur start - AFK API -+ setPlayerListName(name, false); -+ } -+ public void setPlayerListName(String name, boolean useMM) { -+ // Purpur end - AFK API - if (name == null) { - name = this.getName(); - } -- this.getHandle().listName = name.equals(this.getName()) ? null : CraftChatMessage.fromStringOrNull(name); -+ this.getHandle().listName = name.equals(this.getName()) ? null : useMM ? io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(name)) : CraftChatMessage.fromStringOrNull(name); // Purpur - AFK API - if (this.getHandle().connection == null) return; // Paper - Updates are possible before the player has fully joined - for (ServerPlayer player : (List) this.server.getHandle().players) { - if (player.getBukkitEntity().canSee(this)) { -@@ -3572,4 +3577,20 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - return getHandle().purpurClient; - } - // Purpur end - Purpur client support -+ // Purpur start - AFK API -+ @Override -+ public boolean isAfk() { -+ return getHandle().isAfk(); -+ } -+ -+ @Override -+ public void setAfk(boolean setAfk) { -+ getHandle().setAfk(setAfk); -+ } -+ -+ @Override -+ public void resetIdleTimer() { -+ getHandle().resetLastActionTime(); -+ } -+ // Purpur end - AFK API - } -diff --git a/src/main/java/org/purpurmc/purpur/PurpurConfig.java b/src/main/java/org/purpurmc/purpur/PurpurConfig.java -index 1321955eb23272d96e3028c1c46e75f6c1eaade0..82f0ae89ef3f0dc614edb4ccd3caa66cb387044c 100644 ---- a/src/main/java/org/purpurmc/purpur/PurpurConfig.java -+++ b/src/main/java/org/purpurmc/purpur/PurpurConfig.java -@@ -177,8 +177,18 @@ public class PurpurConfig { - } - - public static String cannotRideMob = "You cannot mount that mob"; -+ public static String afkBroadcastAway = "%s is now AFK"; -+ public static String afkBroadcastBack = "%s is no longer AFK"; -+ public static boolean afkBroadcastUseDisplayName = false; -+ public static String afkTabListPrefix = "[AFK] "; -+ public static String afkTabListSuffix = ""; - private static void messages() { - cannotRideMob = getString("settings.messages.cannot-ride-mob", cannotRideMob); -+ afkBroadcastAway = getString("settings.messages.afk-broadcast-away", afkBroadcastAway); -+ afkBroadcastBack = getString("settings.messages.afk-broadcast-back", afkBroadcastBack); -+ afkBroadcastUseDisplayName = getBoolean("settings.messages.afk-broadcast-use-display-name", afkBroadcastUseDisplayName); -+ afkTabListPrefix = MiniMessage.miniMessage().serialize(MiniMessage.miniMessage().deserialize(getString("settings.messages.afk-tab-list-prefix", afkTabListPrefix))); -+ afkTabListSuffix = MiniMessage.miniMessage().serialize(MiniMessage.miniMessage().deserialize(getString("settings.messages.afk-tab-list-suffix", afkTabListSuffix))); - } - - public static int barrelRows = 3; -diff --git a/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java -index ee9bcb7d011f20575cbbbe2e0ded1e53087aba7a..9b1a4502aa6c26c7524ec17697250317b7f381fd 100644 ---- a/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java -+++ b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java -@@ -91,6 +91,24 @@ public class PurpurWorldConfig { - return value.isEmpty() ? fallback : value; - } - -+ public boolean idleTimeoutKick = true; -+ public boolean idleTimeoutTickNearbyEntities = true; -+ public boolean idleTimeoutCountAsSleeping = false; -+ public boolean idleTimeoutUpdateTabList = false; -+ public boolean idleTimeoutTargetPlayer = true; -+ private void playerSettings() { -+ if (PurpurConfig.version < 19) { -+ boolean oldVal = getBoolean("gameplay-mechanics.player.idle-timeout.mods-target", idleTimeoutTargetPlayer); -+ set("gameplay-mechanics.player.idle-timeout.mods-target", null); -+ set("gameplay-mechanics.player.idle-timeout.mobs-target", oldVal); -+ } -+ idleTimeoutKick = System.getenv("PURPUR_FORCE_IDLE_KICK") == null ? getBoolean("gameplay-mechanics.player.idle-timeout.kick-if-idle", idleTimeoutKick) : Boolean.parseBoolean(System.getenv("PURPUR_FORCE_IDLE_KICK")); -+ idleTimeoutTickNearbyEntities = getBoolean("gameplay-mechanics.player.idle-timeout.tick-nearby-entities", idleTimeoutTickNearbyEntities); -+ idleTimeoutCountAsSleeping = getBoolean("gameplay-mechanics.player.idle-timeout.count-as-sleeping", idleTimeoutCountAsSleeping); -+ idleTimeoutUpdateTabList = getBoolean("gameplay-mechanics.player.idle-timeout.update-tab-list", idleTimeoutUpdateTabList); -+ idleTimeoutTargetPlayer = getBoolean("gameplay-mechanics.player.idle-timeout.mobs-target", idleTimeoutTargetPlayer); -+ } -+ - public boolean babiesAreRidable = true; - public boolean untamedTamablesAreRidable = true; - public boolean useNightVisionWhenRiding = false; -diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java -index 1d438ef44cbe4d1eedfba36d8fe5d2ad53464921..24d7eca3f0b06602a1026eda3432f0a4255d8b01 100644 ---- a/src/main/java/org/spigotmc/ActivationRange.java -+++ b/src/main/java/org/spigotmc/ActivationRange.java -@@ -199,6 +199,8 @@ public class ActivationRange - continue; - } - -+ if (!player.level().purpurConfig.idleTimeoutTickNearbyEntities && player.isAfk()) continue; // Purpur -+ - // Paper start - int worldHeight = world.getHeight(); - ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, worldHeight, maxRange ); diff --git a/purpur-api/paper-patches/files/src/main/java/org/bukkit/entity/Player.java.patch b/purpur-api/paper-patches/files/src/main/java/org/bukkit/entity/Player.java.patch index dd3d82e83..d0cb73dfc 100644 --- a/purpur-api/paper-patches/files/src/main/java/org/bukkit/entity/Player.java.patch +++ b/purpur-api/paper-patches/files/src/main/java/org/bukkit/entity/Player.java.patch @@ -1,6 +1,6 @@ --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java -@@ -3911,4 +_,13 @@ +@@ -3911,4 +_,33 @@ */ void sendEntityEffect(org.bukkit.@NotNull EntityEffect effect, @NotNull Entity target); // Paper end - entity effect API @@ -12,5 +12,25 @@ + * @return true if player uses PurpurClient + */ + public boolean usesPurpurClient(); ++ ++ /** ++ * Check if player is AFK ++ * ++ * @return True if AFK ++ */ ++ boolean isAfk(); ++ ++ /** ++ * Set player as AFK ++ * ++ * @param setAfk Whether to set AFK or not ++ */ ++ void setAfk(boolean setAfk); ++ ++ /** ++ * Reset the idle timer back to 0 ++ * @deprecated Use {@link #resetIdleDuration()} instead ++ */ ++ void resetIdleTimer(); + // Purpur end } diff --git a/purpur-api/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java b/purpur-api/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java new file mode 100644 index 000000000..e9637b820 --- /dev/null +++ b/purpur-api/src/main/java/org/purpurmc/purpur/event/PlayerAFKEvent.java @@ -0,0 +1,71 @@ +package org.purpurmc.purpur.event; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public class PlayerAFKEvent extends PlayerEvent implements Cancellable { + private static final HandlerList handlers = new HandlerList(); + private final boolean setAfk; + private boolean shouldKick; + private @Nullable String broadcast; + private boolean cancel; + + @ApiStatus.Internal + public PlayerAFKEvent(Player player, boolean setAfk, boolean shouldKick, @Nullable String broadcast, boolean async) { + super(player, async); + this.setAfk = setAfk; + this.shouldKick = shouldKick; + this.broadcast = broadcast; + } + + /** + * Whether player is going afk or coming back + * + * @return True if going afk. False is coming back + */ + public boolean isGoingAfk() { + return setAfk; + } + + public boolean shouldKick() { + return shouldKick; + } + + public void setShouldKick(boolean shouldKick) { + this.shouldKick = shouldKick; + } + + @Nullable + public String getBroadcastMsg() { + return broadcast; + } + + public void setBroadcastMsg(@Nullable String broadcast) { + this.broadcast = broadcast; + } + + @Override + public boolean isCancelled() { + return cancel; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancel = cancel; + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/purpur-server/minecraft-patches/features/0001-Ridables.patch b/purpur-server/minecraft-patches/features/0001-Ridables.patch index 5ae44edcd..0a0fa29c5 100644 --- a/purpur-server/minecraft-patches/features/0001-Ridables.patch +++ b/purpur-server/minecraft-patches/features/0001-Ridables.patch @@ -42,7 +42,7 @@ index ebeeb63c3dca505a3ce8b88feaa5d2ca20ec24a2..0029717fbd4f2475b07abf4f7036cebb public LevelChunk getChunkIfLoaded(int x, int z) { return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java -index dc146170def1b349291916f13164a5f4c43ee6f2..544b87f020ad2177b6ec86ce2d1e49a14be8c2b4 100644 +index cbdb3b12e11e9b50978febebd2b275997b86c1b8..c6bd1ec93d40d43c151c68ea8892c33db4511d03 100644 --- a/net/minecraft/server/level/ServerPlayer.java +++ b/net/minecraft/server/level/ServerPlayer.java @@ -838,6 +838,15 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc @@ -62,10 +62,10 @@ index dc146170def1b349291916f13164a5f4c43ee6f2..544b87f020ad2177b6ec86ce2d1e49a1 private void updatePlayerAttributes() { diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index d248671b2e1c6256fc4d74320bdb29ca078bad0b..e2d30d0daae4ad088c723a2d85760c340fe83e9d 100644 +index b083228bb3dc87794c6f177ad99832daf6925a39..bf863cfae63a50636c3fcc8fcf9761f1f117d218 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -2746,6 +2746,8 @@ public class ServerGamePacketListenerImpl +@@ -2770,6 +2770,8 @@ public class ServerGamePacketListenerImpl ServerGamePacketListenerImpl.this.cserver.getPluginManager().callEvent(event); @@ -5029,10 +5029,10 @@ index 6655d06e2011e20e7346dfe57527795269094d8a..6c14537c8376bd392524aefde8dfe76b this.openTradingScreen(player, this.getDisplayName(), 1); } diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java -index 58932bc9256e9e2ff054ac007971c03187851b53..dda02ceae600f2909aad60bfef0ce29f83ca5a77 100644 +index 3bc8b32e9eb39745c487d377474358c8c2e5b787..a65023d0929435785116682c1378428120340547 100644 --- a/net/minecraft/world/entity/player/Player.java +++ b/net/minecraft/world/entity/player/Player.java -@@ -211,6 +211,19 @@ public abstract class Player extends LivingEntity { +@@ -218,6 +218,19 @@ public abstract class Player extends LivingEntity { } // CraftBukkit end diff --git a/purpur-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch b/purpur-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch new file mode 100644 index 000000000..a18a3f199 --- /dev/null +++ b/purpur-server/minecraft-patches/sources/io/papermc/paper/entity/activation/ActivationRange.java.patch @@ -0,0 +1,11 @@ +--- a/io/papermc/paper/entity/activation/ActivationRange.java ++++ b/io/papermc/paper/entity/activation/ActivationRange.java +@@ -141,6 +_,8 @@ + continue; + } + ++ if (!player.level().purpurConfig.idleTimeoutTickNearbyEntities && player.isAfk()) continue; // Purpur - AFK API ++ + final int worldHeight = world.getHeight(); + ActivationRange.maxBB = player.getBoundingBox().inflate(maxRange, worldHeight, maxRange); + ActivationType.MISC.boundingBox = player.getBoundingBox().inflate(miscActivationRange, worldHeight, miscActivationRange); diff --git a/purpur-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch b/purpur-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch index ebef12bec..2806699e4 100644 --- a/purpur-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch +++ b/purpur-server/minecraft-patches/sources/net/minecraft/server/level/ServerPlayer.java.patch @@ -35,11 +35,10 @@ @Override public void displayClientMessage(Component chatComponent, boolean actionBar) { this.sendSystemMessage(chatComponent, actionBar); -@@ -2234,6 +_,20 @@ - this +@@ -2235,6 +_,20 @@ ); } -+ + + // Purpur start - Component related conveniences + public void sendMiniMessage(@Nullable String message) { + if (message != null && !message.isEmpty()) { @@ -53,6 +52,76 @@ + } + } + // Purpur end - Component related conveniences - ++ public void sendSystemMessage(Component mesage) { this.sendSystemMessage(mesage, false); + } +@@ -2373,7 +_,67 @@ + + public void resetLastActionTime() { + this.lastActionTime = Util.getMillis(); +- } ++ this.setAfk(false); // Purpur - AFK API ++ } ++ ++ // Purpur start - AFK API ++ private boolean isAfk = false; ++ ++ @Override ++ public void setAfk(boolean afk) { ++ if (this.isAfk == afk) { ++ return; ++ } ++ ++ String msg = afk ? org.purpurmc.purpur.PurpurConfig.afkBroadcastAway : org.purpurmc.purpur.PurpurConfig.afkBroadcastBack; ++ ++ org.purpurmc.purpur.event.PlayerAFKEvent event = new org.purpurmc.purpur.event.PlayerAFKEvent(this.getBukkitEntity(), afk, this.level().purpurConfig.idleTimeoutKick, msg, !org.bukkit.Bukkit.isPrimaryThread()); ++ if (!event.callEvent() || event.shouldKick()) { ++ return; ++ } ++ ++ this.isAfk = afk; ++ ++ if (!afk) { ++ resetLastActionTime(); ++ } ++ ++ msg = event.getBroadcastMsg(); ++ if (msg != null && !msg.isEmpty()) { ++ String playerName = this.getGameProfile().getName(); ++ if (org.purpurmc.purpur.PurpurConfig.afkBroadcastUseDisplayName) { ++ net.kyori.adventure.text.Component playerDisplayNameComponent = net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(this.getBukkitEntity().getDisplayName()); ++ playerName = net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(playerDisplayNameComponent); ++ } ++ server.getPlayerList().broadcastMiniMessage(String.format(msg, playerName), false); ++ } ++ ++ if (this.level().purpurConfig.idleTimeoutUpdateTabList) { ++ String scoreboardName = getScoreboardName(); ++ String playerListName = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().serialize(getBukkitEntity().playerListName()); ++ String[] split = playerListName.split(scoreboardName); ++ String prefix = (split.length > 0 ? split[0] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix, ""); ++ String suffix = (split.length > 1 ? split[1] : "").replace(org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, ""); ++ if (afk) { ++ getBukkitEntity().setPlayerListName(org.purpurmc.purpur.PurpurConfig.afkTabListPrefix + prefix + scoreboardName + suffix + org.purpurmc.purpur.PurpurConfig.afkTabListSuffix, true); ++ } else { ++ getBukkitEntity().setPlayerListName(prefix + scoreboardName + suffix, true); ++ } ++ } ++ ++ ((ServerLevel) this.level()).updateSleepingPlayerList(); ++ } ++ ++ @Override ++ public boolean isAfk() { ++ return this.isAfk; ++ } ++ ++ @Override ++ public boolean canBeCollidedWith() { ++ return !this.isAfk() && super.canBeCollidedWith(); ++ } ++ // Purpur end - AFK API + + public ServerStatsCounter getStats() { + return this.stats; diff --git a/purpur-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch b/purpur-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch new file mode 100644 index 000000000..53c29b89e --- /dev/null +++ b/purpur-server/minecraft-patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch @@ -0,0 +1,63 @@ +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -326,6 +_,20 @@ + this.chatMessageChain = new FutureChain(server.chatExecutor); // CraftBukkit - async chat + } + ++ // Purpur start - AFK API ++ private final com.google.common.cache.LoadingCache kickPermissionCache = com.google.common.cache.CacheBuilder.newBuilder() ++ .maximumSize(1000) ++ .expireAfterWrite(1, java.util.concurrent.TimeUnit.MINUTES) ++ .build( ++ new com.google.common.cache.CacheLoader<>() { ++ @Override ++ public Boolean load(CraftPlayer player) { ++ return player.hasPermission("purpur.bypassIdleKick"); ++ } ++ } ++ ); ++ // Purpur end - AFK API ++ + @Override + public void tick() { + if (this.ackBlockChangesUpTo > -1) { +@@ -384,6 +_,12 @@ + if (this.player.getLastActionTime() > 0L + && this.server.getPlayerIdleTimeout() > 0 + && Util.getMillis() - this.player.getLastActionTime() > this.server.getPlayerIdleTimeout() * 1000L * 60L && !this.player.wonGame) { // Paper - Prevent AFK kick while watching end credits ++ // Purpur start - AFK API ++ this.player.setAfk(true); ++ if (!this.player.level().purpurConfig.idleTimeoutKick || (!Boolean.parseBoolean(System.getenv("PURPUR_FORCE_IDLE_KICK")) && kickPermissionCache.getUnchecked(this.player.getBukkitEntity()))) { ++ return; ++ } ++ // Purpur end - AFK API + this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854 + this.disconnect(Component.translatable("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause + } +@@ -629,6 +_,8 @@ + this.lastYaw = to.getYaw(); + this.lastPitch = to.getPitch(); + ++ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur - AFK API ++ + Location oldTo = to.clone(); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); + this.cserver.getPluginManager().callEvent(event); +@@ -1460,7 +_,7 @@ + movedWrongly = true; + if (event.getLogWarning()) + // Paper end +- LOGGER.warn("{} moved wrongly!", this.player.getName().getString()); ++ LOGGER.warn("{} moved wrongly!, ({})", this.player.getName().getString(), d11); // Purpur - AFK API + } // Paper + } + +@@ -1525,6 +_,8 @@ + this.lastPosZ = to.getZ(); + this.lastYaw = to.getYaw(); + this.lastPitch = to.getPitch(); ++ ++ if (!to.getWorld().getUID().equals(from.getWorld().getUID()) || to.getBlockX() != from.getBlockX() || to.getBlockY() != from.getBlockY() || to.getBlockZ() != from.getBlockZ() || to.getYaw() != from.getYaw() || to.getPitch() != from.getPitch()) this.player.resetLastActionTime(); // Purpur - AFK API + + Location oldTo = to.clone(); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); diff --git a/purpur-server/minecraft-patches/sources/net/minecraft/server/players/SleepStatus.java.patch b/purpur-server/minecraft-patches/sources/net/minecraft/server/players/SleepStatus.java.patch new file mode 100644 index 000000000..998d2f057 --- /dev/null +++ b/purpur-server/minecraft-patches/sources/net/minecraft/server/players/SleepStatus.java.patch @@ -0,0 +1,20 @@ +--- a/net/minecraft/server/players/SleepStatus.java ++++ b/net/minecraft/server/players/SleepStatus.java +@@ -15,7 +_,7 @@ + + public boolean areEnoughDeepSleeping(int requiredSleepPercentage, List sleepingPlayers) { + // CraftBukkit start +- int i = (int) sleepingPlayers.stream().filter(player -> player.isSleepingLongEnough() || player.fauxSleeping).count(); ++ int i = (int) sleepingPlayers.stream().filter(player -> player.isSleepingLongEnough() || player.fauxSleeping || (player.level().purpurConfig.idleTimeoutCountAsSleeping && player.isAfk())).count(); // Purpur - AFK API + boolean anyDeepSleep = sleepingPlayers.stream().anyMatch(Player::isSleepingLongEnough); + return anyDeepSleep && i >= this.sleepersNeeded(requiredSleepPercentage); + // CraftBukkit end +@@ -43,7 +_,7 @@ + for (ServerPlayer serverPlayer : players) { + if (!serverPlayer.isSpectator()) { + this.activePlayers++; +- if (serverPlayer.isSleeping() || serverPlayer.fauxSleeping) { // CraftBukkit ++ if (serverPlayer.isSleeping() || serverPlayer.fauxSleeping || (serverPlayer.level().purpurConfig.idleTimeoutCountAsSleeping && serverPlayer.isAfk())) { // CraftBukkit // Purpur - AFK API + this.sleepingPlayers++; + } + // CraftBukkit start diff --git a/purpur-server/minecraft-patches/sources/net/minecraft/world/entity/EntitySelector.java.patch b/purpur-server/minecraft-patches/sources/net/minecraft/world/entity/EntitySelector.java.patch new file mode 100644 index 000000000..ff1e039a1 --- /dev/null +++ b/purpur-server/minecraft-patches/sources/net/minecraft/world/entity/EntitySelector.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/entity/EntitySelector.java ++++ b/net/minecraft/world/entity/EntitySelector.java +@@ -28,6 +_,8 @@ + return net.minecraft.util.Mth.clamp(serverPlayer.getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE) >= playerInsomniaTicks; + }; + // Paper end - Ability to control player's insomnia and phantoms ++ public static Predicate notAfk = (player) -> !player.isAfk(); // Purpur - AFK API ++ + // Paper start - Affects Spawning API + public static final Predicate PLAYER_AFFECTS_SPAWNING = (entity) -> { + return !entity.isSpectator() && entity.isAlive() && entity instanceof Player player && player.affectsSpawning; diff --git a/purpur-server/minecraft-patches/sources/net/minecraft/world/entity/ai/targeting/TargetingConditions.java.patch b/purpur-server/minecraft-patches/sources/net/minecraft/world/entity/ai/targeting/TargetingConditions.java.patch new file mode 100644 index 000000000..5acb93071 --- /dev/null +++ b/purpur-server/minecraft-patches/sources/net/minecraft/world/entity/ai/targeting/TargetingConditions.java.patch @@ -0,0 +1,13 @@ +--- a/net/minecraft/world/entity/ai/targeting/TargetingConditions.java ++++ b/net/minecraft/world/entity/ai/targeting/TargetingConditions.java +@@ -64,6 +_,10 @@ + return false; + } else if (this.selector != null && !this.selector.test(target, level)) { + return false; ++ // Purpur start - AFK API ++ } else if (!world.purpurConfig.idleTimeoutTargetPlayer && target instanceof net.minecraft.server.level.ServerPlayer player && player.isAfk()) { ++ return false; ++ // Purpur end - AFK API + } else { + if (entity == null) { + if (this.isCombat && (!target.canBeSeenAsEnemy() || level.getDifficulty() == Difficulty.PEACEFUL)) { diff --git a/purpur-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch b/purpur-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch new file mode 100644 index 000000000..6fd1b50f1 --- /dev/null +++ b/purpur-server/minecraft-patches/sources/net/minecraft/world/entity/player/Player.java.patch @@ -0,0 +1,16 @@ +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -205,6 +_,13 @@ + public boolean fauxSleeping; + public int oldLevel = -1; + ++ // Purpur start - AFK API ++ public abstract void setAfk(boolean afk); ++ ++ public boolean isAfk() { ++ return false; ++ } ++ // Purpur end - AFK API + @Override + public org.bukkit.craftbukkit.entity.CraftHumanEntity getBukkitEntity() { + return (org.bukkit.craftbukkit.entity.CraftHumanEntity) super.getBukkitEntity(); diff --git a/purpur-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch b/purpur-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch new file mode 100644 index 000000000..73e6f74b1 --- /dev/null +++ b/purpur-server/minecraft-patches/sources/net/minecraft/world/level/EntityGetter.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/world/level/EntityGetter.java ++++ b/net/minecraft/world/level/EntityGetter.java +@@ -185,7 +_,7 @@ + + default boolean hasNearbyAlivePlayer(double x, double y, double z, double distance) { + for (Player player : this.players()) { +- if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) { ++ if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player) && EntitySelector.notAfk.test(player)) { // Purpur - AFK API + double d = player.distanceToSqr(x, y, z); + if (distance < 0.0 || d < distance * distance) { + return true; diff --git a/purpur-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java.patch b/purpur-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java.patch index bf886aa67..e2400d5f9 100644 --- a/purpur-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java.patch +++ b/purpur-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java.patch @@ -1,6 +1,23 @@ --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -3559,4 +_,10 @@ +@@ -584,10 +_,15 @@ + + @Override + public void setPlayerListName(String name) { ++ // Purpur start - AFK API ++ setPlayerListName(name, false); ++ } ++ public void setPlayerListName(String name, boolean useMM) { ++ // Purpur end - AFK API + if (name == null) { + name = this.getName(); + } +- this.getHandle().listName = name.equals(this.getName()) ? null : CraftChatMessage.fromStringOrNull(name); ++ this.getHandle().listName = name.equals(this.getName()) ? null : useMM ? io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(name)) : CraftChatMessage.fromStringOrNull(name); // Purpur - AFK API + if (this.getHandle().connection == null) return; // Paper - Updates are possible before the player has fully joined + for (ServerPlayer player : (List) this.server.getHandle().players) { + if (player.getBukkitEntity().canSee(this)) { +@@ -3559,4 +_,26 @@ this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundEntityEventPacket(((CraftEntity) target).getHandle(), effect.getData())); } // Paper end - entity effect API @@ -10,4 +27,20 @@ + return getHandle().purpurClient; + } + // Purpur end - Purpur client support ++ // Purpur start - AFK API ++ @Override ++ public boolean isAfk() { ++ return getHandle().isAfk(); ++ } ++ ++ @Override ++ public void setAfk(boolean setAfk) { ++ getHandle().setAfk(setAfk); ++ } ++ ++ @Override ++ public void resetIdleTimer() { ++ getHandle().resetLastActionTime(); ++ } ++ // Purpur end - AFK API } diff --git a/purpur-server/src/main/java/org/purpurmc/purpur/PurpurConfig.java b/purpur-server/src/main/java/org/purpurmc/purpur/PurpurConfig.java index a2316742b..103b90bc6 100644 --- a/purpur-server/src/main/java/org/purpurmc/purpur/PurpurConfig.java +++ b/purpur-server/src/main/java/org/purpurmc/purpur/PurpurConfig.java @@ -2,6 +2,7 @@ package org.purpurmc.purpur; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockBehaviour; @@ -157,8 +158,18 @@ public class PurpurConfig { } public static String cannotRideMob = "You cannot mount that mob"; + public static String afkBroadcastAway = "%s is now AFK"; + public static String afkBroadcastBack = "%s is no longer AFK"; + public static boolean afkBroadcastUseDisplayName = false; + public static String afkTabListPrefix = "[AFK] "; + public static String afkTabListSuffix = ""; private static void messages() { cannotRideMob = getString("settings.messages.cannot-ride-mob", cannotRideMob); + afkBroadcastAway = getString("settings.messages.afk-broadcast-away", afkBroadcastAway); + afkBroadcastBack = getString("settings.messages.afk-broadcast-back", afkBroadcastBack); + afkBroadcastUseDisplayName = getBoolean("settings.messages.afk-broadcast-use-display-name", afkBroadcastUseDisplayName); + afkTabListPrefix = MiniMessage.miniMessage().serialize(MiniMessage.miniMessage().deserialize(getString("settings.messages.afk-tab-list-prefix", afkTabListPrefix))); + afkTabListSuffix = MiniMessage.miniMessage().serialize(MiniMessage.miniMessage().deserialize(getString("settings.messages.afk-tab-list-suffix", afkTabListSuffix))); } public static int barrelRows = 3; diff --git a/purpur-server/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java b/purpur-server/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java index 6a745f39f..60179ffff 100644 --- a/purpur-server/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +++ b/purpur-server/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java @@ -71,6 +71,24 @@ public class PurpurWorldConfig { return value.isEmpty() ? fallback : value; } + public boolean idleTimeoutKick = true; + public boolean idleTimeoutTickNearbyEntities = true; + public boolean idleTimeoutCountAsSleeping = false; + public boolean idleTimeoutUpdateTabList = false; + public boolean idleTimeoutTargetPlayer = true; + private void playerSettings() { + if (PurpurConfig.version < 19) { + boolean oldVal = getBoolean("gameplay-mechanics.player.idle-timeout.mods-target", idleTimeoutTargetPlayer); + set("gameplay-mechanics.player.idle-timeout.mods-target", null); + set("gameplay-mechanics.player.idle-timeout.mobs-target", oldVal); + } + idleTimeoutKick = System.getenv("PURPUR_FORCE_IDLE_KICK") == null ? getBoolean("gameplay-mechanics.player.idle-timeout.kick-if-idle", idleTimeoutKick) : Boolean.parseBoolean(System.getenv("PURPUR_FORCE_IDLE_KICK")); + idleTimeoutTickNearbyEntities = getBoolean("gameplay-mechanics.player.idle-timeout.tick-nearby-entities", idleTimeoutTickNearbyEntities); + idleTimeoutCountAsSleeping = getBoolean("gameplay-mechanics.player.idle-timeout.count-as-sleeping", idleTimeoutCountAsSleeping); + idleTimeoutUpdateTabList = getBoolean("gameplay-mechanics.player.idle-timeout.update-tab-list", idleTimeoutUpdateTabList); + idleTimeoutTargetPlayer = getBoolean("gameplay-mechanics.player.idle-timeout.mobs-target", idleTimeoutTargetPlayer); + } + public boolean babiesAreRidable = true; public boolean untamedTamablesAreRidable = true; public boolean useNightVisionWhenRiding = false;