Compare commits

...

12 Commits

Author SHA1 Message Date
0999c3e264 Tune: Increase buffer and VL thresholds to reduce false positives
- Timer: buffer_ticks 2->10, max_packets 22->25, kick_vl 25->50
- Jesus: buffer_ticks 3->15, kick_vl 25->50
- Fly: fall_buffer 10->15, ground_desync 3->5, kick_vl 25->50
2026-03-15 13:39:21 -03:00
dd0700ab19 Fix: Remove adventure shading - use Paper's built-in adventure API 2026-03-15 13:33:49 -03:00
b63e046f84 Fix: Add adventure-text-minimessage as explicit dependency for shading 2026-03-15 13:27:50 -03:00
0073a26e9c Fix: Shade adventure-text-minimessage dependency
The MiniMessage class was not being shaded, causing ClassNotFoundException at runtime.
2026-03-15 13:23:54 -03:00
112a61cf0c 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
2026-03-15 13:17:28 -03:00
a4a87e62de v1.1.3: Fix config/code consistency issues
- SpeedCheck: tps.enabled and tps.min_tps_threshold now work (was hardcoded)
- ConfigManager/config.yml: Removed orphaned async_task_threads and commands.* keys
- PunishmentManager: database.enabled now correctly disables SQLite logging
- PacketListener: Removed dead code (updatePacketTiming, recordClick, recordAttack)
2026-03-15 12:47:43 -03:00
95e0915d67 v1.1.2: fix reload decay task, nofall blocks, config cleanup, sqrt removal 2026-03-15 12:36:46 -03:00
68e99adf3e v1.1.1: fix CriticalCheck dead code, optimize InventoryMoveCheck 2026-03-15 12:26:28 -03:00
ac5a8e807b XeroAntiCheat v1.1.0 bug fixes 2026-03-15 03:59:30 -03:00
8190b39160 XeroAntiCheat v1.0.9 bug fixes
- BUG-1: Added alerts.cooldown_ms (default 5s) to throttle warn() and
  sendAlert() in PunishmentManager, preventing chat spam during high-frequency
  flags. Verbose output in ViolationManager also respects the cooldown.
  Kick/ban punishments always fire immediately regardless of cooldown.
- BUG-2: Fixed FlyCheck false positive with Jump Boost I - condition was
  incorrectly flagging players with Jump Boost level 1. Now exempts all
  jump boost levels from the sustained-flight flag.
- BUG-3: Optimized SpiderCheck by caching player.getLocation() in a single
  variable instead of calling it 3 times.
2026-03-15 03:51:27 -03:00
38ab1abaf1 XeroAntiCheat v1.0.8 bug fixes
- SpiderCheck: fixed Location mutation bug - bodyBlock was reading y-1 (same as feetBlock) and headBlock was reading y instead of y+1. Now uses block coordinates directly.
- ConfigManager: decay task no longer reads YamlConfiguration from background thread. decayRate is now volatile and refreshed on reload from main thread only.
- JesusCheck, SpeedCheck, NoFallCheck: loc.subtract() now uses .clone() to prevent silent Location mutation.
- VelocityCheck: decrementVelocityCheckTicks() moved past the minExpected threshold check to avoid consuming a tick on packets that are immediately discarded.
2026-03-15 03:42:24 -03:00
daccfedae6 XeroAntiCheat v1.0.8 bug fixes
- SpiderCheck: fixed Location mutation bug - bodyBlock was reading y-1 (same as feetBlock) and headBlock was reading y instead of y+1. Now uses block coordinates directly.
- ConfigManager: decay task no longer reads YamlConfiguration from background thread. decayRate is now volatile and refreshed on reload from main thread only.
- JesusCheck, SpeedCheck, NoFallCheck: loc.subtract() now uses .clone() to prevent silent Location mutation.
- VelocityCheck: decrementVelocityCheckTicks() moved past the minExpected threshold check to avoid consuming a tick on packets that are immediately discarded.
2026-03-15 03:39:39 -03:00
34 changed files with 578 additions and 234 deletions

19
.gitignore vendored
View File

@@ -1,4 +1,4 @@
# ---> Java # Java
# Compiled class file # Compiled class file
*.class *.class
@@ -11,7 +11,7 @@
# Mobile Tools for Java (J2ME) # Mobile Tools for Java (J2ME)
.mtj.tmp/ .mtj.tmp/
# Package Files # # Package Files
*.jar *.jar
*.war *.war
*.nar *.nar
@@ -20,7 +20,20 @@
*.tar.gz *.tar.gz
*.rar *.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs
hs_err_pid* hs_err_pid*
replay_pid* replay_pid*
# Eclipse
.classpath
.project
.settings/
.factorypath
# IntelliJ IDEA
.idea/
*.iml
# Maven
dependency-reduced-pom.xml
target/

View File

@@ -1,2 +1,29 @@
# XeroAntiCheat # XeroAntiCheat
Lightweight, accurate anti-cheat for Paper 1.21.x
## Latest Updates (v1.1.3)
- **SpeedCheck**: `tps.min_tps_threshold` and `tps.enabled` config keys now actually work — previously hardcoded as `18.0` and always-on.
- **config.yml**: Removed orphaned `async_task_threads` key and entire `commands:` section — these were never read by the plugin.
- **PunishmentManager**: `database.enabled: false` now correctly disables SQLite punishment logging as documented.
- **PacketListener**: Removed three unreachable fallback methods (`updatePacketTiming`, `recordClick`, `recordAttack`) — dead code since v1.0.4.
## Latest Updates (v1.1.2)
- **reload**: `violation.decay_interval` changes now take effect immediately — the decay task is cancelled and recreated on every `/xac reload`.
- **NoFallCheck**: Added Beds (all 16 colour variants via `Tag.BEDS`) and Powder Snow to the list of blocks that cancel fall damage. Eliminates false positives when players land on these blocks.
- **config.yml**: Removed orphaned `allow_jump_crits` key from `checks.critical` section (was removed from code in v1.1.1).
- **JesusCheck, GlideCheck**: Eliminated `Math.sqrt()` from hot-path threshold comparisons.
## Latest Updates (v1.1.1)
- **CriticalCheck**: Removed dead code — the "no-air crit" detection branch was logically unreachable because `isCritical=true` requires `!isOnGround`, making the subsequent `isOnGround` check always false. The check now only flags "crit while sprinting" (the only branch that could actually fire). Removed `allow_jump_crits` config key. (Future enhancement: Option B damage-ratio detection.)
- **InventoryMoveCheck**: Replaced `Math.sqrt()` with squared distance comparison (`distanceSquared > 0.01`), consistent with the ReachCheck optimisation.
## Latest Updates (v1.1.0)
- **ReachCheck**: Now measures distance to entity bounding box center instead of feet. Eliminates false negatives when attacking tall entities (horses, iron golems, withers). Also switched from `distance()` to `distanceSquared()` comparison, removing a `Math.sqrt()` from the hot path.
- **AutoClickerCheck**: `checkPattern()` rewritten with zero-allocation two-pass iterator approach. Previously allocated two `ArrayList` objects on every combat click.
- **TimerCheck**: Removed redundant blink detection from `check()` method. Blink detection is fully handled by the 5-tick scheduled task. `setLastMovePacketTime()` retained to feed the task.
- **KillAuraCheck**: Removed unused `EntityEffect` import.

View File

