initial implementation complete

This commit is contained in:
2026-01-25 13:02:40 -05:00
parent 14d40089ca
commit 5d4f0a6714
38 changed files with 2312 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>party.cybsec</groupId>
<artifactId>griefdetect</artifactId>
<name>GriefDetect</name>
<version>1.0.0-SNAPSHOT</version>
<description>Modular, async-first grief pattern detection plugin for Paper 1.21.11</description>
<url>https://git.cybsec.party/cybsec/griefdetect.git</url>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21.11-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.target>17</maven.compiler.target>
<okhttp.version>4.12.0</okhttp.version>
<slf4j.version>2.0.16</slf4j.version>
<maven.compiler.source>17</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
<jackson.version>2.17.1</jackson.version>
</properties>
</project>

108
pom.xml Normal file
View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>party.cybsec</groupId>
<artifactId>griefdetect</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>GriefDetect</name>
<description>Modular, async-first grief pattern detection plugin for Paper 1.21.11</description>
<url>https://git.cybsec.party/cybsec/griefdetect.git</url>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
<jackson.version>2.17.1</jackson.version>
<okhttp.version>4.12.0</okhttp.version>
<slf4j.version>2.0.16</slf4j.version>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>sonatype</id>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<!-- Paper API -->
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>${paper.version}</version>
<scope>provided</scope>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- HTTP Client for Webhooks -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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);
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -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<Player> 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();
}
}

View File

@@ -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<String, Cluster> activeClusters;
private final Map<UUID, PlayerPosition> playerPositions;
// Configuration
private final int analysisInterval;
private final int clusterTimeout;
private final int maxClustersPerWorld;
// Throttling
private final Map<UUID, Long> 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<Cluster> 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<DetectionEvent.NearestPlayerInfo> 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<DetectionEvent.NearestPlayerInfo> getNearestPlayers(Location center) {
List<DetectionEvent.NearestPlayerInfo> players = new ArrayList<>();
World world = center.getWorld();
for (Map.Entry<UUID, PlayerPosition> 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<Map.Entry<String, Cluster>> iterator = activeClusters.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Cluster> 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(); }
}
}

View File

@@ -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<NearestPlayerInfo> 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<NearestPlayerInfo> 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<NearestPlayerInfo> 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; }
}
}

View File

@@ -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<String, DetectionModule> 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<DetectionModule> 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<String, Object> getModuleConfig();
}
}

View File

@@ -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<UUID, Map<String, Boolean>> 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();
}
}

View File

@@ -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<String, Long> areaCooldowns;
private final ConcurrentHashMap<String, Integer> 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");
}
}

View File

@@ -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<String, Object> getModuleConfig() {
Map<String, Object> 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;
}
}

View File

@@ -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

View File

@@ -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: /<command> 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

76
target/classes/config.yml Normal file
View File

@@ -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

28
target/classes/plugin.yml Normal file
View File

@@ -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: /<command> 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

Binary file not shown.

View File

@@ -0,0 +1,3 @@
artifactId=griefdetect
groupId=party.cybsec
version=1.0.0-SNAPSHOT

View File

@@ -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

View File

@@ -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

Binary file not shown.