You are viewing a potentially older version of this package. View all versions.
s6652289-StackmasterFix-1.1.1 icon

StackmasterFix

[AI-generated] Client-only patch for Stackmaster: its Quick Grab Materials 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 2 weeks ago
Version 1.1.1
Download link s6652289-StackmasterFix-1.1.1.zip
Downloads 32
Dependency string s6652289-StackmasterFix-1.1.1

This mod requires the following mods to function

denikson-BepInExPack_Valheim-5.4.2350 icon
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.2350
JStack424-Stackmaster-1.2.0 icon
JStack424-Stackmaster

Turn a messy Viking inventory into a tidy, adventure-ready loadout.

Preferred version: 1.2.0

README

StackmasterFix

适用性声明 / Applicability Notice

中文:本模组是给第三方模组 JStack424-Stackmaster 打的客户端补丁,修的是它在 「建造菜单 Alt + 左键 拉取一整套材料」(上游叫 Quick Grab Materials)时**箱子里的材料不扣减(等于复制)**的问题。

  • 这个 bug 是在我这台装了 30+ 模组、且以客机身份连远程专用服务器的环境里复现并定位的。 原模组作者与这些问题无关,有疑问请先找我,不要去找原作者。
  • 原模组在别的环境下可能并没有这个 bug,症状也可能是我这台的特定组合导致; 不保证适用于你的环境。
  • 本补丁只做两件事:把扣减从「只读快照副本」改到「真实容器」,并在事务成功后强制写回 ZDO。 不改任何数值、不加任何功能。
  • 本补丁未经原作者同意或审阅,也不主张任何「原模组有 bug」的结论 —— 只是我这台机器上的绕行方案。
  • 若上游之后自己修好了,请优先升级上游模组并停用本补丁。

English: This is a client-side patch for the third-party mod JStack424-Stackmaster, fixing its bug where the Quick Grab Materials 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:Quick Grab Materials 那条路径没动(见下),所以本补丁保留那一半。 2026-09-25 对上游 1.2.0 复核:这条路径依旧没修 —— ExecuteAtomic 里仍是 playerInventory.MoveItemToThis(runtime.Inventory, …),而那个 runtime.Inventory 仍来自 CaptureForQuickGrab → Capture → Discover(…, resourceReadOnly: true) 的 detached ZDO 快照 ⇒ 本补丁依然必需,没有可剔除的功能。

依赖因此升到 JStack424-Stackmaster-1.2.0。 ⚠️ 上游 1.2.0 把内部类 ExpeditionKitAction 改名为 QuickGrabMaterialsAction (1.2.0 changelog 把用户可见的"存放/补货"动作改叫 Quick Stack,而 "Quick Grab Materials" 这个名字保留不变 —— 改的是内部类名)。本补丁 1.1.1 已跟随:改成先试新名、再回退旧名, 两代上游都能装上。1.1.0 在 1.2.0 上会整体静默失效(找不到类型、180 秒后报错),请勿使用。


它修的是什么(Quick Grab Materials 路径)

Stackmaster 为了「不拿所有权也能读箱子」,把容器的序列化库存从 ZDO 解进一个临时 Inventory (new Inventory(true)),命名为 ContainerHandle.ResourceInventory,作者注释自述 never claims ownership or mutates the live Container inventory。

crafting 路径的执行期已经换成活库存了,但 Quick Grab Materials 没有:

