Skip to content

Releases: isLumo/LumoAiDetector

v0.1.2

Choose a tag to compare

@isLumo isLumo released this 10 Jun 00:31
9a6de02

Changelog

What changed

Training is reproducible now. Before, every run shuffled the data with a fresh System.nanoTime() seed, so the same dataset gave you a slightly different model every time. Set training.seed to any non-zero value and the split and the forest become deterministic. Leave it at 0 to keep the old random behavior.

Metrics stop lying when there is no validation data. If your dataset was too small to hold out a validation set, the plugin still reported accuracy, precision, recall, and F1 as if they meant something. They were measured on the training data the model already saw. Now the success message and the model metadata say so, and /lad models info shows whether the numbers came from a holdout or from the training set.

Uneven datasets get balanced. If you record 5000 legit windows and 300 cheater windows, the old forest mostly learned to say legit. Now training.balance-classes weights the minority class up, capped by training.class-weight-cap so a tiny class cannot completely take over. On by default.

Predictions run on their own threads. When async-prediction was on, predictions shared the single IO thread with dataset writing. A slow model could stall recording. There is now a dedicated prediction pool sized by performance.prediction-threads, separate from the writer.

Async predictions stopped touching Bukkit off the main thread. The old async path called player.getName(), getWorld(), and the ping lookup from the worker thread, which is not safe. Now all of that is captured on the server thread before the work is handed off, and the worker only does math.

Shutdown saves your data in the right order. On disable it used to stop the timer and train executors first, then drain IO. Queued dataset rows could be lost or the writer reopened mid-shutdown. Now it stops predictions, stops accepting new writes, drains the IO queue, closes the writer, saves stats, then stops the rest.

Status and dataset info no longer freeze on big files. /lad status and /lad dataset info loaded the entire dataset into memory just to count rows. On a large CSV that is a lot of wasted work on the main thread. Both now stream the file off-thread and only count.

Trim streams instead of loading everything. /lad dataset trim read the whole dataset into memory, then wrote the tail back. Now it counts lines, then copies the last N line by line, so memory stays flat no matter how big the file is.

Alert history is thread-safe. With async predictions writing history while a command read it, the deque could be touched from two threads at once. Reads and writes are synchronized now, and /lad check history gets a defensive copy.

Model loading is locked down. Java deserialization will happily build any class on the classpath, so a malicious .bin dropped into the models folder was a real risk. Model files now load through an allow-list stream that only resolves Smile model classes and core JDK types. The SHA-256 wording was also corrected: it is a corruption check, not tamper protection, since the hash lives next to the file.

New features

  • /lad models compare <a> <b> - put two models side by side on accuracy, precision, recall, F1, and rows
  • training.seed in config.yml - fixed seed for reproducible training, 0 for random
  • training.balance-classes and training.class-weight-cap in config.yml - weight the minority class up on uneven datasets
  • detector.min-ping-ms and detector.max-ping-ms in config.yml - skip windows recorded under unusual latency
  • performance.prediction-threads in config.yml - worker count for the dedicated prediction pool
  • /lad models info now shows metrics source, whether classes were balanced, and the training seed

Bug fixes

  • Fixed alert history being read and written from two threads without synchronization.
  • Fixed async predictions calling Bukkit methods off the main thread.
  • Fixed shutdown stopping executors before draining queued dataset writes, which could lose rows.
  • Fixed the dataset writer being reopened during shutdown.
  • Fixed /lad status and /lad dataset info loading the whole dataset into memory on the main thread.
  • Fixed /lad dataset trim materializing the entire dataset instead of streaming.
  • Fixed min-legit-rows and min-cheater-rows allowing 0, which let a single-class model train.

Security

  • Model files now load through an allow-list deserializer that only resolves Smile model and core JDK classes.
  • Clarified that the model SHA-256 is an integrity check against corruption, not tamper protection.

For developers

  • Added a JUnit test suite covering metrics math, GCD grid math, prediction tracking, dataset CSV parsing and counting, and path sanitization. Run it with gradlew test.

What you do to upgrade

  1. Replace the jar.
  2. Start the server.
  3. That is it.

