API
Reading configured and running capture events, starting and stopping one, and reading player and clan statistics.
CaptureService is what another plugin reads and drives ExyliaCapture through: the events an
administrator configured, the ones running right now, starting and stopping them, and the statistics
they leave behind.
ExyliaAPI.get(CaptureService.class).ifPresent(capture ->
capture.activeEvents().forEach(event ->
getLogger().info(event.displayName() + " has " + event.participants() + " players")));The artifact, the repository and the plugin.yml line are the same for every Exylia plugin and live
on the public API page.
In this plugin an "event" is a game mode being played — a KOTH, a conquest, a payload — not
something you register a listener for. CaptureEvent is a plain record describing one run of one.
The three Bukkit events the plugin does publish are listed under
Lifecycle events, and carry a CaptureEvent snapshot.
A config is an event an administrator set up; a run is one occurrence of it. At most one run
of a config exists at a time, and a run carries its config's id — so start(String), stop(String)
and event(String) all take the same string, and a config can be started again once its run has
ended.
Configured events
| Method | What it does |
|---|---|
config(String configId) | One config as an Optional<CaptureConfig>. Empty when no config has that id. |
configs() | Every event config, enabled or not. |
startableConfigs() | The configs that could be started right now: enabled, fully configured and not already running. |
eventTypes() | Every event type the plugin knows how to run, as a Set<String> of type ids. |
A config missing its zone is left out of startableConfigs() but still appears in configs(), so an
admin menu can show it as unfinished rather than hide it.
CaptureConfig.type() is a String because the types are a registry the plugin can be extended
with — KOTH, conquest, payload and the rest are what ships, not what is possible, and a mode
registered by another plugin appears here too. Compare a type against eventTypes() rather than
against a constant of your own.
Running events
| Method | What it does |
|---|---|
event(String eventId) | One running event as an Optional<CaptureEvent>. Empty when nothing with that id is running. |
activeEvents() | Every event running right now. Empty when none are. |
isRunning(String configId) | Whether that config has a run going. |
eventOf(UUID player) | The event a player is taking part in, as an Optional<CaptureEvent>. Empty when they are in none. |
participants(String eventId) | Everybody taking part, as a List<UUID>. Empty when nothing with that id is running. |
points(String eventId, UUID player) | What a player has scored. 0 when the event is not running or is not scored by points. |
scores(String eventId) | Everybody's score, as a Map<UUID, Integer>. Empty for a mode that does not score by points. |
A player counts as taking part from the first moment they stand in a zone, and keeps counting until
the event ends — so eventOf still answers for somebody who has since walked out, which is what a
reward or a scoreboard needs. scores is a snapshot of the tallies, so it can be sorted and drawn
without the next tick changing it underneath.
Actions
| Method | What it does |
|---|---|
start(String configId) | Starts a run of a config. Returns the new run's id, or empty when it could not start. |
stop(String eventId) | Ends a run early. true when something was running under that id. |
start refuses when the config is disabled, unfinished, or already running, and says so in the
plugin's log rather than throwing — an automation that starts events on a schedule should not have to
catch anything.
stop ends the run with the reason STOPPED and no winner passed in. What happens next is the
mode's own, exactly as when its clock runs out: KOTH Points, KOTH Capture Points, Destroy The Core
and Conquest settle from the scores so far, announce the podium and pay the top three; KOTH and
Payload end with nobody winning; a bounty credits and pays its target as the survivor.
Menus
| Method | What it does |
|---|---|
void openMenu(Player) | Opens the capture menu for a player, the one /capture opens. |
The screen a lobby NPC or a hotbar item wants: what is live, what is scheduled and when, drawn from the server's own menu files. Call it on the thread that owns the player, as an interaction handler already is.
Lifecycle events
Three Bukkit events in net.exylia.lib.api.capture.event follow a run. Each carries a
CaptureEvent or CaptureConfig snapshot, never the live game. They, and openMenu, need
exylia-api v1.133.0 or newer.
| Event | When | Cancellable |
|---|---|---|
CaptureStartEvent | A run of a config is about to start. getConfig(). | Yes |
ZoneCaptureEvent | Somebody took a zone. getEvent(), getZone(), getPlayer(), getClan(). | No |
CaptureEndEvent | The run is over. getEvent(), getWinner(), getReason(). | No |
@EventHandler
public void onEnd(CaptureEndEvent event) {
if (event.getReason() == CaptureEndReason.FINISHED) {
event.getWinner().ifPresent(uuid -> announce(event.getEvent().displayName(), uuid));
}
}CaptureStartEvent fires once the config is found, enabled, complete, not already running and of
a known type — and before the run registers a zone, puts up a display or runs a start command. Every
start comes through it: a command, the schedule, the admin menu and start(String) alike. A
cancelled start is refused like any other: start answers empty, the refusal is logged, and an admin
who started it by command is told it failed without a reason, so a handler that cancels should say why
itself.
ZoneCaptureEvent fires each time a zone is taken: the hill in a KOTH (once per capture when it
runs on), each point in KOTH Capture Points, a Conquest zone falling to a clan, and a bounty's target
being killed. It comes after the capture is on the statistics and before the mode decides whether it
won, so a winning capture is followed by CaptureEndEvent. getZone(), getPlayer() and getClan()
are all Optional: the zone is only named in Conquest, and the clan only when the run scores by clan.
KOTH Points, Payload and Destroy The Core have no moment of taking a zone and never fire it.
CaptureEndEvent fires once per run, after the mode has settled it — the result broadcast, the
end commands run and the winner's rewards handed out — and before the run stops being listed, so
scores(String) still answers for it. getWinner() is the one player the run was ended in favour of;
it is empty when nobody won, and also when the result is shared, read from the scores or a bounty's
target outlived the hunt. getReason() is a CaptureEndReason:
| Constant | Meaning |
|---|---|
FINISHED | Somebody reached what the mode plays to: held the hill, hit the target, got the cart home, killed the target. |
TIME_UP | The clock ran out first. |
STOPPED | Stopped early: by an admin, stop(String), the plugin shutting down, or a bounty left with nobody to hunt. |
Each event is fired on the thread that caused it — the sender's own for a command, the global thread for the schedule and for a run ended by its zones or its clock — and is asynchronous when that is not the main thread. On Folia none of those is a single main thread, so schedule anything that touches the wider world.
Statistics
| Method | What it does |
|---|---|
stats(UUID player) | A player's totals across every event. |
stats(UUID player, String configId) | A player's totals in one event. |
clanStats(String clanId) | A clan's totals across every event. |
topPlayers(int limit) | The best players overall, as a CompletableFuture<List<CaptureStats>>, best first. |
topPlayers(String configId, int limit) | The same for one event. |
topClans(int limit) | The best clans overall, as a CompletableFuture<List<CaptureClanStats>>, best first. |
cachedTopPlayers() | The podium the plugin keeps warm for its own placeholders, best first. |
cachedTopClans() | The same for clans. |
stats reads the cache and never fails: a player with no row gets one with every counter at zero, and
the real row is fetched in the background for the next call.
topPlayers and topClans go to the database and complete on a database thread. For a placeholder or
a menu redraw prefer cachedTopPlayers() and cachedTopClans() — ten rows refreshed on a timer, and
empty until the first refresh has run, which is the distinction a placeholder needs: it can print a
fallback rather than block a tick on a query.
Types
CaptureConfig — id (also how the event is started), displayName, type (from the registry),
enabled, iconMaterial and maxDurationMillis, which is 0 when the event runs until somebody
wins. The zone, the reward tables and the mode's own settings are left out: they are the plugin's own
geometry and reward types, and none of them are something a third party can act on without also owning
the mode.
CaptureEvent — a snapshot of a run: id (its config's id), displayName, type, state,
elapsedMillis, remainingMillis, infiniteDuration and participants, the number of players who
have been inside a zone at least once. The plugin ticks a run several times a second, so the times here
are the ones current at the moment of the lookup — ask again rather than holding one across ticks.
CaptureStats — player, eventConfigId, wins, captures, timeCapturedSeconds, points
and leaderboardPoints. One shape covers both scopes: eventConfigId is empty for totals across
every event and names the config for one event's row. A player who never took part still has one of
these with every counter at zero, so a menu or a placeholder never handles an absent row.
CaptureClanStats — clanId, wins, captures, points, leaderboardPoints. Only written when
an event runs in clan mode, which needs a clan plugin installed: without one every event scores
individually and these stay at zero.
CaptureState — where a run is in its life: IDLE (built but not started, or already finished and
about to be forgotten), RUNNING (the clock ticks and the zones are live) or ENDING (a winner is
decided and the rewards are being handed out).
What it does not expose
Zone geometry, reward tables, the scheduler's own writes, admin menus and raw configuration rows are left out on purpose — a public contract cannot be broken later, so it carries what an integration needs and nothing that only made sense inside one version.
If something you need is genuinely missing, ask on Discord.
Something missing on this page? Tell us on Discord