diff --git a/lang/en.yml b/lang/en.yml index 7df36b33..b4287ba1 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -125,6 +125,8 @@ LOOKUP_ENTITY_ORIGIN: "Spawn location" LOOKUP_ENTITY_SPAWN: "{0} spawned {1}." LOOKUP_HEADER: "{0} Lookup Results" LOOKUP_INTERACTION: "{0} {clicked|killed} {1}." +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_PAGE: "Page {0}" diff --git a/src/main/java/net/tfminecraft/coreprotect/CoreProtectAPI.java b/src/main/java/net/tfminecraft/coreprotect/CoreProtectAPI.java index 353f1b4f..1da43942 100755 --- a/src/main/java/net/tfminecraft/coreprotect/CoreProtectAPI.java +++ b/src/main/java/net/tfminecraft/coreprotect/CoreProtectAPI.java @@ -44,6 +44,7 @@ import net.tfminecraft.coreprotect.database.rollback.Rollback; import net.tfminecraft.coreprotect.language.Phrase; import net.tfminecraft.coreprotect.listener.player.InventoryChangeListener; +import net.tfminecraft.coreprotect.model.action.LockChange; import net.tfminecraft.coreprotect.model.action.LookupActions; import net.tfminecraft.coreprotect.utility.Chat; import net.tfminecraft.coreprotect.utility.MaterialUtils; @@ -60,7 +61,7 @@ public class CoreProtectAPI extends Queue { /** * Current version of the API */ - private static final int API_VERSION = 13; + private static final int API_VERSION = 14; private static final AtomicLong API_ROLLBACK_SEQUENCE = new AtomicLong(); public static class ParseResult extends net.tfminecraft.coreprotect.api.result.ParseResult { @@ -508,6 +509,28 @@ public boolean logInteraction(String user, Location location) { return true; } + /** + * Logs a lock state change by a user at a location. Lookups show it as + * "user set block lock to state", and it is never rolled back. + * + * @param user + * The username + * @param location + * The location of the locked block + * @param lockState + * The new lock state, as it should be shown to staff (for example "Private") + * @param staffOverride + * True if staff changed a lock owned by another player + * @return True if the change was queued for logging + */ + public boolean logLockChange(String user, Location location, String lockState, boolean staffOverride) { + if (!isEnabled() || !isValidUserAndLocation(user, location) || lockState == null || lockState.isEmpty()) { + return false; + } + + return Queue.queueLockChange(user, location.getBlock().getState(), location.getBlock().getType(), new LockChange(lockState, staffOverride)); + } + /** * Logs a container transaction by a user at a location. * 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 380b097d..d9f5e811 100644 --- a/src/main/java/net/tfminecraft/coreprotect/command/lookup/StandardLookupThread.java +++ b/src/main/java/net/tfminecraft/coreprotect/command/lookup/StandardLookupThread.java @@ -35,6 +35,7 @@ import net.tfminecraft.coreprotect.listener.channel.PluginChannelHandshakeListener; import net.tfminecraft.coreprotect.listener.channel.PluginChannelListener; import net.tfminecraft.coreprotect.model.action.EntityActionFilter; +import net.tfminecraft.coreprotect.model.action.LockChange; import net.tfminecraft.coreprotect.model.action.LookupActions; import net.tfminecraft.coreprotect.model.action.SessionActions; import net.tfminecraft.coreprotect.model.entity.EntitySpawnRecord; @@ -562,6 +563,7 @@ else if ((daction == LookupActions.ENTITY_KILL || daction == LookupActions.ENTIT Phrase phrase = Phrase.LOOKUP_BLOCK; String selector = Selector.FIRST; String action = "a:block"; + LockChange lockChange = null; if (actions.contains(LookupActions.CONTAINER) || actions.contains(5) || actions.contains(LookupActions.ITEM) || amount > -1) { byte[] metadata = data[11] == null ? null : data[11].getBytes(StandardCharsets.ISO_8859_1); String tooltip = ItemUtils.getEnchantments(metadata, dtype, amount); @@ -615,6 +617,7 @@ else if (daction == LookupActions.INTERACTION || daction == LookupActions.ENTITY action = "a:block"; } else { + lockChange = daction == LookupActions.INTERACTION ? LockChange.fromLookupString(data[11]) : null; phrase = Phrase.LOOKUP_INTERACTION; // {clicked|killed} selector = (daction != LookupActions.ENTITY_KILL ? Selector.FIRST : Selector.SECOND); tag = (daction != LookupActions.ENTITY_KILL ? Color.WHITE + "-" : Color.RED + "-"); @@ -627,7 +630,14 @@ else if (daction == LookupActions.INTERACTION || daction == LookupActions.ENTITY tag = (daction != LookupActions.BLOCK_BREAK ? Color.GREEN + "+" : Color.RED + "-"); } - Chat.sendComponent(player, timeago + " " + tag + " " + Phrase.build(phrase, Color.DARK_AQUA + rbd + dplayer + Color.WHITE + rbd, Color.DARK_AQUA + rbd + dname + Color.WHITE, selector)); + String message; + if (lockChange != null) { + message = Phrase.build(lockChange.isStaffOverride() ? Phrase.LOOKUP_LOCK_CHANGE_STAFF : Phrase.LOOKUP_LOCK_CHANGE, Color.DARK_AQUA + rbd + dplayer + Color.WHITE + rbd, Color.DARK_AQUA + rbd + dname + Color.WHITE + rbd, Color.DARK_AQUA + rbd + lockChange.getLockState() + Color.WHITE); + } + else { + message = Phrase.build(phrase, Color.DARK_AQUA + rbd + dplayer + Color.WHITE + rbd, Color.DARK_AQUA + rbd + dname + Color.WHITE, selector); + } + Chat.sendComponent(player, timeago + " " + tag + " " + message); PluginChannelListener.getInstance().sendData(player, Integer.parseInt(time), phrase, selector, dplayer, dname, (tag.contains("+") ? 1 : -1), dataX, dataY, dataZ, wid, rbd, false, tag.contains("+")); } diff --git a/src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java b/src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java index 06f27192..6ba99cfb 100755 --- a/src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java +++ b/src/main/java/net/tfminecraft/coreprotect/consumer/Queue.java @@ -29,6 +29,7 @@ import net.tfminecraft.coreprotect.consumer.process.Process; import net.tfminecraft.coreprotect.listener.block.BlockUtil; import net.tfminecraft.coreprotect.model.BlockGroup; +import net.tfminecraft.coreprotect.model.action.LockChange; import net.tfminecraft.coreprotect.model.entity.EntityContainerRollbackUpdate; import net.tfminecraft.coreprotect.model.entity.EntityContainerTransaction; import net.tfminecraft.coreprotect.model.entity.EntityInteraction; @@ -508,6 +509,10 @@ protected static void queuePlayerInteraction(String user, BlockState block, Mate queueStandardData(new Object[] { null, Process.PLAYER_INTERACTION, type, 0, null, 0, 0, null }, new String[] { user, null }, block, false, Consumer.reserveConsumer()); } + protected static boolean queueLockChange(String user, BlockState block, Material type, LockChange lockChange) { + return queueStandardData(new Object[] { null, Process.LOCK_CHANGE, type, 0, null, 0, 0, null }, new String[] { user, null }, new Object[] { block, lockChange }, false, Consumer.reserveConsumer()); + } + protected static void queuePlayerKill(String user, Location location, String player) { queueStandardData(new Object[] { null, Process.PLAYER_KILL, null, 0, null, 0, 0, null }, new String[] { user, null }, new Object[] { getBlockLocation(location), player }, false, Consumer.reserveConsumer()); } diff --git a/src/main/java/net/tfminecraft/coreprotect/consumer/process/LockChangeProcess.java b/src/main/java/net/tfminecraft/coreprotect/consumer/process/LockChangeProcess.java new file mode 100644 index 00000000..c56af6f0 --- /dev/null +++ b/src/main/java/net/tfminecraft/coreprotect/consumer/process/LockChangeProcess.java @@ -0,0 +1,24 @@ +package net.tfminecraft.coreprotect.consumer.process; + +import org.bukkit.Material; +import org.bukkit.block.BlockState; + +import net.tfminecraft.coreprotect.database.ConsumerWriteBatch; +import net.tfminecraft.coreprotect.database.logger.PlayerInteractLogger; +import net.tfminecraft.coreprotect.model.action.LockChange; + +class LockChangeProcess { + + private LockChangeProcess() { + throw new IllegalStateException("Process class"); + } + + static void process(ConsumerWriteBatch preparedStmt, int batchCount, String user, Object object, Material type) { + if (object instanceof Object[]) { + Object[] values = (Object[]) object; + if (values.length > 1 && values[0] instanceof BlockState && values[1] instanceof LockChange) { + PlayerInteractLogger.log(preparedStmt, batchCount, user, (BlockState) values[0], type, (LockChange) values[1]); + } + } + } +} 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 23577f55..a3cd58f4 100755 --- a/src/main/java/net/tfminecraft/coreprotect/consumer/process/Process.java +++ b/src/main/java/net/tfminecraft/coreprotect/consumer/process/Process.java @@ -69,6 +69,7 @@ public class Process { public static final int ENTITY_CONTAINER_ROLLBACK_UPDATE = 33; 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 int lastLockUpdate = 0; private static volatile int currentConsumerSize = 0; @@ -294,6 +295,9 @@ else if (action == Process.ENTITY_SPAWN_UPDATE || action == Process.ENTITY_CONTA case Process.PLAYER_INTERACTION: PlayerInteractionProcess.process(writeBatch, i, user, object, blockType); break; + case Process.LOCK_CHANGE: + LockChangeProcess.process(writeBatch, i, user, object, blockType); + break; case Process.CONTAINER_TRANSACTION: ContainerTransactionProcess.process(writeBatch, writeBatch, i, processId, id, blockType, forceData, user, object); break; diff --git a/src/main/java/net/tfminecraft/coreprotect/database/logger/PlayerInteractLogger.java b/src/main/java/net/tfminecraft/coreprotect/database/logger/PlayerInteractLogger.java index 988a0ed9..a7a19c1d 100644 --- a/src/main/java/net/tfminecraft/coreprotect/database/logger/PlayerInteractLogger.java +++ b/src/main/java/net/tfminecraft/coreprotect/database/logger/PlayerInteractLogger.java @@ -1,5 +1,7 @@ package net.tfminecraft.coreprotect.database.logger; +import java.util.List; + import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Location; @@ -13,6 +15,7 @@ import net.tfminecraft.coreprotect.database.statement.BlockStatement; import net.tfminecraft.coreprotect.database.statement.UserStatement; import net.tfminecraft.coreprotect.event.CoreProtectPreLogEvent; +import net.tfminecraft.coreprotect.model.action.LockChange; import net.tfminecraft.coreprotect.model.action.LookupActions; import net.tfminecraft.coreprotect.utility.BlockTypeUtils; import net.tfminecraft.coreprotect.utility.MaterialUtils; @@ -25,6 +28,10 @@ private PlayerInteractLogger() { } public static void log(ConsumerWriteBatch preparedStmt, int batchCount, String user, BlockState block, Material blockType) { + log(preparedStmt, batchCount, user, block, blockType, null); + } + + public static void log(ConsumerWriteBatch preparedStmt, int batchCount, String user, BlockState block, Material blockType, LockChange lockChange) { try { String blockData = block.getBlockData().getAsString(); String blockKey = BlockTypeUtils.getBlockDataKey(blockData); @@ -64,7 +71,8 @@ public static void log(ConsumerWriteBatch preparedStmt, int batchCount, String u int y = eventLocation.getBlockY(); int z = eventLocation.getBlockZ(); int data = 0; - BlockStatement.insert(preparedStmt, batchCount, time, userId, wid, x, y, z, type, data, null, blockData, LookupActions.INTERACTION, 0); + List meta = lockChange == null ? null : lockChange.toMetadata(); + BlockStatement.insert(preparedStmt, batchCount, time, userId, wid, x, y, z, type, data, meta, blockData, LookupActions.INTERACTION, 0); } catch (Exception e) { Database.handleWriteFailure(e); diff --git a/src/main/java/net/tfminecraft/coreprotect/database/lookup/BlockLookup.java b/src/main/java/net/tfminecraft/coreprotect/database/lookup/BlockLookup.java index 913213f1..9f0696b0 100644 --- a/src/main/java/net/tfminecraft/coreprotect/database/lookup/BlockLookup.java +++ b/src/main/java/net/tfminecraft/coreprotect/database/lookup/BlockLookup.java @@ -6,6 +6,7 @@ import net.tfminecraft.coreprotect.language.Phrase; import net.tfminecraft.coreprotect.language.Selector; import net.tfminecraft.coreprotect.listener.channel.PluginChannelListener; +import net.tfminecraft.coreprotect.model.action.LockChange; import net.tfminecraft.coreprotect.model.action.LookupActions; import net.tfminecraft.coreprotect.utility.*; import net.tfminecraft.coreprotect.utility.ErrorReporter; @@ -58,7 +59,8 @@ else if (commandSender.hasPermission("coreprotect.coreprotect")) { } String blockName = block.getType().name().toLowerCase(Locale.ROOT); - String actionPredicate = "(action IN(0,1," + LookupActions.ENTITY_SPAWN + ") OR (action=" + LookupActions.ENTITY_KILL + " AND type IN(" + placedEntityTypeIds() + ")))"; + // Plain clicks have no metadata; interaction rows with metadata are lock changes, which belong in block history. + String actionPredicate = "(action IN(0,1," + LookupActions.ENTITY_SPAWN + ") OR (action=" + LookupActions.ENTITY_KILL + " AND type IN(" + placedEntityTypeIds() + ")) OR (action=" + LookupActions.INTERACTION + " AND meta IS NOT NULL))"; String where = "wid = " + worldId + " AND x = " + x + " AND z = " + z + " AND y = " + y + " AND " + actionPredicate + " AND time >= " + checkTime; boolean combinedDuckDBPage = ConfigHandler.databaseType.isDuckDB(); @@ -66,7 +68,7 @@ else if (commandSender.hasPermission("coreprotect.coreprotect")) { ResultSet results; if (combinedDuckDBPage) { String sourceTable = DuckDBLookupQuery.spatialTable(statement.getConnection(), "block", worldId, x, x, z, z, "spatial_rows"); - String columns = "data_rows.time,data_rows." + ConfigHandler.databaseType.getUserColumn() + ",data_rows.action,data_rows.type,data_rows.data,data_rows.rolled_back"; + String columns = "data_rows.time,data_rows." + ConfigHandler.databaseType.getUserColumn() + ",data_rows.action,data_rows.type,data_rows.data,data_rows.meta,data_rows.rolled_back"; query = DuckDBLookupQuery.pageQuery(sourceTable, ConfigHandler.prefix + "block", where, columns, false, limit, page_start); results = statement.executeQuery(query); } @@ -77,7 +79,7 @@ else if (commandSender.hasPermission("coreprotect.coreprotect")) { count = results.getInt("count"); } results.close(); - query = "SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",action,type,data,rolled_back FROM " + ConfigHandler.prefix + "block " + WorldUtils.getWidIndex("block") + "WHERE " + where + " ORDER BY " + ConfigHandler.getDescendingEventOrder() + " LIMIT " + limit + " OFFSET " + page_start; + query = "SELECT time," + ConfigHandler.databaseType.getUserColumn() + ",action,type,data,meta,rolled_back FROM " + ConfigHandler.prefix + "block " + WorldUtils.getWidIndex("block") + "WHERE " + where + " ORDER BY " + ConfigHandler.getDescendingEventOrder() + " LIMIT " + limit + " OFFSET " + page_start; results = statement.executeQuery(query); } @@ -96,6 +98,7 @@ else if (commandSender.hasPermission("coreprotect.coreprotect")) { int resultData = results.getInt("data"); long resultTime = results.getLong("time"); int resultRolledBack = results.getInt("rolled_back"); + LockChange lockChange = resultAction == LookupActions.INTERACTION ? LockChange.fromMetadata(DatabaseUtils.getBytes(results, "meta")) : null; String resultUser = UserStatement.getName(statement.getConnection(), resultUserId); String timeAgo = ChatUtils.getTimeSince(resultTime, time, true); @@ -153,7 +156,14 @@ else if (resultAction == 2 || resultAction == 3) { target = target.split(":")[1]; } - resultTextBuilder.append(timeAgo + " " + tag + " ").append(Phrase.build(phrase, Color.DARK_AQUA + rbFormat + resultUser + Color.WHITE + rbFormat, Color.DARK_AQUA + rbFormat + target + Color.WHITE, selector)).append("\n"); + String message; + if (lockChange != null) { + message = Phrase.build(lockChange.isStaffOverride() ? Phrase.LOOKUP_LOCK_CHANGE_STAFF : Phrase.LOOKUP_LOCK_CHANGE, Color.DARK_AQUA + rbFormat + resultUser + Color.WHITE + rbFormat, Color.DARK_AQUA + rbFormat + target + Color.WHITE + rbFormat, Color.DARK_AQUA + rbFormat + lockChange.getLockState() + Color.WHITE); + } + else { + message = Phrase.build(phrase, Color.DARK_AQUA + rbFormat + resultUser + Color.WHITE + rbFormat, Color.DARK_AQUA + rbFormat + target + Color.WHITE, selector); + } + resultTextBuilder.append(timeAgo + " " + tag + " ").append(message).append("\n"); PluginChannelListener.getInstance().sendData(commandSender, resultTime, phrase, selector, resultUser, target, -1, x, y, z, worldId, rbFormat, false, tag.contains("+")); } diff --git a/src/main/java/net/tfminecraft/coreprotect/language/Language.java b/src/main/java/net/tfminecraft/coreprotect/language/Language.java index 1895c900..62c1d434 100644 --- a/src/main/java/net/tfminecraft/coreprotect/language/Language.java +++ b/src/main/java/net/tfminecraft/coreprotect/language/Language.java @@ -165,6 +165,8 @@ public static void loadPhrases() { phrases.put(Phrase.LOOKUP_CONTAINER, "{0} {added|removed} {1} {2}."); phrases.put(Phrase.LOOKUP_HEADER, "{0} Lookup Results"); phrases.put(Phrase.LOOKUP_INTERACTION, "{0} {clicked|killed} {1}."); + phrases.put(Phrase.LOOKUP_LOCK_CHANGE, "{0} set {1} lock to {2}."); + phrases.put(Phrase.LOOKUP_LOCK_CHANGE_STAFF, "{0} set {1} lock to {2} (staff override)."); phrases.put(Phrase.LOOKUP_ENTITY_INTERACTION, "{0} {clicked|sheared|leashed|unleashed} {1}."); phrases.put(Phrase.LOOKUP_ENTITY_INTERACTION_ORIGIN, "Origin location"); phrases.put(Phrase.LOOKUP_ENTITY_ORIGIN, "Spawn location"); diff --git a/src/main/java/net/tfminecraft/coreprotect/language/Phrase.java b/src/main/java/net/tfminecraft/coreprotect/language/Phrase.java index 1da8bc24..9c247cc3 100644 --- a/src/main/java/net/tfminecraft/coreprotect/language/Phrase.java +++ b/src/main/java/net/tfminecraft/coreprotect/language/Phrase.java @@ -136,6 +136,8 @@ public enum Phrase { LOOKUP_CONTAINER, LOOKUP_HEADER, LOOKUP_INTERACTION, + LOOKUP_LOCK_CHANGE, + LOOKUP_LOCK_CHANGE_STAFF, LOOKUP_ITEM, LOOKUP_LOGIN, LOOKUP_PAGE, diff --git a/src/main/java/net/tfminecraft/coreprotect/model/action/LockChange.java b/src/main/java/net/tfminecraft/coreprotect/model/action/LockChange.java new file mode 100644 index 00000000..50e7d31c --- /dev/null +++ b/src/main/java/net/tfminecraft/coreprotect/model/action/LockChange.java @@ -0,0 +1,53 @@ +package net.tfminecraft.coreprotect.model.action; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import net.tfminecraft.coreprotect.database.statement.BlockStatement; + +/** + * A lock state change logged by another plugin. It is stored as an interaction row whose block + * metadata starts with {@link #MARKER}, so rollback, purge and user lookups treat it like any + * other click. + */ +public final class LockChange { + + public static final String MARKER = "coreprotect:lock"; + + private final String lockState; + private final boolean staffOverride; + + public LockChange(String lockState, boolean staffOverride) { + this.lockState = lockState; + this.staffOverride = staffOverride; + } + + public String getLockState() { + return lockState; + } + + public boolean isStaffOverride() { + return staffOverride; + } + + public List toMetadata() { + return Arrays.asList(MARKER, lockState, staffOverride); + } + + public static LockChange fromMetadata(List metadata) { + if (metadata == null || metadata.size() < 3 || !MARKER.equals(metadata.get(0)) || !(metadata.get(1) instanceof String)) { + return null; + } + return new LockChange((String) metadata.get(1), Boolean.TRUE.equals(metadata.get(2))); + } + + public static LockChange fromMetadata(byte[] metadata) { + return metadata == null ? null : fromMetadata(BlockStatement.deserializeMetadata(metadata)); + } + + // Lookup rows carry metadata as ISO-8859-1 strings; see LookupConverter. + public static LockChange fromLookupString(String metadata) { + return metadata == null || metadata.isEmpty() ? null : fromMetadata(metadata.getBytes(StandardCharsets.ISO_8859_1)); + } +}