CelestialCosmic / themeblog

blog articles by Celestial_Cosmic,source code by chanshiyucx
0 stars 0 forks source link

dnspy 调试新版 unity #45

Open CelestialCosmic opened 1 year ago

CelestialCosmic commented 1 year ago

dnspy 默认不具备 unity 调试功能,所以需要自己动手

下载必要资源

dnspy 32 和 64 位的都要准备,然后放到一起,我分别命名为 3264

众所周知 dnSpy/dnSpy 早已不受支持,但社区依旧在维护 dnSpyEx/dnSpy

按需取用

这里有 unity 的调试库

Releases · liesauer/Unity-debugging-dlls

下载然后再放一个文件夹,我将其命名为 unityLib

让游戏可以被 dnspy 调试

要让游戏可以调试,首先需要知道一些关键信息

信息收集

判断编辑器、mono版本和替换库

unityLib 中的 dll 是供调试用的,替换对了游戏才能正常启动

判断编辑器版本

有源码的看 ProjectSettings\ProjectVersion.txt

没有则可以通过主程序的详细信息判断,如果不是日期,那就直接是编辑器版本

如果是一个日期,那就需要多方查证了,确定它是编辑器的构建日期还是游戏的构建日期,但大体可以确认是那个时期的东西

最稳定的方法是以文本编辑器方式打开资源文件夹里面的 unity default resources 文件,看第一行给的是什么,那大概率就是那个了

判断 mono 版本

用 dnspy 打开 mono-2.0-bdwgc.dllmono.dll,通过文件的时间戳判断大致的版本

下面会提供两种方法,让 dnspy 可以调试所有版本的游戏

给 dnspy 开调试接口

替换 mono 文件

这个方法适用于 dnspy 原项目支持的 mono(2019 及之前)

unityLib 中抽取文件,替换游戏的 mono-2.0-bdwgc.dllmono.dll

替换前记得备份

dnspy 只提供了 2019 及以前的 mono,可现在都 2023 了,unity 编辑器的 LTS 都 2021 了,自己编译需要一大堆东西......还是另找出路吧

使用 UnityDebug

这个方法适用于 dnspy 原项目没有支持的 mono(2020 以后)

Sl4vP0weR/UnityDebug

根据 readme ,首先安装 .net 库,包括 .NET SDK 7+.NET Runtime + SDK 4.8 ,还有 visual studio(2022 + .net multiplatform + .net 桌面开发 + 使用 Unity 的游戏开发)

网络问题可以尝试改 host ,改 DNS......

然后选择 cpu 和系统并构建

MSB3073 命令“xcopy /Y /R /S /D /I "Doorstop\Debug\x64" "build\Debug_x64\"”已退出,代码为 4。

报这个是因为文件夹只读或者没选 cpu 和系统,去改一改

然后重新生成解决方式,重新生成

好了,弄了这么半天,终于看到了曙光

现在,把 Win_x64 (总之就是之前选择好的 cpu 和系统)里面的东西复制到游戏根目录

重复一遍,复制内容不是复制这个文件夹

调试之前

弄完了这些,现在快要进入正题了:如何去测试一个游戏?

这里以 Cuisineer Demo 为例,因为前阵子正好下了它的 demo ,没来得及玩厂商就把游戏测试下了,不过游戏还躺在硬盘里面,不用 steam 授权也能直接开

下面是可以直接收集到的信息:

mono-2.0-bdwgc.dll
时间戳: 6075201B (2021/4/13 12:37:47)

Cuisineer.exe
文件版本 2020.3.11.51119
产品版本 2020.3.11.10078127

unity default ressources
2020.3.5f1

既然是 2020 的编辑器,那就需要用刚刚构建好的项目来进行调试

准备调试

添加环境变量

DNSPY_UNITY_DBG: --debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,defer=y,no-hide-debugger
DNSPY_UNITY_DBG2: --debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,suspend=n,no-hide-debugger

写成脚本就是这样的

chcp 65001
SETX /M DNSPY_UNITY_DBG: --debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,defer=y,no-hide-debugger
SETX /M DNSPY_UNITY_DBG2: --debugger-agent=transport=dt_socket,server=y,address=127.0.0.1:55555,suspend=n,no-hide-debugger
echo 按下回车后重启 explorer 以立刻应用环境变量
pause
taskkill /im explorer.exe /f
start explorer.exe

把脚本保存,管理员权限执行就可以了

执行脚本前检查一下有没有在进行文件操作

准备连接

unityDebug 默认端口 56000

dnspy 选择 unity(连接)

连接 127.0.0.1:56000

如果位数不对,程序会报错崩溃,但如果看到底下的橙框,就说明成功了

游戏测试

unity 游戏的主要逻辑都在 Assembly-CSharp.dll

有的游戏不这么干,而是生成额外的 dll ,但记住这点就好:代码最有可能放在文件大小最大的那些文件里面(排除 unityplayer.dll 等依赖)

