API 与集成
EmakiStorageApi 是 EmakiStorage 对第三方插件开放的静态 API 门面。第三方插件只需要把 emaki-storage-api 作为编译依赖;运行时由服务器中安装的 EmakiStorage 插件本体提供实现。
服务器服主只安装 EmakiStorage-*.jar,不要把 emaki-storage-api-*.jar 放进 plugins/。
Maven 依赖
<dependency>
<groupId>emaki.jiuwu.craft</groupId>
<artifactId>emaki-storage-api</artifactId>
<version>1.0.1</version>
<scope>provided</scope>
</dependency>Gradle 依赖
dependencies {
compileOnly("emaki.jiuwu.craft:emaki-storage-api:1.0.1")
}可用性检查
import emaki.jiuwu.craft.storage.api.EmakiStorageApi;
if (!EmakiStorageApi.available() || !EmakiStorageApi.isReady()) {
return;
}available() 表示 bridge 已安装;isReady() 表示 bridge 已安装且插件完成初始化。isAvailable() 与 isReady() 当前语义相同。
EmakiStorage 未安装或仍在启动时,所有方法降级为中性返回值,不抛异常。
EmakiStorageApi 静态方法
| 方法 | 返回值 | 未安装时的降级返回值 |
|---|---|---|
available() | boolean | false |
isAvailable() | boolean | false |
isReady() | boolean | false |
apiVersion() | String | 空字符串 |
pluginName() | String | 空字符串 |
readSnapshot(UUID) | CompletableFuture<StorageSnapshot> | 已完成的 StorageSnapshot.empty(playerId) |
deposit(UUID, ItemStack, long) | CompletableFuture<StorageResult> | 已完成的 StorageResult.unavailable() |
withdraw(UUID, ItemStack, long) | CompletableFuture<StorageResult> | 已完成的 StorageResult.unavailable() |
countOf(UUID, ItemStack) | CompletableFuture<Long> | 已完成的 0L |
grantSlots(UUID, int) | CompletableFuture<StorageResult> | 已完成的 StorageResult.unavailable() |
setStackLimit(UUID, long) | CompletableFuture<StorageResult> | 已完成的 StorageResult.unavailable() |
setSlotStackLimit(UUID, int, long) | CompletableFuture<StorageResult> | 已完成的 StorageResult.unavailable() |
install(Bridge) 与 uninstall(Bridge) 由 EmakiStorage 自己的生命周期调用,第三方插件不应使用。
方法语义
所有取 ItemStack 的方法都在内部归一化(clone()、setAmount(1)),并按全量 ItemStack#equals 定位条目。调用方不需要预先计算 key。
所有写方法返回 CompletableFuture,因为条目表只能在归属实体线程上修改;实现会把工作调度到该线程再完成 future。普通的业务拒绝不会让 future 以异常完成,而是作为 StorageResult 状态返回。
| 方法 | 语义 |
|---|---|
readSnapshot(UUID) | 读取分离快照,必要时从磁盘载入,离线玩家同样可读。快照不会观察到之后的存取。 |
deposit(UUID, ItemStack, long) | 存入,不触碰任何背包。达到单槽上限或槽位容量时会部分应用,需检查 appliedAmount()。会触发 StorageDepositEvent。 |
withdraw(UUID, ItemStack, long) | 从仓库扣减,不把物品交给任何人。数量不足时以 PARTIAL 返回,reasonKey 为 insufficient_stock。 |
countOf(UUID, ItemStack) | 统计存量,不存在时为 0。走快照路径,离线玩家可用。 |
grantSlots(UUID, int) | 调整授予槽位池,负值回收。 |
setStackLimit(UUID, long) | 设置玩家级单槽上限,0 恢复继承 config.yml。 |
setSlotStackLimit(UUID, int, long) | 设置某个逻辑槽位的条目级上限,0 恢复继承玩家级。该槽位没有条目时以 FAILED 返回,reasonKey 为 slot_empty。 |
写方法要求玩家在线
deposit、withdraw、grantSlots、setStackLimit、setSlotStackLimit 五个写方法在目标玩家离线时直接返回 StorageResult.unavailable(),不会静默改写未加载的存档。条目表只能与该玩家自己的 region 一同安全修改。玩家在线但数据尚未载入时同样返回 UNAVAILABLE。
只读方法 readSnapshot 与 countOf 没有这个限制。
withdraw 不触发事件
EmakiStorageApi.withdraw 直接扣减条目,不触发 StorageWithdrawEvent;GUI、命令与 Action 路径走的事务实现会触发。这是当前实现的事实,与 deposit 不对称。若你的插件依赖取出事件做校验,请注意这条路径不会被拦截。
StorageApi 接口声明了除 available() / isAvailable() 之外的全部方法(apiVersion()、pluginName()、isReady()、readSnapshot、deposit、withdraw、countOf、grantSlots、setStackLimit、setSlotStackLimit)。EmakiStorageApi.Bridge 继承自 StorageApi。
示例
import java.util.UUID;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import emaki.jiuwu.craft.storage.api.EmakiStorageApi;
import emaki.jiuwu.craft.storage.api.model.StorageResult;
UUID playerId = player.getUniqueId();
ItemStack template = new ItemStack(Material.DIAMOND);
EmakiStorageApi.deposit(playerId, template, 1000L).thenAccept(result -> {
if (result.status() == StorageResult.Status.PARTIAL) {
long left = result.remainingAmount();
// 只存进去一部分,剩余 left 单位需要自行处理。
}
});回调线程不保证是主线程。回调中若需要操作 Bukkit / Paper / Folia 状态,请自行调度到对应 owner 线程。
StorageSnapshot
不可变快照,条目按逻辑槽位顺序排列。
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
playerId() | UUID | 归属玩家。 |
entries() | List<StorageEntrySnapshot> | 按槽位顺序的条目列表,已 List.copyOf。 |
capacity() | StorageCapacity | 快照时刻的容量拆解。 |
defaultStackLimit() | long | 玩家级单槽上限,0 表示继承配置。 |
sortMode() | String | 持久化的排序模式 id。 |
entryCount() | int | 条目数量。 |
totalAmount() | long | 所有条目数量之和,溢出时饱和为 Long.MAX_VALUE。 |
empty(UUID) | 静态方法 | 返回空快照,sortMode 为 amount_desc。 |
StorageEntrySnapshot
单个条目的只读视图。
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
slotIndex() | int | 逻辑槽位索引,0 起且无空洞。 |
template() | ItemStack | 存储的物品模板,amount 归一化为 1。每次调用返回新的 clone(),可安全修改。 |
amount() | long | 存量。 |
stackLimit() | long | 作用于该条目的有效单槽上限。 |
remainingCapacity() | long | 距上限还能存多少;stackLimit <= 0 时为 Long.MAX_VALUE - amount。 |
fillRatio() | double | 占用比例 0..1;stackLimit <= 0 时为 0。 |
StorageCapacity
容量拆解。四个来源分别持久化,所以调低 base_slots 不会消耗玩家已授予或已购买的槽位。
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
baseSlots() | int | 来自 capacity.base_slots。 |
permissionSlots() | int | 来自最高的 emakistorage.slots.<n> 权限档位。 |
grantedSlots() | int | 来自命令或 API 的授予,可为负。 |
purchasedSlots() | int | 通过 GUI 扩容流程购买的槽位。 |
effectiveSlots() | int | 钳制后的实际可用总量,不为负。 |
maxSlots() | int | 配置的硬上限,0 表示无限。 |
usedSlots() | int | 当前占用的槽位数。 |
slotsPerPage() | int | 一页 GUI 渲染多少展示格。 |
freeSlots() | int | 剩余空槽,不为负。 |
overflowing() | boolean | 占用是否已超出有效容量。 |
totalPages() | int | 由有效容量推导的总页数,至少 1。 |
reachablePages() | int | 实际持有条目的最后一页,至少 1。 |
empty() | 静态方法 | 全零容量,slotsPerPage 为 1。 |
StorageResult
写操作的结果。部分成功是一等结果:向剩余空间 300 的槽位存入 1000 单位,会得到 status == PARTIAL、appliedAmount == 300、remainingAmount() == 700。调用方必须检查 appliedAmount(),不能假定请求量已全额应用。
| 字段 / 方法 | 类型 | 说明 |
|---|---|---|
status() | Status | 结果分类。 |
requestedAmount() | long | 调用方请求的数量。 |
appliedAmount() | long | 实际应用的数量。 |
reasonKey() | String | 稳定的机器可读原因,完全成功时为 null。 |
applied() | boolean | 是否应用了任何数量(appliedAmount > 0)。 |
complete() | boolean | 是否全额应用(status == SUCCESS)。 |
remainingAmount() | long | 未能应用的数量,不为负。 |
StorageResult.Status 五种状态:
| 状态 | 含义 |
|---|---|
SUCCESS | 请求量全额应用。 |
PARTIAL | 部分应用,其余被拒绝。 |
FAILED | 完全没有应用。 |
CANCELLED | 被监听器取消,reasonKey 为 cancelled。 |
UNAVAILABLE | 插件、玩家数据或归属线程不可用,reasonKey 为 unavailable。 |
集成边界
- API 只暴露静态门面与
StorageApi接口,不暴露内部服务、缓存、loader 或配置 parser。 - 写方法要求目标玩家在线且数据已载入;不满足时以
UNAVAILABLE返回而不是抛异常。 - 如果只是在配置动作链中存取物品或授予槽位,优先使用 CoreLib 动作;第三方插件代码集成再使用
EmakiStorageApi。 - 需要拦截存入、取出与付费扩容流程时使用 事件 API:这三个事件属于
emaki-storage-api,用标准@EventHandler监听。 - 只需要在文本模板中读取容量与存量时使用 PlaceholderAPI 占位符,不必接入代码。