Content generated with AI — it may contain mistakes.

Referencedev

API

Read arenas, sessions, duels and records, start or stop one, and listen for every pop and every round.

TotemTrainerService is how another plugin finds out whether this one has hold of a player, reads a duel or a solo session, asks the database for a profile, and starts or stops either.

ExyliaAPI.get(TotemTrainerService.class).ifPresent(totems ->
    totems.activityOf(player.getUniqueId())
          .ifPresent(activity -> event.setCancelled(true)));
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.

What most integrations actually want

activityOf(UUID) is the one question worth asking before anything else. A player in training or in a duel has had their inventory and their location taken by this plugin, so teleporting them, giving them items or opening a menu for them will be undone or will break their session.

Everything that returns a value copies what it found out of the plugin's live registries, so a TotemMatch is the match as it was rather than as it is. The actions run exactly what the player's own command runs — the checks, the claims and the messages they see — and return whether the plugin accepted the request, not how it turned out. What happened arrives as an event.

Threads

Call the actions from the main thread, or on Folia from the thread that owns the player in question. The two CompletableFuture methods — loadProfile and the record queries — go to the database and are safe from anywhere.

What the server offers

MethodWhat it does
List<String> modes()The training modes this server has configured, in menu order.
List<TotemArena> arenas()Every arena duels are fought in.
Optional<TotemArena> arena(String arenaId)One arena, or empty when there is none by that id.

Modes are configuration rather than code, so the set differs between servers and an id that works on one is not guaranteed on another — check before offering one. Many matches share one arena at a time, so arenas() is not a list of free space; TotemArena.isReady() says which of them can host anything.

What a player is doing

MethodWhat it does
Optional<PlayerActivity> activityOf(UUID)What this plugin is doing with a player. Empty means it has no hold on them.
Optional<TotemMatch> matchOf(UUID)The duel a player is in, or empty when they are in none.
Optional<TotemMatch> match(UUID matchId)One duel by its id, or empty once it has been cleaned up.
List<TotemMatch> matches()Every duel running right now.
int matchesIn(String arenaId)How many duels are being fought in one arena. A load figure, not an availability one.
Optional<TrainingSession> training(UUID)The solo session a player is in, or empty when they are not training.
List<TrainingSession> trainingSessions()Every solo session running right now.
Optional<UUID> pendingDuelFor(UUID)Who has challenged a player and is still waiting for an answer, or empty when nobody is.

Records

MethodWhat it does
Optional<TotemProfile> profile(UUID)The profile cached for somebody online. Empty for an offline player, and also while their load is still in flight.
CompletableFuture<TotemProfile> loadProfile(UUID)The profile, from the database when it is not in memory. A player who has never trained gets a fresh, empty one rather than nothing.
CompletableFuture<List<MatchRecord>> history(UUID)A player's last duels, newest first, as many as the server keeps.
CompletableFuture<List<TrainingRecord>> records(UUID)A player's best training results, one row per mode and speed.
CompletableFuture<List<TrainingRecord>> leaderboard(String modeId, RecordCategory category, int ticks)One mode's training board, longest-standing records first. ticks restricts it to one interval, or 0 for every speed at once.

profile being empty during a load is deliberate: a caller on the server thread should degrade rather than wait, and one that can wait should ask loadProfile instead. The board is served from a cache the plugin refreshes on its own, so a menu redraw is cheap and a cold board is one query rather than one per viewer.

Actions

MethodWhat it does
boolean startTraining(Player, String modeId, int ticks)Puts a player into a solo session at that interval.
boolean duel(Player from, Player to, String modeId, int ticks, int bestOf)Sends a challenge. The target still has to accept, and the challenge expires on its own if nobody answers.
boolean acceptDuel(Player)Accepts the challenge waiting for a player.
boolean denyDuel(Player)Turns down the challenge waiting for a player.
boolean forfeit(UUID)Gives a duel up on somebody's behalf, handing the series to their opponent.
boolean cancelMatch(UUID matchId)Stops a duel with no winner and returns both players. For an administrator or another plugin that needs them back.
boolean leave(Player)Takes a player out of whatever this plugin has them in: leaving a duel is a forfeit, leaving training just ends the session.

A false from any of these is a refusal the player has already been told about — an unknown mode, being busy already, or no arena able to take them.

Types

Everything here is an immutable record or an enum, and every collection inside one is a copy.

TypeWhat it is
TotemArenaA place duels are fought in: id, displayName, enabled, and the two spawns as Optional<Location>. isReady() is what decides whether it can host anything — an arena missing a spawn, or pointing at a world that is not loaded, cannot take a duel however enabled it is.
TotemMatchOne duel as it was when you asked. has(UUID), opponentOf(UUID), scoreOf(UUID), durationMillis() and formatLabel() (BO5 and the like) save you the arithmetic.
MatchRoundOne played round: number, its clock, an Optional winner and a Performance per player, read with summaryOf(UUID).
TrainingSessionOne player's solo session: id, player, rules, arenaId, startedAt. Solo is physical — the player is moved into an arena and hidden — so treat them as unavailable rather than merely busy.
TrainingRulesEverything a session runs under, resolved from configuration once so a running session never re-reads the file.
PerformanceHow a session, a round or one side of a match went.
TotemProfileA player's lifetime duel record and pop statistics, plus averagePopMillis().
MatchRecordOne participant's view of one finished duel, with both names frozen as they were.
TrainingRecordA player's bests for one mode at one tick interval. Kept per speed because a 10-tick run and a 30-tick run of the same mode are not the same achievement.
PlayerActivityTRAINING or MATCH. There is no constant for "nothing" — absence is the empty Optional from activityOf.
MatchStateWAITING, STARTING, ACTIVE, ROUND_END, ENDING, FINISHED. It only moves forward, so isOver() never goes back to false.
PerformanceGradePERFECT, EXCELLENT, GOOD, OK, SLOW, best first.
RecordCategoryWhat a training board sorts by.