搜索 .dll ,然后按大小排列,不出意外前几个文件就能“中奖”

当然了,激活了 mono 功能的 cheat engine 也很重要

还要记得备份文件!

选择 mono dissect ,根据之前游玩的经验查找最有价值的类

这次 dnspy 一拆,发现函数都在 {-} 里面

卡住时间

游戏中,1 小时等于 60 秒,我希望时间慢点

找到 timemanager ,发现时间流逝的代码

update 函数,一秒执行一次

// TimeManager
// Token: 0x06000C31 RID: 3121 RVA: 0x0003B72C File Offset: 0x0003992C
private void Update()
{
    if (TransitionManager.Transitioning || this.m_CommandTimePaused || this.m_MapTimeFrozen || this.m_DayOverTransition || CutsceneManager.CutsceneActive)
    {
        return;
    }
    if (SimpleSingleton<FlagManager>.Instance.GetFlagState("TIME_PAUSE"))
    {
        return;
    }
    if (this.m_MidnightWaitingForCutscene)
    {
        if (this.CanDoMidnightTransition())
        {
            this.HandleReachMidnight();
        }
        return;
    }
    GlobalEvents.Time.TimeOfDayEvent timeUpdated_PerFrame = GlobalEvents.Time.TimeUpdated_PerFrame;
    if (timeUpdated_PerFrame != null)
    {
        timeUpdated_PerFrame(TimeManager.GlobalTimePercent);
    }
    this.m_TimeSeconds += Time.deltaTime * this.m_Timescale;
    this.m_SecondsTimer += Time.deltaTime * this.m_Timescale;
    if (this.m_SecondsTimer < 1f)
    {
        return;
    }
    this.m_SecondsTimer %= 1f;
    GlobalEvents.Time.TimeOfDayEvent timeUpdated_PerSecond = GlobalEvents.Time.TimeUpdated_PerSecond;
    if (timeUpdated_PerSecond != null)
    {
        timeUpdated_PerSecond(TimeManager.GlobalTimePercent);
    }
    if (this.m_TimeSeconds >= 1440f)
    {
        this.m_TimeSeconds = 1440f;
        if (!this.CanDoMidnightTransition())
        {
            this.m_MidnightWaitingForCutscene = true;
        }
        else
        {
            this.HandleReachMidnight();
        }
    }
    this.m_CurrentTimeHours = TimeManager.GlobalTimeHour;
    this.UpdateTimeOfDayEnum();
    this.UpdateMealTimeEnum();
}

发现了这两行:

        this.m_TimeSeconds += Time.deltaTime * this.m_Timescale;
        this.m_SecondsTimer += Time.deltaTime * this.m_Timescale;

敲断点,准备调试

游戏马上停下来了,但音乐播完了一轮才停,有趣但无关紧要

同时还知道:时间在有暂停标记时会直接返回

if (SimpleSingleton<FlagManager>.Instance.GetFlagState("TIME_PAUSE"))
        {
            return;
        }

那思路就是:通过某个按键,调用触发时间暂停标志的代码,让这个函数直接返回

如果是 public 函数的话,自己编写代码,然后用 SharpMonoInjector 通过外部 dll 注入也是可以的,但 private 的话就给直接修改这部分了

修改代码

选择 update 函数,右键编辑函数

新窗口会红很多东西

如果是 using System; 这样的代码红,把模块引入一下

左下角有一个文件夹图标,点击它,将模块所在的文件添加进去

还红了一堆类成员:m_TimeSeconds m_Timescale m_SecondsTimer m_MidnightWaitingForCutscene m_CurrentTimeHours m_CurrentTimeHours m_SecondsTimer m_MapTimeFrozen

有一些是这样的:

public SomeClass(x, y, z) 
{
    base..ctor(x, y, z);
}

改成这个形式就可以了:

public SomeClass(x, y, z) : base(x, y, z) 
{
}

random 函数错误

因为 dnspy 不知道该用 UnityEngine.RandomSystem.Random 中的哪一个。添加如下代码即可解决

UnityEngine.Random rand = new UnityEngine.Random();

唯独这个没法解决:

CS1061  'TimeManager' does not contain a definition for 'm_Timescale' and no accessible extension method 'm_Timescale' accepting a first argument of type 'TimeManager' could be found (are you missing a using directive or an assembly reference?)    main.cs 33
CS0168  The variable 'm_Timescale' is declared but never used   main.g.cs   220
// TimeManager  
// Token: 0x04000BA0 RID: 2976  
[Header("Settings")]  
[SerializeField]  
private float m_Timescale = 1f;

在类中的私有变量,又同时出现没有定义和声明了没有使用,可能是 dnspy 的问题吧......

其他的游戏还是可以修改的

就这样吧,我真是找了个大坑给自己跳