Dyson Sphere Program
Install with App

Details

Latest version
1.9.3
Last Updated
First Uploaded
Downloads
375
Likes
1
Size
1.4MB
Dependants

更新日志

1.9.3

这一版加了三条线,而它们共用同一件东西:把能量的来路说清楚。 裂变、黑洞矿、 高密度储能——每一条的热值链都严格守恒,EnergyAudit 一条豁免都没用上。

裂变:一条矿脉,两种把核能变成电的办法

铀矿脉(矿种 24)→ 铀矿石 → 浓缩铀 → 铀燃料棒 / 薄膜裂变燃料,外加两座电厂: 裂变发电站(21.6 MW,烧水,η 0.35)和 碎片直转堆(12.96 MW,无汽轮机,η 0.60)。

热值是一条链,三级都带,所以每一级都守恒:矿石 5 MJ ──×6──> 浓缩铀 30 MJ ──×5──> 铀燃料棒 150 MJ。浓缩比 6 不是挑的——天然铀 U-235 占 0.7%,浓缩到约 4.2%。

η 是捕获率不是效率加成,上限 1.0。 0.35 是压水堆的真实热效率(堆芯出口约 330 °C, 卡诺天花板就在那儿);0.60 = 0.80 × 0.75,前者是裂变能量里带电碎片占的份额、 后者是静电收集极效率。直转卖的不是「多发电」,是「少浪费」。

顺带解锁了燃料位 bit 6 以上。 原版那个「只有 bit 0~5」的上限,唯一来源是 ItemProto..cctor 里写死的 new int[64][]InitFuelNeeds 的循环边界读的是数组自身长度, 八个读者全是 fuelNeeds[fuelMask] 的直接取值,整个程序集里燃料掩码附近没有任何 63/64 的比较。撑长数组再让原版自己重建就解锁了,不需要 preloader、不需要转译器。 真上限是 PowerGeneratorComponent.fuelMask 的 Int16 宽度,即 bit 14。

黑洞与中子星:按星体类型投放的矿脉

吸积熔晶(矿种 25,黑洞 + 中子星)和视界凝核(矿种 26,仅黑洞)。

这需要一条新机制,而理由是量出来的:单极磁石根本不走主题表——启动日志的主题表 25 张里矿种 14 出现 0 次,而 PlanetAlgorithm.GenerateVeins IL 016A 读 planet.star.type、 随后直接 veinSpots[14]++。所以 ores.jsonplacement 够不着黑洞矿。 StarVeinPatches 在五份 GenerateVeins 里各插一次调用。

它自带随机数,一次都不消耗原版的抽取序列。 从构造到稀有矿抽取之间的抽取次数是 数据相关的,中间多抽一次会让后面全部错位——症状不是报错,是悄悄换掉一整张矿图。 种子从 planet.seed 派生,先烧六次再播种第二个(照抄原版自己在 GenerateVeins 开头 IL 003A~006A 的去相关做法):DotNet35Random 的首次抽取和种子强相关,不这么做的话 六颗星球的掷点会挤在 0.53~0.61 那一小段里、chance 这个旋钮实际是坏的,而矿照常出现。 开机自检跑 2000 个种子核对分布,均值 0.505、四分位 492/496/503/509。

高密度储能:把电打包运走

电浆蓄能柜(179.82 GJ,原版满蓄电器的 ×333)、过载蓄能柜(359.64 GJ), 外加第十二座巨型建筑 奇点储能厂(60 GW 充放,往返效率 1.00)。

整条线一个热值都没有,审计一个焦耳都看不见——而那是对的。 能量走 PrefabDesc.maxAcuEnergyexchangeEnergyPerTick,不是 ItemProto.HeatValue; 充能消耗的是电网里真实存在的电,原版蓄电器就是这么工作的。所以它不增加总发电量, 它的价值是让跨星系送电变得可行(原版一块 540 MJ 实在太碎)。

一座建筑服务两档,逐台可切,而且原版替我们存。 实测 emptyId/fullId 虽然来自 PrefabDesc,但它们是逐组件字段、进存档(Export @00B2/@00BE、Import @00CA/@00D6), 所以逐台改写之后不需要 IModCanSave。切换在枢纽窗口下方的一行 ◀ 服务:… ▶

这一版踩过、并且都装了检测的坑

  • 模型号:铀矿脉抢走了 machines.json 钉着的 719,引发九座建筑连环位移—— 而模型号进存档。现加 ReportModelBudget 每局报余量,并修正了它自己算错上界的问题 (它按报告时的 dataArray.Length 算,而真正决定能否分配的是注册那一刻的值)。
  • 建造栏槽位 / 合成面板格位:奇点储能厂先后抢了小型速采机的 1213 和 「原油X射线裂解」的 3201。这两个编号空间都由两个配置文件共享,而两边都不写这件事。 位移警告现在会点名是谁抢的并明说「别照这条把配置钉成新号」;格位重复登记会 WARNING。
  • 窗口之争UIGame.OnPlayerInspecteeChange 里 23 个组件判断后匹配的赢, 多一个 powerExcId 就打开了会空引用的枢纽窗口。断言从三处改到四处。
  • 入网PowerExchangerComponent.networkId 的写入点只有 PowerSystem.OnNodeAdded ——isPowerExchanger 只表示「它能换能」,isPowerNode 才表示「它挂在电网上」。 漏了这一步的症状是面板打开、模式能选、功率写着 60 GW,而「电网 #0」,一焦耳都进不去。
  • fuelNeeds 越界InitProductionMaskfuelMask 当数组下标,而撑长那一步原本 排在它后面。现在改在使用点兜底——prefabDesc 是调用方同一个方法里刚连上的, 链上更早的位置都扫不到新电厂的掩码,所以那里是唯一可行的位置。

勘误

  • 原版氢的热值是 9 MJ,不是 8(ores.jsonOreConfig.cs、CLAUDE.md 三处都写错)。
  • EnergyAudit 只审 ores.json 自己的配方,原版配方一条都不看。

1.9.1

这一版是一根新燃料棒,外加它牵出来的三处勘误。 真正值得记的不是那根棒, 而是查它的过程发现:本 mod 为了堵一条永动机而重锚氢的热值,顺手把一条没被碰过的 原版配方变成了发电机,而能量审计结构上看不见原版配方。

preloader 没有改动(仍是 1.7.0 定的那版存档格式),但按惯例版本号一起走。

新物品:精炼油燃料棒(化工厂:精炼油 ×60 + 塑料 ×6 + 钛块 ×2 + 能量矩阵 ×2 + 结构矩阵 ×2 → ×1,10 秒)。把精炼油掺聚合物增稠剂凝成膏体、灌进钛壳,现实对照物是 凝胶烃燃料,胶凝剂用量按真实的 5~10% 质量分数取。

它填的是原版本来就空着的一级:氢燃料棒 54 MJ 到氘核燃料棒 600 MJ 之间是 11 倍断层。

每根能量 机甲功率 单根能撑多久
氢燃料棒 54 MJ ×3 18
精炼油燃料棒 270 MJ ×2 135
氘核燃料棒 600 MJ ×4 150

270 MJ 是推出来的不是挑的:燃料棒是定体积容器,所以比每升能量——压缩氢约 4.8 MJ/L、 液氢 8.5、凝胶烃约 35,比值 4.1~7.3 取 5,54 × 5 = 270;油的份数再由守恒倒推 (270 ÷ 4.5 = 60)。功率故意不跟着往上走ReactorInc 模的是烧得多快, 而氢的层流火焰速度约 2.9 m/s、烃类约 0.4 m/s。于是它和氢燃料棒互不压倒, 氘核在两条轴上都还压着它。

火力发电厂和可燃性液体发电厂都烧得了它(后者 650 °C 一档,比直接烧精炼油的 750 低一级—— 增稠剂积炭、钛壳烧完留 TiO₂)。严格守恒,所以固定发电永远不如直接烧油, 它卖的是单件密度和续航:一格背包 300 根 = 81 GJ。

FuelSurvey 多了一段「燃料能量阶梯」:每种燃料的热值、机甲功率倍率、哪些建筑烧得了它, 外加它自己的来源配方。判据是 FuelType != 0 而不是名字——本来只想打那四根燃料棒, 但那要拿名字当判据,而本仓库为「按名字挑就会按名字漏」已经付过五次账。

修正:ReactorInc 的原版参照表,五条错了四条。 那份表是凭记忆写的, 一路抄进了 machines.json 的注释和中英两份特性文档。实测(原油 ×1.2、精炼油 ×1.3、 蓄电器(满)×2、氢燃料棒 ×3、氘核 ×4、反物质 ×6、金色 ×12)之后, 「锂电池的 ×2.5 落在氢燃料棒和氘核之间」这句面向玩家的说明是假的—— 它落在蓄电器(满)和氢燃料棒之间。数值没改(能量那一轴仍是原版的 6 倍),改的是说法。

修复:氢燃料棒那条原版配方在凭空造能量,氢 ×10 → ×56。

这不是给原版做平衡,是补本 mod 自己捅出来的洞。 原版那条是 90 MJ 进、108 MJ 出 (原版氢 9 MJ),+18 MJ,无伤大雅。而本 mod 为了堵蒸汽重整那条永动机,把氢按煤锚点 压到 1.96 MJ——同一条没被碰过的原版配方于是变成 19.6 MJ 进、108 MJ 出,5.5 倍EnergyAudit 只审 ores.json 自己那张表,看不见它;而万倍速制造台跑得了它, 于是它成了「钛 → 电」的转换器,每块钛约 66 MJ。

56 = ceil(108 ÷ 1.96),取整到 56 是让差额落在的一侧(−1.76 MJ)而不是正的。 代价说清楚:氢燃料棒的氢耗涨了 5.6 倍(每根 5 份 → 28 份)。换来的是两根棒从此 是同一把尺子——都严格守恒,选哪根只看密度和功率,不看谁在偷能量。 不要这条改动就删掉 recipes.json 里那条 setCount,不用重新编译。

为此 vanillaEdits 多了 setCount(改已有原料的份数,写的是绝对值所以天然幂等; 只动值不动数组长度,对存档安全)。而且每条 vanillaEdits 现在都会当场打出可燃能量的 进出账和差额,正差额超过 1 MJ 还额外 WARNING——因为审计够不着原版配方, 那就只能让改动点自己带检查。

记一笔:EnergyAudit 那条绿线的含义要说准——它是「没有本 mod 的配方在造能量」, 不是「没有配方在造能量」。

修正:燃料阶梯原本报的是中间态。 它排在 vanillaEdits 前面,于是实测打出 「氢燃料棒 ← 钛块×1 + 氢×10」,而当局真正生效的是 ×56。那张表存在的全部意义就是 「这条燃料到底要花多少料」,报中间态等于没报。挪到 vanillaEdits 之后了—— 验末态,不验自己那一步。只挪了它一个:另外两个普查读的是 proto 字段和 prefab, 后面没有任何一步会改,跟着一起挪就成了照搬。

修正:原版氢的热值是 9 MJ,不是 8。 ores.jsonOreConfig.cs、CLAUDE.md 三处都写着 8, 都是凭记忆写的。改写那行日志一直在打它真正读到的值,只是没人回头看。

1.9.0

这一版是物品品质从「点数能流动」变成「整条链能用」。 上一版它只能随货物流动、 只有一条效果(建筑省电,而且实际上基本没生效);这一版喂料四条路、出货四条路全部接通, 合成的规则重写过一次,建筑省电真的生效了,拆掉建筑也会把品质退回来。

存档格式没变(仍是 preloader 在 1.7.0 定的那一版),但 AssemblerComponent 多了一个不入档的新槽位 quaPendingItems所以 preloader 必须和插件一起更新

⚠️ 一个还没定位的问题:有玩家报「从菜单覆盖存档、再读回来,品质从 50 变成 0」, 不好复现。这一版装了常驻诊断(存盘前 / 读档后各报一次背包里的品质总分), 两行一对比就能把问题夹在「存的时候就没了」和「读的时候丢了」之间。 碰到的话请把这两行发出来。

修复:拆掉建筑,退回来的货没有品质

玩家报的是「放下、X 拆掉、拿回背包,品质变 0」。那不是丢了,是从来没还过: 建筑的品质存在 QualityBuildStore 里,是一个耗电折扣,不是货物属性; 原版拆除返还的是一件全新的建筑物品,它从来没有过品质。于是「放下再捡起来」 成了一个纯粹的品质销毁器,而挪建筑是玩家天天干的事。

这和已经修过的另外两条拆除路不对称:拆储物箱会把箱里的分还回来、拆装配机会把 产物缓冲的分还回来,唯独拆建筑本身不还。补上之后三条口径一致。

挂点是 PlayerAction_Build.DoDismantleObject,而且是唯一挂点——拖拽框选那条路 (BuildTool_Dismantle.DismantleAction 的 IL 06ED / 08AE)两处都是转调它。 前置写侧信道就够,不用转译器,因为退货那一步调用之前没有任何人写 Q0 (擦除在调用之后的 IL 0163),而 DismantleFinally(里面才删存档条目)排在退货后面, 所以前置去查的时候记录还在。这条依赖是显式的,一次性日志把「退了多少分」打出来, 那行在而货是 0 分就说明被盖了。

改动:秒完成扣料的品质改成量背包差值,不再依赖侧信道

上一版从侧信道接,而本仓库已经在 TakeItemFromPlayerUseHandItems 上栽过两次 ——两个都是算对了再自己擦掉。量背包差值不依赖任何协议,而且精确: 背包少掉的那些分,正是这次建造吃掉的。侧信道的值仍然读一下,只进诊断,两个数不一致时那行就是线索。

修复:「建造秒完成」开着时,建筑完全拿不到材料品质

上一条修完之后,玩家放下一座 50 分材料的建筑,日志仍然一行都没有。 原因不在品质那一侧——建造秒完成 默认开着,而它是本仓库自己重写的一条建造路径InstantBuildPatches.PayConstructionBeforeGameTick 的后置里用 player.package.TakeTailItems 付账,QualityBuildPatches 那七个方法的作用域之外 (五把建造工具的 CreatePrebuilds + DoUpgradeObject + PlaceItems)。

更糟的是它当时特意把侧信道清零了,注释写着「建好的建筑不保留品质(建筑没有品质槽位)」 ——那句在写的时候对,效果层出现之后就过时了:建筑确实没有品质槽位, 但材料的品质会折进它的耗电。每一步都成功,功能整个不在。

这是「本仓库为某个功能加的规则,悄悄限制了后来加的另一个功能」那一类,而这次两边都是我们自己的 ——和催化剂槽位撞 SyncStorageLayout、钻头槽位撞 StorageCount 完全同形。

修法:那条路改成「清 → 调 → → 再清」(出参方向的正确做法是读回来再清, 直接清掉等于把被调方刚算好的答案扔了),按桩号累计到付清为止——背包只够付一半时 这一座会分几个 tick 付,平均分要拿整座建筑的料算——再挂进已有的 QualityBuildStore.SetPending,后面 Promote 一路照旧。 外加这条路自己的一次性日志:它不走「建造扣料」,少了那行就又分不开「接上了」和「没接」。

修复:手上那一摞用掉一部分时品质不扣(以及建筑省电基本没生效)

玩家问「现在制造的建筑按照分数进行节能了吗」。日志里 建造扣料效果层 各 0 行 ——而那分不开「补丁没生效」和「这局没建东西」,因为这个功能压根没有开机状态行。 本仓库记了六次的同一条,这是第七次,状态行已补上。

只能回去读 IL,Player.UseHandItems 那 61 条指令里有两个各漏一半的分支:

分支 A(手上比要用的多,只用掉一部分)——只扣件数,不扣品质。

0024: ldfld <inhandItemQua>  → V_2      // 读到原始【整摞】的分
0031: call split_inc(ref 件数, ref inc, 用几个)   // 只劈了 inc
003A: ldloc.2 ; stsfld Q0               // Q0 = V_2,原始全额
003F: call set_inhandItemInc(V_1)       // 手上的 qua := 全额

对照 Player.TakeItemFromPlayer:它在同样位置有一句 ProjectEdenQualityChannel::Split(...)这里没有。于是从手上拿走几个之后,剩下的货顶着整摞的点数——单件分数越拿越高

分支 B(手上的全用光)——算对了,然后在返回前自己擦掉0061 发布,0070/007B 两次擦除), 和上一轮 TakeItemFromPlayer 一模一样的形状。

所以 QualityBuildPatches 里那句「手上那一格没有品质槽位,按 0 分计」——结论碰巧还对, 理由早就过时了:字段有,值也算出来了,只是被擦了。而手持一摞建筑一个个放, 走的全是这条路,分数全丢;省电比例于是取决于多少料是从背包直接扣的,那是个任意数。

修法是量差值,精确而非近似:前置记下整摞的分和件数,后置按用掉的件数算出该走的那一份, 把剩下的写回去。两个分支同一个式子就都对了(分支 B 里原版已写 0,而「原分 − 全额 = 0」)。 顺带把建造那一刀的手上部分接上真品质。

不往侧信道发布。 后置确实跑在被调方那两次擦除之后、控制权回到调用方之前,技术上发得出去 ——但没有调用方在读它,而一个没人读的寄存器留着非零值,正是「品质凭空长出来」的来源 (实测过一次每件 1010 分对上限 100)。修好字段本身就够:建造和喂料两刀都是 量玩家身上少了多少,字段准了它们自动跟着准。

改动:合成的品质规则从「求和」改成「按件数加权平均」

玩家问的是一个具体问题:电动马达要铁块和齿轮,两种都是 50 品质,造出来为什么是 100?

因为旧规则是求和。 产物品质 = 投入品质之和,按产出件数摊开——于是多件变少件时 每件分数必然往上翻,而翻多少倍由 requireCounts 决定。那是个纯粹的平衡数字, 没人是为品质挑的:4:1 的配方翻四倍,1:4 的砍四分之一。它不是设计,是算术漏出来的。

规则的口号写着「品质是可加点数,永远守恒,只会被稀释」。对合并(掺普通料平均分下降) 和拆分(按比例)都成立,对合成不成立。而它和设计的头条直接冲突——启动日志每局都打 「品质的来源只有提纯厂」,可在求和规则下,顺着生产链每一级都在造品质。

顺着这个问题扫 MaxPerItem 的每一处用法,发现四个夹子互相矛盾:机器合成不夹、 手搓夹在 100、存量巡检每 30 秒把箱子夹回「件数 × 100」、显示也夹在 100。 所以那台马达存的是 150、显示 100、进箱子 30 秒后真的变成 100 ——玩家两次看到的都是 100,中间那个数悄悄变过

新规则:产物的每件分数 = 各投入的每件分数,按件数加权平均。

铁块 ×2 @50 + 齿轮 ×1 @50            → 50 分
铁块 ×2 @50 + 齿轮 ×1 @0(普通齿轮)  → (2×50 + 1×0) / 3 = 33 分

一次解决三件事:口号变成真的(产物永远不会比最好的那种料更好,掺粗料就往下拉)、 提纯厂重新是唯一来源、构造上就出不了上限所以不需要夹子——手搓那个夹子一并删了, 留一个永远不会触发的夹子只会在真出事那天把问题盖住。代价说清楚:总分不再守恒 (多件变少件时点数会少),但守恒从来不是给玩家的承诺,「好料造好东西」才是。

两处实现细节:

  • 在途池多了一个分母。 平均值的分子和分母必须同时跨 tick,只留分子就还原不出 「这些分是多少件料带来的」。所以 ExtraFields 加了 quaPendingItems:Int32。 也试过不加字段(把平均值直接存进 quaPending、后来的 cycle 覆盖它),那是个近似 ——两个 cycle 同时在途且投料品质不同时会取后者,而本仓库的规矩是宁可多一个字段。
  • 顺带修掉一个静默漏洞。 结算的前置原本只在「投料格里有品质」时布防, 于是带品质的料被吃掉、玩家接着喂普通料时,产物出来的那一 tick 布不了防 ——分数卡在在途池里永远出不来,一声不吭。判据改成「这台机器和品质有没有关系」, 把在途池非空也算上。

