Skip to content

相关:API概览.md · 脚本API.md · 条件与表达式.md · ../05-参考/术语表.md

⚙️ 动作系统与技能桥(ActionSystem / QinhActionHandler)

本页面向 Java / Kotlin 模块开发者,讲两件事:

  1. ActionSystem —— QCL 内置的「动作」执行框架(动作链、DSL 程序、注册表、追踪)。
  2. QinhActionHandler 契约 —— QI(QinhItems)把「触发」交给 QS(QinhSkills)等模块去「执行技能」的那道,包括统一的触发类型枚举、Bukkit 事件映射、跨模块事件总线 QISkillUseEvent、桥对象 QISkillBridge

🧩 谁该看这页:这是模块开发者的契约层。服主不直接配这里的任何东西 —— 服主在 QI 里配「触发器 → 动作」,QI 内部才会走到本页描述的契约。如果你是在写一个想接进秦淮生态的新模块(比如一个新的技能引擎),这页就是你要实现的接口。


一、ActionSystem 总览

com.qinhuai.corelib 的 action 包提供一套可独立运行的动作执行框架。它有两种用法:

  • 直接动作链:用 ActionPipeline 串几个 Action 顺序执行。
  • DSL 程序:把动作写成节点树 ActionDslProgram / ActionNode,经 compile / validate / optimizeexecute,执行时把追踪写进 DebugTraceRegistry

1.1 Action 接口与 ActionContext

每个动作实现 Action 接口:

kotlin
interface Action {
    val id: String
    fun execute(context: ActionContext)
}

执行时传入的上下文 ActionContext

成员说明
player(可空)触发动作的玩家,可能为 null(如命令 / API 触发)
variables: MutableMap变量袋,动作之间靠它传值
traceId(默认 UUID本次执行的追踪 id
debug是否开启调试追踪

读写变量与追踪的方法:

kotlin
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

kotlin
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 —— 注册与查找

kotlin
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

kotlin
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,方便事后查每一步:

kotlin
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 接口

kotlin
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 —— 三态结果

kotlin
enum class ActionDispatchResult {
    HANDLED,              // 已处理
    NOT_HANDLED,         // 未处理(轮到下一个 handler 或回退)
    HANDLER_UNAVAILABLE  // 处理器当前不可用
}

2.4 实现一个自定义 handler

kotlin
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_TESTCI 测试触发
COMMAND命令触发
APIAPI 触发
PASSIVE被动触发
UNKNOWN未知

3.1 与旧字符串互转

kotlin
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

kotlin
// 从 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,它是 PlayerEventCancellable,是两个模块之间唯一的事件总线。

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

kotlin
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 只是为了「记录这次是从哪来的」,不要拿它的字段去决定技能怎么走 —— 业务判断请用 payloadtriggerType


六、QISkillBridge —— 默认技能桥对象

QISkillBridge 是一个 object(单例),把上面的事件总线封装成一个标准 handler,HANDLER_ID = "qinhskills:cast"

kotlin
object QISkillBridge {
    const val HANDLER_ID = "qinhskills:cast"
    fun peekCurrentDispatch(): /* 当前派发快照 */
    fun dispatchViaEvent(context: QinhActionContext): ActionDispatchResult
    fun clearDispatch()
}

6.1 dispatchViaEvent 流程

dispatchViaEvent(context) 是核心方法,流程为:

  1. 用传入的 QinhActionContext 构建 一个 QISkillUseEvent
  2. 通过 PluginManager 触发该事件(于是所有监听者,如 §5.2 的技能引擎,都有机会处理);
  3. 按结果返回:根据事件的 skillHandled / cancelled 标志,返回 HANDLEDNOT_HANDLED
kotlin
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

📖 继续阅读