标签类型与合并

计算器里流转的每一个「格子」都是一个 ILabel。本 mod 定义了 4 种标签类型 和一张 5 条的合并规则表。

基本信息

属性 值
接口 me.towdium.jecalculation.data.label.ILabel
标签类型数 4(LItemStack / LFluidStack / LOreDict / LPlaceholder)
空标签 ILabel.EMPTY,实现类是内部类 LEmpty,IDENTIFIER = "empty"
合并表条目数 5
颜色常量 FORMAT_BLUE = "\u00A79"、FORMAT_GREY = "\u00A78"、FORMAT_ITALIC = "\u00A7o"

4 种标签类型

类 IDENTIFIER 承载内容 acceptPercent()
LItemStack "itemStack" Item + meta + NBTTagCompound nbt + fMeta(模糊 meta)+ fNbt(模糊 NBT) true(继承 Impl)
LFluidStack "fluidStack" FluidStack(amount 以 mB 计) false(唯一覆写为 false 的真实类型)
LOreDict "oreDict" 矿物词典名 name(如 ingotIron) true(继承 Impl)
LPlaceholder "placeholder" 一个字符串 name(占位名称,如 "example")+ 数量 true(继承 Impl)

4 个真实类型都 extends ILabel.Impl。Impl 本身不实现 acceptPercent(), 而是提供默认实现 return true;只有 LFluidStack 显式覆写成 false, 空标签 LEmpty 也覆写成 false。所以流体标签不能切到百分比模式。

LItemStack 的 NBT 键(均为 private static final):

