BSAntiCheat-1.0.5.jar
What's new
[1.0.5] - 2026-08-20
A code review pass, then two days of live alert data with debug_mode on. 1.0.4 fixed the
elytra speed check's assumption that one move event equals one tick; this release finds the
same assumption in the two places it was left standing, and closes several holes of the same
shape — a check whose evidence never expired, a guard that covered less ground than it
claimed, state that was only maintained on the paths that reached the end of the method, and
a rate that measured the network rather than the player.
Fixed — false positives
A move event is no longer treated as one tick. The ground speed and vertical checks compared a per-event delta against a per-tick threshold (0.4 blocks walking, 3.5 vertical). A move event is not reliably one tick: a client on a poor connection delivers one event carrying several ticks of travel, and judging that as a single tick multiplies the apparent speed by however many ticks were bundled. This is exactly the mistake 1.0.4 removed from the elytra check, where live data caught it as a 110→100 b/s "over-speed" curve from a routine rocket flight; the ground checks carried it unchanged. The delta is now divided by the ticks it actually spans, clamped at one so a sub-tick event is never scaled up.
The catch-up move after a packet gap is no longer judged. Per-tick scaling handles the ordinary case but cannot rescue the pathological one: a stalled connection flushing seconds of backlog produces a single delta large enough for the TELEPORT threshold, which no division makes innocent. A gap over 400 ms now leaves that one move unjudged — the movement counterpart of the rule the Timer check has had since 1.0.4.
Vehicle speed is measured over real elapsed time.
VehicleCheckerderived blocks per second from one move event × 20, the same assumption again, and a boat crossing loading chunks on an ice highway is precisely the case that breaks it. Speed is now averaged over a 250 ms window, so distance and elapsed time grow together.AutoClicker measures the click rate, not the packet arrival rate. There are two ways to read a rate off arm-swing packets, and both fail upwards — in different situations. Counting arrivals in a sliding second puts the network in charge of the answer: a connection that delivers a tick's swings in bundles makes the count read whatever the bundling lines up with. Taking the typical interval instead is immune to that, but an interval is not a rate: it spans the whole consistency window, so a short fast burst fills the window with burst intervals and reports the speed inside the burst as though it were sustained.
Both were observed in the field. A player placing blocks was flagged at 26 CPS while the interval median sat at exactly 50.0 ms — one server tick, the held-button cadence — through the entire run-up; the MAD of 49 ms against that median describes a bimodal arrival pattern, half the intervals near 0 ms and half near 100 ms, which is bundling and which also broke the held-button exclusion, since that wants the spread tight. Separately, three alerts of 29–30 CPS had arrival counts for the same second of 5, 8 and 10 — a handful of quick clicks in a row, which is an ordinary thing to do.
The rate is therefore the smaller of the two estimates. Neither can fall below the true rate, so the minimum is safe in both directions: bundling is capped by the median, a burst is capped by how many clicks actually arrived, and genuinely sustained fast clicking raises both and is still caught. Note that raising
autoclicker_max_cpsdoes not help against either artefact — the run-up went through 22, 23, 24 and 25 without pausing.Rising is no longer counted as hovering. The sustained-hover check counted every sample that was not falling, which made a climb indistinguishable from hanging in the air. Live data: four alerts fired while the player was moving UP at 0.12–0.20 b/t in a Trial Chamber — a Breeze, a wind charge or a Wind Burst mace throws a player upwards for far longer than the 2 s knockback grace lasts, and the tail of that arc is a slow climb with nothing underneath. Hovering now means what the name says: vertical movement inside one tick of gravity (±0.08 b/t). Falling resets the counter as before; rising does too.
Nuker and FastPlace evidence now expires. Both ask for several one-second windows over the rate limit before flagging, so that one bundled window — a vein miner, a burst of place packets — is not evidence. But the counter could only be reset by flagging: a window that stays under the limit produces no event on that path at all. The count therefore never came down, and unrelated bursts spread across a session added up until the third one flagged. The windows must now fall within 10 s of each other to count as consecutive, which is what "the rate was held up" was supposed to mean.
CombatCheckeralready solved this the same way; these two were the ones that had not.The unloaded-chunk guard now covers the neighbourhood it reads. It checked the chunk the player stands in, but the checks behind it read the player's neighbours: the ground scan samples all four footprint corners, the fall-slowing and liquid exemptions look sideways, and Spider/Jesus look at the walls. On a chunk border those reach into the next chunk, where an unloaded result reads as "nothing below the player" — and forces a synchronous chunk load from inside a movement handler, which is what Folia forbids. The guard now covers one block of margin in every direction, and the vertical checks stand down (dropping their streaks rather than resuming them across the blind spot) when it is not satisfied.
Added
- Sustained ascent (opt-in, off by default). Narrowing the hover band leaves a slow steady
climb unwatched, so
anticheat.sustained_ascent_detectioncovers it — by the one thing that separates thrown from flown. A ballistic rise sheds about one tick of gravity of vertical speed every tick and ends within a second or two; a climb that does not decay is not one anything threw. It ships off because it is a heuristic with no live data behind it yet: turn ondebug_mode, watch the[ASCENT-DEBUG]decay values on your own server, then enable it.
Fixed — other
- The plugin no longer overwrites a
config.ymlit did not change. The config was saved on every shutdown regardless, which made the plugin the last writer of a file it had not edited. An admin who edits a threshold while the server runs and does not run/bsac reloadhad that edit silently replaced by the stale in-memory value at the next stop — on a server with a scheduled restart twice a day, within hours, with nothing in the log to explain it. Saving is now tied to the plugin actually having something of its own to write: the whitelist and ore-exclusion commands, and the validator repairing an invalid value. Everything else on disk stays the admin's./bsac reloadis still what makes an edit take effect on a running server; it just no longer costs you the edit if you forget it. - Log rows carry the time of the violation. The
tscolumn was left toCURRENT_TIMESTAMP, which stamps the moment the row is INSERTed. Entries are batched and flushed every 30 s, so every alert in a batch shared one timestamp, up to half a minute after the fact — visible in the data as clusters of rows on the same second. That is exactly the column needed to line an alert up against the server log. The detection time is now recorded with the entry and written explicitly, in the same UTC format the default produced, so existing rows and queries are unaffected. The fallback logger, which receives whole batches at once during a database outage, had the same problem and takes the same timestamp. - Setback no longer teleports to a stale position. The movement handler returned early for
whitelisted, bypassing, OP-exempt, creative and spectator players without recording where
they were. The last known position is what a setback teleports to, so a spell in creative
froze it at the point of entry: the first violation after returning to survival sent the
player back there, possibly thousands of blocks and many minutes ago. Every exempt path now
keeps the baseline current. (Only reachable with
punishments.setbackenabled, which is off by default.) - The PacketFlood alert reports the rate it measured. It reported the configured limit plus one — the same number every time, from which neither the severity of a logged flood nor a sensible limit could be read afterwards.
- Suspending checks under lag is announced. Below
lag_exempt_tpsnearly every check backs off, so a server sitting at 17 TPS runs with the anticheat effectively switched off. It now logs when it stands down and when it resumes, after five confirming samples so a server hovering on the threshold does not log every second.
Diagnostics
debug_mode said nothing about the checks whose alerts most needed explaining, so they were
instrumented:
- ChestStealer had no debug output at all. It now logs the interval, click type, slot and streak for every counted container click, including the pairs below the physical floor that are deliberately ignored.
- The hover check only logged on
getLogger().fine(), which the default log level drops. The counting path now logs on INFO: vertical speed, what the ground scan found below the feet, the on-ground flag, the pillar grace and the tick span of the sample. - AutoClicker logs both rate estimates side by side (
cps=andarrivals=), so a future alert shows at a glance which one is driving it. - The X-Ray "OP is still being checked" notice fired on every block broken — 2843 identical lines in one debug session, burying what the mode was turned on for. Once per player now.
Build
maven-compiler-plugin3.11.0 → 3.14.0. 3.11.0 cannot drive a JDK 25 toolchain: its incremental-compile scan throwsCompilerException: ConcurrentModificationExceptionbefore javac runs, somvn clean packagefailed outright on any machine whose default JDK had moved past 21. The bytecode target is unchanged —releaseis still 21.
Tests
74 → 96, each behavioural fix verified to go red against the behaviour it replaces.
The AutoClicker fixtures reproduce the live statistics exactly — median 50.0 ms with a MAD of
49.0 ms for the bundled case, the 33 ms cadence for the burst — and assert the rates that
follow (20, not 26; 8, not 30) while a genuinely sustained 30 CPS still flags. Ascent is
covered as a pair: a ballistic arc raises nothing, genuine hanging still raises FLY, and the
opt-in ascent check fires on a climb that never slows while ignoring one that does. Config
ownership is covered byte for byte — Bukkit's saveConfig() rewrites YAML in its own style,
so "was the file written" is directly observable against a fixture that is the shipped default
verbatim, including an edit made without a reload having to survive the next shutdown. The
rate-streak lapse is tested as a pure function with the clock passed in, so no test has to
sleep for the length of the window it tests.
Not covered end to end: the vehicle speed window, which needs a ridden vehicle MockBukkit
cannot supply — the same gap PacketChecker has.
Investigated, not changed — elytra over-speed
Seven alerts of 204–311 b/s against the 140 ceiling, one player, one flight of roughly 1300 blocks. The hypothesis was the same class of bug as the rest of this release: a stalled client flushing its backlog delivers real distance with almost no real time attached, and the window that closes afterwards reports the lot. It did not survive a test — a stall poisons a single window, and the low-speed window the stall itself produces resets the streak before it can reach the three consecutive windows the check wants. Repeating the stall does not get there either.
The data cannot settle it. Under 1.0.4 the ts column was the flush time, so all seven rows
carry the same batch write and the only bound on the flight is "somewhere inside a 30 s
window": 1300 blocks in 30 s is ordinary, in 4.5 s it is not. The geometry cannot break the
tie either — the distance between alert positions and the reported speed both derive from the
same 250 ms windows, so they agree whether or not the elapsed time was measured correctly.
No change was made. A stall guard on the elytra path would have been a fix for an unproven cause, and a 3 s unjudged window after every 400 ms packet gap is something a cheat can ask for on purpose. The detection-time fix above is what makes this answerable next time; until then the elytra path at least has an end-to-end test that it fires on sustained over-speed, which nothing verified before.
This mod has no additional files

