Frostyh

SentinelAC

25 checks, an AI-assisted combat score that learns on your server, and evidence on every flag.

Price
$13.99
Runs on
Paper 1.21.11+, Java 21
Version
v2.0.2

Buy on BuiltByBitTry it liveRead the docs

SentinelAC cover

In game

See it on a real server

5 screenshots, all taken on a real server. Click a screenshot to see it full size.

SentinelAC: reach alerts
SentinelAC: alert hover
SentinelAC: profile ai breakdown
SentinelAC: staff panel
SentinelAC: replays

01 / 05

Overview

A second pair of eyes for your staff that shows exactly what it saw, without kicking your legit players

SentinelAC 2.0 runs 25 checks across combat, movement, world and player actions, and adds an AI-assisted combat score: a small statistical model that learns what normal fighting looks like on YOUR server and scores every fight against it. Every flag comes with its evidence (the numbers it measured, the score, the last rotations and a replay), so staff decide on facts.

Out of the box it only alerts. When you do switch actions on, certain-only mode lets only flags that a normal client cannot produce on any connection run a command, and only after several separate flags on a healthy server.

In short

  • 25 checks across combat, movement, world and player actions
  • An AI-assisted combat score that learns from your own players, on your own server
  • Evidence on every flag: the numbers, the score, the last rotations and a replay
  • Alert-only by default, and a certain-only mode for when you turn actions on
  • No dependencies, no cloud service
Latest version
2.0.2
Runs on
Paper or Purpur 1.21.11 (or a newer 1.21.x), Java 21
What changed in 2.0.2
  • Fixed: mining no longer flags AutoClicker. Holding the button on a block swings the arm every tick at a perfectly even rhythm, and once the dig took longer than a moment those swings were counted as clicks (about 16 to 20 cps with almost no spread), which read as a machine.
  • Only real clicks are counted now: swings that hit a player or mob, and swings into the air. A swing at a block within reach is mining, including blocks the player is not allowed to break (spawn protection, adventure mode, region plugins).
  • Autoclickers are still caught: a 50 ms clicker into the air or at a mob, and a randomised 60 to 90 ms one, still flag in our tests.
  • Nothing to change in config.yml.
  • Thanks to peterspeeder for the report.

Features

  1. 01What makes it different

    • The AI-assisted score is computed on your server: no cloud service and no network call, and what it learns stays in plugins/SentinelAC/ai-model.yml
    • Every flag says whether it is CERTAIN (something a normal client cannot do on any connection) or HEURISTIC, and certain-only mode keeps the second kind away from your punishments
    • Reach is lag-compensated: measured against every position the target held inside both players' latency window, so a laggy hit on a sprinting player is not counted as reach
  2. 02New in v2.0.0

    • AI-assisted detection: while a player fights, SentinelAC measures camera turns, click timing, hit distance and angle, hits per swing, target switches and movement beyond the prediction. Every 20 hits these become 15 measurements, compared with what is normal on your server and combined into a score from 0 to 100%. One odd number never reaches the alert line on its own; several have to agree.
    • Training on your server: /sac ai train on while only trusted players play, off after a few hundred fights; it keeps learning slowly from clean players, and /sac ai label <player> cheater|legit teaches it from a confirmed case. It never runs a command on its own unless you set ai.may-punish: true.
    • 18 new checks: killaura, criticals, velocity, hitbox, step, jesus, phase, noslow, elytra, vehicle, badpackets, fastbreak, nuker, scaffold, fastplace, ghosthand, fastuse and inventory
    • Stronger old checks: lag-compensated reach with an "impossible" tier that refuses the hit, a per-tick movement prediction in speed, client tick packets in timer, ground spoofing in nofall, and timing shape in autoclicker
    • Certain-only actions: only CERTAIN flags can run an action, after several separate flags spread over time, with no ping spike, teleport or join just before; a held action tells staff why
    • Evidence and profiles: /sac evidence <player> lists the latest flags with the numbers, the AI score and the last rotations, the recording one click away; /sac profile <player> shows the score feature by feature, every level, client brand and connection
    • Staff controls: /sac alerts off|certain|normal|verbose|debug, /sac debug <player>, /sac testmode (checks run and alert, nothing is cancelled or punished), /sac timings, and punishment ladders per check with /sac pardon
  3. 03Checks

    Combat (7)

    • reach: lag-compensated distance against the player's interaction range attribute, with an "impossible" tier and a "pattern" tier
    • killaura: two targets in one client tick, hits on someone behind you, hits far off aim, attacks without an arm swing, silent rotations
    • aim: impossible pitch, and snap-and-stop turns a mouse does not make
    • autoclicker: clicks per second above a hand's, machine-like rhythm, and the shape of the click timing
    • criticals, velocity (anti-knockback) and hitbox (expanders)

    Movement (11)

    • fly: measured against gravity itself, so hovering, rising and slow gliding are all caught
    • speed: the sprint-jump peak plus a per-tick movement prediction, and sprinting backwards
    • nofall: a real drop with no damage, and a client claiming ground while falling
    • timer: movement and tick packets against the clock
    • step, jesus (walking on water or lava), phase, noslow, elytra (height and speed without a boost), vehicle (boat and mount speed, boat fly) and badpackets (several moves in one tick, blink, sprinting while starving)

    World (5)

    • fastbreak, nuker, scaffold (and tower), fastplace and ghosthand (breaking or opening through walls)

    Player (2)

    • fastuse and inventory (moving while clicking in an inventory, chest stealers)

    On top of these, several high AI scores within ten minutes raise the separate ai check, which is always HEURISTIC.

  4. 04Why it does not flag your legit players

    • Nothing is judged above your ping limit, below your TPS limit, for 3 seconds after a join, teleport, respawn or world change, during a ping spike, or right after the server itself froze
    • Knockback, explosions and plugin velocity give a grace period; so do taking damage, leaving a vehicle and the momentum after an elytra glide or a riptide
    • Elytra, riptide, levitation, slow falling, vehicles, creative, spectator, flight permission, water, lava, ladders, vines, webs, scaffolding, bubble columns, trapdoors, fence gates, pistons, beds and slime are exempt before a movement check runs
    • Most checks fill a buffer that drains over time and flag only when it is full; movement checks can set the player back first and world checks can refuse the block first
    • Players joining through Geyser and Floodgate are skipped by the checks listed in gate.bedrock-skips, because their aim, clicks and ticks come from Geyser, not from a Java client
    • Cancelled events (another plugin blocked the hit or the move) are never counted, and staff can exempt a player on the spot
  5. 05What you get

    • Violation levels per player per check, decaying every second at a rate you set
    • Alerts with the detail, the AI score, the confidence, the player's ping and the server TPS; hover for the numbers, click for the replay; each staff member picks their own alert level
    • Discord alerts through a channel webhook, rate limited per player, with the AI score and the confidence
    • Actions per check at any level, or punishment ladders that remember earlier offences; all off by default, with a repeat cooldown
    • Every threshold, weight and message in the config; reload without a restart
    • No packet library, no NMS: built only on the Paper API, so it survives Minecraft updates
    • The AI scoring runs on its own background thread; /sac timings shows what each check costs per tick
  6. 06Replay

    • The seconds before the flag, played back: when a check flags somebody, Sentinel keeps the movement it just saw and offers it to staff as a recording
    • Watched in the world, not in a log: a stand-in wearing the suspect's skin walks the exact path that was recorded, where it happened, with the path drawn in particles
    • Only you see it: the stand-in is hidden from every other player
    • The numbers travel with it: the label over its head shows the check, the level, the ping and whether they were on the ground at that moment
    • Kept in memory only: recordings stay for the minutes you configure and then go
    • /sac replay <player> plays the last one, or records the last seconds right now if nothing was flagged
  7. 07Staff mode

    • Your own things stay yours: the inventory you had before staff mode is written to disk, so a restart in the middle of a moderation session does not cost you your gear
    • A hotbar that does the job: inspect, freeze, jump to a random player, vanish and leave, each named in the config
    • Vanish that holds: hidden from everybody without the see permission, and players who join while you are vanished are not shown you either
    • Freeze: they can look around and answer you, they cannot walk, fight or run commands except the ones you allow
    • In staff mode you cannot break, place or drop anything by accident
  8. 08Inventory audit

    • "Who has 400 netherite blocks?": /sac find netherite_block answers it for every player online, biggest holding first
    • It looks where things are actually hidden: inventory, armour, offhand, ender chest, and inside every shulker box in either
    • It says where: 64 carried, 320 in the ender chest, 900 in shulkers
    • Straight to the player: a button opens a read-only copy of their inventory, another teleports you to them
  9. 09The staff panel

    • /sac on its own opens a real Minecraft dialog window: who is flagging right now, worst level first, with ping and the check that fired
    • Click a player for a card of every check with their level on it, and reset or exempt them from there
    • A checks screen switches a single check on or off for the server, written to config.yml
    • The switch that turns the punishment actions on says plainly that they then run by themselves; it stays off until somebody decides otherwise

