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 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.apibtcrenaud.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ção | Retorna |
|---|---|
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. |
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:
| Resultado | Significado |
|---|---|
Done | A 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ção | O que faz | Refused 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
aliveMobsconta 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.
leaveRunchamado da thread do servidor: o jogador saiu quando ela retornaDone. Chamado de outra thread: a saída roda no próximo tick da thread do servidor eDonesignifica aceito; o evento diz que ela aconteceu.placeChest:Donesignifica aceito. O baú aparece no próximo tick, na thread dona do seu bloco;DungeonChestPlaceEventdiz 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.
| Campo | Tipo | Significado |
|---|---|---|
runId | String | Id único da run; some junto com ela. |
dungeonId | String | Id da entrada dungeon_instance que a run joga. |
dungeonName | String | Nome de exibição dessa entrada, como o autor o escreveu (pode estar vazio). |
state | DungeonRunState | Em que ponto da vida a run está. |
members | List<UUID> | Os jogadores da run na ordem de chegada: o primeiro é o líder. Online ou não (um lugar reservado conta). |
worldName | String? | O mundo em que a run é jogada; null até ficar pronto. |
startedAtEpochMs | Long | Quando o lugar da run foi reservado, em milissegundos epoch. |
activatedAtEpochMs | Long? | Quando os jogadores foram deixados entrar; null durante a preparação e em uma run retomada após um reload. |
deadlineEpochMs | Long? | Quando a run deve terminar; null para uma run sem limite de tempo (ou ainda não ativa). |
seed | Long? | A seed que criou o dungeon, apenas para um dungeon gerado; null para um fixo. Reutilizá-la dá o mesmo dungeon. |
generated | Boolean | Propriedade calculada: true quando o dungeon foi feito por um gerador (tem uma seed). |
DungeonRunState
| Valor | Significado |
|---|---|
PREPARING | O lugar está reservado e o mundo está sendo preparado; os jogadores ainda não estão dentro. |
ACTIVE | Os jogadores estão dentro e o dungeon está sendo jogado. |
ENDING | A 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
| Valor | Significado |
|---|---|
COMPLETED | O dungeon foi concluído (dungeon_complete). |
TIMEOUT | O limite de tempo do dungeon acabou. |
ABANDONED | Todos os jogadores saíram. |
ADMIN | Um administrador a fechou (dungeon_close, o comando de fechar, DungeonApi.endRun). |
DungeonDeparture
| Valor | Significado |
|---|---|
SENT_HOME | Saiu de propósito (a ação ou o comando de sair, DungeonApi.leaveRun) e é teleportado de volta para onde veio. |
DISCONNECTED | Desconectou (ou o prazo de reconexão acabou): será trazido de volta para onde veio na próxima entrada. |
LEFT_THE_WORLD | Saiu 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.
| Evento | Cancelável | Quando | Campos |
|---|---|---|---|
DungeonRunPreLaunchEvent | Sim | Um 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) |
DungeonRunStartEvent | Não | Uma run começou: está ACTIVE e seus jogadores estão dentro, depois que os gatilhos onStart e os eventos on_dungeon_start rodaram. | run |
DungeonRunEndEvent | Não | Uma 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 |
DungeonPlayerLeaveRunEvent | Não | Um 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 |
DungeonRoomEnterEvent | Não | Um membro entrou em uma sala, mesmo que o enterOnce da sala já tenha gasto seus gatilhos. | run, player: Player, roomId: String, templateId: String? |
DungeonRoomLeaveEvent | Não | Um 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? |
DungeonMobDeathEvent | Não | Um 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 |
DungeonMobsClearedEvent | Não | Um local foi limpo: morreu seu último mob da última onda. Uma vez por limpeza, não uma vez por membro. | run, mobEntryId: String |
DungeonChestPlaceEvent | Não | Um 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) |
DungeonChestOpenEvent | Sim | Um 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 entradadungeon_roomem um dungeon fixo, egenerated:<n>em uma sala de um dungeon gerado, cujotemplateIdé o id do modelo de sala de que ela foi feita (nullem um dungeon fixo).chestIdnomeia 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 entradadungeon_loot_chest.DungeonRunPreLaunchEventnão temDungeonRunView: a run ainda não existe. Um lançamento que passou por este evento ainda pode falhar depois; sóDungeonRunStartEventdiz que uma run realmente começou. Um lançamento que falhou antes de a run ficarACTIVEnã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 snippetcontent.dungeon_launch.cancelledestá 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
DungeonRunEndEventpor 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
DungeonEventnão é suportado: o Bukkit se recusa a registrá-la. Escute os eventos concretos. - Os eventos descrevem o carregamento atual da extensão. Um
/tw reloadnã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,
DungeonRoomLeaveEventvem antes doDungeonPlayerLeaveRunEventda mesma saída, e seurun.membersnã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)"
}