diff --git a/pom.xml b/pom.xml
index c8804c2..8c677d5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.xeroth
xeroanticheat
- 1.1.3
+ 1.2.0
jar
XeroAntiCheat
diff --git a/src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java b/src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java
index 19ea3ac..d178083 100644
--- a/src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java
+++ b/src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java
@@ -1,5 +1,6 @@
package com.xeroth.xeroanticheat;
+import com.xeroth.xeroanticheat.api.XACApi;
import com.xeroth.xeroanticheat.checks.combat.*;
import com.xeroth.xeroanticheat.checks.misc.*;
import com.xeroth.xeroanticheat.checks.movement.*;
@@ -11,6 +12,7 @@ import com.xeroth.xeroanticheat.listener.MovementListener;
import com.xeroth.xeroanticheat.manager.CheckManager;
import com.xeroth.xeroanticheat.manager.ConfigManager;
import com.xeroth.xeroanticheat.manager.DatabaseManager;
+import com.xeroth.xeroanticheat.manager.MetricsManager;
import com.xeroth.xeroanticheat.manager.PunishmentManager;
import com.xeroth.xeroanticheat.manager.ViolationManager;
import com.xeroth.xeroanticheat.protocol.PacketListener;
@@ -42,6 +44,7 @@ public final class XeroAntiCheat extends JavaPlugin {
private CheckManager checkManager;
private PacketListener packetListener;
private DatabaseManager databaseManager;
+ private MetricsManager metricsManager;
private boolean protocolLibLoaded = false;
private org.bukkit.scheduler.BukkitTask decayTask;
@@ -56,6 +59,9 @@ public final class XeroAntiCheat extends JavaPlugin {
public void onEnable() {
instance = this;
+ // Initialize API
+ XACApi.init(this);
+
// Check for ProtocolLib
checkProtocolLib();
@@ -103,6 +109,9 @@ public final class XeroAntiCheat extends JavaPlugin {
databaseManager.close();
}
+ // Shutdown API
+ XACApi.shutdown();
+
getLogger().info("XeroAntiCheat disabled!");
}
@@ -122,6 +131,7 @@ public final class XeroAntiCheat extends JavaPlugin {
violationManager = new ViolationManager(this);
punishmentManager = new PunishmentManager(this, violationManager);
checkManager = new CheckManager(this);
+ metricsManager = new MetricsManager();
databaseManager = new DatabaseManager(this);
databaseManager.initialize();
@@ -267,6 +277,10 @@ public final class XeroAntiCheat extends JavaPlugin {
public DatabaseManager getDatabaseManager() {
return databaseManager;
}
+
+ public MetricsManager getMetricsManager() {
+ return metricsManager;
+ }
public boolean isProtocolLibLoaded() {
return protocolLibLoaded;
diff --git a/src/main/java/com/xeroth/xeroanticheat/api/XACApi.java b/src/main/java/com/xeroth/xeroanticheat/api/XACApi.java
new file mode 100644
index 0000000..951d061
--- /dev/null
+++ b/src/main/java/com/xeroth/xeroanticheat/api/XACApi.java
@@ -0,0 +1,42 @@
+package com.xeroth.xeroanticheat.api;
+
+import com.xeroth.xeroanticheat.XeroAntiCheat;
+import org.bukkit.entity.Player;
+
+public class XACApi {
+
+ private static XACApi instance;
+ private final XeroAntiCheat plugin;
+
+ private XACApi(XeroAntiCheat plugin) {
+ this.plugin = plugin;
+ }
+
+ public static XACApi get() {
+ return instance;
+ }
+
+ public static void init(XeroAntiCheat plugin) {
+ instance = new XACApi(plugin);
+ }
+
+ public static void shutdown() {
+ instance = null;
+ }
+
+ public boolean isFlagged(Player player) {
+ return plugin.getViolationManager().hasViolations(player, 1.0);
+ }
+
+ public double getViolationLevel(Player player, String checkName) {
+ return plugin.getViolationManager().getViolationLevel(player, checkName);
+ }
+
+ public double getTotalViolations(Player player) {
+ return plugin.getViolationManager().getTotalViolations(player);
+ }
+
+ public boolean isBypassed(Player player) {
+ return player.hasPermission("xac.bypass");
+ }
+}
diff --git a/src/main/java/com/xeroth/xeroanticheat/check/Check.java b/src/main/java/com/xeroth/xeroanticheat/check/Check.java
index 1bb36ac..fe654c5 100644
--- a/src/main/java/com/xeroth/xeroanticheat/check/Check.java
+++ b/src/main/java/com/xeroth/xeroanticheat/check/Check.java
@@ -2,6 +2,7 @@ package com.xeroth.xeroanticheat.check;
import com.xeroth.xeroanticheat.XeroAntiCheat;
import com.xeroth.xeroanticheat.data.PlayerData;
+import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
@@ -125,4 +126,19 @@ public abstract class Check {
|| player.hasPermission("xac.bypass." + getCategory())
|| player.hasPermission("xac.bypass." + name.toLowerCase());
}
+
+ protected void setback(Player player, PlayerData data) {
+ if (!getConfigBoolean("setback", false)) return;
+ org.bukkit.Location safe = data.getLastSafeLocation();
+ if (safe == null) return;
+ data.clearPositionHistory();
+ player.teleport(safe);
+ }
+
+ protected boolean isServerLagging() {
+ if (!plugin.getConfigManager().getBoolean("tps.enabled", true)) return false;
+ double tps = Bukkit.getTPS()[0];
+ double minTps = plugin.getConfigManager().getDouble("tps.min_tps_threshold", 18.0);
+ return tps < minTps;
+ }
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastPlaceCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastPlaceCheck.java
index acd704b..f70c88c 100644
--- a/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastPlaceCheck.java
+++ b/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastPlaceCheck.java
@@ -35,7 +35,14 @@ public class FastPlaceCheck extends Check {
int bps = data.getBlocksPerSecond();
if (bps > maxBlocksPerSecond) {
- flag(data, player, (bps - maxBlocksPerSecond) * 0.5);
+ data.incrementFastPlaceBuffer();
+ int buffer = getConfigInt("buffer_ticks", 2);
+ if (data.getFastPlaceBuffer() >= buffer) {
+ flag(data, player, (bps - maxBlocksPerSecond) * 0.5);
+ data.resetFastPlaceBuffer();
+ }
+ } else {
+ data.resetFastPlaceBuffer();
}
}
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/FlyCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/FlyCheck.java
index 5c51c4e..33071b6 100644
--- a/src/main/java/com/xeroth/xeroanticheat/checks/movement/FlyCheck.java
+++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/FlyCheck.java
@@ -31,6 +31,9 @@ public class FlyCheck extends Check {
// Ignore if player has bypass permission
if (isBypassed(player)) return;
+ // Skip if server is lagging
+ if (isServerLagging()) return;
+
// Ignore elytra gliding
if (player.isGliding()) {
return;
@@ -56,6 +59,7 @@ public class FlyCheck extends Check {
if (clientOnGround != serverOnGround) {
if (data.getAirTicks() > fallBuffer + desyncThreshold) {
flag(data, player);
+ setback(player, data);
}
}
@@ -69,6 +73,7 @@ public class FlyCheck extends Check {
if (data.getAirTicks() > fallBuffer) {
if (!data.hasJumpBoost()) {
flag(data, player);
+ setback(player, data);
}
}
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/GlideCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/GlideCheck.java
index 4b1476a..47c8069 100644
--- a/src/main/java/com/xeroth/xeroanticheat/checks/movement/GlideCheck.java
+++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/GlideCheck.java
@@ -30,6 +30,9 @@ public class GlideCheck extends Check {
// Ignore if player has bypass permission
if (isBypassed(player)) return;
+ // Skip if server is lagging
+ if (isServerLagging()) return;
+
// Get thresholds
double minHorizontalSpeed = getConfigDouble("min_horizontal_speed", 0.5);
double maxYDecrease = getConfigDouble("max_y_decrease", 0.1);
@@ -63,6 +66,7 @@ public class GlideCheck extends Check {
if (data.getGlideTicks() > 5) {
flag(data, player);
+ setback(player, data);
}
} else {
data.resetGlideTicks();
diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/JesusCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/JesusCheck.java
index 3927df9..e09a484 100644
--- a/src/main/java/com/xeroth/xeroanticheat/checks/movement/JesusCheck.java
+++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/JesusCheck.java
@@ -76,7 +76,15 @@ public class JesusCheck extends Check {
// If moving at reasonable speed on water, flag
if (horizontalSpeedSq > 0.01) {
- flag(data, player);
+ data.incrementJesusBuffer();
+ int buffer = getConfigInt("buffer_ticks", 3);
+ if (data.getJesusBuffer() >= buffer) {
+ flag(data, player);
+ setback(player, data);
+ data.resetJesusBuffer();
+ }
+ } else {
+ data.resetJesusBuffer();
}
}
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/PhaseCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/PhaseCheck.java
index 467b774..e2b17fc 100644
--- a/src/main/java/com/xeroth/xeroanticheat/checks/movement/PhaseCheck.java
+++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/PhaseCheck.java
@@ -60,6 +60,7 @@ public class PhaseCheck extends Check {
if (result != null && result.getHitBlock() != null
&& result.getHitBlock().getType().isSolid()) {
flag(data, player, 3.0);
+ setback(player, data);
}
}
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpeedCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpeedCheck.java
index 58ef5e3..b77146c 100644
--- a/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpeedCheck.java
+++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpeedCheck.java
@@ -84,6 +84,7 @@ public class SpeedCheck extends Check {
int bufferTicks = getConfigInt("buffer_ticks", 5);
if (data.getSpeedViolationTicks() >= bufferTicks) {
flag(data, player, (speed - maxSpeed) * 2);
+ setback(player, data);
data.resetSpeedViolationTicks();
}
} else {
diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpiderCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpiderCheck.java
index fb9ac8a..4a8019d 100644
--- a/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpiderCheck.java
+++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpiderCheck.java
@@ -31,6 +31,9 @@ public class SpiderCheck extends Check {
// Ignore if player has bypass permission
if (isBypassed(player)) return;
+ // Skip if server is lagging
+ if (isServerLagging()) return;
+
// Get velocity
org.bukkit.util.Vector velocity = player.getVelocity();
@@ -69,6 +72,7 @@ public class SpiderCheck extends Check {
if (data.getSpiderTicks() > 5 && velocity.getY() > 0.1) {
flag(data, player);
+ setback(player, data);
}
} else {
data.resetSpiderTicks();
diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/TimerCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/TimerCheck.java
index 11b876b..1fa30a5 100644
--- a/src/main/java/com/xeroth/xeroanticheat/checks/movement/TimerCheck.java
+++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/TimerCheck.java
@@ -36,7 +36,14 @@ public class TimerCheck extends Check {
// If ProtocolLib is active, packet counting is handled by PacketListener
if (plugin.isProtocolLibLoaded()) {
if (data.getPacketsThisSecond() > maxPacketsPerSecond) {
- flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5);
+ data.incrementTimerBuffer();
+ int buffer = getConfigInt("buffer_ticks", 2);
+ if (data.getTimerBuffer() >= buffer) {
+ flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5);
+ data.resetTimerBuffer();
+ }
+ } else {
+ data.resetTimerBuffer();
}
return;
}
@@ -53,7 +60,14 @@ public class TimerCheck extends Check {
// Check for too many packets
if (data.getPacketsThisSecond() > maxPacketsPerSecond) {
- flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5);
+ data.incrementTimerBuffer();
+ int buffer = getConfigInt("buffer_ticks", 2);
+ if (data.getTimerBuffer() >= buffer) {
+ flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5);
+ data.resetTimerBuffer();
+ }
+ } else {
+ data.resetTimerBuffer();
}
data.setLastMovePacketTime(now);
diff --git a/src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java b/src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java
index 0e242d2..ae2b906 100644
--- a/src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java
+++ b/src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java
@@ -79,6 +79,18 @@ public class XACCommand implements CommandExecutor, TabCompleter {
}
toggleAlerts(sender, args[1]);
}
+ case "debug" -> {
+ if (!has(sender, "xac.command.verbose")) return true;
+ if (args.length < 3) {
+ sender.sendMessage(usage("/xac debug "));
+ return true;
+ }
+ showDebug(sender, args[1], args[2]);
+ }
+ case "stats" -> {
+ if (!has(sender, "xac.admin")) return true;
+ showStats(sender);
+ }
case "version" -> showVersion(sender);
default -> sendHelp(sender);
}
@@ -107,7 +119,9 @@ public class XACCommand implements CommandExecutor, TabCompleter {
new Cmd("/xac punish ", "manual punishment", "xac.command.punish"),
new Cmd("/xac clearviolations ", "clear all VL for player", "xac.command.clearviolations"),
new Cmd("/xac verbose ", "toggle per-flag debug", "xac.command.verbose"),
+ new Cmd("/xac debug ", "show check debug info", "xac.command.verbose"),
new Cmd("/xac alerts [on|off]", "toggle alert receiving", "xac.command.alerts"),
+ new Cmd("/xac stats", "show plugin stats", "xac.admin"),
new Cmd("/xac version", "show version", "xac.command.version")
).forEach(cmd -> {
if (sender.hasPermission(cmd.perm()) || sender.hasPermission("xac.admin")) {
@@ -219,11 +233,71 @@ public class XACCommand implements CommandExecutor, TabCompleter {
NamedTextColor.GOLD));
sender.sendMessage(Component.text("Built for Paper 1.21.x", NamedTextColor.WHITE));
}
+
+ private void showStats(CommandSender sender) {
+ var m = plugin.getMetricsManager();
+ sender.sendMessage(Component.text("— XeroAntiCheat Stats —", NamedTextColor.GOLD));
+ sender.sendMessage(Component.text("Uptime: " + m.getUptimeSeconds() + "s", NamedTextColor.WHITE));
+ sender.sendMessage(Component.text("Checks run: " + m.getTotalChecks(), NamedTextColor.WHITE));
+ sender.sendMessage(Component.text("Checks/sec: " + String.format("%.1f", m.getChecksPerSecond()), NamedTextColor.WHITE));
+ sender.sendMessage(Component.text("Flags issued: " + m.getTotalFlags(), NamedTextColor.WHITE));
+ sender.sendMessage(Component.text("Punishments: " + m.getTotalPunishments(), NamedTextColor.WHITE));
+ sender.sendMessage(Component.text("Players monitored: " + Bukkit.getOnlinePlayers().size(), NamedTextColor.WHITE));
+ }
+
+ private void showDebug(CommandSender sender, String playerName, String checkName) {
+ Player target = Bukkit.getPlayer(playerName);
+ if (target == null) {
+ sender.sendMessage(Component.text("Player not found.", NamedTextColor.RED));
+ return;
+ }
+
+ PlayerData data = plugin.getViolationManager().getPlayerData(target);
+ if (data == null) {
+ sender.sendMessage(Component.text("No data available.", NamedTextColor.RED));
+ return;
+ }
+
+ sender.sendMessage(Component.text(
+ "— Debug: " + checkName + " for " + target.getName() + " —",
+ NamedTextColor.GOLD));
+
+ sender.sendMessage(Component.text("VL: " + String.format("%.1f", data.getViolationLevel(checkName)), NamedTextColor.WHITE));
+ sender.sendMessage(Component.text("Ping: " + target.getPing() + "ms", NamedTextColor.WHITE));
+ sender.sendMessage(Component.text("airTicks: " + data.getAirTicks(), NamedTextColor.WHITE));
+ sender.sendMessage(Component.text("onGround(server): " + target.isOnGround(), NamedTextColor.WHITE));
+
+ switch (checkName.toLowerCase()) {
+ case "speed" -> {
+ PlayerData.PositionSnapshot curr = data.getLastPosition();
+ PlayerData.PositionSnapshot prev = data.getSecondLastPosition();
+ if (curr != null && prev != null) {
+ double dx = curr.x() - prev.x();
+ double dz = curr.z() - prev.z();
+ double speed = Math.sqrt(dx*dx + dz*dz);
+ sender.sendMessage(Component.text("speed(measured): " + String.format("%.4f", speed), NamedTextColor.WHITE));
+ sender.sendMessage(Component.text("speedViolationTicks: " + data.getSpeedViolationTicks(), NamedTextColor.WHITE));
+ }
+ }
+ case "killaura" -> {
+ sender.sendMessage(Component.text("lastAttackYaw: " + data.getLastAttackYaw(), NamedTextColor.WHITE));
+ }
+ case "autoclicker" -> {
+ sender.sendMessage(Component.text("CPS: " + data.getCPS(), NamedTextColor.WHITE));
+ }
+ case "spider" -> {
+ sender.sendMessage(Component.text("spiderTicks: " + data.getSpiderTicks(), NamedTextColor.WHITE));
+ }
+ case "glide" -> {
+ sender.sendMessage(Component.text("glideTicks: " + data.getGlideTicks(), NamedTextColor.WHITE));
+ }
+ }
+ }
@Override
public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) {
- return Stream.of("reload","status","punish","clearviolations","verbose","alerts","version")
+ return Stream.of("reload","status","punish","clearviolations","verbose","debug","alerts","stats","version")
.filter(sub -> sender.hasPermission("xac.command." + sub)
|| sender.hasPermission("xac.admin"))
.filter(sub -> sub.startsWith(args[0].toLowerCase()))
@@ -231,7 +305,7 @@ public class XACCommand implements CommandExecutor, TabCompleter {
}
if (args.length == 2) {
String sub = args[0].toLowerCase();
- if (List.of("status","punish","clearviolations","verbose").contains(sub)) {
+ if (List.of("status","punish","clearviolations","verbose","debug").contains(sub)) {
return Bukkit.getOnlinePlayers().stream()
.map(Player::getName)
.filter(n -> n.toLowerCase().startsWith(args[1].toLowerCase()))
@@ -245,6 +319,12 @@ public class XACCommand implements CommandExecutor, TabCompleter {
.filter(n -> n.toLowerCase().startsWith(args[2].toLowerCase()))
.toList();
}
+ if (args.length == 3 && args[0].equalsIgnoreCase("debug")) {
+ return plugin.getCheckManager().getRegisteredChecks().stream()
+ .map(c -> c.getName())
+ .filter(n -> n.toLowerCase().startsWith(args[2].toLowerCase()))
+ .toList();
+ }
return List.of();
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java b/src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java
index 0308931..6c76f08 100644
--- a/src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java
+++ b/src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java
@@ -111,6 +111,18 @@ public class PlayerData {
// SpeedCheck tracking
private int speedViolationTicks = 0;
+ // Universal buffers for checks without built-in tick counters
+ private int jesusBuffer = 0;
+ private int timerBuffer = 0;
+ private int reachBuffer = 0;
+ private int killAuraBuffer = 0;
+ private int fastPlaceBuffer = 0;
+ private int scaffoldBuffer = 0;
+ private int fastEatBuffer = 0;
+
+ // Last safe location for setback
+ private Location lastSafeLocation = null;
+
// Alert cooldown tracking
private final Map lastWarnTime = new ConcurrentHashMap<>();
@@ -621,6 +633,41 @@ public class PlayerData {
this.speedViolationTicks = 0;
}
+ // Jesus buffer
+ public int getJesusBuffer() { return jesusBuffer; }
+ public void incrementJesusBuffer() { jesusBuffer++; }
+ public void resetJesusBuffer() { jesusBuffer = 0; }
+
+ // Timer buffer
+ public int getTimerBuffer() { return timerBuffer; }
+ public void incrementTimerBuffer() { timerBuffer++; }
+ public void resetTimerBuffer() { timerBuffer = 0; }
+
+ // Reach buffer
+ public int getReachBuffer() { return reachBuffer; }
+ public void incrementReachBuffer() { reachBuffer++; }
+ public void resetReachBuffer() { reachBuffer = 0; }
+
+ // KillAura buffer
+ public int getKillAuraBuffer() { return killAuraBuffer; }
+ public void incrementKillAuraBuffer() { killAuraBuffer++; }
+ public void resetKillAuraBuffer() { killAuraBuffer = 0; }
+
+ // FastPlace buffer
+ public int getFastPlaceBuffer() { return fastPlaceBuffer; }
+ public void incrementFastPlaceBuffer() { fastPlaceBuffer++; }
+ public void resetFastPlaceBuffer() { fastPlaceBuffer = 0; }
+
+ // Scaffold buffer
+ public int getScaffoldBuffer() { return scaffoldBuffer; }
+ public void incrementScaffoldBuffer() { scaffoldBuffer++; }
+ public void resetScaffoldBuffer() { scaffoldBuffer = 0; }
+
+ // FastEat buffer
+ public int getFastEatBuffer() { return fastEatBuffer; }
+ public void incrementFastEatBuffer() { fastEatBuffer++; }
+ public void resetFastEatBuffer() { fastEatBuffer = 0; }
+
public long getLastWarnTime(String checkName) {
return lastWarnTime.getOrDefault(checkName, 0L);
}
@@ -636,6 +683,21 @@ public class PlayerData {
spiderTicks = 0;
glideTicks = 0;
speedViolationTicks = 0;
+ jesusBuffer = 0;
+ timerBuffer = 0;
+ reachBuffer = 0;
+ killAuraBuffer = 0;
+ fastPlaceBuffer = 0;
+ scaffoldBuffer = 0;
+ lastSafeLocation = null;
+ }
+
+ public Location getLastSafeLocation() {
+ return lastSafeLocation;
+ }
+
+ public void setLastSafeLocation(Location loc) {
+ this.lastSafeLocation = loc != null ? loc.clone() : null;
}
public Deque getBlockPlaceTimestamps() {
diff --git a/src/main/java/com/xeroth/xeroanticheat/listener/CombatListener.java b/src/main/java/com/xeroth/xeroanticheat/listener/CombatListener.java
index 4150412..9dac402 100644
--- a/src/main/java/com/xeroth/xeroanticheat/listener/CombatListener.java
+++ b/src/main/java/com/xeroth/xeroanticheat/listener/CombatListener.java
@@ -52,17 +52,31 @@ public class CombatListener implements Listener {
// Check for reach
if (reachCheck != null && reachCheck.checkReach(player, target)) {
if (!reachCheck.isBypassed(player)) {
- plugin.getViolationManager().addViolation(player, "Reach", 1.0);
- plugin.getPunishmentManager().evaluate(player, "Reach");
+ int buffer = plugin.getConfigManager().getInt("checks.reach.buffer_hits", 2);
+ data.incrementReachBuffer();
+ if (data.getReachBuffer() >= buffer) {
+ plugin.getViolationManager().addViolation(player, "Reach", 1.0);
+ plugin.getPunishmentManager().evaluate(player, "Reach");
+ data.resetReachBuffer();
+ }
}
+ } else {
+ data.resetReachBuffer();
}
// Check for kill aura (angle)
if (killAuraCheck != null && killAuraCheck.checkAngle(player, target)) {
if (!killAuraCheck.isBypassed(player)) {
- plugin.getViolationManager().addViolation(player, "KillAura", 1.0);
- plugin.getPunishmentManager().evaluate(player, "KillAura");
+ int buffer = plugin.getConfigManager().getInt("checks.killaura.buffer_ticks", 2);
+ data.incrementKillAuraBuffer();
+ if (data.getKillAuraBuffer() >= buffer) {
+ plugin.getViolationManager().addViolation(player, "KillAura", 1.0);
+ plugin.getPunishmentManager().evaluate(player, "KillAura");
+ data.resetKillAuraBuffer();
+ }
}
+ } else {
+ data.resetKillAuraBuffer();
}
// Check for multi-target
diff --git a/src/main/java/com/xeroth/xeroanticheat/listener/MiscListener.java b/src/main/java/com/xeroth/xeroanticheat/listener/MiscListener.java
index a5556cc..a2f5a92 100644
--- a/src/main/java/com/xeroth/xeroanticheat/listener/MiscListener.java
+++ b/src/main/java/com/xeroth/xeroanticheat/listener/MiscListener.java
@@ -55,9 +55,16 @@ public class MiscListener implements Listener {
ScaffoldCheck scaffoldCheck = (ScaffoldCheck) plugin.getCheckManager().getCheck("Scaffold");
if (scaffoldCheck != null && scaffoldCheck.checkScaffold(player, event.getBlockPlaced(), data)) {
if (!scaffoldCheck.isBypassed(player)) {
- plugin.getViolationManager().addViolation(player, "Scaffold", 1.0);
- plugin.getPunishmentManager().evaluate(player, "Scaffold");
+ int buffer = plugin.getConfigManager().getInt("checks.scaffold.buffer_ticks", 2);
+ data.incrementScaffoldBuffer();
+ if (data.getScaffoldBuffer() >= buffer) {
+ plugin.getViolationManager().addViolation(player, "Scaffold", 1.0);
+ plugin.getPunishmentManager().evaluate(player, "Scaffold");
+ data.resetScaffoldBuffer();
+ }
}
+ } else {
+ data.resetScaffoldBuffer();
}
}
@@ -79,9 +86,16 @@ public class MiscListener implements Listener {
FastEatCheck fastEatCheck = (FastEatCheck) plugin.getCheckManager().getCheck("FastEat");
if (fastEatCheck != null && fastEatCheck.checkFastEat(player, data, System.currentTimeMillis())) {
if (!fastEatCheck.isBypassed(player)) {
- plugin.getViolationManager().addViolation(player, "FastEat", 1.0);
- plugin.getPunishmentManager().evaluate(player, "FastEat");
+ int buffer = plugin.getConfigManager().getInt("checks.fasteat.buffer_ticks", 2);
+ data.incrementFastEatBuffer();
+ if (data.getFastEatBuffer() >= buffer) {
+ plugin.getViolationManager().addViolation(player, "FastEat", 1.0);
+ plugin.getPunishmentManager().evaluate(player, "FastEat");
+ data.resetFastEatBuffer();
+ }
}
+ } else {
+ data.resetFastEatBuffer();
}
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/listener/MovementListener.java b/src/main/java/com/xeroth/xeroanticheat/listener/MovementListener.java
index cfa7fd1..8515d66 100644
--- a/src/main/java/com/xeroth/xeroanticheat/listener/MovementListener.java
+++ b/src/main/java/com/xeroth/xeroanticheat/listener/MovementListener.java
@@ -94,6 +94,7 @@ public class MovementListener implements Listener {
} else {
data.setWasAirborne(false);
data.resetAirTicks();
+ data.setLastSafeLocation(event.getTo());
}
// Check ice at feet
@@ -135,5 +136,6 @@ public class MovementListener implements Listener {
data.resetAirTicks();
data.clearServerVelocity();
data.setLastPlacementYaw(Float.NaN);
+ data.setLastSafeLocation(event.getTo());
}
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/manager/CheckManager.java b/src/main/java/com/xeroth/xeroanticheat/manager/CheckManager.java
index 9211065..99ea4c1 100644
--- a/src/main/java/com/xeroth/xeroanticheat/manager/CheckManager.java
+++ b/src/main/java/com/xeroth/xeroanticheat/manager/CheckManager.java
@@ -47,6 +47,7 @@ public class CheckManager {
public void runCheck(String checkName, PlayerData data, Player player) {
Check check = checksByName.get(checkName.toLowerCase());
if (check != null && check.isEnabled()) {
+ plugin.getMetricsManager().recordCheck();
check.check(data, player);
}
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/manager/MetricsManager.java b/src/main/java/com/xeroth/xeroanticheat/manager/MetricsManager.java
new file mode 100644
index 0000000..291dbf0
--- /dev/null
+++ b/src/main/java/com/xeroth/xeroanticheat/manager/MetricsManager.java
@@ -0,0 +1,51 @@
+package com.xeroth.xeroanticheat.manager;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+public class MetricsManager {
+
+ private final AtomicLong totalChecksRun = new AtomicLong();
+ private final AtomicLong totalFlagsIssued = new AtomicLong();
+ private final AtomicLong totalPunishments = new AtomicLong();
+ private long startTimeMs = System.currentTimeMillis();
+
+ public void recordCheck() {
+ totalChecksRun.incrementAndGet();
+ }
+
+ public void recordFlag() {
+ totalFlagsIssued.incrementAndGet();
+ }
+
+ public void recordPunishment() {
+ totalPunishments.incrementAndGet();
+ }
+
+ public long getTotalChecks() {
+ return totalChecksRun.get();
+ }
+
+ public long getTotalFlags() {
+ return totalFlagsIssued.get();
+ }
+
+ public long getTotalPunishments() {
+ return totalPunishments.get();
+ }
+
+ public long getUptimeSeconds() {
+ return (System.currentTimeMillis() - startTimeMs) / 1000;
+ }
+
+ public double getChecksPerSecond() {
+ long uptime = getUptimeSeconds();
+ return uptime > 0 ? (double) totalChecksRun.get() / uptime : 0;
+ }
+
+ public void reset() {
+ totalChecksRun.set(0);
+ totalFlagsIssued.set(0);
+ totalPunishments.set(0);
+ startTimeMs = System.currentTimeMillis();
+ }
+}
diff --git a/src/main/java/com/xeroth/xeroanticheat/manager/PunishmentManager.java b/src/main/java/com/xeroth/xeroanticheat/manager/PunishmentManager.java
index dab874f..c09c3b4 100644
--- a/src/main/java/com/xeroth/xeroanticheat/manager/PunishmentManager.java
+++ b/src/main/java/com/xeroth/xeroanticheat/manager/PunishmentManager.java
@@ -123,6 +123,7 @@ public class PunishmentManager {
kickCmd = kickCmd.replace("%player%", playerName).replace("%reason%", reason);
executeCommand(kickCmd);
logPunishment(type, player, checkName, vl);
+ plugin.getMetricsManager().recordPunishment();
}
case "TEMPBAN" -> {
String tempbanCmd = plugin.getConfigManager().getString("punishments.tempban_command",
@@ -130,6 +131,7 @@ public class PunishmentManager {
tempbanCmd = tempbanCmd.replace("%player%", playerName).replace("%reason%", reason);
executeCommand(tempbanCmd);
logPunishment(type, player, checkName, vl);
+ plugin.getMetricsManager().recordPunishment();
}
case "PERMBAN" -> {
String permbanCmd = plugin.getConfigManager().getString("punishments.permban_command",
@@ -137,6 +139,7 @@ public class PunishmentManager {
permbanCmd = permbanCmd.replace("%player%", playerName).replace("%reason%", reason);
executeCommand(permbanCmd);
logPunishment(type, player, checkName, vl);
+ plugin.getMetricsManager().recordPunishment();
}
}
}
diff --git a/src/main/java/com/xeroth/xeroanticheat/manager/ViolationManager.java b/src/main/java/com/xeroth/xeroanticheat/manager/ViolationManager.java
index 638b28a..a0932b3 100644
--- a/src/main/java/com/xeroth/xeroanticheat/manager/ViolationManager.java
+++ b/src/main/java/com/xeroth/xeroanticheat/manager/ViolationManager.java
@@ -83,6 +83,8 @@ public class ViolationManager {
if (plugin.getConfigManager().isDebug()) {
plugin.getLogger().info(player.getName() + " violated " + checkName + " (VL: " + newVl + ")");
}
+
+ plugin.getMetricsManager().recordFlag();
}
/**
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index ec80946..aae9c12 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -40,6 +40,8 @@ checks:
# ----------------------------------------
speed:
enabled: true
+ # Teleport player back to last safe location when flagged
+ setback: false
# Base maximum speed (blocks per tick)
max_speed: 0.56
# Ping compensation factor (scales latency leniency)
@@ -58,6 +60,8 @@ checks:
# ----------------------------------------
fly:
enabled: true
+ # Teleport player back to last safe location when flagged
+ setback: false
# Number of ticks to allow for stepping/slabs
fall_buffer: 10
# Maximum ground desync ticks before flagging
@@ -73,6 +77,10 @@ checks:
# ----------------------------------------
jesus:
enabled: true
+ # Teleport player back to last safe location when flagged
+ setback: false
+ # Number of consecutive ticks to flag before VL is added
+ buffer_ticks: 3
warn_vl: 10
kick_vl: 25
tempban_vl: 50
@@ -97,6 +105,8 @@ checks:
# ----------------------------------------
timer:
enabled: true
+ # Number of consecutive ticks exceeding max packets before flagging
+ buffer_ticks: 2
# Maximum packets per second allowed
max_packets_per_second: 22
# Milliseconds of no packets before flagging blink
@@ -112,6 +122,8 @@ checks:
# ----------------------------------------
spider:
enabled: true
+ # Teleport player back to last safe location when flagged
+ setback: false
warn_vl: 10
kick_vl: 25
tempban_vl: 50
@@ -123,6 +135,8 @@ checks:
# ----------------------------------------
glide:
enabled: true
+ # Teleport player back to last safe location when flagged
+ setback: false
# Minimum horizontal speed for glide detection
min_horizontal_speed: 0.5
# Maximum Y decrease per tick for glide curve
@@ -138,6 +152,8 @@ checks:
# ----------------------------------------
killaura:
enabled: true
+ # Number of consecutive out-of-angle attacks before flagging
+ buffer_ticks: 2
# Maximum angle in degrees from look direction
max_angle: 100
# Maximum rotation change between attacks
@@ -155,6 +171,8 @@ checks:
# ----------------------------------------
reach:
enabled: true
+ # Number of consecutive attacks exceeding reach before flagging
+ buffer_hits: 2
# Maximum reach in blocks (survival)
max_reach: 3.2
# Maximum reach in blocks (creative)
@@ -198,6 +216,8 @@ checks:
# ----------------------------------------
fastplace:
enabled: true
+ # Number of consecutive ticks exceeding max blocks before flagging
+ buffer_ticks: 2
# Maximum blocks per second
max_blocks_per_second: 20
warn_vl: 10
@@ -211,6 +231,8 @@ checks:
# ----------------------------------------
phase:
enabled: true
+ # Teleport player back to last safe location when flagged
+ setback: false
# Minimum movement distance before ray-cast runs (blocks)
min_distance: 0.5
# Maximum movement delta — larger values are treated as teleports
@@ -241,6 +263,8 @@ checks:
# ----------------------------------------
scaffold:
enabled: true
+ # Number of consecutive signal accumulations before flagging
+ buffer_ticks: 2
# Minimum pitch angle for suspicious placement
min_pitch: 75
# Number of signals required to flag
@@ -264,6 +288,8 @@ checks:
# ----------------------------------------
fasteat:
enabled: true
+ # Number of consecutive fast eats before flagging
+ buffer_ticks: 2
# Maximum eating duration in ticks (32 = 1.6s)
max_eat_ticks: 32
warn_vl: 10
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index b363e8d..afb1536 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -1,5 +1,5 @@
name: XeroAntiCheat
-version: 1.1.3
+version: 1.2.0
main: com.xeroth.xeroanticheat.XeroAntiCheat
author: Xeroth
description: Lightweight, accurate anti-cheat for Paper 1.21.x