StackmasterFix
[AI-generated] Client-only patch for Stackmaster: its expedition-kit shortcut (Alt+click a build piece) took materials from a detached snapshot, so chests were never debited (item duplication). Redirects the withdrawal to the live container.StackmasterFix
适用性声明 / Applicability Notice
中文:本模组是给第三方模组 JStack424-Stackmaster 打的客户端补丁,修的是它在
「建造菜单 Alt + 左键 拉取远征包(expedition kit)」时**箱子里的材料不扣减(等于复制)**的问题。
- 这个 bug 是在我这台装了 30+ 模组、且以客机身份连远程专用服务器的环境里复现并定位的。 原模组作者与这些问题无关,有疑问请先找我,不要去找原作者。
- 原模组在别的环境下可能并没有这个 bug,症状也可能是我这台的特定组合导致; 不保证适用于你的环境。
- 本补丁只做两件事:把扣减从「只读快照副本」改到「真实容器」,并在事务成功后强制写回 ZDO。 不改任何数值、不加任何功能。
- 本补丁未经原作者同意或审阅,也不主张任何「原模组有 bug」的结论 —— 只是我这台机器上的绕行方案。
- 若上游之后自己修好了,请优先升级上游模组并停用本补丁。
English: This is a client-side patch for the third-party mod JStack424-Stackmaster,
fixing its bug where the expedition-kit shortcut (Alt + left-click a build piece) does not
consume the materials it takes from nearby chests (i.e. item duplication).
- The bug was reproduced and diagnosed on my setup: a 30+ mod client connecting to a remote dedicated server. The original mod's author is not responsible for any of this.
- The original mod may be perfectly fine in your setup; this patch is not guaranteed to apply to you.
- The patch only (1) redirects the withdrawal from a read-only snapshot copy to the live container and (2) forces the container's ZDO write after a successful transaction. It changes no values and adds no features.
- This patch is not endorsed or reviewed by the original author, and makes no claim that the original mod is buggy — it is merely a "works-on-my-machine" workaround.
- If upstream fixes it, update Stackmaster and disable this patch.
🤖 本模组的代码由 AI 生成(AI Generated),需求、验证与发布由人类完成。
⚠️ v1.1.0:合成/升级那半已剔除
Stackmaster 1.1.5 自己修好了「用箱子材料合成/升级不扣料」。 依据:
- 1.1.5 changelog 首条即声明该修复(
… recaptured from each selected owned Container.GetInventory() rather than from detached ZDO inventory snapshots …),并写明已在游戏内验证。 - 源码层面,
NearbyResourceService在执行期改用活库存重建 runtime stack (NearbyResources.cs:1288取Container.GetInventory(),:1294-1313用它重建 handle), 并在扣减前用ReferenceEquals(runtime.Inventory, runtime.Container.Container.GetInventory())做 fail-closed 身份校验(:1673-1681),另配精确扣减账本与逆序回滚。
⇒ 本补丁原先针对 ExecuteWithRollback 的 transpiler 已退化为恒等替换,
故 1.1.0 连同 FixCraftingFromStorage 配置项一起删除。
⚠️ 但 1.1.5 的修复只覆盖 crafting:远征包那条路径没动(见下),所以本补丁保留那一半。
依赖因此升到
JStack424-Stackmaster-1.1.5。1.1.4 及更早请勿与本版混用。
它修的是什么(远征包路径)
Stackmaster 为了「不拿所有权也能读箱子」,把容器的序列化库存从 ZDO 解进一个临时 Inventory
(new Inventory(true)),命名为 ContainerHandle.ResourceInventory,作者注释自述
never claims ownership or mutates the live Container inventory。
crafting 路径的执行期已经换成活库存了,但远征包没有:
ExpeditionKitAction.Finish()
→ TryPrepare → CaptureForExpedition(player, true, true)
→ NearbyResources.Capture → ContainerDiscovery.Discover(…, resourceReadOnly: true)
→ ContainerDiscovery.Inspect:refreshed = !resourceReadOnly && … ⇒ false
inventory = refreshed ? container.GetInventory() : null ⇒ null
→ 于是只解 ZDO 字节到 new Inventory(true),塞进 handle.ResourceInventory
→ NearbyResources.cs:1104 AddInventory(…, handle.ResourceInventory, …) ← runtimeStack 指向副本
→ ExpeditionKitAction.cs:656 MoveItemToThis(runtime.Inventory, …) ← 从副本里搬走
(crafting 专用的 TryCaptureOwnedPlan / mutableCapture 从未被远征包调用。)
而 new Inventory(true) 的 m_temoraryInventory == true,游戏里 Inventory.Changed() 第一句就是
ldfld m_temoraryInventory; brfalse; ret ⇒ 永不发 m_onChanged ⇒ 不触发
Container.OnContainerChanged ⇒ 从不调用 Container.Save() ⇒ ZDO 里材料一点没少。
同时模组用 before - after 在同一个副本上量差值,必然判定成功 ⇒ 玩家白拿一套材料。
它是怎么修的
transpiler 只挂一个目标方法:
| 目标方法 | 替换内容 |
|---|---|
Stackmaster.ExpeditionKitAction::ExecuteAtomic |
RuntimeResourceStack.get_Inventory、get_Item |
get_Inventory→ 真实容器的活库存(Container.GetInventory())get_Item→ 活库存里同一槽位的那个物品对象 (必须成对替换:MoveItemToThis内部的扣减带m_inventory.Contains(item)的引用包含检查, 只换 Inventory 不换 Item 会直接扣不动)- 事务成功后:对本次真的重定向过、且确实是本机 owner 的容器执行
Container.Save()+ZDOMan.ForceSendZDO,确保写回权威数据
安全取向:拿不到安全写入权(IsOwner() == false、容器已卸载、反射句柄缺失)时,返回一个空库存,
让模组的 ExecuteAtomic 自己判定失败并取消本次操作。
宁可取消,也不放行复制。
配置
BepInEx/config/s6652289.StackmasterFix.cfg
| 项 | 默认 | 说明 |
|---|---|---|
FixExpeditionKit |
true |
修「建造菜单 Alt + 左键拉取远征包」的材料复制 |
Verbose |
true |
打印每次重定向与强制写回的细节 |
旧版遗留在 cfg 里的
FixCraftingFromStorage键已无用,可手动删除(不删也无副作用)。
怎么看它生效了
日志(BepInEx/LogOutput.log)里搜 [Fix]:
[Fix] 已打补丁:ExpeditionKitAction.ExecuteAtomic(transpiler + 写回 postfix)
[Fix] transpiler 生效:ExpeditionKitAction.ExecuteAtomic(N 条指令)→ get_Inventory 替换 4 处,get_Item 替换 5 处
[Fix] 扣减重定向 → 真实容器 $piece_chest_blackmetal(Clone) uid=… 活库存总数=369
[Fix] 已强制写回 ZDO: $piece_chest_blackmetal(Clone) uid=… 库存=368 zdoRev=1067
关键判据:拉一次远征包后,箱子真的少了对应材料 = 修好了。
若看到 [Fix] ★transpiler 未匹配到任何目标指令 ⇒ Stackmaster 版本变了,本补丁失效,请反馈。
安装 / 卸载
- 安装:解压把
StackmasterFix.dll放到BepInEx/plugins/StackmasterFix/ - 卸载:删掉该目录
- 依赖:
denikson-BepInExPack_Valheim+JStack424-Stackmaster(不需要 Jotunn、不联网、纯客户端)
源码
D:\WorkBuddy\StackmasterFix\(本地工作区)
已知边界
- 本文只验证到「材料确实从真实容器扣掉、并写回 ZDO」。联机双端一致性、专用服务器长时间运行、 容器被他人同时打开等边界场景未做穷尽测试。
- 上游若改变
RuntimeResourceStack/ExecuteAtomic的结构,本补丁会打印transpiler 未匹配并自然失效(不会造成新的破坏)。 - 合成/升级不再属于本补丁的职责范围:那由上游 1.1.5 自行处理,本补丁不再提供任何兜底。


