first commit

This commit is contained in:
2026-06-08 00:02:34 -04:00
commit 1ccd3d9586
17 changed files with 715 additions and 0 deletions
View File
+47
View File
@@ -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
) { }
}
+44
View File
@@ -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."
+16
View File
@@ -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.
+44
View File
@@ -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."
+16
View File
@@ -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
+5
View File
@@ -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