initial implementation complete
This commit is contained in:
110
src/main/java/party/cybsec/griefdetect/GriefDetectPlugin.java
Normal file
110
src/main/java/party/cybsec/griefdetect/GriefDetectPlugin.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
254
src/main/java/party/cybsec/griefdetect/config/PluginConfig.java
Normal file
254
src/main/java/party/cybsec/griefdetect/config/PluginConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
379
src/main/java/party/cybsec/griefdetect/core/DetectionEngine.java
Normal file
379
src/main/java/party/cybsec/griefdetect/core/DetectionEngine.java
Normal 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(); }
|
||||
}
|
||||
}
|
||||
106
src/main/java/party/cybsec/griefdetect/core/DetectionEvent.java
Normal file
106
src/main/java/party/cybsec/griefdetect/core/DetectionEvent.java
Normal 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; }
|
||||
}
|
||||
}
|
||||
118
src/main/java/party/cybsec/griefdetect/core/ModuleManager.java
Normal file
118
src/main/java/party/cybsec/griefdetect/core/ModuleManager.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
240
src/main/java/party/cybsec/griefdetect/core/WebhookManager.java
Normal file
240
src/main/java/party/cybsec/griefdetect/core/WebhookManager.java
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
76
src/main/resources/config.yml
Normal file
76
src/main/resources/config.yml
Normal 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
src/main/resources/plugin.yml
Normal file
28
src/main/resources/plugin.yml
Normal 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
|
||||
Reference in New Issue
Block a user