dota2-fork
Version:
A node-steam plugin for Dota 2. Fork with fresh updates
372 lines (327 loc) • 13.8 kB
JavaScript
/**
* Dota 2 module
* @module Dota2
*/
/**
* A Long class for representing a 64 bit two's-complement integer value
* derived from the Closure Library for stand-alone use and extended with unsigned support.
* @external Long
* @see {@link https://www.npmjs.com/package/long|long} npm package
*/
/**
* The Steam for Node JS package, allowing interaction with Steam.
* @external steam
* @see {@link https://www.npmjs.com/package/steam|steam} npm package
*/
const util = require("util");
const Long = require("long");
const steam_resources = require("steam-resources-fork");
const { createLogger, format, transports } = require('winston');
const { EventEmitter } = require('events');
const DOTA_APP_ID = 570;
var Dota2 = exports;
/**
* Protobuf schema created by Steam Resources. This is an alias of `steam.GC.Dota.Internal`.
* This object can be used to obtain Dota2 specific protobuf types.
* Object types can be created by `new Dota2.schema.<TypeName>(payload :Object);`.
* Enum types can be referenced by `Dota2.schema.<EnumName>`, which returns an object array representing the enum.
* @alias module:Dota2.schema
*/
Dota2.schema = steam_resources.GC.Dota.Internal;
/**
* The Dota 2 client that communicates with the GC
* @class
* @alias module:Dota2.Dota2Client
* @param {Object} steamUser - node-steam-user client instance
* @param {boolean} debug - Print debug information to console
* @param {boolean} debugMore - Print even more debug information to console
* @extends {EventEmitter} EventEmitter
* @fires module:Dota2.Dota2Client#event:ready
* @fires module:Dota2.Dota2Client#event:unhandled
* @fires module:Dota2.Dota2Client#event:hellotimeout
* @fires module:Dota2.Dota2Client#event:popup
* @fires module:Dota2.Dota2Client#event:sourceTVGamesData
* @fires module:Dota2.Dota2Client#event:inventoryUpdate
* @fires module:Dota2.Dota2Client#event:practiceLobbyUpdate
* @fires module:Dota2.Dota2Client#event:practiceLobbyCleared
* @fires module:Dota2.Dota2Client#event:lobbyInviteUpdate
* @fires module:Dota2.Dota2Client#event:lobbyInviteCleared
* @fires module:Dota2.Dota2Client#event:practiceLobbyJoinResponse
* @fires module:Dota2.Dota2Client#event:practiceLobbyListData
* @fires module:Dota2.Dota2Client#event:practiceLobbyResponse
* @fires module:Dota2.Dota2Client#event:lobbyDestroyed
* @fires module:Dota2.Dota2Client#event:friendPracticeLobbyListData
* @fires module:Dota2.Dota2Client#event:inviteCreated
* @fires module:Dota2.Dota2Client#event:partyUpdate
* @fires module:Dota2.Dota2Client#event:partyCleared
* @fires module:Dota2.Dota2Client#event:partyInviteUpdate
* @fires module:Dota2.Dota2Client#event:partyInviteCleared
* @fires module:Dota2.Dota2Client#event:joinableCustomGameModes
* @fires module:Dota2.Dota2Client#event:chatChannelsData
* @fires module:Dota2.Dota2Client#event:chatJoin
* @fires module:Dota2.Dota2Client#event:chatJoined
* @fires module:Dota2.Dota2Client#event:chatLeave
* @fires module:Dota2.Dota2Client#event:chatMessage
* @fires module:Dota2.Dota2Client#event:profileCardData
* @fires module:Dota2.Dota2Client#event:playerMatchHistoryData
* @fires module:Dota2.Dota2Client#event:playerInfoData
* @fires module:Dota2.Dota2Client#event:playerStatsData
* @fires module:Dota2.Dota2Client#event:trophyListData
* @fires module:Dota2.Dota2Client#event:hallOfFameData
* @fires module:Dota2.Dota2Client#event:playerCardRoster
* @fires module:Dota2.Dota2Client#event:playerCardDrafted
* @fires module:Dota2.Dota2Client#event:liveLeagueGamesUpdate
* @fires module:Dota2.Dota2Client#event:topLeagueMatchesData
* @fires module:Dota2.Dota2Client#event:teamData
* @fires module:Dota2.Dota2Client#event:matchesData
* @fires module:Dota2.Dota2Client#event:matchDetailsData
* @fires module:Dota2.Dota2Client#event:matchMinimalDetailsData
* @fires module:Dota2.Dota2Client#event:matchmakingStatsData
* @fires module:Dota2.Dota2Client#event:topFriendMatchesData
* @fires module:Dota2.Dota2Client#event:tipResponse
* @fires module:Dota2.Dota2Client#event:tipped
*/
Dota2.Dota2Client = function Dota2Client(steamUser, debug, debugMore) {
EventEmitter.call(this);
this.debug = debug || false;
this.debugMore = debugMore || false;
/**
* The logger used to write debug messages. This is a WinstonJS logger,
* feel free to configure it as you like
* @type {winston.Logger}
*/
this.Logger = createLogger({
transports: [new transports.Console()],
format: format.combine(
format.colorize(),
format.timestamp({
format: 'D MMM HH:mm:ss'
}),
format.printf(nfo => {
return `${nfo.timestamp} - ${nfo.message}`
})
)
});
if(debug) this.Logger.level = "debug";
if(debugMore) this.Logger.level = "silly";
/** The current state of the bot's inventory. Contains cosmetics, player cards, ...
* @type {CSOEconItem[]}
*/
this.Inventory = [];
/** The chat channels the bot has joined
* @type {CMsgDOTAJoinChatChannelResponse[]}
*/
this.chatChannels = []; // Map channel names to channel data.
/** The lobby the bot is currently in. Falsy if the bot isn't in a lobby.
* @type {CSODOTALobby}
*/
this.Lobby = null;
/** The currently active lobby invitation. Falsy if the bot has not been invited.
* @type {CSODOTALobbyInvite}
*/
this.LobbyInvite = null;
/** The party the bot is currently in. Falsy if the bot isn't in a party.
* @type {CSODOTAParty}
*/
this.Party = null;
/** The currently active party invitation. Falsy if the bot has not been invited.
* @type {CSODOTAPartyInvite}
*/
this.PartyInvite = null;
this._user = steamUser;
this._appid = DOTA_APP_ID;
this._gcReady = false;
this._gcClientHelloIntervalId = null;
this._gcConnectionStatus = Dota2.schema.GCConnectionStatus.GCConnectionStatus_NO_SESSION;
this._protoBufHeader = {
"msg": "",
"proto": {
"client_steam_id": this._user.steamID,
"source_app_id": this._appid
}
};
this._user.on('receivedFromGC', (appId, msgType, payload) => {
this.Logger.debug("Dota2 fromGC: " + Dota2._getMessageName(msgType));
if (msgType in this._handlers) {
this._handlers[msgType].call(this, payload);
} else {
this.emit('unhandled', msgType, Dota2._getMessageName(msgType));
}
});
this._sendClientHello = () => {
if (this._gcReady) {
if (this._gcClientHelloIntervalId) {
clearInterval(this._gcClientHelloIntervalId);
this._gcClientHelloIntervalId = null;
}
return;
}
if (this._gcClientHelloCount > 10) {
this.Logger.warn("ClientHello has taken longer than 30 seconds! Reporting timeout...")
this._gcClientHelloCount = 0;
this.emit("hellotimeout");
}
this.Logger.debug("Sending ClientHello");
if (!this._user) {
this.Logger.error("Where the fuck is _gc?");
} else {
this._user.sendToGC(
this._appid,
Dota2.schema.EGCBaseClientMsg.k_EMsgGCClientHello,
{},
new Dota2.schema.CMsgClientHello({}).toBuffer()
);
}
this._gcClientHelloCount++;
};
};
util.inherits(Dota2.Dota2Client, EventEmitter);
// Methods
/**
* Converts a 64bit Steam ID to a Dota2 account ID by deleting the 32 most significant bits
* @alias module:Dota2.Dota2Client.ToAccountID
* @param {string} steamID - String representation of a 64bit Steam ID
* @returns {number} Dota2 account ID corresponding with steamID
*/
Dota2.Dota2Client.prototype.ToAccountID = function(steamID) {
return new Long.fromString(""+steamID).sub('76561197960265728').toNumber();
};
/**
* Converts a Dota2 account ID to a 64bit Steam ID
* @alias module:Dota2.Dota2Client.ToSteamID
* @param {string} accid - String representation of a Dota 2 account ID
* @returns {external:Long} 64bit Steam ID corresponding to the given Dota 2 account ID
*/
Dota2.Dota2Client.prototype.ToSteamID = function(accid) {
return new Long.fromString(accid+"").add('76561197960265728');
};
/**
* Reports to Steam that you're playing Dota 2, and then initiates communication with the Game Coordinator.
* @alias module:Dota2.Dota2Client#launch
*/
Dota2.Dota2Client.prototype.launch = function() {
/* Reports to Steam that we are running Dota 2. Initiates communication with GC with EMsgGCClientHello */
this.Logger.debug("Launching Dota 2");
this.AccountID = this.ToAccountID(this._user.steamID);
this.Party = null;
this.Lobby = null;
this.PartyInvite = null;
this.Inventory = [];
this.chatChannels = [];
this._user.gamesPlayed([this._appid]);
// Keep knocking on the GCs door until it accepts us.
// This is very bad practice and quite trackable.
// The real client tends to send only one of these.
// Really we should just send one when the connection status is GC online
this._gcClientHelloCount = 0;
this._gcClientHelloIntervalId = setInterval(this._sendClientHello, 6000);
//Also immediately send clienthello
setTimeout(this._sendClientHello, 1000);
};
/**
* Stop sending a heartbeat to the GC and report to steam you're no longer playing Dota 2
* @alias module:Dota2.Dota2Client#exit
*/
Dota2.Dota2Client.prototype.exit = function() {
/* Reports to Steam we are not running any apps. */
this.Logger.debug("Exiting Dota 2");
/* stop knocking if exit comes before ready event */
if (this._gcClientHelloIntervalId) {
clearInterval(this._gcClientHelloIntervalId);
this._gcClientHelloIntervalId = null;
}
this._gcReady = false;
if (this._user.steamID) {
this._user.gamesPlayed([]);
}
};
Dota2.Dota2Client.prototype.sendToGC = function(type, payload, handler, callback) {
if (!this._gcReady) {
this.Logger.warn("GC not ready, please listen for the 'ready' event.");
if (callback) callback(-1, null);
return null;
}
this._user.sendToGC(
this._appid,
type,
{},
payload.toBuffer(),
(appId, type, message) => {
Dota2._convertCallback(this, message, handler, callback)
}
);
}
// Events
/**
* Emitted when the connection with the GC has been established
* and the client is ready to take requests.
* @event module:Dota2.Dota2Client#ready
*/
/**
* Emitted when the GC sends a message that isn't yet treated by the library.
* @event module:Dota2.Dota2Client#unhandled
* @param {number} kMsg - Proto message type ID
* @param {string} kMsg_name - Proto message type name
*/
/**
* Emitted when the connection with the GC takes longer than 30s
* @event module:Dota2.Dota2Client#hellotimeout
*/
// Handlers
var handlers = Dota2.Dota2Client.prototype._handlers = {};
handlers[Dota2.schema.EGCBaseClientMsg.k_EMsgGCClientWelcome] = function clientWelcomeHandler(message) {
/* Response to our k_EMsgGCClientHello, now we can execute other GC commands. */
// Only execute if _gcClientHelloIntervalID, otherwise it's already been handled (and we don't want to emit multiple 'ready');
if (this._gcClientHelloIntervalId) {
clearInterval(this._gcClientHelloIntervalId);
this._gcClientHelloIntervalId = null;
}
this.Logger.debug("Received client welcome.");
// Parse any caches
this._gcReady = true;
this._handleWelcomeCaches(message);
this.emit("ready");
};
handlers[Dota2.schema.EGCBaseClientMsg.k_EMsgGCClientConnectionStatus] = function gcClientConnectionStatus(message) {
/* Catch and handle changes in connection status, cuz reasons u know. */
var status = Dota2.schema.CMsgConnectionStatus.decode(message).status;
if (status) this._gcConnectionStatus = status;
switch (status) {
case Dota2.schema.GCConnectionStatus.GCConnectionStatus_HAVE_SESSION:
this.Logger.debug("GC Connection Status regained.");
// Only execute if _gcClientHelloIntervalID, otherwise it's already been handled (and we don't want to emit multiple 'ready');
if (this._gcClientHelloIntervalId) {
clearInterval(this._gcClientHelloIntervalId);
this._gcClientHelloIntervalId = null;
this._gcReady = true;
this.emit("ready");
}
break;
default:
this.Logger.debug(Object.keys(Dota2.schema.GCConnectionStatus).find(key => Dota2.schema.GCConnectionStatus[key] === status));
this.Logger.debug("GC Connection Status unreliable - " + status);
// Only execute if !_gcClientHelloIntervalID, otherwise it's already been handled (and we don't want to emit multiple 'unready');
if (!this._gcClientHelloIntervalId) {
this._gcClientHelloIntervalId = setInterval(this._sendClientHello, 5000); // Continually try regain GC session
this._gcReady = false;
this.emit("unready");
}
break;
}
};
require("./handlers/cache");
require("./handlers/inventory");
require("./handlers/chat");
require("./handlers/guild");
require("./handlers/community");
require("./handlers/helper");
require("./handlers/match");
require("./handlers/lobbies");
require("./handlers/parties");
require("./handlers/leagues");
require("./handlers/sourcetv");
require("./handlers/team");
require("./handlers/custom");
require("./handlers/general");
require("./handlers/fantasy");
require("./handlers/compendium");