新增:手动喂料也带品质了(喂料侧)

分拣器喂料一直是好的(PlanetFactory.InsertInto 两个重载各有 6 处 quaServed,1c 自动孪生的)。 手动喂料的四条路一条都没接上——全程序集扫一遍「谁写 AssemblerComponent.incServed」, 缺孪生的正好是这四个,而且它们是一组:

方法 方向
PlanetFactory.EntityFastFillIn Shift 点一下,背包/手上 → 机器
UIAssemblerWindow.OnServingBoxChange 打开面板时铺那个临时投料框(机器 → 框)
UIAssemblerWindow.SyncServingStorage 面板开着时每帧刷新那个框(机器 → 框)
UIAssemblerWindow.OnManualServingContentChange 玩家拖完写回机器(框 → 机器)

后三条必须一起接。 只接「框 → 机器」,每帧刷新就把框里的品质抹成 0,下一次拖拽把 0 写回机器; 只接「机器 → 框」,玩家放进去的好料一写回就变 0。中间那个 servingStorage 是个真的 StorageComponent,它的 GRID.qua 早就是主干道载荷——鼠标拖拽(UIStorageGrid.HandPut/HandTake) 一直在给它写品质,缺的只是它和 quaServed 之间的那两句拷贝。

Shift 塞料这条只能靠量,而且是被迫的。 侧信道的协议是「被调方在返回前写、调用方读回来」, 而实测 Player.TakeItemFromPlayer 两条分支都把它擦掉了:背包那一支在 TakeTailItems 之后紧跟 ldc.i4.0 ; stsfld Q0(IL 0025);手上那一支明明算出了拿走的那份品质 (ProjectEdenQualityChannel::Split,IL 00A6),却只拿它算了个余数,随后同样擦掉(IL 00E6)。 所以 EntityFastFillIn 读回来的永远是 0——这是「擦除不等于覆盖」的又一例,而且擦除在 被调方体内,调用方那边再怎么转译都够不着

但玩家那一侧已经正确扣掉了(背包格子和手上那一格都是孪生过的), 所以「玩家少了多少分」就是「机器该多多少分」——量差值是精确的,不是近似。

新增:产物品质走得出机器了(出货侧)

结算那一步把投入的品质算进了 quaProduced,但那是个只有本 mod 认识的新槽位 ——原版没有 incProduced,所以没有任何一条原版出货路径会去读它。 货一搬出机器,品质就留在缓冲区里,表现是「机器里算出来了,搬出来还是 0」。

produced[]四条出路,四条全接上了:

出路 做法
分拣器取货(PlanetFactory.PickFrom 装配机分支) 前置记 produced,后置比差值,把分写进侧信道
拆除退料(FactorySystem.TakeBackItems_Assembler 转译器:一处 Q0 = 0 改成读 quaProduced[j]
面板点产物图标(OnProductIcon0/1Click 改读真的 quaProduced,不再只有提纯表那份
巨型建筑自己的带子和物流槽位 两处出口各扣一次

四条必须一起接,漏一条就是静默膨胀。 漏掉的那条路上是「件数搬走了、分数留在原地」, 剩下的货顶着整格的点数,单件分数凭空往上跳——这和本仓库在 StationStore.inc 上记过的 「每次搬运都白送一次增产」是同一个形状,只是换了个字段,而且同样一个字都不报。 DrainSlot 是这四条共用的那一个扣减口,所以它们不可能再各说各话。

三个只有读 IL 才看得出来的点:

  • PickFrom(uint ioTargetTypedId, …) 的低 24 位不是实体号。 开头解出 kind = typedId & 0xFF000000,然后按 kind 跳七个分支,而每个分支把那 24 位当成 自己那张池子的下标0x01000000beltPool0x02000000assemblerPool, 再往后是实验室 / 储物箱 / 物流站),都不是 entityPool。 拿它去查 entityPool[id].assemblerId 会查到一台毫不相干的机器,然后把那台机器的 品质扣掉发给这一次的货——分数长在错的地方,而且不报错。第一版就是这么写的, 是把两个重载的开头各 dump 一遍才发现的。两个重载也不是转调关系 (1087 条 vs 1026 条指令),所以两个都得打。
  • 拆除退料那一处只能用转译器。 要写的值每一格产物各不相同,前置只跑一次、 看不见循环变量;写的时机又必须在 TryAddItemToPackage 之前。 形状 ldc.i4.0 ; stsfld Q0 ; call TryAddItemToPackage 在整个方法体里恰好一处 ——投料那一条前面是 ldloc V_13(1c 已经接好的真品质),调用之后的擦除后面跟的是 stloc,两者都落不进这个形状。数不对就一条都不改并大声报错。
  • 先算后扣。 巨型建筑往带子上塞货时 InsertAtHead 会因为带子满了而失败, 一件货都没走;先扣后搬的写法会在那一刻把分数凭空销毁,而且带子越堵丢得越快。 所以那条路上先 PeekSlot 出要带走的分,塞进去成功了再 DrainSlot

新增:制造会把投入的品质结算进产物

在这之前品质走到机器的投料格就停住了。实测:AssemblerComponent.InternalUpdateincServed 4 次、读 quaServed 0 次,而 produced 根本没有对应的品质数组 ——提纯过的料进了装配机,造出来的东西是 0 分。

1c 不能自动补这一段,这是设计而不是遗漏。 搬运层可以照着 inc 机械复制,因为那里 每一次 inc 访问都是「把点数搬到另一个容器」;而结算不是搬运——原版读 incServed 是为了算增产加成(多出几个、快多少),照抄那套数学会让品质变成第二种增产剂。 规则必须由人定。

规则是守恒:产物拿到的品质 = 这一次结算真正消耗掉的那些投入的品质之和, 按产出件数摊到各个产物上。和合并(相加)、拆分(按比例分)同一条规则, 对玩家只有一句话要解释:品质是可加点数,永远守恒,只会被稀释。

三处实现细节,每一处都是本仓库已有的规矩在起作用:

  • 产物缓冲区多了 quaProduced,而它不是孪生字段。 原版没有 incProduced (增产点数不进产物缓冲),所以没有可镜像的源。1a 为此多了一张「品质独有的新槽位」表, 而且绝不能把它写进 MainlinePayload——那样 produced[i] += 件数 会被镜像成 quaProduced[i] += 件数把件数当成品质,一路不报错。字段数 30 → 31, 孪生语句仍是 1108(新槽位确实在变换作用域之外)。
  • 「这一 tick 到底产出没有」是量出来的。 前置记 served/produced,后置比差值 ——和 RunExtraCyclesproduced[0] 同一个做法:原版哪天多加一道门, 重算那套条件会悄悄错位,量差值不会。
  • tick 路径而且并行_assembler_parallel):快照缓冲区 [ThreadStatic] 惰性建, 没有品质就第一时间返回,不分配、不拼串、不走 LINQ。

两个守恒上的细节,都是「不这么做就会静默漏分」:整格吃光时整格带走(否则整数除法的零头 永远出不去),按件数分配的余数给最后一个有产出的产物(否则每结算一次漏几分, 漏的量随产量线性增长)。吃了料却没产出的那一 tick,品质退回投料格而不是蒸发。

游戏里验过了(不是只跑离线断言):

提纯厂第一次注入——配方 6673 产出 1360 件,每件 50 分
带子普查:带上 18 堆货,其中 1 堆带品质,最高每件 50 分   ← 巨型建筑→带子这一段以前没单独验过
制造:**投料格里第一次出现品质**
制造:第一次结算出品质——这一次吃掉的投入带着 100 分,摊到 1 件产物上,每件 100 分

守恒对得上:吃掉 2 件 50 分的铁块 = 100 分,产出 1 件齿轮拿到 100 分。

出货口已经接上了(见下一条),所以这条链现在是通的。

排查这一步花了七个来回,六个是探针的错,值得单独记。 每一次「日志里没有那一行」 都有三种互斥的解释——玩家还没搭好 / 搭好了但喂料路径丢品质 / 喂到了但结算没触发 ——而它们在日志上长得一模一样:什么都没发生。事件式探针天生分不开这三种。

三次改进各解决一层,最后一层才真正终结了它:

  1. 加「投料格里第一次出现品质」——「什么都没发生」的分支也要留一行(本仓库第七次栽在这条上);
  2. 改成状态式普查(每 30 秒数一遍:几台在跑 / 几台投料格有品质 / 几台产物有品质) ——状态分得开三种情况,事件分不开;
  3. 汇总数仍然指不出下一步,于是逐台摊开配方和每格的件数与品质。 那一行一出来立刻见分晓:第 3 台(配方 5):[铁块] 5 件/品质 0 ——机器在吃铁块,而那批铁块本身就是 0 分。提纯过的和普通的是同一个物品、同一个图标, 玩家肉眼分不出来,我也没法从汇总数里看出来。

期间还栽了两次已经写在 CLAUDE.md 里的坑:把两条不该闩死的普查挂进了一个第一次成功 就闩死的方法里(同一个方法第三次),以及直接访问 preloader 加宽过的 Cargo.stack 导致线上崩溃(那条规矩当时只为方法签名写过,字段那一半没写下来)。

修复(preloader):放下一部分,手上剩下的品质被清零

探针一行抓住的:

放 1 件|前 格子 ×0 品质 0 | 手上 铜块×50 品质 2000
       → 后 格子 铜块×1 品质 40 | 手上 铜块×49 品质 0

放出去那 1 件拿到 40 分(对),手上剩的 49 件从 1960 掉到 0

根因在 UIStorageGrid::HandPut 的写回那一笔: SetHandItemInc_Unsafe(退回来的零头 + 拆剩的份额)——两个操作数都是载体局部, 整条语句里一个载荷字段都没有,所以按载荷切语句看不见它; 而转发合成当时只认「一条 ldarg/一条 ldloc/一个常量」,认不出一整段算式。 于是那次调用前没人写侧信道,访问器把手上品质写成了残留的 0。

放宽了转发合成的实参判据:整段是纯取值、而且里面真有品质来源就认。 发射器本来就够用(BuildForwardToCallee 把整段交给 TwinValue,加减乘除自己会递归), 缺的一直只是这道闸。

放宽之后立刻撞出第二个,它是这次唯一真正新的教训: taken 记的是语句的末指令,而 V = X.inc … AddCargo(…) 这类真语句的末指令是 stloc 而不是那次 call,所以同一次调用被转发合成又认领了一遍。 两条语句改同一处,先插入的把后面的下标全顶走,后一条就「认得但拼不出来」 ——PilerComponent::InternalUpdate 4 处 Blocker。 现在另记一份真语句覆盖到的全部下标,只给转发合成用。 去重要按「这条指令被谁占了」算,不是按「谁的末指令是它」算。

孤儿剪枝跟着改了:转发语句现在记下实参那一段的区间(ArgFrom/ArgTo), 按那一段里的局部剪。按整次调用的区间剪会把每一条转发都误杀——那里面总有几个 itemId、件数之类和品质无关的局部。

末态:孪生语句 1105 → 1108,侧信道 887 → 891HandPut 里 11 处 SetHandItemInc_Unsafe 调用点全部在调用前写了寄存器; Unhandled/Pending/Blockers 全空,26168 个方法体悬空分支 0、收窄后写品质 0。

探针的额度改成只花在带品质的搬运上。 上一轮 40 行额度被一次搬 0 分普通铜的连点 全部吃光,真正想看的那次操作一行都没留下。这和「日志要报无聊状态」那条规矩方向相反, 两者不冲突:状态行要无条件打印(不然「关着」和「没装」分不开), 而有额度的采样探针,额度花在没信息量的样本上就等于没探。

提示栏的品质行同时显示每件分和整格总分

原来只写每件分(「优秀(47)」),所有者要两个都看得见,现在是 「优秀(47) 总 75200」。两个数各有各的用处:每件分是可比的那个量 ——「这堆料比那堆好吗」只有它答得了;总分是真正存在字段里的数, 品质是可加量、合并就是两堆总分相加,对账要用它。

顺带订正了两份特性指南里一句过时的断言:它们写着「品质不可能出现在物品 tip 里」, 而 QualityTipPatches 早就把它放进去了——提示栏反过来问鼠标压着哪一格, 所以容器的属性也能显示。现在写成三个显示位置(提示栏 / 物流站槽位 / 提纯厂面板)。

总分按原样打印,不跟着每件分一起夹上限。 夹的只是显示用的每件分, 而总分是字段实数——两者在上限巡检的两次之间可能对不上, 那时候「顶尖(100) 总 11140」一眼就能看出这一格越界了、正等着被压回去。 拿夹过的每件分乘件数倒推总分会把这个信号抹掉。

物流站槽位那条标签仍然只写每件分:它是贴着进度条的窄标签,再加一个数就把进度条盖掉了。

修复(preloader):合并两堆,品质归零而且会传染

玩家报:50 个品质 50 的铜块和 50 个品质 0 的铜块合在一起,显示 0,而且旁边那几堆也跟着变 0。

根因是鼠标手上那一格Player.inhandItemInc)。拖拽、拆分、手动合并、Shift 塞进建筑—— 全都要在手上过一道,而它整个不搬品质:手上没有品质,放下去就是往目的地写 0, 写完的那一堆自然把原来的分数冲没了。

它被漏掉的原因是个新花样:手上那一格是自动属性,IL 里的名字是 <inhandItemInc>k__BackingField,写入口是 set_inhandItemInc(Int32 value), 15 处写、81 处读全部走访问器,一条 ldfld 都不出现。于是

  • 孪生字段那一关:TwinName 的四条命名规则全不认识带尖括号的后备字段;
  • 载荷参数那一关:形参名是编译器给的 valueLooksLikeInc 认不出来—— 这是名字启发式第五次漏东西(前四次是 _stackitemInccacheCargoInc1、和这次);
  • 切语句那一关:那些方法体里「一个载荷指令都没有」,连选都选不中。

三条都改成按形状判定,不按名字:后备字段剥壳再套回去;方法体正好是 ldarg.0 ; ldarg.1 ; stfld 载荷 ; ret 的就是平凡 set 访问器,它的形参就是载荷参数; 一次平凡访问器调用在形状表里就归一化成一次 ldfld:PAY / stfld:PAY

顺手收掉三处同源的脆弱:

  • ExpectedFields 相减相错了:GPU 那三个字段本来就不在载荷清单里,是并列的另一张表, 第一版把两者相减,于是 30 条清单期望出 27 个字段,1a 被自己的断言挡死。
  • 「读一次载荷、送进一个带载荷形参的调用」不再逐个形状列举。前缀 (get_inhandItemIdget_mecha、几个 ldc……)的组合是无穷的,而要做的事只有一件: 在那个 call 之前把品质写进寄存器。按语义归一化,一条规则收掉十三种形状。
  • 「纯取值」改成递归判定。原来只认三条指令的取值器,而 UIMechaWindow::get_mechaldarg.0 ; call get_data() ; isinst Mecha ; ret——四条,于是机甲反应堆那条路整个重放不出来。

末态(离线跑完整五段管线,再读回改写后的程序集核对):

之前 现在
Player.<inhandItemQua> 存在
写孪生字段的地方 0 1(唯一写入口,值取自侧信道)
读孪生字段的方法 0 23 个,含 UIStorageGrid::HandTake/HandPutPlayer::UseHandItems/PutHandItems/ThrowHandItemsPlanetFactory::EntityFastTakeOut
孪生语句 / 方法体 / 孪生局部 / 侧信道 986 / 189 / 251 / 785 1105 / 201 / 268 / 887

Unhandled/Pending/Blockers 全空;26168 个方法体悬空分支 0、收窄后写品质 0; verify_preloader.ps1verify_harmony.ps1 均通过。

核对末态时又抓出两个,都只会在真正的改写里发作:

  • 访问器被自己的孪生改写之后,后面的调用点就认不出它了。 set 访问器的方法体从四条 变成七条(多了 qua = Q0),于是「是不是平凡访问器」这个现场判定在发射遍走到一半时 开始返回 false——分析遍全认得,发射遍认不得,而且不报错:那些语句直接从语句表里 消失,没有 Unhandled、没有 Pending、没有 Blocker。实测 15 个写入口里有 3 个这样掉了队 (SetHandItemsTakeItemFromPlayerImport),末态是「手上那一格拿到擦干净的 0」。 改成在任何改写之前建表、两遍共用BuildAccessorMap),孪生语句 1030 → 1038
  • 调用点别直接写孪生字段。 第一版在 call 之前直接写 qua = 值,紧接着那次 call 就用 Q0 把它盖掉——而 Q0 这时往往已经被擦成 0 了(SetHandItems 的品质由 TakeItemFromGrid 的出参接进局部,那次调用之后 Q0 就被擦)。现在调用点一律写寄存器, 由访问器落地,一条路走到底。末态核对:15 个写入口全部写了寄存器,0 处漏
  • X.SetInc(0) 这种「清空」也要写寄存器。品质是 0,不是「不用管」。 不写的话寄存器里 留着上一个人的品质,被调方照读不误——擦除发生在 call 之后,救不了这一次。 转发的来源因此加了第三种(常量),孪生语句 1038 → 1105、侧信道 820 → 887

修复:活性透镜喂不进传送带——品质改写撞坏了本 mod 自己的转译器

启动日志里 活性透镜:应当改写 7 处,实际 5 处(…GameTick_Gamma.取货口 0…)。 原因不在透镜:品质改写把语句插进了 276 个原版方法体,而 GameTick_GammaPickFrom 之后原本紧跟 ldarg.0 ; ldfld catalystId,现在中间多了 ldsfld Q0 ; stloc(把出参的品质接回局部),于是两个取货口同时失配。

这是一整类回归:凡是靠「紧挨着的那几条指令」定位的转译器,落在被改写的方法体里都可能中招。 新增共用跳过器 QualityAccess.SkipChannelNoise——只跳认得出是我们自己插的那两种成对形状, 认不出就停(放宽窗口会让锚点落到真正的原版指令上,而那是不报错的)。

一次启动的日志就是完整的审计:276 个被改写的方法体里只有这一个转译器倒了, 其余每一行都报着它期望的计数——每个转译器都报替换计数这条老规矩是它唯一被发现的原因。

过程上值得记一条PlanetFactory::_test_take_player_inhand_inc 名字里写着 test, 差点被当成调试残留放过去——数了调用点才发现有两个,而且都是 Shift 点一下塞东西进建筑/传送带 走的路。名字像什么不是证据,调用点才是。

修复(preloader):从储物仓手动搜回背包,品质变 0

玩家报:铁块在大型储物仓里显示 50,拖回背包就是 0。

UIStorageGridTransferToOther / HandTake / HandPut / OnGridMouseDown 先调 TakeItemFromGrid产出品质,写 Q0)再调 AddItem消费品质,读 Q0)。 两个被调方都已经孪生好了,但中间那一层是 UI*,被 IsDisplayOnly 整个排除在改写之外; 而全模块的擦除照常执行——实测四个方法真实访问 0、擦除 5/6/23/18

CLAUDE.md 早就写着这条启发式「一旦判错方向(把真正搬运的类当成 UI)会让品质静默蒸发」。

修法不是硬编码名单,是按行为判定MovesPayload):一个界面方法如果既调了能产出品质的方法 (byref 载荷形参)又调了能消费品质的方法(按值载荷形参),它就是在搬货,而不是在画画。 名单会烂(游戏更新改个方法名就静默失效),行为判定不会。当场认出 19 个,并在启动日志里逐个点名 ——那一行变短就意味着手动搞货又开始掉品质了

末态(离线跑完整五段管线,只统计真实搬运):

之前 现在
TransferToOther(shift 搜走) 0 8
HandTake(抓起) 0 5
HandPut(放下) 0 25
OnGridMouseDown(点格子) 0 27

总量:孪生语句 871 → 986、方法体 170 → 189、侧信道 674 → 785Unhandled/Pending/Blockers 全空;26168 个方法体悬空分支 0、收窄后写品质 0。

链路打通:提纯厂 → 带子 → 分拣器 → 储物箱,品质全程跟着走

日志与截图两边证实:物品品质·终点到货:储物柜里出现了带品质的「铜块」,每件 50 分。

