Saltar al contenido principal

API para desarrolladores

Dungeon expone una pequeña API para que otras extensiones de Typewriter puedan leer las partidas (runs) del servidor, actuar sobre ellas y reaccionar a lo que ocurre en ellas. Esta página es para desarrolladores; para construir mazmorras no la necesitas.

Quién puede usarla​

Typewriter carga todas las extensiones en un class loader propio, recreado en cada /tw reload. Por eso las clases de la API son visibles para otras extensiones de Typewriter que declaren dependency("btcrenaud", "Dungeon"), y no para un plugin de Bukkit corriente.

Un plugin que necesite datos de mazmorra debe usar los placeholders y las entradas de Dungeon, o pasar por una pequeña extensión de Typewriter que sirva de puente.

No conserves nada a través de una recarga

No conserves un DungeonRunView ni un listener de eventos a través de una recarga de la extensión Dungeon. Lee una vista nueva y vuelve a registrar tus listeners cuando se cargue tu extensión.

Solo dos paquetes son públicos y estables:

  • btcrenaud.dungeons.api
  • btcrenaud.dungeons.api.event

Todo lo demás es interno y puede cambiar en cualquier versión sin previo aviso.

Puesta en marcha​

En el build.gradle.kts de tu extensión, añade Dungeon al classpath de compilación y decláralo como dependencia de Typewriter, como hace la propia Dungeon con otra extensión:

dependencies {
compileOnly(files("libs/TypeWriter-DungeonExtension.jar"))
}

typewriter {
namespace = "yournamespace"

extension {
name = "YourExtension"
// ...

dependencies {
dependency("btcrenaud", "Dungeon")
}
}
}

DungeonApi​

DungeonApi es un object de Kotlin. Todas sus funciones pueden llamarse desde cualquier hilo.

Lecturas​

FunciónDevuelve
runOf(player: UUID)El DungeonRunView de la partida en la que está el jugador, sea cual sea su estado (una partida que termina sigue listando a sus miembros hasta que se libera), o null.
run(runId: String)La partida runId, o null si ya no existe. Un id de partida es único y nunca se reutiliza.
runs()Todas las partidas del servidor en este momento, sin un orden concreto. Cada vista tiene su propio instante; la lista no es atómica entre partidas.
runsOf(dungeonId: String)Las partidas de la mazmorra cuyo id de entrada dungeon_instance es dungeonId. Vacío para una mazmorra desconocida.
isInRun(player: UUID)Si el jugador está en una partida en preparación o activa: la misma pregunta que se hace la mazmorra antes de dejar que alguien inicie o se una a una partida. false para un jugador cuya partida ya está terminando.
roomsOf(player: UUID)Los ids de las salas en las que está el jugador, tal como la mazmorra las vio por última vez: los ids de entrada dungeon_room de una mazmorra fija, generated:<n> en una generada. Vacío si no está en ninguna partida o está en una partida sin salas.
aliveMobs(runId: String, mobEntryId: String? = null)Cuántos mobs de la mazmorra de la partida están vivos ahora, solo los creados por la entrada dungeon_mob_spawn mobEntryId si se indica. 0 para una partida desconocida.
Cero mobs vivos no significa despejado

El límite de mobs vivos puede retener algunos, y justo después de una recarga los mobs se vuelven a contar chunk a chunk. Para saber que una zona se despejó, escucha DungeonMobsClearedEvent.

Acciones​

Cada acción devuelve un DungeonActionResult:

ResultadoSignificado
DoneLa acción fue aceptada. Algunas acciones terminan en un tick posterior; el evento correspondiente dice cuándo.
NotFound(what)Lo que nombra la llamada no existe (ya); what indica qué cosa falta.
Refused(reason)La llamada nombra cosas reales pero la mazmorra se niega a hacerlo ahora. No se cambió nada.

Los textos de what y reason están en inglés y son para el desarrollador que lee un registro; nunca se muestran a los jugadores.

