first commit
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
<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>com.bitnix</groupId>
|
||||
<artifactId>FarmGuard</artifactId>
|
||||
<version>1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>FarmGuard</name>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>1.21.8-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>FarmGuard</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>21</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,536 @@
|
||||
package com.bitnix.farmguard;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
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.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.player.PlayerAnimationEvent;
|
||||
import org.bukkit.event.player.PlayerAnimationType;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerItemConsumeEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class FarmGuardPlugin extends JavaPlugin implements Listener {
|
||||
|
||||
private final Map<UUID, PlayerData> playerDataMap = new HashMap<>();
|
||||
private BukkitTask analysisTask;
|
||||
|
||||
private boolean opBypass;
|
||||
private String bypassPermission;
|
||||
private boolean alertConsole;
|
||||
private boolean alertOps;
|
||||
|
||||
private long checkPeriodTicks;
|
||||
private long windowMillis;
|
||||
private int minActions;
|
||||
private double maxCameraMovement;
|
||||
private double maxDistanceMoved;
|
||||
private long maxIntervalDeviationMs;
|
||||
private int suspicionIncrease;
|
||||
private int suspicionDecrease;
|
||||
private int alertThreshold;
|
||||
private int actionThreshold;
|
||||
private long alertCooldownMillis;
|
||||
private long actionCooldownMillis;
|
||||
|
||||
private boolean checkAttack;
|
||||
private boolean checkInteract;
|
||||
private boolean checkBlockBreak;
|
||||
private boolean checkBlockPlace;
|
||||
private boolean checkItemConsume;
|
||||
private boolean checkAnimation;
|
||||
|
||||
private String msgOpAlert;
|
||||
private String msgConsoleLog;
|
||||
private String msgKickMessage;
|
||||
|
||||
private List<ActionDefinition> onAlertActions = new ArrayList<>();
|
||||
private List<ActionDefinition> onActionActions = new ArrayList<>();
|
||||
|
||||
private final DecimalFormat decimalFormat = new DecimalFormat("0.00");
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
loadSettings();
|
||||
|
||||
getServer().getPluginManager().registerEvents(this, this);
|
||||
startAnalysisTask();
|
||||
|
||||
getLogger().info("FarmGuard enabled.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (analysisTask != null) {
|
||||
analysisTask.cancel();
|
||||
analysisTask = null;
|
||||
}
|
||||
|
||||
playerDataMap.clear();
|
||||
onAlertActions.clear();
|
||||
onActionActions.clear();
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
reloadConfig();
|
||||
FileConfiguration config = getConfig();
|
||||
|
||||
this.opBypass = config.getBoolean("op-bypass", true);
|
||||
this.bypassPermission = config.getString("permission-bypass", "farmguard.bypass");
|
||||
this.alertConsole = config.getBoolean("alert-console", true);
|
||||
this.alertOps = config.getBoolean("alert-ops", true);
|
||||
|
||||
this.checkPeriodTicks = Math.max(20L, config.getLong("detection.check-period-ticks", 100L));
|
||||
this.windowMillis = Math.max(30L, config.getLong("detection.window-seconds", 180L)) * 1000L;
|
||||
this.minActions = Math.max(10, config.getInt("detection.min-actions", 80));
|
||||
this.maxCameraMovement = Math.max(0.1, config.getDouble("detection.max-camera-movement", 12.0));
|
||||
this.maxDistanceMoved = Math.max(0.1, config.getDouble("detection.max-distance-moved", 6.0));
|
||||
this.maxIntervalDeviationMs = Math.max(1L, config.getLong("detection.max-interval-deviation-ms", 90L));
|
||||
this.suspicionIncrease = Math.max(1, config.getInt("detection.suspicion-increase", 15));
|
||||
this.suspicionDecrease = Math.max(1, config.getInt("detection.suspicion-decrease", 5));
|
||||
this.alertThreshold = Math.max(1, config.getInt("detection.alert-threshold", 70));
|
||||
this.actionThreshold = Math.max(this.alertThreshold, config.getInt("detection.action-threshold", 90));
|
||||
this.alertCooldownMillis = Math.max(1L, config.getLong("detection.alert-cooldown-seconds", 300L)) * 1000L;
|
||||
this.actionCooldownMillis = Math.max(1L, config.getLong("detection.action-cooldown-seconds", 300L)) * 1000L;
|
||||
|
||||
this.checkAttack = config.getBoolean("checks.attack", true);
|
||||
this.checkInteract = config.getBoolean("checks.interact", true);
|
||||
this.checkBlockBreak = config.getBoolean("checks.block-break", true);
|
||||
this.checkBlockPlace = config.getBoolean("checks.block-place", false);
|
||||
this.checkItemConsume = config.getBoolean("checks.item-consume", false);
|
||||
this.checkAnimation = config.getBoolean("checks.animation", true);
|
||||
|
||||
this.msgOpAlert = color(config.getString("messages.op-alert",
|
||||
"&c[FarmGuard] &e%player% &cmay be using unattended repetitive actions. Score=&e%score%&c Actions=&e%actions%&c Camera=&e%camera%&c Move=&e%move%&c Deviation=&e%deviation%"));
|
||||
|
||||
this.msgConsoleLog = color(config.getString("messages.console-log",
|
||||
"[FarmGuard] %player% flagged. Score=%score% Actions=%actions% Camera=%camera% Move=%move% Deviation=%deviation%"));
|
||||
|
||||
this.msgKickMessage = color(config.getString("messages.kick-message",
|
||||
"&cSuspicious unattended repetitive activity detected."));
|
||||
|
||||
this.onAlertActions = loadActions(config.getMapList("on-alert"));
|
||||
this.onActionActions = loadActions(config.getMapList("on-action"));
|
||||
}
|
||||
|
||||
private List<ActionDefinition> loadActions(List<Map<?, ?>> rawList) {
|
||||
List<ActionDefinition> actions = new ArrayList<>();
|
||||
|
||||
for (Map<?, ?> map : rawList) {
|
||||
Object typeObj = map.get("type");
|
||||
if (typeObj == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String type = String.valueOf(typeObj).trim().toUpperCase();
|
||||
String command = map.containsKey("command") ? String.valueOf(map.get("command")) : "";
|
||||
String message = map.containsKey("message") ? String.valueOf(map.get("message")) : "";
|
||||
|
||||
actions.add(new ActionDefinition(type, command, color(message)));
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
private void startAnalysisTask() {
|
||||
if (analysisTask != null) {
|
||||
analysisTask.cancel();
|
||||
}
|
||||
|
||||
analysisTask = Bukkit.getScheduler().runTaskTimer(this, this::runAnalysis, checkPeriodTicks, checkPeriodTicks);
|
||||
}
|
||||
|
||||
private void runAnalysis() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (shouldBypass(player)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerData data = playerDataMap.computeIfAbsent(player.getUniqueId(), uuid -> new PlayerData());
|
||||
|
||||
pruneOldData(data, now);
|
||||
|
||||
int actions = data.actionTimes.size();
|
||||
if (actions < minActions) {
|
||||
data.suspicion = Math.max(0, data.suspicion - suspicionDecrease);
|
||||
continue;
|
||||
}
|
||||
|
||||
double cameraMovement = data.cameraMovement;
|
||||
double distanceMoved = data.distanceMoved;
|
||||
long intervalDeviation = calculateIntervalDeviation(data.actionTimes);
|
||||
|
||||
boolean suspicious =
|
||||
cameraMovement <= maxCameraMovement &&
|
||||
distanceMoved <= maxDistanceMoved &&
|
||||
intervalDeviation <= maxIntervalDeviationMs;
|
||||
|
||||
if (suspicious) {
|
||||
data.suspicion += suspicionIncrease;
|
||||
} else {
|
||||
data.suspicion = Math.max(0, data.suspicion - suspicionDecrease);
|
||||
}
|
||||
|
||||
DetectionContext context = new DetectionContext(
|
||||
player,
|
||||
data.suspicion,
|
||||
actions,
|
||||
cameraMovement,
|
||||
distanceMoved,
|
||||
intervalDeviation
|
||||
);
|
||||
|
||||
if (data.suspicion >= alertThreshold && (now - data.lastAlertTime) >= alertCooldownMillis) {
|
||||
data.lastAlertTime = now;
|
||||
executeActions(onAlertActions, context);
|
||||
|
||||
if (onAlertActions.isEmpty()) {
|
||||
if (alertOps) {
|
||||
sendOpAlert(context);
|
||||
}
|
||||
if (alertConsole) {
|
||||
getLogger().info(stripColors(formatMessage(msgConsoleLog, context)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.suspicion >= actionThreshold && (now - data.lastActionTime) >= actionCooldownMillis) {
|
||||
data.lastActionTime = now;
|
||||
executeActions(onActionActions, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void pruneOldData(PlayerData data, long now) {
|
||||
while (!data.actionTimes.isEmpty() && now - data.actionTimes.peekFirst() > windowMillis) {
|
||||
data.actionTimes.pollFirst();
|
||||
}
|
||||
|
||||
while (!data.cameraEvents.isEmpty() && now - data.cameraEvents.peekFirst().time > windowMillis) {
|
||||
CameraSample removed = data.cameraEvents.pollFirst();
|
||||
data.cameraMovement = Math.max(0.0, data.cameraMovement - removed.amount);
|
||||
}
|
||||
|
||||
while (!data.moveEvents.isEmpty() && now - data.moveEvents.peekFirst().time > windowMillis) {
|
||||
MoveSample removed = data.moveEvents.pollFirst();
|
||||
data.distanceMoved = Math.max(0.0, data.distanceMoved - removed.amount);
|
||||
}
|
||||
}
|
||||
|
||||
private long calculateIntervalDeviation(Deque<Long> actionTimes) {
|
||||
if (actionTimes.size() < 3) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
List<Long> intervals = new ArrayList<>();
|
||||
Long previous = null;
|
||||
|
||||
for (Long time : actionTimes) {
|
||||
if (previous != null) {
|
||||
intervals.add(time - previous);
|
||||
}
|
||||
previous = time;
|
||||
}
|
||||
|
||||
if (intervals.size() < 2) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
double average = 0.0;
|
||||
for (Long interval : intervals) {
|
||||
average += interval;
|
||||
}
|
||||
average /= intervals.size();
|
||||
|
||||
double totalDeviation = 0.0;
|
||||
for (Long interval : intervals) {
|
||||
totalDeviation += Math.abs(interval - average);
|
||||
}
|
||||
|
||||
return Math.round(totalDeviation / intervals.size());
|
||||
}
|
||||
|
||||
private boolean shouldBypass(Player player) {
|
||||
if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (opBypass && player.isOp()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return bypassPermission != null && !bypassPermission.isBlank() && player.hasPermission(bypassPermission);
|
||||
}
|
||||
|
||||
private void recordAction(Player player) {
|
||||
if (shouldBypass(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerData data = playerDataMap.computeIfAbsent(player.getUniqueId(), uuid -> new PlayerData());
|
||||
data.actionTimes.addLast(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onAttack(EntityDamageByEntityEvent event) {
|
||||
if (!checkAttack) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getDamager() instanceof Player player) {
|
||||
recordAction(player);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onInteract(PlayerInteractEvent event) {
|
||||
if (!checkInteract) {
|
||||
return;
|
||||
}
|
||||
|
||||
recordAction(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBlockBreak(BlockBreakEvent event) {
|
||||
if (!checkBlockBreak) {
|
||||
return;
|
||||
}
|
||||
|
||||
recordAction(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onBlockPlace(BlockPlaceEvent event) {
|
||||
if (!checkBlockPlace) {
|
||||
return;
|
||||
}
|
||||
|
||||
recordAction(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onConsume(PlayerItemConsumeEvent event) {
|
||||
if (!checkItemConsume) {
|
||||
return;
|
||||
}
|
||||
|
||||
recordAction(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onAnimation(PlayerAnimationEvent event) {
|
||||
if (!checkAnimation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getAnimationType() == PlayerAnimationType.ARM_SWING) {
|
||||
recordAction(event.getPlayer());
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onMove(PlayerMoveEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
|
||||
if (shouldBypass(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerData data = playerDataMap.computeIfAbsent(player.getUniqueId(), uuid -> new PlayerData());
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
float fromYaw = event.getFrom().getYaw();
|
||||
float toYaw = event.getTo().getYaw();
|
||||
float fromPitch = event.getFrom().getPitch();
|
||||
float toPitch = event.getTo().getPitch();
|
||||
|
||||
double yawDelta = angleDistance(fromYaw, toYaw);
|
||||
double pitchDelta = Math.abs(toPitch - fromPitch);
|
||||
double cameraDelta = yawDelta + pitchDelta;
|
||||
|
||||
if (cameraDelta > 0.0) {
|
||||
data.cameraEvents.addLast(new CameraSample(now, cameraDelta));
|
||||
data.cameraMovement += cameraDelta;
|
||||
}
|
||||
|
||||
double distance = 0.0;
|
||||
if (event.getFrom().getWorld().equals(event.getTo().getWorld())) {
|
||||
distance = event.getFrom().distance(event.getTo());
|
||||
}
|
||||
|
||||
if (distance > 0.0) {
|
||||
data.moveEvents.addLast(new MoveSample(now, distance));
|
||||
data.distanceMoved += distance;
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
playerDataMap.putIfAbsent(event.getPlayer().getUniqueId(), new PlayerData());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
playerDataMap.remove(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
private double angleDistance(float from, float to) {
|
||||
double diff = Math.abs(to - from) % 360.0;
|
||||
return diff > 180.0 ? 360.0 - diff : diff;
|
||||
}
|
||||
|
||||
private void executeActions(List<ActionDefinition> actions, DetectionContext context) {
|
||||
for (ActionDefinition action : actions) {
|
||||
switch (action.type) {
|
||||
case "OP_ALERT" -> sendOpAlert(context);
|
||||
case "CONSOLE_LOG" -> getLogger().info(stripColors(formatMessage(msgConsoleLog, context)));
|
||||
case "CONSOLE_COMMAND" -> {
|
||||
if (action.command != null && !action.command.isBlank()) {
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), formatMessage(action.command, context));
|
||||
}
|
||||
}
|
||||
case "PLAYER_COMMAND" -> {
|
||||
if (action.command != null && !action.command.isBlank()) {
|
||||
context.player.performCommand(stripLeadingSlash(formatMessage(action.command, context)));
|
||||
}
|
||||
}
|
||||
case "KICK" -> {
|
||||
String kickMessage = (action.message != null && !action.message.isBlank()) ? action.message : msgKickMessage;
|
||||
context.player.kickPlayer(formatMessage(kickMessage, context));
|
||||
}
|
||||
default -> getLogger().warning("Unknown action type in config: " + action.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendOpAlert(DetectionContext context) {
|
||||
String message = formatMessage(msgOpAlert, context);
|
||||
|
||||
for (Player online : Bukkit.getOnlinePlayers()) {
|
||||
if (online.isOp()) {
|
||||
online.sendMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String formatMessage(String input, DetectionContext context) {
|
||||
return color(input)
|
||||
.replace("%player%", context.player.getName())
|
||||
.replace("%score%", String.valueOf(context.score))
|
||||
.replace("%actions%", String.valueOf(context.actions))
|
||||
.replace("%camera%", decimalFormat.format(context.cameraMovement))
|
||||
.replace("%move%", decimalFormat.format(context.distanceMoved))
|
||||
.replace("%deviation%", String.valueOf(context.intervalDeviation));
|
||||
}
|
||||
|
||||
private String color(String input) {
|
||||
return input == null ? "" : ChatColor.translateAlternateColorCodes('&', input);
|
||||
}
|
||||
|
||||
private String stripColors(String input) {
|
||||
return ChatColor.stripColor(input);
|
||||
}
|
||||
|
||||
private String stripLeadingSlash(String input) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
return input.startsWith("/") ? input.substring(1) : input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 1 && args[0].equalsIgnoreCase("reload")) {
|
||||
if (!sender.hasPermission("farmguard.admin")) {
|
||||
sender.sendMessage(color("&cYou do not have permission."));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (analysisTask != null) {
|
||||
analysisTask.cancel();
|
||||
analysisTask = null;
|
||||
}
|
||||
|
||||
loadSettings();
|
||||
startAnalysisTask();
|
||||
sender.sendMessage(color("&aFarmGuard config reloaded."));
|
||||
return true;
|
||||
}
|
||||
|
||||
sender.sendMessage(color("&eUsage: /farmguard reload"));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final class PlayerData {
|
||||
private final Deque<Long> actionTimes = new ArrayDeque<>();
|
||||
private final Deque<CameraSample> cameraEvents = new ArrayDeque<>();
|
||||
private final Deque<MoveSample> moveEvents = new ArrayDeque<>();
|
||||
private double cameraMovement = 0.0;
|
||||
private double distanceMoved = 0.0;
|
||||
private int suspicion = 0;
|
||||
private long lastAlertTime = 0L;
|
||||
private long lastActionTime = 0L;
|
||||
}
|
||||
|
||||
private static final class CameraSample {
|
||||
private final long time;
|
||||
private final double amount;
|
||||
|
||||
private CameraSample(long time, double amount) {
|
||||
this.time = time;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MoveSample {
|
||||
private final long time;
|
||||
private final double amount;
|
||||
|
||||
private MoveSample(long time, double amount) {
|
||||
this.time = time;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
|
||||
private record DetectionContext(
|
||||
Player player,
|
||||
int score,
|
||||
int actions,
|
||||
double cameraMovement,
|
||||
double distanceMoved,
|
||||
long intervalDeviation
|
||||
) { }
|
||||
|
||||
private record ActionDefinition(
|
||||
String type,
|
||||
String command,
|
||||
String message
|
||||
) { }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
op-bypass: true
|
||||
permission-bypass: "farmguard.bypass"
|
||||
|
||||
alert-console: true
|
||||
alert-ops: true
|
||||
|
||||
detection:
|
||||
check-period-ticks: 100
|
||||
window-seconds: 180
|
||||
min-actions: 80
|
||||
max-camera-movement: 12.0
|
||||
max-distance-moved: 6.0
|
||||
max-interval-deviation-ms: 90
|
||||
suspicion-increase: 15
|
||||
suspicion-decrease: 5
|
||||
alert-threshold: 70
|
||||
action-threshold: 90
|
||||
alert-cooldown-seconds: 300
|
||||
action-cooldown-seconds: 300
|
||||
|
||||
checks:
|
||||
attack: true
|
||||
interact: true
|
||||
block-break: true
|
||||
block-place: false
|
||||
item-consume: false
|
||||
animation: true
|
||||
|
||||
messages:
|
||||
op-alert: "&c[FarmGuard] &e%player% &cmay be using unattended repetitive actions. Score=&e%score%&c Actions=&e%actions%&c Camera=&e%camera%&c Move=&e%move%&c Deviation=&e%deviation%"
|
||||
console-log: "[FarmGuard] %player% flagged. Score=%score% Actions=%actions% Camera=%camera% Move=%move% Deviation=%deviation%"
|
||||
kick-message: "&cSuspicious unattended repetitive activity detected."
|
||||
|
||||
on-alert:
|
||||
- type: OP_ALERT
|
||||
- type: CONSOLE_LOG
|
||||
|
||||
on-action: []
|
||||
# Example:
|
||||
# on-action:
|
||||
# - type: CONSOLE_COMMAND
|
||||
# command: "cmi warp %player% checkroom"
|
||||
# - type: KICK
|
||||
# message: "&cPlease contact staff."
|
||||
@@ -0,0 +1,16 @@
|
||||
name: FarmGuard
|
||||
version: 1.0
|
||||
main: com.bitnix.farmguard.FarmGuardPlugin
|
||||
api-version: '1.21'
|
||||
author: bitnix
|
||||
description: Detects suspicious repetitive unattended behavior and triggers configurable actions.
|
||||
commands:
|
||||
farmguard:
|
||||
description: FarmGuard admin command
|
||||
usage: /farmguard reload
|
||||
permission: farmguard.admin
|
||||
permissions:
|
||||
farmguard.admin:
|
||||
default: op
|
||||
farmguard.bypass:
|
||||
default: false
|
||||
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.
@@ -0,0 +1,44 @@
|
||||
op-bypass: true
|
||||
permission-bypass: "farmguard.bypass"
|
||||
|
||||
alert-console: true
|
||||
alert-ops: true
|
||||
|
||||
detection:
|
||||
check-period-ticks: 100
|
||||
window-seconds: 180
|
||||
min-actions: 80
|
||||
max-camera-movement: 12.0
|
||||
max-distance-moved: 6.0
|
||||
max-interval-deviation-ms: 90
|
||||
suspicion-increase: 15
|
||||
suspicion-decrease: 5
|
||||
alert-threshold: 70
|
||||
action-threshold: 90
|
||||
alert-cooldown-seconds: 300
|
||||
action-cooldown-seconds: 300
|
||||
|
||||
checks:
|
||||
attack: true
|
||||
interact: true
|
||||
block-break: true
|
||||
block-place: false
|
||||
item-consume: false
|
||||
animation: true
|
||||
|
||||
messages:
|
||||
op-alert: "&c[FarmGuard] &e%player% &cmay be using unattended repetitive actions. Score=&e%score%&c Actions=&e%actions%&c Camera=&e%camera%&c Move=&e%move%&c Deviation=&e%deviation%"
|
||||
console-log: "[FarmGuard] %player% flagged. Score=%score% Actions=%actions% Camera=%camera% Move=%move% Deviation=%deviation%"
|
||||
kick-message: "&cSuspicious unattended repetitive activity detected."
|
||||
|
||||
on-alert:
|
||||
- type: OP_ALERT
|
||||
- type: CONSOLE_LOG
|
||||
|
||||
on-action: []
|
||||
# Example:
|
||||
# on-action:
|
||||
# - type: CONSOLE_COMMAND
|
||||
# command: "cmi warp %player% checkroom"
|
||||
# - type: KICK
|
||||
# message: "&cPlease contact staff."
|
||||
@@ -0,0 +1,16 @@
|
||||
name: FarmGuard
|
||||
version: 1.0
|
||||
main: com.bitnix.farmguard.FarmGuardPlugin
|
||||
api-version: '1.21'
|
||||
author: bitnix
|
||||
description: Detects suspicious repetitive unattended behavior and triggers configurable actions.
|
||||
commands:
|
||||
farmguard:
|
||||
description: FarmGuard admin command
|
||||
usage: /farmguard reload
|
||||
permission: farmguard.admin
|
||||
permissions:
|
||||
farmguard.admin:
|
||||
default: op
|
||||
farmguard.bypass:
|
||||
default: false
|
||||
@@ -0,0 +1,5 @@
|
||||
#Generated by Maven
|
||||
#Sun Jun 07 21:02:20 EDT 2026
|
||||
artifactId=FarmGuard
|
||||
groupId=com.bitnix
|
||||
version=1.0
|
||||
@@ -0,0 +1,6 @@
|
||||
com/bitnix/farmguard/FarmGuardPlugin$PlayerData.class
|
||||
com/bitnix/farmguard/FarmGuardPlugin$ActionDefinition.class
|
||||
com/bitnix/farmguard/FarmGuardPlugin$CameraSample.class
|
||||
com/bitnix/farmguard/FarmGuardPlugin$DetectionContext.class
|
||||
com/bitnix/farmguard/FarmGuardPlugin$MoveSample.class
|
||||
com/bitnix/farmguard/FarmGuardPlugin.class
|
||||
@@ -0,0 +1 @@
|
||||
/home/bitnix/Desktop/FarmGuard/src/main/java/com/bitnix/farmguard/FarmGuardPlugin.java
|
||||
Reference in New Issue
Block a user