@@ -6,7 +6,7 @@
<groupId>com.xeroth</groupId> <groupId>com.xeroth</groupId>
<artifactId>xeroanticheat</artifactId> <artifactId>xeroanticheat</artifactId>
<version>1.0.7</version> <version>1.2.0</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>XeroAntiCheat</name> <name>XeroAntiCheat</name>
@@ -80,10 +80,6 @@
</goals> </goals>
<configuration> <configuration>
<relocations> <relocations>
<relocation>
<pattern>net.kyori.adventure</pattern>
<shadedPattern>com.xeroth.xeroanticheat.adventure</shadedPattern>
</relocation>
<relocation> <relocation>
<pattern>org.sqlite</pattern> <pattern>org.sqlite</pattern>
<shadedPattern>com.xeroth.xeroanticheat.sqlite</shadedPattern> <shadedPattern>com.xeroth.xeroanticheat.sqlite</shadedPattern>

View File

@@ -1,5 +1,6 @@
package com.xeroth.xeroanticheat; package com.xeroth.xeroanticheat;
import com.xeroth.xeroanticheat.api.XACApi;
import com.xeroth.xeroanticheat.checks.combat.*; import com.xeroth.xeroanticheat.checks.combat.*;
import com.xeroth.xeroanticheat.checks.misc.*; import com.xeroth.xeroanticheat.checks.misc.*;
import com.xeroth.xeroanticheat.checks.movement.*; 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.CheckManager;
import com.xeroth.xeroanticheat.manager.ConfigManager; import com.xeroth.xeroanticheat.manager.ConfigManager;
import com.xeroth.xeroanticheat.manager.DatabaseManager; import com.xeroth.xeroanticheat.manager.DatabaseManager;
import com.xeroth.xeroanticheat.manager.MetricsManager;
import com.xeroth.xeroanticheat.manager.PunishmentManager; import com.xeroth.xeroanticheat.manager.PunishmentManager;
import com.xeroth.xeroanticheat.manager.ViolationManager; import com.xeroth.xeroanticheat.manager.ViolationManager;
import com.xeroth.xeroanticheat.protocol.PacketListener; import com.xeroth.xeroanticheat.protocol.PacketListener;
@@ -42,8 +44,10 @@ public final class XeroAntiCheat extends JavaPlugin {
private CheckManager checkManager; private CheckManager checkManager;
private PacketListener packetListener; private PacketListener packetListener;
private DatabaseManager databaseManager; private DatabaseManager databaseManager;
private MetricsManager metricsManager;
private boolean protocolLibLoaded = false; private boolean protocolLibLoaded = false;
private org.bukkit.scheduler.BukkitTask decayTask;
// Staff alert toggles // Staff alert toggles
private final Map<UUID, Boolean> alertToggles = new ConcurrentHashMap<>(); private final Map<UUID, Boolean> alertToggles = new ConcurrentHashMap<>();
@@ -55,6 +59,9 @@ public final class XeroAntiCheat extends JavaPlugin {
public void onEnable() { public void onEnable() {
instance = this; instance = this;
// Initialize API
XACApi.init(this);
// Check for ProtocolLib // Check for ProtocolLib
checkProtocolLib(); checkProtocolLib();
@@ -102,6 +109,9 @@ public final class XeroAntiCheat extends JavaPlugin {
databaseManager.close(); databaseManager.close();
} }
// Shutdown API
XACApi.shutdown();
getLogger().info("XeroAntiCheat disabled!"); getLogger().info("XeroAntiCheat disabled!");
} }
@@ -121,6 +131,7 @@ public final class XeroAntiCheat extends JavaPlugin {
violationManager = new ViolationManager(this); violationManager = new ViolationManager(this);
punishmentManager = new PunishmentManager(this, violationManager); punishmentManager = new PunishmentManager(this, violationManager);
checkManager = new CheckManager(this); checkManager = new CheckManager(this);
metricsManager = new MetricsManager();
databaseManager = new DatabaseManager(this); databaseManager = new DatabaseManager(this);
databaseManager.initialize(); databaseManager.initialize();
@@ -174,7 +185,7 @@ public final class XeroAntiCheat extends JavaPlugin {
private void startDecayTask() { private void startDecayTask() {
int interval = configManager.getInt("violation.decay_interval", 30) * 20; int interval = configManager.getInt("violation.decay_interval", 30) * 20;
Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> { decayTask = Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> {
violationManager.decayAll(); violationManager.decayAll();
}, interval, interval); }, interval, interval);
} }
@@ -231,6 +242,14 @@ public final class XeroAntiCheat extends JavaPlugin {
public void reload() { public void reload() {
configManager.loadConfig(); configManager.loadConfig();
violationManager.clearAll(); violationManager.clearAll();
violationManager.setDecayRate(
configManager.getDouble("violation.decay_rate", 0.5));
if (decayTask != null) {
decayTask.cancel();
}
startDecayTask();
getLogger().info("Configuration reloaded!"); getLogger().info("Configuration reloaded!");
} }
@@ -258,6 +277,10 @@ public final class XeroAntiCheat extends JavaPlugin {
public DatabaseManager getDatabaseManager() { public DatabaseManager getDatabaseManager() {
return databaseManager; return databaseManager;
} }
public MetricsManager getMetricsManager() {
return metricsManager;
}
public boolean isProtocolLibLoaded() { public boolean isProtocolLibLoaded() {
return protocolLibLoaded; return protocolLibLoaded;

View File

@@ -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");
}
}

View File

@@ -2,6 +2,7 @@ package com.xeroth.xeroanticheat.check;
import com.xeroth.xeroanticheat.XeroAntiCheat; import com.xeroth.xeroanticheat.XeroAntiCheat;
import com.xeroth.xeroanticheat.data.PlayerData; import com.xeroth.xeroanticheat.data.PlayerData;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
/** /**
@@ -125,4 +126,19 @@ public abstract class Check {
|| player.hasPermission("xac.bypass." + getCategory()) || player.hasPermission("xac.bypass." + getCategory())
|| player.hasPermission("xac.bypass." + name.toLowerCase()); || 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;
}
} }

View File

@@ -5,9 +5,6 @@ import com.xeroth.xeroanticheat.check.Check;
import com.xeroth.xeroanticheat.data.PlayerData; import com.xeroth.xeroanticheat.data.PlayerData;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
/** /**
* AutoClickerCheck - Tracks CPS (clicks per second) over a 1-second sliding window. * AutoClickerCheck - Tracks CPS (clicks per second) over a 1-second sliding window.
* *
@@ -54,35 +51,33 @@ public class AutoClickerCheck extends Check {
* Check click pattern for suspiciously low variance * Check click pattern for suspiciously low variance
*/ */
private void checkPattern(PlayerData data, Player player, double minVariance) { private void checkPattern(PlayerData data, Player player, double minVariance) {
List<Long> clicks = new ArrayList<>(data.getClickTimestamps()); if (data.getClickTimestamps().size() < 5) return;
if (clicks.size() < 5) return;
// Calculate intervals between clicks
List<Long> intervals = new ArrayList<>();
for (int i = 0; i < clicks.size() - 1; i++) {
intervals.add(clicks.get(i) - clicks.get(i + 1));
}
if (intervals.isEmpty()) return;
// Calculate mean
double sum = 0; double sum = 0;
for (Long interval : intervals) { int intervalCount = 0;
sum += interval; Long prev = null;
for (Long ts : data.getClickTimestamps()) {
if (prev != null) {
sum += (prev - ts);
intervalCount++;
}
prev = ts;
} }
double mean = sum / intervals.size(); if (intervalCount == 0) return;
double mean = sum / intervalCount;
// Calculate variance
double varianceSum = 0; double varianceSum = 0;
for (Long interval : intervals) { prev = null;
double diff = interval - mean; for (Long ts : data.getClickTimestamps()) {
varianceSum += diff * diff; if (prev != null) {
double diff = (prev - ts) - mean;
varianceSum += diff * diff;
}
prev = ts;
} }
double variance = varianceSum / intervals.size();
double stdDev = Math.sqrt(variance); double stdDev = Math.sqrt(varianceSum / intervalCount);
// If variance is too low, flag (too perfect)
if (stdDev < minVariance && data.getCPS() > 10) { if (stdDev < minVariance && data.getCPS() > 10) {
flag(data, player, 1.0); flag(data, player, 1.0);
} }

View File

@@ -6,10 +6,10 @@ import com.xeroth.xeroanticheat.data.PlayerData;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
/** /**
* CriticalCheck - Detects critical hits when player is NOT in the air. * CriticalCheck - Detects suspicious critical hits.
* *
* Also detects if player is sprinting (criticals cancel sprint). * Minecraft cancels sprint on critical hits. A hacked client can send both
* Accounts for jump-crits (legitimate): allows if player was airborne in previous 3 ticks. * sprint and crit flags simultaneously.
*/ */
public class CriticalCheck extends Check { public class CriticalCheck extends Check {
@@ -40,28 +40,12 @@ public class CriticalCheck extends Check {
*/ */
public boolean checkCritical(Player player, PlayerData data, boolean isCritical) { public boolean checkCritical(Player player, PlayerData data, boolean isCritical) {
if (!isCritical) return false; if (!isCritical) return false;
boolean allowJumpCrits = getConfigBoolean("allow_jump_crits", true); // Crit while sprinting — Minecraft cancels sprint on crits
// Get player state
boolean isOnGround = player.isOnGround();
double yVelocity = player.getVelocity().getY();
boolean wasAirborne = data.wasAirborne();
// If player is on ground and not moving up (no jump), flag
if (isOnGround && yVelocity <= 0.0) {
// Check if was recently airborne (jump-crit)
if (allowJumpCrits && wasAirborne) {
return false; // Legitimate jump-crit
}
return true; // Suspicious - no-air crit
}
// Check if sprinting (criticals cancel sprint)
if (player.isSprinting()) { if (player.isSprinting()) {
return true; // Suspicious - crit while sprinting return true;
} }
return false; return false;
} }
} }

View File

@@ -3,7 +3,6 @@ package com.xeroth.xeroanticheat.checks.combat;
import com.xeroth.xeroanticheat.XeroAntiCheat; import com.xeroth.xeroanticheat.XeroAntiCheat;
import com.xeroth.xeroanticheat.check.Check; import com.xeroth.xeroanticheat.check.Check;
import com.xeroth.xeroanticheat.data.PlayerData; import com.xeroth.xeroanticheat.data.PlayerData;
import org.bukkit.EntityEffect;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Entity; import org.bukkit.entity.Entity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;

View File

@@ -60,12 +60,20 @@ public class ReachCheck extends Check {
// Get player eye location // Get player eye location
Location eyeLoc = player.getEyeLocation(); Location eyeLoc = player.getEyeLocation();
// Get target location (center of entity hitbox) // Use center of entity bounding box, not feet position.
Location targetLoc = target.getLocation(); // getLocation() returns feet; bounding box center is more accurate for tall entities.
org.bukkit.util.BoundingBox bb = target.getBoundingBox();
double targetX = (bb.getMinX() + bb.getMaxX()) / 2.0;
double targetY = (bb.getMinY() + bb.getMaxY()) / 2.0;
double targetZ = (bb.getMinZ() + bb.getMaxZ()) / 2.0;
// Calculate 3D distance // Calculate 3D distance from player eye to entity center (no Location object needed)
double distance = eyeLoc.distance(targetLoc); double dx = eyeLoc.getX() - targetX;
double dy = eyeLoc.getY() - targetY;
double dz = eyeLoc.getZ() - targetZ;
double distanceSquared = dx * dx + dy * dy + dz * dz;
return distance > maxReach; double maxReachSquared = maxReach * maxReach;
return distanceSquared > maxReachSquared;
} }
} }

View File

@@ -34,8 +34,6 @@ public class VelocityCheck extends Check {
Vector expected = data.getLastServerVelocity(); Vector expected = data.getLastServerVelocity();
if (expected == null || data.getVelocityCheckTicks() <= 0) return; if (expected == null || data.getVelocityCheckTicks() <= 0) return;
data.decrementVelocityCheckTicks();
double expectedHorizontal = Math.sqrt( double expectedHorizontal = Math.sqrt(
expected.getX() * expected.getX() + expected.getZ() * expected.getZ()); expected.getX() * expected.getX() + expected.getZ() * expected.getZ());
@@ -45,6 +43,8 @@ public class VelocityCheck extends Check {
return; return;
} }
data.decrementVelocityCheckTicks();
PlayerData.PositionSnapshot curr = data.getLastPosition(); PlayerData.PositionSnapshot curr = data.getLastPosition();
PlayerData.PositionSnapshot prev = data.getSecondLastPosition(); PlayerData.PositionSnapshot prev = data.getSecondLastPosition();
if (curr == null || prev == null) return; if (curr == null || prev == null) return;

View File

@@ -35,7 +35,14 @@ public class FastPlaceCheck extends Check {
int bps = data.getBlocksPerSecond(); int bps = data.getBlocksPerSecond();
if (bps > maxBlocksPerSecond) { 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();
} }
} }
} }

