简易种植修复

创作者: s6652289
已更新:2026-09-19 11:05:41
27KB

关于此 MOD

[AI生成] Advize-PlantEasily 兼容补丁:当其他模组操作同一放置组件时,保持网格种植和自动补种的正确性,包括大型网格。请先阅读适用性说明。

种植容易修复

⚠️ 适用性须知 — 安装前请阅读。

该补丁专为 我自己深度魔改过的配置 (已加载30多个模组——BepInEx 5.4.2350,Valheim 1.0.15,由r2modman管理的30个已启用模组)。以下描述的症状 可能是我特定的模组组合导致的,而不是原模组本身的 bug.

  • 如果你是 不 正是看到这个症状, 请不要安装这个 ——它对你可能毫无作用。
  • 这个补丁可能根本不适用于你的配置 ——使用不同的模组列表,该症状可能永远不会出现。
  • 即使症状相同, 根本原因可能不同 ——这只是根据我在自己机器上发现的情况所采用的变通方法。
  • 我并不声称原模组有缺陷,也无意指责其作者。

如果你认为原模组行为异常, 请先向其作者反馈。 这只是我个人的“在我机器上能用”的临时办法, 但不保证对其他人也有效。

⚠️ 适用性声明 —— 请先读这一段,再决定装不装。

本补丁是在我自己那套环境里做出来的:当时本机同时装着 30+ 个模组 (BepInEx 5.4.2350 / 游戏 1.0.15 / r2modman 管理的 30 个启用模组)。 下面描述的“症状”有可能是我的这套模组组合互相冲突造成的,并不一定是原模组本身的 bug。

  • 如果你没有遇到完全相同的症状,请不要安装本补丁 —— 它可能什么都不做;
  • 本补丁可能不适用于你的环境:换一套模组组合,症状也许根本不会出现;
  • 就算症状一样,成因也可能不同 —— 本补丁只是我按本机情况查出来的绕行方案;
  • 我没有、也不主张任何“原模组有 bug”的结论,原模组作者与此事无关。

如果你觉得原模组有问题,请先向原模组作者反馈。这只是我个人“能跑就行”的绕行方案, 不保证适用于任何人。

简易种植 兼容性补丁(纯客户端,不改写 PlantEasily 任何文件)。 它不针对任何具体模组 —— 只是把几个模组共用的那几处运行时状态归位,使多方同时操作同一部件时不再互相干扰:

# 归位什么 一句话
1 网格幽灵的坐标缓存 整片作物不再叠在同一个点(症状只在多方共存时出现,单独装 PlantEasily 时不会发生)
2 上次选中的作物 收什么就补回什么,不再一路沿用同一种
3 网格幽灵的全部格位坐标 大网格下不再有零星几株叠在一起(网格调大后才会出现)
项 值
适用模组 轻松种植 2.2.2(2.0.0+ 逻辑相同,理论通用)
本补丁版本 1.2.2
前置 BepInEx 5.4.2333+
类型 客户端补丁(单人 / 联机客户端皆可,主机无需安装)
依赖 无(未安装 PlantEasily 时静默不做事)

补丁 1 · 网格幽灵坐标

症状

使用PlantEasily一次种植一片时,整片作物全部叠在同一个点上。实测:4×4 = 15 个额外幽灵,坐标完全相同。

⚠️ 前提这个症状只在多方共存的环境里出现过只装 BepInExPack_Valheim 5.4.2350 + Advize-PlantEasily 2.2.0 时不复现。

例(探针日志):

ghost[0..14] 全部 pos = (2062.731, 49.423, -4402.94) 不同坐标数 = 1 / 15 落点 1..15 全部 pos = (2062.731, 49.423, -4402.94)

机制

放置幽灵的坐标并不是每帧都重算的 —— 它带一层版本号缓存。把这条链拆开看(下面都是反编译实测,不是推测):

① 幽灵网格.初始化幽灵 先把所有额外幽灵叠在一点

// 重建网格时,每个额外幽灵的位置被直接设成「主幽灵位置」 ExtraGhosts[i].transform.position = 主幽灵位置;

② 真正展开坐标靠 幽灵网格更新,但它有脏检查会提前退出

// GhostGrid.UpdateGhost if (cache.lastUpdatedVersion == _gridVersion) return; // ← 判定“已是最新”,直接返回,不更新位置

③ 而 幽灵缓存.初始化 不重置 最后更新版本

// GhostCache.Init —— 只设 piece / plant this.piece = go.GetComponent<Piece>(); this.plant = go.GetComponent<Plant>(); // ★ 没有重置 lastUpdatedVersion ★