A few of these carry something that is not obvious from the field names.

TrainingRules is three independent choices, which is why nothing in it is named after a mode: totems says how the inventory is stocked (isStocked()), a window says the interval is drawn rather than kept (drawsInterval()), and a speed-up says it shortens as hits add up (speedsUp()). A mode may do both of the last two; one that does neither runs flat. ticks is the interval the player chose, and a window is shifted so that interval sits at its centre — a 10..30 window on a 25-tick session draws from 15..35 — so a picked speed means the same thing in every mode. intervalAt(int hits) is the speed a scoreboard should show, computed without consuming any of the session's randomness. seed is what lets both sides of a duel draw the same sequence, and what makes a session replayable exactly.

Performance counts pops and fails, keeps the best, worst, average and total reaction in milliseconds, the streak standing at the end and the best one, the duration, a score from 0 to 100 as Grading computes it, and a count per grade read with gradeCount(PerformanceGrade). bestSeconds(), averageSeconds() and durationSeconds() are the same numbers in seconds. A bestMillis of zero means no pop was recorded rather than an instantaneous one — the distinction to make before dividing by anything.

RecordCategory is RATING, BEST_POPS, FASTEST_AVERAGE, BEST_STREAK and LONGEST_RUN. Every one is a best rather than a total, which is what lets a board built over rows kept per tick speed read as a board over players. Only RATING compares across speeds honestly, because it carries the weight of the interval the score was set at; the rest are raw bests that a slower interval inflates on its own, so a board over every speed names the speed on each row. ascending() is true only for FASTEST_AVERAGE, where the smallest number wins.

PerformanceGrade thresholds live in the server's configuration, so the same reaction can grade differently on two servers. Read the grade rather than re-deriving it from a reaction time.

Events

Seven events, in net.exylia.lib.api.totemtrainer.event. All of them extend TotemTrainerEvent, all are synchronous and fire on the thread that owns the player they are about — on Folia, that player's region thread — and none of them is cancellable. Every one reports something that has already been decided, so receiving one is a guarantee that it happened; there is nothing here to gate a duel or a session with.

Every event that names a match carries a TotemMatch, and that is a snapshot: the live match belongs to this plugin's own classloader and its own thread, so a handle to it would be neither visible to the plugin holding one nor safe to read. Call match(UUID) when you need the current state.

EventFires whenCarries
TrainingStartEventA solo session was accepted; the player is about to be moved and equipped.player(), rules()
TrainingEndEventA solo session is over; the numbers are final and about to be stored.player(), rules(), summary(), reason()
TotemPopEventA totem popped and the player got the next one into a hand.player(), reactionMillis(), grade(), inMatch()
MatchStartEventBoth players arrived in the arena; the first round's countdown starts next.match()
RoundEndEventA round was scored.match(), round()
MatchEndEventThe match is decided or cancelled.match(), winner(), cancelled()

TotemTrainerEvent itself is the abstract base. It is never fired and has no handler list of its own, so it is not something to register a listener for — it is there so a helper can take any of the six, and so an instanceof check covers them all.

TotemPopEvent.reactionMillis() is measured from the hit to the re-equip, not from the pop animation, and inMatch() is false in solo training.

TrainingEndEvent.Reason is one of FAILED (no totem in hand when a hit landed), COMPLETED (a bounded mode ran out of totems), LEFT, DISCONNECTED, or CANCELLED (another plugin took the player, or the server is stopping).

MatchEndEvent.winner() is null when cancelled() is true. It fires before ratings and history are written, which happens asynchronously afterwards — read the winner and the scores from the event, not from the profiles.

RoundEndEvent is where the per-round detail lives

TotemMatch deliberately does not carry the rounds already played: a listing of matches would copy every player's per-round statistics for a screen that only wants the score. RoundEndEvent hands you the MatchRound as it is scored, which is the moment that detail is worth having.

An empty round().winner() is a draw — both players dropped their totem in the same tick and nobody earned the round. A draw is replayed rather than moved past, and the drawn attempt is still a round: it happened and its pops count, it just does not advance the series, so a round number can appear twice.

@EventHandler
public void onPop(TotemPopEvent event) {
    if (event.grade() == PerformanceGrade.PERFECT && !event.inMatch()) {
        reward(event.player(), event.reactionMillis());
    }
}

What it does not expose

No menu openers, no arena editor, no writes to modes or to the grading profile, and no way to edit a profile or a record row. Rounds are read through RoundEndEvent rather than off a match, and a duel is built by duel(...) with a mode id and a speed rather than by handing the plugin a rules object of your own.

If an integration genuinely needs something that is not here, ask on Discord — a method added to a service is a minor release.

Something missing on this page? Tell us on Discord