You are viewing a potentially older version of this package. View all versions.
s6652289-PlantEasilyFix-1.1.0 icon

PlantEasilyFix

[AI-generated] Patches for Advize-PlantEasily: (1) fixes grids planting every crop on one spot; (2) auto-replant grows back the crop you harvested, not the one you last selected. No cfg edit needed.

Date uploaded a month ago
Version 1.1.0
Download link s6652289-PlantEasilyFix-1.1.0.zip
Downloads 70
Dependency string s6652289-PlantEasilyFix-1.1.0

README

PlantEasily Fix

给 Advize-PlantEasily 打的两个行为修正补丁(纯客户端,不改写 PlantEasily 任何文件):

# 修什么 一句话
1 重叠种植 网格种植时整片作物全部叠在同一个点 → 摆正
2 自动补种 收什么都补成同一种 → 改成每种作物各补各的
项 值
适用模组 Advize-PlantEasily 2.2.0(2.0.0+ 逻辑相同,理论通用)
本补丁版本 1.1.0
前置 BepInEx 5.4.2333+
类型 客户端补丁(单人 / 联机客户端皆可,主机无需安装)
依赖 无(未安装 PlantEasily 时静默不做事)

补丁 1 · 重叠种植

症状

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

例(修复前的探针日志):

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 ★

④ 触发点在批量种植协程的末尾

// <BulkPlanting>d__10.MoveNext 尾部(IL 偏移 +0389 起)
_isPlanting = false;
Player.SetupPlacementGhost();      // ← 重建幽灵网格
ReEnableRenderers(...);

SetupPlacementGhost → BuildGrid → GrowPoolIfNeeded(复用旧幽灵池)+ InitializeGhosts ⇒ 位置被重设为"全叠住",而复用幽灵身上残留的 lastUpdatedVersion 仍等于当前 _gridVersion ⇒ 下一帧 UpdateGhost 全部早退 ⇒ 位置永远停在"全叠住"状态。

修复方式

在 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 不破坏其单调性。 全局搜索确认它是纯模组内部使用(readonly 初始化 + 两处使用),无跨模组引用。

修复效果(探针实测,同一台机器)

修复前 修复后
幽灵不同坐标数 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 ⇒ 只要种过一次某种作物,这个值就长期留着"那一种"。

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

  1. InteractPatches.Prefix —— 决定补种哪一种;
  2. 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;                    // 让它退回"按被收获物"分支
}

怎么自己在游戏里验证

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

配置

首次运行会在 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 —— 不碰位置、资源、存档数据;
  • 不修改 PlantEasily 的任何配置项,无需改 advize.PlantEasily.cfg;
  • 未安装 PlantEasily 时,本插件等 2 分钟后静默退出,不做任何事;
  • 所有反射调用都包在 try/catch 里 —— 补丁自身出问题最多是"这一次没修到", 不会让游戏崩溃;
  • PlantEasily 版本变化导致方法/字段找不到时,只记一条 Error 并放弃该条补丁,不影响游戏;
  • 两条补丁互相独立:某一条挂不上不影响另一条。

卸载

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


备注

  • 本补丁是给第三方模组打的行为修正,不改写 PlantEasily 的文件; PlantEasily 作者一旦自行修复,本补丁可直接卸载。
  • 与其它补丁/模组无冲突:只挂两个 postfix(GhostGrid.BuildGrid、 PlacementState.SetReferences),都不改原方法返回值。

AI 生成(AI-generated)

CHANGELOG

Changelog

1.2.2

