From 112a61cf0cc62c851485f8fc8f27faf8a6c806f3 Mon Sep 17 00:00:00 2001 From: Axel Date: Sun, 15 Mar 2026 13:17:28 -0300 Subject: [PATCH] v1.2.0: Implement improvement plan features - Setback System: Teleports flagged players to lastSafeLocation (opt-in per check) - TPS Lag Compensation: isServerLagging() helper, guards in Fly/Spider/Glide checks - Universal Buffer System: Buffer fields for Jesus/Reach/KillAura/Timer/FastPlace/Scaffold/FastEat - /xac debug command: Shows check-specific debug info for players - Public API: XACApi with isFlagged(), getViolationLevel(), getTotalViolations(), isBypassed() - Performance Metrics: /xac stats command with checks/flags/punishments tracking --- pom.xml | 2 +- .../xeroth/xeroanticheat/XeroAntiCheat.java | 14 ++++ .../com/xeroth/xeroanticheat/api/XACApi.java | 42 ++++++++++ .../com/xeroth/xeroanticheat/check/Check.java | 16 ++++ .../checks/misc/FastPlaceCheck.java | 9 +- .../checks/movement/FlyCheck.java | 5 ++ .../checks/movement/GlideCheck.java | 4 + .../checks/movement/JesusCheck.java | 10 ++- .../checks/movement/PhaseCheck.java | 1 + .../checks/movement/SpeedCheck.java | 1 + .../checks/movement/SpiderCheck.java | 4 + .../checks/movement/TimerCheck.java | 18 +++- .../xeroanticheat/command/XACCommand.java | 84 ++++++++++++++++++- .../xeroth/xeroanticheat/data/PlayerData.java | 62 ++++++++++++++ .../listener/CombatListener.java | 22 ++++- .../xeroanticheat/listener/MiscListener.java | 22 ++++- .../listener/MovementListener.java | 2 + .../xeroanticheat/manager/CheckManager.java | 1 + .../xeroanticheat/manager/MetricsManager.java | 51 +++++++++++ .../manager/PunishmentManager.java | 3 + .../manager/ViolationManager.java | 2 + src/main/resources/config.yml | 26 ++++++ src/main/resources/plugin.yml | 2 +- 23 files changed, 387 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/xeroth/xeroanticheat/api/XACApi.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/manager/MetricsManager.java 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