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

JAVA
Events events = api.events();

RegisteredListener handle = events.listen(FlagEvent.class, event -> {
    // ...
});

events.ignore(handle);

Priority is 01000, lowest first. The default is 500.

JAVA
events.listen(FlagEvent.class, this::onFlag, 100);   // runs early

Catalogue

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

JAVA
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.

JAVA
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

JAVA
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

JAVA
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

JAVA
@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.

JAVA
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 cancelled

Two 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 100 runs early, so any listener at the default 500 sees 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

JAVA
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

JAVA
events.fire(myEvent);

Listeners are invoked in priority order. This exists so addons can drive the same pipeline. Use it deliberately.

Last updated