修复(preloader):品质被截成 Int16,铁块显示负数

发射器靠重放原值表达式拼出品质表达式,而原值末尾常挂着一条 conv.i2 / conv.u1 ——那是为了适配原版的窄字段(Cargo.incInserterComponent.itemInc 都是 Int16)。 而品质的目的地全是 Int32:孪生字段、侧信道寄存器、孪生局部。跟着收窄就是截掉高位。

**按档位才发作,所以铜对铁错:**铜每件 50 分、铁每件 100 分,同样堆叠下铁的总分翻倍后 越过 32767,conv.i2 一截就是负数。上限巡检也接不住:它开头就是 if (qua <= 0) return;

修在 Build 的单一出口上(StripNarrowing),而不是逐个发射器改——实测这个错同时出现在 转发(写寄存器)和字段赋值(写 itemQua)两类发射器里,共 26 处;逐个改早晚漏一个, 而漏掉的那一个不报错。全模块扫描:写品质之前还带收窄的点数 0(原 26)。

修复:存档里已经坏掉的负数不会自己好

上限巡检只管「超上限」,开头 qua <= 0 就返回,所以负品质会在存档里永远待下去。 现在负数归零并报一行(整局一次)。品质是可加量,永远不该为负,出负数就是某处回绕了。

修复(preloader):中转方法只擦不搬,品质走不出物流站

有一类方法一个载荷字段都不碰,只把品质从自己的参数或一个局部转给下一个调用。 按载荷切语句永远切不出东西,而 ScrubAfterCalls 是全模块扫的——于是它们里只有擦除、没有搬运。 这恰好是【物流槽位 → 带子】和【手捡】两条路的全部。

三个改动,全部只动分析侧、发射器本身一行没改(BuildForwardToCallee 本来就会干这件事, 缺的只是一条语句让它挂靠):

  1. 选集放宽——不再要求「方法体里有载荷字段」;自己带载荷参数、或者调了能产出品质的 方法(byref 载荷形参),都算理由。
  2. 新形状 call:forward——给转发调用合成一条语句,转发源可以是本方法的载荷参数, 也可以是一个载体局部。
  3. 孤儿剪枝——合成跑在不动点之前,候选集还是乐观的;被转发的局部如果后来被剔掉, 转发语句要跟着消失,否则以「拼不出来」的身份挡住整个变换(实测卡住过 2 处)。

又一条 conv 没跳。 载荷形参是 Int16、本地算出来的份额是 Int32,实参常常是 ldloc V ; conv.i2。 不跳就认不出来——和分拣器那一轮栍过的是同一个坑,而这次卡住的是 StationComponent::UpdateOutputSlots,正是【物流槽位 → 带子】的源头。

核对的是末态(离线跑完整五段管线的产物,只统计真实搬运、不把擦除算进去):

环节 之前 现在
StationComponent::UpdateOutputSlots(槽位→带子) 0 2
CargoTraffic::TryInsertItem 0 2
CargoTraffic::PutItemOnBelt 0 2
CargoPath::TryInsertItem 0 8
InserterComponent::InternalUpdate(分拣器) 0 15itemQua 18)
CargoTraffic::PickupBeltItems手捡 0 3
StorageComponent::AddItem(储物箱) 2 2

总量:孪生语句 272 → 871、方法体 81 → 170、孪生局部 53 → 211、侧信道 139 → 674Unhandled / Pending / Blockers 全空;26168 个方法体分支目标全部可解析;verify_preloader.ps1 通过。

修复(preloader):栈深从不归零的方法,出参写入切不出语句

Split 按「栈深归零」切语句。有一类方法把返回值提前压栈、一直留到 ret, 中间那几条出参写入永远回不到 0,于是一条语句都切不出来

实测擞上的:CargoPath::TryPickItem 三个重载各有 1 条含载荷的指令, 5 参 / 6 参各切出 1 条语句,4 参切出 0 条——差别在它把 cargoPool[i].item(返回值) 提前压了栈,而另两个重载中间有一道 filter 判断把栈抽干了。

现在 Split 末尾多一遍,只找一种自包含的形状(ldarg <载荷出参> … ldfld:PAY … stind), 不依赖外层栈深;已被正常切进某条语句的不重复抓。三个重载现在都写 Q0(qua=2/2/2)。

还没通:手从传送带上捡货仍然是 0 分。 它的调用方 CargoTraffic::PickupBeltItems 根本不在处理集里:选集条件是「方法体里有载荷字段指令」,而它只是把品质从 一个调用中转给另一个调用,自己一个载荷字段都不碰(实测:真实品质访问 0、擦除 2)。 放开这个条件会把大量「只中转」的方法拉进来,很可能冒出一批没实现的形状而让 1c 整体不生效 ——单独一步做。

修复(preloader):分拣器现在会搬运品质,【传送带 → 储物柜】打通

症状是「提纯的铜经带子进储物柜,品质是 0」。逐段查过,注入 ✅、物流槽位 ✅、 槽位→带子 ✅、Cargo.qua ✅、储物箱 AddItem ✅——只有中间那只爪子不搬

两个叠在一起的洞:

一、两张表的差异。 InserterComponent::itemIncDeclaredPayload(决定给哪些字段 建孪生字段)里,所以 itemQua 建出来了、类型对、进存档;却不在 MainlinePayload (决定改写哪些搬运代码)里。于是三个 tick 变体在选集阶段就被跳过:没有语句、 没有分类、没有 Unhandled、没有 Blocker——报告从头到尾说一切正常,所有探针都在它下游。

二、复制传播没跳 conv 把分拣器加进主干道后暴露了三种缺的发射形状, 而其中两种的根是同一个:

06A6: ldloc.s V_13
06A8: conv.u1          ← 合成只看紧邻的前一条,这里不是 ldloc,复制语句没被合成
06A9: stloc.s V_14
06C4: ldloca.s V_14    ← 同一个局部又当 InsertInto 的 out remainInc

V_14 于是多出一处「不属于任何合格语句」的 stlocSafeCarriers 按规矩把整个局部判死, 连带 call:outitemInc -= 已送出 - 剩余 一起拼不出来。旁边两个合成分支早就跳了这条 conv,只有复制这一支没跳——而它是最常见的一支,因为载荷字段是 Byte / Int16, 复制进局部几乎总要收窄一次。

另外实现了两种发射形状:ldfld:PAY add stfld:PAY(从带上取货后累加,15 处)和 ldfld:PAY sub sub stfld:PAY(送出后按实际量扣减,3 处)。

核对的是末态,不是报告自己说了什么(离线跑完整五段管线的产物):

之前 现在
分拣器三个变体的 itemQua 访问 0 / 0 / 0 18 / 18 / 18(对应 itemInc 各 18)
孪生语句 272 406
方法体 81 90
孪生局部 / 侧信道 53 / 139 81 / 214
Unhandled / Pending / Blockers 空 / 空 / 0 空 / 空 / 0
26168 个方法体的分支目标 全可解析 全可解析

剩下的独立缺口(未修)CargoPath::TryPickItem4 参重载没被孪生(另两个孪生了), 走它的是手从带子上捡货PickupBeltItems)。所以手捡进背包仍然是 0 分。

定位:品质走不出传送带,因为分拣器不在品质主干道表里

玩家报的是「提纯的铜经带子进储物柜,品质是 0」。逐段查过,提纯注入✅、物流槽位✅、 槽位→带子✅、带子上的 Cargo.qua ✅、储物箱 AddItem ✅——只有中间那只爪子不搬。 实测改写后的程序集:三个 tick 变体里 itemInc 各 18 处、itemQua0 处。

根因是两张表的差异:itemIncDeclaredPayload(决定给哪些字段建孪生字段)里, 所以 itemQua 建出来了、类型对、进存档;但不在 MainlinePayload(决定改写哪些搬运代码)里。 于是三个变体在选集阶段就被跳过:没有语句、没有分类、没有 Unhandled、没有 Blocker ——报告从头到尾说一切正常,也是四轮没找到它的原因。

本次未修,而且是有意的。 把它加进主干道之后 1c 会整体拒绝生效(这是对的: 形状没实现就不假装成功),结果是品质全关,比现状更差。缺的三种发射形状已由报告点名:

ldfld:PAY add stfld:PAY       ×15   itemInc += V_1(从带上取货后累加)
ldfld:PAY sub sub stfld:PAY   × 3   插入后按实际送出量扣减
call:out [opaque]             × 6   PickFrom 的 out 出参,Build 拼不出来

最后一种是根:出参的品质走侧信道 Q0,发射器要在 call 之后补一句「孪生局部 = Q0」, 前两种才有值可加。详情写在 QualityFieldAnalyzer.MainlinePayload 那一行的注释里,连同放开它的条件。

新增:两个静默丢失探测器(只报告,不改行为)

上面那个 bug 能藏这么久,是因为转换器对它一声不响。现在:

  • 切出了含载荷的语句、却一条都没孪生的方法会被点名。当场拓出 7 个 (都是 Export / 统计面板,属于已知的存档跳过,但以前看不见)。
  • 发射阶段那句 if (job.To >= code.Count) continue; 原先不记账,现在改成 Blocker—— 分析遍算进 Twinned、发射遍静默跳过,正是这个文件到处在防的那种自欺。

新增:终点到货探针

品质这条链调试了好几轮,每一轮都卡在同一件事上:源头有日志(注入那一行), 终点没有,中间又大半是原版方法(槽位 → 带子 → 分拣器 → 柜子),埋不进去也不该埋。 于是「链路通了」和「断在某一段」只能靠玩家去悬停看一眼,每次都多花一个往返。

现在直接埋终点:储物柜或背包里第一次出现带品质的货就报一行,带物品名和每件分数。 终点有值就证明整条链通了,不需要逐段埋点。

第一版挂错了地方:挂在 Run 里,而 Run 只在读档后跑一次,那时玩家什么都还没生产, 探针永远打不出来。每 30 秒那一遍是另一个方法,而且它故意不扫储物箱(全扫在大存档上卡顿)。 现在挂在每 30 秒那一遍,并按它已有的口径只看当前星球的储物箱 + 背包

修复(preloader):【传送带 → 分拣器 → 储物柜】整条链品质恒为 0

玩家报:提纯出来的铜放进储物柜,品质是 0。而日志里注入那一行是对的 (1600 件 / 80000 分),槽位也没有「扣了件数没扣品质」的残留。

病根在 ScrubAfterCalls。它在每个载荷方法调用后插一句「寄存器置 0」,目的是让寄存器 只活一次调用(防止凭空发明品质)。它确实跳过了一种出参读回—— 「调用方把品质存进局部变量」。但还有第二种,它没认:调用方本身就是中继, 被调方写的那个值就是它要往上传的出参。

实测现场:

CargoTraffic::TryPickItem
  0072: callvirt CargoPath::TryPickItem(...)   ← 被调方刚写好 Q0
  0077: ldc.i4.0
  0078: stsfld ProjectEdenQualityChannel::Q0   ← 擦掉
  007D: ret                                    ← 上游 PlanetFactory::PickFrom 读到 0

CargoPath::TryPickItem 本身孪生得好好的,PickFrom 也孪生得好好的, 每一步都「成功」,功能却不在——本仓库最怕的那种形状。

判据故意写得窄:调用方自己带载荷参数,且插入点后面紧跟 ret。宽一点就会把 「调完还要干别的事」那些点一并放过,而寄存器多活一步就是凭空发明品质,比丢失难查得多。

离线验过(跑完整五段管线的产物,不是只跑加宽的那份):两个 CargoTraffic::TryPickItem 重载的 scrub 都降到 0;26168 个方法体的分支目标全部能解析。

已知剩下的一半(本次未修)CargoPath::TryPickItem4 参重载根本没被孪生 (另两个孪生了),而且它被静默算进了「12 条确认不需要孪生」,Unhandled 是空的—— 报告说全处理完了。那个重载的调用方是 PickupBeltItems(玩家手捡带上的货) 等,不在分拣器这条链上,所以不影响本次修复;单独追。

修复:在配方窗口里点产物图标拿货,品质恒为 0

这是 produced[]第三条出路,而品质原先只挂在前两条上(物流槽位、传送带)。

实测改写后的程序集,UIAssemblerWindow.OnProductIcon0Click

0092: callvirt Player::TryAddItemToPackage(...)   ← 调用前一个字都没写侧信道
0097: ldc.i4.0
0098: stsfld ProjectEdenQualityChannel::Q0        ← 这是调用之后的擦除

协议是「调用方在调用前写」,没写就等于送 0 分。这是构造上的,不是偶发。

preloader 没管它的原因是 QualityFieldAnalyzer.IsDisplayOnly 按名字前缀把所有 UI* 类型整个跳过。那条规则对绝大多数界面类是对的(只画不搬), 而这两个点击处理器是真的在搬货

现在用前置在调用前写寄存器,查同一张提纯表、用同一个每件分数——三条出路不可能再各说各话。

改进:空格子里的品质残留现在点名

count == 0qua > 0 有两种成因,应对方式相反:舍入灰尘(个位数,无害), 和「有一条路扣了件数没扣品质」(成百上千,真 bug)。区别就在量级, 而原先那行日志只说「1 格」、连 worst 都因为 count > 0 不成立而停在 0—— 两种成因在日志上长得一模一样。现在整局点一次名,带物品名和残留分数。

修复:从传送带取料时丢品质

被调方其实把品质送回来了(实测改写后的程序集: TryPickItemAtRearldfld Cargo::qua 之后 stsfld Q0),是我们自己的 finally { Gate(); } 在读它之前先把寄存器抹了

现在是「读完再清」:PickAtRear 多一个 out int qua 重载,旧签名转发过去, 不关心品质的调用点一字未变。清不能省:寄存器活过一次调用, 下一个没写就读的人就会凭空拿到品质(实测过单件涨到 1010,上限是 100)。

接到了两处:进料按件数进 quaServed;多余原料退回带子时按件数带走。 产物回流那一笔明确丢弃:产物侧没有可存品质的字段(原版连 incProduced 都没有),拿回来也无处可放,写在代码里而不是静默丢。

修复:verify_preloader.ps1 只验了五段管线里的一段

它直接调 CargoIncWidener.Apply,而上线的 Patcher.Patch 是五段链(加宽 → 加孪生字段 → 建侧信道 → 改写品质流 → 存档)。它写出来的那份二进制一个品质孪生 字段都没有,却照样打印 all checks passed

这不是潜在风险,它已经造出了一个错误结论:对着那份二进制做普查,得出 「TryPickItemAtRear 没被孪生过」——假的,而且差点据此去改 preloader。

现在它会枚举 preloader 里所有 *.Apply 阶段,并把自己没验到的那几个点名报出来。

修复:提纯厂用传送带发出去的金属,品质恒为 0

玩家报的是「品质 10 的材料提纯后还是 10」——而 10 正是底线分,也就是存的 0。

日志里注入那一行是对的(提纯厂第一次注入——配方 6673 产出 80 件,每件 50 分), 所以问题不在注入。produced[] 有两条出路,品质只挂在其中一条上:

出路 代码 品质
produced[] → 本建筑物流槽位 → 物流网 MegaStationPatches.UpdateStationStorage OnProduced 注入 50 分
produced[] → 直接上传送带 MegaAssemblerPatches.UpdateOutputSlots ❌ 从来没有任何注入

同一台机器两条出路两个答案,而且一个字也不报。

第二道门在 CargoWidening:它包装的每一笔入库都先把品质侧信道清零, 注释里写着「阶段 3 把这几条路接上真正的品质之后,这里就从『清零』变成『赋值』」—— 阶段 3 一直没做。 现在做了:InsertAtHead 多一个 qua 参数,大于 0 时写寄存器而不是清零; 传 0 时两者等价,所以其它调用点行为一字未变。调用仍然清,寄存器寿命仍压在一次调用以内。

两件离线核实过的事,不是假设: TryInsertItemAtHeadAndFillBlank(Int32, Byte stack, Byte inc) 只有一个载荷参数, 所以它读的是 Q0(对比 PlanetFactory::InsertInto 有两个,用 Q0+Q1); 孪生字段是 Int32,所以 5000 层 × 每件 100 分也不会回绕。

还加了一行只报一次的状态,带品质和不带品质两种都报——只在有品质时打一行的话, 「没走带子」和「走了带子但品质没接上」在日志上一模一样,而这次的 bug 恰好就是后者。

已知剩下的口子:取货那一侧(PickAtRear)仍然丢品质,所以用带子把带品质的料 送进另一台巨型建筑会归 0。送进储物箱 / 物流站不受影响(那是原版路径)。

修复:提示栏现在会明写「品质 普通(0)」,而不是整行不显示

原先那句 if (perItem <= 0) return; 的理由写的是「0 分和查不到在显示上应当一样」。 那把两件不同的事混在了一起:鼠标不在储物格上确实不该画,但「查到了,是 0 分」 是个真实的答案。两者当时都返回 0,现在查不到返回 -1,严格分开。

撤回:普通材料的 10 分底线

上一版加了一个「读取时把 0 顶到 10」的底线。它和求和模型是互斥的,所以拿掉了 (所有者定的):品质存的是一格货的总分,底线如果不真的存进去,总分里就没有它。 100 件普通货 + 100 件 50 分的货混在一格,总分是 5000,摊下来 25 分; 而按「每件都有分」的直觉应该是 30。求和模型更重要。

顺带取消了它带来的「每座新建建筑白拿 3% 省电」。

删除:「手搓提纯配方就按档位给分」那段分支是死代码

上一版加它的理由听起来成立:品质的注入点挂在「产物落进提纯厂自己的物流站槽位」, 手搓够不着它,于是同一条配方在机器上有品质、手搓没有。推理没错,前提错了—— 玩家根本搓不了提纯配方。

实测原版 IL,UIReplicatorWindow.OnOkButtonClick

0138: ldfld RecipeProto::Handcraft
013D: brtrue.s IL_016B          // 真才继续
013F: ldstr "该配方" … "生产"   // 假就弹提示
016A: ret                       // ← AddTask 在 IL 01DE,在这个 return 后面

ores.json 的每条配方都是 Handcraft = false,所以那个分支一次也跑不到。 品质的来源仍然只有提纯厂一处,和设计一致;手搓只负责一件事——拿带品质的料搓东西时别把它弄丢。

连带纠正了 CLAUDE.md 里一条只对了一半的撤回:「UIReplicatorWindow 并不以 Handcraft 为门槛」——对 RefreshRecipeIcons(画)成立,对 OnOkButtonClick(做)不成立。 读了一个方法就推广到整个窗口,正是仓库自己记着的「枚举出口再下结论」。

新增:QualityCraftPatches 的启动状态行

这个类原先一行启动日志都没有。「手搓没品质」报上来时日志里一个字没有, 于是**「补丁没挂上」和「挂上了但这一局没人去点合成」长得一模一样**,白花一轮。 逐分支的 trace 补不了这个缺口:它们全在挂点下游,挂点没上时会一起沉默。 状态行读的是 Harmony 自己的补丁表(GetAllPatchedMethods),也就是实际生效的状态

新增:巨型建筑缺电时按供电率线性降速

在这之前 10000 倍速对缺电几乎免疫,然后一头撞死。那不是设计,是倍率撞上原版公式的 副作用,两条指令就看得明白:

InternalUpdate IL 0000: if (power < 0.1f) return 0;
               IL 0576: time += (int)(power * speedOverride);

原版 1 倍机器的 speedOverride 是 10000,time 涨得慢一半产量就慢一半——产出对供电率 本来就是线性的。可巨型建筑是 1e8,单次调用加的 time 比任何配方的 timeSpend 都大 一两个数量级,于是供电率 0.11 和 1.00 结算出来一模一样,掉到 0.1 以下则整台停摆。 玩家看到的就是「一点都不减速,然后突然全死」。

所以节流阀只能是每 tick 跑几个周期,和生物温室按日照缩放共用同一个旋钮 (speed 绝对不能动:巨型建筑靠 speed >= 阈值 识别,降到阈值以下就再也不被接管)。

