Counter-Strike 2 Game Coordinator Protocol Reverse-Engineering

Overview

This document describes the Steam Game Coordinator (GC) protocol used by Counter-Strike 2 (app ID 730) to exchange player account data, matchmaking state, and rank information. It serves as a technical reference for connecting to the CS2 GC and extracting player level, experience points (XP), Prime status, and Premier rating from the ClientHello response.

The protocol operates within the broader Steam network architecture, where GC messages are tunneled inside Steam Client-to-Server (CM) messages via the CMsgGCClient envelope.

Verification caveats from the audit pass — read before relying on the Prime / Premier mechanics below

Two claims in this document are repeated multiple times but are not verifiable from the source repos. They came from common knowledge / community reverse-engineering, not from code:

  1. “Prime status = bit 4 (0x10) of player_xp_bonus_flagsplayer_xp_bonus_flags exists in the proto, but the node-globaloffensive README explicitly says this field “Seems to always be null (README lines 206, 458). The alternative path (read CSOEconGameAccountClient.elevated_state from SO cache type 7) is commented out in handlers.js:40-43 with the note “Account metadata - this doesn’t appear to be useful in CS:GO”. node-globaloffensive does not expose a Prime indicator for CS2 via the GC. Treat the bit-4 claim as an empirical hypothesis to confirm against a known-Prime account, not as fact.

  2. rank_type_id == 11 means Premier” — node-globaloffensive’s README only documents rank_type_id: 6=Matchmaking, 7=Wingman, 10=Danger Zone. Premier is not enumerated in the proto (the field is just optional uint32 rank_type_id = 6 with no enum). The value 11 is community-asserted, not repo-confirmed. The structural mechanism (walk rankings[], match by rank_type_id, read rank_id for the rating) is correct; only the magic number is uncertain.

The transport-layer and Auth-layer claims in this doc set ARE verified — see Validation Report for the full per-claim audit.


Section A: GC Message Envelope on the CM

The CMsgGCClient Envelope

GC messages are not sent directly; they are wrapped in a network message called CMsgGCClient, which serves as the transport envelope on the Steam CM network.

Proto definition (/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_clientserver_2.proto:200):

message CMsgGCClient {
    enum EFlag {
        VALVE_DS = 1;
    }
    optional uint32 appid = 1;           // App ID (e.g., 730 for CS2)
    optional uint32 msgtype = 2;         // GC message type ID
    optional bytes payload = 3;          // Serialized GC message
    optional fixed64 steamid = 4;        // Client's SteamID
    optional string gcname = 5;          // GC name (optional)
    optional uint32 ip = 6;              // Client IP (optional)
    optional uint32 flags = 7;           // Message flags
}

The appid field specifies which app’s GC should handle this message. For CS2, this is always 730. The msgtype field is the GC-specific message ID, which is encoded with a high bit flag to signal protobuf wrapping.

EMsg Values for GC Transport

Steam uses two primary EMsg values to carry CMsgGCClient envelopes:

  • EMsg.ClientToGC = 5452 (/Volumes/bandi/coding/hypelevels/node-steam-user/enums/EMsg.js): Client → GC direction. Sent by the client to transmit a message to the GC.
  • EMsg.ClientFromGC = 5453: GC → Client direction. Received by the client containing a response from the GC.

These constants are used in the message routing layer (/Volumes/bandi/coding/hypelevels/node-steam-user/components/gamecoordinator.js:49):

this._send({
    msg: EMsg.ClientToGC,          // EMsg 5452
    proto: {
        routing_appid: appid       // 730 for CS2
    }
}, {
    appid,
    msgtype: msgType,              // GC message ID
    payload: Buffer.concat([header, payload])
});

Protobuf Bit Masking in msgtype

Proto-mask high bit 0x80000000

GC messages can be encoded as either legacy binary structs or as Protocol Buffers. The high bit (0x80000000) of the msgtype field signals whether a message is protobuf-wrapped. Forgetting the proto-mask on outbound msgtype is a classic bug — outbound CS2 messages must OR with 0x80000000, and inbound messages have the bit set too; mask it off before matching against language.js IDs.

Masking definition (/Volumes/bandi/coding/hypelevels/node-steam-user/components/gamecoordinator.js:11,33,62):

const PROTO_MASK = 0x80000000;
 
// When sending a protobuf GC message:
msgType = (msgType | PROTO_MASK) >>> 0;
 
