From 9f6613f652b1107364f6202a74ff5c95d70187cf Mon Sep 17 00:00:00 2001 From: TonyJamesStark Date: Mon, 22 Dec 2025 06:42:31 -0800 Subject: [PATCH 1/6] claude init --- CLAUDE.md | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1512bfdcc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,124 @@ +# CLAUDE.md - CoreProtect + +## Project Overview + +CoreProtect is a Minecraft server plugin (Bukkit/Paper) that provides data logging and anti-griefing protection. It tracks block changes, player actions, and server events to help prevent and investigate griefing. + +- **Version**: 23.1 +- **Java**: 11+ (compile target: 11) +- **Minecraft**: 1.14 - 1.21 +- **Build System**: Maven +- **Main Class**: `net.coreprotect.CoreProtect` + +## Build Commands + +```bash +# Build the plugin JAR +mvn clean package + +# Build with tests enabled (tests are skipped by default) +mvn clean package -DskipTests=false + +# Run tests only +mvn test -DskipTests=false + +# Full verification build (used in CI) +mvn -B verify +``` + +**Output**: `target/CoreProtect-23.1.jar` + +## Project Structure + +``` +src/main/java/net/coreprotect/ +├── CoreProtect.java # Plugin entry point +├── CoreProtectAPI.java # Public API (v11) +├── api/ # API classes +├── bukkit/ # Bukkit compatibility +├── command/ # Command handlers (/co, /core, /coreprotect) +│ ├── lookup/ # Lookup command threads +│ └── parser/ # Argument parsers +├── config/ # Configuration handling +├── consumer/ # Async queue processing for logging +├── database/ # Database layer (SQLite/MySQL) +│ ├── logger/ # Event loggers +│ ├── lookup/ # Query implementations +│ ├── rollback/ # Rollback/restore logic +│ └── statement/ # SQL builders +├── listener/ # Bukkit event listeners (60+) +│ ├── block/ # Block events +│ ├── entity/ # Entity events +│ ├── player/ # Player events +│ │ └── inspector/ # Block inspection tool +│ └── world/ # World events +├── language/ # i18n (15 languages) +├── paper/ # Paper-specific adapters +├── patch/ # DB schema migrations +├── spigot/ # Spigot compatibility +└── utility/ # Utility classes +``` + +## Key Entry Points + +- **Plugin Main**: `src/main/java/net/coreprotect/CoreProtect.java` +- **Public API**: `src/main/java/net/coreprotect/CoreProtectAPI.java` +- **Plugin Manifest**: `src/main/resources/plugin.yml` +- **Configuration**: `src/main/java/net/coreprotect/config/Config.java` +- **Database Core**: `src/main/java/net/coreprotect/database/Database.java` + +## Code Style Guidelines + +- **Indentation**: Spaces only (no tabs) +- **Naming**: `descriptiveCamelCase` - avoid underscores and abbreviations +- **Commits**: Single event or section of code per commit +- **PRs**: Keep modifications small and readable +- **Analysis**: Use SonarLint for code quality + +## Testing + +- **Framework**: JUnit 5 with Mockito +- **Mock Server**: MockBukkit for Bukkit server simulation +- **Database**: SQLite JDBC for test database +- **Location**: `src/test/java/net/coreprotect/` + +Tests are skipped by default. Enable with `-DskipTests=false`. + +## Dependencies + +**Runtime** (shaded into JAR): +- HikariCP 5.0.1 - Database connection pooling +- bStats 3.0.2 - Anonymous usage statistics + +**Provided** (by server): +- Paper API 1.21.11 +- FastAsyncWorldEdit 2.13.1 (optional) +- AdvancedChests API (optional) + +## Commands + +The plugin registers three command aliases: +- `/co` - Primary command (default enabled) +- `/core` - Alternative +- `/coreprotect` - Full name + +## Database Support + +- SQLite (default, file-based) +- MySQL (via HikariCP connection pool) + +Schema migrations handled in `database/patch/` directory. + +## Important Patterns + +1. **Consumer Queue**: All logging is async via consumer queue (`consumer/` package) +2. **Database Abstraction**: SQL statements built via `database/statement/` classes +3. **Event Listeners**: Extensive listener coverage in `listener/` (60+ listeners) +4. **Rollback System**: Block-by-block restoration in `database/rollback/` +5. **Inspector Tool**: Click-based block history in `listener/player/inspector/` + +## Integrations + +- FastAsyncWorldEdit - Block logging integration +- AdvancedChests - Custom chest support +- WorldEdit - Edit session logging From 4e2512f09475c7b8bc7394fa185ebbbad26649f2 Mon Sep 17 00:00:00 2001 From: TonyJamesStark Date: Mon, 22 Dec 2025 14:54:17 +0000 Subject: [PATCH 2/6] Add GZIP compression and indexing for entity logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GZIP compression to entity data storage (60-80% size reduction) - Backwards compatible: detects legacy uncompressed data via magic bytes - Add entity_type column for fast entity-type filtering - Add time and entity_type indexes for improved query performance - Include database migration for existing databases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../net/coreprotect/database/Database.java | 57 ++++++++++++++++++- .../database/logger/EntityKillLogger.java | 2 +- .../database/statement/EntityStatement.java | 28 +++++++-- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/main/java/net/coreprotect/database/Database.java b/src/main/java/net/coreprotect/database/Database.java index bc5530853..8ef423a47 100755 --- a/src/main/java/net/coreprotect/database/Database.java +++ b/src/main/java/net/coreprotect/database/Database.java @@ -59,7 +59,7 @@ public class Database extends Queue { SQL_QUERIES.put(CHAT, "INSERT INTO %sprefix%chat (time, user, wid, x, y, z, message) VALUES (?, ?, ?, ?, ?, ?, ?)"); SQL_QUERIES.put(COMMAND, "INSERT INTO %sprefix%command (time, user, wid, x, y, z, message) VALUES (?, ?, ?, ?, ?, ?, ?)"); SQL_QUERIES.put(SESSION, "INSERT INTO %sprefix%session (time, user, wid, x, y, z, action) VALUES (?, ?, ?, ?, ?, ?, ?)"); - SQL_QUERIES.put(ENTITY, "INSERT INTO %sprefix%entity (time, data) VALUES (?, ?)"); + SQL_QUERIES.put(ENTITY, "INSERT INTO %sprefix%entity (time, data, entity_type) VALUES (?, ?, ?)"); SQL_QUERIES.put(MATERIAL, "INSERT INTO %sprefix%material_map (id, material) VALUES (?, ?)"); SQL_QUERIES.put(ART, "INSERT INTO %sprefix%art_map (id, art) VALUES (?, ?)"); SQL_QUERIES.put(ENTITY_MAP, "INSERT INTO %sprefix%entity_map (id, entity) VALUES (?, ?)"); @@ -327,6 +327,7 @@ private static void createMySQLTables(String prefix, Connection forceConnection, Statement statement = connection.createStatement(); createMySQLTableStructures(prefix, statement); if (!purge && forceConnection == null) { + migrateEntityTable(connection, prefix, true); initializeTables(prefix, statement); } statement.close(); @@ -372,7 +373,8 @@ private static void createMySQLTableStructures(String prefix, Statement statemen statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "database_lock(rowid int NOT NULL AUTO_INCREMENT,PRIMARY KEY(rowid),status tinyint,time int) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4"); // Entity - statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "entity(rowid int NOT NULL AUTO_INCREMENT,PRIMARY KEY(rowid), time int, data blob) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4"); + index = ", INDEX(time), INDEX(entity_type,time)"; + statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "entity(rowid int NOT NULL AUTO_INCREMENT,PRIMARY KEY(rowid), time int, data blob, entity_type int DEFAULT 0" + index + ") ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4"); // Entity map index = ", INDEX(id)"; @@ -433,6 +435,7 @@ private static void createSQLiteTables(String prefix, boolean forcePrefix, Conne createSQLiteIndexes(forcePrefix == true ? prefix : ConfigHandler.prefix, statement, indexData, attachDatabase, purge); if (!purge && forceConnection == null) { + migrateEntityTable(connection, prefix, false); initializeTables(prefix, statement); } statement.close(); @@ -480,7 +483,7 @@ private static void createSQLiteTableStructures(String prefix, Statement stateme statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "database_lock (status INTEGER, time INTEGER);"); } if (!tableData.contains(prefix + "entity")) { - statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "entity (id INTEGER PRIMARY KEY ASC, time INTEGER, data BLOB);"); + statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "entity (id INTEGER PRIMARY KEY ASC, time INTEGER, data BLOB, entity_type INTEGER DEFAULT 0);"); } if (!tableData.contains(prefix + "entity_map")) { statement.executeUpdate("CREATE TABLE IF NOT EXISTS " + prefix + "entity_map (id INTEGER, entity TEXT);"); @@ -533,6 +536,8 @@ private static void createSQLiteIndexes(String prefix, Statement statement, List createSQLiteIndex(statement, indexData, attachDatabase, "item_index", prefix + "item(wid,x,z,time)"); createSQLiteIndex(statement, indexData, attachDatabase, "item_user_index", prefix + "item(user,time)"); createSQLiteIndex(statement, indexData, attachDatabase, "item_type_index", prefix + "item(type,time)"); + createSQLiteIndex(statement, indexData, attachDatabase, "entity_time_index", prefix + "entity(time)"); + createSQLiteIndex(statement, indexData, attachDatabase, "entity_type_index", prefix + "entity(entity_type,time)"); createSQLiteIndex(statement, indexData, attachDatabase, "entity_map_id_index", prefix + "entity_map(id)"); createSQLiteIndex(statement, indexData, attachDatabase, "material_map_id_index", prefix + "material_map(id)"); createSQLiteIndex(statement, indexData, attachDatabase, "session_index", prefix + "session(wid,x,z,time)"); @@ -561,4 +566,50 @@ private static void createSQLiteIndex(Statement statement, List indexDat } } + /** + * Migrates the entity table to add the entity_type column if it doesn't exist. + * This is needed for databases created before this feature was added. + */ + public static void migrateEntityTable(Connection connection, String prefix, boolean isMySQL) { + try { + Statement statement = connection.createStatement(); + boolean hasEntityTypeColumn = false; + + // Check if entity_type column exists + if (isMySQL) { + ResultSet rs = statement.executeQuery("SHOW COLUMNS FROM " + prefix + "entity LIKE 'entity_type'"); + hasEntityTypeColumn = rs.next(); + rs.close(); + } + else { + ResultSet rs = statement.executeQuery("PRAGMA table_info(" + prefix + "entity)"); + while (rs.next()) { + if ("entity_type".equals(rs.getString("name"))) { + hasEntityTypeColumn = true; + break; + } + } + rs.close(); + } + + // Add entity_type column if it doesn't exist + if (!hasEntityTypeColumn) { + Chat.console(Phrase.build(Phrase.DATABASE_INDEX_ERROR)); // Reusing existing phrase for migration message + if (isMySQL) { + statement.executeUpdate("ALTER TABLE " + prefix + "entity ADD COLUMN entity_type int DEFAULT 0"); + statement.executeUpdate("CREATE INDEX entity_type_time_idx ON " + prefix + "entity(entity_type, time)"); + statement.executeUpdate("CREATE INDEX entity_time_idx ON " + prefix + "entity(time)"); + } + else { + statement.executeUpdate("ALTER TABLE " + prefix + "entity ADD COLUMN entity_type INTEGER DEFAULT 0"); + } + } + + statement.close(); + } + catch (Exception e) { + // Table might not exist yet, which is fine + } + } + } diff --git a/src/main/java/net/coreprotect/database/logger/EntityKillLogger.java b/src/main/java/net/coreprotect/database/logger/EntityKillLogger.java index f929bf8fa..a6ba2edb6 100644 --- a/src/main/java/net/coreprotect/database/logger/EntityKillLogger.java +++ b/src/main/java/net/coreprotect/database/logger/EntityKillLogger.java @@ -50,7 +50,7 @@ public static void log(PreparedStatement preparedStmt, PreparedStatement prepare int z = eventLocation.getBlockZ(); int entity_key = 0; - ResultSet resultSet = EntityStatement.insert(preparedStmt2, time, data); + ResultSet resultSet = EntityStatement.insert(preparedStmt2, time, data, type); if (Database.hasReturningKeys()) { resultSet.next(); entity_key = resultSet.getInt(1); diff --git a/src/main/java/net/coreprotect/database/statement/EntityStatement.java b/src/main/java/net/coreprotect/database/statement/EntityStatement.java index 355e8d575..96526ba5d 100644 --- a/src/main/java/net/coreprotect/database/statement/EntityStatement.java +++ b/src/main/java/net/coreprotect/database/statement/EntityStatement.java @@ -2,11 +2,14 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.InputStream; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; import java.util.ArrayList; import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import org.bukkit.block.BlockState; import org.bukkit.util.io.BukkitObjectInputStream; @@ -22,17 +25,24 @@ private EntityStatement() { } public static ResultSet insert(PreparedStatement preparedStmt, int time, List data) { + return insert(preparedStmt, time, data, 0); + } + + public static ResultSet insert(PreparedStatement preparedStmt, int time, List data, int entityType) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); - BukkitObjectOutputStream oos = new BukkitObjectOutputStream(bos); + GZIPOutputStream gzip = new GZIPOutputStream(bos); + BukkitObjectOutputStream oos = new BukkitObjectOutputStream(gzip); oos.writeObject(data); oos.flush(); oos.close(); + gzip.close(); bos.close(); byte[] byte_data = bos.toByteArray(); preparedStmt.setInt(1, time); preparedStmt.setObject(2, byte_data); + preparedStmt.setInt(3, entityType); if (Database.hasReturningKeys()) { return preparedStmt.executeQuery(); } @@ -54,12 +64,22 @@ public static List getData(Statement statement, BlockState block, String ResultSet resultSet = statement.executeQuery(query); while (resultSet.next()) { byte[] data = resultSet.getBytes("data"); - ByteArrayInputStream bais = new ByteArrayInputStream(data); - BukkitObjectInputStream ins = new BukkitObjectInputStream(bais); + InputStream inputStream; + + // Check for GZIP magic bytes (0x1F 0x8B) to detect compressed data + // Maintains backwards compatibility with legacy uncompressed data + if (data.length >= 2 && (data[0] & 0xFF) == 0x1F && (data[1] & 0xFF) == 0x8B) { + inputStream = new GZIPInputStream(new ByteArrayInputStream(data)); + } + else { + inputStream = new ByteArrayInputStream(data); + } + + BukkitObjectInputStream ins = new BukkitObjectInputStream(inputStream); @SuppressWarnings("unchecked") List input = (List) ins.readObject(); ins.close(); - bais.close(); + inputStream.close(); result = input; } From 9a4dac5acc873b06dd88fec61d5ab4b075669406 Mon Sep 17 00:00:00 2001 From: TonyJamesStark Date: Mon, 22 Dec 2025 14:54:32 +0000 Subject: [PATCH 3/6] Extend purge command with entity type and radius filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enable entity type filtering: /co purge t:30d i:zombie,creeper - Enable radius-based purging: /co purge t:30d r:100 - Support combining filters: /co purge t:30d r:100 w:world_nether - Apply spatial filtering to tables with coordinates (block, container, sign, item, etc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../net/coreprotect/command/PurgeCommand.java | 105 +++++++++++++----- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/src/main/java/net/coreprotect/command/PurgeCommand.java b/src/main/java/net/coreprotect/command/PurgeCommand.java index daf6e56f1..0a281a84f 100755 --- a/src/main/java/net/coreprotect/command/PurgeCommand.java +++ b/src/main/java/net/coreprotect/command/PurgeCommand.java @@ -72,10 +72,7 @@ protected static void runCommand(final CommandSender player, boolean permission, Chat.sendMessage(player, Color.DARK_AQUA + "CoreProtect " + Color.WHITE + "- " + Phrase.build(Phrase.MISSING_PARAMETERS, "/co purge t: