globaloffensive
Version:
Exposes a simple API for interacting with the Counter-Strike: Global Offensive/CS2 game coordinator
414 lines (344 loc) • 12.3 kB
JavaScript
const ByteBuffer = require('bytebuffer');
const EventEmitter = require('events').EventEmitter;
const {ShareCode} = require('globaloffensive-sharecode');
const SteamID = require('steamid');
const Util = require('util');
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 != '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 = function(owner, assetid, d, callback) {
let match;
if (typeof owner === 'string' && (match = owner.match(/[SM](\d+)A(\d+)D(\d+)$/))) {
callback = assetid;
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);
if (callback) {
let timeout;
let listener = (item) => {
clearTimeout(timeout);
callback(item);
};
timeout = setTimeout(() => {
this.removeListener('inspectItemInfo#' + assetid, listener);
this.emit('inspectItemTimedOut', assetid);
this.emit('inspectItemTimedOut#' + assetid, assetid);
}, 10000);
this.once('inspectItemInfo#' + assetid, listener);
}
};
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} itemId
*/
GlobalOffensive.prototype.addToCasket = function(casketId, itemId) {
this._send(Language.CasketItemAdd, Protos.CMsgCasketItem, {
casket_item_id: casketId,
item_item_id: itemId
});
};
/**
* Remove an item from a casket (aka a storage unit) into your inventory.
* @param {int} casketId
* @param {int} itemId
*/
GlobalOffensive.prototype.removeFromCasket = function(casketId, itemId) {
this._send(Language.CasketItemExtract, Protos.CMsgCasketItem, {
casket_item_id: casketId,
item_item_id: itemId
});
};
/**
* Get the contents of a casket (aka a storage unit).
* @param {int} casketId
* @param {function} callback
*/
GlobalOffensive.prototype.getCasketContents = function(casketId, callback) {
// First see if we already have this casket's contents in our inventory
let casketItem = this.inventory.find(item => item.id == casketId);
if (!casketItem) {
callback(new Error(`No casket matching ID ${casketId} was found`));
return;
}
if (!casketItem.casket_contained_item_count) {
// Casket is empty, I guess
callback(null, []);
return;
}
let loadedItems = this.inventory.filter(item => item.casket_id == casketId);
if (loadedItems.length == casketItem.casket_contained_item_count) {
callback(null, loadedItems);
return;
}
// We need to load casket contents from the GC
this._send(Language.CasketItemLoadContents, Protos.CMsgCasketItem, {
casket_item_id: casketId,
item_item_id: casketId
});
// Set a 30 second timeout in case the GC isn't being cooperative
let timedOut = false;
let timeout = setTimeout(() => {
if (timedOut) {
return;
}
callback(new Error('Loading casket contents timed out'));
}, 30000);
let customizationNotification = (itemIds, notificationType) => {
if (timedOut) {
this.off('itemCustomizationNotification', customizationNotification);
return;
}
if (itemIds[0] != casketId || notificationType != GlobalOffensive.ItemCustomizationNotification.CasketContents) {
return;
}
// This is our casket, and it's the correct notification
clearTimeout(timeout);
timedOut = true;
this.off('itemCustomizationNotification', customizationNotification);
callback(null, this.inventory.filter(item => item.casket_id == casketId));
};
this.on('itemCustomizationNotification', customizationNotification);
};
GlobalOffensive.prototype._handlers = {};
require('./enums.js');
require('./handlers.js');