计算器指令

计算器是 OpenModsLib 唯一直接面向玩家的功能。通过客户端的 =* 指令可以即时求值数学表达式、定义变量与函数、切换数值后端,并从磁盘脚本批量执行。整个指令树的构建入口是 CommandCalcFactory(src/main/java/openmods/calc/command/CommandCalcFactory.java:35)。

引擎内部机制见计算器引擎,后端差异见类型后端,其余 OpenModsLib 指令见指令总览。

基本信息

属性 值
指令前缀 =(由 CommandCalc 构造函数强制拼接 "=" + name)
根组件构建 CommandCalcFactory 的 root 字段(CommandCalcFactory.java:39-251)
指令注册 src/main/java/openmods/proxy/OpenClientProxy.java:101-105
开关 LibConfig.enableCalculatorCommands(默认 true)
脚本目录 <游戏目录>/scripts(OpenClientProxy.java:99)
侧限制 仅客户端,服务端无此指令
默认模式 INFIX(CalcState.java:269)

功能

根组件的完整子命令表

CommandCalcFactory.root(CommandCalcFactory.java:39)是一个 MapCommandComponent,含 6 个子命令:

根子命令 定义行 是否注册为顶层指令 组件类型 说明
config :40 是 → /=config MapCommandComponent 管理计算器实例(8 个嵌套子命令)
execute :163 是 → /=execute TerminalCommandComponent 执行脚本文件
let :201 是 → /=let TerminalCommandComponent 绑定全局变量
fun :211 是 → /=fun TerminalCommandComponent 定义全局函数
eval :229 是 → /=eval(别名 /=) TerminalCommandComponent 求值表达式
echo :245 否 TerminalCommandComponent 原样输出文本

echo 是本条目最需要注意的一处不对称:它在根组件中已完整实现(CommandCalcFactory.java:245-250,直接 sender.addChatMessage(new ChatComponentText(args.getTail()))),但 OpenClientProxy.java:101-105 只注册了 config/eval/fun/let/execute 五条,没有为 echo 注册顶层 ICommand。因此 /echo 不可直接输入,但 /=execute <脚本> 执行脚本时每行都经由 root.execute()(CommandCalcFactory.java executeScript 方法),echo 在脚本内完全可用。

CommandCalc 构造函数 this.name = "=" + name(src/main/java/openmods/calc/command/CommandCalc.java:27)强制给所有顶层指令加 = 前缀。=eval 注册时额外传入别名 "="(OpenClientProxy.java:102),得到 /=eval + 别名 /=。

/=config — 计算器实例管理

config 是唯一的嵌套 MapCommandComponent(CommandCalcFactory.java:40-161),含 8 个终端子命令:

子命令 定义行 参数 实际行为 非法输入的处理
new :43 <类型> 新建计算器,类型取自 CalculatorType(5 选 1) 抛 openmodslib.command.calc_invalid_type,并列出全部合法类型
load :65 <名字> 切换到已命名计算器 抛 openmodslib.command.calc_invalid_name
store :81 <名字> 给当前计算器命名 —
pop :88 无 压栈(见下方命名缺陷) —
push :95 无 出栈(见下方命名缺陷) 栈下溢抛 openmodslib.command.calc_stack_underflow
set :106 <键> <值> 设置计算器属性 键不存在抛 calc_invalid_key,其它异常抛 calc_cant_set
get :130 <键> 读取计算器属性 键不存在抛 calc_invalid_key
mode :149 <模式> 切换 PREFIX/INFIX/POSTFIX 抛 openmodslib.command.calc_invalid_mode,列出全部合法模式

两个栈指令回报的栈大小分别使用 openmodslib.command.calc_after_push / calc_after_pop 语言键。

已核实缺陷:/=config push 与 /=config pop 的指令名与实际行为互换。

指令 pop(CommandCalcFactory.java:88)调用的是 state.pushCalculator()(:90)并回报 calc_after_push;指令 push(:95)调用的是 state.popCalculator()(:97)并回报 calc_after_pop。

CalcState 侧的实现本身正确且符合直觉(src/main/java/openmods/calc/command/CalcState.java):

public int pushCalculator() {                     // :294
    calculatorStack.push(active);                 // :295  确实压栈
    return calculatorStack.size();
}

public int popCalculator() {                      // :299
    setActiveCalculator(calculatorStack.pop());    // :300  确实出栈
    return calculatorStack.size();
}

即方法名与行为正确,指令名与所调用的方法相反。玩家输入 /=config pop 实际执行压栈,输出提示 “Pushed calculator, stack size: N”;StackUnderflowException 也只会在 /=config push(真正出栈)时抛出。需按行为而非名称理解。

/=eval — 表达式求值