Installation

Requirements

Server
Paper, or a Paper fork such as Purpur. Spigot and CraftBukkit won't work, because it uses Paper-only APIs.
Minecraft
1.21.11 (or a newer 1.21.x); plugin.yml api-version 1.21; built against Paper API 1.21.11
Java
21 or newer
Needs
No other plugin
Works with
Nothing optional

The listing also says:

  • No other plugins required
  • Discord alerts through a channel webhook, optional
  • No DRM, no licence key and no licence server; the code is not obfuscated

Steps

  1. Download SentinelAC.jar from BuiltByBit. There's no licence key to enter.
  2. Stop the server and put the jar in the plugins folder.
  3. Start the server. The plugin writes plugins/SentinelAC/config.yml with the defaults listed under Configuration.
  4. Change config.yml to fit your server.
  5. Run /sac reload (reload the config) to apply your changes, or restart the server.

Setup guide

  1. Drop SentinelAC.jar into /plugins and restart. Give staff sentinelac.alerts.
  2. Run /sac ai train on while only trusted players are fighting, and /sac ai train off once /sac ai status shows a few hundred fights.
  3. Play normally for a few days and watch the alerts. If a legit player keeps tripping one check, raise that check's tolerance or alert-at.
  4. When the alerts match what your staff would have said anyway, set actions.enabled to true and put your ban or kick command under the levels you want. Keep certain-only on.

SMP: fly, speed, nofall, nuker and the world checks matter most; leave actions off and let staff spectate on alert.

Duel / FFA / KitPvP: reach, killaura, autoclicker, aim and the AI score are the ones that decide fights. A temp ban is still a human decision.

Networks: run it on the game servers, not the lobby, and put the server name into discord.server-name so staff know where to jump.

Commands

Straight from plugin.yml. Where the permission column says "not set", plugin.yml puts no permission on the command itself; the nodes the plugin checks are under Permissions.

CommandAliasesUsageWhat it doesPermission
/sac/sentinel /sentinelac/sac alerts|vl|profile|evidence|ai|debug|testmode|timings|replay|replays|staff|freeze|inv|ec|find|reset|pardon|exempt|checks|testalert|reloadSentinelAC staff commands.not set

Every subcommand

/sac alerts [off|certain|normal|verbose|debug]   Your own alert level.
/sac vl [player]         Current violation levels of a player, with their ping.
/sac profile <player>    The AI score feature by feature, every level, evidence, client and connection.
/sac evidence <player>   The latest flags with the numbers, the AI score and the rotations.
/sac ai status|train on|off|label <player> cheater|legit|reset|save   (admin) The AI-assisted layer.
/sac debug <player>      (admin) Every buffer step and dropped flag of one player.
/sac testmode            (admin) Checks run and alert, nothing is cancelled, set back or run.
/sac timings             (admin) What each check costs per tick.
/sac replay <player> [n] Watch the seconds before their flag, where it happened.
/sac replay stop · /sac replays
/sac staff               Staff mode: vanish, tools, your gear kept safe.
/sac freeze <player>     Hold somebody where they stand.
/sac inv <player> · /sac ec <player>   Read-only copies of inventory and ender chest.
/sac find <item> [name]  Who is holding it, and where they keep it.
/sac reset <player>      (admin) Clear a player's levels.
/sac pardon <player>     (admin) Clear their punishment ladder.
/sac exempt <player>     (admin) Toggle exemption for a player.
/sac checks              Which checks are on, and whether actions are enabled.
/sac testalert           (admin) Send a sample alert, and one to Discord.
/sac reload              (admin) Reload the config.

Permissions

From plugin.yml.

NodeDefaultWhat it allowsIncludes
sentinelac.alertsoperatorsSee alerts and use /sac alerts and /sac vl.none
sentinelac.adminoperatorsReset, pardon, exempt, debug, testmode, timings, AI training and labels, testalert, reload.none
sentinelac.staffoperatorsStaff mode, freeze, inventory inspection and the item audit.none
sentinelac.staff.seeoperatorsSee staff who are vanished.none
sentinelac.bypassnobody until grantedNever checked.none

Placeholders

This plugin doesn't add any PlaceholderAPI placeholders.

Configuration

The default files exactly as they ship in the jar, comments included. The plugin writes them to plugins/SentinelAC/ the first time it starts.

config.yml