④ 版本号只在「位置/朝向/格数变化」时才自增

// GhostGrid.UpdateGridVersion —— 只有这四种情况会 _gridVersion++ 位移 > 0.0001(约 1cm) | BaseRotation 变化 > 1° 有效朝向变化 > 0.1° | Rows / Columns 被改

⇒ 于是当**「复用了旧幽灵池 + 位置被重设 + 本帧版本号没变」三件事撞在一起时(放置流程尾部会经 玩家.设置放置幽灵 重建网格,而站在原地不动、不转视角**时版本号恰好不变),被复用的幽灵就会带着“已是最新版本”的缓存直接提前退出 ⇒ 坐标停在被重设的那一刻,再也不被修正。

这也解释了为什么症状看起来时有时无:移动超过 1cm 或转视角超过 1° 就会让版本号自增,下一次种植便自动恢复正常。

归位方式

在 幽灵网格.构建网格 之后让版本号 +1:

[HarmonyPostfix] static void BuildGridPostfix() { int before = (int)_gridVersionField.GetValue(null); _gridVersionField.SetValue(null, before + 1); // 让所有幽灵缓存失效 }

缓存立即失效 ⇒ 下一帧 幽灵网格更新 必定重算每个幽灵的网格坐标 ⇒ 玩家点击种植时幽灵已经摆正。

网格版本 是 私有静态整数,只有 更新网格版本() 会写它,且该函数只会 +1 或保持不变、绝不回退,所以外部 +1 不破坏其单调性。 全程序集反查确认它只被模组内部使用,无跨模组引用。

效果(本机探针实测)

之前 之后
幽灵不同坐标数 1 / 15 15 / 15
落点坐标 15 个完全相同 15 个各不相同

日志(探针版):

[修复] BuildGrid 完成 → _gridVersion 340 → 341 [诊断] Update 后已恢复正常:15 个幽灵分布到 15 种不同坐标 —— 生效

连续 88 次网格重建(含多次种植)全部正常。


补丁 2 · 自动补种按「被收获的那种作物」

默认行为

开 [采集]收获后重新种植 时,无论收获什么作物,都会补种成“你最近用耕耘机选中的那一种”。 于是混种地块越收越单一:今天最后种的是胡萝卜,明天收大麦、收萝卜,全给你补胡萝卜。

这个补丁把它改成了每种作物各补各的。

实现位置(交互补丁.前缀 第二重载的 IL 逐条实证)

IL 偏移 逻辑
003D–0042 读 最后选中的植物名称 并 为空或空字符串
0049 非空 ⇒ 查表键 = 上次选择的植物名称 ← ★统一补种走这边
0050 为空 ⇒ 查表键 = 工具.获取预制体名称(被收获物) ← 各补各的走这条
005C ReplantDB.Registry.TryGetValue(键, out db),失败 ⇒ 不补种(交回原版)
006C ModContext.PrefabRefs.TryGetValue(db.PlantName, out Piece)
0090 玩家.满足要求 / 无放置费用 检查
00A6 PlacementController.PlacePiece(localPlayer, 被收获物, Plant.prefab)
00C6 消耗资源

最后选择的植物名称 的唯一写点是 设置引用 (方法开头先置 null,仅当幽灵身上有 种植 组件时才重新赋值), 而它的唯一调用者是 幽灵网格.初始化幽灵 ⇒ 只要种过一次某种作物,这个值就长期保留“那一种”。

该字段的读取点只有两处(全程序集成员反查,非推测):

  1. 交互补丁.前缀 —— 决定补种哪一种;
  2. 悬停文本补丁。后置修复 —— 拼悬停提示 重新种植为:X。

两处都带 为空或null 分支,空值是合法状态(该字段在 .cctor 里初始就是 空) ⇒ 把它压制为 null 只会让两边都退回“按被收获物”分支,没有别的副作用。

顺便一提:回收数据库.注册表 同时登记了 可拾取物名称与Plant名两套键(模组初始化补丁.前缀 里 注册表.添加(可拾取物.名称, 数据库)),所以“按被收获物查表”这条路本来就是通的 ——只是被上面那个非空值挡住了。

归位方式

[和谐后置补丁] static void 设置引用后置补丁() { if (最后选中的植物名称 == null) return; // 本来就是空,不动 最后选中的植物名称 = null; // 让它退回"按被收获物"分支 }

怎么自己在游戏里验证

  1. 看提示:悬停一株成熟作物,悬停文本会多一行 重新种植为:<绿>X</绿> —— X 就是本次实际会补种的那种。改动生效后收大麦应显示大麦,收胡萝卜应显示胡萝卜。
  2. 看日志:BepInEx/LogOutput.log 里会出现 首次拦截成功:LastSelectedPlantName「…」→ 置空,此后补种按实际收获的作物进行。
  3. 想对比原行为:把 [重新种植] 同种作物重植 改成 假,不用重启游戏,下一次补种立刻恢复“统一成一种”。

补丁 3 · 大网格幽灵「全量补摆」

症状

网格调大之后,仍有零星几株作物种在同一个点上(不是整片叠住,是“有几株挤在一起”)。网格小的时候完全正常。

实测分界:本机 4×4(16 格)从不复现;1×20(20 格)起就能碰到。 关键变量是每帧能摆几个幽灵(幽灵更新批处理大小,本机 50)与帧内时序。

机制

补丁 1 只解决了“整片叠住”,解决不了这个 —— 因为它保证的是 下一帧会被摆正,而不是**“点下去的时候已经摆正了”**。这里有两重缺口。

缺口一:帧内时序(小网格也会中招)

玩家.设置放置预览 重建网格(构建网格)与每帧的 幽灵网格.更新 在同一帧里的先后顺序不固定。若 更新 先跑:

更新 → 把各幽灵摆到网格坐标 BuildGrid→ InitializeGhosts 把所有幽灵位置【整体覆盖回主幽灵位置】 本帧再无第二次更新 ⇒ 停在主幽灵位置

缺口二:分批轮转(大网格才中招)

幽灵网格。更新 不是一次摆完,而是每帧只摆一批(IL逐条实证):

0046 ldc.i4.1 / get_MaxActiveGhosts / add → N = MaxActiveGhosts + 1 004E config.GhostUpdateBatchSize 0059 Mathf::Min → B = min(GhostUpdateBatchSize, N) 0064 _ghostUpdateIndex % N → 从轮转游标开始 ★ 游标只在 Update 里 +1,永不复位 00C1 for (k = 0; k < B; k++) 每帧只处理 B 个
网格 总格数 每帧处理 B 首帧漏掉
4×4 16 16 0
1×20 20 20 0
10×20 200 50 150
20×20 400 50 350

为什么“鼠标一动就恢复正常”

网格版本 的自增条件只有四条(更新网格版本 逐条实证): 位移 > 0.0001(约 1cm) / 朝向变化 > 1° / 有效朝向变化 > 0.1° / 行·列 变化。

鼠标一停(松手、切物品、站着不动)这四条全不成立 ⇒ 版本号冻结 ⇒ 更新幽灵 开头的脏检查(lastUpdatedVersion == _gridVersion ⇒ 早退) 把没轮到的幽灵全部挡在门外 ⇒ 它们永久停在主幽灵位置。

按下种植时,获取幽灵位置(行=0, 列=0) 对它们全部返回同一个 基础位置, 它们被摆成一摞同坐标幽灵、全部通过 评估状态 判定为合法,而 玩家放置棋子.后缀 只按 for (i = 0; i < MaxActiveGhosts; i++) 逐槽收集(没有坐标去重)⇒ 这几株全部种在同一点。

这也解释了为什么是“零星几株而不是整片:残留的永远只是上次游标之后的那一小段。

归位方式

网格建造 之后不再依赖后续帧 —— 当场把 [1, MaxActiveGhosts] 全部再摆一次:

[HarmonyPostfix] static void BuildGridPostfix() { _gridVersionField.SetValue(null, before + 1); // ① 缓存失效(补丁 1) object cfg = _modConfigField.GetValue(null); // ModContext.config ⇒ ModConfig 实例 int maxActive = get_MaxActiveGhosts(); // ② 全量补摆(补丁 3) int columns = get_Columns(cfg); // ⚠️ 实例属性,必须带 target(1.2.2 修) for (int slot = 1; slot <= maxActive; slot++) { var go = GetGhostObject(slot); // ExtraGhosts[slot - 1] // ⚠️ 上游签名 = (int row, int column, int index),共 **3** 个参数,不含幽灵对象(1.2.1 修) var pos = GetGhostPosition(slot / columns, slot % columns, slot); UpdateGhostTransform(go, pos); // set_position + set_rotation GetGhostCache(go).lastUpdatedVersion = _gridVersion; // 防下帧被早退 } }
  • 方位分解 槽位 / 列 与 槽位 % 列 与原版 更新 里那两行 div / 移除 完全一致 (已核对编译后的 IL:div 与 雷姆 指令确实存在)。
  • ✅ 只写坐标与缓存版本号,不调 更新幽灵状态 ⇒ 不介入任何合法性判定,写错也绝不会凭空把一株作物判成“可种”或“不可种”。
  • ✅ 不推高 幽灵更新批大小 ⇒ 只在 建造网格 那一刻做一次全量,之后每帧仍保持原版的分批节奏,不吃性能。

效果(离线等价性验证)

检查 结果
补摆后每个格位坐标唯一(9 种网格尺寸,含 20×20) 全部唯一,无重合
方位分解与原版 div/雷姆 一致(逐槽比对) 全部一致
缺口一(更新先跑)补摆后 全部摆正
缺口二(大网格分批)补摆后 全部摆正

怎么自己在游戏里验证

把 [通用] 详细日志 改成 真,然后每次放下一片网格都会打一行:

[补摆 3] 网格 20 格(MaxActiveGhosts=19, Columns=20)→ 已摆正 19 个幽灵、 同步 19 个坐标缓存;累计补摆 3 次 / 57 个。

看 已摆正 N 个 是否等于 最大活动幽灵数 —— 相等就说明整片都摆正了。 若你看到的数字小于它,说明补摆被降级了(日志里会有 补摆降级:… 的警告),把日志发我。

⚠️ 特别注意,如果看到 补摆降级:… 的参数个数不符 或 补摆降级:… 是**实例**成员,但调用时没有目标实例, 说明句柄自检(1.2.1 起改为「参数个数 + 静态性」双重校验)把上游的签名变化拦截下来了——补摆会停用自保(补丁 1/2 不受影响),请把日志和 PlantEasily 版本号发我。


配置

首次运行会在 BepInEx/config/s6652289.PlantEasilyFix.cfg 生成:

项 默认 说明
[通用] 已启用 真 总开关。关掉并重启即等同于未安装
[通用] 详细日志 假 每次修正 / 补摆写一行日志(排查用)。正常游玩建议关闭
[补种] 同作物补种 真 补种是否按“被收获的那种作物”来算。改完立即生效,无需重启

安装

手动把 PlantEasilyFix.dll 放进去 BepInEx/plugins/。

r2modman:直接拖入本 zip,或把整个包放进 %APPDATA%\r2modmanPlus-local\Valheim\profiles\<profile>\BepInEx\plugins\PlantEasilyFix\。


安全性

  • 补丁 1 只动一个 整数 版本号,补丁2只设置了一个静态字符串为 空, 补丁 3 只写幽灵的 变形 位置/朝向与一个缓存版本号 —— 不碰资源、不碰存档数据、不介入任何种植合法性判定;
  • 不修改 PlantEasily 的任何配置项,无需改 advize.轻松种植.cfg;
  • 补丁 3 的所有反射句柄(获取幽灵位置 / 更新幽灵变形 / 获取幽灵对象 / 幽灵缓存 / 最后更新版本 / 额外幽灵 / 最大活动幽灵数 / 列 及其宿主实例 ModContext.config) 缺任何一个都只是补摆降级关闭(日志会打一条警告),补丁 1 与补丁 2 照常工作; 其中 模组配置.列 是实例属性,必须搭配 配置 获取到的实例来调用 —— 1.2.0 / 1.2.1 漏了这一步(拿 null 当目标),每次调用都抛出 目标异常 被吞掉, 补摆整整两代从来没运行起来过(2026-09-25 真机实证:54 次调用、0 次成功)。1.2.2 修正;
  • 未安装 PlantEasily 时,本插件等 2 分钟后静默退出,不做任何事;
  • 所有反射调用都包在 try/catch 里 —— 补丁自身出问题最多是“这一次没生效”, 不会让游戏崩溃;
  • PlantEasily 版本变化导致方法/字段找不到时,只记一条 Error 并放弃该条补丁,不影响游戏;
  • 三条补丁互相独立:某一条挂不上不影响其余两条。

卸载

删掉 植物种植修复.dll 即可。没有任何持久化改动需要还原(配置项只影响本插件自身)。


备注

  • 本补丁是兼容性补丁只把共用的运行时状态归位,不改写 轻松种植的文件,也不针对任何具体模组;上游或其他模组做了等效处理后,本补丁可直接卸载。
  • 与其他补丁/模组无冲突:只挂两个 postfix(幽灵网格.构建网格、 放置状态.设置引用),都不改原方法返回值。

AI生成(AI生成)

Valheim: 英灵神殿 的热门 MOD

探索适用于 Valheim: 英灵神殿 的最佳MOD,带来新功能、升级画面,以及令人兴奋的方式来改变您的游戏体验。

使用 Xmod 解锁 Valheim: 英灵神殿 的全部潜力 — 今天就探索这些顶级 MOD!