再修 bug:补丁 3「大网格全量补摆」仍然从未生效 —— 1.2.1 只修了一半。

  • 根因(2026-09-25 真机实证):ModConfig.Columns 是实例属性 (上游 ModConfig 是 sealed class,该属性无 static),而 1.2.0 / 1.2.1 用 Invoke(null, null) 调它 ⇒ 每次都抛 TargetException: Non-static method requires a target., 被 RepositionAllGhosts 自己的 catch 吞成一条 LogError。 该行在 for 循环之前,所以补摆一次都没进入过循环。 真机日志:补摆幽灵坐标失败(…):Non-static method requires a target. × 54, 而 [补摆 …] 成功自证行 0。
  • 为什么 1.2.1 的自检没拦住:当时新增的 RequireArity 只核对参数个数 (get_Columns 是 0 参 ✓),没核对静态性 ⇒ 句柄解析通过、启动日志照样打 「已生效:…并当场补摆全部网格格位」。这与 1.2.0 那次(4 参)是同一个 「解析得到 ≠ 调用得动」失效模式的第二次出现。
  • 修正:
    1. 解析期从静态字段 ModContext.config 取 ModConfig 实例(新增句柄 _modConfigInstanceField), 调用期改为 _columnsGetter.Invoke(modConfigInstance, null);
    2. RequireArity 升级为 参数个数 + 静态性双检(新增 hasInstance 形参): 实例成员拿不到实例时,在解析阶段就停用补摆并打出明确日志,不再等到运行时被吞掉;
    3. 不再尝试 GhostGrid.Columns —— 上游 GhostGrid 根本没有该成员,而 HarmonyX 的 AccessTools.Property 找不到时会打一条 Could not find property … GhostGrid and name Columns 的 Warning,与「补摆降级」混淆,纯属噪音(真机日志行 242 就是它)。
  • IL 复核(asm_ildump.py):_columnsGetter 的 Invoke 前已是 ldloc.0(实例)而不再是 ldnull; 同类静态句柄(_maxActiveGhostsGetter)则仍是 ldnull —— 形成对照。
  • 适配确认(官方 2.2.2 DLL metadata):ModConfig.get_Columns / get_Rows = mdStatic=False(实例); 而 GhostGrid 的 GetGhostPosition / GetGhostObject / GetGhostCache / UpdateGhostTransform / get_MaxActiveGhosts / _gridVersion 全部 static=True。
  • 顺带订正文档:README 的伪代码片段此前仍写着 1.2.0 的错误调用 (GetGhostPosition(go, slot % columns, slot / columns, slot) —— 4 个实参,且 row/col 顺序颠倒), 已改为与实现一致的 3 参 GetGhostPosition(slot / columns, slot % columns, slot)。
  • DLL 重新编译:15,360 字节,sha256 0c8bc0bfe52d51daee245f18935c0a739317d18c5e2b6322a2c76380ccfd7340。

连读三代:1.2.0 跑不了(4 参)→ 1.2.1 还是跑不了(实例当 null 调)→ 1.2.2 才真正跑起来。 两次根因同类,都是"反射句柄解析成功 ≠ 调用得动"。

1.2.1

修 bug:补丁 3「大网格全量补摆」自 1.2.0 起从未真正生效。

  • 根因:GhostGrid.GetGhostPosition 的签名是 3 个参数 (int row, int column, int index),而 1.2.0 按 (go, col, row, slot) 传了 4 个实参 ⇒ 每次调用抛 TargetParameterCountException,被 RepositionAllGhosts 自己的 catch 吞成一条 LogError ⇒ 补摆一次都没成功过。 而启动自检照样打印「已生效:…并当场补摆全部网格格位」⇒ 极具误导性。
  • 修正:调用改回 3 参 (row, col, slot)(row = slot / Columns、col = slot % Columns, 与上游 Update 的 div/rem 一致)。行为与 1.2.0 的设计意图完全相同,只是这次真的会跑。
  • 防复发:新增 RequireArity —— 解析阶段逐个核对每个反射句柄的参数个数 (GetGhostPosition 3、UpdateGhostTransform 2、GetGhostObject/GetGhostCache 1、 MaxActiveGhosts/Columns 0),不符即停用补摆并打出「上游给 N 个、本补丁按 M 个调用」的明确日志, 不再静默。
  • 可自证:首次成功补摆时无条件打一行 [补摆 …] 已摆正 N 个幽灵、同步 N 个坐标缓存 (不再依赖 [General] VerboseLog)—— 1.2.0 的失效正是藏在静默里,这次让它一开机就看得出来。
  • 适配确认:对 Advize-PlantEasily 2.2.1 官方 DLL 做了 metadata 核对 —— GetGhostPosition = 3 参、UpdateGhostTransform = 2 参、GhostCache 字段 piece / plant / lastUpdatedVersion 均在;2.2.0 与 2.2.1 的这些签名完全相同 (⇒ 1.2.0 的 bug 与版本升级无关,是从写下来那天就有的)。
  • DLL 重新编译:14,336 字节,sha256 9ff095b1dead21af54fff1ad21719ee9cabc319e289a1c440389d9a2318c8da9。
  • 编译后 IL 复核(asm_ildump.py):newarr System.Object 前的 ldc.i4.3 已从 4 变 3, 且三个实参装载顺序为 ldloc row → ldloc col → ldloc slot,与上游 GetGhostPosition(int row, int column, int index) 逐一对应。

一处方法学更正