AcciónQué haceRefused cuando
endRun(runId)Termina la partida como lo haría un administrador (dungeon_close): los jugadores son enviados a casa y el mundo se libera. La secuencia de fin se ejecuta en un tick posterior; DungeonRunEndEvent (motivo ADMIN) indica que empezó.La partida aún se está preparando (un lanzamiento no se cancela desde aquí: escucha DungeonRunPreLaunchEvent), o ya otra llamada posee su fin: una partida termina una sola vez. NotFound("run") para una partida desconocida.
leaveRun(player)Saca al jugador de su partida, como la acción dungeon_leave: se le devuelve a donde estaba. Si era el último, la partida termina justo después. DungeonPlayerLeaveRunEvent indica que se fue.La partida aún se está preparando (el lanzamiento posee a sus miembros hasta que está activa), o ya está terminando (su secuencia de fin envía a los miembros a casa). NotFound si el jugador no está en ninguna partida.
placeChest(runId, chestEntryId)Coloca el cofre de botín de la entrada dungeon_loot_chest en la partida, como un Loot Chest Trigger pero sin su sonido, partículas ni mensaje. Colocar un cofre ya colocado no cambia nada.La entrada no está en modo manual, pertenece a otra mazmorra distinta de la de la partida, la partida no está activa o el mundo de la partida no está cargado. NotFound para una partida desconocida ("run") o una entrada desconocida ("loot chest entry").

Hilos​

  • Las lecturas son seguras desde cualquier hilo: leen una instantánea inmutable del registro, y aliveMobs cuenta sin tomar ningún bloqueo.
  • Las acciones también son seguras desde cualquier hilo y nunca esperan al juego: lo que debe ejecutarse en el hilo del servidor se entrega a los planificadores del propio servidor.
  • leaveRun llamado desde el hilo del servidor: el jugador se ha ido cuando devuelve Done. Llamado desde otro hilo: la salida se ejecuta en el siguiente tick del hilo del servidor y Done significa aceptado; el evento indica que ocurrió.
  • placeChest: Done significa aceptado. El cofre aparece en el siguiente tick, en el hilo que posee su bloque; DungeonChestPlaceEvent indica que ya está puesto. Puede que no (un bloque que no se puede reemplazar), lo cual se registra en el log.

DungeonRunView​

Una partida congelada en un instante. Es una copia que nunca cambia tras crearse: se puede conservar, comparar y leer desde cualquier hilo; pide a DungeonApi una nueva cuando la necesites. Todos los campos proceden de una sola lectura del registro, así que state y members siempre concuerdan.

CampoTipoSignificado
runIdStringId único de la partida; desaparece con ella.
dungeonIdStringId de la entrada dungeon_instance que juega la partida.
dungeonNameStringNombre visible de esa entrada, como lo escribió el autor (puede estar vacío).
stateDungeonRunStateEn qué punto de su vida está la partida.
membersList<UUID>Los jugadores de la partida en orden de llegada: el primero es el líder. Conectados o no (un sitio reservado cuenta).
worldNameString?El mundo donde se juega la partida; null hasta que está listo.
startedAtEpochMsLongCuándo se reservó el sitio de la partida, en milisegundos epoch.
activatedAtEpochMsLong?Cuándo se dejó entrar a los jugadores; null durante la preparación y en una partida recuperada tras una recarga.
deadlineEpochMsLong?Cuándo debe terminar la partida; null en una partida sin límite de tiempo (o aún no activa).
seedLong?La semilla que creó la mazmorra, solo en una mazmorra generada; null en una fija. Reutilizarla da la misma mazmorra.
generatedBooleanPropiedad calculada: true cuando la mazmorra la hizo un generador (tiene seed).

DungeonRunState​

