-
Notifications
You must be signed in to change notification settings - Fork 5
Addon Cost
HoshinoYumeka edited this page Sep 7, 2026
·
1 revision
给某个操作定价:下载 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 |
⚠ 本包的实现类两端都会加载,其中不得出现任何客户端类型,否则专用服务器启动即崩。 见两端安全。
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 用于物品种类之外还要看别的条件的情形,例如"空白的书与笔"——它与写过字的是同一种物品,区别在数据组件上。
public record ItemCost(Predicate<ItemStack> filter, int count, Component description)
implements ICost从背包扣物品的实现。多数情况用 ICost.of 即可。
| 行为 | 说明 |
|---|---|
| 计数范围 | 遍历 Inventory 的全部隔间,盔甲栏与副手也算
|
| 创造模式 |
getAbilities().instabuild 为真时,canAfford 与 consume 一律返回 true 且不扣物品 |
| 扣减顺序 | 先数总量,不足直接返回 false;够了才按槽位顺序逐格 shrink
|
public record EmcCost(long amount) implements ICost以 EMC 计价,真正扣钱的是当前接上的 IEmcWallet。
| 行为 | 说明 |
|---|---|
| 创造模式 | 同 ItemCost,一律通过 |
| 钱包不可用 |
canAfford / consume 返回 false;describe() 用 mcphone.cost.emc_unavailable 把"要多少"与"为什么用不了"一起说 |
consume |
不抢先调用 canAfford:把"判"和"扣"拆开正是竞态的来源,由钱包一次做完 |
amount 用 long 而非 BigInteger:这是价格,由定价者手写;余额的表示方式留给钱包实现自己决定。
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。
EmcCost 自身不知道 EMC 从何而来。实现 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就得在"判断够不够"之前截断。比较与扣减都在实现内部完成。
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() 返回 NONE:isAvailable() 恒为 false,canAfford / withdraw 恒为 false
|
NONE 的 canAfford 返回 false 而非 true:返回 true 会变成"没接 EMC 反而白送"。
截至当前版本,尚无真实的
IEmcWallet实现。EmcCost因此永远付不起,按钮画灰并说明原因,既不崩溃也不白送。