Compare commits
12 Commits
71daf9ffc1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0999c3e264 | |||
| dd0700ab19 | |||
| b63e046f84 | |||
| 0073a26e9c | |||
| 112a61cf0c | |||
| a4a87e62de | |||
| 95e0915d67 | |||
| 68e99adf3e | |||
| ac5a8e807b | |||
| 8190b39160 | |||
| 38ab1abaf1 | |||
| daccfedae6 |
19
.gitignore
vendored
19
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -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.
|
||||||
|
|||||||
6
pom.xml
6
pom.xml
@@ -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>
|
||||||
|
|||||||
@@ -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!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +278,10 @@ public final class XeroAntiCheat extends JavaPlugin {
|
|||||||
return databaseManager;
|
return databaseManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MetricsManager getMetricsManager() {
|
||||||
|
return metricsManager;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isProtocolLibLoaded() {
|
public boolean isProtocolLibLoaded() {
|
||||||
return protocolLibLoaded;
|
return protocolLibLoaded;
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/main/java/com/xeroth/xeroanticheat/api/XACApi.java
Normal file
42
src/main/java/com/xeroth/xeroanticheat/api/XACApi.java
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
double mean = sum / intervals.size();
|
prev = ts;
|
||||||
|
}
|
||||||
|
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()) {
|
||||||
|
if (prev != null) {
|
||||||
|
double diff = (prev - ts) - mean;
|
||||||
varianceSum += diff * diff;
|
varianceSum += diff * diff;
|
||||||
}
|
}
|
||||||
double variance = varianceSum / intervals.size();
|
prev = ts;
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
@@ -41,25 +41,9 @@ 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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -35,7 +35,14 @@ public class FastPlaceCheck extends Check {
|
|||||||
int bps = data.getBlocksPerSecond();
|
int bps = data.getBlocksPerSecond();
|
||||||
|
|
||||||
if (bps > maxBlocksPerSecond) {
|
if (bps > maxBlocksPerSecond) {
|
||||||
|
data.incrementFastPlaceBuffer();
|
||||||
|
int buffer = getConfigInt("buffer_ticks", 2);
|
||||||
|
if (data.getFastPlaceBuffer() >= buffer) {
|
||||||
flag(data, player, (bps - maxBlocksPerSecond) * 0.5);
|
flag(data, player, (bps - maxBlocksPerSecond) * 0.5);
|
||||||
|
data.resetFastPlaceBuffer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetFastPlaceBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
data.incrementJesusBuffer();
|
||||||
|
int buffer = getConfigInt("buffer_ticks", 3);
|
||||||
|
if (data.getJesusBuffer() >= buffer) {
|
||||||
flag(data, player);
|
flag(data, player);
|
||||||
|
setback(player, data);
|
||||||
|
data.resetJesusBuffer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetJesusBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 tpsMultiplier = 1.0;
|
||||||
|
if (plugin.getConfigManager().getBoolean("tps.enabled", true)) {
|
||||||
double tps = org.bukkit.Bukkit.getTPS()[0];
|
double tps = org.bukkit.Bukkit.getTPS()[0];
|
||||||
double tpsMultiplier = 20.0 / Math.max(tps, 18.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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
int blockX = loc.getBlockX();
|
||||||
|
int blockY = loc.getBlockY();
|
||||||
|
int blockZ = loc.getBlockZ();
|
||||||
|
|
||||||
// Check block at feet
|
Material feetBlock = world.getBlockAt(blockX, blockY - 1, blockZ).getType();
|
||||||
Material feetBlock = loc.subtract(0, 1, 0).getBlock().getType();
|
Material bodyBlock = world.getBlockAt(blockX, blockY, blockZ).getType();
|
||||||
|
Material headBlock = world.getBlockAt(blockX, blockY + 1, blockZ).getType();
|
||||||
// Check block at body level
|
|
||||||
Material bodyBlock = loc.getBlock().getType();
|
|
||||||
|
|
||||||
// Check block above head
|
|
||||||
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();
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
data.incrementTimerBuffer();
|
||||||
|
int buffer = getConfigInt("buffer_ticks", 2);
|
||||||
|
if (data.getTimerBuffer() >= buffer) {
|
||||||
flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5);
|
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) {
|
||||||
|
data.incrementTimerBuffer();
|
||||||
|
int buffer = getConfigInt("buffer_ticks", 2);
|
||||||
|
if (data.getTimerBuffer() >= buffer) {
|
||||||
flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5);
|
flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5);
|
||||||
|
data.resetTimerBuffer();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Check for blink (packet suppression)
|
data.resetTimerBuffer();
|
||||||
long lastMoveTime = data.getLastMovePacketTime();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.setLastMovePacketTime(now);
|
data.setLastMovePacketTime(now);
|
||||||
|
|||||||
@@ -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")) {
|
||||||
@@ -220,10 +234,70 @@ public class XACCommand implements CommandExecutor, TabCompleter {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -52,18 +52,32 @@ 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)) {
|
||||||
|
int buffer = plugin.getConfigManager().getInt("checks.reach.buffer_hits", 2);
|
||||||
|
data.incrementReachBuffer();
|
||||||
|
if (data.getReachBuffer() >= buffer) {
|
||||||
plugin.getViolationManager().addViolation(player, "Reach", 1.0);
|
plugin.getViolationManager().addViolation(player, "Reach", 1.0);
|
||||||
plugin.getPunishmentManager().evaluate(player, "Reach");
|
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)) {
|
||||||
|
int buffer = plugin.getConfigManager().getInt("checks.killaura.buffer_ticks", 2);
|
||||||
|
data.incrementKillAuraBuffer();
|
||||||
|
if (data.getKillAuraBuffer() >= buffer) {
|
||||||
plugin.getViolationManager().addViolation(player, "KillAura", 1.0);
|
plugin.getViolationManager().addViolation(player, "KillAura", 1.0);
|
||||||
plugin.getPunishmentManager().evaluate(player, "KillAura");
|
plugin.getPunishmentManager().evaluate(player, "KillAura");
|
||||||
|
data.resetKillAuraBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
data.resetKillAuraBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
// Check for multi-target
|
// Check for multi-target
|
||||||
if (killAuraCheck != null && killAuraCheck.checkMultiTarget(data, player, target)) {
|
if (killAuraCheck != null && killAuraCheck.checkMultiTarget(data, player, target)) {
|
||||||
|
|||||||
@@ -55,10 +55,17 @@ 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)) {
|
||||||
|
int buffer = plugin.getConfigManager().getInt("checks.scaffold.buffer_ticks", 2);
|
||||||
|
data.incrementScaffoldBuffer();
|
||||||
|
if (data.getScaffoldBuffer() >= buffer) {
|
||||||
plugin.getViolationManager().addViolation(player, "Scaffold", 1.0);
|
plugin.getViolationManager().addViolation(player, "Scaffold", 1.0);
|
||||||
plugin.getPunishmentManager().evaluate(player, "Scaffold");
|
plugin.getPunishmentManager().evaluate(player, "Scaffold");
|
||||||
|
data.resetScaffoldBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
data.resetScaffoldBuffer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
@@ -79,10 +86,17 @@ 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)) {
|
||||||
|
int buffer = plugin.getConfigManager().getInt("checks.fasteat.buffer_ticks", 2);
|
||||||
|
data.incrementFastEatBuffer();
|
||||||
|
if (data.getFastEatBuffer() >= buffer) {
|
||||||
plugin.getViolationManager().addViolation(player, "FastEat", 1.0);
|
plugin.getViolationManager().addViolation(player, "FastEat", 1.0);
|
||||||
plugin.getPunishmentManager().evaluate(player, "FastEat");
|
plugin.getPunishmentManager().evaluate(player, "FastEat");
|
||||||
|
data.resetFastEatBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
data.resetFastEatBuffer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|
||||||
|
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);
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,6 +62,9 @@ public class ViolationManager {
|
|||||||
double newVl = data.getViolationLevel(checkName);
|
double newVl = data.getViolationLevel(checkName);
|
||||||
|
|
||||||
if (plugin.isVerboseTarget(player.getUniqueId())) {
|
if (plugin.isVerboseTarget(player.getUniqueId())) {
|
||||||
|
long cooldownMs = plugin.getConfigManager().getInt("alerts.cooldown_ms", 5000);
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if ((now - data.getLastWarnTime(checkName)) >= cooldownMs) {
|
||||||
Component verbose = miniMessage.deserialize(
|
Component verbose = miniMessage.deserialize(
|
||||||
"<gray>[<white>VERBOSE<gray>] <yellow>" + player.getName()
|
"<gray>[<white>VERBOSE<gray>] <yellow>" + player.getName()
|
||||||
+ " <gray>» <white>" + checkName
|
+ " <gray>» <white>" + checkName
|
||||||
@@ -69,11 +76,15 @@ public class ViolationManager {
|
|||||||
staff.sendMessage(verbose);
|
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);
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -316,17 +337,10 @@ 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user