Unity游戏逆向与mod开发:与你共享Lo-Fi故事(1)

2383 字
12 分钟
Unity游戏逆向与mod开发:与你共享Lo-Fi故事(1)

前言#

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

steamwebhelper_5QFVfBDr7h.jpg
steamwebhelper_5QFVfBDr7h.jpg

基本上就是一个番茄钟+音乐+ToDo List的游戏&工具 顺便放了一个少女陪你干活

做的还是很精致的 我也很喜欢

Mod已经写完了 是一个同步音乐信息到游戏的mod 感兴趣的可以下下来看看

ChillMusicInformationSyncMod

游戏只能播放内置的和自己添加的音乐,我干活喜欢听自己喜欢的

但我并不大可能把音乐全下下来,况且现在全都是加密格式(说你呢网易云ncm)

Todo的话我用的是Microsoft Todo 我希望他能同步到游戏里 甚至在游戏里去增加Todo事项

也算借助微软全平台同步了(

上次用的MelonLoader,我看到鉴赏下面已经有人写了个同步时间的插件,基于BepInEX的

所以我打算用相同的BepInEX版本去写一个插件,实现上述功能

计划#

首先的话我不打算让游戏直接读取wav之类的,那我还不如直接用播放器放然后把游戏静音了

况且这样容易分散注意力

所以我想做的只需要读取Windows多媒体的API(SMTC) 获得歌曲名字 显示在游戏里

然后可以用游戏自带的上一曲下一曲 以及调节音量就可以

这样的话通用性也强,有时候Apple Music没歌我会跑去网易云 也支持浏览器放的歌

Todo的话就先放Todo列表( 我还没研究Microsoft Todo有没有API去读取之类的

游戏的逆向#

音乐播放器#

逆向游戏#

点开游戏文件夹发现是mono封装 好事

丢进dnSpy里

游戏体量不是很大 类名命名也很规范

游戏的主逻辑都在Bulbul这个命名空间里

音乐相关的基本都在Music开头这里

dnSpy_pmvvyRy20E.png
dnSpy_pmvvyRy20E.png

然后找到了这里

dnSpy_noudfUIA3j.png
dnSpy_noudfUIA3j.png

后面发现并不是这个(

我们可以用Unity Explore去找

这里直接说结论了,游戏使用TextMeshProGUI去渲染字体

歌名位于名为MusicTitleText的GameObject中

打开dnSpy发现父对象MusicUI不位于任何命名空间中 被坑了

可以发现歌名就是在这里被赋值

Discord_N41EQ9UOta.png
Discord_N41EQ9UOta.png

但是其实有个比较蛋疼的问题 就是Unity游戏怎么调用SMTC呢

SMTC读取#

一开始是想直接调用Windows API 但马上发现不大现实

这不就成大便了 你一个游戏插件引用几个Windows库是要干嘛 而且兼容性也会彻底变成一坨屎

而且Unity Mono不能加载winmd

GPT给我的方案是纯COM读取 这也是狗屎 我上一曲下一曲岂不是都得写一个方法 我还要处理不同的数据类型类型和异步 这也是噩梦

我啪的一下找到了这个库

chrome_5qEAw7Vfzw.png
chrome_5qEAw7Vfzw.png

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

吗?

devenv_P0a0dSrAwp.png
devenv_P0a0dSrAwp.png

我们想监听事件更新还是逃不过Windows Runtime的API

那咋办

找了一下Unity文档发现想用WinRT API只能在UWP下使用

(挠头

思考了一个小时我算是明白为啥游戏作者没有这个功能了(

第二天又折腾了一个早上,基本上逃不过这个问题

Discord_IE61hcbfza.png
Discord_IE61hcbfza.png

基本上的话Windows.Foundation.UniversalApiContract这个合约是在Unity上UWP平台才能有 Unity Standalone是不能调用的

靠北啊

C++中间层的编写#

有句经典名言,没有什么是不能加一个中间层解决的

我们可以在Native层写一个C++的dll,直接让C++调用WinRT API,这不需要.NET运行时参与

如果用C#那就要直接处理COM口 还要解决异步的问题 那还是睡觉吧

也就不会有兼容问题(大概)Mono也不会因为我引入了WinRT来跟我爆了

这里完全触及我的盲区 我一直都不想学没有GC机制的语言 而且Win API调用更是一坨

搞了半天不仅要手搓轮子还要换个语言搓

还好有Ai 几个小时就完事了 已经放到Github了

SMTC-Bridge-Cpp

然后用P/Invoke导入这个dll里面的方法

Chill_With_You_iAfHeBSVtX.png
Chill_With_You_iAfHeBSVtX.png

终于是解决在Mono里读取SMTC的问题了 接下来就是逆向和Patch了

2025.12.26更新:一坨狗屎#

解决上下一曲之后我想解决音量调整和循环随机按钮

这时候Windows的傻逼生态就显现出来了

首先是进程名字:SMTC不支持调音量,但我们可以用音量合成器单独调一个进程的音量,只要我们知道他的进程名字

正常x86的进程注册SMTC Session的时候就是neteastmusic.exe

他的进程名字也是neteastmusic.exe

UWP的就牛逼了 是AppleInc.AppleMusicWin_nzyj5cx40ttqa!App

结果他的进程名字是AppleMusic.exe

那我怎么匹配

SMTC对于播放控制的逻辑是随机播放(Shuffle Mode)为开启或者关闭

然后随机播放是 None Track List三个模式

然而AppleMusic是不支持SMTC控制播放模式的,尽管他的实现是符合SMTC逻辑的

然后网易云音乐装了BetterNCM之后支持随机播放控制,尽管他把随机列表单曲心动模式塞进了一个按钮里

然后网易云设计也是人才 播放器前后端分离,实际播放音乐的是C++后端,音量播放器不控制

写了半天发现发现根本没必要做 做了也没法用 放弃

游戏歌曲名#

根据我们上面得出来的东西去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;
}
}
}

Discord_0PiIX1wtRP.png
Discord_0PiIX1wtRP.png

但这样不大完美 他需要在游戏里手动点一下上一曲下一曲才可以替换歌名

所以我们还要获取游戏的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就好,这里不过多赘述了

接下来基本就是写代码

基本上都是对着FacilityMusic这个类Patch,也不过多赘述了 毕竟Mono封装和看源码也差不多

ToDo List同步#

已放进Todo列表中()

一些踩雷#

过程中还踩了些雷 一个是获取歌曲信息cpp库的线程

Mutex锁问题#

QQ_CfAAtjaldu.png
QQ_CfAAtjaldu.png

在实际测试的过程中我遇到过很多这个报错

因为在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函数一直不跑我都纳闷死了

实例的查找#

后续发现其实根本不需要创建GameObject去使用Update函数

因为是事件驱动+异步获取的,所以FindObjectOfType这个操作我们可以用Harmony去实现

也就是Patch游戏实例化代码的地方让他顺便把实例传给我们的函数就好了

[HarmonyPatch(typeof(FacilityMusic), "Setup")]
public static class MusicUISetupPatch
{
private static readonly FieldInfo MusicUIField = AccessTools.Field(typeof(FacilityMusic), "_musicUI");
[HarmonyPostfix]
public static void Postfix(FacilityMusic __instance)
{
// 检查 MusicUISync 是否已经捕获了实例
if (!MusicUISync.isMusicUISynced())
{
// 确保只执行一次初始同步逻辑
MusicUI musicUIInstance = MusicUIField?.GetValue(__instance) as MusicUI;
MusicUISync.GetInstance(__instance , musicUIInstance);
MusicUIHider.GetInstance(musicUIInstance);
}
}
}

我后续是同时需要FacilityMusic和MusicUI这两个实例,一次Patch直接拿到两个实例 刚刚好

其实用实例的话我们就不用Patch音乐名的部分了,手动调用Onchangemusic去更新也可以

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Unity游戏逆向与mod开发:与你共享Lo-Fi故事(1)
https://cainongw.github.io/posts/beplnex-develop01/
作者
Cainong
发布于
2025-12-04
许可协议
CC BY-NC-SA 4.0
相关文章 智能推荐
1
Unity游戏逆向与mod开发:与你共享Lo-Fi故事(2) Todo同步
逆向 前言 我大概是写完了那个同步SMTC的mod,接下来就是用DOTween加动效还有一些细节修改就好了 那么我要实现我第二个目标了,就是Todo的同步
2
记一次Chunithm的逆向
逆向 前言 之前写过一篇关于中二的逆向,但是没什么成果,加上我确实没什么逆向的能力,所以删掉重新研究了一下 这次目标依然是实现AutoPlay,得益于Agent的发展,现在我们可以让Agent直接去访问IDA MCP来逆向,我们就不用啃反编译出来的狗屎了
3
记一次某八按键音游的逆向
逆向 前言 兄弟炒币赚了大钱,买了ADX放家里 由于兄弟几个都很变态(当然不包括我),就想到是不是可以买个郊狼回来玩玩 当然不是电那种奇怪的地方(),只是电大腿之类的 我的提议就是,Miss一次电一次 开始 逆向基于的版本是DX 1.56 也就是Prism Plus
4
VoiceMeeter的逆向与破解
逆向 前言 如果您看到了这,您需要知道的: 我不是专业的逆向人员,一定会有错误,遗漏的地方,您可以联系我指正或交流 其次,这不是教程,您需要有一定的逆向基础 疫情期间,由于 每天上课摸鱼 ,大部分时间都和朋友在语音 我便需要一些整活神器,来在与语音里 爆破我的朋友 我想起几年前发现的神器——VoiceMeter
5
记一次从 Hexo Blog 到 Astro Firefly Blog 的迁移
前端 Hexo 目前还是不怎么够我用,一个是 JavaScript 并不是很好写(相较于Astro 而言。一个就是 Hexo 的主题和生态确实远远比不上新的基于 Astro Svelte这些前端框架,因此我决定迁移
随机文章 随机推荐
Profile Image of the Author
Cainong
Caiw there 👋
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
站点统计
文章
38
分类
16
标签
48
总字数
57,650
运行时长
0
最后活动
0 天前

文章目录