View File

@@ -39,14 +39,14 @@ public class InventoryMoveCheck extends Check {
if (current == null || last == null) return; if (current == null || last == null) return;
// Calculate position change // Calculate position change (squared to avoid Math.sqrt())
double dx = current.x() - last.x(); double dx = current.x() - last.x();
double dy = current.y() - last.y(); double dy = current.y() - last.y();
double dz = current.z() - last.z(); double dz = current.z() - last.z();
double distance = Math.sqrt(dx*dx + dy*dy + dz*dz); double distanceSquared = dx*dx + dy*dy + dz*dz;
// If significant movement while inventory open, flag // If significant movement while inventory open, flag
if (distance > 0.1) { if (distanceSquared > 0.01) {
flag(data, player); flag(data, player);
} }
} }

View File

@@ -31,6 +31,9 @@ public class FlyCheck extends Check {
// Ignore if player has bypass permission // Ignore if player has bypass permission
if (isBypassed(player)) return; if (isBypassed(player)) return;
// Skip if server is lagging
if (isServerLagging()) return;
// Ignore elytra gliding // Ignore elytra gliding
if (player.isGliding()) { if (player.isGliding()) {
return; return;
@@ -56,6 +59,7 @@ public class FlyCheck extends Check {
if (clientOnGround != serverOnGround) { if (clientOnGround != serverOnGround) {
if (data.getAirTicks() > fallBuffer + desyncThreshold) { if (data.getAirTicks() > fallBuffer + desyncThreshold) {
flag(data, player); flag(data, player);
setback(player, data);
} }
} }
@@ -67,9 +71,9 @@ public class FlyCheck extends Check {
// If moving up or staying at same height while not supposed to // If moving up or staying at same height while not supposed to
if (velocity.getY() > 0.1 || Math.abs(velocity.getY()) < 0.01) { if (velocity.getY() > 0.1 || Math.abs(velocity.getY()) < 0.01) {
if (data.getAirTicks() > fallBuffer) { if (data.getAirTicks() > fallBuffer) {
// Additional check: see if player has jump boost if (!data.hasJumpBoost()) {
if (!data.hasJumpBoost() || data.getJumpBoostLevel() <= 1) {
flag(data, player); flag(data, player);
setback(player, data);
} }
} }
} }

View File

@@ -30,6 +30,9 @@ public class GlideCheck extends Check {
// Ignore if player has bypass permission // Ignore if player has bypass permission
if (isBypassed(player)) return; if (isBypassed(player)) return;
// Skip if server is lagging
if (isServerLagging()) return;
// Get thresholds // Get thresholds
double minHorizontalSpeed = getConfigDouble("min_horizontal_speed", 0.5); double minHorizontalSpeed = getConfigDouble("min_horizontal_speed", 0.5);
double maxYDecrease = getConfigDouble("max_y_decrease", 0.1); double maxYDecrease = getConfigDouble("max_y_decrease", 0.1);
@@ -37,11 +40,12 @@ public class GlideCheck extends Check {
// Get velocity // Get velocity
org.bukkit.util.Vector velocity = player.getVelocity(); org.bukkit.util.Vector velocity = player.getVelocity();
// Calculate horizontal speed // Calculate horizontal speed (squared to avoid Math.sqrt())
double horizontalSpeed = Math.sqrt(velocity.getX() * velocity.getX() + velocity.getZ() * velocity.getZ()); double horizontalSpeedSq = velocity.getX() * velocity.getX() + velocity.getZ() * velocity.getZ();
// Check if moving fast horizontally // Check if moving fast horizontally
if (horizontalSpeed < minHorizontalSpeed) { double minHorizSq = minHorizontalSpeed * minHorizontalSpeed;
if (horizontalSpeedSq < minHorizSq) {
data.resetGlideTicks(); data.resetGlideTicks();
return; return;
} }
@@ -62,6 +66,7 @@ public class GlideCheck extends Check {
if (data.getGlideTicks() > 5) { if (data.getGlideTicks() > 5) {
flag(data, player); flag(data, player);
setback(player, data);
} }
} else { } else {
data.resetGlideTicks(); data.resetGlideTicks();

View File

@@ -37,7 +37,7 @@ public class JesusCheck extends Check {
// Get block below player // Get block below player
Location loc = player.getLocation(); Location loc = player.getLocation();
Material blockBelow = loc.subtract(0, 1, 0).getBlock().getType(); Material blockBelow = loc.clone().subtract(0, 1, 0).getBlock().getType();
// Check if player is on water or lava // Check if player is on water or lava
boolean onWater = blockBelow == Material.WATER; boolean onWater = blockBelow == Material.WATER;
@@ -72,11 +72,19 @@ public class JesusCheck extends Check {
if (current != null && last != null) { if (current != null && last != null) {
double dx = current.x() - last.x(); double dx = current.x() - last.x();
double dz = current.z() - last.z(); double dz = current.z() - last.z();
double horizontalSpeed = Math.sqrt(dx * dx + dz * dz); double horizontalSpeedSq = dx * dx + dz * dz;
// If moving at reasonable speed on water, flag // If moving at reasonable speed on water, flag
if (horizontalSpeed > 0.1) { 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();
} }
} }
} }

View File

@@ -54,14 +54,16 @@ public class NoFallCheck extends Check {
// Check for damage-reducing blocks // Check for damage-reducing blocks
Location loc = player.getLocation(); Location loc = player.getLocation();
Material blockBelow = loc.subtract(0, 1, 0).getBlock().getType(); Material blockBelow = loc.clone().subtract(0, 1, 0).getBlock().getType();
// Blocks that reduce/cancel fall damage // Blocks that reduce/cancel fall damage
if (blockBelow == Material.WATER || if (blockBelow == Material.WATER ||
blockBelow == Material.HONEY_BLOCK || blockBelow == Material.HONEY_BLOCK ||
blockBelow == Material.HAY_BLOCK || blockBelow == Material.HAY_BLOCK ||
blockBelow == Material.SLIME_BLOCK || blockBelow == Material.SLIME_BLOCK ||
blockBelow == Material.COBWEB) { blockBelow == Material.COBWEB ||
blockBelow == Material.POWDER_SNOW ||
org.bukkit.Tag.BEDS.isTagged(blockBelow)) {
data.setLastExpectedFallDamage(0.0); data.setLastExpectedFallDamage(0.0);
return; return;
} }

View File

@@ -60,6 +60,7 @@ public class PhaseCheck extends Check {
if (result != null && result.getHitBlock() != null if (result != null && result.getHitBlock() != null
&& result.getHitBlock().getType().isSolid()) { && result.getHitBlock().getType().isSolid()) {
flag(data, player, 3.0); flag(data, player, 3.0);
setback(player, data);
} }
} }
} }

View File

@@ -53,8 +53,12 @@ public class SpeedCheck extends Check {
double speed = horizontalDistance / (timeDelta / 50.0); double speed = horizontalDistance / (timeDelta / 50.0);
// Get server TPS // Get server TPS
double tps = org.bukkit.Bukkit.getTPS()[0]; double tpsMultiplier = 1.0;
double tpsMultiplier = 20.0 / Math.max(tps, 18.0); if (plugin.getConfigManager().getBoolean("tps.enabled", true)) {
double tps = org.bukkit.Bukkit.getTPS()[0];
double minTps = plugin.getConfigManager().getDouble("tps.min_tps_threshold", 18.0);
tpsMultiplier = 20.0 / Math.max(tps, minTps);
}
// Calculate max speed based on player state // Calculate max speed based on player state
double maxSpeed = calculateMaxSpeed(player, data); double maxSpeed = calculateMaxSpeed(player, data);
@@ -80,6 +84,7 @@ public class SpeedCheck extends Check {
int bufferTicks = getConfigInt("buffer_ticks", 5); int bufferTicks = getConfigInt("buffer_ticks", 5);
if (data.getSpeedViolationTicks() >= bufferTicks) { if (data.getSpeedViolationTicks() >= bufferTicks) {
flag(data, player, (speed - maxSpeed) * 2); flag(data, player, (speed - maxSpeed) * 2);
setback(player, data);
data.resetSpeedViolationTicks(); data.resetSpeedViolationTicks();
} }
} else { } else {
@@ -130,7 +135,7 @@ public class SpeedCheck extends Check {
// Soul sand slows // Soul sand slows
Location loc = player.getLocation(); Location loc = player.getLocation();
Material blockBelow = loc.subtract(0, 1, 0).getBlock().getType(); Material blockBelow = loc.clone().subtract(0, 1, 0).getBlock().getType();
if (blockBelow == Material.SOUL_SAND || blockBelow == Material.SOUL_SOIL) { if (blockBelow == Material.SOUL_SAND || blockBelow == Material.SOUL_SOIL) {
baseSpeed *= 0.75; baseSpeed *= 0.75;
} }

View File

@@ -3,7 +3,6 @@ package com.xeroth.xeroanticheat.checks.movement;
import com.xeroth.xeroanticheat.XeroAntiCheat; import com.xeroth.xeroanticheat.XeroAntiCheat;
import com.xeroth.xeroanticheat.check.Check; import com.xeroth.xeroanticheat.check.Check;
import com.xeroth.xeroanticheat.data.PlayerData; import com.xeroth.xeroanticheat.data.PlayerData;
import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -32,6 +31,9 @@ public class SpiderCheck extends Check {
// Ignore if player has bypass permission // Ignore if player has bypass permission
if (isBypassed(player)) return; if (isBypassed(player)) return;
// Skip if server is lagging
if (isServerLagging()) return;
// Get velocity // Get velocity
org.bukkit.util.Vector velocity = player.getVelocity(); org.bukkit.util.Vector velocity = player.getVelocity();
@@ -47,17 +49,16 @@ public class SpiderCheck extends Check {
return; return;
} }
// Get blocks around player // Get blocks around player using block coordinates (no Location mutation)
Location loc = player.getLocation(); org.bukkit.World world = player.getWorld();
org.bukkit.Location loc = player.getLocation();
// Check block at feet int blockX = loc.getBlockX();
Material feetBlock = loc.subtract(0, 1, 0).getBlock().getType(); int blockY = loc.getBlockY();
int blockZ = loc.getBlockZ();
// Check block at body level
Material bodyBlock = loc.getBlock().getType(); Material feetBlock = world.getBlockAt(blockX, blockY - 1, blockZ).getType();
Material bodyBlock = world.getBlockAt(blockX, blockY, blockZ).getType();
// Check block above head Material headBlock = world.getBlockAt(blockX, blockY + 1, blockZ).getType();
Material headBlock = loc.add(0, 1, 0).getBlock().getType();
// Check if any of these blocks are climbable // Check if any of these blocks are climbable
boolean feetClimbable = isClimbable(feetBlock); boolean feetClimbable = isClimbable(feetBlock);
@@ -71,6 +72,7 @@ public class SpiderCheck extends Check {
if (data.getSpiderTicks() > 5 && velocity.getY() > 0.1) { if (data.getSpiderTicks() > 5 && velocity.getY() > 0.1) {
flag(data, player); flag(data, player);
setback(player, data);
} }
} else { } else {
data.resetSpiderTicks(); data.resetSpiderTicks();

View File

@@ -32,12 +32,18 @@ public class TimerCheck extends Check {
// Get thresholds // Get thresholds
int maxPacketsPerSecond = getConfigInt("max_packets_per_second", 22); int maxPacketsPerSecond = getConfigInt("max_packets_per_second", 22);
long blinkThresholdMs = getConfigInt("blink_threshold_ms", 500);
// If ProtocolLib is active, packet counting is handled by PacketListener // If ProtocolLib is active, packet counting is handled by PacketListener
if (plugin.isProtocolLibLoaded()) { if (plugin.isProtocolLibLoaded()) {
if (data.getPacketsThisSecond() > maxPacketsPerSecond) { 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; return;
} }
@@ -54,29 +60,14 @@ public class TimerCheck extends Check {
// Check for too many packets // Check for too many packets
if (data.getPacketsThisSecond() > maxPacketsPerSecond) { if (data.getPacketsThisSecond() > maxPacketsPerSecond) {
flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5); data.incrementTimerBuffer();
} int buffer = getConfigInt("buffer_ticks", 2);
if (data.getTimerBuffer() >= buffer) {
// Check for blink (packet suppression) flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5);
long lastMoveTime = data.getLastMovePacketTime(); data.resetTimerBuffer();
if (lastMoveTime > 0) {
long gap = now - lastMoveTime;
if (gap > blinkThresholdMs) {
// Check if player teleported during the gap
PlayerData.PositionSnapshot current = data.getLastPosition();
PlayerData.PositionSnapshot last = data.getSecondLastPosition();
if (current != null && last != null) {
double dx = current.x() - last.x();
double dy = current.y() - last.y();
double dz = current.z() - last.z();
double distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
// If teleported more than 10 blocks, likely blink
if (distance > 10) {
flag(data, player, 2.0);
}
}
} }
} else {
data.resetTimerBuffer();
} }
data.setLastMovePacketTime(now); data.setLastMovePacketTime(now);

View File

@@ -79,6 +79,18 @@ public class XACCommand implements CommandExecutor, TabCompleter {
} }
toggleAlerts(sender, args[1]); toggleAlerts(sender, args[1]);
} }
case "debug" -> {
if (!has(sender, "xac.command.verbose")) return true;
if (args.length < 3) {
sender.sendMessage(usage("/xac debug <player> <check>"));
return true;
}
showDebug(sender, args[1], args[2]);
}
case "stats" -> {
if (!has(sender, "xac.admin")) return true;
showStats(sender);
}
case "version" -> showVersion(sender); case "version" -> showVersion(sender);
default -> sendHelp(sender); default -> sendHelp(sender);
} }
@@ -107,7 +119,9 @@ public class XACCommand implements CommandExecutor, TabCompleter {
new Cmd("/xac punish <player> <check>", "manual punishment", "xac.command.punish"), new Cmd("/xac punish <player> <check>", "manual punishment", "xac.command.punish"),
new Cmd("/xac clearviolations <player>", "clear all VL for player", "xac.command.clearviolations"), new Cmd("/xac clearviolations <player>", "clear all VL for player", "xac.command.clearviolations"),
new Cmd("/xac verbose <player>", "toggle per-flag debug", "xac.command.verbose"), new Cmd("/xac verbose <player>", "toggle per-flag debug", "xac.command.verbose"),
new Cmd("/xac debug <player> <check>", "show check debug info", "xac.command.verbose"),
new Cmd("/xac alerts [on|off]", "toggle alert receiving", "xac.command.alerts"), 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") new Cmd("/xac version", "show version", "xac.command.version")
).forEach(cmd -> { ).forEach(cmd -> {
if (sender.hasPermission(cmd.perm()) || sender.hasPermission("xac.admin")) { if (sender.hasPermission(cmd.perm()) || sender.hasPermission("xac.admin")) {
@@ -219,11 +233,71 @@ public class XACCommand implements CommandExecutor, TabCompleter {
NamedTextColor.GOLD)); NamedTextColor.GOLD));
sender.sendMessage(Component.text("Built for Paper 1.21.x", NamedTextColor.WHITE)); 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 @Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) { 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) .filter(sub -> sender.hasPermission("xac.command." + sub)
|| sender.hasPermission("xac.admin")) || sender.hasPermission("xac.admin"))
.filter(sub -> sub.startsWith(args[0].toLowerCase())) .filter(sub -> sub.startsWith(args[0].toLowerCase()))
@@ -231,7 +305,7 @@ public class XACCommand implements CommandExecutor, TabCompleter {
} }
if (args.length == 2) { if (args.length == 2) {
String sub = args[0].toLowerCase(); 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() return Bukkit.getOnlinePlayers().stream()
.map(Player::getName) .map(Player::getName)
.filter(n -> n.toLowerCase().startsWith(args[1].toLowerCase())) .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())) .filter(n -> n.toLowerCase().startsWith(args[2].toLowerCase()))
.toList(); .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(); return List.of();
} }

View File

@@ -111,6 +111,21 @@ public class PlayerData {
// SpeedCheck tracking // SpeedCheck tracking
private int speedViolationTicks = 0; 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<String, Long> lastWarnTime = new ConcurrentHashMap<>();
public PlayerData(Player player) { public PlayerData(Player player) {
this.uuid = player.getUniqueId(); this.uuid = player.getUniqueId();
this.name = player.getName(); this.name = player.getName();
@@ -618,6 +633,49 @@ public class PlayerData {
this.speedViolationTicks = 0; 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);
}
public void setLastWarnTime(String checkName, long time) {
lastWarnTime.put(checkName, time);
}
public void clearPositionHistory() { public void clearPositionHistory() {
positionHistory.clear(); positionHistory.clear();
rotationHistory.clear(); rotationHistory.clear();
@@ -625,6 +683,21 @@ public class PlayerData {
spiderTicks = 0; spiderTicks = 0;
glideTicks = 0; glideTicks = 0;
speedViolationTicks = 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<Long> getBlockPlaceTimestamps() { public Deque<Long> getBlockPlaceTimestamps() {

View File

@@ -52,17 +52,31 @@ public class CombatListener implements Listener {
// Check for reach // Check for reach
if (reachCheck != null && reachCheck.checkReach(player, target)) { if (reachCheck != null && reachCheck.checkReach(player, target)) {
if (!reachCheck.isBypassed(player)) { if (!reachCheck.isBypassed(player)) {
plugin.getViolationManager().addViolation(player, "Reach", 1.0); int buffer = plugin.getConfigManager().getInt("checks.reach.buffer_hits", 2);
plugin.getPunishmentManager().evaluate(player, "Reach"); 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) // Check for kill aura (angle)
if (killAuraCheck != null && killAuraCheck.checkAngle(player, target)) { if (killAuraCheck != null && killAuraCheck.checkAngle(player, target)) {
if (!killAuraCheck.isBypassed(player)) { if (!killAuraCheck.isBypassed(player)) {
plugin.getViolationManager().addViolation(player, "KillAura", 1.0); int buffer = plugin.getConfigManager().getInt("checks.killaura.buffer_ticks", 2);
plugin.getPunishmentManager().evaluate(player, "KillAura"); 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 // Check for multi-target

View File

@@ -55,9 +55,16 @@ public class MiscListener implements Listener {
ScaffoldCheck scaffoldCheck = (ScaffoldCheck) plugin.getCheckManager().getCheck("Scaffold"); ScaffoldCheck scaffoldCheck = (ScaffoldCheck) plugin.getCheckManager().getCheck("Scaffold");
if (scaffoldCheck != null && scaffoldCheck.checkScaffold(player, event.getBlockPlaced(), data)) { if (scaffoldCheck != null && scaffoldCheck.checkScaffold(player, event.getBlockPlaced(), data)) {
if (!scaffoldCheck.isBypassed(player)) { if (!scaffoldCheck.isBypassed(player)) {
plugin.getViolationManager().addViolation(player, "Scaffold", 1.0); int buffer = plugin.getConfigManager().getInt("checks.scaffold.buffer_ticks", 2);
plugin.getPunishmentManager().evaluate(player, "Scaffold"); 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"); FastEatCheck fastEatCheck = (FastEatCheck) plugin.getCheckManager().getCheck("FastEat");
if (fastEatCheck != null && fastEatCheck.checkFastEat(player, data, System.currentTimeMillis())) { if (fastEatCheck != null && fastEatCheck.checkFastEat(player, data, System.currentTimeMillis())) {
if (!fastEatCheck.isBypassed(player)) { if (!fastEatCheck.isBypassed(player)) {
plugin.getViolationManager().addViolation(player, "FastEat", 1.0); int buffer = plugin.getConfigManager().getInt("checks.fasteat.buffer_ticks", 2);
plugin.getPunishmentManager().evaluate(player, "FastEat"); data.incrementFastEatBuffer();
if (data.getFastEatBuffer() >= buffer) {
plugin.getViolationManager().addViolation(player, "FastEat", 1.0);
plugin.getPunishmentManager().evaluate(player, "FastEat");
data.resetFastEatBuffer();
}
} }
} else {
data.resetFastEatBuffer();
} }
} }

View File

@@ -94,6 +94,7 @@ public class MovementListener implements Listener {
} else { } else {
data.setWasAirborne(false); data.setWasAirborne(false);
data.resetAirTicks(); data.resetAirTicks();
data.setLastSafeLocation(event.getTo());
} }
// Check ice at feet // Check ice at feet
@@ -135,5 +136,6 @@ public class MovementListener implements Listener {
data.resetAirTicks(); data.resetAirTicks();
data.clearServerVelocity(); data.clearServerVelocity();
data.setLastPlacementYaw(Float.NaN); data.setLastPlacementYaw(Float.NaN);
data.setLastSafeLocation(event.getTo());
} }
} }

View File

@@ -47,6 +47,7 @@ public class CheckManager {
public void runCheck(String checkName, PlayerData data, Player player) { public void runCheck(String checkName, PlayerData data, Player player) {
Check check = checksByName.get(checkName.toLowerCase()); Check check = checksByName.get(checkName.toLowerCase());
if (check != null && check.isEnabled()) { if (check != null && check.isEnabled()) {
plugin.getMetricsManager().recordCheck();
check.check(data, player); check.check(data, player);
} }
} }

View File

@@ -27,7 +27,6 @@ public class ConfigManager {
// General // General
DEFAULTS.put("enabled", true); DEFAULTS.put("enabled", true);
DEFAULTS.put("debug", false); DEFAULTS.put("debug", false);
DEFAULTS.put("async_task_threads", 2);
// Violation // Violation
DEFAULTS.put("violation.decay_interval", 30); DEFAULTS.put("violation.decay_interval", 30);
@@ -114,7 +113,6 @@ public class ConfigManager {
// Checks - Critical // Checks - Critical
DEFAULTS.put("checks.critical.enabled", true); DEFAULTS.put("checks.critical.enabled", true);
DEFAULTS.put("checks.critical.allow_jump_crits", true);
DEFAULTS.put("checks.critical.warn_vl", 10); DEFAULTS.put("checks.critical.warn_vl", 10);
DEFAULTS.put("checks.critical.kick_vl", 25); DEFAULTS.put("checks.critical.kick_vl", 25);
DEFAULTS.put("checks.critical.tempban_vl", 50); DEFAULTS.put("checks.critical.tempban_vl", 50);
@@ -171,11 +169,7 @@ public class ConfigManager {
DEFAULTS.put("alerts.enabled", true); DEFAULTS.put("alerts.enabled", true);
DEFAULTS.put("alerts.format", "<dark_red>[<red>XAC<dark_red>] <white>%player% <red>failed <white>%check% <red>(VL: <white>%vl%<red>)"); DEFAULTS.put("alerts.format", "<dark_red>[<red>XAC<dark_red>] <white>%player% <red>failed <white>%check% <red>(VL: <white>%vl%<red>)");
DEFAULTS.put("alerts.staff_format", "<gray>[%time%] %message%"); DEFAULTS.put("alerts.staff_format", "<gray>[%time%] %message%");
DEFAULTS.put("alerts.cooldown_ms", 5000);
// Commands
DEFAULTS.put("commands.reload_permission", "xac.admin");
DEFAULTS.put("commands.bypass_permission", "xac.bypass");
DEFAULTS.put("commands.alerts_permission", "xac.alerts");
// TPS // TPS
DEFAULTS.put("tps.enabled", true); DEFAULTS.put("tps.enabled", true);

View File

@@ -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();
}
}

View File

@@ -76,7 +76,17 @@ public class PunishmentManager {
Check check = plugin.getCheckManager().getCheck(checkName); Check check = plugin.getCheckManager().getCheck(checkName);
String category = check != null ? check.getCategory() : "misc"; String category = check != null ? check.getCategory() : "misc";
sendAlert(player, checkName, vl, category); long cooldownMs = plugin.getConfigManager().getInt("alerts.cooldown_ms", 5000);
long now = System.currentTimeMillis();
long lastWarn = data.getLastWarnTime(checkName);
boolean cooledDown = (now - lastWarn) >= cooldownMs;
boolean isPunishment = vl >= kickVl;
if (cooledDown || isPunishment) {
sendAlert(player, checkName, vl, category);
data.setLastWarnTime(checkName, now);
}
if (vl >= permbanVl) { if (vl >= permbanVl) {
punish(player, checkName, "PERMBAN", permbanVl); punish(player, checkName, "PERMBAN", permbanVl);
@@ -84,7 +94,7 @@ public class PunishmentManager {
punish(player, checkName, "TEMPBAN", tempbanVl); punish(player, checkName, "TEMPBAN", tempbanVl);
} else if (vl >= kickVl) { } else if (vl >= kickVl) {
punish(player, checkName, "KICK", kickVl); punish(player, checkName, "KICK", kickVl);
} else if (vl >= warnVl) { } else if (vl >= warnVl && cooledDown) {
warn(player, checkName); warn(player, checkName);
} }
} }
@@ -113,6 +123,7 @@ public class PunishmentManager {
kickCmd = kickCmd.replace("%player%", playerName).replace("%reason%", reason); kickCmd = kickCmd.replace("%player%", playerName).replace("%reason%", reason);
executeCommand(kickCmd); executeCommand(kickCmd);
logPunishment(type, player, checkName, vl); logPunishment(type, player, checkName, vl);
plugin.getMetricsManager().recordPunishment();
} }
case "TEMPBAN" -> { case "TEMPBAN" -> {
String tempbanCmd = plugin.getConfigManager().getString("punishments.tempban_command", String tempbanCmd = plugin.getConfigManager().getString("punishments.tempban_command",
@@ -120,6 +131,7 @@ public class PunishmentManager {
tempbanCmd = tempbanCmd.replace("%player%", playerName).replace("%reason%", reason); tempbanCmd = tempbanCmd.replace("%player%", playerName).replace("%reason%", reason);
executeCommand(tempbanCmd); executeCommand(tempbanCmd);
logPunishment(type, player, checkName, vl); logPunishment(type, player, checkName, vl);
plugin.getMetricsManager().recordPunishment();
} }
case "PERMBAN" -> { case "PERMBAN" -> {
String permbanCmd = plugin.getConfigManager().getString("punishments.permban_command", String permbanCmd = plugin.getConfigManager().getString("punishments.permban_command",
@@ -127,6 +139,7 @@ public class PunishmentManager {
permbanCmd = permbanCmd.replace("%player%", playerName).replace("%reason%", reason); permbanCmd = permbanCmd.replace("%player%", playerName).replace("%reason%", reason);
executeCommand(permbanCmd); executeCommand(permbanCmd);
logPunishment(type, player, checkName, vl); logPunishment(type, player, checkName, vl);
plugin.getMetricsManager().recordPunishment();
} }
} }
} }
@@ -197,7 +210,8 @@ public class PunishmentManager {
} }
DatabaseManager db = plugin.getDatabaseManager(); DatabaseManager db = plugin.getDatabaseManager();
if (db != null && db.isAvailable()) { boolean dbEnabled = plugin.getConfigManager().getBoolean("database.enabled", true);
if (db != null && db.isAvailable() && dbEnabled) {
db.insertPunishment(timestamp, type, playerUuid, playerName, checkName, vl); db.insertPunishment(timestamp, type, playerUuid, playerName, checkName, vl);
} }
}); });

View File

@@ -22,13 +22,17 @@ public class ViolationManager {
private final Map<UUID, PlayerData> playerDataCache = new ConcurrentHashMap<>(); private final Map<UUID, PlayerData> playerDataCache = new ConcurrentHashMap<>();
private final MiniMessage miniMessage = MiniMessage.miniMessage(); private final MiniMessage miniMessage = MiniMessage.miniMessage();
private double decayRate; private volatile double decayRate;
public ViolationManager(XeroAntiCheat plugin) { public ViolationManager(XeroAntiCheat plugin) {
this.plugin = plugin; this.plugin = plugin;
this.decayRate = plugin.getConfigManager().getDouble("violation.decay_rate", 0.5); this.decayRate = plugin.getConfigManager().getDouble("violation.decay_rate", 0.5);
} }
public void setDecayRate(double rate) {
this.decayRate = rate;
}
/** /**
* Get or create player data for a player * Get or create player data for a player
*/ */
@@ -58,22 +62,29 @@ public class ViolationManager {
double newVl = data.getViolationLevel(checkName); double newVl = data.getViolationLevel(checkName);
if (plugin.isVerboseTarget(player.getUniqueId())) { if (plugin.isVerboseTarget(player.getUniqueId())) {
Component verbose = miniMessage.deserialize( long cooldownMs = plugin.getConfigManager().getInt("alerts.cooldown_ms", 5000);
"<gray>[<white>VERBOSE<gray>] <yellow>" + player.getName() long now = System.currentTimeMillis();
+ " <gray>» <white>" + checkName if ((now - data.getLastWarnTime(checkName)) >= cooldownMs) {
+ " <gray>+<green>" + String.format("%.1f", weight) Component verbose = miniMessage.deserialize(
+ " <gray>(VL: <white>" + String.format("%.1f", newVl) + "<gray>)" "<gray>[<white>VERBOSE<gray>] <yellow>" + player.getName()
); + " <gray>» <white>" + checkName
for (Player staff : Bukkit.getOnlinePlayers()) { + " <gray>+<green>" + String.format("%.1f", weight)
if (staff.hasPermission("xac.command.verbose") || staff.hasPermission("xac.admin")) { + " <gray>(VL: <white>" + String.format("%.1f", newVl) + "<gray>)"
staff.sendMessage(verbose); );
for (Player staff : Bukkit.getOnlinePlayers()) {
if (staff.hasPermission("xac.command.verbose") || staff.hasPermission("xac.admin")) {
staff.sendMessage(verbose);
}
} }
data.setLastWarnTime(checkName, now);
} }
} }
if (plugin.getConfigManager().isDebug()) { if (plugin.getConfigManager().isDebug()) {
plugin.getLogger().info(player.getName() + " violated " + checkName + " (VL: " + newVl + ")"); plugin.getLogger().info(player.getName() + " violated " + checkName + " (VL: " + newVl + ")");
} }
plugin.getMetricsManager().recordFlag();
} }
/** /**
@@ -89,8 +100,6 @@ public class ViolationManager {
* Decay all violation levels for all players * Decay all violation levels for all players
*/ */
public void decayAll() { public void decayAll() {
decayRate = plugin.getConfigManager().getDouble("violation.decay_rate", 0.5);
for (PlayerData data : playerDataCache.values()) { for (PlayerData data : playerDataCache.values()) {
for (String checkName : data.getViolationLevels().keySet()) { for (String checkName : data.getViolationLevels().keySet()) {
data.decayViolation(checkName, decayRate); data.decayViolation(checkName, decayRate);

View File

@@ -152,56 +152,6 @@ public class PacketListener {
return protocolLibAvailable; return protocolLibAvailable;
} }
/**
* Update packet timing (called from event listeners when ProtocolLib unavailable)
*/
public void updatePacketTiming(Player player) {
if (protocolLibAvailable) {
return;
}
PlayerData data = plugin.getViolationManager().getPlayerData(player);
if (data == null) return;
long now = System.currentTimeMillis();
data.setLastMovePacketTime(now);
if (now - data.getLastPacketCountReset() > 1000) {
data.setPacketsThisSecond(0);
data.setLastPacketCountReset(now);
}
data.incrementPacketsThisSecond();
}
/**
* Record click (called from event listeners when ProtocolLib unavailable)
*/
public void recordClick(Player player) {
if (protocolLibAvailable) {
return;
}
PlayerData data = plugin.getViolationManager().getPlayerData(player);
if (data != null) {
data.addClick();
}
}
/**
* Record attack (called from event listeners when ProtocolLib unavailable)
*/
public void recordAttack(Player player, java.util.UUID entityUuid) {
if (protocolLibAvailable) {
return;
}
PlayerData data = plugin.getViolationManager().getPlayerData(player);
if (data != null) {
data.addAttack(entityUuid);
data.setLastAttackYaw(player.getLocation().getYaw());
}
}
/** /**
* Unregister all packet listeners * Unregister all packet listeners
*/ */

View File

@@ -12,9 +12,6 @@ enabled: true
# Enable debug mode (logs additional information) # Enable debug mode (logs additional information)
debug: false debug: false
# Number of async threads for background tasks
async_task_threads: 2
# Database settings # Database settings
database: database:
# Set to false to disable SQLite logging (flat-file log always active) # Set to false to disable SQLite logging (flat-file log always active)
@@ -43,6 +40,8 @@ checks:
# ---------------------------------------- # ----------------------------------------
speed: speed:
enabled: true enabled: true
# Teleport player back to last safe location when flagged
setback: false
# Base maximum speed (blocks per tick) # Base maximum speed (blocks per tick)
max_speed: 0.56 max_speed: 0.56
# Ping compensation factor (scales latency leniency) # Ping compensation factor (scales latency leniency)
@@ -61,13 +60,15 @@ checks:
# ---------------------------------------- # ----------------------------------------
fly: fly:
enabled: true enabled: true
# Teleport player back to last safe location when flagged
setback: false
# Number of ticks to allow for stepping/slabs # Number of ticks to allow for stepping/slabs
fall_buffer: 10 fall_buffer: 15
# Maximum ground desync ticks before flagging # Maximum ground desync ticks before flagging
ground_desync_threshold: 3 ground_desync_threshold: 5
warn_vl: 10 warn_vl: 25
kick_vl: 25 kick_vl: 50
tempban_vl: 50 tempban_vl: 75
permban_vl: 100 permban_vl: 100
# ---------------------------------------- # ----------------------------------------
@@ -76,9 +77,13 @@ checks:
# ---------------------------------------- # ----------------------------------------
jesus: jesus:
enabled: true enabled: true
warn_vl: 10 # Teleport player back to last safe location when flagged
kick_vl: 25 setback: false
tempban_vl: 50 # Number of consecutive ticks to flag before VL is added
buffer_ticks: 15
warn_vl: 25
kick_vl: 50
tempban_vl: 75
permban_vl: 100 permban_vl: 100
# ---------------------------------------- # ----------------------------------------
@@ -100,13 +105,15 @@ checks:
# ---------------------------------------- # ----------------------------------------
timer: timer:
enabled: true enabled: true
# Number of consecutive ticks exceeding max packets before flagging
buffer_ticks: 10
# Maximum packets per second allowed # Maximum packets per second allowed
max_packets_per_second: 22 max_packets_per_second: 25
# Milliseconds of no packets before flagging blink # Milliseconds of no packets before flagging blink
blink_threshold_ms: 500 blink_threshold_ms: 500
warn_vl: 10 warn_vl: 25
kick_vl: 25 kick_vl: 50
tempban_vl: 50 tempban_vl: 75
permban_vl: 100 permban_vl: 100
# ---------------------------------------- # ----------------------------------------
@@ -115,6 +122,8 @@ checks:
# ---------------------------------------- # ----------------------------------------
spider: spider:
enabled: true enabled: true
# Teleport player back to last safe location when flagged
setback: false
warn_vl: 10 warn_vl: 10
kick_vl: 25 kick_vl: 25
tempban_vl: 50 tempban_vl: 50
@@ -126,6 +135,8 @@ checks:
# ---------------------------------------- # ----------------------------------------
glide: glide:
enabled: true enabled: true
# Teleport player back to last safe location when flagged
setback: false
# Minimum horizontal speed for glide detection # Minimum horizontal speed for glide detection
min_horizontal_speed: 0.5 min_horizontal_speed: 0.5
# Maximum Y decrease per tick for glide curve # Maximum Y decrease per tick for glide curve
@@ -141,6 +152,8 @@ checks:
# ---------------------------------------- # ----------------------------------------
killaura: killaura:
enabled: true enabled: true
# Number of consecutive out-of-angle attacks before flagging
buffer_ticks: 2
# Maximum angle in degrees from look direction # Maximum angle in degrees from look direction
max_angle: 100 max_angle: 100
# Maximum rotation change between attacks # Maximum rotation change between attacks
@@ -158,6 +171,8 @@ checks:
# ---------------------------------------- # ----------------------------------------
reach: reach:
enabled: true enabled: true
# Number of consecutive attacks exceeding reach before flagging
buffer_hits: 2
# Maximum reach in blocks (survival) # Maximum reach in blocks (survival)
max_reach: 3.2 max_reach: 3.2
# Maximum reach in blocks (creative) # Maximum reach in blocks (creative)
@@ -175,8 +190,6 @@ checks:
# ---------------------------------------- # ----------------------------------------
critical: critical:
enabled: true enabled: true
# Allow legitimate jump-crits
allow_jump_crits: true
warn_vl: 10 warn_vl: 10
kick_vl: 25 kick_vl: 25
tempban_vl: 50 tempban_vl: 50
@@ -203,6 +216,8 @@ checks:
# ---------------------------------------- # ----------------------------------------
fastplace: fastplace:
enabled: true enabled: true
# Number of consecutive ticks exceeding max blocks before flagging
buffer_ticks: 2
# Maximum blocks per second # Maximum blocks per second
max_blocks_per_second: 20 max_blocks_per_second: 20
warn_vl: 10 warn_vl: 10
@@ -216,6 +231,8 @@ checks:
# ---------------------------------------- # ----------------------------------------
phase: phase:
enabled: true enabled: true
# Teleport player back to last safe location when flagged
setback: false
# Minimum movement distance before ray-cast runs (blocks) # Minimum movement distance before ray-cast runs (blocks)
min_distance: 0.5 min_distance: 0.5
# Maximum movement delta — larger values are treated as teleports # Maximum movement delta — larger values are treated as teleports
@@ -246,6 +263,8 @@ checks:
# ---------------------------------------- # ----------------------------------------
scaffold: scaffold:
enabled: true enabled: true
# Number of consecutive signal accumulations before flagging
buffer_ticks: 2
# Minimum pitch angle for suspicious placement # Minimum pitch angle for suspicious placement
min_pitch: 75 min_pitch: 75
# Number of signals required to flag # Number of signals required to flag
@@ -269,6 +288,8 @@ checks:
# ---------------------------------------- # ----------------------------------------
fasteat: fasteat:
enabled: true enabled: true
# Number of consecutive fast eats before flagging
buffer_ticks: 2
# Maximum eating duration in ticks (32 = 1.6s) # Maximum eating duration in ticks (32 = 1.6s)
max_eat_ticks: 32 max_eat_ticks: 32
warn_vl: 10 warn_vl: 10
@@ -315,18 +336,11 @@ alerts:
# Staff-only alert format # Staff-only alert format
staff_format: "<gray>[%time%] %message%" staff_format: "<gray>[%time%] %message%"
# ========================================== # Minimum milliseconds between alert/warn messages for the same player+check.
# COMMANDS # Prevents chat spam when a player is flagging at high frequency.
# ========================================== # Default: 5000ms (5 seconds). Set to 0 to disable throttling.
cooldown_ms: 5000
commands:
# Permission required for admin commands
reload_permission: "xac.admin"
# Permission to bypass all checks
bypass_permission: "xac.bypass"
# Permission to receive alerts
alerts_permission: "xac.alerts"
# ========================================== # ==========================================
# TPS COMPENSATION # TPS COMPENSATION

View File

@@ -1,5 +1,5 @@
name: XeroAntiCheat name: XeroAntiCheat
version: 1.0.7 version: 1.2.0
main: com.xeroth.xeroanticheat.XeroAntiCheat main: com.xeroth.xeroanticheat.XeroAntiCheat
author: Xeroth author: Xeroth
description: Lightweight, accurate anti-cheat for Paper 1.21.x description: Lightweight, accurate anti-cheat for Paper 1.21.x