L4D2 Lobby 数据结构完整分析
概述
本文档深入分析 L4D2 服务器向 Steam 发送的 Lobby 数据的完整结构和含义。这些数据通过 ISteamMatchmaking::SetLobbyData() 接口发送到 Steam,使玩家能够通过匹配系统找到并加入服务器。
数据组织结构
KeyValues 树形结构
Lobby 数据以 KeyValues 树形结构组织,通过递归方式转换为 Steam Lobby 数据格式。顶层结构名为 "Settings",包含以下主要分支:
Settings
├─ System
│ ├─ network
│ ├─ access
│ └─ dependentlobby (特殊处理)
├─ Members
│ ├─ numMachines
│ ├─ numPlayers
│ ├─ numSlots
│ └─ machine0
│ ├─ id
│ ├─ numPlayers
│ ├─ dlcmask
│ ├─ tuver
│ ├─ ping
│ └─ player0
│ ├─ xuid
│ └─ name
├─ Options
│ └─ server
├─ Game
│ └─ (游戏相关设置)
└─ Server
└─ (服务器相关设置)
数据转换机制
LobbySetDataFromKeyValues 函数
函数: CSysSessionBase::LobbySetDataFromKeyValues()
地址: 0x69d00
功能: 将 KeyValues 数据结构递归转换为 Steam Lobby 数据格式
转换规则
-
简单值(叶子节点):
- 如果 KeyValues 节点有值(
GetDataType() != 0),直接调用SteamMatchmaking::SetLobbyData() - 键名格式:使用传入的前缀参数(如
"Members:numSlots") - 值格式:通过
PrintValue()函数格式化为字符串
- 如果 KeyValues 节点有值(
-
嵌套结构(父节点):
- 如果 KeyValues 节点没有值,遍历所有子节点
- 键名格式:
"父键:子键"(如"System:network","Members:machine0:id") - 递归处理每个子节点
-
特殊处理 - System:dependentlobby:
- 如果键名为
"System:dependentlobby",使用SetLobbyDependentData()而不是SetLobbyData() - 这是 Steam API 的特殊功能,用于设置依赖的 Lobby
- 如果键名为
代码逻辑
unsigned int CSysSessionBase::LobbySetDataFromKeyValues(
KeyValues *a1, // KeyValues节点
CSysSessionBase *a2, // 会话对象
const char *a3, // 键名前缀
char a4) // 标志位
{
if (KeyValues::GetDataType(a1, 0)) {
// 简单值:直接设置
CSysSessionBase::PrintValue(a2, a1, s, 0x100u);
DevMsg("LobbySetData: '%s' = '%s'\n", a3, s);
SteamMatchmaking::SetLobbyData(
steamapicontext[4],
LobbyID_low, // Lobby ID (低32位)
LobbyID_high, // Lobby ID (高32位)
a3, // 键名
s); // 值(字符串)
} else {
// 嵌套结构:递归处理
FirstSubKey = KeyValues::GetFirstSubKey(a1);
while (FirstSubKey) {
Name = KeyValues::GetName(FirstSubKey);
// 构建键名: "父键:子键"
V_snprintf(s, 0x100u, "%s:%s", a3, Name);
// 特殊处理 dependentlobby
if (_V_stricmp(s, "System:dependentlobby")) {
// 普通键:递归处理
CSysSessionBase::LobbySetDataFromKeyValues(a2, s, FirstSubKey, a4);
} else {
// 特殊键:使用 SetLobbyDependentData
Uint64 = KeyValues::GetUint64(FirstSubKey, 0, 0LL);
SteamMatchmaking::SetLobbyDependentData(
steamapicontext[4],
LobbyID_low,
LobbyID_high,
Uint64, // 依赖的Lobby ID
...);
}
FirstSubKey = KeyValues::GetNextKey(FirstSubKey);
}
}
}
数据结构详细说明
1. System 分支
路径: Settings/System
Steam 键名前缀: "System:"
System/network
- 类型: 字符串
- Steam 键名:
"System:network" - 可能的值:
"LIVE"- 在线网络(默认)"offline"- 离线模式- 其他网络类型
- 设置位置:
CMatchSessionOnlineHost::InitializeGameSettings()(0x5a600) - 说明: 标识服务器的网络类型
System/access
- 类型: 字符串
- Steam 键名:
"System:access" - 可能的值:
"public"- 公开访问(默认)- 其他访问级别
- 设置位置:
CMatchSessionOnlineHost::InitializeGameSettings()(0x5a600) - 说明: 标识服务器的访问权限
System/dependentlobby
- 类型: 64位整数(Steam ID)
- Steam 键名:
"System:dependentlobby"(特殊处理) - 设置方法:
SteamMatchmaking::SetLobbyDependentData()(偏移 +148) - 说明: 设置依赖的 Lobby ID,用于关联多个 Lobby
2. Members 分支
路径: Settings/Members
Steam 键名前缀: "Members:"
Members/numMachines
- 类型: 整数
- Steam 键名:
"Members:numMachines" - 默认值:
1 - 设置位置:
CMatchSessionOnlineHost::InitializeGameSettings()(0x5a600) - 说明: 服务器中的机器数量(通常为1,表示单服务器)
Members/numPlayers
- 类型: 整数
- Steam 键名:
"Members:numPlayers" - 默认值:
1 - 设置位置:
CMatchSessionOnlineHost::InitializeGameSettings()(0x5a600) - 说明: 当前玩家数量
Members/numSlots
- 类型: 整数
- Steam 键名:
"Members:numSlots" - 默认值:
1 - 设置位置:
CMatchSessionOnlineHost::InitializeGameSettings()(0x5a600) - 用途:
- 用于
SteamMatchmaking::CreateLobby()的参数(最大成员数) - 用于
SteamMatchmaking::SetLobbyMemberLimit()设置成员限制
- 用于
- 说明: 服务器的最大玩家槽位数
Members/machine0
路径: Settings/Members/machine0
Steam 键名前缀: "Members:machine0:"
Members/machine0/id
- 类型: 64位整数(Steam ID)
- Steam 键名:
"Members:machine0:id" - 获取方式:
PlayerManager::GetPlayerXUID(0)- 获取第一个玩家的 Steam ID - 说明: 服务器的 Steam ID(通常是第一个玩家的 ID)
Members/machine0/numPlayers
- 类型: 整数
- Steam 键名:
"Members:machine0:numPlayers" - 默认值:
1 - 说明: 该机器上的玩家数量
Members/machine0/dlcmask
- 类型: 64位整数
- Steam 键名:
"Members:machine0:dlcmask" - 获取方式:
MatchSession_GetDlcInstalledMask() - 说明: DLC 安装掩码,标识服务器安装了哪些 DLC
Members/machine0/tuver
- 类型: 字符串
- Steam 键名:
"Members:machine0:tuver" - 获取方式:
MatchSession_GetTuInstalledString() - 说明: 更新版本字符串(Title Update version)
Members/machine0/ping
- 类型: 整数
- Steam 键名:
"Members:machine0:ping" - 默认值:
0 - 说明: 服务器延迟(ping值)
Members/machine0/player0
路径: Settings/Members/machine0/player0
Steam 键名前缀: "Members:machine0:player0:"
Members/machine0/player0/xuid
- 类型: 64位整数(XUID)
- Steam 键名:
"Members:machine0:player0:xuid" - 获取方式:
PlayerManager::GetPlayerXUID(0) - 说明: 玩家的 XUID(Xbox User ID,在 Steam 上等同于 Steam ID)
Members/machine0/player0/name
- 类型: 字符串
- Steam 键名:
"Members:machine0:player0:name" - 获取方式:
PlayerManager::GetPlayerName(0) - 说明: 玩家的名称
3. Options 分支
路径: Settings/Options
Steam 键名前缀: "Options:"
Options/server
- 类型: 字符串
- Steam 键名:
"Options:server" - 可能的值:
"official"- 官方服务器(当 network = “LIVE” 时)"listen"- 监听服务器(当 network != “LIVE” 时)
- 设置位置:
CMatchSessionOnlineHost::InitializeGameSettings()(0x5a600) - 说明: 服务器类型标识
Options/action
- 类型: 字符串(可选,可能被删除)
- Steam 键名:
"Options:action" - 处理: 如果值为空或
"create",该键会被删除 - 说明: 操作类型(创建会话时使用)
4. Game 分支
路径: Settings/Game
Steam 键名前缀: "Game:"
- 说明: 游戏相关的设置
- 具体内容: 需要进一步分析,可能包含游戏模式、地图等信息
5. Server 分支
路径: Settings/Server
Steam 键名前缀: "Server:"
- 说明: 服务器相关的设置
- 具体内容: 需要进一步分析
数据设置流程
1. 初始化阶段
函数: CMatchSessionOnlineHost::InitializeGameSettings()
地址: 0x5a600
// 1. 设置顶层结构名
KeyValues::SetName(this[2], "Settings");
// 2. 清理不需要的键(只保留 System, Game, Options, Members, Server)
// 删除其他所有键
// 3. 设置 System 分支
KeyValues::SetString("System/network", "LIVE");
KeyValues::SetString("System/access", "public");
// 4. 设置 Options 分支
String network = KeyValues::GetString("System/network", "");
if (network == "LIVE")
KeyValues::SetString("Options/server", "official");
else
KeyValues::SetString("Options/server", "listen");
// 5. 设置 Members 分支
KeyValues::SetInt("Members/numMachines", 1);
KeyValues::SetInt("Members/numPlayers", 1);
KeyValues::SetInt("Members/numSlots", 1);
// 6. 设置 machine0 信息
KeyValues::SetUint64("Members/machine0/id", PlayerManager::GetPlayerXUID(0));
KeyValues::SetInt("Members/machine0/numPlayers", 1);
KeyValues::SetUint64("Members/machine0/dlcmask", MatchSession_GetDlcInstalledMask());
KeyValues::SetString("Members/machine0/tuver", MatchSession_GetTuInstalledString());
KeyValues::SetInt("Members/machine0/ping", 0);
// 7. 设置 player0 信息
KeyValues::SetUint64("Members/machine0/player0/xuid", PlayerManager::GetPlayerXUID(0));
KeyValues::SetString("Members/machine0/player0/name", PlayerManager::GetPlayerName(0));
2. 发送到 Steam
函数: CSysSessionHost::InitSessionProperties()
地址: 0x6a2b0
// 1. 设置 Lobby 成员限制
Int numSlots = KeyValues::GetInt("Members/numSlots", 1);
SteamMatchmaking::SetLobbyMemberLimit(
steamapicontext[4],
LobbyID_low,
LobbyID_high,
numSlots);
// 2. 发送 Members 分支的所有数据
KeyValues *members = KeyValues::FindKey("Members", 0);
CSysSessionBase::LobbySetDataFromKeyValues(members, 0);
// 这会递归发送所有 Members 下的数据
函数: CSysSessionBase::LobbySetDataFromKeyValues()
地址: 0x69d00
递归遍历 KeyValues 树,将每个键值对转换为 Steam Lobby 数据:
// 示例转换:
// KeyValues: "Members/numSlots" = 4
// → Steam: SetLobbyData(..., "Members:numSlots", "4")
// KeyValues: "Members/machine0/id" = 76561198012345678
// → Steam: SetLobbyData(..., "Members:machine0:id", "76561198012345678")
// KeyValues: "System/network" = "LIVE"
// → Steam: SetLobbyData(..., "System:network", "LIVE")
数据读取机制
UnpackGameDetailsFromSteamLobbyInKey 函数
函数: UnpackGameDetailsFromSteamLobbyInKey()
地址: 0x56360
功能: 从 Steam Lobby 读取数据并填充到 KeyValues 结构
读取逻辑
unsigned int UnpackGameDetailsFromSteamLobbyInKey(
int LobbyID_low, // Lobby ID (低32位)
int LobbyID_high, // Lobby ID (高32位)
const char *prefix, // 键名前缀
KeyValues *schema) // KeyValues 结构(作为schema)
{
// 1. 遍历 schema 中的所有值节点
for (i = KeyValues::GetFirstValue(schema); i; i = KeyValues::GetNextValue(i)) {
Name = KeyValues::GetName(i);
// 构建完整的键名: "prefix" + "Name"
CFmtStrN<256>::CFmtStrN("%s%s", prefix, Name);
// 从 Steam 读取数据
v8 = SteamMatchmaking::GetLobbyData(
steamapicontext[4],
LobbyID_low,
LobbyID_high,
full_key_name,
buffer);
// 根据数据类型设置值
DataType = KeyValues::GetDataType(i, 0);
if (DataType == 2) { // 整数
v10 = strtol(v8, 0, 10);
KeyValues::SetInt(i, "", v10);
} else if (DataType == 7) { // 64位整数
sscanf(v8, "%llX", &v20);
KeyValues::SetUint64(i, "", v20);
} else if (DataType == 1) { // 字符串
KeyValues::SetString(i, "", v8);
}
}
// 2. 递归处理所有子节点
for (j = KeyValues::GetFirstTrueSubKey(schema); j; j = KeyValues::GetNextTrueSubKey(j)) {
v12 = KeyValues::GetName(j);
CFmtStrN<256>::CFmtStrN("%s%s:", prefix, v12);
UnpackGameDetailsFromSteamLobbyInKey(LobbyID_low, LobbyID_high, new_prefix, j);
}
}
数据类型映射
| KeyValues 类型 | 值 | Steam 存储格式 | 说明 |
|---|---|---|---|
TYPE_STRING |
1 | 字符串 | 直接存储为字符串 |
TYPE_INT |
2 | 字符串(十进制) | 转换为整数后存储 |
TYPE_UINT64 |
7 | 字符串(十六进制) | 转换为64位整数后存储 |
完整的 Steam Lobby 数据示例
发送到 Steam 的键值对
当服务器调用 SetLobbyData() 时,实际发送的数据如下:
"System:network" = "LIVE"
"System:access" = "public"
"Members:numMachines" = "1"
"Members:numPlayers" = "1"
"Members:numSlots" = "4"
"Members:machine0:id" = "76561198012345678"
"Members:machine0:numPlayers" = "1"
"Members:machine0:dlcmask" = "FFFFFFFFFFFFFFFF"
"Members:machine0:tuver" = "2.2.2.7"
"Members:machine0:ping" = "0"
"Members:machine0:player0:xuid" = "76561198012345678"
"Members:machine0:player0:name" = "PlayerName"
"Options:server" = "official"
特殊处理
System:dependentlobby
如果存在 System:dependentlobby 键,使用 SetLobbyDependentData() 而不是 SetLobbyData():
// 特殊处理
if (key == "System:dependentlobby") {
Uint64 dependentLobbyID = KeyValues::GetUint64(FirstSubKey, 0, 0LL);
SteamMatchmaking::SetLobbyDependentData(
steamapicontext[4],
LobbyID_low,
LobbyID_high,
dependentLobbyID, // 依赖的Lobby ID
...);
}
数据用途分析
1. 匹配搜索
玩家搜索服务器时,Steam 会返回匹配的 Lobby 数据,客户端可以读取这些数据来:
- 显示服务器信息(玩家数量、DLC、版本等)
- 过滤搜索结果
- 决定是否加入服务器
2. 服务器发现
通过 System:network、System:access 等键,Steam 可以:
- 区分在线/离线服务器
- 控制服务器的可见性
- 管理服务器的访问权限
3. 兼容性检查
通过 Members:machine0:dlcmask 和 Members:machine0:tuver:
- 检查 DLC 兼容性
- 检查版本兼容性
- 确保玩家可以加入服务器
4. 玩家信息
通过 Members:machine0:player0:* 数据:
- 显示服务器中的玩家
- 显示服务器所有者
- 用于好友加入功能
关键函数地址汇总
| 函数名 | 地址 | 功能 |
|---|---|---|
CMatchSessionOnlineHost::InitializeGameSettings |
0x5a600 |
初始化并设置 Lobby 数据结构 |
CSysSessionHost::InitSessionProperties |
0x6a2b0 |
发送 Lobby 数据到 Steam |
CSysSessionBase::LobbySetDataFromKeyValues |
0x69d00 |
递归转换 KeyValues 为 Steam 数据 |
UnpackGameDetailsFromSteamLobbyInKey |
0x56360 |
从 Steam 读取 Lobby 数据 |
Steam API 调用
SetLobbyData
- 接口:
ISteamMatchmaking(steamapicontext[4]) - 偏移: +80
- 函数签名:
SetLobbyData(HSteamMatchmaking hSteamMatchmaking, CSteamID steamIDLobby, const char *pchKey, const char *pchValue) - 用途: 设置 Lobby 的键值对数据
SetLobbyMemberLimit
- 接口:
ISteamMatchmaking(steamapicontext[4]) - 偏移: +124
- 函数签名:
SetLobbyMemberLimit(HSteamMatchmaking hSteamMatchmaking, CSteamID steamIDLobby, int cMaxMembers) - 用途: 设置 Lobby 的最大成员数
SetLobbyDependentData
- 接口:
ISteamMatchmaking(steamapicontext[4]) - 偏移: +148
- 函数签名:
SetLobbyDependentData(HSteamMatchmaking hSteamMatchmaking, CSteamID steamIDLobby, CSteamID steamIDLobbyDependent) - 用途: 设置依赖的 Lobby ID
GetLobbyData
- 接口:
ISteamMatchmaking(steamapicontext[4]) - 偏移: +76
- 函数签名:
GetLobbyData(HSteamMatchmaking hSteamMatchmaking, CSteamID steamIDLobby, const char *pchKey, char *pchValue, int cchValueBufferSize) - 用途: 从 Steam 读取 Lobby 数据
数据格式总结
键名格式
- 简单键: 直接使用键名(如
"numSlots") - 嵌套键: 使用
"父键:子键"格式(如"Members:numSlots","System:network") - 深层嵌套: 使用
"父键:子键:孙键"格式(如"Members:machine0:id")
值格式
- 字符串: 直接存储(如
"LIVE","public") - 整数: 转换为十进制字符串(如
"4","1") - 64位整数: 转换为十六进制字符串(如
"FFFFFFFFFFFFFFFF")
数据大小限制
- 键名长度: 最大 256 字节(
CFmtStrN<256>) - 值长度: Steam API 限制(通常为 255 字节)
- 总数据量: Steam Lobby 数据有总大小限制
结论
-
数据结构: Lobby 数据以 KeyValues 树形结构组织,包含 System、Members、Options、Game、Server 等分支。
-
转换机制: 通过
LobbySetDataFromKeyValues()递归转换,键名使用"父键:子键"格式。 -
关键信息:
- 服务器网络类型和访问权限(System)
- 玩家和机器信息(Members)
- 服务器类型(Options)
- DLC 和版本信息(Members/machine0)
-
特殊处理:
System:dependentlobby使用特殊的SetLobbyDependentData()API。 -
数据用途: 用于 Steam 匹配系统,使玩家能够搜索、过滤和加入服务器。
分析日期: 2025-01-05
分析工具: IDA Pro
目标文件: matchmaking_srv.so
分析人员: AI Assistant
评论区