玩异星工厂玩到一定年头迟早会走到这一步手里攒了几十个蓝图每个都想改一点——信号名换一套、阈值统一调、组装机配方整批替换然后在游戏里一张一张点开、一个一个改改完发现漏了两处再重来。我最初也是这么硬点的直到某天一个 800 实体的总线蓝图改到第四遍才决定把蓝图字符串拆开来看。这篇文章讲的就是这条路把压缩过的蓝图字符串还原成看得懂的结构在结构层面做批量修改再重新打包回能被游戏吃下去的字符串电路网络那一块单独展开因为它是整条链路上最容易改坏、也最容易省事的部分。不管你是刚学会导出蓝图、还没碰过字符串的新手还是已经能写点脚本、想搞清楚字段结构的老玩家下面的内容都能直接照着用。1. 蓝图字符串里到底塞了什么东西先把一个最基本的事实说清楚那串看起来像乱码的长字符串从头到尾只有三个处理步骤没有任何加密。版本字节 base64 编码的 zlib 压缩数据就这样。游戏导出蓝图时会先把整个蓝图结构序列化成一段二进制用 zlib 压一遍再套一层 base64最后在前面补一个字符表示格式版本。导入时反过来做一遍。明白了这条链路剩下的一切都只是那段二进制里有什么的问题而这个问题可以一层一层剥。1.1 首字符不属于 base64这是最常见的解码错误我第一次写解码脚本时就栽在这里。直接拿整串去 base64 解码报错去掉首字符成功。原因很简单游戏在字符串最前面放了一个版本标记字符早期是0后来出现过别的值。这个字符参与 base64 的字符集所以有些在线工具不报错只是解出来一堆垃圾数据——这比直接报错更坑因为你会以为压缩算法有问题方向全跑偏。正确顺序是先去掉全部空白字符复制粘贴经常带换行再剥离首字符剩下的才是真正的 base64 载荷。import base64, zlib def decode_blueprint_string(s: str): s .join(s.split()) # 去掉换行、空格、制表符 ver s[0] # 版本字节单独保存 raw base64.b64decode(s[1:]) data zlib.decompress(raw) return ver, data这里还有一个容易忽略的点base64.b64decode默认会静默丢弃非法字符所以字符串里混进了标点或者中文引号时它不报错只是解出一段莫名其妙的数据。加上validateTrue能强制校验调试阶段建议打开能省掉大量明明代码没错结果就是不对的时间。反方向的打包同样简单压缩级别用默认值即可游戏侧不关心你用了几级压缩只要 deflate 结果能被正确解压def encode_blueprint_string(ver: str, data: bytes) - str: return ver base64.b64encode(zlib.compress(data)).decode(ascii)1.2 解开 zlib 之后看到的是一套带字段号的结构压缩层剥掉得到的就是一份二进制数据它采用的是一种非常常见的序列化方式每个字段前面带一个字段号 数据类型的标记后面跟值。字符串和嵌套结构用长度前缀整数用变长编码浮点用定长。这意味着什么意味着你不需要拿到官方的字段定义文件也能把整份数据完整解析出来。你不知道某个字段号叫recipe还是recipe_name但你能看到字段 4 是一个字符串、值是iron-plate结合上下文基本就能猜出来。这是我在没有文档的情况下摸清结构的主要手段也是我认为这条路最适合普通玩家的原因完全可以靠样本反推。一份蓝图顶层通常只有一二十个字段其中绝大部分是重复出现的子结构。我按实际解码经验整理了一个高置信度的字段对照其余字段建议自己拿一张小蓝图试出来顶层字段号含义类型备注1item字符串普通蓝图是blueprint蓝图书是blueprint-book2label字符串蓝图显示名称长度有上限3label_color结构图标底色可省略4entities重复结构所有实体这是批量修改的主战场5tiles重复结构铺设的地砖只有坐标和名称6icons重复结构右上角图标最多四个7schedules重复结构列车时刻表跨蓝图复制时经常被忽略实体层里字段 1 是实体编号一个从 1 开始的整数、字段 2 是实体名称、字段 3 是坐标含 x 和 y 两个浮点、字段 4 是朝向0、4、8、12 分别对应四个方向。这四个字段覆盖了 80% 的批量操作需求剩下的配方、库存、控制行为、连线字段号靠实测更靠谱。1.3 用通用解析器盲解不用字段定义也能看懂既然结构自描述写一个通用的解析器就够了。核心是一个变长整数读取函数加上按数据类型分支的循环def read_varint(buf, i): result, shift 0, 0 while True: b buf[i]; i 1 result | (b 0x7F) shift if not (b 0x80): return result, i shift 7 def parse_message(buf, depth0): fields, i [], 0 while i len(buf): key, i read_varint(buf, i) field, wire key 3, key 7 if wire 0: val, i read_varint(buf, i) elif wire 2: ln, i read_varint(buf, i) val buf[i:i ln]; i ln elif wire 1: val buf[i:i 8]; i 8 elif wire 5: val buf[i:i 4]; i 4 else: raise ValueError(f未知类型 {wire}数据可能已损坏) fields.append((field, wire, val)) return fields对于wire 2的那一类值既可能是字符串也可能是嵌套结构。判断方法很土但有效递归解析一遍如果能完整消费且所有字段号都大于零就当它是个子结构否则按文本处理。这个启发式方法在蓝图数据上几乎不会误判因为字符串内容里出现能凑成合法结构的字节序列的概率极低。跑通之后你可以选择把结果打印成一棵缩进树或者转成 JSON 直接扔进任意编辑器。我个人的习惯是转 JSON 并用支持折叠的编辑器看800 个实体的蓝图也就几兆体积翻起来毫无压力。2. 把二进制翻成可编辑结构解码层的三个取舍解析出字段只是第一步真正决定后面好不好用的是中间表示怎么设计。同一个蓝图你可以把它表示成带字段号的原始列表也可以表示成带名字的对象树两者的开发体验差得非常远。2.1 我不建议从字段定义文件入手社区里确实有整理好的字段定义直接生成代码看起来很省事。我试过这条路线结论是性价比不高原因有三个。第一是版本漂移。这套结构会随着游戏版本增加字段定义文件往往滞后你拿到的版本和你游戏里导出的蓝图对不上解析时遇到未知字段要么报错要么静默丢弃后者更危险。第二是定义文件通常只覆盖常用字段冷门实体比如某些带特殊控制行为的建筑的字段可能缺失而恰恰是这些地方你最需要改。第三也是最重要的你的需求是改不是完整还原。你只需要能定位到想改的那个字段其它字段原样保留即可根本没必要把它们全部理解清楚。所以我最后采用的方案是保留原始字节 附加解读解析时把每个字段的原始字节切片也存下来修改时只替换目标字段的值其余字段原封不动地写回去。这样即使有完全不认识的字段也不会在往返过程中丢失。2.2 实体表、地砖表、连线表要分开建索引蓝图里最容易出错的地方是引用关系。实体之间通过实体编号互相引用连线、控制行为、参数化替换全是靠编号串起来的。我在中间表示里建了三张索引表实体索引编号 - 实体对象用于连线解引用。坐标索引(x, y) - 实体编号列表用于按区域框选、检查重叠。连线索引实体编号 - 出线列表每条线记录对端编号、线色、连接点。有了这三张表批量操作就从遍历数组变成了查表 改字段代码可读性完全不是一个级别。举个具体例子把某个区域内的所有实体整体平移 10 格需要改动的是所有实体的坐标而连线不需要动——因为连线引用的是编号编号不变拓扑关系自动跟着走。这个特性非常关键它意味着平移、复制、旋转这类操作在结构层面是廉价的真正昂贵的只有删实体和改实体类型因为它们会破坏编号引用。2.3 编码回写时最容易翻车的三处打包回去的时候有三件事必须检查我基本每次写新脚本都会在其中至少一项上栽一次。版本字节必须和内部记录一致。顶层结构里通常有个字段单独记录格式版本字符串首字符是另一个。这两个值不匹配时有些版本会直接拒绝导入有些不报错但字段解释错位表现是蓝图导进去之后颜色全乱或者实体错位。我的做法是解码时把首字符和内部版本号都存下来编码时用解码得到的首字符绝不自己编一个。坐标浮点精度要够。坐标是浮点数而游戏里的实体位置大量出现.5这种半格偏移。如果你在中间表示里用了单精度或者只保留两位小数往返几次之后位置就会漂移最终表现为实体重叠、传送带接不上。用双精度或者更稳妥的做法是把坐标量化成 1/256 格的整数全程整数运算回写时再除以 256。空字段和默认值不要瞎填。有些字段存在与否会改变游戏行为比如控制行为的disabled标志。你的脚本如果无脑给每个实体都写一个false绝大多数情况下没问题但某些实体类型不接受这个字段导入时就会报实体无效。原则很简单只改你确认要改的字段其余保持原样。3. 批量修改把逐个点变成写一条规则批量修改的价值不在于省时间而在于保证一致性。手动改 200 个实体一定会有几个改漏或者改错而这种错误在游戏里往往要跑上几十分钟才会暴露。写成规则跑一遍要么全对要么全错至少你知道该从哪查。3.1 实体替换的适用边界最常见的需求是替换实体类型。比如把所有某型号的组装机换成另一种或者把一批箱子统一换成某种规格。这个操作在结构层面就是改实体名称字段看起来毫无难度但有几个隐藏约束。新实体必须能容纳原有实体的所有附加信息。原来的机器里有配方、有模块、有库存换成新实体之后如果不支持这些字段游戏导入时会清洗掉或者直接判无效。我的经验是先拿一个样本改一次、导入验证确认没问题再批量跑。另外占地尺寸变化会导致重叠把一个 3x3 的实体换成 5x5周围原本紧挨着的实体就会压在一起。脚本里加一个简单的包围盒检测把所有潜在重叠的实体编号列出来人工过一遍比导入之后在游戏里找哪两块红了要高效得多。配方的批量替换更安全一些因为只改字符串字段不涉及几何。但要注意配方和机器的匹配关系把配方改成某个机器不支持的类型游戏会接受这份蓝图但机器是空转的等于埋了个隐形雷。3.2 平移与复制必须做编号重映射复制一段实体比如把一条产线复制成四条是批量操作的典型场景也是最容易写出 bug 的地方。核心步骤只有三步但每一步都不能省遍历选中实体坐标按偏移量整体平移。为每个新实体分配全新的实体编号并建立旧编号 - 新编号的映射表。遍历选中实体内部的所有连线把两端的编号按映射表替换指向选区外部的连线直接丢弃。第三步经常被忘掉忘掉之后的表现是导入成功但新复制出来的实体带着一堆指向旧实体的连线红绿线在空中乱拉整个电路网络全乱。我建议把这三步封装成一个函数任何复制操作都走它别图省事直接改坐标。还有一个细节平移量必须是整数或者半整数。有些实体比如奇数格宽的机器的中心点在整数坐标上偶数格的则在半格上。平移时如果加了 0.3 这种量实体会被判为无法放置。3.3 参数化蓝图和图标会连带失效如果你的蓝图里用了参数化把某些信号、配方、数量做成可以在放置时选择的变量批量修改必须同步处理相关的引用字段。这类蓝图里的参数用特殊名字标记实体里凡是引用到参数的地方都存着这个名字。你如果只改了实体里的实际值参数定义没跟着改导入后参数选择就失效了蓝图退化成一堆固定值。图标和标签也值得顺手处理。批量生成的系列蓝图比如一级到五级如果标签不跟着流水号改最后你会得到五个名字一模一样的蓝图找起来非常痛苦。标签字段是纯字符串加个自增后缀就行图标也是一个重复结构通常按索引顺序引用物品改动不大。4. 电路网络的批量调整电路部分是我认为整条链路上收益最高的地方。原因很实在电路逻辑是参数化的同样的结构换个数值就能复用而手动改数值恰恰是最枯燥、最容易改错的。4.1 控制行为是一棵条件树不是扁平字段控制行为是每个实体内部的一个子结构它内部按实体类型分成不同的分支常量箱用一套、判断器用一套、算数运算器用一套。批量调整时脚本的第一步永远是按实体名称筛选而不是按字段号硬改。信号本身的表示是个小结构里面通常有信号名、信号类型物品、流体、虚拟信号这两个关键字段。这里的信号名对虚拟信号来说是形如signal-A、signal-each这样的字符串跟真实物品名混在一起。做批量替换时最容易犯的错是把signal-A换成signal-a之类的大小写错误或者把物品信号和虚拟信号张冠李戴——类型字段不改只改名字导入时这个信号就变成未知信号游戏会静默把它变成问号。我的做法是维护一张名字 - 类型的对照表所有替换都查表进行查不到的直接报错中断宁可失败也不要不声不响地写进去。常量箱还额外多一层结构较新的版本里它被拆成了若干分组每组有自己的开关和一组常量。批量修改时如果只遍历最外层会漏掉分组里的内容。判断方法是先拿一个多组常量箱的样本解码一次看清楚层级再写循环。4.2 连线的红绿两色是独立编号电路连线的两个端点各自带一个编号来区分红线和绿线。这意味着同一个实体对之间可以同时有红线和绿线两条连接脚本在处理重连时必须把颜色一起带上否则会出现想加一条红线结果把原来的绿线覆盖了的情况。批量重连的典型场景是你有几十个判断器全部接到了同一个网络的信号源上现在想把其中一半挪到另一个信号源。做法是先按坐标范围筛出要改的实体收集它们的出线把指向旧源头的线删掉再按新源头重新生成。删线的时候要注意线是双向记录的还是单向记录的——我遇到的实现基本都是单个端点记录对端编号也就是成对存在只删一侧会留下悬空引用。写脚本时统一用先收集所有待删的配对再一次性双向删除的流程比边遍历边删稳。4.3 一个实际改造整版阈值统一放大说个具体的例子。我有一张按信号阈值触发开关的控制蓝图里面三十多个判断器阈值都是 1000。后来上游产能扩大需要全部改成 2500。流程是这样的解码按实体名称筛出判断器遍历每一个的条件树找到条件里那个数值字段判断它是否等于 1000等于才替换成 2500其它一律不动。跑完重新编码导入游戏验证。这里的关键在于等于才替换这个判断。如果无脑把所有数值乘以 2.5那些本来就不该动的常量比如输出信号里的固定值也会被改掉一版脑子正常运转的电路瞬间变成噪声发生器。批量操作永远应该有精确的匹配条件而不是模糊的变换规则——这是我在被自己的脚本坑过好几次之后定下的规矩。验证环节也有技巧改完之后先别急着导入主存档。开一个测试存档导进去用信号发生器把各种边界值打一轮看输出对不对。这个过程大概五分钟比在主存档里发现问题再回滚要省事得多。5. 可视化这一层我最后是怎么搭的可视化这个词听起来唬人实际做起来比你想象的简单——因为你不需要做得多好看你只需要能看明白自己在改什么。5.1 渲染与编辑的最小可用形态我的第一版编辑器只有两个面板左边一个画布右边一个树形结构。画布做的事非常朴素按坐标把每个实体画成一个带颜色的方块方块上写实体名称的首字母缩写鼠标悬停时在状态栏显示完整信息。就这样一个东西已经能解决 90% 的问题——你不用在几百个实体里靠编号猜哪个是你想改的。坐标到屏幕的映射是纯线性的缩放倍数控制在 0.25 到 4 倍之间。这里有一个坑网格线一定要画而且一定要和实际坐标对齐。我一开始没画网格结果平移操作之后完全无法判断有没有偏移半格因为这个错误在视觉上非常隐蔽。选中逻辑用矩形框选框选结果除了返回实体编号列表还顺带返回它们占据的包围盒这个包围盒后面批量平移直接能用上。右键点击实体弹一个小菜单里面就几个动作改名称、改配方、看连线、删掉。功能越少越好因为真正复杂的批量操作我会切回脚本里写规则图形界面只做快速定位和单点修改。5.2 导入前的自检清单不管用什么方式改重新编码之后我都会跑一遍自检。这份清单是从无数次导入失败里攒出来的检查项判据不通过的后果实体编号唯一性无重复、无零值连线错乱或实体丢失连线引用完整性所有端点在实体索引里存在悬空连线导入报错实体重叠包围盒两两不相交导入后被判无效或自动剔除坐标量化全部落在 1/256 格点上位置漂移传送带接不上版本字节一致首字符与内部版本号匹配导入被拒或解释错位标签与图标数量图标 1 到 4 个图标显示异常这些检查本身都不复杂加起来不到一百行代码但它们能把导入失败这个模糊的结果变成一条明确的信息。花这半小时非常值。6. 文档里不会写的几个手感问题前面讲的是怎么做这一节讲的是我做完之后回头看觉得当初应该早点知道的东西。别用真实存档做实验。这条听起来像废话但我真的见过改坏蓝图之后还在同一个档里反复导入的情况。批量操作的正确姿势是所有验证都在测试存档里做主存档只用最终确认过的成品。蓝图本身不占多少空间多存几份中间版本出问题能立刻回退。游戏对空白字符的容忍度比你想的高对你的脚本却不一定。从聊天框或者网页复制蓝图字符串时中间很可能会混进换行和空格。游戏自己会清洗所以你看不出问题但你的脚本如果没做清洗就会在后面某个环节莫名其妙地失败。解码入口第一行就把所有空白字符去掉成本为零。大蓝图会撞到实体数量上限。批量复制的时候尤其要注意一个原本 2000 实体的蓝图复制四份变 8000再叠一层就超了。具体上限随版本变化但那种编码成功、导入失败、提示信息还很含糊的情况八成是这个原因。脚本里加一个数量断言比事后排查省心。改动日志比你想的重要。我给自己的脚本加了个简单的日志每次修改都记录改了什么、改了哪些实体编号、原值是什么。一开始觉得多余直到有一次网络调错了想回滚靠日志三分钟就定位到了问题所在。从最小样本开始。学习这套结构最有效的方法是拿一个只有两三个实体的蓝图反复解码、修改、导入把每个字段改一次看效果。拿大蓝图练手的结果是你根本不知道是哪一处改动导致的异常。这套东西搭起来之后我改蓝图的流程彻底变了以前是打开游戏、找实体、点开面板、改数值现在是把字符串粘进脚本、写一条筛选规则、跑一遍、粘回游戏。一个下午的工作量压缩到几分钟而且改完之后的一致性有保证。真正花时间的部分反而前移了——变成想清楚到底要改什么而这个部分本来就不该交给工具。
阅读完成 · 觉得有帮助?