侧边栏壁纸
博主头像
水果不是水果 博主等级

日复一日,明日复明日。

  • 累计撰写 12 篇文章
  • 累计创建 4 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

L4D2_Lobby数据结构完整分析

水果不是水果
2026-01-12 / 0 评论 / 1 点赞 / 3 阅读 / 0 字

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 数据格式

转换规则

  1. 简单值(叶子节点):

    • 如果 KeyValues 节点有值(GetDataType() != 0),直接调用 SteamMatchmaking::SetLobbyData()
    • 键名格式:使用传入的前缀参数(如 "Members:numSlots"
    • 值格式:通过 PrintValue() 函数格式化为字符串
  2. 嵌套结构(父节点):

    • 如果 KeyValues 节点没有值,遍历所有子节点
    • 键名格式:"父键:子键"(如 "System:network", "Members:machine0:id"
    • 递归处理每个子节点
  3. 特殊处理 - 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:networkSystem:access 等键,Steam 可以:

  • 区分在线/离线服务器
  • 控制服务器的可见性
  • 管理服务器的访问权限

3. 兼容性检查

通过 Members:machine0:dlcmaskMembers: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 数据有总大小限制

结论

  1. 数据结构: Lobby 数据以 KeyValues 树形结构组织,包含 System、Members、Options、Game、Server 等分支。

  2. 转换机制: 通过 LobbySetDataFromKeyValues() 递归转换,键名使用 "父键:子键" 格式。

  3. 关键信息:

    • 服务器网络类型和访问权限(System)
    • 玩家和机器信息(Members)
    • 服务器类型(Options)
    • DLC 和版本信息(Members/machine0)
  4. 特殊处理: System:dependentlobby 使用特殊的 SetLobbyDependentData() API。

  5. 数据用途: 用于 Steam 匹配系统,使玩家能够搜索、过滤和加入服务器。


分析日期: 2025-01-05
分析工具: IDA Pro
目标文件: matchmaking_srv.so
分析人员: AI Assistant

1

评论区