The config, dataset, and models from 0.1.1 are backward compatible. New config keys have safe defaults, and old model metadata loads with sensible fallbacks.

Full Changelog: v0.1.1...v0.1.2

v0.1.1

Choose a tag to compare

@isLumo isLumo released this 02 Jun 17:12
2d3376a

Changelog

What changed

Alert history no longer sits in memory forever. When a player left, their state was cleaned up but the alert history stayed. On a server with a few thousand unique players over a week, that turned into a slow leak. Fixed now.

The IO executor waits for pending writes before shutdown. It was sending a shutdown signal and walking away. If the dataset had queued rows, they got lost. Now it waits up to ten seconds. If the writes do not finish in time, it logs a warning and moves on.

pruneStatesIfNeeded stopped hammering every tick. It ran on every single PlayerMoveEvent, checking the state map size and calling Bukkit.getPlayer for every UUID. That is pointless work. Now it runs at most once every five seconds.

Windows path traversal in dataset-path is blocked. A config value like C:\evil\file passed the existing check because it does not contain .. or start with /. Now drive-letter paths are rejected too.

Model metadata is written once, not twice. saveModel wrote the metadata file, read it back, added the SHA-256, and wrote it again. Now SHA-256 is computed before the first write and included from the start.

CSV formatting is faster. String.format with Locale.US on every feature value adds up across thousands of windows. Replaced with a ThreadLocal DecimalFormat. About three times faster.

Model training adapts to your data now. Small datasets get conservative trees with limited depth to avoid overfitting. Large datasets get deeper trees for better accuracy. The train/validation split is stratified, so class ratios are preserved. If one class heavily outnumbers the other, the model adds regularization to cut down false positives.

New features

  • /lad dataset trim <rows> - keep only the last N rows and delete the rest
  • LumoAiDetector.bypass permission - exempt a player from detection entirely
  • detector.disabled-worlds in config.yml - disable detection in specific worlds
  • detector.whitelisted-uuids in config.yml - exempt players by UUID without a permission plugin
  • punishment.notify-player in config.yml - send a warning to the player when punishment triggers
  • {world} and {ping} placeholders for punishment commands
  • performance.async-prediction in config.yml - run ML predictions off the main thread

Bug fixes

  • Fixed alert history not being cleaned up when a player disconnects.
  • Fixed hardcoded English text in /lad dataset info (now reads from messages.yml).
  • Fixed IO executor not waiting for pending writes on server shutdown.
  • Fixed pruneStatesIfNeeded running on every PlayerMoveEvent.
  • Fixed Windows drive-letter paths passing the dataset-path security check.
  • Fixed saveModel writing metadata file twice instead of once.
  • Fixed String.format being called 120 times per recorded window in CSV writing.
  • Fixed target() checking every entity in the bounding box without an early distance filter.

Security

  • Windows drive-letter paths in dataset-path config are now rejected.
  • LumoAiDetector.bypass permission added for staff and trusted players.

What you do to upgrade

  1. Replace the jar.
  2. Start the server.
  3. That is it.

The config and dataset from 0.1.0 are backward compatible. New config keys have safe defaults.

Full Changelog: v0.1.0...v0.1.1

v0.1.0

Choose a tag to compare

@isLumo isLumo released this 01 Jun 22:29
1369f98

Changelog

What changed

Threading is no longer aspirational. The old code had ScheduledFuture fields with no visibility guarantees, shared DecimalFormat instances, a BufferedWriter that got replaced mid-write, and computeIfAbsent called from concurrent paths without a ConcurrentMap. Every one of those caused data corruption or silent drops under load. All of them are fixed.

Model training no longer blocks your entire server. It now runs on a dedicated trainExecutor thread pool. You can train a fifty-tree model on fifty thousand rows without timing out the main thread.

Recording stops when the server stops. Not after. There is a proper shutdown hook now. Your last session will not be truncated.

Dates are UTC everywhere. Not JVM default, not Moscow time, not whatever the hosting provider configured. UTC.

