initial implementation complete
This commit is contained in:
74
dependency-reduced-pom.xml
Normal file
74
dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>party.cybsec</groupId>
|
||||
<artifactId>griefdetect</artifactId>
|
||||
<name>GriefDetect</name>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<description>Modular, async-first grief pattern detection plugin for Paper 1.21.11</description>
|
||||
<url>https://git.cybsec.party/cybsec/griefdetect.git</url>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>true</createDependencyReducedPom>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>sonatype</id>
|
||||
<url>https://oss.sonatype.org/content/groups/public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>1.21.11-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<properties>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<okhttp.version>4.12.0</okhttp.version>
|
||||
<slf4j.version>2.0.16</slf4j.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
|
||||
<jackson.version>2.17.1</jackson.version>
|
||||
</properties>
|
||||
</project>
|
||||
108
pom.xml
Normal file
108
pom.xml
Normal file
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>party.cybsec</groupId>
|
||||
<artifactId>griefdetect</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>GriefDetect</name>
|
||||
<description>Modular, async-first grief pattern detection plugin for Paper 1.21.11</description>
|
||||
<url>https://git.cybsec.party/cybsec/griefdetect.git</url>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<paper.version>1.21.11-R0.1-SNAPSHOT</paper.version>
|
||||
<jackson.version>2.17.1</jackson.version>
|
||||
<okhttp.version>4.12.0</okhttp.version>
|
||||
<slf4j.version>2.0.16</slf4j.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>sonatype</id>
|
||||
<url>https://oss.sonatype.org/content/groups/public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!-- Paper API -->
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>${paper.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP Client for Webhooks -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>${okhttp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Logging -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>true</createDependencyReducedPom>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
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
|
||||
76
target/classes/config.yml
Normal file
76
target/classes/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
|
||||
BIN
target/classes/party/cybsec/griefdetect/GriefDetectPlugin.class
Normal file
BIN
target/classes/party/cybsec/griefdetect/GriefDetectPlugin.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/party/cybsec/griefdetect/core/ModuleManager.class
Normal file
BIN
target/classes/party/cybsec/griefdetect/core/ModuleManager.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
target/classes/plugin.yml
Normal file
28
target/classes/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
|
||||
BIN
target/griefdetect-1.0.0-SNAPSHOT.jar
Normal file
BIN
target/griefdetect-1.0.0-SNAPSHOT.jar
Normal file
Binary file not shown.
3
target/maven-archiver/pom.properties
Normal file
3
target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
artifactId=griefdetect
|
||||
groupId=party.cybsec
|
||||
version=1.0.0-SNAPSHOT
|
||||
@@ -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
|
||||
@@ -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
|
||||
BIN
target/original-griefdetect-1.0.0-SNAPSHOT.jar
Normal file
BIN
target/original-griefdetect-1.0.0-SNAPSHOT.jar
Normal file
Binary file not shown.
Reference in New Issue
Block a user