Detects client mods on a Paper server by asking the client to translate something only that mod can translate.
Paper 1.21.11, Java 21, PacketEvents. No NMS, no world edits, no database.
A sign line is a chat component, and a translate component carries a
fallback the client renders when it has no entry for that key. So a sign whose
lines are translate components keyed to a mod's language entries reads back as
the fallback on vanilla, and as the mod's own wording on a client that ships
those entries. The client hands the rendered lines straight back in
UPDATE_SIGN, which is the whole detection.
The sequence per probe:
BLOCK_CHANGEturns a block below the player into a sign, for that client only.BLOCK_ENTITY_DATAsets its four lines to translate components.OPEN_SIGN_EDITORthenCLOSE_WINDOW, so the client renders the lines without the editor reaching the screen.- The client replies with
UPDATE_SIGN. Any line that is not the fallback names a mod that is present. BLOCK_CHANGEputs the original block back.
Nothing is placed in the world. The block is only ever changed for one client, and the state captured before the swap is what gets restored.
A sign has four lines, so each round trip tests four mods rather than one.
Sixteen targets cost four probes. The batching is in ProbeSettings.batches().
fallback: "mp0"
targets:
"Meteor": "key.meteor-client.open-gui"
"Freecam": "key.freecam.toggle"
start-delay-ticks: 20
response-timeout-ticks: 100
probe-depth: 5
sign-block: "minecraft:oak_sign"
on-detect:
type: message
content:
- "<red>ModProbe: <white>%mod%<red> is not allowed here."
on-timeout:
type: none
content: []type is none, message, kick or command. Content is MiniMessage, with
%mod% and %player% replaced.
Open the mod's jar, read assets/<modid>/lang/en_us.json, and pick a key the
mod is certain to ship and unlikely to share with anything else. Keybind and
config-screen titles are good choices. A key that vanilla also defines will
match everyone, so check it is not in the vanilla language file.
If the client never replies, the verdict is inconclusive and on-timeout
defaults to doing nothing. A silent client is a loading or lagging client far
more often than a modded one, and a check that punishes a bad connection is
worse than no check.
Two other things the probe cannot tell you: a mod that ships no language entry for the key you chose is invisible to it, and a client can lie about what it renders. Treat a hit as evidence, not proof.
Needs JDK 21 or newer.
mvn packageOutput is target/ModProbe-1.0.0.jar. Compiles at -Xlint:all -Werror.
Drop it in plugins/ alongside PacketEvents.
| Symptom | Cause |
|---|---|
| Every player flagged for one mod | The key is also defined by vanilla or by a resource pack |
| Nobody ever flagged | The mod ships no entry for that key, or the key is misspelled |
| Everyone inconclusive | start-delay-ticks is too low and clients are still loading |
| Warning about the sign block on boot | sign-block is not a valid state string for this server version |
| Phantom sign under a player | A probe was interrupted before the restore; it clears on relog or chunk reload |
MIT. A derivative of the technique shown by AppleModDetector, also MIT.