New features

  • /lad models info <name> - see accuracy, precision, recall, and F1-score for any model
  • /lad dataset info - row counts by class, file size, skipped rows
  • /lad check history <player> - replay recent check results for a player
  • /lad record stop all - stop every active recording session at once
  • Command aliases - active works like modelactivate, deactivate like modeldeactive, delete like modeldeleted. Old names still work.
  • max-dataset-rows in config.yml - cap the dataset size before it eats your disk
  • Build scripts - build.ps1 for Windows, build.sh for Linux

Bug fixes

  • Fixed Location.subtract() mutating the entity's actual position in the world. The detector was not just reading location data, it was moving the player.
  • Fixed SimpleDateFormat breaking under concurrent access from multiple threads.
  • Fixed ScheduledFuture field visibility across threads. The cancel method was sometimes running on a null handle.
  • Fixed DecimalFormat instance shared across threads without synchronization.
  • Fixed BufferedWriter being replaced mid-recording, losing buffered data.
  • Fixed catch(Throwable) masking OutOfMemoryError and other serious JVM problems.
  • Fixed reader and serializer streams not closing when constructors threw.
  • Fixed PluginSettings field not visible across threads on reload.
  • Fixed GCD calculation producing negative values for large tick differences.
  • Fixed dataset row limit not being enforced during recording.
  • Fixed label change warning message being untranslated.
  • Fixed RuntimeStateService returning stale cached state after state transitions.
  • Fixed /lad models info with no argument causing a StackOverflowError from infinite recursion.
  • Fixed recordings not stopping cleanly when the server shuts down.

Security

  • SHA-256 hash verification for all downloaded model files
  • Path traversal prevention - model names containing .. or / are rejected
  • Model name length limited to 64 characters to prevent filesystem abuse

What you do to upgrade

  1. Replace the jar
  2. Start the server
  3. That is it

The config file and dataset are backward-compatible. If you use the old command names in scripts or chat binds, they still work - the aliases are transparent.

License

Apache License 2.0. Do not remove my name or reupload as your own. If you fork it, use a different name and mention the original. Full terms in the LICENSE file.

Full Changelog: v0.0.1...v0.1.0

v0.0.1

Choose a tag to compare

@isLumo isLumo released this 31 May 23:53
59fd793

v0.0.1 - First public release

I built LumoAiDetector for my own server, then decided to publish it. There are free anti-cheats out there, and a few projects doing ML, but I could not find one that works like this: record your own dataset, train a Random Forest model right inside Java, manage model files in game, and keep every message editable.

This version is the source release. Test it on a local server before putting it anywhere near production.

How it works

The plugin watches combat rotations. Not random movement, not reach, not velocity. It takes 15-tick windows of mouse movement during active combat, runs them through filtering gates (target check, combat timer, movement threshold, ping), and feeds the ones that pass into a Smile Random Forest model.

The model is trained on data you record through /lad record. Legit gameplay. Cheat profiles. Your server, your players, your settings.

Changes before release

  • Fixed Location.subtract() mutating the entity's actual position in the world. The detector was not just reading location data, it was moving the player.
  • Fixed SimpleDateFormat breaking under concurrent access from multiple threads.
  • Fixed ScheduledFuture field visibility across threads. The cancel method was sometimes running on a null handle.
  • Fixed catch(Throwable) masking OutOfMemoryError and other serious JVM problems.
  • Fixed reader and serializer streams not closing when constructors threw.
  • Fixed PluginSettings field not visible across threads on reload.
  • Replaced the Russian-language config and messages with English ones.
  • Set up CI builds and automatic releases when you push a tag.

What you get

  • Gradle project, Java 8 target, no NMS or ProtocolLib
  • Bukkit / Spigot / Paper / Purpur / Folia
  • Dataset recording in CSV format, 120 features per window
  • Local model training with Smile Random Forest
  • Model activation, deactivation, deletion, and backup in game
  • Configurable alert and punishment thresholds
  • Permission-based tab complete

What you do

  1. Build the jar or download it from the release assets
  2. Drop it in plugins and start the server once
  3. Record some data through /lad record
  4. Train a model with /lad train
  5. Activate it with /lad active <model>

Read the README for the full command list and training workflow.

License

Apache License 2.0. Do not remove my name or reupload as your own. If you fork it, use a different name and mention the original. Full terms in the LICENSE file.