Pular para o conteúdo principal

API para desenvolvedores

O Dungeon expõe uma pequena API para que outras extensões do Typewriter possam ler as runs do servidor, agir sobre elas e reagir ao que acontece nelas. Esta página é para desenvolvedores; para construir dungeons você não precisa dela.

Quem pode usá-la​

O Typewriter carrega todas as extensões em um class loader próprio, recriado a cada /tw reload. As classes da API ficam, portanto, visíveis para outras extensões do Typewriter que declarem dependency("btcrenaud", "Dungeon"), e não para um plugin Bukkit comum.

Um plugin que precise de dados de dungeon deve usar os placeholders e as entradas do Dungeon, ou passar por uma pequena extensão do Typewriter que faça a ponte.

Não guarde nada através de um reload

Não guarde um DungeonRunView nem um listener de eventos através de um reload da extensão Dungeon. Leia uma visão nova e registre seus listeners de novo quando sua extensão for carregada.

Apenas dois pacotes são públicos e estáveis:

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

Todo o resto é interno e pode mudar em qualquer versão sem aviso.

Configuração​

No build.gradle.kts da sua extensão, adicione o Dungeon ao classpath de compilação e declare-o como dependência do Typewriter, como o próprio Dungeon faz com outra extensão:

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

typewriter {
namespace = "yournamespace"

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

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

DungeonApi​

DungeonApi é um object Kotlin. Todas as suas funções podem ser chamadas de qualquer thread.

Leituras​

FunçãoRetorna
runOf(player: UUID)O DungeonRunView da run em que o jogador está, qualquer que seja o estado (uma run que está terminando ainda lista seus membros até ser liberada), ou null.
run(runId: String)A run runId, ou null quando ela não existe mais. Um id de run é único e nunca reutilizado.
runs()Todas as runs do servidor agora, sem ordem específica. Cada visão tem seu próprio instante; a lista não é atômica entre runs.
runsOf(dungeonId: String)As runs do dungeon cujo id de entrada dungeon_instance é dungeonId. Vazio para um dungeon desconhecido.
isInRun(player: UUID)Se o jogador está em uma run em preparação ou ativa: a mesma pergunta que o dungeon faz antes de deixar alguém iniciar ou entrar em uma run. false para um jogador cuja run já está terminando.
roomsOf(player: UUID)Os ids das salas em que o jogador está, como o dungeon as viu por último: os ids de entrada dungeon_room de um dungeon fixo, generated:<n> em um gerado. Vazio quando ele não está em nenhuma run ou está em uma run sem salas.
aliveMobs(runId: String, mobEntryId: String? = null)Quantos mobs do dungeon da run estão vivos agora, apenas os criados pela entrada dungeon_mob_spawn mobEntryId quando informada. 0 para uma run desconhecida.
Zero mobs vivos não significa limpo

O limite de mobs vivos pode segurar alguns, e logo após um reload os mobs são recontados chunk a chunk. Para saber que um local foi limpo, escute DungeonMobsClearedEvent.

Ações​

Toda ação retorna um DungeonActionResult:

ResultadoSignificado
DoneA ação foi aceita. Algumas ações terminam em um tick posterior; o evento correspondente diz quando.
NotFound(what)O que a chamada nomeou não existe (mais); what diz qual coisa falta.
Refused(reason)A chamada nomeou coisas reais, mas o dungeon se recusa a fazê-lo agora. Nada foi alterado.

Os textos de what e reason estão em inglês e são para o desenvolvedor que lê um log; nunca são mostrados aos jogadores.

AçãoO que fazRefused quando
endRun(runId)Termina a run como um administrador faria (dungeon_close): os jogadores são mandados para casa e o mundo é liberado. A sequência de fim roda em um tick posterior; DungeonRunEndEvent (motivo ADMIN) diz que ela começou.A run ainda está em preparação (um lançamento não é cancelado daqui: escute DungeonRunPreLaunchEvent), ou outra chamada já possui o seu fim: uma run termina uma única vez. NotFound("run") para uma run desconhecida.
leaveRun(player)Tira o jogador da run em que está, como a ação dungeon_leave: ele é mandado de volta para onde veio. Se era o último, a run termina logo em seguida. DungeonPlayerLeaveRunEvent diz que ele saiu.A run ainda está em preparação (o lançamento possui seus membros até ela ficar ativa), ou já está terminando (sua sequência de fim manda os membros para casa). NotFound quando o jogador não está em nenhuma run.
placeChest(runId, chestEntryId)Coloca o baú de loot da entrada dungeon_loot_chest na run, como um Loot Chest Trigger, mas sem seu som, partículas ou mensagem. Colocar um baú já colocado não muda nada.A entrada não está em modo manual, pertence a outro dungeon que não o da run, a run não está ativa ou o mundo da run não está carregado. NotFound para uma run desconhecida ("run") ou uma entrada desconhecida ("loot chest entry").

Threads​

  • As leituras são seguras de qualquer thread: leem um instantâneo imutável do registro, e aliveMobs conta sem tomar nenhum lock.
  • As ações também são seguras de qualquer thread e nunca esperam o jogo: o que precisa rodar na thread do servidor é entregue aos agendadores do próprio servidor.
  • leaveRun chamado da thread do servidor: o jogador saiu quando ela retorna Done. Chamado de outra thread: a saída roda no próximo tick da thread do servidor e Done significa aceito; o evento diz que ela aconteceu.
  • placeChest: Done significa aceito. O baú aparece no próximo tick, na thread dona do seu bloco; DungeonChestPlaceEvent diz que ele está de pé. Pode não estar (um bloco que não pode ser substituído), o que fica registrado no log.

DungeonRunView​

Uma run congelada em um instante. É uma cópia que nunca muda depois de criada: pode ser guardada, comparada e lida de qualquer thread; peça uma nova ao DungeonApi. Todos os campos vêm de uma única leitura do registro, então state e members sempre concordam.

CampoTipoSignificado
runIdStringId único da run; some junto com ela.
dungeonIdStringId da entrada dungeon_instance que a run joga.
dungeonNameStringNome de exibição dessa entrada, como o autor o escreveu (pode estar vazio).
stateDungeonRunStateEm que ponto da vida a run está.
membersList<UUID>Os jogadores da run na ordem de chegada: o primeiro é o líder. Online ou não (um lugar reservado conta).
worldNameString?O mundo em que a run é jogada; null até ficar pronto.
startedAtEpochMsLongQuando o lugar da run foi reservado, em milissegundos epoch.
activatedAtEpochMsLong?Quando os jogadores foram deixados entrar; null durante a preparação e em uma run retomada após um reload.
deadlineEpochMsLong?Quando a run deve terminar; null para uma run sem limite de tempo (ou ainda não ativa).
seedLong?A seed que criou o dungeon, apenas para um dungeon gerado; null para um fixo. Reutilizá-la dá o mesmo dungeon.
generatedBooleanPropriedade calculada: true quando o dungeon foi feito por um gerador (tem uma seed).

DungeonRunState​

ValorSignificado
PREPARINGO lugar está reservado e o mundo está sendo preparado; os jogadores ainda não estão dentro.
ACTIVEOs jogadores estão dentro e o dungeon está sendo jogado.
ENDINGA run acabou ou está sendo fechada: os membros são mandados para casa e o mundo é liberado. Cobre também o instante entre o fim dessa sequência e o desaparecimento da run, e um lançamento que falhou após a reserva.

Este enum pertence à API e não segue a máquina de estados interna: a plataforma pode adicionar um estado interno sem quebrar o seu when.

DungeonEndReason​

ValorSignificado
COMPLETEDO dungeon foi concluído (dungeon_complete).
TIMEOUTO limite de tempo do dungeon acabou.
ABANDONEDTodos os jogadores saíram.
ADMINUm administrador a fechou (dungeon_close, o comando de fechar, DungeonApi.endRun).

DungeonDeparture​

ValorSignificado
SENT_HOMESaiu de propósito (a ação ou o comando de sair, DungeonApi.leaveRun) e é teleportado de volta para onde veio.
DISCONNECTEDDesconectou (ou o prazo de reconexão acabou): será trazido de volta para onde veio na próxima entrada.
LEFT_THE_WORLDSaiu do mundo da run por conta própria, então não há de onde mandá-lo de volta.

Eventos​

Todos os eventos são eventos Bukkit do pacote btcrenaud.dungeons.api.event. Escute o evento concreto de que precisa.

EventoCancelávelQuandoCampos
DungeonRunPreLaunchEventSimUm grupo está prestes a iniciar um dungeon: o lançamento passou pelas próprias verificações (tamanho do grupo, critérios, custo de entrada...) e nada foi reservado, cobrado ou criado ainda.dungeonId: String, members: List<UUID> (apenas jogadores online, na ordem recebida), forced: Boolean (um administrador forçou o início), seed: Long? (a seed pedida; null quando o lançamento sorteia a sua, ou em um dungeon fixo)
DungeonRunStartEventNãoUma run começou: está ACTIVE e seus jogadores estão dentro, depois que os gatilhos onStart e os eventos on_dungeon_start rodaram.run
DungeonRunEndEventNãoUma run está terminando: assim que a sequência de fim começa e as estatísticas são pagas, antes dos gatilhos onEnd, da limpeza de baús e mobs e da volta dos jogadores.run (estado ENDING, membros ainda nela naquele instante), reason: DungeonEndReason
DungeonPlayerLeaveRunEventNãoUm jogador saiu de uma run, de qualquer forma. Logo depois de ser removido da run.run (não lista mais o jogador), player: UUID (pode estar offline), departure: DungeonDeparture
DungeonRoomEnterEventNãoUm membro entrou em uma sala, mesmo que o enterOnce da sala já tenha gasto seus gatilhos.run, player: Player, roomId: String, templateId: String?
DungeonRoomLeaveEventNãoUm membro saiu de uma sala, mesmo que leaveOnce já tenha gasto seus gatilhos. Também acompanha a saída de um jogador da run (ou de seu mundo) enquanto estava em uma sala.run, player: Player, roomId: String, templateId: String?
DungeonMobDeathEventNãoUm mob do dungeon morreu enquanto sua run estava ACTIVE. Os pedaços de um mob que se divide (slimes) e outros descendentes não disparam nada.run, entity: LivingEntity, killer: Player? (null quando outra coisa o matou), mobEntryId: String
DungeonMobsClearedEventNãoUm local foi limpo: morreu seu último mob da última onda. Uma vez por limpeza, não uma vez por membro.run, mobEntryId: String
DungeonChestPlaceEventNãoUm baú de loot foi colocado, uma vez por baú, quando é registrado pela primeira vez. Um baú retomado após um /tw reload não dispara nada.run, chestId: String, chestEntryId: String, location: Location (cópia da posição do bloco)
DungeonChestOpenEventSimUm membro está prestes a abrir (ou acionar) um baú de loot, clicando em seu bloco ou em seu NPC.run, player: Player, chestId: String, chestEntryId: String, location: Location

Notas sobre os campos:

  • roomId é o id da entrada dungeon_room em um dungeon fixo, e generated:<n> em uma sala de um dungeon gerado, cujo templateId é o id do modelo de sala de que ela foi feita (null em um dungeon fixo).
  • chestId nomeia este baú específico na run: o id da entrada para seu lugar principal, <entry>#<n> para um local adicional, <entry>@<room>.<marker> para o baú de um marcador. chestEntryId é, em todos os casos, o id da entrada dungeon_loot_chest.
  • DungeonRunPreLaunchEvent não tem DungeonRunView: a run ainda não existe. Um lançamento que passou por este evento ainda pode falhar depois; só DungeonRunStartEvent diz que uma run realmente começou. Um lançamento que falhou antes de a run ficar ACTIVE não dispara evento de fim.

Cancelando​

  • DungeonRunPreLaunchEvent: um lançamento cancelado é recusado sem deixar rastro (nenhum lugar reservado, nenhum custo cobrado, nenhum mundo criado). Por padrão os jogadores não são avisados de nada, porque o snippet content.dungeon_launch.cancelled está vazio: a extensão que cancela deve se explicar aos jogadores, ou o dono do servidor preenche esse snippet. Um início a partir de um lobby devolve o grupo ao lobby.
  • DungeonChestOpenEvent: um clique cancelado é consumido. O baú não abre, nada é sorteado, nenhuma ação roda, nenhuma estatística conta, e o baú permanece como estava para o jogador poder tentar de novo. Explique você mesmo o motivo ao jogador; por padrão nada é dito.

Contrato comum a todos os eventos​

  • Um evento é disparado uma vez por fato, nunca uma vez por membro: um DungeonRunEndEvent por run, quantos jogadores ela tenha tido.
  • É disparado na thread que produziu o fato: a thread do servidor no Paper.
  • Um listener que lança exceção é registrado em log pelo servidor e ignorado; ele nunca pode quebrar uma run. Um listener também não deve bloquear, pois roda em linha no meio da sequência da própria run.
  • Escutar a classe base DungeonEvent não é suportado: o Bukkit se recusa a registrá-la. Escute os eventos concretos.
  • Os eventos descrevem o carregamento atual da extensão. Um /tw reload não dispara evento de fim nem de saída para as runs que ele mantém: elas são readotadas em silêncio e seus fatos posteriores são disparados normalmente.
  • Quando um jogador sai de uma run estando em uma sala, DungeonRoomLeaveEvent vem antes do DungeonPlayerLeaveRunEvent da mesma saída, e seu run.members não lista mais o jogador.

Exemplos​

Um listener que cancela um lançamento sob uma condição e avisa os jogadores:

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
// Ninguém mais vai explicar: o snippet padrão está vazio.
event.members
.mapNotNull { Bukkit.getPlayer(it) }
.forEach { it.sendMessage(Component.text("Os dungeons estão fechados para manutenção.")) }
}
}

Ler a run de um jogador:

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

fun describeRun(player: UUID): String {
val run = DungeonApi.runOf(player) ?: return "Não está em um dungeon."
return "${run.dungeonName} (${run.state}): ${run.members.size} jogador(es)"
}

Relacionado​