// When receiving, check the bit to determine message format:
let msgType = body.msgtype & ~PROTO_MASK;
if (body.msgtype & PROTO_MASK) {
    // This is a protobuf message; extract the header
}

For example:

  • Request message ID (raw): 9109 (MatchmakingClient2GCHello)
  • Sent msgtype: 9109 | 0x80000000 = 0x80002395 (protobuf-encoded)
  • Response message ID (raw): 9110 (MatchmakingGC2ClientHello)
  • Received msgtype: 9110 | 0x80000000 = 0x80002396 (protobuf-encoded)

GC Protobuf Header (CMsgProtoBufHeader)

Unlike the CM’s CMsgProtoBufHeader (which is for CM-level messages), the GC layer includes its own protobuf header within the GC message payload. This header is managed by the gamecoordinator component.

Definition (/Volumes/bandi/coding/hypelevels/node-steam-user/components/gamecoordinator.js:35,69):

When sending:

let protoHeader = SteamUserGameCoordinator._encodeProto(
    Schema.CMsgProtoBufHeader,
    protoBufHeader
);

When receiving (for protobuf messages):

let headerLength = body.payload.readInt32LE(4);
let protoHeader = SteamUserGameCoordinator._decodeProto(
    Schema.CMsgProtoBufHeader,
    body.payload.slice(8, 8 + headerLength)
);
targetJobID = protoHeader.jobid_target || JOBID_NONE;
payload = body.payload.slice(8 + headerLength);

The header contains job tracking fields for request-response correlation (jobid_source, jobid_target).


Section B: Mounting the CS2 App Environment

CMsgClientGamesPlayed Structure

Before the GC will accept messages, the client must announce that it is “playing” app 730 (CS2). This is done by sending a CMsgClientGamesPlayed message to the CM.

Proto definition (/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_clientserver.proto:82,89):

message CMsgClientGamesPlayed {
    message GamePlayed {
        optional fixed64 game_id = 2;           // App ID (730)
        optional uint32 launch_source = 21;     // How the app was launched
        optional bool is_secure = 5;            // Secure mode flag
        optional uint32 game_flags = 11;        // Game-specific flags
        // ... other fields for VR, streaming, controllers, etc.
    }
    repeated GamePlayed games_played = 1;       // Array of apps being played
}

Building the Games-Played Message in node-steam-user

The gamesPlayed() method in node-steam-user constructs and sends this message (/Volumes/bandi/coding/hypelevels/node-steam-user/components/apps.js:51,61):

gamesPlayed(apps, force) {
    if (!(apps instanceof Array)) {
        apps = [apps];
    }
    
    let processedApps = apps.map((app) => {
        if (typeof app == 'string') {
            app = {game_id: '15190414816125648896', game_extra_info: app};
        } else if (typeof app != 'object') {
            app = {game_id: app};  // Convert appid to object
        }
        return app;
    });
    
    this._send(EMsg.ClientGamesPlayedWithDataBlob, {
        games_played: processedApps
    });
    
    // Emit events for appLaunched when app is added
    processedApps.forEach((app) => {
        let appid = parseInt(app.game_id, 10);
        if (!this._playingAppIds.includes(appid)) {
            this.emit('appLaunched', appid);  // Triggers GC connection logic
        }
    });
}

When the SteamUser emits appLaunched with appid 730, the GlobalOffensive client detects this event and initiates the GC connection sequence (/Volumes/bandi/coding/hypelevels/node-globaloffensive/index.js:57,62):

this._steam.on('appLaunched', (appid) => {
    if (this._isInCSGO) {
        return;
    }
    
    if (appid == STEAM_APPID) {  // 730
        this._isInCSGO = true;
        if (!this.haveGCSession) {
            this._connect();  // Start the GC hello handshake
        }
    }
});

Requirement to Mount Games Before GC Response

The GC will not respond to any messages until the client has declared via CMsgClientGamesPlayed that it is “playing” the app. This is enforced server-side. Attempting to send GC messages (hello, etc.) before mounting 730 will result in the GC not responding.

Retry Loop for GC Hello

If the GC doesn’t respond to a hello message, the client must retry. The _connect() method in GlobalOffensive implements exponential backoff (/Volumes/bandi/coding/hypelevels/node-globaloffensive/index.js:103,134):

