跳到主要内容

开发者 API

Dungeon 提供了一个小型 API,让其他 Typewriter 扩展可以读取服务器的运行、对其执行操作,并对其中发生的事情作出反应。本页面向开发者;构建副本并不需要它。

谁可以使用​

Typewriter 会把所有扩展加载到它自己的同一个类加载器中,每次 /tw reload 都会重建。因此,该 API 的类对声明了 dependency("btcrenaud", "Dungeon") 的其他 Typewriter 扩展可见,而对普通的 Bukkit 插件不可见。

需要副本数据的插件应使用 Dungeon 的占位符和条目,或通过一个起桥接作用的小型 Typewriter 扩展。

不要跨重载保留任何东西

不要跨 Dungeon 扩展的重载保留 DungeonRunView 或事件监听器。请读取新的视图,并在你的扩展加载时重新注册监听器。

只有两个包是公开且稳定的:

  • btcrenaud.dungeons.api
  • btcrenaud.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 始终一致。

字段类型含义
runIdString运行的唯一 id;随运行一同消失。
dungeonIdString该运行所玩的 dungeon_instance 条目的 id。
dungeonNameString该条目的显示名称,与作者所写一致(可能为空)。
stateDungeonRunState运行处于其生命周期的哪个阶段。
membersList<UUID>运行中的玩家,按到达顺序排列:第一个是队长。无论是否在线(保留的位置也算)。
worldNameString?运行所在的世界;在其就绪前为 null。
startedAtEpochMsLong运行的位置被预留的时间,单位为 epoch 毫秒。
activatedAtEpochMsLong?玩家被放入的时间;准备中以及重载后接管的运行为 null。
deadlineEpochMsLong?运行必须结束的时间;没有时间限制(或尚未激活)的运行为 null。
seedLong?生成该副本的种子,仅限生成副本;固定副本为 null。重放它会得到同样的副本。
generatedBoolean计算属性:副本由生成器创建(有 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} 名玩家"
}