曲线照抄原版自己那条:线性,不另外发明一条。 这和钻头消耗「逐项照抄原版的产出表达式」 是同一条规矩——两边才不会在边界上各说各话。地板取 1 个周期/tick,正是原版 1 倍机器的满速: 缺电该变慢,不该变成完全停工;真正的停工交给原版那道 10% 的闸,我们不重复它。

功耗不受影响,也就没有反馈回路:SetPCState 算的是 workEnergyPerTick,和 speed 无关。

开关是 megabuildings.jsonpowerScalesCycles(默认 true)。第一次真的降速时日志留一行 (Interlocked 抢占在字符串插值之前——这条跑在并行 tick 路径上)。

改动:风力发电机集群和可燃性液体发电厂可以进制造台了,不再只能手搓

一千台风机要一台一台手点,实在不像在玩工厂游戏。两台的 recipeHandcraftOnly 翻成 false,配方类型从 None(0) 变成 4(组装),制造台和天工装配厂都认得它们了。

手搓没有因此失去。 MachineRegistryHandcraft = true 是无条件写的, 这个开关只管 Type——也就是「机器能不能选到它」,两条路现在都通。

顺带修掉一个副作用:Type 为 0 时产物拿不到 productionMask 位 (ItemProto.InitProductionMask 开头就是 if (recipe.Type == 0) continue;), 因而不出现在「参考速率」面板里。过去那是可接受的代价——手搓配方本来就没有工厂产能; 现在能自动化了,这一位也跟着回来,方向正好。

小型速采机仍然只能手搓,这是有意的:它是开局第一台采矿机,无前置、1 铁块 + 1 铜块, 不该等你先造出一条产线。

修复(进行中):活性透镜的传送带取货口

GameTick_Gamma 里两个传送带取货口的转译匹配不上(日志报「实际 0 个」), 表现是活性透镜只能手动放进射线接收站,传送带喂不进去

这一轮把匹配器返工了两次,两次都是同一类毛病——认死一种写法

  • 字段比较用 ReferenceEquals。反射对象的引用相等在大多数情况下成立(运行时会缓存 RuntimeFieldInfo),所以这种匹配器能跑很久,直到某次不成立——而那时的表现是 匹配数 0、零报错,和「原版改了形状」分不开。新增 SameField 按 「声明类型 + 名字」兜底,本文件三处全换
  • 锚点认死 ldarg.0。「载入第 0 号参数」有 ldarg.0 / ldarg.s 0 / ldarg 0 三种编码,新增 IsLdargZero 一次认全

同时把锚点从通用操作码(ldnull)挪到语义唯一的东西上(名字叫 PickFromcallvirt, 以及取货之后那次 ldfld catalystId),并让失败时自己说出是哪一条谓词否掉的—— 上一版失配时一条信息都没有,只能靠猜。诊断第一次上线就把范围从六七条谓词缩到了一条。

1.8.3

病因订正(实测):下面这条崩溃的真正原因不是 PrepareWorks 的时序,而是同一批里 蓝图诊断的参数名写错、PatchAll 抛异常 —— 异常从 Awake 里冒出来,后面那一大段 LDBTool.PreAddDataAction += OreRegistry.OnPreAddData 根本没执行,矿脉从没注册, LDB.veins 只有 14 条、数组 15,而存档里早就存着 15~23 号矿脉。参数名一修,崩溃就没了; 下面这套矿种数组的补齐逻辑一次都没触发过(日志两次都报「够用」)。

PrepareWorks 跑两次、第一次早于我们的矿脉进表」这个发现是真的,但它不是这次的病因, 是我拿一个能解释症状的机制当成了病因——正是 CLAUDE.md 反复警告的那条。 补齐逻辑保留,当作使用点上的保险;但要知道它当时并没有起作用, 而且 OreRegistry.MaxVeinId 那一半在第一次调用时是 0,同样是空的。

加固(原本当成崩溃修复发的):矿种数组在使用点上再兜一次

报错是 PlanetModelingManager.LoadingPlanetFactoryMain (IL_035D),堆栈里一个 mod 的名字都没有。 落点是 veinProtos[矿种编号]——按矿种编号索引的那个数组太短了。

根因是本仓库自己文档里一句被实测推翻的话。 CLAUDE.md 原先写着 「PrepareWorksPlanetModelingManager.Start 跑,远晚于 LDBTool 的 PostAddDataAction, 所以时序天然没问题」。上一局的日志证明它跑了两次

矿种数组核对:矿种表 14 条 … 按 15 定容,需要 15 → 够用
…
矿种数组核对:矿种表 23 条 … 按 24 定容,需要 24 → 够用

第一次在我们的矿种进 LDB 之前,表里只有原版 14 条,数组被定成 15, 而那个体检当场报了「够用」——在那一刻完全正确、对最终状态完全错误的假绿灯。 健康的一局靠第二次调用救了回来;没有第二次的那一局,矿种 15~23 一索引就炸。

两条改动:

  • 需要多大不再只问 LDB.veins 第一次调用时它还没有我们的矿种, 但 OreRegistry.MaxVeinId 在那之前就知道,取两者较大值——第一次就补到位, 不再依赖「会有第二次」。
  • 在真正用到的地方再兜一次LoadingPlanetFactoryMain 的前置。 任何排在使用点之前的准备工作都可能被别的 mod 或别的时序绕过去,使用点本身绕不过去。

填充改成无条件执行。只补容量不补槽位的话数组够大但 15~23 全是 null, 原版对 null 是 continue——不崩了,但矿脉在地表一个都画不出来而且一声不吭, 等于把崩溃换成了本仓库最怕的那种静默失败。

修复:蓝图诊断的参数名写错,把它后面所有补丁类一起废掉了

上面那条矿脉崩溃的修复第一次发出去没生效,原因是同一批里的蓝图诊断: 前置写的是 (EBuildCondition type, BuildPreview preview),而原版声明的是 (EBuildCondition _bdCondition, BuildPreview _bp)Harmony 按参数名注入, 对不上就是 Parameter "type" not foundPatchAll 里抛出来。

它的杀伤面是「部分」的,这比全死更难查:排在它前面的补丁类照常生效, 后面的全部被静默跳过——于是 OreVeinColorPatchesVeinProtoArrayPatches 都没被应用, 表现出来是一个看起来毫不相干的原版崩溃又回来了。

堆栈里其实早就写着答案:那一帧是 in <mvid>:IL_035D没有 (wrapper dynamic-method), 说明那个方法根本没被打补丁、偏移也不需要换算。为此白白重放了两轮 Harmony 的短跳转展开。 先看有没有 wrapper 帧,再决定偏移要不要换算。

tools/verify_harmony.ps1 增加第三项检查:逐个比对每个前置/后置的非 __ 参数名 是否真的存在于目标方法里。改代码之前先让它在未修复的源码上报错,确认它真的抓得住。

修复:蓝图粘贴时四个作弊开关全都不生效(从来就没生效过)

报障是「巨型建筑蓝图复制之后粘不下去」,而所有者自己的判断是对的: 巨型建筑身上带着 isStation,两座挨近就被当成两个物流站,触发 TowerTooClose(8)

但病根比那更大:BuildConditionCheatPatches.Relax 遍历的是 BuildTool.buildPreviews, 而 BuildTool_BlueprintPaste 的预览在它自己的 bpPool / bpCursor,基类那个列表是空的。 于是这个后置在蓝图粘贴这条路上一次都没跑过——而且因为它一进来就 Count == 0 直接 return, 连那行一次性日志都不会打,所以日志里从来没有任何线索。

所以这和巨型建筑无关:蓝图粘贴里任何被条件拦下的东西,四个开关都放行不了。 只是 TowerTooClose 只在物流站之间触发,而巨型建筑正是会被挨着摆的那一类。

两个值得记住的「征兆」:

  • 转译器那一半一直是好的。 CursorText_Transpiler 是注入到方法体里的,不依赖那个集合, 所以光标提示被接管了、条件却没被清。「功能只生效了一半」说明的是哪个机制坏了, 不是功能坏了一半。
  • 探针是把 CreatePrebuilds 的三道闸并排打出来的,不是只打怀疑的那一个。 当时的假设是 ArrangeOverlapBPbpgpuiModelId 清成了 -1;日志回来是 bpgpuiModelId=1、condition=TowerTooClose(8),一轮就把那个假设毙了。 把每一道闸都打出来,不要只打你相信的那一道。

顺带记下 CreatePrebuilds 的完整跳过清单,第一条是这次才发现的: if (bp.bpgpuiModelId <= 0) continue;(IL 0030)排在 condition(IL 003B)和 coverObjId(IL 0196)之前ArrangeOverlapBP 四处都是成对bpgpuiModelId = -1 + condition = BlueprintBPOverlap(51)——将来要放行那条路, 两个字段都得还原,和 coverObjId 是同一个道理。

新增(诊断):蓝图粘贴被拒时点名是哪道闸

「巨型建筑蓝图复制完粘不下去」报上来时日志里一行相关的都没有。读 IL 发现粘贴有 两个阶段、两套互不相干的拒绝机制CheckBuildConditionsBuildPreview.condition (作弊开关擦的是它),而先跑的 CheckBuildConditionsPrestageAddErrorMessage, 压根不碰 condition——所以「无条件建造」开着也拦不住它,而且不留任何日志。

前置挂在 AddErrorMessage 上,那是粘贴工具所有拒绝理由的唯一收口点,一处覆盖两个阶段; 再加一个后置把格框几何那四个分量的具体数值打出来。按条件去重,一次粘贴最多 12 行。 只诊断,不改行为——还没确认踩的是哪一条之前不发修复。

改动:处理器要多吃两个电磁矩阵

这是一条平衡改动,不是从化学推出来的,所以按仓库的规矩在配置、README 和两份指南里都明说了。 处理器下游极广(量子芯片、位面过滤器、一大批建筑),给它挂一个矩阵依赖,等于把研究站提前 变成产线的一部分——有意为之。退回原版是把 recipes.json 里那条 vanillaEditsadd 清空, profile 里放一份覆盖即可,不用重新编译。

做法是 recipes.json 新开的 vanillaEdits 段:就地给原版配方加原料,不克隆。 只做「加料」一件事——删料和改份数没做,需要什么再加什么。

  • 时机放在 PostAddDataAction,刷新是白送的RecipeProto.InitRecipeItems 会把整张 recipeExecuteData 表重建,而 LDBTool 在这个动作之后才调它,所以改完 Items 就被自动吸收。 同时排在 EnergyAudit 之前,审计读到的是改完的配方。
  • 按产物认配方,不写配方号:原版配方号在 resources.assets 里离线枚举不到。同一产物匹配到 多条而配置又没点名类型时,一条都不改并把候选列进日志——装了别的内容 mod 时 「改中哪条全看运气」比不生效糟得多。已经在配方里的原料会跳过,所以热重载是幂等的。

老存档直接能用,而且这一条是读 IL 确认的,不是推测。 加原料让投料槽从 2 变成 3; AssemblerComponent.Export 每个数组都先写自己的长度再写内容(IL 00E2 写 requires 长度、 0109 写 served 长度,都是一个字节),Import 读回来之后在 IL 040B–0446 按当前配方 Array.Resize。 存档里 2 长的投料数组会被补成 3,旧料保留、新槽从 0 开始,正在造处理器的机器不会卡住。

顺带收窄了 CLAUDE.md 里一句过宽的话:原先写的是「改配方的原料条数会和存档打架」。 那只对逐台建筑的 recipeExecuteData 克隆成立,原因很具体——Import 是从 RecipeProto静态字典重新取对象(IL 03F5 / 06AB),克隆的形状活不过一次读档。 全局改 RecipeProto 本身是安全的,两边长度一致,字节流又自带长度。

新增:屏蔽「数据异常」判定,成就和元数据恢复正常(abnormality.json,默认开)

这不是一条作弊开关,虽然它长得像。cheats.json 六个开关全关掉,它照样会响—— 原版的 ABN_ProtoData.CheckProtoLDB.items / techs / recipes / veges / veins 各算一个签名, 和表里烘死的那个比。本 mod 往物品表塞了近百个物品、往配方表塞了八十多个配方、往矿脉表塞了 九种矿脉,这三张表的签名都不可能对上;它挂在 onGameBeginbeforeGameSave 上, minRecordVersion 传的是 0。实测每次读档各响四次——物品(2)、科技(3)、配方(4)、矿脉(6)。 第四条当初没预测到:没有任何地方科技,但集装等级和研究速度那几条科技的 UnlockValues 是就地改写的,而签名算的是表的内容不是长度——改一条和加一条触发的是同一道检查

代价也不只是 Steam 成就:NothingAbnormal() 有九个消费方,其中 PropertyLogic.active元数据,对玩家多半更疼。

三处补丁,每一处都是 IL 里找出来的收口点:

  • 前置 TriggerAbnormality——二十个 ABN_* 判定器全部经由这一个方法落盘(枚举过, 无一例外),一处就够,不用逐个去关判定器。管的是「以后不再往存档里记」。
  • 后置 NothingAbnormal 返回真——管的是已经被标脏的存档。而任何装过本 mod 的存档都 已经被标脏了,所以缺了它这个功能对老存档完全无效,正是「两道闸一个症状」那条老坑。
  • 后置 IsAbnormalTriggerred 返回假——成就面板除了总的那一问还有逐条查询。

存档里已有的记录一个字节都不动。 ClearAbnormality(0) 能一把洗掉全部 3000 格,看着更彻底, 但那是不可逆地改写玩家存档;而改返回值同样覆盖九个消费方,开关一关全都回来。 能用返回值解决的,就不要去改别人的数据。

顺带记下一条不能走的路:前置 AbnormalityLogic.InitDeterminators 返回 false 会让 determinators 留在 null 上,而 GameTick 第二条指令就去取它的迭代器——每 tick 一个空引用; 改成后置 Clear() 也仍然不完整,事件驱动的判定器在 Init 里已经订阅出去了, 而 ABN_ProtoData 恰恰就是事件驱动的。

⚠️ 开着它,成就会真的解锁并进 Steam。 只想本地解锁不上传的话得另外拦 SteamAchievementManager,本开关没做这一层;不想要就把 enabled 改成 false。

单独一个配置文件而不是并进 cheats.jsonJsonHelper 的磁盘覆盖是整文件生效的, 为翻一个布尔值去影子掉整份作弊配置,以后对内嵌 cheats.json 的每次修改都会被静默忽略 (cargoprobe.json 已经为同一条理由单独成过一次文件)。开着关着都会往日志里写一行, 每拦下一种异常再单独报一行(每种只报一次)。

1.8.2

新增:原油可以直接烧了,而且必须是最差的那一档

原油是十三种可燃液体里唯一采出来就能烧的,所以它一旦好烧,整条炼油线就没有存在理由。 定在 400 °C,排在藻油(250)和钒渣油(500)之间。

为什么比渣油还低,依据是这套设计原本那条规矩(温度看的是积灰与热腐蚀,不是火焰温度): 钒渣油把钒和硫浓缩了、看着更脏,可它是已经脱过盐的馏分;原油是未处理的整桶,除了同样 含钒含硫,还带着盐和水——而钠正是 Na₂SO₄–V₂O₅ 共熔体热腐蚀的另一半,再加上轻组分闪蒸。 两样都脏,对只脏一样。

账是算过才定的:直接烧 10 桶原油得 15.8 MJ 电,先蒸馏再各自按自己的温度烧得 21.5 MJ—— 精炼一遍多 36%,够大到值得建炼油线,又没大到让原油变成废物。 热值 4.05 MJ 用的是原版的,而且和这条自洽:ores.json 里本来就写着「原油比一个 CH₂ 应有的 4.5 低约 10%,因为它含硫含氮含灰」——低热值和低温度是同一个事实的两面

修复:物流站的单格容量被锁死了,玩家改不动

那个 max 正是物流站面板上「每格库存上限」输入框写的字段,而代码里每 tick 都把它压回配置值, 于是玩家改完一松手就弹回去。这是同一个字段级错误犯的第二次——「最大充能功率」滑条当年 栽过一次,结论「必须一次性引导、不能每 tick 强制」就写在同一个文件下面几十行的地方, 而 max 没跟着改。

配置里那个数的正确身份是默认值:新建的站点由 prefabDesc 直接拿到它,老存档里已建成的 在第一次 tick 时抬一次,之后这一格就归玩家。两道闸缺一不可:每座站点只引导一次 (否则玩家把上限设成正好等于原版默认值时会被每 tick 抢方向盘),且只抬还停在原版值或 0 上的格子 (否则读档时会把玩家调小过的上限顶回去)。引导记录用 ConcurrentDictionary(那条 tick 并行跑在 约 31 个线程上),换存档时清表(站点号会重复使用)。

修复:30 格物流站只有前 5 格能用传送带喂料

原版那条链是 UpdateNeedsTryPickItemAtRear(needs, out needIdx)InputItem(itemId, needIdx)白名单下标和储物格下标是同一个数,而两头都只认得 0..5。

改法:UpdateNeeds 后置从全部 30 格挑想要货的填进白名单;InputItem 前置不信任传进来的 needIdx,按 itemId 现扫一遍落到真正要它的那一格。这样不管白名单填的是哪一格的货都安全, 找不到就原样放行、退化成原版行为。完全没碰 TryPickItemAtRear——那是货物 tick 的热路径, 而且它是六个完整复制的代码块(各带一份「清缓冲、写 needIdx、取货、返回」),改成循环是一次 大重写,收益和风险不成比例。

说在明处的限制:同一时刻最多 6 种货能被传送带接(星际站 5 种,曲速器占一位),这是 TryPickItemAtRear 复制成六块带来的硬限制。想要货的格子更多时按秒轮换,每一格都轮得到—— 和综合物流枢纽配送器那条是同一个解法。

中途写出过一版会让传送带吃掉物品的代码,记在这里。 第一版以为 needs 是一张可以随便填的 白名单,把第 20 格的货填进 needs[0];而 InputItem 是「对不上就什么都不做」——货已经从 传送带上取下来了,于是直接消失。根因不是手滑:只读了填表的那一侧,没读消费它的那一侧。 最终的修法之所以安全,正是因为它不再依赖任何关于下标含义的假设。

文档订正

  • CLAUDE.md 里「StationCapacityPatches 每 tick 强制 max」的描述已经为假,改写成一次性引导, 并把「这个数是默认值不是锁死值」作为一条同时管容量和充能功率的规矩写下来—— 同一个字段级错误在这里犯了两次,值得只写一次、管住两处。
  • 两份指南补上:点击矩阵研究站不会开出物流站面板。它身上根本没有物流站组件 (stationId 是 0),和巨型建筑那种「真的挂了一个站、能并排开两个面板」不是一回事; 它走的是虚拟供料。巨型建筑现在能开双面板了,不写清楚的话玩家会合理地以为研究站也该能。

1.8.1

修复:高速传送带会在 CargoPath.Update 里抛「长度不能为负」

并行的 FactoryCargoPath 阶段崩溃,Array.Copy 收到负数长度。那一句(IL 03F5)是缓冲区 整体前移:Array.Copy(buffer, at, buffer, at + shift, size - shift),前面只判过 shift > 0,没有任何地方保证 shift <= sizeshift 正比于传送带速度,所以原版速度 ≤ 5 时这个不变量永远成立、漏写的边界检查永远碰不到;本 mod 提到 8 之后它就会破。

和同一个文件里已有的「回扫越界」是同一个形状,而那处的类注释当初就预告过 「只修掉了实测炸过的那一处,其余没有逐个验证」——这次就撞上了另一处。 改法照旧:把那一条 call Array::Copy 原地换成自带钳位的版本。 钳到 0 是正确语义不是把错误吞掉——移动距离比待移动区还大,意味着没有东西需要移。 真的钳过一次打一行 WARNING(整局一次),那是「速度已经顶到原版假设之外」的信号。 实测:这行警告确实出现了,说明崩溃会稳定复现。

修复:开着无碰撞时,蓝图粘贴出来的分拣器挂不到传送带上

真正断的是传送带之间的连接,分拣器只是受害者——病因和症状隔着两层。