GlobalOffensive.prototype._connect = function() {
    if (!this._isInCSGO || this._helloTimer) {
        return;  // Already trying to connect
    }
    
    let sendHello = () => {
        if (!this._isInCSGO || this.haveGCSession) {
            clearTimeout(this._helloTimer);
            delete this._helloTimer;
            return;
        }
        
        this._send(Language.ClientHello, Protos.CMsgClientHello, {
            version: 2000244,
            client_session_need: 0,
            client_launcher: 0,
            steam_launcher: 0
        });
        
        // Exponential backoff: start at 1000ms, double each attempt, cap at 60s
        this._helloTimerMs = Math.min(60000, (this._helloTimerMs || 1000) * 2);
        this._helloTimer = setTimeout(sendHello, this._helloTimerMs);
    };
    
    this._helloTimer = setTimeout(sendHello, 500);
};

Each hello attempt is scheduled with increasing delays. Once a response is received (the ClientWelcome message), the timer is cleared and the session is considered established.


Section C: The ClientHello Request

Message IDs: ClientHello vs. MatchmakingClient2GCHello

There are two “hello” messages in the CS2 GC protocol:

  1. Base GC Hello (generic base message):

    • Message ID: 4006 (k_EMsgGCClientHello)
    • Proto: CMsgClientHello (defined in gcsdk_gcmessages.proto)
    • Used in node-globaloffensive’s initial handshake
  2. CS2 Matchmaking Client Hello (CS2-specific):

    • Message ID: 9109 (k_EMsgGCCStrike15_v2_MatchmakingClient2GCHello)
    • Proto: CMsgGCCStrike15_v2_MatchmakingClient2GCHello (empty message)
    • Used for initial sync after ClientWelcome is received

Language mapping (/Volumes/bandi/coding/hypelevels/node-globaloffensive/language.js:162,179):

// EGCBaseClientMsg
ClientWelcome: 4004,
ServerWelcome: 4005,
ClientHello: 4006,         // <-- Initial handshake
ServerHello: 4007,
 
// ECsgoGCMsg
MatchmakingClient2GCHello: 9109,    // <-- CS2 account sync
MatchmakingGC2ClientHello: 9110,    // <-- CS2 response (contains player data)

CMsgClientHello Fields

The initial hello message (/Volumes/bandi/coding/hypelevels/node-globaloffensive/index.js:121,126) sends:

this._send(Language.ClientHello, Protos.CMsgClientHello, {
    version: 2000244,              // Client version
    client_session_need: 0,         // Don't need a new session
    client_launcher: 0,             // Not launched via special launcher
    steam_launcher: 0               // Not launched via Steam launcher
});

All these fields can be sent as empty/zero—they are optional parameters that the GC accepts with default values.

CMsgGCCStrike15_v2_MatchmakingClient2GCHello

After the GC acknowledges with ClientWelcome, the client should send the CS2-specific hello.

Proto definition (/Volumes/bandi/coding/hypelevels/Protobufs/csgo/cstrike15_gcmessages.proto:629):

message CMsgGCCStrike15_v2_MatchmakingClient2GCHello {
    // This message is intentionally empty. No fields required.
}

This is an empty message—no fields are populated. The client simply sends an empty payload:

this._send(9109, Protos.CMsgGCCStrike15_v2_MatchmakingClient2GCHello, {});

Section D: The 9110 Response (MatchmakingGC2ClientHello)

Confirming the Message ID

Message ID 9110 maps to k_EMsgGCCStrike15_v2_MatchmakingGC2ClientHello in the CS2 GC message enum.

Confirmation (/Volumes/bandi/coding/hypelevels/node-globaloffensive/language.js:180):

MatchmakingGC2ClientHello: 9110,

Proto Structure and Key Fields

The response message is defined at /Volumes/bandi/coding/hypelevels/Protobufs/csgo/cstrike15_gcmessages.proto:632. The full structure:

