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:
“Prime status = bit 4 (0x10) of
player_xp_bonus_flags” —player_xp_bonus_flagsexists in the proto, but the node-globaloffensive README explicitly says this field “Seems to always benull” (README lines 206, 458). The alternative path (readCSOEconGameAccountClient.elevated_statefrom SO cache type 7) is commented out inhandlers.js:40-43with 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.“
rank_type_id == 11means Premier” — node-globaloffensive’s README only documentsrank_type_id: 6=Matchmaking, 7=Wingman, 10=Danger Zone. Premier is not enumerated in the proto (the field is justoptional uint32 rank_type_id = 6with no enum). The value11is community-asserted, not repo-confirmed. The structural mechanism (walkrankings[], match byrank_type_id, readrank_idfor 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
0x80000000GC messages can be encoded as either legacy binary structs or as Protocol Buffers. The high bit (
0x80000000) of themsgtypefield signals whether a message is protobuf-wrapped. Forgetting the proto-mask on outboundmsgtypeis a classic bug — outbound CS2 messages must OR with0x80000000, and inbound messages have the bit set too; mask it off before matching againstlanguage.jsIDs.
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:
-
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
- Message ID: 4006 (
-
CS2 Matchmaking Client Hello (CS2-specific):
- Message ID: 9109 (
k_EMsgGCCStrike15_v2_MatchmakingClient2GCHello) - Proto:
CMsgGCCStrike15_v2_MatchmakingClient2GCHello(empty message) - Used for initial sync after
ClientWelcomeis received
- Message ID: 9109 (
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), notranking(singular) for PremierThe singular
rankingfield is the legacy CS:GO competitive rank only. Premier and the rest are in the repeatedrankings[]. To get Premier, walk the array and pick the entry whererank_type_id == 11— itsrank_idis 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 CS2Implementation reference: /Volumes/bandi/coding/hypelevels/node-steam-user/components/apps.js:51,75
This sends EMsg.ClientGamesPlayedWithDataBlob with CMsgClientGamesPlayed.games_played[] containing:
game_id: 730launch_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:
- EMsg = 5452 (ClientToGC)
- CMsgGCClient.appid = 730
- CMsgGCClient.msgtype = 4006 | 0x80000000 = 0x80000F9E
- 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:
- EMsg = 5453 (ClientFromGC)
- CMsgGCClient.msgtype = 4004 | 0x80000000 = 0x80000FA4
- 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:
- Empty proto body (no fields)
- Encoded to protobuf bytes
- Sent via
steam-user.sendToGC(730, 9109, {}, bytes)
Message on wire:
- EMsg = 5452 (ClientToGC)
- CMsgGCClient.msgtype = 9109 | 0x80000000 = 0x80002395
- 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:
- EMsg = 5453 (ClientFromGC)
- CMsgGCClient.msgtype = 9110 | 0x80000000 = 0x80002396
- 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
| Aspect | Value / Reference |
|---|---|
| App ID | 730 (Counter-Strike 2) |
| CM EMsg (outbound) | 5452 (ClientToGC) |
| CM EMsg (inbound) | 5453 (ClientFromGC) |
| Protobuf Mask | 0x80000000 |
| Base Hello Message ID | 4006 (ClientHello) |
| Base Hello Response | 4004 (ClientWelcome) |
| CS2 Hello Message ID | 9109 (MatchmakingClient2GCHello) |
| CS2 Hello Response ID | 9110 (MatchmakingGC2ClientHello) |
| Player Level Field | player_level (int32, field 17) |
| Player XP Field | player_cur_xp (int32, field 18) |
| Prime Status Flag | Bit 4 of player_xp_bonus_flags (value 16 / 0x10) |
| Premier Rating | rankings[] 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
Related
- hypelevels README
- Validation Report — per-claim audit; explicitly downgrades the Prime-via-bit-4 and Premier-rank_type_id-11 claims.
- Steam Protocol Overview
- TCP Transport and Handshake
- Auth v2 Login Flow