UNPKG

globaloffensive-promise

Version:

Simply a complete Fork of globaloffensive with some Functions adjusted to support Promises. | A simple API for interacting with the Counter-Strike: Global Offensive/CS2 game coordinator

581 lines (508 loc) 15.5 kB
const ByteBuffer = require("bytebuffer"); const EventEmitter = require("events").EventEmitter; const { ShareCode } = require("globaloffensive-sharecode"); const SteamID = require("steamid"); const Util = require("util"); const { once } = require("events"); const Language = require("./language.js"); const Protos = require("./protobufs/generated/_load.js"); const STEAM_APPID = 730; module.exports = GlobalOffensive; Util.inherits(GlobalOffensive, EventEmitter); function GlobalOffensive(steam) { if ( !steam.packageName.includes("steam-user") || !steam.packageVersion || !steam.constructor ) { throw new Error( "globaloffensive v2 only supports steam-user v4.2.0 or later." ); } else { let [major, minor] = steam.packageVersion.split("."); if (major < 4 || (major == 4 && minor < 2)) { throw new Error( `globaloffensive v2 only supports steam-user v4.2.0 or later. ${steam.constructor.name} v${steam.packageVersion} given.` ); } } this._steam = steam; this.haveGCSession = false; this._isInCSGO = false; this._steam.on("receivedFromGC", (appid, msgType, payload) => { if (appid != STEAM_APPID) { return; // we don't care } let isProtobuf = !Buffer.isBuffer(payload); let handler = null; if (this._handlers[msgType]) { handler = this._handlers[msgType]; } let msgName = msgType; for (let i in Language) { if (Language.hasOwnProperty(i) && Language[i] == msgType) { msgName = i; break; } } this.emit( "debug", "Got " + (handler ? "handled" : "unhandled") + " GC message " + msgName + (isProtobuf ? " (protobuf)" : "") ); if (handler) { handler.call( this, isProtobuf ? payload : ByteBuffer.wrap(payload, ByteBuffer.LITTLE_ENDIAN) ); } }); this._steam.on("appLaunched", (appid) => { if (this._isInCSGO) { return; // we don't care if it was launched again } if (appid == STEAM_APPID) { this._isInCSGO = true; if (!this.haveGCSession) { this._connect(); } } }); let handleAppQuit = (emitDisconnectEvent) => { if (this._helloInterval) { clearInterval(this._helloInterval); this._helloInterval = null; } if (this.haveGCSession && emitDisconnectEvent) { this.emit( "disconnectedFromGC", GlobalOffensive.GCConnectionStatus.NO_SESSION ); } this._isInCSGO = false; this.haveGCSession = false; }; this._steam.on("appQuit", (appid) => { if (!this._isInCSGO) { return; } if (appid == STEAM_APPID) { handleAppQuit(false); } }); this._steam.on("disconnected", () => { handleAppQuit(true); }); this._steam.on("error", (err) => { handleAppQuit(true); }); } GlobalOffensive.prototype._connect = function () { if (!this._isInCSGO || this._helloTimer) { this.emit( "debug", "Not trying to connect due to " + (!this._isInCSGO ? "not in CS:GO" : "has helloTimer") ); return; // We're not in CS:GO or we're already trying to connect } let sendHello = () => { if (!this._isInCSGO) { this.emit("debug", "Not sending hello because we're no longer in CS:GO"); delete this._helloTimer; return; } else if (this.haveGCSession) { this.emit("debug", "Not sending hello because we have a session"); 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, }); this._helloTimerMs = Math.min(60000, (this._helloTimerMs || 1000) * 2); // exponential backoff, max 60 seconds this._helloTimer = setTimeout(sendHello, this._helloTimerMs); this.emit( "debug", `Sending hello, setting timer for next attempt to ${this._helloTimerMs} ms` ); }; this._helloTimer = setTimeout(sendHello, 500); }; GlobalOffensive.prototype._send = function (type, protobuf, body) { if (!this._steam.steamID) { return false; } let msgName = type; for (let i in Language) { if (Language[i] == type) { msgName = i; break; } } this.emit("debug", "Sending GC message " + msgName); if (protobuf) { this._steam.sendToGC(STEAM_APPID, type, {}, protobuf.encode(body).finish()); } else { // This is a ByteBuffer this._steam.sendToGC(STEAM_APPID, type, null, body.flip().toBuffer()); } return true; }; GlobalOffensive.prototype.requestGame = function (shareCodeOrDetails) { if (typeof shareCodeOrDetails == "string") { shareCodeOrDetails = new ShareCode(shareCodeOrDetails).decode(); } if (typeof shareCodeOrDetails != "object" || !shareCodeOrDetails) { throw new Error( "shareCodeOrDetails must be a sharecode or an object with properties matchId, outcomeId, token" ); } let requiredProps = ["matchId", "outcomeId", "token"]; requiredProps.sort(); let extantProps = Object.keys(shareCodeOrDetails); extantProps.sort(); if (extantProps.join() != requiredProps.join()) { throw new Error( "shareCodeOrDetails must be a sharecode or an object with properties matchId, outcomeId, token" ); } this._send( Language.MatchListRequestFullGameInfo, Protos.CMsgGCCStrike15_v2_MatchListRequestFullGameInfo, { matchid: shareCodeOrDetails.matchId, outcomeid: shareCodeOrDetails.outcomeId, token: shareCodeOrDetails.token, } ); }; GlobalOffensive.prototype.requestLiveGames = function () { this._send( Language.MatchListRequestCurrentLiveGames, Protos.CMsgGCCStrike15_v2_MatchListRequestCurrentLiveGames, {} ); }; GlobalOffensive.prototype.requestRecentGames = function (steamid) { if (typeof steamid === "string") { steamid = new SteamID(steamid); } if ( !steamid.isValid() || steamid.universe != SteamID.Universe.PUBLIC || steamid.type != SteamID.Type.INDIVIDUAL || steamid.instance != SteamID.Instance.DESKTOP ) { return false; } this._send( Language.MatchListRequestRecentUserGames, Protos.CMsgGCCStrike15_v2_MatchListRequestRecentUserGames, { accountid: steamid.accountid, } ); }; GlobalOffensive.prototype.requestLiveGameForUser = function (steamid) { if (typeof steamid === "string") { steamid = new SteamID(steamid); } if ( !steamid.isValid() || steamid.universe != SteamID.Universe.PUBLIC || steamid.type != SteamID.Type.INDIVIDUAL || steamid.instance != SteamID.Instance.DESKTOP ) { return false; } this._send( Language.MatchListRequestLiveGameForUser, Protos.CMsgGCCStrike15_v2_MatchListRequestLiveGameForUser, { accountid: steamid.accountid, } ); }; GlobalOffensive.prototype.inspectItem = async function (owner, assetid, d) { let match; if ( typeof owner === "string" && (match = owner.match(/[SM](\d+)A(\d+)D(\d+)$/)) ) { owner = match[1]; assetid = match[2]; d = match[3]; } let msg = { param_a: assetid, param_d: d, param_s: 0, param_m: 0, }; if (typeof owner === "object") { owner = owner.toString(); } try { let sid = new SteamID(owner); if ( !sid.isValid() || sid.universe != SteamID.Universe.PUBLIC || sid.type != SteamID.Type.INDIVIDUAL || sid.instance != SteamID.Instance.DESKTOP ) { throw 0; } // it's a valid steamid msg.param_s = owner; } catch (e) { msg.param_m = owner; } this._send( Language.Client2GCEconPreviewDataBlockRequest, Protos.CMsgGCCStrike15_v2_Client2GCEconPreviewDataBlockRequest, msg ); const [error, item] = await catchError( once(this, `inspectItemInfo#${assetid}`, { signal: AbortSignal.timeout(10 * 1000), }) ); if (!error) return [ undefined, { paintIndex: item.paintindex, paintWear: item.paintwear, paintSeed: item.paintseed, stickers: item.stickers, keychains: item.keychains, defIndex: item.defindex, quality: item.quality, rarity: item.rarity, killeaterscoretype: item.killeaterscoretype, killeatervalue: item.killeatervalue, customName: item.customname, origin: item.origin, inventory: item.inventory, questid: item.questid, dropreason: item.dropreason, musicindex: item.musicindex, entindex: item.entindex, accountid: item.accountid, itemid: item.itemid, }, ]; if (error.name === "AbortError") return [`inspectItemTimedOut`, undefined]; }; GlobalOffensive.prototype.requestPlayersProfile = function (steamid, callback) { if (typeof steamid == "string") { steamid = new SteamID(steamid); } if ( !steamid.isValid() || steamid.universe != SteamID.Universe.PUBLIC || steamid.type != SteamID.Type.INDIVIDUAL || steamid.instance != SteamID.Instance.DESKTOP ) { return false; } this._send( Language.ClientRequestPlayersProfile, Protos.CMsgGCCStrike15_v2_ClientRequestPlayersProfile, { account_id: steamid.accountid, request_level: 32, } ); if (callback) { this.once("playersProfile#" + steamid.getSteamID64(), callback); } }; /** * Rename an item in your inventory using a name tag. * @param {int} nameTagId * @param {int} itemId * @param {string} name */ GlobalOffensive.prototype.nameItem = function (nameTagId, itemId, name) { let buffer = new ByteBuffer( 18 + Buffer.byteLength(name), ByteBuffer.LITTLE_ENDIAN ); buffer.writeUint64(nameTagId); buffer.writeUint64(itemId); buffer.writeByte(0x00); // unknown buffer.writeCString(name); this._send(Language.NameItem, null, buffer); }; /** * Permanently delete an item from your inventory. * @param {int} itemId */ GlobalOffensive.prototype.deleteItem = function (itemId) { let buffer = new ByteBuffer(8, ByteBuffer.LITTLE_ENDIAN); buffer.writeUint64(itemId); this._send(Language.Delete, null, buffer); }; /** * Craft some items using a given recipe. * @param {int[]} items - IDs of items to craft * @param {int} recipe - The ID of the recipe to use */ GlobalOffensive.prototype.craft = function (items, recipe) { let buffer = new ByteBuffer( 2 + 2 + 8 * items.length, ByteBuffer.LITTLE_ENDIAN ); buffer.writeInt16(recipe); buffer.writeInt16(items.length); for (let i = 0; i < items.length; i++) { buffer.writeUint64(items[i]); } this._send(Language.Craft, null, buffer); }; // Storage units /** * Put an item from your inventory into a casket (aka a storage unit). * @param {int} casketId * @param {int} assetId */ GlobalOffensive.prototype.addToCasket = async function (casketId, assetId) { if ( (typeof casketId !== "string" && typeof casketId !== "number") || (typeof assetId !== "string" && typeof assetId !== "number") ) { throw new Error("TypeError: Expected string or number"); } this._send(Language.CasketItemAdd, Protos.CMsgCasketItem, { casket_item_id: casketId, item_item_id: assetId, }); const abortSignal = AbortSignal.timeout(5 * 1000); while (true) { const [error, itemId, notificationType] = await catchError( once(this, "itemCustomizationNotification", { signal: abortSignal, }) ); if ( notificationType === GlobalOffensive.ItemCustomizationNotification.CasketTooFull ) return [ { message: `Casket ${casketId} is full`, id: GlobalOffensive.ItemCustomizationNotification.CasketTooFull, }, undefined, ]; if ( notificationType === GlobalOffensive.ItemCustomizationNotification.CasketAdded ) return [undefined, itemId]; if (error) return [error, undefined]; } }; /** * Remove an item from a casket (aka a storage unit) into your inventory. * @param {int} casketId * @param {int} assetId */ GlobalOffensive.prototype.removeFromCasket = async function ( casketId, assetId ) { if ( (typeof casketId !== "string" && typeof casketId !== "number") || (typeof assetId !== "string" && typeof assetId !== "number") ) { throw new Error("TypeError: Expected string or number"); } this._send(Language.CasketItemExtract, Protos.CMsgCasketItem, { casket_item_id: casketId, item_item_id: assetId, }); const [error, removedItem] = await catchError( once(this, "itemAcquired", { signal: AbortSignal.timeout(5 * 1000), }) ); if (!error) return [undefined, removedItem]; return [error, undefined]; }; /** * Get the contents of a casket (aka a storage unit). * @param {int} casketId * @ */ GlobalOffensive.prototype.getCasketContents = async function (casketId) { if (typeof casketId !== "string" && typeof casketId !== "number") { throw new Error("TypeError: Expected string or number"); } // First see if we already have this casket's contents in our inventory let casketItem = this.inventory.find((item) => item.id == casketId); if (!casketItem) { return [ new Error(`No casket matching ID ${casketId} was found`), undefined, ]; } if (!casketItem.casket_contained_item_count) { // Casket is empty, I guess return [undefined, []]; } let loadedItems = this.inventory.filter((item) => item.casket_id == casketId); if (loadedItems.length == casketItem.casket_contained_item_count) { return [undefined, loadedItems]; } // We need to load casket contents from the GC this._send(Language.CasketItemLoadContents, Protos.CMsgCasketItem, { casket_item_id: casketId, item_item_id: casketId, }); const abortSignal = AbortSignal.timeout(15 * 1000); while (true) { const [error, itemIds, notificationType] = await catchError( once(this, "itemCustomizationNotification", { signal: abortSignal, }) ); if (error?.name === "AbortError") return [error, undefined]; if ( itemIds?.[0] != casketId || notificationType != GlobalOffensive.ItemCustomizationNotification.CasketContents ) continue; if (!error) return [ undefined, this.inventory.filter((item) => item.casket_id == casketId), ]; } }; function catchError(promise, errorsToCatch) { return promise .then((data) => { if (Array.isArray(data)) return [undefined, ...data]; return [undefined, data]; }) .catch((error) => { if (errorsToCatch === undefined) return [error]; if (errorsToCatch.some((e) => error instanceof e)) return [error]; throw error; }); } GlobalOffensive.prototype._handlers = {}; require("./enums.js"); require("./handlers.js");