message CMsgGCCStrike15_v2_MatchmakingGC2ClientHello {
    optional uint32 account_id = 1;
    optional CMsgGCCStrike15_v2_MatchmakingGC2ClientReserve ongoingmatch = 2;
    optional GlobalStatistics global_stats = 3;
    optional uint32 penalty_seconds = 4;
    optional uint32 penalty_reason = 5;
    optional int32 vac_banned = 6;
    optional PlayerRankingInfo ranking = 7;           // Primary rank (legacy)
    optional PlayerCommendationInfo commendation = 8; // Friend/teacher/leader counts
    optional PlayerMedalsInfo medals = 9;             // Display medals
    optional TournamentEvent my_current_event = 10;
    repeated TournamentTeam my_current_event_teams = 11;
    optional TournamentTeam my_current_team = 12;
    repeated TournamentEvent my_current_event_stages = 13;
    optional uint32 survey_vote = 14;
    optional AccountActivity activity = 15;
    optional int32 player_level = 17;                 // <-- PLAYER LEVEL (Competitive coin)
    optional int32 player_cur_xp = 18;                // <-- CURRENT XP IN LEVEL
    optional int32 player_xp_bonus_flags = 19;        // <-- PRIME STATUS (bit field)
    repeated PlayerRankingInfo rankings = 20;         // <-- PREMIER & OTHER RANKINGS
    optional uint64 owcaseid = 21;
}

Player Level (player_level)

The player_level field (int32, field 17) contains the player’s competitive matchmaking level, commonly known as the “rank coin” level. This is a number from 1 to 40 (approximately). The level is not the same as the competitive rank (which is in the rankings array).

Extraction location (/Volumes/bandi/coding/hypelevels/node-globaloffensive/handlers.js:65):

handlers[Language.MatchmakingGC2ClientHello] = function(body) {
    let proto = decodeProto(Protos.CMsgGCCStrike15_v2_MatchmakingGC2ClientHello, body);
    this.emit('accountData', proto);
    this.accountData = proto;
    // proto.player_level is available here
};

Player Current XP (player_cur_xp)

The player_cur_xp field (int32, field 18) contains the player’s experience points progress within the current level. This is a cumulative XP counter toward the next level (player_level + 1).

XP is earned through various activities: competitive matches, danger zone, deathmatch, etc. The field directly exposes the raw XP value.

Prime Status (player_xp_bonus_flags)

The player_xp_bonus_flags field (int32, field 19) is a bitfield encoding various XP bonus flags. Prime membership status is encoded as bit 4 (value = 16 or 0x10).

Bit meaning:

  • Bit 0 (value 1): Unknown bonus flag
  • Bit 1 (value 2): Unknown bonus flag
  • Bit 2 (value 4): Unknown bonus flag
  • Bit 3 (value 8): Unknown bonus flag
  • Bit 4 (value 16 or 0x10): Prime Status ← This indicates the account has an active Prime subscription
  • Bits 5+ (values 32, 64, 128, …): Potentially other bonus sources (battle pass, events, etc.)

Extraction pattern:

const PRIME_BONUS_FLAG = 16;  // 0x10 = bit 4
const hasPrime = (proto.player_xp_bonus_flags & PRIME_BONUS_FLAG) !== 0;

If (player_xp_bonus_flags & 16) != 0, the account has Prime membership.

Rankings Array and Premier Rating

Read rankings[] (plural), not ranking (singular) for Premier

The singular ranking field is the legacy CS:GO competitive rank only. Premier and the rest are in the repeated rankings[]. To get Premier, walk the array and pick the entry where rank_type_id == 11 — its rank_id is the integer rating (e.g. 12734).

The rankings field is a repeated array of PlayerRankingInfo messages, each representing the player’s rating/rank in a different competitive mode.

PlayerRankingInfo definition (/Volumes/bandi/coding/hypelevels/Protobufs/csgo/cstrike15_gcmessages.proto:233):

message PlayerRankingInfo {
    optional uint32 account_id = 1;
    optional uint32 rank_id = 2;           // Rank/rating value for this mode
    optional uint32 wins = 3;              // Number of wins in this mode
    optional float rank_change = 4;        // Change in rank since last update
    optional uint32 rank_type_id = 6;      // Mode type: 1=Competitive, 11=Premier, etc.
    optional uint32 tv_control = 7;
    optional uint64 rank_window_stats = 8;
    optional string leaderboard_name = 9;
    optional uint32 rank_if_win = 10;
    optional uint32 rank_if_lose = 11;
    optional uint32 rank_if_tie = 12;
    repeated PerMapRank per_map_rank = 13; // Per-map breakdown
    optional uint32 leaderboard_name_status = 14;
    optional uint32 highest_rank = 15;
    optional uint32 rank_expiry = 16;
}

Rank Type IDs

Each entry in the rankings array has a rank_type_id that identifies which competitive mode it represents:

  • rank_type_id = 1: Competitive (5v5 Competitive Matchmaking)
  • rank_type_id = 2: Wingman (2v2 mode)
  • rank_type_id = 3: Danger Zone (battle royale mode)
  • rank_type_id = 11: Premier (5v5 Premier mode, CS2’s new ranked system)
  • Other IDs may represent event-specific or experimental modes

