API
Leer arenas, sesiones, duelos y récords, arrancar o cortar uno, y escuchar cada pop y cada ronda.
TotemTrainerService es con lo que otro plugin averigua si este tiene agarrado a un jugador, lee un
duelo o una sesión en solitario, le pide un perfil a la base de datos, y arranca o corta cualquiera de
los dos.
ExyliaAPI.get(TotemTrainerService.class).ifPresent(totems ->
totems.activityOf(player.getUniqueId())
.ifPresent(activity -> event.setCancelled(true)));El artefacto, el repositorio y la línea del plugin.yml son los mismos para todos los plugins de
Exylia y viven en la página de la API pública.
Lo que la mayoría de las integraciones realmente quiere
activityOf(UUID) es la pregunta que vale la pena hacer antes que cualquier otra. A un jugador que
está entrenando o en un duelo este plugin ya le tomó el inventario y la ubicación, así que
teletransportarlo, darle ítems o abrirle un menú se va a deshacer o le va a romper la sesión.
Todo lo que devuelve un valor copia lo que encontró en los registros vivos del plugin, así que un
TotemMatch es la partida como estaba, no como está. Las acciones ejecutan exactamente lo mismo que
el comando del propio jugador — los chequeos, las reservas y los mensajes que ve — y devuelven si el
plugin aceptó el pedido, no cómo terminó. Lo que pasó llega como evento.
Llamá a las acciones desde el hilo principal, o en Folia desde el hilo dueño del jugador en cuestión.
Los dos métodos que devuelven CompletableFuture — loadProfile y las consultas de récords — van a la
base de datos y son seguros desde cualquier lado.
Lo que ofrece el servidor
| Método | Qué hace |
|---|---|
List<String> modes() | Los modos de entrenamiento configurados en este servidor, en el orden del menú. |
List<TotemArena> arenas() | Todas las arenas donde se pelean los duelos. |
Optional<TotemArena> arena(String arenaId) | Una arena, o vacío si no hay ninguna con ese id. |
Los modos son configuración y no código, así que el conjunto cambia entre servidores y un id que
funciona en uno no está garantizado en otro — verificá antes de ofrecerlo. Varias partidas comparten
una misma arena al mismo tiempo, así que arenas() no es una lista de lugares libres;
TotemArena.isReady() dice cuáles pueden alojar algo.
Qué está haciendo un jugador
| Método | Qué hace |
|---|---|
Optional<PlayerActivity> activityOf(UUID) | Qué está haciendo este plugin con un jugador. Vacío significa que no lo tiene agarrado. |
Optional<TotemMatch> matchOf(UUID) | El duelo en el que está, o vacío si no está en ninguno. |
Optional<TotemMatch> match(UUID matchId) | Un duelo por su id, o vacío una vez que se limpió. |
List<TotemMatch> matches() | Todos los duelos que corren ahora mismo. |
int matchesIn(String arenaId) | Cuántos duelos se pelean en una arena. Es una cifra de carga, no de disponibilidad. |
Optional<TrainingSession> training(UUID) | La sesión en solitario en la que está, o vacío si no está entrenando. |
List<TrainingSession> trainingSessions() | Todas las sesiones en solitario que corren ahora mismo. |
Optional<UUID> pendingDuelFor(UUID) | Quién lo desafió y sigue esperando respuesta, o vacío si no hay nadie. |
Récords
| Método | Qué hace |
|---|---|
Optional<TotemProfile> profile(UUID) | El perfil en caché de alguien conectado. Vacío para un jugador desconectado, y también mientras su carga sigue en vuelo. |
CompletableFuture<TotemProfile> loadProfile(UUID) | El perfil, desde la base de datos cuando no está en memoria. Un jugador que nunca entrenó recibe uno nuevo y vacío, no nada. |
CompletableFuture<List<MatchRecord>> history(UUID) | Los últimos duelos del jugador, del más nuevo al más viejo, tantos como guarde el servidor. |
CompletableFuture<List<TrainingRecord>> records(UUID) | Sus mejores resultados de entrenamiento, una fila por modo y velocidad. |
CompletableFuture<List<TrainingRecord>> leaderboard(String modeId, RecordCategory category, int ticks) | La tabla de un modo, con los récords más antiguos primero. ticks la restringe a un intervalo, o 0 para todas las velocidades juntas. |
Que profile esté vacío durante una carga es a propósito: quien llama desde el hilo del servidor
debería degradar en vez de esperar, y quien sí puede esperar debería usar loadProfile. La tabla se
sirve de una caché que el plugin refresca solo, así que redibujar un menú sale barato y una tabla fría
es una consulta y no una por espectador.
Acciones
| Método | Qué hace |
|---|---|
boolean startTraining(Player, String modeId, int ticks) | Mete a un jugador en una sesión en solitario a ese intervalo. |
boolean duel(Player from, Player to, String modeId, int ticks, int bestOf) | Manda un desafío. El objetivo todavía tiene que aceptar, y el desafío vence solo si nadie responde. |
boolean acceptDuel(Player) | Acepta el desafío que espera al jugador. |
boolean denyDuel(Player) | Rechaza el desafío que espera al jugador. |
boolean forfeit(UUID) | Abandona un duelo en nombre de alguien, lo que le entrega la serie a su rival. |
boolean cancelMatch(UUID matchId) | Corta un duelo sin ganador y devuelve a los dos jugadores. Para un administrador u otro plugin que los necesite de vuelta. |
boolean leave(Player) | Saca al jugador de lo que sea que este plugin lo tenga: salir de un duelo es abandonar, salir del entrenamiento simplemente termina la sesión. |
Un false de cualquiera de estas es una negativa que al jugador ya se le explicó: modo desconocido, que
ya esté ocupado, o que ninguna arena pueda recibirlo.
Tipos
Todo acá es un record inmutable o un enum, y cada colección adentro de uno es una copia.
| Tipo | Qué es |
|---|---|
TotemArena | El lugar donde se pelea un duelo: id, displayName, enabled y los dos spawns como Optional<Location>. isReady() es lo que decide si puede alojar algo — una arena sin un spawn, o que apunta a un mundo que no está cargado, no puede recibir un duelo por más habilitada que esté. |
TotemMatch | Un duelo como estaba cuando preguntaste. has(UUID), opponentOf(UUID), scoreOf(UUID), durationMillis() y formatLabel() (BO5 y similares) te ahorran la cuenta. |
MatchRound | Una ronda jugada: number, su reloj, un ganador Optional y un Performance por jugador, que se lee con summaryOf(UUID). |
TrainingSession | La sesión en solitario de un jugador: id, player, rules, arenaId, startedAt. El solitario es físico — al jugador lo mueven a una arena y lo ocultan — así que tratalo como no disponible, no como meramente ocupado. |
TrainingRules | Todo lo que rige una sesión, resuelto desde la configuración una sola vez para que una sesión en curso nunca relea el archivo. |
Performance | Cómo salió una sesión, una ronda o un lado de una partida. |
TotemProfile | El historial de duelos de por vida de un jugador y sus estadísticas de pops, más averagePopMillis(). |
MatchRecord | La vista de un participante sobre un duelo terminado, con los dos nombres congelados como eran. |
TrainingRecord | Los mejores registros de un jugador para un modo a un intervalo. Se guarda por velocidad porque una corrida a 10 ticks y una a 30 del mismo modo no son el mismo logro. |
PlayerActivity | TRAINING o MATCH. No hay una constante para "nada": la ausencia es el Optional vacío de activityOf. |
MatchState | WAITING, STARTING, ACTIVE, ROUND_END, ENDING, FINISHED. Solo avanza, así que isOver() nunca vuelve a ser false. |
PerformanceGrade | PERFECT, EXCELLENT, GOOD, OK, SLOW, del mejor al peor. |
RecordCategory | Por qué ordena una tabla de entrenamiento. |
Algunos de estos traen cosas que no se deducen de los nombres de los campos.
TrainingRules son tres decisiones independientes, y por eso nada adentro lleva el nombre de un
modo: totems dice cómo se llena el inventario (isStocked()), una ventana dice que el intervalo se
sortea en vez de mantenerse fijo (drawsInterval()), y una aceleración dice que se acorta a medida que
se suman golpes (speedsUp()). Un modo puede hacer las dos últimas; uno que no hace ninguna corre
plano. ticks es el intervalo que eligió el jugador, y la ventana se desplaza para que ese intervalo
quede en su centro — una ventana 10..30 en una sesión de 25 ticks sortea entre 15 y 35 — así que una
velocidad elegida significa lo mismo en todos los modos. intervalAt(int hits) es la velocidad que
debería mostrar un scoreboard, calculada sin consumir nada de la aleatoriedad de la sesión. seed es lo
que hace que los dos lados de un duelo sorteen la misma secuencia, y lo que permite repetir una sesión
exactamente igual.
Performance cuenta pops y fails, guarda la mejor, la peor, la media y la suma de reacciones en
milisegundos, la racha en pie al final y la mejor de todas, la duración, un score de 0 a 100 como lo
calcula Calificación, y un conteo por grado que se lee con
gradeCount(PerformanceGrade). bestSeconds(), averageSeconds() y durationSeconds() son los mismos
números en segundos. Un bestMillis en cero significa que no se registró ningún pop, no uno
instantáneo: es la distinción que hay que hacer antes de dividir por algo.
RecordCategory es RATING, BEST_POPS, FASTEST_AVERAGE, BEST_STREAK y LONGEST_RUN. Cada
una es un mejor y no un total, que es lo que permite que una tabla armada sobre filas guardadas por
velocidad se lea como una tabla de jugadores. Solo RATING compara entre velocidades con honestidad,
porque carga el peso del intervalo en el que se hizo la marca; el resto son mejores crudos que un
intervalo más lento infla por sí solo, así que una tabla sobre todas las velocidades nombra la velocidad
en cada fila. ascending() es true solo para FASTEST_AVERAGE, donde gana el número más chico.
PerformanceGrade tiene sus umbrales en la configuración del servidor, así que la misma reacción
puede calificar distinto en dos servidores. Leé el grado en vez de volver a deducirlo de un tiempo de
reacción.
Eventos
Siete eventos, en net.exylia.lib.api.totemtrainer.event. Todos extienden TotemTrainerEvent, todos
son síncronos y se disparan en el hilo dueño del jugador del que tratan — en Folia, el hilo de la región
de ese jugador — y ninguno es cancelable. Cada uno informa algo que ya está decidido, así que
recibir uno es la garantía de que ocurrió; acá no hay nada con lo que frenar un duelo o una sesión.
Todo evento que nombra una partida trae un TotemMatch, y eso es una foto: la partida viva pertenece al
classloader y al hilo propios de este plugin, así que una referencia a ella no sería ni visible para el
plugin que la tuviera ni segura de leer. Llamá a match(UUID) cuando necesites el estado actual.
| Evento | Se dispara cuando | Trae |
|---|---|---|
TrainingStartEvent | Se aceptó una sesión en solitario; al jugador están por moverlo y equiparlo. | player(), rules() |
TrainingEndEvent | Terminó una sesión en solitario; los números son finales y están por guardarse. | player(), rules(), summary(), reason() |
TotemPopEvent | Reventó un tótem y el jugador logró llevar el siguiente a una mano. | player(), reactionMillis(), grade(), inMatch() |
MatchStartEvent | Los dos jugadores llegaron a la arena; lo próximo es la cuenta atrás de la primera ronda. | match() |
RoundEndEvent | Se puntuó una ronda. | match(), round() |
MatchEndEvent | La partida se decidió o se canceló. | match(), winner(), cancelled() |
TotemTrainerEvent es la clase base abstracta. Nunca se dispara y no tiene lista de handlers propia, así
que no es algo para lo que registres un listener: está para que un helper pueda recibir cualquiera de
los seis, y para que un instanceof los cubra a todos.
TotemPopEvent.reactionMillis() se mide desde el golpe hasta el reequipamiento, no desde la animación
del tótem, e inMatch() es false en entrenamiento en solitario.
TrainingEndEvent.Reason es uno de FAILED (sin tótem en la mano cuando llegó el golpe), COMPLETED
(un modo acotado se quedó sin tótems), LEFT, DISCONNECTED o CANCELLED (otro plugin se llevó al
jugador, o el servidor se está apagando).
MatchEndEvent.winner() es null cuando cancelled() es true. Se dispara antes de que se escriban
el rating y el historial, que se guardan en asíncrono después — leé el ganador y los tanteos del evento,
no de los perfiles.
El detalle por ronda vive en RoundEndEvent
TotemMatch a propósito no trae las rondas ya jugadas: un listado de partidas copiaría las estadísticas
por ronda de cada jugador para una pantalla que solo quiere el tanteo. RoundEndEvent te entrega el
MatchRound en el momento en que se puntúa, que es cuando ese detalle vale la pena.
Un round().winner() vacío es un empate: los dos jugadores soltaron el tótem en el mismo tick y nadie
se ganó la ronda. Un empate se vuelve a jugar en vez de darse por pasado, y el intento empatado sigue
siendo una ronda — ocurrió y sus pops cuentan — solo que no adelanta la serie, así que un número de
ronda puede aparecer dos veces.
@EventHandler
public void onPop(TotemPopEvent event) {
if (event.grade() == PerformanceGrade.PERFECT && !event.inMatch()) {
reward(event.player(), event.reactionMillis());
}
}Lo que no expone
Nada de abridores de menú, editor de arenas, escrituras sobre los modos o sobre el perfil de
calificación, ni forma de editar un perfil o una fila de récord. Las rondas se leen por RoundEndEvent
y no desde una partida, y un duelo se arma con duel(...) pasando un id de modo y una velocidad, no
entregándole al plugin un objeto de reglas propio.
Si una integración realmente necesita algo que no está acá, pídelo en Discord — agregar un método a un service es un release menor.
¿Falta algo en esta página? Dínoslo en Discord