From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath <blake.galbreath@gmail.com>
Date: Thu, 8 Aug 2019 15:29:15 -0500
Subject: [PATCH] AFK API


diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index 651423979d4d5acbcf0bc25d6225a72a16e21826..bd4583ff7d76b83003ca327e8301fb1d5ff18f66 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -1955,8 +1955,58 @@ public class ServerPlayer extends Player {
 
     public void resetLastActionTime() {
         this.lastActionTime = Util.getMillis();
+        this.setAfk(false); // Purpur
     }
 
+    // Purpur Start
+    private boolean isAfk = false;
+
+    @Override
+    public void setAfk(boolean afk) {
+        if (this.isAfk == afk) {
+            return;
+        }
+
+        String msg = afk ? net.pl3x.purpur.PurpurConfig.afkBroadcastAway : net.pl3x.purpur.PurpurConfig.afkBroadcastBack;
+
+        net.pl3x.purpur.event.PlayerAFKEvent event = new net.pl3x.purpur.event.PlayerAFKEvent(this.getBukkitEntity(), afk, 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()) {
+            server.getPlayerList().broadcast(String.format(msg, this.getGameProfile().getName()));
+        }
+
+        if (level.purpurConfig.idleTimeoutUpdateTabList) {
+            if (afk) {
+                getBukkitEntity().setPlayerListName(net.pl3x.purpur.PurpurConfig.afkTabListPrefix + getScoreboardName() + net.pl3x.purpur.PurpurConfig.afkTabListSuffix, true);
+            } else {
+                getBukkitEntity().setPlayerListName(getScoreboardName());
+            }
+        }
+
+        ((ServerLevel) level).updateSleepingPlayerList();
+    }
+
+    @Override
+    public boolean isAfk() {
+        return this.isAfk;
+    }
+
+    @Override
+    public boolean canBeCollidedWith() {
+        return !this.isAfk() && super.canBeCollidedWith();
+    }
+    // Purpur End
+
     public ServerStatsCounter getStats() {
         return this.stats;
     }
diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index c02b3cdd93df7806d52138b02612dd48a451962b..f6a091ab2f549761b8d19fb35e1db907954a9717 100644
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -390,6 +390,12 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
         }
 
         if (this.player.getLastActionTime() > 0L && this.server.getPlayerIdleTimeout() > 0 && Util.getMillis() - this.player.getLastActionTime() > (long) (this.server.getPlayerIdleTimeout() * 1000 * 60) && !this.player.wonGame) { // Paper - Prevent AFK kick while watching end credits.
+            // Purpur start
+            this.player.setAfk(true);
+            if (!this.player.level.purpurConfig.idleTimeoutKick) {
+                return;
+            }
+            // Purpur end
             this.player.resetLastActionTime(); // CraftBukkit - SPIGOT-854
             this.disconnect(new TranslatableComponent("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause
         }
@@ -662,6 +668,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
                     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
+
                     // Skip the first time we do this
                     if (true) { // Spigot - don't skip any move events
                         Location oldTo = to.clone();
@@ -1420,7 +1428,7 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
 
                             if (!this.player.isChangingDimension() && d11 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot
                                 flag1 = true; // Tuinity - diff on change, this should be moved wrongly
-                                ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!", this.player.getName().getString());
+                                ServerGamePacketListenerImpl.LOGGER.warn("{} moved wrongly!, ({})", this.player.getName().getString(), d11); // Purpur
                             }
 
                             this.player.absMoveTo(d0, d1, d2, f, f1);
@@ -1470,6 +1478,8 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Ser
                                     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
+
                                     // Skip the first time we do this
                                     if (from.getX() != Double.MAX_VALUE) {
                                         Location oldTo = to.clone();
diff --git a/src/main/java/net/minecraft/server/players/SleepStatus.java b/src/main/java/net/minecraft/server/players/SleepStatus.java
index 823efad652d8ff9e96b99375b102fef6f017716e..60f89d7c77a5e792e21e93e35ed1670bd565799a 100644
--- a/src/main/java/net/minecraft/server/players/SleepStatus.java
+++ b/src/main/java/net/minecraft/server/players/SleepStatus.java
@@ -19,7 +19,7 @@ public class SleepStatus {
 
     public boolean areEnoughDeepSleeping(int percentage, List<ServerPlayer> 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
         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
                     ++this.sleepingPlayers;
                 }
                 // CraftBukkit start
diff --git a/src/main/java/net/minecraft/world/entity/EntitySelector.java b/src/main/java/net/minecraft/world/entity/EntitySelector.java
index a060cca08631fb42041e3a79a9abc422fe7757af..e7b11d1ba984ea14f0cdf8e84f9eaab46b3f1684 100644
--- a/src/main/java/net/minecraft/world/entity/EntitySelector.java
+++ b/src/main/java/net/minecraft/world/entity/EntitySelector.java
@@ -27,6 +27,7 @@ public final class EntitySelector {
         return !entity.isSpectator();
     };
     public static Predicate<Player> isInsomniac = (player) -> net.minecraft.util.Mth.clamp(((net.minecraft.server.level.ServerPlayer) player).getStats().getValue(net.minecraft.stats.Stats.CUSTOM.get(net.minecraft.stats.Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE) >= 72000; // Paper
+    public static Predicate<Player> notAfk = (player) -> !player.isAfk(); // Purpur
 
     private EntitySelector() {}
     // Paper start
diff --git a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java
index 8a0aea6b28295e03aaac1768336b1bc36d9ad9e9..30fef94aa85d7caa4c785ef0a7de4f3cd19811f5 100644
--- a/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java
+++ b/src/main/java/net/minecraft/world/entity/ai/targeting/TargetingConditions.java
@@ -63,6 +63,10 @@ public class TargetingConditions {
             return false;
         } else if (this.selector != null && !this.selector.test(targetEntity)) {
             return false;
+        // Purpur start
+        } else if (!targetEntity.level.purpurConfig.idleTimeoutTargetPlayer && targetEntity instanceof net.minecraft.server.level.ServerPlayer player && player.isAfk()) {
+            return false;
+        // Purpur end
         } else {
             if (baseEntity == null) {
                 if (this.isCombat && (!targetEntity.canBeSeenAsEnemy() || targetEntity.level.getDifficulty() == Difficulty.PEACEFUL)) {
diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java
index 19b65c0d2fd96b4516c0717511a6bb02b2d66065..d761865eabfc67651b0a2a92ad35fc236886b61d 100644
--- a/src/main/java/net/minecraft/world/entity/player/Player.java
+++ b/src/main/java/net/minecraft/world/entity/player/Player.java
@@ -196,6 +196,13 @@ public abstract class Player extends LivingEntity {
     // Purpur start
     public abstract void resetLastActionTime();
 
+    public void setAfk(boolean afk) {
+    }
+
+    public boolean isAfk() {
+        return false;
+    }
+
     @Override
     public boolean processClick(InteractionHand hand) {
         Entity vehicle = getRootVehicle();
diff --git a/src/main/java/net/minecraft/world/level/EntityGetter.java b/src/main/java/net/minecraft/world/level/EntityGetter.java
index 6a4e44dd8935018d1b5283761dfb8e855be62987..afe70b0d5bd98d05bbb7afc756108f09d35a5848 100644
--- a/src/main/java/net/minecraft/world/level/EntityGetter.java
+++ b/src/main/java/net/minecraft/world/level/EntityGetter.java
@@ -161,7 +161,7 @@ public interface EntityGetter {
 
     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)) {
                 double d = player.distanceToSqr(x, y, z);
                 if (range < 0.0D || d < range * range) {
                     return true;
diff --git a/src/main/java/net/pl3x/purpur/PurpurConfig.java b/src/main/java/net/pl3x/purpur/PurpurConfig.java
index e9c2ad24aea7861d8d3c1f8879f70e8fa514fa05..4451d9fca6dad1406b8da348e34d62123b0a08e5 100644
--- a/src/main/java/net/pl3x/purpur/PurpurConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurConfig.java
@@ -158,8 +158,16 @@ public class PurpurConfig {
     }
 
     public static String cannotRideMob = "<red>You cannot mount that mob";
+    public static String afkBroadcastAway = "<yellow><italic>%s is now AFK";
+    public static String afkBroadcastBack = "<yellow><italic>%s is no longer AFK";
+    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);
+        afkTabListPrefix = getString("settings.messages.afk-tab-list-prefix", afkTabListPrefix);
+        afkTabListSuffix = getString("settings.messages.afk-tab-list-suffix", afkTabListSuffix);
     }
 
     private static void timingsSettings() {
diff --git a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
index 3627b50dd3af5da225004cd3f8158fcbd30d8ea3..61a0bc8acff72431318bf8fe4f9d5018f85bacc5 100644
--- a/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurWorldConfig.java
@@ -93,6 +93,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 = getBoolean("gameplay-mechanics.player.idle-timeout.kick-if-idle", idleTimeoutKick);
+        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/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
index 03ac2878f54ca994b5faf28fa68aa547bd7388c3..4254b3168f34b5d6ab9ac8bcece37ab02478b8e9 100644
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
@@ -424,10 +424,15 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
 
     @Override
     public void setPlayerListName(String name) {
+        // Purpur start
+        setPlayerListName(name, false);
+    }
+    public void setPlayerListName(String name, boolean useMM) {
+        // Purpur end
         if (name == null) {
             name = getName();
         }
-        this.getHandle().listName = name.equals(getName()) ? null : CraftChatMessage.fromStringOrNull(name);
+        this.getHandle().listName = name.equals(getName()) ? null : useMM ? io.papermc.paper.adventure.PaperAdventure.asVanilla(net.kyori.adventure.text.minimessage.MiniMessage.get().parse(name)) : CraftChatMessage.fromStringOrNull(name); // Purpur
         for (ServerPlayer player : (List<ServerPlayer>) server.getHandle().players) {
             if (player.getBukkitEntity().canSee(this)) {
                 player.connection.send(new ClientboundPlayerInfoPacket(ClientboundPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME, this.getHandle()));
@@ -2555,5 +2560,20 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
     public boolean usesPurpurClient() {
         return getHandle().purpurClient;
     }
+
+    @Override
+    public boolean isAfk() {
+        return getHandle().isAfk();
+    }
+
+    @Override
+    public void setAfk(boolean setAfk) {
+        getHandle().setAfk(setAfk);
+    }
+
+    @Override
+    public void resetIdleTimer() {
+        getHandle().resetLastActionTime();
+    }
     // Purpur end
 }
diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java
index 0d56df555b8d2de12447cc2e869c9eba20c5ea6d..e58db9acba9fd4f0b7e93ad82ae823c43246fde7 100644
--- a/src/main/java/org/spigotmc/ActivationRange.java
+++ b/src/main/java/org/spigotmc/ActivationRange.java
@@ -198,6 +198,7 @@ public class ActivationRange
         {
 
             player.activatedTick = MinecraftServer.currentTick;
+            if (!player.level.purpurConfig.idleTimeoutTickNearbyEntities && player.isAfk()) continue; // Purpur
             ActivationRange.maxBB = player.getBoundingBox().inflate( maxRange, 256, maxRange );
             ActivationType.MISC.boundingBox = player.getBoundingBox().inflate( miscActivationRange, 256, miscActivationRange );
             ActivationType.RAIDER.boundingBox = player.getBoundingBox().inflate( raiderActivationRange, 256, raiderActivationRange );