Premier Rating

To extract the Premier rating, iterate through rankings and find the entry where rank_type_id == 11:

const premierRanking = proto.rankings.find(r => r.rank_type_id === 11);
if (premierRanking) {
    const premierRating = premierRanking.rank_id;  // Rating value
    const premierWins = premierRanking.wins;        // Wins in Premier
    // ...
}

The Premier rating is the rank_id field of the matching entry.

Handler and Public API

The handler in node-globaloffensive emits the decoded proto as the 'accountData' event and stores it on the instance:

Handler (/Volumes/bandi/coding/hypelevels/node-globaloffensive/handlers.js:65):

handlers[Language.MatchmakingGC2ClientHello] = function(body) {
    let proto = decodeProto(Protos.CMsgGCCStrike15_v2_MatchmakingGC2ClientHello, body);
    this.emit('accountData', proto);     // Emit the full proto
    this.accountData = proto;             // Store on instance
};

Consumer code can listen for the event:

csgo.on('accountData', (proto) => {
    console.log('Player Level:', proto.player_level);
    console.log('Current XP:', proto.player_cur_xp);
    console.log('Has Prime:', (proto.player_xp_bonus_flags & 16) !== 0);
    
    const premierRank = proto.rankings.find(r => r.rank_type_id === 11);
    console.log('Premier Rating:', premierRank?.rank_id);
});

Section E: End-to-End CS2 GC Fetch Flow

This section provides a numbered step-by-step walkthrough of the complete protocol sequence to extract player data from the CS2 GC.

Step 1: Connected and Authenticated to CM

Assume the client is already connected to the Steam CM and has logged in with a valid account. The connection and login flow are handled by the steam-user module prior to any GC interaction.

Reference: /Volumes/bandi/coding/hypelevels/node-steam-user/index.js

Step 2: Send CMsgClientGamesPlayed (Mount AppID 730)

Announce to Steam that the client is “playing” CS2 (app ID 730). This registers the app with the GC and makes the GC willing to accept messages from this client.

const SteamUser = require('steam-user');
const user = new SteamUser();
 
// ... login flow ...
 
// Mount CS2 app
user.gamesPlayed([730]);  // AppID for CS2

Implementation reference: /Volumes/bandi/coding/hypelevels/node-steam-user/components/apps.js:51,75

This sends EMsg.ClientGamesPlayedWithDataBlob with CMsgClientGamesPlayed.games_played[] containing:

  • game_id: 730
  • launch_source: <value>
  • Other optional fields

The CM broadcasts an appLaunched event, which triggers the GlobalOffensive module to begin the GC handshake.

Step 3: Construct and Send ClientHello (EMsg.ClientHello, ID 4006)

The GlobalOffensive client receives the appLaunched(730) event and initiates the GC connection by sending the base GC hello message.

const GlobalOffensive = require('globaloffensive');
const csgo = new GlobalOffensive(user);
 
csgo.on('appLaunched', (appid) => {
    // This triggers internally
});

Implementation reference: /Volumes/bandi/coding/hypelevels/node-globaloffensive/index.js:57,134

The _connect() method constructs:

this._send(Language.ClientHello, Protos.CMsgClientHello, {
    version: 2000244,
    client_session_need: 0,
    client_launcher: 0,
    steam_launcher: 0
});

Which calls _send() (/Volumes/bandi/coding/hypelevels/node-globaloffensive/index.js:136):

GlobalOffensive.prototype._send = function(type, protobuf, body) {
    if (!this._steam.steamID) {
        return false;
    }
    
    if (protobuf) {
        this._steam.sendToGC(STEAM_APPID, type, {}, protobuf.encode(body).finish());
    }
};

This invokes the steam-user sendToGC method (/Volumes/bandi/coding/hypelevels/node-steam-user/components/gamecoordinator.js:22):

