12 Commits

Author SHA1 Message Date
4797f0dc87 add player listener (#15)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 45s
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Reviewed-on: #15
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-29 10:30:29 +00:00
ab2c9bf1d6 Add default value to jdbc url (#11)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 40s
Co-authored-by: Ardakaz <ardakaz@duck.com>
Reviewed-on: #11
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-18 18:25:28 +00:00
7aec608825 add new db tables (#13)
Some checks failed
Build / Explore-Gitea-Actions (push) Has been cancelled
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Co-authored-by: Ardakaz <ardakaz@duck.com>
Reviewed-on: #13
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-18 18:24:26 +00:00
e980768210 Fix lack of space after animal type (#12)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 44s
Reviewed-on: #12
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-15 12:13:12 +00:00
5064ac5903 Restrict block break if condition (#10)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 51s
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Reviewed-on: #10
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-13 15:12:00 +00:00
55881630bc Set up parameterized jdbc connection (#8)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 42s
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Reviewed-on: #8
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-07-03 11:07:53 +00:00
Ardakaz
4cd24ad467 Just organizing a bit and adding comments
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 45s
2026-06-29 10:52:24 +03:00
db3649a2f5 add newline at the end of files (#5)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 44s
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Reviewed-on: #5
Reviewed-by: Ardakaz <ardakaz@duck.com>
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-06-28 11:49:23 +00:00
fec5f78cbc Introduce build pipeline (#4)
All checks were successful
Build / Explore-Gitea-Actions (push) Successful in 44s
Co-authored-by: ThePindabaas <219561634+ThePindabaas@users.noreply.github.com>
Reviewed-on: #4
Co-authored-by: ThePindabaas <thepindabaas@noreply.localhost>
Co-committed-by: ThePindabaas <thepindabaas@noreply.localhost>
2026-06-28 11:23:50 +00:00
7d3363812f Merge pull request 'changed Armorstand Logic' (#3) from armorstand-DeathLogic into main
Reviewed-on: #3
2026-06-27 14:38:18 +00:00
urtypicalblahajlunatic
1329b616f5 debugged and fixed EntityDeathEvent 2026-06-27 16:21:36 +02:00
urtypicalblahajlunatic
4e65e1bcb2 armorstand changed from EntityHitEvent to EntityDeathEvent, SHOULD cause less fake errors 2026-06-27 14:29:49 +02:00
11 changed files with 467 additions and 67 deletions

View File

@@ -0,0 +1,16 @@
name: Build
run-name: ${{ gitea.ref }} - ${{ gitea.sha }}
on: [push, workflow_dispatch]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
- run: ./gradlew assemble
- run: ./gradlew test
- run: ./gradlew jar

19
.gitea/workflows/demo.yml Normal file
View File

@@ -0,0 +1,19 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [workflow_dispatch]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."

View File

@@ -1,5 +1,9 @@
package net.ardakaz.griefalert; package net.ardakaz.griefalert;
import net.ardakaz.griefalert.database.DatabaseService;
import net.ardakaz.griefalert.database.Migrations;
import net.ardakaz.griefalert.database.PlayerRepository;
import net.ardakaz.griefalert.listener.PlayerListener;
import net.coreprotect.CoreProtect; import net.coreprotect.CoreProtect;
import net.coreprotect.CoreProtectAPI; import net.coreprotect.CoreProtectAPI;
import net.coreprotect.CoreProtectAPI.ParseResult; import net.coreprotect.CoreProtectAPI.ParseResult;
@@ -28,6 +32,7 @@ import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.sql.*; import java.sql.*;
@@ -35,13 +40,16 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class GriefAlert extends JavaPlugin implements Listener { public class GriefAlert extends JavaPlugin implements Listener {
private static CoreProtectAPI coreProtectAPI; private static CoreProtectAPI coreProtectAPI;
private final String DB_FILE = "ignored_locations.db";
private Integer identicalAlerts = 1; private Integer identicalAlerts = 1;
private String lastAlert; private String lastAlert;
private Set<Material> EXCLUDED_BLOCKS; private Set<Material> EXCLUDED_BLOCKS;
@@ -49,6 +57,12 @@ public class GriefAlert extends JavaPlugin implements Listener {
private String MAP_LINK; private String MAP_LINK;
private Boolean ALLOW_STEALING; private Boolean ALLOW_STEALING;
private Connection connection; private Connection connection;
private DatabaseService database;
private Logger logger;
/*
UTILITY FUNCTIONS
*/
// Block inspector: only the most recent placement counts for ownership. // Block inspector: only the most recent placement counts for ownership.
private static String inspectBlock(Block block, Player player) { private static String inspectBlock(Block block, Player player) {
@@ -70,7 +84,7 @@ public class GriefAlert extends JavaPlugin implements Listener {
} }
} }
// If we see a break before a placement, stop (block is gone) // If we see a break before a placement, stop (block is gone)
if (parseResult.getActionId() == 0) { if (parseResult.getActionId() == 0 && !parseResult.isRolledBack()) {
break; break;
} }
} }
@@ -90,9 +104,69 @@ public class GriefAlert extends JavaPlugin implements Listener {
return world; return world;
} }
// Sends the alert (or cancels it)
private void alert(String message, String playerName, String mapLink, String target, int x, int y, int z, String world) {
// Exclude trusted people
Player griefer = Bukkit.getPlayer(playerName);
if (griefer.hasPermission("griefalert.exclude") || griefer.hasPermission("griefalert.exclude." + target)) {
return;
}
// Spam limiter
String realAlertMessage = message;
String[] alert1 = null;
if (lastAlert != null) {
alert1 = lastAlert.split(" ");
}
String[] alert2 = message.split(" ");
if (alert1 != null) {
if (alert1[2].equals(alert2[2]) && alert1[5].equals(alert2[5]) && alert1[1].equals("broke") && alert2[1].equals("broke")) {
identicalAlerts += 1;
} else if (Arrays.equals(alert1, alert2)) {
identicalAlerts += 1;
} else {
identicalAlerts = 1;
}
}
if (identicalAlerts == 4) {
message = ChatColor.GRAY + "Same behavior continues.";
mapLink = null;
}
if (identicalAlerts > 4) {
return;
}
// Use direct location check
if (world != null && isLocationIgnored(x, y, z, world)) {
return; // Do not alert if location is ignored
}
// Send an event for external hooks
GriefAlertEvent griefalert_event;
if (MAP_LINK != null && !MAP_LINK.isEmpty() && mapLink != null) {
griefalert_event = new GriefAlertEvent(message + " (" + mapLink + ")");
} else {
griefalert_event = new GriefAlertEvent(message);
}
getServer().getPluginManager().callEvent(griefalert_event);
// Notify staff ingame
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission("griefalert.notify")) {
player.sendMessage(message);
}
}
lastAlert = realAlertMessage;
}
/*
INITIALIZATION
*/
// Init GriefAlert // Init GriefAlert
@Override @Override
public void onEnable() { public void onEnable() {
logger = getLogger();
coreProtectAPI = getCoreProtect(); coreProtectAPI = getCoreProtect();
if (coreProtectAPI == null) { if (coreProtectAPI == null) {
@@ -101,7 +175,6 @@ public class GriefAlert extends JavaPlugin implements Listener {
return; return;
} }
getServer().getPluginManager().registerEvents(this, this);
// Config // Config
saveDefaultConfig(); saveDefaultConfig();
@@ -114,9 +187,18 @@ public class GriefAlert extends JavaPlugin implements Listener {
setupDatabase(); setupDatabase();
PlayerRepository playerRepository = new PlayerRepository(database, logger);
PlayerListener playerListener = new PlayerListener(playerRepository, logger);
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(this, this);
pluginManager.registerEvents(playerListener, this);
getCommand("griefalert").setTabCompleter(this); getCommand("griefalert").setTabCompleter(this);
getLogger().info("GriefAlert has been enabled."); getLogger().info("GriefAlert has been enabled.");
} }
@Override @Override
@@ -131,16 +213,26 @@ public class GriefAlert extends JavaPlugin implements Listener {
} }
private void setupDatabase() { private void setupDatabase() {
ExecutorService executor = Executors.newSingleThreadExecutor();
String jdbcUrl = getConfig().getString("jdbc-url");
try { try {
connection = DriverManager.getConnection("jdbc:sqlite:" + getDataFolder().getAbsolutePath() + "/" + DB_FILE); connection = DriverManager.getConnection(jdbcUrl);
Statement stmt = connection.createStatement(); database = new DatabaseService(executor, connection);
stmt.executeUpdate("CREATE TABLE IF NOT EXISTS ignored_locations (x INTEGER, y INTEGER, z INTEGER, world TEXT, PRIMARY KEY (x, y, z, world))"); logger.info("Connected to " + jdbcUrl);
stmt.close();
CompletableFuture<Void> migrationExecution = new Migrations(database, logger).execute();
migrationExecution.join();
logger.info("Database migration finished");
} catch (SQLException e) { } catch (SQLException e) {
getLogger().severe("Could not set up SQLite database: " + e.getMessage()); logger.severe("Could not set up SQLite database: " + e.getMessage());
} }
} }
/*
LOCATION IGNORING
*/
private boolean isLocationIgnored(int x, int y, int z, String world) { private boolean isLocationIgnored(int x, int y, int z, String world) {
try { try {
PreparedStatement ps = connection.prepareStatement("SELECT 1 FROM ignored_locations WHERE x=? AND y=? AND z=? AND world=?"); PreparedStatement ps = connection.prepareStatement("SELECT 1 FROM ignored_locations WHERE x=? AND y=? AND z=? AND world=?");
@@ -191,6 +283,10 @@ public class GriefAlert extends JavaPlugin implements Listener {
} }
} }
/*
EVENT LISTENERS
*/
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
// Block break alerts // Block break alerts
public void onBlockBreak(BlockBreakEvent event) { public void onBlockBreak(BlockBreakEvent event) {
@@ -272,9 +368,9 @@ public class GriefAlert extends JavaPlugin implements Listener {
// Armor Stand break alerts // Armor Stand break alerts
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onArmorStandBreak(EntityDamageByEntityEvent event) { public void onArmorStandBreak(EntityDeathEvent event) {
if (!(event.getEntity() instanceof ArmorStand armorStand)) return; if (!(event.getEntity() instanceof ArmorStand armorStand)) return;
if (!(event.getDamager() instanceof Player player)) return; if(!(event.getDamageSource().getCausingEntity() instanceof Player player)) return;
int x = armorStand.getLocation().getBlockX(); int x = armorStand.getLocation().getBlockX();
int y = armorStand.getLocation().getBlockY(); int y = armorStand.getLocation().getBlockY();
int z = armorStand.getLocation().getBlockZ(); int z = armorStand.getLocation().getBlockZ();
@@ -288,6 +384,7 @@ public class GriefAlert extends JavaPlugin implements Listener {
String message = ChatColor.GRAY + playerName + " broke an armor stand placed by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName); String message = ChatColor.GRAY + playerName + " broke an armor stand placed by " + target + " at " + x + " " + y + " " + z + getHumanWorldName(worldName);
alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName); alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName);
} }
} }
// Armor Stand item interaction alerts // Armor Stand item interaction alerts
@@ -335,7 +432,7 @@ public class GriefAlert extends JavaPlugin implements Listener {
} }
} }
//event handler for item frames // Placing item to an item frame
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onPlayerInteractEntity(PlayerInteractEntityEvent event) { public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
if (!(event.getRightClicked() instanceof ItemFrame frame)) return; if (!(event.getRightClicked() instanceof ItemFrame frame)) return;
@@ -369,7 +466,7 @@ public class GriefAlert extends JavaPlugin implements Listener {
} }
} }
// Breaking item frame item (uses a different event then placing an item) // Item frame stealing alerts
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onItemFrameDamage(EntityDamageByEntityEvent event) { public void onItemFrameDamage(EntityDamageByEntityEvent event) {
if (!(event.getEntity() instanceof ItemFrame frame)) return; if (!(event.getEntity() instanceof ItemFrame frame)) return;
@@ -397,6 +494,7 @@ public class GriefAlert extends JavaPlugin implements Listener {
alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName); alert(message, playerName, "[Map Link](" + MAP_LINK + "/?worldname=" + worldName + "&zoom=7&x=" + x + "&y=" + y + "&z=" + z + ")", target, x, y, z, worldName);
} }
// Item frame breaking alerts
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onItemFrameBreak(HangingBreakByEntityEvent event) { public void onItemFrameBreak(HangingBreakByEntityEvent event) {
if (!(event.getEntity() instanceof ItemFrame frame)) return; if (!(event.getEntity() instanceof ItemFrame frame)) return;
@@ -422,6 +520,7 @@ public class GriefAlert extends JavaPlugin implements Listener {
} }
// Command handler
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!command.getName().equalsIgnoreCase("griefalert")) return false; if (!command.getName().equalsIgnoreCase("griefalert")) return false;
@@ -583,57 +682,6 @@ public class GriefAlert extends JavaPlugin implements Listener {
return null; return null;
} }
// Sends the alert (or cancels it)
private void alert(String message, String playerName, String mapLink, String target, int x, int y, int z, String world) {
// Exclude trusted people
Player griefer = Bukkit.getPlayer(playerName);
if (griefer.hasPermission("griefalert.exclude") || griefer.hasPermission("griefalert.exclude." + target)) {
return;
}
// Spam limiter
String realAlertMessage = message;
String[] alert1 = null;
if (lastAlert != null) {
alert1 = lastAlert.split(" ");
}
String[] alert2 = message.split(" ");
if (alert1 != null) {
if (alert1[2].equals(alert2[2]) && alert1[5].equals(alert2[5]) && alert1[1].equals("broke") && alert2[1].equals("broke")) {
identicalAlerts += 1;
} else if (Arrays.equals(alert1, alert2)) {
identicalAlerts += 1;
} else {
identicalAlerts = 1;
}
}
if (identicalAlerts == 4) {
message = ChatColor.GRAY + "Same behavior continues.";
mapLink = null;
}
if (identicalAlerts > 4) {
return;
}
// Use direct location check
if (world != null && isLocationIgnored(x, y, z, world)) {
return; // Do not alert if location is ignored
}
// Send an event for external hooks
GriefAlertEvent griefalert_event;
if (MAP_LINK != null && !MAP_LINK.isEmpty() && mapLink != null) {
griefalert_event = new GriefAlertEvent(message + " (" + mapLink + ")");
} else {
griefalert_event = new GriefAlertEvent(message);
}
getServer().getPluginManager().callEvent(griefalert_event);
// Notify staff ingame
for (Player player : Bukkit.getOnlinePlayers()) {
if (player.hasPermission("griefalert.notify")) {
player.sendMessage(message);
}
}
lastAlert = realAlertMessage;
}
//pet alert(dog and cat only) //pet alert(dog and cat only)
@EventHandler @EventHandler
public void onPetKill(EntityDeathEvent event) { public void onPetKill(EntityDeathEvent event) {
@@ -661,7 +709,7 @@ public class GriefAlert extends JavaPlugin implements Listener {
} }
//farm animal handler // Mob killing alerts
@EventHandler @EventHandler
public void onFarmAnimalDeath(EntityDeathEvent event) { public void onFarmAnimalDeath(EntityDeathEvent event) {
if (!(event.getEntity().getKiller() instanceof Player killer)) return; if (!(event.getEntity().getKiller() instanceof Player killer)) return;
@@ -697,6 +745,7 @@ public class GriefAlert extends JavaPlugin implements Listener {
and its waxed, ill assume ur unwaxing, and therefor sending an alert and its waxed, ill assume ur unwaxing, and therefor sending an alert
but some of this code was written at 1am so idk how and why im why and howing */ but some of this code was written at 1am so idk how and why im why and howing */
// Waxing/unwaxing alerts
@EventHandler @EventHandler
public void onBlockClick(PlayerInteractEvent event) { public void onBlockClick(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
@@ -759,7 +808,7 @@ public class GriefAlert extends JavaPlugin implements Listener {
}, 1L); }, 1L);
} }
//stripping logs // Log stripping alerts
@EventHandler @EventHandler
public void onLogStrip(PlayerInteractEvent event) { public void onLogStrip(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
@@ -810,6 +859,10 @@ public class GriefAlert extends JavaPlugin implements Listener {
}, 1L); }, 1L);
} }
/*
CoreProtect API
*/
private CoreProtectAPI getCoreProtect() { private CoreProtectAPI getCoreProtect() {
Plugin plugin = getServer().getPluginManager().getPlugin("CoreProtect"); Plugin plugin = getServer().getPluginManager().getPlugin("CoreProtect");

View File

@@ -0,0 +1,9 @@
package net.ardakaz.griefalert.database;
import java.sql.Connection;
import java.sql.SQLException;
@FunctionalInterface
public interface DatabaseOperation<T> {
T execute(Connection connection) throws SQLException;
}

View File

@@ -0,0 +1,27 @@
package net.ardakaz.griefalert.database;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
public class DatabaseService {
private final ExecutorService executor;
private final Connection connection;
public DatabaseService(ExecutorService executor, Connection connection) {
this.executor = executor;
this.connection = connection;
}
public <T> CompletableFuture<T> submit(DatabaseOperation<T> operation) {
return CompletableFuture.supplyAsync(() -> {
try {
return operation.execute(connection);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}, executor);
}
}

View File

@@ -0,0 +1,96 @@
package net.ardakaz.griefalert.database;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Logger;
public class Migrations {
private DatabaseService database;
private Logger logger;
public Migrations(DatabaseService database, Logger logger) {
this.database = database;
this.logger = logger;
}
private static final String CREATE_TABLE_IGNORED_LOCATIONS =
"CREATE TABLE IF NOT EXISTS ignored_locations (" +
"x INTEGER, " +
"y INTEGER, " +
"z INTEGER, " +
"world TEXT, " +
"PRIMARY KEY (x, y, z, world)" +
")";
private static final String CREATE_TABLE_PLAYERS =
"CREATE TABLE IF NOT EXISTS players (" +
"id TEXT PRIMARY KEY, " +
"names TEXT NOT NULL, " +
"firstseen INTEGER NOT NULL, " +
"lastseen INTEGER NOT NULL, " +
"playtime INTEGER NOT NULL DEFAULT 0 " +
")";
private static final String CREATE_TABLE_ACTIONS =
"CREATE TABLE IF NOT EXISTS actions (" +
"id INTEGER PRIMARY KEY, " +
"player TEXT NOT NULL, " +
"type TEXT NOT NULL, " +
"moderator TEXT NOT NULL, " +
"reason TEXT, " +
"time TEXT, " +
"timestamp INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"FOREIGN KEY(player) REFERENCES players(id)" +
")";
private static final String CREATE_TABLE_ALERTS =
"CREATE TABLE IF NOT EXISTS alerts (" +
"id INTEGER PRIMARY KEY, " +
"player TEXT NOT NULL, " +
"action TEXT NOT NULL, " +
"target TEXT NOT NULL, " +
"target_amount INTEGER, " +
"target_player TEXT NOT NULL, " +
"x INTEGER NOT NULL, " +
"y INTEGER NOT NULL, " +
"z INTEGER NOT NULL, " +
"world TEXT NOT NULL, " +
"is_flagged INTEGER, " +
"flag_text TEXT, " +
"resolved_by TEXT, " +
"state TEXT NOT NULL DEFAULT 'UNRESOLVED', " +
"timestamp INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"FOREIGN KEY(player) REFERENCES players(id)" +
")";
private static final String CREATE_TABLE_REPORTS =
"CREATE TABLE IF NOT EXISTS reports (" +
"id INTEGER PRIMARY KEY, " +
"player TEXT NOT NULL, " +
"x INTEGER NOT NULL, " +
"y INTEGER NOT NULL, " +
"z INTEGER NOT NULL, " +
"world TEXT NOT NULL, " +
"text TEXT NOT NULL, " +
"resolved_by TEXT, " +
"timestamp INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
"state TEXT NOT NULL DEFAULT 'UNRESOLVED'" +
")";
public CompletableFuture<Void> execute() {
return database.submit(connection -> {
try(Statement stmt = connection.createStatement()){
stmt.executeUpdate(Migrations.CREATE_TABLE_IGNORED_LOCATIONS);
stmt.executeUpdate(Migrations.CREATE_TABLE_PLAYERS);
stmt.executeUpdate(Migrations.CREATE_TABLE_ACTIONS);
stmt.executeUpdate(Migrations.CREATE_TABLE_ALERTS);
stmt.executeUpdate(Migrations.CREATE_TABLE_REPORTS);
} catch (SQLException e) {
logger.severe("Failed to execute database migrations: " + e.getMessage());
}
return null;
});
}
}

View File

@@ -0,0 +1,6 @@
package net.ardakaz.griefalert.database;
import java.util.List;
public record PlayerEntity(String id, List<String> names, long playtime, long firstSeen, long lastSeen) {
}

View File

@@ -0,0 +1,95 @@
package net.ardakaz.griefalert.database;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Logger;
public class PlayerRepository {
private Logger logger;
private final DatabaseService database;
public PlayerRepository(DatabaseService database, Logger logger) {
this.database = database;
this.logger = logger;
}
public CompletableFuture<PlayerEntity> get(String uuid) {
return database.submit(connection -> {
try (PreparedStatement ps = connection.prepareStatement("SELECT * FROM players WHERE id=?")) {
ps.setString(1, uuid);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
return new PlayerEntity(
rs.getString("id"),
Arrays.stream(rs.getString("names").split(",")).toList(),
rs.getLong("playtime"),
rs.getLong("firstseen"),
rs.getLong("lastseen")
);
}
}
} catch (SQLException e) {
logger.severe("DB error: " + e.getMessage());
}
return null;
});
}
public CompletableFuture<Void> upsert(PlayerEntity entity) {
return database.submit(connection -> {
String upsert = """
INSERT INTO players(id, names, firstseen, lastseen) VALUES(?,?,?,?)
ON CONFLICT(id)
DO UPDATE SET names=excluded.names, playtime=?, lastseen=excluded.lastseen;
""";
try (PreparedStatement ps = connection.prepareStatement(upsert)) {
ps.setString(1, entity.id());
String names = String.join(",", entity.names());
ps.setString(2, names);
ps.setLong(3, entity.firstSeen());
ps.setLong(4, entity.lastSeen());
ps.setLong(5, entity.playtime());
ps.executeUpdate();
} catch (SQLException e) {
logger.severe("Failed to save player: " + entity);
logger.severe(e.getMessage());
}
return null;
});
}
public CompletableFuture<Void> updateNames(String id, List<String> names) {
return database.submit(connection -> {
String update = """
UPDATE players
SET names = ?
WHERE id = ?
""";
try (PreparedStatement ps = connection.prepareStatement(update)) {
ps.setString(1, String.join(",", names));
ps.setString(2, id);
ps.executeUpdate();
} catch (SQLException e) {
logger.severe("Failed to update names for playerID: " + id);
logger.severe(e.getMessage());
}
return null;
});
}
}

View File

@@ -0,0 +1,77 @@
package net.ardakaz.griefalert.listener;
import net.ardakaz.griefalert.database.PlayerEntity;
import net.ardakaz.griefalert.database.PlayerRepository;
import org.bukkit.Statistic;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
public class PlayerListener implements Listener {
private Logger logger;
private PlayerRepository repository;
public PlayerListener(PlayerRepository repository, Logger logger) {
this.logger = logger;
this.repository = repository;
}
@EventHandler
public void onPlayerLogin(PlayerJoinEvent event) {
Player eventPlayer = event.getPlayer();
repository.get(
eventPlayer.getUniqueId().toString()
).thenAccept(dbPlayer -> {
if (dbPlayer == null) {
long now = System.currentTimeMillis();
repository.upsert(new PlayerEntity(
eventPlayer.getUniqueId().toString(),
List.of(eventPlayer.getName()),
0,
now,
now
)
);
} else {
List<String> newNames = updateNames(eventPlayer.getName(), dbPlayer.names());
repository.updateNames(dbPlayer.id(), newNames);
}
});
}
@EventHandler
public void onPlayerLogout(PlayerQuitEvent event) {
Player eventPlayer = event.getPlayer();
long playtimeSeconds = eventPlayer.getStatistic(Statistic.PLAY_ONE_MINUTE) / 20L;
repository.get(
eventPlayer.getUniqueId().toString()
).thenAccept(dbPlayer -> {
if (dbPlayer != null) {
PlayerEntity updatedPlayer = new PlayerEntity(
dbPlayer.id(),
dbPlayer.names(),
playtimeSeconds,
dbPlayer.firstSeen(),
System.currentTimeMillis()
);
repository.upsert(updatedPlayer);
}
});
}
private List<String> updateNames(String currentName, List<String> historicalNames) {
List<String> newNames = new ArrayList<>(historicalNames);
while (newNames.contains(currentName)) {
newNames.remove(currentName);
}
newNames.add(currentName);
return newNames;
}
}

View File

@@ -1,6 +1,8 @@
# Ignore all stealing? # Ignore all stealing?
allow-stealing: false allow-stealing: false
jdbc-url: "./plugins/GriefAlert/database.db"
# Breaking these blocks don't cause alerts. (Material) # Breaking these blocks don't cause alerts. (Material)
excluded-blocks: excluded-blocks:
- WHEAT - WHEAT