最近在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
函数加入指定玩家的房间(如下图)
因此,只需记录玩家id,即可计算出房间代码。
PS:可通过http://steamcommunity.com/profiles/
+steamID,打开个人主页
搜索PlayerList
,找到LobbyPlayerList
和IngamePlayerList
,分别对应匹配时玩家列表
和游戏时玩家列表
。
可通过LobbyPlayerList.Instance.Players
直接获取到玩家列表。
可使用IngamePlayerList.CurrentList
的ContainedPlayers
字段获取玩家列表,由于修饰符为private
(非public
),需要通过反射获取。
查看AddPlayer
调用栈:
由此可确定以下两个函数:
RoundManager.UserCode_RpcSetInfo
(对应LobbyPlayerList
)
LobbyPlayerInfo.UserCode_RpcSetPlayerInfo
(对应IngamePlayerList
)
注意到后者传入的是数组,推测该函数是每次按下Tab
,显示玩家列表时调用,需要手动按键才能触发。
考虑到两者先后顺序(LobbyPlayerList
先于IngamePlayerList
),故选择使用LobbyPlayerList.Instance.Players
获取玩家列表。
搜索字符串游戏将在
,定位到SimplifiedChineseLocalisation.UIGameStartingIn
变量,查看其调用栈:
双击UserCode_RpcGetCountdown
函数,查看函数体:
功能是调用LobbyChat.Instance.ReceiveChat
函数,更新聊天框内容
由此,选择在LobbyPlayerInfo.UserCode_RpcGetCountdown
函数调用后(游戏即将开始时),保存房间代码及玩家列表。(保存上次调用时间,超过10秒则保存房间数据到本地)
同时,为了提示模块已经加载,选择在LobbyChat.Start
函数调用后输出提示信息。
PS:由于LobbyChat
实际上是更新一个TextMeshProUGUI
(支持富文本标签),所以可以指定字体大小、颜色等属性。(在别人房间里发送,会被服务器断开连接)
TextMesh Pro
支持的富文本标签见Rich Text
基于MonoHook,开发一个注入dll
在Visual Studio
中创建一个.Net 4.0
类库项目,将必要的游戏dll添加为依赖。
此处仅贴出关键代码,完整项目代码见github:DAGHistory
hook LobbyChat.Start
函数,在聊天界面创建后输出信息。
使用该类保存房间数据。
重载ToString
函数,使用Json序列化该对象。
遍历LobbyPlayerList.Instance.Players
,并生成房间代码,保存到RoomData
对象。
hook LobbyPlayerInfo.UserCode_RpcGetCountdown
函数,判断是否需要保存房间数据到文件。
分析发现该功能最终调用的是LobbyManager.s_Singleton.Join
函数,传入steamID
,照搬即可。
参考LogFileOpener.ReturnLogPath
函数,编写以下代码(将默认值替换为临时文件夹)。
点击左下角Logs
打开该目录:
为方便使用,增加图形操作界面,提供手动保存、复制上局房间代码、加入上局房间功能。
并设置按后引号键(esc下方)隐藏该界面。
在Visual Studio
中选择生成DAGHistory
,得到bin\Debug\DAGHistory.dll
使用SharpMonoInjector
提供的命令行注入工具SharpMonoInjector.Console
进行注入
将smi.exe
、SharpMonoInjector.dll
、待注入dll放到同一目录下,在该目录执行以下命令:
之后使用相关功能即可。
点击游戏左下角Logs
打开日志目录(Windows:C:\Users\用户名\AppData\LocalLow\Acureus\Draw_Guess
)
其中LastRoom.json
和DAGHistory.log
即注入模块生成的日志文件。
前者保存上次游玩的房间数据(用于复制代码及快速加入),后者保存历史记录。
public static LobbyPlayerList Instance;
public
List
<RectTransform> PlayerList
=
new
List
<RectTransform>();
public
List
<LobbyPlayerInfo> Players
=
new
List
<LobbyPlayerInfo>();
public static LobbyPlayerList Instance;
public
List
<RectTransform> PlayerList
=
new
List
<RectTransform>();
public
List
<LobbyPlayerInfo> Players
=
new
List
<LobbyPlayerInfo>();
private Dictionary<ulong, IngamePlayerListEntry> ContainedPlayers
=
new Dictionary<ulong, IngamePlayerListEntry>();
public static IngamePlayerList CurrentList;
private Dictionary<ulong, IngamePlayerListEntry> ContainedPlayers
=
new Dictionary<ulong, IngamePlayerListEntry>();
public static IngamePlayerList CurrentList;
public static void StartReplace()
{
/
/
先调用原函数
StartProxy();
/
/
输出信息到聊天框
LobbyChat.Instance.ReceiveChat(
"<color=#778899>[DAGHistory]: Loaded</color>"
);
}
public static void StartReplace()
{
/
/
先调用原函数
StartProxy();
/
/
输出信息到聊天框
LobbyChat.Instance.ReceiveChat(
"<color=#778899>[DAGHistory]: Loaded</color>"
);
}
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);
}
}
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
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!