Events
Every event extends EcstacyEvent. Events that can be cancelled implement EcstacyCancellable;
the user-related ones extend UserAction, which is cancellable and carries user().
Registering
Events events = api.events();
RegisteredListener handle = events.listen(FlagEvent.class, event -> {
// ...
});
events.ignore(handle);Priority is 0–1000, lowest first. The default is 500.
events.listen(FlagEvent.class, this::onFlag, 100); // runs earlyCatalogue
| Event | Cancellable | Fired when |
|---|---|---|
FlagEvent |
yes | A player triggers a detection |
PunishEvent |
yes | A punishment is applied |
BanWaveEvent |
yes | A ban wave is flushed |
CloudEvent |
no | The cloud connection state changes |
MitigateEvent |
yes | A mitigation is applied. not yet wired |
UserEvent |
no | Base class for non-cancellable user events |
FlagEvent
events.listen(FlagEvent.class, event -> {
EcstacyUser user = event.user();
Check check = event.check();
String name = event.checkName();
AiVerdict verdict = event.verdict(); // CHEATING for a model check, null for a heuristic
String details = event.details(); // the rule name on heuristics; the verdict on model checks
int vl = event.vl();
int maxVl = event.maxVl();
if (event.punishable()) {
// vl has reached maxVl. The punishment is about to run
}
});punishable() is maxVl > 0 && vl >= maxVl.
Note
Model-backed checks report an AiVerdict, never a number. The decision value stays server-side,
because a figure attached to a flag also describes where the detector's boundary sits. Prefer
verdict() over parsing details(); it returns null for heuristic checks, whose details() is
the rule that tripped.
ViolationData and FlagRecord lost their fingerprintClient/fingerprintConfidence pair in
2.1.0. Nothing ever populated it, and the confidence half was a detection value.
Important
Cancelling a FlagEvent prevents the punishment. The detection is still recorded: the
violation level still increases and the flag still reaches the dashboard. Cancelling is how you
take over enforcement, not how you erase a detection.
PunishEvent
Fired after the punishment has been executed.
events.listen(PunishEvent.class, event -> {
event.user();
event.type(); // "BAN", "KICK", "WARN"
event.reason();
event.check(); // nullable. The check that caused it, if known
});BanWaveEvent
events.listen(BanWaveEvent.class, event -> {
event.executed(); // bans in this wave
event.windowCount(); // total bans in the configured window
event.windowDays(); // that window, in days
event.broadcastMessage("&d" + event.executed() + " cheaters removed.");
});broadcastMessage is mutable and already colour-translated.
Note
Cancelling a BanWaveEvent suppresses only the broadcast. The bans still execute. If you
want the announcement handled by your own plugin, cancel and post it yourself.
See Enforcement for how ban waves are configured.
CloudEvent
events.listen(CloudEvent.class, event -> {
if (event.state() == ConnectionState.DISCONNECTED) {
// detections are not running until this comes back
}
});Not cancellable. It reports a state change that has already happened.
MitigateEvent
@ComingSoon("Mitigation wiring in progress")Declared and stable as a type, but not yet fired. Compiling against it is safe; relying on it firing is not.
Which event you actually want
| You want to… | Listen to | Note |
|---|---|---|
| Log every detection | FlagEvent |
Fires on every flag, not only at the threshold. Filter on punishable() if you only want the last one |
| Replace the punishment with your own | FlagEvent, cancel when punishable() |
Cancelling stops the punishment, not the detection |
| React after a ban happened | PunishEvent |
Fired after execution. Too late to prevent it |
| Announce ban waves yourself | BanWaveEvent, cancel |
Cancelling suppresses only the broadcast; the bans still run |
| Know detections have stopped | CloudEvent |
DISCONNECTED means nothing is being scored until it returns |
The distinction that trips people up: FlagEvent is the one you can prevent, PunishEvent is
the one that tells you it already happened. If you cancel a FlagEvent on a punishable flag, no
PunishEvent follows.
Worked example: take over enforcement
Cancel Ecstacy's punishment and run your own punishment plugin's command instead, keeping the detection, the violation level and the dashboard entry intact.
api.events().listen(FlagEvent.class, event -> {
if (!event.punishable()) return; // not at the threshold yet
event.setCancelled(true); // stops Ecstacy's punish block only
String name = event.user().name();
String reason = "Ecstacy: " + event.checkName()
+ " (" + event.vl() + "/" + event.maxVl() + ")";
getServer().getScheduler().runTask(this, () ->
getServer().dispatchCommand(getServer().getConsoleSender(),
"tempban " + name + " 7d " + reason));
}, 100); // early, so later listeners see it cancelledTwo things worth copying from this:
- Dispatch the command on the main thread. Bukkit's command dispatch is not thread-safe and a listener is not a guaranteed place to be on it.
- Priority
100runs early, so any listener at the default500sees the event already cancelled. Use a low number when you are the one making the decision, a high number when you are observing it.
The same result is available without any code, by emptying punish.commands in checks.yml and
putting your command there instead. Reach for the event when the decision needs logic. A
different punishment per check, a check against your own database, an appeal state.
Worked example: route flags out of the game
api.events().listen(FlagEvent.class, event -> {
if (event.vl() % 5 != 0) return; // don't post every single flag
payload.put("player", event.user().name());
payload.put("check", event.checkName());
payload.put("vl", event.vl() + "/" + event.maxVl());
payload.put("verdict", String.valueOf(event.verdict())); // null on heuristics
postAsync(webhookUrl, payload); // never block the calling thread
});verdict() returns an AiVerdict for model-backed checks and null for heuristic ones, whose
details() carries the rule name instead. Handle both rather than assuming one.
Caution
Do not do network I/O inline in a listener, and do not call the cloud-backed
repo().profile() / .reputation() / .flags() / .logs() from inside FlagEvent. On a busy
server flags arrive in bursts, and those lookups are rate-limited. Cache reputation on join, and
hand webhook posts to your own executor.
Cancellation, precisely
Cancelling means something different on each event, and none of them mean "undo".
| Event | Cancelling stops | Cancelling does not stop |
|---|---|---|
FlagEvent |
The punishment | The flag being recorded, the VL increasing, the dashboard entry |
PunishEvent |
Nothing useful. It fires after execution | The punishment, which already ran |
BanWaveEvent |
The broadcast message | The bans in the wave |
MitigateEvent |
Nothing yet. Not wired | n/a |
CloudEvent |
Not cancellable | n/a |
There is no event that erases a detection. That is deliberate: a flag you disagreed with is
still evidence, and /ecstacy feedback is how you say so.
Firing your own
events.fire(myEvent);Listeners are invoked in priority order. This exists so addons can drive the same pipeline. Use it deliberately.