UNPKG

globaloffensive

Version:

Exposes a simple API for interacting with the Counter-Strike: Global Offensive/CS2 game coordinator

412 lines (336 loc) 12.4 kB
const ByteBuffer = require('bytebuffer'); const Long = require('long'); const SteamID = require('steamid'); const GlobalOffensive = require('./index.js'); const Language = require('./language.js'); const Protos = require('./protobufs/generated/_load.js'); let handlers = GlobalOffensive.prototype._handlers; // ClientWelcome and ClientConnectionStatus handlers[Language.ClientLogonFatalError] = function(body) { let proto = decodeProto(Protos.CMsgGCCStrike15_v2_ClientLogonFatalError, body); clearTimeout(this._helloTimer); let err = new Error(`Logon Fatal Error: ${proto.message || proto.errorcode}`); err.code = proto.errorcode; err.country = proto.country; this.emit('error', err); }; handlers[Language.ClientWelcome] = function(body) { let proto = decodeProto(Protos.CMsgClientWelcome, body); if (proto.outofdate_subscribed_caches && proto.outofdate_subscribed_caches.length) { proto.outofdate_subscribed_caches[0].objects.forEach((cache) => { switch (cache.type_id) { case 1: // Inventory let items = cache.object_data.map((object) => { let item = decodeProto(Protos.CSOEconItem, object); this._processSOEconItem(item); return item; }); this.inventory = items; break; /*case 7: // Account metadata - this doesn't appear to be useful in CS:GO let data = decodeProto(Protos.CSOEconGameAccountClient, cache.object_data[0]); break;*/ /*case 43: // Most likely item presets (multiple) let data = decodeProto(Protos.CSOSelectedItemPreset, cache.object_data[0]); break;*/ default: this.emit('debug', "Unknown SO type " + cache.type_id + " with " + cache.object_data.length + " items"); break; } }); } 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'); }; handlers[Language.MatchmakingGC2ClientHello] = function(body) { let proto = decodeProto(Protos.CMsgGCCStrike15_v2_MatchmakingGC2ClientHello, body); this.emit('accountData', proto); this.accountData = proto; }; handlers[Language.ClientConnectionStatus] = function(body) { let proto = decodeProto(Protos.CMsgConnectionStatus, body); this.emit('connectionStatus', proto.status, proto); let statusStr = proto.status; for (let i in GlobalOffensive.GCConnectionStatus) { if (GlobalOffensive.GCConnectionStatus.hasOwnProperty(i) && GlobalOffensive.GCConnectionStatus[i] == proto.status) { statusStr = i; } } this.emit('debug', "Connection status: " + statusStr + " (" + proto.status + "); have session: " + (this.haveGCSession ? 'yes' : 'no')); if (proto.status != GlobalOffensive.GCConnectionStatus.HAVE_SESSION && this.haveGCSession) { this.emit('disconnectedFromGC', proto.status); this.haveGCSession = false; this._connect(); // Try to reconnect } }; // MatchList handlers[Language.MatchList] = function(body) { let proto = decodeProto(Protos.CMsgGCCStrike15_v2_MatchList, body); this.emit('matchList', proto.matches, proto); }; // PlayersProfile handlers[Language.PlayersProfile] = function(body) { let proto = decodeProto(Protos.CMsgGCCStrike15_v2_PlayersProfile, body); if (!proto.account_profiles[0]) { return; } let profile = proto.account_profiles[0]; let sid = SteamID.fromIndividualAccountID(profile.account_id); this.emit('playersProfile', profile); this.emit('playersProfile#' + sid.getSteamID64(), profile); }; // Inspecting items handlers[Language.Client2GCEconPreviewDataBlockResponse] = function(body) { let proto = decodeProto(Protos.CMsgGCCStrike15_v2_Client2GCEconPreviewDataBlockResponse, body); if (!proto.iteminfo) { return; } let item = proto.iteminfo; // decode the wear let buf = Buffer.alloc(4); buf.writeUInt32BE(item.paintwear, 0); item.paintwear = buf.readFloatBE(0); this.emit('inspectItemInfo', item); this.emit('inspectItemInfo#' + item.itemid, item); }; // Item manipulation handlers[Language.CraftResponse] = function(body) { let blueprint = body.readInt16(); // recipe ID let unknown = body.readUint32(); // always 0 in my experience let idCount = body.readUint16(); let idList = []; // let's form an array of IDs for (let i = 0; i < idCount; i++) { let id = body.readUint64().toString(); // grab the next id idList.push(id); // item id } this.emit('craftingComplete', blueprint, idList); }; handlers[Language.ItemCustomizationNotification] = function(body) { let proto = decodeProto(Protos.CMsgGCItemCustomizationNotification, body); if (!proto.item_id || proto.item_id.length == 0 || !proto.request) { return; } this.emit('itemCustomizationNotification', proto.item_id, proto.request); }; // SO GlobalOffensive.prototype._processSOEconItem = function(item) { // Inventory position let isNew = (item.inventory >>> 30) & 1; item.position = (isNew ? 0 : item.inventory & 0xFFFF); // Is this item contained in a casket? let casketIdLow = getAttributeValueBytes(272); let casketIdHigh = getAttributeValueBytes(273); if (casketIdLow && casketIdHigh) { let casketIdLong = new Long(casketIdLow.readUInt32LE(0), casketIdHigh.readUInt32LE(0)); item.casket_id = casketIdLong.toString(); } // Item custom names let customNameBytes = getAttributeValueBytes(111); if (customNameBytes && !item.custom_name) { item.custom_name = customNameBytes.slice(2).toString('utf8'); } // Paint index/seed/wear let paintIndexBytes = getAttributeValueBytes(6); if (paintIndexBytes) { item.paint_index = paintIndexBytes.readFloatLE(0); } let paintSeedBytes = getAttributeValueBytes(7); if (paintSeedBytes) { item.paint_seed = Math.floor(paintSeedBytes.readFloatLE(0)); } let paintWearBytes = getAttributeValueBytes(8); if (paintWearBytes) { item.paint_wear = paintWearBytes.readFloatLE(0); } let tradableAfterDateBytes = getAttributeValueBytes(75); if (tradableAfterDateBytes) { item.tradable_after = new Date(tradableAfterDateBytes.readUInt32LE(0) * 1000); } let killEaterBytes = getAttributeValueBytes(80); if (killEaterBytes) { item.kill_eater_value = killEaterBytes.readUInt32LE(0); } let killEaterScoreTypeBytes = getAttributeValueBytes(81); if (killEaterScoreTypeBytes) { item.kill_eater_score_type = killEaterScoreTypeBytes.readUInt32LE(0); } let questIdBytes = getAttributeValueBytes(168); if (questIdBytes) { item.quest_id = questIdBytes.readUInt32LE(0); } let stickers = []; for (let i = 0; i <= 5; i++) { let stickerIdBytes = getAttributeValueBytes(113 + (i * 4)); if (stickerIdBytes) { let sticker = { slot: i, sticker_id: stickerIdBytes.readUInt32LE(0), wear: null, scale: null, rotation: null, offset_x: null, offset_y: null }; // As of the 2024-02-06 update, the value of the "sticker slot x schema" attribute (290-295) seems to indicate // which slot the sticker occupies, rather than the actual slot named by the attribute. Why? Who knows? // I sure hope no items exist with schema set for some stickers but not for others. let schemaBytes = getAttributeValueBytes(290 + i); if (schemaBytes) { sticker.slot = schemaBytes.readUInt32LE(0); } ['wear', 'scale', 'rotation'].forEach((attrib, idx) => { let bytes = getAttributeValueBytes(114 + (i * 4) + idx); if (bytes) { sticker[attrib] = bytes.readFloatLE(0); } }); ['offset_x', 'offset_y'].forEach((attrib, idx) => { let bytes = getAttributeValueBytes(278 + (i * 2) + idx); if (bytes) { sticker[attrib] = bytes.readFloatLE(0); } }); stickers.push(sticker); } } if (stickers.length > 0) { item.stickers = stickers; } // def_index-specific attribute parsing switch (item.def_index) { case 1201: // Storage Unit item.casket_contained_item_count = 0; let itemCountBytes = getAttributeValueBytes(270); if (itemCountBytes) { item.casket_contained_item_count = itemCountBytes.readUInt32LE(0); } break; } /** * @param {int} attribDefIndex * @returns {null|Buffer} */ function getAttributeValueBytes(attribDefIndex) { let attrib = (item.attribute || []).find(attrib => attrib.def_index == attribDefIndex); return attrib ? attrib.value_bytes : null; } }; handlers[Language.SO_Create] = function(body) { let proto = decodeProto(Protos.CMsgSOSingleObject, body); this._handleSOCreate(proto); }; GlobalOffensive.prototype._handleSOCreate = function(proto) { if (proto.type_id != 1) { return; // Not an item } if (!this.inventory) { return; // We don't have our inventory yet! (this shouldn't be possible in CS:GO, but wutevs) } let item = decodeProto(Protos.CSOEconItem, proto.object_data); this._processSOEconItem(item); this.inventory.push(item); this.emit('itemAcquired', item); }; handlers[Language.SO_Update] = function(body) { let proto = decodeProto(Protos.CMsgSOSingleObject, body); this._handleSOUpdate(proto); }; GlobalOffensive.prototype._handleSOUpdate = function(so) { if (so.type_id != 1) { return; // Not an item, we don't care } if (!this.inventory) { return; // We somehow don't have our inventory yet! } let item = decodeProto(Protos.CSOEconItem, so.object_data); this._processSOEconItem(item); for (let i = 0; i < this.inventory.length; i++) { if (this.inventory[i].id == item.id) { let oldItem = this.inventory[i]; this.inventory[i] = item; this.emit('itemChanged', oldItem, item); break; } } }; handlers[Language.SO_Destroy] = function(body) { let proto = decodeProto(Protos.CMsgSOSingleObject, body); this._handleSODestroy(proto); }; GlobalOffensive.prototype._handleSODestroy = function(proto) { if (proto.type_id != 1) { return; // Not an item } if (!this.inventory) { return; // Inventory not loaded yet } let item = decodeProto(Protos.CSOEconItem, proto.object_data); item.id = item.id.toString(); let itemData = null; for (let i = 0; i < this.inventory.length; i++) { if (this.inventory[i].id == item.id) { itemData = this.inventory[i]; this.inventory.splice(i, 1); break; } } this.emit('itemRemoved', itemData); }; handlers[Language.SO_UpdateMultiple] = function(body) { let proto = decodeProto(Protos.CMsgSOMultipleObjects, body); (proto.objects_added || []).forEach(item => this._handleSOCreate(item)); (proto.objects_modified || []).forEach(item => this._handleSOUpdate(item)); (proto.objects_removed || []).forEach(item => this._handleSODestroy(item)); }; function decodeProto(proto, encoded) { if (ByteBuffer.isByteBuffer(encoded)) { encoded = encoded.toBuffer(); } let decoded = proto.decode(encoded); let objNoDefaults = proto.toObject(decoded, {"longs": String}); let objWithDefaults = proto.toObject(decoded, {"defaults": true, "longs": String}); return replaceDefaults(objNoDefaults, objWithDefaults); function replaceDefaults(noDefaults, withDefaults) { if (Array.isArray(withDefaults)) { return withDefaults.map((val, idx) => replaceDefaults(noDefaults[idx], val)); } for (let i in withDefaults) { if (!withDefaults.hasOwnProperty(i)) { continue; } if (withDefaults[i] && typeof withDefaults[i] === 'object' && !Buffer.isBuffer(withDefaults[i])) { // Covers both object and array cases, both of which will work // Won't replace empty arrays, but that's desired behavior withDefaults[i] = replaceDefaults(noDefaults[i], withDefaults[i]); } else if (typeof noDefaults[i] === 'undefined' && isReplaceableDefaultValue(withDefaults[i])) { withDefaults[i] = null; } } return withDefaults; } function isReplaceableDefaultValue(val) { if (Buffer.isBuffer(val) && val.length == 0) { // empty buffer is replaceable return true; } if (Array.isArray(val)) { // empty array is not replaceable (empty repeated fields) return false; } if (val === '0') { // Zero as a string is replaceable (64-bit integer) return true; } // Anything falsy is true return !val; } }