KeyDefaultThe comment above it
test-modefalseTest mode: every check runs and alerts as normal, but nothing is cancelled, nobody is set back and no action command runs (the console says what would have run). /sac testmode toggles it.
timingssection, 1 keyA timing log of the main-thread cost of each part of the plugin, in the console every this many minutes (0 = only when you ask with /sac timings).
menutrueBare /sac opens the staff panel instead of printing the help.
replaysection, 11 keysReplay: the seconds before a flag, played back to a staff member as a stand-in wearing the suspect's skin walking the exact path that was recorded. Nothing is written to disk and only the watching staff member sees any of it.
staffsection, 2 keysStaff mode: vanish, a hotbar of tools, and the staff member's own inventory kept safe on disk so a restart in the middle of a session does not cost them their gear.
menu-hides-clean-playerstrueThe panel lists only players with a violation level. Off lists everyone online.
gatesection, 7 keysnone
movementsection, 4 keysnone
alertssection, 4 keysnone
discordsection, 6 keysAlerts in a Discord channel, for when no staff are online. Make a webhook in the channel's settings (Integrations > Webhooks), paste its address here and /sac reload. Empty = off, and then nothing is ever sent anywhere. /sac testalert sends one post to...
actionssection, 4 keysnone
aisection, 13 keysA statistical model of how YOUR players fight, running on your server. No data leaves it: there is no cloud service and no network call. How it works, plainly: 1. While a player fights, it records numbers the server can see: how their camera turns every
checkssection, 26 keysnone
themesection, 11 keysEvery message below is written against these names, not against hex codes: <accent>Open</accent> rather than <#ef5350>Open</#ef5350>. Change a colour here and it changes on every screen at once. Ordinary MiniMessage still works, so <gradient:#a:#b> and...
messagessection, 169 keysnone
review-remindertrueOne thank-you line with the BuiltByBit review link: once in the console after install and once in chat for the first operator who joins. It never repeats and sends nothing anywhere. false turns it off.
plugins/SentinelAC/config.yml724 lines
# SentinelAC - an honest anticheat.
#
# Every check adds to a violation level (VL) that decays every second. At alert-at, staff with
# sentinelac.alerts see a message. Commands under actions run when a VL is reached, but ONLY when
# actions.enabled is true. Out of the box nothing is punished automatically: watch the alerts on
# your own server for a few days, then decide what you trust.
#
# Ping, low TPS, a frozen server, teleports, knockback, elytra, riptide, water, ladders, vehicles,
# creative and (for the checks listed in gate.bedrock-skips) Bedrock players are exempt before any
# check runs. Staff can toggle a player with /sac exempt <player>; sentinelac.bypass skips everything.
#
# Every flag has a confidence. CERTAIN means a vanilla client cannot produce it on any connection
# (two targets hit inside one client tick, a hit on someone behind every look you sent, a reach no
# ping explains, a client clock forty seconds fast). HEURISTIC is everything a bad connection, a mod
# or an unusual player could also produce. With actions.certain-only only CERTAIN flags act.
#
# No anticheat catches every client. This one aims for the best coverage it can get while keeping
# false flags very rare, which is why so much of it alerts and so little of it acts.

# Test mode: every check runs and alerts as normal, but nothing is cancelled, nobody is set back and
# no action command runs (the console says what would have run). /sac testmode toggles it.
test-mode: false

# A timing log of the main-thread cost of each part of the plugin, in the console every this many
# minutes (0 = only when you ask with /sac timings).
timings:
  log-every-minutes: 0

# Bare /sac opens the staff panel instead of printing the help.
menu: true

# Replay: the seconds before a flag, played back to a staff member as a stand-in wearing the
# suspect's skin walking the exact path that was recorded. Nothing is written to disk and only the
# watching staff member sees any of it.
replay:
  enabled: true
  seconds: 20              # how much movement is kept per player
  sample-every-ticks: 2    # 2 = ten samples a second
  record-at-vl: 5          # a flag at or above this level saves a recording
  min-seconds-between: 20  # do not save a second recording of the same player within this
  keep-per-player: 3
  keep-minutes: 20         # recordings older than this are dropped
  speed: 1.0               # 0.5 is half speed, 2.0 is double
  trail: true              # draw the path in particles for the watcher
  teleport-viewer: true    # put the staff member beside the start of the recording
  tell-staff: true         # say in chat when a recording was saved

# Staff mode: vanish, a hotbar of tools, and the staff member's own inventory kept safe on disk so a
# restart in the middle of a session does not cost them their gear.
staff:
  vanish-on-enter: true
  # Commands a frozen player may still run, so they can answer you.
  frozen-may-use: [msg, r, reply, tell, w, discord]
# The panel lists only players with a violation level. Off lists everyone online.
menu-hides-clean-players: true

gate:
  max-ping: 300        # above this, nothing flags
  min-tps: 17.5        # below this, nothing flags
  grace-ms: 3000       # after join, teleport, respawn or world change
  timing-max-ping: 250 # above this, timing checks (click rhythm, packet order) stand down
  ping-spike-ms: 150   # a ping jump this big marks the connection unsteady for a few seconds
  stall-grace-ms: 4000 # after the server itself froze for over a second, nothing flags for this long
  # Bedrock players (Geyser + Floodgate, found by their Floodgate UUID or the Floodgate API) are not
  # judged by these checks: their aim, clicks and ticks come from Geyser, not from a Java client.
  # "all" skips every check for them.
  bedrock-skips: [aim, killaura, hitbox, autoclicker, ai, badpackets, timer, criticals, velocity, inventory, scaffold, noslow, speed, elytra]

movement:
  velocity-grace-ms: 2500   # after knockback / explosion / plugin velocity
  damage-grace-ms: 1500
  aerial-grace-ms: 2500     # after an elytra glide or a riptide ends, the momentum keeps going
  vehicle-grace-ms: 1500    # after leaving a vehicle

alerts:
  console: true
  console-every-flag: false # every flag in the console, not only those past alert-at (on in test mode)
  min-interval-ms: 1000     # per player, so a run of flags is one line not twenty
  # Staff pick their own level with /sac alerts <level>: off, certain (only CERTAIN flags), normal
  # (past alert-at, the default), verbose (every flag) or debug (verbose plus every AI-scored fight).
  # What clicking an alert line in chat runs, as the staff member who clicked. {player} and {check}
  # are filled in. The default plays back the seconds before the flag. "/tp {player}" or
  # "/sac vl {player}" work too; "" makes alerts plain text. Hovering always shows the numbers.
  click-command: "/sac replay {player}"

# Alerts in a Discord channel, for when no staff are online. Make a webhook in the channel's
# settings (Integrations > Webhooks), paste its address here and /sac reload. Empty = off, and then
# nothing is ever sent anywhere. /sac testalert sends one post to check it.
discord:
  webhook-url: ""
  per-player-seconds: 30    # one post per player per this many seconds, so a fight is one message
  actions: true             # also post when a configured action (kick, ban command) runs
  server-name: ""           # shown on every post, for networks with several servers
  alert-title: "{player} failed {check} (VL {vl})"
  action-title: "Action taken: {player} reached {check} VL {vl}"

actions:
  enabled: false
  repeat-after-ms: 60000    # an action fires again only after this, even if the VL stays high
  # Certain only: only CERTAIN flags can run an action, the level compared is the one built from
  # CERTAIN flags alone, and the gate below must agree. Recommended. (A config.yml from before 2.0
  # has no such line and keeps the old behaviour, where any flag counts.)
  certain-only: true
  gate:
    min-flags: 3            # this many separate certain flags of the same check...
    min-spread-seconds: 3   # ...spread over at least this long, not one burst...
    window-seconds: 120     # ...inside this window
    min-tps: 18.5           # and the server healthy
    quiet-seconds: 10       # and no ping spike, teleport or join in this many seconds
  # Besides the "actions" map on each check (a command at a level), each check can have a
  # punishment ladder: the first time it is reached the first line runs, the second time the second,
  # and so on (remembered in offences.yml; /sac pardon <player> clears it). Several commands on one
  # rung are joined with " && ". Example, under checks.reach:
  #   punish:
  #     at-vl: 10
  #     ladder:
  #       - "kick {player} Unfair advantage ({check})"
  #       - "tempban {player} 7d Unfair advantage ({check}) && broadcast {player} was removed"
  #       - "ban {player} Unfair advantage ({check})"
  # Placeholders: {player} {uuid} {check} {vl} {detail} {ai}