sendToGC(appid, msgType, protoBufHeader, payload, callback) {
    let sourceJobId = JOBID_NONE;
    
    let header;
    if (protoBufHeader) {
        msgType = (msgType | PROTO_MASK) >>> 0;  // Set high bit for protobuf
        protoBufHeader.jobid_source = sourceJobId;
        let protoHeader = SteamUserGameCoordinator._encodeProto(
            Schema.CMsgProtoBufHeader,
            protoBufHeader
        );
        header = Buffer.alloc(8);
        header.writeUInt32LE(msgType, 0);
        header.writeInt32LE(protoHeader.length, 4);
        header = Buffer.concat([header, protoHeader]);
    } else {
        // Legacy binary struct format (not used for protobuf messages)
    }
    
    this._send({
        msg: EMsg.ClientToGC,  // 5452
        proto: {
            routing_appid: appid  // 730
        }
    }, {
        appid,
        msgtype: msgType,  // 4006 | 0x80000000 = 0x80000F9E
        payload: Buffer.concat([header, payload])
    });
}

Message flow on wire:

  1. EMsg = 5452 (ClientToGC)
  2. CMsgGCClient.appid = 730
  3. CMsgGCClient.msgtype = 4006 | 0x80000000 = 0x80000F9E
  4. CMsgGCClient.payload = GC protobuf header + encoded CMsgClientHello

Step 4: Receive ClientWelcome (Response ID 4004)

The GC responds with a ClientWelcome message (ID 4004), confirming that the session is established. The handler (/Volumes/bandi/coding/hypelevels/node-globaloffensive/handlers.js:24) processes this:

handlers[Language.ClientWelcome] = function(body) {
    let proto = decodeProto(Protos.CMsgClientWelcome, body);
    
    if (proto.outofdate_subscribed_caches && proto.outofdate_subscribed_caches.length) {
        // Process Subscribed Objects (SO) caches (inventory, etc.)
    }
    
    this.inventory = this.inventory || [];
    
    this.emit('debug', "GC connection established");
    this.haveGCSession = true;
    clearTimeout(this._helloTimer);
    this._helloTimer = null;
    this._helloTimerMs = 1000;
    this.emit('connectedToGC');
};

Once ClientWelcome is received, haveGCSession is set to true and the connectedToGC event is emitted.

Message received on wire:

  1. EMsg = 5453 (ClientFromGC)
  2. CMsgGCClient.msgtype = 4004 | 0x80000000 = 0x80000FA4
  3. CMsgGCClient.payload = GC protobuf header + encoded CMsgClientWelcome

Step 5: Send MatchmakingClient2GCHello (EMsg.MatchmakingClient2GCHello, ID 9109)

After ClientWelcome, the client sends the CS2-specific hello to populate account data. This can be done in the connectedToGC event handler:

csgo.on('connectedToGC', () => {
    console.log('Connected to GC, sending matchmaking hello...');
    csgo._send(9109, Protos.CMsgGCCStrike15_v2_MatchmakingClient2GCHello, {});
});

Implementation: /Volumes/bandi/coding/hypelevels/node-globaloffensive/index.js:136,158

The message is built as:

  1. Empty proto body (no fields)
  2. Encoded to protobuf bytes
  3. Sent via steam-user.sendToGC(730, 9109, {}, bytes)

Message on wire:

  1. EMsg = 5452 (ClientToGC)
  2. CMsgGCClient.msgtype = 9109 | 0x80000000 = 0x80002395
  3. CMsgGCClient.payload = GC protobuf header + empty CMsgGCCStrike15_v2_MatchmakingClient2GCHello

Step 6: Receive MatchmakingGC2ClientHello (ID 9110)

The GC responds with ID 9110, containing the complete player account data.

Message received on wire:

  1. EMsg = 5453 (ClientFromGC)
  2. CMsgGCClient.msgtype = 9110 | 0x80000000 = 0x80002396
  3. CMsgGCClient.payload = GC protobuf header + encoded CMsgGCCStrike15_v2_MatchmakingGC2ClientHello

Step 7: Strip GC Protobuf Header from Payload

The receivedFromGC handler in steam-user (/Volumes/bandi/coding/hypelevels/node-steam-user/components/gamecoordinator.js:61) handles this:

SteamUserBase.prototype._handlerManager.add(EMsg.ClientFromGC, function(body) {
    let msgType = body.msgtype & ~PROTO_MASK;
    let targetJobID;
    let payload;
    
    if (body.msgtype & PROTO_MASK) {
        // This is a protobuf message
        let headerLength = body.payload.readInt32LE(4);
        let protoHeader = SteamUserGameCoordinator._decodeProto(
            Schema.CMsgProtoBufHeader,
            body.payload.slice(8, 8 + headerLength)
        );
        targetJobID = protoHeader.jobid_target || JOBID_NONE;
        payload = body.payload.slice(8 + headerLength);  // <-- Stripped payload
    } else {
        // Legacy binary format
    }
    
    this.emit('receivedFromGC', body.appid, msgType, payload);
});