coverObjId 对建筑是「压在别的建筑上」的障碍标记,对传送带却是**「接到这条已有带子上」 的意图本身**。这一点上一次报障时就修过,但当时的排除条件是按工具写的 (!(tool is BuildTool_Path))——而蓝图里的传送带不走 BuildTool_Path, 它走 BuildTool_BlueprintPaste。于是那条保护在「框选复制 + 粘贴」这条路上整个失效: 带子各自独立成段,分拣器自然找不到要挂的那条。

判据改成这个预览是什么desc.isBelt || desc.isInserter),不再是哪把工具在跑—— 同一类东西可以由点建、拖拽、蓝图粘贴三条路造出来,按工具排除必然漏掉其中一条。 分拣器也一并纳入,依据是 IL:CreatePrebuilds 016F 处 coverObjId != 0 就不建新 prebuild, 对它这个字段同样是连接/替换语义。

实测验证(新增的一行诊断):input=86 output=88 cover=85 条件=Ok—— 两头都拿到了连接对象,而 cover 活了下来,正是保护生效的证据。

物品品质:效果层开了第一刀,合成传递打通了手搓那条

  • 建造时把材料品质截下来。材料在建造那一刻就被消耗了,之后建筑身上没有任何地方还记得 它是用什么料造的——品质是一格货的属性,货没了属性就没了。扣料收敛到七个方法 (五把工具的 CreatePrebuilds + DoUpgradeObject + PlaceItems),按 prebuildId 记, AddEntityDataWithComponents 那一刻搬到 entityId。存档 SaveVersion 4 → 5。
  • 通用轴 · 耗电:顶尖 −30%,按品质线性插值,只在建造那一刻写一次 (每 tick 强制会和物流站充能滑条打架;那个字段进存档,读档再乘一次就是复利)。
  • 手搓传递品质:投入总点数 ÷ 产出总件数。加权平均是免费的,而且只会稀释、 永远不会超过最好的那个投入。

两处设计稿的错误,实测纠正,记在这里。 其一:设计稿说建造扣料的两个方法都会把品质吐回来,实测 StorageComponent.TakeTailItems 会(写 Q0),而 Player.UseHandItems 一个寄存器都不写Player.inhandItemInc 连孪生 字段都没有——手上那一摞货没有品质槽位。好在实测常规建造 100% 走背包那条 (日志:手上 0 件),所以不用去补那个字段。 其二:设计稿的「合成传递品质」是意图不是实现——产物侧只有 produced, 没有 incProduced 可供孪生。所以装配台批量生产目前传不了品质,只有手搓能传。

新增 tools/verify_harmony.ps1

离线查两种会把整个 mod 打下线的注解错误:TargetMethod(s) 和单独 [HarmonyPatch] 混在同一个类里;以及 [HarmonyPatch(typeof(T), "name")] 打在重载方法上。 两者都在编译期看不见,都在 Awake 里抛,后果不是「某个补丁失效」而是一个补丁都打不上。 这两个错误在同一个功能里各犯了一次,所以做成常设脚本;脚本自己验过会响(塞进两个错误,两条都报)。

1.8.0

巨型建筑现在同时开出两个面板,而且储物格的方向可以自己改了

它本来就是「一台机器 + 一座行星内物流站」,可原先只看得见制造台那一半, 30 个储物格看不到也改不了。两件事各有各的病因:

看不到,是因为 UIGame.OnPlayerInspecteeChange 里每个组件各有一段 「上次没有而这次有 → ShutAllFunctionWindow() + 开自己的窗口」, 制造台那一段在 IL 0354、物流站那一段在 IL 076C——后者把前者刚开的窗口关掉了, 所以最后命中的赢。原先的做法是把 stationId 报成 0,只留制造台。

现在物流站窗口完全由本 mod 自己开关stationId 仍然报 0(原版对这台建筑的窗口记账 一个字都不写),窗口由一个挂在 UIGame._OnUpdate 上的每帧检查来开—— 判据是「制造台窗口开着吗」,于是 Esc、点别处、ShutAllFunctionWindow 等所有关窗路径 都自动跟上,不需要去枚举它们。

这一版是拿一次回归换来的,记在这里。 中途试过反过来做:不再过滤 stationId, 让原版去开物流站窗口,我们只补开制造台。结果 inspectStationId 被写成了非零值, 而窗口最终却是关的;此后再点任何带物流站的建筑——大型采矿机、普通物流站—— 原版的开窗条件 inspectStationId == 0 && stationId > 0 永远不成立,全都开不出窗口。 一个只想加功能的改动,破坏面扩散到了整类建筑。 教训写进了代码注释:不要和原版抢一个它自己记着状态的开关——要么完全交给它, 要么完全自己来,中间地带会把它的状态机留在一个它自己回不去的位置, 而症状出现在完全无关的建筑上。

改不了,是因为自动布局每 tick 把 localLogic 强制写回去: 面板上那三个「需求 / 仓储 / 供应」按钮点下去、松手就弹回来,而玩家完全看不出为什么。 现在方向只在这一格刚被指派给某种货时写一次,之后归玩家;自动布局仍然负责 「哪一格放哪种货」,那是跟着配方走的。于是「把原料格改成仓储、改用传送带喂料」 成立了——虚拟物流那几处本来就是严格按 localLogic 判的,运输机也会绕开它。 配方换掉后残留的旧格子仍然会从「需求」翻成「供应」(不然它会一直向物流网要一种 本配方不用的货),但仓储和供应一律不碰,那是玩家自己设的。

窗口位置属于「你改了就得负责还回去」:制造台窗口是所有制造设备共用的, 挪过之后不还原,下次点开一台普通制造台它还停在旁边——本仓库 MultiProductUIPatches.RestoreSlot1 那条规矩的又一次应用。第一次见到时记下原位, 不是巨型建筑就放回去。并排的坐标是从两个窗口的 rect 现取的,并且会把量到的 锚点 / 轴心 / 宽度打一行进日志——对着截图调 UI 这件事本仓库付过三轮的账。

仍然是自动的、改了会被写回去的:运输机数量、运送量、储能充满、储物格容量。

修复:巨型建筑换配方会让存货凭空变质

产了 100 个电路板,换成齿轮配方,那 100 个电路板当场变成 100 个齿轮。 病因在 MegaStationPatches.SetSlot:它按新配方重排储物格时直接写 itemId, 而 count 一动不动——于是「换个配方就能把便宜货变成贵货」。 inc 和品质点数也一并被新物品继承了。

第一次的修法是「把旧货搬到空格」,玩家的反馈是「换个配方,铜矿铜块噌地跳到最后两格去了」 ——所以第二次才找对:谁都别动。 布局不再是「从 0 号格往后顺次写」,改成三遍认领:

  1. 新配方要的货,优先认领已经放着这种货的格子——位置一格都不动;
  2. 剩下的去占没有存货的格子;
  3. 没被认领、又还有存货的格子,就是旧配方剩下的东西:原地不动,只把方向改成本地供应, 让运输机取走;取空之后下一轮自然被清掉。

于是变质在结构上不可能发生(写入只会落在「本来就是这种货」或「一件存货都没有」的格子上), 而且没有任何东西会跳位置。三遍的顺序是必须的:「已有的先认」要在「找空格」之前全部做完, 否则先处理的那一种货会占掉后一种货正在用的那个空标签格。

一个空格都找不到时宁可不排这一格:机器会因为找不到格子而停着等,那是看得见、 取走旧货就能恢复的故障,而变质是不可逆的。催化反应器那两格也走同一套认领规则—— 它没有理由成为例外,而它的容量必须在认领之后重申一次,否则新占的格子会带着 物流站的一千万上限,让第一座反应器把全网的催化剂吸光。

而且玩家亲手换配方时,机器里的东西直接退回伊卡洛斯机甲。

换配方前机器里有三处存货:储物格(看得见的那一堆)、served[](已经喂进去、还没消耗的原料)、 produced[](还没排进储物格的产物)。后两处原本会被 SetRecipe 连数组一起重新分配掉—— 这是原版行为,但原版的备料只有几个,巨型建筑是 requireCounts × 200,量级完全不同。 现在三处一并退回机甲。

挂在 UIAssemblerWindow.OnRecipePickerReturn前置上:SetRecipe 一跑完, served / produced 已经是新数组、recipeExecuteData 已经是新配方,就再也没有依据 知道「刚才那台机器里装的是什么」了。挂界面而不是挂 SetRecipe 还有两个理由: 后者拿不到 PlanetFactory,而且它会被读档后的重建路径调到,那时候往机甲里塞东西是错的。

一件不留在机器里,装不下的掉在脚下——所有者拍板。 实测一次换配方要处理 9 万个铜矿,而机甲背包满打满算约三万六(每堆 300 × 约 120 格),所以「都进背包」 物理上做不到。TryAddItemToPackagethrowTrash 正是原版为这种情况准备的: 背包 → 配送背包 → 掉在玩家脚下,三级之后机器里一定是空的。

掉地上既不丢也不卡,两条都读过 IL:TrashSystem.AddTrash 按堆叠上限循环下料 (V_11 = min(count, StackSize) 配 IL 046C 的回跳),9 万个铜矿约 300 个掉落物而不是 9 万个; TrashContainer.NewTrash 池子满了翻倍扩容,不会覆盖也不会丢;而且这条路给的 lifeexpire 都是 0,GameTick 的老化整段被 expire > 0 挡在外面,不会过期消失

一个会凭空复制物品的坑,写在这里。 开了 throwTrash 之后 TryAddItemToPackage 的返回值含义变了:它只报进了背包的那一部分(IL 00E8 返回的是 AddItemStacked 的结果),而配送背包和地面那两级同样消费了物品。按返回值去扣源头, 等于把掉地上的那部分又在机器里留一份。所以这里整份扣掉,不看返回值。

配方选择面板之外还有第二条路:复制 / 粘贴按钮和蓝图粘贴到已建好的机器上走的是 BuildingParameters.PasteToFactoryObject,完全不经过那个面板。两条都挂了,日志标明是哪一条。

背包装不下时只扣真正收下的那部分(原版的产物取出按钮是无条件清零的,塞不下就没了, 那个 bug 不抄)。

诊断的账也记一笔。 第一版的日志只在「真的退了东西」时才说话,于是玩家反馈 「没退回背包」时,日志里既没有成功行也没有失败行——「补丁没跑」和「跑了但提前 return 了」 长得一模一样,白白花掉一个往返。这条规矩本仓库已经付过五次账,这是第六次。 现在每条分支都打一行并说明原因。第二版还出过一次自相矛盾的输出: 「机器里本来就是空的」后面紧跟一条「背包装不下」的警告——真相是机器不空、 而是背包一件都没吃下。「什么都没退成」和「本来就没东西」必须分开报。

顺带发现并补上了品质检查的一个缺口。 preloader 把 Player.TryAddItemToPackage 也改写成了「从侧信道读品质」(它在 BuildForwardToCallee 那一族里), 可 tools/verify_quality.ps1 的消费者名单里没有它——于是本 mod 里 两处早就存在的调用点一直没门禁HubCourierSlotPatches::TakeOutMultiProductUIPatches::TakeProduct),它们消费的是上一个调用者留在寄存器里的值, 品质会凭空长出来。名单已补,两处都加了清零,新的退货点写的是真实品质。 检查现在报 6 个调用点、全部已声明。

这正是那条检查存在的理由:它防不住「写错」,但能防住「漏掉」,而漏掉正是实际发生的那一种。

物品品质:同一种金属可以更纯,而纯度是一格货的属性

preloader 给游戏加了 29 个孪生字段,品质从此和增产点数一样在整条主干道上流动: 一格记整格总分,单件分数 = 总分 ÷ 件数,合并相加、拆分按件数带走——加权平均自动成立。 掺粗料会把平均拉下来,那正是设计要的压力。

唯一来源是第十一座巨型建筑「同位提纯厂」(配方类型 18,180 MW),采出来的矿品质恒为 0。 三条配方各是一道真实工艺:电解精炼(收率 80%,每件 50 分)、区域熔炼(40%,75 分)、 羰基提纯(15%,100 分)。收率就是成本,不必再设计第二种成本。 三级的门槛也各不一样:一级只要本地的硫酸和水,二级要气巨采来的氮气(门槛是物流不是化工), 三级吃一氧化碳——顶尖品质的门槛是整条 C1 化学链

⚠️ 品质目前能产、能流、能存、能看,但还不产生任何效果。 效果层(品质沉淀进建筑)是下一步的活。这一条写在明处,免得被当成 bug。

三级全部万用,而且进料是金属块不是矿石

三条配方不是「铜一条、硅一条、铁一条」,那会变成三条死配方。它们是三个万用模板: 提哪一种金属由每台建筑在面板上自己选。

进料是金属块,两个原因缺一不可:本 mod 的大型采矿机对铜矿、硅石、钛石和三种稀有矿 直接出产物,按矿石进料的话铜这条线连原料都凑不出来;而既然不能给每种金属再开一个 「高纯 X」物品(那是枚举,21 种金属乘三档放不进合成面板), 「提纯过的铜」和「粗铜」就只能是同一个物品,差别记在那一格的品质上—— 这恰好就是品质系统存在的理由。

候选清单是推导出来的:先查采矿机的产物映射,再查本 mod 矿种声明的锭, 都没有才扫原版熔炉配方并取第一个既非流体也非矿石的产物。加新矿它会自己长。 整张表每次启动都打进日志,每条还写着是靠哪一级规则定的——原版配方表在 resources.assets 里离线读不到,那张日志是唯一的验收手段。

反复提纯不叠加:注入的是「每件固定分数」而不是在原有品质上加,所以回炉只亏料。

这条线逼出来的两刀,都在「同一种物品既进又出」上

  • MegaStationPatches.FindSlot 原来只按物品 ID 找格子,于是产物被倒回进料的需求格, 一件都出不了厂——机器在转、电在耗、产量为零。现在按方向找:原料找需求格、产物找供应格。
  • 虚拟物流没有「站点不和自己做买卖」这条边界(原版 RematchLocalPairs 的内层循环从 this.id + 1 起,天然没有这个问题),于是刚提纯好的货会被搬回自己的需求格无限自循环。

顺带补上了虚拟物流搬品质quainc 是同一种量,此前只搬了 inc, 结果是货一离开建筑品质就没了、源头那一格反倒单件分数虚高。

面板:选料行改成物品选择器,燃烧厂同步

提纯厂和氧化还原燃烧厂的选料行不再是 ◀ 名字 ▶ 循环,而是 名字 ▼ —— 点开原版的物品选择器,限定在允许的名单里,搜索框照常能用。 候选一多循环就不好使,而那个「带滚动条的下拉框」游戏里本来就有。

存档格式:CargoContainer 版本 2 → 3,带本 mod 存的档没有它打不开,老档照常能读。 已知缺口:自动集装机「正在叠的那一堆」的品质不入档(它的 Import 没保留版本号), 每台最多损失一堆在制品,有界。

活性透镜:一枚会在射线里自己长回来的引力透镜

插的还是原版射线接收站,不加建筑。原版透镜烧得毫无条件——戴森云还没铺、一度电没发, 照样十分钟烧完;活性透镜每扣一点的同时,按这一刻真的收到多少射线加回去一点。 满照下寿命 2.5 倍,背光面和引力透镜一样老。愈合速率严格小于 1,所以净消耗永远为正, 它是更耐用的消耗品而不是永动机。

发电 ×5、临界光子 ×3,两个倍率互相独立

一条被订正的判断(同一轮里自己推翻的)

先说「要把光子和电力拆开,就得动那三个方法」——错的。光子产量是 productCount += capacityCurrentTick / productHeat,而 productHeat 就是 PrefabDesc.powerProductHeat,和分子完全解耦。所以两个倍率是两个独立旋钮, 一处 IL 一处配置,那三处一个都不用多碰。

不加新建筑的代价,写在这里

引擎不从透镜物品上读任何东西catalystId 只是一次相等比较,cata = 2 × (1 + inc) 是硬写在三个方法里的字面量。所以「只加一个新透镜、让它进原版接收站」除了换个图标, 行为和引力透镜一字不差——倍率只能落在 transpiler 上,也就是设计稿里明确否掉过的那条 「同一个乘积写在三个方法里」。三处现在一起改并断言;漏一处的症状是功率曲线抖而不是报错, 因为接收站实发的电和它报给电网的最大出力会对不上。

自愈必须同时补喷涂点数,这是设计稿漏掉的

原版消耗块是两个字段一起走的:扣 1 点的同时按当前喷涂等级扣对应的点数,比值才不变。 只往一边加回去的话,比值单调下滑——healRate 0.6 时喷涂点数在第 3600 步就归零而寿命要到 第 9000 步,延长出来的那 15 分钟透镜是完全没喷涂的,×5 退回 ×2。全程不报错, 只有功率曲线慢慢往下溜。

「放不进射线接收站」——漏了一条插入路径

插入侧有三条路,而且都不是比对白名单,是照着 catalystId 点名要货。第一版只改了 窗口和传送带两条,漏了 PlanetFactory.EntityFastFillIn(背包 shift 点击建筑)—— 它读 catalystId 然后照着那个号去背包里拿,所以 shift 点永远只塞引力透镜, 而且什么提示都没有

更该记的是第二件事:那一版注册、图标、六处 transpiler 全是绿的,日志因此分不出 「补丁没跑」和「补丁跑了但某个闸挡住了」。这是 CLAUDE.md 里已经记了三次的同一条规矩 ——有「什么都不做」分支的功能,那条分支要在同一次提交里带上日志行——又犯了一次。 现在每次放入都会打一行,直接说是哪道闸挡的。

顺带补上一条早就生效、文档里一直没写的事

喷过增产剂的透镜会按喷涂等级提高接收站增益,而引擎的钳位是 10 级不是原版顶格的 4 级。 本 mod 的增产剂能到 6 级,于是 Mk.V·浓缩 喷的透镜把接收站推到 ×5(原版顶格是 ×4)。 这不是新做的东西,是两件既有事实撞在一起的结果。

其它

  • ores.jsonitems 段新增 iconFromName:按 ItemProto.Name 反查原版模板, 不必写裸号。原版 proto 在 resources.assets 里离线枚举不出来,写错号不报错、 只会静默拿另一件物品当模板,连图标带 DescFields 一起错。
  • 修掉两份特性文档「配置速查」里的陈数据:巨型建筑八座 → 十座、新建筑七台 → 八台, 并补上一直缺席的 redox.json 一行。

小型采矿机的机内缓存 50 → 10000

普通那台只改这一样:不提速、不免消耗、不换产物,只是让传送带偶尔堵一下不至于停产。 新开关是 advancedminer.jsonsmallMinerCapacity,填 0 或 ≤50 就是保持原版。

而这一条差点做成「改完更慢」,原因又是同一个形状:一个常数两种身份。

原版那个 50 既是「缓存满了停采」的闸,也是节流分母: speedDamper = min(1, −2.45 × min(1, 缓存 / 50) + 2.47) —— 装到 30 件开始减速,装满 50 降到 2%。 只把闸抬到 10000,采矿机就会以 2% 的速度从 50 往 10000 爬,比不改还糟。

而且这次两种身份不在同一个方法里,所以比以往更难看见:闸是 MinerComponent.InternalUpdate 里的三处 ldc.i4.s 50(本 mod 早就转译接管了), 节流却写在 GameLogic._miner_parallel @0580 和 FactorySystem.GameTick @04D9, 两处本 mod 一行都没碰过——大型采矿机是靠前缀把算好的值覆盖成 1 绕过去的, 所以这个分母从来没有暴露过。

现在按同一条公式、拿新上限当分母重算:装到 6000 开始减速、装满 10000 降到 2%, 原版的背压手感一点没变,只是刻度大了 200 倍。

顺带实测清楚了分支判据:_miner_parallel 是按这台采矿机有没有物流站分路的—— 有站的按 max(槽位上限, 3000) 算,没站的才走 productCount / 50。 所以大型采矿机的节流其实早在槽位被抬到一千万时就已经失效了,这次的改动碰不到它。

新增「小型速采机」:一台产量被钉死的采矿机

