前言
看Steam家庭共享库的时候发现个这个游戏

基本上就是一个番茄钟+音乐+ToDo List的游戏&工具 顺便放了一个少女陪你干活
做的还是很精致的 我也很喜欢
但他游戏只能播放内置的和自己添加的音乐,我干活喜欢听自己喜欢的
但我并不大可能把音乐全下下来,况且现在全都是加密格式(说你呢网易云ncm)
Todo的话我用的是Microsoft Todo 我希望他能同步到游戏里 甚至在游戏里去增加Todo事项
也算借助微软全平台同步了(
上次用的MelonLoader,我看到鉴赏下面已经有人写了个同步时间的插件,基于BepInEX的
所以我打算用相同的BepInEX版本去写一个插件,实现上述功能
计划
首先的话我不打算让游戏直接读取wav之类的,那我还不如直接用播放器放然后把游戏静音了
况且这样容易分散注意力
所以我想做的只需要读取Windows多媒体的API(SMTC) 获得歌曲名字 显示在游戏里
然后可以用游戏自带的上一曲下一曲 以及调节音量就可以
这样的话通用性也强,有时候Apple Music没歌我会跑去网易云 也支持浏览器放的歌
Todo的话就先放Todo列表( 我还没研究Microsoft Todo有没有API去读取之类的
游戏的逆向
音乐播放器
逆向游戏
点开游戏文件夹发现是mono封装 好事
丢进dnSpy里
游戏体量不是很大 类名命名也很规范
游戏的主逻辑都在Bulbul这个命名空间里
音乐相关的基本都在Music开头这里

然后找到了这里

后面发现并不是这个(
我们可以用Unity Explore去找
这里直接说结论了,游戏使用TextMeshProGUI去渲染字体
歌名位于名为MusicTitleText的GameObject中
打开dnSpy发现父对象MusicUI不位于任何命名空间中 被坑了
可以发现歌名就是在这里被赋值

但是其实有个比较蛋疼的问题 就是Unity游戏怎么调用SMTC呢
SMTC读取
一开始是想直接调用Windows API 但马上发现不大现实
这不就成大便了 你一个游戏插件引用几个Windows库是要干嘛 而且兼容性也会彻底变成一坨屎
而且Unity Mono不能加载winmd
GPT给我的方案是纯COM读取 这也是狗屎 我上一曲下一曲岂不是都得写一个方法 我还要处理不同的数据类型类型和异步 这也是噩梦
我啪的一下找到了这个库

支持.NET4.6+ 刚好满足我们需要
吗?

我们想监听事件更新还是逃不过Windows Runtime的API
那咋办
找了一下Unity文档发现想用WinRT API只能在UWP下使用
(挠头
思考了一个小时我算是明白为啥游戏作者没有这个功能了(
第二天又折腾了一个早上,基本上逃不过这个问题

基本上的话Windows.Foundation.UniversalApiContract这个合约是在Unity上UWP平台才能有 Unity Standalone是不能调用的
靠北啊
C++中间层的编写
有句经典名言,没有什么是不能加一个中间层解决的
我们可以在Native层写一个C++的dll,直接让C++调用WinRT API,这不需要.NET运行时参与
如果用C#那就要直接处理COM口 还要解决异步的问题 那还是睡觉吧
也就不会有兼容问题(大概)Mono也不会因为我引入了WinRT来跟我爆了
这里完全触及我的盲区 我一直都不想学没有GC机制的语言 而且Win API调用更是一坨
搞了半天不仅要手搓轮子还要换个语言搓
还好有Ai 几个小时就完事了 已经放到Github了
然后用P/Invoke导入这个dll里面的方法

终于是解决在Mono里读取SMTC的问题了 接下来就是逆向和Patch了
游戏歌曲名
根据我们上面得出来的东西去Patch一下,核心代码如下
[HarmonyPatch(typeof(MusicUI), "OnChangeMusic", new Type[] { typeof(string), typeof(string), typeof(MusicChangeKind) })]
public static class DynamicSMTCUISyncPatch
{
[HarmonyPrefix]
public static bool Prefix(ref string musicTitle, ref string artistName)
{
if (SMTCStatus.IsPlaying)
{
musicTitle = SMTCStatus.Title;
artistName = SMTCStatus.Artist;
return false; // 替换游戏内的方法
}
else
{
// 返回 true,允许原始方法继续执行。
return true;
}
}
}

但这样不大完美 他需要在游戏里手动点一下上一曲下一曲才可以替换歌名
所以我们还要获取游戏的MusicUI实例去手动触发游戏逻辑让其更新UI
_musicUIInstance = FindObjectOfType<MusicUI>();
if (_musicUIInstance != null)
// 成功找到实例,继续准备反射方法
Type musicChangeKindType = AccessTools.TypeByName("MusicChangeKind");
if (musicChangeKindType != null)
{
_onChangeMusicMethod = AccessTools.Method(
typeof(MusicUI),
"OnChangeMusic",
new Type[] { typeof(string), typeof(string), musicChangeKindType }
);
}
以及触发游戏内的更新逻辑
private static void TriggerGameUIUpdate(bool smtcIsPlaying)
{
if (smtcIsPlaying)
{
// SMTC 正在播放 -> 游戏 UI 应该显示“暂停”图标
_musicUIInstance.OnPlayMusic();
}
else
{
// SMTC 处于暂停/停止 -> 游戏 UI 应该显示“播放”图标
_musicUIInstance.OnPauseMusic();
}
object musicChangeKindManual = Enum.Parse(AccessTools.TypeByName("MusicChangeKind"), "Manual");
object[] parameters = new object[]
{
"Placeholder Title",
"Placeholder Artist",
musicChangeKindManual
};
_onChangeMusicMethod.Invoke(_musicUIInstance, parameters);
_logger.LogDebug($"主动调用 OnChangeMusic。SMTC IsPlaying={smtcIsPlaying}");
}
游戏内的OnChangeMusic方法接受三个参数,Title Artist和ChangeKind
前面两个很显然,第三个就是改变歌曲的方式,游戏里是一个枚举,有Auto Manual和PlaylistCellClick
也就是自动切换 手动 点击列表歌曲
我们手动替换的话应该适合Manual或者Auto
别的与音乐有关的UI代码都在MusicUI类中,用UE找到并Patch就好,这里不过多赘述了
接下来基本就是
ToDo List同步
已放进Todo列表中()
一些踩雷
过程中还踩了些雷 一个是获取歌曲信息cpp库的线程
Mutex锁问题

在实际测试的过程中我遇到过很多这个报错
因为在Cpp库中我们注册了SessionMananger去管理SMTC的session,并使用了mutex锁去保护Unity线程和C++库之前的读写,确保每次Unity读到的是完整且正确的数据
在实际调用,需要Unity在OnDestry生命周期调用Shutdown函数去让C++清理这些锁
但是实际游戏mod中不正常退出是很常见的,尤其是任务管理器杀进程和游戏崩溃的时候
这个说实话我没啥办法 只能一直改一直测 我也没经验
OnUpdate函数的类
一个就是在BepInEX中,含有Update函数的组件必须继承MonoBehaviour类(而不是MelonLoader一样继承MelonMod类)
并且要创建新的GameObject并挂载
以及最重要的 hideFlags 否则Mono会销毁这个组件
private static GameObject _runner;
_runner = new GameObject("SMTCSyncRunner");
_runner.hideFlags = HideFlags.HideAndDontSave;
DontDestroyOnLoad(_runner);
_runner.SetActive(true);
//挂载组件
var behaviour = _runner.AddComponent<SMTCSyncBehaviour>();
也折腾了我几个小时 Update函数一直不跑我都纳闷死了