1.2.0 的"离线验证"(verify_reposition_logic.py 等)用 Python 重算了坐标算法、 枚举了 BuildGrid/Update 的帧内时序,但从没做过真实反射调用 ⇒ 正好落在「离线模拟只能验证拿到输入之后的分支,输入从哪来必须真机验」的盲区里。 本次改为直接对官方 DLL 读 metadata 核对参数个数。

⚠️ 1.2.2 补注:本版声称的「只是这次真的会跑」并未成立 —— columns 那行(_columnsGetter.Invoke(null, null))在 for 循环之前就抛了 TargetException。 也就是说 1.2.1 修对了参数个数,但补摆依然一次都没跑起来。详见 1.2.2 条目。

1.2.0

新增第三条机制:大网格幽灵「全量补摆」—— 修复网格调大后仍有零星几株作物种到一起。 DLL 重新编译:13824 字节。

  • 起因:用户反馈「小范围种植没问题,范围调大后还是会有一些种到一起」。
  • 为什么补丁 1(BuildGrid 后 _gridVersion++)不够 —— 它保证的是 "下一帧会被摆正",而不是"点下去的时候已经摆正了"。查下来有两重缺口:
    • 缺口一(帧内时序,小网格也会中招):Player.SetupPlacementGhost 重建网格(BuildGrid) 与每帧的 GhostGrid.Update 在同一帧内的先后顺序不固定。若 Update 先跑, 它摆好的坐标随后被 BuildGrid → InitializeGhosts 整体覆盖回主幽灵位置, 而本帧再无第二次 Update(BuildGrid 的唯一调用点是 PlayerSetupPlacementGhost.Postfix 0052)。
    • 缺口二(分批轮转,大网格才中招):GhostGrid.Update 每帧只摆 B = min(GhostUpdateBatchSize, 总格数) 个,且从 _ghostUpdateIndex(只 +1、永不复位) 开始轮转。本机 GhostUpdateBatchSize=50:4×4(16) 与 1×20(20) 一帧摆完, 10×20(200) 首帧漏 150 个、20×20(400) 首帧漏 350 个。
    • 叠加条件:_gridVersion 只在位移 > 1cm / 朝向 > 1° / 有效朝向 > 0.1° / Rows·Columns 变化时才自增 ⇒ 鼠标一停,版本号冻结 ⇒ UpdateGhost 开头的脏检查把没轮到的幽灵全部早退挡掉 ⇒ 它们永久停在主幽灵位置。
    • 按下种植时 GetGhostPosition(0, 0) 对它们全返回同一个 BasePosition, 而 PlayerPlacePiece.Postfix 只按 for (i = 0; i < MaxActiveGhosts; i++) 逐槽收集 (无坐标去重)⇒ 这几株全部种在同一点。
  • 修复:BuildGrid 之后不再依赖后续帧 —— 当场把 [1, MaxActiveGhosts] 全部补摆: GetGhostObject(slot) → GetGhostPosition(go, slot % Columns, slot / Columns, slot) → UpdateGhostTransform(go, pos) → GetGhostCache(go).lastUpdatedVersion = _gridVersion。
    • 方位分解 slot / Columns、slot % Columns 与原版 Update 里那两行 div/rem 完全一致 (已核对编译后 IL:两条指令确实都在)。
    • ✅ 只写坐标与缓存版本号,不调 UpdateGhostStatus ⇒ 不介入任何合法性判定。
    • ✅ 不推高 GhostUpdateBatchSize ⇒ 只在 BuildGrid 那一刻做一次全量, 之后每帧仍是原版分批节奏,不吃性能(这是本方案优于"抬批次"之处)。
  • 自证日志([General] VerboseLog = true 时):
    [补摆 3] 网格 20 格(MaxActiveGhosts=19, Columns=20)→ 已摆正 19 个幽灵、
             同步 19 个坐标缓存;累计补摆 3 次 / 57 个。
    
    已摆正 N 个 应等于 MaxActiveGhosts。
  • 反射句柄缺失时自动降级:新增的 8 个反射目标(GetGhostPosition / UpdateGhostTransform / GetGhostObject / GetGhostCache / lastUpdatedVersion / ExtraGhosts / MaxActiveGhosts / Columns)缺任何一个都只关闭补摆 (各打一条 补摆降级:… 警告),补丁 1 / 2 照常工作。

离线验证

  • verify_plantEasilyFix_1_2_0.py:DLL 自证 全部命中 PASS —— 三条机制的方法名与目标成员、补摆链路的 8 个反射目标、版本号与配置键, 并确认 RepositionAllGhosts 的 IL 里 div / rem 指令确实存在。
  • verify_reposition_logic.py:9 种网格尺寸(含 20×20)下 补摆后每个格位坐标唯一、方位分解与原版 div/rem 逐槽一致 ✅。
  • verify_reposition_order.py:枚举 BuildGrid / Update 的帧内先后顺序, 确认两重缺口都真实存在、且补摆对两种顺序都成立 ✅。