ValorSignificado
PREPARINGEl sitio está reservado y el mundo se está preparando; los jugadores aún no están dentro.
ACTIVELos jugadores están dentro y se está jugando la mazmorra.
ENDINGLa partida ha terminado o se está cerrando: los miembros son enviados a casa y el mundo liberado. Cubre también el instante entre el fin de esa secuencia y la desaparición de la partida, y un lanzamiento que falló tras su reserva.

Este enum pertenece a la API y no sigue la máquina de estados interna: la plataforma puede añadir un estado interno sin romper tu when.

DungeonEndReason​

ValorSignificado
COMPLETEDLa mazmorra se completó (dungeon_complete).
TIMEOUTSe agotó el límite de tiempo de la mazmorra.
ABANDONEDTodos los jugadores se fueron.
ADMINUn administrador la cerró (dungeon_close, el comando de cierre, DungeonApi.endRun).

DungeonDeparture​

ValorSignificado
SENT_HOMESe fue a propósito (la acción o el comando de salir, DungeonApi.leaveRun) y se le teletransporta a donde estaba.
DISCONNECTEDSe desconectó (o se agotó su plazo de reconexión): se le devolverá a donde estaba en su próxima conexión.
LEFT_THE_WORLDSalió del mundo de la partida por sus propios medios, así que no hay desde dónde devolverlo.

Eventos​

Todos los eventos son eventos de Bukkit del paquete btcrenaud.dungeons.api.event. Escucha el evento concreto que necesites.

EventoCancelableCuándoCampos
DungeonRunPreLaunchEventSíUn grupo está a punto de iniciar una mazmorra: el lanzamiento superó sus propias comprobaciones (tamaño del grupo, criterios, coste de entrada...) y aún no se ha reservado, cobrado ni creado nada.dungeonId: String, members: List<UUID> (solo jugadores conectados, en el orden dado), forced: Boolean (un administrador forzó el inicio), seed: Long? (la semilla pedida; null cuando el lanzamiento sortea la suya, o en una mazmorra fija)
DungeonRunStartEventNoUna partida empezó: está ACTIVE y sus jugadores están dentro, tras ejecutarse los disparadores onStart y los eventos on_dungeon_start.run
DungeonRunEndEventNoUna partida está terminando: en cuanto empieza la secuencia de fin y se pagan las estadísticas, antes de los disparadores onEnd, la limpieza de cofres y mobs y el regreso de los jugadores.run (estado ENDING, miembros que siguen dentro en ese instante), reason: DungeonEndReason
DungeonPlayerLeaveRunEventNoUn jugador salió de una partida, de la forma que sea. Justo después de retirarlo de la partida.run (ya no lista al jugador), player: UUID (puede estar desconectado), departure: DungeonDeparture
DungeonRoomEnterEventNoUn miembro entró en una sala, incluso si el enterOnce de la sala ya gastó sus disparadores.run, player: Player, roomId: String, templateId: String?
DungeonRoomLeaveEventNoUn miembro salió de una sala, incluso si leaveOnce ya gastó sus disparadores. También sigue a la salida de un jugador de la partida (o de su mundo) mientras estaba en una sala.run, player: Player, roomId: String, templateId: String?
DungeonMobDeathEventNoUn mob de la mazmorra murió mientras su partida estaba ACTIVE. Los trozos de un mob que se divide (slimes) y otros descendientes no lanzan nada.run, entity: LivingEntity, killer: Player? (null si lo mató otra cosa), mobEntryId: String
DungeonMobsClearedEventNoUna zona se despejó: murió su último mob de su última oleada. Una vez por despeje, no una vez por miembro.run, mobEntryId: String
DungeonChestPlaceEventNoSe colocó un cofre de botín, una vez por cofre, al registrarse por primera vez. Un cofre recuperado tras un /tw reload no lanza nada.run, chestId: String, chestEntryId: String, location: Location (copia de la posición del bloque)
DungeonChestOpenEventSíUn miembro está a punto de abrir (o activar) un cofre de botín, haciendo clic en su bloque o en su NPC.run, player: Player, chestId: String, chestEntryId: String, location: Location

