From 7cf9caf05b90f00fe9fa3a22806648218994306a Mon Sep 17 00:00:00 2001 From: Dart2112 Date: Fri, 31 Jan 2025 17:21:38 +1030 Subject: [PATCH] Fixes #83 Minor JavaDoc cleanup Implemented sessions in a way that isn't destructive to the current way of tracking AFK state --- .idea/dictionaries/benja.xml | 1 + .../java/net/lapismc/afkplus/AFKPlus.java | 14 ++- .../net/lapismc/afkplus/AFKPlusListeners.java | 17 +-- .../afkplus/playerdata/AFKPlusPlayer.java | 76 +++++++++++-- .../afkplus/playerdata/AFKSession.java | 101 ++++++++++++++++++ src/main/resources/config.yml | 7 +- src/main/resources/messages.yml | 4 +- 7 files changed, 198 insertions(+), 22 deletions(-) create mode 100644 src/main/java/net/lapismc/afkplus/playerdata/AFKSession.java diff --git a/.idea/dictionaries/benja.xml b/.idea/dictionaries/benja.xml index 842a205..51d3505 100644 --- a/.idea/dictionaries/benja.xml +++ b/.idea/dictionaries/benja.xml @@ -5,6 +5,7 @@ collidable levelup redstone + relogging \ No newline at end of file diff --git a/src/main/java/net/lapismc/afkplus/AFKPlus.java b/src/main/java/net/lapismc/afkplus/AFKPlus.java index 66a20f7..e1394d1 100644 --- a/src/main/java/net/lapismc/afkplus/AFKPlus.java +++ b/src/main/java/net/lapismc/afkplus/AFKPlus.java @@ -21,6 +21,7 @@ import net.lapismc.afkplus.commands.AFK; import net.lapismc.afkplus.commands.AFKPlusCmd; import net.lapismc.afkplus.playerdata.AFKPlusPlayer; +import net.lapismc.afkplus.playerdata.AFKSession; import net.lapismc.afkplus.util.AFKPlusConfiguration; import net.lapismc.afkplus.util.AFKPlusContext; import net.lapismc.lapiscore.LapisCorePlugin; @@ -44,16 +45,17 @@ public final class AFKPlus extends LapisCorePlugin { public PrettyTime prettyTime; public LapisUpdater updater; private final HashMap players = new HashMap<>(); + private final HashMap playerSessions = new HashMap<>(); private AFKPlusListeners listeners; @Override public void onEnable() { saveDefaultConfig(); - registerConfiguration(new AFKPlusConfiguration(this, 17, 6)); + registerConfiguration(new AFKPlusConfiguration(this, 18, 7)); registerPermissions(new AFKPlusPermissions(this)); registerLuckPermsContext(); update(); - new LapisCoreFileWatcher(this); + fileWatcher = new LapisCoreFileWatcher(this); Locale loc = new Locale(config.getMessage("PrettyTimeLocale")); prettyTime = new PrettyTime(loc); prettyTime.removeUnit(JustNow.class); @@ -93,6 +95,14 @@ public AFKPlusPlayer getPlayer(OfflinePlayer op) { return getPlayer(op.getUniqueId()); } + public AFKSession getPlayerSession(UUID uuid) { + return playerSessions.getOrDefault(uuid, null); + } + + public void storeAFKSession(AFKSession session) { + playerSessions.put(session.getUUID(), session); + } + private void update() { //Don't check for updates if the update check setting is set to false if (!getConfig().getBoolean("UpdateCheck")) diff --git a/src/main/java/net/lapismc/afkplus/AFKPlusListeners.java b/src/main/java/net/lapismc/afkplus/AFKPlusListeners.java index a008c82..de6569a 100644 --- a/src/main/java/net/lapismc/afkplus/AFKPlusListeners.java +++ b/src/main/java/net/lapismc/afkplus/AFKPlusListeners.java @@ -18,6 +18,7 @@ import net.lapismc.afkplus.api.AFKMachineDetectEvent; import net.lapismc.afkplus.playerdata.AFKPlusPlayer; +import net.lapismc.afkplus.playerdata.AFKSession; import net.lapismc.afkplus.util.EntitySpawnManager; import net.lapismc.afkplus.util.PlayerMovementMonitoring; import net.lapismc.afkplus.util.PlayerMovementStorage; @@ -63,17 +64,19 @@ public class AFKPlusListeners implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent e) { - //TODO: Check here if the player has been offline for some amount of time. - //If they have only recently left then we wont run forceStop since it triggers an interact - //This will stop players from reconnecting to reset their AFK timer - //If the player has been offline for more than a few minutes, we can run forceStop as we used to - //This will ensure that all settings are reset to start tracking AFK time and interacts from right now - plugin.getPlayer(e.getPlayer()).forceStopAFK(); + //Load the players session if one is stored + AFKSession session = plugin.getPlayerSession(e.getPlayer().getUniqueId()); + //If the session is null, it means we didn't have one stored, so run the old code + //But otherwise we let the session handle it + if (session == null) + plugin.getPlayer(e.getPlayer()).forceStopAFK(); + else + session.processReconnect(plugin.getPlayer(e.getPlayer())); } @EventHandler public void onPlayerQuit(PlayerQuitEvent e) { - plugin.getPlayer(e.getPlayer()).stopAFK(true); + plugin.storeAFKSession(new AFKSession(plugin, plugin.getPlayer(e.getPlayer()))); } @EventHandler diff --git a/src/main/java/net/lapismc/afkplus/playerdata/AFKPlusPlayer.java b/src/main/java/net/lapismc/afkplus/playerdata/AFKPlusPlayer.java index 7b4f081..8864ec5 100644 --- a/src/main/java/net/lapismc/afkplus/playerdata/AFKPlusPlayer.java +++ b/src/main/java/net/lapismc/afkplus/playerdata/AFKPlusPlayer.java @@ -64,7 +64,7 @@ public AFKPlusPlayer(AFKPlus plugin, UUID uuid) { /** * Get the UUID of the player * - * @return Returns the UUID of the player + * @return the UUID of the player */ public UUID getUUID() { return uuid; @@ -73,7 +73,7 @@ public UUID getUUID() { /** * Get the players username * - * @return Returns the name of the player + * @return the name of the player */ public String getName() { return Bukkit.getOfflinePlayer(uuid).getName(); @@ -104,7 +104,7 @@ public boolean isInactive() { * Permissions are stored in {@link Permission} as an Enumeration * * @param perm The permission you wish to check - * @return Returns true if the player DOESN'T have the permission + * @return true if the player DOESN'T have the permission */ public boolean isNotPermitted(Permission perm) { return !plugin.perms.isPermitted(uuid, perm.getPermission()); @@ -126,10 +126,28 @@ public void warnPlayer() { playSound("WarningSound", XSound.ENTITY_PLAYER_LEVELUP); } + /** + * Check if the user has received a warning about being acted upon for being AFK + * + * @return true if the player has been warned, otherwise false + */ + public boolean isWarned() { + return isWarned; + } + + /** + * Set the warned state of the user, this is only used when resuming sessions + * + * @param isWarned the desired state of isWarned + */ + protected void setIsWarned(boolean isWarned) { + this.isWarned = isWarned; + } + /** * Check if the player is AFK * - * @return returns true if the player is currently AFK + * @return true if the player is currently AFK */ public boolean isAFK() { return isAFK; @@ -138,22 +156,40 @@ public boolean isAFK() { /** * Check if the players AFK state is fake * - * @return returns true if the player is both AFK and the AFK state is faked + * @return true if the player is both AFK and the AFK state is faked */ public boolean isFakeAFK() { return isAFK && isFakeAFK; } + /** + * Set the fake AFK attribute after the fact, this is only used to restore sessions + * + * @param isFakeAFK the desired value for isFakeAFK + */ + protected void setFakeAFK(boolean isFakeAFK) { + this.isFakeAFK = isFakeAFK; + } + /** * Get the system time when the player became AFK * Could be null if the player is not AFK * - * @return Returns the System.currentTimeMillis() when the player was set AFK + * @return the System.currentTimeMillis() when the player was set AFK */ public Long getAFKStart() { return afkStart; } + /** + * Set the time when the player entered AFK, this is used for resuming AFK after reconnect + * + * @param afkStart The UNIX Epoch of when the player entered AFK + */ + protected void setAFKStart(Long afkStart) { + this.afkStart = afkStart; + } + /** * Starts AFK for this player with a broadcast, Use {@link #forceStartAFK()} for silent AFK * This can be cancelled with {@link AFKStartEvent} @@ -286,7 +322,7 @@ public void forceStopAFK() { /** * Check if the player's AFK status should be broadcast to other players * - * @return Returns whether the player's AFK message should be broadcast to other players + * @return whether the player's AFK message should be broadcast to other players */ public boolean shouldBroadcastToOthers() { boolean vanish = plugin.getConfig().getBoolean("Broadcast.Vanish"); @@ -353,8 +389,8 @@ public void takeAction() { * This will stop AFK if a player is AFK and update the lastInteract value */ public void interact() { - //Don't allow interact when the player is inactive - //Inactive is decided by the listener class checking location data + //Don't allow the player to interact when the player is inactive + //Inactivity is decided by the listener class checking location data if (isInactive) return; lastInteract = System.currentTimeMillis(); @@ -363,10 +399,28 @@ public void interact() { stopAFK(); } + /** + * This is the UNIX Epoch at the time that the player last triggered an interact event based on the current config + * + * @return the last time the player interacted + */ + public Long getLastInteract() { + return lastInteract; + } + + /** + * Set the last interact time for this player, used when resuming sessions + * + * @param lastInteract the UNIX Epoch at the time the player last interacted with the world + */ + protected void setLastInteract(Long lastInteract) { + this.lastInteract = lastInteract; + } + /** * Check if a player is currently vanished * - * @return Returns true if the player is currently vanished + * @return true if the player is currently vanished */ public boolean isVanished() { if (!isOnline()) { @@ -492,7 +546,7 @@ private void recordTimeStatistic() { * It is run every second by default * This should not be used else where * - * @return Returns the runnable used for AFK detection + * @return the runnable used for AFK detection */ public Runnable getRepeatingTask() { return () -> { diff --git a/src/main/java/net/lapismc/afkplus/playerdata/AFKSession.java b/src/main/java/net/lapismc/afkplus/playerdata/AFKSession.java new file mode 100644 index 0000000..ac37f67 --- /dev/null +++ b/src/main/java/net/lapismc/afkplus/playerdata/AFKSession.java @@ -0,0 +1,101 @@ +/* + * Copyright 2025 Benjamin Martin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.lapismc.afkplus.playerdata; + +import net.lapismc.afkplus.AFKPlus; +import org.bukkit.Bukkit; + +import java.util.UUID; + +/** + * A class for tracking players who have disconnected from the server + * This class is used to ensure that players cannot skirt AFK tracking by relogging + */ +public class AFKSession { + + private final AFKPlus plugin; + private final UUID playerUUID; + private final boolean isAFK, isFakeAFK, isWarned, isInactive; + private final Long disconnectTime, relativeLastInteract, relativeAFKStart; + + /** + * Create a session for the provided player and record the disconnect time as now + * + * @param plugin The plugins main class for config access + * @param player The player who is disconnecting + */ + public AFKSession(AFKPlus plugin, AFKPlusPlayer player) { + this.plugin = plugin; + playerUUID = player.getUUID(); + isAFK = player.isAFK(); + relativeAFKStart = System.currentTimeMillis() - player.getAFKStart(); + isFakeAFK = player.isFakeAFK(); + isWarned = player.isWarned(); + isInactive = player.isInactive(); + relativeLastInteract = System.currentTimeMillis() - player.getLastInteract(); + disconnectTime = System.currentTimeMillis(); + } + + /** + * Process a player reconnecting, this is mostly setting AFKPlayer values + * + * @param p The player who has connected + */ + public void processReconnect(AFKPlusPlayer p) { + //Check if it's a short enough offline time to process this as a reconnect vs a new session + //Milliseconds since the user disconnected + long millisOffline = System.currentTimeMillis() - disconnectTime; + //Minutes since the user disconnected + float minutesOffline = millisOffline / 1000.0f / 60.0f; + double sessionLength = plugin.getConfig().getDouble("SessionLength"); + if (sessionLength < minutesOffline) { + //The user has been offline longer than the session length + //Therefore we ignore the users session and reset them + p.forceStopAFK(); + //return so that the session logic is skipped + return; + } + if (isAFK) { + //If the player was AFK, set them as AFK and set the AFKTime + p.forceStartAFK(); + //Calculate what the AFK start time would've been if the disconnect didn't happen + Long AFKStart = System.currentTimeMillis() - relativeAFKStart; + p.setAFKStart(AFKStart); + //Make sure we update the FakeAFK status + if (isFakeAFK) + p.setFakeAFK(true); + if (isWarned) + p.setIsWarned(true); + //Send a message to the player to let them know that their AFK state has been resumed + Bukkit.getScheduler().runTaskLater(plugin, () -> + Bukkit.getPlayer(playerUUID).sendMessage(plugin.config.getMessage("Self.Resume")), 20); + } + p.setInactive(isInactive); + //Set the last interact time based on the value at disconnect + p.setLastInteract(System.currentTimeMillis() - relativeLastInteract); + } + + /** + * Get the UUID of the player that this class represents + * + * @return a player UUID + */ + public UUID getUUID() { + return playerUUID; + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 39ab135..87be6a8 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,4 +1,4 @@ -ConfigVersion: 17 +ConfigVersion: 18 #Should the plugin check for new updates, UpdateDownload will not work if this is set to false UpdateCheck: true @@ -23,6 +23,11 @@ Commands: #Setting to 0 means that players will always be acted upon when they reach their time to action ActionPlayerRequirement: 0 +#Session Length refers to how long a player needs to be offline before their AFK state is reset +#If a player quits while AFK and rejoins before this number of minutes, they will be set as AFK automatically on join +#This value is in minutes, you can enter fractions of a minute, e.g. 0.5 = 30 seconds +SessionLength: 5.0 + #Enabling this setting will make AFKPlus update a players AFK status in essentials to match their AFKPlus AFK state #This may be useful for other plugins that check if a player is AFK by checking with Essentials EssentialsAFKHook: false diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 6f9e23a..15e90a1 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -1,4 +1,4 @@ -ConfigVersion: 6 +ConfigVersion: 7 #Primary and secondary colors will replace &p and &s in any messages PrimaryColor: "&6" @@ -23,6 +23,8 @@ Self: Start: "{PREFIX} &sYou&p are now AFK" #You may also add a {TIME} variable that will be replaced by how long the player was AFK Stop: "{PREFIX} &sYou&p are no longer AFK" + #The message sent to players when they join and are immediately set as AFK + Resume: "{PREFIX} &pYour AFK State has been resumed!" Updater: NoUpdate: "&pThere is no update available"