# ------------------------------------------------------------------------------------------------
# AI-ASSISTED DETECTION
# ------------------------------------------------------------------------------------------------
# A statistical model of how YOUR players fight, running on your server. No data leaves it: there
# is no cloud service and no network call.
#
# How it works, plainly:
#  1. While a player fights, it records numbers the server can see: how their camera turns every
#     tick (steps of a mouse, smooth curves, dead stops after a snap), the timing of every click
#     (how even, whether there are the pauses and doubles a hand makes), the distance and angle of
#     every hit, how often a swing hits, how often the target switches, and how far their movement
#     goes beyond the movement prediction.
#  2. After twenty hits (or at the end of a fight with ten) those numbers become fifteen features
#     and are compared with what is normal ON THIS SERVER: the median and spread of each feature,
#     learned from fights of players who were not flagged. Only the suspicious direction counts
#     (very even clicks count, messy ones do not) and no feature can count more than six spreads.
#  3. A small logistic model with shipped weights combines them into a score from 0 to 100%. It is
#     tuned so that ONE strange number, however strange, stays under the line: it takes several
#     measurements agreeing. The line is calibrated: once 200 clean fights have been seen it is
#     raised to the score only target-false-alarm of your own clean fights reach.
#  4. Several high fights within period-minutes raise the "ai" check (always HEURISTIC). The score
#     is in every alert, every piece of evidence, /sac profile and the Discord posts.
# It never acts on its own unless may-punish is true.
#
# Training: /sac ai train on while only trusted players play (an event, staff, regulars) makes it
# learn every fight as normal and raise no AI flags. /sac ai train off when you have a few hundred
# fights (/sac ai status shows the count). It keeps learning slowly from clean players afterwards.
# Labels: /sac ai label <player> cheater (after you have confirmed one) teaches it from their recent
# fights; "legit" does the opposite. /sac ai reset starts again from the shipped baselines.
ai:
  enabled: true
  training: false
  may-punish: false          # true lets the "ai" check run its actions (actions.enabled still needed)
  window-hits: 20            # a fight is scored every this many hits
  min-hits: 10               # a fight that ends with fewer is not scored
  flag-score: 0.90           # never alert below this score, however the calibration comes out
  target-false-alarm: 0.005  # the calibrated line: half a percent of clean fights may reach it
  windows-needed: 2          # this many fights over the line...
  period-minutes: 10         # ...within this long raise the "ai" check
  learn: true                # keep learning from clean fights after training
  clean-minutes: 10          # a player with any flag in this many minutes is not learned from
  prior-strength: 30         # how many fights the shipped baseline is worth against what is learned
  # Optional: override the weight of a feature (0 turns it off). Keys: click_cv, click_kurtosis,
  # click_outliers, click_entropy, gcd_off, rot_jerk, snap_stop, aim_error, aim_error_sd, aim_lock,
  # accuracy, reach_p90, snap_hit, switch_rate, move_residual. Example: gcd_off: 0
  weights: {}

