CoreLib 公开 API
编译依赖
<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>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()。install、uninstall 是 @ApiStatus.Internal,Bridge 是 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,包括status、reasonKey/reasonArguments、diagnostics、有序stages与keptTargets;状态为SUCCESS、SKIPPED、PARTIAL、COMPILE_FAILED、EXECUTION_FAILED、INVALID_REQUEST、UNAVAILABLE。 - 注册表查询为
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 只回答"加载好了没有",回答不了"之后又重载过没有"。缓存了其他模块内容的消费方需要后者:
EmakiCoreLibApi.addModuleListener(this, "EmakiItem", phase -> {
switch (phase) {
case LOADING, ABSENT -> myCache.invalidate();
case READY -> myCache.rebuild();
}
});ModuleReadinessPhase 三个相位:
| 相位 | 含义 | 消费方该做什么 |
|---|---|---|
LOADING | 模块开始替换数据 | 立即失效缓存 |
READY | 数据已加载可用 | 重建缓存。首次加载与每次 reload 后都会触发 |
ABSENT | 模块已停用 | 丢弃缓存。同一会话内可能再次启用,届时会依次收到 LOADING、READY |
与 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();而返回 Set、List、boolean 等无法表达不可用的纯查询方法,在 reload 期间可能返回陈旧或空结果——不要把空结果读成"不存在该条目"。
EmakiResult
EmakiResult.ok() 返回 EmakiResult<Unit>。穷尽处理三种变体:Success(value)、Partial(value,reasonKey)、Failure(kind,reasonKey,placeholders)。在 EmakiResult<T> 接口层统一读取可选负载时使用 optionalValue();具体 Success/Partial record 仍有直接 value() 访问器。
FailureKind 精确为 UNAVAILABLE、NOT_FOUND、INVALID_INPUT、REJECTED、CANCELLED、TARGET_OFFLINE、WRONG_THREAD、INTERNAL_ERROR。reasonKey 是机器键而非玩家文本。