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