node-cs2
Version:
Modern CS2/CS:GO Game Coordinator integration with latest GameTracking-CS2 protobuf definitions. Includes support for highlight_reel, wrapped_sticker, variations, Promise-based API, crate opening, sticker/patch/keychain operations, and all modern CS2 fiel
1,263 lines (1,091 loc) • 43.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 = NodeCS2;
Util.inherits(NodeCS2, EventEmitter);
function NodeCS2(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', NodeCS2.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);
});
}
NodeCS2.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);
};
NodeCS2.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;
};
NodeCS2.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
});
};
NodeCS2.prototype.requestLiveGames = function() {
this._send(Language.MatchListRequestCurrentLiveGames, Protos.CMsgGCCStrike15_v2_MatchListRequestCurrentLiveGames, {});
};
NodeCS2.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
});
};
NodeCS2.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
});
};
NodeCS2.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);
// Support both callback and Promise-based API
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);
}, this._inspectTimeout || 10000);
this.once('inspectItemInfo#' + assetid, listener);
} else {
// Return Promise when no callback provided
return new Promise((resolve, reject) => {
let timeout;
let successListener = (item) => {
clearTimeout(timeout);
this.removeListener('inspectItemTimedOut#' + assetid, timeoutListener);
resolve(item);
};
let timeoutListener = () => {
this.removeListener('inspectItemInfo#' + assetid, successListener);
reject(new Error(`Inspect item timed out for assetid: ${assetid}`));
};
timeout = setTimeout(() => {
this.removeListener('inspectItemInfo#' + assetid, successListener);
this.emit('inspectItemTimedOut', assetid);
this.emit('inspectItemTimedOut#' + assetid, assetid);
}, this._inspectTimeout || 10000);
this.once('inspectItemInfo#' + assetid, successListener);
this.once('inspectItemTimedOut#' + assetid, timeoutListener);
});
}
};
NodeCS2.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) {
if (callback) {
callback(new Error('Invalid SteamID'));
return false;
}
return Promise.reject(new Error('Invalid SteamID'));
}
this._send(Language.ClientRequestPlayersProfile, Protos.CMsgGCCStrike15_v2_ClientRequestPlayersProfile, {
account_id: steamid.accountid,
request_level: 32
});
if (callback) {
this.once('playersProfile#' + steamid.getSteamID64(), callback);
} else {
// Return Promise when no callback provided
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('playersProfile#' + steamid.getSteamID64(), resolve);
reject(new Error(`Request players profile timed out for SteamID: ${steamid.getSteamID64()}`));
}, this._profileTimeout || 10000);
this.once('playersProfile#' + steamid.getSteamID64(), (profile) => {
clearTimeout(timeout);
resolve(profile);
});
});
}
};
/**
* Rename an item in your inventory using a name tag.
* @param {int} nameTagId
* @param {int} itemId
* @param {string} name
*/
NodeCS2.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
*/
NodeCS2.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
*/
NodeCS2.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
*/
NodeCS2.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
*/
NodeCS2.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 - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.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) {
const error = new Error(`No casket matching ID ${casketId} was found`);
if (callback) {
callback(error);
return;
}
return Promise.reject(error);
}
if (!casketItem.casket_contained_item_count) {
// Casket is empty, I guess
if (callback) {
callback(null, []);
return;
}
return Promise.resolve([]);
}
let loadedItems = this.inventory.filter(item => item.casket_id == casketId);
if (loadedItems.length == casketItem.casket_contained_item_count) {
if (callback) {
callback(null, loadedItems);
return;
}
return Promise.resolve(loadedItems);
}
// 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 timeout in case the GC isn't being cooperative (configurable via _casketTimeout)
let timedOut = false;
let timeout;
let customizationNotification;
if (callback) {
// Callback-based API
timeout = setTimeout(() => {
if (timedOut) {
return;
}
timedOut = true;
this.off('itemCustomizationNotification', customizationNotification);
callback(new Error('Loading casket contents timed out'));
}, this._casketTimeout || 30000);
customizationNotification = (itemIds, notificationType) => {
if (timedOut) {
this.off('itemCustomizationNotification', customizationNotification);
return;
}
if (itemIds[0] != casketId || notificationType != NodeCS2.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);
} else {
// Promise-based API
return new Promise((resolve, reject) => {
timeout = setTimeout(() => {
if (timedOut) {
return;
}
timedOut = true;
this.off('itemCustomizationNotification', customizationNotification);
reject(new Error('Loading casket contents timed out'));
}, this._casketTimeout || 30000);
customizationNotification = (itemIds, notificationType) => {
if (timedOut) {
this.off('itemCustomizationNotification', customizationNotification);
return;
}
if (itemIds[0] != casketId || notificationType != NodeCS2.ItemCustomizationNotification.CasketContents) {
return;
}
// This is our casket, and it's the correct notification
clearTimeout(timeout);
timedOut = true;
this.off('itemCustomizationNotification', customizationNotification);
resolve(this.inventory.filter(item => item.casket_id == casketId));
};
this.on('itemCustomizationNotification', customizationNotification);
});
}
};
// ============================================================================
// Volatile Items
// ============================================================================
/**
* Load the contents of a volatile item (rental item, temporary item).
* @param {int} volatileItemId - The ID of the volatile item
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.loadVolatileItemContents = function(volatileItemId, callback) {
// Similar to getCasketContents, but for volatile items
let volatileItem = this.inventory.find(item => item.id == volatileItemId);
if (!volatileItem) {
const error = new Error(`No volatile item matching ID ${volatileItemId} was found`);
if (callback) {
callback(error);
return;
}
return Promise.reject(error);
}
// We need to load volatile item contents from the GC
this._send(Language.VolatileItemLoadContents, Protos.CMsgCasketItem, {
casket_item_id: volatileItemId,
item_item_id: volatileItemId
});
// Set a timeout (configurable via _volatileItemTimeout)
let timedOut = false;
let timeout;
let customizationNotification;
if (callback) {
timeout = setTimeout(() => {
if (timedOut) {
return;
}
timedOut = true;
this.off('itemCustomizationNotification', customizationNotification);
callback(new Error('Loading volatile item contents timed out'));
}, this._volatileItemTimeout || 30000);
customizationNotification = (itemIds, notificationType) => {
if (timedOut) {
this.off('itemCustomizationNotification', customizationNotification);
return;
}
// Check for volatile item notification (may use VolatileItemClaimReward notification type)
if (itemIds[0] != volatileItemId) {
return;
}
clearTimeout(timeout);
timedOut = true;
this.off('itemCustomizationNotification', customizationNotification);
callback(null, this.inventory.filter(item => item.volatile_item_id == volatileItemId || item.id == volatileItemId));
};
this.on('itemCustomizationNotification', customizationNotification);
} else {
return new Promise((resolve, reject) => {
timeout = setTimeout(() => {
if (timedOut) {
return;
}
timedOut = true;
this.off('itemCustomizationNotification', customizationNotification);
reject(new Error('Loading volatile item contents timed out'));
}, this._volatileItemTimeout || 30000);
customizationNotification = (itemIds, notificationType) => {
if (timedOut) {
this.off('itemCustomizationNotification', customizationNotification);
return;
}
if (itemIds[0] != volatileItemId) {
return;
}
clearTimeout(timeout);
timedOut = true;
this.off('itemCustomizationNotification', customizationNotification);
resolve(this.inventory.filter(item => item.volatile_item_id == volatileItemId || item.id == volatileItemId));
};
this.on('itemCustomizationNotification', customizationNotification);
});
}
};
/**
* Claim a reward from a volatile item.
* Note: Response comes via ItemCustomizationNotification.
* @param {int} defindex - The definition index of the volatile item
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.claimVolatileItemReward = function(defindex, callback) {
// VolatileItemClaimReward doesn't have a protobuf message definition
// Send as empty ByteBuffer - response comes via ItemCustomizationNotification
let buffer = new ByteBuffer(4, ByteBuffer.LITTLE_ENDIAN);
buffer.writeUint32(defindex);
this._send(Language.VolatileItemClaimReward, null, buffer);
if (callback) {
// Listen for itemCustomizationNotification with VolatileItemClaimReward type
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Claiming volatile item reward timed out'));
}, this._volatileItemTimeout || 30000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ClientRedeemFreeReward) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Claiming volatile item reward timed out'));
}, this._volatileItemTimeout || 30000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ClientRedeemFreeReward) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
/**
* Acknowledge rental expiration for a crate/item.
* @param {int} crateItemId - The ID of the crate/item
*/
NodeCS2.prototype.acknowledgeRentalExpiration = function(crateItemId) {
this._send(Language.AcknowledgeRentalExpiration, Protos.CMsgAcknowledgeRentalExpiration, {
crate_item_id: crateItemId
});
};
// ============================================================================
// Recurring Missions
// ============================================================================
/**
* Request the recurring mission schedule.
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.requestRecurringMissionSchedule = function(callback) {
this._send(Language.RequestRecurringMissionSchedule, Protos.CMsgRequestRecurringMissionSchedule, {});
if (callback) {
// Listen for RecurringMissionSchema response
let timeout = setTimeout(() => {
this.removeListener('recurringMissionSchema', schemaListener);
callback(new Error('Requesting recurring mission schedule timed out'));
}, this._missionTimeout || 10000);
let schemaListener = (schema) => {
clearTimeout(timeout);
this.removeListener('recurringMissionSchema', schemaListener);
callback(null, schema);
};
this.once('recurringMissionSchema', schemaListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('recurringMissionSchema', schemaListener);
reject(new Error('Requesting recurring mission schedule timed out'));
}, this._missionTimeout || 10000);
let schemaListener = (schema) => {
clearTimeout(timeout);
this.removeListener('recurringMissionSchema', schemaListener);
resolve(schema);
};
this.once('recurringMissionSchema', schemaListener);
});
}
};
// ============================================================================
// XP Shop & Rewards
// ============================================================================
/**
* Acknowledge XP shop tracks.
*/
NodeCS2.prototype.acknowledgeXPShopTracks = function() {
this._send(Language.Client2GcAckXPShopTracks, Protos.CMsgGCCStrike15_v2_Client2GcAckXPShopTracks, {});
};
/**
* Redeem a free reward.
* @param {int} generationTime - Generation time of the reward
* @param {int} redeemableBalance - Redeemable balance
* @param {int[]} items - Array of item IDs
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.redeemFreeReward = function(generationTime, redeemableBalance, items, callback) {
this._send(Language.ClientRedeemFreeReward, Protos.CMsgGCCstrike15_v2_ClientRedeemFreeReward, {
generation_time: generationTime,
redeemable_balance: redeemableBalance,
items: items || []
});
if (callback) {
// Listen for itemCustomizationNotification
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Redeeming free reward timed out'));
}, this._rewardTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ClientRedeemFreeReward) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Redeeming free reward timed out'));
}, this._rewardTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ClientRedeemFreeReward) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
/**
* Redeem a mission reward.
* @param {int} campaignId - Campaign ID
* @param {int} redeemId - Redeem ID
* @param {int} redeemableBalance - Redeemable balance
* @param {int} expectedCost - Expected cost
* @param {int} bidControl - Bid control (optional)
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.redeemMissionReward = function(campaignId, redeemId, redeemableBalance, expectedCost, bidControl, callback) {
// Handle optional bidControl parameter
if (typeof bidControl === 'function') {
callback = bidControl;
bidControl = undefined;
}
this._send(Language.ClientRedeemMissionReward, Protos.CMsgGCCstrike15_v2_ClientRedeemMissionReward, {
campaign_id: campaignId,
redeem_id: redeemId,
redeemable_balance: redeemableBalance,
expected_cost: expectedCost,
bid_control: bidControl
});
if (callback) {
// Listen for itemCustomizationNotification
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Redeeming mission reward timed out'));
}, this._rewardTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ClientRedeemMissionReward) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Redeeming mission reward timed out'));
}, this._rewardTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ClientRedeemMissionReward) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
// ============================================================================
// Premier Season & Leaderboards
// ============================================================================
/**
* Set player leaderboard safe name.
* @param {string} leaderboardSafeName - The safe name for leaderboards
*/
NodeCS2.prototype.setLeaderboardSafeName = function(leaderboardSafeName) {
if (!leaderboardSafeName || typeof leaderboardSafeName !== 'string') {
throw new Error('leaderboardSafeName must be a non-empty string');
}
this._send(Language.SetPlayerLeaderboardSafeName, Protos.CMsgGCCStrike15_v2_SetPlayerLeaderboardSafeName, {
leaderboard_safe_name: leaderboardSafeName
});
};
// ============================================================================
// Crate Opening
// ============================================================================
/**
* Open a crate.
* @param {int} toolItemId - The ID of the tool (key) item
* @param {int} subjectItemId - The ID of the crate item
* @param {boolean} forRental - Whether this is for a rental (optional)
* @param {int} pointsRemaining - Points remaining (optional)
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.openCrate = function(toolItemId, subjectItemId, forRental, pointsRemaining, callback) {
// Handle optional parameters
if (typeof forRental === 'function') {
callback = forRental;
forRental = undefined;
pointsRemaining = undefined;
} else if (typeof pointsRemaining === 'function') {
callback = pointsRemaining;
pointsRemaining = undefined;
}
this._send(Language.OpenCrate, Protos.CMsgOpenCrate, {
tool_item_id: toolItemId,
subject_item_id: subjectItemId,
for_rental: forRental,
points_remaining: pointsRemaining
});
if (callback) {
// Listen for ItemCustomizationNotification with UnlockCrate type
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Opening crate timed out'));
}, this._crateTimeout || 30000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.UnlockCrate) {
// Check if this is our crate
if (itemIds.indexOf(subjectItemId.toString()) !== -1 || itemIds.indexOf(subjectItemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Opening crate timed out'));
}, this._crateTimeout || 30000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.UnlockCrate) {
// Check if this is our crate
if (itemIds.indexOf(subjectItemId.toString()) !== -1 || itemIds.indexOf(subjectItemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
// ============================================================================
// Sticker Operations
// ============================================================================
/**
* Extract a sticker from an item.
* @param {int} itemId - The ID of the item with the sticker
* @param {int} stickerSlot - The slot number of the sticker to extract
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.extractSticker = function(itemId, stickerSlot, callback) {
// Send request via ItemCustomizationNotification
this._send(Language.ItemCustomizationNotification, Protos.CMsgGCItemCustomizationNotification, {
item_id: [itemId],
request: NodeCS2.ItemCustomizationNotification.ExtractSticker,
extra_data: stickerSlot !== undefined ? [stickerSlot] : []
});
if (callback) {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Extracting sticker timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ExtractSticker) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Extracting sticker timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ExtractSticker) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
/**
* Encapsulate a sticker.
* @param {int} stickerId - The ID of the sticker to encapsulate
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.encapsulateSticker = function(stickerId, callback) {
// Send request via ItemCustomizationNotification
this._send(Language.ItemCustomizationNotification, Protos.CMsgGCItemCustomizationNotification, {
item_id: [stickerId],
request: NodeCS2.ItemCustomizationNotification.EncapsulateSticker,
extra_data: []
});
if (callback) {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Encapsulating sticker timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.EncapsulateSticker) {
if (itemIds.indexOf(stickerId.toString()) !== -1 || itemIds.indexOf(stickerId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Encapsulating sticker timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.EncapsulateSticker) {
if (itemIds.indexOf(stickerId.toString()) !== -1 || itemIds.indexOf(stickerId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
// ============================================================================
// Patch Operations
// ============================================================================
/**
* Apply a patch to an item.
* @param {int} itemId - The ID of the item to apply patch to
* @param {int} patchId - The ID of the patch item
* @param {int} patchSlot - The slot number for the patch (optional)
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.applyPatch = function(itemId, patchId, patchSlot, callback) {
if (typeof patchSlot === 'function') {
callback = patchSlot;
patchSlot = undefined;
}
// Send request via ItemCustomizationNotification
this._send(Language.ItemCustomizationNotification, Protos.CMsgGCItemCustomizationNotification, {
item_id: [itemId, patchId],
request: NodeCS2.ItemCustomizationNotification.ApplyPatch,
extra_data: patchSlot !== undefined ? [patchSlot] : []
});
if (callback) {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Applying patch timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ApplyPatch) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Applying patch timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ApplyPatch) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
/**
* Remove a patch from an item.
* @param {int} itemId - The ID of the item with the patch
* @param {int} patchSlot - The slot number of the patch to remove
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.removePatch = function(itemId, patchSlot, callback) {
// Send request via ItemCustomizationNotification
this._send(Language.ItemCustomizationNotification, Protos.CMsgGCItemCustomizationNotification, {
item_id: [itemId],
request: NodeCS2.ItemCustomizationNotification.RemovePatch,
extra_data: patchSlot !== undefined ? [patchSlot] : []
});
if (callback) {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Removing patch timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.RemovePatch) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Removing patch timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.RemovePatch) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
// ============================================================================
// Keychain Operations
// ============================================================================
/**
* Apply a keychain to an item.
* @param {int} itemId - The ID of the item to apply keychain to
* @param {int} keychainId - The ID of the keychain item
* @param {int} keychainSlot - The slot number for the keychain (optional)
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.applyKeychain = function(itemId, keychainId, keychainSlot, callback) {
if (typeof keychainSlot === 'function') {
callback = keychainSlot;
keychainSlot = undefined;
}
// Send request via ItemCustomizationNotification
this._send(Language.ItemCustomizationNotification, Protos.CMsgGCItemCustomizationNotification, {
item_id: [itemId, keychainId],
request: NodeCS2.ItemCustomizationNotification.ApplyKeychain,
extra_data: keychainSlot !== undefined ? [keychainSlot] : []
});
if (callback) {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Applying keychain timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ApplyKeychain) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Applying keychain timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.ApplyKeychain) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
/**
* Remove a keychain from an item.
* @param {int} itemId - The ID of the item with the keychain
* @param {int} keychainSlot - The slot number of the keychain to remove
* @param {function} callback - Optional callback. If not provided, returns a Promise.
* @returns {Promise|undefined} Returns a Promise if no callback is provided
*/
NodeCS2.prototype.removeKeychain = function(itemId, keychainSlot, callback) {
// Send request via ItemCustomizationNotification
this._send(Language.ItemCustomizationNotification, Protos.CMsgGCItemCustomizationNotification, {
item_id: [itemId],
request: NodeCS2.ItemCustomizationNotification.RemoveKeychain,
extra_data: keychainSlot !== undefined ? [keychainSlot] : []
});
if (callback) {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
callback(new Error('Removing keychain timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.RemoveKeychain) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
callback(null, itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
} else {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
this.removeListener('itemCustomizationNotification', notificationListener);
reject(new Error('Removing keychain timed out'));
}, this._stickerTimeout || 10000);
let notificationListener = (itemIds, notificationType) => {
if (notificationType == NodeCS2.ItemCustomizationNotification.RemoveKeychain) {
if (itemIds.indexOf(itemId.toString()) !== -1 || itemIds.indexOf(itemId) !== -1) {
clearTimeout(timeout);
this.removeListener('itemCustomizationNotification', notificationListener);
resolve(itemIds);
}
}
};
this.on('itemCustomizationNotification', notificationListener);
});
}
};
NodeCS2.prototype._handlers = {};
require('./enums.js');
require('./handlers.js');