API для разработчиков
Dungeon предоставляет небольшой API, чтобы другие расширения Typewriter могли читать забеги (runs) сервера, действовать на них и реагировать на происходящее в них. Эта страница для разработчиков; для создания подземелий она не нужна.
Кто может им пользоваться
Typewriter загружает все расширения в один собственный class loader, пересоздаваемый при каждом /tw reload. Поэтому классы API видны другим расширениям Typewriter, объявившим dependency("btcrenaud", "Dungeon"), и не видны обычному плагину Bukkit.
Плагину, которому нужны данные подземелий, следует использовать плейсхолдеры и записи Dungeon либо пойти через небольшое расширение Typewriter, служащее мостом.
Не храните DungeonRunView и слушатели событий через перезагрузку расширения Dungeon. Читайте свежее представление и заново регистрируйте слушатели при загрузке своего расширения.
Публичны и стабильны только два пакета:
btcrenaud.dungeons.apibtcrenaud.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 всегда согласованы.
| Поле | Тип | Значение |
|---|---|---|
runId | String | Уникальный id забега; исчезает вместе с ним. |
dungeonId | String | Id записи dungeon_instance, которую проходит забег. |
dungeonName | String | Отображаемое имя этой записи в том виде, как его написал автор (может быть пустым). |
state | DungeonRunState | На каком этапе жизни находится забег. |
members | List<UUID> | Игроки забега в порядке прибытия: первый — лидер. В сети или нет (удерживаемое место учитывается). |
worldName | String? | Мир, в котором идёт забег; null, пока он не готов. |
startedAtEpochMs | Long | Когда зарезервировано место забега, в миллисекундах epoch. |
activatedAtEpochMs | Long? | Когда игроков впустили; null во время подготовки и для забега, восстановленного после перезагрузки. |
deadlineEpochMs | Long? | Когда забег должен завершиться; null для забега без лимита времени (или ещё не активного). |
seed | Long? | Сид, создавший подземелье, только для сгенерированного; null для фиксированного. Повторное использование даёт то же подземелье. |
generated | Boolean | Вычисляемое свойство: 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}"
}