用法 /=eval <表达式>。行为由当前模式(ExprType.hasSingleResult)决定:

  • 单值模式(PREFIX/INFIX):state.compileExecuteAndPrint(sender, expr),直接以文本形式回报结果
  • 栈模式(POSTFIX):state.compileAndExecute(sender, expr),不返回值,改为回报栈大小(openmodslib.command.calc_stack_size)

表达式解析失败时抛 openmodslib.command.calc_syntax_error_path,运行失败时抛 openmodslib.command.calc_runtime_error_path。

/=let 与 /=fun — 定义符号

指令 用法 行为 回报键
/=let =let <名字> <初始化表达式> state.compileAndSetGlobalSymbol,把结果绑为全局符号 openmodslib.command.calc_set
/=fun =fun <名字> <参数个数> <函数体> state.compileAndDefineGlobalFunction,定义全局函数 openmodslib.command.calc_function_defined

/=fun 的参数个数必须能解析为整数,否则抛 openmodslib.command.calc_invalid_number。

/=execute — 脚本执行

用法 /=execute <路径>,路径相对于 <游戏目录>/scripts。执行流程:

  1. 目录逃逸防护:checkIsParent(scriptDir, scriptFile) 逐级上溯 getParentFile() 比对规范路径,若目标不在 scripts 目录内则抛 openmodslib.command.calc_not_child(CommandCalcFactory checkIsParent 方法)。这是明确的路径穿越防护。
  2. 文件存在性检查:非文件抛 openmodslib.command.calc_not_file
  3. 逐行执行:executeScript 用 BufferedReader 逐行读取,每行经 WhitespaceSplitters.fromString 拆词后交给 root.execute(sender, args),行尾 # 起始的注释行为空串,不产生副作用
  4. 回报:输出 openmodslib.command.calc_executed_count(已执行行数)

Tab 补全会扫描 scripts 下的文件与目录,目录补全时追加 /(CommandCalcFactory execute 分支的 getTabCompletions)。

计算器栈

CalcState(src/main/java/openmods/calc/command/CalcState.java:41)维护:

  • 活动计算器 active(:265,初始为 CalculatorType.DOUBLE)
  • 计算器栈 —— pushCalculator(:294)/ popCalculator(:299)/ restorePreviousCalculator(:284)
  • 命名表 —— nameCalculator(:304)/ loadCalculator(:312)/ getCalculatorsNames(:308)
  • 表达式模式 exprType(:269,默认 INFIX)

popCalculator 在栈空时抛 StackUnderflowException,由指令层转为 openmodslib.command.calc_stack_underflow。

游戏内上下文符号

所有后端都由 SenderHolder.addPrinter 注入 p(打印栈值,零返回)与 print(取值,:78,101)。数值类后端额外注入发送者方块坐标:

符号 含义 注入位置
_x / _y / _z 指令发送者的方块 X / Y / Z 坐标 CalcState.java:128,136,144(DOUBLE)、:161,169,177(FRACTION)、:194,202,210(BIGINT)
player 玩家对象 CalcState.java:229(MULTI 专有)

坐标读取经 SenderHolder.getX/getY/getZ(CalcState.java:61,66,71),对非玩家发送者会因 Preconditions.checkNotNull 抛异常。

数值

数值名 值
根组件子命令数 6(config execute let fun eval echo)
注册为顶层 ICommand 的 5(缺 echo)
config 嵌套子命令数 8
计算器后端类型数 5(DOUBLE FRACTION BIGINT MULTI BOOL)
表达式模式数 3
Tab 补全覆盖的子命令 config.new(后端名)、config.load/config.store(已命名计算器)、config.set/config.get(属性名)、execute(脚本文件与目录)

交互

触发 行为
按下 = 键(无其它 GUI 打开时) CalcKey 打开聊天框并预填 = (src/main/java/openmods/calc/command/CalcKey.java)
直接输入 /=<表达式> 走 =eval 别名
聊天框未打开时按任意鼠标键 同样触发 CalcKey(CalcKey.onMouseInput)
表达式求值出错 红色聊天消息(CommandCalc.processCommand 对 NestedCommandException 设 EnumChatFormatting.RED)

CalcKey 注册的 KeyBinding 名称为 openmodslib.key.calc(显示名 “Open Calculator”),分类 openmodslib.key.category(显示名 “OpenMods”),默认键位为 Keyboard.KEY_EQUALS,可在选项视频设置中改绑。

错误处理

CommandCalc.processCommand(src/main/java/openmods/calc/command/CommandCalc.java:70)分三层处理:

  1. NestedCommandException —— 推入命令名后转红色聊天消息
  2. 其它 Exception —— 沿 getCause() 链收集全部消息,用 ', caused by ' 连接后抛 CommandException("openmodslib.command.calc_error", ...)
  3. 参数未消费完 —— 抛 openmodslib.command.calc_extra_args 并回报剩余部分

相关条目