From c6492323956a3efc719054449a48ab26cec48f18 Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 1 Oct 2026 18:58:18 +0000 Subject: [PATCH 1/2] feat: log each online player's position once a minute Add a "player-pings" option (seconds, default 60, 0 disables). A repeating task records each online player's position as a session row with the new ping action, so purge, per-world config, every storage backend and the existing (user,time) index already handle it. No schema change is needed. "/co lookup a:ping" lists the pings. "a:session" now lists only logins and logouts, and "a:session,ping" lists both. The session API methods keep returning only logins and logouts. Session lookups no longer require the block or click lookup permissions, which they only hit because session actions reuse those ids. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 2 +- lang/en.yml | 1 + .../coreprotect/api/SessionLookup.java | 4 +- .../coreprotect/command/LookupCommand.java | 6 +- .../coreprotect/command/TabHandler.java | 2 +- .../command/lookup/StandardLookupThread.java | 6 ++ .../command/parser/ActionParser.java | 6 ++ .../coreprotect/config/Config.java | 4 ++ .../coreprotect/consumer/Queue.java | 4 ++ .../consumer/process/PlayerPingProcess.java | 18 +++++ .../coreprotect/consumer/process/Process.java | 4 ++ .../coreprotect/database/Lookup.java | 4 ++ .../coreprotect/database/LookupRaw.java | 7 ++ .../coreprotect/language/Language.java | 1 + .../coreprotect/language/Phrase.java | 1 + .../coreprotect/listener/ListenerHandler.java | 2 + .../listener/player/PlayerPingListener.java | 68 +++++++++++++++++++ .../model/action/SessionActions.java | 3 + .../services/PluginInitializationService.java | 4 ++ 19 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/tfminecraft/coreprotect/consumer/process/PlayerPingProcess.java create mode 100644 src/main/java/net/tfminecraft/coreprotect/listener/player/PlayerPingListener.java diff --git a/README.md b/README.md index 68927ad..8965ef3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This repository is TF-Minecraft's fork of CoreProtect, developed by [PlayPro](ht - **Detailed activity searches** — narrow results by player, time, action, location, and affected materials. - **Targeted recovery** — roll back selected changes and restore previously rolled-back activity. - **Recovery previews** — review the effect of a rollback or restore before applying it. -- **Broad world coverage** — logging includes player block changes, container contents, explosions, fire, liquids, and many entity interactions. +- **Broad world coverage** — logging includes player block changes, container contents, explosions, fire, liquids, many entity interactions, and where each online player is once a minute. - **Staff investigation tools** — paginated lookups, inventory history, and WorldEdit selection support help investigate incidents of different sizes. CoreProtect supports both investigation and recovery; the recorded history available to staff depends on the server's logging settings. diff --git a/lang/en.yml b/lang/en.yml index b4287ba..214db09 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -129,6 +129,7 @@ LOOKUP_LOCK_CHANGE: "{0} set {1} lock to {2}." LOOKUP_LOCK_CHANGE_STAFF: "{0} set {1} lock to {2} (staff override)." LOOKUP_ITEM: "{0} {picked up|dropped} {1} {2}." LOOKUP_LOGIN: "{0} logged {in|out}." +LOOKUP_PING: "{0} was online here." LOOKUP_PAGE: "Page {0}" LOOKUP_PROJECTILE: "{0} {threw|shot} {1} {2}." LOOKUP_ROWS_FOUND: "{0} {row|rows} found." diff --git a/src/main/java/net/tfminecraft/coreprotect/api/SessionLookup.java b/src/main/java/net/tfminecraft/coreprotect/api/SessionLookup.java index 24cffee..6035295 100644 --- a/src/main/java/net/tfminecraft/coreprotect/api/SessionLookup.java +++ b/src/main/java/net/tfminecraft/coreprotect/api/SessionLookup.java @@ -12,6 +12,7 @@ import net.tfminecraft.coreprotect.config.ConfigHandler; import net.tfminecraft.coreprotect.database.Database; import net.tfminecraft.coreprotect.database.statement.UserStatement; +import net.tfminecraft.coreprotect.model.action.SessionActions; import net.tfminecraft.coreprotect.utility.WorldUtils; import net.tfminecraft.coreprotect.utility.ErrorReporter; @@ -119,6 +120,7 @@ public static List performLookup(LookupOptions options) { query.append(WorldUtils.getWidIndex("session")); } filter.appendWhere(query); + query.append(" AND action IN(").append(SessionActions.LOGIN).append(',').append(SessionActions.LOGOUT).append(')'); query.append(" ORDER BY ").append(ConfigHandler.getDescendingEventOrder()); filter.appendLimit(query); @@ -162,7 +164,7 @@ private static int calculateCheckTime(int currentTime, int offset) { * @return The SQL query string */ private static String buildSessionQuery(int userId, int checkTime) { - return "SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",wid,x,y,z,action FROM " + ConfigHandler.prefix + "session WHERE " + ConfigHandler.databaseType.getUserColumn() + " = " + userId + " AND time > " + checkTime + " ORDER BY " + ConfigHandler.getDescendingEventOrder(); + return "SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",wid,x,y,z,action FROM " + ConfigHandler.prefix + "session WHERE " + ConfigHandler.databaseType.getUserColumn() + " = " + userId + " AND time > " + checkTime + " AND action IN(" + SessionActions.LOGIN + "," + SessionActions.LOGOUT + ") ORDER BY " + ConfigHandler.getDescendingEventOrder(); } /** diff --git a/src/main/java/net/tfminecraft/coreprotect/command/LookupCommand.java b/src/main/java/net/tfminecraft/coreprotect/command/LookupCommand.java index 5b6d70f..789647d 100755 --- a/src/main/java/net/tfminecraft/coreprotect/command/LookupCommand.java +++ b/src/main/java/net/tfminecraft/coreprotect/command/LookupCommand.java @@ -230,11 +230,13 @@ else if (resultc > 1) { allPermission = true; } if (!allPermission) { - if (!pageLookup && (argAction.isEmpty() || argAction.contains(LookupActions.BLOCK_BREAK) || argAction.contains(LookupActions.BLOCK_PLACE)) && !player.hasPermission("coreprotect.lookup.block")) { + // Session lookups reuse the block action ids for login, logout and ping + boolean sessionLookup = argAction.contains(LookupActions.SESSION); + if (!pageLookup && !sessionLookup && (argAction.isEmpty() || argAction.contains(LookupActions.BLOCK_BREAK) || argAction.contains(LookupActions.BLOCK_PLACE)) && !player.hasPermission("coreprotect.lookup.block")) { Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION)); return; } - if (argAction.contains(LookupActions.INTERACTION) && !player.hasPermission("coreprotect.lookup.click")) { + if (!sessionLookup && argAction.contains(LookupActions.INTERACTION) && !player.hasPermission("coreprotect.lookup.click")) { Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.NO_PERMISSION)); return; } diff --git a/src/main/java/net/tfminecraft/coreprotect/command/TabHandler.java b/src/main/java/net/tfminecraft/coreprotect/command/TabHandler.java index bddb7b2..bbe39a6 100755 --- a/src/main/java/net/tfminecraft/coreprotect/command/TabHandler.java +++ b/src/main/java/net/tfminecraft/coreprotect/command/TabHandler.java @@ -25,7 +25,7 @@ public class TabHandler implements TabCompleter { // private static String[] COMMANDS = new String[] { "help", "inspect", "rollback", "restore", "lookup", "purge", "reload", "status", "near", "undo" }; // max 10! private static final String[] HELP = new String[] { "inspect", "rollback", "restore", "lookup", "purge", "teleport", "status", "params", "users", "time", "radius", "action", "include", "exclude", "filter" }; private static final String[] PARAMS = new String[] { "user:", "time:", "radius:", "action:", "include:", "exclude:", "filter:", "#container" }; - private static final String[] ACTIONS = new String[] { "block", "+block", "-block", "click", "kill", "spawn", "+container", "-container", "container", "chat", "command", "+inventory", "-inventory", "inventory", "item", "+item", "-item", "sign", "session", "+session", "-session", "username" }; + private static final String[] ACTIONS = new String[] { "block", "+block", "-block", "click", "kill", "spawn", "+container", "-container", "container", "chat", "command", "+inventory", "-inventory", "inventory", "item", "+item", "-item", "sign", "session", "+session", "-session", "ping", "username" }; private static final String[] NUMBERS = new String[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }; private static final String[] TIMES = new String[] { "w", "d", "h", "m", "s" }; private static ArrayList materials = null; diff --git a/src/main/java/net/tfminecraft/coreprotect/command/lookup/StandardLookupThread.java b/src/main/java/net/tfminecraft/coreprotect/command/lookup/StandardLookupThread.java index d9f5e81..28563a1 100644 --- a/src/main/java/net/tfminecraft/coreprotect/command/lookup/StandardLookupThread.java +++ b/src/main/java/net/tfminecraft/coreprotect/command/lookup/StandardLookupThread.java @@ -392,6 +392,12 @@ else if (actions.contains(LookupActions.SESSION)) { leftPadding = leftPadding + Color.WHITE + Strings.padStart("", (timeLength - 50) / 4, ' '); } + if (action == SessionActions.PING) { + Chat.sendComponent(player, timeago + " " + Color.WHITE + "* " + Color.DARK_AQUA + Phrase.build(Phrase.LOOKUP_PING, Color.DARK_AQUA + dplayer + Color.WHITE)); + Chat.sendComponent(player, Color.WHITE + leftPadding + Color.GREY + "^ " + ChatUtils.getCoordinates(command.getName(), wid, dataX, dataY, dataZ, true, true) + ""); + continue; + } + String tag = (action != SessionActions.LOGOUT ? Color.GREEN + "+" : Color.RED + "-"); Chat.sendComponent(player, timeago + " " + tag + " " + Color.DARK_AQUA + Phrase.build(Phrase.LOOKUP_LOGIN, Color.DARK_AQUA + dplayer + Color.WHITE, (action != SessionActions.LOGOUT ? Selector.FIRST : Selector.SECOND))); Chat.sendComponent(player, Color.WHITE + leftPadding + Color.GREY + "^ " + ChatUtils.getCoordinates(command.getName(), wid, dataX, dataY, dataZ, true, true) + ""); diff --git a/src/main/java/net/tfminecraft/coreprotect/command/parser/ActionParser.java b/src/main/java/net/tfminecraft/coreprotect/command/parser/ActionParser.java index 660c1f2..a8ccc3b 100644 --- a/src/main/java/net/tfminecraft/coreprotect/command/parser/ActionParser.java +++ b/src/main/java/net/tfminecraft/coreprotect/command/parser/ActionParser.java @@ -131,6 +131,12 @@ else if (argument.equals("logout") || argument.equals("logouts") || argument.equ } else if (argument.equals("session") || argument.equals("sessions") || argument.equals("connection") || argument.equals("connections")) { result.add(LookupActions.SESSION); + result.add(SessionActions.LOGIN); + result.add(SessionActions.LOGOUT); + } + else if (argument.equals("ping") || argument.equals("pings") || argument.equals("position") || argument.equals("positions")) { + result.add(LookupActions.SESSION); + result.add(SessionActions.PING); } else if (argument.equals("username") || argument.equals("usernames") || argument.equals("user") || argument.equals("users") || argument.equals("name") || argument.equals("names") || argument.equals("uuid") || argument.equals("uuids") || argument.equals("username-change") || argument.equals("username-changes") || argument.equals("name-change") || argument.equals("name-changes")) { result.add(LookupActions.USERNAME); diff --git a/src/main/java/net/tfminecraft/coreprotect/config/Config.java b/src/main/java/net/tfminecraft/coreprotect/config/Config.java index 86e5957..8e3dcda 100644 --- a/src/main/java/net/tfminecraft/coreprotect/config/Config.java +++ b/src/main/java/net/tfminecraft/coreprotect/config/Config.java @@ -111,6 +111,7 @@ public class Config extends Language { public int DEFAULT_RADIUS; public int DUCKDB_THREADS; public int MAX_RADIUS; + public int PLAYER_PINGS; static { DEFAULT_VALUES.put("donation-key", ""); @@ -172,6 +173,7 @@ public class Config extends Language { DEFAULT_VALUES.put("player-messages", "true"); DEFAULT_VALUES.put("player-commands", "true"); DEFAULT_VALUES.put("player-sessions", "true"); + DEFAULT_VALUES.put("player-pings", "60"); DEFAULT_VALUES.put("username-changes", "true"); DEFAULT_VALUES.put("worldedit", "true"); @@ -222,6 +224,7 @@ public class Config extends Language { HEADERS.put("player-messages", new String[] { "# Logs messages that players send in the chat." }); HEADERS.put("player-commands", new String[] { "# Logs all commands used by players." }); HEADERS.put("player-sessions", new String[] { "# Logs the logins and logouts of players." }); + HEADERS.put("player-pings", new String[] { "# Logs the position of each online player at this interval, in seconds.", "# Search them with \"/co lookup a:ping\". Set to \"0\" to disable." }); HEADERS.put("username-changes", new String[] { "# Logs when a player changes their Minecraft username." }); HEADERS.put("worldedit", new String[] { "# Logs changes made via the plugin \"WorldEdit\" if it's in use on your server." }); } @@ -301,6 +304,7 @@ private void readValues() { this.PLAYER_MESSAGES = this.getBoolean("player-messages"); this.PLAYER_COMMANDS = this.getBoolean("player-commands"); this.PLAYER_SESSIONS = this.getBoolean("player-sessions"); + this.PLAYER_PINGS = this.getInt("player-pings"); this.USERNAME_CHANGES = this.getBoolean("username-changes"); this.WORLDEDIT = this.getBoolean("worldedit"); } diff --git a/src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java b/src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java index 6ba99cf..a8df118 100755 --- a/src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java +++ b/src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java @@ -528,6 +528,10 @@ protected static void queuePlayerQuit(Player player, int time) { queueStandardData(new Object[] { null, Process.PLAYER_LOGOUT, null, 0, null, 0, time, null }, new String[] { player.getName(), null }, player.getLocation().clone(), false, Consumer.reserveConsumer()); } + protected static void queuePlayerPing(Player player, int time) { + queueStandardData(new Object[] { null, Process.PLAYER_PING, null, 0, null, 0, time, null }, new String[] { player.getName(), null }, player.getLocation().clone(), false, Consumer.reserveConsumer()); + } + protected static void queueRollbackUpdate(String user, Location location, List list, int table, int action) { if (location == null) { location = new Location(Bukkit.getServer().getWorlds().get(0), 0, 0, 0); diff --git a/src/main/java/net/tfminecraft/coreprotect/consumer/process/PlayerPingProcess.java b/src/main/java/net/tfminecraft/coreprotect/consumer/process/PlayerPingProcess.java new file mode 100644 index 0000000..dc7f0be --- /dev/null +++ b/src/main/java/net/tfminecraft/coreprotect/consumer/process/PlayerPingProcess.java @@ -0,0 +1,18 @@ +package net.tfminecraft.coreprotect.consumer.process; + +import net.tfminecraft.coreprotect.database.ConsumerWriteBatch; + +import org.bukkit.Location; + +import net.tfminecraft.coreprotect.database.logger.PlayerSessionLogger; +import net.tfminecraft.coreprotect.model.action.SessionActions; + +class PlayerPingProcess { + + static void process(ConsumerWriteBatch preparedStmt, int batchCount, Object object, int time, String user) { + if (object instanceof Location) { + Location location = (Location) object; + PlayerSessionLogger.log(preparedStmt, batchCount, user, location, time, SessionActions.PING); + } + } +} diff --git a/src/main/java/net/tfminecraft/coreprotect/consumer/process/Process.java b/src/main/java/net/tfminecraft/coreprotect/consumer/process/Process.java index a3cd58f..dc3e2d9 100755 --- a/src/main/java/net/tfminecraft/coreprotect/consumer/process/Process.java +++ b/src/main/java/net/tfminecraft/coreprotect/consumer/process/Process.java @@ -70,6 +70,7 @@ public class Process { public static final int ENTITY_CONTAINER_TRANSITION_UPDATE = 34; public static final int ENTITY_INTERACTION = 35; public static final int LOCK_CHANGE = 36; + public static final int PLAYER_PING = 37; public static int lastLockUpdate = 0; private static volatile int currentConsumerSize = 0; @@ -414,6 +415,9 @@ else if (ConfigHandler.databaseType.isColumnar()) { case Process.PLAYER_LOGOUT: PlayerLogoutProcess.process(writeBatch, i, object, forceData, user); break; + case Process.PLAYER_PING: + PlayerPingProcess.process(writeBatch, i, object, forceData, user); + break; case Process.ENTITY_KILL: EntityKillProcess.process(writeBatch, writeBatch, writeBatch, i, processId, id, object, user); break; diff --git a/src/main/java/net/tfminecraft/coreprotect/database/Lookup.java b/src/main/java/net/tfminecraft/coreprotect/database/Lookup.java index 953d95a..869ff22 100755 --- a/src/main/java/net/tfminecraft/coreprotect/database/Lookup.java +++ b/src/main/java/net/tfminecraft/coreprotect/database/Lookup.java @@ -235,6 +235,10 @@ private static LookupSummaryRow summaryRow(ResultSet results) throws Exception { } private static boolean hasSummaryActions(List actionList) { + // Session lookups reuse the block action ids for login, logout and ping + if (actionList.contains(LookupActions.SESSION)) { + return false; + } return actionList.isEmpty() || actionList.contains(LookupActions.BLOCK_BREAK) || actionList.contains(LookupActions.BLOCK_PLACE) || actionList.contains(LookupActions.CONTAINER) || actionList.contains(LookupActions.ITEM); } diff --git a/src/main/java/net/tfminecraft/coreprotect/database/LookupRaw.java b/src/main/java/net/tfminecraft/coreprotect/database/LookupRaw.java index f9c2aeb..297c571 100644 --- a/src/main/java/net/tfminecraft/coreprotect/database/LookupRaw.java +++ b/src/main/java/net/tfminecraft/coreprotect/database/LookupRaw.java @@ -29,6 +29,7 @@ import net.tfminecraft.coreprotect.listener.channel.PluginChannelHandshakeListener; import net.tfminecraft.coreprotect.model.action.EntityActionFilter; import net.tfminecraft.coreprotect.model.action.LookupActions; +import net.tfminecraft.coreprotect.model.action.SessionActions; import net.tfminecraft.coreprotect.model.action.SignActions; import net.tfminecraft.coreprotect.model.item.InventorySources; import net.tfminecraft.coreprotect.model.item.ItemTransactionActions; @@ -735,6 +736,12 @@ else if (actionList.contains(LookupActions.ITEM) && !actionList.contains(LookupA } } + // Pings share the session table, so a session lookup without a session action lists only logins and logouts + if (actionList.contains(LookupActions.SESSION) && !validAction) { + action = SessionActions.LOGIN + "," + SessionActions.LOGOUT; + validAction = true; + } + String bounds = ""; if (radius != null) { Integer xmin = radius[1]; diff --git a/src/main/java/net/tfminecraft/coreprotect/language/Language.java b/src/main/java/net/tfminecraft/coreprotect/language/Language.java index 62c1d43..ba6da3e 100644 --- a/src/main/java/net/tfminecraft/coreprotect/language/Language.java +++ b/src/main/java/net/tfminecraft/coreprotect/language/Language.java @@ -173,6 +173,7 @@ public static void loadPhrases() { phrases.put(Phrase.LOOKUP_ENTITY_SPAWN, "{0} spawned {1}."); phrases.put(Phrase.LOOKUP_ITEM, "{0} {picked up|dropped} {1} {2}."); phrases.put(Phrase.LOOKUP_LOGIN, "{0} logged {in|out}."); + phrases.put(Phrase.LOOKUP_PING, "{0} was online here."); phrases.put(Phrase.LOOKUP_PAGE, "Page {0}"); phrases.put(Phrase.LOOKUP_PROJECTILE, "{0} {threw|shot} {1} {2}."); phrases.put(Phrase.LOOKUP_ROWS_FOUND, "{0} {row|rows} found."); diff --git a/src/main/java/net/tfminecraft/coreprotect/language/Phrase.java b/src/main/java/net/tfminecraft/coreprotect/language/Phrase.java index 9c247cc..5505c55 100644 --- a/src/main/java/net/tfminecraft/coreprotect/language/Phrase.java +++ b/src/main/java/net/tfminecraft/coreprotect/language/Phrase.java @@ -140,6 +140,7 @@ public enum Phrase { LOOKUP_LOCK_CHANGE_STAFF, LOOKUP_ITEM, LOOKUP_LOGIN, + LOOKUP_PING, LOOKUP_PAGE, LOOKUP_PROJECTILE, LOOKUP_ROWS_FOUND, diff --git a/src/main/java/net/tfminecraft/coreprotect/listener/ListenerHandler.java b/src/main/java/net/tfminecraft/coreprotect/listener/ListenerHandler.java index e90d683..341b7b8 100644 --- a/src/main/java/net/tfminecraft/coreprotect/listener/ListenerHandler.java +++ b/src/main/java/net/tfminecraft/coreprotect/listener/ListenerHandler.java @@ -56,6 +56,7 @@ import net.tfminecraft.coreprotect.listener.player.PlayerInteractListener; import net.tfminecraft.coreprotect.listener.player.PlayerItemBreakListener; import net.tfminecraft.coreprotect.listener.player.PlayerJoinListener; +import net.tfminecraft.coreprotect.listener.player.PlayerPingListener; import net.tfminecraft.coreprotect.listener.player.PlayerPickupArrowListener; import net.tfminecraft.coreprotect.listener.player.PlayerQuitListener; import net.tfminecraft.coreprotect.listener.player.PlayerTakeLecternBookListener; @@ -197,6 +198,7 @@ public ListenerHandler(CoreProtect plugin) { pluginManager.registerEvents(new PlayerItemBreakListener(), plugin); pluginManager.registerEvents(new PlayerJoinListener(), plugin); pluginManager.registerEvents(new PlayerQuitListener(), plugin); + pluginManager.registerEvents(new PlayerPingListener(), plugin); pluginManager.registerEvents(new SignChangeListener(), plugin); pluginManager.registerEvents(new SpawnEggUseListener(), plugin); pluginManager.registerEvents(new PlayerTakeLecternBookListener(), plugin); diff --git a/src/main/java/net/tfminecraft/coreprotect/listener/player/PlayerPingListener.java b/src/main/java/net/tfminecraft/coreprotect/listener/player/PlayerPingListener.java new file mode 100644 index 0000000..eb02446 --- /dev/null +++ b/src/main/java/net/tfminecraft/coreprotect/listener/player/PlayerPingListener.java @@ -0,0 +1,68 @@ +package net.tfminecraft.coreprotect.listener.player; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +import net.tfminecraft.coreprotect.CoreProtect; +import net.tfminecraft.coreprotect.config.Config; +import net.tfminecraft.coreprotect.config.ConfigHandler; +import net.tfminecraft.coreprotect.consumer.Queue; +import net.tfminecraft.coreprotect.thread.Scheduler; + +/** + * Logs each online player's position as a session row every "player-pings" seconds. + * A player's first ping is one interval after they join, since the login row already holds their position. + */ +public final class PlayerPingListener extends Queue implements Listener { + + private static final int CHECK_INTERVAL_TICKS = 20; + private static final Map NEXT_PING = new ConcurrentHashMap<>(); + + public static void initialize(CoreProtect plugin) { + NEXT_PING.clear(); + Scheduler.scheduleSyncRepeatingTask(plugin, () -> checkPlayers(plugin), null, CHECK_INTERVAL_TICKS, CHECK_INTERVAL_TICKS); + } + + private static void checkPlayers(CoreProtect plugin) { + int time = (int) (System.currentTimeMillis() / 1000L); + for (Player player : plugin.getServer().getOnlinePlayers()) { + if (ConfigHandler.isFolia) { + // A player's location may only be read on the thread that owns them + Scheduler.runTask(plugin, () -> checkPlayer(player, time), player); + } + else { + checkPlayer(player, time); + } + } + } + + private static void checkPlayer(Player player, int time) { + if (!player.isOnline()) { + return; + } + + int interval = Config.getConfig(player.getWorld()).PLAYER_PINGS; + if (interval <= 0) { + NEXT_PING.remove(player.getUniqueId()); + return; + } + + Integer nextPing = NEXT_PING.putIfAbsent(player.getUniqueId(), time + interval); + if (nextPing != null && time >= nextPing) { + NEXT_PING.put(player.getUniqueId(), time + interval); + Queue.queuePlayerPing(player, time); + } + } + + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + NEXT_PING.remove(event.getPlayer().getUniqueId()); + } +} diff --git a/src/main/java/net/tfminecraft/coreprotect/model/action/SessionActions.java b/src/main/java/net/tfminecraft/coreprotect/model/action/SessionActions.java index 50b379d..1c3293d 100644 --- a/src/main/java/net/tfminecraft/coreprotect/model/action/SessionActions.java +++ b/src/main/java/net/tfminecraft/coreprotect/model/action/SessionActions.java @@ -3,6 +3,7 @@ public final class SessionActions { public static final int LOGOUT = 0; public static final int LOGIN = 1; + public static final int PING = 2; private SessionActions() { throw new IllegalStateException("Model class"); @@ -14,6 +15,8 @@ public static String getActionString(int action) { return "logout"; case LOGIN: return "login"; + case PING: + return "ping"; default: return "unknown"; } diff --git a/src/main/java/net/tfminecraft/coreprotect/services/PluginInitializationService.java b/src/main/java/net/tfminecraft/coreprotect/services/PluginInitializationService.java index 5e520e2..cb631d2 100644 --- a/src/main/java/net/tfminecraft/coreprotect/services/PluginInitializationService.java +++ b/src/main/java/net/tfminecraft/coreprotect/services/PluginInitializationService.java @@ -16,6 +16,7 @@ import net.tfminecraft.coreprotect.language.Language; import net.tfminecraft.coreprotect.language.Phrase; import net.tfminecraft.coreprotect.listener.ListenerHandler; +import net.tfminecraft.coreprotect.listener.player.PlayerPingListener; import net.tfminecraft.coreprotect.thread.CacheHandler; import net.tfminecraft.coreprotect.thread.NetworkHandler; import net.tfminecraft.coreprotect.thread.Scheduler; @@ -162,6 +163,9 @@ private static void startBackgroundServices(CoreProtect plugin) { // Start tick time monitor (only used where native tick timings are unavailable) TickTimeMonitor.initialize(plugin); + // Start player position pings + PlayerPingListener.initialize(plugin); + // Start cache cleanup thread Thread cacheCleanUpThread = new Thread(new CacheHandler()); cacheCleanUpThread.start(); From e97f44f64c5d2e25f39c78444f2e2b8945f7f0fb Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:20:56 +0000 Subject: [PATCH 2/2] fix: apply the current ping interval after a world change or reload Co-Authored-By: Claude Opus 5.5 (1M context) --- .../listener/player/PlayerPingListener.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/net/tfminecraft/coreprotect/listener/player/PlayerPingListener.java b/src/main/java/net/tfminecraft/coreprotect/listener/player/PlayerPingListener.java index eb02446..05418d3 100644 --- a/src/main/java/net/tfminecraft/coreprotect/listener/player/PlayerPingListener.java +++ b/src/main/java/net/tfminecraft/coreprotect/listener/player/PlayerPingListener.java @@ -23,10 +23,11 @@ public final class PlayerPingListener extends Queue implements Listener { private static final int CHECK_INTERVAL_TICKS = 20; - private static final Map NEXT_PING = new ConcurrentHashMap<>(); + // Time of each player's last ping, or of when the task first saw them + private static final Map LAST_PING = new ConcurrentHashMap<>(); public static void initialize(CoreProtect plugin) { - NEXT_PING.clear(); + LAST_PING.clear(); Scheduler.scheduleSyncRepeatingTask(plugin, () -> checkPlayers(plugin), null, CHECK_INTERVAL_TICKS, CHECK_INTERVAL_TICKS); } @@ -50,19 +51,20 @@ private static void checkPlayer(Player player, int time) { int interval = Config.getConfig(player.getWorld()).PLAYER_PINGS; if (interval <= 0) { - NEXT_PING.remove(player.getUniqueId()); + LAST_PING.remove(player.getUniqueId()); return; } - Integer nextPing = NEXT_PING.putIfAbsent(player.getUniqueId(), time + interval); - if (nextPing != null && time >= nextPing) { - NEXT_PING.put(player.getUniqueId(), time + interval); + // Compare against the current interval, so a world change or reload applies at once + Integer lastPing = LAST_PING.putIfAbsent(player.getUniqueId(), time); + if (lastPing != null && time - lastPing >= interval) { + LAST_PING.put(player.getUniqueId(), time); Queue.queuePlayerPing(player, time); } } @EventHandler(priority = EventPriority.MONITOR) public void onPlayerQuit(PlayerQuitEvent event) { - NEXT_PING.remove(event.getPlayer().getUniqueId()); + LAST_PING.remove(event.getPlayer().getUniqueId()); } }