Skip to content

CoreLib 公开 API

编译依赖

xml
<repositories><repository><id>jiuwu-releases</id><url>https://repo.crypticlib.com/repository/maven-public/</url></repository></repositories>
<dependency><groupId>emaki.jiuwu.craft</groupId><artifactId>emaki-corelib-api</artifactId><version>4.8.1</version><scope>provided</scope></dependency>
kotlin
repositories { maven("https://repo.crypticlib.com/repository/maven-public/") }
dependencies { compileOnly("emaki.jiuwu.craft:emaki-corelib-api:4.8.1") }

仅编译依赖

API Jar 不能放入 plugins/,也不得 bundle、shade 或 relocate。EmakiCoreLib-4.8.1.jar 已内嵌未 relocate 的 API 类;同名类由不同 ClassLoader 定义后,Bukkit 事件类型和静态 bridge 都会失配。

路线 A:窄的第三方门面

EmakiCoreLibApi 只服务第三方插件。其余 Emaki 业务模块继续通过共享 runtime classpath 直接连接 CoreLib 实现类;本门面不是完整隔离层,也不会扩展成 CoreLib 全部内部服务。

公开范围包括:dialogs()、Folia-safe scheduling()、统一物品显示名、configured item 构建/patch、组件 capability 元数据,以及 owner-scoped 自定义 action 注册。文本/YAML、GUI 基础设施、表达式与条件引擎、语言/生命周期/PDC/经济/占位符、内部事件总线和 assembly 都保持内部。

先检查 EmakiCoreLibApi.status().usable()installuninstall@ApiStatus.InternalBridge 是 runtime-owned @ApiStatus.NonExtendable;第三方不得调用、缓存或实现。

线程与窄门

  • itemDisplayName、configured item build/patch、component capability 与 action registry 查询可任意线程;实时 stack 不得并发修改。
  • EmakiScheduling 提供 global/entity/location/async 的 owner-aware 调度;异步 body 不得访问 Bukkit。
  • CoreLibDialogs.show/close 要求目标玩家 owner thread;使用 scheduling 跳转。
  • ItemBuildResult 是独立完整结果模型(success、克隆 item 与 issues),不再包装为 EmakiResult
  • 自定义段按种类分三个方法注册:registerActionStage(Plugin,CoreActionStage)registerActionSource(Plugin,CoreActionSource)registerActionGate(Plugin,CoreActionGate),都返回可关闭的 CoreStageRegistration。没有 source 参数,也没有按 id 注销的方法:段只能通过返回的句柄撤销,或在 owner 插件禁用时由 CoreLib 自动撤销,因此一个插件无法注销另一个插件的段。
  • executeActionLineAsync(Plugin, String, CoreActionExecutionContext) 可从任意线程调用。future 可能在调用线程或最后 stage 的执行域完成;continuation 访问 Bukkit 前必须通过 scheduling() 调度。编译失败与业务失败都会正常完成为结构化结果,不依赖异常。
  • 结果是 CoreActionExecutionResult,包括 statusreasonKey/reasonArgumentsdiagnostics、有序 stageskeptTargets;状态为 SUCCESSSKIPPEDPARTIALCOMPILE_FAILEDEXECUTION_FAILEDINVALID_REQUESTUNAVAILABLE
  • 注册表查询为 actionStages() / actionStage(id)actionTriggers() / actionTrigger(id)onStageRegistryRebuilt(owner, callback) 保留兼容的“同 owner 后注册替换前一个”语义;需要同一插件注册多个相互独立回调时使用 addStageRegistryRebuildListener(owner, callback),各回调独立追加,返回可关闭的 CoreStageRebuildRegistration

就绪契约

每个 Emaki 模块在自己的数据加载完成、进入 reload、以及关闭后,向 CoreLib 集中发布就绪状态。消费方据此区分"数据还没加载好"和"确实没有这条数据"。

