开发者 API
Dungeon 提供了一个小型 API,让其他 Typewriter 扩展可以读取服务器的运行、对其执行操作,并对其中发生的事情作出反应。本页面向开发者;构建副本并不需要它。
谁可以使用
Typewriter 会把所有扩展加载到它自己的同一个类加载器中,每次 /tw reload 都会重建。因此,该 API 的类对声明了 dependency("btcrenaud", "Dungeon") 的其他 Typewriter 扩展可见,而对普通的 Bukkit 插件不可见。
需要副本数据的插件应使用 Dungeon 的占位符和条目,或通过一个起桥接作用的小型 Typewriter 扩展。
不要跨 Dungeon 扩展的重载保留 DungeonRunView 或事件监听器。请读取新的视图,并在你的扩展加载时重新注册监听器。
只有两个包是公开且稳定的:
btcrenaud.dungeons.apibtcrenaud.dungeons.api.event
其余一切都是内部实现,可能在任何版本中不经通知而更改。
设置
在你的扩展的 build.gradle.kts 中,把 Dungeon 加入编译类路径,并将其声明为 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) | dungeon_instance 条目 id 为 dungeonId 的副本的运行。未知副本为空。 |
isInRun(player: UUID) | 玩家是否处于准备中或进行中的运行:即副本在允许某人开始或加入运行前所问的同一个问题。对于所在运行已在结束的玩家为 false。 |
roomsOf(player: UUID) | 玩家所在房间的 id,以副本最后一次看到的为准:固定副本为 dungeon_room 条目 id,生成副本为 generated:<n>。不在任何运行中或所在运行没有房间时为空。 |
aliveMobs(runId: String, mobEntryId: String? = null) | 该运行中此刻存活的副本怪物数量;若给出 mobEntryId,则只计算由该 dungeon_mob_spawn 条目生成的。未知运行为 0。 |
存活怪物的上限可能压住了一部分,而且重载后怪物会逐区块重新计数。要知道某个据点是否已被清空,请监听 DungeonMobsClearedEvent。
操作
每个操作都会返回一个 DungeonActionResult:
| 结果 | 含义 |
|---|---|
Done | 操作已被接受。部分操作会在之后的 tick 完成;对应的事件会告诉你何时完成。 |
NotFound(what) | 调用所指的对象不存在(或已不存在);what 说明缺少的是哪样东西。 |
Refused(reason) | 调用所指的对象确实存在,但副本此刻拒绝执行。没有任何改动。 |
what 和 reason 的文字是英文,供阅读日志的开发者使用,绝不会显示给玩家。
| 操作 | 作用 | 何时 Refused |
|---|---|---|
endRun(runId) | 像管理员那样结束运行(dungeon_close):玩家被送回,世界被释放。结束流程在之后的 tick 运行;DungeonRunEndEvent(原因 ADMIN)告知它已开始。 | 运行仍在准备中(无法从这里取消启动,请监听 DungeonRunPreLaunchEvent),或另一个调用已拥有其结束:一个运行只会结束一次。未知运行为 NotFound("run")。 |
leaveRun(player) | 像 dungeon_leave 操作一样,把玩家带出其所在的运行:他们会被送回原来的位置。若他们是最后一人,运行随后立即结束。DungeonPlayerLeaveRunEvent 告知他们已离开。 | 运行仍在准备中(在激活之前,启动流程拥有其成员),或已在结束(其结束流程会自行送成员回去)。玩家不在任何运行中时为 NotFound。 |
placeChest(runId, chestEntryId) | 在运行中放置 dungeon_loot_chest 条目的战利品箱,与 Loot Chest Trigger 类似,但没有其音效、粒子或消息。放置已放置的箱子不会有任何变化。 | 该条目不是手动模式、属于与运行不同的副本、运行未激活,或运行的世界未加载。未知运行为 NotFound("run"),未知条目为 NotFound("loot chest entry")。 |
线程
- 读取可从任意线程安全进行:它们读取注册表的不可变快照,
aliveMobs不加任何锁地计数。 - 操作同样可从任意线程安全进行,且从不等待游戏:必须在服务器线程运行的部分会交给服务器自己的调度器。
- 从服务器线程调用
leaveRun:返回Done时玩家已经离开。从其他线程调用:离开在服务器线程的下一个 tick 执行,Done表示已接受;事件会告知它已发生。 placeChest:Done表示已接受。箱子会在下一个 tick、在拥有其方块的线程上出现;DungeonChestPlaceEvent告知它已立起。它也可能没有成功(方块无法被替换),这会记录到日志中。
DungeonRunView
在某一时刻冻结的运行。它是一份创建后永不改变的副本,因此可以保存、比较并在任意线程读取;需要新的请再次向 DungeonApi 询问。所有字段来自对注册表的一次读取,因此 state 与 members 始终一致。
| 字段 | 类型 | 含义 |
|---|---|---|
runId | String | 运行的唯一 id;随运行一同消失。 |
dungeonId | String | 该运行所玩的 dungeon_instance 条目的 id。 |
dungeonName | String | 该条目的显示名称,与作者所写一致(可能为空)。 |
state | DungeonRunState | 运行处于其生命周期的哪个阶段。 |
members | List<UUID> | 运行中的玩家,按到达顺序排列:第一个是队长。无论是否在线(保留的位置也算)。 |
worldName | String? | 运行所在的世界;在其就绪前为 null。 |
startedAtEpochMs | Long | 运行的位置被预留的时间,单位为 epoch 毫秒。 |
activatedAtEpochMs | Long? | 玩家被放入的时间;准备中以及重载后接管的运行为 null。 |
deadlineEpochMs | Long? | 运行必须结束的时间;没有时间限制(或尚未激活)的运行为 null。 |
seed | Long? | 生成该副本的种子,仅限生成副本;固定副本为 null。重放它会得到同样的副本。 |
generated | Boolean | 计算属性:副本由生成器创建(有 seed)时为 true。 |
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 | 玩家以自己的方式离开了运行所在的世界,因此无处可送回。 |
事件
所有事件都是包 btcrenaud.dungeons.api.event 中的 Bukkit 事件。请监听你需要的具体事件。
| 事件 | 可取消 | 何时 | 字段 |
|---|---|---|---|
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在固定副本中是dungeon_room条目的 id,在生成副本的房间中是generated:<n>,其templateId是该房间所用房间模板的 id(固定副本为null)。chestId指运行中这个确切的箱子:主位置为条目 id,额外位置为<entry>#<n>,标记点的箱子为<entry>@<room>.<marker>。chestEntryId在所有情况下都是dungeon_loot_chest条目的 id。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} 名玩家"
}