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.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 参)是同一个 「解析得到 ≠ 调用得动」失效模式的第二次出现。 - 修正:
- 解析期从静态字段
ModContext.config取ModConfig实例(新增句柄_modConfigInstanceField), 调用期改为_columnsGetter.Invoke(modConfigInstance, null); RequireArity升级为 参数个数 + 静态性双检(新增hasInstance形参): 实例成员拿不到实例时,在解析阶段就停用补摆并打出明确日志,不再等到运行时被吞掉;- 不再尝试
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—— 解析阶段逐个核对每个反射句柄的参数个数 (GetGhostPosition3、UpdateGhostTransform2、GetGhostObject/GetGhostCache1、MaxActiveGhosts/Columns0),不符即停用补摆并打出「上游给 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.Postfix0052)。 - 缺口二(分批轮转,大网格才中招):
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.dll10752 字节, sha256c1700599807e408753f766a17868199ce1bf8632413947bad33db763478ce05b—— 与 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++, 让所有幽灵的坐标缓存失效,下一帧必定重算。