whenReady(Plugin owner, String moduleName, Runnable callback) 在目标模块数据就绪后执行回调,返回可关闭的 ReadinessRegistration。要点:

  • 回调只跑一次。模块 reload 会重新经历 not-ready → ready,但已触发的回调不会重放。需要跟随每次 reload 用 addModuleListener(见下节),或在使用点重新判断。
  • 不要在回调内重新注册 whenReady 来模拟常驻监听:回调运行时模块已被标记为就绪,重新注册会走"已就绪"分支同步执行,再注册再执行,直至 StackOverflowError
  • 同一 owner 注册两次是追加第二个回调,不是替换。
  • moduleName 传字面量(如 "EmakiItem")而非目标模块 API jar 里的常量,类加载原因同 ApiCapability.of(String)
  • 目标模块已就绪时回调同步执行,此时返回的句柄是 inactive。CoreLib 不可用或参数不可用时同样返回 inactive 句柄。
  • 线程:任意线程可调用;回调运行在把模块标记为就绪的那个线程上,不保证是 Bukkit owner thread。触碰玩家、背包、世界或 GUI 前必须显式调度。
  • owner 插件被禁用时,它注册的回调会被丢弃。

isModuleReady(String moduleName) 是轮询版本,适合诊断或"没就绪就跳过"的调用点;需要"就绪后必然执行一次"时用 whenReady

监听重载

whenReady 只回答"加载好了没有",回答不了"之后又重载过没有"。缓存了其他模块内容的消费方需要后者:

java
EmakiCoreLibApi.addModuleListener(this, "EmakiItem", phase -> {
    switch (phase) {
        case LOADING, ABSENT -> myCache.invalidate();
        case READY -> myCache.rebuild();
    }
});

ModuleReadinessPhase 三个相位:

相位含义消费方该做什么
LOADING模块开始替换数据立即失效缓存
READY数据已加载可用重建缓存。首次加载与每次 reload 后都会触发
ABSENT模块已停用丢弃缓存。同一会话内可能再次启用,届时会依次收到 LOADINGREADY

whenReady 的差异:

  • 常驻,每次状态跃变都通知,直到句柄被关闭。
  • 同一 owner 对同一模块重复注册是替换而非追加,onEnable 跑两次不会让缓存重建两遍。
  • 注册时若模块已就绪,不会立即回调。立即回调是 whenReady 用来补漏信号窗口的手段,常驻监听没有这个窗口;需要注册时的当前状态就查 isModuleReady
  • 状态被重复发布时不会重复通知(相位只在真正跃变时触发)。多个模块在同步与异步 reload 两条路径上都会发布就绪,去重收在 CoreLib 内部。
  • 同一次 READY 里,常驻监听先于 whenReady 的一次性回调执行。这样同时用了两者的消费方,其一次性初始化读到的是已重建的缓存。
  • 线程whenReady:运行在发布状态的那个线程上,不保证是 Bukkit owner thread。
  • owner 插件被禁用时监听器被丢弃;ABSENT 相位丢弃监听器,因为消费方无从得知自己需要重新注册。

只需要"知道重载发生了"就用这个接口。CoreLib 不提供让第三方触发其他模块重载的入口。

LOADING 到数据真正被替换之间存在窗口:异步 reload 的模块(Item、Attribute、Cooking、Gem、Skills、Strengthen)在 LOADING 之后仍会短暂返回旧数据。这是既有时序特性,不是保证。

模块自身的 status().ready() 判据是"数据已加载",不是"组件已构造"。因此 reload 窗口内 status() 会报 loading,返回 EmakiResult 的方法会返回 unavailable();而返回 SetListboolean 等无法表达不可用的纯查询方法,在 reload 期间可能返回陈旧或空结果——不要把空结果读成"不存在该条目"。

EmakiResult

EmakiResult.ok() 返回 EmakiResult<Unit>。穷尽处理三种变体:Success(value)Partial(value,reasonKey)Failure(kind,reasonKey,placeholders)。在 EmakiResult<T> 接口层统一读取可选负载时使用 optionalValue();具体 Success/Partial record 仍有直接 value() 访问器。

FailureKind 精确为 UNAVAILABLENOT_FOUNDINVALID_INPUTREJECTEDCANCELLEDTARGET_OFFLINEWRONG_THREADINTERNAL_ERRORreasonKey 是机器键而非玩家文本。