The payload at this point is the serialized GC message with the header removed.

Step 8: Decode the MatchmakingGC2ClientHello Proto

The GlobalOffensive client’s handler (/Volumes/bandi/coding/hypelevels/node-globaloffensive/handlers.js:65) receives the payload:

handlers[Language.MatchmakingGC2ClientHello] = function(body) {
    let proto = decodeProto(
        Protos.CMsgGCCStrike15_v2_MatchmakingGC2ClientHello,
        body
    );
    this.emit('accountData', proto);
    this.accountData = proto;
};

The proto is decoded using the protobuf definition (/Volumes/bandi/coding/hypelevels/Protobufs/csgo/cstrike15_gcmessages.proto:632).

Step 9: Extract Player Data

Now the complete proto is available in proto and can be queried:

csgo.on('accountData', (proto) => {
    // Extract all required fields:
    
    const playerLevel = proto.player_level;
    const playerXP = proto.player_cur_xp;
    
    const hasPrime = (proto.player_xp_bonus_flags & 16) !== 0;
    
    const premierRanking = proto.rankings.find(r => r.rank_type_id === 11);
    const premierRating = premierRanking ? premierRanking.rank_id : null;
    
    console.log({
        level: playerLevel,
        xp: playerXP,
        prime: hasPrime,
        premierRating: premierRating
    });
});

Implementation Example

const SteamUser = require('steam-user');
const GlobalOffensive = require('globaloffensive');
 
const user = new SteamUser();
const csgo = new GlobalOffensive(user);
 
user.logOn({
    accountName: 'your_account',
    password: 'your_password'
});
 
user.on('loggedOn', () => {
    console.log('Logged into Steam');
    
    // Step 2: Mount app 730
    user.gamesPlayed([730]);
});
 
csgo.on('connectedToGC', () => {
    console.log('Connected to GC, requesting account data');
    // This can also be done via a property/method if already handled
});
 
csgo.on('accountData', (proto) => {
    console.log('Received account data from GC:');
    console.log('  Player Level:', proto.player_level);
    console.log('  Current XP:', proto.player_cur_xp);
    console.log('  Has Prime:', (proto.player_xp_bonus_flags & 16) !== 0);
    
    const premierRank = proto.rankings.find(r => r.rank_type_id === 11);
    console.log('  Premier Rating:', premierRank?.rank_id || 'N/A');
});

Summary Table

AspectValue / Reference
App ID730 (Counter-Strike 2)
CM EMsg (outbound)5452 (ClientToGC)
CM EMsg (inbound)5453 (ClientFromGC)
Protobuf Mask0x80000000
Base Hello Message ID4006 (ClientHello)
Base Hello Response4004 (ClientWelcome)
CS2 Hello Message ID9109 (MatchmakingClient2GCHello)
CS2 Hello Response ID9110 (MatchmakingGC2ClientHello)
Player Level Fieldplayer_level (int32, field 17)
Player XP Fieldplayer_cur_xp (int32, field 18)
Prime Status FlagBit 4 of player_xp_bonus_flags (value 16 / 0x10)
Premier Ratingrankings[] entry where rank_type_id == 11, field rank_id

References

  • node-globaloffensive main client: /Volumes/bandi/coding/hypelevels/node-globaloffensive/index.js
  • Message handlers: /Volumes/bandi/coding/hypelevels/node-globaloffensive/handlers.js
  • Language/ID mappings: /Volumes/bandi/coding/hypelevels/node-globaloffensive/language.js
  • GC component (CM side): /Volumes/bandi/coding/hypelevels/node-steam-user/components/gamecoordinator.js
  • Apps/games played: /Volumes/bandi/coding/hypelevels/node-steam-user/components/apps.js
  • CS2 GC messages proto: /Volumes/bandi/coding/hypelevels/Protobufs/csgo/cstrike15_gcmessages.proto
  • Steam client-server protos: /Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_clientserver.proto
  • GC SDK messages proto: /Volumes/bandi/coding/hypelevels/Protobufs/csgo/gcsdk_gcmessages.proto
  • Base GC messages proto: /Volumes/bandi/coding/hypelevels/Protobufs/csgo/base_gcmessages.proto