Notas sobre los campos:

  • roomId es el id de la entrada dungeon_room en una mazmorra fija, y generated:<n> en una sala de una mazmorra generada, cuyo templateId es el id de la plantilla de sala de la que se creó (null en una mazmorra fija).
  • chestId nombra este cofre concreto en la partida: el id de la entrada para su lugar principal, <entry>#<n> para una ubicación adicional, <entry>@<room>.<marker> para el cofre de un marcador. chestEntryId es en todos los casos el id de la entrada dungeon_loot_chest.
  • DungeonRunPreLaunchEvent no tiene DungeonRunView: la partida aún no existe. Un lanzamiento que pasó este evento aún puede fallar después; solo DungeonRunStartEvent dice que una partida empezó de verdad. Un lanzamiento que falló antes de que la partida pasara a ACTIVE no lanza ningún evento de fin.

Cancelar​

  • DungeonRunPreLaunchEvent: un lanzamiento cancelado se rechaza sin dejar rastro (sin sitio reservado, sin coste cobrado, sin mundo creado). Por defecto a los jugadores no se les dice nada, porque el snippet content.dungeon_launch.cancelled está vacío: la extensión que cancela debe explicárselo ella misma a los jugadores, o el dueño del servidor rellena ese snippet. Un inicio desde un lobby devuelve al grupo al lobby.
  • DungeonChestOpenEvent: un clic cancelado se consume. El cofre no se abre, no se sortea nada, no se ejecuta ninguna acción, ninguna estadística cuenta, y el cofre queda como estaba para que el jugador pueda reintentarlo. Explícale tú mismo el motivo al jugador; por defecto no se dice nada.

Contrato común a todos los eventos​

  • Un evento se lanza una vez por hecho, nunca una vez por miembro: un DungeonRunEndEvent por partida, tenga los jugadores que tenga.
  • Se lanza en el hilo que produjo el hecho: el hilo del servidor en Paper.
  • Un listener que lanza una excepción es registrado por el servidor e ignorado; nunca puede romper una partida. Un listener tampoco debe bloquear, ya que se ejecuta en línea en medio de la secuencia propia de la partida.
  • Escuchar la clase base DungeonEvent no está soportado: Bukkit se niega a registrarla. Escucha los eventos concretos.
  • Los eventos describen la carga actual de la extensión. Un /tw reload no lanza ningún evento de fin ni de salida para las partidas que conserva: se readoptan en silencio y sus hechos posteriores se lanzan con normalidad.
  • Cuando un jugador sale de una partida estando en una sala, DungeonRoomLeaveEvent llega antes que el DungeonPlayerLeaveRunEvent de la misma salida, y su run.members ya no lista al jugador.

Ejemplos​

Un listener que cancela un lanzamiento bajo una condición y avisa a los jugadores:

import btcrenaud.dungeons.api.event.DungeonRunPreLaunchEvent
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.event.EventHandler
import org.bukkit.event.Listener

class LaunchGate(private val isMaintenance: () -> Boolean) : Listener {
@EventHandler
fun onPreLaunch(event: DungeonRunPreLaunchEvent) {
if (event.forced || !isMaintenance()) return
event.isCancelled = true
// Nadie más lo explicará: el snippet por defecto está vacío.
event.members
.mapNotNull { Bukkit.getPlayer(it) }
.forEach { it.sendMessage(Component.text("Las mazmorras están cerradas por mantenimiento.")) }
}
}

Leer la partida de un jugador:

import btcrenaud.dungeons.api.DungeonApi
import java.util.UUID

fun describeRun(player: UUID): String {
val run = DungeonApi.runOf(player) ?: return "No está en una mazmorra."
return "${run.dungeonName} (${run.state}): ${run.members.size} jugador(es)"
}