Compare commits
14 Commits
303860adc8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0999c3e264 | |||
| dd0700ab19 | |||
| b63e046f84 | |||
| 0073a26e9c | |||
| 112a61cf0c | |||
| a4a87e62de | |||
| 95e0915d67 | |||
| 68e99adf3e | |||
| ac5a8e807b | |||
| 8190b39160 | |||
| 38ab1abaf1 | |||
| daccfedae6 | |||
| 71daf9ffc1 | |||
| f55d71c35d |
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.
|
||||||
|
|||||||
94
pom.xml
Normal file
94
pom.xml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.xeroth</groupId>
|
||||||
|
<artifactId>xeroanticheat</artifactId>
|
||||||
|
<version>1.2.0</version>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>XeroAntiCheat</name>
|
||||||
|
<description>Lightweight, accurate anti-cheat for Paper 1.21.x</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>papermc</id>
|
||||||
|
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>dmulloy2-repo</id>
|
||||||
|
<url>https://repo.dmulloy2.net/repository/public/</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.papermc.paper</groupId>
|
||||||
|
<artifactId>paper-api</artifactId>
|
||||||
|
<version>1.21.1-R0.1-SNAPSHOT</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.comphenix.protocol</groupId>
|
||||||
|
<artifactId>ProtocolLib</artifactId>
|
||||||
|
<version>5.3.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.xerial</groupId>
|
||||||
|
<artifactId>sqlite-jdbc</artifactId>
|
||||||
|
<version>3.47.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<finalName>${project.name}</finalName>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>true</filtering>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.13.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>21</source>
|
||||||
|
<target>21</target>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<relocations>
|
||||||
|
<relocation>
|
||||||
|
<pattern>org.sqlite</pattern>
|
||||||
|
<shadedPattern>com.xeroth.xeroanticheat.sqlite</shadedPattern>
|
||||||
|
</relocation>
|
||||||
|
</relocations>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
306
src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java
Normal file
306
src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
package com.xeroth.xeroanticheat;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.api.XACApi;
|
||||||
|
import com.xeroth.xeroanticheat.checks.combat.*;
|
||||||
|
import com.xeroth.xeroanticheat.checks.misc.*;
|
||||||
|
import com.xeroth.xeroanticheat.checks.movement.*;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.command.XACCommand;
|
||||||
|
import com.xeroth.xeroanticheat.listener.CombatListener;
|
||||||
|
import com.xeroth.xeroanticheat.listener.MiscListener;
|
||||||
|
import com.xeroth.xeroanticheat.listener.MovementListener;
|
||||||
|
import com.xeroth.xeroanticheat.manager.CheckManager;
|
||||||
|
import com.xeroth.xeroanticheat.manager.ConfigManager;
|
||||||
|
import com.xeroth.xeroanticheat.manager.DatabaseManager;
|
||||||
|
import com.xeroth.xeroanticheat.manager.MetricsManager;
|
||||||
|
import com.xeroth.xeroanticheat.manager.PunishmentManager;
|
||||||
|
import com.xeroth.xeroanticheat.manager.ViolationManager;
|
||||||
|
import com.xeroth.xeroanticheat.protocol.PacketListener;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.potion.PotionEffectType;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XeroAntiCheat - Lightweight, accurate anti-cheat for Paper 1.21.x
|
||||||
|
*
|
||||||
|
* This plugin provides comprehensive cheat detection including movement,
|
||||||
|
* combat, and miscellaneous checks with minimal false positives.
|
||||||
|
*/
|
||||||
|
public final class XeroAntiCheat extends JavaPlugin {
|
||||||
|
|
||||||
|
private static XeroAntiCheat instance;
|
||||||
|
|
||||||
|
private ConfigManager configManager;
|
||||||
|
private ViolationManager violationManager;
|
||||||
|
private PunishmentManager punishmentManager;
|
||||||
|
private CheckManager checkManager;
|
||||||
|
private PacketListener packetListener;
|
||||||
|
private DatabaseManager databaseManager;
|
||||||
|
private MetricsManager metricsManager;
|
||||||
|
|
||||||
|
private boolean protocolLibLoaded = false;
|
||||||
|
private org.bukkit.scheduler.BukkitTask decayTask;
|
||||||
|
|
||||||
|
// Staff alert toggles
|
||||||
|
private final Map<UUID, Boolean> alertToggles = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Verbose targets (per-player debug output)
|
||||||
|
private final Set<UUID> verboseTargets = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
instance = this;
|
||||||
|
|
||||||
|
// Initialize API
|
||||||
|
XACApi.init(this);
|
||||||
|
|
||||||
|
// Check for ProtocolLib
|
||||||
|
checkProtocolLib();
|
||||||
|
|
||||||
|
// Initialize managers
|
||||||
|
initializeManagers();
|
||||||
|
|
||||||
|
// Register listeners
|
||||||
|
registerListeners();
|
||||||
|
|
||||||
|
// Register commands
|
||||||
|
registerCommands();
|
||||||
|
|
||||||
|
// Register checks
|
||||||
|
registerChecks();
|
||||||
|
|
||||||
|
// Start decay task
|
||||||
|
startDecayTask();
|
||||||
|
|
||||||
|
// Start potion effect refresh task
|
||||||
|
startPotionRefreshTask();
|
||||||
|
|
||||||
|
// Start TimerCheck blink detection task
|
||||||
|
startTimerBlinkTask();
|
||||||
|
|
||||||
|
getLogger().info("XeroAntiCheat v" + getDescription().getVersion() + " enabled!");
|
||||||
|
if (protocolLibLoaded) {
|
||||||
|
getLogger().info("ProtocolLib detected - packet-level checks enabled");
|
||||||
|
} else {
|
||||||
|
getLogger().info("ProtocolLib not found - using event-based detection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
// Cancel all tasks
|
||||||
|
Bukkit.getScheduler().cancelTasks(this);
|
||||||
|
|
||||||
|
// Save any pending data
|
||||||
|
if (violationManager != null) {
|
||||||
|
violationManager.saveAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close database
|
||||||
|
if (databaseManager != null) {
|
||||||
|
databaseManager.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown API
|
||||||
|
XACApi.shutdown();
|
||||||
|
|
||||||
|
getLogger().info("XeroAntiCheat disabled!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkProtocolLib() {
|
||||||
|
try {
|
||||||
|
Class.forName("com.comphenix.protocol.ProtocolLibrary");
|
||||||
|
protocolLibLoaded = true;
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
protocolLibLoaded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeManagers() {
|
||||||
|
configManager = new ConfigManager(this);
|
||||||
|
configManager.loadConfig();
|
||||||
|
|
||||||
|
violationManager = new ViolationManager(this);
|
||||||
|
punishmentManager = new PunishmentManager(this, violationManager);
|
||||||
|
checkManager = new CheckManager(this);
|
||||||
|
metricsManager = new MetricsManager();
|
||||||
|
|
||||||
|
databaseManager = new DatabaseManager(this);
|
||||||
|
databaseManager.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerListeners() {
|
||||||
|
getServer().getPluginManager().registerEvents(new MovementListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new CombatListener(this), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new MiscListener(this), this);
|
||||||
|
|
||||||
|
if (protocolLibLoaded) {
|
||||||
|
packetListener = new PacketListener(this);
|
||||||
|
packetListener.register();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerCommands() {
|
||||||
|
getCommand("xac").setExecutor(new XACCommand(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerChecks() {
|
||||||
|
// Movement checks
|
||||||
|
checkManager.registerCheck(new SpeedCheck(this));
|
||||||
|
checkManager.registerCheck(new FlyCheck(this));
|
||||||
|
checkManager.registerCheck(new JesusCheck(this));
|
||||||
|
checkManager.registerCheck(new NoFallCheck(this));
|
||||||
|
checkManager.registerCheck(new TimerCheck(this));
|
||||||
|
checkManager.registerCheck(new SpiderCheck(this));
|
||||||
|
checkManager.registerCheck(new GlideCheck(this));
|
||||||
|
checkManager.registerCheck(new PhaseCheck(this));
|
||||||
|
|
||||||
|
// Combat checks
|
||||||
|
checkManager.registerCheck(new KillAuraCheck(this));
|
||||||
|
checkManager.registerCheck(new ReachCheck(this));
|
||||||
|
checkManager.registerCheck(new CriticalCheck(this));
|
||||||
|
checkManager.registerCheck(new AutoClickerCheck(this));
|
||||||
|
|
||||||
|
// VelocityCheck requires ProtocolLib
|
||||||
|
if (protocolLibLoaded) {
|
||||||
|
checkManager.registerCheck(new VelocityCheck(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Misc checks
|
||||||
|
checkManager.registerCheck(new FastPlaceCheck(this));
|
||||||
|
checkManager.registerCheck(new ScaffoldCheck(this));
|
||||||
|
checkManager.registerCheck(new FastEatCheck(this));
|
||||||
|
checkManager.registerCheck(new InventoryMoveCheck(this));
|
||||||
|
|
||||||
|
getLogger().info("Registered " + checkManager.getRegisteredChecks().size() + " checks");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDecayTask() {
|
||||||
|
int interval = configManager.getInt("violation.decay_interval", 30) * 20;
|
||||||
|
decayTask = Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> {
|
||||||
|
violationManager.decayAll();
|
||||||
|
}, interval, interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPotionRefreshTask() {
|
||||||
|
Bukkit.getScheduler().runTaskTimer(this, () -> {
|
||||||
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
PlayerData data = violationManager.getPlayerData(player);
|
||||||
|
if (data == null) continue;
|
||||||
|
|
||||||
|
data.setHasSpeedEffect(player.hasPotionEffect(PotionEffectType.SPEED));
|
||||||
|
data.setHasSlownessEffect(player.hasPotionEffect(PotionEffectType.SLOWNESS));
|
||||||
|
data.setHasLevitation(player.hasPotionEffect(PotionEffectType.LEVITATION));
|
||||||
|
data.setHasDolphinsGrace(player.hasPotionEffect(PotionEffectType.DOLPHINS_GRACE));
|
||||||
|
data.setHasJumpBoost(player.hasPotionEffect(PotionEffectType.JUMP_BOOST));
|
||||||
|
data.setHasSlowFalling(player.hasPotionEffect(PotionEffectType.SLOW_FALLING));
|
||||||
|
|
||||||
|
var speed = player.getPotionEffect(PotionEffectType.SPEED);
|
||||||
|
data.setSpeedLevel(speed != null ? speed.getAmplifier() + 1 : 0);
|
||||||
|
|
||||||
|
var slowness = player.getPotionEffect(PotionEffectType.SLOWNESS);
|
||||||
|
data.setSlownessLevel(slowness != null ? slowness.getAmplifier() + 1 : 0);
|
||||||
|
|
||||||
|
var jump = player.getPotionEffect(PotionEffectType.JUMP_BOOST);
|
||||||
|
data.setJumpBoostLevel(jump != null ? jump.getAmplifier() + 1 : 0);
|
||||||
|
}
|
||||||
|
}, 10L, 10L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startTimerBlinkTask() {
|
||||||
|
Bukkit.getScheduler().runTaskTimer(this, () -> {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long threshold = configManager.getInt("checks.timer.blink_threshold_ms", 500);
|
||||||
|
Check timerCheck = checkManager.getCheck("Timer");
|
||||||
|
|
||||||
|
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||||
|
if (timerCheck != null && timerCheck.isBypassed(player)) continue;
|
||||||
|
PlayerData data = violationManager.getPlayerData(player);
|
||||||
|
if (data == null || data.getLastMovePacketTime() == 0) continue;
|
||||||
|
|
||||||
|
long gap = now - data.getLastMovePacketTime();
|
||||||
|
if (gap > threshold) {
|
||||||
|
violationManager.addViolation(player, "Timer", 2.0);
|
||||||
|
punishmentManager.evaluate(player, "Timer");
|
||||||
|
data.setLastMovePacketTime(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5L, 5L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the plugin configuration and re-register checks
|
||||||
|
*/
|
||||||
|
public void reload() {
|
||||||
|
configManager.loadConfig();
|
||||||
|
violationManager.clearAll();
|
||||||
|
violationManager.setDecayRate(
|
||||||
|
configManager.getDouble("violation.decay_rate", 0.5));
|
||||||
|
|
||||||
|
if (decayTask != null) {
|
||||||
|
decayTask.cancel();
|
||||||
|
}
|
||||||
|
startDecayTask();
|
||||||
|
|
||||||
|
getLogger().info("Configuration reloaded!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
public static XeroAntiCheat getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigManager getConfigManager() {
|
||||||
|
return configManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ViolationManager getViolationManager() {
|
||||||
|
return violationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PunishmentManager getPunishmentManager() {
|
||||||
|
return punishmentManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CheckManager getCheckManager() {
|
||||||
|
return checkManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DatabaseManager getDatabaseManager() {
|
||||||
|
return databaseManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetricsManager getMetricsManager() {
|
||||||
|
return metricsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProtocolLibLoaded() {
|
||||||
|
return protocolLibLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAlertsEnabled(java.util.UUID uuid) {
|
||||||
|
return alertToggles.getOrDefault(uuid, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlertsEnabled(java.util.UUID uuid, boolean enabled) {
|
||||||
|
alertToggles.put(uuid, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVerboseTarget(UUID uuid) {
|
||||||
|
return verboseTargets.contains(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleVerboseTarget(UUID uuid) {
|
||||||
|
if (!verboseTargets.remove(uuid)) {
|
||||||
|
verboseTargets.add(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/main/java/com/xeroth/xeroanticheat/check/Check.java
Normal file
144
src/main/java/com/xeroth/xeroanticheat/check/Check.java
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package com.xeroth.xeroanticheat.check;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for all anti-cheat checks.
|
||||||
|
* All checks must extend this class and implement the check logic.
|
||||||
|
*/
|
||||||
|
public abstract class Check {
|
||||||
|
|
||||||
|
protected final XeroAntiCheat plugin;
|
||||||
|
protected final String name;
|
||||||
|
protected final String configPath;
|
||||||
|
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
public Check(XeroAntiCheat plugin, String name) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.name = name;
|
||||||
|
this.configPath = "checks." + name.toLowerCase() + ".";
|
||||||
|
this.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the check logic.
|
||||||
|
*
|
||||||
|
* @param data Player data for the player being checked
|
||||||
|
* @param player The player being checked
|
||||||
|
*/
|
||||||
|
public abstract void check(PlayerData data, Player player);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the check detects a violation.
|
||||||
|
*
|
||||||
|
* @param data Player data
|
||||||
|
* @param player The player
|
||||||
|
* @param weight Violation weight to add
|
||||||
|
*/
|
||||||
|
protected void flag(PlayerData data, Player player, double weight) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
if (player.hasPermission("xac.bypass")) return;
|
||||||
|
if (player.hasPermission("xac.bypass." + getCategory())) return;
|
||||||
|
if (player.hasPermission("xac.bypass." + name.toLowerCase())) return;
|
||||||
|
|
||||||
|
plugin.getViolationManager().addViolation(player, name, weight);
|
||||||
|
plugin.getPunishmentManager().evaluate(player, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplified flag with default weight
|
||||||
|
*/
|
||||||
|
protected void flag(PlayerData data, Player player) {
|
||||||
|
flag(data, player, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the check name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this check is enabled
|
||||||
|
*/
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled && plugin.getConfigManager().getBoolean(configPath + "enabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set enabled state
|
||||||
|
*/
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a configuration value
|
||||||
|
*/
|
||||||
|
protected boolean getConfigBoolean(String key, boolean defaultValue) {
|
||||||
|
return plugin.getConfigManager().getBoolean(configPath + key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a configuration value
|
||||||
|
*/
|
||||||
|
protected int getConfigInt(String key, int defaultValue) {
|
||||||
|
return plugin.getConfigManager().getInt(configPath + key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a configuration value
|
||||||
|
*/
|
||||||
|
protected double getConfigDouble(String key, double defaultValue) {
|
||||||
|
return plugin.getConfigManager().getDouble(configPath + key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a configuration value
|
||||||
|
*/
|
||||||
|
protected String getConfigString(String key, String defaultValue) {
|
||||||
|
return plugin.getConfigManager().getString(configPath + key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCategory() {
|
||||||
|
String pkg = getClass().getPackageName();
|
||||||
|
if (pkg.endsWith("movement")) return "movement";
|
||||||
|
if (pkg.endsWith("combat")) return "combat";
|
||||||
|
return "misc";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given player should be exempt from this check,
|
||||||
|
* based on the three-tier bypass permission hierarchy:
|
||||||
|
* xac.bypass → xac.bypass.<category> → xac.bypass.<checkname>
|
||||||
|
*
|
||||||
|
* @param player The player to test
|
||||||
|
* @return true if the player has any applicable bypass permission
|
||||||
|
*/
|
||||||
|
public boolean isBypassed(Player player) {
|
||||||
|
return player.hasPermission("xac.bypass")
|
||||||
|
|| player.hasPermission("xac.bypass." + getCategory())
|
||||||
|
|| player.hasPermission("xac.bypass." + name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setback(Player player, PlayerData data) {
|
||||||
|
if (!getConfigBoolean("setback", false)) return;
|
||||||
|
org.bukkit.Location safe = data.getLastSafeLocation();
|
||||||
|
if (safe == null) return;
|
||||||
|
data.clearPositionHistory();
|
||||||
|
player.teleport(safe);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isServerLagging() {
|
||||||
|
if (!plugin.getConfigManager().getBoolean("tps.enabled", true)) return false;
|
||||||
|
double tps = Bukkit.getTPS()[0];
|
||||||
|
double minTps = plugin.getConfigManager().getDouble("tps.min_tps_threshold", 18.0);
|
||||||
|
return tps < minTps;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.combat;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AutoClickerCheck - Tracks CPS (clicks per second) over a 1-second sliding window.
|
||||||
|
*
|
||||||
|
* Flags if CPS > configurable threshold (default: 20 CPS).
|
||||||
|
* Also analyzes inter-click intervals - legitimate humans show variance.
|
||||||
|
* Flags if variance is suspiciously low (jitter hack / butterfly click pattern).
|
||||||
|
*/
|
||||||
|
public class AutoClickerCheck extends Check {
|
||||||
|
|
||||||
|
public AutoClickerCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "AutoClicker");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Get thresholds
|
||||||
|
int maxCPS = getConfigInt("max_cps", 20);
|
||||||
|
double minVariance = getConfigDouble("min_variance", 2.0);
|
||||||
|
|
||||||
|
// Get CPS
|
||||||
|
int cps = data.getCPS();
|
||||||
|
|
||||||
|
// Check CPS threshold
|
||||||
|
if (cps > maxCPS) {
|
||||||
|
flag(data, player, (cps - maxCPS) * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pattern (low variance = suspicious)
|
||||||
|
checkPattern(data, player, minVariance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check click pattern for suspiciously low variance
|
||||||
|
*/
|
||||||
|
private void checkPattern(PlayerData data, Player player, double minVariance) {
|
||||||
|
if (data.getClickTimestamps().size() < 5) return;
|
||||||
|
|
||||||
|
double sum = 0;
|
||||||
|
int intervalCount = 0;
|
||||||
|
Long prev = null;
|
||||||
|
for (Long ts : data.getClickTimestamps()) {
|
||||||
|
if (prev != null) {
|
||||||
|
sum += (prev - ts);
|
||||||
|
intervalCount++;
|
||||||
|
}
|
||||||
|
prev = ts;
|
||||||
|
}
|
||||||
|
if (intervalCount == 0) return;
|
||||||
|
double mean = sum / intervalCount;
|
||||||
|
|
||||||
|
double varianceSum = 0;
|
||||||
|
prev = null;
|
||||||
|
for (Long ts : data.getClickTimestamps()) {
|
||||||
|
if (prev != null) {
|
||||||
|
double diff = (prev - ts) - mean;
|
||||||
|
varianceSum += diff * diff;
|
||||||
|
}
|
||||||
|
prev = ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
double stdDev = Math.sqrt(varianceSum / intervalCount);
|
||||||
|
|
||||||
|
if (stdDev < minVariance && data.getCPS() > 10) {
|
||||||
|
flag(data, player, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.combat;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CriticalCheck - Detects suspicious critical hits.
|
||||||
|
*
|
||||||
|
* Minecraft cancels sprint on critical hits. A hacked client can send both
|
||||||
|
* sprint and crit flags simultaneously.
|
||||||
|
*/
|
||||||
|
public class CriticalCheck extends Check {
|
||||||
|
|
||||||
|
public CriticalCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Critical");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a critical hit is valid
|
||||||
|
* @param player The player
|
||||||
|
* @param data Player data
|
||||||
|
* @param isCritical Whether the hit was critical
|
||||||
|
* @return true if the critical is suspicious
|
||||||
|
*/
|
||||||
|
public boolean checkCritical(Player player, PlayerData data, boolean isCritical) {
|
||||||
|
if (!isCritical) return false;
|
||||||
|
|
||||||
|
// Crit while sprinting — Minecraft cancels sprint on crits
|
||||||
|
if (player.isSprinting()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.combat;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KillAuraCheck - Detects attacks on entities outside realistic FOV, snap rotations,
|
||||||
|
* and multi-targeting within a single tick.
|
||||||
|
*
|
||||||
|
* Uses angle computation between player's look vector and entity vector.
|
||||||
|
*/
|
||||||
|
public class KillAuraCheck extends Check {
|
||||||
|
|
||||||
|
public KillAuraCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "KillAura");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Note: Rotation analysis moved to CombatListener.onEntityDamageByEntity()
|
||||||
|
// This method is kept for potential future use
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check angle between player look direction and entity
|
||||||
|
*/
|
||||||
|
public boolean checkAngle(Player player, Entity target) {
|
||||||
|
if (!isEnabled()) return false;
|
||||||
|
|
||||||
|
Location playerLoc = player.getLocation();
|
||||||
|
Vector playerDirection = playerLoc.getDirection();
|
||||||
|
|
||||||
|
Vector toTarget = target.getLocation().toVector().subtract(playerLoc.toVector()).normalize();
|
||||||
|
|
||||||
|
double dot = Math.max(-1.0, Math.min(1.0, playerDirection.dot(toTarget)));
|
||||||
|
double angle = Math.toDegrees(Math.acos(dot));
|
||||||
|
|
||||||
|
double maxAngle = getConfigDouble("max_angle", 100);
|
||||||
|
return angle > maxAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for multi-targeting
|
||||||
|
*/
|
||||||
|
public boolean checkMultiTarget(PlayerData data, Player player, Entity target) {
|
||||||
|
if (!isEnabled()) return false;
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long window = (long) getConfigDouble("multitarget_window_ms", 100);
|
||||||
|
|
||||||
|
data.addAttack(target.getUniqueId());
|
||||||
|
|
||||||
|
Set<UUID> uniqueInWindow = new HashSet<>();
|
||||||
|
Iterator<Long> timeIter = data.getAttackTimestamps().iterator();
|
||||||
|
Iterator<UUID> entityIter = data.getAttackedEntities().iterator();
|
||||||
|
|
||||||
|
while (timeIter.hasNext() && entityIter.hasNext()) {
|
||||||
|
long timestamp = timeIter.next();
|
||||||
|
UUID entityId = entityIter.next();
|
||||||
|
|
||||||
|
if (now - timestamp > window) break;
|
||||||
|
|
||||||
|
uniqueInWindow.add(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueInWindow.size() > 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.combat;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReachCheck - Computes 3D distance between player's eye position and
|
||||||
|
* attacked entity's hitbox at attack time.
|
||||||
|
*
|
||||||
|
* Default threshold: 3.2 blocks (creative: 5.0). Adds ping compensation.
|
||||||
|
*/
|
||||||
|
public class ReachCheck extends Check {
|
||||||
|
|
||||||
|
public ReachCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Reach");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check reach distance to a target entity
|
||||||
|
*/
|
||||||
|
public boolean checkReach(Player player, Entity target) {
|
||||||
|
if (!isEnabled()) return false;
|
||||||
|
|
||||||
|
// Get thresholds
|
||||||
|
double maxReach = getConfigDouble("max_reach", 3.2);
|
||||||
|
double creativeMaxReach = getConfigDouble("creative_max_reach", 5.0);
|
||||||
|
double pingFactor = getConfigDouble("ping_factor", 1.0);
|
||||||
|
|
||||||
|
// Adjust for creative mode
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE) {
|
||||||
|
maxReach = creativeMaxReach;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ping compensation (0.03 blocks per 100ms of ping, capped at 0.3)
|
||||||
|
int ping = player.getPing();
|
||||||
|
double pingComp = Math.min(ping / 100.0 * 0.03 * pingFactor, 0.3);
|
||||||
|
maxReach += pingComp;
|
||||||
|
|
||||||
|
// Add small leniency
|
||||||
|
maxReach += 0.3;
|
||||||
|
|
||||||
|
// Get player eye location
|
||||||
|
Location eyeLoc = player.getEyeLocation();
|
||||||
|
|
||||||
|
// Use center of entity bounding box, not feet position.
|
||||||
|
// 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 from player eye to entity center (no Location object needed)
|
||||||
|
double dx = eyeLoc.getX() - targetX;
|
||||||
|
double dy = eyeLoc.getY() - targetY;
|
||||||
|
double dz = eyeLoc.getZ() - targetZ;
|
||||||
|
double distanceSquared = dx * dx + dy * dy + dz * dz;
|
||||||
|
|
||||||
|
double maxReachSquared = maxReach * maxReach;
|
||||||
|
return distanceSquared > maxReachSquared;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.combat;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VelocityCheck - Detects players ignoring server-sent knockback velocity.
|
||||||
|
*
|
||||||
|
* Requires ProtocolLib. Listens for ENTITY_VELOCITY packets sent to the player,
|
||||||
|
* stores the expected velocity vector, then verifies that the player's actual
|
||||||
|
* movement on the following ticks reflects that velocity. Players using
|
||||||
|
* no-knockback hacks will show near-zero displacement after receiving a
|
||||||
|
* significant velocity packet.
|
||||||
|
*/
|
||||||
|
public class VelocityCheck extends Check {
|
||||||
|
|
||||||
|
public VelocityCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Velocity");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
if (!plugin.isProtocolLibLoaded()) return;
|
||||||
|
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE
|
||||||
|
|| player.getGameMode() == org.bukkit.GameMode.SPECTATOR) return;
|
||||||
|
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
Vector expected = data.getLastServerVelocity();
|
||||||
|
if (expected == null || data.getVelocityCheckTicks() <= 0) return;
|
||||||
|
|
||||||
|
double expectedHorizontal = Math.sqrt(
|
||||||
|
expected.getX() * expected.getX() + expected.getZ() * expected.getZ());
|
||||||
|
|
||||||
|
double minExpected = getConfigDouble("min_expected_velocity", 0.15);
|
||||||
|
if (expectedHorizontal < minExpected) {
|
||||||
|
data.clearServerVelocity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.decrementVelocityCheckTicks();
|
||||||
|
|
||||||
|
PlayerData.PositionSnapshot curr = data.getLastPosition();
|
||||||
|
PlayerData.PositionSnapshot prev = data.getSecondLastPosition();
|
||||||
|
if (curr == null || prev == null) return;
|
||||||
|
|
||||||
|
double actualHorizontal = Math.sqrt(
|
||||||
|
Math.pow(curr.x() - prev.x(), 2) + Math.pow(curr.z() - prev.z(), 2));
|
||||||
|
|
||||||
|
double ratio = getConfigDouble("min_displacement_ratio", 0.2);
|
||||||
|
if (actualHorizontal < expectedHorizontal * ratio) {
|
||||||
|
flag(data, player, 2.0);
|
||||||
|
data.clearServerVelocity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.misc;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FastEatCheck - Detects food consumption faster than the standard 1.61-second eating duration.
|
||||||
|
*
|
||||||
|
* Standard eat time is 32 ticks (1.6 seconds).
|
||||||
|
* Tracks item use start time via PlayerItemConsumeEvent.
|
||||||
|
*/
|
||||||
|
public class FastEatCheck extends Check {
|
||||||
|
|
||||||
|
public FastEatCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "FastEat");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if eating is too fast
|
||||||
|
* @param player The player
|
||||||
|
* @param data Player data
|
||||||
|
* @param consumeTime Time of consumption
|
||||||
|
* @return true if eating too fast
|
||||||
|
*/
|
||||||
|
public boolean checkFastEat(Player player, PlayerData data, long consumeTime) {
|
||||||
|
if (!isEnabled()) return false;
|
||||||
|
|
||||||
|
long startedEating = data.getLastStartedEatingTime();
|
||||||
|
if (startedEating <= 0) return false;
|
||||||
|
|
||||||
|
int maxEatTicks = getConfigInt("max_eat_ticks", 32);
|
||||||
|
long minMs = (long) maxEatTicks * 50;
|
||||||
|
|
||||||
|
long elapsed = consumeTime - startedEating;
|
||||||
|
|
||||||
|
data.setLastStartedEatingTime(0);
|
||||||
|
|
||||||
|
return elapsed < minMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.misc;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FastPlaceCheck - Detects block placement faster than 1 block per tick (>20 blocks/second).
|
||||||
|
*
|
||||||
|
* Uses a timestamp deque; flags if N blocks placed within M milliseconds.
|
||||||
|
*/
|
||||||
|
public class FastPlaceCheck extends Check {
|
||||||
|
|
||||||
|
public FastPlaceCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "FastPlace");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative mode
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Get threshold
|
||||||
|
int maxBlocksPerSecond = getConfigInt("max_blocks_per_second", 20);
|
||||||
|
|
||||||
|
// Get blocks per second
|
||||||
|
int bps = data.getBlocksPerSecond();
|
||||||
|
|
||||||
|
if (bps > maxBlocksPerSecond) {
|
||||||
|
data.incrementFastPlaceBuffer();
|
||||||
|
int buffer = getConfigInt("buffer_ticks", 2);
|
||||||
|
if (data.getFastPlaceBuffer() >= buffer) {
|
||||||
|
flag(data, player, (bps - maxBlocksPerSecond) * 0.5);
|
||||||
|
data.resetFastPlaceBuffer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetFastPlaceBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.misc;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InventoryMoveCheck - Detects player movement while an inventory is open.
|
||||||
|
*
|
||||||
|
* Uses InventoryOpenEvent / InventoryCloseEvent to track state.
|
||||||
|
* Flags significant position change while inventory is open.
|
||||||
|
*/
|
||||||
|
public class InventoryMoveCheck extends Check {
|
||||||
|
|
||||||
|
public InventoryMoveCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "InventoryMove");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Only check if inventory is open
|
||||||
|
if (!data.isInventoryOpen()) return;
|
||||||
|
|
||||||
|
// Get position history
|
||||||
|
PlayerData.PositionSnapshot current = data.getLastPosition();
|
||||||
|
PlayerData.PositionSnapshot last = data.getSecondLastPosition();
|
||||||
|
|
||||||
|
if (current == null || last == null) return;
|
||||||
|
|
||||||
|
// Calculate position change (squared to avoid Math.sqrt())
|
||||||
|
double dx = current.x() - last.x();
|
||||||
|
double dy = current.y() - last.y();
|
||||||
|
double dz = current.z() - last.z();
|
||||||
|
double distanceSquared = dx*dx + dy*dy + dz*dz;
|
||||||
|
|
||||||
|
// If significant movement while inventory open, flag
|
||||||
|
if (distanceSquared > 0.01) {
|
||||||
|
flag(data, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.misc;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Deque;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScaffoldCheck - Detects suspicious block placement patterns characteristic of scaffold hacks.
|
||||||
|
*
|
||||||
|
* Combines signals:
|
||||||
|
* 1. Placing blocks behind/below while moving forward
|
||||||
|
* 2. Placing blocks with pitch angle >75° (looking almost straight down)
|
||||||
|
* 3. Placing blocks without valid adjacent support face
|
||||||
|
* 4. Rotation lock (yaw barely changes while side-bridging)
|
||||||
|
* 5. Placement interval variance (too-perfect timing)
|
||||||
|
*
|
||||||
|
* Requires at least 2 signals to flag.
|
||||||
|
*/
|
||||||
|
public class ScaffoldCheck extends Check {
|
||||||
|
|
||||||
|
public ScaffoldCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Scaffold");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Reset signals periodically
|
||||||
|
if (data.getScaffoldSignals() > 10) {
|
||||||
|
data.resetScaffoldSignals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check scaffold signals for a block placement
|
||||||
|
* @param player The player
|
||||||
|
* @param blockPlaced The block that was placed
|
||||||
|
* @param data Player data
|
||||||
|
* @return true if scaffold is detected
|
||||||
|
*/
|
||||||
|
public boolean checkScaffold(Player player, Block blockPlaced, PlayerData data) {
|
||||||
|
if (!isEnabled()) return false;
|
||||||
|
|
||||||
|
int signalsRequired = getConfigInt("signals_required", 2);
|
||||||
|
int signalCount = 0;
|
||||||
|
|
||||||
|
// Signal 1: Pitch check - looking down while placing
|
||||||
|
float pitch = player.getLocation().getPitch();
|
||||||
|
int minPitch = getConfigInt("min_pitch", 75);
|
||||||
|
if (pitch > minPitch) {
|
||||||
|
signalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal 2: Position check - placing below/behind player
|
||||||
|
Location playerLoc = player.getLocation();
|
||||||
|
Location blockLoc = blockPlaced.getLocation();
|
||||||
|
|
||||||
|
double dx = blockLoc.getX() - playerLoc.getX();
|
||||||
|
double dy = blockLoc.getY() - playerLoc.getY();
|
||||||
|
double dz = blockLoc.getZ() - playerLoc.getZ();
|
||||||
|
|
||||||
|
// If placing below
|
||||||
|
if (dy < -0.5) {
|
||||||
|
signalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal 3: No valid support face
|
||||||
|
boolean hasSupport = hasValidSupport(blockPlaced);
|
||||||
|
if (!hasSupport) {
|
||||||
|
signalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal 4: Rotation lock
|
||||||
|
float currentYaw = player.getLocation().getYaw();
|
||||||
|
|
||||||
|
if (!Float.isNaN(data.getLastPlacementYaw())) {
|
||||||
|
float yawDelta = Math.abs(currentYaw - data.getLastPlacementYaw());
|
||||||
|
if (yawDelta > 180) yawDelta = 360 - yawDelta;
|
||||||
|
|
||||||
|
PlayerData.PositionSnapshot curr = data.getLastPosition();
|
||||||
|
PlayerData.PositionSnapshot prev = data.getSecondLastPosition();
|
||||||
|
double horizSpeed = 0;
|
||||||
|
if (curr != null && prev != null) {
|
||||||
|
double hdx = curr.x() - prev.x();
|
||||||
|
double hdz = curr.z() - prev.z();
|
||||||
|
horizSpeed = Math.sqrt(hdx * hdx + hdz * hdz);
|
||||||
|
}
|
||||||
|
|
||||||
|
float rotLockThreshold = (float) getConfigDouble("rotation_lock_threshold", 2.0);
|
||||||
|
double minMoveSpeed = getConfigDouble("min_move_speed", 0.15);
|
||||||
|
|
||||||
|
if (yawDelta < rotLockThreshold && horizSpeed > minMoveSpeed) {
|
||||||
|
signalCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.setLastPlacementYaw(currentYaw);
|
||||||
|
|
||||||
|
// Signal 5: Placement interval variance
|
||||||
|
double minVarianceMs = getConfigDouble("min_placement_variance_ms", 30.0);
|
||||||
|
int minBpsForCheck = getConfigInt("min_bps_for_variance_check", 5);
|
||||||
|
|
||||||
|
if (data.getBlocksPerSecond() >= minBpsForCheck) {
|
||||||
|
double stdDev = computePlacementIntervalStdDev(data.getBlockPlaceTimestamps());
|
||||||
|
if (stdDev < minVarianceMs) {
|
||||||
|
signalCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return signalCount >= signalsRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double computePlacementIntervalStdDev(Deque<Long> timestamps) {
|
||||||
|
int size = timestamps.size();
|
||||||
|
if (size < 5) return Double.MAX_VALUE;
|
||||||
|
|
||||||
|
double sum = 0;
|
||||||
|
int intervalCount = 0;
|
||||||
|
Long prev = null;
|
||||||
|
for (Long ts : timestamps) {
|
||||||
|
if (prev != null) {
|
||||||
|
sum += (prev - ts);
|
||||||
|
intervalCount++;
|
||||||
|
}
|
||||||
|
prev = ts;
|
||||||
|
}
|
||||||
|
if (intervalCount == 0) return Double.MAX_VALUE;
|
||||||
|
double mean = sum / intervalCount;
|
||||||
|
|
||||||
|
double varianceSum = 0;
|
||||||
|
prev = null;
|
||||||
|
for (Long ts : timestamps) {
|
||||||
|
if (prev != null) {
|
||||||
|
double diff = (prev - ts) - mean;
|
||||||
|
varianceSum += diff * diff;
|
||||||
|
}
|
||||||
|
prev = ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.sqrt(varianceSum / intervalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasValidSupport(Block block) {
|
||||||
|
// Check adjacent blocks for support
|
||||||
|
Block[] adjacent = {
|
||||||
|
block.getRelative(org.bukkit.block.BlockFace.NORTH),
|
||||||
|
block.getRelative(org.bukkit.block.BlockFace.SOUTH),
|
||||||
|
block.getRelative(org.bukkit.block.BlockFace.EAST),
|
||||||
|
block.getRelative(org.bukkit.block.BlockFace.WEST),
|
||||||
|
block.getRelative(org.bukkit.block.BlockFace.DOWN)
|
||||||
|
};
|
||||||
|
|
||||||
|
for (Block adj : adjacent) {
|
||||||
|
if (!adj.getType().isAir()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.movement;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FlyCheck - Detects sustained upward or horizontal movement while the player
|
||||||
|
* has no creative/spectator mode, no elytra, no levitation, no jump boost.
|
||||||
|
*
|
||||||
|
* Uses a fall buffer to allow for block stepping, slabs, trapdoors.
|
||||||
|
* Tracks ground desync between client and server.
|
||||||
|
*/
|
||||||
|
public class FlyCheck extends Check {
|
||||||
|
|
||||||
|
public FlyCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Fly");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Skip if server is lagging
|
||||||
|
if (isServerLagging()) return;
|
||||||
|
|
||||||
|
// Ignore elytra gliding
|
||||||
|
if (player.isGliding()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has levitation effect
|
||||||
|
if (data.hasLevitation()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get fall buffer
|
||||||
|
int fallBuffer = getConfigInt("fall_buffer", 10);
|
||||||
|
|
||||||
|
// Get position data
|
||||||
|
PlayerData.PositionSnapshot current = data.getLastPosition();
|
||||||
|
if (current == null) return;
|
||||||
|
|
||||||
|
boolean clientOnGround = current.onGround();
|
||||||
|
boolean serverOnGround = player.isOnGround();
|
||||||
|
|
||||||
|
// Check ground desync
|
||||||
|
int desyncThreshold = getConfigInt("ground_desync_threshold", 3);
|
||||||
|
if (clientOnGround != serverOnGround) {
|
||||||
|
if (data.getAirTicks() > fallBuffer + desyncThreshold) {
|
||||||
|
flag(data, player);
|
||||||
|
setback(player, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for sustained flight without ground
|
||||||
|
if (!serverOnGround && !clientOnGround) {
|
||||||
|
// Get velocity
|
||||||
|
org.bukkit.util.Vector velocity = player.getVelocity();
|
||||||
|
|
||||||
|
// If moving up or staying at same height while not supposed to
|
||||||
|
if (velocity.getY() > 0.1 || Math.abs(velocity.getY()) < 0.01) {
|
||||||
|
if (data.getAirTicks() > fallBuffer) {
|
||||||
|
if (!data.hasJumpBoost()) {
|
||||||
|
flag(data, player);
|
||||||
|
setback(player, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.movement;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GlideCheck - Detects players in non-elytra mode exhibiting glide-like fall curves
|
||||||
|
* (very slow Y decrease while moving horizontally fast).
|
||||||
|
*
|
||||||
|
* Cross-references isGliding() with actual velocity.
|
||||||
|
*/
|
||||||
|
public class GlideCheck extends Check {
|
||||||
|
|
||||||
|
public GlideCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Glide");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Skip if server is lagging
|
||||||
|
if (isServerLagging()) return;
|
||||||
|
|
||||||
|
// Get thresholds
|
||||||
|
double minHorizontalSpeed = getConfigDouble("min_horizontal_speed", 0.5);
|
||||||
|
double maxYDecrease = getConfigDouble("max_y_decrease", 0.1);
|
||||||
|
|
||||||
|
// Get velocity
|
||||||
|
org.bukkit.util.Vector velocity = player.getVelocity();
|
||||||
|
|
||||||
|
// Calculate horizontal speed (squared to avoid Math.sqrt())
|
||||||
|
double horizontalSpeedSq = velocity.getX() * velocity.getX() + velocity.getZ() * velocity.getZ();
|
||||||
|
|
||||||
|
// Check if moving fast horizontally
|
||||||
|
double minHorizSq = minHorizontalSpeed * minHorizontalSpeed;
|
||||||
|
if (horizontalSpeedSq < minHorizSq) {
|
||||||
|
data.resetGlideTicks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if player is gliding (with elytra)
|
||||||
|
if (player.isGliding()) {
|
||||||
|
data.resetGlideTicks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Y velocity - should be negative (falling)
|
||||||
|
double yVel = velocity.getY();
|
||||||
|
|
||||||
|
// If falling very slowly (glide-like), flag
|
||||||
|
if (yVel < 0 && yVel > -maxYDecrease) {
|
||||||
|
// Only flag if sustained over multiple ticks
|
||||||
|
data.incrementGlideTicks();
|
||||||
|
|
||||||
|
if (data.getGlideTicks() > 5) {
|
||||||
|
flag(data, player);
|
||||||
|
setback(player, data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetGlideTicks();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track last gliding state
|
||||||
|
data.setLastWasGliding(player.isGliding());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.movement;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JesusCheck - Detects walking on water without Frost Walker enchantment or boat.
|
||||||
|
*
|
||||||
|
* Checks block type beneath player feet on server side.
|
||||||
|
* Flags if water/lava but player maintains constant Y.
|
||||||
|
*/
|
||||||
|
public class JesusCheck extends Check {
|
||||||
|
|
||||||
|
public JesusCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Jesus");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Skip players swimming underwater
|
||||||
|
if (player.isSwimming()) return;
|
||||||
|
|
||||||
|
// Get block below player
|
||||||
|
Location loc = player.getLocation();
|
||||||
|
Material blockBelow = loc.clone().subtract(0, 1, 0).getBlock().getType();
|
||||||
|
|
||||||
|
// Check if player is on water or lava
|
||||||
|
boolean onWater = blockBelow == Material.WATER;
|
||||||
|
boolean onLava = blockBelow == Material.LAVA;
|
||||||
|
|
||||||
|
if (!onWater && !onLava) return;
|
||||||
|
|
||||||
|
// Check if player is on a boat
|
||||||
|
if (player.getVehicle() != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Frost Walker enchantment
|
||||||
|
org.bukkit.inventory.ItemStack boots = player.getInventory().getBoots();
|
||||||
|
boolean hasFrostWalker = false;
|
||||||
|
|
||||||
|
if (boots != null && boots.hasItemMeta()) {
|
||||||
|
hasFrostWalker = boots.getItemMeta().hasEnchant(org.bukkit.enchantments.Enchantment.FROST_WALKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if player has Dolphins Grace (can swim faster)
|
||||||
|
if (data.hasDolphinsGrace()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If player is walking on water without Frost Walker, flag
|
||||||
|
if (!hasFrostWalker) {
|
||||||
|
// Additional check: check if player is actually moving horizontally
|
||||||
|
PlayerData.PositionSnapshot current = data.getLastPosition();
|
||||||
|
PlayerData.PositionSnapshot last = data.getSecondLastPosition();
|
||||||
|
|
||||||
|
if (current != null && last != null) {
|
||||||
|
double dx = current.x() - last.x();
|
||||||
|
double dz = current.z() - last.z();
|
||||||
|
double horizontalSpeedSq = dx * dx + dz * dz;
|
||||||
|
|
||||||
|
// If moving at reasonable speed on water, flag
|
||||||
|
if (horizontalSpeedSq > 0.01) {
|
||||||
|
data.incrementJesusBuffer();
|
||||||
|
int buffer = getConfigInt("buffer_ticks", 3);
|
||||||
|
if (data.getJesusBuffer() >= buffer) {
|
||||||
|
flag(data, player);
|
||||||
|
setback(player, data);
|
||||||
|
data.resetJesusBuffer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetJesusBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.movement;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NoFallCheck - Detects when a player takes no fall damage after falling more than 3 blocks.
|
||||||
|
*
|
||||||
|
* Calculates expected fall damage when landing and stores it in PlayerData.
|
||||||
|
* Actual flagging happens in MiscListener via EntityDamageEvent.
|
||||||
|
*/
|
||||||
|
public class NoFallCheck extends Check {
|
||||||
|
|
||||||
|
public NoFallCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "NoFall");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Ignore if player has slow falling (immune to fall damage)
|
||||||
|
if (data.hasSlowFalling()) {
|
||||||
|
data.setLastExpectedFallDamage(0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get minimum fall distance
|
||||||
|
int minFallDistance = getConfigInt("min_fall_distance", 3);
|
||||||
|
|
||||||
|
// Get server-computed fall distance
|
||||||
|
float fallDistance = player.getFallDistance();
|
||||||
|
|
||||||
|
// Store fall distance
|
||||||
|
data.setLastFallDistance(fallDistance);
|
||||||
|
|
||||||
|
// Check if player is on ground now with significant fall distance
|
||||||
|
if (player.isOnGround() && fallDistance > minFallDistance) {
|
||||||
|
// Calculate expected damage
|
||||||
|
double expectedDamage = (fallDistance - minFallDistance) / 2.0;
|
||||||
|
|
||||||
|
// Check for damage-reducing blocks
|
||||||
|
Location loc = player.getLocation();
|
||||||
|
Material blockBelow = loc.clone().subtract(0, 1, 0).getBlock().getType();
|
||||||
|
|
||||||
|
// Blocks that reduce/cancel fall damage
|
||||||
|
if (blockBelow == Material.WATER ||
|
||||||
|
blockBelow == Material.HONEY_BLOCK ||
|
||||||
|
blockBelow == Material.HAY_BLOCK ||
|
||||||
|
blockBelow == Material.SLIME_BLOCK ||
|
||||||
|
blockBelow == Material.COBWEB ||
|
||||||
|
blockBelow == Material.POWDER_SNOW ||
|
||||||
|
org.bukkit.Tag.BEDS.isTagged(blockBelow)) {
|
||||||
|
data.setLastExpectedFallDamage(0.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for feather falling
|
||||||
|
org.bukkit.inventory.ItemStack boots = player.getInventory().getBoots();
|
||||||
|
if (boots != null && boots.hasItemMeta() && boots.getItemMeta().hasEnchant(org.bukkit.enchantments.Enchantment.FEATHER_FALLING)) {
|
||||||
|
int featherFallingLevel = boots.getItemMeta().getEnchantLevel(org.bukkit.enchantments.Enchantment.FEATHER_FALLING);
|
||||||
|
double multiplier = Math.max(0.0, 1.0 - (featherFallingLevel * 0.12));
|
||||||
|
expectedDamage *= multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store expected damage for later comparison in EntityDamageEvent
|
||||||
|
data.setLastExpectedFallDamage(expectedDamage);
|
||||||
|
} else if (player.isOnGround()) {
|
||||||
|
// Reset when on ground with no fall
|
||||||
|
data.setLastExpectedFallDamage(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.movement;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.FluidCollisionMode;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.util.RayTraceResult;
|
||||||
|
import org.bukkit.util.Vector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PhaseCheck - Detects players clipping through solid blocks (NoClip/Phase).
|
||||||
|
*
|
||||||
|
* Uses a server-side ray-cast between the player's last two positions.
|
||||||
|
* If a solid block intersects the movement path, the player phased through it.
|
||||||
|
* Only runs when horizontal+vertical distance exceeds 0.5 blocks to avoid
|
||||||
|
* unnecessary ray-cast calls on micro-movements.
|
||||||
|
*/
|
||||||
|
public class PhaseCheck extends Check {
|
||||||
|
|
||||||
|
public PhaseCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Phase");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE
|
||||||
|
|| player.getGameMode() == org.bukkit.GameMode.SPECTATOR) return;
|
||||||
|
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
PlayerData.PositionSnapshot curr = data.getLastPosition();
|
||||||
|
PlayerData.PositionSnapshot prev = data.getSecondLastPosition();
|
||||||
|
if (curr == null || prev == null) return;
|
||||||
|
|
||||||
|
double dx = curr.x() - prev.x();
|
||||||
|
double dy = curr.y() - prev.y();
|
||||||
|
double dz = curr.z() - prev.z();
|
||||||
|
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||||
|
|
||||||
|
double minDist = getConfigDouble("min_distance", 0.5);
|
||||||
|
if (dist < minDist) return;
|
||||||
|
|
||||||
|
double maxDist = getConfigDouble("max_distance", 5.0);
|
||||||
|
if (dist > maxDist) return;
|
||||||
|
|
||||||
|
Location from = player.getEyeLocation().clone();
|
||||||
|
from.setX(prev.x());
|
||||||
|
from.setY(prev.y() + player.getEyeHeight());
|
||||||
|
from.setZ(prev.z());
|
||||||
|
Vector direction = new Vector(dx, dy, dz).normalize();
|
||||||
|
|
||||||
|
RayTraceResult result = player.getWorld().rayTraceBlocks(
|
||||||
|
from, direction, dist,
|
||||||
|
FluidCollisionMode.NEVER, true);
|
||||||
|
|
||||||
|
if (result != null && result.getHitBlock() != null
|
||||||
|
&& result.getHitBlock().getType().isSolid()) {
|
||||||
|
flag(data, player, 3.0);
|
||||||
|
setback(player, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.movement;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.potion.PotionEffectType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpeedCheck - Detects horizontal movement faster than the maximum possible speed
|
||||||
|
* for the player's current state (sprinting, walking, sneaking, swimming, on ice, etc.)
|
||||||
|
*
|
||||||
|
* Uses rolling average over N ticks to avoid spike false positives.
|
||||||
|
* Accounts for server TPS fluctuation, ping compensation, and potion effects.
|
||||||
|
*/
|
||||||
|
public class SpeedCheck extends Check {
|
||||||
|
|
||||||
|
public SpeedCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Speed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator mode
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Get current and last position
|
||||||
|
PlayerData.PositionSnapshot current = data.getLastPosition();
|
||||||
|
PlayerData.PositionSnapshot last = data.getSecondLastPosition();
|
||||||
|
|
||||||
|
if (current == null || last == null) return;
|
||||||
|
|
||||||
|
// Calculate time delta
|
||||||
|
long timeDelta = current.timestamp() - last.timestamp();
|
||||||
|
if (timeDelta <= 0) return;
|
||||||
|
|
||||||
|
// Calculate horizontal distance
|
||||||
|
double dx = current.x() - last.x();
|
||||||
|
double dz = current.z() - last.z();
|
||||||
|
double horizontalDistance = Math.sqrt(dx * dx + dz * dz);
|
||||||
|
|
||||||
|
// Calculate speed (blocks per tick)
|
||||||
|
double speed = horizontalDistance / (timeDelta / 50.0);
|
||||||
|
|
||||||
|
// Get server TPS
|
||||||
|
double tpsMultiplier = 1.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
|
||||||
|
double maxSpeed = calculateMaxSpeed(player, data);
|
||||||
|
|
||||||
|
// Apply TPS compensation
|
||||||
|
maxSpeed *= tpsMultiplier;
|
||||||
|
|
||||||
|
// Add latency compensation (scales with actual ping)
|
||||||
|
int ping = player.getPing();
|
||||||
|
double pingFactor = getConfigDouble("ping_factor", 1.0);
|
||||||
|
double latencyComp = Math.min(ping / 100.0 * 0.01 * pingFactor, 0.15);
|
||||||
|
maxSpeed += latencyComp;
|
||||||
|
|
||||||
|
// Allow 10% tolerance
|
||||||
|
maxSpeed *= 1.1;
|
||||||
|
|
||||||
|
// Check for knockback (recently damaged)
|
||||||
|
long knockbackGrace = 500; // ms
|
||||||
|
boolean recentlyDamaged = (System.currentTimeMillis() - data.getLastKnockbackTime()) < knockbackGrace;
|
||||||
|
|
||||||
|
if (speed > maxSpeed * 1.5 && !recentlyDamaged) {
|
||||||
|
data.incrementSpeedViolationTicks();
|
||||||
|
int bufferTicks = getConfigInt("buffer_ticks", 5);
|
||||||
|
if (data.getSpeedViolationTicks() >= bufferTicks) {
|
||||||
|
flag(data, player, (speed - maxSpeed) * 2);
|
||||||
|
setback(player, data);
|
||||||
|
data.resetSpeedViolationTicks();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetSpeedViolationTicks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double calculateMaxSpeed(Player player, PlayerData data) {
|
||||||
|
double baseSpeed = 0.56; // Base walking speed
|
||||||
|
|
||||||
|
// Sprinting
|
||||||
|
if (player.isSprinting()) {
|
||||||
|
baseSpeed *= 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sneaking
|
||||||
|
if (player.isSneaking()) {
|
||||||
|
baseSpeed *= 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swimming
|
||||||
|
if (player.isSwimming()) {
|
||||||
|
baseSpeed *= 0.52;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flying
|
||||||
|
if (player.isFlying()) {
|
||||||
|
baseSpeed = 1.0; // Fly speed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potion effects
|
||||||
|
if (data.hasSpeedEffect()) {
|
||||||
|
baseSpeed *= (1.0 + (data.getSpeedLevel() * 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.hasSlownessEffect()) {
|
||||||
|
baseSpeed *= (1.0 - (data.getSlownessLevel() * 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.hasDolphinsGrace()) {
|
||||||
|
baseSpeed *= 1.33;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ice
|
||||||
|
if (data.isOnIce()) {
|
||||||
|
baseSpeed *= 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soul sand slows
|
||||||
|
Location loc = player.getLocation();
|
||||||
|
Material blockBelow = loc.clone().subtract(0, 1, 0).getBlock().getType();
|
||||||
|
if (blockBelow == Material.SOUL_SAND || blockBelow == Material.SOUL_SOIL) {
|
||||||
|
baseSpeed *= 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.movement;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpiderCheck - Detects climbing non-climbable blocks (non-ladder, non-vine, non-scaffolding)
|
||||||
|
* with upward Y velocity.
|
||||||
|
*
|
||||||
|
* Verifies block type at player's feet and body on server side.
|
||||||
|
*/
|
||||||
|
public class SpiderCheck extends Check {
|
||||||
|
|
||||||
|
public SpiderCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Spider");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Skip if server is lagging
|
||||||
|
if (isServerLagging()) return;
|
||||||
|
|
||||||
|
// Get velocity
|
||||||
|
org.bukkit.util.Vector velocity = player.getVelocity();
|
||||||
|
|
||||||
|
// Only check if moving upward
|
||||||
|
if (velocity.getY() <= 0) {
|
||||||
|
data.resetSpiderTicks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if player is actually on ground (server-side)
|
||||||
|
if (player.isOnGround()) {
|
||||||
|
data.resetSpiderTicks();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get blocks around player using block coordinates (no Location mutation)
|
||||||
|
org.bukkit.World world = player.getWorld();
|
||||||
|
org.bukkit.Location loc = player.getLocation();
|
||||||
|
int blockX = loc.getBlockX();
|
||||||
|
int blockY = loc.getBlockY();
|
||||||
|
int blockZ = loc.getBlockZ();
|
||||||
|
|
||||||
|
Material feetBlock = world.getBlockAt(blockX, blockY - 1, blockZ).getType();
|
||||||
|
Material bodyBlock = world.getBlockAt(blockX, blockY, blockZ).getType();
|
||||||
|
Material headBlock = world.getBlockAt(blockX, blockY + 1, blockZ).getType();
|
||||||
|
|
||||||
|
// Check if any of these blocks are climbable
|
||||||
|
boolean feetClimbable = isClimbable(feetBlock);
|
||||||
|
boolean bodyClimbable = isClimbable(bodyBlock);
|
||||||
|
boolean headClimbable = isClimbable(headBlock);
|
||||||
|
|
||||||
|
// If not climbing any climbable block but moving up, flag
|
||||||
|
if (!feetClimbable && !bodyClimbable && !headClimbable) {
|
||||||
|
// Additional check - only flag if sustained upward movement
|
||||||
|
data.incrementSpiderTicks();
|
||||||
|
|
||||||
|
if (data.getSpiderTicks() > 5 && velocity.getY() > 0.1) {
|
||||||
|
flag(data, player);
|
||||||
|
setback(player, data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetSpiderTicks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isClimbable(Material block) {
|
||||||
|
return block == Material.LADDER ||
|
||||||
|
block == Material.VINE ||
|
||||||
|
block == Material.SCAFFOLDING ||
|
||||||
|
block == Material.TWISTING_VINES ||
|
||||||
|
block == Material.WEEPING_VINES ||
|
||||||
|
block == Material.CAVE_VINES ||
|
||||||
|
block == Material.CAVE_VINES_PLANT;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.xeroth.xeroanticheat.checks.movement;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TimerCheck - Detects players sending move packets faster than 20/second
|
||||||
|
* or packet suppression (blink) - player sends no move packets for >500ms then teleports.
|
||||||
|
*
|
||||||
|
* Requires ProtocolLib for packet-level accuracy; falls back to event timing without it.
|
||||||
|
*/
|
||||||
|
public class TimerCheck extends Check {
|
||||||
|
|
||||||
|
public TimerCheck(XeroAntiCheat plugin) {
|
||||||
|
super(plugin, "Timer");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(PlayerData data, Player player) {
|
||||||
|
if (!isEnabled()) return;
|
||||||
|
|
||||||
|
// Ignore creative/spectator
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE ||
|
||||||
|
player.getGameMode() == org.bukkit.GameMode.SPECTATOR) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore if player has bypass permission
|
||||||
|
if (isBypassed(player)) return;
|
||||||
|
|
||||||
|
// Get thresholds
|
||||||
|
int maxPacketsPerSecond = getConfigInt("max_packets_per_second", 22);
|
||||||
|
|
||||||
|
// If ProtocolLib is active, packet counting is handled by PacketListener
|
||||||
|
if (plugin.isProtocolLibLoaded()) {
|
||||||
|
if (data.getPacketsThisSecond() > maxPacketsPerSecond) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset packet count every second
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now - data.getLastPacketCountReset() > 1000) {
|
||||||
|
data.setPacketsThisSecond(0);
|
||||||
|
data.setLastPacketCountReset(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment packet count
|
||||||
|
data.incrementPacketsThisSecond();
|
||||||
|
|
||||||
|
// Check for too many packets
|
||||||
|
if (data.getPacketsThisSecond() > maxPacketsPerSecond) {
|
||||||
|
data.incrementTimerBuffer();
|
||||||
|
int buffer = getConfigInt("buffer_ticks", 2);
|
||||||
|
if (data.getTimerBuffer() >= buffer) {
|
||||||
|
flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5);
|
||||||
|
data.resetTimerBuffer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetTimerBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
data.setLastMovePacketTime(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
334
src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java
Normal file
334
src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
package com.xeroth.xeroanticheat.command;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.TabCompleter;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class XACCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
|
||||||
|
public XACCommand(XeroAntiCheat plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
sendHelp(sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (args[0].toLowerCase()) {
|
||||||
|
case "reload" -> {
|
||||||
|
if (!has(sender, "xac.command.reload")) return true;
|
||||||
|
reload(sender);
|
||||||
|
}
|
||||||
|
case "status" -> {
|
||||||
|
if (!has(sender, "xac.command.status")) return true;
|
||||||
|
if (args.length < 2) {
|
||||||
|
sender.sendMessage(usage("/xac status <player>"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
showStatus(sender, args[1]);
|
||||||
|
}
|
||||||
|
case "punish" -> {
|
||||||
|
if (!has(sender, "xac.command.punish")) return true;
|
||||||
|
if (args.length < 3) {
|
||||||
|
sender.sendMessage(usage("/xac punish <player> <check>"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
punish(sender, args[1], args[2]);
|
||||||
|
}
|
||||||
|
case "clearviolations" -> {
|
||||||
|
if (!has(sender, "xac.command.clearviolations")) return true;
|
||||||
|
if (args.length < 2) {
|
||||||
|
sender.sendMessage(usage("/xac clearviolations <player>"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
clearViolations(sender, args[1]);
|
||||||
|
}
|
||||||
|
case "verbose" -> {
|
||||||
|
if (!has(sender, "xac.command.verbose")) return true;
|
||||||
|
if (args.length < 2) {
|
||||||
|
sender.sendMessage(usage("/xac verbose <player>"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
toggleVerbose(sender, args[1]);
|
||||||
|
}
|
||||||
|
case "alerts" -> {
|
||||||
|
if (!has(sender, "xac.command.alerts")) return true;
|
||||||
|
if (args.length < 2) {
|
||||||
|
boolean current = sender instanceof Player p && plugin.isAlertsEnabled(p.getUniqueId());
|
||||||
|
sender.sendMessage(Component.text("Alerts: " + (current ? "ON" : "OFF"), NamedTextColor.YELLOW));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
default -> sendHelp(sender);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean has(CommandSender sender, String permission) {
|
||||||
|
if (sender.hasPermission(permission) || sender.hasPermission("xac.admin")) return true;
|
||||||
|
sender.sendMessage(Component.text(
|
||||||
|
"You don't have permission to use this command. (requires " + permission + ")",
|
||||||
|
NamedTextColor.RED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Component usage(String text) {
|
||||||
|
return Component.text("Usage: " + text, NamedTextColor.RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendHelp(CommandSender sender) {
|
||||||
|
sender.sendMessage(Component.text("— XeroAntiCheat —", NamedTextColor.GOLD));
|
||||||
|
|
||||||
|
record Cmd(String syntax, String desc, String perm) {}
|
||||||
|
List.of(
|
||||||
|
new Cmd("/xac reload", "reload config", "xac.command.reload"),
|
||||||
|
new Cmd("/xac status <player>", "view violation levels", "xac.command.status"),
|
||||||
|
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 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 stats", "show plugin stats", "xac.admin"),
|
||||||
|
new Cmd("/xac version", "show version", "xac.command.version")
|
||||||
|
).forEach(cmd -> {
|
||||||
|
if (sender.hasPermission(cmd.perm()) || sender.hasPermission("xac.admin")) {
|
||||||
|
sender.sendMessage(
|
||||||
|
Component.text(cmd.syntax(), NamedTextColor.AQUA)
|
||||||
|
.append(Component.text(" — " + cmd.desc(), NamedTextColor.GRAY))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reload(CommandSender sender) {
|
||||||
|
plugin.reload();
|
||||||
|
plugin.getCheckManager().reloadChecks();
|
||||||
|
sender.sendMessage(Component.text("Configuration reloaded!", NamedTextColor.GREEN));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showStatus(CommandSender sender, String playerName) {
|
||||||
|
Player target = Bukkit.getPlayer(playerName);
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
sender.sendMessage(Component.text("Player not found or not online.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(target);
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
sender.sendMessage(Component.text("No data for player", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.sendMessage(Component.text("=== " + target.getName() + " Violation Status ===", NamedTextColor.GOLD));
|
||||||
|
|
||||||
|
if (data.getViolationLevels().isEmpty()) {
|
||||||
|
sender.sendMessage(Component.text("No violations", NamedTextColor.GREEN));
|
||||||
|
} else {
|
||||||
|
for (var entry : data.getViolationLevels().entrySet()) {
|
||||||
|
sender.sendMessage(Component.text(entry.getKey() + ": " + String.format("%.1f", entry.getValue()),
|
||||||
|
NamedTextColor.WHITE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.sendMessage(Component.text("Ping: " + target.getPing() + "ms", NamedTextColor.YELLOW));
|
||||||
|
|
||||||
|
if (!data.getLastFlaggedCheck().isEmpty()) {
|
||||||
|
sender.sendMessage(Component.text("Last flagged: " + data.getLastFlaggedCheck(), NamedTextColor.YELLOW));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void punish(CommandSender sender, String playerName, String checkName) {
|
||||||
|
Player target = Bukkit.getPlayer(playerName);
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
sender.sendMessage(Component.text("Player not found or not online.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getPunishmentManager().manualPunish(target, checkName);
|
||||||
|
sender.sendMessage(Component.text("Punishment applied to " + target.getName() + " for " + checkName,
|
||||||
|
NamedTextColor.GREEN));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearViolations(CommandSender sender, String playerName) {
|
||||||
|
Player target = Bukkit.getPlayer(playerName);
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
sender.sendMessage(Component.text("Player not found or not online.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getViolationManager().getPlayerData(target).getViolationLevels().clear();
|
||||||
|
sender.sendMessage(Component.text(
|
||||||
|
"Cleared all violations for " + target.getName() + ".", NamedTextColor.GREEN));
|
||||||
|
plugin.getLogger().info(
|
||||||
|
sender.getName() + " cleared violations for " + target.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleVerbose(CommandSender sender, String playerName) {
|
||||||
|
Player target = Bukkit.getPlayer(playerName);
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
sender.sendMessage(Component.text("Player not found or not online.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.toggleVerboseTarget(target.getUniqueId());
|
||||||
|
boolean now = plugin.isVerboseTarget(target.getUniqueId());
|
||||||
|
sender.sendMessage(Component.text(
|
||||||
|
"Verbose for " + target.getName() + ": " + (now ? "ON" : "OFF"),
|
||||||
|
now ? NamedTextColor.GREEN : NamedTextColor.YELLOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleAlerts(CommandSender sender, String state) {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage(Component.text("Only players can toggle alerts", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean newState = state.equalsIgnoreCase("on");
|
||||||
|
plugin.setAlertsEnabled(player.getUniqueId(), newState);
|
||||||
|
|
||||||
|
sender.sendMessage(Component.text("Alerts " + (newState ? "enabled" : "disabled"),
|
||||||
|
NamedTextColor.GREEN));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showVersion(CommandSender sender) {
|
||||||
|
sender.sendMessage(Component.text("XeroAntiCheat v" + plugin.getDescription().getVersion(),
|
||||||
|
NamedTextColor.GOLD));
|
||||||
|
sender.sendMessage(Component.text("Built for Paper 1.21.x", NamedTextColor.WHITE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showStats(CommandSender sender) {
|
||||||
|
var m = plugin.getMetricsManager();
|
||||||
|
sender.sendMessage(Component.text("— XeroAntiCheat Stats —", NamedTextColor.GOLD));
|
||||||
|
sender.sendMessage(Component.text("Uptime: " + m.getUptimeSeconds() + "s", NamedTextColor.WHITE));
|
||||||
|
sender.sendMessage(Component.text("Checks run: " + m.getTotalChecks(), NamedTextColor.WHITE));
|
||||||
|
sender.sendMessage(Component.text("Checks/sec: " + String.format("%.1f", m.getChecksPerSecond()), NamedTextColor.WHITE));
|
||||||
|
sender.sendMessage(Component.text("Flags issued: " + m.getTotalFlags(), NamedTextColor.WHITE));
|
||||||
|
sender.sendMessage(Component.text("Punishments: " + m.getTotalPunishments(), NamedTextColor.WHITE));
|
||||||
|
sender.sendMessage(Component.text("Players monitored: " + Bukkit.getOnlinePlayers().size(), NamedTextColor.WHITE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDebug(CommandSender sender, String playerName, String checkName) {
|
||||||
|
Player target = Bukkit.getPlayer(playerName);
|
||||||
|
if (target == null) {
|
||||||
|
sender.sendMessage(Component.text("Player not found.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(target);
|
||||||
|
if (data == null) {
|
||||||
|
sender.sendMessage(Component.text("No data available.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.sendMessage(Component.text(
|
||||||
|
"— Debug: " + checkName + " for " + target.getName() + " —",
|
||||||
|
NamedTextColor.GOLD));
|
||||||
|
|
||||||
|
sender.sendMessage(Component.text("VL: " + String.format("%.1f", data.getViolationLevel(checkName)), NamedTextColor.WHITE));
|
||||||
|
sender.sendMessage(Component.text("Ping: " + target.getPing() + "ms", NamedTextColor.WHITE));
|
||||||
|
sender.sendMessage(Component.text("airTicks: " + data.getAirTicks(), NamedTextColor.WHITE));
|
||||||
|
sender.sendMessage(Component.text("onGround(server): " + target.isOnGround(), NamedTextColor.WHITE));
|
||||||
|
|
||||||
|
switch (checkName.toLowerCase()) {
|
||||||
|
case "speed" -> {
|
||||||
|
PlayerData.PositionSnapshot curr = data.getLastPosition();
|
||||||
|
PlayerData.PositionSnapshot prev = data.getSecondLastPosition();
|
||||||
|
if (curr != null && prev != null) {
|
||||||
|
double dx = curr.x() - prev.x();
|
||||||
|
double dz = curr.z() - prev.z();
|
||||||
|
double speed = Math.sqrt(dx*dx + dz*dz);
|
||||||
|
sender.sendMessage(Component.text("speed(measured): " + String.format("%.4f", speed), NamedTextColor.WHITE));
|
||||||
|
sender.sendMessage(Component.text("speedViolationTicks: " + data.getSpeedViolationTicks(), NamedTextColor.WHITE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "killaura" -> {
|
||||||
|
sender.sendMessage(Component.text("lastAttackYaw: " + data.getLastAttackYaw(), NamedTextColor.WHITE));
|
||||||
|
}
|
||||||
|
case "autoclicker" -> {
|
||||||
|
sender.sendMessage(Component.text("CPS: " + data.getCPS(), NamedTextColor.WHITE));
|
||||||
|
}
|
||||||
|
case "spider" -> {
|
||||||
|
sender.sendMessage(Component.text("spiderTicks: " + data.getSpiderTicks(), NamedTextColor.WHITE));
|
||||||
|
}
|
||||||
|
case "glide" -> {
|
||||||
|
sender.sendMessage(Component.text("glideTicks: " + data.getGlideTicks(), NamedTextColor.WHITE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||||
|
if (args.length == 1) {
|
||||||
|
return Stream.of("reload","status","punish","clearviolations","verbose","debug","alerts","stats","version")
|
||||||
|
.filter(sub -> sender.hasPermission("xac.command." + sub)
|
||||||
|
|| sender.hasPermission("xac.admin"))
|
||||||
|
.filter(sub -> sub.startsWith(args[0].toLowerCase()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
if (args.length == 2) {
|
||||||
|
String sub = args[0].toLowerCase();
|
||||||
|
if (List.of("status","punish","clearviolations","verbose","debug").contains(sub)) {
|
||||||
|
return Bukkit.getOnlinePlayers().stream()
|
||||||
|
.map(Player::getName)
|
||||||
|
.filter(n -> n.toLowerCase().startsWith(args[1].toLowerCase()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
if (sub.equals("alerts")) return List.of("on","off");
|
||||||
|
}
|
||||||
|
if (args.length == 3 && args[0].equalsIgnoreCase("punish")) {
|
||||||
|
return plugin.getCheckManager().getRegisteredChecks().stream()
|
||||||
|
.map(c -> c.getName())
|
||||||
|
.filter(n -> n.toLowerCase().startsWith(args[2].toLowerCase()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
if (args.length == 3 && args[0].equalsIgnoreCase("debug")) {
|
||||||
|
return plugin.getCheckManager().getRegisteredChecks().stream()
|
||||||
|
.map(c -> c.getName())
|
||||||
|
.filter(n -> n.toLowerCase().startsWith(args[2].toLowerCase()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAlertsEnabled(UUID uuid) {
|
||||||
|
return plugin.isAlertsEnabled(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
713
src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java
Normal file
713
src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
package com.xeroth.xeroanticheat.data;
|
||||||
|
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.potion.PotionEffectType;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores per-player state for anti-cheat tracking.
|
||||||
|
* Thread-safe implementation using ConcurrentHashMap and ArrayDeque.
|
||||||
|
*/
|
||||||
|
public class PlayerData {
|
||||||
|
|
||||||
|
private final UUID uuid;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
// Position history (fixed-size ring buffer)
|
||||||
|
private final Deque<PositionSnapshot> positionHistory = new ArrayDeque<>(20);
|
||||||
|
|
||||||
|
// Velocity history
|
||||||
|
private final Deque<VelocitySnapshot> velocityHistory = new ArrayDeque<>(10);
|
||||||
|
|
||||||
|
// Rotation history
|
||||||
|
private final Deque<RotationSnapshot> rotationHistory = new ArrayDeque<>(10);
|
||||||
|
|
||||||
|
// Violation levels per check
|
||||||
|
private final Map<String, Double> violationLevels = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Combat tracking
|
||||||
|
private final Deque<Long> clickTimestamps = new ArrayDeque<>(20);
|
||||||
|
private final Deque<Long> attackTimestamps = new ArrayDeque<>(10);
|
||||||
|
private final Deque<UUID> attackedEntities = new ArrayDeque<>(10);
|
||||||
|
|
||||||
|
// Movement state
|
||||||
|
private boolean lastOnGround = true;
|
||||||
|
private boolean isOnIce = false;
|
||||||
|
private boolean wasAirborne = false;
|
||||||
|
private int airTicks = 0;
|
||||||
|
private int groundTicks = 0;
|
||||||
|
private float lastFallDistance = 0f;
|
||||||
|
private boolean lastWasGliding = false;
|
||||||
|
|
||||||
|
// Ping
|
||||||
|
private int ping = 0;
|
||||||
|
|
||||||
|
// Potion effects (cached)
|
||||||
|
private boolean hasSpeedEffect = false;
|
||||||
|
private boolean hasSlownessEffect = false;
|
||||||
|
private boolean hasDolphinsGrace = false;
|
||||||
|
private boolean hasLevitation = false;
|
||||||
|
private boolean hasJumpBoost = false;
|
||||||
|
private boolean hasSlowFalling = false;
|
||||||
|
private int speedLevel = 0;
|
||||||
|
private int slownessLevel = 0;
|
||||||
|
private int jumpBoostLevel = 0;
|
||||||
|
|
||||||
|
// Player state
|
||||||
|
private boolean inventoryOpen = false;
|
||||||
|
private long lastEatTime = 0;
|
||||||
|
private boolean wasSprinting = false;
|
||||||
|
|
||||||
|
// Block placement tracking
|
||||||
|
private final Deque<Long> blockPlaceTimestamps = new ArrayDeque<>(20);
|
||||||
|
|
||||||
|
// Scaffold tracking
|
||||||
|
private int scaffoldSignals = 0;
|
||||||
|
|
||||||
|
// Timer check
|
||||||
|
private long lastMovePacketTime = 0;
|
||||||
|
private int packetsThisSecond = 0;
|
||||||
|
private long lastPacketCountReset = 0;
|
||||||
|
|
||||||
|
// Last flagged check
|
||||||
|
private String lastFlaggedCheck = "";
|
||||||
|
private long lastFlagTime = 0;
|
||||||
|
|
||||||
|
// Combat angles
|
||||||
|
private float lastYaw = 0;
|
||||||
|
private float lastPitch = 0;
|
||||||
|
private float lastAttackYaw = 0;
|
||||||
|
|
||||||
|
// Knockback tracking
|
||||||
|
private long lastKnockbackTime = 0;
|
||||||
|
|
||||||
|
// NoFall tracking
|
||||||
|
private double lastExpectedFallDamage = 0.0;
|
||||||
|
|
||||||
|
// FastEat tracking
|
||||||
|
private long lastStartedEatingTime = 0;
|
||||||
|
|
||||||
|
// Scaffold tracking
|
||||||
|
private float lastPlacementYaw = Float.NaN;
|
||||||
|
|
||||||
|
// VelocityCheck tracking
|
||||||
|
private org.bukkit.util.Vector lastServerVelocity = null;
|
||||||
|
private int velocityCheckTicks = 0;
|
||||||
|
|
||||||
|
// SpiderCheck tracking
|
||||||
|
private int spiderTicks = 0;
|
||||||
|
|
||||||
|
// GlideCheck tracking
|
||||||
|
private int glideTicks = 0;
|
||||||
|
|
||||||
|
// SpeedCheck tracking
|
||||||
|
private int speedViolationTicks = 0;
|
||||||
|
|
||||||
|
// Universal buffers for checks without built-in tick counters
|
||||||
|
private int jesusBuffer = 0;
|
||||||
|
private int timerBuffer = 0;
|
||||||
|
private int reachBuffer = 0;
|
||||||
|
private int killAuraBuffer = 0;
|
||||||
|
private int fastPlaceBuffer = 0;
|
||||||
|
private int scaffoldBuffer = 0;
|
||||||
|
private int fastEatBuffer = 0;
|
||||||
|
|
||||||
|
// Last safe location for setback
|
||||||
|
private Location lastSafeLocation = null;
|
||||||
|
|
||||||
|
// Alert cooldown tracking
|
||||||
|
private final Map<String, Long> lastWarnTime = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public PlayerData(Player player) {
|
||||||
|
this.uuid = player.getUniqueId();
|
||||||
|
this.name = player.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position methods
|
||||||
|
public void addPosition(double x, double y, double z, boolean onGround) {
|
||||||
|
positionHistory.addFirst(new PositionSnapshot(x, y, z, onGround, System.currentTimeMillis()));
|
||||||
|
if (positionHistory.size() > 20) {
|
||||||
|
positionHistory.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PositionSnapshot getLastPosition() {
|
||||||
|
return positionHistory.peekFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PositionSnapshot getSecondLastPosition() {
|
||||||
|
if (positionHistory.size() < 2) return null;
|
||||||
|
var it = positionHistory.iterator();
|
||||||
|
it.next();
|
||||||
|
return it.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Velocity methods
|
||||||
|
public void addVelocity(double x, double y, double z) {
|
||||||
|
velocityHistory.addFirst(new VelocitySnapshot(x, y, z, System.currentTimeMillis()));
|
||||||
|
if (velocityHistory.size() > 10) {
|
||||||
|
velocityHistory.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotation methods
|
||||||
|
public void addRotation(float yaw, float pitch) {
|
||||||
|
rotationHistory.addFirst(new RotationSnapshot(yaw, pitch, System.currentTimeMillis()));
|
||||||
|
if (rotationHistory.size() > 10) {
|
||||||
|
rotationHistory.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RotationSnapshot getLastRotation() {
|
||||||
|
return rotationHistory.peekFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click tracking
|
||||||
|
public void addClick() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
clickTimestamps.addFirst(now);
|
||||||
|
|
||||||
|
// Remove old clicks (>1 second)
|
||||||
|
while (!clickTimestamps.isEmpty() && now - clickTimestamps.peekLast() > 1000) {
|
||||||
|
clickTimestamps.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCPS() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
int count = 0;
|
||||||
|
for (Long timestamp : clickTimestamps) {
|
||||||
|
if (now - timestamp <= 1000) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attack tracking
|
||||||
|
public void addAttack(UUID entityId) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
attackTimestamps.addFirst(now);
|
||||||
|
attackedEntities.addFirst(entityId);
|
||||||
|
|
||||||
|
while (attackTimestamps.size() > 10) {
|
||||||
|
attackTimestamps.removeLast();
|
||||||
|
}
|
||||||
|
while (attackedEntities.size() > 10) {
|
||||||
|
attackedEntities.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAttacksPerSecond() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
int count = 0;
|
||||||
|
for (Long timestamp : attackTimestamps) {
|
||||||
|
if (now - timestamp <= 1000) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Violation methods
|
||||||
|
public void setViolationLevel(String check, double vl) {
|
||||||
|
violationLevels.put(check, vl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getViolationLevel(String check) {
|
||||||
|
return violationLevels.getOrDefault(check, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementViolation(String check, double amount) {
|
||||||
|
violationLevels.merge(check, amount, (a, b) -> a + b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decayViolation(String check, double amount) {
|
||||||
|
violationLevels.computeIfPresent(check, (k, v) -> Math.max(0, v - amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block place tracking
|
||||||
|
public void addBlockPlace() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
blockPlaceTimestamps.addFirst(now);
|
||||||
|
|
||||||
|
while (!blockPlaceTimestamps.isEmpty() && now - blockPlaceTimestamps.peekLast() > 1000) {
|
||||||
|
blockPlaceTimestamps.removeLast();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBlocksPerSecond() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
int count = 0;
|
||||||
|
for (Long timestamp : blockPlaceTimestamps) {
|
||||||
|
if (now - timestamp <= 1000) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and setters
|
||||||
|
public UUID getUuid() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLastOnGround() {
|
||||||
|
return lastOnGround;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastOnGround(boolean lastOnGround) {
|
||||||
|
this.lastOnGround = lastOnGround;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOnIce() {
|
||||||
|
return isOnIce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnIce(boolean onIce) {
|
||||||
|
isOnIce = onIce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean wasAirborne() {
|
||||||
|
return wasAirborne;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWasAirborne(boolean wasAirborne) {
|
||||||
|
this.wasAirborne = wasAirborne;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAirTicks() {
|
||||||
|
return airTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAirTicks(int airTicks) {
|
||||||
|
this.airTicks = airTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementAirTicks() {
|
||||||
|
this.airTicks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetAirTicks() {
|
||||||
|
this.airTicks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getGroundTicks() {
|
||||||
|
return groundTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGroundTicks(int groundTicks) {
|
||||||
|
this.groundTicks = groundTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLastFallDistance() {
|
||||||
|
return lastFallDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastFallDistance(float lastFallDistance) {
|
||||||
|
this.lastFallDistance = lastFallDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean wasLastGliding() {
|
||||||
|
return lastWasGliding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastWasGliding(boolean lastWasGliding) {
|
||||||
|
this.lastWasGliding = lastWasGliding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPing() {
|
||||||
|
return ping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPing(int ping) {
|
||||||
|
this.ping = ping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSpeedEffect() {
|
||||||
|
return hasSpeedEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHasSpeedEffect(boolean hasSpeedEffect) {
|
||||||
|
this.hasSpeedEffect = hasSpeedEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSlownessEffect() {
|
||||||
|
return hasSlownessEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHasSlownessEffect(boolean hasSlownessEffect) {
|
||||||
|
this.hasSlownessEffect = hasSlownessEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasDolphinsGrace() {
|
||||||
|
return hasDolphinsGrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHasDolphinsGrace(boolean hasDolphinsGrace) {
|
||||||
|
this.hasDolphinsGrace = hasDolphinsGrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasLevitation() {
|
||||||
|
return hasLevitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHasLevitation(boolean hasLevitation) {
|
||||||
|
this.hasLevitation = hasLevitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasJumpBoost() {
|
||||||
|
return hasJumpBoost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHasJumpBoost(boolean hasJumpBoost) {
|
||||||
|
this.hasJumpBoost = hasJumpBoost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSlowFalling() {
|
||||||
|
return hasSlowFalling;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHasSlowFalling(boolean hasSlowFalling) {
|
||||||
|
this.hasSlowFalling = hasSlowFalling;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSpeedLevel() {
|
||||||
|
return speedLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSpeedLevel(int speedLevel) {
|
||||||
|
this.speedLevel = speedLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSlownessLevel() {
|
||||||
|
return slownessLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSlownessLevel(int slownessLevel) {
|
||||||
|
this.slownessLevel = slownessLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getJumpBoostLevel() {
|
||||||
|
return jumpBoostLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJumpBoostLevel(int jumpBoostLevel) {
|
||||||
|
this.jumpBoostLevel = jumpBoostLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInventoryOpen() {
|
||||||
|
return inventoryOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInventoryOpen(boolean inventoryOpen) {
|
||||||
|
this.inventoryOpen = inventoryOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastEatTime() {
|
||||||
|
return lastEatTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastEatTime(long lastEatTime) {
|
||||||
|
this.lastEatTime = lastEatTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean wasSprinting() {
|
||||||
|
return wasSprinting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWasSprinting(boolean wasSprinting) {
|
||||||
|
this.wasSprinting = wasSprinting;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getScaffoldSignals() {
|
||||||
|
return scaffoldSignals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScaffoldSignals(int scaffoldSignals) {
|
||||||
|
this.scaffoldSignals = scaffoldSignals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementScaffoldSignals() {
|
||||||
|
this.scaffoldSignals++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetScaffoldSignals() {
|
||||||
|
this.scaffoldSignals = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastMovePacketTime() {
|
||||||
|
return lastMovePacketTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastMovePacketTime(long lastMovePacketTime) {
|
||||||
|
this.lastMovePacketTime = lastMovePacketTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPacketsThisSecond() {
|
||||||
|
return packetsThisSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPacketsThisSecond(int packetsThisSecond) {
|
||||||
|
this.packetsThisSecond = packetsThisSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementPacketsThisSecond() {
|
||||||
|
this.packetsThisSecond++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastPacketCountReset() {
|
||||||
|
return lastPacketCountReset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastPacketCountReset(long lastPacketCountReset) {
|
||||||
|
this.lastPacketCountReset = lastPacketCountReset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastFlaggedCheck() {
|
||||||
|
return lastFlaggedCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastFlaggedCheck(String lastFlaggedCheck) {
|
||||||
|
this.lastFlaggedCheck = lastFlaggedCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastFlagTime() {
|
||||||
|
return lastFlagTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastFlagTime(long lastFlagTime) {
|
||||||
|
this.lastFlagTime = lastFlagTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLastYaw() {
|
||||||
|
return lastYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastYaw(float lastYaw) {
|
||||||
|
this.lastYaw = lastYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLastPitch() {
|
||||||
|
return lastPitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastPitch(float lastPitch) {
|
||||||
|
this.lastPitch = lastPitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLastAttackYaw() {
|
||||||
|
return lastAttackYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastAttackYaw(float lastAttackYaw) {
|
||||||
|
this.lastAttackYaw = lastAttackYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Deque<Long> getClickTimestamps() {
|
||||||
|
return clickTimestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Deque<UUID> getAttackedEntities() {
|
||||||
|
return attackedEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Deque<Long> getAttackTimestamps() {
|
||||||
|
return attackTimestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Double> getViolationLevels() {
|
||||||
|
return violationLevels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastKnockbackTime() {
|
||||||
|
return lastKnockbackTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastKnockbackTime(long lastKnockbackTime) {
|
||||||
|
this.lastKnockbackTime = lastKnockbackTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getLastExpectedFallDamage() {
|
||||||
|
return lastExpectedFallDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastExpectedFallDamage(double lastExpectedFallDamage) {
|
||||||
|
this.lastExpectedFallDamage = lastExpectedFallDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLastStartedEatingTime() {
|
||||||
|
return lastStartedEatingTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastStartedEatingTime(long lastStartedEatingTime) {
|
||||||
|
this.lastStartedEatingTime = lastStartedEatingTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLastPlacementYaw() {
|
||||||
|
return lastPlacementYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastPlacementYaw(float lastPlacementYaw) {
|
||||||
|
this.lastPlacementYaw = lastPlacementYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public org.bukkit.util.Vector getLastServerVelocity() {
|
||||||
|
return lastServerVelocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastServerVelocity(org.bukkit.util.Vector v) {
|
||||||
|
this.lastServerVelocity = v;
|
||||||
|
this.velocityCheckTicks = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getVelocityCheckTicks() {
|
||||||
|
return velocityCheckTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decrementVelocityCheckTicks() {
|
||||||
|
if (velocityCheckTicks > 0) velocityCheckTicks--;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearServerVelocity() {
|
||||||
|
lastServerVelocity = null;
|
||||||
|
velocityCheckTicks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spider tick methods
|
||||||
|
public int getSpiderTicks() {
|
||||||
|
return spiderTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSpiderTicks(int spiderTicks) {
|
||||||
|
this.spiderTicks = spiderTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementSpiderTicks() {
|
||||||
|
this.spiderTicks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetSpiderTicks() {
|
||||||
|
this.spiderTicks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glide tick methods
|
||||||
|
public int getGlideTicks() {
|
||||||
|
return glideTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGlideTicks(int glideTicks) {
|
||||||
|
this.glideTicks = glideTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementGlideTicks() {
|
||||||
|
this.glideTicks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetGlideTicks() {
|
||||||
|
this.glideTicks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speed violation tick methods
|
||||||
|
public int getSpeedViolationTicks() {
|
||||||
|
return speedViolationTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementSpeedViolationTicks() {
|
||||||
|
this.speedViolationTicks++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetSpeedViolationTicks() {
|
||||||
|
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() {
|
||||||
|
positionHistory.clear();
|
||||||
|
rotationHistory.clear();
|
||||||
|
velocityHistory.clear();
|
||||||
|
spiderTicks = 0;
|
||||||
|
glideTicks = 0;
|
||||||
|
speedViolationTicks = 0;
|
||||||
|
jesusBuffer = 0;
|
||||||
|
timerBuffer = 0;
|
||||||
|
reachBuffer = 0;
|
||||||
|
killAuraBuffer = 0;
|
||||||
|
fastPlaceBuffer = 0;
|
||||||
|
scaffoldBuffer = 0;
|
||||||
|
lastSafeLocation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Location getLastSafeLocation() {
|
||||||
|
return lastSafeLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastSafeLocation(Location loc) {
|
||||||
|
this.lastSafeLocation = loc != null ? loc.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Deque<Long> getBlockPlaceTimestamps() {
|
||||||
|
return blockPlaceTimestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshots
|
||||||
|
public record PositionSnapshot(double x, double y, double z, boolean onGround, long timestamp) {}
|
||||||
|
|
||||||
|
public record VelocitySnapshot(double x, double y, double z, long timestamp) {}
|
||||||
|
|
||||||
|
public record RotationSnapshot(float yaw, float pitch, long timestamp) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package com.xeroth.xeroanticheat.listener;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.checks.combat.AutoClickerCheck;
|
||||||
|
import com.xeroth.xeroanticheat.checks.combat.CriticalCheck;
|
||||||
|
import com.xeroth.xeroanticheat.checks.combat.KillAuraCheck;
|
||||||
|
import com.xeroth.xeroanticheat.checks.combat.ReachCheck;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CombatListener - Handles combat-related events for anti-cheat checks.
|
||||||
|
*/
|
||||||
|
public class CombatListener implements Listener {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
|
||||||
|
public CombatListener(XeroAntiCheat plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOW)
|
||||||
|
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
|
||||||
|
Entity damager = event.getDamager();
|
||||||
|
|
||||||
|
// Only check if damager is a player
|
||||||
|
if (!(damager instanceof Player player)) return;
|
||||||
|
|
||||||
|
// Early-return for players with global bypass only.
|
||||||
|
// Category and per-check bypass is enforced inside Check.flag() and isBypassed() guards.
|
||||||
|
if (player.hasPermission("xac.bypass")) return;
|
||||||
|
|
||||||
|
Entity target = event.getEntity();
|
||||||
|
|
||||||
|
// Get player data
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
|
||||||
|
// Record attack
|
||||||
|
data.addAttack(target.getUniqueId());
|
||||||
|
|
||||||
|
// Get checks
|
||||||
|
KillAuraCheck killAuraCheck = (KillAuraCheck) plugin.getCheckManager().getCheck("KillAura");
|
||||||
|
ReachCheck reachCheck = (ReachCheck) plugin.getCheckManager().getCheck("Reach");
|
||||||
|
CriticalCheck criticalCheck = (CriticalCheck) plugin.getCheckManager().getCheck("Critical");
|
||||||
|
|
||||||
|
// Check for reach
|
||||||
|
if (reachCheck != null && reachCheck.checkReach(player, target)) {
|
||||||
|
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.getPunishmentManager().evaluate(player, "Reach");
|
||||||
|
data.resetReachBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetReachBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for kill aura (angle)
|
||||||
|
if (killAuraCheck != null && killAuraCheck.checkAngle(player, target)) {
|
||||||
|
if (!killAuraCheck.isBypassed(player)) {
|
||||||
|
int buffer = plugin.getConfigManager().getInt("checks.killaura.buffer_ticks", 2);
|
||||||
|
data.incrementKillAuraBuffer();
|
||||||
|
if (data.getKillAuraBuffer() >= buffer) {
|
||||||
|
plugin.getViolationManager().addViolation(player, "KillAura", 1.0);
|
||||||
|
plugin.getPunishmentManager().evaluate(player, "KillAura");
|
||||||
|
data.resetKillAuraBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetKillAuraBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for multi-target
|
||||||
|
if (killAuraCheck != null && killAuraCheck.checkMultiTarget(data, player, target)) {
|
||||||
|
if (!killAuraCheck.isBypassed(player)) {
|
||||||
|
plugin.getViolationManager().addViolation(player, "KillAura", 2.0);
|
||||||
|
plugin.getPunishmentManager().evaluate(player, "KillAura");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for critical hits
|
||||||
|
if (criticalCheck != null) {
|
||||||
|
double yVelocity = player.getVelocity().getY();
|
||||||
|
boolean isCritical = yVelocity < -0.08
|
||||||
|
&& !player.isOnGround()
|
||||||
|
&& !player.isInsideVehicle()
|
||||||
|
&& player.getFallDistance() > 0.0;
|
||||||
|
|
||||||
|
if (criticalCheck.checkCritical(player, data, isCritical)) {
|
||||||
|
if (!criticalCheck.isBypassed(player)) {
|
||||||
|
plugin.getViolationManager().addViolation(player, "Critical", 1.0);
|
||||||
|
plugin.getPunishmentManager().evaluate(player, "Critical");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update wasAirborne state after attack
|
||||||
|
data.setWasAirborne(!player.isOnGround());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOW)
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
// Early-return for players with global bypass only.
|
||||||
|
// Category and per-check bypass is enforced inside Check.flag() and isBypassed() guards.
|
||||||
|
if (player.hasPermission("xac.bypass")) return;
|
||||||
|
|
||||||
|
// Get player data
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
|
||||||
|
// Record click for autoclicker check
|
||||||
|
// Only count left clicks (attack)
|
||||||
|
if (event.getAction().name().contains("LEFT_CLICK")) {
|
||||||
|
if (!plugin.isProtocolLibLoaded()) {
|
||||||
|
data.addClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run autoclicker check
|
||||||
|
plugin.getCheckManager().runCheck("AutoClicker", data, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package com.xeroth.xeroanticheat.listener;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.checks.misc.FastEatCheck;
|
||||||
|
import com.xeroth.xeroanticheat.checks.misc.FastPlaceCheck;
|
||||||
|
import com.xeroth.xeroanticheat.checks.misc.ScaffoldCheck;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
|
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
import org.bukkit.event.player.PlayerItemConsumeEvent;
|
||||||
|
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MiscListener - Handles miscellaneous events for anti-cheat checks.
|
||||||
|
*/
|
||||||
|
public class MiscListener implements Listener {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
|
||||||
|
public MiscListener(XeroAntiCheat plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOW)
|
||||||
|
public void onBlockPlace(BlockPlaceEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
// Early-return for players with global bypass only.
|
||||||
|
// Category and per-check bypass is enforced inside Check.flag() and isBypassed() guards.
|
||||||
|
if (player.hasPermission("xac.bypass")) return;
|
||||||
|
|
||||||
|
// Ignore creative mode
|
||||||
|
if (player.getGameMode() == org.bukkit.GameMode.CREATIVE) return;
|
||||||
|
|
||||||
|
// Get player data
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
|
||||||
|
// Record block placement
|
||||||
|
data.addBlockPlace();
|
||||||
|
|
||||||
|
// Run fastplace check
|
||||||
|
plugin.getCheckManager().runCheck("FastPlace", data, player);
|
||||||
|
|
||||||
|
// Run scaffold check
|
||||||
|
ScaffoldCheck scaffoldCheck = (ScaffoldCheck) plugin.getCheckManager().getCheck("Scaffold");
|
||||||
|
if (scaffoldCheck != null && scaffoldCheck.checkScaffold(player, event.getBlockPlaced(), data)) {
|
||||||
|
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.getPunishmentManager().evaluate(player, "Scaffold");
|
||||||
|
data.resetScaffoldBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetScaffoldBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerItemConsume(PlayerItemConsumeEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
// Early-return for players with global bypass only.
|
||||||
|
// Category and per-check bypass is enforced inside Check.flag() and isBypassed() guards.
|
||||||
|
if (player.hasPermission("xac.bypass")) return;
|
||||||
|
|
||||||
|
// Get player data
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
|
||||||
|
// Check if it's food using Paper API
|
||||||
|
if (!event.getItem().getType().isEdible()) return;
|
||||||
|
|
||||||
|
// Run fasteat check
|
||||||
|
FastEatCheck fastEatCheck = (FastEatCheck) plugin.getCheckManager().getCheck("FastEat");
|
||||||
|
if (fastEatCheck != null && fastEatCheck.checkFastEat(player, data, System.currentTimeMillis())) {
|
||||||
|
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.getPunishmentManager().evaluate(player, "FastEat");
|
||||||
|
data.resetFastEatBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data.resetFastEatBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onInventoryOpen(InventoryOpenEvent event) {
|
||||||
|
if (!(event.getPlayer() instanceof Player player)) return;
|
||||||
|
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
data.setInventoryOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onInventoryClose(InventoryCloseEvent event) {
|
||||||
|
if (!(event.getPlayer() instanceof Player player)) return;
|
||||||
|
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
data.setInventoryOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onEntityDamage(EntityDamageEvent event) {
|
||||||
|
if (event.getCause() != EntityDamageEvent.DamageCause.FALL) return;
|
||||||
|
if (!(event.getEntity() instanceof Player player)) return;
|
||||||
|
// Early-return for players with global bypass only.
|
||||||
|
// Category and per-check bypass is enforced inside Check.flag() and isBypassed() guards.
|
||||||
|
if (player.hasPermission("xac.bypass")) return;
|
||||||
|
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
double expected = data.getLastExpectedFallDamage();
|
||||||
|
if (expected <= 0) return;
|
||||||
|
|
||||||
|
if (event.getFinalDamage() < expected * 0.5) {
|
||||||
|
Check noFallCheck = plugin.getCheckManager().getCheck("NoFall");
|
||||||
|
if (noFallCheck == null || !noFallCheck.isBypassed(player)) {
|
||||||
|
plugin.getViolationManager().addViolation(player, "NoFall", 1.0);
|
||||||
|
plugin.getPunishmentManager().evaluate(player, "NoFall");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.setLastExpectedFallDamage(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
|
||||||
|
if (!(event.getDamager() instanceof Player player)) return;
|
||||||
|
// Early-return for players with global bypass only.
|
||||||
|
// Category and per-check bypass is enforced inside Check.flag() and isBypassed() guards.
|
||||||
|
if (player.hasPermission("xac.bypass")) return;
|
||||||
|
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
data.setLastKnockbackTime(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerInteract(PlayerInteractEvent event) {
|
||||||
|
if (event.getAction() != org.bukkit.event.block.Action.RIGHT_CLICK_AIR &&
|
||||||
|
event.getAction() != org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK) return;
|
||||||
|
if (!(event.getPlayer() instanceof Player player)) return;
|
||||||
|
// Early-return for players with global bypass only.
|
||||||
|
// Category and per-check bypass is enforced inside Check.flag() and isBypassed() guards.
|
||||||
|
if (player.hasPermission("xac.bypass")) return;
|
||||||
|
|
||||||
|
var item = event.getItem();
|
||||||
|
if (item == null || !item.getType().isEdible()) return;
|
||||||
|
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
data.setLastStartedEatingTime(System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerRespawn(PlayerRespawnEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
if (data == null) return;
|
||||||
|
data.clearServerVelocity();
|
||||||
|
data.clearPositionHistory();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package com.xeroth.xeroanticheat.listener;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import com.xeroth.xeroanticheat.manager.ViolationManager;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerMoveEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
|
import org.bukkit.event.player.PlayerToggleFlightEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MovementListener - Handles movement-related events for anti-cheat checks.
|
||||||
|
*/
|
||||||
|
public class MovementListener implements Listener {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
|
||||||
|
public MovementListener(XeroAntiCheat plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
// Create player data on join
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
|
||||||
|
// Initialize position
|
||||||
|
data.addPosition(
|
||||||
|
player.getLocation().getX(),
|
||||||
|
player.getLocation().getY(),
|
||||||
|
player.getLocation().getZ(),
|
||||||
|
player.isOnGround()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize rotation
|
||||||
|
data.addRotation(
|
||||||
|
player.getLocation().getYaw(),
|
||||||
|
player.getLocation().getPitch()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
// Remove player data on quit
|
||||||
|
plugin.getViolationManager().removePlayer(player.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.LOW)
|
||||||
|
public void onPlayerMove(PlayerMoveEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
// Early-return for players with global bypass only.
|
||||||
|
// Category and per-check bypass is enforced inside Check.flag() and isBypassed() guards.
|
||||||
|
if (player.hasPermission("xac.bypass")) return;
|
||||||
|
|
||||||
|
// Only check if position actually changed
|
||||||
|
if (!event.hasChangedPosition()) return;
|
||||||
|
|
||||||
|
// Get player data
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
|
||||||
|
// Store previous onGround state
|
||||||
|
boolean wasOnGround = data.isLastOnGround();
|
||||||
|
data.setLastOnGround(player.isOnGround());
|
||||||
|
|
||||||
|
// Update position history
|
||||||
|
data.addPosition(
|
||||||
|
event.getTo().getX(),
|
||||||
|
event.getTo().getY(),
|
||||||
|
event.getTo().getZ(),
|
||||||
|
player.isOnGround()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update rotation history
|
||||||
|
data.addRotation(
|
||||||
|
event.getTo().getYaw(),
|
||||||
|
event.getTo().getPitch()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update wasAirborne
|
||||||
|
if (!player.isOnGround()) {
|
||||||
|
data.setWasAirborne(true);
|
||||||
|
data.incrementAirTicks();
|
||||||
|
} else {
|
||||||
|
data.setWasAirborne(false);
|
||||||
|
data.resetAirTicks();
|
||||||
|
data.setLastSafeLocation(event.getTo());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ice at feet
|
||||||
|
Material blockBelow = event.getTo().clone().subtract(0, 1, 0).getBlock().getType();
|
||||||
|
data.setOnIce(blockBelow == Material.ICE || blockBelow == Material.PACKED_ICE || blockBelow == Material.FROSTED_ICE);
|
||||||
|
|
||||||
|
// Update ping
|
||||||
|
data.setPing(player.getPing());
|
||||||
|
|
||||||
|
// Run movement checks
|
||||||
|
plugin.getCheckManager().runCheck("Speed", data, player);
|
||||||
|
plugin.getCheckManager().runCheck("Fly", data, player);
|
||||||
|
plugin.getCheckManager().runCheck("Jesus", data, player);
|
||||||
|
plugin.getCheckManager().runCheck("NoFall", data, player);
|
||||||
|
plugin.getCheckManager().runCheck("Timer", data, player);
|
||||||
|
plugin.getCheckManager().runCheck("Spider", data, player);
|
||||||
|
plugin.getCheckManager().runCheck("Glide", data, player);
|
||||||
|
plugin.getCheckManager().runCheck("Phase", data, player);
|
||||||
|
plugin.getCheckManager().runCheck("Velocity", data, player);
|
||||||
|
plugin.getCheckManager().runCheck("InventoryMove", data, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPlayerToggleFlight(PlayerToggleFlightEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
|
||||||
|
// Update gliding state
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
data.setLastWasGliding(player.isGliding());
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onPlayerTeleport(PlayerTeleportEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
PlayerData data = plugin.getViolationManager().getPlayerData(player);
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
data.clearPositionHistory();
|
||||||
|
data.resetAirTicks();
|
||||||
|
data.clearServerVelocity();
|
||||||
|
data.setLastPlacementYaw(Float.NaN);
|
||||||
|
data.setLastSafeLocation(event.getTo());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.xeroth.xeroanticheat.manager;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages registration and execution of all checks.
|
||||||
|
*/
|
||||||
|
public class CheckManager {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
|
||||||
|
private final Map<String, Check> checksByName = new HashMap<>();
|
||||||
|
private final List<Check> registeredChecks = new ArrayList<>();
|
||||||
|
|
||||||
|
public CheckManager(XeroAntiCheat plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a check
|
||||||
|
*/
|
||||||
|
public void registerCheck(Check check) {
|
||||||
|
String name = check.getName().toLowerCase();
|
||||||
|
checksByName.put(name, check);
|
||||||
|
registeredChecks.add(check);
|
||||||
|
plugin.getLogger().info("Registered check: " + check.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a check by name
|
||||||
|
*/
|
||||||
|
public Check getCheck(String name) {
|
||||||
|
return checksByName.get(name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a specific check
|
||||||
|
*/
|
||||||
|
public void runCheck(String checkName, PlayerData data, Player player) {
|
||||||
|
Check check = checksByName.get(checkName.toLowerCase());
|
||||||
|
if (check != null && check.isEnabled()) {
|
||||||
|
plugin.getMetricsManager().recordCheck();
|
||||||
|
check.check(data, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered checks
|
||||||
|
*/
|
||||||
|
public List<Check> getRegisteredChecks() {
|
||||||
|
return new ArrayList<>(registeredChecks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all enabled checks
|
||||||
|
*/
|
||||||
|
public List<Check> getEnabledChecks() {
|
||||||
|
return registeredChecks.stream()
|
||||||
|
.filter(Check::isEnabled)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload all checks (re-read config)
|
||||||
|
*/
|
||||||
|
public void reloadChecks() {
|
||||||
|
for (Check check : registeredChecks) {
|
||||||
|
// Re-check enabled state from config
|
||||||
|
String path = "checks." + check.getName().toLowerCase() + ".enabled";
|
||||||
|
boolean enabled = plugin.getConfigManager().getBoolean(path, true);
|
||||||
|
check.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
plugin.getLogger().info("Reloaded " + registeredChecks.size() + " checks");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get check by class name
|
||||||
|
*/
|
||||||
|
public Check getCheckByClass(Class<? extends Check> clazz) {
|
||||||
|
for (Check check : registeredChecks) {
|
||||||
|
if (check.getClass().equals(clazz)) {
|
||||||
|
return check;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package com.xeroth.xeroanticheat.manager;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages plugin configuration with validation and type-safe getters.
|
||||||
|
*/
|
||||||
|
public class ConfigManager {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
private FileConfiguration config;
|
||||||
|
private File configFile;
|
||||||
|
|
||||||
|
private static final Map<String, Object> DEFAULTS = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
// General
|
||||||
|
DEFAULTS.put("enabled", true);
|
||||||
|
DEFAULTS.put("debug", false);
|
||||||
|
|
||||||
|
// Violation
|
||||||
|
DEFAULTS.put("violation.decay_interval", 30);
|
||||||
|
DEFAULTS.put("violation.decay_rate", 0.5);
|
||||||
|
|
||||||
|
// Checks - Speed
|
||||||
|
DEFAULTS.put("checks.speed.enabled", true);
|
||||||
|
DEFAULTS.put("checks.speed.max_speed", 0.56);
|
||||||
|
DEFAULTS.put("checks.speed.ping_factor", 1.0);
|
||||||
|
DEFAULTS.put("checks.speed.buffer_ticks", 5);
|
||||||
|
DEFAULTS.put("checks.speed.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.speed.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.speed.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.speed.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - Fly
|
||||||
|
DEFAULTS.put("checks.fly.enabled", true);
|
||||||
|
DEFAULTS.put("checks.fly.fall_buffer", 10);
|
||||||
|
DEFAULTS.put("checks.fly.ground_desync_threshold", 3);
|
||||||
|
DEFAULTS.put("checks.fly.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.fly.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.fly.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.fly.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - Jesus
|
||||||
|
DEFAULTS.put("checks.jesus.enabled", true);
|
||||||
|
DEFAULTS.put("checks.jesus.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.jesus.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.jesus.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.jesus.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - NoFall
|
||||||
|
DEFAULTS.put("checks.nofall.enabled", true);
|
||||||
|
DEFAULTS.put("checks.nofall.min_fall_distance", 3);
|
||||||
|
DEFAULTS.put("checks.nofall.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.nofall.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.nofall.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.nofall.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - Timer
|
||||||
|
DEFAULTS.put("checks.timer.enabled", true);
|
||||||
|
DEFAULTS.put("checks.timer.max_packets_per_second", 22);
|
||||||
|
DEFAULTS.put("checks.timer.blink_threshold_ms", 500);
|
||||||
|
DEFAULTS.put("checks.timer.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.timer.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.timer.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.timer.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - Spider
|
||||||
|
DEFAULTS.put("checks.spider.enabled", true);
|
||||||
|
DEFAULTS.put("checks.spider.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.spider.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.spider.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.spider.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - Glide
|
||||||
|
DEFAULTS.put("checks.glide.enabled", true);
|
||||||
|
DEFAULTS.put("checks.glide.min_horizontal_speed", 0.5);
|
||||||
|
DEFAULTS.put("checks.glide.max_y_decrease", 0.1);
|
||||||
|
DEFAULTS.put("checks.glide.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.glide.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.glide.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.glide.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - KillAura
|
||||||
|
DEFAULTS.put("checks.killaura.enabled", true);
|
||||||
|
DEFAULTS.put("checks.killaura.max_angle", 100);
|
||||||
|
DEFAULTS.put("checks.killaura.max_rotation_change", 45);
|
||||||
|
DEFAULTS.put("checks.killaura.multitarget_window_ms", 100);
|
||||||
|
DEFAULTS.put("checks.killaura.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.killaura.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.killaura.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.killaura.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - Reach
|
||||||
|
DEFAULTS.put("checks.reach.enabled", true);
|
||||||
|
DEFAULTS.put("checks.reach.max_reach", 3.2);
|
||||||
|
DEFAULTS.put("checks.reach.creative_max_reach", 5.0);
|
||||||
|
DEFAULTS.put("checks.reach.ping_factor", 1.0);
|
||||||
|
DEFAULTS.put("checks.reach.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.reach.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.reach.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.reach.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - Critical
|
||||||
|
DEFAULTS.put("checks.critical.enabled", true);
|
||||||
|
DEFAULTS.put("checks.critical.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.critical.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.critical.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.critical.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - AutoClicker
|
||||||
|
DEFAULTS.put("checks.autoclicker.enabled", true);
|
||||||
|
DEFAULTS.put("checks.autoclicker.max_cps", 20);
|
||||||
|
DEFAULTS.put("checks.autoclicker.min_variance", 2.0);
|
||||||
|
DEFAULTS.put("checks.autoclicker.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.autoclicker.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.autoclicker.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.autoclicker.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - FastPlace
|
||||||
|
DEFAULTS.put("checks.fastplace.enabled", true);
|
||||||
|
DEFAULTS.put("checks.fastplace.max_blocks_per_second", 20);
|
||||||
|
DEFAULTS.put("checks.fastplace.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.fastplace.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.fastplace.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.fastplace.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - Scaffold
|
||||||
|
DEFAULTS.put("checks.scaffold.enabled", true);
|
||||||
|
DEFAULTS.put("checks.scaffold.min_pitch", 75);
|
||||||
|
DEFAULTS.put("checks.scaffold.signals_required", 2);
|
||||||
|
DEFAULTS.put("checks.scaffold.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.scaffold.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.scaffold.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.scaffold.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - FastEat
|
||||||
|
DEFAULTS.put("checks.fasteat.enabled", true);
|
||||||
|
DEFAULTS.put("checks.fasteat.max_eat_ticks", 32);
|
||||||
|
DEFAULTS.put("checks.fasteat.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.fasteat.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.fasteat.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.fasteat.permban_vl", 100);
|
||||||
|
|
||||||
|
// Checks - InventoryMove
|
||||||
|
DEFAULTS.put("checks.inventorymove.enabled", true);
|
||||||
|
DEFAULTS.put("checks.inventorymove.warn_vl", 10);
|
||||||
|
DEFAULTS.put("checks.inventorymove.kick_vl", 25);
|
||||||
|
DEFAULTS.put("checks.inventorymove.tempban_vl", 50);
|
||||||
|
DEFAULTS.put("checks.inventorymove.permban_vl", 100);
|
||||||
|
|
||||||
|
// Punishments
|
||||||
|
DEFAULTS.put("punishments.kick_command", "kick %player% &c[XAC] Illegal activity detected");
|
||||||
|
DEFAULTS.put("punishments.tempban_command", "tempban %player% 30d %reason%");
|
||||||
|
DEFAULTS.put("punishments.permban_command", "ban %player% %reason%");
|
||||||
|
DEFAULTS.put("punishments.default_reason", "[XeroAntiCheat] Suspicious activity");
|
||||||
|
|
||||||
|
// Alerts
|
||||||
|
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.staff_format", "<gray>[%time%] %message%");
|
||||||
|
DEFAULTS.put("alerts.cooldown_ms", 5000);
|
||||||
|
|
||||||
|
// TPS
|
||||||
|
DEFAULTS.put("tps.enabled", true);
|
||||||
|
DEFAULTS.put("tps.min_tps_threshold", 18.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigManager(XeroAntiCheat plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load or reload the configuration file
|
||||||
|
*/
|
||||||
|
public void loadConfig() {
|
||||||
|
configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||||
|
|
||||||
|
if (!configFile.exists()) {
|
||||||
|
plugin.getDataFolder().mkdirs();
|
||||||
|
try (InputStream in = plugin.getResource("config.yml")) {
|
||||||
|
if (in != null) {
|
||||||
|
Files.copy(in, configFile.toPath());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Could not create default config", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config = YamlConfiguration.loadConfiguration(configFile);
|
||||||
|
|
||||||
|
// Apply defaults
|
||||||
|
for (Map.Entry<String, Object> entry : DEFAULTS.entrySet()) {
|
||||||
|
if (config.get(entry.getKey()) == null) {
|
||||||
|
config.set(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save if new keys were added
|
||||||
|
try {
|
||||||
|
config.save(configFile);
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Could not save config", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate configuration values
|
||||||
|
*/
|
||||||
|
private void validateConfig() {
|
||||||
|
// Check for invalid values and log warnings
|
||||||
|
if (config.getDouble("checks.speed.max_speed", 0.0) <= 0.0) {
|
||||||
|
plugin.getLogger().warning("Invalid checks.speed.max_speed, using default 0.56");
|
||||||
|
config.set("checks.speed.max_speed", 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.getInt("violation.decay_interval", 0) <= 0) {
|
||||||
|
plugin.getLogger().warning("Invalid violation.decay_interval, using default 30");
|
||||||
|
config.set("violation.decay_interval", 30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type-safe getters
|
||||||
|
|
||||||
|
public boolean getBoolean(String path, boolean defaultValue) {
|
||||||
|
return config.getBoolean(path, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getBoolean(String path) {
|
||||||
|
return config.getBoolean(path, DEFAULTS.containsKey(path) && DEFAULTS.get(path) instanceof Boolean
|
||||||
|
? (Boolean) DEFAULTS.get(path) : false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInt(String path, int defaultValue) {
|
||||||
|
return config.getInt(path, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInt(String path) {
|
||||||
|
return config.getInt(path, DEFAULTS.containsKey(path) && DEFAULTS.get(path) instanceof Integer
|
||||||
|
? (Integer) DEFAULTS.get(path) : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getDouble(String path, double defaultValue) {
|
||||||
|
return config.getDouble(path, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getDouble(String path) {
|
||||||
|
return config.getDouble(path, DEFAULTS.containsKey(path) && DEFAULTS.get(path) instanceof Double
|
||||||
|
? (Double) DEFAULTS.get(path) : 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getString(String path) {
|
||||||
|
return config.getString(path, DEFAULTS.containsKey(path) ? String.valueOf(DEFAULTS.get(path)) : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getString(String path, String defaultValue) {
|
||||||
|
return config.getString(path, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileConfiguration getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return getBoolean("enabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDebug() {
|
||||||
|
return getBoolean("debug", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlertsFormat() {
|
||||||
|
return getString("alerts.format");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAlertsEnabled() {
|
||||||
|
return getBoolean("alerts.enabled", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.xeroth.xeroanticheat.manager;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public class DatabaseManager {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
private Connection connection;
|
||||||
|
|
||||||
|
private static final String CREATE_TABLE = """
|
||||||
|
CREATE TABLE IF NOT EXISTS punishments (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
player_uuid TEXT NOT NULL,
|
||||||
|
player_name TEXT NOT NULL,
|
||||||
|
check_name TEXT NOT NULL,
|
||||||
|
vl REAL NOT NULL
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
|
||||||
|
private static final String INSERT = """
|
||||||
|
INSERT INTO punishments (timestamp, type, player_uuid, player_name, check_name, vl)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""";
|
||||||
|
|
||||||
|
public DatabaseManager(XeroAntiCheat plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize() {
|
||||||
|
try {
|
||||||
|
File dbFile = new File(plugin.getDataFolder(), "data.db");
|
||||||
|
plugin.getDataFolder().mkdirs();
|
||||||
|
String url = "jdbc:sqlite:" + dbFile.getAbsolutePath();
|
||||||
|
connection = DriverManager.getConnection(url);
|
||||||
|
|
||||||
|
try (var stmt = connection.createStatement()) {
|
||||||
|
stmt.execute(CREATE_TABLE);
|
||||||
|
}
|
||||||
|
plugin.getLogger().info("SQLite database initialized.");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Failed to initialize SQLite database", e);
|
||||||
|
connection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void insertPunishment(
|
||||||
|
String timestamp, String type,
|
||||||
|
String playerUuid, String playerName,
|
||||||
|
String checkName, double vl) {
|
||||||
|
|
||||||
|
if (connection == null) return;
|
||||||
|
|
||||||
|
try (PreparedStatement ps = connection.prepareStatement(INSERT)) {
|
||||||
|
ps.setString(1, timestamp);
|
||||||
|
ps.setString(2, type);
|
||||||
|
ps.setString(3, playerUuid);
|
||||||
|
ps.setString(4, playerName);
|
||||||
|
ps.setString(5, checkName);
|
||||||
|
ps.setDouble(6, vl);
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to insert punishment record", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void close() {
|
||||||
|
if (connection != null) {
|
||||||
|
try {
|
||||||
|
connection.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to close SQLite connection", e);
|
||||||
|
} finally {
|
||||||
|
connection = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return connection != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package com.xeroth.xeroanticheat.manager;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles punishment execution and logging.
|
||||||
|
*/
|
||||||
|
public class PunishmentManager {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
private final ViolationManager violationManager;
|
||||||
|
|
||||||
|
private final MiniMessage miniMessage = MiniMessage.miniMessage();
|
||||||
|
private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private File logsFolder;
|
||||||
|
private File punishmentsLog;
|
||||||
|
|
||||||
|
public PunishmentManager(XeroAntiCheat plugin, ViolationManager violationManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.violationManager = violationManager;
|
||||||
|
initializeLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeLogs() {
|
||||||
|
logsFolder = new File(plugin.getDataFolder(), "logs");
|
||||||
|
if (!logsFolder.exists()) {
|
||||||
|
logsFolder.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
punishmentsLog = new File(logsFolder, "punishments.log");
|
||||||
|
if (!punishmentsLog.exists()) {
|
||||||
|
try {
|
||||||
|
punishmentsLog.createNewFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Could not create punishments log", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate violations and apply appropriate punishment
|
||||||
|
*/
|
||||||
|
public void evaluate(Player player, String checkName) {
|
||||||
|
if (!plugin.getConfigManager().isEnabled()) return;
|
||||||
|
|
||||||
|
PlayerData data = violationManager.getPlayerData(player);
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
double vl = data.getViolationLevel(checkName);
|
||||||
|
|
||||||
|
String checkPath = "checks." + checkName.toLowerCase() + ".";
|
||||||
|
|
||||||
|
int warnVl = plugin.getConfigManager().getInt(checkPath + "warn_vl", 10);
|
||||||
|
int kickVl = plugin.getConfigManager().getInt(checkPath + "kick_vl", 25);
|
||||||
|
int tempbanVl = plugin.getConfigManager().getInt(checkPath + "tempban_vl", 50);
|
||||||
|
int permbanVl = plugin.getConfigManager().getInt(checkPath + "permban_vl", 100);
|
||||||
|
|
||||||
|
Check check = plugin.getCheckManager().getCheck(checkName);
|
||||||
|
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);
|
||||||
|
data.setLastWarnTime(checkName, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vl >= permbanVl) {
|
||||||
|
punish(player, checkName, "PERMBAN", permbanVl);
|
||||||
|
} else if (vl >= tempbanVl) {
|
||||||
|
punish(player, checkName, "TEMPBAN", tempbanVl);
|
||||||
|
} else if (vl >= kickVl) {
|
||||||
|
punish(player, checkName, "KICK", kickVl);
|
||||||
|
} else if (vl >= warnVl && cooledDown) {
|
||||||
|
warn(player, checkName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send warning to player
|
||||||
|
*/
|
||||||
|
private void warn(Player player, String checkName) {
|
||||||
|
Component message = miniMessage.deserialize(
|
||||||
|
"<red>Warning: <white>Suspicious behavior detected (" + checkName + ")"
|
||||||
|
);
|
||||||
|
player.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply punishment based on type
|
||||||
|
*/
|
||||||
|
private void punish(Player player, String checkName, String type, double vl) {
|
||||||
|
String reason = plugin.getConfigManager().getString("punishments.default_reason", "Suspicious activity");
|
||||||
|
String playerName = player.getName();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "KICK" -> {
|
||||||
|
String kickCmd = plugin.getConfigManager().getString("punishments.kick_command",
|
||||||
|
"kick %player% &c[XAC] Illegal activity detected");
|
||||||
|
kickCmd = kickCmd.replace("%player%", playerName).replace("%reason%", reason);
|
||||||
|
executeCommand(kickCmd);
|
||||||
|
logPunishment(type, player, checkName, vl);
|
||||||
|
plugin.getMetricsManager().recordPunishment();
|
||||||
|
}
|
||||||
|
case "TEMPBAN" -> {
|
||||||
|
String tempbanCmd = plugin.getConfigManager().getString("punishments.tempban_command",
|
||||||
|
"tempban %player% 30d %reason%");
|
||||||
|
tempbanCmd = tempbanCmd.replace("%player%", playerName).replace("%reason%", reason);
|
||||||
|
executeCommand(tempbanCmd);
|
||||||
|
logPunishment(type, player, checkName, vl);
|
||||||
|
plugin.getMetricsManager().recordPunishment();
|
||||||
|
}
|
||||||
|
case "PERMBAN" -> {
|
||||||
|
String permbanCmd = plugin.getConfigManager().getString("punishments.permban_command",
|
||||||
|
"ban %player% %reason%");
|
||||||
|
permbanCmd = permbanCmd.replace("%player%", playerName).replace("%reason%", reason);
|
||||||
|
executeCommand(permbanCmd);
|
||||||
|
logPunishment(type, player, checkName, vl);
|
||||||
|
plugin.getMetricsManager().recordPunishment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a console command
|
||||||
|
*/
|
||||||
|
private void executeCommand(String command) {
|
||||||
|
final String cmd = command.startsWith("/") ? command.substring(1) : command;
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send alert to staff members
|
||||||
|
*/
|
||||||
|
public void sendAlert(Player player, String checkName, double vl, String category) {
|
||||||
|
if (!plugin.getConfigManager().isAlertsEnabled()) return;
|
||||||
|
|
||||||
|
String format = plugin.getConfigManager().getAlertsFormat();
|
||||||
|
format = format.replace("%player%", player.getName())
|
||||||
|
.replace("%check%", checkName)
|
||||||
|
.replace("%vl%", String.valueOf((int) vl))
|
||||||
|
.replace("%category%", category);
|
||||||
|
|
||||||
|
Component message = miniMessage.deserialize(format);
|
||||||
|
|
||||||
|
String categoryPerm = "xac.alerts." + category;
|
||||||
|
|
||||||
|
for (Player staff : Bukkit.getOnlinePlayers()) {
|
||||||
|
boolean hasPermission = staff.hasPermission("xac.alerts")
|
||||||
|
|| staff.hasPermission("xac.admin")
|
||||||
|
|| staff.hasPermission(categoryPerm);
|
||||||
|
if (!hasPermission) continue;
|
||||||
|
if (!plugin.isAlertsEnabled(staff.getUniqueId())) continue;
|
||||||
|
staff.sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getLogger().info(
|
||||||
|
player.getName() + " failed " + checkName
|
||||||
|
+ " [" + category + "] (VL: " + (int) vl + ")"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log punishment to file and database
|
||||||
|
*/
|
||||||
|
private void logPunishment(String type, Player player, String checkName, double vl) {
|
||||||
|
final String timestamp = dateFormat.format(new Date());
|
||||||
|
final String playerName = player.getName();
|
||||||
|
final String playerUuid = player.getUniqueId().toString();
|
||||||
|
|
||||||
|
final String logLine = String.format("[%s] %s | %s | %s | %s | %.1f",
|
||||||
|
timestamp,
|
||||||
|
type,
|
||||||
|
playerName,
|
||||||
|
playerUuid,
|
||||||
|
checkName,
|
||||||
|
vl);
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
try (FileWriter fw = new FileWriter(punishmentsLog, true);
|
||||||
|
PrintWriter pw = new PrintWriter(fw)) {
|
||||||
|
pw.println(logLine);
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Could not write to punishments log", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseManager db = plugin.getDatabaseManager();
|
||||||
|
boolean dbEnabled = plugin.getConfigManager().getBoolean("database.enabled", true);
|
||||||
|
if (db != null && db.isAvailable() && dbEnabled) {
|
||||||
|
db.insertPunishment(timestamp, type, playerUuid, playerName, checkName, vl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually punish a player for a check
|
||||||
|
*/
|
||||||
|
public void manualPunish(Player player, String checkName) {
|
||||||
|
PlayerData data = violationManager.getPlayerData(player);
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
// Set VL to kick threshold and evaluate
|
||||||
|
data.setViolationLevel(checkName, plugin.getConfigManager().getInt("checks." + checkName.toLowerCase() + ".kick_vl", 25));
|
||||||
|
evaluate(player, checkName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package com.xeroth.xeroanticheat.manager;
|
||||||
|
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages violation levels for all players and checks.
|
||||||
|
* Thread-safe implementation with temporal decay support.
|
||||||
|
*/
|
||||||
|
public class ViolationManager {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
|
||||||
|
private final Map<UUID, PlayerData> playerDataCache = new ConcurrentHashMap<>();
|
||||||
|
private final MiniMessage miniMessage = MiniMessage.miniMessage();
|
||||||
|
|
||||||
|
private volatile double decayRate;
|
||||||
|
|
||||||
|
public ViolationManager(XeroAntiCheat plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
public PlayerData getPlayerData(Player player) {
|
||||||
|
return playerDataCache.computeIfAbsent(player.getUniqueId(), k -> new PlayerData(player));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get player data by UUID
|
||||||
|
*/
|
||||||
|
public PlayerData getPlayerData(UUID uuid) {
|
||||||
|
return playerDataCache.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add violation to a player for a specific check
|
||||||
|
*/
|
||||||
|
public void addViolation(Player player, String checkName, double weight) {
|
||||||
|
PlayerData data = getPlayerData(player);
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
data.incrementViolation(checkName, weight);
|
||||||
|
|
||||||
|
data.setLastFlaggedCheck(checkName);
|
||||||
|
data.setLastFlagTime(System.currentTimeMillis());
|
||||||
|
|
||||||
|
double newVl = data.getViolationLevel(checkName);
|
||||||
|
|
||||||
|
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(
|
||||||
|
"<gray>[<white>VERBOSE<gray>] <yellow>" + player.getName()
|
||||||
|
+ " <gray>» <white>" + checkName
|
||||||
|
+ " <gray>+<green>" + String.format("%.1f", weight)
|
||||||
|
+ " <gray>(VL: <white>" + String.format("%.1f", newVl) + "<gray>)"
|
||||||
|
);
|
||||||
|
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()) {
|
||||||
|
plugin.getLogger().info(player.getName() + " violated " + checkName + " (VL: " + newVl + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getMetricsManager().recordFlag();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get violation level for a player and check
|
||||||
|
*/
|
||||||
|
public double getViolationLevel(Player player, String checkName) {
|
||||||
|
PlayerData data = getPlayerData(player);
|
||||||
|
if (data == null) return 0.0;
|
||||||
|
return data.getViolationLevel(checkName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decay all violation levels for all players
|
||||||
|
*/
|
||||||
|
public void decayAll() {
|
||||||
|
for (PlayerData data : playerDataCache.values()) {
|
||||||
|
for (String checkName : data.getViolationLevels().keySet()) {
|
||||||
|
data.decayViolation(checkName, decayRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all violation levels for a player
|
||||||
|
*/
|
||||||
|
public void clearPlayer(UUID uuid) {
|
||||||
|
playerDataCache.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all violation levels for all players
|
||||||
|
*/
|
||||||
|
public void clearAll() {
|
||||||
|
playerDataCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove player data on quit
|
||||||
|
*/
|
||||||
|
public void removePlayer(UUID uuid) {
|
||||||
|
playerDataCache.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save all pending data (for shutdown)
|
||||||
|
*/
|
||||||
|
public void saveAll() {
|
||||||
|
// Currently just clearing, but could be extended to persist to disk
|
||||||
|
if (plugin.getConfigManager().isDebug()) {
|
||||||
|
plugin.getLogger().info("Saved " + playerDataCache.size() + " player data records");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a player has any violations above threshold
|
||||||
|
*/
|
||||||
|
public boolean hasViolations(Player player, double threshold) {
|
||||||
|
PlayerData data = getPlayerData(player);
|
||||||
|
if (data == null) return false;
|
||||||
|
|
||||||
|
for (double vl : data.getViolationLevels().values()) {
|
||||||
|
if (vl >= threshold) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total violations for a player
|
||||||
|
*/
|
||||||
|
public double getTotalViolations(Player player) {
|
||||||
|
PlayerData data = getPlayerData(player);
|
||||||
|
if (data == null) return 0.0;
|
||||||
|
|
||||||
|
double total = 0.0;
|
||||||
|
for (double vl : data.getViolationLevels().values()) {
|
||||||
|
total += vl;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package com.xeroth.xeroanticheat.protocol;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.PacketType;
|
||||||
|
import com.comphenix.protocol.ProtocolLibrary;
|
||||||
|
import com.comphenix.protocol.ProtocolManager;
|
||||||
|
import com.comphenix.protocol.events.PacketAdapter;
|
||||||
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
|
import com.comphenix.protocol.events.ListenerPriority;
|
||||||
|
import com.xeroth.xeroanticheat.XeroAntiCheat;
|
||||||
|
import com.xeroth.xeroanticheat.check.Check;
|
||||||
|
import com.xeroth.xeroanticheat.data.PlayerData;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PacketListener - Handles ProtocolLib packet events for enhanced detection.
|
||||||
|
*
|
||||||
|
* This provides more accurate timing and data for:
|
||||||
|
* - TimerCheck (packet timing)
|
||||||
|
* - KillAuraCheck (rotation analysis)
|
||||||
|
* - AutoClickerCheck (click timing)
|
||||||
|
* - VelocityCheck (server velocity packets)
|
||||||
|
*
|
||||||
|
* Gracefully disabled if ProtocolLib is not present.
|
||||||
|
*/
|
||||||
|
public class PacketListener {
|
||||||
|
|
||||||
|
private final XeroAntiCheat plugin;
|
||||||
|
private ProtocolManager manager;
|
||||||
|
private boolean protocolLibAvailable = false;
|
||||||
|
|
||||||
|
public PacketListener(XeroAntiCheat plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register packet listeners if ProtocolLib is available
|
||||||
|
*/
|
||||||
|
public void register() {
|
||||||
|
try {
|
||||||
|
manager = ProtocolLibrary.getProtocolManager();
|
||||||
|
protocolLibAvailable = true;
|
||||||
|
|
||||||
|
registerMovementAdapter();
|
||||||
|
registerArmAnimationAdapter();
|
||||||
|
registerVelocityAdapter();
|
||||||
|
|
||||||
|
plugin.getLogger().info("ProtocolLib detected - packet-level checks enabled");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
protocolLibAvailable = false;
|
||||||
|
plugin.getLogger().info("ProtocolLib not found - using event-based detection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerMovementAdapter() {
|
||||||
|
final XeroAntiCheat self = this.plugin;
|
||||||
|
manager.addPacketListener(new PacketAdapter(
|
||||||
|
plugin,
|
||||||
|
ListenerPriority.MONITOR,
|
||||||
|
PacketType.Play.Client.POSITION,
|
||||||
|
PacketType.Play.Client.POSITION_LOOK,
|
||||||
|
PacketType.Play.Client.LOOK) {
|
||||||
|
@Override
|
||||||
|
public void onPacketReceiving(PacketEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
if (player == null || !player.isOnline()) return;
|
||||||
|
|
||||||
|
Check timerCheck = self.getCheckManager().getCheck("Timer");
|
||||||
|
if (timerCheck != null && timerCheck.isBypassed(player)) return;
|
||||||
|
|
||||||
|
PlayerData data = self.getViolationManager().getPlayerData(player);
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (now - data.getLastPacketCountReset() > 1000) {
|
||||||
|
data.setPacketsThisSecond(0);
|
||||||
|
data.setLastPacketCountReset(now);
|
||||||
|
}
|
||||||
|
data.incrementPacketsThisSecond();
|
||||||
|
data.setLastMovePacketTime(now);
|
||||||
|
|
||||||
|
PacketType type = event.getPacketType();
|
||||||
|
if (type == PacketType.Play.Client.POSITION_LOOK
|
||||||
|
|| type == PacketType.Play.Client.LOOK) {
|
||||||
|
try {
|
||||||
|
float yaw = event.getPacket().getFloat().read(0);
|
||||||
|
float pitch = event.getPacket().getFloat().read(1);
|
||||||
|
data.addRotation(yaw, pitch);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerArmAnimationAdapter() {
|
||||||
|
final XeroAntiCheat self = this.plugin;
|
||||||
|
manager.addPacketListener(new PacketAdapter(
|
||||||
|
plugin,
|
||||||
|
ListenerPriority.MONITOR,
|
||||||
|
PacketType.Play.Client.ARM_ANIMATION) {
|
||||||
|
@Override
|
||||||
|
public void onPacketReceiving(PacketEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
if (player == null || !player.isOnline()) return;
|
||||||
|
|
||||||
|
Check autoClickerCheck = self.getCheckManager().getCheck("AutoClicker");
|
||||||
|
if (autoClickerCheck != null && autoClickerCheck.isBypassed(player)) return;
|
||||||
|
|
||||||
|
PlayerData data = self.getViolationManager().getPlayerData(player);
|
||||||
|
if (data != null) {
|
||||||
|
data.addClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerVelocityAdapter() {
|
||||||
|
final XeroAntiCheat self = this.plugin;
|
||||||
|
manager.addPacketListener(new PacketAdapter(
|
||||||
|
plugin,
|
||||||
|
ListenerPriority.MONITOR,
|
||||||
|
PacketType.Play.Server.ENTITY_VELOCITY) {
|
||||||
|
@Override
|
||||||
|
public void onPacketSending(PacketEvent event) {
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
if (player == null || !player.isOnline()) return;
|
||||||
|
|
||||||
|
int entityId = event.getPacket().getIntegers().read(0);
|
||||||
|
if (entityId != player.getEntityId()) return;
|
||||||
|
|
||||||
|
Check velCheck = self.getCheckManager().getCheck("Velocity");
|
||||||
|
if (velCheck != null && velCheck.isBypassed(player)) return;
|
||||||
|
|
||||||
|
PlayerData data = self.getViolationManager().getPlayerData(player);
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
double vx = event.getPacket().getIntegers().read(1) / 8000.0;
|
||||||
|
double vy = event.getPacket().getIntegers().read(2) / 8000.0;
|
||||||
|
double vz = event.getPacket().getIntegers().read(3) / 8000.0;
|
||||||
|
|
||||||
|
data.setLastServerVelocity(new org.bukkit.util.Vector(vx, vy, vz));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if ProtocolLib is available
|
||||||
|
*/
|
||||||
|
public boolean isProtocolLibAvailable() {
|
||||||
|
return protocolLibAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister all packet listeners
|
||||||
|
*/
|
||||||
|
public void unregister() {
|
||||||
|
if (!protocolLibAvailable || manager == null) return;
|
||||||
|
|
||||||
|
manager.removePacketListeners(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
353
src/main/resources/config.yml
Normal file
353
src/main/resources/config.yml
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
# XeroAntiCheat Configuration File
|
||||||
|
# Version: 1.0.0
|
||||||
|
# Target: Paper 1.21.x (compatible with 1.20-1.22)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# GENERAL SETTINGS
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Enable or disable the anti-cheat
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Enable debug mode (logs additional information)
|
||||||
|
debug: false
|
||||||
|
|
||||||
|
# Database settings
|
||||||
|
database:
|
||||||
|
# Set to false to disable SQLite logging (flat-file log always active)
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# VIOLATION SYSTEM
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
violation:
|
||||||
|
# Time in seconds between violation level decay
|
||||||
|
decay_interval: 30
|
||||||
|
|
||||||
|
# Amount to reduce VL by each decay interval
|
||||||
|
decay_rate: 0.5
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# CHECK CONFIGURATION
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# Movement Checks
|
||||||
|
checks:
|
||||||
|
# ----------------------------------------
|
||||||
|
# SPEED CHECK
|
||||||
|
# Detects horizontal movement faster than possible
|
||||||
|
# ----------------------------------------
|
||||||
|
speed:
|
||||||
|
enabled: true
|
||||||
|
# Teleport player back to last safe location when flagged
|
||||||
|
setback: false
|
||||||
|
# Base maximum speed (blocks per tick)
|
||||||
|
max_speed: 0.56
|
||||||
|
# Ping compensation factor (scales latency leniency)
|
||||||
|
ping_factor: 1.0
|
||||||
|
# Number of ticks to buffer for rolling average
|
||||||
|
buffer_ticks: 5
|
||||||
|
# VL thresholds
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# FLY CHECK
|
||||||
|
# Detects flying without elytra/creative/spectator
|
||||||
|
# ----------------------------------------
|
||||||
|
fly:
|
||||||
|
enabled: true
|
||||||
|
# Teleport player back to last safe location when flagged
|
||||||
|
setback: false
|
||||||
|
# Number of ticks to allow for stepping/slabs
|
||||||
|
fall_buffer: 15
|
||||||
|
# Maximum ground desync ticks before flagging
|
||||||
|
ground_desync_threshold: 5
|
||||||
|
warn_vl: 25
|
||||||
|
kick_vl: 50
|
||||||
|
tempban_vl: 75
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# JESUS CHECK (NoWaterWalk)
|
||||||
|
# Detects walking on water without Frost Walker
|
||||||
|
# ----------------------------------------
|
||||||
|
jesus:
|
||||||
|
enabled: true
|
||||||
|
# Teleport player back to last safe location when flagged
|
||||||
|
setback: false
|
||||||
|
# Number of consecutive ticks to flag before VL is added
|
||||||
|
buffer_ticks: 15
|
||||||
|
warn_vl: 25
|
||||||
|
kick_vl: 50
|
||||||
|
tempban_vl: 75
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# NOFALL CHECK
|
||||||
|
# Detects no fall damage after falling >3 blocks
|
||||||
|
# ----------------------------------------
|
||||||
|
nofall:
|
||||||
|
enabled: true
|
||||||
|
# Minimum fall distance to track
|
||||||
|
min_fall_distance: 3
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# TIMER CHECK (Blink/Packet Timer)
|
||||||
|
# Detects packet timing anomalies
|
||||||
|
# ----------------------------------------
|
||||||
|
timer:
|
||||||
|
enabled: true
|
||||||
|
# Number of consecutive ticks exceeding max packets before flagging
|
||||||
|
buffer_ticks: 10
|
||||||
|
# Maximum packets per second allowed
|
||||||
|
max_packets_per_second: 25
|
||||||
|
# Milliseconds of no packets before flagging blink
|
||||||
|
blink_threshold_ms: 500
|
||||||
|
warn_vl: 25
|
||||||
|
kick_vl: 50
|
||||||
|
tempban_vl: 75
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# SPIDER CHECK
|
||||||
|
# Detects climbing non-climbable blocks
|
||||||
|
# ----------------------------------------
|
||||||
|
spider:
|
||||||
|
enabled: true
|
||||||
|
# Teleport player back to last safe location when flagged
|
||||||
|
setback: false
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# GLIDE CHECK (ElytraHack)
|
||||||
|
# Detects glide-like movement without elytra
|
||||||
|
# ----------------------------------------
|
||||||
|
glide:
|
||||||
|
enabled: true
|
||||||
|
# Teleport player back to last safe location when flagged
|
||||||
|
setback: false
|
||||||
|
# Minimum horizontal speed for glide detection
|
||||||
|
min_horizontal_speed: 0.5
|
||||||
|
# Maximum Y decrease per tick for glide curve
|
||||||
|
max_y_decrease: 0.1
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# KILLAURA CHECK
|
||||||
|
# Detects impossible combat angles/rotations
|
||||||
|
# ----------------------------------------
|
||||||
|
killaura:
|
||||||
|
enabled: true
|
||||||
|
# Number of consecutive out-of-angle attacks before flagging
|
||||||
|
buffer_ticks: 2
|
||||||
|
# Maximum angle in degrees from look direction
|
||||||
|
max_angle: 100
|
||||||
|
# Maximum rotation change between attacks
|
||||||
|
max_rotation_change: 45
|
||||||
|
# Window for multi-target detection (ms)
|
||||||
|
multitarget_window_ms: 100
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# REACH CHECK
|
||||||
|
# Detects attacking beyond reach
|
||||||
|
# ----------------------------------------
|
||||||
|
reach:
|
||||||
|
enabled: true
|
||||||
|
# Number of consecutive attacks exceeding reach before flagging
|
||||||
|
buffer_hits: 2
|
||||||
|
# Maximum reach in blocks (survival)
|
||||||
|
max_reach: 3.2
|
||||||
|
# Maximum reach in blocks (creative)
|
||||||
|
creative_max_reach: 5.0
|
||||||
|
# Ping compensation factor
|
||||||
|
ping_factor: 1.0
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# CRITICAL CHECK
|
||||||
|
# Detects critical hits without being airborne
|
||||||
|
# ----------------------------------------
|
||||||
|
critical:
|
||||||
|
enabled: true
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# AUTOCLICKER CHECK
|
||||||
|
# Detects excessive CPS or perfect patterns
|
||||||
|
# ----------------------------------------
|
||||||
|
autoclicker:
|
||||||
|
enabled: true
|
||||||
|
# Maximum clicks per second
|
||||||
|
max_cps: 20
|
||||||
|
# Minimum variance (lower = more suspicious)
|
||||||
|
min_variance: 2.0
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# FASTPLACE CHECK
|
||||||
|
# Detects block placement too fast
|
||||||
|
# ----------------------------------------
|
||||||
|
fastplace:
|
||||||
|
enabled: true
|
||||||
|
# Number of consecutive ticks exceeding max blocks before flagging
|
||||||
|
buffer_ticks: 2
|
||||||
|
# Maximum blocks per second
|
||||||
|
max_blocks_per_second: 20
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# PHASE CHECK
|
||||||
|
# Detects players clipping through solid blocks
|
||||||
|
# ----------------------------------------
|
||||||
|
phase:
|
||||||
|
enabled: true
|
||||||
|
# Teleport player back to last safe location when flagged
|
||||||
|
setback: false
|
||||||
|
# Minimum movement distance before ray-cast runs (blocks)
|
||||||
|
min_distance: 0.5
|
||||||
|
# Maximum movement delta — larger values are treated as teleports
|
||||||
|
max_distance: 5.0
|
||||||
|
warn_vl: 5
|
||||||
|
kick_vl: 15
|
||||||
|
tempban_vl: 30
|
||||||
|
permban_vl: 60
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# VELOCITY CHECK
|
||||||
|
# Detects players ignoring server-sent knockback (requires ProtocolLib)
|
||||||
|
# ----------------------------------------
|
||||||
|
velocity:
|
||||||
|
enabled: true
|
||||||
|
# Minimum server-sent velocity magnitude to check
|
||||||
|
min_expected_velocity: 0.15
|
||||||
|
# Player must move at least 20% of expected knockback
|
||||||
|
min_displacement_ratio: 0.2
|
||||||
|
warn_vl: 8
|
||||||
|
kick_vl: 20
|
||||||
|
tempban_vl: 40
|
||||||
|
permban_vl: 80
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# SCAFFOLD CHECK
|
||||||
|
# Detects automated scaffolding
|
||||||
|
# ----------------------------------------
|
||||||
|
scaffold:
|
||||||
|
enabled: true
|
||||||
|
# Number of consecutive signal accumulations before flagging
|
||||||
|
buffer_ticks: 2
|
||||||
|
# Minimum pitch angle for suspicious placement
|
||||||
|
min_pitch: 75
|
||||||
|
# Number of signals required to flag
|
||||||
|
signals_required: 2
|
||||||
|
# Signal 4: Max yaw change (degrees) between placements
|
||||||
|
rotation_lock_threshold: 2.0
|
||||||
|
# Signal 4: Min horizontal speed (blocks/tick) required
|
||||||
|
min_move_speed: 0.15
|
||||||
|
# Signal 5: StdDev below this triggers signal (too-perfect timing)
|
||||||
|
min_placement_variance_ms: 30.0
|
||||||
|
# Signal 5: Min blocks/sec before signal 5 is evaluated
|
||||||
|
min_bps_for_variance_check: 5
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# FASTEAT CHECK
|
||||||
|
# Detects eating faster than possible
|
||||||
|
# ----------------------------------------
|
||||||
|
fasteat:
|
||||||
|
enabled: true
|
||||||
|
# Number of consecutive fast eats before flagging
|
||||||
|
buffer_ticks: 2
|
||||||
|
# Maximum eating duration in ticks (32 = 1.6s)
|
||||||
|
max_eat_ticks: 32
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ----------------------------------------
|
||||||
|
# INVENTORYMOVE CHECK
|
||||||
|
# Detects movement while inventory open
|
||||||
|
# ----------------------------------------
|
||||||
|
inventorymove:
|
||||||
|
enabled: true
|
||||||
|
warn_vl: 10
|
||||||
|
kick_vl: 25
|
||||||
|
tempban_vl: 50
|
||||||
|
permban_vl: 100
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# PUNISHMENT SETTINGS
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
punishments:
|
||||||
|
# Commands to execute for each punishment level
|
||||||
|
# Use %player% for player name, %reason% for reason
|
||||||
|
kick_command: "kick %player% &c[XAC] Illegal activity detected"
|
||||||
|
tempban_command: "tempban %player% 30d %reason%"
|
||||||
|
permban_command: "ban %player% %reason%"
|
||||||
|
|
||||||
|
# Default reason for bans
|
||||||
|
default_reason: "[XeroAntiCheat] Suspicious activity"
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ALERT SYSTEM
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
alerts:
|
||||||
|
# Enable or disable alert broadcasts
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Alert format (MiniMessage)
|
||||||
|
# Available placeholders: %player%, %check%, %vl%
|
||||||
|
format: "<dark_red>[<red>XAC<dark_red>] <white>%player% <red>failed <white>%check% <red>(VL: <white>%vl%<red>)"
|
||||||
|
|
||||||
|
# Staff-only alert format
|
||||||
|
staff_format: "<gray>[%time%] %message%"
|
||||||
|
|
||||||
|
# Minimum milliseconds between alert/warn messages for the same player+check.
|
||||||
|
# Prevents chat spam when a player is flagging at high frequency.
|
||||||
|
# Default: 5000ms (5 seconds). Set to 0 to disable throttling.
|
||||||
|
cooldown_ms: 5000
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# TPS COMPENSATION
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
tps:
|
||||||
|
# Enable TPS-based threshold scaling
|
||||||
|
enabled: true
|
||||||
|
# Minimum TPS to apply compensation
|
||||||
|
min_tps_threshold: 18.0
|
||||||
185
src/main/resources/plugin.yml
Normal file
185
src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
name: XeroAntiCheat
|
||||||
|
version: 1.2.0
|
||||||
|
main: com.xeroth.xeroanticheat.XeroAntiCheat
|
||||||
|
author: Xeroth
|
||||||
|
description: Lightweight, accurate anti-cheat for Paper 1.21.x
|
||||||
|
api-version: 1.21
|
||||||
|
softdepend:
|
||||||
|
- ProtocolLib
|
||||||
|
|
||||||
|
commands:
|
||||||
|
xac:
|
||||||
|
description: XeroAntiCheat main command
|
||||||
|
usage: /xac <reload|status|punish|clearviolations|verbose|alerts|version>
|
||||||
|
permission: xac.command.version
|
||||||
|
aliases: [xeroanticheat, anticheat]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
|
||||||
|
# ── Wildcards ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
xac.*:
|
||||||
|
description: Grants all XeroAntiCheat permissions including bypass
|
||||||
|
default: false
|
||||||
|
children:
|
||||||
|
xac.admin: true
|
||||||
|
xac.bypass: true
|
||||||
|
|
||||||
|
xac.admin:
|
||||||
|
description: Grants all staff commands and alert access (does NOT grant bypass)
|
||||||
|
default: op
|
||||||
|
children:
|
||||||
|
xac.command.reload: true
|
||||||
|
xac.command.status: true
|
||||||
|
xac.command.punish: true
|
||||||
|
xac.command.clearviolations: true
|
||||||
|
xac.command.verbose: true
|
||||||
|
xac.command.alerts: true
|
||||||
|
xac.command.version: true
|
||||||
|
xac.alerts: true
|
||||||
|
|
||||||
|
# ── Commands ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
xac.command.reload:
|
||||||
|
description: Reload XAC configuration
|
||||||
|
default: op
|
||||||
|
|
||||||
|
xac.command.status:
|
||||||
|
description: View a player's violation levels and ping
|
||||||
|
default: op
|
||||||
|
|
||||||
|
xac.command.punish:
|
||||||
|
description: Manually trigger a punishment for a player
|
||||||
|
default: op
|
||||||
|
|
||||||
|
xac.command.clearviolations:
|
||||||
|
description: Clear all violation levels for a player
|
||||||
|
default: op
|
||||||
|
|
||||||
|
xac.command.verbose:
|
||||||
|
description: Toggle verbose per-flag output for a specific player
|
||||||
|
default: op
|
||||||
|
|
||||||
|
xac.command.alerts:
|
||||||
|
description: Toggle receiving anti-cheat alerts in chat
|
||||||
|
default: op
|
||||||
|
|
||||||
|
xac.command.version:
|
||||||
|
description: Show the plugin version
|
||||||
|
default: true
|
||||||
|
|
||||||
|
# ── Alerts ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
xac.alerts:
|
||||||
|
description: Receive alerts for all checks
|
||||||
|
default: op
|
||||||
|
children:
|
||||||
|
xac.alerts.movement: true
|
||||||
|
xac.alerts.combat: true
|
||||||
|
xac.alerts.misc: true
|
||||||
|
|
||||||
|
xac.alerts.movement:
|
||||||
|
description: Receive alerts for movement checks only
|
||||||
|
default: false
|
||||||
|
|
||||||
|
xac.alerts.combat:
|
||||||
|
description: Receive alerts for combat checks only
|
||||||
|
default: false
|
||||||
|
|
||||||
|
xac.alerts.misc:
|
||||||
|
description: Receive alerts for misc checks only
|
||||||
|
default: false
|
||||||
|
|
||||||
|
# ── Bypass ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
xac.bypass:
|
||||||
|
description: Bypass all anti-cheat checks
|
||||||
|
default: false
|
||||||
|
children:
|
||||||
|
xac.bypass.movement: true
|
||||||
|
xac.bypass.combat: true
|
||||||
|
xac.bypass.misc: true
|
||||||
|
|
||||||
|
xac.bypass.movement:
|
||||||
|
description: Bypass all movement checks
|
||||||
|
default: false
|
||||||
|
children:
|
||||||
|
xac.bypass.speed: true
|
||||||
|
xac.bypass.fly: true
|
||||||
|
xac.bypass.jesus: true
|
||||||
|
xac.bypass.nofall: true
|
||||||
|
xac.bypass.timer: true
|
||||||
|
xac.bypass.spider: true
|
||||||
|
xac.bypass.glide: true
|
||||||
|
xac.bypass.phase: true
|
||||||
|
|
||||||
|
xac.bypass.combat:
|
||||||
|
description: Bypass all combat checks
|
||||||
|
default: false
|
||||||
|
children:
|
||||||
|
xac.bypass.killaura: true
|
||||||
|
xac.bypass.reach: true
|
||||||
|
xac.bypass.critical: true
|
||||||
|
xac.bypass.autoclicker: true
|
||||||
|
xac.bypass.velocity: true
|
||||||
|
|
||||||
|
xac.bypass.misc:
|
||||||
|
description: Bypass all miscellaneous checks
|
||||||
|
default: false
|
||||||
|
children:
|
||||||
|
xac.bypass.fastplace: true
|
||||||
|
xac.bypass.scaffold: true
|
||||||
|
xac.bypass.fasteat: true
|
||||||
|
xac.bypass.inventorymove: true
|
||||||
|
|
||||||
|
xac.bypass.speed:
|
||||||
|
description: Bypass SpeedCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.fly:
|
||||||
|
description: Bypass FlyCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.jesus:
|
||||||
|
description: Bypass JesusCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.nofall:
|
||||||
|
description: Bypass NoFallCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.timer:
|
||||||
|
description: Bypass TimerCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.spider:
|
||||||
|
description: Bypass SpiderCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.glide:
|
||||||
|
description: Bypass GlideCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.phase:
|
||||||
|
description: Bypass PhaseCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.killaura:
|
||||||
|
description: Bypass KillAuraCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.reach:
|
||||||
|
description: Bypass ReachCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.critical:
|
||||||
|
description: Bypass CriticalCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.autoclicker:
|
||||||
|
description: Bypass AutoClickerCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.velocity:
|
||||||
|
description: Bypass VelocityCheck (requires ProtocolLib)
|
||||||
|
default: false
|
||||||
|
xac.bypass.fastplace:
|
||||||
|
description: Bypass FastPlaceCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.scaffold:
|
||||||
|
description: Bypass ScaffoldCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.fasteat:
|
||||||
|
description: Bypass FastEatCheck
|
||||||
|
default: false
|
||||||
|
xac.bypass.inventorymove:
|
||||||
|
description: Bypass InventoryMoveCheck
|
||||||
|
default: false
|
||||||
Reference in New Issue
Block a user