一处诊断更正(记录在案)

初版诊断把本机 1×20 归因于"分批轮转漏掉 30 个"——这是错的: 1×20 总格数只有 20,batch=50 一帧就能扫完全部,分批不构成缺口。 真正的缺口是上面的缺口一(帧内时序);分批轮转只在 10×20 及以上的大网格上 作为第二重缺口叠加进来。补摆方案对两者都覆盖,故结论与实现不变,仅归因更正。

1.1.2

仅文档 —— DLL 仍与 1.1.0 逐字节一致(10752 字节,sha256 c1700599807e4087…)。

  • 把「兼容性补丁」这一新定位推到线上:1.1.1 只带了适用性声明,正文仍按"修 bug"叙述; 本版把正文一并改掉。
  • README 正文重写:章节标题从「症状 / 根因 / 修复方式」改为中性的 「默认行为 / 实现位置 / 归位方式」;明确写出 「只装 BepInExPack_Valheim + PlantEasily 时该症状不复现」;撤掉全部归因措辞; 开头的概述改成"把几个模组共用的那两处运行时状态归位"。
  • manifest.json 描述改为 [AI-generated] Compatibility patch for Advize-PlantEasily: keeps grid planting and auto-replant correct when other mods operate on the same placement piece; auto-replant also grows back the crop you harvested. Read the applicability notice first.
  • 源码 src/PlantEasilyFixPlugin.cs 的类级 XML 注释同步为同一口径 (注释不进 IL,故仍不重新编译)。

1.1.1

仅文档 —— DLL 一字未改。

  • README 顶部新增中英双语「适用性声明 / Applicability Notice」:本补丁是在本机 (BepInEx 5.4.2350 / 游戏 1.0.15 / 30 个启用模组)那套环境下做出来的,所修症状 有可能是模组组合互相冲突造成的,并不一定是原模组本身的 bug,可能不适用于他人的环境; 如遇问题请先向原模组作者反馈。
  • manifest.json 的 description 末尾加一句 Read the applicability notice first.; README 表格里的「本补丁版本」同步更新为 1.1.1。
  • 正文定位整体改为「兼容性补丁」:确认「只装 BepInExPack + PlantEasily 时该症状不复现」之后, README 里原先按"原模组自身的 bug"叙述的措辞全部撤掉,统一表述为 "把几个模组共用的运行时状态归位",且不指名任何具体模组; manifest.json 的描述同步改为 Compatibility patch ...; 章节标题也从「修复方式 / 症状」改成中性的「归位方式 / 默认行为」。
  • 源码 src/PlantEasilyFixPlugin.cs 的类级 XML 注释同步改为同一口径 (注释不进 IL,所以不影响产物,也就没有重新编译)。
  • 补丁本体未重新编译:PlantEasilyFix.dll 10752 字节, sha256 c1700599807e408753f766a17868199ce1bf8632413947bad33db763478ce05b —— 与 1.1.0 完全一致,已装 1.1.0 的不需要换文件。

1.1.0

新增「按作物补种」修正 —— 自动补种不再统一成同一种作物。

  • 新增 Harmony postfix:Advize_PlantEasily.PlacementState.SetReferences → 把静态字段 LastSelectedPlantName 置回 null。
  • 效果:开启 [Harvesting]ReplantOnHarvest 后,每一种被收割的作物都补种回它自己, 而不是统一补成"你最近用耕耘机选中的那一种"。混种地块不再越收越单一。
  • 该字段只有两处读取(补种判定 + 悬停提示),且都有空值分支 —— 置空无副作用; 悬停提示 Replant as: X 也会同步显示"实际会补种的那种",可直接在游戏里核对。
  • 新增配置项:[Replant] SameCropReplant = true。关掉即恢复 PlantEasily 原行为, 改动无需重启游戏,下一次补种即时生效。
  • 新增模组图标 icon.png(两株高矮不同的作物 + 补丁对勾)。

1.0.0

  • 首个版本:修复 PlantEasily 网格种植(Rows/Columns ≥ 2)时,整片作物 (如 4×4 = 16 株)全部叠在同一个点上的 bug。
  • 实现:Harmony postfix Advize_PlantEasily.GhostGrid.BuildGrid → _gridVersion++, 让所有幽灵的坐标缓存失效,下一帧必定重算。