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..05418d3 --- /dev/null +++ b/src/main/java/net/tfminecraft/coreprotect/listener/player/PlayerPingListener.java @@ -0,0 +1,70 @@ +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; + // 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) { + LAST_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) { + LAST_PING.remove(player.getUniqueId()); + return; + } + + // 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) { + LAST_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();