它和大型采矿机不是「大小之分」,是两条曲线:大型采矿机跟着「矿物利用」系列科技一路涨, 这一台从头到尾恒定 10000 矿/分钟、固定 1 MW不吃科技加成、也不看脚下压着几条矿脉。 无前置科技,1 铁块 + 1 铜块手搓 1 秒;整台克隆大型采矿机(模型换色), 自带物流站槽位、容量 10 万,行星内物流运输机照常来取货。 建造栏在「巨型建筑」分页第 13 格,靠子项行横向滑动翻到。

machines.json 因此多了一种 kind: "miner",四个数都在它的 miner 段里: oresPerMinute / workEnergyWatt / stationCapacity / consumeVeins

「钉死」是它全部的实现难度,而反解的对象必须是 miningSpeed 不是 speed

原版每 tick 累加 time += power × speedDamper × speed × miningSpeed × veinCount, 其中 miningSpeed 被科技放大过、veinCount 是脚下的矿脉数——两个都不是常数。 所以「把速度写进 prefabDesc」只会得到「面板上的数对、产量随科技涨」, 正是本仓库反复踩的那种「界面和逻辑读的不是同一个源」。真正的做法是每 tick 反解, 把这两项都除掉,让它们乘回来时正好抵消。

为什么反解 miningSpeed 而不是 speed,这一条是算出来的不是试出来的speed 是整数,把整个比例塞进它,矿脉越多、科技越高,商越小、截断误差越大—— 「12 条矿脉 + 满级科技」时低约 10%。一台卖点是产量钉死的机器,产量随矿脉数浮动 10% 就不叫钉死。 现在 speed 固定 10000(单位是百分之一,于是采矿面板读作 100%,对定速机器这正是该显示的), 比例由浮点的 miningSpeed 承担。实测:period=600000、12 条矿脉、科技 1.5 倍, 反解出 miningSpeed=13.88889,折合 10000 矿/分钟。

两处已知的不一致写在明处:莫桑石的钻头消耗对它不生效(那条规矩只作用于「被本 mod 加速的采矿设备」, 这一台走自己的一套);放置规则是原版的,不像大型采矿机那样放开了重叠与油井。

面板得单独修一次:反解做对了,面板照样报 1080。

实测三个 10 秒窗口,这台机器实产 9993 / 9998 / 10094 矿每分钟,每 tick 累加 time=1666666 对目标 1666667——机器一直是对的。错的是面板:它另有一条公式,而且读的是 GameMain.history.miningSpeedScale 这个字段,而结算读的是 ldarg.s miningSpeed 这个参数, 我们替换的正是后者。于是面板算出的是「100% × 科技 1.5 × 12 条矿脉 = 1080」, 也就是一台普通采矿机在同样条件下的产量。这是第 3 号坑:界面和逻辑读的不是同一个源。

两条公式除这一项外逐项相同,所以只换这一项就够:采矿机面板、矿脉采集面板、控制面板里的同一行, 各接管 1 处。反解的公式只写一份,结算和面板共用——同一个事实抄成两份一定会分叉。

每个面板读两次 miningSpeedScale,只能改其中一次。 另一次在「预计可采 X 小时」那一段, 那里是拿采矿速率去的,换成反解值会让预估时间凭空缩短一个数量级。 判据取自公式本身而不是位置:产量那一段以 ldc.r8 0.0001(即 speed/10000)起头, 「预计可采」那段没有这个常数。命中数不等于 1 就整段放弃并报错。

「参考速率」和「理论产能」要按另一种方式改一次。 这两处不是在采矿机身上读科技倍率的—— 它们在进入采矿机循环之前就把它取进一个局部变量,全星球的采矿机共用那一个数, 所以在取值处替换会把这一台的数算到别的采矿机头上,必须在循环内部换。 两处各有矿脉 / 原油 / 水三个分支、形状完全一样,而且采矿机就在旁边一条指令上; 那个局部实测是 MinerComponent&,把它那条 ldloc 原样复制一份压栈就是现成的 ref 实参—— 不必数它是第几个局部、也不必判断它是值还是托管指针,而那正是这类改写最容易静默出错的地方。 各命中 3 处,不是 3 就整段放弃。

顺带补掉一个读档后才会露头的洞:认这台机器用的 pcId 表原先只在 CreateEntityLogicComponents 里填,而读档不走那条路(存档是直接还原组件池的), 于是读档之后功率覆盖和这次的速率换算都会静默失效。现在结算路径上也补登记一次, 建造、读档、蓝图粘贴三条路都自愈。

顺带给 machines.jsonminer 段加了 debugLog(默认 false):打开后每台每 10 秒一行, 报这一窗口的真实产量、power/damper/speed/矿脉数每 tick 真正累加的 time 和仓位。 生产统计是全星球汇总,同一颗星上有第二台采矿机就分不开——正是这条状态行把 「机器不对」和「面板不对」分开的。计时用 Stopwatch 而不是 Time.realtimeSinceStartup(那个只能在主线程调,而这条路跑在 _miner_parallel 上)。

模型 ID 按解析器自己的警告钉死成 699。 配置里原先写 706,启动时解析器打了 「期望的模型 ID 706 不可用,改用 699」——模型 ID 进存档,那行警告报告的是一个不稳定的号, 不是一次成功的兜底,看见就该当场钉住。

1.7.0

氧化还原燃烧厂:第十座巨型建筑,也是第一台既是组装机又是发电机的机器

它把还原剂和氧化剂压成药柱(组装机那一侧),再把刚压出来的药柱直接烧掉发电(发电机那一侧), 药柱全程不上传送带。这正是它能烧那些没法用管道输送的东西(金属粉、稠得挂壁的重馏分)的原因。

一条被订正的旧断言

CLAUDE.md 一直写着「DSP 的组件模型表达不了『边转化边发电』」。那句话只对了一半: 一个组件确实做不到,但一台实体挂两个组件做得到。证据在三条指令之内—— CreateEntityLogicComponentsisPowerGen(IL 059E) 和 isAssembler(IL 1122) 是两个 完全独立的顺序 if,中间隔着十几个别的组件判断;EntityData 也为它们各留了 assemblerId / stationId / powerGenId / powerConId 四个字段。 先例就在本仓库里:综合物流枢纽同时挂 station 和 dispenser。

那句话从来没被测过,是从「配方没有发电输出字段」推出来的——前半句是真的,后半句不跟着成立。 和「ERecipeType 只剩 14 个」「kMaxCargoFlowSpeedPerSecond 是上限」是同一类错误: 这份文档里的每一句话,在重读 IL 之前都只是主张。

喂燃料不需要任何 transpiler

PowerGeneratorComponent.SetNewFuel(itemId, count, inc) 是公开方法,自己会从 LDB 取 HeatValuefuelHeatEnergyCap_Fuel 只看 fuelCount > 0烧的时候根本不查 fuelMask(掩码只是传送带取料和手动塞料的过滤器)。所以整条链就是一次方法调用。

滑动条:配氧比

面板是第七种模式,也是第一种三行的——两行选料 + 一行滑条。

  • 贫氧那一侧是严格推出来的:氧化剂只够烧掉 φ 比例的燃料,产量就乘 φ。
  • 富氧那一侧燃料已经烧完,再投不多出东西——多的纯属浪费。
  • 但只有这样的话最优解恒定在 100%,滑条就是摆设(合金牌号那次栽过同一个坑)。 所以富氧还有第二个效果:烧得更完全 → 更致密 → 够密度就跳档。 这一项在 redox.json 里明写是平衡旋钮,不假装是推出来的。

十二种组合,六种还原剂 × 两种氧化剂

能量密度 = 热值 ÷(1 + 氧需求 ÷ 氧供给),跨度 1.49 ~ 3.59 MJ/件,分三档药柱 (双元推进剂 7 MJ / 金属浆料燃料 11 MJ / 固体复合推进剂 14 MJ)。

金属排在最上面不是偏袒:Huggett 常数(每 MJ 约 13.1 MJ/kg 氧)是对含碳燃料成立的经验律, 而铝硅不含碳、本来就跳出那条线——有机物 0.51~0.67 氧/MJ,硅 0.32、铝 0.26。

两个氧化剂被自己的数值否掉了,记下来免得再被加回去:二氧化氮放的氧和氧气一样多(2.0) 却要多走两步氮链;硝酸铵只放 1.0(自己那四个氢先占掉两个氧)。留下氧气 2.0(便宜)和 硝酸 2.5(贵)。顺带补上一个洞:硝酸此前产出之后没有任何配方吃它。

功率 30 GW,以及一个反直觉的测算

造它要 100 台可燃性液体发电厂(自身合计 21.6 GW)+ 1000 能量矩阵。 它卖的不可能是效率,因为卡诺已经饱和:3000 ℃ 折 0.636,而烧氨的 2000 ℃ 是 0.6082, 再高一千度只多三个百分点。预混药柱买到的是功率密度——不吸空气、没有烟气体积。

能量守恒是构造性的:产量 = floor(份数 × 热值 × min(1, φ) ÷ 档位热值)。 全部 6 × 2 × 61 = 732 个组合逐一验算,没有一个产出热值高于投入,所以这条配方 在启动自检面前不需要豁免。

一次崩溃和它的修法

挂上发电组件之后点开建筑直接抛 NullReferenceException at UIPowerGeneratorWindow._OnOpenUIGame.OnPlayerInspecteeChange 里那一串窗口判断是顺序排下来的独立 if,最后命中的赢, 而 powerGenId(IL 0182) 排在 assemblerId(IL 00A7) 后面,于是发电机窗口把制造台窗口顶掉了。 修法不是去把那个窗口伺候好,而是压根别开它——像 stationId 一样在读取处报 0, 发电组件照常 tick、照常发电。顺手把那个 transpiler 改成两处一起改并报匹配数。

跟着改的

  • 金属可作燃料:铝块 5.75 MJ、高纯硅块 6.25 MJ,只给药柱燃料位、不给化学燃料位 (实心锭子扔进燃煤锅炉不会烧,给了就是白送火电一次膨胀)
  • ores.jsonvanillaHeat 新增可选 fuelType,并在「有热值没燃料位」时告警
  • OreEntry 新增 ingotFuelType / ingotHeatValue,在 PostAddData 按最终状态核对后写入
  • 新配置 redox.json(单独成文件:JsonHelper 的磁盘覆盖是整份文件的)
  • 四张自绘图标;三档药柱三种剪影(双罐 / 矮胖桶 / 细高柱),不是同一只罐换盖子

能量审计:配方图在漏,修掉了 87%

按「产出可燃热值 > 投入可燃热值」扫全部 77 条配方,14 条为正、合计 +528.6 MJ, 最严重的 苯 · 蒸汽裂解 一条 4 秒配方就凭空多出 114.8 MJ。而万倍速建筑把耗电 摊薄到几乎为零——1 倍速机器约 30 倍回报,巨型建筑约 800 倍。这是个能跑的永动机。

三个各自独立的错误:

  1. 「一件油 = 几个 CH₂」在同一份配置里有两个答案。 催化裂化的注释按 4 算, 而费托合成(4 CO + 8 H₂ → 精炼油×4 + 水×4)要碳守恒只能是 1。同一个物品、差 4 倍, 于是所有烃类配方的配比都偏了
  2. 四个馏分的热值是按「越重单位体积能量越高」排的(3.0 / 4.5 / 8.0 / 12.0), 而记账单位是每 CH₂——每件含碳量相同就该热值相同。那道阶梯正是裂解"点石成金"的来源
  3. 原版氢是本 mod 摩尔锚点的 4.1 倍(286 kJ/mol 应折 1.96,原版给 8.0)

决定性的一条测量:原版自己就是按「一件 = 一个 CH₂」定的。 1 个 CH₂ 的燃烧焓 679 kJ/mol 按煤锚点折 4.66 MJ,而原版精炼油是 4.50——差 4%。所以修法是 向原版对齐,不是改它。

改了什么

  • 四个馏分热值统一为 4.5 MJ(石脑油 3.0、蜡油 8.0、钒渣油 12.0 全部改)
  • 六条烃类配方按「一件 = 一个 CH₂」重推配比:
丙烯 · 催化裂化     蜡油×12 → 丙烯×2 + 乙烯×3
苯 · 蒸汽裂解      石脑油×18 → 乙烯×3 + 丙烯×2 + 苯×1 + 氢×3
苯 · 催化重整      石脑油×12 → 苯×2 + 氢×6
氢 · 蒸汽重整      石脑油×4 + 水×4 → 一氧化碳×4 + 氢×8
石脑油 · 加氢裂化    蜡油×5 + 氢×1 → 石脑油×3 + 精炼油×2     (原先碳都不守恒)
石脑油 · 常减压蒸馏   不变(物理分离,本来就 10 进 10 出)
  • ores.json 新增 vanillaHeat 段:改原版物品热值。目前只有一条—— 氢 8.0 → 1.96 MJ。ID 是写死的而原版 proto 离线枚举不了,所以配置里同时写名字、 运行时交叉核对 Name,对不上就拒绝改并报 ERROR

代价说清楚:烧氢发电的收益变成原来的四分之一。 不想要就把 vanillaHeat.enabled 改成 false(覆盖文件即可,不用重新编译)——但那样蒸汽重整一条 4 秒配方仍然凭空多出 约 50 MJ。

残留 +67.2 MJ / 11 条,绝大部分是正当的:真实吸热(蒸汽重整 +5.5、蒸汽裂解 +4.6、 水煤气 +2.4)、阳光(生物温室的零原料配方)、以及「刻意不给热值的中间体」造成的假阳性 (甲醛和聚丙烯腈按 fuelType 判据不给热值,于是吃它们的配方看着像在造能量, 而整条链其实是负的)。

这次审计做成了启动自检。 EnergyAudit 挂在 PostAddDataAction 的最后,和 I18N.VerifyCoverageProtoArrayCheck 一族,读的是 LDB 里的最终热值(所以它看得到 vanillaHeat 改过之后的值,而不是配置里写的)。

三类正当情况由配方自己的 energyNote 声明,而那个值本身就是理由——内置一张硬编码白名单会随配方变动慢慢腐烂,而且没人看得见它为什么在那儿。 当前状态:77 条配方、11 条已声明豁免、0 条无法解释。 以后加新配方,产出比投入值钱又写不出理由的,启动就会被点名。

有一件事任何数值都修不了:万倍速让电几乎免费。 电解水在综合化学厂里仍然是 1.2 kJ 电换 3.9 MJ 氢。这个速度下任何吸热的产燃料配方都是发电机, 真正的杠杆只有「哪些配方允许进巨型建筑」和「产物到底能不能烧」。

巨型建筑不再发黑,九座的体量也拉开了

报上来的是「建模大多呈现为黑色而且用到的细节都差不多」。两半都是量出来的,不是猜的。

黑的第一层:_Color 是乘在自绘图集上的。 那九组 tint 是「九座共用同一个原版网格、 只能靠染色区分」那个时代调的,所以饱和度一路推到 0.6~0.83。而现在每座都有自己的程序化网格 和自己那张浅灰细节图集(主钢板 196,200,208),高饱和低明度的 tint 会把整张图乘成近黑—— 算了一遍 图集 × tint九座里六座主面亮度低于 110,熔岩冷却厂 60、观微对撞机 52。

现在的规则是色相负责识别、图集负责明暗:饱和度封顶 0.45、明度托底 0.88, 一个色相都没动,九座全部抬到 108 以上、多数在 130 以上。

黑的第二层:_MS_Tex 仍在用原版那张图、却按我们完全不同的 UV 去取。 这个开关默认仍然关着 (那条「实测整座不可见」的推翻结论不变),但常量不再是凭空写的 (70,150,0,150) 了—— 改成读原版那张图取全图平均。通道怎么打包依然不知道,而且依然不需要知道: 平均值天然落在原版自己用过的取值范围里,最坏是「像一面普通的原版表面」,不可能是全透。 读不出来就返回 null、退回原版那张并打一行 WARNING。

体量雷同的根因在 MeshKit.Place,不在细节。 它先把形体缩放到填满原版占地, 再拿原版高度封顶——于是细高的设计被压两次:高度封顶把缩放压下来,占地也跟着缩。 精馏塔和环形加速器最后落到同一坨方块上,画多少细节都救不回来

Place 加了高度倍率,九座各自在 megabuildings.json 里声明体量:

占地 高度上限
观微对撞机 ×0.74 ×0.78 全场最宽最矮
生物温室 ×0.72 ×0.75 一大片平铺顶棚
锤锻精工厂 ×0.70 ×0.85 低矮机身加龙门架
天工装配厂 ×0.68 ×0.90 装配线横着铺开
冶铸熔炉 ×0.64 ×1.00 标准体量
熔岩冷却厂 ×0.62 ×1.05 结晶塔略高
燔石化工厂 ×0.58 ×1.30 三只立罐
综合化学厂 ×0.56 ×1.50 精馏塔顶着
催化反应器 ×0.55 ×1.45 提升管加再生器

图标的颜色改成从建筑的 tint 算出来。 之前这是两份手工维护的副本: make_icons.py 里写死九个十六进制,megabuildings.json 里写 tintR/G/B。 量过之后发现色相从来没走散(九座全部差在 0~12° 以内),走散的是饱和度和明度—— 而且正是上面那次调 tint 让它们分的家。

现在 make_icons.py 自己读 megabuildings.jsonbuilding_color() 返回 图集主钢板 × tint,也就是建筑在游戏里真实呈现的颜色。九个十六进制全删了, 建造栏里的图标从此是成品预览,不是近似。想让图标更艳只要改一个函数里的一个系数。

两条实测出来的坑(都是启动之后才发现的,不是读出来的)

其一:第四个窗口来抢这一次点击。 UIGame.OnPlayerInspecteeChange 里那一串组件判断是 顺序排下来的独立 if,每个都先 ShutAllFunctionWindow(),所以最后命中的赢—— 顺序是 assemblerId@00A7 → powerGenId@0182 → stationId@01AA。给建筑挂上发电组件之后, 点开它直接抛 NullReferenceException at UIPowerGeneratorWindow._OnOpen。 把 DMD 偏移换算回原始体(0x50 之前有两条短跳转,各 +3 字节)落在 ldfld powerNetworkDesc@0046 / callvirt _Open()@004B 上——那个窗口的某个子部件对这台建筑 根本没被装配。修法不是去把那个窗口伺候好,而是压根别开它,和 stationId 从第一座巨型建筑起 就在做的事完全一样。

而修完之后同一个洞在隔壁又开了一次:补上 isPowerNode(见下一条)的同时它也就有了 powerNodeIdOpenNodeWindow()@0640 同样排在 OpenAssemblerWindow()@0361 后面—— 电力节点窗口接替发电机窗口,症状从「点开就崩」变成「点开了但没有配方按钮」。 教训不是「再补一个」,而是这一族要按清单一次数清:那个方法里有 23 个组件判断, 判据是「这台机器有没有这个组件」而不是「我想不想要这个窗口」。 巨型建筑命中 assembler / station / powerCon / powerGen / powerNode 五个, 其中 powerCon 不开窗口,其余三个全压掉,改写数不等于 3 就报错。

其二:NewGeneratorComponent 根本不负责并网,而不对称本身就是证据。 全汇编往 PowerNetwork.generators 里加元素的只有一处——PowerSystem.OnNodeAdded, IL 04DD 的 list_sorted_add(net.generators, node.genId);而 OnNodeAdded 只被 NewNodeComponent 调用。对照旁边那条路:NewConsumerComponent OnConsumerAdded。 也就是说耗电体后建也能立刻并网,而发电机只能顺着「节点」进电网—— isPowerGen 的意思是「它能发电」,isPowerNode 才是「它接在电网上」。 原版每座电厂脚下那根连接线就是这件事;而我们的 prefab 克隆自物流运输站,那是个纯耗电体。

症状是本仓库最怕的那一种:每一步都成功,而功能不存在。 组装机在跑、配方在产、电在扣、燃料也搬进燃料舱了,只有 networkId 停在 0, 于是一焦耳都没进电网,而且哪里都不报错。

现在 ApplyGridHookup真实的原版电厂 prefab 上现读 isPowerNode / powerConnectDistance / powerCoverRadiusconnectFromItemId,默认 2204),不写死—— 那几个值在 resources.assets 里,离线读不到,和蓄电器、发电机克隆只收倍率是同一个理由。 覆盖半径也照抄,免得这座电厂悄悄变成一座变电站。

