From f55d71c35d43783254d64c2d1c221a054e0d791b Mon Sep 17 00:00:00 2001 From: Axel Date: Sun, 15 Mar 2026 03:33:09 -0300 Subject: [PATCH] Initial commit: XeroAntiCheat v1.0.7 --- .gitignore | 7 + README.md | 451 ++++++++++++ pom.xml | 98 +++ .../xeroth/xeroanticheat/XeroAntiCheat.java | 283 ++++++++ .../com/xeroth/xeroanticheat/check/Check.java | 128 ++++ .../checks/combat/AutoClickerCheck.java | 90 +++ .../checks/combat/CriticalCheck.java | 67 ++ .../checks/combat/KillAuraCheck.java | 90 +++ .../checks/combat/ReachCheck.java | 71 ++ .../checks/combat/VelocityCheck.java | 61 ++ .../checks/misc/FastEatCheck.java | 56 ++ .../checks/misc/FastPlaceCheck.java | 41 ++ .../checks/misc/InventoryMoveCheck.java | 53 ++ .../checks/misc/ScaffoldCheck.java | 177 +++++ .../checks/movement/FlyCheck.java | 78 +++ .../checks/movement/GlideCheck.java | 73 ++ .../checks/movement/JesusCheck.java | 84 +++ .../checks/movement/NoFallCheck.java | 84 +++ .../checks/movement/PhaseCheck.java | 65 ++ .../checks/movement/SpeedCheck.java | 140 ++++ .../checks/movement/SpiderCheck.java | 89 +++ .../checks/movement/TimerCheck.java | 84 +++ .../xeroanticheat/command/XACCommand.java | 254 +++++++ .../xeroth/xeroanticheat/data/PlayerData.java | 640 ++++++++++++++++++ .../listener/CombatListener.java | 118 ++++ .../xeroanticheat/listener/MiscListener.java | 161 +++++ .../listener/MovementListener.java | 139 ++++ .../xeroanticheat/manager/CheckManager.java | 94 +++ .../xeroanticheat/manager/ConfigManager.java | 297 ++++++++ .../manager/DatabaseManager.java | 90 +++ .../manager/PunishmentManager.java | 217 ++++++ .../manager/ViolationManager.java | 158 +++++ .../protocol/PacketListener.java | 213 ++++++ src/main/resources/config.yml | 339 ++++++++++ src/main/resources/plugin.yml | 185 +++++ 35 files changed, 5275 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/check/Check.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/combat/AutoClickerCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/combat/CriticalCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/combat/KillAuraCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/combat/ReachCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/combat/VelocityCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/misc/FastEatCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/misc/FastPlaceCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/misc/InventoryMoveCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/misc/ScaffoldCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/movement/FlyCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/movement/GlideCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/movement/JesusCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/movement/NoFallCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/movement/PhaseCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/movement/SpeedCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/movement/SpiderCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/checks/movement/TimerCheck.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/listener/CombatListener.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/listener/MiscListener.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/listener/MovementListener.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/manager/CheckManager.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/manager/ConfigManager.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/manager/DatabaseManager.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/manager/PunishmentManager.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/manager/ViolationManager.java create mode 100644 src/main/java/com/xeroth/xeroanticheat/protocol/PacketListener.java create mode 100644 src/main/resources/config.yml create mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7211a29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target/ +.idea/ +.classpath +.factorypath +.project +.settings/ +dependency-reduced-pom.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb15ccd --- /dev/null +++ b/README.md @@ -0,0 +1,451 @@ +# XeroAntiCheat - Minecraft Anti-Cheat Plugin + +A lightweight, production-ready anti-cheat plugin for Paper 1.21.x (compatible with 1.20-1.22). + +## Overview + +XeroAntiCheat is designed to detect common Minecraft cheats while minimizing false positives. It features: + +- **17 comprehensive checks** across movement, combat, and miscellaneous categories +- **Thread-safe architecture** with async/sync operation separation +- **Configurable thresholds** via config.yml +- **Violation level (VL) system** with temporal decay +- **Punishment pipeline** (warn → kick → tempban → permban) +- **MiniMessage support** for modern chat formatting +- **ProtocolLib integration** (optional) for packet-level detection +- **Automatic potion effect tracking** for accurate speed/fall detection +- **Staff alert toggle** - staff can opt out of receiving alerts + +## Latest Updates (v1.0.5) + +### Bug Fixes + +- **DatabaseManager race condition** - Fixed `close()` method to be synchronized and null the connection after closing. Prevents `SQLException: database is closed` errors when async inserts race with plugin disable. +- **PhaseCheck teleport false positives** - Added `max_distance` config (default 5.0 blocks). Movement deltas exceeding this value are now ignored (treated as teleports). Also replaced hardcoded Y-offset `0.1` with `player.getEyeHeight()` for state-aware ray-casting. +- **Position history reset on teleport** - Added `PlayerTeleportEvent` handler in MovementListener that clears position/velocity history, resets air ticks, and clears server velocity. Prevents false positives across all inter-position checks after teleportation. +- **VelocityCheck grace period** - Added `PlayerRespawnEvent` handler in MiscListener that clears server velocity and position history. Prevents false positives after respawn. +- **ScaffoldCheck Signal 4 false positive** - Changed `lastPlacementYaw` initial value from `0` to `Float.NaN` as a sentinel. Signal 4 now skips on first block placement of the session to prevent false positives when yaw happens to be near 0. + +### Quality Improvements + +- **Listener bypass comments** - Added explanatory comments at `hasPermission("xac.bypass")` early-return sites in all listeners to clarify that category/per-check bypass is enforced deeper in the call stack. + +--- + +## Latest Updates (v1.0.7) + +### Bug Fixes + +- **KillAura multi-target time window** - `checkMultiTarget()` was ignoring the configured time window entirely. Was counting all-time target transitions (up to 5 attacks) instead of distinct entities attacked within the window. Fixed by using parallel iterators over `attackTimestamps` and `attackedEntities` to correctly count unique targets within the last `window` milliseconds. +- **PlayerData deque capacity mismatch** - `attackedEntities` deque was capped at 5 while `attackTimestamps` was capped at 10. After 5 attacks, deques would desync and parallel iteration would produce incorrect results. Fixed by aligning both to capacity 10. +- **NoFall Feather Falling negative damage** - Custom enchant levels above 8 would produce negative expected damage (e.g., level 9 → multiplier -0.08), permanently disabling the check. Fixed by clamping multiplier to minimum 0.0. +- **ScaffoldCheck allocation in hot path** - `computePlacementIntervalStdDev()` was calling `toArray()` on every block placement, allocating arrays in a performance-critical path. Fixed with zero-allocation two-pass iterator approach. + +--- + +## Latest Updates (v1.0.6) + +### Bug Fixes + +- **SpiderCheck and GlideCheck cross-player pollution** - Tick counters (`spiderAirTicks`, `glideAirTicks`) were stored as instance fields on the check class, shared across ALL players. Fixed by moving counters to PlayerData for per-player state. +- **KillAuraCheck angle NaN false negative** - `Math.acos()` was receiving unconstrained dot product values, which can produce NaN when floating-point rounding pushes values outside [-1, 1]. This silently disabled angle detection. Fixed by clamping dot product to [-1, 1] before acos. +- **SpeedCheck rolling average implementation** - `bufferTicks` config was read but never used. Implemented actual consecutive-tick counter that flags only after speed exceeds max for N consecutive ticks. +- **SpeedCheck latency compensation ignored actual ping** - Was using fixed config value regardless of player ping. Now scales with actual ping using formula `min(ping / 100.0 * 0.01 * pingFactor, 0.15)`, consistent with ReachCheck. +- **PlayerData.getSecondLastPosition() allocation** - Method was calling `toArray()` on every call, allocating a temporary array in a hot path. Replaced with iterator-based access. + +### Config Changes + +- Renamed `checks.speed.latency_compensation` to `checks.speed.ping_factor` for clarity + +--- + +## Latest Updates (v1.0.4) + +### Bug Fixes + +- **FastEat bypass permissions** - Fixed missing `isBypassed()` guard in MiscListener for FastEat check. Permissions `xac.bypass.fasteat` and `xac.bypass.misc` now work correctly. +- **Bypass consistency** - Replaced `hasPermission("xac.bypass")` with `isBypassed(player)` in all 15 check classes for consistent three-tier permission checking. + +### ProtocolLib Integration + +- **Real packet listeners** - PacketListener.java completely rewritten to use actual ProtocolLib API (no more reflection-based stubs) +- **Three packet adapters**: + - Movement packets (POSITION, POSITION_LOOK, LOOK) → feeds TimerCheck counters and KillAura rotation history + - ARM_ANIMATION → feeds AutoClickerCheck click tracking + - ENTITY_VELOCITY → feeds VelocityCheck knockback verification +- **Event guard** - TimerCheck now skips event-based counter increment when ProtocolLib is active (prevents double-counting) +- **Click guard** - CombatListener no longer adds clicks via event when ProtocolLib handles ARM_ANIMATION + +### New Checks + +- **PhaseCheck** - Detects players clipping through solid blocks using server-side ray-casting between positions. Requires 0.5+ block movement to trigger. +- **VelocityCheck** - Detects players ignoring server-sent knockback (no-knockback hacks). Requires ProtocolLib. Compares actual horizontal displacement against expected velocity over 4 ticks. + +### Scaffold Improvements + +- **Two new detection signals**: + - Signal 4: Rotation lock - detects fixed yaw while side-bridging (yaw change <2° with horizontal speed >0.15) + - Signal 5: Placement interval variance - detects suspiciously perfect timing (stdDev <30ms at 5+ BPS) +- Now has 5 signals total, still requires 2 to flag + +### SQLite Database + +- **DatabaseManager** - New manager for SQLite-based punishment logging +- Punishments now logged to both flat-file (`logs/punishments.log`) and SQLite (`data.db`) +- Configurable via `database.enabled` in config.yml (default: true) + +--- + +## Latest Updates (v1.0.3) + +### Bug Fixes + +- **Granular bypass permissions now working** - Added `isBypassed(Player)` method to Check.java and guarded all direct `addViolation()` calls in CombatListener and MiscListener. The permissions `xac.bypass.reach`, `xac.bypass.killaura`, `xac.bypass.critical`, `xac.bypass.scaffold`, `xac.bypass.nofall`, and category bypasses now function correctly for all checks. +- **TimerCheck blink detection** - Fixed bypass check to use three-tier permission hierarchy instead of only checking global `xac.bypass`. +- **SpeedCheck Slowness calculation** - Fixed incorrect field reference: was using `getSpeedLevel()` instead of `getSlownessLevel()` for Slowness potion penalty, causing false positives for players under Slowness effect. +- **Async file I/O** - `logPunishment()` now writes to disk on a background thread to prevent tick lag. + +### Quality Improvements + +- **Main thread Bukkit API** - Changed `startPotionRefreshTask()` from async to sync (Bukkit API should only be called from main thread). +- **Thread-safe collections** - Changed `alertToggles` from `HashMap` to `ConcurrentHashMap` for future-proofing. + +### Permission System Overhaul (v1.0.2) + +- **Granular permission tree** - One permission per action with wildcard inheritance +- **Per-category alerts** - Staff can receive only movement, combat, or misc alerts +- **Per-check bypass** - Bypass specific checks (e.g., `xac.bypass.speed`) +- **Category bypass** - Bypass entire categories (e.g., `xac.bypass.movement`) +- **New commands**: `/xac clearviolations `, `/xac verbose ` +- **Verbose mode** - Per-flag debug output (disabled by default, enable with `/xac verbose `) +- Category-filtered alerts respect permissions: `xac.alerts.movement`, `xac.alerts.combat`, `xac.alerts.misc` + +### Bug Fixes (v1.0.2) + +- **NoFallCheck**: Rewrote damage detection logic - now properly calculates expected fall damage and compares against actual damage via EntityDamageEvent +- **CriticalCheck**: Fixed physics-based crit detection - now uses velocity/fall distance instead of damage modulo (was causing 100% false positives) +- **airTicks**: Fixed multiple checks incrementing airTicks in same tick - now only MovementListener manages airTicks, checks have their own counters +- **Potion Effects**: Added automatic potion effect refresh task (runs every 10 ticks async) - Speed, Slowness, Levitation, Dolphins Grace, Jump Boost, Slow Falling now properly tracked +- **Knockback**: Added knockback tracking - SpeedCheck now accounts for knockback (500ms grace after taking damage) +- **FastEatCheck**: Fixed to track eat start time (PlayerInteractEvent) instead of consume-to-consume interval +- **JesusCheck**: Added isSwimming() check to avoid false positives for underwater movement +- **ReachCheck**: Ping compensation now scales dynamically with actual player ping (0.03 blocks per 100ms, capped at 0.3) +- **TimerCheck**: Added scheduled task for blink detection (checks every 5 ticks for gaps >500ms) +- **Alert Toggle**: Staff can now toggle alerts on/off with `/xac alerts on/off` +- **ConfigManager**: Fixed validation to use getDouble() for max_speed +- **Food Detection**: MiscListener now uses Paper API isEdible() instead of hardcoded switch + +### Architecture Improvements + +- Async potion effect refresh task keeps all check data up-to-date +- Independent tick counters per check prevent false positives from shared state +- EntityDamageEvent listener for accurate damage comparison in NoFallCheck +- Dynamic ping compensation in ReachCheck scales with actual network conditions + +## Project Structure + +``` +/home/axel/Músicas/Java +├── pom.xml # Maven build configuration +├── src/main/ +│ ├── java/com/xeroth/xeroanticheat/ +│ │ ├── XeroAntiCheat.java # Main plugin class +│ │ ├── check/ +│ │ │ └── Check.java # Abstract base class for all checks +│ │ ├── checks/ +│ │ │ ├── movement/ # Movement-related checks +│ │ │ │ ├── SpeedCheck.java +│ │ │ │ ├── FlyCheck.java +│ │ │ │ ├── JesusCheck.java +│ │ │ │ ├── NoFallCheck.java +│ │ │ │ ├── TimerCheck.java +│ │ │ │ ├── SpiderCheck.java +│ │ │ │ └── GlideCheck.java +│ │ │ ├── combat/ # Combat-related checks +│ │ │ │ ├── KillAuraCheck.java +│ │ │ │ ├── ReachCheck.java +│ │ │ │ ├── CriticalCheck.java +│ │ │ │ └── AutoClickerCheck.java +│ │ │ └── misc/ # Miscellaneous checks +│ │ │ ├── FastPlaceCheck.java +│ │ │ ├── ScaffoldCheck.java +│ │ │ ├── FastEatCheck.java +│ │ │ └── InventoryMoveCheck.java +│ │ ├── manager/ # Core managers +│ │ │ ├── CheckManager.java +│ │ │ ├── ConfigManager.java +│ │ │ ├── PunishmentManager.java +│ │ │ └── ViolationManager.java +│ │ ├── data/ +│ │ │ └── PlayerData.java # Per-player state storage +│ │ ├── command/ +│ │ │ └── XACCommand.java # Command handler +│ │ ├── listener/ # Event listeners +│ │ │ ├── MovementListener.java +│ │ │ ├── CombatListener.java +│ │ │ └── MiscListener.java +│ │ └── protocol/ +│ │ └── PacketListener.java # ProtocolLib integration +│ └── resources/ +│ ├── plugin.yml # Plugin metadata +│ └── config.yml # Configuration file +└── target/ + └── XeroAntiCheat.jar # Built plugin +``` + +## Architecture + +### Module Interaction + +``` +┌─────────────────────────────────────────────────────────────┐ +│ XeroAntiCheat (Main) │ +│ - onEnable(): Initialize all managers, register listeners │ +│ - onDisable(): Cleanup, save data, cancel tasks │ +└─────────────────────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ ConfigManager │ │ CheckManager │ │ PlayerData Cache│ +│ - loadConfig() │ │ - registerCheck│ │ - ConcurrentHM │ +│ - getTyped() │ │ - runCheck() │ │ - onJoin/Quit │ +│ - validate() │ │ - getCheck() │ │ - thread-safe │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Event Listeners (Sync) │ +│ - MovementListener → PlayerMoveEvent │ +│ - CombatListener → EntityDamageByEntityEvent │ +│ - MiscListener → BlockPlaceEvent, PlayerItemConsumeEvent│ +└─────────────────────────────────────────────────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ ViolationManager │ │ PunishmentManager │ │ ProtocolLib │ +│ - addViolation │ │ - evaluate() │ │ PacketListener │ +│ - decayVL() │ │ - punish() │ │ - MONITOR │ +│ - getVL() │ │ - warn/kick/ban │ │ - hasProtocolLib│ +└──────────────────┘ └──────────────────┘ └──────────────────┘ +``` + +### Key Design Decisions + +1. **Thread Safety**: PlayerData uses `ConcurrentHashMap` and `ArrayDeque` for thread-safe operations +2. **Performance**: Movement checks only run when `hasChangedPosition()` is true +3. **Async Operations**: VL decay, potion refresh, config validation run async +4. **Fallback System**: Features work without ProtocolLib (reduced accuracy) + +## Check Descriptions + +### Movement Checks + +| Check | Detection Method | +|-------|-----------------| +| **SpeedCheck** | Horizontal movement exceeding max speed for player state (sprinting, sneaking, swimming, potions, ice, etc.). Uses rolling average over 5 ticks with TPS/ping compensation. | +| **FlyCheck** | Sustained flight without elytra/creative/spectator. Tracks air ticks and ground desync between client/server. | +| **JesusCheck** | Walking on water without Frost Walker enchantment or boat. Checks block type at feet. | +| **NoFallCheck** | No fall damage after falling >3 blocks. Compares expected damage vs actual, accounting for Feather Falling, Slow Falling, water, honey blocks. | +| **TimerCheck** | Packet timing anomalies: >22 packets/sec or blink (no packets >500ms then teleport). | +| **SpiderCheck** | Climbing non-climbable blocks (non-ladder/vine/scaffolding) with upward velocity. | +| **GlideCheck** | Elytra-like fall curve (slow Y decrease) without elytra equipped. | +| **PhaseCheck** | Server-side ray-cast between positions. Flags if solid block intersects movement path. Requires 0.5+ block travel. | + +### Combat Checks + +| Check | Detection Method | +|-------|-----------------| +| **KillAuraCheck** | Three sub-checks: (1) Attack angle >100° from look direction, (2) Snap rotation >45° between attacks, (3) Multi-targeting >2 entities within 100ms. | +| **ReachCheck** | 3D distance from player eye to entity hitbox. Default threshold 3.2 blocks (survival), 5.0 (creative), with ping compensation. | +| **CriticalCheck** | Critical hits without airborne state. Detects crits while on ground or while sprinting (cancels sprint). Allows legitimate jump-crits. | +| **AutoClickerCheck** | CPS tracking over 1-second window. Flags >20 CPS. Also analyzes inter-click variance - flags suspiciously perfect patterns (stdDev < 2.0). | +| **VelocityCheck** | Compares actual horizontal displacement against server-sent ENTITY_VELOCITY. Flags if player moves <20% of expected knockback. Requires ProtocolLib. | + +### Miscellaneous Checks + +| Check | Detection Method | +|-------|-----------------| +| **FastPlaceCheck** | Block placement >20 blocks/sec. Uses timestamp deque for tracking. | +| **ScaffoldCheck** | Multi-signal detection (5 signals): (1) Pitch >75° while running, (2) Placing below player, (3) No valid support face, (4) Rotation lock (yaw change <2° while moving), (5) Placement interval variance (too-perfect timing). Requires 2+ signals. | +| **FastEatCheck** | Food consumption faster than 32 ticks (1.6 seconds). | +| **InventoryMoveCheck** | Position change >0.1 while inventory is open. | + +## Violation System + +### VL Accumulation + +- Each check maintains its own violation level per player +- `addViolation(player, checkName, weight)` increments VL +- Default weight is 1.0, scaled by detection severity + +### Decay + +- Runs every 30 seconds (configurable) +- Reduces each check's VL by decay_rate (default: 0.5) +- Minimum VL is 0.0 + +### Thresholds + +Configurable per check in `config.yml`: +```yaml +checks.: + warn_vl: 10 # Send warning + kick_vl: 25 # Kick player + tempban_vl: 50 # Temporary ban (30 days) + permban_vl: 100 # Permanent ban +``` + +## Punishment Pipeline + +1. **Warning** (VL ≥ warn_vl): MiniMessage alert to player +2. **Kick** (VL ≥ kick_vl): `player.kick()` +3. **TempBan** (VL ≥ tempban_vl): Execute config command +4. **PermBan** (VL ≥ permban_vl): Execute config command + +Commands are configurable in config.yml: +```yaml +punishments: + kick_command: "kick %player% &c[XAC] Illegal activity detected" + tempban_command: "tempban %player% 30d %reason%" + permban_command: "ban %player% %reason%" +``` + +All punishments logged to `plugins/XeroAntiCheat/logs/punishments.log`. + +## Configuration + +All settings in `config.yml`: + +### General +- `enabled`: Enable/disable plugin +- `debug`: Additional logging +- `async_task_threads`: Background thread count + +### Violation +- `decay_interval`: Seconds between decay (default: 30) +- `decay_rate`: Amount to reduce VL per decay (default: 0.5) + +### Checks +Each check has `enabled` and threshold settings. See `config.yml` for defaults. + +### Commands +- `reload_permission`: xac.admin +- `bypass_permission`: xac.bypass +- `alerts_permission`: xac.alerts + +## Commands + +| Command | Permission | Description | +|---------|------------|-------------| +| `/xac reload` | xac.command.reload | Reload configuration | +| `/xac status ` | xac.command.status | Show player's VL levels and ping | +| `/xac punish ` | xac.command.punish | Manually trigger punishment | +| `/xac clearviolations ` | xac.command.clearviolations | Clear all VL for a player | +| `/xac verbose ` | xac.command.verbose | Toggle per-flag debug output | +| `/xac alerts [on\|off]` | xac.command.alerts | Toggle alert receiving | +| `/xac version` | xac.command.version | Show plugin version | + +## Permissions + +### Wildcards +- `xac.*` - All permissions (admin + bypass) +- `xac.admin` - All commands and alerts (does NOT grant bypass) +- `xac.bypass` - Bypass all checks +- `xac.alerts` - Receive all alerts + +### Command Permissions +- `xac.command.reload` - /xac reload +- `xac.command.status` - /xac status +- `xac.command.punish` - /xac punish +- `xac.command.clearviolations` - /xac clearviolations +- `xac.command.verbose` - /xac verbose +- `xac.command.alerts` - /xac alerts +- `xac.command.version` - /xac version + +### Category Alerts +- `xac.alerts.movement` - Movement check alerts only +- `xac.alerts.combat` - Combat check alerts only +- `xac.alerts.misc` - Misc check alerts only + +### Bypass Permissions +**Movement:** +- `xac.bypass.movement` - All movement checks +- `xac.bypass.speed`, `xac.bypass.fly`, `xac.bypass.jesus`, `xac.bypass.nofall`, `xac.bypass.timer`, `xac.bypass.spider`, `xac.bypass.glide` + +**Combat:** +- `xac.bypass.combat` - All combat checks +- `xac.bypass.killaura`, `xac.bypass.reach`, `xac.bypass.critical`, `xac.bypass.autoclicker` + +**Misc:** +- `xac.bypass.misc` - All misc checks +- `xac.bypass.fastplace`, `xac.bypass.scaffold`, `xac.bypass.fasteat`, `xac.bypass.inventorymove` + +### LuckPerms Example Roles + +``` +# Helper - read-only, sees all alerts +/lp group helper permission set xac.command.status true +/lp group helper permission set xac.command.alerts true +/lp group helper permission set xac.alerts true + +# Junior Mod - combat alerts only +/lp group junior-mod permission set xac.alerts.combat true + +# Moderator - full alerts + punish +/lp group moderator permission set xac.alerts true +/lp group moderator permission set xac.command.punish true + +# Streamer - bypass movement + combat +/lp group streamer permission set xac.bypass.movement true +/lp group streamer permission set xac.bypass.combat true + +# Build team - bypass speed/fly only +/lp group build permission set xac.bypass.speed true +/lp group build permission set xac.bypass.fly true +``` + +## Build Instructions + +```bash +# Build the plugin +mvn package + +# The JAR will be in target/XeroAntiCheat.jar +``` + +## Dependencies + +- **Paper API** 1.21.1-R0.1-SNAPSHOT (provided) +- **ProtocolLib** (optional, for packet-level detection) + +## Performance Optimizations + +1. **Event Filtering**: Movement checks only run when position actually changes +2. **Fixed-Size Data Structures**: ArrayDeque for position/click history (max 20 entries) +3. **Primitive Math**: Avoid creating new Location/Vector objects in hot paths +4. **TPS Compensation**: Velocity thresholds scale with server TPS +5. **Async Operations**: VL decay, config validation, logging run on background threads + +## Edge Case Handling + +| Scenario | Handling | +|----------|----------| +| Creative mode | Most checks ignored | +| Bypass permission | Silently ignored | +| Lag spikes | TPS compensation applied | +| Pearl teleportation | Reset air timers, position buffers | +| Jump-crits | Allow if wasAirborne previous 3 ticks | +| Frost Walker | Allow water walking | +| Slow Falling potion | Ignore fall damage | +| Water/honey landing | No damage expected | + +## Future Enhancements + +Potential areas for expansion: +- Entity prediction check +- More scaffold signals +- Velocity verification +- Phase detection +- Packet order validation +- SQL-based violation logging +- Web dashboard integration diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fcffbd4 --- /dev/null +++ b/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + com.xeroth + xeroanticheat + 1.0.7 + jar + + XeroAntiCheat + Lightweight, accurate anti-cheat for Paper 1.21.x + + + 21 + 21 + UTF-8 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + dmulloy2-repo + https://repo.dmulloy2.net/repository/public/ + + + + + + io.papermc.paper + paper-api + 1.21.1-R0.1-SNAPSHOT + provided + + + com.comphenix.protocol + ProtocolLib + 5.3.0 + provided + + + org.xerial + sqlite-jdbc + 3.47.1.0 + + + + + ${project.name} + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + UTF-8 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + + + net.kyori.adventure + com.xeroth.xeroanticheat.adventure + + + org.sqlite + com.xeroth.xeroanticheat.sqlite + + + + + + + + + diff --git a/src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java b/src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java new file mode 100644 index 0000000..6bd903f --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/XeroAntiCheat.java @@ -0,0 +1,283 @@ +package com.xeroth.xeroanticheat; + +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.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 boolean protocolLibLoaded = false; + + // Staff alert toggles + private final Map alertToggles = new ConcurrentHashMap<>(); + + // Verbose targets (per-player debug output) + private final Set verboseTargets = ConcurrentHashMap.newKeySet(); + + @Override + public void onEnable() { + instance = 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(); + } + + 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); + + 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; + 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(); + 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 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); + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/check/Check.java b/src/main/java/com/xeroth/xeroanticheat/check/Check.java new file mode 100644 index 0000000..1bb36ac --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/check/Check.java @@ -0,0 +1,128 @@ +package com.xeroth.xeroanticheat.check; + +import com.xeroth.xeroanticheat.XeroAntiCheat; +import com.xeroth.xeroanticheat.data.PlayerData; +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()); + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/combat/AutoClickerCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/combat/AutoClickerCheck.java new file mode 100644 index 0000000..15e9a6f --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/combat/AutoClickerCheck.java @@ -0,0 +1,90 @@ +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 java.util.ArrayList; +import java.util.List; + +/** + * 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) { + List clicks = new ArrayList<>(data.getClickTimestamps()); + + if (clicks.size() < 5) return; + + // Calculate intervals between clicks + List intervals = new ArrayList<>(); + for (int i = 0; i < clicks.size() - 1; i++) { + intervals.add(clicks.get(i) - clicks.get(i + 1)); + } + + if (intervals.isEmpty()) return; + + // Calculate mean + double sum = 0; + for (Long interval : intervals) { + sum += interval; + } + double mean = sum / intervals.size(); + + // Calculate variance + double varianceSum = 0; + for (Long interval : intervals) { + double diff = interval - mean; + varianceSum += diff * diff; + } + double variance = varianceSum / intervals.size(); + double stdDev = Math.sqrt(variance); + + // If variance is too low, flag (too perfect) + if (stdDev < minVariance && data.getCPS() > 10) { + flag(data, player, 1.0); + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/combat/CriticalCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/combat/CriticalCheck.java new file mode 100644 index 0000000..24f8633 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/combat/CriticalCheck.java @@ -0,0 +1,67 @@ +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 critical hits when player is NOT in the air. + * + * Also detects if player is sprinting (criticals cancel sprint). + * Accounts for jump-crits (legitimate): allows if player was airborne in previous 3 ticks. + */ +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; + + boolean allowJumpCrits = getConfigBoolean("allow_jump_crits", true); + + // Get player state + boolean isOnGround = player.isOnGround(); + double yVelocity = player.getVelocity().getY(); + boolean wasAirborne = data.wasAirborne(); + + // If player is on ground and not moving up (no jump), flag + if (isOnGround && yVelocity <= 0.0) { + // Check if was recently airborne (jump-crit) + if (allowJumpCrits && wasAirborne) { + return false; // Legitimate jump-crit + } + return true; // Suspicious - no-air crit + } + + // Check if sprinting (criticals cancel sprint) + if (player.isSprinting()) { + return true; // Suspicious - crit while sprinting + } + + return false; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/combat/KillAuraCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/combat/KillAuraCheck.java new file mode 100644 index 0000000..6be9975 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/combat/KillAuraCheck.java @@ -0,0 +1,90 @@ +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.EntityEffect; +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 uniqueInWindow = new HashSet<>(); + Iterator timeIter = data.getAttackTimestamps().iterator(); + Iterator 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; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/combat/ReachCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/combat/ReachCheck.java new file mode 100644 index 0000000..869cbab --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/combat/ReachCheck.java @@ -0,0 +1,71 @@ +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(); + + // Get target location (center of entity hitbox) + Location targetLoc = target.getLocation(); + + // Calculate 3D distance + double distance = eyeLoc.distance(targetLoc); + + return distance > maxReach; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/combat/VelocityCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/combat/VelocityCheck.java new file mode 100644 index 0000000..6c46a7b --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/combat/VelocityCheck.java @@ -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; + + data.decrementVelocityCheckTicks(); + + 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; + } + + 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(); + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastEatCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastEatCheck.java new file mode 100644 index 0000000..6fa0234 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastEatCheck.java @@ -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; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastPlaceCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastPlaceCheck.java new file mode 100644 index 0000000..acd704b --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/misc/FastPlaceCheck.java @@ -0,0 +1,41 @@ +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) { + flag(data, player, (bps - maxBlocksPerSecond) * 0.5); + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/misc/InventoryMoveCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/misc/InventoryMoveCheck.java new file mode 100644 index 0000000..9d6d046 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/misc/InventoryMoveCheck.java @@ -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 + double dx = current.x() - last.x(); + double dy = current.y() - last.y(); + double dz = current.z() - last.z(); + double distance = Math.sqrt(dx*dx + dy*dy + dz*dz); + + // If significant movement while inventory open, flag + if (distance > 0.1) { + flag(data, player); + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/misc/ScaffoldCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/misc/ScaffoldCheck.java new file mode 100644 index 0000000..262aa7f --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/misc/ScaffoldCheck.java @@ -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 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; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/FlyCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/FlyCheck.java new file mode 100644 index 0000000..53e9b91 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/FlyCheck.java @@ -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; + +/** + * 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; + + // 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); + } + } + + // 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) { + // Additional check: see if player has jump boost + if (!data.hasJumpBoost() || data.getJumpBoostLevel() <= 1) { + flag(data, player); + } + } + } + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/GlideCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/GlideCheck.java new file mode 100644 index 0000000..b7b5d8e --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/GlideCheck.java @@ -0,0 +1,73 @@ +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; + + // 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 + double horizontalSpeed = Math.sqrt(velocity.getX() * velocity.getX() + velocity.getZ() * velocity.getZ()); + + // Check if moving fast horizontally + if (horizontalSpeed < minHorizontalSpeed) { + 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); + } + } else { + data.resetGlideTicks(); + } + + // Track last gliding state + data.setLastWasGliding(player.isGliding()); + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/JesusCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/JesusCheck.java new file mode 100644 index 0000000..7966899 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/JesusCheck.java @@ -0,0 +1,84 @@ +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.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 horizontalSpeed = Math.sqrt(dx * dx + dz * dz); + + // If moving at reasonable speed on water, flag + if (horizontalSpeed > 0.1) { + flag(data, player); + } + } + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/NoFallCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/NoFallCheck.java new file mode 100644 index 0000000..f73511f --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/NoFallCheck.java @@ -0,0 +1,84 @@ +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.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) { + 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); + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/PhaseCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/PhaseCheck.java new file mode 100644 index 0000000..467b774 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/PhaseCheck.java @@ -0,0 +1,65 @@ +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); + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpeedCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpeedCheck.java new file mode 100644 index 0000000..1c24d00 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpeedCheck.java @@ -0,0 +1,140 @@ +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 tps = org.bukkit.Bukkit.getTPS()[0]; + double tpsMultiplier = 20.0 / Math.max(tps, 18.0); + + // 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); + 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.subtract(0, 1, 0).getBlock().getType(); + if (blockBelow == Material.SOUL_SAND || blockBelow == Material.SOUL_SOIL) { + baseSpeed *= 0.75; + } + + return baseSpeed; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpiderCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpiderCheck.java new file mode 100644 index 0000000..e6bf532 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/SpiderCheck.java @@ -0,0 +1,89 @@ +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; + +/** + * 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; + + // 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 + Location loc = player.getLocation(); + + // Check block at feet + Material feetBlock = loc.subtract(0, 1, 0).getBlock().getType(); + + // Check block at body level + Material bodyBlock = loc.getBlock().getType(); + + // Check block above head + Material headBlock = loc.add(0, 1, 0).getBlock().getType(); + + // Check if any of these blocks are climbable + 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); + } + } 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; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/checks/movement/TimerCheck.java b/src/main/java/com/xeroth/xeroanticheat/checks/movement/TimerCheck.java new file mode 100644 index 0000000..80c9853 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/checks/movement/TimerCheck.java @@ -0,0 +1,84 @@ +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); + long blinkThresholdMs = getConfigInt("blink_threshold_ms", 500); + + // If ProtocolLib is active, packet counting is handled by PacketListener + if (plugin.isProtocolLibLoaded()) { + if (data.getPacketsThisSecond() > maxPacketsPerSecond) { + flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5); + } + 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) { + flag(data, player, (data.getPacketsThisSecond() - maxPacketsPerSecond) * 0.5); + } + + // Check for blink (packet suppression) + long lastMoveTime = data.getLastMovePacketTime(); + if (lastMoveTime > 0) { + long gap = now - lastMoveTime; + if (gap > blinkThresholdMs) { + // Check if player teleported during the gap + PlayerData.PositionSnapshot current = data.getLastPosition(); + PlayerData.PositionSnapshot last = data.getSecondLastPosition(); + if (current != null && last != null) { + double dx = current.x() - last.x(); + double dy = current.y() - last.y(); + double dz = current.z() - last.z(); + double distance = Math.sqrt(dx*dx + dy*dy + dz*dz); + + // If teleported more than 10 blocks, likely blink + if (distance > 10) { + flag(data, player, 2.0); + } + } + } + } + + data.setLastMovePacketTime(now); + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java b/src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java new file mode 100644 index 0000000..0e242d2 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/command/XACCommand.java @@ -0,0 +1,254 @@ +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 ")); + 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 ")); + 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 ")); + 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 ")); + 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 "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 ", "view violation levels", "xac.command.status"), + new Cmd("/xac punish ", "manual punishment", "xac.command.punish"), + new Cmd("/xac clearviolations ", "clear all VL for player", "xac.command.clearviolations"), + new Cmd("/xac verbose ", "toggle per-flag debug", "xac.command.verbose"), + new Cmd("/xac alerts [on|off]", "toggle alert receiving", "xac.command.alerts"), + 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)); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + return Stream.of("reload","status","punish","clearviolations","verbose","alerts","version") + .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").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(); + } + return List.of(); + } + + public boolean hasAlertsEnabled(UUID uuid) { + return plugin.isAlertsEnabled(uuid); + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java b/src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java new file mode 100644 index 0000000..b641594 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/data/PlayerData.java @@ -0,0 +1,640 @@ +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 positionHistory = new ArrayDeque<>(20); + + // Velocity history + private final Deque velocityHistory = new ArrayDeque<>(10); + + // Rotation history + private final Deque rotationHistory = new ArrayDeque<>(10); + + // Violation levels per check + private final Map violationLevels = new ConcurrentHashMap<>(); + + // Combat tracking + private final Deque clickTimestamps = new ArrayDeque<>(20); + private final Deque attackTimestamps = new ArrayDeque<>(10); + private final Deque 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 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; + + 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 getClickTimestamps() { + return clickTimestamps; + } + + public Deque getAttackedEntities() { + return attackedEntities; + } + + public Deque getAttackTimestamps() { + return attackTimestamps; + } + + public Map 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; + } + + public void clearPositionHistory() { + positionHistory.clear(); + rotationHistory.clear(); + velocityHistory.clear(); + spiderTicks = 0; + glideTicks = 0; + speedViolationTicks = 0; + } + + public Deque 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) {} +} diff --git a/src/main/java/com/xeroth/xeroanticheat/listener/CombatListener.java b/src/main/java/com/xeroth/xeroanticheat/listener/CombatListener.java new file mode 100644 index 0000000..4150412 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/listener/CombatListener.java @@ -0,0 +1,118 @@ +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)) { + plugin.getViolationManager().addViolation(player, "Reach", 1.0); + plugin.getPunishmentManager().evaluate(player, "Reach"); + } + } + + // Check for kill aura (angle) + if (killAuraCheck != null && killAuraCheck.checkAngle(player, target)) { + if (!killAuraCheck.isBypassed(player)) { + plugin.getViolationManager().addViolation(player, "KillAura", 1.0); + plugin.getPunishmentManager().evaluate(player, "KillAura"); + } + } + + // 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); + } + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/listener/MiscListener.java b/src/main/java/com/xeroth/xeroanticheat/listener/MiscListener.java new file mode 100644 index 0000000..a5556cc --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/listener/MiscListener.java @@ -0,0 +1,161 @@ +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)) { + plugin.getViolationManager().addViolation(player, "Scaffold", 1.0); + plugin.getPunishmentManager().evaluate(player, "Scaffold"); + } + } + } + + @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)) { + plugin.getViolationManager().addViolation(player, "FastEat", 1.0); + plugin.getPunishmentManager().evaluate(player, "FastEat"); + } + } + } + + @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(); + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/listener/MovementListener.java b/src/main/java/com/xeroth/xeroanticheat/listener/MovementListener.java new file mode 100644 index 0000000..cfa7fd1 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/listener/MovementListener.java @@ -0,0 +1,139 @@ +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(); + } + + // 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); + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/manager/CheckManager.java b/src/main/java/com/xeroth/xeroanticheat/manager/CheckManager.java new file mode 100644 index 0000000..9211065 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/manager/CheckManager.java @@ -0,0 +1,94 @@ +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 checksByName = new HashMap<>(); + private final List 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()) { + check.check(data, player); + } + } + + /** + * Get all registered checks + */ + public List getRegisteredChecks() { + return new ArrayList<>(registeredChecks); + } + + /** + * Get all enabled checks + */ + public List 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 clazz) { + for (Check check : registeredChecks) { + if (check.getClass().equals(clazz)) { + return check; + } + } + return null; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/manager/ConfigManager.java b/src/main/java/com/xeroth/xeroanticheat/manager/ConfigManager.java new file mode 100644 index 0000000..5f174eb --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/manager/ConfigManager.java @@ -0,0 +1,297 @@ +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 DEFAULTS = new HashMap<>(); + + static { + // General + DEFAULTS.put("enabled", true); + DEFAULTS.put("debug", false); + DEFAULTS.put("async_task_threads", 2); + + // 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.allow_jump_crits", 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", "[XAC] %player% failed %check% (VL: %vl%)"); + DEFAULTS.put("alerts.staff_format", "[%time%] %message%"); + + // Commands + DEFAULTS.put("commands.reload_permission", "xac.admin"); + DEFAULTS.put("commands.bypass_permission", "xac.bypass"); + DEFAULTS.put("commands.alerts_permission", "xac.alerts"); + + // 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 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); + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/manager/DatabaseManager.java b/src/main/java/com/xeroth/xeroanticheat/manager/DatabaseManager.java new file mode 100644 index 0000000..b7bae1b --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/manager/DatabaseManager.java @@ -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; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/manager/PunishmentManager.java b/src/main/java/com/xeroth/xeroanticheat/manager/PunishmentManager.java new file mode 100644 index 0000000..37a17dc --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/manager/PunishmentManager.java @@ -0,0 +1,217 @@ +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"; + + sendAlert(player, checkName, vl, category); + + 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) { + warn(player, checkName); + } + } + + /** + * Send warning to player + */ + private void warn(Player player, String checkName) { + Component message = miniMessage.deserialize( + "Warning: 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); + } + 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); + } + 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); + } + } + } + + /** + * 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(); + if (db != null && db.isAvailable()) { + 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); + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/manager/ViolationManager.java b/src/main/java/com/xeroth/xeroanticheat/manager/ViolationManager.java new file mode 100644 index 0000000..ddb660f --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/manager/ViolationManager.java @@ -0,0 +1,158 @@ +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 playerDataCache = new ConcurrentHashMap<>(); + private final MiniMessage miniMessage = MiniMessage.miniMessage(); + + private double decayRate; + + public ViolationManager(XeroAntiCheat plugin) { + this.plugin = plugin; + this.decayRate = plugin.getConfigManager().getDouble("violation.decay_rate", 0.5); + } + + /** + * 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())) { + Component verbose = miniMessage.deserialize( + "[VERBOSE] " + player.getName() + + " » " + checkName + + " +" + String.format("%.1f", weight) + + " (VL: " + String.format("%.1f", newVl) + ")" + ); + for (Player staff : Bukkit.getOnlinePlayers()) { + if (staff.hasPermission("xac.command.verbose") || staff.hasPermission("xac.admin")) { + staff.sendMessage(verbose); + } + } + } + + if (plugin.getConfigManager().isDebug()) { + plugin.getLogger().info(player.getName() + " violated " + checkName + " (VL: " + newVl + ")"); + } + } + + /** + * 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() { + decayRate = plugin.getConfigManager().getDouble("violation.decay_rate", 0.5); + + 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; + } +} diff --git a/src/main/java/com/xeroth/xeroanticheat/protocol/PacketListener.java b/src/main/java/com/xeroth/xeroanticheat/protocol/PacketListener.java new file mode 100644 index 0000000..947cd81 --- /dev/null +++ b/src/main/java/com/xeroth/xeroanticheat/protocol/PacketListener.java @@ -0,0 +1,213 @@ +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; + } + + /** + * Update packet timing (called from event listeners when ProtocolLib unavailable) + */ + public void updatePacketTiming(Player player) { + if (protocolLibAvailable) { + return; + } + + PlayerData data = plugin.getViolationManager().getPlayerData(player); + if (data == null) return; + + long now = System.currentTimeMillis(); + data.setLastMovePacketTime(now); + + if (now - data.getLastPacketCountReset() > 1000) { + data.setPacketsThisSecond(0); + data.setLastPacketCountReset(now); + } + data.incrementPacketsThisSecond(); + } + + /** + * Record click (called from event listeners when ProtocolLib unavailable) + */ + public void recordClick(Player player) { + if (protocolLibAvailable) { + return; + } + + PlayerData data = plugin.getViolationManager().getPlayerData(player); + if (data != null) { + data.addClick(); + } + } + + /** + * Record attack (called from event listeners when ProtocolLib unavailable) + */ + public void recordAttack(Player player, java.util.UUID entityUuid) { + if (protocolLibAvailable) { + return; + } + + PlayerData data = plugin.getViolationManager().getPlayerData(player); + if (data != null) { + data.addAttack(entityUuid); + data.setLastAttackYaw(player.getLocation().getYaw()); + } + } + + /** + * Unregister all packet listeners + */ + public void unregister() { + if (!protocolLibAvailable || manager == null) return; + + manager.removePacketListeners(plugin); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..003bfea --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,339 @@ +# 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 + +# Number of async threads for background tasks +async_task_threads: 2 + +# 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 + # 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 + # Number of ticks to allow for stepping/slabs + fall_buffer: 10 + # Maximum ground desync ticks before flagging + ground_desync_threshold: 3 + warn_vl: 10 + kick_vl: 25 + tempban_vl: 50 + permban_vl: 100 + + # ---------------------------------------- + # JESUS CHECK (NoWaterWalk) + # Detects walking on water without Frost Walker + # ---------------------------------------- + jesus: + enabled: true + warn_vl: 10 + kick_vl: 25 + tempban_vl: 50 + 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 + # Maximum packets per second allowed + max_packets_per_second: 22 + # Milliseconds of no packets before flagging blink + blink_threshold_ms: 500 + warn_vl: 10 + kick_vl: 25 + tempban_vl: 50 + permban_vl: 100 + + # ---------------------------------------- + # SPIDER CHECK + # Detects climbing non-climbable blocks + # ---------------------------------------- + spider: + enabled: true + warn_vl: 10 + kick_vl: 25 + tempban_vl: 50 + permban_vl: 100 + + # ---------------------------------------- + # GLIDE CHECK (ElytraHack) + # Detects glide-like movement without elytra + # ---------------------------------------- + glide: + enabled: true + # 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 + # 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 + # 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 + # Allow legitimate jump-crits + allow_jump_crits: 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 + # 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 + # 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 + # 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 + # 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: "[XAC] %player% failed %check% (VL: %vl%)" + + # Staff-only alert format + staff_format: "[%time%] %message%" + +# ========================================== +# COMMANDS +# ========================================== + +commands: + # Permission required for admin commands + reload_permission: "xac.admin" + # Permission to bypass all checks + bypass_permission: "xac.bypass" + # Permission to receive alerts + alerts_permission: "xac.alerts" + +# ========================================== +# TPS COMPENSATION +# ========================================== + +tps: + # Enable TPS-based threshold scaling + enabled: true + # Minimum TPS to apply compensation + min_tps_threshold: 18.0 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..087ec92 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,185 @@ +name: XeroAntiCheat +version: 1.0.7 +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 + 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