键 对应字段
"item" Item item
"meta" int meta
"tag" NBTTagCompound nbt(源码注释 // don't modify it)
"fMeta" boolean fMeta
"fNbt" boolean fNbt

fMeta 为真时构造器会把 meta 归零(this.meta = fMeta ? 0 : meta),实现「忽略 meta」。 tooltip 会据此追加灰色提示行 label.item_stack.fuzzy_meta / label.item_stack.fuzzy_nbt, 并追加一行蓝色斜体的 mod 名。

IDENTIFIER 会被序列化进 NBT 的 "type" 键(ILabel.Serializer.KEY_IDENTIFIER = "type"), 因此这 4 个字符串是存档格式的一部分,改动会导致旧存档无法读取。

合并规则表(5 条)

ILabel.MERGER 在 initClient() 中注册:

# 左类型 右类型 合并函数 语义
1 itemStack itemStack LItemStack::merge 同一物品(含 meta/NBT 宽松规则)→ 数量相加
2 oreDict oreDict LOreDict::mergeSame 词典名完全相等 才合并
3 oreDict itemStack LOreDict::mergeFuzzy 模糊匹配:物品属于该词典且符号相反
4 fluidStack fluidStack LFluidStack::merge 同一流体 → 数量相加
5 placeholder placeholder LPlaceholder::merge 同一占位名 → 数量相加

没有 itemStack → oreDict 方向的注册。反向的合并会由框架尝试调换参数顺序 后重新查表(Merger.merge 的注释:

For different type, the framework will try reversing the order for MergeFunctions to work. So generally speaking, a and b has no priority in this function

)——即 merge(itemStack, oreDict) 会退化成查 merge(oreDict, itemStack),即规则 3。

Merger.merge 用 functions.get(a.getIdentifier(), b.getIdentifier()) 查表, 查不到就返回 Optional.empty(),即「不可合并」。

LOreDict.mergeSame(规则 2)

if (a instanceof LOreDict && b instanceof LOreDict) {
    return ((LOreDict) a).getName().equals(((LOreDict) b).getName());
}
return false;

只比 name,不比数量。

LOreDict.mergeFuzzy(规则 3)——符号检查是硬门槛

if (lod.getAmount() * lis.getAmount() < 0) {
    for (ItemStack ore : OreDictionary.getOres(lod.name)) {
        if (LItemStack.merge(Converter.from(ore), lis)) return true;
    }
}
return false;

两个关键条件:

  1. 数量乘积必须为负(一正一负)。源码里带着 // TODO check performance 注释—— 这里每次都要遍历 OreDictionary.getOres(name) 的完整列表。
  2. 必须能被 LItemStack.merge 逐个匹配上。

也就是说:同号(两个都是需求,或两个都是富余)的标签不会被模糊合并。 这与 代价列表算法 里 positive => generate; negative => require 的符号约定配合,使得「富余的铁锭」不会被并进「需求的矿物词典铁锭」。

数量与百分比

每个标签都同时携带 amount(long)和 isPercent()(读 Impl.percent 布尔)。 基类 Impl 的 NBT 键是 KEY_AMOUNT = "amount" 与 KEY_PERCENT = "percent"。

表示 percent 界面按钮 在 Recipe.multiplier() 中
绝对数量 false # 先 × 100
百分比 true % 不缩放

切换百分比会同步改写数量,这是 Impl.setPercent 的实际算术:

public ILabel setPercent(boolean p) {
    if (p && !acceptPercent()) throw new UnsupportedOperationException();
    if (p && !percent) {
        amount *= 100;          // ← 绝对 → 百分比:数量乘 100
        percent = true;
    } else if (!p && percent) {
        amount = (amount + 99) / 100;   // ← 百分比 → 绝对:向上取整除以 100
        percent = false;
    }
    return this;
}

(amount + 99) / 100 是向上取整——所以 150% 会变成 2 而不是 1, 与 代价列表算法 中 Recipe.multiplier 的 (amountB + amountA - 1) / amountB 是同一种取整风格。 对 acceptPercent() 为 false 的类型(流体、空标签)切到百分比会抛 UnsupportedOperationException;LEmpty.setPercent 更是无条件抛。

Impl.multiply(float i)

float amount = i * getAmount();
if (amount > Long.MAX_VALUE) throw new ArithmeticException("Multiply overflow");
return setAmount((long) amount);

用 float 中转,溢出前就抛 ArithmeticException("Multiply overflow")。

Impl.equals

return obj instanceof Impl && amount == ((Impl) obj).amount && matches(obj);

数量必须完全相等才判等。这正是 代价列表算法 里 set.contains(result) 循环检测能成立的前提。

LPlaceholder.state 是一个全局静态开关,Controller.loadFromLocal() 在反序列化 前后临时把它置 true 再还原,让占位符能读出百分比信息。见 数据文件。

反序列化 / 选择器注册

ILabel.initClient() 还注册了两组转换器:

组 注册内容 优先级
CONVERTER LItemStack::suggest、LOreDict::suggest、LFluidStack::suggest Priority.SUGGEST
CONVERTER LItemStack::fallback、LOreDict::fallback Priority.FALLBACK
EDITOR PickerSimple.FluidStack::new,默认 new LFluidStack(1000, FluidRegistry.WATER) "fluid"
EDITOR PickerSimple.OreDict::new,默认 new LOreDict("ingotIron") "ore"
EDITOR PickerPlaceholder::new,默认 new LPlaceholder("example", 1, true) "placeholder"
EDITOR PickerItemStack::new,默认 new LItemStack(new ItemStack(Items.iron_pickaxe)).setFMeta(true) "item"

SUGGEST 优先于 FALLBACK:suggest 用于「帮玩家猜一个更宽松的标签」 (例如把一个铁锭物品标签建议成 oreDict),fallback 只在无建议时兜底。 配方消歧 里的建议浮层就是走这条 CONVERTER.guess 路径。

CONVERTER.Priority 与标签解析优先级

还有一个两值优先级枚举(ILabel.Converter.Priority):SUGGEST 与 FALLBACK。 它在 JecaOverlayHandler.merge() 里被间接使用:Recipe.IO.INPUT 且该槽 不止一个候选时,才调用 ILabel.CONVERTER.first(list, context) 去挑代表标签; 单个候选时直接用它自己。

相关条目