checks:
  # Flight is measured against gravity itself. Airborne vertical speed in Minecraft is
  # v = (v - 0.08) * 0.98 every tick, with no exception for a player in open air, so it has to fall
  # away tick after tick. Hovering holds it at zero, ascending holds it positive and a slow glide
  # holds it just under zero: all three are the same violation and all three are caught.
  #
  # A jump, a wind charge, an explosion and a crystal all decay exactly as the formula says, so a
  # launch is one violating move and the move after it clears the streak. A stalled connection
  # delivers several ticks of falling as one move, which looks like falling FASTER, never like being
  # held up, so lag cannot manufacture this.
  fly:
    enabled: true
    gravity-streak: 10        # consecutive moves with no gravity: half a second of flight
    gravity-window: 60        # and the window the second tier measures over
    gravity-window-hits: 50   # how much of that window has to be gravity-defying, for a client that bobs
    weight-gravity: 4.0
    alert-at: 6
    decay-per-second: 0.5
    actions:
      "20": ["kick {player} Unfair advantage ({check})"]
  # Two tiers. The peak: horizontal distance per move against a sprint-jump with your speed
  # attribute and effects. The prediction (2.0): per client tick, movement is last tick's times the
  # friction of the block plus at most one tick of acceleration (plus 0.2 on a sprint-jump), so any
  # movement beyond the larger of the ground and air answers is speed the game did not give. That
  # catches strafe and low-hop modules that stay under the peak. Also sprinting backwards.
  speed:
    enabled: true
    base-per-tick: 0.62     # sprint-jump peak on flat ground at speed 0.1
    tolerance: 1.15
    ice-multiplier: 1.8
    streak: 6               # consecutive too-fast ticks before a flag
    weight: 1.0
    prediction-threshold: 0.04  # blocks per tick beyond the prediction that count as suspicious
    prediction-slack: 0.02
    weight-prediction: 1.0
    omni-ticks: 10              # ticks of sprinting away from where you look
    weight-omni: 1.0
    buffer: 12                  # suspicious ticks needed (the buffer drains by buffer-decay a second)
    buffer-decay: 4
    setback: false
    alert-at: 6
    decay-per-second: 0.5
    actions:
      "25": ["kick {player} Unfair advantage ({check})"]
  # Landing after a real drop with no fall damage, and (2.0) a client claiming to stand on the
  # ground while falling with nothing under it, which is how most nofall modules work.
  nofall:
    enabled: true
    min-drop: 4.0
    weight: 2.0
    weight-spoof: 1.5
    buffer: 3
    buffer-decay: 0.5
    alert-at: 4
    decay-per-second: 0.25
    actions: {}
  # Lag-compensated (2.0): measured against every position the victim held inside both players'
  # latency window, so a laggy hit on a sprinting target is not charged as reach. Three tiers:
  # past range + tolerance (heuristic), past what both pings can explain + impossible-margin
  # (CERTAIN, and the hit is refused), and half of the last thirty hits past 3.05 (pattern).
  reach:
    enabled: true
    tolerance: 0.35         # on top of the player's entity interaction range (3.0 vanilla)
    high-ping-bonus: 0.25   # extra when ping is above 100
    weight: 1.0
    impossible-margin: 1.0
    cancel-impossible: true
    pattern: true
    pattern-share: 0.5
    weight-pattern: 2.0
    alert-at: 5
    decay-per-second: 0.3
    actions:
      "20": ["kick {player} Unfair advantage ({check})"]
  # Too many clicks, too even a rhythm, and (2.0) the wrong shape: forty click gaps with no human
  # tails (negative kurtosis, not one outlier) at a real clicking pace, which is what randomised
  # autoclickers produce.
  autoclicker:
    enabled: true
    max-cps: 22
    rhythm-samples: 16
    rhythm-max-deviation-ms: 4.0
    weight-cps: 1.0
    weight-rhythm: 1.5
    stats-samples: 40
    stats-min-cps: 8
    stats-max-kurtosis: -0.9
    stats-max-outliers: 0
    weight-stats: 1.5
    alert-at: 5
    decay-per-second: 0.3
    actions: {}
  aim:
    enabled: true
    window: 40
    snap-degrees: 25.0
    snaps: 4
    weight-snap: 1.0
    weight-invalid: 5.0
    alert-at: 5
    decay-per-second: 0.3
    actions: {}
  # Moves per second over a window, and (2.0) the balance of the client's own tick-end packets
  # against the clock: a client running fast drifts ahead and keeps drifting, a lag spike only
  # bunches ticks up. Past balance-certain-ms (longer than any connection survives a stall) it is
  # CERTAIN.
  timer:
    enabled: true
    window-seconds: 4
    max-ratio: 1.12
    weight: 1.0
    balance-alert-ms: 5000
    balance-certain-ms: 40000
    balance-step-ms: 2000
    weight-balance-certain: 3.5
    alert-at: 4
    decay-per-second: 0.3
    actions:
      "15": ["kick {player} Unfair advantage ({check})"]

  # ---- 2.0: combat ------------------------------------------------------------------------------
  # Two entities attacked inside one client tick (CERTAIN), a hit on someone behind every look
  # direction sent in the latency window (CERTAIN), a hit far off aim, an attack without an arm
  # swing, and silent rotations (the look jumps to the target for the attack's tick and back).
  killaura:
    enabled: true
    suspicious-degrees: 70.0
    impossible-degrees: 95.0
    weight-multi: 3.0
    weight-noswing: 1.0
    silent-snap-degrees: 20.0
    silent-return-degrees: 1.0
    silent-count: 3
    weight-silent: 2.0
    buffer: 4
    buffer-decay: 0.5
    alert-at: 3
    decay-per-second: 0.3
    actions: {}
  # A critical hit from a player who has not gone up or down for six client ticks (standing on
  # flat ground, where no critical exists). Uses positions, not the client's on-ground flag.
  criticals:
    enabled: true
    still-ticks: 6
    weight: 2.0
    buffer: 2
    buffer-decay: 0.2
    alert-at: 4
    decay-per-second: 0.3
    actions: {}
  # Anti-knockback: on the tick a knockback lands, a vanilla client moves along it by most of its
  # strength. Walls, water, webs and crowds excuse it.
  velocity:
    enabled: true
    min-strength: 0.25
    min-taken: 0.35
    weight: 1.5
    buffer: 3
    buffer-decay: 0.1
    alert-at: 3
    decay-per-second: 0.2
    actions: {}
  # Hitbox expanders: forty swings whose median passes more than 0.22 blocks outside the target.
  hitbox:
    enabled: true
    sample: 40
    median-miss: 0.22
    miss-share: 0.65
    weight: 3.0
    alert-at: 3
    decay-per-second: 0.1
    actions: {}

  # ---- 2.0: movement -----------------------------------------------------------------------------
  # setback: true puts the player back where every movement check last agreed with them as soon as
  # the buffer is half full, before any flag. An honest player gets one rubber band at worst.
  step:
    enabled: true
    tolerance: 0.05
    weight: 2.0
    buffer: 2
    buffer-decay: 0.2
    setback: true
    alert-at: 4
    decay-per-second: 0.3
    actions: {}
  jesus:
    enabled: true
    moves: 10
    weight: 2.0
    buffer: 2
    buffer-decay: 0.2
    setback: true
    alert-at: 4
    decay-per-second: 0.3
    actions: {}
  phase:
    enabled: true
    weight: 2.0
    buffer: 2
    buffer-decay: 0.2
    setback: true
    alert-at: 4
    decay-per-second: 0.3
    actions: {}
  noslow:
    enabled: true
    grace-ms: 350
    max-speed: 0.13
    ticks: 7
    weight: 1.0
    buffer: 3
    buffer-decay: 0.3
    setback: false
    alert-at: 3
    decay-per-second: 0.3
    actions: {}
  # Gliding that holds its height and speed for two seconds without a firework, wind charge or
  # riptide (the glide physics always pays with one or the other), or faster than 4 blocks a tick.
  elytra:
    enabled: true
    window-ticks: 40
    min-drop: 1.0
    max-speed: 4.0
    boost-grace-ms: 3500
    weight: 2.0
    buffer: 2
    buffer-decay: 0.1
    setback: false
    alert-at: 4
    decay-per-second: 0.3
    actions: {}
  vehicle:
    enabled: true
    max-speed-boat: 1.2
    max-speed-boat-ice: 4.2
    max-speed-mount: 1.4
    fly-moves: 10
    weight: 1.5
    buffer: 5
    buffer-decay: 1.0
    alert-at: 3
    decay-per-second: 0.3
    actions: {}
  # Several movement packets inside one client tick, blink (a silence then a burst of held ticks),
  # and sprinting with six food points or fewer.
  badpackets:
    enabled: true
    weight: 1.0
    weight-blink: 0.5
    blink-gap-ms: 1000
    blink-burst: 15
    hungry-sprint-moves: 20
    buffer: 4
    buffer-decay: 0.5
    alert-at: 4
    decay-per-second: 0.3
    actions: {}

  # ---- 2.0: world --------------------------------------------------------------------------------
  # cancel: true refuses the block break or placement first; flags come only past the buffer.
  fastbreak:
    enabled: true
    ratio: 0.9              # under this share of the game's own break time...
    latency-ms: 50          # ...minus this, is too fast
    weight: 1.0
    buffer: 3
    buffer-decay: 0.1
    cancel: true
    alert-at: 3
    decay-per-second: 0.2
    actions: {}
  nuker:
    enabled: true
    max-per-second: 22
    max-angle: 100.0
    weight: 2.0
    buffer: 3
    buffer-decay: 0.5
    cancel: true
    alert-at: 4
    decay-per-second: 0.3
    actions: {}
  scaffold:
    enabled: true
    face-tolerance: 0.4
    look-margin: 0.25
    tower-min-ms: 300
    bridge-max-pitch: 40.0
    bridge-blocks: 6
    weight: 1.5
    buffer: 3
    buffer-decay: 0.2
    cancel: true
    alert-at: 3
    decay-per-second: 0.3
    actions: {}
  fastplace:
    enabled: true
    max-per-second: 24
    weight: 1.0
    buffer: 3
    buffer-decay: 0.5
    cancel: true
    alert-at: 3
    decay-per-second: 0.3
    actions: {}
  ghosthand:
    enabled: true
    weight: 1.5
    buffer: 3
    buffer-decay: 0.05
    cancel: true
    alert-at: 3
    decay-per-second: 0.1
    actions: {}

  # ---- 2.0: player -------------------------------------------------------------------------------
  fastuse:
    enabled: true
    ratio: 0.75
    latency-ms: 100
    bow-min-ms: 650
    weight: 1.5
    buffer: 2
    buffer-decay: 0.05
    alert-at: 3
    decay-per-second: 0.1
    actions: {}
  # Inventory move (clicking slots while the client still reports a movement key held: a screen
  # always releases them; also what auto-armour does while running) and chest stealers.
  inventory:
    enabled: true
    move-held-ms: 400
    first-click-ms: 40
    steal-clicks: 6
    steal-max-gap-ms: 70
    steal-max-deviation-ms: 4.0
    weight: 1.5
    buffer: 2
    buffer-decay: 0.05
    alert-at: 3
    decay-per-second: 0.1
    actions: {}
  # The AI-assisted layer as a check (see the ai section above). Always heuristic.
  ai:
    enabled: true
    weight: 2.0
    alert-at: 2
    decay-per-second: 0.02
    actions: {}