QuickGrabMaterialsAction.Finish()
  → TryPrepare → CaptureForQuickGrab(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:1157  AddInventory(…, handle.ResourceInventory, …)   ← runtimeStack 指向副本
  → QuickGrabMaterialsAction.cs:656  MoveItemToThis(runtime.Inventory, …)   ← 从副本里搬走

(crafting 专用的 TryCaptureOwnedPlan / mutableCapture 从未被它调用。)

行号按 1.2.0 的源码给出;1.1.5 的对应位置在 ExpeditionKitAction.cs 里,内容一字不差 —— 也就是说上游这次只是改了文件与类的名字,没改行为。

而 new Inventory(true) 的 m_temoraryInventory == true,游戏里 Inventory.Changed() 第一句就是 ldfld m_temoraryInventory; brfalse; ret ⇒ 永不发 m_onChanged ⇒ 不触发 Container.OnContainerChanged ⇒ 从不调用 Container.Save() ⇒ ZDO 里材料一点没少。 同时模组用 before - after 在同一个副本上量差值,必然判定成功 ⇒ 玩家白拿一套材料。

它是怎么修的

transpiler 只挂一个目标方法:

目标方法 替换内容
Stackmaster.QuickGrabMaterialsAction::ExecuteAtomic
(上游 1.1.5 及更早叫 ExpeditionKitAction)
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 + 左键拉取一整套材料(上游叫 Quick Grab Materials)」的材料复制。
键名沿用旧版的 FixExpeditionKit(上游 1.2.0 之前管它叫 expedition kit),改名会让已有配置失效,故不改
Verbose true 打印每次重定向与强制写回的细节

旧版遗留在 cfg 里的 FixCraftingFromStorage 键已无用,可手动删除(不删也无副作用)。

怎么看它生效了

日志(BepInEx/LogOutput.log)里搜 [Fix]:

[Fix] 已打补丁:QuickGrabMaterialsAction.ExecuteAtomic(transpiler + 写回 postfix)
[Fix] transpiler 生效:QuickGrabMaterialsAction.ExecuteAtomic(N 条指令)→ get_Inventory 替换 4 处,get_Item 替换 4 处
[Fix] 扣减重定向 → 真实容器 $piece_chest_blackmetal(Clone) uid=… 活库存总数=369
[Fix] 已强制写回 ZDO: $piece_chest_blackmetal(Clone) uid=… 库存=368 zdoRev=1067

关键判据:拉一次材料后,箱子真的少了对应材料 = 修好了。 (在 1.1.5 及更早的上游上,日志里的名字会是 ExpeditionKitAction.ExecuteAtomic —— 同一个方法,只是上游改过类名。) 若看到 [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.2.0

新增 B 项:拦截上游 1.2.0 的「记忆上次用过的箱子」回退 —— 快速堆叠不再把物品塞回你刚清空的箱子。

  • 症状:把箱子取空 → 走到别的箱子按 Alt+E 快速堆叠 → 刚取出来的东西又被塞回那个空箱子。
  • 根因:上游 1.2.0(及 1.1.9 测试版)changelog 原话 —— 「Quick Stack remembers the last directly observed chest for each exact item type, even after that chest reaches zero stock」,即箱子被清空也不会抹掉这条记忆。 随后 StorageTransferPlanner.PlanDeposits 在「剩余量 > 0 且本次一件都没搬走」时, 用这张记忆表把物品回退进那个箱子。
  • 判据 A(正常路由不会填空箱):<>c__DisplayClass2_0::<PlanDeposits>b__1 的整个筛选逻辑就是 container.InitialCompatibilityKeys.Contains(source.CompatibilityKey) ⇒ 空箱子的候选键集合为空 ⇒ 永远进不了正常候选列表。 ⇒ 所以「往清空的箱子塞东西」只有"记忆箱子回退"这一条通道。
  • 判据 B(回退只在箱子已无同类物品时才会发生):见 PlanDeposits IL 0x00FF-0x014A 的四重条件。⇒「拦截回退」与「不再填空箱」是同一件事,不存在误伤正常堆叠的中间态。
  • 修法:Postfix 打在 IReadOnlyDictionary<string,string> Stackmaster.RememberedChestDestinations.ResolveFallbacks(Player, InventorySnapshot, DiscoveryResult) 上,把返回的 StackId → ContainerId 回退表换成空表 ⇒ TryGetValue 永远 miss。
  • 影响面封闭(三条实证):ResolveFallbacks 全 DLL 只有 2 个调用点 (StorageAction.Begin / <FinishAfterOwnership>d__17.MoveNext,都是 Quick Stack 存料路径); 记忆数据的唯一读取点 RememberedDestinationState::TryGet 全 DLL 只被 ResolveFallbacks 调 1 次; 不涉及合成扣料与快速取料(A 项那两套类是另外的)。
  • 刻意不做:不动上游的记忆存档(ObserveDirect / PlayerPrefs 原样保留) ⇒ 本修复可逆,DisableRememberedChests = false 即恢复上游行为,不用重建记忆。
  • 新配置:DisableRememberedChests(默认 true)。
  • ⚠️ 已知副作用:上游 1.2.0 会对「合格但最终留在背包里」的物品临时打 !数量 红框 (悬浮一次即清除)。本修复让这部分物品真的留在背包里 ⇒ 红框出现机会略增。 该机制是上游既有的,不是本补丁引入的。
  • 结构改动:A / B 两个修复项改为独立安装 —— 任何一项配置关掉、或任何一项挂载失败, 都不再连累另一项(1.1.1 及更早是"任一早退就整体不挂载")。
  • ⚠️ 适用性声明已更新:B 项不是上游 bug,上游是有意这么设计的; 本补丁只是按个人偏好关掉它(详见 README 顶部)。

上游 1.2.0 没有修 A 项(继续保留)

QuickGrabMaterialsAction 的扣减源仍是 detached ZDO 快照 ⇒ A 项补丁依然必需(1.1.1 已核对,本轮复核未变)。

1.1.1

修 bug:上游 1.2.0 改了内部类名,本补丁不改就会整体静默失效。

  • 根因:Stackmaster 1.2.0 把 ExpeditionKitAction 改名为 QuickGrabMaterialsAction (1.2.0 changelog:把用户可见的"存放/补货"动作改叫 Quick Stack,而 Quick Grab Materials 这个名字保留不变 —— 改的只是内部类名)。 本补丁原先写死 FindType("Stackmaster.ExpeditionKitAction") ⇒ 在 1.2.0 上永远拿到 null,TryInstall 每秒空转、180 秒后报 "180 秒内没等到 Stackmaster 类型,补丁未安装" ⇒ 整条补丁一点作用都没有。
  • 修正:改为先试新名、再回退旧名: FindType("Stackmaster.QuickGrabMaterialsAction") ?? FindType("Stackmaster.ExpeditionKitAction") ⇒ 1.2.0 与 1.1.5 及更早两代上游都能装上。 安装日志也会打印实际用到的类型名,方便一眼看出上游又换没换名字。
  • 依赖:JStack424-Stackmaster 1.1.5 → 1.2.0。
  • 配置键保持不变:仍叫 FixExpeditionKit(上游 1.2.0 之前管这个功能叫 expedition kit)。 改名会让老用户已写好的 cfg 失效 —— BepInEx 不会用新默认值覆盖已存在的键, 结果是"开关突然变回默认",得不偿失;故键名不动,只在说明里点明它对应上游的 Quick Grab Materials。

复核结论:没有可剔除的功能

同时按最新源码逐项复核了"上游是否已经自己修好这条路径",结论是没有:

  • QuickGrabMaterialsAction.cs:626-634(1.2.0)里仍然是 var sourceItem = runtime.Inventory.GetItemAt(runtime.Item.m_gridPos.x, runtime.Item.m_gridPos.y); 加 ReferenceEquals(sourceItem, runtime.Item) 校验;
  • :655-660 仍然是 playerInventory.MoveItemToThis(runtime.Inventory, sourceItem, …) —— runtime.Inventory 依旧是那个 detached 快照;
  • 而它的来源 QuickGrabMaterialsAction.Finish() → CaptureForQuickGrab(player, true, true) → NearbyResources.Capture → ContainerDiscovery.Discover(…, resourceReadOnly: true) 一字未改(NearbyResources.cs:1145,ContainerDiscovery.cs:387 仍是 new Inventory(true))。

⇒ 与 1.1.5 相比,上游这次只是把文件与类改了名字,行为完全没动 (对照 1.1.5 的 ExpeditionKitAction.cs:626/627/650/656/670,行号语义一一对应)。 本补丁依然必需。

验证

  • dotnet build -c Release → 0 警告 0 错误;DLL 17,920 字节, sha256 00bd424e96bcb56c5aaed0b15a080fa8e358e196248f57fa4f724dc1e3535f7c。
  • DLL 字节检索:Stackmaster.QuickGrabMaterialsAction 与 Stackmaster.ExpeditionKitAction 两个名字都在(双名查找);ExecuteAtomic / RuntimeResourceStack / ContainerHandle / FixExpeditionKit 均在;版本常量与 Win32 版本资源均为 1.1.1。
  • 上游官方 1.2.0 DLL metadata 核对: ExpeditionKitAction 已不存在 ✓(印证本 bug); QuickGrabMaterialsAction.ExecuteAtomic 6 参 ✓(与补丁的 FindMethod(…, 6) 一致); RuntimeResourceStack.get_Inventory/get_Item/get_Container、ContainerHandle.get_Container ✓ 全在。
  • 装机 DLL 与官方 1.2.0 包逐字节一致(316,416 B / be2fc8941b1abd96)。

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 已退化为恒等替换,属纯冗余。
  • 移除:ExecuteWithRollback 的 transpiler + 写回 postfix 分支、FixCraftingFromStorage 配置项、以及 NearbyResourceService 的类型查找。 DLL 内已确认 FixCraftingFromStorage 与 ExecuteWithRollback 零引用。
  • 保留:ExpeditionKitAction.ExecuteAtomic(远征包)。1.1.5 没有修这条路径 —— 它仍走 CaptureForExpedition → Discover(…, resourceReadOnly: true) 的 detached ZDO 快照, 而 crafting 专用的 TryCaptureOwnedPlan/mutableCapture 从未被它调用。故这一半原样保留。
  • 依赖:JStack424-Stackmaster 1.1.4 → 1.1.5(不升会让 r2modman 要求把已升的上游降回去)。
  • 日志改进:transpiler 不再拿「指令总数」当方法指纹(上游一改 IL 就会退化成 未知方法(指令数 N),纯误导),改用 Harmony 注入的 __originalMethod 直接取方法名。
  • 阈值变化:FixExpeditionKit=false 时补丁不再尝试挂载,直接打日志说明并退出(等同于未安装)。

DLL 重新编译:16,896 字节,sha256 da4e8133d5e5b8ef991cc6f366f8e64ed31022d1d02bd27b0f497c8af1f5342e。

1.0.0

  • 首个版本。修复上游 JStack424-Stackmaster 1.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(均默认开)。