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.
EasyCraft
[AI-generated] Load 5 items per interaction into smelters/refineries, with per-station material limits (e.g. charcoal kiln = wood only). Refuel torches/bonfires/hearths, auto-pull from nearby chests. Campfire stays 1-per-press. Client-only.
By s6652289
| Date uploaded | a week ago |
| Version | 1.0.2 |
| Download link | s6652289-EasyCraft-1.0.2.zip |
| Downloads | 26 |
| Dependency string | s6652289-EasyCraft-1.0.2 |
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.2350README
EasyCraft
AI 生成声明:本模组由 AI 智能体(WorkBuddy)在用户的明确需求与逐项验收下编写; 功能设计、真机测试与发布决定均由用户主导。
炼制站点 / 火源的装填增强。 由 EasyBatchFill 与 EasyNearbyFill 合并而成 —— 合并同时消掉了原先两者之间那条「必须成对升级、单升一边 BepInEx 直接加载失败」的跨程序集硬依赖。
功能
- 批量装填:一次交互装入
BatchSize个(默认 5),取代原版的一次 1 个。 站点:熔炉 / 高炉 / 炭窑 / 风车 / 纺车 / Eitr 精炼厂。 - 站点进料限制(1.0.2 新增):限定某个站点只允许自动装填指定材料。 默认 = 炭窑只烧木材(不烧细木 / 圆木)。
- 火源加燃料:火炬 / 壁灯 / 篝火 / 火盆 / 灶 一次加
BatchSize个; 营火(fire_pit)默认走「一次一个」名单:数量保持原版 1 个, 但仍然会从附近箱子自动取木头 —— 做饭的基础火一次烧 5 个木头太快。 - 就近取料:背包料不够时,从半径 30 米内的箱子自动补足。 默认只认木箱 + 强化箱;私人箱与宝藏箱有意排除。
- 占用保护:队友站在箱子旁就跳过它,避免抢走所有权导致对方物品回弹。
配置(5 节)
| 节 | 内容 |
|---|---|
1 - Batch Fill |
Enabled / BatchSize / StationOreAllow |
2 - Fire Sources |
Enabled / Exclude / Sources(留空 = 全部火源生效)/ OneAtATime / HoldInterval / RespectToggle |
3 - Nearby Chests |
Enabled / Radius / AllowedContainers / RespectWards / ClaimOwnership |
4 - Safety |
SkipInUse / SkipIfPlayerNear / PlayerNearDistance |
5 - Debug |
DebugLogging |
BepInEx/config/easycraft.client.only.cfg ⚠️ 改完必须重启游戏。
站点进料限制 StationOreAllow(1.0.2 起)
原版炭窑能烧 木材 / 细木 / 圆木 三种,价值差很多。这一项让本模组的自动装填 只挑你允许的材料 —— 默认就是「炭窑只烧木材」:
charcoal_kiln=$item_wood
想同时限制别的站点,用分号加组:
charcoal_kiln=$item_wood ; smelter=$item_copperore,$item_tinore
-
材料名三种写法都认(忽略大小写):
$item_wood/item_wood/Wood -
站点名也三种都认:
charcoal_kiln(GameObject 名)/piece_charcoalkiln(本地化键)/charcoalkiln—— 内部会去掉piece_与下划线后归一化比较,所以顺手怎么写都能匹配上。 -
常用站点名:
站点 GameObject 名 炭窑 charcoal_kiln冶炼炉 smelter高炉 blastfurnace风车 windmill纺织机 piece_spinningwheel埃达之力提炼器 piece_eitrrefinery -
只影响本模组的自动装填(批量装填 + 就近取料);你自己手动往站点里放什么都不受限制。
-
留空 = 不限制任何站点(完全回到原版);某个站点写成
站点=(右边留空)= 单独放行该站点。 -
名字万一写错,站点不会变成"什么都装不进去" —— 会退回原版行为, 同时打一条 Warning 把该站点实际可用的材料名列出来,直接照抄即可。
-
生效情形看日志里的
站点进料限制生效:charcoal_kiln 只允许装填 [$item_wood](该站点可用共 3 种:…)。
三张火源名单叠加后的语义(1.0.1 起)
| Exclude | OneAtATime | 行为 |
|---|---|---|
| ✅ 命中 | ❌ 不在 | 完全交回原版(一次 1 个、也不从箱子取料) |
| ✅ 命中 | ✅ 在 | 数量固定 1 个,但料源(附近箱子)可用 ← 营火默认 |
| ❌ 未命中 | — | 正常接管,一次装 BatchSize 个 |
想给别的火源也用「一次一个 + 拉料」,把它的 GameObject 名同时加进 Exclude 和 OneAtATime。
⚠️ 填火源名字前先看这里
Exclude / Sources 里填的是 GameObject 名,它不一定等于本地化键:
| 官方中文 | GameObject 名 | 容易误写 |
|---|---|---|
| 营火(能做饭的基础火) | fire_pit |
piece_firepit |
| 篝火 | bonfire |
piece_bonfire |
| 灶 | hearth |
piece_hearth |
| 壁灯 | piece_dvergr_lantern |
— |
| 火炬 | piece_groundtorch / piece_groundtorchwood … |
— |
不确定某个火源到底叫什么,看日志里的 [火源探测] 行 ——
它会把「组件在=…」和「ZDO名=…」都打出来。
实现要点
- 站点装填:
Smelter.OnAddOre/OnAddFuel的 prefix;火源:Fireplace.Interact的 prefix。 - 料源:
Container.Awake/OnDestroyed维护活箱子登记表,搜索时以【机器】和【玩家】各自为圆心。 取料顺序:先吃背包 → 按距离更近优先逐个箱子取 → 取够即停。 - 取料前用
ZNetView.ClaimOwnership()接管箱子 ZDO 所有权 —— 必须, 否则Container.OnContainerChanged()的if (!IsOwner()) return;会让改动不被保存,产生复制。 - 🔴 Harmony prefix 铁律:处理不了就
return true完整交回原版,绝不静默return false—— 那会表现为「按 E 完全没反应」,极难排查。
联机
纯客户端。 主机、其他玩家都不需要装。
不声明 NetworkCompatibilityAttribute,不引用 Jotunn,不内嵌 ServerSync / CCS。
CHANGELOG
更新日志
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 合并而成(同时消掉了两者之间那条跨程序集硬依赖)。
- 批量装填(站点 + 火源)、就近取料、占用保护。