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
590 lines (505 loc) • 18.6 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 { once } = require("events");
const Language = require("./language.js");
const Protos = require("./protobufs/generated/_load.js");
const language = require("./language.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("microTxnAuth", (txn) => {
if (!txn) return;
const Long = require("long");
const longVal = new Long(txn.low, txn.high, true);
console.log(`Received microTxnAuth event from Steam: \x1b[32m${longVal.toString()}\x1b[0m`);
this._send(
Language.StorePurchaseFinalize,
Protos.CMsgGCStorePurchaseFinalize,
{
txn_id: longVal.toNumber(),
},
(appId, msgType, payload) => {
if (Language.StorePurchaseFinalizeResponse != msgType) return;
return this._handlers[msgType].call(this, payload);
},
);
});
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, callback) {
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(), callback);
} else {
// This is a ByteBuffer
this._steam.sendToGC(STEAM_APPID, type, null, body.flip().toBuffer(), callback);
}
return true;
};
GlobalOffensive.prototype.redeemFreeReward = async function ({ items, generationTime: generation_time }) {
if (!items.length) return console.warn("No items to redeem for free reward");
this._send(language.ClientRedeemFreeReward, Protos.CMsgGCCstrike15_v2_ClientRedeemFreeReward, {
generation_time,
redeemable_balance: Math.min(2, items.length),
items: items.map((i) => Number(i)),
});
//console.log(`Redeemed weekly drop with items: ${itemsToClaim.map((i) => i.marketName).join(", ")}`);
return;
const [error, result] = await catchError(once(this, "itemAcquired"));
if (result) return [undefined, result];
return [error || `redeemArmoryRewardFailed`, undefined];
};
GlobalOffensive.prototype.reedemArmoryReward = async function (campaingId = 11, redeemId = 1243526888, armoryBalance, expectedCost) {
const galleryCaseRedeemId = 1243526888;
this._send(language.ClientRedeemMissionReward, Protos.CMsgGCCstrike15_v2_ClientRedeemMissionReward, {
campaign_id: campaingId,
redeem_id: redeemId,
redeemable_balance: armoryBalance,
expected_cost: expectedCost,
});
const [error, result] = await catchError(
once(this, "itemAcquired", {
signal: AbortSignal.timeout(10 * 1000),
}),
);
if (result) return [undefined, result];
return [error || `redeemArmoryRewardFailed`, undefined];
};
GlobalOffensive.prototype.useItemRequest = async function (itemId) {
this._send(Language.UseItemRequest, Protos.CMsgUseItem, {
item_id: itemId,
});
const [error, itemData] = await catchError(
once(this, "itemRemoved", {
signal: AbortSignal.timeout(10 * 1000),
}),
);
if (itemData?.id !== itemId) return [console.log(`Item not removed: ${itemId}`), null];
return [undefined, itemData];
};
GlobalOffensive.prototype.purchaseArmory = async function () {
this._send(language.StorePurchaseInit, Protos.CMsgGCStorePurchaseInit, {
country: "",
language: 0,
currency: 2,
line_items: [
Protos.CGCStorePurchaseInit_LineItem.create({
item_def_id: 1354,
quantity: 1,
cost_in_local_currency: 1399,
supplemental_data: 0,
}),
],
});
const [error, result] = await catchError(
once(this, "storePurchaseFinalizeResponse", {
signal: AbortSignal.timeout(20 * 1000),
}),
);
if (result?.result !== 1) {
console.log(`Failed to init store purchase: ${error || result?.result}`);
return [`storePurchaseInitFailed`, undefined];
//this.emit("error", new Error("Failed to init store purchase"));
}
return [undefined, result.item_ids];
};
GlobalOffensive.prototype.acknowledgeXPReset = async function () {
this._send(language.Client2GcAckXPShopTracks, Protos.CMsgGCCStrike15_v2_Client2GcAckXPShopTracks, {});
};
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),
}),
);
const isInInventory = this.inventory.some((item) => item.id === assetId && !item.casket_id);
if (!error) return [undefined, removedItem];
if (isInInventory) return [undefined, assetId];
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");