Перейти к основному содержимому

API для разработчиков

Dungeon предоставляет небольшой API, чтобы другие расширения Typewriter могли читать забеги (runs) сервера, действовать на них и реагировать на происходящее в них. Эта страница для разработчиков; для создания подземелий она не нужна.

Кто может им пользоваться​

Typewriter загружает все расширения в один собственный class loader, пересоздаваемый при каждом /tw reload. Поэтому классы API видны другим расширениям Typewriter, объявившим dependency("btcrenaud", "Dungeon"), и не видны обычному плагину Bukkit.

Плагину, которому нужны данные подземелий, следует использовать плейсхолдеры и записи Dungeon либо пойти через небольшое расширение Typewriter, служащее мостом.

Ничего не храните через перезагрузку

Не храните DungeonRunView и слушатели событий через перезагрузку расширения Dungeon. Читайте свежее представление и заново регистрируйте слушатели при загрузке своего расширения.

Публичны и стабильны только два пакета:

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

Всё остальное внутреннее и может измениться в любом выпуске без предупреждения.

Подключение​

В build.gradle.kts вашего расширения добавьте Dungeon в compile classpath и объявите его зависимостью Typewriter, как это делает сам Dungeon для другого расширения:

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

typewriter {
namespace = "yournamespace"

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

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

DungeonApi​

DungeonApi — это Kotlin-object. Все его функции можно вызывать из любого потока.

Чтение​

ФункцияВозвращает
runOf(player: UUID)DungeonRunView забега, в котором находится игрок, в любом состоянии (завершающийся забег продолжает перечислять участников до освобождения), либо null.
run(runId: String)Забег runId или null, если его больше нет. Id забега уникален и никогда не используется повторно.
runs()Все забеги сервера на данный момент, в произвольном порядке. У каждого представления свой момент времени; список не атомарен по забегам.
runsOf(dungeonId: String)Забеги подземелья, у которого id записи dungeon_instance равен dungeonId. Пусто для неизвестного подземелья.
isInRun(player: UUID)Находится ли игрок в забеге, который готовится или активен: тот же вопрос, который подземелье задаёт перед тем, как позволить начать забег или присоединиться. false для игрока, чей забег уже завершается.
roomsOf(player: UUID)Id комнат, в которых стоит игрок, как их видело подземелье в последний раз: id записей dungeon_room для фиксированного подземелья, generated:<n> для сгенерированного. Пусто, если игрок не в забеге или в забеге без комнат.
aliveMobs(runId: String, mobEntryId: String? = null)Сколько мобов подземелья забега сейчас живы; если указан mobEntryId, то только созданные записью dungeon_mob_spawn. 0 для неизвестного забега.
Ноль живых мобов не значит «зачищено»

Лимит живых мобов может сдерживать некоторых, а сразу после перезагрузки мобов пересчитывают чанк за чанком. Чтобы узнать, что зона зачищена, слушайте DungeonMobsClearedEvent.

Действия​

Каждое действие возвращает DungeonActionResult:

РезультатЗначение
DoneДействие принято. Некоторые действия завершаются на более позднем тике; соответствующее событие сообщает когда.
NotFound(what)То, что назвал вызов, не существует (или уже не существует); what говорит, чего именно нет.
Refused(reason)Вызов назвал реальные вещи, но подземелье сейчас отказывается это делать. Ничего не изменено.

Тексты what и reason написаны по-английски для разработчика, читающего журнал; игрокам они никогда не показываются.

ДействиеЧто делаетRefused, когда
endRun(runId)Завершает забег так, как это сделал бы администратор (dungeon_close): игроков отправляют домой, мир освобождается. Последовательность завершения идёт на более позднем тике; DungeonRunEndEvent (причина ADMIN) сообщает, что она началась.Забег ещё готовится (запуск отсюда не отменяется: слушайте DungeonRunPreLaunchEvent), либо другой вызов уже владеет его завершением: забег завершается один раз. NotFound("run") для неизвестного забега.
leaveRun(player)Выводит игрока из его забега, как действие dungeon_leave: его отправляют обратно туда, откуда он пришёл. Если он был последним, забег завершается сразу после. DungeonPlayerLeaveRunEvent сообщает, что он ушёл.Забег ещё готовится (запуск владеет участниками, пока забег не станет активным) или уже завершается (его последовательность завершения сама отправляет участников домой). NotFound, если игрок не в забеге.
placeChest(runId, chestEntryId)Размещает сундук с добычей записи dungeon_loot_chest в забеге, как Loot Chest Trigger, но без его звука, частиц и сообщения. Размещение уже размещённого сундука ничего не меняет.Запись не в ручном режиме, принадлежит не тому подземелью, что забег, забег не активен или мир забега не загружен. NotFound для неизвестного забега ("run") или неизвестной записи ("loot chest entry").

Потоки​

  • Чтение безопасно из любого потока: оно читает неизменяемый снимок реестра, а aliveMobs считает без блокировок.
  • Действия тоже безопасны из любого потока и никогда не ждут игру: то, что должно выполняться в потоке сервера, передаётся собственным планировщикам сервера.
  • leaveRun, вызванный из потока сервера: игрок ушёл, когда функция вернула Done. Вызванный из другого потока: уход выполняется на следующем тике потока сервера, а Done означает принято; событие сообщает, что это произошло.
  • placeChest: Done означает принято. Сундук появляется на следующем тике в потоке, владеющем его блоком; DungeonChestPlaceEvent сообщает, что он стоит. Он может не встать (блок, который нельзя заменить) — это записывается в журнал.

DungeonRunView​

Забег, замороженный в один момент. Это копия, которая после создания никогда не меняется: её можно хранить, сравнивать и читать из любого потока; за свежей снова обращайтесь к DungeonApi. Все поля берутся из одного чтения реестра, поэтому state и members всегда согласованы.

ПолеТипЗначение
runIdStringУникальный id забега; исчезает вместе с ним.
dungeonIdStringId записи dungeon_instance, которую проходит забег.
dungeonNameStringОтображаемое имя этой записи в том виде, как его написал автор (может быть пустым).
stateDungeonRunStateНа каком этапе жизни находится забег.
membersList<UUID>Игроки забега в порядке прибытия: первый — лидер. В сети или нет (удерживаемое место учитывается).
worldNameString?Мир, в котором идёт забег; null, пока он не готов.
startedAtEpochMsLongКогда зарезервировано место забега, в миллисекундах epoch.
activatedAtEpochMsLong?Когда игроков впустили; null во время подготовки и для забега, восстановленного после перезагрузки.
deadlineEpochMsLong?Когда забег должен завершиться; null для забега без лимита времени (или ещё не активного).
seedLong?Сид, создавший подземелье, только для сгенерированного; null для фиксированного. Повторное использование даёт то же подземелье.
generatedBooleanВычисляемое свойство: true, когда подземелье создано генератором (есть seed).

DungeonRunState​

ЗначениеЗначение
PREPARINGМесто зарезервировано, мир готовится; игроков внутри ещё нет.
ACTIVEИгроки внутри, подземелье проходится.
ENDINGЗабег закончен или закрывается: участников отправляют домой, мир освобождается. Охватывает также момент между концом этой последовательности и исчезновением забега, а также запуск, не удавшийся после резервирования.

Это перечисление принадлежит API и не следует внутреннему автомату состояний: платформа может добавить внутреннее состояние, не сломав ваш when.

DungeonEndReason​

ЗначениеЗначение
COMPLETEDПодземелье пройдено (dungeon_complete).
TIMEOUTЛимит времени подземелья истёк.
ABANDONEDВсе игроки ушли.
ADMINАдминистратор закрыл его (dungeon_close, команда закрытия, DungeonApi.endRun).

DungeonDeparture​

ЗначениеЗначение
SENT_HOMEУшёл намеренно (действие или команда выхода, DungeonApi.leaveRun) и телепортируется обратно туда, откуда пришёл.
DISCONNECTEDОтключился (или истёк срок его переподключения): при следующем входе его вернут туда, откуда он пришёл.
LEFT_THE_WORLDПокинул мир забега своими средствами, поэтому отправлять его обратно неоткуда.

События​

Все события — события Bukkit из пакета btcrenaud.dungeons.api.event. Слушайте конкретное событие, которое вам нужно.

СобытиеОтменяемоеКогдаПоля
DungeonRunPreLaunchEventДаГруппа собирается запустить подземелье: запуск прошёл собственные проверки (размер группы, критерии, плата за вход...), и пока ничего не зарезервировано, не взято и не создано.dungeonId: String, members: List<UUID> (только игроки в сети, в заданном порядке), forced: Boolean (администратор принудительно запустил), seed: Long? (запрошенный сид; null, когда запуск выберет свой, или для фиксированного подземелья)
DungeonRunStartEventНетЗабег начался: он ACTIVE, его игроки внутри, после выполнения триггеров onStart и событий on_dungeon_start.run
DungeonRunEndEventНетЗабег завершается: как только начинается последовательность завершения и выплачивается статистика, до триггеров onEnd, очистки сундуков и мобов и возвращения игроков.run (состояние ENDING, участники, ещё находящиеся в нём в этот момент), reason: DungeonEndReason
DungeonPlayerLeaveRunEventНетИгрок покинул забег любым способом. Сразу после того, как игрок удалён из забега.run (игрока в списке уже нет), player: UUID (может быть не в сети), departure: DungeonDeparture
DungeonRoomEnterEventНетУчастник вошёл в комнату, даже если enterOnce комнаты уже израсходовал её триггеры.run, player: Player, roomId: String, templateId: String?
DungeonRoomLeaveEventНетУчастник покинул комнату, даже если leaveOnce уже израсходовал её триггеры. Следует также за уходом игрока из забега (или из его мира), пока он стоял в комнате.run, player: Player, roomId: String, templateId: String?
DungeonMobDeathEventНетМоб подземелья погиб, пока его забег был ACTIVE. Части делящегося моба (слаймы) и прочие потомки ничего не вызывают.run, entity: LivingEntity, killer: Player? (null, если убило что-то другое), mobEntryId: String
DungeonMobsClearedEventНетЗона зачищена: погиб её последний моб последней волны. Один раз на зачистку, а не на участника.run, mobEntryId: String
DungeonChestPlaceEventНетСундук с добычей размещён, один раз на сундук, при первой регистрации. Сундук, восстановленный после /tw reload, ничего не вызывает.run, chestId: String, chestEntryId: String, location: Location (копия позиции блока)
DungeonChestOpenEventДаУчастник собирается открыть (или активировать) сундук с добычей, кликнув по его блоку или его NPC.run, player: Player, chestId: String, chestEntryId: String, location: Location

Пояснения к полям:

  • roomId — id записи dungeon_room для фиксированного подземелья и generated:<n> для комнаты сгенерированного, у которой templateId — id шаблона комнаты, из которого она создана (null для фиксированного).
  • chestId называет именно этот сундук в забеге: id записи для его основного места, <entry>#<n> для дополнительной позиции, <entry>@<room>.<marker> для сундука маркера. chestEntryId во всех случаях — id записи dungeon_loot_chest.
  • У DungeonRunPreLaunchEvent нет DungeonRunView: забега ещё не существует. Запуск, прошедший это событие, всё ещё может позже провалиться; только DungeonRunStartEvent говорит, что забег действительно начался. Запуск, провалившийся до того, как забег стал ACTIVE, не вызывает события завершения.

Отмена​

  • DungeonRunPreLaunchEvent: отменённый запуск отклоняется без следа (место не зарезервировано, плата не взята, мир не создан). По умолчанию игрокам ничего не сообщается, потому что сниппет content.dungeon_launch.cancelled пуст: отменяющее расширение должно само объясниться с игроками, либо владелец сервера заполняет этот сниппет. Запуск из лобби возвращает группу в лобби.
  • DungeonChestOpenEvent: отменённый клик поглощается. Сундук не открывается, ничего не разыгрывается, действия не выполняются, статистика не засчитывается, и сундук остаётся как был, чтобы игрок мог попробовать снова. Объясните игроку причину сами; по умолчанию ничего не говорится.

Общий контракт всех событий​

  • Событие вызывается один раз на факт, а не один раз на участника: один DungeonRunEndEvent на забег, сколько бы в нём ни было игроков.
  • Оно вызывается в потоке, создавшем факт: на Paper это поток сервера.
  • Слушатель, выбросивший исключение, записывается сервером в журнал и игнорируется; он никогда не сломает забег. Слушатель также не должен блокировать, так как выполняется прямо посреди собственной последовательности забега.
  • Прослушивание базового класса DungeonEvent не поддерживается: Bukkit отказывается его регистрировать. Слушайте конкретные события.
  • События описывают текущую загрузку расширения. /tw reload не вызывает событий завершения или ухода для забегов, которые он переносит: они молча принимаются заново, а их последующие факты вызываются как обычно.
  • Когда игрок покидает забег, стоя в комнате, DungeonRoomLeaveEvent приходит раньше DungeonPlayerLeaveRunEvent того же ухода, и в его run.members игрока уже нет.

Примеры​

Слушатель, отменяющий запуск при условии и предупреждающий игроков:

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
// Больше никто не объяснит: сниппет по умолчанию пуст.
event.members
.mapNotNull { Bukkit.getPlayer(it) }
.forEach { it.sendMessage(Component.text("Подземелья закрыты на техническое обслуживание.")) }
}
}

Чтение забега игрока:

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

fun describeRun(player: UUID): String {
val run = DungeonApi.runOf(player) ?: return "Не в подземелье."
return "${run.dungeonName} (${run.state}): игроков — ${run.members.size}"
}

См. также​