EasyCraft
[AI-generated] Load 5 items per interaction into smelters/refineries, with per-station material limits (e.g. charcoal kiln = wood only). Refuel torches/hearths, and auto-pull from nearby chests and carts. Client-only.更新日志
1.0.6(2026-09-19)
修复:所有箱子名匹配从「精确比对」改为「归一化比对」—— 黑金宝箱等此前一直取不到料。
根因(真机日志钉死):GameObject 名 ≠ 本地化键
1.0.5 装好后玩家实测,日志给出了决定性证据(黑金宝箱采样 260 次):
· [未通过白名单] GameObject[piece_chest_blackmetal] 本地化键[$piece_chestblackmetal] 板车[否] ← ×14 个全被拒
· GameObject[piece_chest] 本地化键[$piece_chest] → 不含目标材料,箱内共 ... ← 通过并读到内容
| GameObject 名(白名单要匹配这个) | 本地化键 | 一致? |
|---|---|---|
piece_chest |
$piece_chest |
✅ |
piece_chest_blackmetal |
$piece_chestblackmetal |
❌ 不一致 |
piece_chest_private |
$piece_chestprivate |
❌ 不一致 |
⇒ 多词箱子的 GameObject 名带下划线、本地化键不带;单词的 piece_chest 两者相同。
这就解释了为什么大箱子一直好使、其它箱子全静默失配。
并且 1.0.4 的"修正"方向是错的 —— 它扫了 resources.assets,而那份里是本地化键,
不是预制体名(预制体本体在压缩过的 bundle 里,裸字节扫描扫不到:
piece_chest_private 在全库裸扫描里 0 命中,但运行时 GameObject 名确实是它)。
于是 1.0.4 把默认值改成了不带下划线的形式,黑金箱等仍然失配,只是从一种错法换成另一种。
复盘三个版本:1.0.0~1.0.3 用带下划线的名字 + 精确比对(名字其实写对了,但名单不全); 1.0.4 改成不带下划线的名字(反而把原本能匹配的黑金箱写坏了);1.0.6 归一化 —— 两种写法都通。
修复内容
- 新增
NormalizeContainerName():去$前缀 → 去piece_前缀 → 去掉所有下划线 → 转小写。piece_chest_blackmetal/piece_chestblackmetal/CHESTBLACKMETAL→ 全部得到chestblackmetal,怎么写都认。 ReloadAllowed()改为存归一化键;IsAllowed匹配时把实际容器名也归一化再比(两边口径一致)。- 保留
AllowedRaw(用户 cfg 原文)只用于日志回显,启动自检多打一行生效匹配键, 名字对不对得上可以当场看出来。 - 诊断行
DescribeNames增加归一化键一列(原来打 GameObject/本地化键/ZDO 三列, 但没打"实际参与匹配的那个键",所以看不出到底差在哪)。
顺带修复(同批,玩家上一轮日志暴露的)
- 板车容器取不到料的第二个原因:
EnsureOwner原先用c.GetComponent<ZNetView>(), 而板车的容器挂在子物体上(日志里它的 GameObject 名直接叫Container), 自己身上没有 ZNetView —— 它靠Container.m_rootObjectOverride指向 Vagon 的那个 ZNetView (Container.Awake的 IL 就是这么解析m_nview的)。 ⇒ 新增ResolveNView():优先读游戏解析好的m_nview(反射),退回 public 的m_rootObjectOverride, 最后才GetComponent。日志里原先打的接管所有权失败(没有 ZNetView 组件)就是这个。 - 板车"正在被拖行"判定加子信号诊断:无参
IsAttached()=m_attachJoin != null || zdo.GetBool("attachJoint", false),两条语义不同 —— 日志现在会说明是"本机看得到实体关节"还是"只有 ZDO 标志为真(别人在拖 / 标志残留)", 便于判断"用户说车停着、却有一批被判成拖行"到底哪一种。
重要文档更正
- README 与配置说明里「箱子名一律不带下划线」的说法是错的,已改为: 匹配已归一化,带不带下划线都认,并附上 GameObject 名 ↔ 本地化键的对照。
- 离线回归
verify_container_whitelist.py扩到 44 组,新增第 ⑥ 组专测归一化, 含【安全】piece_chest 不会误匹配 piece_chest_blackmetal这类"不能过度匹配"的用例。
1.0.5(2026-09-19)
修复两处,都会让"就近取料"静默失效: ① 取料顺序错 → 从 1.0.0 上线起一次都没成功取到过料(38 条日志 0 次成功); ② 板车识别只靠一个可能为 null 的字段 → 停着的板车取不到料。
① 就近取料从未成功过(顺序错)
- 起因:用户反馈「黑金宝箱还是没有被正确拉取材料」。查 1.0.4 日志发现比这严重得多:
⇒ 就近取料从来没有工作过,且不报任何错。[就近料源] 记录 38 条,其中成功(从 N 个箱子取了 X 个)**0 条** [火源装填] 0 条 - 排除项(都已逐条实测排除):白名单、cfg 生效、半径、材料名格式、箱子类型判定。 日志明确显示「候选箱 17~18 个」且全部通过白名单 —— 问题不在"找不到箱子"。
- 根因(IL 实证):
Container.GetInventory()的实现只有一句return m_inventory;—— 它是Container.Awake/Load()那一刻从 ZDO 反序列化出来的缓存,不是实时数据。 而Container.Load()的反汇编显示它只在ZDO.DataRevision变化时才真正反序列化:
客户端若从没打开过那只箱子,0000 DataRevision == m_lastRevision → return false ← 版本没变 = 不加载 001A if (m_inUse) return false ← 使用中 = 也不加载 005D m_inventory.Load(new ZPackage(zdo.GetByteArray(s_items)))m_inventory可能一直停在初始空库。 - 原代码的顺序错误(这是关键):
先读库存、再接管所有权 ⇒ 读到空快照就var item = cinv.GetItem(n, -1, false); // ← 读到空库,直接 continue if (item == null) continue; ... if (!EnsureOwner(c)) continue; // ← 永远走不到continue,ClaimOwnership永远执行不到。 - 修复:把顺序倒过来 —— 先接管所有权 → 强制刷新 ZDO 库存 → 再读。
EnsureOwner()前置,并回报失败原因(原来失败是静默的)- 新增
RefreshChestInventory():反射调(private 的)Container.Load()强制刷新 M_ContainerLoad加兜底解析ResolveContainerLoad():AccessTools.Method在参数表 不匹配时会静默返回 null(游戏换版本给Load加可选参数就会),null 会让刷新被跳过、 症状又变回"箱子是空的"。现退回「按名找唯一无参重载」- 新增
VerifyOwnerAfterTake():取料后补校验,若那一刻仍无写权(改动不会保存 → 会复制) 打 Warning 且不受DebugLogging控制,让这类问题无法再静默发生
② 停着的板车取不到料(识别依据不可靠)
-
起因:用户反馈「静止在地上、没人拉也没人开着的板车,不会被正常拉取材料」。
-
根因(IL 实证):原先只靠
Container.m_wagon != null认板车。扫字段引用发现:字段 写入点 读取点 Container.m_wagon零处(只靠预制体序列化) 仅 3 处,全在开箱 RPC( RPC_RequestOpen/RequestStack/RequestTakeAll),且全部带 null 判断Vagon.m_container零处(同上) Vagon.InUse/UpdateMass/UpdateLoadVisualization⇒
m_wagon的三处读取都写(m_wagon != null && m_wagon.InUse()),说明游戏本身就预期它可能为 null; 为 null 时游戏毫无症状。而Vagon.m_container若不连,板车就不按载重变沉(核心可见行为)⇒ 它才可靠。 旧代码只认m_wagon⇒ 一旦为 null 就掉进"名字白名单"分支, 而Cart不在AllowedContainers里 ⇒ 静默失败。 -
修复:改为两条信号任一命中,主信号换到可靠的那一侧:
- 新增
CartRegistry(EasyCraftPlugin.cs)—— 用 Harmony 补丁复刻游戏自己的Vagon.m_instances清单(staticList<Vagon>,Awake里 Add、OnDestroy里 Remove,IL 实证):
查询时用[HarmonyPatch(typeof(Vagon), "Awake")] // postfix → CartRegistry.Add [HarmonyPatch(typeof(Vagon), "OnDestroy")] // prefix → CartRegistry.RemoveVagon.m_container反查 Container(表里存 Vagon 而非 Container, 避免Vagon.Awake/Container.Awake先后顺序的坑) Container.m_wagon保留为兜底信号- 新增
IsCart():只做识别,与IsAllowed共用同一套信号,避免两处口径不一致
- 新增
③ 顺带修正一处文档/注释错误
Vagon.IsAttached()(无参)的旧注释写的是 m_attachJoin != null。实际 IL:
0000 m_attachJoin != null → true
000E else if (m_nview.IsValid())
001B zdo.GetBool(ZDOVars::s_attachJointHash, false) ← 还有 ZDO 网络标志
⇒ 同时看【本地关节】与【ZDO 网络标志】。行为本身是对的(更严格),只是注释误导。
④ 日志不再能骗人
现在同时打「候选 N 个 / 通过 M 个 / 取到 K 件 / 场上板车 J 辆」四个数字;
每个候选容器都会打出 GameObject名 / ZDO预制体名 / 本地化键 / **板车[是/否]及其命中信号** 四连,
板车被跳过时还会说明原因(IncludeCarts 开关是关的 或 正在被拖行(停放后即可取))。
以后同类"静默空手而归"一眼就能定位。
1.0.4(2026-09-19)
修复:箱子白名单从 1.0.0 起就一直写错,导致木箱等取不了料;并补上黑金箱子等分类。
-
起因:用户反馈「拉取材料的来源没有黑金箱子」。查下来问题比这严重得多 —— 不是少一个黑金箱子,而是整个白名单的写法就是错的。
-
根因(决定性证据):游戏里所有箱子预制体名一律不带下划线。二进制扫描
valheim_Data/resources.assets取得现存全部 8 种:游戏实际预制体名 简中名 piece_chestwood宝箱(木箱) piece_chest加固型宝箱(大箱子) piece_chestblackmetal黑金宝箱 piece_chestgrausten灰石箱 piece_chestbarrel桶 piece_chestwarderobe衣柜 piece_chestprivate个人宝箱 piece_chesttreasure珍宝箱 而 1.0.0~1.0.3 的默认值写的是
piece_chest_wood,piece_chest—— 已在同一份资源文件里确认piece_chest_wood、piece_chest_blackmetal等 NOT FOUND(根本不存在)。匹配是AllowedSet.Contains(prefab)的精确比对 (仅忽略大小写)⇒ 除piece_chest外全部失配: 木箱从上线起就没取过料,用户按文档加黑金箱子也永远不生效。 -
修复:
AllowedContainers默认值改为正确的 6 项 ——piece_chestwood,piece_chest,piece_chestblackmetal,piece_chestgrausten,piece_chestbarrel,piece_chestwarderobe个人宝箱(
piece_chestprivate)与珍宝箱(piece_chesttreasure)仍有意排除。 -
新增配置迁移
MigrateAllowedContainers(): 若 cfg 里仍是 1.0.3 的旧默认值,自动替换为新默认值并打一条 Warning 说明原因 —— 用户无需手动改配置,升级即生效。 ⚠️ 只在"恰好等于旧默认值"时才替换:用户自己改过(含清空、写了别的名字)一律原样尊重, 绝不覆盖他手工调过的配置。 -
配置说明与 README 全部重写:把 8 种箱子的真实名字、哪些默认纳入、哪些有意排除列清楚, 并明确标出 5 个高频误写(
piece_chest_wood→piece_chestwood等)。 -
自检日志改进:原来直接打 cfg 原始串(写错了也看不出来), 现在打解析后的生效清单 + 项数 —— 配错了一眼可见:
就近取料=开:半径 30 米,允许的箱子 6 种 [piece_chest,piece_chestbarrel,…](板车=开),…
⚠️ 一个容易混淆的点(已写进 README)
站点名(StationOreAllow)做了归一化 —— 去掉 piece_ 前缀与所有下划线再比,
所以 charcoal_kiln / piece_charcoalkiln / charcoalkiln 怎么写都认。
箱子名(AllowedContainers)是精确比对,必须与游戏资源里的名字完全一致(仅忽略大小写)。
两者规则不同,不要把站点名的宽松写法套到箱子上。
离线验证
新增 verify_container_whitelist.py,26 组用例全过,五组:
- 新默认值 6 项逐一命中游戏真实预制体名;个人宝箱 / 珍宝箱确认被排除
- 旧默认值被迁移;5 种用户自定义值(含清空、带空格、只留一个名字)均不被覆盖
- 边界:大小写不敏感、留空 → 空集(不取任何箱子)、逗号空段、纯空格串、
None不崩 - 回归:确认旧默认值里带下划线的名字确实命中不了真实名 —— 反证迁移的必要性
- 与游戏资源对账:脚本自己扫
resources.assets, 校验「硬编码清单」与「游戏实际」完全一致(游戏更新改名时不会悄悄失真)
1.0.3(2026-09-17)
新增:板车也算作取料来源。
- 起因:板车上装着一堆材料,但就近取料只认箱子,够不到板车。
- 板车(
Cart)与雪橇(Sled)都用Vagon组件 +Container, 所以它们本来就被ContainerRegistry收录了(注册挂在Container.Awake上), 只是名字不在AllowedContainers白名单里、永远匹配不到。 - 🔑 判据用
Container.m_wagon != null,不是按名字匹配。理由:- 板车的
Container可能挂在子节点上,GameObject 名未必是Cart(箱子是piece_chest_*,名字匹配一直好使;板车没有piece_前缀) m_wagon是语义明确的引用,游戏自己就用它(Vagon.InUse()也依赖它) ⇒ 不怕以后改名/换层级,也不需要把Cart写进AllowedContainers。
- 板车的
- 新增配置
3 - Nearby Chests → IncludeCarts(默认 开)。 - ⚠️ 正在被拖行的板车会被跳过。两个理由:
① 位置一直在变,距离筛选与"取完就不算了"的假设都不成立;
② 拖行时所有权在拖车人手里,
EnsureOwner的ClaimOwnership有可能把车抢走。 停下不动了才会被取料。 - ⚠️ 这里没有用
Vagon.InUse()去做占用判定 —— 它等于Container.IsInUse() || IsAttached(),会把"有人正开着板车容器"也拦掉, 而那件事已由IsBusy()统一处理、且受4 - Safety → SkipInUse开关控制。 在此重复拦截会让那个开关对板车失效。所以IsAllowed里只拦"被拖行"。 - 另一处连带修复:
Provide()开头原本"白名单为空就直接返回 0",改为 "白名单为空 且 板车也未启用" 才早退 —— 否则清空AllowedContainers只想用板车取料时会被直接挡掉。 - 自检行加上板车开关状态:
… 允许的箱子 [...](板车=开)…
离线验证
verify_cart_source.py 12 组用例全过,含三条专门回归:
- 拖行中的板车必须被跳过(且理由明确)
IsAllowed不因"容器被打开中"拦板车,而IsBusy会拦、SkipInUse=false时放行 (保证那个开关对板车同样有效)- 白名单为空 + 板车启用 → 不能早退;两者都关才早退
另含:开关关时不认板车、普通箱子路径不受影响、私人箱仍排除、雪橇同理、
m_wagon取不到时安全退回名字匹配、空候选不崩。
1.0.2(2026-09-17)
新增:站点进料限制 —— 限定某个炼制站点只允许自动装填指定材料。默认「炭窑只烧木材」。
- 起因:炭窑原版能烧 木材 / 细木 / 圆木 三种,而它们价值差很多。
实测日志(改前):
[站点进料口装填] charcoal_kiln|$item_finewood 装了 5 个—— 自动装填把细木挑去烧了。 - 新增配置
1 - Batch Fill → StationOreAllow,默认值:
⇒ 炭窑只会自动装木材,细木 / 圆木不再被自动投进去。charcoal_kiln=$item_wood - 格式为分号分组(
站点=材料1,材料2),可同时限制多个站点。 - 材料名三种写法都认:
$item_wood/item_wood/Wood(忽略大小写)。 - 站点名也三种都认:
charcoal_kiln/piece_charcoalkiln/charcoalkiln—— 内部去掉piece_前缀与下划线后归一化比较,所以「本地化键」与「GameObject 名」的 写法差异(charcoal_kiln↔piece_charcoalkiln、fire_pit↔piece_firepit)都能匹配上。 - 只影响本模组的自动装填(批量装填 + 就近取料),手动放置不受限制。
过滤点选在
Engine.OreSharedNames():候选列表会一路传给扩展料源 (IFillSource.Provide(target, user, sharedNames, …)),所以过滤一处、两处同时生效。 - 安全设计:绝不让站点变成"什么都装不进去"。 若配置的材料名在该站点上一个都对不上(多半是写错),会退回原版行为并打一条 Warning 把该站点实际可用的材料名列出来,方便直接照抄修正。
- 启动自检新增一行,打生效值(解析后的规则组数与内容),而不是 cfg 原始串 —— 否则"配了但没解析成功"时日志会显示得像是生效了。
- 首次装填某站点时会打一行生效明细:
站点进料限制生效:charcoal_kiln 只允许装填 [$item_wood](该站点可用共 3 种:…)
离线验证
把 C# 判定逻辑逐行照搬成 Python,10 个场景全过(verify_station_ore_filter.py):
默认配置只留木材 / 五种材料写法都认 / piece_ 前缀站点名也认 / 无规则站点不受影响 /
多站点同时限制 / 名字写错则退回原版并警告 / 右边留空则不限制 / 整串留空则全局回原版 /
配置串缺 = 不崩 / 组件挂在子节点时沿父链找站点名。
⚠️ 其中「站点名用
piece_前缀写法」这一条当场抓出了一个真 bug: 原实现是"给 GameObject 名拼上piece_再比",但本地化键是piece_charcoalkiln(charcoalkiln是一个词、没有下划线),永远拼不出来。 已改为归一化比较(去piece_+ 去下划线)。
1.0.1
- 营火改为「一次一个 + 允许从附近箱子拉料」:新增
OneAtATime名单 (Exclude命中且OneAtATime在 → 数量固定 1 但料源可用)。
1.0.0
- 由 EasyBatchFill + EasyNearbyFill 合并而成(同时消掉了两者之间那条跨程序集硬依赖)。
- 批量装填(站点 + 火源)、就近取料、占用保护。


