Content generated with AI — it may contain mistakes.

Referencedev

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")));
Adding it to your project

The artifact, the repository and the plugin.yml line are the same for every Exylia plugin and live on the public API page.

CaptureEvent is not a Bukkit event

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

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

Event types are a registry, not an enum

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

MethodWhat 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

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

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

EventWhenCancellable
CaptureStartEventA run of a config is about to start. getConfig().Yes
ZoneCaptureEventSomebody took a zone. getEvent(), getZone(), getPlayer(), getClan().No
CaptureEndEventThe 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:

ConstantMeaning
FINISHEDSomebody reached what the mode plays to: held the hill, hit the target, got the cart home, killed the target.
TIME_UPThe clock ran out first.
STOPPEDStopped early: by an admin, stop(String), the plugin shutting down, or a bounty left with nobody to hunt.
Not always the main thread

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

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

The futures are the only calls that query

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