Skip to content

Addon Cost

HoshinoYumeka edited this page Sep 7, 2026 · 1 revision

ICost 家族

首页附属接口文档 | 下一页:版本与两端安全

给某个操作定价:下载 App、打印笔记之类。

com.november.mcphone.api.cost.ICost              一项代价
com.november.mcphone.api.cost.ItemCost           扣物品的实现
com.november.mcphone.api.cost.EmcCost            扣 EMC 的实现
com.november.mcphone.api.cost.IAppPriceProvider  给 App 报价
com.november.mcphone.api.cost.IEmcWallet         EMC 来源
com.november.mcphone.api.cost.EmcWallets         EMC 来源的挂载点
两端。签名中没有客户端类型,服务端代码可以引用
起于 1.0.40

⚠ 本包的实现类两端都会加载,其中不得出现任何客户端类型,否则专用服务器启动即崩。 见两端安全


ICost

public interface ICost {
    boolean canAfford(Player player);   // 付得起吗
    boolean consume(Player player);     // 真扣;扣不动返回 false
    Component describe();               // 给玩家看的一句话
}

约定

方法 约定
canAfford 界面每帧调用,必须廉价且无副作用
consume 只在服务端调用;必须先确认够了再动手,不能扣到一半发现不够
describe 例如 "1 × 末影箱"

consume 只在服务端调用,是因为客户端物品栏只是副本,扣了不存档还对不上号。

常量与工厂方法

成员 说明
ICost.FREE 白送。占位用,省得调用方到处判 null
ICost.of(ItemLike item, int count) 要几个某种物品;描述取 mcphone.cost.item
ICost.emc(long amount) 要多少 EMC,见下
ICost.matching(Predicate<ItemStack> filter, int count, Component description) 自定规则的物品消耗

matching 用于物品种类之外还要看别的条件的情形,例如"空白的书与笔"——它与写过字的是同一种物品,区别在数据组件上。

ItemCost

public record ItemCost(Predicate<ItemStack> filter, int count, Component description)
        implements ICost

从背包扣物品的实现。多数情况用 ICost.of 即可。

行为 说明
计数范围 遍历 Inventory 的全部隔间,盔甲栏与副手也算
创造模式 getAbilities().instabuild 为真时,canAffordconsume 一律返回 true 且不扣物品
扣减顺序 先数总量,不足直接返回 false;够了才按槽位顺序逐格 shrink

EmcCost

public record EmcCost(long amount) implements ICost

以 EMC 计价,真正扣钱的是当前接上的 IEmcWallet

行为 说明
创造模式 ItemCost,一律通过
钱包不可用 canAfford / consume 返回 falsedescribe()mcphone.cost.emc_unavailable 把"要多少"与"为什么用不了"一起说
consume 不抢先调用 canAfford:把"判"和"扣"拆开正是竞态的来源,由钱包一次做完

amountlong 而非 BigInteger:这是价格,由定价者手写;余额的表示方式留给钱包实现自己决定。


IAppPriceProvider

public interface IAppPriceProvider {
    Map<ResourceLocation, ICost> prices();   // App id → 代价
}
注册 SPI:META-INF/services/com.november.mcphone.api.cost.IAppPriceProvider
两端
扫描时机 第一次有人查价格时扫描一次
未被报价的 App 即为免费,不需要显式声明 ICost.FREE
同一 App id 被多方报价 先注册者生效,后来者被忽略并告警
返回空表 代表"我不给任何 App 报价",不是错误

扫描发生在注册表就绪之后,因此可以按注册名查询别的模组的物品;查不到就不要登记这一条。

内建 App 的价格同样走这条路,见 feature/store/BuiltinAppPrices


IEmcWallet / EmcWallets

EmcCost 自身不知道 EMC 从何而来。实现 IEmcWallet 并在加载阶段挂载一次即可。

IEmcWallet

方法 说明
boolean isAvailable() false 时界面把购买按钮画灰并显示 unavailableReason()
Component unavailableReason() 不可用的原因,界面直接显示这句话
boolean canAfford(Player player, long amount) 每帧可能被调用,必须廉价且无副作用
boolean withdraw(Player player, long amount) 真扣,只在服务端调用;不够则原样不动
Component describeBalance(Player player) 当前余额,返回 Component 而非数字,排版由实现方决定

接口刻意不提供"读余额"的数值方法:ProjectE 的 EMC 早已超出 long(用 BigInteger),接口写死 long 就得在"判断够不够"之前截断。比较与扣减都在实现内部完成。

EmcWallets

public final class EmcWallets {
    public static final IEmcWallet NONE;              // 永远不可用
    public static IEmcWallet get();                   // 永不为 null
    public static boolean set(IEmcWallet wallet);     // true 表示接上了
    public static boolean isAvailable();
}
EmcWallets.set(new ProjectEWallet());
行为 说明
挂载点数量 全局一个。已有人接管时 set 返回 false、保留原来那个并告警,不静默顶掉
传入 null 返回 false 并告警
无人接管 get() 返回 NONEisAvailable() 恒为 falsecanAfford / withdraw 恒为 false

NONEcanAfford 返回 false 而非 true:返回 true 会变成"没接 EMC 反而白送"。

截至当前版本,尚无真实的 IEmcWallet 实现。EmcCost 因此永远付不起,按钮画灰并说明原因,既不崩溃也不白送。


首页附属接口文档 | 下一页:版本与两端安全

Clone this wiki locally