Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions lang/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -119,6 +120,7 @@ public static List<SessionResult> 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);

Expand Down Expand Up @@ -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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> materials = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) + "");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/net/tfminecraft/coreprotect/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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", "");
Expand Down Expand Up @@ -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");

Expand Down Expand Up @@ -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." });
}
Expand Down Expand Up @@ -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");
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object[]> list, int table, int action) {
if (location == null) {
location = new Location(Bukkit.getServer().getWorlds().get(0), 0, 0, 0);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ private static LookupSummaryRow summaryRow(ResultSet results) throws Exception {
}

private static boolean hasSummaryActions(List<Integer> 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ public enum Phrase {
LOOKUP_LOCK_CHANGE_STAFF,
LOOKUP_ITEM,
LOOKUP_LOGIN,
LOOKUP_PING,
LOOKUP_PAGE,
LOOKUP_PROJECTILE,
LOOKUP_ROWS_FOUND,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UUID, Integer> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -14,6 +15,8 @@ public static String getActionString(int action) {
return "logout";
case LOGIN:
return "login";
case PING:
return "ping";
default:
return "unknown";
}
Expand Down
Loading