不修改 exe,恢复 PCL2 的 Minecraft 下载入口

一次PCL2的修复记录

一、前言

PCL2 是很多 Minecraft 玩家熟悉的启动器。它启动快、界面清爽,整合包、Mod、光影资源的下载也一直很方便。但在 2.99 版本之后,不少人和我一样发现:下载页里找不到“原版游戏”了,Minecraft 区域只剩下一句提示——

为遵守相关法律法规,PCL 不再提供 MC 下载功能,非常抱歉 qwq……

我去网上查找为什么会变成这样子的时候发现有人把Minecraft的圈钱服务器举报了也顺便把PCL2举报了,我以为只有PCL2不能下载了,可我发现其他启动器也有被关掉下载的渠道...

二、解决问题

1. 为什么不能直接改 exe

最直接的想法是静态修改 IL:把 LoaderInit 的方法体重写,把左侧入口的 Collapsed 改成 Visible,再保存回 exe。

但这个方案很快被否掉了。

静态改 IL 不只是改几个字节。至少要处理:

  • 方法体的重新计算和写入;
  • 元数据 token 的修复;
  • 异常表的调整;
  • 强名称、资源、BAML 等潜在影响;
  • PCL 更新后偏移和方法结构全部变化。

更重要的是,PCL 是一个会自动更新的启动器。今天按 2.99.0.1 的偏移改好,明天换个版本就可能全部失效。对普通用户来说,改坏 exe 的代价也太高。

所以我最后选择了运行时注入:不碰 exe,在 PCL 启动后从内存里修补目标方法。

2. 用 AppDomainManager 找到注入点

.NET Framework 允许通过应用程序配置文件注册 AppDomainManager。在 Plain Craft Launcher 2.exe.config 里加上:

1
2
3
4
5
6
7
<runtime>
<AppContextSwitchOverrides
value="Switch.System.IO.UseLegacyPathHandling=false;Switch.System.IO.BlockLongPaths=false" />
<appDomainManagerAssembly
value="PclRuntime, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<appDomainManagerType value="PclRuntime.PclRuntimeManager" />
</runtime>

CLR 会在 PCL 的 Main 执行之前创建这个 AppDomainManager。我在这时启动一个后台线程,等 PCL 的入口程序集和 WPF 环境准备得差不多了,再用 Harmony 给目标方法打补丁。

这里有一个细节:只加 appDomainManager* 会让 CLR 退回旧式路径处理,PCL 内部使用的 \\?\ 长路径会失效,启动时可能直接报“没有对当前文件夹的权限”。所以配置里还要同时关掉 UseLegacyPathHandling 和 BlockLongPaths。

3. 第一个补丁:把加载器换回来

第一个目标是 PCL.PageDownloadInstall.LoaderInit。

思路不复杂:在这个方法的 IL 里找到 ldstr "关闭下载",它后面跟着的是那个会抛异常的假 LoaderTask。把从这条字符串开始、到下一个 ldarg.0 之前的整段指令替换成:

1
ldsfld PCL.ModDownload::DlClientListLoader

这样,PageLoaderInit 收到的就是真正的版本列表加载器,而不是一个一执行就抛异常的替身。

用 Harmony transpiler 表达,大概是这样:

1
2
3
4
5
6
7
8
9
10
int start = list.FindIndex(i =>
i.opcode == OpCodes.Ldstr && (string)i.operand == "关闭下载");

int end = list.FindIndex(start + 1, i => i.opcode == OpCodes.Ldarg_0);

var loader = AccessTools.Field("PCL.ModDownload:DlClientListLoader");
var replacement = new CodeInstruction(OpCodes.Ldsfld, loader);

list.RemoveRange(start, end - start);
list.Insert(start, replacement);

当然,实际代码还要处理标签、异常块和指令边界,但核心就是“找到假加载器,换成真加载器”。

4. 第二个补丁:恢复左侧入口

第二个目标是 PCL.PageDownloadLeft..ctor。

需要改三处:

  • 把两处 ldc.i4.2(Visibility.Collapsed)改成 ldc.i4.0(Visibility.Visible);
  • 把 PageID 的初始值从 DownloadMod 改成 DownloadInstall;
  • 把 ItemMod.Checked = true 改成 ItemInstall.Checked = true。

这样,下载页打开时左侧会重新出现“原版游戏”,并且默认停在这个页面。

5. 补丁成功,按钮却没出现

这是整个过程中最有意思、也最容易被忽略的一个坑。

第一次运行时,日志明确显示两个方法都 patch 成功了:

1
2
patched PCL.PageDownloadInstall.LoaderInit
patched PCL.PageDownloadLeft..ctor

但打开 PCL,左侧还是没有“原版游戏”。

原因不在补丁本身,而在对象的生命周期:PCL 在 Harmony 生效之前,就已经创建了 PageDownloadLeft 的实例。构造函数补丁只对“以后创建的对象”生效,救不了那个已经存在的实例。它的 ItemInstall.Visibility 仍然是 Collapsed。

解决方法是加一层“活动实例修复”:在 PCL 启动后,通过 WPF 的 Dispatcher 找到 ModMain.FrmDownloadLeft,把 ItemInstall 和 LabResource 强制设为 Visible。日志里会多出一行:

1
sidebar repaired: ItemInstall Visible

这件事给我的提醒是:补丁成功,不等于目标对象被修复。 做运行时修改时,除了方法有没有被替换,还要关心对象是什么时候创建的、当前实例在哪里。

6. 把补丁做成工具

补丁跑通之后,我开始把它做成普通用户能用的工具。

工具是一个 WinForms 单文件程序,内部嵌入了 PclRuntime.dll 和 0Harmony.dll。它的工作流程是:

  1. 找到 PCL 目录,校验 exe 的版本和 SHA-256;
  2. 备份原来的 .exe.config;
  3. 写入注入配置;
  4. 释放 PclRuntime.dll 和 0Harmony.dll;
  5. 启动 PCL 后由运行时补丁接管。

卸载时反向操作:恢复配置备份,删除两个 DLL 和标记文件。整个过程中,Plain Craft Launcher 2.exe 本体不被修改。

后来我又把界面重新做了一版:去掉多余说明和日志框,只保留路径、状态和三个按钮;日志改为关闭工具时写入工具目录下的 日志.txt。这样界面更干净,排查问题时也仍然有记录可看。

7. 验证

修复后的日志大致是这样:

1
2
3
4
injected, base=...
patched PCL.PageDownloadInstall.LoaderInit token=...
patched PCL.PageDownloadLeft..ctor token=...
sidebar repaired: ItemInstall Visible

打开 PCL,下载页左侧出现了“原版游戏”,右侧能看到最新版本、正式版、预览版、远古版和愚人节版。选择版本后可以进入安装页,Forge、NeoForge、Fabric、OptiFine 等选项也都还在。

我还在测试副本里实际点击了 26.3 的“开始下载”,确认它开始拉取原版 libraries 和 assets,而不是只把 UI 显示出来。测试完成后我取消了下载并清理了测试目录。

最后核对了一下 exe 的哈希,确认没有变化:

1
0DB34E19D9EEB6D496D20FABE786EC16BBD36BA6B0CCDC8D29CB7E909C114397

这也是运行时补丁相对静态修改的一个好处:PCL 本体仍然是原来的文件,卸载时只需要删掉配置和两个 DLL。

四、结尾

这个工具几乎都是AI完成的,如果有什么BUG可以到评论区反馈

祝PCL和龙猫越来越好

下载源码及软件密码:PCL2