公式加载器

公式加载器

为模组和资源包用户加载JSON到公式中。

基础库

一款面向开发者和数据包作者的模组。它可以从 JSON 文件中读取自定义公式,并通过游戏内指令或 Java 方法传入参数进行求值:

/formula <namespace:name> <parameters...>

支持的操作

算术运算符: +, -, *, /, % (向下取整余数:a - floor(a / b) * b)

双参数公式: min(x, y), max(x, y), pow(x, y), log(x, y) (以 x 为底 y 的对数), div(x, y) (向负无穷大方向的整数除法)

单参数公式: round(x), floor(x), ceil(x), abs(x), exp(x), trunc(x) (向零取整)

三角函数: sin(x), cos(x), tan(x), asin(x), acos(x), atan(x)

特殊函数 - rem(x, y): 返回余数 x - floor(x / y) * y,同时将商 floor(x / y) 写回 x。请注意,如果编写 #a = rem(#a, #b),赋值操作 #a = ... 会用返回值(余数)覆盖 rem 刚刚存入 #a 的商,因此 #a 最终保存的是余数,而非商。

单目负号无需括号即可使用:-3、-x、-min(a, b)、1 + -x 均可正常工作。

性能

公式在加载时被预编译为基于栈的字节码虚拟机,并进行了优化。短公式(4个或更少操作)受益于循环展开和超指令合并(例如,变量-变量二元运算成为单个操作码)。编译时会应用常量折叠。在典型桌面 JVM 上的基准测试:

  • 简单算术:每次求值约 9 纳秒
  • 链式条件(3 个分支):每次求值约 15 纳秒
  • 顺序步骤(3 步):每次求值约 14 纳秒
  • 三角运算带舍入:每次求值约 40 纳秒

实际上,即使是 300 万次调用也只需不到 120 毫秒。性能无需担忧。

客户端安装

