单机 galgame 凭什么非要我装客户端
《Love in Login》是单机的,剧情和存档都在本地。可它不让进。点开始就弹一个 Windows 对话框:「获取打开此 'sgup' 链接的应用」

系统里没有能处理这个链接的程序,于是推荐我去 Microsoft Store 找。
sgup 是韩国平台 STOVE 的自定义协议。
先看它在干什么
我没急着动手,先查这个弹窗是谁弹的。
插件目录里躺着一个 sgup_api64.dll,名字里带 sgup,第一眼就像元凶。反编译出来,它干的事是:建一块叫 SGUPSharedUserData 的共享内存,去注册表 HKCU\Software\SGUP 下读一个 WorkingDir,再从那个目录加载 Qt5Core.dll 和 sgup_local_control64.dll。整套动作都在游戏进程和已经跑起来的 STOVE 客户端之间搭桥。
它从不调用 ShellExecute,也不含 sgup:// 字样。第一眼就认错了对象。
字符串要按宽字符扫
STOVE SDK 的字符串大多是 UTF-16。我一开始用普通 strings 扫,什么都扫不到,一度以为这条路断了。换成宽字符扫之后,BaseSDK.dll 里的一句露了出来:
sgup{}://run/{}这是一条格式串。env 留空,gameId 填上,拼出来就是 sgup://run/lovein_login_secretplus,跟游戏安装清单里那条 sgup://run/...?auto_action=PrepareAndLaunch 对得上。
同一个 DLL 里还有:
Please run the game using the launcher.Launcher is not running and launchLauncher is false, so the game will not be restarted.stove_error_need_launcher
导入表里它引了 ShellExecuteW,还引了 RegOpenKeyExW 去读 HKCU\Software\SGUP。类名 IPCForLauncher 也在。
链条到这里就清楚了。SDK 启动时想连本机运行中的 STOVE 客户端,走一套命名管道加共享内存的 IPC。连不上,分两条路走:launch_launcher 为真,就 ShellExecute 打开那个 sgup:// 链接试着把客户端拉起来,客户端没装,Windows 弹框;为假,只写一行日志,不重启进程。
游戏那边还有第二道门
弹窗是第一步。就算把它按掉,STOVE 初始化仍会失败,游戏跟着自己退出。
游戏逻辑编译在托管程序集 Assembly-CSharp.dll 里,用的是 Mono。关键在这:Mono 意味着能改托管层,不必碰原生 DLL。里面有个 PlatformManager,靠场景里几个布尔量选后端。本作有 Steam 和 STOVE 两套,这个包开的是 STOVE。
STOVE 那套叫 StoveManager。它在 Awake 里调 StovePC.Initialize,然后等回调:拿用户、查所有权,查不到就 Application.Quit()。失败路径一样,初始化或取用户出错,退进程。
换成 Steam 后端也不行
看起来有条捷径:把后端标志改成 Steam。
试了,不通。代码里到处是这种分支:非 Steam 时用 gallerySpr 这套数据,Steam 时用 gallerySprSwitch。gallerySprSwitch 是 Switch 变体的资源,这个 STOVE 包里是空的。硬切过去,第一处访问 .Length 就空引用崩溃。
那就保留 STOVE 标志,让它继续用 gallerySpr,换个地方动手。
改了四个字节
STOVE 的初始化和退出,全在 StoveManager 里。我把这几个方法的开头改成返回:
| 方法 | 原字节 | 改成 |
|---|---|---|
StoveManager.Initialize | 73 | 2A |
StoveManager.QuitApplication | 73 | 2A |
StoveAchievementHandler.UnlockAchievement | 02 6F | 16 2A |
StoveAchievementHandler.SetStat | 02 16 | 16 2A |
2A 是 IL 的 ret。把它打在方法体第一个字节上,方法就空了,后面的指令成了不可达代码。动的是方法体开头那几个字节,长度没变,元数据没动,后面方法的偏移不会连锁变化,不用重新编译程序集。
四个里最要紧的是第一个。Initialize 一空,StovePC.Initialize 就不会被调用,原生 SDK 一次都不启动,也就没有那个 sgup://,没有弹窗。没有失败回调,就没有自杀。QuitApplication 一起清空,是保险。
成就那两个方法是剩下唯一会碰原生 STOVE 接口的地方。返回值没人用,让它返回 NoError 就行。
动手前我把整份反编译源码搜过:STOVE 相关的引用只在 StoveManager 和成就那两个类里。没有别的代码读 STOVE 的用户或所有权,也没有界面卡在等 STOVE 回调。
STOVE 不初始化,剧情和界面照走。
