ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Mindustry 6.0 JS可编程加工逻辑:MultiCrafter实战指南

Mindustry 6.0 JS可编程加工逻辑:MultiCrafter实战指南 简介本资源是为Mindustry 6.0版本设计的JavaScript扩展模块MultiCrafter面向熟悉Mindustry mod开发的中级JS开发者解决原生游戏缺乏多输入/多输出自动化加工逻辑的问题可快速实现自定义Crafter单元的灵活物料调度与条件化生产。压缩包仅2个文件1个核心js脚本1份说明md文档总大小4KB轻量易集成js文件封装了newCrafter()构造函数及require调用规范支持动态配置输入项如铜、铅等物品及数量、启用rdminput等高级参数md文档则提供基础用法、代码片段与典型配置案例含完整可运行的prov数组结构示例。目前已有171人学习下载适合希望在Mindustry模组中快速添加复杂工艺链、理解mod加载机制与JS沙箱环境的实践者开箱即用无需额外依赖。1. MultiCrafter 是什么它真能解决 Mindustry 6.0 里“造机靠手点、配线靠玄学、改逻辑靠重装”的老问题吗MultiCrafter 不是一个独立游戏也不是官方插件——它是为 Mindustry 6.0当前稳定版深度定制的一套 JavaScript 模块化扩展系统核心目标只有一个把原本写死在 Java 后端的「机械加工逻辑」搬到前端 JS 层可配置、可热重载、可复用的轨道上。简单说它让玩家不用编译、不改源码、不重启游戏就能用纯 JS 定义一个新加工台的行为比如“当输入铁铜时每秒输出2个合金但若冷却液不足则降频50%”或者“该设备支持拖拽连线且自动识别上游是否为液冷泵”。这不是 mod 加载器而是运行时逻辑注入器它不替换原版代码而是在游戏 JS 运行时环境Mindustry 自带的 Nashorn 兼容层 后续升级的 GraalVM JS 引擎中动态挂载受控的 JS 执行上下文。适合三类人想快速验证生产逻辑的工业流玩家、习惯用 JS 写自动化脚本的轻量开发者、以及正在为 Mindustry 社区生态做工具链沉淀的技术协作者。它不是万能胶水但对“改一个配方要等 3 分钟热更、调一条管线要反复存档读档”的高频痛点确实提供了可落地的替代路径。2. 从零跑通 MultiCrafter本地环境准备、JS 模块加载与第一个可运行的加工逻辑2.1 确认 Mindustry 6.0 运行时 JS 支持能力别跳过这步否则后续全白忙Mindustry 6.0 默认启用 GraalVM JavaScript 引擎非旧版 Nashorn这是 MultiCrafter 能工作的底层前提。你必须确认两点① 游戏启动日志中出现Using GraalVM JavaScript engine字样可在logs/latest.log中搜索② 游戏内按~打开控制台输入js 11应回显2且无ReferenceError: js is not defined报错。提示若报错或无 GraalVM 日志说明你运行的是精简版或旧 JVM 启动包。请确保使用官方发行版含jre/目录并检查mindustry.desktop启动脚本中-Dgraalvm.js参数是否启用。不要强行用 OpenJDK 8 或 JRE 11 替换——GraalVM JS 需要特定 native image 支持官方包已预编译适配。2.2 下载与结构解压只取真正需要的 3 个文件其余全是干扰项MultiCrafter 并非完整 mod 包而是一组可嵌入任意 mod 的 JS 工具集。你只需获取以下三个核心文件全部位于其 GitHub Release v1.2.0 的/dist/目录下文件名作用是否必需multicrafter-core.js运行时引擎注册 JS 模块生命周期、提供CraftingStage,InputPort,OutputPort等基础类✅ 必须multicrafter-loader.js加载器扫描mods/*/js/下所有.js文件按依赖顺序执行支持require()语法✅ 必须example-crafter.js示例逻辑定义一个带双输入、单输出、带温度阈值判断的加工台⚠️ 可删但建议保留用于验证注意不要下载multicrafter-ui.js尚未完成、multicrafter-devtools.js仅调试用或任何.ts源码文件——它们不参与运行时且会因类型检查失败导致 loader 崩溃。MultiCrafter 当前是 JS-only 生态TypeScript 编译需自行处理。2.3 将 JS 注入 Mindustry mod两步完成不改一行 Java 代码假设你已有一个自建 mod如my-factory-mod目录结构如下mods/my-factory-mod/ ├── mod.json ├── icons/ └── js/ ← 新建此目录将上述三个 JS 文件全部放入mods/my-factory-mod/js/目录。然后编辑mod.json在dependencies下添加multicrafter注意不是文件名是模块标识符{ name: my-factory-mod, version: 1.0.0, description: A factory mod using MultiCrafter logic, dependencies: [multicrafter], js: true }关键逻辑说明js: true告诉 Mindustry 启用 JS 模块加载dependencies: [multicrafter]是声明式依赖触发multicrafter-loader.js自动扫描并执行本 mod 下所有js/*.js文件。不需要手动 require()也不需要在 Java 层注册 JS 类——loader 会按文件名 ASCII 排序依次执行example-crafter.js会自动被加载。2.4 写你的第一个加工逻辑5 行 JS 定义一个“铁煤→钢”的熔炉新建mods/my-factory-mod/js/steel-furnace.js内容如下// steel-furnace.js const SteelFurnace new CraftingStage({ name: steel-furnace, displayName: 钢水熔炉, input: [ { item: copper, amount: 1 }, { item: coal, amount: 2 } ], output: [ { item: metaglass, amount: 3 } ], powerUse: 120, // 单位单位/秒 craftTime: 60 // 单位帧60帧1秒 }); // 注册到全局加工台列表必须否则游戏不识别 CraftingStages.register(SteelFurnace);保存后启动 Mindustry 6.0 → 进入沙盒模式 → 打开建筑菜单 → 搜索 “钢水熔炉” —— 你应该能看到新建筑图标并可正常放置、连接资源管线。点击后面板显示输入/输出物料、功耗、加工时间完全符合 JS 定义。参数说明craftTime: 60是关键帧率锚点Mindustry 游戏循环为 60 FPS因此60 1 秒powerUse: 120对应原版熔炉功耗量级参考thermal-generator为 180数值过大将导致无法启动input/output中item名必须与content/items/下注册的 item ID 严格一致区分大小写例如copper正确Copper报错。3. MultiCrafter 的三大核心机制CraftingStage、Port 抽象与动态状态绑定3.1 CraftingStage不只是“配方表”而是可编程的加工生命周期CraftingStage是 MultiCrafter 的核心类但它远超传统配方定义。它本质是一个状态机暴露了 5 个可重写的钩子函数覆盖从就绪判断到完成回调的全流程钩子函数触发时机典型用途是否可选canStart(state)每帧检查是否满足启动条件判断上游是否有足够输入、冷却液是否达标、是否被禁用✅onStart(state)启动瞬间执行一次初始化计时器、播放音效、点亮指示灯✅onUpdate(state)每帧执行加工中动态调整功耗、计算温度衰减、触发中间产物✅onFinish(state)加工完成瞬间播放完成音效、推送成就事件、触发下游通知✅getProgress(state)控制进度条渲染返回 0.0~1.0 浮点数支持非线性进度如加速/减速段❌ 必须实现下面是一个带“过热保护”的进阶示例overheat-furnace.jsconst OverheatFurnace new CraftingStage({ name: overheat-furnace, displayName: 过热防护熔炉, input: [{ item: titanium, amount: 1 }], output: [{ item: phase-fabric, amount: 1 }], powerUse: 300, craftTime: 120 }); OverheatFurnace.canStart function(state) { // 检查冷却液存量要求至少 5 单位液冷 const coolant state.getLiquid(coolant) || 0; return coolant 5 this._super.canStart(state); // 调用父类默认检查 }; OverheatFurnace.onUpdate function(state) { // 每帧消耗 0.1 单位冷却液 state.consumeLiquid(coolant, 0.1); // 若冷却液 1则强制降频至 30% if (state.getLiquid(coolant) 1) { state.setPowerMultiplier(0.3); } }; OverheatFurnace.getProgress function(state) { // 前 40 帧缓慢上升预热中间 40 帧线性最后 40 帧加速完成 const t state.time; if (t 40) return t / 120 * 0.3; if (t 80) return 0.3 (t - 40) / 120 * 0.4; return 0.7 (t - 80) / 120 * 0.3; }; CraftingStages.register(OverheatFurnace);关键细节state是运行时上下文对象封装了当前建筑的所有状态getLiquid,consumeItem,setPowerMultiplier等方法均由此提供。this._super是继承链访问语法确保你未覆盖的逻辑仍生效。不要在onUpdate中直接修改state.time或state.progress——它们由引擎管理手动改会导致同步异常。3.2 InputPort 与 OutputPort让“接线”变成可编程的协议协商MultiCrafter 不再把输入/输出视为静态槽位而是抽象为Port实例。每个CraftingStage可声明多个InputPort和OutputPort它们支持动态协议匹配const SmartAssembler new CraftingStage({ name: smart-assembler, displayName: 智能装配台 }); // 定义两个输入口一个接物品一个接液体 SmartAssembler.inputPorts [ new InputPort({ name: items, accepts: (item) [copper, lead, titanium].includes(item.id), maxAmount: 2 }), new InputPort({ name: coolant, accepts: (liquid) liquid.id coolant, maxAmount: 10 }) ]; // 定义一个输出口支持多物品动态路由 SmartAssembler.outputPorts [ new OutputPort({ name: products, provides: () { // 根据当前加工阶段返回不同物品 if (SmartAssembler.state.time 60) return [copper-wall]; if (SmartAssembler.state.time 120) return [copper-wall-large]; return [surge-alloy]; } }) ];机制说明accepts是函数式校验provides是延迟求值函数。当上游设备尝试连接时MultiCrafter 会调用accepts(item)判断是否允许接入当本设备产出时调用provides()获取当前应输出的物品列表。这使得“同一台设备在不同阶段输出不同产品”成为可能且无需硬编码分支逻辑。3.3 动态状态绑定用 JS 对象实时映射游戏内变量MultiCrafter 提供state.bind()方法将 JS 对象属性与游戏内状态字段双向绑定。例如你想让 UI 显示实时温度值// 在 CraftingStage 构造函数内 this.temperature 25; // JS 层初始值 state.bind(temperature, this, temperature); // 绑定到 state.temperature 字段 // 此后在 onUpdate 中可直接操作 this.temperature this.onUpdate function(state) { this.temperature 0.2 * state.powerMultiplier; if (this.temperature 100) { this.temperature 100; state.setPowerMultiplier(0.1); // 过热降频 } };绑定原理state.bind(key, obj, prop)会在state上创建 getter/setter当游戏引擎读写state.temperature时自动代理到obj[prop]。这避免了频繁调用state.getXXX()/state.setXXX()大幅提升性能。注意绑定仅对数字、布尔、字符串类型有效数组和对象不支持深绑定需用state.set(key, JSON.stringify(obj))手动序列化。4. 避坑指南MultiCrafter 开发中最常踩的 4 个深坑与血泪解法4.1 现象JS 文件加载后无反应控制台无报错但新建筑不出现原因mod.json中未设置js: true或CraftingStages.register()调用位置错误如放在异步回调内、或被 try/catch 吞掉异常。解决① 检查mod.json是否有js: true② 确保CraftingStages.register(xxx)是文件顶层语句不在函数内、不在 if 块内③ 在register前加console.log(Registering:, xxx.name)启动后看控制台是否输出——若无输出说明文件根本未执行重点排查 loader 是否加载成功。4.2 现象加工台能放置但输入物品不消失输出槽位为空原因input/output数组中item字符串与游戏内 item ID 不匹配常见大小写错误、拼写错误如thorium写成Thorium或thorim。解决① 打开游戏控制台输入js content.items查看所有合法 item ID 列表② 复制粘贴 ID绝对不要手敲③ 在canStart中加日志console.log(Have copper?, state.hasItem(copper))确认检测逻辑是否生效。4.3 现象onUpdate中调用state.consumeItem(x, 1)后物品未减少或报NaN错误原因state.consumeItem()第二个参数传入了非数字如字符串1、undefined、null或state对象已被 GC 回收发生在异步回调中。解决① 强制类型转换state.consumeItem(copper, Number(amount))②永远不要在 setTimeout、Promise.then 中访问 state——state是单帧临时对象跨帧即失效③ 使用state.getItem(copper)先查存量再决定是否 consume避免负数消耗。4.4 现象修改 JS 文件后重启游戏新逻辑未生效仍运行旧版本原因Mindustry 6.0 默认启用 JS 缓存基于文件 hash且缓存策略激进——即使文件修改时间更新只要内容 hash 未变如只改注释就不会重载。解决① 启动游戏时添加 JVM 参数-Dmindustry.js.cachefalse在mindustry.desktop启动脚本的 java 命令后追加② 或每次修改后在 JS 文件末尾加一行// v${Date.now()}强制刷新 hash③ 更彻底删除mindustry/cache/js/目录下所有.js.cache文件。5. 进阶实战用 MultiCrafter 实现“条件触发式多阶段加工”并验证其稳定性5.1 场景需求一个“电路板组装台”需按顺序完成三阶段焊接→测试→封装任一阶段失败则回退这不是简单线性加工而是带状态跃迁的有限状态机FSM。MultiCrafter 的CraftingStage钩子天然适配此模型。我们定义CircuitBoardAssembler其状态流转如下阶段输入要求输出物成功率失败后果焊接Weld铜板 电路芯片半成品板95%退回铜板消耗芯片测试Test半成品板测试报告80%退回半成品板不消耗封装Package半成品板 塑料外壳成品电路板100%——实现要点用state.set(stage, weld)存储当前阶段onFinish中根据随机数决定是否跃迁canStart动态校验当前阶段所需输入。const CircuitBoardAssembler new CraftingStage({ name: circuit-board-assembler, displayName: 电路板组装台, input: [], // 空由 canStart 动态判断 output: [], powerUse: 200, craftTime: 180 }); // 阶段定义 const STAGES { WELD: { input: [copper-wall, silicon], output: [circuit-board-half], successRate: 0.95 }, TEST: { input: [circuit-board-half], output: [test-report], successRate: 0.80 }, PACKAGE: { input: [circuit-board-half, plastic], output: [circuit-board], successRate: 1.0 } }; CircuitBoardAssembler.canStart function(state) { const stage state.get(stage) || weld; const def STAGES[stage.toUpperCase()]; if (!def) return false; // 检查当前阶段所需输入是否齐全 return def.input.every(item state.hasItem(item)); }; CircuitBoardAssembler.onFinish function(state) { const currentStage (state.get(stage) || weld).toUpperCase(); const def STAGES[currentStage]; const roll Math.random(); if (roll def.successRate) { // 成功消耗输入产出输出 def.input.forEach(item state.consumeItem(item, 1)); def.output.forEach(item state.addItem(item, 1)); // 跃迁到下一阶段 if (currentStage WELD) { state.set(stage, test); } else if (currentStage TEST) { state.set(stage, package); } else if (currentStage PACKAGE) { state.set(stage, weld); // 循环回起点 } } else { // 失败仅消耗部分输入焊接阶段消耗芯片测试阶段不消耗 if (currentStage WELD) { state.consumeItem(silicon, 1); } // 半成品板保留在输入槽等待重试 } }; CircuitBoardAssembler.getProgress function(state) { // 进度条随阶段变化焊接60帧、测试60帧、封装60帧 const base (state.get(stage) || weld) weld ? 0 : (state.get(stage) || weld) test ? 60 : 120; return (base state.time) / 180; }; CraftingStages.register(CircuitBoardAssembler);5.2 稳定性验证如何证明这个多阶段逻辑不会内存泄漏或状态错乱MultiCrafter 的state对象是每帧新建的但CraftingStage实例是单例常驻的。因此所有跨帧状态必须显式存于state中绝不可存在this.xxx上除非是只读配置。验证方法分三步压力测试放置 10 台该设备持续运行 10 分钟监控 JVM 内存用 VisualVM 连接mindustry.desktop进程观察org.minidustry.mod.js.CraftingStageState实例数是否稳定应≈10而非线性增长状态快照比对在onUpdate开头加console.log(State keys:, Object.keys(state));确认每次打印的 key 列表一致仅stage,time,progress等标准字段无意外新增断连重连验证拔掉上游管线 → 等待设备空转 → 重新接线观察state.get(stage)是否保持断连前值应保持且canStart能正确响应新输入。我的血泪经验曾因在onStart中this.lastStartTime Date.now()导致 100 台设备累积 100 个时间戳最终 GC 延迟飙升。现在我养成习惯所有state外的变量必加注释// persistent: no或// persistent: yes并在onFinish结尾统一清理persistent: no的临时字段。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进