我有一个文件夹,里面躺着 615 个 .txt 文件。每个文件里是一串以 0eJz... 开头的字符——这是《异星工厂》(Factorio)的蓝图字符串。
蓝图是玩家花了几个月搭出来的工厂。它现在只是一串文本,你看不出里面有什么,算不出它能产多少东西,也没法验证它是不是真的能跑。
我想让它变成能看、能算、能验证的东西。
一、字符串里到底装了什么
官方没有公开蓝图格式。资料里的说法是:Base64 编码的 zlib 流,解出来是 JSON。
照着做,第一步就失败了:
| |
我换了好几种字母表、试了 URL-safe 变体、补了不同数量的填充,全都不对。最后不猜了,直接读原始字节:
| |
0e 是 Base64 解码的第一个字节?不是。真相是——第一个字符 0 根本不属于负载。
0 是版本号,本身就是一个 Base64 字符(数值 52)。真正的负载从下标 1 开始。从第二个字符往后解,第一个字节正好是 78,那是 zlib 的合法头。
坑一:开头那个字符不是负载。
坑二:填充没被去掉。 好几处资料说 Factorio 会去掉 =,但三个文件全都以 = 或 == 结尾。去掉就刚好差两个字符,解出来是残的。
结论写在项目里的一句话:以文件为准,不以文档为准。
验证方式是往返:decode 之后再 encode,必须逐字符完全相同。三个原始文件、以及内部嵌的全部 615 个蓝图,逐字符都通过了。
二、数值不能抄,得读
蓝图里只有"这台机器在这里、朝这个方向",没有"它有多快"。速度表在游戏安装目录里。
所以我写了一个受限的 Lua 求值器(fablue/lua.py):它能处理 data:extend 语句、表达式、内置库和辅助函数,但绝不执行宿主代码——没有 require、没有文件系统、没有外部依赖。
第一版解出 1,367 个原型,还有 149 个 data:extend 解不出来。加了对跨文件、局部变量、函数返回值的支持之后:
| 修复前 | 现在 | |
|---|---|---|
| 解出的原型 | 1,367 | 4,633 |
解不出的 data:extend | 149 | 25 |
| 传送带速度 | 0 | 12 |
| 机器速度 | 0 | 17 |
剩下 25 个全是跨文件 data.raw 派生的瓦片/装饰类,不含速度表——这个上限是我报出来的,不是藏起来的。
读出来的真值(全部来自 data.raw):
| 项目 | 值 |
|---|---|
| 传送带速度 | 0.03125 / 0.0625 / 0.09375 / 0.125 tile/tick |
| 双 lane 吞吐 | 15 / 30 / 45 / 60 items/s |
| 单 lane 吞吐 | 7.5 / 15 / 22.5 / 30 items/s |
| 组装机 1/2/3 | crafting_speed 0.5 / 0.75 / 1.25 |
| 电炉 / 石炉 / 钢炉 | 2 / 1 / 2 |
一个犯了两次的 2 倍错误
×480 是双 lane 合计。推导是 0.03125 × 60 / 0.25 = 7.5(单 lane),乘 2 才得 15。
早期版本把这个常数标成 “per lane”,同时 GameDB.belt_items_per_second 又乘了一次 lanes——同一个 2 倍错误,在两个地方各犯了一次。因为是"自洽的错",测试抓不到。
现在常量改名为"双 lane 因子",新增 belt_items_per_second_lane 属性,并把推导写进注释。单位写清楚,就不会再被隐含。
三、615 个蓝图,一个 canvas
查看器的约束很硬:单文件、离线、无外链。
615 个蓝图、334,856 个实体,如果每个实体画一个 SVG 矩形,是数十 MB 的 DOM。所以:
- 一个 canvas,不是每蓝图一个
- 每实体只存 4 个整数:
x2, y2, 类别, 朝向。坐标翻倍是因为 Factorio 有大量实体放在半格上(整数存不下 0.5) - 全部数据一次性嵌进页面 → 6.04 MB
性能靠视口裁剪:懒建空间哈希 + Int32Array。一个 34,650 实体的蓝图,在深度缩放下一帧只画 13 个实体;全可见时 p95 是 8.8 ms,中位 0.6 ms。
实体还按真实尺寸绘制——尺寸取自每个原型的 collision_box(3×3 的机器是 2.4,补齐到 3 格;火箭发射井 8.4 → 9 格;传送带 0.8 → 1 格),690 个尺寸表项随页面发出。
四、让它算
看图不够,我关心的是:这个工厂到底产多少、卡在哪。
计算内核输出四类东西:净产出速率、外部输入、配比失衡、传送带饱和。
一个真实例子(34,650 个实体):
| |
核心公式全部写在代码注释里,不从别处抄:
| |
模块效应也计入了:同一个机器内的模块加成相加(三个 +50% 速度模块 = ×2.5,不是 1.5³)。有一条专门的断言守着:组装机 3 做铜缆,无模块 5.0/s,装三个速度模块 12.5/s。
五、几处"刻意做对"的判断
这些地方很容易写成"看起来对、其实错":
stone-furnace这类实体在蓝图里没有recipe字段,但 Factorio 也不把默认配方写进蓝图。所以必须报成"未知配方(可用 overrides 指定)",而不是当成"这台机器闲置"。- 信标(59 个)持有模块但不执行配方,如果当机器算,每个信标都会变成一条假"未知配方"。
- 实验室和采矿机的产出取决于科技/矿脉,单独报,不归入"未知配方"。
- “蓝图没给配方"和"蓝图给了但游戏数据里查不到"分开报。后者我核实过
mods目录确实没装 mod,所以那批 mod 蓝图算不出来是如实缺口,不是静默失败。
六、验证:334 项断言 + 三个浏览器终检
| 测试文件 | 断言 | 在验证什么 |
|---|---|---|
| test_blueprint | 14 | 编码往返、格式事实、拒绝畸形输入 |
| test_all_blueprints | 8 | 全部 615 个蓝图逐字符往返 |
| test_gamedata | 79 | 合成数据证明数值真来自文件 |
| test_gamedata_independent | 45 | 正则直接从 Lua 抠数值 + 物理独立推导 |
| test_calc | 90 | 精确数值、未知配方不静默丢弃、循环保护 |
| test_integration | 22 | 跨模块接线:模块效应→计算、尺寸→渲染 |
| test_view | 76 | 页面无外链、嵌入数据条数等于输入 |
test_gamedata_independent.py 是关键一条:它不经过提取器的 API,而是用正则直接从原始 Lua 文件里抠数值比对——提取器的 bug 无法靠"自洽"藏住。
三个真浏览器终检不是检查 HTML 文本,而是真的操作页面:缩放/平移真的改变渲染、点到饱和蓝图真的显示警告、放大后每实体像素数真的从 3.1 涨到 50,181.6。
这一层抓到了 5 个纯 Python 测试看不见的 bug:相机 NaN、缺边界、图例按数字索引查却按名字存、renderLegend() 重建时销毁了正在处理点击的节点、data-i="0" 的 falsy 边界。
这就是为什么要用真浏览器驱动页面。DOM 里的字符串说"我很正常”,不算数。
最终:334/334 通过,7/7 浏览器终检全过,端到端 6.6 秒。
七、还没做到的
- 信标效应没建模——需要空间分析(每台机器周围有几个信标),目前只在 notes 里计数。
- 传送带饱和是按物品总量、不是按段判定——蓝图 JSON 不记录机器和传送带的连接关系,要按段判就得从坐标和朝向重建拓扑,这是独立的一大块。
- K2SE 的 mod 配方读不到,因为这台机器没装 mod。装上、把
data_root指过去就能算。 - 品质加成、插入机吞吐、电力网络都还没建模。
这些限制我都写在页面的结论里,不含糊过去。
