Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
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.
| Date uploaded | a day ago |
| Version | 1.1.0 |
| Download link | s6652289-StackmasterFix-1.1.0.zip |
| Downloads | 22 |
| Dependency string | s6652289-StackmasterFix-1.1.0 |
This mod requires the following mods to function
denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2350JStack424-Stackmaster
Turn a messy Viking inventory into a tidy, adventure-ready loadout.
Preferred version: 1.1.5README
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 自行处理,本补丁不再提供任何兜底。
CHANGELOG
Changelog
1.1.0
剔除冗余功能:上游 Stackmaster 1.1.5 已自行修好「合成/升级不扣料」,本补丁不再介入那一半。
- 依据(只读上游源码 + 官方包 metadata 核对):
- 1.1.5 changelog 首条声明该修复,并写明已在游戏内验证
(
prepared withdrawals are recaptured from each selected owned Container.GetInventory() rather than from detached ZDO inventory snapshots …)。 NearbyResources.cs:1288取Container.GetInventory()活库存,:1294-1313用它重建mutableHandle与runtimeStacks⇒ 执行期对象就是活库存;:1673-1681另有ReferenceEquals(runtime.Inventory, runtime.Container.Container.GetInventory())的 fail-closed 身份校验(注释直指 "grants a free crafted item"),并配ExactResourceDebit精确账本与逆序回滚。- ⇒ 本补丁针对
ExecuteWithRollback的 transpiler 已退化为恒等替换,属纯冗余。
- 1.1.5 changelog 首条声明该修复,并写明已在游戏内验证
(
- 移除:
ExecuteWithRollback的 transpiler + 写回 postfix 分支、FixCraftingFromStorage配置项、以及NearbyResourceService的类型查找。 DLL 内已确认FixCraftingFromStorage与ExecuteWithRollback零引用。 - 保留:
ExpeditionKitAction.ExecuteAtomic(远征包)。1.1.5 没有修这条路径 —— 它仍走CaptureForExpedition → Discover(…, resourceReadOnly: true)的 detached ZDO 快照, 而 crafting 专用的TryCaptureOwnedPlan/mutableCapture从未被它调用。故这一半原样保留。 - 依赖:
JStack424-Stackmaster1.1.4 → 1.1.5(不升会让 r2modman 要求把已升的上游降回去)。 - 日志改进:transpiler 不再拿「指令总数」当方法指纹(上游一改 IL 就会退化成
未知方法(指令数 N),纯误导),改用 Harmony 注入的__originalMethod直接取方法名。 - 阈值变化:
FixExpeditionKit=false时补丁不再尝试挂载,直接打日志说明并退出(等同于未安装)。
DLL 重新编译:16,896 字节,sha256 da4e8133d5e5b8ef991cc6f366f8e64ed31022d1d02bd27b0f497c8af1f5342e。
1.0.0
- 首个版本。修复上游
JStack424-Stackmaster1.1.4 的「用箱子材料合成/升级不扣料(复制)」问题。 - 根因:Stackmaster 为了「不拿所有权也能读箱子」,把容器的序列化库存从 ZDO 解进一个
临时 Inventory(
new Inventory(true),ContainerHandle.ResourceInventory), 但写入路径复用了同一个 capture ⇒ExecuteWithRollback实际是扣在只读副本上。 临时 Inventory 永不触发Inventory.Changed()⇒ 不会调用Container.Save()⇒ ZDO 里材料一点没少,而模组的自检在同一个副本上量差值必然判定成功 ⇒ 玩家白拿成品。 - 修法:transpiler 把写入方法里的
RuntimeResourceStack.get_Inventory/get_Item整体重定向到真实容器的活库存与活物品对象;事务成功后强制Container.Save()+ZDOMan.ForceSendZDO。拿不到安全写入权时 fail-closed(取消操作,不放行复制)。 - 覆盖:
NearbyResourceService.ExecuteWithRollback(合成/升级/建造)与ExpeditionKitAction.ExecuteAtomic(建造菜单 Alt+左键拉取远征包)。 - 实测(2026-09-19):修复后逐笔扣减量与配方需求精确吻合
(建造 20/10/2 = 32、合成 −3、另一组 4/1 = 5),且合成事务内首次出现
Container.OnContainerChanged与Container.Save(修复前为 0 条)。 - 配置项:
FixCraftingFromStorage、FixExpeditionKit、Verbose(均默认开)。