Skip to main content

Developer API

Dungeon exposes a small API so that other Typewriter extensions can read the runs of the server, act on them and react to what happens in them. This page is for developers; to build dungeons you do not need it.

Who can use it​

Typewriter loads every extension into one class loader of its own, rebuilt at each /tw reload. The classes of the API are therefore visible to other Typewriter extensions that declare dependency("btcrenaud", "Dungeon"), and not to a regular Bukkit plugin.

A plugin that needs dungeon data should use the dungeon placeholders and entries, or go through a small Typewriter extension that bridges to it.

Do not keep anything across a reload

Do not keep a DungeonRunView or an event listener across a reload of the Dungeon extension. Read a fresh view, and register your listeners again when your extension loads.

Only two packages are public and stable:

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

Everything else is internal and may change in any release without notice.

Setup​

In the build.gradle.kts of your extension, add Dungeon to the compile classpath and declare it as a Typewriter dependency, the way Dungeon itself does for another extension:

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

typewriter {
namespace = "yournamespace"

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

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

DungeonApi​

DungeonApi is a Kotlin object. All its functions can be called from any thread.

Reads​

FunctionReturns
runOf(player: UUID)The DungeonRunView of the run the player is in, whatever its state (a run that is ending still lists its members until it is released), or null.
run(runId: String)The run runId, or null when it does not exist any more. A run id is unique and never reused.
runs()Every run of the server now, in no particular order. Each view is its own instant; the list is not atomic across runs.
runsOf(dungeonId: String)The runs of the dungeon whose dungeon_instance entry id is dungeonId. Empty for an unknown dungeon.
isInRun(player: UUID)Whether the player is in a run that is preparing or active: the question the dungeon asks before letting somebody start or join a run. false for a player whose run is already ending.
roomsOf(player: UUID)The ids of the rooms the player stands in, as the dungeon last saw them: the dungeon_room entry ids of a fixed dungeon, generated:<n> for a generated one. Empty when they are in no run or in a run without rooms.
aliveMobs(runId: String, mobEntryId: String? = null)How many dungeon mobs of the run are alive now, only those made by the dungeon_mob_spawn entry mobEntryId when given. 0 for an unknown run.
Zero living mobs does not mean cleared

The cap of living mobs may hold some back, and right after a reload the mobs are counted again chunk by chunk. To know that a site was cleared, listen to DungeonMobsClearedEvent.

Actions​

Every action returns a DungeonActionResult:

ResultMeaning
DoneThe action was accepted. Some actions finish on a later tick; the matching event says when.
NotFound(what)What the call named does not exist (any more); what says which thing is missing.
Refused(reason)The call named real things but the dungeon refuses to do it now. Nothing was changed.

The texts of what and reason are in English and meant for a developer reading a log; they are never shown to players.

ActionWhat it doesRefused when
endRun(runId)Ends the run as an administrator would (dungeon_close): the players are sent home and the world is released. The end sequence runs on a later tick; DungeonRunEndEvent (reason ADMIN) says it began.The run is still preparing (a launch is not cancelled from here: listen to DungeonRunPreLaunchEvent instead), or another call already owns its end: a run ends once. NotFound("run") for an unknown run.
leaveRun(player)Takes the player out of their run, as the dungeon_leave action does: they are sent back to where they came from. When they were the last one, the run ends right after. DungeonPlayerLeaveRunEvent says they left.The run is still preparing (the launch owns its members until it is active), or already ending (its end sequence sends its members home). NotFound when the player is in no run.
placeChest(runId, chestEntryId)Places the loot chest of the dungeon_loot_chest entry in the run, like a Loot Chest Trigger but without its sound, particles or message. Placing a chest that is already placed changes nothing.The entry is not in manual mode, it belongs to another dungeon than the run, the run is not active, or the world of the run is not loaded. NotFound for an unknown run ("run") or an unknown entry ("loot chest entry").

Threads​

  • Reads are safe from any thread: they read an immutable snapshot of the registry, and aliveMobs counts without taking any lock.
  • Actions are safe from any thread too and never wait for the game: what must run on the server thread is handed to the server's own schedulers.
  • leaveRun called from the server thread: the player has left when it returns Done. Called from any other thread: the departure runs on the next tick of the server thread and Done means accepted; the event says it happened.
  • placeChest: Done means accepted. The chest appears on the next tick, on the thread that owns its block; DungeonChestPlaceEvent says it stands. It may not (a block that cannot be replaced), which is logged.

DungeonRunView​

A run frozen at one instant. It is a copy that never changes after it was made, so it can be kept, compared and read from any thread; ask DungeonApi again for a fresh one. All fields come from one read of the registry, so state and members always agree.

FieldTypeMeaning
runIdStringUnique id of the run; it is gone with the run.
dungeonIdStringId of the dungeon_instance entry the run plays.
dungeonNameStringDisplay name of that entry, as the author wrote it (may be empty).
stateDungeonRunStateWhere the run stands in its life.
membersList<UUID>The players in the run in the order they arrived: the first one is the leader. Online or not (a held place counts).
worldNameString?The world the run is played in; null until it is ready.
startedAtEpochMsLongWhen the place of the run was reserved, in epoch milliseconds.
activatedAtEpochMsLong?When the players were let in; null while preparing and for a run taken back after a reload.
deadlineEpochMsLong?When the run must end; null for a run without time limit (or not active yet).
seedLong?The seed that made the dungeon, only for a generated dungeon; null for a fixed one. Replaying it gives the same dungeon.
generatedBooleanComputed property: true when the dungeon was made by a generator (it has a seed).

DungeonRunState​

ValueMeaning
PREPARINGThe place is reserved and the world is being made ready; the players are not inside yet.
ACTIVEThe players are inside and the dungeon is being played.
ENDINGThe run is over or being closed: the members are being sent home and the world released. Also covers the instant between the end of that sequence and the disappearance of the run, and a launch that failed after its reservation.

This enum belongs to the API and does not follow the internal state machine: the platform can add an internal state without breaking your when.

DungeonEndReason​

ValueMeaning
COMPLETEDThe dungeon was finished (dungeon_complete).
TIMEOUTThe time limit of the dungeon ran out.
ABANDONEDEvery player left.
ADMINAn administrator closed it (dungeon_close, the close command, DungeonApi.endRun).

DungeonDeparture​

ValueMeaning
SENT_HOMEThey left on purpose (the leave action or command, DungeonApi.leaveRun) and are teleported back to where they came from.
DISCONNECTEDThey disconnected (or their reconnection grace ran out): they are brought back to where they came from at their next join.
LEFT_THE_WORLDThey left the world of the run by their own means, so there is nowhere to send them back from.

Events​

All events are Bukkit events of the package btcrenaud.dungeons.api.event. Listen to the concrete event you need.

EventCancellableWhenFields
DungeonRunPreLaunchEventYesA group is about to start a dungeon: the launch passed its own checks (group size, criteria, entry cost...) and nothing is reserved, taken or created yet.dungeonId: String, members: List<UUID> (online players only, in the order given), forced: Boolean (an administrator forced the start), seed: Long? (the seed asked for; null when the launch draws its own, or for a fixed dungeon)
DungeonRunStartEventNoA run began: it is ACTIVE and its players are inside, after the onStart triggers and on_dungeon_start events ran.run
DungeonRunEndEventNoA run is ending: as soon as the end sequence begins and the statistics are paid out, before the onEnd triggers, the cleaning of chests and mobs and the return of the players.run (state ENDING, members still in it at that instant), reason: DungeonEndReason
DungeonPlayerLeaveRunEventNoA player left a run, whatever the way. Right after the player was removed from the run.run (no longer lists the player), player: UUID (may be offline), departure: DungeonDeparture
DungeonRoomEnterEventNoA member entered a room, even when the enterOnce of the room already spent its triggers.run, player: Player, roomId: String, templateId: String?
DungeonRoomLeaveEventNoA member left a room, even when leaveOnce already spent its triggers. Also follows the departure of a player from the run (or its world) while they stood in a room.run, player: Player, roomId: String, templateId: String?
DungeonMobDeathEventNoA mob of the dungeon died while its run was ACTIVE. Pieces of a splitting mob (slimes) and other descendants raise nothing.run, entity: LivingEntity, killer: Player? (null when something else killed it), mobEntryId: String
DungeonMobsClearedEventNoA site was cleared: its last mob of its last wave died. Once per clearing, not once per member.run, mobEntryId: String
DungeonChestPlaceEventNoA loot chest was placed, once per chest, when it is first registered. A chest taken back after a /tw reload raises nothing.run, chestId: String, chestEntryId: String, location: Location (a copy of the block position)
DungeonChestOpenEventYesA member is about to open (or trigger) a loot chest, by clicking its block or its NPC.run, player: Player, chestId: String, chestEntryId: String, location: Location

Notes on the fields:

  • roomId is the dungeon_room entry id for a fixed dungeon, and generated:<n> for a room of a generated dungeon, whose templateId is the id of the room template it was made from (null for a fixed dungeon).
  • chestId names this very chest in the run: the entry id for its main place, <entry>#<n> for an additional location, <entry>@<room>.<marker> for the chest of a marker. chestEntryId is the id of the dungeon_loot_chest entry in every case.
  • DungeonRunPreLaunchEvent has no DungeonRunView: the run does not exist yet. A launch that passed this event can still fail later; only DungeonRunStartEvent says a run really began. A launch that failed before the run became ACTIVE raises no end event.

Cancelling​

  • DungeonRunPreLaunchEvent: a cancelled launch is refused without a trace (no place reserved, no cost taken, no world made). The players are told nothing by default, because the snippet content.dungeon_launch.cancelled is empty: the extension that cancels must explain itself to the players, or the server owner fills that snippet in. A start from a lobby puts the group back in the lobby.
  • DungeonChestOpenEvent: a cancelled click is consumed. The chest does not open, nothing is rolled, no action runs, no statistic counts, and the chest stays as it was so the player can try again. Tell the player yourself why; nothing is said by default.

Contract shared by all events​

  • An event is raised once per fact, never once per member: one DungeonRunEndEvent per run, however many players it had.
  • It is raised on the thread that produced the fact: the server thread on Paper.
  • A listener that throws is logged by the server and ignored; it can never break a run. A listener must not block either, since it runs inline in the middle of the run's own sequence.
  • Listening to the base class DungeonEvent is not supported: Bukkit refuses to register it. Listen to the concrete events.
  • Events describe the current load of the extension. A /tw reload raises no end or leave event for the runs it carries over: they are adopted again silently and their later facts are raised normally.
  • When a player leaves a run while standing in a room, DungeonRoomLeaveEvent comes before the DungeonPlayerLeaveRunEvent of the same departure, and its run.members no longer lists the player.

Examples​

A listener that cancels a launch under a condition and tells the players:

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
// Nobody else will explain: the default snippet is empty.
event.members
.mapNotNull { Bukkit.getPlayer(it) }
.forEach { it.sendMessage(Component.text("Dungeons are closed for maintenance.")) }
}
}

Reading the run of a player:

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

fun describeRun(player: UUID): String {
val run = DungeonApi.runOf(player) ?: return "Not in a dungeon."
return "${run.dungeonName} (${run.state}): ${run.members.size} player(s)"
}