基于Mono注入 保存你画我猜历史房间数据
本帖最后由 XhyEax 于 2021-8-2 16:28 编辑## 概述
最近在Steam购买了你画我猜(`Draw & Guess`),游戏并不支持保存最近房间代码,以及玩家id。
由于众所周知的服务器原因(土豆做的),经常有人掉线或闪退(也可能是自己),重连后无法返回之前的房间。
于是考虑使用Mono注入技术,实现历史房间数据的保存。
## 逆向分析过程
### 文件结构分析
查看游戏目录结构,发现是使用Unity编写的Mono平台游戏,关键dll文件位于`..\steamapps\common\Draw & Guess\Draw&Guess_Data\Managed`,并且未加密。
### 房间代码生成
搜索`RoomCode`,定位到类`RoomCodeEncoder`,发现房间代码是由房主的`SteamID`进行`base36`编码生成,对应`DecimalToArbitrarySystem`函数。
`ArbitraryToDecimalSystem`函数用于将房间代码转换为`SteamID`,在`MenuClicks.EnterRoomCode`函数中被调用,通过`LobbyManager`的`Join`函数加入指定玩家的房间(如下图)
![](https://xhy-1252675344.cos.ap-beijing.myqcloud.com/imgs/dag-history-1.png)
因此,只需记录玩家id,即可计算出房间代码。
PS:可通过`http://steamcommunity.com/profiles/`+steamID,打开个人主页
### 记录玩家id
搜索`PlayerList`,找到`LobbyPlayerList`和`IngamePlayerList`,分别对应`匹配时玩家列表`和`游戏时玩家列表`。
#### 匹配时玩家列表
##### 类结构:
```cs
public static LobbyPlayerList Instance;
public List<RectTransform> PlayerList = new List<RectTransform>();
public List<LobbyPlayerInfo> Players = new List<LobbyPlayerInfo>();
```
##### AddPlayer 函数:
![](https://xhy-1252675344.cos.ap-beijing.myqcloud.com/imgs/dag-history-2.png)
可通过`LobbyPlayerList.Instance.Players`直接获取到玩家列表。
#### 游戏时玩家列表
##### 类结构:
```cs
private Dictionary<ulong, IngamePlayerListEntry> ContainedPlayers = new Dictionary<ulong, IngamePlayerListEntry>();
public static IngamePlayerList CurrentList;
```
##### AddPlayer 函数:
![](https://xhy-1252675344.cos.ap-beijing.myqcloud.com/imgs/dag-history-3.png)
可使用`IngamePlayerList.CurrentList`的`ContainedPlayers`字段获取玩家列表,由于修饰符为`private`(非`public`),需要通过反射获取。
#### 确定获取时机
查看`AddPlayer`调用栈:
![](https://xhy-1252675344.cos.ap-beijing.myqcloud.com/imgs/dag-history-4.png)
由此可确定以下两个函数:
`RoundManager.UserCode_RpcSetInfo`(对应`LobbyPlayerList`)
`LobbyPlayerInfo.UserCode_RpcSetPlayerInfo`(对应`IngamePlayerList`)
注意到后者传入的是数组,推测该函数是每次按下`Tab`,显示玩家列表时调用,需要手动按键才能触发。
考虑到两者先后顺序(`LobbyPlayerList`先于`IngamePlayerList`),故选择使用`LobbyPlayerList.Instance.Players`获取玩家列表。
#### 确定Hook函数
搜索字符串`游戏将在`,定位到`SimplifiedChineseLocalisation.UIGameStartingIn`变量,查看其调用栈:
![](https://xhy-1252675344.cos.ap-beijing.myqcloud.com/imgs/dag-history-5.png)
双击`UserCode_RpcGetCountdown`函数,查看函数体:
![](https://xhy-1252675344.cos.ap-beijing.myqcloud.com/imgs/dag-history-6.png)
功能是调用`LobbyChat.Instance.ReceiveChat`函数,更新聊天框内容
由此,选择在`LobbyPlayerInfo.UserCode_RpcGetCountdown`函数调用后(游戏即将开始时),保存房间代码及玩家列表。(保存上次调用时间,超过10秒则保存房间数据到本地)
同时,为了提示模块已经加载,选择在`LobbyChat.Start`函数调用后输出提示信息。
PS:由于`LobbyChat`实际上是更新一个`TextMeshProUGUI`(支持富文本标签),所以可以指定字体大小、颜色等属性。(在别人房间里发送,会被服务器断开连接)
`TextMesh Pro`支持的富文本标签见(http://digitalnativestudios.com/textmeshpro/docs/rich-text/)
## 注入模块开发
基于(https://github.com/Misaka-Mikoto-Tech/MonoHook),开发一个注入dll
### 创建项目并导入依赖
在`Visual Studio`中创建一个`.Net 4.0`类库项目,将必要的游戏dll添加为依赖。
### 编写注入代码
此处仅贴出关键代码,完整项目代码见github:(https://github.com/XhyEax/DAGHistory)
#### 模块加载提示
hook `LobbyChat.Start`函数,在聊天界面创建后输出信息。
```cs
public static void StartReplace()
{
// 先调用原函数
StartProxy();
// 输出信息到聊天框
LobbyChat.Instance.ReceiveChat("<color=#778899>: Loaded</color>");
}
```
![](https://xhy-1252675344.cos.ap-beijing.myqcloud.com/imgs/dag-history-7.png)
#### 自定义类RoomData
使用该类保存房间数据。
```cs
public class Player
{
ulong steamID;
string name;
public Player(string name, ulong steamID)
{
this.name = name;
this.steamID = steamID;
}
public string Name { get => name; set => name = value; }
public ulong SteamID { get => steamID; set => steamID = value; }
}
public class RoomData
{
string roomCode;
List<Player> playerList;
public string RoomCode { get => roomCode; set => roomCode = value; }
public List<Player> PlayerList { get => playerList; set => playerList = value; }
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
```
重载`ToString`函数,使用Json序列化该对象。
#### 获取房间数据
遍历`LobbyPlayerList.Instance.Players`,并生成房间代码,保存到`RoomData`对象。
```cs
public static RoomData getRoomData()
{
RoomData roomData = new RoomData();
if (LobbyPlayerList.Instance != null && LobbyPlayerList.Instance.Players != null
&& LobbyPlayerList.Instance.Players.Count != 0)
{
List<LobbyPlayerInfo> players = LobbyPlayerList.Instance.Players;
roomData.PlayerList = new List<Player>();
for (int i = 0; i < players.Count; i++)
{
LobbyPlayerInfo lobbyPlayerInfo = players;
string name = lobbyPlayerInfo.Name;
ulong steamID = lobbyPlayerInfo.SteamID.m_SteamID;
if (i == 0)
{
roomData.RoomCode = CodeEncoder.codeEncode(steamID);
}
roomData.PlayerList.Add(new Player(name, steamID));
}
}
return roomData;
}
```
#### 自动保存房间数据
hook `LobbyPlayerInfo.UserCode_RpcGetCountdown`函数,判断是否需要保存房间数据到文件。
```cs
public static void UserCode_RpcGetCountdownReplace(byte s)
{
// 调用原函数
UserCode_RpcGetCountdownProxy(s);
// 如果距离上一次调用该函数超过10秒,获取房间数据并保存到文件
if (Time.realtimeSinceStartup - lastCallTime > 10)
{
RoomUtil.saveRoomData();
LobbyChat.Instance.ReceiveChat("<color=#778899>: Saved</color>");
lastCallTime = Time.realtimeSinceStartup;
}
}
```
#### 加入房间
分析发现该功能最终调用的是`LobbyManager.s_Singleton.Join`函数,传入`steamID`,照搬即可。
```cs
public static void joinRoom(string code)
{
if (code != null)
{
LobbyManager.s_Singleton.Join(CodeEncoder.codeDecode(code).ToString());
}
}
```
#### 获取日志路径
参考`LogFileOpener.ReturnLogPath`函数,编写以下代码(将默认值替换为临时文件夹)。
```cs
private static string ReturnLogPath()
{
RuntimePlatform platform = Application.platform;
switch (platform)
{
case RuntimePlatform.OSXEditor:
case RuntimePlatform.OSXPlayer:
return "~/Library/Logs/Unity/";
case RuntimePlatform.LinuxPlayer:
case RuntimePlatform.LinuxEditor:
return Path.Combine("~/.config/unity3d", Application.companyName, "Draw_Guess");
case RuntimePlatform.WindowsPlayer:
return Path.Combine(Environment.GetEnvironmentVariable("AppData"), "..", "LocalLow",
Application.companyName, "Draw_Guess");
default:
return Path.GetTempPath();
}
}
```
点击左下角`Logs`打开该目录:
![](https://xhy-1252675344.cos.ap-beijing.myqcloud.com/imgs/dag-history-8.png)
#### GUI
为方便使用,增加图形操作界面,提供手动保存、复制上局房间代码、加入上局房间功能。
![](https://xhy-1252675344.cos.ap-beijing.myqcloud.com/imgs/dag-history-9.png)
并设置按后引号键(esc下方)隐藏该界面。
### 生成dll文件
在`Visual Studio`中选择生成`DAGHistory`,得到`bin\Debug\DAGHistory.dll`
## 测试
使用(https://github.com/warbler/SharpMonoInjector/releases/tag/v2.2)
提供的命令行注入工具`SharpMonoInjector.Console`进行注入
### 注入dll并调用Load函数
将`smi.exe`、`SharpMonoInjector.dll`、待注入dll放到同一目录下,在该目录执行以下命令:
```
.\smi.exe -p "Draw&Guess" -a "DAGHistory.dll" -n "DAGHistory" -c "Loader" -m "Load" inject
```
之后使用相关功能即可。
### 查看日志
点击游戏左下角`Logs`打开日志目录(Windows:`C:\Users\用户名\AppData\LocalLow\Acureus\Draw_Guess`)
其中`LastRoom.json`和`DAGHistory.log`即注入模块生成的日志文件。
前者保存上次游玩的房间数据(用于复制代码及快速加入),后者保存历史记录。 学习学习,语言也刚好会~ {:1_921:}直接帮官方软件修好了, 补丁大佬~ 我也是做Unity开发的。学习学习,语言也刚好会~ 学到了,受益匪浅 语言是Java的么?好厉害啊!大佬{:301_1003:}
我也想学习相关知识,菜鸟上可以学不? 直接帮官方软件修好了, 补丁大佬~ +1
大佬有破解il2cpp方式的吗 官方:来来来,你来开发游戏,我把源码给你?……什么,不用源码直接修复好了? 靓仔小黄 发表于 2021-8-3 04:18
语言是Java的么?好厉害啊!大佬
我也想学习相关知识,菜鸟上可以学不?
语言是C#,需要学习Unity相关知识,C#和Java很像 我感觉官方得把你收进去,才更好,不然浪费人才了
页:
[1]
2