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

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.

Date uploaded 2 weeks ago
Version 1.2.2
Download link s6652289-PlantEasilyFix-1.2.2.zip
Downloads 805
Dependency string s6652289-PlantEasilyFix-1.2.2

README

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.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)

机制

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

① 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 ⇒ 只要种过一次某种作物,这个值就长期留着"那一种"。

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

  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,不用重启游戏, 下一次补种立刻恢复"统一成一种"。

补丁 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)

    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;  // 防下帧被早退
    }
}
  • 方位分解 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 —— 相等就说明整片都摆正了。 若你看到的数字小于它,说明补摆被降级了(日志里会有 补摆降级:… 的警告),把日志发我。

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


配置

首次运行会在 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 及其宿主实例 ModContext.config) 缺任何一个都只是补摆降级关闭(日志会打一条警告),补丁 1 与补丁 2 照常工作; 其中 ModConfig.Columns 是实例属性,必须配 ModContext.config 取到的实例来调用 —— 1.2.0 / 1.2.1 漏了这一步(拿 null 当 target),每次调用都抛 TargetException 被吞掉, 补摆整整两代从未跑起来(2026-09-25 真机实证:54 次调用、0 次成功)。1.2.2 修正;
  • 未安装 PlantEasily 时,本插件等 2 分钟后静默退出,不做任何事;
  • 所有反射调用都包在 try/catch 里 —— 补丁自身出问题最多是"这一次没生效", 不会让游戏崩溃;
  • PlantEasily 版本变化导致方法/字段找不到时,只记一条 Error 并放弃该条补丁,不影响游戏;
  • 三条补丁互相独立:某一条挂不上不影响其余两条。

卸载

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


备注

  • 本补丁是兼容性补丁:只把共用的运行时状态归位,不改写 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++, 让所有幽灵的坐标缓存失效,下一帧必定重算。