相关:API概览.md · 脚本API.md · 条件与表达式.md · ../05-参考/术语表.md
⚙️ 动作系统与技能桥(ActionSystem / QinhActionHandler)
本页面向 Java / Kotlin 模块开发者,讲两件事:
- ActionSystem —— QCL 内置的「动作」执行框架(动作链、DSL 程序、注册表、追踪)。
- QinhActionHandler 契约 —— QI(QinhItems)把「触发」交给 QS(QinhSkills)等模块去「执行技能」的那道缝,包括统一的触发类型枚举、Bukkit 事件映射、跨模块事件总线
QISkillUseEvent、桥对象QISkillBridge。
🧩 谁该看这页:这是模块开发者的契约层。服主不直接配这里的任何东西 —— 服主在 QI 里配「触发器 → 动作」,QI 内部才会走到本页描述的契约。如果你是在写一个想接进秦淮生态的新模块(比如一个新的技能引擎),这页就是你要实现的接口。
一、ActionSystem 总览
com.qinhuai.corelib 的 action 包提供一套可独立运行的动作执行框架。它有两种用法:
- 直接动作链:用
ActionPipeline串几个Action顺序执行。 - DSL 程序:把动作写成节点树
ActionDslProgram/ActionNode,经compile/validate/optimize后execute,执行时把追踪写进DebugTraceRegistry。
1.1 Action 接口与 ActionContext
每个动作实现 Action 接口:
interface Action {
val id: String
fun execute(context: ActionContext)
}执行时传入的上下文 ActionContext:
| 成员 | 说明 |
|---|---|
player(可空) | 触发动作的玩家,可能为 null(如命令 / API 触发) |
variables: MutableMap | 变量袋,动作之间靠它传值 |
traceId(默认 UUID) | 本次执行的追踪 id |
debug | 是否开启调试追踪 |
读写变量与追踪的方法:
context.setVar("damage", 12.5)
val dmg: Double? = context.getVar("damage")
val name: String = context.getVarString("targetName") // 取字符串
val count: Int = context.getVarInt("count") // 取整数
context.traceBuilder() // 取追踪构建器,记录这一步发生了什么1.2 实现一个自定义 Action
class HealAction : Action {
override val id = "heal"
override fun execute(context: ActionContext) {
val player = context.player ?: return
val amount = context.getVar<Double>("amount") ?: 4.0
player.health = (player.health + amount).coerceAtMost(player.maxHealth)
context.traceBuilder() // 记录追踪
}
}1.3 ActionRegistry —— 注册与查找
ActionRegistry.register(HealAction()) // 注册
val action = ActionRegistry.get("HEAL") // get(id) 大小写不敏感
val every = ActionRegistry.all() // 取全部
// 把一段动作字符串解析成动作
val parsed = ActionRegistry.parseActionString("heal{amount=6}")
get(id)大小写不敏感,所以"heal"、"HEAL"、"Heal"取到同一个。
1.4 直接动作链:ActionPipeline
val pipeline = ActionPipeline()
pipeline.addAction(HealAction())
pipeline.addAction(SomeOtherAction())
val ctx = ActionContext(player = somePlayer, variables = mutableMapOf("amount" to 6.0))
pipeline.execute(ctx)1.5 DSL 程序:ActionDslProgram / ActionNode
把动作写成节点树,先编译校验再执行,执行过程会写入 DebugTraceRegistry,方便事后查每一步:
val program = ActionDslProgram(
id = "fireball_cast",
nodes = listOf(
ActionNode(/* ... */),
ActionNode(/* ... */),
)
)
program.compile() // 编译
program.validate() // 校验
program.optimize() // 优化
program.execute(ctx) // 执行 —— 追踪写入 DebugTraceRegistry二、QinhActionHandler 契约(QI → QS 的缝)
ActionSystem 是 QCL 自己的动作框架;而当 QI(物品)触发后,需要把「执行什么技能」交给 QS(技能引擎)或其他模块去办。这道跨模块的缝就是 QinhActionHandler。
2.1 QinhActionHandler 接口
interface QinhActionHandler {
val handlerId: String
fun isAvailable(): Boolean = true // 默认可用
fun dispatch(context: QinhActionContext): ActionDispatchResult
}handlerId—— 处理器唯一 id(例如技能桥用"qinhskills:cast")。isAvailable()—— 处理器当前是否可用(依赖插件没装时返回false),默认true。dispatch(...)—— 真正处理一次动作派发,返回一个三态结果。
2.2 QinhActionContext —— 派发上下文
QI 把触发信息打包成 QinhActionContext 交给 handler:
| 字段 | 说明 |
|---|---|
trigger | 触发标识 |
player | 触发玩家 |
item | 触发用的物品 |
itemId | 物品 id |
handlerId | 目标处理器 id |
payload | 不透明载荷 —— QI 原样透传,handler 自行解释 |
compileEpoch(可空) | 编译纪元,用于失效判断 |
providerSnapshot(可空) | 提供方快照 |
triggerType(可空) | 统一触发类型(见下节 TriggerType) |
rawContext(可空) | 原始上下文(RawSkillContext,仅审计追踪用) |
🔑 payload 是不透明的:QI 不解释它的内容,原样递给 handler;具体格式由 QI 与 handler 双方约定,handler 负责解析。
2.3 ActionDispatchResult —— 三态结果
enum class ActionDispatchResult {
HANDLED, // 已处理
NOT_HANDLED, // 未处理(轮到下一个 handler 或回退)
HANDLER_UNAVAILABLE // 处理器当前不可用
}2.4 实现一个自定义 handler
class MyCustomHandler : QinhActionHandler {
override val handlerId = "mymodule:do"
override fun isAvailable(): Boolean = MyModule.isLoaded()
override fun dispatch(context: QinhActionContext): ActionDispatchResult {
if (!isAvailable()) return ActionDispatchResult.HANDLER_UNAVAILABLE
val player = context.player ?: return ActionDispatchResult.NOT_HANDLED
// payload 由本 handler 自行解释
val spec = context.payload as? Map<*, *> ?: return ActionDispatchResult.NOT_HANDLED
// ... 执行你的逻辑 ...
return ActionDispatchResult.HANDLED
}
}三、TriggerType —— 统一触发类型枚举
整个秦淮生态用同一套触发类型,避免各模块各写各的字符串。
| 枚举值 | 含义 |
|---|---|
RIGHT_CLICK | 右键 |
LEFT_CLICK | 左键 |
SHIFT_RIGHT_CLICK | 潜行 + 右键 |
SHIFT_LEFT_CLICK | 潜行 + 左键 |
SHIFT_TOGGLE | 潜行切换(按下/松开) |
DOUBLE_SHIFT_TOGGLE | 双击潜行切换 |
DOUBLE_RIGHT_CLICK | 双击右键 |
DOUBLE_LEFT_CLICK | 双击左键 |
HOLD_RIGHT_CLICK | 长按右键 |
HOLD_LEFT_CLICK | 长按左键 |
CI_TEST | CI 测试触发 |
COMMAND | 命令触发 |
API | API 触发 |
PASSIVE | 被动触发 |
UNKNOWN | 未知 |
3.1 与旧字符串互转
val key: String = TriggerType.RIGHT_CLICK.legacyActionKey() // → "right_click"
val type = TriggerType.fromLegacy("right_click") // 伴生方法,字符串 → 枚举legacyActionKey()—— 把枚举转成旧的字符串键(如right_click),兼容老配置。fromLegacy(字符串)—— 伴生对象方法,从旧字符串解析回枚举。
四、QiTriggerMapper —— 把 Bukkit 事件映射成 TriggerType
QiTriggerMapper 负责把 Bukkit 原生事件翻译成统一的 TriggerType:
// 从 PlayerInteractEvent 映射(仅主手;右/左键的 air/block,潜行则映射成 SHIFT_*)
val type: TriggerType? = QiTriggerMapper.fromInteract(interactEvent)
// 从潜行切换事件映射
val sneakType = QiTriggerMapper.fromSneakToggle(toggleEvent) // → SHIFT_TOGGLE
// 取旧字符串键
val legacy: String = QiTriggerMapper.legacyKey(type)
fromInteract只处理主手,右键 / 左键的 air 与 block 都映射,玩家处于潜行时映射为SHIFT_*;若不匹配返回null。
五、QISkillUseEvent —— 跨模块事件总线
QI 与 QS 共用同一个事件 QISkillUseEvent,它是 PlayerEvent 且 Cancellable,是两个模块之间唯一的事件总线。
5.1 字段与标志
构造:QISkillUseEvent(player, payload, trigger, item?, itemId?, triggerType, rawContext?)
| 标志 | 说明 |
|---|---|
skillHandled | 技能是否已被处理(handler 设它来声明「我接了」) |
castResult(可空) | 施法结果 |
castAttempted | 是否尝试过施法 |
fallbackInvoked | 是否走了回退 |
mythicInvoked | 是否调用了 MythicMobs |
primaryPipeline | 是否主管线处理 |
cancelled | 是否取消(Cancellable) |
另有静态 HANDLER_LIST(Bukkit 事件所需的处理器列表)。
5.2 监听示例:读 payload,设 skillHandled
技能引擎一侧的典型监听 —— 收到事件后解释 payload,处理完把 skillHandled 标记为 true:
class SkillEngineListener : Listener {
@EventHandler
fun onSkillUse(event: QISkillUseEvent) {
val player = event.player
val payload = event.payload // 不透明载荷,本引擎自行解释
val trigger = event.triggerType
val skill = resolveSkill(payload) ?: return // 解析不出技能就不接
val result = cast(player, skill)
event.castAttempted = true
if (result.success) {
event.skillHandled = true // 声明:这次我处理了
event.castResult = result
}
// 需要的话也可以 event.isCancelled = true
}
}5.3 RawSkillContext —— 只读审计上下文
构造:RawSkillContext(itemId?, item?, sneak, source = "qi")
RawSkillContext 是 QI 透传给 QS 的只读上下文,仅供审计追踪用。
⚠️ 不应据此做业务分支:
rawContext只是为了「记录这次是从哪来的」,不要拿它的字段去决定技能怎么走 —— 业务判断请用payload与triggerType。
六、QISkillBridge —— 默认技能桥对象
QISkillBridge 是一个 object(单例),把上面的事件总线封装成一个标准 handler,HANDLER_ID = "qinhskills:cast"。
object QISkillBridge {
const val HANDLER_ID = "qinhskills:cast"
fun peekCurrentDispatch(): /* 当前派发快照 */
fun dispatchViaEvent(context: QinhActionContext): ActionDispatchResult
fun clearDispatch()
}6.1 dispatchViaEvent 流程
dispatchViaEvent(context) 是核心方法,流程为:
- 用传入的
QinhActionContext构建 一个QISkillUseEvent; - 通过
PluginManager触发该事件(于是所有监听者,如 §5.2 的技能引擎,都有机会处理); - 按结果返回:根据事件的
skillHandled/cancelled标志,返回HANDLED或NOT_HANDLED。
val result = QISkillBridge.dispatchViaEvent(qinhActionContext)
when (result) {
ActionDispatchResult.HANDLED -> { /* 已有引擎接住 */ }
ActionDispatchResult.NOT_HANDLED -> { /* 没人处理,可回退 */ }
ActionDispatchResult.HANDLER_UNAVAILABLE -> { /* 不可用 */ }
}辅助方法:peekCurrentDispatch() 查看当前派发快照,clearDispatch() 清理派发状态。
七、整条链小结
玩家交互(Bukkit 事件)
└─ QiTriggerMapper.fromInteract → TriggerType
└─ QI 打包 QinhActionContext(trigger / payload / triggerType / rawContext)
└─ QinhActionHandler.dispatch(...) ← 各模块实现的契约
└─(默认实现)QISkillBridge.dispatchViaEvent
└─ 构建并触发 QISkillUseEvent(共享总线)
└─ 各引擎监听 → 设 skillHandled
└─ 返回 HANDLED / NOT_HANDLED / HANDLER_UNAVAILABLE📖 继续阅读
- 条件与表达式.md —— 与动作搭配的条件判断 / 表达式求值
- 脚本API.md —— 脚本化扩展
- API概览.md —— QCL 对外 API 全貌
- ../05-参考/术语表.md —— QI / QS / handler / payload / trigger 等术语定义