StackmasterFix
[AI-generated] Two client-only patches for Stackmaster 1.2.0: (A) Quick Grab Materials never debited chests (duplication); (B) its new remembered-chest fallback refills chests you just emptied (intentional upstream, not a bug) - disabled.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(回退只在箱子已无同类物品时才会发生):见
PlanDepositsIL0x00FF-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-Stackmaster1.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 字节, sha25600bd424e96bcb56c5aaed0b15a080fa8e358e196248f57fa4f724dc1e3535f7c。- DLL 字节检索:
Stackmaster.QuickGrabMaterialsAction与Stackmaster.ExpeditionKitAction两个名字都在(双名查找);ExecuteAtomic/RuntimeResourceStack/ContainerHandle/FixExpeditionKit均在;版本常量与 Win32 版本资源均为1.1.1。 - 上游官方 1.2.0 DLL metadata 核对:
ExpeditionKitAction已不存在 ✓(印证本 bug);QuickGrabMaterialsAction.ExecuteAtomic6 参 ✓(与补丁的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 已退化为恒等替换,属纯冗余。
- 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(均默认开)。
