PlantEasilyFix
[AI-generated] Compatibility patch for Advize-PlantEasily: keeps grid planting and auto-replant correct when other mods operate on the same placement piece, including large grids. Read the applicability notice first.PlantEasily Fix
⚠️ APPLICABILITY NOTICE — please read this before installing.
This patch was written for my own, heavily modded setup (30+ mods loaded — BepInEx 5.4.2350, Valheim 1.0.15, 30 enabled mods managed by r2modman). The symptom described below may be caused by my particular mod combination rather than by a bug in the original mod.
- If you are not seeing exactly this symptom, please don't install this — it may do nothing for you.
- This patch may not apply to your setup at all — with a different mod list the symptom may never appear.
- Even with the same symptom, the root cause may be different — this is only a workaround derived from what I found on my machine.
- I do not claim the original mod is buggy, and no blame is directed at its author.
If you think the original mod misbehaves, please report it to its author first. This is just my personal "works-on-my-machine" workaround, with no guarantee it works for anyone else.
⚠️ 适用性声明 —— 请先读这一段,再决定装不装。
本补丁是在我自己那套环境里做出来的:当时本机同时装着 30+ 个模组 (BepInEx 5.4.2350 / 游戏 1.0.15 / r2modman 管理的 30 个启用模组)。 下面描述的“症状”有可能是我这套模组组合互相冲突造成的,并不一定是原模组本身的 bug。
- 如果你没有遇到完全相同的症状,请不要安装本补丁 —— 它可能什么都不做;
- 本补丁可能不适用于你的环境:换一套模组组合,症状也许根本不会出现;
- 就算症状一样,成因也可能不同 —— 本补丁只是我按本机情况查出来的绕行方案;
- 我没有、也不主张任何“原模组有 bug”的结论,原模组作者与此事无关。
如果你觉得原模组有问题,请先向原模组作者反馈。这只是我个人“能跑就行”的绕行方案, 不保证适用于任何人。
Advize-PlantEasily 的兼容性补丁(纯客户端,不改写 PlantEasily 任何文件)。
它不针对任何具体模组 —— 只是把几个模组共用的那几处运行时状态归位,
使多方同时操作同一部件时不再互相干扰:
| # | 归位什么 | 一句话 |
|---|---|---|
| 1 | 网格幽灵的坐标缓存 | 整片作物不再叠在同一个点(症状只在多方共存时出现,单独装 PlantEasily 时不会发生) |
| 2 | 补种时用的「上次选中作物」 | 收什么就补回什么,不再一路沿用同一种 |
| 3 | 网格幽灵的全部格位坐标 | 大网格下不再有零星几株叠在一起(网格调大后才会出现) |
| 项 | 值 |
|---|---|
| 适用模组 | Advize-PlantEasily 2.2.1(2.0.0+ 逻辑相同,理论通用) |
| 本补丁版本 | 1.2.1 |
| 前置 | 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)
机制
放置幽灵的坐标并不是每帧都重算的 —— 它带一层版本号缓存。 把这条链拆开看(下面都是反编译实测,不是推测):
① GhostGrid.InitializeGhosts 先把所有额外幽灵叠在一点
// 重建网格时,每个额外幽灵的位置被直接设成「主幽灵位置」
ExtraGhosts[i].transform.position = 主幽灵位置;
② 真正摆开坐标靠 GhostGrid.Update,但它有脏检查会早退
// GhostGrid.UpdateGhost
if (cache.lastUpdatedVersion == _gridVersion)
return; // ← 判定"已是最新",直接返回,不更新位置
③ 而 GhostCache.Init 不重置 lastUpdatedVersion
// 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 被改
⇒ 于是当**「复用了旧幽灵池 + 位置被重设 + 本帧版本号没变」三件事撞在一起时
(放置流程尾部会经 Player.SetupPlacementGhost 重建网格,
而站在原处不动、不转视角**时版本号恰好不变),
被复用的幽灵就会带着"已是最新版本"的缓存直接早退
⇒ 坐标停在被重设的那一刻,再也不被修正。
这也解释了为什么症状看起来时有时无:移动超过 1cm 或转视角超过 1° 就会让版本号自增,下一次种植便自动恢复正常。
归位方式
在 GhostGrid.BuildGrid 之后让版本号 +1:
[HarmonyPostfix]
static void BuildGridPostfix()
{
int before = (int)_gridVersionField.GetValue(null);
_gridVersionField.SetValue(null, before + 1); // 让所有幽灵缓存失效
}
缓存立即失效 ⇒ 下一帧 GhostGrid.Update 必定重算每个幽灵的网格坐标
⇒ 玩家点击种植时幽灵已经摆正。
_gridVersion 是 private static int,只有 UpdateGridVersion() 会写它,
且该函数只会 +1 或保持不变、绝不回退,所以外部 +1 不破坏其单调性。
全程序集反查确认它只被模组内部使用,无跨模组引用。
效果(本机探针实测)
| 之前 | 之后 | |
|---|---|---|
| 幽灵不同坐标数 | 1 / 15 | 15 / 15 |
| 落点坐标 | 15 个完全相同 | 15 个各不相同 |
日志(探针版):
[修复] BuildGrid 完成 → _gridVersion 340 → 341
[诊断] Update 后已恢复正常:15 个幽灵分布到 15 种不同坐标 —— 生效
连续 88 次网格重建(含多次种植)全部正常。
补丁 2 · 自动补种按「被收获的那种作物」
默认行为
开着 [Harvesting]ReplantOnHarvest 时,收任何作物都补种成"你最近用耕耘机选中的那一种"。
于是混种地块越收越单一:今天最后种的是胡萝卜,明天收大麦、收萝卜,全给你补胡萝卜。
本补丁把它改成每种作物各补各的。
实现位置(InteractPatches.Prefix 第二重载的 IL 逐条实证)
| IL 偏移 | 逻辑 |
|---|---|
003D–0042 |
读 PlacementState.LastSelectedPlantName 并 IsNullOrEmpty |
0049 |
非空 ⇒ 查表键 = LastSelectedPlantName ← ★统一补种走这条 |
0050 |
为空 ⇒ 查表键 = Utils.GetPrefabName(被收获物) ← 各补各的走这条 |
005C |
ReplantDB.Registry.TryGetValue(键, out db),失败 ⇒ 不补种(交回原版) |
006C |
ModContext.PrefabRefs.TryGetValue(db.PlantName, out Piece) |
0090 |
Player.HaveRequirements / m_noPlacementCost 检查 |
00A6 |
PlacementController.PlacePiece(localPlayer, 被收获物, Plant.prefab) |
00C6 |
Player.ConsumeResources |
LastSelectedPlantName 的唯一写点是 PlacementState.SetReferences
(方法开头先置 null,仅当幽灵身上有 Plant 组件时才重新赋值),
而它的唯一调用者是 GhostGrid.InitializeGhosts
⇒ 只要种过一次某种作物,这个值就长期留着"那一种"。
该字段的读取点只有两处(全程序集成员反查,非推测):
InteractPatches.Prefix—— 决定补种哪一种;HoverTextPatches.Postfix—— 拼悬停提示Replant as: X。
两处都带 IsNullOrEmpty 分支,空值是合法状态(该字段在 .cctor 里初始就是 null)
⇒ 把它压制为 null 只会让两边都退回"按被收获物"分支,没有别的副作用。
顺带一提:
ReplantDB.Registry里同时登记了 Pickable 名与 Plant 名两套键 (ModInitPatches.Prefix里Registry.Add(pickable.name, db)), 所以"按被收获物查表"这条路本来就是通的 —— 只是被上面那个非空值挡住了。
归位方式
[HarmonyPostfix]
static void SetReferencesPostfix()
{
if (LastSelectedPlantName == null) return; // 本来就是空,不动
LastSelectedPlantName = null; // 让它退回"按被收获物"分支
}
怎么自己在游戏里验证
- 看提示:悬停一株成熟作物,hover 文本会多一行
Replant as: <绿>X</绿>—— X 就是本次实际会补种的那种。改动生效后收大麦应显示大麦,收胡萝卜应显示胡萝卜。 - 看日志:
BepInEx/LogOutput.log里会出现首次拦截成功:LastSelectedPlantName「…」→ 置空,此后补种按实际收获的作物进行。 - 想对比原行为:把
[Replant] SameCropReplant改成false,不用重启游戏, 下一次补种立刻恢复"统一成一种"。
补丁 3 · 大网格幽灵「全量补摆」
症状
网格调大之后,仍有零星几株作物种在同一个点上(不是整片叠住,是"有几株挤在一起")。 网格小的时候完全正常。
实测分界:本机
4×4(16 格)从不复现;1×20(20 格)起就能碰到。 关键变量是每帧能摆几个幽灵(GhostUpdateBatchSize,本机 50)与帧内时序。
机制
补丁 1 只解决了"整片叠住",解决不了这个 —— 因为它保证的是 "下一帧会被摆正",而不是**"点下去的时候已经摆正了"**。这里有两重缺口。
缺口一:帧内时序(小网格也会中招)
Player.SetupPlacementGhost 重建网格(BuildGrid)与每帧的 GhostGrid.Update
在同一帧里的先后顺序不固定。若 Update 先跑:
Update → 把各幽灵摆到网格坐标
BuildGrid→ InitializeGhosts 把所有幽灵位置【整体覆盖回主幽灵位置】
本帧再无第二次 Update ⇒ 停在主幽灵位置
缺口二:分批轮转(大网格才中招)
GhostGrid.Update 并不是一次摆完,而是每帧只摆一批(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 |
为什么"鼠标一动就恢复正常"
_gridVersion 的自增条件只有四条(UpdateGridVersion 逐条实证):
位移 > 0.0001(约 1cm) / 朝向变化 > 1° / 有效朝向变化 > 0.1° / Rows·Columns 变化。
鼠标一停(松手、切物品、站着不动)这四条全不成立 ⇒ 版本号冻结
⇒ UpdateGhost 开头的脏检查(lastUpdatedVersion == _gridVersion ⇒ 早退)
把没轮到的幽灵全部挡在门外 ⇒ 它们永久停在主幽灵位置。
按下种植时,GetGhostPosition(row=0, col=0) 对它们全部返回同一个 BasePosition,
它们被摆成一摞同坐标幽灵、全部通过 EvaluateStatus 判定为合法,
而 PlayerPlacePiece.Postfix 只按 for (i = 0; i < MaxActiveGhosts; i++) 逐槽收集
(没有坐标去重)⇒ 这几株全部种在同一点。
这也解释了为什么是"零星几株"而不是整片:残留的永远只是上次游标之后的那一小段。
归位方式
BuildGrid 之后不再依赖后续帧 —— 当场把 [1, MaxActiveGhosts] 全部补摆一遍:
[HarmonyPostfix]
static void BuildGridPostfix()
{
_gridVersionField.SetValue(null, before + 1); // ① 缓存失效(补丁 1)
int maxActive = get_MaxActiveGhosts(); // ② 全量补摆(补丁 3)
int columns = get_Columns();
for (int slot = 1; slot <= maxActive; slot++)
{
var go = GetGhostObject(slot); // ExtraGhosts[slot - 1]
var pos = GetGhostPosition(go, slot % columns, slot / columns, slot);
UpdateGhostTransform(go, pos); // set_position + set_rotation
GetGhostCache(go).lastUpdatedVersion = _gridVersion; // 防下帧被早退
}
}
- 方位分解
slot / Columns与slot % Columns与原版Update里那两行div/rem完全一致 (已核对编译后的 IL:div与rem指令都确实存在)。 - ✅ 只写坐标与缓存版本号,不调
UpdateGhostStatus⇒ 不介入任何合法性判定, 写错也绝不会凭空把一株作物判成"可种"或"不可种"。 - ✅ 不推高
GhostUpdateBatchSize⇒ 只在BuildGrid那一刻做一次全量, 之后每帧仍保持原版的分批节奏,不吃性能。
效果(离线等价性验证)
| 检查 | 结果 |
|---|---|
| 补摆后每个格位坐标唯一(9 种网格尺寸,含 20×20) | 全部唯一,无重合 |
方位分解与原版 div/rem 一致(逐槽比对) |
全部一致 |
| 缺口一(Update 先跑)补摆后 | 全部摆正 |
| 缺口二(大网格分批)补摆后 | 全部摆正 |
怎么自己在游戏里验证
把 [General] VerboseLog 改成 true,然后每次放下一片网格都会打一行:
[补摆 3] 网格 20 格(MaxActiveGhosts=19, Columns=20)→ 已摆正 19 个幽灵、
同步 19 个坐标缓存;累计补摆 3 次 / 57 个。
看 已摆正 N 个 是否等于 MaxActiveGhosts —— 相等就说明整片都摆正了。
若你看到的数字小于它,说明补摆被降级了(日志里会有 补摆降级:… 的警告),把日志发我。
配置
首次运行会在 BepInEx/config/s6652289.PlantEasilyFix.cfg 生成:
| 项 | 默认 | 说明 |
|---|---|---|
[General] Enabled |
true |
总开关。关掉并重启即等同于未安装 |
[General] VerboseLog |
false |
每次修正 / 补摆写一行日志(排查用)。正常游玩建议关闭 |
[Replant] SameCropReplant |
true |
补种是否按「被收获的那种作物」。改完立即生效,无需重启 |
安装
手动:把 PlantEasilyFix.dll 放进 BepInEx/plugins/。
r2modman:直接拖入本 zip,或把整个包放进
%APPDATA%\r2modmanPlus-local\Valheim\profiles\<profile>\BepInEx\plugins\PlantEasilyFix\。
安全性
- 补丁 1 只动一个
int版本号,补丁 2 只置一个静态字符串为null, 补丁 3 只写幽灵的Transform位置/朝向与一个缓存版本号 —— 不碰资源、不碰存档数据、不介入任何种植合法性判定; - 不修改 PlantEasily 的任何配置项,无需改
advize.PlantEasily.cfg; - 补丁 3 的所有反射句柄(
GetGhostPosition/UpdateGhostTransform/GetGhostObject/GetGhostCache/lastUpdatedVersion/ExtraGhosts/MaxActiveGhosts/Columns) 缺任何一个都只是补摆降级关闭(日志会打一条警告),补丁 1 与补丁 2 照常工作; - 未安装 PlantEasily 时,本插件等 2 分钟后静默退出,不做任何事;
- 所有反射调用都包在 try/catch 里 —— 补丁自身出问题最多是"这一次没生效", 不会让游戏崩溃;
- PlantEasily 版本变化导致方法/字段找不到时,只记一条 Error 并放弃该条补丁,不影响游戏;
- 三条补丁互相独立:某一条挂不上不影响其余两条。
卸载
删掉 PlantEasilyFix.dll 即可。没有任何持久化改动需要还原(配置项只影响本插件自身)。
备注
- 本补丁是兼容性补丁:只把共用的运行时状态归位,不改写 PlantEasily 的文件, 也不针对任何具体模组;上游或其它模组做了等价处理之后,本补丁可直接卸载。
- 与其它补丁/模组无冲突:只挂两个 postfix(
GhostGrid.BuildGrid、PlacementState.SetReferences),都不改原方法返回值。
AI 生成(AI-generated)