# ------------------------------------------------------------------------------------------------
# THEME
# ------------------------------------------------------------------------------------------------
# Every message below is written against these names, not against hex codes: <accent>Open</accent>
# rather than <#ef5350>Open</#ef5350>. Change a colour here and it changes on every screen at
# once. Ordinary MiniMessage still works, so <gradient:#a:#b> and <#ff0000> are fine anywhere.
theme:
  accent: "#ef5350"     # your colour: headings and anything the player is meant to click
  accent2: "#f6a09f"    # a lighter accent, for a second level of emphasis
  title: "#f4f7ff"      # screen titles
  text: "#c2ccdd"       # ordinary prose
  muted: "#6b7a94"      # hints, separators, anything the eye should skip
  value: "#ffffff"      # the numbers and names the player opened the screen to read
  good: "#57e08a"
  warn: "#ffc857"
  bad: "#ff6b6b"
  small-caps: false     # ᴇхᴀᴍᴘʟᴇ - a garnish; whole menus in it read badly
  # Real Minecraft item pictures beside the buttons and in the chest screens. Drawn from the
  # client's own textures, so there is nothing to install and no resource pack: it needs a
  # 1.21.9 or newer client, and older ones simply see the buttons as words.
  item-icons: true

messages:
  prefix: "<#c0392b><bold>Sentinel</bold></#c0392b> <muted>»</muted> "
  alert: "<value>{player}</value><text> failed </text><#e74c3c>{check}</#e74c3c><text> (VL {vl}, {confidence}) </text><muted>{detail} · {ping}ms · {tps} tps · {ai}</muted>"
  # Added after the alert line of a config.yml from before 2.0, which has no {ai} in it.
  alert-ai-suffix: "<muted> · {ai}</muted>"
  # Hovering over an alert line in chat. The second one is used when alerts.click-command is set.
  alert-hover: "<value>{player}</value><text> · {check} · VL {vl}</text><newline><muted>{detail}</muted><newline><text>ping {ping}ms · server {tps} tps</text>"
  alert-hover-click: "<value>{player}</value><text> · {check} · VL {vl}</text><newline><muted>{detail}</muted><newline><text>ping {ping}ms · server {tps} tps</text><newline><newline><accent>Click: {command}</accent>"
  action-held: "<warn>Held an action for </warn><value>{player}</value><warn> ({check}): </warn><text>{reason}.</text>"
  test-would-run: "<warn>[test mode]</warn><text> would run: </text><value>{command}</value>"
  debug-line: "<muted>[debug] {player} {check}: {line}</muted>"
  debug-on: "<text>Debug output for </text><value>{player}</value><text> on. Same command again to stop.</text>"
  debug-off: "<text>Debug output for </text><value>{player}</value><text> off.</text>"
  testmode-on: "<warn>Test mode on.</warn><text> Checks alert, but nothing is cancelled, set back or run.</text>"
  testmode-off: "<good>Test mode off.</good><text> Cancels, setbacks and actions work again.</text>"
  timings-reset: "<text>Timings reset.</text>"
  usage-alerts: "<text>/sac alerts [off|certain|normal|verbose|debug]</text>"
  alerts-level: "<text>Your alerts: </text><value>{level}</value>"
  usage-pardon: "<text>/sac pardon <player> clears their punishment ladder.</text>"
  pardoned: "<text>Punishment ladder of </text><value>{player}</value><text> cleared.</text>"
  usage-ai: "<text>/sac ai status · /sac ai train on|off · /sac ai label <player> cheater|legit · /sac ai reset · /sac ai save</text>"
  ai-training-on: "<warn>AI training on.</warn><text> Every fight is learned as normal play and no AI flags are raised. Turn it off with /sac ai train off.</text>"
  ai-training-off: "<good>AI training off.</good><text> Fights are scored against what was learned.</text>"
  ai-labelled: "<text>Learned from </text><value>{count}</value><text> recent fights of {player} as </text><value>{label}</value><text>.</text>"
  ai-label-none: "<text>{player} has no scored fights yet to learn from.</text>"
  ai-reset: "<text>The AI model is back to the shipped baselines and weights.</text>"
  ai-saved: "<text>The AI model was written to ai-model.yml.</text>"
  ai-window: "<muted>[ai] {player}: {line}</muted>"
  profile-head: "<value>{player}</value><text> · {ping}ms · client {brand} · Bedrock {bedrock} · tick packets {ticks}</text>"
  profile-ai: "<text>AI-assisted score: </text><value>{score}</value><text> latest, {smoothed} smoothed, {windows} fights, alert line {line}</text>"
  profile-ai-none: "<text>AI-assisted score: no fight scored yet ({windows}).</text>"
  profile-feature: "<muted> - </muted><text>{feature} </text><value>{value}</value><muted> (normal {normal} ± {spread}) z {z}, +{points}</muted>"
  profile-levels: "<text>Levels: </text><value>{levels}</value>"
  profile-evidence: "<muted> - {ago}s ago </muted><text>{check} [{confidence}] </text><muted>{detail}</muted>"
  profile-offences: "<text>Punishment ladder: </text><value>{offences}</value>"
  evidence-head: "<text>Evidence for </text><value>{player}</value><text> ({count} kept, newest first). Hover for the numbers, click to watch.</text>"
  evidence-row: "<muted>{ago}s </muted><#e74c3c>{check}</#e74c3c><text> [{confidence}] VL {vl} </text><muted>{detail}</muted>"
  evidence-hover: "<value>{check}</value><text> · {confidence} · +{weight} · VL {vl}</text><newline><muted>{detail}</muted><newline><text>ping {ping}ms · server {tps} tps</text><newline><text>AI-assisted {ai}: {top}</text><newline><muted>last rotations (yaw/pitch per sample): {rotations}</muted><newline>{replay}"
  evidence-replay: "<accent>Click to watch the recording.</accent>"
  evidence-no-replay: "<muted>No recording for this flag.</muted>"
  replay-expired: "<text>That recording is no longer kept.</text>"
  menu-home-ai-on: "<text>AI-assisted detection: </text><value>watching</value><text>, {windows} fights scored, alert line {line}</text>"
  menu-home-ai-training: "<warn>AI-assisted detection is training:</warn><text> {windows} fights so far, no AI flags.</text>"
  menu-home-ai-off: "<text>AI-assisted detection is off.</text>"
  menu-player-ai: "<text>AI-assisted: </text><value>{ai}</value><muted> ({top})</muted>"
  menu-player-clean: "<muted>{count} checks at zero.</muted>"
  menu-profile: "<white><icon:stats></white><value>Profile of {player}</value>"
  menu-profile-tip: "<text>The AI-assisted score feature by feature, the levels and the latest evidence.</text>"
  menu-profile-title: "<value>{player}</value><text> · profile</text>"
  menu-evidence: "<white><icon:search></white><text>Evidence in chat</text>"
  menu-evidence-tip: "<text>Every recent flag with its numbers on hover and its recording on click.</text>"
  menu-debug: "<white><icon:settings></white><text>Debug {player}</text>"
  menu-debug-tip: "<text>Every buffer step and dropped flag of this player in your chat. Click again to stop.</text>"
  testalert-discord: "<text>Test alert shown above and posted to Discord. If it does not arrive, the console says why.</text>"
  testalert-no-discord: "<text>Test alert shown above. Discord is off: set discord.webhook-url in config.yml.</text>"
  help: "<text>/sac alerts [level] · /sac vl [player] · /sac profile <player> · /sac evidence <player> · /sac ai · /sac debug <player> · /sac testmode · /sac timings · /sac replay <player> · /sac replays · /sac staff · /sac freeze <player> · /sac inv <player> · /sac ec <player> · /sac find <item> · /sac reset <player> · /sac pardon <player> · /sac exempt <player> · /sac checks · /sac testalert · /sac reload</text>"
  no-permission: "<bad>You cannot do that.</bad>"
  unknown-player: "<bad>Unknown player: </bad><value>{name}</value>"
  alerts-on: "<value>Alerts on.</value>"
  alerts-off: "<value>Alerts off.</value>"
  vl: "<value>{player}</value><text> ({ping}ms): </text><value>{levels}</value>"
  reset: "<text>Levels of </text><value>{player}</value><text> reset.</text>"
  exempt-on: "<value>{player}</value><text> is now exempt.</text>"
  exempt-off: "<value>{player}</value><text> is checked again.</text>"
  checks: "{checks}  <text>actions: {actions}</text>"
  reloaded: "<value>Config reloaded.</value>"

  # The staff panel. Every line here is a screen title, a button label or a tooltip.
  menu-title: "<value>Sentinel</value>"
  menu-close: "<white><icon:close></white><text>Close</text>"
  menu-back: "<white><icon:back></white><text>Back</text>"
  menu-back-tip: "<text>Back to the screen before.</text>"
  menu-word-on: "<good>on</good>"
  menu-word-off: "<bad>off</bad>"
  menu-home-checks: "<text>Checks running: </text><value>{on} of {all}</value>"
  menu-home-actions-on: "<bad>Actions are on: configured commands run by themselves.</bad>"
  menu-home-actions-off: "<text>Actions are off. Alerts only, nobody is punished automatically.</text>"
  menu-home-flagging: "<text>Players with a violation level: </text><value>{count}</value>"
  menu-players: "<white><icon:players></white><#c0392b>Players</#c0392b>"
  menu-players-tip: "<text>Who is flagging, worst first.</text>"
  menu-players-title: "<value>Players</value>"
  menu-players-clean: "<text>Nobody has flagged anything.</text>"
  menu-player-line: "<value>{player}</value><text> · {check} VL {vl} · {ping}ms</text>"
  menu-player-open: "<white><icon:players></white><value>{player}</value>"
  menu-player-open-tip: "<text>Every check on {player}.</text>"
  menu-player-title: "<value>{player}</value>"
  menu-player-head: "<text>{player} · {ping}ms · server {tps} tps</text>"
  menu-player-check: "<text> - {check}: </text><value>VL {vl}</value><text> ({state})</text>"
  menu-reset: "<white><icon:dot></white><value>Reset levels</value>"
  menu-reset-tip: "<text>Back to zero on every check.</text>"
  menu-exempt-on: "<good>■</good> <value>{player} is exempt</value>"
  menu-exempt-off: "<muted>■</muted> <text>{player} is checked</text>"
  menu-exempt-tip: "<text>Exempt players are never flagged.</text>"
  menu-checks: "<white><icon:check></white><#c0392b>Checks</#c0392b>"
  menu-checks-tip: "<text>Turn a single check on or off.</text>"
  menu-checks-title: "<value>Checks</value>"
  menu-checks-intro: "<text>A check that is off never flags and never acts.</text>"
  menu-check-tip: "<text>Click to switch this check.</text>"
  menu-check-readonly: "<text>Admins only.</text>"
  menu-alerts-on: "<good>■</good> <value>My alerts on</value>"
  menu-alerts-off: "<muted>■</muted> <text>My alerts off</text>"
  menu-alerts-tip: "<text>Whether you see the alert messages.</text>"
  menu-actions-on: "<bad>■</bad> <value>Actions on</value>"
  menu-actions-off: "<muted>■</muted> <text>Actions off</text>"
  menu-actions-tip: "<text>On means the configured commands run by themselves. Watch the alerts first.</text>"
  menu-reload: "<white><icon:reload></white><text>Reload config</text>"
  menu-reload-tip: "<text>Read config.yml again.</text>"
  switch-on: "<white><icon:yes></white><good><bold>ON</bold></good>  <value>{title}</value>"
  switch-off: "<white><icon:no></white><bad><bold>OFF</bold></bad> <text>{title}</text>"

  # Replay.
  usage-replay: "<text>/sac replay <player> [n] · /sac replay stop · /sac replays</text>"
  replay-saved: "<text>Recording kept: </text><value>{player}</value><text> {check}, {seconds}s. </text><value>/sac replays</value>"
  replay-none: "<text>Nothing recorded for </text><value>{player}</value><text>.</text>"
  replay-playing: "<value>{player}</value><text> · {check} · {seconds}s from {ago}s ago. </text><value>/sac replay stop</value>"
  replay-stopped: "<text>Replay stopped.</text>"
  replay-world-gone: "<bad>That recording is from {world}, which is not loaded.</bad>"
  replay-ghost-name: "<text>{player}</text>"
  replay-label: "<#e74c3c><bold>REPLAY</bold></#e74c3c> <value>{player}</value><text> · {check} VL {vl} · {time}s of {total}s · {ping}ms · {state}</text>"
  replay-ground: "on ground"
  replay-air: "<warn>in the air</warn>"
  replays-header: "<text>Recordings kept: </text><value>{count}</value>"
  replays-row: "<text> - </text><value>{player}</value><text> {check}, {seconds}s, {ago}s ago</text>"
  menu-replays: "<white><icon:replay></white><#c0392b>Replays</#c0392b> <muted>({count})</muted>"
  menu-replays-tip: "<text>Watch the seconds before a flag.</text>"
  menu-replays-title: "<value>Replays</value>"
  menu-replays-intro: "<text>Every recording is the movement the checks saw, played back to you alone.</text>"
  menu-replays-off: "<text>Replay is switched off in config.yml.</text>"
  menu-replays-empty: "<text>Nothing has been recorded yet.</text>"
  menu-replay-line: "<value>{player}</value><text> · {check} VL {vl} · {seconds}s · {ago}s ago</text>"
  menu-replay-play: "<white><icon:replay></white><value>Watch {player}</value> <muted>({check})</muted>"
  menu-replay-play-tip: "<text>Play the {seconds} seconds back where they happened.</text>"
  menu-replay-last: "<white><icon:replay></white><value>Watch the last flag</value>"
  menu-replay-now: "<value>Record the last seconds now</value>"
  menu-replay-last-tip: "<text>Play back what {player} just did.</text>"
  menu-replay-stop: "<white><icon:delete></white><text>Stop watching</text>"
  menu-replay-stop-tip: "<text>Remove the stand-in and stop the replay.</text>"

  # Staff mode and the inventory audit.
  usage-freeze: "<text>/sac freeze <player></text>"
  usage-inv: "<text>/sac inv <player> · /sac ec <player></text>"
  usage-find: "<text>/sac find <item> [name contains]</text>"
  unknown-item: "<bad>No item called </bad><value>{item}</value>"
  staff-on: "<value>Staff mode on.</value><text> Your things are kept safe until you leave it.</text>"
  staff-off: "<text>Staff mode off. Everything you had is back.</text>"
  staff-recovered: "<text>You were still in staff mode from before the restart.</text>"
  staff-tool-inspect: "<value>Inspect</value>"
  staff-tool-inspect-lore: "<text>Right-click a player to look in their inventory.</text>"
  staff-tool-freeze: "<accent2>Freeze</accent2>"
  staff-tool-freeze-lore: "<text>Right-click a player to hold them where they stand.</text>"
  staff-tool-random: "<value>Visit somebody</value>"
  staff-tool-random-lore: "<text>Right-click to jump to a random player.</text>"
  staff-tool-vanish: "<good>Vanish</good>"
  staff-tool-vanish-lore: "<text>Right-click to disappear.</text>"
  staff-tool-unvanish: "<text>Come back</text>"
  staff-tool-unvanish-lore: "<text>Right-click to be seen again.</text>"
  staff-tool-exit: "<bad>Leave staff mode</bad>"
  staff-tool-exit-lore: "<text>Right-click to get your own inventory back.</text>"
  frozen: "<bad><bold>You are frozen.</bold></bad> <text>Staff are talking to you. Do not log out.</text>"
  frozen-reminder: "<bad>You are frozen.</bad>"
  unfrozen: "<good>You can move again.</good>"
  froze: "<value>{player}</value><text> is frozen.</text>"
  unfroze: "<value>{player}</value><text> can move again.</text>"
  visiting: "<text>Dropped in on </text><value>{player}</value><text>.</text>"
  nobody-to-visit: "<text>Nobody else is online.</text>"
  audit-title: "<value>Who has {item}</value>"
  audit-header: "<text>{total} of </text><value>{item}</value><text> across {players} player(s) online.</text>"
  audit-row: "<value>{player}</value><text> · {total} · {where}</text>"
  audit-none: "<text>Nobody online is carrying {item}.</text>"
  audit-open: "<white><icon:check></white><value>Look at {player}</value>"
  audit-open-tip: "<text>Open a copy of {player}'s inventory.</text>"
  audit-goto: "<white><icon:teleport></white><text>Go to {player}</text>"
  audit-goto-tip: "<text>Teleport to them.</text>"
  audit-inv-title: "<muted>{player}'s inventory</muted>"
  audit-ender-title: "<muted>{player}'s ender chest</muted>"
  menu-staff-on: "<good>■</good> <value>Staff mode on</value>"
  menu-staff-off: "<muted>■</muted> <text>Staff mode off</text>"
  menu-staff-tip: "<text>Vanish, the staff hotbar, and your own inventory kept safe.</text>"