改动之前建的那台要拆了重建:prefabDesc 是第 1 号坑,只对新建实体生效。 另外新增了一行一次性自检,把配方号、powerGenId、networkId、燃料舱、发电上限和实发功率 打在同一行——下次再出「能产不能发」,一行日志就够定位,不用一轮一个假设。


1.6.5

综合化学厂:第九座巨型建筑,也是第一台吃多种配方类型的机器

化学(2)、电化学(9)、氧化还原(10)三类它全接,速度仍是 10000 倍。

它补的是一个真空:本 mod 的化学配方分散在三个类型上,而巨型建筑只有燔石化工厂 一座(类型 2)——类型 9 和 10 至今没有任何巨型建筑,整条 C1 链、氮链、三期有机化学 和炼油线的主力配方全卡在 1 倍速的克隆建筑上。

  • 建造:能量矩阵 ×1000 + 燔石化工厂 ×1000,10 秒
  • 功耗:工作 360 MW / 待机 60 MW——比全场第二贵的那台(观微对撞机 45 MW)高一个数量级。 这个数是平衡旋钮:按「它顶三台燔石化工厂」推是 72 MW,实际取五倍,因为它是三类化学配方 唯一的 10000 倍速通路,而建造门槛是一次性的,长期制衡只能落在电价上
  • 位置:本 mod 分页建造栏第 10 格

它没有把燔石化工厂变成死内容,因为它吃掉那一座。 同样 10000 倍速、只能跑一种类型的 建筑本来会被它支配成死内容,而这条建造配方让那一座变成了它的前置。先例是碳化硅能量枢纽 ——它服务的仍然是同一批锂电池蓄电器。

原版说「一台机器一种类型」,而且这句话是硬的。 突破它要改代码,但实际代价比当初估的 小得多:全仓库 15 处读 assemblerRecipeType真闸门只有 8 处,而且全是同一个形状, 换成一次查表即可。另外 7 处是假警报(ReadPrefab 是写入、FactorySystem.Import 比的是 == 4 只为挑动画长度、两处纯显示)。没有建筑登记多类型时整套补丁不打,原版 IL 一字节不动。

三件事是白送的,它们才是这个做法成立的原因:SetRecipe 完全不校验类型; AssemblerComponent.recipeType 存的是配方的类型而不是机器的(所以化工配方在这台机器上 音效动画自动还是化工厂那套);那个字段虽然进存档,但 18 个读写点没有一处拿它和 prefab 对过 ——不存在「读档发现类型对不上、把配方清掉」。

「制造于」改成追加而不是替换制造于 化工厂 / 综合化学厂。闸门改了却没人知道等于没改, 这一半和转译器一样重要。

顺带修掉一处过期判据MachineRegistry 判断「是不是本 mod 的配方类型」原先写死 9 <= t <= 14,而 ERecipeType 从来没有上限,16 就被那个区间漏在外面了。 现在是 t >= 9 && t != 15——原版占 1~8 和 15,其余全是我们的。

新增配置字段megabuildings.json

  • acceptsRecipeTypes:这台机器额外接受的配方类型
  • recipeItems / recipeItemCounts / recipeTimeSpend:单座建造配方,覆盖顶层那份全局的 (此前八座共用「铁块 ×1 + 铜块 ×1」)

1.6.0

有机物第一期:把甲醛和二氧化碳接出去

C1 链铺完之后链上有两处堵着:甲醛只有一条下游(还原钴),二氧化碳只有一个吃口 (加氢制甲醇)。这三条配方就是冲着它们去的,不需要任何新前置——用的全是 已有的甲醛、氨、二氧化碳。

  • 尿素(新物品):氨×2 + 二氧化碳×1 → 尿素 + 水。二氧化碳的第二个吃口。 现实中这是工业上固碳体量最大的一条路,合成氨厂排的碳大半从这里走
  • 乌洛托品(新物品,燃料 28.8 MJ):甲醛×6 + 氨×4 → 乌洛托品 + 水×6。 就是野外炉具烧的那种白色固体燃料块,无烟无灰。甲醛终于有了一个「烧掉就没了」的终端出口
  • 塑料 · 脲醛树脂:尿素×2 + 甲醛×3 → 原版塑料×2 + 水×2。 一条一滴原油都不沾的塑料路线:煤和水进去,塑料出来

前两条严格配平;第三条是交联网络没有固定分子式,配比锚在真实摩尔比 F/U = 1.5, 已在注释里写明。三条都是缩合脱水、价态不变,所以按反应类别全归原版化工厂

结果:甲醛从一条下游变成三条,氨从两条变成三条,二氧化碳从一个吃口变成两个。

为什么脲醛树脂直接产原版塑料,而不是新开一个「树脂」物品 新物品最贵的不是画图标,是给它找下游。本 mod 已经有一个反面例子——硝酸, 氮链造到它就到头了,全仓库没有任何配方吃它。再加一个没有下游的树脂就是第二个硝酸。 脲醛树脂现实中本来就是一种塑料,所以直接落到原版塑料上。

这一期没有做的

  • 乙酸(甲醇 + 一氧化碳)被推迟了。它在零前置范围内唯一的去处是当钴的还原剂, 而那条被「甲醇和一氧化碳分开用」精确支配:一份乙酸还 4 份矿,而造它花掉的 一份甲醇(还 3 份)加一份一氧化碳(还 1 份)也正好是 4 份,一模一样。 照仓库自己的支配判据,那是死内容。它真正的去处(醋酸乙烯 → 聚乙烯醇)要再加两个物品
  • 三聚氰胺同理:尿素能吃掉它,但它自己没有下游

1.5.0

催化反应器:第八座巨型建筑,也是第一台会记住自己状态的机器

前七座都是无记忆的——给够原料就出货,拆了重建一模一样。这一座记得床里那批催化剂 还剩多少活性

  • 催化剂不是「一炉一份」的原料,而是装在床里的一批:跑约 10 分钟、孔道结焦、 整床被吹进待生仓,等再生炉烧完碳送回来
  • 再生走氧化还原化工厂,不新开机器——烧积碳就是氧化,真实的 FCC 也正是 「反应器 + 再生器」两个塔
  • 回收九成:沸石在再生温度下会脱铝失活,工业上每天要补 1~2% 的新鲜剂。 所以这个环是闭的,但不是永动的,你得一直有一条补新剂的支线
  • 再生尾气是二氧化碳,而「甲醇 · 二氧化碳加氢」正好吃它,甲醇又是反应器的原料—— 这条闭环不是设计出来的,是把真实工艺接上之后自己出现的
  • 活性只在真的出货的 tick 才掉。断电、缺料、产物堆满这三种情况下机器本来就不产出, 活性一点不扣——不会出现「机器停着、催化剂照烧」
  • 催化剂和待生催化剂走机身自带的物流站,不占配方的原料格和产物格, 两边都不用接分拣器
  • 窗口下方新增一块只读面板:床内装填 / 剩余活性 / 待生仓,外加一行「还能跑多久」。 巨型建筑的储物格对玩家本来是不可见的,这块面板是唯一的窗口

三条只有它做得了的配方(碳氢全部严格配平)

  • 乙烯 · 甲醇制烯烃(流化床):甲醇 ×7 → 乙烯 ×2 + 丙烯 ×1 + 水 ×7
  • 丙烯 · 催化裂化:精炼油 ×6 → 丙烯 ×4 + 乙烯 ×6
  • 氢 · 催化重整:精炼油 ×1 + 水 ×4 → 一氧化碳 ×4 + 氢 ×8

第一条不会让原版那条 MTO 变成死内容:折成单位甲醇的乙烯产率两条完全一样, 流化床买到的是丙烯,不是更高的乙烯收率。

新物品:沸石催化剂、待生沸石催化剂、丙烯

  • 丙烯不是死路——它和乙烯一样裂解出单质碳,直接落进现有的烃热还原一族。 2 Al₂O₃ + 2 C₃H₆ → 4 Al + 6 CO + 6 H₂ 严格配平,两份丙烯顶三份乙烯 是碳数算出来的,不是拍的
  • 热值 14.1 MJ,按煤锚点从燃烧焓 2058 kJ/mol 折算

顺带

  • 修正了一条查证过的错误说法:ERecipeType 没有上限。原先文档里「只剩 14 一个空位」 数的是 Research(15) 以下还没用的值,不是真的天花板——枚举没有 Max 哨兵、 全仓库没有数组按它下标、两个 switch 超范围都落 default、productionMask 也不是按类型置位的
  • 巨型建筑的储物格诊断行改成按建筑类型各报一次。原先是一个全局 bool, 八种建筑里只有最先 tick 到的那一种会打出储物格清单

有机物第二期:丙烯 + 氨 → 碳纳米管的第三条路

把烯烃链和氮链汇到一起,终点落在原版碳纳米管上。

  • 丙烯腈(新物品):丙烯×2 + 氨×2 + 氧气×3 → 丙烯腈×2 + 水×6。SOHIO 法,一步氨氧化。 乙烯做不了这个反应——氨氧化要先拿走一个 α-氢,乙烯没有 α 位,丙烯的甲基才有
  • 聚丙烯腈(新物品):丙烯腈×2 → 聚丙烯腈×2。加成聚合没有副产物,两边数目相同
  • 碳纳米管 · 聚丙烯腈碳化:聚丙烯腈×2 → 原版碳纳米管×1 + 氮气×1 + 氢×3。 一条不依赖刺笋结晶矿脉的碳纳米管路线

氮在这条线上是全额循环的:2 份氨要 1 份氮气 + 3 份氢,碳化正好放出 1 份氮气 + 3 份氢。 不是凑的——氮的任务是把氰基挂上去、让链受热时环化成梯形,碳化时任务完成就被赶出去。 所以这条线的净消耗只有丙烯和氧。

碳纤维没有做成新物品:原版已经有碳纳米管这个「后期纯碳结构材料」,再加一个同位物品 就撞上支配判据。现实里 PAN 碳化得到的是碳纤维 / 碳纳米纤维,和碳纳米管同族不同形, 这处近似已在配置注释里写明。

顺带

  • ores.json 的配方引用新增 vanilla:<原版物品中文名> 写法。原版 proto 全在 resources.assets 里、离线枚举不了,靠人记号码写错了不会报错、只会安静产出别的东西; 现在可以写名字,解析不到会报 ERROR 并跳过整条配方
  • 修掉「塑料 · 脲醛树脂」的配方图标:原先指着原料尿素,合成面板上那一格看着就是一条尿素配方, 产物反而找不着——去掉之后回退到「图标跟随产物」

有机物第三期:芳烃主干,只做异丙苯法这一条

芳烃是整棵树上物品最多的一族,全做会撑爆物品栏,所以砍到只做一条: 苯 → 异丙苯 → 苯酚 + 丙酮,净新增四个物品,每一个都有去处

  • (新物品,燃料 22.4 MJ):两个入口——精炼油×6 走蒸汽裂解 (→ 乙烯×6 + 丙烯×2 + 苯×1 + 氢×3),或甲醇×6 走甲醇制芳烃(→ 苯×1 + 水×6 + 氢×3)。 后者放在催化反应器上:芳构化结焦比制烯烃更凶,苯环本来就是积碳的前一步
  • 异丙苯(新物品,燃料 35.8 MJ):苯×1 + 丙烯×1 → 异丙苯×1。乙烯做不了—— 氧化裂解要的是只剩一个氢的叔碳,只有异丙基的分叉处满足
  • 苯酚 + 丙酮(两个新物品):异丙苯×2 + 氧气×2 → 苯酚×2 + 丙酮×2。 单进双出,比例是配平的结果不是旋钮,所以丙酮过剩是这条线天然的取舍(它有热值,烧得掉)
  • 塑料 · 酚醛树脂:苯酚×3 + 甲醛×4 → 原版塑料×4 + 水×4。1907 年的电木, 第一种完全人工合成的塑料

两条通向原版塑料的路为什么不重复:脲醛那条吃氨,上游是氮气,得先有气态巨星和轨道采集器; 酚醛这条吃苯,煤和水就够。一条不要油、一条不要气巨,而每份塑料的树脂投料刻意对齐 (105 g vs 101 g),效率上谁也不比谁划算。

两个反直觉的相态:苯熔点只有 5.5 ℃(寒天会在管线里冻住,但游戏里仍是流体); 苯酚常温是固体,走传送带不进储液罐。

还没接上的:硝酸依然没有下游。苯已经有了,硝化本可以做,但苯胺自己没有去处—— 加两个物品最后还是停在一个没人要的东西上,不如等它的下游想清楚了一起做。

汽油化工联动:炼厂的三个单元

有机合成铺完三期之后,油这边和它之间还缺三样东西。补上之后精炼油从一个「原料」 变成了真正的枢纽,减压蒸馏塔底那桶渣也终于有了三个去处。

  • 提钒改吃渣油,这原本是个断头钒渣油 的注释一直写着「钒块 · 残渣提取提的就是它」, 而那条配方吃的其实是精炼油×40,跟渣油毫无关系。现在是 钒渣油×12 + 硫酸×4 → 钒块×1 + 二氧化碳×8——真空渣油的钒含量 100~1000 ppm, 轻馏分几乎为零,钒本来就全在塔底。不是给钒开后门:12 份渣油折 48 份原油, 和原先 40 份精炼油折回去的约 53 份同一量级,区别是馏分油不再被烧掉
  • 苯 · 催化重整(新配方):精炼油×3 → 苯×2 + 氢×6,严格配平,在催化反应器上。 这才是芳烃的主干来源——炼厂跑重整是为了提辛烷值,化工厂要的是它吐出来的芳环。 它同时是炼厂的氢源,正好喂下面那条脱硫。铂铼催化剂结焦,工业装置就叫 CCR 连续催化再生,和那座建筑的机制是同一件事
  • 硫这条线打通了硫磺(新物品)← 钒渣油×4 + 氢×2 → 硫磺×1 + 精炼油×3; 硫酸 · 接触法 硫磺×2 + 氧气×3 + 水×2 → 硫酸×2(严格配平)。 硫酸从此有两条路:石膏法要矿脉、顺带出玻璃,接触法要原油和氢。缺哪样走哪条
  • 四种新有机液体接进可燃液体发电:苯 700 ℃、异丙苯 800 ℃、丙烯 950 ℃、丙酮 1300 ℃。 它们无灰无硫无金属,位次完全由积碳定——苯是炭黑的骨架,所以能量密度是精炼油的五倍 却排在它下面。「能量密的反而烧不了高温」这条机制,苯是最干净的例子

一处改名:原先那条 氢 · 催化重整 做的是 steam reforming(烃 + 水 → 合成气), 而「催化重整」在炼厂里专指另一件事,重名会把人带偏。已改为 氢 · 蒸汽重整, 配方 ID 没动(存档引的是 ID 不是名字)。

一个真实的巧合:接触法的催化剂正是五氧化二钒,而 V₂O₅ 也正是钒渣油烧不了高温的原因。 同一种东西在一处是毒、在另一处是催化剂。

没做脱硫精炼油:「脱过硫的油该烧得更热」物理上对,但要表达它就得新开一个物品, 而那个物品作为燃料会严格支配原版精炼油。所以脱硫的产物直接落回原版精炼油。

作弊开关改成默认全开

cheats.json 里那六项此前默认全关,理由是「绕过规则的东西不该默认生效」。 现在默认全开:这个 mod 本来就是把后期产线整体拉满的,装它的人基本都会把它们打开, 默认关只是多一道手续。

要恢复原来的行为:在 profile 里新建 BepInEx\config\ProjectEden\cheats.json 把它们改回 false,重开游戏即生效,不用重新编译。总开关 enabled 改成 false 也能一次全废。

六条里只有 noCollisionPhysics 带实打实的副作用,而它现在也默认开着: 它把行星碰撞体对象池整个关掉,好处是机甲能穿墙,代价是建造工具的射线取点失灵—— 最明显的后果是传送带接不到已有传送带上。重叠建造不需要它(noCollision 就够了), 铺带子不顺手就先把它关掉。

顺带修掉一处过期计数:ReportCheats 的两条日志和英文特性文档里都写着「五项 / Five switches」, 实际是六项。

一桶原油切四段

汽油化工那三条补完之后剩的最后一个问题:所有这些配方都在吃同一种「精炼油」, 而现实里它们吃的根本不是同一种油。

常减压蒸馏   原油×10 → 石脑油×2 + 精炼油×3 + 蜡油×3 + 钒渣油×2

收率 2:3:3:2 锚在中质原油的真实切割比例。常压塔和减压塔合成一条配方—— 中文炼厂里「常减压」本来就是一套装置。它替换掉原先那条「钒渣油 · 减压蒸馏」, 配方 ID 没动。

四条已有配方因此改了原料,每条都有硬理由:

  • 苯 · 蒸汽裂解 ← 石脑油(乙烯装置的正式名字就叫「石脑油裂解装置」)
  • 苯 · 催化重整 ← 石脑油(炼厂里这一段就叫「重整石脑油」)
  • 氢 · 蒸汽重整 ← 石脑油(轻烃才好重整,重组分直接结焦)
  • 丙烯 · 催化裂化 ← 蜡油(FCC 存在的全部理由就是把重的变轻)

FCC 吃蜡油不吃渣油,这条界线正好就是催化反应器的机制:渣油里的钒和镍会在分子筛上 沉积、永久毒死催化剂。渣油催化裂化现实中确实存在,代价就是催化剂消耗翻几倍。

新增 · 石脑油 · 加氢裂化:蜡油×4 + 氢×3 → 石脑油×3 + 精炼油×2。炼厂的核心转化单元。 ⚠️ 不配平(馏分没有定式),但 4 进 5 出的方向是对的——加氢裂化液收率超过 100%, 多出来那份正是加进去的氢。它和催化重整是一对:重整产氢、加氢裂化耗氢, 而加氢裂化产的石脑油又回去喂重整,现实炼厂的氢平衡就是这两台在对冲。

两个新物品接进可燃液体发电:石脑油 900 ℃、蜡油 600 ℃。加上原有的精炼油 750 和 钒渣油 500,同一桶油的四个切段自己就把「脏的能量密、干净的能量稀」演完了—— 越往塔底能量越密(3.0 → 12.0 MJ)、也越烧不了高温(900 → 500 ℃),两条梯子严格反向。

一个说出来的抽象:本 mod 的油品全部按「一件 = 4 个 CH₂ 单元」记账,所以四个馏分在 化学计量上可以互换。这让已有那三条配平过的方程换了原料之后一个字都不用改。 真正区分四个馏分的不是含碳量,是它能喂哪个反应。

老存档注意:改了原料的四条配方,原料格数没变所以不坏档, 但已经在跑的产线会停下来等新料,需要重接。

存档:本 mod 自己的存档块推到版本 4(多了催化剂床那一段)。 老存档能直接读,但用本版存过之后不能退回旧版本

1.4.0

一条新资源线:熔岩星球上那片橙红的海,以前只是布景,现在可以抽。

老存档直接能用,什么都不用改。 这一版只新增物品、配方和一座建筑, 没有改动任何已有物品的 ID,也没有改配方、没有动存档格式。 (上一版 1.3.0 那条「宇宙矩阵要多接一条生物矩阵投料线」仍然适用, 如果你是从 1.2.x 直接跳过来的,先看一眼 1.3.0 那一节。)

岩浆:把抽水站架到熔岩海边

  • 用的就是原版抽水站,不需要新建筑也不需要新科技,和在水边摆一台一模一样
  • 岩浆是流体:进得了储液罐、上得了传送带、进得了物流站
  • 烧不了。岩浆是熔融的硅酸盐,已经是完全氧化态,和二氧化碳一样烧不出能量; 它携带的是一千度的显热,不是化学能
  • 顺带修了一个看着像 bug 的旧行为:开着「平地抽水」作弊时,抽水站本来就能 摆到熔岩上——然后干转。那不是作弊坏了,是出料那边还有另一道闸,现在两道都通了