此模组在客户端是可选的。如果只需要服务端计算,则无需安装在客户端。如果需要在 UI 元素中显示数值,则需安装在客户端以提供本地公式。当另一个需要双端安装的模组(例如 Tinkers' Armor Extension)深度集成本模组时,本模组成为了其传递性依赖,因此也必须在客户端安装。

开发者指南

  1. 通过 FormulaManager.getOrNull(ResourceLocation) 访问已加载的公式,并使用 formula.accept(double...) 进行求值。
  2. 使用 FormulaBuilder 以编程方式构建公式 JSON。公式在启动时从 data/<namespace>/formula/ 目录加载。

数据包创建者指南

使用 /formula 指令在游戏中传入参数并求值。这取代了原本需要数十条指令的基于计分板的计算,一次调用即可完成,显著提高了数据包的性能和可读性。

JSON 格式参考

简单公式

{
    "inputs": ["result", "left", "right"],
    "formula": "#result = #left + #right * 2"
}

顺序步骤

{
    "inputs": ["a", "b", "c"],
    "formula": [
        "#a = #b + #c",
        "#b = #a * #c",
        "#a = #b + 1"
    ]
}

If/Else 分支

{
    "inputs": ["score", "kills", "deaths"],
    "formula": {
        "if": "#kills > #deaths",
        "formula": "#score = #kills * 3 - #deaths * 2",
        "else": "#score = max(0, #kills - #deaths)"
    }
}

条件链 (if / else-if / else)

{
    "inputs": ["result", "value"],
    "formula": {
        "chain": [
            {"condition": "#value >= 100", "formula": "#result = #value * 2"},
            {"condition": "#value >= 50",  "formula": "#result = #value * 1.5"},
            {"condition": "#value >= 10",  "formula": "#result = #value"}
        ],
        "default": "#result = 0"
    }
}

混合嵌套 (顺序 + 条件)

{
    "inputs": ["result", "base", "modifier"],
    "caches": ["scaled"],
    "formula": [
        "#scaled = log(2, #base + 1)",
        {
            "chain": [
                {"condition": "#modifier > 10", "formula": "#result = #scaled * #modifier * 2"},
                {"condition": "#modifier > 0",  "formula": "#result = #scaled * #modifier"}
            ],
            "default": "#result = #scaled"
        }
    ]
}

显式输出槽

默认返回最后一步的结果。使用 "output" 可以指定其他变量:

{
    "inputs": ["dummy", "x", "y"],
    "caches": ["tmp", "extra"],
    "output": "#extra",
    "formula": [
        "#tmp = #x * #y",
        "#dummy = #tmp + #x",
        "#extra = #tmp * 2 + #y"
    ]
}

数值模式 (缩写形式)

{
    "input": 3,
    "cache": 2,
    "output": "#4",
    "formula": [
        "#0 = #1 + #2",
        "#3 = #0 * #1",
        "#4 = #3 + #2"
    ]
}

FormulaBuilder — 开发者指南

FormulaBuilder 是一个流畅的 API,用于在 Java 中生成公式 JSON。它会自动为命名变量添加 # 前缀。

I. 快速入门

// 命名变量 — 自动添加 # 前缀。"result = x + y" 会变成 "#result = #x + #y"
String json = FormulaBuilder
    .inputs("result", "x", "y")
    .flat("result = x + y + 2")
    .build();

// 数值模式 — 手动编写 #
String json = FormulaBuilder
    .input(3)
    .flat("#0 = #1 + #2 + 2")
    .build();

II. 定义输入

// 命名输入 (推荐) — 自动添加 # 前缀
FormulaBuilder.inputs("result", "amount", "tick")

// 数值输入 — 生成 "input": 3;手动编写 #0, #1, #2
FormulaBuilder.input(3)

III. 定义缓存 (中间变量)

// 命名缓存 — 自动添加 # 前缀
.caches("tmp1", "tmp2")    // 生成 "caches": ["tmp1","tmp2"]

// 数值缓存 — 手动编写 #3, #4
.cache(2)                  // 生成 "cache": 2

IV. 输出字段

.output("#result")    // 指定要返回的变量
// 如果省略,则返回最后一步的结果。

V. 公式形态

flat — 单个公式字符串。返回 FormulaBuilder。

.flat("result = x + y + 2")

array — 顺序步骤。每个步骤可以读取之前步骤的结果。返回 FormulaBuilder。

.array(
    "a = b + c",
    "b = a * 2"
)

ifelse — 简单的 if/else 简写。返回 FormulaBuilder。

.ifelse(
    "kills > deaths",           // 条件
    "score = kills * 3",        // if-true 分支
    "score = deaths"            // else 分支
)

mixed() — 进入一个 MixedBlock<P>。条目按顺序执行,可以包含 flat 公式、内联条件和嵌套块。调用 .exit() 返回父上下文(P 类型参数)。

b.inputs("a", "b").mixed()
    .flat("a = b + 1")                       // 纯公式
    .cond("b > 10", "a = b * 2")             // 内联条件简写
    .exit()                                   // 返回 FormulaBuilder
.build();

在 MixedBlock 内部,可以使用以下方法:

  • .flat(String) — 单个公式
  • .array(String...) — 顺序公式
  • .cond(String condition, String formula) — 内联条件简写
  • .cond(String condition) — 进入一个 CondBlock<MixedBlock<P>> 以处理复杂主体
  • .ifelse(String cond, String ifTrue, String ifFalse) — if/else 简写
  • .chain() — 进入一个 ChainBlock<MixedBlock<P>>
  • .mixed() — 进入一个嵌套的 MixedBlock<MixedBlock<P>>

cond(String) — 进入一个 CondBlock<P>,代表一个 {"condition":..., "formula":...} 条目。支持 .flat(), .array(), .mixed()。.exit() 返回父对象。

b.inputs("a", "b").mixed()
    .cond("b > 10").mixed()                  // 通过嵌套 mixed 实现复杂主体
        .flat("a = b * 2")
        .flat("a = a + 1")
    .exit()                                   // 退出嵌套 mixed
    .exit()                                   // 退出 CondBlock,返回 MixedBlock
.exit()
.build();

chain() — 进入一个 ChainBlock<P>。条件从上到下测试;第一个匹配的被执行。如果都不匹配,则执行 def 分支。.exit() 返回父对象。

b.inputs("result", "value").chain()
    .cond("value >= 100", "result = value * 2")   // 内联: cond(c, f)
    .cond("value >= 50").mixed()                   // 复杂主体的 CondBlock
        .flat("result = value * 1.5")
    .exit()
    .def("result = 0")                              // 默认分支
.exit()
.build();

在 ChainBlock 内部,可以使用以下方法:

  • .cond(String condition, String formula) — 内联条件简写
  • .cond(String condition) — 进入一个 CondBlock<ChainBlock<P>>
  • .def(String formula) — 默认分支的单个公式
  • .defArray(String... formulas) — 默认分支的顺序公式
  • .defMixed() — 进入一个 MixedBlock<ChainBlock<P>> 作为默认分支

VI. 嵌套块总结

入口点 返回类型 可用方法 退出返回
.mixed() MixedBlock<P> .flat() .array() .cond() .ifelse() .chain() .mixed() P
.cond(String) CondBlock<P> .flat() .array() .mixed() P
.chain() ChainBlock<P> .cond() .def() .defArray() .defMixed() P

参数化的 P 类型在编译时追踪父上下文 — .exit() 总能在不进行类型转换的情况下返回正确的父类型。

VII. 构建输出

.build()          // 返回 String — 格式化的 JSON,用于写入文件
.buildFormula()   // 返回 JsonFormula — 用于代码中直接求值

VIII. 自动添加 # 前缀规则

声明 inputs("result","x","y") 后,以下字符串会被自动处理:

输入 输出
"result = x + y" "#result = #x + #y"
"max(0, min(1, x))" "max(0, min(1, #x))"
"x >= 10" (条件) "#x >= 10"

当 # 前缀已存在时,不会再次添加:"#result = #x + #y" 保持不变,数字引用如 "#0 = #1 + #2" 也不受影响。

重要提示: 不要使用公式名称来命名变量(避免使用 min、max、pow、log 等作为变量标识符)。

IX. JSON 格式对应关系

构建器方法 生成的 JSON
.flat("x") "formula": "..."
.array("a","b") "formula": ["...", "..."]
.chain() ... .exit() "formula": {"chain": [...], "default": ...}
.ifelse(c,a,b) "formula": {"if": "...", "formula": ..., "else": ...}
.output("x") "output": "#x"
.inputs("a","b") "inputs": ["a", "b"]
.input(3) "input": 3
.caches("a","b") "caches": ["a", "b"]
.cache(2) "cache": 2

X. 典型用例

// 基本算术
FormulaBuilder.inputs("r", "a", "b")
    .flat("r = a + b * 2")
    .build();

// 带缓存的顺序计算
FormulaBuilder.inputs("r", "base", "mod")
    .caches("logval")
    .array(
        "logval = log(2, base + 1)",
        "r = logval * mod"
    )
    .build();

// 多条件链
FormulaBuilder.inputs("result", "val").chain()
    .cond("val >= 100", "result = val * 2")
    .cond("val >= 50", "result = val")
    .def("result = 0")
.exit().build();

// If/else 简写
FormulaBuilder.inputs("score", "kills", "deaths")
    .ifelse(
        "kills > deaths",
        "score = kills * 3 - deaths * 2",
        "score = max(0, kills - deaths)"
    )
    .build();

// 数值模式 (无命名变量)
FormulaBuilder.input(3).cache(1).output("#3")
    .array(
        "#0 = #1 + #2",
        "#3 = #0 * 2 + #1"
    )
    .build();

// 带条件和链的混合块
FormulaBuilder.inputs("a", "b", "c").mixed()
    .flat("a = b + c")
    .cond("b > 10", "a = a * 2")
    .chain()
        .cond("a > 100", "a = 100")
        .def("a = a")
    .exit()
.exit().build();