# One thank-you line with the BuiltByBit review link: once in the console after install and once
# in chat for the first operator who joins. It never repeats and sends nothing anywhere. false turns it off.
review-reminder: true

Config guide

The short annotated version from the listing. If it and the files above disagree, the files are what actually ships.

gate:
  max-ping: 300          above this nothing flags
  min-tps: 17.5          below this nothing flags
  grace-ms: 3000         after join / teleport / respawn / world change
actions:
  enabled: false         turn on only once you trust the alerts on YOUR server
  certain-only: true     only CERTAIN flags can run an action
  gate:
    min-flags: 3         separate certain flags of the same check...
    min-spread-seconds: 3
    window-seconds: 120  ...inside this window, on a healthy server
ai:
  enabled: true
  may-punish: false      the "ai" check never acts unless this is true
  flag-score: 0.90
discord:
  webhook-url: ""        empty = nothing is ever sent anywhere
checks:
  reach:
    enabled: true
    alert-at: 5
    actions:
      "20": ["kick {player} Unfair advantage ({check})"]

Placeholders in actions: {player}, {uuid}, {check}, {vl}, {detail}, {ai}. Each check has its own thresholds; the file explains every one in a comment. An existing config.yml keeps working: the new checks only alert, and certain-only mode is added by one line.