熔岩冷却厂:第七座巨型建筑

  • 建造栏「巨型建筑」分页第 8 格,10000 倍速,自带行星内物流站
  • 七座里唯一把发光面露在外头的:中间一口敞开的熔池,四座冷却塔把热抽走, 池面上悬一圈粒化环。远处看就靠这一点认
  • 它用的是本 mod 自己的 13 号「熔岩处理」配方类型,三条配方只有它做得了

三条配方:冷到哪一步,就结晶出什么

  • 这不是三档强度,是层状基性侵入体里三个真实的堆晶层位: 铬铁矿约 1300 °C 最早结晶沉底,钒钛磁铁矿要再冷到约 1050 °C, 而钴亲硫、得先熔离出一滴硫化物液体才扫得出来
  • 钴那条要额外投石膏矿和煤矿,这是机理不是配重:岩浆自带的硫不够, 现实里诺里尔斯克那座全球最大的岩浆硫化物矿床,硫就来自同化围岩里的硬石膏
  • 本 mod 的钴 / 铬 / 钒矿脉本来就铺在熔岩与火山灰两种主题上,用的是同一套地质

产量故意压得很低,这是设计不是保守

  • 一个周期只出 1 份矿石,而且要吃掉 6000 / 6400 / 40000 份岩浆
  • 投料比不是拍的:玄武质熔体里 Cr≈300 ppm、V≈280 ppm、Co≈45 ppm, 所以每出一份矿要的岩浆之比就是 1 : 1.07 : 6.7
  • 它是补缺口的,不是替代找矿的。跑出去找稀有矿脉仍然是这条线的主路, 要是岩浆能便宜换出铬和钒,那套玩法就死了
  • 实际跑起来它会一直被岩浆卡着,面板上的产量远低于 10000 倍—— 那不是坏了,是投料不够。觉得机器没干活先看岩浆供应

顺带:莫桑石现在只有大型采矿机采得了

  • 拿普通采矿机往莫桑石矿脉上盖会当场被拒,而不是盖下去之后干转
  • 这不是新的平衡规则:钻头槽是物流站仓位,普通采矿机结构上就没有那一格, 所以它本来就挖不动——只是以前不告诉你,现在在建造时就说清楚

还没做的

  • 岩浆的热还没真正用起来:现在「一千度」只体现在结晶出什么上, 没发电也没给别的高温工序供热

1.3.0

三块新内容,围绕同一条线索:一种只在外星系出现的矿,和它一路通到终局的去处。

老存档可以用,但有一处要动手:宇宙矩阵的配方从六样原料变成了七样。 已经建好的宇宙矩阵产线需要多接一条生物矩阵的投料线,否则研究站会停在 「缺少原材料」。研究站内部的数据读档时自动补齐,不用拆了重建。 除此之外只新增物品、配方、矿脉类型,没有改动已有物品的 ID,存档格式也没动。

外星矿脉:莫桑石,挖它要烧钻头

  • 新矿脉只在出生星系之外生成,是本 mod 第一种需要「跑出去找」的资源
  • 开采时按产量消耗钻头:一个物品,但谓词在注册时展开成四条配方—— 凡是硬度够的材料都能锻,锻出来的都是同一种钻头
  • 钻头走锤锻精工厂(本 mod 自己的 12 号「锻造」类型)。那台建筑此前和 天工装配厂是同一个 Assemble 类型、参数也一样,等于两份同样的东西; 给它专属配方类型之后它才真正成立
  • 采矿机多出一个钻头槽位(本地需求,默认容量 3000)。采满或没电时不再空烧钻头
  • 新增稀有矿脉探矿:启动时报告每种稀有矿在哪些星系哪些行星,不用自己一颗颗飞过去看

碳化硅:莫桑石的去处,买的是「搬多快」

  • 莫桑石 → 高纯碳化硅 →(+氮化铝)→ 碳化硅功率模块 → 碳化硅能量枢纽
  • 吞吐是原版枢纽的 30 倍,但服务的还是锂电池蓄电器—— 碳化硅在现实里是变流器件,一焦耳也不存。所以它和锂电池不是上下级, 是两个不同的瓶颈:电不够存就加蓄电器,充放不进来才加它
  • 你的锂电池不作废,升级时换的是变流器不是电池

生物矩阵:第七种科研矩阵,是养出来的

  • 原版六种矩阵都在矩阵研究站里合成,第七种不是——它由生物温室培养 (菌丝基体 ×2 + 菌落 ×2 → 1,3 秒)。在「矩阵合成」里选不到它,这是设计
  • 它是宇宙矩阵的第七样原料。终局科技的要求一条没变,仍然只要宇宙矩阵, 只是宇宙矩阵里现在含一份生物矩阵——所以它是终局的硬性需求,且只算一遍
  • 这也让藻类那条生物链从「原料副线」变成了科技主线的一环
  • 矩阵研究站界面跟着变:环上 5 格时是正五边形(和原版一样), 造宇宙矩阵时变成正六边形,多出来那格就是生物矩阵

修复

  • 修掉一处扫描线程崩溃:矿种数超过原版 14 种后,PlanetAlgorithm 的两处数组 (512 个矿点的缓冲、按矿种 ID 索引的统计数组)都会越界
  • 修掉稀有矿脉「只在出生星系生成」的反转——四种自定义稀有矿此前全部只出现在 出生星系,与设计正好相反
  • 修掉三种物流站储物格错位:储物格控件是整个窗口共用的,给大型采矿机摆过行 位置后没有还原,而原版只重写第 0 行

1.2.0

两条新链。老存档可以直接用——本版只新增物品、配方、建筑和配置, 没有改动已有物品的 ID,存档格式也没动。

可燃性液体发电厂:烧什么,决定能榨出多少电

  • 一百台火力发电厂压进一座塔,216 MW,只烧可燃液体
  • 液体有两个属性:工作温度决定能量利用率,热值决定一份烧多久。 输出功率恒定不随燃料变,差别只在传送带上看得见
  • 六种液体里五种上游已经有了,只新增钒渣油(原油减压蒸馏的塔底重组分)
  • 温度不是火焰温度,是这种燃料允许的热通道工作温度,受灰分、碱金属、硫、钒 造成的积灰与热腐蚀限制——这正是温度和热值两个属性互不相干的原因。 整张表是一条洁净度阶梯:碱金属灰 → 钒的热腐蚀 → 硫与芳烃 → 积碳 → 无碳黑 → 无碳
  • 效率按卡诺 × 真实电厂的二级效率算,顶格 61%,低于原版火力发电厂的 80%。 这座建筑卖的是一百倍功率密度和「烧产线本来就在排的东西」,不是效率
  • 完整推导与二十篇文献出处在仓库根目录的 可燃液体发电V1.md

活性增产剂:原版之上的两档,而且分两种性格

  • 一条配方两个选料位,四种活性复合材两两组合(10 对)决定产出哪一种
  • 四个新物品:Mk.IV / Mk.V,各分广延型(等级低、一份喷得多)与 浓缩型(等级高、一份喷得少)。同档里等级高的必然喷得少, 这是两个变体分同一笔总点数预算的必然结果
  • 顶格 Mk.V 浓缩是等级 6:增产 +30% / 加速 +150%。原版顶格是 4 级
  • 配方不公开,而且直觉是错的:把最高级的活性复合材 IV 双份堆进去, 出的是四种里倒数第二
  • 这条链让活性复合材的四维属性第一次真正被读——在这之前它们只显示在提示栏里, 不参与任何计算

两件从引擎里读出来、值得单独说的事

  • 增产剂的等级表其实填到 10 级,原版只用到 4,喷涂机对等级没有任何钳制。 挡住我们的是传送带:货物增产点数 = 垛数 × 等级,字段是 Int16, 集装 5000 层就只剩 6 级。集装层数和增产剂等级是同一笔预算里的两项开销
  • 原版火力发电厂不是 1:1,是 0.8(2.16 MW 发电对 2.7 MW 耗料)。 原版唯一有转化损失的就是烧化学燃料那一座,聚变和反物质都是 1.0

其他

  • 新配置文件两个:combustibles.jsonproliferator.json
  • 新增五张自绘图标(钒渣油 + 四种增产剂),图标总数 55
  • 两份特性指南各加两节,并各加一个目录(锚点按 GitHub 的规则生成,不是猜的)
  • 修掉四维属性行(硬度/韧性/耐蚀/导电)在英文客户端下仍显示中文的问题—— 译文一直都在,只是没人去查

1.1.3

内容更新:给生物温室加了一整条下游,也顺带把九种合金里的六种接上了去处—— 在这之前它们造得出来但没有任何东西要。老存档可以直接用,本版只新增物品、 配方和配置,没有改动已有物品的 ID,存档格式也没动。

活性复合材:一条配方吃任意一种合金

  • 新物品五个:菌丝基体 + 活性复合材 I~IV
  • 菌落 + 氢(+ 光照)→ 菌丝基体,再 菌丝基体 + 你选的合金 → 活性复合材
  • 6 种填料 × 4 个等级 = 24 种组合,合成面板上只占一格。技术和合金弹药同一套: requires[i] 是 int,逐台建筑改它就能让同一条配方吃不同原料
  • 组装机窗口下面多出一块面板:第一行点左右半边换填料合金,第二行拖动条调配比。 等级由配比决定,不单独选
  • 逐台建筑的选择进存档,新建和蓝图粘贴出来的机器继承最后一次选的组合

四级不是升级阶梯,是四种不同的材料

  • 六对两两互不支配:低级赢韧性与耐蚀,高级赢硬度与导电
  • 定这组数时确立了三条硬约束:硬度 ≤ 填料、导电 ≤ 填料、韧性 > 填料 (最后一条不满足的话,不如直接用那块合金)

烧结析出:放哪一级进去,就出哪一样原版后期材料

  • 冶炼厂(巨型建筑里的冶铸熔炉也行): I 散生 → 框架材料 / II 渗流 → 粒子宽带 / III 贯通 → 钛晶石 / IV 刚化 → 金刚石
  • 四级因此会被同时需要——这正是它们互不支配的用处
  • IV → 金刚石有真实依据:HPHT 人造金刚石用的就是铁镍钴金属触媒 + 碳源, 而 IV 恰好是钴铬 / 硬质合金颗粒 + 大量有机碳基体。另外三条是味道不是机制
  • 数值是第一版,启动日志里的「平衡对照 · …」几行给出每个目标物品现有路线的 原料与耗时,照着调 composite.json

图标

  • 五张新图标,仍由 tools/make_icons.py 自绘
  • 四级复合材做成抛光截面圆片(金相试样那种),和九种合金的等距锭块刻意分开, 物品栏并排能一眼分出「锭」和「料」。圆片里画的就是网络本身:孤岛 → 一条贯穿通路 → 整张网 → 三角化填实

其他

  • 新配置文件 composite.json
  • README 里两份特性文档的链接提到了页首,.md 链接全部改成仓库 URL
  • 打包时新增链接校验:README 里凡是相对链接,目标必须真的在包里,否则拒绝打包
  • 修掉几处 1.1.2 之后没跟上的说法:README 仍写「五座巨型建筑」、 两份指南里风力发电机集群仍写「第 6 格」(1.1.2 已挪到第 7 格)

本版新增的界面(四种模式共用一块面板)尚未在游戏里实测过, 如果面板出现异常,配方本身仍然可用——默认组合在注册时就写好了。

1.1.2

内容更新:第六座巨型建筑,以及围绕它的一条生物链。老存档可以直接用—— 本版只新增物品、配方和建筑,没有改动已有物品的 ID。

新增第六座巨型建筑:生物温室

  • 配方类型是本 mod 自己的 11 号「生化培养」,三条配方只有它做得了 (前五座借的都是原版类型,原版机器也做得了那些配方)
  • 三条配方:木材 · 光合育林(零原料)→ 菌落 · 藻菌共培养(四原料)→ 藻油 · 溶剂萃取
  • 新物品:菌落(生物量滤饼,非流体、无热值)、藻油(流体、化学燃料 4.2 MJ)
  • 氧气接自已有的电解水,二氧化碳接回已有的「甲醇 · 二氧化碳加氢」——碳绕一圈没有丢

生物温室整座建筑受日照约束(本 mod 第一座产量随环境变化的建筑)

  • 满日照 = 满速 10000 倍,零日照 = 完全停工,三条配方一起停
  • 公式原样照抄太阳能板的 PowerGeneratorComponent.EnergyCap_PV, 输入取自 PlanetData.runtimeLocalSunDirectionluminosity; 有黄昏而不是一刀切,潮汐锁定星球的暗面永久停工
  • 缩放的是每 tick 的配方周期数,不是 speed——巨型建筑靠 speed >= 阈值 识别, 降速到阈值以下会让这台建筑再也不被接管。 副作用:面板上的「制造速度」始终显示 10000 倍,随光照变的是产量
  • 开关是 megabuildings.jsonlightDependent,按建筑生效

藻油接上了石化下游

  • 新配方「精炼油 · 酯交换」(原版化工厂):藻油 ×2 + 甲醇 ×6 → 精炼油 ×6
  • 走原版化工厂而不是氧化还原化工厂,因为酯交换不是氧化还原—— 碳的氧化态没变,和 MTO 脱水留在化工厂是同一个判据
  • 甘油副产物游戏里没有对应物品,略去不产,不拿别的东西顶替
  • 顺带给甲醇添了一个大客户,C1 链原先的下游只有甲醛和乙烯

其他

  • ores.json 的配方现在允许原料侧为空(零原料配方),并会为此打一行 INFO—— 零原料和「漏写了原料」在日志里长得一样,不报就分不出来
  • 自定义配方类型(9~14)的「制造于」和物品「类型」两行现在也认巨型建筑, 以前只认 machines.json 里的机器
  • 新图标四张(生物温室、菌落、藻油、光合育林),仍由 tools/make_icons.py 自绘
  • 巨型建筑分页里风力发电机集群从第 6 格挪到第 7 格,把第 6 格让给生物温室。 槽位不进存档,不影响已建成的建筑

1.1.1

首个公开发布的版本。1.1.0 只做过内部测试,本版在它基础上修正了署名与打包格式。

署名与版权

  • 修正致谢里创世之书的仓库链接(应为 Awbugl/ProjectGenesis)
  • 两份特性指南里「巨型建筑美术取自创世之书」的说法已与事实不符,改为自绘说明
  • 新增 NOTICE:按 GPL 要求保留上游版权声明,并逐文件列明哪些属于 ProjectGenesis 的衍生作品、哪些只是借鉴思路;八个移植文件各加版权头
  • NOTICE 随发布包一起分发

打包

  • 改为 Thunderstore 格式:plugins/patchers/ 平级放在包根,不加 BepInEx/ 前缀 (对照 CommonAPI 与创世之书的正式包核对)
  • manifest.json 补上 website_url
  • 打包前自动校验 manifest:名称字符集、版本号格式、描述长度、依赖串格式、 图标必须 256×256,不合规直接拒绝打包

开发

  • 构建与校验脚本不再写死本机路径,fork 后可直接使用

1.1.0

传送带集装上限从 63 抬到 8191(新增 preloader)

  • 新增一个 BepInEx patcher,把 Cargo.incCargo.stack 加宽为 Int16, 「高层集装 + 满级增产剂」现在可以同时成立(以前超过 63 层增产点数会溢出)
  • 分拣器的 stackInput/Output 同步加宽,上限改为从传送带层数推导而不再写死
  • 集装层数配置上调到 5000
  • ⚠️ 货物块存档格式从版本 2 升到 3。装过之后存的档,卸载后打不开(老档仍可读)

新增内容

  • 四种新矿脉:锰、铬、钒、钨(合计八种,矿脉类型 15–22)
  • 钨链:白钨矿 → 三氧化钨 → 钨块 → 碳化钨
  • 氮链:哈伯法制氨 → 奥斯特瓦尔德法制硝酸
  • 九种合金 + 每种金属的四维属性(硬度/韧性/耐蚀/导电)
  • 硬质合金:WC:Co 配比逐台建筑用滑动条调,落到产量与制造时间上
  • 五阶合金弹药:伤害与产量由喂进去的那两种合金决定,共用一条配方
  • 风力发电机集群(1000 倍风力涡轮机,仅手制)

巨型建筑的外形

  • 以前五座克隆的是同一个原版模型(物流运输站)、只靠颜色区分, 现在每座的几何都由代码生成,轮廓各不相同: 台座塔柱、散热高塔、罐群管线、龙门架、圆环靶室
  • 贴图也是生成的:钢板分块、板缝、铆钉、格栅走道、警示条、混凝土基座, 以及夜里会亮的窗带;另有护栏、爬梯、泵阀、导轨滑块等小件
  • 只换被画出来的网格与贴图;占地、碰撞体、传送带接口仍沿用原有的
  • 不满意可关:megabuildings.jsonproceduralModelsfalse; 大小调 modelScale(默认 0.6)

巨型建筑改名

  • 五座全部重命名,新名取自中国古代工艺术语(多数是《天工开物》篇名), 每个名字直指工序:天工装配厂、冶铸熔炉、燔石化工厂、锤锻精工厂、观微对撞机
  • 原名里有一个用了真实公司商标,仓库公开后是实打实的风险,一并换掉
  • 五座的描述从共用一句改成各写各的

开源

  • 仓库代码采用 GPL-3.0;自绘美术仅授权用于《戴森球计划》的 mod
  • 最后 6 张来源不明的图标(五座巨型建筑 + 建造栏页签)已全部换成自绘, 图标轮廓与各自的程序化 3D 模型一一对应;至此仓库不含任何第三方美术资源
  • README 重写

采矿机

  • 煤矿不再在源头炼成高能石墨,采什么就出什么—— 煤矿既是燃料又是石墨/能量矩阵/化工链的原料,在源头烧掉会让下游拿不到原料。 现在和铁矿、石矿归为一类:只有唯一明确下游的矿才做映射
  • 修正产物替换的一处缺陷:原版那道「产物对不上就不开采」的判断没被一并接管, 替换之后采矿机会停摆(影响铜矿 / 硅石 / 钛石三条映射)

界面

  • 物品选取窗口(物流站槽位、分拣器过滤、储物箱过滤)新增搜索框: 开窗即可打字,按物品名或 ID 过滤,回车取第一个命中项;右侧 ◀ ▶ 横向翻页
  • 制造台窗口与合成面板的制造树现在支持两个以上的产物(本 mod 有 9 条三产物 + 1 条四产物配方); 顺带修了原版一个缺陷:点产物图标取货时,背包装不下的那部分会被直接清空
  • 传送带三档速度提到原版 4 倍

作弊开关(默认全关)

  • 新增「建造秒完成」,共六个开关

1.0.0

首个可分发版本。

产线

  • 五座 10000 倍速巨型建筑,自带行星内物流站与传送带直连,另有虚拟物流省渲染
  • 大型采矿机 / 抽水站 / 原油萃取站:速度拉满、矿脉不消耗、矿石直接产锭、可叠放、可采原油涌泉
  • 物流站 30 格 × 1000 万,星际站充能 30 GW;运载与集装层数拉满
  • 矩阵研究站生产侧 10000 倍速,与物流站双向直通
  • 卫星配电站全球覆盖

新增内容

  • 四种矿脉:钴、铝、石膏、锂(矿脉素材复用铁矿脉运行时改色)
  • C1 化工链:水煤气 → 甲醇 → 甲醛 / 乙烯,以及费托合成产精炼油
  • 提锂链:硫酸焙烧 → 电渗析 → 熔盐电解,硫酸在链内循环
  • 五台新建筑:电化学厂、氧化还原化工厂、综合物流枢纽、锂电池蓄电器、锂电池能量枢纽
  • 气态巨星新增可采的氮气
  • 13 张自绘矢量图标(分子结构式 / 晶体结构 / 金属锭)

界面

  • 合成面板、配方选取窗口、建造栏子项加横向翻页与滚动条
  • 建造栏双击建筑可正确跳转到合成面板对应配方

兼容

  • 创世之书:检测到即停用巨型建筑功能
  • 银河尺度:新矿脉走兼容层;并修复了 GS2 一个会导致存档读取失败的问题
Thunderstore development is made possible with ads. Please consider making an exception to your adblock.