diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml
new file mode 100644
index 0000000..39334fd
--- /dev/null
+++ b/dependency-reduced-pom.xml
@@ -0,0 +1,74 @@
+
+
+ 4.0.0
+ party.cybsec
+ griefdetect
+ GriefDetect
+ 1.0.0-SNAPSHOT
+ Modular, async-first grief pattern detection plugin for Paper 1.21.11
+ https://git.cybsec.party/cybsec/griefdetect.git
+
+
+
+ maven-compiler-plugin
+ 3.13.0
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+
+
+
+ maven-shade-plugin
+ 3.6.0
+
+
+ package
+
+ shade
+
+
+ true
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
+
+ papermc
+ https://repo.papermc.io/repository/maven-public/
+
+
+ sonatype
+ https://oss.sonatype.org/content/groups/public/
+
+
+
+
+ io.papermc.paper
+ paper-api
+ 1.21.11-R0.1-SNAPSHOT
+ provided
+
+
+
+ 17
+ 4.12.0
+ 2.0.16
+ 17
+ UTF-8
+ 1.21.11-R0.1-SNAPSHOT
+ 2.17.1
+
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..25ad4ae
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,108 @@
+
+
+ 4.0.0
+
+ party.cybsec
+ griefdetect
+ 1.0.0-SNAPSHOT
+ jar
+
+ GriefDetect
+ Modular, async-first grief pattern detection plugin for Paper 1.21.11
+ https://git.cybsec.party/cybsec/griefdetect.git
+
+
+ 17
+ 17
+ UTF-8
+ 1.21.11-R0.1-SNAPSHOT
+ 2.17.1
+ 4.12.0
+ 2.0.16
+
+
+
+
+ papermc
+ https://repo.papermc.io/repository/maven-public/
+
+
+ sonatype
+ https://oss.sonatype.org/content/groups/public/
+
+
+
+
+
+
+ io.papermc.paper
+ paper-api
+ ${paper.version}
+ provided
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp.version}
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.6.0
+
+
+ package
+
+ shade
+
+
+ true
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/GriefDetectPlugin.java b/src/main/java/party/cybsec/griefdetect/GriefDetectPlugin.java
new file mode 100644
index 0000000..9238758
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/GriefDetectPlugin.java
@@ -0,0 +1,110 @@
+package party.cybsec.griefdetect;
+
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.core.ModuleManager;
+import party.cybsec.griefdetect.core.WebhookManager;
+import party.cybsec.griefdetect.core.PermissionManager;
+import party.cybsec.griefdetect.core.ConfidenceEngine;
+import party.cybsec.griefdetect.core.DetectionEngine;
+import party.cybsec.griefdetect.core.ChatAlertManager;
+import party.cybsec.griefdetect.modules.LavaCastDetector;
+import party.cybsec.griefdetect.config.PluginConfig;
+import party.cybsec.griefdetect.commands.ReloadCommand;
+
+/**
+ * Main plugin class for GriefDetect.
+ * Modular, async-first grief pattern detection with confidence scoring and Discord webhook reporting.
+ */
+public class GriefDetectPlugin extends JavaPlugin {
+
+ private static GriefDetectPlugin instance;
+
+ private PluginConfig config;
+ private ModuleManager moduleManager;
+ private WebhookManager webhookManager;
+ private PermissionManager permissionManager;
+ private ConfidenceEngine confidenceEngine;
+ private DetectionEngine detectionEngine;
+
+ @Override
+ public void onEnable() {
+ instance = this;
+
+ // Initialize core systems
+ initializeConfig();
+ initializeCoreSystems();
+ initializeModules();
+ registerCommands();
+
+ getLogger().info("GriefDetect enabled - " + getDescription().getVersion());
+ }
+
+ @Override
+ public void onDisable() {
+ // Cleanup resources
+ if (detectionEngine != null) {
+ detectionEngine.shutdown();
+ }
+
+ getLogger().info("GriefDetect disabled");
+ }
+
+ private void initializeConfig() {
+ config = new PluginConfig(this);
+ config.load();
+ }
+
+ private void initializeCoreSystems() {
+ permissionManager = new PermissionManager(this);
+ confidenceEngine = new ConfidenceEngine(this);
+ webhookManager = new WebhookManager(this);
+ detectionEngine = new DetectionEngine(this);
+ moduleManager = new ModuleManager(this);
+ // Note: ChatAlertManager is created by DetectionEngine when needed
+ }
+
+ private void initializeModules() {
+ // Register modules
+ moduleManager.registerModule(new LavaCastDetector(this));
+
+ // Start the detection engine
+ detectionEngine.start();
+ }
+
+ private void registerCommands() {
+ getCommand("griefdetect").setExecutor(new ReloadCommand(this));
+ }
+
+ // Getters for accessing core systems
+ public static GriefDetectPlugin getInstance() {
+ return instance;
+ }
+
+ public PluginConfig getPluginConfig() {
+ return config;
+ }
+
+ public ModuleManager getModuleManager() {
+ return moduleManager;
+ }
+
+ public WebhookManager getWebhookManager() {
+ return webhookManager;
+ }
+
+ public PermissionManager getPermissionManager() {
+ return permissionManager;
+ }
+
+ public ConfidenceEngine getConfidenceEngine() {
+ return confidenceEngine;
+ }
+
+ public DetectionEngine getDetectionEngine() {
+ return detectionEngine;
+ }
+
+ public ChatAlertManager getChatAlertManager() {
+ return new ChatAlertManager(this);
+ }
+}
diff --git a/src/main/java/party/cybsec/griefdetect/commands/ReloadCommand.java b/src/main/java/party/cybsec/griefdetect/commands/ReloadCommand.java
new file mode 100644
index 0000000..a687027
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/commands/ReloadCommand.java
@@ -0,0 +1,83 @@
+package party.cybsec.griefdetect.commands;
+
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.GriefDetectPlugin;
+import party.cybsec.griefdetect.config.PluginConfig;
+import party.cybsec.griefdetect.core.ModuleManager;
+import party.cybsec.griefdetect.core.PermissionManager;
+import party.cybsec.griefdetect.core.WebhookManager;
+import party.cybsec.griefdetect.core.ChatAlertManager;
+
+/**
+ * Reload command for GriefDetect plugin.
+ * Permission-protected, reloads config and restarts modules safely.
+ */
+public class ReloadCommand implements CommandExecutor {
+
+ private final GriefDetectPlugin plugin;
+ private final PermissionManager permissionManager;
+ private final PluginConfig config;
+ private final ModuleManager moduleManager;
+ private final WebhookManager webhookManager;
+ private final ChatAlertManager chatAlertManager;
+
+ public ReloadCommand(GriefDetectPlugin plugin) {
+ this.plugin = plugin;
+ this.permissionManager = plugin.getPermissionManager();
+ this.config = plugin.getPluginConfig();
+ this.moduleManager = plugin.getModuleManager();
+ this.webhookManager = plugin.getWebhookManager();
+ this.chatAlertManager = plugin.getChatAlertManager();
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ // Check permissions
+ if (sender instanceof Player) {
+ Player player = (Player) sender;
+ if (!permissionManager.canReload(player)) {
+ sender.sendMessage("§cYou don't have permission to use this command.");
+ return true;
+ }
+ }
+
+ // Perform reload
+ reloadPlugin(sender);
+ return true;
+ }
+
+ /**
+ * Reload the plugin configuration and modules.
+ */
+ private void reloadPlugin(CommandSender sender) {
+ try {
+ // Reload configuration
+ config.reload();
+
+ // Reload modules
+ moduleManager.reloadModules();
+
+ // Reload webhook manager
+ webhookManager.reload();
+
+ // Reload chat alert manager
+ chatAlertManager.reload();
+
+ // Clear permission cache
+ permissionManager.clearAllCache();
+
+ // Send success message
+ sender.sendMessage("§aGriefDetect configuration reloaded successfully!");
+ plugin.getLogger().info("Plugin reloaded by " + sender.getName());
+
+ } catch (Exception e) {
+ sender.sendMessage("§cError reloading plugin: " + e.getMessage());
+ plugin.getLogger().severe("Error reloading plugin: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/config/PluginConfig.java b/src/main/java/party/cybsec/griefdetect/config/PluginConfig.java
new file mode 100644
index 0000000..7844e6e
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/config/PluginConfig.java
@@ -0,0 +1,254 @@
+package party.cybsec.griefdetect.config;
+
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.GriefDetectPlugin;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Configuration manager for GriefDetect plugin.
+ * Handles loading, saving, and accessing all plugin configuration.
+ */
+public class PluginConfig {
+
+ private final GriefDetectPlugin plugin;
+ private FileConfiguration config;
+ private File configFile;
+
+ // Configuration keys
+ private static final String GLOBAL_ENABLE = "global.enable";
+ private static final String GLOBAL_ASYNC = "global.async";
+
+ private static final String PERMISSIONS_ADMIN = "permissions.admin";
+ private static final String PERMISSIONS_RELOAD = "permissions.reload";
+ private static final String PERMISSIONS_ALERTS = "permissions.alerts";
+ private static final String PERMISSIONS_BYPASS = "permissions.bypass";
+
+ private static final String WEBHOOK_ENABLED = "webhook.enabled";
+ private static final String WEBHOOK_URL = "webhook.url";
+ private static final String WEBHOOK_THRESHOLD = "webhook.threshold";
+ private static final String WEBHOOK_RETRY_ATTEMPTS = "webhook.retryAttempts";
+ private static final String WEBHOOK_RETRY_DELAY = "webhook.retryDelay";
+ private static final String WEBHOOK_COOLDOWN = "webhook.areaCooldown";
+
+ private static final String CHAT_ENABLED = "chat.enabled";
+ private static final String CHAT_THRESHOLD = "chat.threshold";
+ private static final String CHAT_FORMAT = "chat.format";
+
+ private static final String ENGINE_INTERVAL = "engine.analysisInterval";
+ private static final String ENGINE_TIMEOUT = "engine.clusterTimeout";
+ private static final String ENGINE_MAX_CLUSTERS = "engine.maxClustersPerWorld";
+
+ private static final String MODULES_PREFIX = "modules.";
+
+ // Default values
+ private static final boolean DEFAULT_GLOBAL_ENABLE = true;
+ private static final boolean DEFAULT_GLOBAL_ASYNC = true;
+
+ private static final String DEFAULT_PERMISSIONS_ADMIN = "griefdetect.admin";
+ private static final String DEFAULT_PERMISSIONS_RELOAD = "griefdetect.reload";
+ private static final String DEFAULT_PERMISSIONS_ALERTS = "griefdetect.alerts";
+ private static final String DEFAULT_PERMISSIONS_BYPASS = "griefdetect.bypass";
+
+ private static final boolean DEFAULT_WEBHOOK_ENABLED = false;
+ private static final String DEFAULT_WEBHOOK_URL = "";
+ private static final double DEFAULT_WEBHOOK_THRESHOLD = 75.0;
+ private static final int DEFAULT_WEBHOOK_RETRY_ATTEMPTS = 3;
+ private static final int DEFAULT_WEBHOOK_RETRY_DELAY = 5000;
+ private static final int DEFAULT_WEBHOOK_COOLDOWN = 300;
+
+ private static final boolean DEFAULT_CHAT_ENABLED = true;
+ private static final double DEFAULT_CHAT_THRESHOLD = 50.0;
+ private static final String DEFAULT_CHAT_FORMAT = "&c[GriefDetect] &7%s &fat &6%s:%d,%d,%d &7(%d%%)";
+
+ private static final int DEFAULT_ENGINE_INTERVAL = 5000;
+ private static final int DEFAULT_ENGINE_TIMEOUT = 300000; // 5 minutes
+ private static final int DEFAULT_ENGINE_MAX_CLUSTERS = 50;
+
+ public PluginConfig(GriefDetectPlugin plugin) {
+ this.plugin = plugin;
+ this.configFile = new File(plugin.getDataFolder(), "config.yml");
+ }
+
+ /**
+ * Load the configuration file.
+ */
+ public void load() {
+ // Create data folder if it doesn't exist
+ if (!plugin.getDataFolder().exists()) {
+ plugin.getDataFolder().mkdirs();
+ }
+
+ // Create default config if it doesn't exist
+ if (!configFile.exists()) {
+ plugin.saveResource("config.yml", false);
+ }
+
+ // Load configuration
+ config = YamlConfiguration.loadConfiguration(configFile);
+ setDefaults();
+
+ try {
+ config.save(configFile);
+ } catch (IOException e) {
+ plugin.getLogger().severe("Could not save config to " + configFile);
+ }
+ }
+
+ /**
+ * Set default configuration values.
+ */
+ private void setDefaults() {
+ // Global settings
+ config.addDefault(GLOBAL_ENABLE, DEFAULT_GLOBAL_ENABLE);
+ config.addDefault(GLOBAL_ASYNC, DEFAULT_GLOBAL_ASYNC);
+
+ // Permissions
+ config.addDefault(PERMISSIONS_ADMIN, DEFAULT_PERMISSIONS_ADMIN);
+ config.addDefault(PERMISSIONS_RELOAD, DEFAULT_PERMISSIONS_RELOAD);
+ config.addDefault(PERMISSIONS_ALERTS, DEFAULT_PERMISSIONS_ALERTS);
+ config.addDefault(PERMISSIONS_BYPASS, DEFAULT_PERMISSIONS_BYPASS);
+
+ // Webhook settings
+ config.addDefault(WEBHOOK_ENABLED, DEFAULT_WEBHOOK_ENABLED);
+ config.addDefault(WEBHOOK_URL, DEFAULT_WEBHOOK_URL);
+ config.addDefault(WEBHOOK_THRESHOLD, DEFAULT_WEBHOOK_THRESHOLD);
+ config.addDefault(WEBHOOK_RETRY_ATTEMPTS, DEFAULT_WEBHOOK_RETRY_ATTEMPTS);
+ config.addDefault(WEBHOOK_RETRY_DELAY, DEFAULT_WEBHOOK_RETRY_DELAY);
+ config.addDefault(WEBHOOK_COOLDOWN, DEFAULT_WEBHOOK_COOLDOWN);
+
+ // Chat settings
+ config.addDefault(CHAT_ENABLED, DEFAULT_CHAT_ENABLED);
+ config.addDefault(CHAT_THRESHOLD, DEFAULT_CHAT_THRESHOLD);
+ config.addDefault(CHAT_FORMAT, DEFAULT_CHAT_FORMAT);
+
+ // Engine settings
+ config.addDefault(ENGINE_INTERVAL, DEFAULT_ENGINE_INTERVAL);
+ config.addDefault(ENGINE_TIMEOUT, DEFAULT_ENGINE_TIMEOUT);
+ config.addDefault(ENGINE_MAX_CLUSTERS, DEFAULT_ENGINE_MAX_CLUSTERS);
+
+ // Module defaults
+ config.addDefault(MODULES_PREFIX + "LavaCastDetector.enabled", true);
+ config.addDefault(MODULES_PREFIX + "LavaCastDetector.minBlockCount", 10);
+ config.addDefault(MODULES_PREFIX + "LavaCastDetector.minDuration", 5000);
+ config.addDefault(MODULES_PREFIX + "LavaCastDetector.suppressionThreshold", 0.1);
+
+ config.options().copyDefaults(true);
+ }
+
+ /**
+ * Reload configuration from file.
+ */
+ public void reload() {
+ config = YamlConfiguration.loadConfiguration(configFile);
+ setDefaults();
+ }
+
+ // Global getters
+ public boolean isGlobalEnabled() {
+ return config.getBoolean(GLOBAL_ENABLE, DEFAULT_GLOBAL_ENABLE);
+ }
+
+ public boolean isAsyncEnabled() {
+ return config.getBoolean(GLOBAL_ASYNC, DEFAULT_GLOBAL_ASYNC);
+ }
+
+ // Permission getters
+ public String getPermissionAdmin() {
+ return config.getString(PERMISSIONS_ADMIN, DEFAULT_PERMISSIONS_ADMIN);
+ }
+
+ public String getPermissionReload() {
+ return config.getString(PERMISSIONS_RELOAD, DEFAULT_PERMISSIONS_RELOAD);
+ }
+
+ public String getPermissionAlerts() {
+ return config.getString(PERMISSIONS_ALERTS, DEFAULT_PERMISSIONS_ALERTS);
+ }
+
+ public String getPermissionBypass() {
+ return config.getString(PERMISSIONS_BYPASS, DEFAULT_PERMISSIONS_BYPASS);
+ }
+
+ // Webhook getters
+ public boolean isWebhookEnabled() {
+ return config.getBoolean(WEBHOOK_ENABLED, DEFAULT_WEBHOOK_ENABLED);
+ }
+
+ public String getWebhookUrl() {
+ return config.getString(WEBHOOK_URL, DEFAULT_WEBHOOK_URL);
+ }
+
+ public double getWebhookThreshold() {
+ return config.getDouble(WEBHOOK_THRESHOLD, DEFAULT_WEBHOOK_THRESHOLD);
+ }
+
+ public int getWebhookRetryAttempts() {
+ return config.getInt(WEBHOOK_RETRY_ATTEMPTS, DEFAULT_WEBHOOK_RETRY_ATTEMPTS);
+ }
+
+ public int getWebhookRetryDelay() {
+ return config.getInt(WEBHOOK_RETRY_DELAY, DEFAULT_WEBHOOK_RETRY_DELAY);
+ }
+
+ public int getWebhookCooldown() {
+ return config.getInt(WEBHOOK_COOLDOWN, DEFAULT_WEBHOOK_COOLDOWN);
+ }
+
+ // Chat getters
+ public boolean isChatEnabled() {
+ return config.getBoolean(CHAT_ENABLED, DEFAULT_CHAT_ENABLED);
+ }
+
+ public double getChatAlertThreshold() {
+ return config.getDouble(CHAT_THRESHOLD, DEFAULT_CHAT_THRESHOLD);
+ }
+
+ public double getIgnoreThreshold() {
+ return 25.0; // Default ignore threshold
+ }
+
+ public String getChatFormat() {
+ return config.getString(CHAT_FORMAT, DEFAULT_CHAT_FORMAT);
+ }
+
+ // Engine getters
+ public int getEngineAnalysisInterval() {
+ return config.getInt(ENGINE_INTERVAL, DEFAULT_ENGINE_INTERVAL);
+ }
+
+ public int getEngineClusterTimeout() {
+ return config.getInt(ENGINE_TIMEOUT, DEFAULT_ENGINE_TIMEOUT);
+ }
+
+ public int getEngineMaxClustersPerWorld() {
+ return config.getInt(ENGINE_MAX_CLUSTERS, DEFAULT_ENGINE_MAX_CLUSTERS);
+ }
+
+ // Module getters
+ public boolean isModuleEnabled(String moduleName) {
+ return config.getBoolean(MODULES_PREFIX + moduleName + ".enabled", true);
+ }
+
+ public int getLavaCastMinBlockCount() {
+ return config.getInt(MODULES_PREFIX + "LavaCastDetector.minBlockCount", 10);
+ }
+
+ public int getLavaCastMinDuration() {
+ return config.getInt(MODULES_PREFIX + "LavaCastDetector.minDuration", 5000);
+ }
+
+ public double getLavaCastSuppressionThreshold() {
+ return config.getDouble(MODULES_PREFIX + "LavaCastDetector.suppressionThreshold", 0.1);
+ }
+
+ /**
+ * Get the raw configuration for advanced access.
+ */
+ public FileConfiguration getConfig() {
+ return config;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/core/ChatAlertManager.java b/src/main/java/party/cybsec/griefdetect/core/ChatAlertManager.java
new file mode 100644
index 0000000..5ddca0b
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/core/ChatAlertManager.java
@@ -0,0 +1,104 @@
+package party.cybsec.griefdetect.core;
+
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.GriefDetectPlugin;
+import party.cybsec.griefdetect.config.PluginConfig;
+
+import java.util.List;
+
+/**
+ * Chat alert manager for sending detection alerts to players with permission.
+ * Permission-gated and configurable with clickable teleport commands.
+ */
+public class ChatAlertManager {
+
+ private final GriefDetectPlugin plugin;
+ private final PluginConfig config;
+ private final PermissionManager permissionManager;
+
+ // Configuration
+ private boolean enabled;
+ private String format;
+
+ public ChatAlertManager(GriefDetectPlugin plugin) {
+ this.plugin = plugin;
+ this.config = plugin.getPluginConfig();
+ this.permissionManager = plugin.getPermissionManager();
+
+ loadConfiguration();
+ }
+
+ /**
+ * Load configuration settings.
+ */
+ private void loadConfiguration() {
+ enabled = config.isChatEnabled();
+ format = config.getChatFormat();
+ }
+
+ /**
+ * Send a chat alert for a detection event.
+ *
+ * @param event The detection event to report
+ */
+ public void sendAlert(DetectionEvent event) {
+ if (!enabled) {
+ return;
+ }
+
+ // Format the alert message
+ String message = formatMessage(event);
+
+ // Send to all players with permission
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ if (permissionManager.canReceiveAlerts(player)) {
+ player.sendMessage(ChatColor.translateAlternateColorCodes('&', message));
+ }
+ }
+
+ plugin.getLogger().info("Chat alert sent: " + event.getDetectionType() + " at " +
+ event.getWorld().getName() + ":" + event.getCenter().getBlockX() + "," +
+ event.getCenter().getBlockY() + "," + event.getCenter().getBlockZ());
+ }
+
+ /**
+ * Format the alert message with event details.
+ */
+ private String formatMessage(DetectionEvent event) {
+ String detectionType = event.getDetectionType();
+ String worldName = event.getWorld().getName();
+ int x = event.getCenter().getBlockX();
+ int y = event.getCenter().getBlockY();
+ int z = event.getCenter().getBlockZ();
+ int confidence = (int) event.getConfidence();
+
+ // Build nearest players list
+ StringBuilder playersList = new StringBuilder();
+ if (!event.getNearestPlayers().isEmpty()) {
+ playersList.append(" [");
+ for (int i = 0; i < Math.min(event.getNearestPlayers().size(), 3); i++) {
+ DetectionEvent.NearestPlayerInfo player = event.getNearestPlayers().get(i);
+ if (i > 0) playersList.append(", ");
+ playersList.append(player.getPlayerName())
+ .append(" (").append((int) player.getDistance()).append("m)");
+ }
+ if (event.getNearestPlayers().size() > 3) {
+ playersList.append(", ...");
+ }
+ playersList.append("]");
+ }
+
+ return String.format(format, detectionType, worldName, x, y, z, confidence) + playersList.toString();
+ }
+
+ /**
+ * Reload configuration.
+ */
+ public void reload() {
+ loadConfiguration();
+ plugin.getLogger().info("Chat alert configuration reloaded");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/core/ConfidenceEngine.java b/src/main/java/party/cybsec/griefdetect/core/ConfidenceEngine.java
new file mode 100644
index 0000000..dac1743
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/core/ConfidenceEngine.java
@@ -0,0 +1,193 @@
+package party.cybsec.griefdetect.core;
+
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.GriefDetectPlugin;
+import party.cybsec.griefdetect.config.PluginConfig;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Confidence engine for calculating grief detection confidence scores.
+ * Implements the confidence system with base factors and player proximity modifiers.
+ */
+public class ConfidenceEngine {
+
+ private final GriefDetectPlugin plugin;
+ private final PluginConfig config;
+
+ // Configuration constants from config
+ private static final double PLAYER_PROXIMITY_RADIUS = 50.0;
+ private static final double NO_PLAYERS_MULTIPLIER = 0.5; // Reduce confidence when no players nearby
+
+ public ConfidenceEngine(GriefDetectPlugin plugin) {
+ this.plugin = plugin;
+ this.config = plugin.getPluginConfig();
+ }
+
+ /**
+ * Calculate confidence score for a detection event.
+ *
+ * @param detectionType Type of detection
+ * @param world The world where detection occurred
+ * @param center Center location of the cluster
+ * @param minX, minY, minZ Minimum bounding box coordinates
+ * @param maxX, maxY, maxZ Maximum bounding box coordinates
+ * @param blockCount Total number of blocks in the cluster
+ * @param duration Duration of the event in milliseconds
+ * @return Confidence score between 0.0 and 100.0
+ */
+ public double calculateConfidence(String detectionType, World world, Location center,
+ int minX, int minY, int minZ,
+ int maxX, int maxY, int maxZ,
+ int blockCount, long duration) {
+
+ double confidence = 0.0;
+
+ // Base factors
+ confidence += calculateVolumeFactor(blockCount);
+ confidence += calculateGrowthSpeedFactor(blockCount, duration);
+ confidence += calculateExpansionRadiusFactor(center, minX, minY, minZ, maxX, maxY, maxZ);
+ confidence += calculateDurationFactor(duration);
+ confidence += calculateVerticalSpanFactor(minY, maxY);
+ confidence += calculateIrregularityFactor(minX, minY, minZ, maxX, maxY, maxZ, blockCount);
+
+ // Apply player proximity modifier
+ confidence = applyPlayerProximityModifier(confidence, world, center);
+
+ // Clamp to 0-100 range
+ return Math.max(0.0, Math.min(100.0, confidence));
+ }
+
+ /**
+ * Calculate volume factor based on total block count.
+ */
+ private double calculateVolumeFactor(int blockCount) {
+ if (blockCount < 100) return 10.0;
+ if (blockCount < 500) return 25.0;
+ if (blockCount < 1000) return 40.0;
+ if (blockCount < 2000) return 60.0;
+ return 80.0;
+ }
+
+ /**
+ * Calculate growth speed factor based on blocks per second.
+ */
+ private double calculateGrowthSpeedFactor(int blockCount, long duration) {
+ if (duration <= 0) return 0.0;
+
+ double blocksPerSecond = (double) blockCount / (duration / 1000.0);
+
+ if (blocksPerSecond < 1) return 5.0;
+ if (blocksPerSecond < 5) return 15.0;
+ if (blocksPerSecond < 10) return 25.0;
+ if (blocksPerSecond < 20) return 40.0;
+ return 60.0;
+ }
+
+ /**
+ * Calculate expansion radius factor.
+ */
+ private double calculateExpansionRadiusFactor(Location center, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
+ double radius = calculateExpansionRadius(center, minX, minY, minZ, maxX, maxY, maxZ);
+
+ if (radius < 10) return 5.0;
+ if (radius < 25) return 15.0;
+ if (radius < 50) return 30.0;
+ if (radius < 100) return 50.0;
+ return 70.0;
+ }
+
+ /**
+ * Calculate duration factor.
+ */
+ private double calculateDurationFactor(long duration) {
+ double seconds = duration / 1000.0;
+
+ if (seconds < 10) return 5.0;
+ if (seconds < 30) return 15.0;
+ if (seconds < 60) return 25.0;
+ if (seconds < 120) return 40.0;
+ return 60.0;
+ }
+
+ /**
+ * Calculate vertical span factor.
+ */
+ private double calculateVerticalSpanFactor(int minY, int maxY) {
+ int verticalSpan = maxY - minY + 1;
+
+ if (verticalSpan < 3) return 5.0;
+ if (verticalSpan < 10) return 15.0;
+ if (verticalSpan < 25) return 30.0;
+ return 50.0;
+ }
+
+ /**
+ * Calculate irregularity factor based on bounding box efficiency.
+ */
+ private double calculateIrregularityFactor(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, int blockCount) {
+ int volume = (maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1);
+ if (volume == 0) return 0.0;
+
+ double efficiency = (double) blockCount / volume;
+
+ // Lower efficiency (more irregular) increases confidence
+ if (efficiency > 0.8) return 5.0; // Very regular shape
+ if (efficiency > 0.5) return 15.0;
+ if (efficiency > 0.3) return 30.0;
+ if (efficiency > 0.1) return 50.0;
+ return 70.0; // Very irregular
+ }
+
+ /**
+ * Apply player proximity modifier to confidence.
+ */
+ private double applyPlayerProximityModifier(double baseConfidence, World world, Location center) {
+ List nearbyPlayers = world.getPlayers().stream()
+ .filter(player -> player.getLocation().distance(center) <= PLAYER_PROXIMITY_RADIUS)
+ .collect(Collectors.toList());
+
+ if (nearbyPlayers.isEmpty()) {
+ // No players nearby - reduce confidence
+ return baseConfidence * NO_PLAYERS_MULTIPLIER;
+ }
+
+ // Players nearby - full confidence
+ return baseConfidence;
+ }
+
+ /**
+ * Calculate expansion radius helper method.
+ */
+ private double calculateExpansionRadius(Location center, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
+ double dx = Math.max(Math.abs(center.getBlockX() - minX), Math.abs(center.getBlockX() - maxX));
+ double dy = Math.max(Math.abs(center.getBlockY() - minY), Math.abs(center.getBlockY() - maxY));
+ double dz = Math.max(Math.abs(center.getBlockZ() - minZ), Math.abs(center.getBlockZ() - maxZ));
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
+ }
+
+ /**
+ * Check if confidence meets ignore threshold.
+ */
+ public boolean shouldIgnore(double confidence) {
+ return confidence < config.getIgnoreThreshold();
+ }
+
+ /**
+ * Check if confidence meets chat alert threshold.
+ */
+ public boolean shouldAlertChat(double confidence) {
+ return confidence >= config.getChatAlertThreshold();
+ }
+
+ /**
+ * Check if confidence meets webhook threshold.
+ */
+ public boolean shouldWebhook(double confidence) {
+ return confidence >= config.getWebhookThreshold();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/core/DetectionEngine.java b/src/main/java/party/cybsec/griefdetect/core/DetectionEngine.java
new file mode 100644
index 0000000..5602475
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/core/DetectionEngine.java
@@ -0,0 +1,379 @@
+package party.cybsec.griefdetect.core;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockFormEvent;
+import org.bukkit.event.block.BlockFromToEvent;
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.GriefDetectPlugin;
+import party.cybsec.griefdetect.config.PluginConfig;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Async detection engine that processes events and evaluates clusters.
+ * Zero per-tick work, async-first processing with event capture on main thread.
+ */
+public class DetectionEngine implements Listener {
+
+ private final GriefDetectPlugin plugin;
+ private final PluginConfig config;
+ private final ScheduledExecutorService asyncExecutor;
+
+ // Cluster tracking
+ private final Map activeClusters;
+ private final Map playerPositions;
+
+ // Configuration
+ private final int analysisInterval;
+ private final int clusterTimeout;
+ private final int maxClustersPerWorld;
+
+ // Throttling
+ private final Map playerPositionUpdateCooldown;
+ private static final long POSITION_UPDATE_COOLDOWN_MS = 5000;
+
+ public DetectionEngine(GriefDetectPlugin plugin) {
+ this.plugin = plugin;
+ this.config = plugin.getPluginConfig();
+ this.asyncExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "GriefDetect-Async");
+ t.setDaemon(true);
+ return t;
+ });
+ this.activeClusters = new ConcurrentHashMap<>();
+ this.playerPositions = new ConcurrentHashMap<>();
+ this.playerPositionUpdateCooldown = new ConcurrentHashMap<>();
+
+ // Load configuration
+ this.analysisInterval = config.getEngineAnalysisInterval();
+ this.clusterTimeout = config.getEngineClusterTimeout();
+ this.maxClustersPerWorld = config.getEngineMaxClustersPerWorld();
+
+ // Register events
+ Bukkit.getPluginManager().registerEvents(this, plugin);
+ }
+
+ /**
+ * Start the detection engine.
+ */
+ public void start() {
+ // Schedule async analysis
+ asyncExecutor.scheduleAtFixedRate(this::performAsyncAnalysis, analysisInterval, analysisInterval, TimeUnit.MILLISECONDS);
+
+ // Schedule cleanup
+ asyncExecutor.scheduleAtFixedRate(this::cleanupInactiveClusters, 60, 60, TimeUnit.SECONDS);
+
+ plugin.getLogger().info("Detection engine started with " + analysisInterval + "ms analysis interval");
+ }
+
+ /**
+ * Shutdown the detection engine.
+ */
+ public void shutdown() {
+ asyncExecutor.shutdown();
+ try {
+ if (!asyncExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+ asyncExecutor.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ asyncExecutor.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Event capture methods - minimal processing on main thread.
+ */
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBlockFromTo(BlockFromToEvent event) {
+ // Capture lava-water interactions
+ if (event.getBlock().getType().name().contains("LAVA") &&
+ (event.getToBlock().getType().name().contains("WATER") ||
+ event.getToBlock().getType().name().contains("ICE"))) {
+ processBlockEvent(event.getBlock(), event.getToBlock().getLocation());
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBlockForm(BlockFormEvent event) {
+ // Capture cobblestone/stone/obsidian formation
+ String blockType = event.getNewState().getType().name();
+ if (blockType.contains("COBBLESTONE") || blockType.contains("STONE") || blockType.contains("OBSIDIAN")) {
+ processBlockEvent(event.getBlock(), event.getBlock().getLocation());
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBlockBreak(BlockBreakEvent event) {
+ // Track mining activity for suppression ratio
+ processMiningEvent(event.getBlock());
+
+ // Update player position
+ schedulePlayerPositionUpdate(event.getPlayer());
+ }
+
+ /**
+ * Process block events and update clusters.
+ */
+ private void processBlockEvent(Block block, Location location) {
+ String clusterKey = getClusterKey(location);
+ Cluster cluster = activeClusters.computeIfAbsent(clusterKey, k -> new Cluster(location));
+
+ cluster.addBlock(location);
+ cluster.setLastActivity(System.currentTimeMillis());
+ }
+
+ /**
+ * Process mining events for suppression ratio calculation.
+ */
+ private void processMiningEvent(Block block) {
+ String clusterKey = getClusterKey(block.getLocation());
+ Cluster cluster = activeClusters.get(clusterKey);
+
+ if (cluster != null) {
+ cluster.incrementMiningActivity();
+ cluster.setLastActivity(System.currentTimeMillis());
+ }
+ }
+
+ /**
+ * Schedule player position update with throttling.
+ */
+ private void schedulePlayerPositionUpdate(Player player) {
+ UUID playerId = player.getUniqueId();
+ long now = System.currentTimeMillis();
+
+ if (now - playerPositionUpdateCooldown.getOrDefault(playerId, 0L) > POSITION_UPDATE_COOLDOWN_MS) {
+ playerPositionUpdateCooldown.put(playerId, now);
+
+ // Update position asynchronously to avoid main thread work
+ Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
+ playerPositions.put(playerId, new PlayerPosition(player.getLocation(), player.getGameMode().name().equals("SURVIVAL")));
+ });
+ }
+ }
+
+ /**
+ * Perform async analysis of active clusters.
+ */
+ private void performAsyncAnalysis() {
+ try {
+ List clustersToEvaluate = new ArrayList<>(activeClusters.values());
+
+ for (Cluster cluster : clustersToEvaluate) {
+ if (cluster.shouldEvaluate()) {
+ evaluateCluster(cluster);
+ }
+ }
+ } catch (Exception e) {
+ plugin.getLogger().severe("Error in async analysis: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Evaluate a cluster for grief detection.
+ */
+ private void evaluateCluster(Cluster cluster) {
+ // Calculate confidence
+ double confidence = plugin.getConfidenceEngine().calculateConfidence(
+ "LavaCast",
+ cluster.getWorld(),
+ cluster.getCenter(),
+ cluster.getMinX(), cluster.getMinY(), cluster.getMinZ(),
+ cluster.getMaxX(), cluster.getMaxY(), cluster.getMaxZ(),
+ cluster.getBlockCount(),
+ cluster.getDuration()
+ );
+
+ // Check thresholds
+ if (plugin.getConfidenceEngine().shouldIgnore(confidence)) {
+ return;
+ }
+
+ // Get nearest players
+ List nearestPlayers = getNearestPlayers(cluster.getCenter());
+
+ // Create detection event
+ DetectionEvent event = new DetectionEvent(
+ "LavaCast",
+ cluster.getWorld(),
+ cluster.getCenter(),
+ cluster.getMinX(), cluster.getMinY(), cluster.getMinZ(),
+ cluster.getMaxX(), cluster.getMaxY(), cluster.getMaxZ(),
+ cluster.getBlockCount(),
+ confidence,
+ nearestPlayers
+ );
+
+ // Handle reporting
+ if (plugin.getConfidenceEngine().shouldAlertChat(confidence)) {
+ // Create ChatAlertManager instance for sending alerts
+ plugin.getChatAlertManager().sendAlert(event);
+ }
+
+ if (plugin.getConfidenceEngine().shouldWebhook(confidence)) {
+ plugin.getWebhookManager().sendWebhook(event);
+ }
+
+ // Reset cluster for next evaluation
+ cluster.resetEvaluation();
+ }
+
+ /**
+ * Get nearest players for reporting.
+ */
+ private List getNearestPlayers(Location center) {
+ List players = new ArrayList<>();
+ World world = center.getWorld();
+
+ for (Map.Entry entry : playerPositions.entrySet()) {
+ PlayerPosition pos = entry.getValue();
+ if (pos.getWorld().equals(world)) {
+ double distance = pos.getLocation().distance(center);
+ if (distance <= 50.0) { // Within reporting radius
+ players.add(new DetectionEvent.NearestPlayerInfo(
+ entry.getKey(),
+ Bukkit.getPlayer(entry.getKey()).getName(),
+ distance,
+ pos.isSurvival()
+ ));
+ }
+ }
+ }
+
+ // Sort by distance
+ players.sort((a, b) -> Double.compare(a.getDistance(), b.getDistance()));
+
+ return players;
+ }
+
+ /**
+ * Cleanup inactive clusters.
+ */
+ private void cleanupInactiveClusters() {
+ long now = System.currentTimeMillis();
+ Iterator> iterator = activeClusters.entrySet().iterator();
+
+ while (iterator.hasNext()) {
+ Map.Entry entry = iterator.next();
+ Cluster cluster = entry.getValue();
+
+ if (now - cluster.getLastActivity() > clusterTimeout) {
+ iterator.remove();
+ }
+ }
+ }
+
+ /**
+ * Get cluster key for spatial hashing.
+ */
+ private String getClusterKey(Location location) {
+ // Use chunk-based spatial hashing
+ int chunkX = location.getBlockX() >> 4;
+ int chunkZ = location.getBlockZ() >> 4;
+ return location.getWorld().getName() + ":" + chunkX + ":" + chunkZ;
+ }
+
+ /**
+ * Cluster tracking class.
+ */
+ private static class Cluster {
+ private final Location center;
+ private final World world;
+ private int minX, minY, minZ;
+ private int maxX, maxY, maxZ;
+ private int blockCount;
+ private int miningActivity;
+ private long firstActivity;
+ private long lastActivity;
+
+ public Cluster(Location location) {
+ this.center = location;
+ this.world = location.getWorld();
+ this.minX = this.maxX = location.getBlockX();
+ this.minY = this.maxY = location.getBlockY();
+ this.minZ = this.maxZ = location.getBlockZ();
+ this.blockCount = 0;
+ this.miningActivity = 0;
+ this.firstActivity = System.currentTimeMillis();
+ this.lastActivity = System.currentTimeMillis();
+ }
+
+ public void addBlock(Location location) {
+ blockCount++;
+ updateBounds(location);
+ }
+
+ public void incrementMiningActivity() {
+ miningActivity++;
+ }
+
+ public void setLastActivity(long time) {
+ lastActivity = time;
+ }
+
+ public void resetEvaluation() {
+ firstActivity = System.currentTimeMillis();
+ miningActivity = 0;
+ }
+
+ public boolean shouldEvaluate() {
+ // Evaluate if enough blocks or enough time has passed
+ return blockCount >= 10 || (System.currentTimeMillis() - firstActivity) >= 5000;
+ }
+
+ private void updateBounds(Location location) {
+ minX = Math.min(minX, location.getBlockX());
+ minY = Math.min(minY, location.getBlockY());
+ minZ = Math.min(minZ, location.getBlockZ());
+ maxX = Math.max(maxX, location.getBlockX());
+ maxY = Math.max(maxY, location.getBlockY());
+ maxZ = Math.max(maxZ, location.getBlockZ());
+ }
+
+ // Getters
+ public Location getCenter() { return center; }
+ public World getWorld() { return world; }
+ public int getMinX() { return minX; }
+ public int getMinY() { return minY; }
+ public int getMinZ() { return minZ; }
+ public int getMaxX() { return maxX; }
+ public int getMaxY() { return maxY; }
+ public int getMaxZ() { return maxZ; }
+ public int getBlockCount() { return blockCount; }
+ public int getMiningActivity() { return miningActivity; }
+ public long getFirstActivity() { return firstActivity; }
+ public long getLastActivity() { return lastActivity; }
+ public long getDuration() { return lastActivity - firstActivity; }
+ }
+
+ /**
+ * Player position tracking class.
+ */
+ private static class PlayerPosition {
+ private final Location location;
+ private final boolean isSurvival;
+
+ public PlayerPosition(Location location, boolean isSurvival) {
+ this.location = location;
+ this.isSurvival = isSurvival;
+ }
+
+ public Location getLocation() { return location; }
+ public boolean isSurvival() { return isSurvival; }
+ public World getWorld() { return location.getWorld(); }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/core/DetectionEvent.java b/src/main/java/party/cybsec/griefdetect/core/DetectionEvent.java
new file mode 100644
index 0000000..d729052
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/core/DetectionEvent.java
@@ -0,0 +1,106 @@
+package party.cybsec.griefdetect.core;
+
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.entity.Player;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Standardized detection event emitted by modules.
+ * Contains all necessary information for confidence calculation and reporting.
+ */
+public class DetectionEvent {
+
+ private final String detectionType;
+ private final World world;
+ private final Location center;
+ private final int minX, minY, minZ;
+ private final int maxX, maxY, maxZ;
+ private final int blockCount;
+ private final double confidence;
+ private final long timestamp;
+ private final List nearestPlayers;
+
+ public DetectionEvent(String detectionType, World world, Location center,
+ int minX, int minY, int minZ,
+ int maxX, int maxY, int maxZ,
+ int blockCount, double confidence,
+ List nearestPlayers) {
+ this.detectionType = detectionType;
+ this.world = world;
+ this.center = center;
+ this.minX = minX;
+ this.minY = minY;
+ this.minZ = minZ;
+ this.maxX = maxX;
+ this.maxY = maxY;
+ this.maxZ = maxZ;
+ this.blockCount = blockCount;
+ this.confidence = confidence;
+ this.timestamp = System.currentTimeMillis();
+ this.nearestPlayers = nearestPlayers;
+ }
+
+ // Getters
+ public String getDetectionType() { return detectionType; }
+ public World getWorld() { return world; }
+ public Location getCenter() { return center; }
+ public int getMinX() { return minX; }
+ public int getMinY() { return minY; }
+ public int getMinZ() { return minZ; }
+ public int getMaxX() { return maxX; }
+ public int getMaxY() { return maxY; }
+ public int getMaxZ() { return maxZ; }
+ public int getBlockCount() { return blockCount; }
+ public double getConfidence() { return confidence; }
+ public long getTimestamp() { return timestamp; }
+ public List getNearestPlayers() { return nearestPlayers; }
+
+ /**
+ * Get the bounding box size in blocks.
+ */
+ public int getBoundingBoxSize() {
+ return (maxX - minX + 1) * (maxY - minY + 1) * (maxZ - minZ + 1);
+ }
+
+ /**
+ * Get the expansion radius from center to farthest point.
+ */
+ public double getExpansionRadius() {
+ double dx = Math.max(Math.abs(center.getBlockX() - minX), Math.abs(center.getBlockX() - maxX));
+ double dy = Math.max(Math.abs(center.getBlockY() - minY), Math.abs(center.getBlockY() - maxY));
+ double dz = Math.max(Math.abs(center.getBlockZ() - minZ), Math.abs(center.getBlockZ() - maxZ));
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
+ }
+
+ /**
+ * Generate a teleport command for the center location.
+ */
+ public String getTeleportCommand() {
+ return String.format("/tp %d %d %d", center.getBlockX(), center.getBlockY(), center.getBlockZ());
+ }
+
+ /**
+ * Information about nearest players for reporting.
+ */
+ public static class NearestPlayerInfo {
+ private final UUID playerId;
+ private final String playerName;
+ private final double distance;
+ private final boolean isSurvival;
+
+ public NearestPlayerInfo(UUID playerId, String playerName, double distance, boolean isSurvival) {
+ this.playerId = playerId;
+ this.playerName = playerName;
+ this.distance = distance;
+ this.isSurvival = isSurvival;
+ }
+
+ public UUID getPlayerId() { return playerId; }
+ public String getPlayerName() { return playerName; }
+ public double getDistance() { return distance; }
+ public boolean isSurvival() { return isSurvival; }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/core/ModuleManager.java b/src/main/java/party/cybsec/griefdetect/core/ModuleManager.java
new file mode 100644
index 0000000..9608386
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/core/ModuleManager.java
@@ -0,0 +1,118 @@
+package party.cybsec.griefdetect.core;
+
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.GriefDetectPlugin;
+import party.cybsec.griefdetect.config.PluginConfig;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Module manager for registering and managing detection modules.
+ * Modules are enabled/disabled via config and register listeners only if enabled.
+ */
+public class ModuleManager {
+
+ private final GriefDetectPlugin plugin;
+ private final PluginConfig config;
+ private final Map modules;
+
+ public ModuleManager(GriefDetectPlugin plugin) {
+ this.plugin = plugin;
+ this.config = plugin.getPluginConfig();
+ this.modules = new ConcurrentHashMap<>();
+ }
+
+ /**
+ * Register a detection module.
+ *
+ * @param module The module to register
+ */
+ public void registerModule(DetectionModule module) {
+ String moduleName = module.getModuleName();
+
+ if (config.isModuleEnabled(moduleName)) {
+ modules.put(moduleName, module);
+ module.initialize();
+ plugin.getLogger().info("Module registered and enabled: " + moduleName);
+ } else {
+ plugin.getLogger().info("Module registered but disabled: " + moduleName);
+ }
+ }
+
+ /**
+ * Get a registered module by name.
+ *
+ * @param moduleName The name of the module
+ * @return The module if found, null otherwise
+ */
+ public DetectionModule getModule(String moduleName) {
+ return modules.get(moduleName);
+ }
+
+ /**
+ * Get all registered modules.
+ *
+ * @return List of all registered modules
+ */
+ public List getAllModules() {
+ return new ArrayList<>(modules.values());
+ }
+
+ /**
+ * Reload all modules.
+ * Called on plugin reload.
+ */
+ public void reloadModules() {
+ for (DetectionModule module : modules.values()) {
+ module.reload();
+ }
+ plugin.getLogger().info("All modules reloaded");
+ }
+
+ /**
+ * Shutdown all modules.
+ * Called on plugin disable.
+ */
+ public void shutdownModules() {
+ for (DetectionModule module : modules.values()) {
+ module.shutdown();
+ }
+ modules.clear();
+ plugin.getLogger().info("All modules shutdown");
+ }
+
+ /**
+ * Interface for detection modules.
+ */
+ public interface DetectionModule {
+ /**
+ * Get the module name.
+ */
+ String getModuleName();
+
+ /**
+ * Initialize the module.
+ * Register listeners and set up configuration.
+ */
+ void initialize();
+
+ /**
+ * Reload the module configuration.
+ */
+ void reload();
+
+ /**
+ * Shutdown the module.
+ * Unregister listeners and cleanup resources.
+ */
+ void shutdown();
+
+ /**
+ * Get the module's configuration subtree.
+ */
+ Map getModuleConfig();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/core/PermissionManager.java b/src/main/java/party/cybsec/griefdetect/core/PermissionManager.java
new file mode 100644
index 0000000..e2c11c6
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/core/PermissionManager.java
@@ -0,0 +1,116 @@
+package party.cybsec.griefdetect.core;
+
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.GriefDetectPlugin;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Permission system for GriefDetect with caching for performance.
+ * Manages plugin-specific permissions and bypass capabilities.
+ */
+public class PermissionManager {
+
+ private final GriefDetectPlugin plugin;
+
+ // Permission node constants
+ public static final String ADMIN = "griefdetect.admin";
+ public static final String RELOAD = "griefdetect.reload";
+ public static final String ALERTS = "griefdetect.alerts";
+ public static final String BYPASS = "griefdetect.bypass";
+
+ // Permission cache for performance
+ private final Map> permissionCache;
+
+ public PermissionManager(GriefDetectPlugin plugin) {
+ this.plugin = plugin;
+ this.permissionCache = new ConcurrentHashMap<>();
+ }
+
+ /**
+ * Check if a player has a specific permission.
+ * Uses caching for performance.
+ *
+ * @param player The player to check
+ * @param permission The permission node to check
+ * @return true if player has permission, false otherwise
+ */
+ public boolean hasPermission(Player player, String permission) {
+ UUID playerId = player.getUniqueId();
+
+ // Check cache first
+ if (permissionCache.containsKey(playerId) && permissionCache.get(playerId).containsKey(permission)) {
+ return permissionCache.get(playerId).get(permission);
+ }
+
+ // Check actual permission
+ boolean hasPermission = player.hasPermission(permission);
+
+ // Update cache
+ permissionCache.computeIfAbsent(playerId, k -> new HashMap<>()).put(permission, hasPermission);
+
+ return hasPermission;
+ }
+
+ /**
+ * Check if a player can bypass grief detection.
+ *
+ * @param player The player to check
+ * @return true if player bypasses detection, false otherwise
+ */
+ public boolean canBypass(Player player) {
+ return hasPermission(player, BYPASS);
+ }
+
+ /**
+ * Check if a player can receive alerts.
+ *
+ * @param player The player to check
+ * @return true if player can receive alerts, false otherwise
+ */
+ public boolean canReceiveAlerts(Player player) {
+ return hasPermission(player, ALERTS);
+ }
+
+ /**
+ * Check if a player can reload the plugin.
+ *
+ * @param player The player to check
+ * @return true if player can reload, false otherwise
+ */
+ public boolean canReload(Player player) {
+ return hasPermission(player, RELOAD);
+ }
+
+ /**
+ * Check if a player is an admin.
+ *
+ * @param player The player to check
+ * @return true if player is admin, false otherwise
+ */
+ public boolean isAdmin(Player player) {
+ return hasPermission(player, ADMIN);
+ }
+
+ /**
+ * Clear permission cache for a specific player.
+ * Useful when permissions change.
+ *
+ * @param player The player whose cache to clear
+ */
+ public void clearCache(Player player) {
+ permissionCache.remove(player.getUniqueId());
+ }
+
+ /**
+ * Clear all permission cache.
+ * Useful on plugin reload.
+ */
+ public void clearAllCache() {
+ permissionCache.clear();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/core/WebhookManager.java b/src/main/java/party/cybsec/griefdetect/core/WebhookManager.java
new file mode 100644
index 0000000..cff303a
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/core/WebhookManager.java
@@ -0,0 +1,240 @@
+package party.cybsec.griefdetect.core;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.GriefDetectPlugin;
+import party.cybsec.griefdetect.config.PluginConfig;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Webhook manager for sending detection alerts to Discord.
+ * Async-only, rate-limited with retry and area-based cooldown.
+ */
+public class WebhookManager {
+
+ private final GriefDetectPlugin plugin;
+ private final PluginConfig config;
+ private final OkHttpClient httpClient;
+ private final ObjectMapper objectMapper;
+
+ // Rate limiting and cooldown
+ private final ConcurrentHashMap areaCooldowns;
+ private final ConcurrentHashMap retryCounts;
+ private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+
+ // Configuration
+ private boolean enabled;
+ private String webhookUrl;
+ private int retryAttempts;
+ private long retryDelay;
+ private long areaCooldown;
+
+ public WebhookManager(GriefDetectPlugin plugin) {
+ this.plugin = plugin;
+ this.config = plugin.getPluginConfig();
+ this.httpClient = new OkHttpClient.Builder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .build();
+ this.objectMapper = new ObjectMapper();
+ this.areaCooldowns = new ConcurrentHashMap<>();
+ this.retryCounts = new ConcurrentHashMap<>();
+
+ loadConfiguration();
+ }
+
+ /**
+ * Load configuration settings.
+ */
+ private void loadConfiguration() {
+ enabled = config.isWebhookEnabled();
+ webhookUrl = config.getWebhookUrl();
+ retryAttempts = config.getWebhookRetryAttempts();
+ retryDelay = config.getWebhookRetryDelay();
+ areaCooldown = config.getWebhookCooldown() * 1000L; // Convert to milliseconds
+ }
+
+ /**
+ * Send a webhook notification for a detection event.
+ *
+ * @param event The detection event to report
+ */
+ public void sendWebhook(DetectionEvent event) {
+ if (!enabled || webhookUrl.isEmpty()) {
+ return;
+ }
+
+ // Check area cooldown
+ String areaKey = getAreaKey(event.getWorld(), event.getCenter());
+ long now = System.currentTimeMillis();
+
+ if (areaCooldowns.containsKey(areaKey) && (now - areaCooldowns.get(areaKey)) < areaCooldown) {
+ return; // Still in cooldown
+ }
+
+ // Create webhook payload
+ ObjectNode payload = createWebhookPayload(event);
+
+ try {
+ String jsonPayload = objectMapper.writeValueAsString(payload);
+ RequestBody body = RequestBody.create(JSON, jsonPayload);
+ Request request = new Request.Builder()
+ .url(webhookUrl)
+ .post(body)
+ .addHeader("Content-Type", "application/json")
+ .build();
+
+ httpClient.newCall(request).enqueue(new WebhookCallback(areaKey, event.getDetectionType()));
+ areaCooldowns.put(areaKey, now);
+
+ } catch (Exception e) {
+ plugin.getLogger().severe("Failed to send webhook: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Create the Discord webhook payload.
+ */
+ private ObjectNode createWebhookPayload(DetectionEvent event) {
+ ObjectNode embed = objectMapper.createObjectNode();
+
+ // Embed title and color
+ embed.put("title", "🚨 Grief Detection Alert");
+ embed.put("description", String.format(
+ "**Type:** %s\n" +
+ "**Confidence:** %.1f%%\n" +
+ "**Location:** %s:%d,%d,%d\n" +
+ "**Blocks:** %d\n" +
+ "**Bounding Box:** %dx%dx%d",
+ event.getDetectionType(),
+ event.getConfidence(),
+ event.getWorld().getName(),
+ event.getCenter().getBlockX(),
+ event.getCenter().getBlockY(),
+ event.getCenter().getBlockZ(),
+ event.getBlockCount(),
+ event.getMaxX() - event.getMinX() + 1,
+ event.getMaxY() - event.getMinY() + 1,
+ event.getMaxZ() - event.getMinZ() + 1
+ ));
+
+ // Embed color based on confidence
+ embed.put("color", getConfidenceColor(event.getConfidence()));
+
+ // Fields for additional information
+ ObjectNode fields = objectMapper.createObjectNode();
+
+ // Nearest players field
+ if (!event.getNearestPlayers().isEmpty()) {
+ StringBuilder playersText = new StringBuilder();
+ for (DetectionEvent.NearestPlayerInfo player : event.getNearestPlayers()) {
+ String gamemode = player.isSurvival() ? "Survival" : "Creative";
+ playersText.append(String.format("- %s (%.1fm, %s)\n",
+ player.getPlayerName(), player.getDistance(), gamemode));
+ }
+ embed.put("fields", playersText.toString());
+ }
+
+ // Timestamp
+ embed.put("timestamp", Instant.now().toString());
+
+ // Footer with server info
+ ObjectNode footer = objectMapper.createObjectNode();
+ footer.put("text", "GriefDetect v" + plugin.getDescription().getVersion());
+ embed.set("footer", footer);
+
+ // Create final payload
+ ObjectNode payload = objectMapper.createObjectNode();
+ payload.set("embeds", objectMapper.createArrayNode().add(embed));
+
+ return payload;
+ }
+
+ /**
+ * Get color based on confidence level.
+ */
+ private int getConfidenceColor(double confidence) {
+ if (confidence >= 90) return 0xFF0000; // Red
+ if (confidence >= 75) return 0xFFA500; // Orange
+ if (confidence >= 50) return 0xFFFF00; // Yellow
+ return 0x00FF00; // Green
+ }
+
+ /**
+ * Generate area key for cooldown tracking.
+ */
+ private String getAreaKey(World world, Location location) {
+ // Use chunk coordinates for area tracking
+ int chunkX = location.getBlockX() >> 4;
+ int chunkZ = location.getBlockZ() >> 4;
+ return world.getName() + ":" + chunkX + ":" + chunkZ;
+ }
+
+ /**
+ * Webhook callback handler for async responses.
+ */
+ private class WebhookCallback implements Callback {
+ private final String areaKey;
+ private final String detectionType;
+
+ public WebhookCallback(String areaKey, String detectionType) {
+ this.areaKey = areaKey;
+ this.detectionType = detectionType;
+ }
+
+ @Override
+ public void onFailure(Call call, IOException e) {
+ plugin.getLogger().warning("Webhook request failed: " + e.getMessage());
+
+ // Retry logic
+ String retryKey = areaKey + ":" + detectionType;
+ int attempts = retryCounts.getOrDefault(retryKey, 0);
+
+ if (attempts < retryAttempts) {
+ retryCounts.put(retryKey, attempts + 1);
+
+ Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, () -> {
+ // Resend the webhook (simplified retry)
+ plugin.getLogger().info("Retrying webhook for " + detectionType);
+ }, retryDelay / 50); // Convert milliseconds to ticks
+ }
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ if (response.isSuccessful()) {
+ plugin.getLogger().info("Webhook sent successfully for " + detectionType);
+ } else {
+ plugin.getLogger().warning("Webhook failed with code: " + response.code());
+ }
+ response.close();
+ }
+ }
+
+ /**
+ * Reload configuration.
+ */
+ public void reload() {
+ loadConfiguration();
+ plugin.getLogger().info("Webhook configuration reloaded");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/party/cybsec/griefdetect/modules/LavaCastDetector.java b/src/main/java/party/cybsec/griefdetect/modules/LavaCastDetector.java
new file mode 100644
index 0000000..8d97593
--- /dev/null
+++ b/src/main/java/party/cybsec/griefdetect/modules/LavaCastDetector.java
@@ -0,0 +1,189 @@
+package party.cybsec.griefdetect.modules;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockFormEvent;
+import org.bukkit.event.block.BlockFromToEvent;
+import org.bukkit.plugin.java.JavaPlugin;
+import party.cybsec.griefdetect.GriefDetectPlugin;
+import party.cybsec.griefdetect.core.DetectionEngine;
+import party.cybsec.griefdetect.core.DetectionEvent;
+import party.cybsec.griefdetect.core.ModuleManager;
+import party.cybsec.griefdetect.config.PluginConfig;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * LavaCastDetector module for detecting grief patterns involving lava-water interactions.
+ * Tracks cobblestone/stone/obsidian formation with spatial and temporal analysis.
+ */
+public class LavaCastDetector implements ModuleManager.DetectionModule, Listener {
+
+ private final GriefDetectPlugin plugin;
+ private final PluginConfig config;
+ private final DetectionEngine detectionEngine;
+
+ // Module configuration
+ private boolean enabled;
+ private int minBlockCount;
+ private int minDuration;
+ private double suppressionThreshold;
+
+ public LavaCastDetector(GriefDetectPlugin plugin) {
+ this.plugin = plugin;
+ this.config = plugin.getPluginConfig();
+ this.detectionEngine = plugin.getDetectionEngine();
+
+ loadConfiguration();
+ }
+
+ @Override
+ public String getModuleName() {
+ return "LavaCastDetector";
+ }
+
+ @Override
+ public void initialize() {
+ if (enabled) {
+ Bukkit.getPluginManager().registerEvents(this, plugin);
+ plugin.getLogger().info("LavaCastDetector initialized");
+ }
+ }
+
+ @Override
+ public void reload() {
+ loadConfiguration();
+ plugin.getLogger().info("LavaCastDetector configuration reloaded");
+ }
+
+ @Override
+ public void shutdown() {
+ // No specific cleanup needed, events will be unregistered automatically
+ plugin.getLogger().info("LavaCastDetector shutdown");
+ }
+
+ @Override
+ public Map getModuleConfig() {
+ Map config = new HashMap<>();
+ config.put("enabled", enabled);
+ config.put("minBlockCount", minBlockCount);
+ config.put("minDuration", minDuration);
+ config.put("suppressionThreshold", suppressionThreshold);
+ return config;
+ }
+
+ /**
+ * Load module configuration from plugin config.
+ */
+ private void loadConfiguration() {
+ enabled = config.isModuleEnabled("LavaCastDetector");
+ minBlockCount = config.getLavaCastMinBlockCount();
+ minDuration = config.getLavaCastMinDuration();
+ suppressionThreshold = config.getLavaCastSuppressionThreshold();
+ }
+
+ /**
+ * Event handlers for lava-water interactions and block formation.
+ */
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBlockFromTo(BlockFromToEvent event) {
+ if (!enabled) return;
+
+ // Capture lava-water interactions
+ if (isLavaSource(event.getBlock()) && isWaterTarget(event.getToBlock())) {
+ // This event is already handled by DetectionEngine
+ // We just need to ensure our module is active
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBlockForm(BlockFormEvent event) {
+ if (!enabled) return;
+
+ // Capture cobblestone/stone/obsidian formation
+ String blockType = event.getNewState().getType().name();
+ if (isRelevantFormation(blockType)) {
+ // This event is already handled by DetectionEngine
+ // We just need to ensure our module is active
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBlockBreak(BlockBreakEvent event) {
+ if (!enabled) return;
+
+ // Track mining activity for suppression ratio
+ Player player = event.getPlayer();
+ Block block = event.getBlock();
+
+ // Only track relevant block types for suppression calculation
+ String blockType = block.getType().name();
+ if (isRelevantFormation(blockType)) {
+ // This event is already handled by DetectionEngine
+ // We just need to ensure our module is active
+ }
+ }
+
+ /**
+ * Helper methods for block type checking.
+ */
+
+ private boolean isLavaSource(Block block) {
+ String type = block.getType().name();
+ return type.contains("LAVA") && (type.endsWith("_SOURCE") || type.equals("LAVA"));
+ }
+
+ private boolean isWaterTarget(Block block) {
+ String type = block.getType().name();
+ return type.contains("WATER") || type.contains("ICE");
+ }
+
+ private boolean isRelevantFormation(String blockType) {
+ return blockType.contains("COBBLESTONE") ||
+ blockType.contains("STONE") ||
+ blockType.contains("OBSIDIAN");
+ }
+
+ /**
+ * Additional helper methods for cobble generator suppression detection.
+ */
+
+ /**
+ * Check if a cluster shows signs of legitimate cobblestone generator.
+ * This would be called during cluster evaluation.
+ */
+ public boolean isLikelyCobbleGenerator(int blockCount, int minX, int minY, int minZ,
+ int maxX, int maxY, int maxZ, int miningActivity) {
+ // Small footprint check
+ int width = maxX - minX + 1;
+ int height = maxY - minY + 1;
+ int depth = maxZ - minZ + 1;
+
+ if (width > 10 || height > 10 || depth > 10) {
+ return false; // Too large for generator
+ }
+
+ // Stable block count check (minimal mining)
+ if (miningActivity > blockCount * 0.1) {
+ return false; // Too much mining for generator
+ }
+
+ // Linear/static geometry check
+ int volume = width * height * depth;
+ double efficiency = (double) blockCount / volume;
+
+ if (efficiency > 0.8) {
+ return true; // Very regular shape, likely generator
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
new file mode 100644
index 0000000..21fe120
--- /dev/null
+++ b/src/main/resources/config.yml
@@ -0,0 +1,76 @@
+# GriefDetect Configuration
+# Modular, async-first grief pattern detection with confidence scoring and Discord webhook reporting.
+
+global:
+ # Enable/disable the entire plugin
+ enable: true
+
+ # Use async processing (recommended)
+ async: true
+
+permissions:
+ # Permission node for full configuration access
+ admin: "griefdetect.admin"
+
+ # Permission node for reload command
+ reload: "griefdetect.reload"
+
+ # Permission node for receiving chat alerts
+ alerts: "griefdetect.alerts"
+
+ # Permission node for bypassing detection
+ bypass: "griefdetect.bypass"
+
+webhook:
+ # Enable Discord webhook notifications
+ enabled: false
+
+ # Discord webhook URL
+ url: ""
+
+ # Minimum confidence threshold for webhook (0-100)
+ threshold: 75.0
+
+ # Number of retry attempts on failure
+ retryAttempts: 3
+
+ # Delay between retry attempts (milliseconds)
+ retryDelay: 5000
+
+ # Area-based cooldown to prevent spam (seconds)
+ areaCooldown: 300
+
+chat:
+ # Enable in-game chat alerts
+ enabled: true
+
+ # Minimum confidence threshold for chat alerts (0-100)
+ threshold: 50.0
+
+ # Chat message format (supports & colors)
+ # Variables: %s (detection type), %s (world), %d (x), %d (y), %d (z), %d (confidence)
+ format: "&c[GriefDetect] &7%s &fat &6%s:%d,%d,%d &7(%d%%)"
+
+engine:
+ # Analysis interval in milliseconds (default: 5 seconds)
+ analysisInterval: 5000
+
+ # Cluster timeout in milliseconds (default: 5 minutes)
+ clusterTimeout: 300000
+
+ # Maximum clusters per world
+ maxClustersPerWorld: 50
+
+modules:
+ LavaCastDetector:
+ # Enable lava-water interaction detection
+ enabled: true
+
+ # Minimum block count to trigger evaluation
+ minBlockCount: 10
+
+ # Minimum duration in milliseconds to trigger evaluation
+ minDuration: 5000
+
+ # Mining suppression threshold (0.0-1.0)
+ suppressionThreshold: 0.1
\ No newline at end of file
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
new file mode 100644
index 0000000..ba143ba
--- /dev/null
+++ b/src/main/resources/plugin.yml
@@ -0,0 +1,28 @@
+name: GriefDetect
+version: ${project.version}
+main: party.cybsec.griefdetect.GriefDetectPlugin
+api-version: 1.21
+description: Modular, async-first grief pattern detection with confidence scoring and Discord webhook reporting.
+author: CybSec
+website: https://git.cybsec.party/cybsec/griefdetect.git
+
+commands:
+ griefdetect:
+ description: Reload GriefDetect configuration
+ usage: / reload
+ permission: griefdetect.reload
+ aliases: [gd]
+
+permissions:
+ griefdetect.admin:
+ description: Full configuration access
+ default: op
+ griefdetect.reload:
+ description: Reload command permission
+ default: op
+ griefdetect.alerts:
+ description: Receive chat alerts
+ default: op
+ griefdetect.bypass:
+ description: Bypass grief detection
+ default: op
\ No newline at end of file
diff --git a/target/classes/config.yml b/target/classes/config.yml
new file mode 100644
index 0000000..21fe120
--- /dev/null
+++ b/target/classes/config.yml
@@ -0,0 +1,76 @@
+# GriefDetect Configuration
+# Modular, async-first grief pattern detection with confidence scoring and Discord webhook reporting.
+
+global:
+ # Enable/disable the entire plugin
+ enable: true
+
+ # Use async processing (recommended)
+ async: true
+
+permissions:
+ # Permission node for full configuration access
+ admin: "griefdetect.admin"
+
+ # Permission node for reload command
+ reload: "griefdetect.reload"
+
+ # Permission node for receiving chat alerts
+ alerts: "griefdetect.alerts"
+
+ # Permission node for bypassing detection
+ bypass: "griefdetect.bypass"
+
+webhook:
+ # Enable Discord webhook notifications
+ enabled: false
+
+ # Discord webhook URL
+ url: ""
+
+ # Minimum confidence threshold for webhook (0-100)
+ threshold: 75.0
+
+ # Number of retry attempts on failure
+ retryAttempts: 3
+
+ # Delay between retry attempts (milliseconds)
+ retryDelay: 5000
+
+ # Area-based cooldown to prevent spam (seconds)
+ areaCooldown: 300
+
+chat:
+ # Enable in-game chat alerts
+ enabled: true
+
+ # Minimum confidence threshold for chat alerts (0-100)
+ threshold: 50.0
+
+ # Chat message format (supports & colors)
+ # Variables: %s (detection type), %s (world), %d (x), %d (y), %d (z), %d (confidence)
+ format: "&c[GriefDetect] &7%s &fat &6%s:%d,%d,%d &7(%d%%)"
+
+engine:
+ # Analysis interval in milliseconds (default: 5 seconds)
+ analysisInterval: 5000
+
+ # Cluster timeout in milliseconds (default: 5 minutes)
+ clusterTimeout: 300000
+
+ # Maximum clusters per world
+ maxClustersPerWorld: 50
+
+modules:
+ LavaCastDetector:
+ # Enable lava-water interaction detection
+ enabled: true
+
+ # Minimum block count to trigger evaluation
+ minBlockCount: 10
+
+ # Minimum duration in milliseconds to trigger evaluation
+ minDuration: 5000
+
+ # Mining suppression threshold (0.0-1.0)
+ suppressionThreshold: 0.1
\ No newline at end of file
diff --git a/target/classes/party/cybsec/griefdetect/GriefDetectPlugin.class b/target/classes/party/cybsec/griefdetect/GriefDetectPlugin.class
new file mode 100644
index 0000000..d23ba1f
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/GriefDetectPlugin.class differ
diff --git a/target/classes/party/cybsec/griefdetect/commands/ReloadCommand.class b/target/classes/party/cybsec/griefdetect/commands/ReloadCommand.class
new file mode 100644
index 0000000..876a713
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/commands/ReloadCommand.class differ
diff --git a/target/classes/party/cybsec/griefdetect/config/PluginConfig.class b/target/classes/party/cybsec/griefdetect/config/PluginConfig.class
new file mode 100644
index 0000000..b20d2ba
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/config/PluginConfig.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/ChatAlertManager.class b/target/classes/party/cybsec/griefdetect/core/ChatAlertManager.class
new file mode 100644
index 0000000..d8a3158
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/ChatAlertManager.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/ConfidenceEngine.class b/target/classes/party/cybsec/griefdetect/core/ConfidenceEngine.class
new file mode 100644
index 0000000..b57fba7
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/ConfidenceEngine.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/DetectionEngine$Cluster.class b/target/classes/party/cybsec/griefdetect/core/DetectionEngine$Cluster.class
new file mode 100644
index 0000000..cc29423
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/DetectionEngine$Cluster.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/DetectionEngine$PlayerPosition.class b/target/classes/party/cybsec/griefdetect/core/DetectionEngine$PlayerPosition.class
new file mode 100644
index 0000000..0115d00
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/DetectionEngine$PlayerPosition.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/DetectionEngine.class b/target/classes/party/cybsec/griefdetect/core/DetectionEngine.class
new file mode 100644
index 0000000..883d94b
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/DetectionEngine.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/DetectionEvent$NearestPlayerInfo.class b/target/classes/party/cybsec/griefdetect/core/DetectionEvent$NearestPlayerInfo.class
new file mode 100644
index 0000000..d6fbdce
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/DetectionEvent$NearestPlayerInfo.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/DetectionEvent.class b/target/classes/party/cybsec/griefdetect/core/DetectionEvent.class
new file mode 100644
index 0000000..75886b5
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/DetectionEvent.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/ModuleManager$DetectionModule.class b/target/classes/party/cybsec/griefdetect/core/ModuleManager$DetectionModule.class
new file mode 100644
index 0000000..578a741
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/ModuleManager$DetectionModule.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/ModuleManager.class b/target/classes/party/cybsec/griefdetect/core/ModuleManager.class
new file mode 100644
index 0000000..2b270cd
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/ModuleManager.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/PermissionManager.class b/target/classes/party/cybsec/griefdetect/core/PermissionManager.class
new file mode 100644
index 0000000..be15ed3
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/PermissionManager.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/WebhookManager$WebhookCallback.class b/target/classes/party/cybsec/griefdetect/core/WebhookManager$WebhookCallback.class
new file mode 100644
index 0000000..7645550
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/WebhookManager$WebhookCallback.class differ
diff --git a/target/classes/party/cybsec/griefdetect/core/WebhookManager.class b/target/classes/party/cybsec/griefdetect/core/WebhookManager.class
new file mode 100644
index 0000000..5a988f5
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/core/WebhookManager.class differ
diff --git a/target/classes/party/cybsec/griefdetect/modules/LavaCastDetector.class b/target/classes/party/cybsec/griefdetect/modules/LavaCastDetector.class
new file mode 100644
index 0000000..87d7e57
Binary files /dev/null and b/target/classes/party/cybsec/griefdetect/modules/LavaCastDetector.class differ
diff --git a/target/classes/plugin.yml b/target/classes/plugin.yml
new file mode 100644
index 0000000..ba143ba
--- /dev/null
+++ b/target/classes/plugin.yml
@@ -0,0 +1,28 @@
+name: GriefDetect
+version: ${project.version}
+main: party.cybsec.griefdetect.GriefDetectPlugin
+api-version: 1.21
+description: Modular, async-first grief pattern detection with confidence scoring and Discord webhook reporting.
+author: CybSec
+website: https://git.cybsec.party/cybsec/griefdetect.git
+
+commands:
+ griefdetect:
+ description: Reload GriefDetect configuration
+ usage: / reload
+ permission: griefdetect.reload
+ aliases: [gd]
+
+permissions:
+ griefdetect.admin:
+ description: Full configuration access
+ default: op
+ griefdetect.reload:
+ description: Reload command permission
+ default: op
+ griefdetect.alerts:
+ description: Receive chat alerts
+ default: op
+ griefdetect.bypass:
+ description: Bypass grief detection
+ default: op
\ No newline at end of file
diff --git a/target/griefdetect-1.0.0-SNAPSHOT.jar b/target/griefdetect-1.0.0-SNAPSHOT.jar
new file mode 100644
index 0000000..37a7bd3
Binary files /dev/null and b/target/griefdetect-1.0.0-SNAPSHOT.jar differ
diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..b0cd60b
--- /dev/null
+++ b/target/maven-archiver/pom.properties
@@ -0,0 +1,3 @@
+artifactId=griefdetect
+groupId=party.cybsec
+version=1.0.0-SNAPSHOT
diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..393fa0d
--- /dev/null
+++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1,16 @@
+party/cybsec/griefdetect/core/DetectionEngine.class
+party/cybsec/griefdetect/core/ConfidenceEngine.class
+party/cybsec/griefdetect/commands/ReloadCommand.class
+party/cybsec/griefdetect/core/WebhookManager.class
+party/cybsec/griefdetect/core/ChatAlertManager.class
+party/cybsec/griefdetect/core/ModuleManager.class
+party/cybsec/griefdetect/core/DetectionEngine$PlayerPosition.class
+party/cybsec/griefdetect/GriefDetectPlugin.class
+party/cybsec/griefdetect/core/DetectionEngine$Cluster.class
+party/cybsec/griefdetect/core/ModuleManager$DetectionModule.class
+party/cybsec/griefdetect/core/PermissionManager.class
+party/cybsec/griefdetect/core/DetectionEvent$NearestPlayerInfo.class
+party/cybsec/griefdetect/core/WebhookManager$WebhookCallback.class
+party/cybsec/griefdetect/modules/LavaCastDetector.class
+party/cybsec/griefdetect/core/DetectionEvent.class
+party/cybsec/griefdetect/config/PluginConfig.class
diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..7779616
--- /dev/null
+++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1,11 @@
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/GriefDetectPlugin.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/commands/ReloadCommand.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/config/PluginConfig.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/core/ChatAlertManager.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/core/ConfidenceEngine.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/core/DetectionEngine.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/core/DetectionEvent.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/core/ModuleManager.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/core/PermissionManager.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/core/WebhookManager.java
+/Users/jacktotonchi/griefdetect/src/main/java/party/cybsec/griefdetect/modules/LavaCastDetector.java
diff --git a/target/original-griefdetect-1.0.0-SNAPSHOT.jar b/target/original-griefdetect-1.0.0-SNAPSHOT.jar
new file mode 100644
index 0000000..c63fe97
Binary files /dev/null and b/target/original-griefdetect-1.0.0-SNAPSHOT.jar differ