FAQ

Which servers and versions does it run on?

It needs Paper or a Paper fork like Purpur, because it uses Paper-only APIs (the dialog API for its menus, Adventure text components, Paper player profiles). Spigot and CraftBukkit won't work. The listing says Minecraft 1.21.11 (or a newer 1.21.x). plugin.yml declares api-version 1.21, the jar is built against Paper API 1.21.11, and it needs Java 21 or newer.

Does it need other plugins?

No, it runs without any other plugin (plugin.yml has no hard dependencies). The listing adds: Discord alerts through a channel webhook, optional.

How do I apply config changes?

Edit plugins/SentinelAC/config.yml and run /sac reload (reload the config). Restarting works too.

Does it run on Folia, or for Bedrock players?

I haven't tested it on Folia, and plugin.yml doesn't declare folia-supported. I haven't tested Bedrock players through Geyser either.

Is there DRM or a licence key?

No. There is nothing to activate and no licence server to reach, and the code is not obfuscated.

Does the AI part send data anywhere?

No. The model runs on your server and learns from your server; there is no cloud service and no network call. The only thing that ever leaves the server is a Discord alert, and only if you paste a webhook address.

Will it ban people on its own?

Not unless you tell it to. Actions are off by default, certain-only mode limits them to flags a normal client cannot produce, and the AI check never acts unless ai.may-punish is true.

Does it catch every cheat client?

No anticheat does. SentinelAC covers 25 checks plus the AI score, and the alerts show what it measured so your staff can judge the rest.

How long does setup take?

Drop the jar into /plugins and restart. The setup guide above has 4 short steps.

Can alerts go to Discord?

Yes. Paste a channel webhook address into discord.webhook-url and run /sac reload.

What should I not expect from it?

No anticheat catches every client, and this one does not claim to. SentinelAC is built to stay quiet when nothing is wrong, to show its evidence when something is, and to punish only what you configured. It is a tool for your staff, not a replacement for them.

Try it live

Play with it first

SentinelAC runs on my public demo server, set up so you can use it yourself. Join, try it, then decide.

try.zeropvp.xyz:25570

Java Edition 1.21.11. In Minecraft: Multiplayer, Add Server, paste the address, Join. Type /demo go sentinelac to land at its booth, or right-click the compass and pick it. The world resets every night.

Also mine

Run a Discord too?

ZeroBot is my Discord bot: moderation, tickets, welcome messages, logs, backups and raid protection, all set up from a web dashboard. It's free to add; Premium plans unlock more.

Its Minecraft Link connects your server to your Discord through the free ZeroBot Link plugin (coming to Modrinth soon): a live status card, a chat bridge, join and death lines, and account linking. Outgoing HTTPS only, no port to open.

See ZeroBot

Custom work

Need something that isn't here? I make custom Paper plugins, change these ones to fit your server, and do server setup and config work. Small plugins start at 50 EUR; bigger jobs get a quote.

How commissions work