简体中文
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.28</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.28") }仅编译依赖
API Jar 不能放入 plugins/, 也不得 bundle, shade 或 relocate. EmakiCoreLib-4.8.36.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 不得访问 BukkitCoreLibDialogs.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
动画入口
CoreLib 的动画时序基础设施 (定义注册, K帧调度, 优先级抢占与冲突裁决) 通过同一门面暴露, 接口约定类在 emaki.jiuwu.craft.corelib.api.animation: AnimationDefinition, AnimationKeyframe, AnimationPriority, AnimationConflictPolicy, AnimationPlaybackHandle, AnimationListener, AnimationRegistration
registerAnimation(Plugin, AnimationDefinition): 注册一条 owner-scoped 定义, 返回可关闭的AnimationRegistration; 同一 owner 重注册同一 id 会替换旧定义, 并阻止旧定义的后续播放playAnimationAsync(Plugin, Entity, String, CoreActionExecutionContext): 返回CompletableFuture<EmakiResult<AnimationPlaybackHandle>>, 任意线程可调, future 完成线程不固定. 未知定义, owner 已禁用, 或命中AnimationConflictPolicy.REJECT冲突都以结构化失败完成. 高优先级播放会抢占当前播放, 同优先级按定义自身的冲突策略裁决. K帧动作行以传入的context作为执行输入 (context 无 caster 时把播放实体补为 caster), 其中的 Bukkit 访问在实体 owner 线程执行stopAnimation(Plugin, Entity, String): 停止该实体上由该 owner 发起的指定定义播放;definitionId为null或空白时停止该 owner 在该实体上的全部播放. 返回是否实际终止了在飞播放addAnimationListener(Plugin, AnimationListener): 注册播放监听, 返回可关闭的AnimationRegistration; 同一 owner 重复注册是替换. 回调 (onPlay/onKeyframe/onStop) 运行在实体 owner 线程AnimationPlaybackHandle三态为PLAYING / FINISHED / CANCELLED;playing()判断是否仍在推进,cancel()可重复调用且结果不变, 播放已结束或已取消后再调用不会抛异常- 注册句柄必须在
onDisable关闭; owner 插件被禁用时, 其定义, 监听器与在飞播放都会被 CoreLib 自动撤销 - 动画定义注册表在 CoreLib reload 后存活, 无需重建回调; 这与动作段表不同 -- 段表在 reload 时重建, 必须通过
onStageRegistryRebuilt/addStageRegistryRebuildListener重放注册 (见上节) - CoreLib 不可用时, 注册与监听入口返回稳定的 inactive 句柄,
playAnimationAsync以EmakiResult.unavailable()完成,stopAnimation返回false, 均不抛异常
就绪状态约定
每个 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 只回答是否完成首次加载, 不回答其后是否发生过 reload. 缓存其他模块内容的消费方需要后者:
java
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相位不丢弃监听器, 因为消费方无从得知自己需要重新注册
只需感知 reload 已发生时使用本接口. 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 是机器键而非玩家文本