<|start_filename|>components/logon.js<|end_filename|>
var Steam = require('steam-client');
var SteamGameServer = require('../index.js');
var SteamID = require('steamid');
var Helpers = require('./helpers.js');
var Schema = require('./protobufs.js');
var Crypto = require('crypto');
var ByteBuffer = require('bytebuffer');

SteamGameServer.prototype.logOn = function(details) {
	if (this.client.loggedOn) {
		throw new Error("Already logged on, cannot log on again");
	}

	this.steamID = null;
	this.secure = false;
	this.users = {};

	this._loggingOff = false;

	if(details !== true) {
		// We're not logging on with saved details
		details = details || {};

		var gamedirs = require('../resources/gamedirs.json');

		this.appID = details.appID;
		this.gameDir = details.gameDirectory || gamedirs[details.appID];
		this.gameVersion = details.gameVersion;
		this.port = details.gamePort || 27015;
		this.queryPort = details.queryPort || 27015;

		if (!this.appID || !this.gameDir || !this.gameVersion) {
			throw new Error("AppID, gameDirectory, and gameVersion are required to log on to Steam");
		}

		this.flags = SteamGameServer.EServerFlags.Private;
		if (details.secure) {
			this.flags |= SteamGameServer.EServerFlags.Secure;
		}

		if (details.dedicated !== false) {
			// default true
			this.flags |= SteamGameServer.EServerFlags.Dedicated;
		}

		if (['freebsd', 'linux', 'openbsd'].indexOf(require('os').platform()) != -1) {
			this.flags |= SteamGameServer.EServerFlags.Linux;
		}

		// Find private IP
		var privateIp = null;
		var interfaces = require('os').networkInterfaces();
		var i, j;
		ifaceLoop:
		for (i in interfaces) {
			if (interfaces.hasOwnProperty(i)) {
				for (j = 0; j < interfaces[i]; j++) {
					if (!interfaces[i][j].internal && interfaces[i][j].family == 'IPv4') {
						privateIp = Helpers.ipStringToInt(interfaces[i][j].address);
						break ifaceLoop;
					}
				}
			}
		}

		this._logOnDetails = {
			"game_server_token": details.token || "",
			"obfustucated_private_ip": privateIp ? (privateIp ^ 0xBAADF00D) : 0,
			"protocol_version": 65575
		};
	}

	var machineID;

	var anonLogin = !this._logOnDetails.game_server_token;
	if (!anonLogin) {
		this._logOnDetails.game_server_app_id = this.appID;
		this._logOnDetails.supports_rate_limit_response = true;

		if (!this._logOnDetails.machine_id && this.options.machineIdType == SteamGameServer.EMachineIDType.PersistentRandom) {
			filenames.push('machineid.bin');
		}
	}

	// Read the required files
	var filenames = [];

	if(!Steam['__SteamGameServerServersSet__']) {
		filenames.push('servers.json');
	}

	if (!this._logOnDetails.cell_id) {
		// Some people might be redirecting their storage to a database and running across multiple servers in multiple regions
		// Let's account for this by saving cellid by a "machine ID" so different boxes will store different cellids
		filenames.push('cellid-' + Helpers.getInternalMachineID() + '.txt');
	}

	this.storage.readFiles(filenames, (err, files) => {
		files = files || [];

		files.forEach((file) => {
			if (file.filename == 'servers.json' && file.contents) {
				try {
					Steam.servers = JSON.parse(file.contents.toString('utf8'));
					Steam['__SteamGameServerServersSet__'] = true;
				} catch(e) {
					// don't care
				}
			}

			if (file.filename.match(/^cellid/) && file.contents) {
				var cellID = parseInt(file.contents.toString('utf8'), 10);
				if(!isNaN(cellID)) {
					this._logOnDetails.cell_id = cellID;
				}
			}

			if (file.filename == 'machineid.bin' && file.contents) {
				machineID = file.contents;
			}
		});

		if (!anonLogin && !this._logOnDetails.machine_id) {
			this._logOnDetails.machine_id = this._getMachineID(machineID);
		}

		// Do the login
		var sid = new SteamID();
		sid.universe = SteamID.Universe.PUBLIC;
		sid.type = anonLogin ? SteamID.Type.ANON_GAMESERVER : SteamID.Type.GAMESERVER;
		sid.instance = SteamID.Instance.ALL;
		sid.accountid = 0;
		this.client.steamID = sid.getSteamID64();

		if (this.client.connected) {
			onConnected.call(this);
		} else {
			this.client.connect();

			this._onConnected = onConnected.bind(this);
			this.client.once('connected', this._onConnected);
		}
	});
};

function onConnected() {
	console.log(this._logOnDetails);
	if (this.client.constructor.name === 'CMClient') {
		// We need to use this since CMClient defines the protocol version itself
		this.client.logOn(this._logOnDetails);
	} else {
		this._send(this._logOnDetails.game_server_token ? SteamGameServer.EMsg.ClientLogonGameServer : SteamGameServer.EMsg.ClientLogon, this._logOnDetails);
	}
}

SteamGameServer.prototype.logOff = SteamGameServer.prototype.disconnect = function(suppressLogoff) {
	if (this._onConnected) {
		this.client.removeListener('connected', this._onConnected);
		this._onConnected = null;
	}

	if (this.client.connected && !suppressLogoff) {
		this._loggingOff = true;
		this._send(SteamGameServer.EMsg.ClientLogOff, {});

		var timeout = setTimeout(() => {
			this.emit('disconnected', 0, "Logged off");
			this._loggingOff = false;
			this.client.disconnect();
		}, 4000);

		this.once('disconnected', (eresult) => {
			clearTimeout(timeout);
		});
	} else {
		this.client.disconnect();
	}
};

SteamGameServer.prototype._getMachineID = function(localFile) {
	if (!this._logOnDetails.game_server_token || this.options.machineIdType == SteamGameServer.EMachineIDType.None) {
		// No machine IDs for anonymous logons
		return null;
	}

	// The user wants to use a random machine ID that's saved to dataDirectory
	if (this.options.machineIdType == SteamGameServer.EMachineIDType.PersistentRandom) {
		if (localFile) {
			return localFile;
		}

		var file = getRandomID();
		this.storage.writeFile('machineid.bin', file);
		return file;
	}

	// The user wants to use a machine ID that's generated off the account name
	if (this.options.machineIdType == SteamGameServer.EMachineIDType.AccountTokenGenerated) {
		return createMachineID(
			this.options.machineIdFormat[0].replace(/\{token\}/g, this._logOnDetails.game_server_token),
			this.options.machineIdFormat[1].replace(/\{token\}/g, this._logOnDetails.game_server_token),
			this.options.machineIdFormat[2].replace(/\{token\}/g, this._logOnDetails.game_server_token)
		);
	}

	// Default to random
	return getRandomID();

	function getRandomID() {
		return createMachineID(Math.random().toString(), Math.random().toString(), Math.random().toString());
	}
};

// Handlers

SteamGameServer.prototype._handlers[SteamGameServer.EMsg.ClientLogOnResponse] = function(body) {
	var self = this;
	switch(body.eresult) {
		case SteamGameServer.EResult.OK:
			this.steamID = new SteamID(body.client_supplied_steamid.toString());

			this.publicIP = Helpers.ipIntToString(body.public_ip);
			this.cellID = body.cell_id;

			this.storage.saveFile('cellid-' + Helpers.getInternalMachineID() + '.txt', body.cell_id.toString());

			// Send the GS info
			this._send(SteamGameServer.EMsg.GSServerType, {
				"app_id_served": this.appID,
				"flags": this.flags,
				"game_ip_address": 0,
				"game_port": this.port,
				"game_dir": this.gameDir,
				"game_version": this.gameVersion,
				"game_query_port": this.queryPort
			});

			this.emit('loggedOn', body);

			break;

		case SteamGameServer.EResult.ServiceUnavailable:
		case SteamGameServer.EResult.TryAnotherCM:
			this.emit('debug', 'Log on response: ' + (body.eresult == SteamGameServer.EResult.ServiceUnavailable ? "ServiceUnavailable" : "TryAnotherCM"));
			this.disconnect(true);

			setTimeout(function() {
				self.logOn(true);
			}, 1000);

			break;

		default:
			var result = SteamGameServer.EResult[body.eresult] || body.eresult;

			var error = new Error(result);
			error.eresult = body.eresult;
			this.disconnect(true);
			this.emit('error', error);
	}
};

SteamGameServer.prototype._handlers[SteamGameServer.EMsg.ClientLoggedOff] = function(body) {
	var msg = SteamGameServer.EResult[body.eresult] || body.eresult;

	this.emit('debug', 'Logged off: ' + msg);
	this._handleLogOff(body.eresult, msg);
};

SteamGameServer.prototype._handleLogOff = function(result, msg) {
	var fatal = true;

	if (this.options.autoRelogin && [0, SteamGameServer.EResult.Fail, SteamGameServer.EResult.NoConnection, SteamGameServer.EResult.ServiceUnavailable, SteamGameServer.EResult.TryAnotherCM].indexOf(result) != -1) {
		fatal = false;
	}

	delete this.publicIP;
	delete this.cellID;

	if (fatal && !this._loggingOff) {
		var e = new Error(msg);
		e.eresult = result;

		var steamID = this.steamID;
		this.disconnect(true);

		this.steamID = steamID;
		this.emit('error', e);
		this.steamID = null;
	} else {
		// Only emit "disconnected" if we were previously logged on
		if (this.steamID) {
			this.emit('disconnected', result, msg);
		}

		this.disconnect(true);

		if (!this._loggingOff) {
			setTimeout(() => {
				this.logOn(true);
			}, 1000);
		}

		this._loggingOff = false;
	}
};

SteamGameServer.prototype._handlers[SteamGameServer.EMsg.GSStatusReply] = function(body) {
	this.emit('vac', body.is_secure);
	this.secure = body.is_secure;
};

// Private functions

function createMachineID(val_bb3, val_ff2, val_3b3) {
	// Machine IDs are binary KV objects with root key MessageObject and three hashes named BB3, FF2, and 3B3.
	// I don't feel like writing a proper BinaryKV serializer, so this will work fine.

	var buffer = new ByteBuffer(155, ByteBuffer.LITTLE_ENDIAN);
	buffer.writeByte(0); // 1 byte, total 1
	buffer.writeCString("MessageObject"); // 14 bytes, total 15

	buffer.writeByte(1); // 1 byte, total 16
	buffer.writeCString("BB3"); // 4 bytes, total 20
	buffer.writeCString(sha1(val_bb3)); // 41 bytes, total 61

	buffer.writeByte(1); // 1 byte, total 62
	buffer.writeCString("FF2"); // 4 bytes, total 66
	buffer.writeCString(sha1(val_ff2)); // 41 bytes, total 107

	buffer.writeByte(1); // 1 byte, total 108
	buffer.writeCString("3B3"); // 4 bytes, total 112
	buffer.writeCString(sha1(val_3b3)); // 41 bytes, total 153

	buffer.writeByte(8); // 1 byte, total 154
	buffer.writeByte(8); // 1 byte, total 155

	return buffer.flip().toBuffer();

	function sha1(input) {
		var hash = Crypto.createHash('sha1');
		hash.update(input, 'utf8');
		return hash.digest('hex');
	}
}


<|start_filename|>enums/EResult.js<|end_filename|>
/**
 * @enum EResult
 */
module.exports = {
	"Invalid": 0,
	"OK": 1,
	"Fail": 2,
	"NoConnection": 3,
	"InvalidPassword": 5,
	"LoggedInElsewhere": 6,
	"InvalidProtocolVer": 7,
	"InvalidParam": 8,
	"FileNotFound": 9,
	"Busy": 10,
	"InvalidState": 11,
	"InvalidName": 12,
	"InvalidEmail": 13,
	"DuplicateName": 14,
	"AccessDenied": 15,
	"Timeout": 16,
	"Banned": 17,
	"AccountNotFound": 18,
	"InvalidSteamID": 19,
	"ServiceUnavailable": 20,
	"NotLoggedOn": 21,
	"Pending": 22,
	"EncryptionFailure": 23,
	"InsufficientPrivilege": 24,
	"LimitExceeded": 25,
	"Revoked": 26,
	"Expired": 27,
	"AlreadyRedeemed": 28,
	"DuplicateRequest": 29,
	"AlreadyOwned": 30,
	"IPNotFound": 31,
	"PersistFailed": 32,
	"LockingFailed": 33,
	"LogonSessionReplaced": 34,
	"ConnectFailed": 35,
	"HandshakeFailed": 36,
	"IOFailure": 37,
	"RemoteDisconnect": 38,
	"ShoppingCartNotFound": 39,
	"Blocked": 40,
	"Ignored": 41,
	"NoMatch": 42,
	"AccountDisabled": 43,
	"ServiceReadOnly": 44,
	"AccountNotFeatured": 45,
	"AdministratorOK": 46,
	"ContentVersion": 47,
	"TryAnotherCM": 48,
	"PasswordRequiredToKickSession": 49,
	"AlreadyLoggedInElsewhere": 50,
	"Suspended": 51,
	"Cancelled": 52,
	"DataCorruption": 53,
	"DiskFull": 54,
	"RemoteCallFailed": 55,
	"PasswordNotSet": 56, // obsolete "renamed to PasswordUnset"
	"PasswordUnset": 56,
	"ExternalAccountUnlinked": 57,
	"PSNTicketInvalid": 58,
	"ExternalAccountAlreadyLinked": 59,
	"RemoteFileConflict": 60,
	"IllegalPassword": 61,
	"SameAsPreviousValue": 62,
	"AccountLogonDenied": 63,
	"CannotUseOldPassword": 64,
	"InvalidLoginAuthCode": 65,
	"AccountLogonDeniedNoMailSent": 66, // obsolete "renamed to AccountLogonDeniedNoMail"
	"AccountLogonDeniedNoMail": 66,
	"HardwareNotCapableOfIPT": 67,
	"IPTInitError": 68,
	"ParentalControlRestricted": 69,
	"FacebookQueryError": 70,
	"ExpiredLoginAuthCode": 71,
	"IPLoginRestrictionFailed": 72,
	"AccountLocked": 73, // obsolete "renamed to AccountLockedDown"
	"AccountLockedDown": 73,
	"AccountLogonDeniedVerifiedEmailRequired": 74,
	"NoMatchingURL": 75,
	"BadResponse": 76,
	"RequirePasswordReEntry": 77,
	"ValueOutOfRange": 78,
	"UnexpectedError": 79,
	"Disabled": 80,
	"InvalidCEGSubmission": 81,
	"RestrictedDevice": 82,
	"RegionLocked": 83,
	"RateLimitExceeded": 84,
	"AccountLogonDeniedNeedTwoFactorCode": 85, // obsolete "renamed to AccountLoginDeniedNeedTwoFactor"
	"AccountLoginDeniedNeedTwoFactor": 85,
	"ItemOrEntryHasBeenDeleted": 86, // obsolete "renamed to ItemDeleted"
	"ItemDeleted": 86,
	"AccountLoginDeniedThrottle": 87,
	"TwoFactorCodeMismatch": 88,
	"TwoFactorActivationCodeMismatch": 89,
	"AccountAssociatedToMultiplePlayers": 90, // obsolete "renamed to AccountAssociatedToMultiplePartners"
	"AccountAssociatedToMultiplePartners": 90,
	"NotModified": 91,
	"NoMobileDeviceAvailable": 92, // obsolete "renamed to NoMobileDevice"
	"NoMobileDevice": 92,
	"TimeIsOutOfSync": 93, // obsolete "renamed to TimeNotSynced"
	"TimeNotSynced": 93,
	"SMSCodeFailed": 94,
	"TooManyAccountsAccessThisResource": 95, // obsolete "renamed to AccountLimitExceeded"
	"AccountLimitExceeded": 95,
	"AccountActivityLimitExceeded": 96,
	"PhoneActivityLimitExceeded": 97,
	"RefundToWallet": 98,
	"EmailSendFailure": 99,
	"NotSettled": 100,
	"NeedCaptcha": 101,
	"GSLTDenied": 102,
	"GSOwnerDenied": 103,
	"InvalidItemType": 104,
	"IPBanned": 105,
	"GSLTExpired": 106,

	// Value-to-name mapping for convenience
	"0": "Invalid",
	"1": "OK",
	"2": "Fail",
	"3": "NoConnection",
	"5": "InvalidPassword",
	"6": "LoggedInElsewhere",
	"7": "InvalidProtocolVer",
	"8": "InvalidParam",
	"9": "FileNotFound",
	"10": "Busy",
	"11": "InvalidState",
	"12": "InvalidName",
	"13": "InvalidEmail",
	"14": "DuplicateName",
	"15": "AccessDenied",
	"16": "Timeout",
	"17": "Banned",
	"18": "AccountNotFound",
	"19": "InvalidSteamID",
	"20": "ServiceUnavailable",
	"21": "NotLoggedOn",
	"22": "Pending",
	"23": "EncryptionFailure",
	"24": "InsufficientPrivilege",
	"25": "LimitExceeded",
	"26": "Revoked",
	"27": "Expired",
	"28": "AlreadyRedeemed",
	"29": "DuplicateRequest",
	"30": "AlreadyOwned",
	"31": "IPNotFound",
	"32": "PersistFailed",
	"33": "LockingFailed",
	"34": "LogonSessionReplaced",
	"35": "ConnectFailed",
	"36": "HandshakeFailed",
	"37": "IOFailure",
	"38": "RemoteDisconnect",
	"39": "ShoppingCartNotFound",
	"40": "Blocked",
	"41": "Ignored",
	"42": "NoMatch",
	"43": "AccountDisabled",
	"44": "ServiceReadOnly",
	"45": "AccountNotFeatured",
	"46": "AdministratorOK",
	"47": "ContentVersion",
	"48": "TryAnotherCM",
	"49": "PasswordRequiredToKickSession",
	"50": "AlreadyLoggedInElsewhere",
	"51": "Suspended",
	"52": "Cancelled",
	"53": "DataCorruption",
	"54": "DiskFull",
	"55": "RemoteCallFailed",
	"56": "PasswordUnset",
	"57": "ExternalAccountUnlinked",
	"58": "PSNTicketInvalid",
	"59": "ExternalAccountAlreadyLinked",
	"60": "RemoteFileConflict",
	"61": "IllegalPassword",
	"62": "SameAsPreviousValue",
	"63": "AccountLogonDenied",
	"64": "CannotUseOldPassword",
	"65": "InvalidLoginAuthCode",
	"66": "AccountLogonDeniedNoMail",
	"67": "HardwareNotCapableOfIPT",
	"68": "IPTInitError",
	"69": "ParentalControlRestricted",
	"70": "FacebookQueryError",
	"71": "ExpiredLoginAuthCode",
	"72": "IPLoginRestrictionFailed",
	"73": "AccountLockedDown",
	"74": "AccountLogonDeniedVerifiedEmailRequired",
	"75": "NoMatchingURL",
	"76": "BadResponse",
	"77": "RequirePasswordReEntry",
	"78": "ValueOutOfRange",
	"79": "UnexpectedError",
	"80": "Disabled",
	"81": "InvalidCEGSubmission",
	"82": "RestrictedDevice",
	"83": "RegionLocked",
	"84": "RateLimitExceeded",
	"85": "AccountLoginDeniedNeedTwoFactor",
	"86": "ItemDeleted",
	"87": "AccountLoginDeniedThrottle",
	"88": "TwoFactorCodeMismatch",
	"89": "TwoFactorActivationCodeMismatch",
	"90": "AccountAssociatedToMultiplePartners",
	"91": "NotModified",
	"92": "NoMobileDevice",
	"93": "TimeNotSynced",
	"94": "SMSCodeFailed",
	"95": "AccountLimitExceeded",
	"96": "AccountActivityLimitExceeded",
	"97": "PhoneActivityLimitExceeded",
	"98": "RefundToWallet",
	"99": "EmailSendFailure",
	"100": "NotSettled",
	"101": "NeedCaptcha",
	"102": "GSLTDenied",
	"103": "GSOwnerDenied",
	"104": "InvalidItemType",
	"105": "IPBanned",
	"106": "GSLTExpired",
};


<|start_filename|>components/protobufs.js<|end_filename|>
var Protobuf = require('protobufjs');
var fs = require('fs');

var builder = new Protobuf.Builder();
fs.readdirSync(__dirname + '/../protobufs').forEach(function(file) {
	Protobuf.loadProtoFile(__dirname + '/../protobufs/' + file, builder);
});

module.exports = builder.build();


<|start_filename|>resources/EMachineIDType.js<|end_filename|>
/**
 * What type of machine ID SteamGameServer will use to logon to Steam with, if logging into a persistent account.
 * @readonly
 * @enum {number}
 */
module.exports = {
	/** No machine ID will be provided to Steam */
	"None": 1,

	/** A randomly-generated machine ID will be created on each logon */
	"AlwaysRandom": 2,

	/** A machine ID will be generated from your account's login token */
	"AccountTokenGenerated": 3,

	/** A random machine ID will be generated and saved to the {dataDirectory}, and will be used for future logons */
	"PersistentRandom": 4
};


<|start_filename|>enums/EPersonaStateFlag.js<|end_filename|>
/**
 * @enum EPersonaStateFlag
 */
module.exports = {
	"HasRichPresence": 1,
	"InJoinableGame": 2,
	"OnlineUsingWeb": 256,
	"OnlineUsingMobile": 512,
	"OnlineUsingBigPicture": 1024,
	"OnlineUsingVR": 2048,

	// Value-to-name mapping for convenience
	"1": "HasRichPresence",
	"2": "InJoinableGame",
	"256": "OnlineUsingWeb",
	"512": "OnlineUsingMobile",
	"1024": "OnlineUsingBigPicture",
	"2048": "OnlineUsingVR",
};


<|start_filename|>enums/EDepotFileFlag.js<|end_filename|>
/**
 * @enum EDepotFileFlag
 */
module.exports = {
	"UserConfig": 1,
	"VersionedUserConfig": 2,
	"Encrypted": 4,
	"ReadOnly": 8,
	"Hidden": 16,
	"Executable": 32,
	"Directory": 64,
	"CustomExecutable": 128,
	"InstallScript": 256,

	// Value-to-name mapping for convenience
	"1": "UserConfig",
	"2": "VersionedUserConfig",
	"4": "Encrypted",
	"8": "ReadOnly",
	"16": "Hidden",
	"32": "Executable",
	"64": "Directory",
	"128": "CustomExecutable",
	"256": "InstallScript",
};


<|start_filename|>enums/EAccountFlags.js<|end_filename|>
/**
 * @enum EAccountFlags
 */
module.exports = {
	"NormalUser": 0,
	"PersonaNameSet": 1,
	"Unbannable": 2,
	"PasswordSet": 4,
	"Support": 8,
	"Admin": 16,
	"Supervisor": 32,
	"AppEditor": 64,
	"HWIDSet": 128,
	"PersonalQASet": 256,
	"VacBeta": 512,
	"Debug": 1024,
	"Disabled": 2048,
	"LimitedUser": 4096,
	"LimitedUserForce": 8192,
	"EmailValidated": 16384,
	"MarketingTreatment": 32768,
	"OGGInviteOptOut": 65536,
	"ForcePasswordChange": 131072,
	"ForceEmailVerification": 262144,
	"LogonExtraSecurity": 524288,
	"LogonExtraSecurityDisabled": 1048576,
	"Steam2MigrationComplete": 2097152,
	"NeedLogs": 4194304,
	"Lockdown": 8388608,
	"MasterAppEditor": 16777216,
	"BannedFromWebAPI": 33554432,
	"ClansOnlyFromFriends": 67108864,
	"GlobalModerator": 134217728,

	// Value-to-name mapping for convenience
	"0": "NormalUser",
	"1": "PersonaNameSet",
	"2": "Unbannable",
	"4": "PasswordSet",
	"8": "Support",
	"16": "Admin",
	"32": "Supervisor",
	"64": "AppEditor",
	"128": "HWIDSet",
	"256": "PersonalQASet",
	"512": "VacBeta",
	"1024": "Debug",
	"2048": "Disabled",
	"4096": "LimitedUser",
	"8192": "LimitedUserForce",
	"16384": "EmailValidated",
	"32768": "MarketingTreatment",
	"65536": "OGGInviteOptOut",
	"131072": "ForcePasswordChange",
	"262144": "ForceEmailVerification",
	"524288": "LogonExtraSecurity",
	"1048576": "LogonExtraSecurityDisabled",
	"2097152": "Steam2MigrationComplete",
	"4194304": "NeedLogs",
	"8388608": "Lockdown",
	"16777216": "MasterAppEditor",
	"33554432": "BannedFromWebAPI",
	"67108864": "ClansOnlyFromFriends",
	"134217728": "GlobalModerator",
};


<|start_filename|>enums/EServerType.js<|end_filename|>
/**
 * @enum EServerType
 */
module.exports = {
	"Invalid": -1,
	"First": 0,
	"Shell": 0,
	"GM": 1,
	"BUM": 2,
	"AM": 3,
	"BS": 4,
	"VS": 5,
	"ATS": 6,
	"CM": 7,
	"FBS": 8,
	"FG": 9, // obsolete "renamed to BoxMonitor"
	"BoxMonitor": 9,
	"SS": 10,
	"DRMS": 11,
	"HubOBSOLETE": 12, // obsolete
	"Console": 13,
	"ASBOBSOLETE": 14, // obsolete
	"PICS": 14,
	"Client": 15,
	"BootstrapOBSOLETE": 16, // obsolete;
	"DP": 17,
	"WG": 18,
	"SM": 19,
	"UFS": 21,
	"Util": 23,
	"DSS": 24, // obsolete "renamed to Community"
	"Community": 24,
	"P2PRelayOBSOLETE": 25, // obsolete
	"AppInformation": 26,
	"Spare": 27,
	"FTS": 28,
	"EPM": 29,
	"PS": 30,
	"IS": 31,
	"CCS": 32,
	"DFS": 33,
	"LBS": 34,
	"MDS": 35,
	"CS": 36,
	"GC": 37,
	"NS": 38,
	"OGS": 39,
	"WebAPI": 40,
	"UDS": 41,
	"MMS": 42,
	"GMS": 43,
	"KGS": 44,
	"UCM": 45,
	"RM": 46,
	"FS": 47,
	"Econ": 48,
	"Backpack": 49,
	"UGS": 50,
	"Store": 51,
	"MoneyStats": 52,
	"CRE": 53,
	"UMQ": 54,
	"Workshop": 55,
	"BRP": 56,
	"GCH": 57,
	"MPAS": 58,
	"Trade": 59,
	"Secrets": 60,
	"Logsink": 61,
	"Market": 62,
	"Quest": 63,
	"WDS": 64,
	"ACS": 65,
	"PNP": 66,
	"Max": 67,

	// Value-to-name mapping for convenience
	"-1": "Invalid",
	"0": "Shell",
	"1": "GM",
	"2": "BUM",
	"3": "AM",
	"4": "BS",
	"5": "VS",
	"6": "ATS",
	"7": "CM",
	"8": "FBS",
	"9": "BoxMonitor",
	"10": "SS",
	"11": "DRMS",
	"12": "HubOBSOLETE",
	"13": "Console",
	"14": "PICS",
	"15": "Client",
	"16": "BootstrapOBSOLETE",
	"17": "DP",
	"18": "WG",
	"19": "SM",
	"21": "UFS",
	"23": "Util",
	"24": "Community",
	"25": "P2PRelayOBSOLETE",
	"26": "AppInformation",
	"27": "Spare",
	"28": "FTS",
	"29": "EPM",
	"30": "PS",
	"31": "IS",
	"32": "CCS",
	"33": "DFS",
	"34": "LBS",
	"35": "MDS",
	"36": "CS",
	"37": "GC",
	"38": "NS",
	"39": "OGS",
	"40": "WebAPI",
	"41": "UDS",
	"42": "MMS",
	"43": "GMS",
	"44": "KGS",
	"45": "UCM",
	"46": "RM",
	"47": "FS",
	"48": "Econ",
	"49": "Backpack",
	"50": "UGS",
	"51": "Store",
	"52": "MoneyStats",
	"53": "CRE",
	"54": "UMQ",
	"55": "Workshop",
	"56": "BRP",
	"57": "GCH",
	"58": "MPAS",
	"59": "Trade",
	"60": "Secrets",
	"61": "Logsink",
	"62": "Market",
	"63": "Quest",
	"64": "WDS",
	"65": "ACS",
	"66": "PNP",
	"67": "Max",
};


<|start_filename|>components/gameservers.js<|end_filename|>
var SteamUser = require('../index.js');
var SteamID = require('steamid');

SteamUser.prototype.serverQuery = function(conditions, callback) {
	if(typeof conditions === 'string') {
		conditions = {"filter_text": conditions};
	}

	if(conditions.geo_location_ip) {
		var parts = conditions.geo_location_ip.split('.');
		var buf = new Buffer(4);

		parts.forEach(function(octet, index) {
			buf.writeUInt8(octet, index);
		});

		conditions.geo_location_ip = buf.readUInt32(0);
	}

	this._send(SteamUser.EMsg.ClientGMSServerQuery, conditions, function(body) {
		if(body.error) {
			callback(new Error(body.error));
			return;
		}

		callback(null, (body.servers || []).map(function(server) {
			var buf = new Buffer(4);
			buf.writeUInt32BE(server.server_ip, 0);

			return {
				"ip": Array.prototype.join.call(buf, '.'),
				"port": server.server_port,
				"players": server.auth_players
			};
		}))
	});
};

SteamUser.prototype.getServerList = function(filter, limit, callback) {
	this._sendUnified("GameServers.GetServerList#1", {
		"filter": filter,
		"limit": limit
	}, false, function(body) {
		callback(body.servers.map(function(server) {
			server.steamid = server.steamid ? new SteamID(server.steamid.toString()) : null;
			return server;
		}));
	});
};

SteamUser.prototype.getServerSteamIDsByIP = function(ips, callback) {
	this._sendUnified("GameServers.GetServerSteamIDsByIP#1", {
		"server_ips": ips
	}, false, function(body) {
		var servers = {};

		(body.servers || []).forEach(function(server) {
			servers[server.addr] = new SteamID(server.steamid.toString());
		});

		callback(servers);
	});
};

SteamUser.prototype.getServerIPsBySteamID = function(steamids, callback) {
	steamids = steamids.map(function(id) {
		if(typeof id === 'object') {
			return id.toString();
		}

		return new SteamID(id).getSteamID64();
	});

	this._sendUnified("GameServers.GetServerIPsBySteamID#1", {
		"server_steamids": steamids
	}, false, function(body) {
		var servers = {};

		(body.servers || []).forEach(function(server) {
			servers[server.steamid.toString()] = server.addr;
		});

		callback(servers);
	});
};


<|start_filename|>enums/ELicenseType.js<|end_filename|>
/**
 * @enum ELicenseType
 */
module.exports = {
	"NoLicense": 0,
	"SinglePurchase": 1,
	"SinglePurchaseLimitedUse": 2,
	"RecurringCharge": 3,
	"RecurringChargeLimitedUse": 4,
	"RecurringChargeLimitedUseWithOverages": 5,
	"RecurringOption": 6,

	// Value-to-name mapping for convenience
	"0": "NoLicense",
	"1": "SinglePurchase",
	"2": "SinglePurchaseLimitedUse",
	"3": "RecurringCharge",
	"4": "RecurringChargeLimitedUse",
	"5": "RecurringChargeLimitedUseWithOverages",
	"6": "RecurringOption",
};


<|start_filename|>package.json<|end_filename|>
{
	"name": "steam-gameserver",
	"version": "1.0.0",
	"description": "node-steam client handler for Gameserver and AnonGameserver account types",
	"keywords": ["steam", "steam client", "steam server", "steam gameserver", "steam game server"],
	"homepage": "https://github.com/DoctorMcKay/node-steam-gameserver",
	"bugs": {
		"url": "https://github.com/DoctorMcKay/node-steam-gameserver/issues"
	},
	"license": "MIT",
	"author": {
		"name": "<NAME>",
		"email": "<EMAIL>",
		"url": "https://www.doctormckay.com"
	},
	"repository": {
		"type": "git",
		"url": "https://github.com/DoctorMcKay/node-steam-gameserver.git"
	},
	"dependencies": {
		"steamid": "^1.1.0",
		"appdirectory": "^0.1.0",
		"protobufjs": "^5.0.0",
		"steam-client": "^2.2.2",
		"bytebuffer": "^5.0.0",
		"@doctormckay/steam-crypto": "^1.0.0",
		"file-manager": "^1.0.0",
		"vdf": "^0.0.2",
		"binarykvparser": "^2.0.0"
	},
	"scripts": {
		"prepublish": "node scripts/prepublish.js",
		"generate-enums": "node scripts/generate-enums.js"
	},
	"engines": {
		"node": ">=4.0.0"
	}
}


<|start_filename|>components/friends.js<|end_filename|>
var SteamGameServer = require('../index.js');
var SteamID = require('steamid');
var ByteBuffer = require('bytebuffer');
var Helpers = require('./helpers.js');

/**
 * Requests information about one or more user profiles.
 * @param {(SteamID[]|string[])} steamids - An array of SteamID objects or strings which can parse into them.
 * @param {function} [callback] - Optional. Called with an object whose keys are 64-bit SteamIDs as strings, and whose values are persona objects.
 */
SteamGameServer.prototype.getPersonas = function(steamids, callback) {
	var Flags = SteamGameServer.EClientPersonaStateFlag;
	var flags = Flags.Status|Flags.PlayerName|Flags.QueryPort|Flags.SourceID|Flags.Presence|
		Flags.Metadata|Flags.LastSeen|Flags.ClanInfo|Flags.GameExtraInfo|Flags.GameDataBlob|
		Flags.ClanTag|Flags.Facebook;

	var ids = steamids.map(function(id) {
		if (typeof id === 'string') {
			return (new SteamID(id)).getSteamID64();
		}

		return id.toString();
	});

	this._send(SteamGameServer.EMsg.ClientRequestFriendData, {
		"friends": ids,
		"persona_state_requested": flags
	});

	if (callback) {
		var output = {};

		var self = this;
		ids.forEach(function(id) {
			self.once('user#' + id, receive);
		});

		function receive(sid, user) {
			var sid64 = sid.getSteamID64();
			output[sid64] = user;

			var index = ids.indexOf(sid64);
			if (index != -1) {
				ids.splice(index, 1);
			}

			if (ids.length === 0) {
				callback(output);
			}
		}
	}
};

/**
 * Gets the Steam Level of one or more Steam users.
 * @param {(SteamID[]|string[])} steamids - An array of SteamID objects, or strings which can parse into one.
 * @param {function} callback - Called on completion with an object whose keys are 64-bit SteamIDs as strings, and whose values are Steam Level numbers.
 */
SteamGameServer.prototype.getSteamLevels = function(steamids, callback) {
	var accountids = steamids.map(function(steamID) {
		if (typeof steamID === 'string') {
			return (new SteamID(steamID)).accountid;
		} else {
			return steamID.accountid;
		}
	});

	this._send(SteamGameServer.EMsg.ClientFSGetFriendsSteamLevels, {"accountids": accountids}, function(body) {
		var output = {};

		var sid = new SteamID();
		sid.universe = SteamID.Universe.PUBLIC;
		sid.type = SteamID.Type.INDIVIDUAL;
		sid.instance = SteamID.Instance.DESKTOP;

		(body.friends || []).forEach(function(user) {
			sid.accountid = user.accountid;
			output[sid.getSteamID64()] = user.level;
		});

		callback(output);
	});
};

// Handlers

SteamGameServer.prototype._handlers[SteamGameServer.EMsg.ClientPersonaState] = function(body) {
	console.log(body);
	body.friends.forEach((user) => {
		var sid = new SteamID(user.friendid.toString());
		var sid64 = sid.getSteamID64();
		delete user.friendid;

		var i;
		if (!this.users[sid64]) {
			this.users[sid64] = user;
			processUser(this.users[sid64]);
		} else {
			// Replace unknown data in the received object with already-known data
			for (i in this.users[sid64]) {
				if(this.users[sid64].hasOwnProperty(i) && user.hasOwnProperty(i) && user[i] === null) {
					user[i] = this.users[sid64][i];
				}
			}
		}

		processUser(user);

		/**
		 * Emitted when we receive persona info about a user.
		 * You can also listen for user#steamid64 to get info only for a specific user.
		 *
		 * @event SteamGameServer#user
		 * @param {SteamID} steamID - The SteamID of the user
		 * @param {Object} user - An object containing the user's persona info
		 */

		this._emitIdEvent('user', sid, user);

		for (i in user) {
			if (user.hasOwnProperty(i) && user[i] !== null) {
				this.users[sid][i] = user[i];
			}
		}
	});
};

function processUser(user) {
	if (typeof user.gameid === 'object' && user.gameid !== null) {
		user.gameid = user.gameid.toNumber();
	}

	if (typeof user.last_logoff === 'number') {
		user.last_logoff = new Date(user.last_logoff * 1000);
	}

	if (typeof user.last_logon === 'number') {
		user.last_logon = new Date(user.last_logon * 1000);
	}

	if (typeof user.avatar_hash === 'object' && (Buffer.isBuffer(user.avatar_hash) || ByteBuffer.isByteBuffer(user.avatar_hash))) {
		var hash = user.avatar_hash.toString('hex');
		user.avatar_url_icon = "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/" + hash.substring(0, 2) + "/" + hash;
		user.avatar_url_medium = user.avatar_url_icon + "_medium.jpg";
		user.avatar_url_full = user.avatar_url_icon + "_full.jpg";
		user.avatar_url_icon += ".jpg";
	}
}


<|start_filename|>index.js<|end_filename|>
var Steam = require('steam-client');
var AppDirectory = require('appdirectory');
var FileStorage = require('file-manager');

require('util').inherits(SteamGameServer, require('events').EventEmitter);

module.exports = SteamGameServer;

SteamGameServer.Steam = Steam;
SteamGameServer.EMachineIDType = require('./resources/EMachineIDType.js');

require('./resources/enums.js');

try {
	SteamGameServer.Steam.servers = require('./resources/servers.json');
} catch(e) {
	// It's okay if it isn't there
}

function SteamGameServer(client, options) {
	if(client && client.constructor.name !== 'SteamClient' && client.constructor.name !== 'CMClient') {
		options = client;
		client = null;
	}

	this.client = client ? client : new Steam.CMClient();
	this.steamID = null;

	// Server info
	this.appID = null;
	this.flags = SteamGameServer.EServerFlags.None;
	this.secure = null;
	this.users = null;

	this.options = options || {};

	var defaultOptions = {
		"autoRelogin": true,
		"machineIdType": SteamGameServer.EMachineIDType.AccountTokenGenerated,
		"machineIdFormat": ["SteamGameServer Hash BB3 {token}", "SteamGameServer Hash FF2 {token}", "SteamGameServer Hash 3B3 {token}"],
		"debug": false
	};

	for(var i in defaultOptions) {
		if(!defaultOptions.hasOwnProperty(i)) {
			continue;
		}

		if(typeof this.options[i] === 'undefined') {
			this.options[i] = defaultOptions[i];
		}
	}

	if (!this.options.dataDirectory) {
		// Might as well just reuse node-steamuser's data dir, as we'll be saving the same things
		if (process.env.OPENSHIFT_DATA_DIR) {
			this.options.dataDirectory = process.env.OPENSHIFT_DATA_DIR + "/node-steamuser";
		} else {
			this.options.dataDirectory = (new AppDirectory({"appName": "node-steamuser", "appAuthor": "doctormckay"})).userData();
		}
	}

	this.storage = new FileStorage(this.options.dataDirectory);

	this.client.on('message', this._handleMessage.bind(this));

	var self = this;
	this.client.on('error', function(e) {
		if(!self.steamID) {
			return; // We've already handled this
		}

		self._handleLogOff(e.eresult || SteamGameServer.EResult.NoConnection, e.message || "NoConnection");
	});

	this.client.on('servers', function(servers) {
		self.storage.writeFile('servers.json', JSON.stringify(servers, null, "\t"));
		if(!client) {
			// It's an internal client, so we know that our Steam has an up-to-date server list
			Steam['__SteamUserServersSet__'] = true;
		}
	});
}

SteamGameServer.prototype.setOption = function(option, value) {
	this.options[option] = value;

	// Handle anything that needs to happen when particular options update
	switch(option) {
		case 'dataDirectory':
			this.storage.directory = value;
			break;
	}
};

SteamGameServer.prototype.setOptions = function(options) {
	for(var i in options) {
		if(!options.hasOwnProperty(i)) {
			continue;
		}

		this.setOption(i, options[i]);
	}
};

/**
 * Same as emit() except the second argument (the first provided to the callback function) is a SteamID and will be appended to the event name with a hash.
 * @private
 */
SteamGameServer.prototype._emitIdEvent = function() {
	this.emit.apply(this, arguments);
	arguments[0] += '#' + arguments[1].getSteamID64();
	this.emit.apply(this, arguments);
};

require('./components/messages.js');
require('./components/logon.js');
require('./components/gameservers.js');
require('./components/friends.js');
require('./components/apps.js');
require('./components/pubfiles.js');

/**
 * Called when the request completes.
 * @callback SteamUser~genericEResultCallback
 * @param {EResult} eresult - The result of the operation
 */


<|start_filename|>components/helpers.js<|end_filename|>
var SteamID = require('steamid');
var Crypto = require('crypto');

/**
 * If the input isn't already a SteamID object, converts it into one and returns it
 * @param input
 * @returns {SteamID}
 */
exports.steamID = function(input) {
	if(typeof input !== 'string') {
		var keys = Object.keys(input);
		if (keys.indexOf('universe') != -1 && keys.indexOf('type') != -1 && keys.indexOf('instance') != -1 && keys.indexOf('accountid') != -1 && keys.indexOf('isValid') != -1) {
			return input; // Looks like it's already a SteamID
		}

		input = input.toString();
	}

	return new SteamID(input);
};

/**
 * Convert an IP in integer notation to dotted-decimal notation
 * @param {int} input
 * @returns {string}
 */
exports.ipIntToString = function(input) {
	return ((input >> 24) & 0xFF) + "." + ((input >> 16) & 0xFF) + "." + ((input >> 8) & 0xFF) + "." + (input & 0xFF);
};

/**
 * Convert an IP in dotted-decimal notation to integer notation
 * @param {string} input
 * @returns {int}
 */
exports.ipStringToInt = function(input) {
	var parts = input.split('.');
	return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
};

/**
 * Get an machine ID for internal use (not sent to Steam)
 * @returns {string}
 */
exports.getInternalMachineID = function() {
	// Build our machine ID off of hostname, memory, and CPU data
	var os = require('os');
	var cpus = os.cpus().map(function(cpu) {
		return cpu.model;
	});

	cpus.sort();

	var id = os.hostname() + os.totalmem() + cpus.join('');
	var hash = Crypto.createHash('md5');
	hash.update(id);
	return hash.digest('hex');
};

/**
 * Get an Error object for a particular EResult
 * @param {int} eresult
 * @returns {null|Error}
 */
exports.eresultError = function(eresult) {
	if (eresult == SteamUser.EResult.OK) {
		// no error
		return null;
	}

	var err = new Error("Error " + eresult);
	for (var i in SteamUser.EResult) {
		if (SteamUser.EResult.hasOwnProperty(i) && SteamUser.EResult[i] == eresult) {
			err.message = i;
		}
	}

	err.eresult = eresult;
	return err;
};


<|start_filename|>components/messages.js<|end_filename|>
var SteamGameServer = require('../index.js');
var ByteBuffer = require('bytebuffer');

var Schema = require('./protobufs.js');

var protobufs = {};
protobufs[SteamGameServer.EMsg.ClientLogon] = Schema.CMsgClientLogon;
protobufs[SteamGameServer.EMsg.ClientLogOnResponse] = Schema.CMsgClientLogonResponse;
protobufs[SteamGameServer.EMsg.ClientLogOff] = Schema.CMsgClientLogOff;
protobufs[SteamGameServer.EMsg.ClientLoggedOff] = Schema.CMsgClientLoggedOff;
protobufs[SteamGameServer.EMsg.GSServerType] = Schema.CMsgGSServerType;
protobufs[SteamGameServer.EMsg.GSStatusReply] = Schema.CMsgGSStatusReply;
protobufs[SteamGameServer.EMsg.ClientServiceMethod] = Schema.CMsgClientServiceMethod;
protobufs[SteamGameServer.EMsg.ClientServiceMethodResponse] = Schema.CMsgClientServiceMethodResponse;
protobufs[SteamGameServer.EMsg.ClientGMSServerQuery] = Schema.CMsgClientGMSServerQuery;
protobufs[SteamGameServer.EMsg.GMSClientServerQueryResponse] = Schema.CMsgGMSClientServerQueryResponse;
protobufs[SteamGameServer.EMsg.ClientFSGetFriendsSteamLevels] = Schema.CMsgClientFSGetFriendsSteamLevels;
protobufs[SteamGameServer.EMsg.ClientFSGetFriendsSteamLevelsResponse] = Schema.CMsgClientFSGetFriendsSteamLevelsResponse;
protobufs[SteamGameServer.EMsg.ClientRequestFriendData] = Schema.CMsgClientRequestFriendData;
protobufs[SteamGameServer.EMsg.ClientPersonaState] = Schema.CMsgClientPersonaState;
protobufs[SteamGameServer.EMsg.ClientGetNumberOfCurrentPlayersDP] = Schema.CMsgDPGetNumberOfCurrentPlayers;
protobufs[SteamGameServer.EMsg.ClientGetNumberOfCurrentPlayersDPResponse] = Schema.CMsgDPGetNumberOfCurrentPlayersResponse;
protobufs[SteamGameServer.EMsg.ClientAuthList] = Schema.CMsgClientAuthList;
protobufs[SteamGameServer.EMsg.ClientAuthListAck] = Schema.CMsgClientAuthListAck;
protobufs[SteamGameServer.EMsg.ClientTicketAuthComplete] = Schema.CMsgClientTicketAuthComplete;
protobufs[SteamGameServer.EMsg.ClientPICSChangesSinceRequest] = Schema.CMsgClientPICSChangesSinceRequest;
protobufs[SteamGameServer.EMsg.ClientPICSChangesSinceResponse] = Schema.CMsgClientPICSChangesSinceResponse;
protobufs[SteamGameServer.EMsg.ClientPICSProductInfoRequest] = Schema.CMsgClientPICSProductInfoRequest;
protobufs[SteamGameServer.EMsg.ClientPICSProductInfoResponse] = Schema.CMsgClientPICSProductInfoResponse;
protobufs[SteamGameServer.EMsg.ClientPICSAccessTokenRequest] = Schema.CMsgClientPICSAccessTokenRequest;
protobufs[SteamGameServer.EMsg.ClientPICSAccessTokenResponse] = Schema.CMsgClientPICSAccessTokenResponse;

// Unified protobufs
protobufs['GameServers.GetServerList#1_Request'] = Schema.CGameServers_GetServerList_Request;
protobufs['GameServers.GetServerList#1_Response'] = Schema.CGameServers_GetServerList_Response;
protobufs['GameServers.GetServerSteamIDsByIP#1_Request'] = Schema.CGameServers_GetServerSteamIDsByIP_Request;
protobufs['GameServers.GetServerSteamIDsByIP#1_Response'] = Schema.CGameServers_IPsWithSteamIDs_Response;
protobufs['GameServers.GetServerIPsBySteamID#1_Request'] = Schema.CGameServers_GetServerIPsBySteamID_Request;
protobufs['GameServers.GetServerIPsBySteamID#1_Response'] = Schema.CGameServers_IPsWithSteamIDs_Response;
protobufs['PublishedFile.GetDetails#1_Request'] = Schema.CPublishedFile_GetDetails_Request;
protobufs['PublishedFile.GetDetails#1_Response'] = Schema.CPublishedFile_GetDetails_Response;
protobufs['Player.GetGameBadgeLevels#1_Request'] = Schema.CPlayer_GetGameBadgeLevels_Request;
protobufs['Player.GetGameBadgeLevels#1_Response'] = Schema.CPlayer_GetGameBadgeLevels_Response;

ByteBuffer.DEFAULT_ENDIAN = ByteBuffer.LITTLE_ENDIAN;

SteamGameServer.prototype._send = function(emsg, body, callback) {
	if((!this.steamID || !this.client.connected) && [SteamGameServer.EMsg.ChannelEncryptRequest, SteamGameServer.EMsg.ChannelEncryptResponse, SteamGameServer.EMsg.ChannelEncryptResult, SteamGameServer.EMsg.ClientLogon].indexOf(emsg) == -1) {
		// We're disconnected, drop it
		this.emit('debug', 'Dropping message ' + emsg + ' because we\'re not logged on.');
		return;
	}

	var header = {
		"msg": emsg
	};

	var Proto = protobufs[emsg];
	if(Proto) {
		header.proto = {};
		body = new Proto(body).toBuffer();
	} else if(ByteBuffer.isByteBuffer(body)) {
		body = body.toBuffer();
	}

	var cb = null;
	if(callback) {
		cb = function(header, body) {
			if(protobufs[header.msg]) {
				body = protobufs[header.msg].decode(body);
			} else {
				body = ByteBuffer.wrap(body);
			}

			callback(body);
		};
	}

	if(this.options.debug) {
		for(var i in SteamGameServer.EMsg) {
			if(SteamGameServer.EMsg.hasOwnProperty(i) && SteamGameServer.EMsg[i] == emsg) {
				emsg = i;
				break;
			}
		}
	}

	this.emit('debug', 'Sending message: ' + emsg);
	this.client.send(header, body, cb);
};

SteamGameServer.prototype._handleMessage = function(header, body, callback) {
	var msgName = header.msg;

	if(this.options.debug) {
		for(var i in SteamGameServer.EMsg) {
			if(SteamGameServer.EMsg.hasOwnProperty(i) && SteamGameServer.EMsg[i] == header.msg) {
				msgName = i;
				break;
			}
		}
	}

	if(!this._handlers[header.msg]) {
		if(header.msg != SteamGameServer.EMsg.Multi) {
			this.emit('debug', 'Unhandled message: ' + msgName);
		}

		return;
	}

	if(protobufs[header.msg]) {
		body = protobufs[header.msg].decode(body);
	} else {
		body = ByteBuffer.wrap(body);
	}

	this.emit('debug', 'Handled message: ' + msgName);

	var cb = null;
	if(callback) {
		cb = function(emsg, body) {
			var header = {"msg": emsg};

			if(protobufs[emsg]) {
				header.proto = {};
				body = new protobufs[emsg](body).toBuffer();
			}

			callback(header, body);
		}
	}

	this._handlers[header.msg].call(this, body, cb);
};

SteamGameServer.prototype._handlers = {};

// Unified messages

SteamGameServer.prototype._sendUnified = function(methodName, methodData, notification, callback) {
	var cb;
	if(callback && protobufs[methodName + '_Response']) {
		cb = function(body) {
			var Proto = protobufs[methodName + '_Response'];
			callback(Proto.decode(body.serialized_method_response));
		};
	}

	var Proto = protobufs[methodName + '_Request'];
	this._send(SteamGameServer.EMsg.ClientServiceMethod, {
		"method_name": methodName,
		"serialized_method": new Proto(methodData).toBuffer(),
		"is_notification": notification
	}, cb);
};


<|start_filename|>components/apps.js<|end_filename|>
var SteamGameServer = require('../index.js');
var VDF = require('vdf');
var BinaryKVParser = require('binarykvparser');

SteamGameServer.prototype.getPlayerCount = function(appid, callback) {
	this._send(SteamGameServer.EMsg.ClientGetNumberOfCurrentPlayersDP, {"appid": appid}, (body) => {
		if (body.eresult != SteamGameServer.EResult.OK) {
			var err = new Error(SteamGameServer.EResult[body.eresult] || body.eresult);
			err.eresult = body.eresult;
			callback(err);
			return;
		}

		callback(null, body.player_count);
	});
};

SteamGameServer.prototype.getProductChanges = function(sinceChangenumber, callback) {
	this._send(SteamGameServer.EMsg.ClientPICSChangesSinceRequest, {
		"since_change_number": sinceChangenumber,
		"send_app_info_changes": true,
		"send_package_info_changes": true
	}, (body) => {
		callback(body.current_change_number, body.app_changes, body.package_changes);
	});
};

SteamGameServer.prototype.getProductInfo = function(apps, packages, callback) {
	// Steam can send us the full response in multiple responses, so we need to buffer them into one callback
	var appids = [];
	var packageids = [];
	var response = {
		"apps": {},
		"packages": {},
		"unknownApps": [],
		"unknownPackages": []
	};

	apps = apps.map((app) => {
		if (typeof app === 'object') {
			appids.push(app.appid);
			return app;
		} else {
			appids.push(app);
			return {"appid": app};
		}
	});

	packages = packages.map((pkg) => {
		if (typeof pkg === 'object') {
			packageids.push(pkg.packageid);
			return pkg;
		} else {
			packageids.push(pkg);
			return {"packageid": pkg};
		}
	});
	
	this._send(SteamGameServer.EMsg.ClientPICSProductInfoRequest, {
		"apps": apps,
		"packages": packages
	}, (body) => {
		// If we're using the PICS cache, then add the items in this response to it
		if (this.options.enablePicsCache) {
			var cache = this.picsCache;
			cache.apps = cache.apps || {};
			cache.packages = cache.packages || {};

			(body.apps || []).forEach((app) => {
				var data = {
					"changenumber": app.change_number,
					"missingToken": !!app.missing_token,
					"appinfo": VDF.parse(app.buffer.toString('utf8')).appinfo
				};

				app._parsedData = data;
			});

			(body.packages || []).forEach((pkg) => {
				var data = {
					"changenumber": pkg.change_number,
					"missingToken": !!pkg.missing_token,
					"packageinfo": BinaryKVParser.parse(pkg.buffer)[pkg.packageid]
				};

				pkg._parsedData = data;
			});
		}

		if (!callback) {
			return;
		}

		(body.unknown_appids || []).forEach((appid) => {
			response.unknownApps.push(appid);
			var index = appids.indexOf(appid);
			if (index != -1) {
				appids.splice(index, 1);
			}
		});

		(body.unknown_packageids || []).forEach((packageid) => {
			response.unknownPackages.push(packageid);
			var index = packageids.indexOf(packageid);
			if (index != -1) {
				packageids.splice(index, 1);
			}
		});

		(body.apps || []).forEach((app) => {
			response.apps[app.appid] = app._parsedData || {
					"changenumber": app.change_number,
					"missingToken": !!app.missing_token,
					"appinfo": VDF.parse(app.buffer.toString('utf8')).appinfo
				};

			var index = appids.indexOf(app.appid);
			if (index != -1) {
				appids.splice(index, 1);
			}
		});

		(body.packages || []).forEach((pkg) => {
			response.packages[pkg.packageid] = pkg._parsedData || {
					"changenumber": pkg.change_number,
					"missingToken": !!pkg.missing_token,
					"packageinfo": BinaryKVParser.parse(pkg.buffer)[pkg.packageid]
				};

			var index = packageids.indexOf(pkg.packageid);
			if (index != -1) {
				packageids.splice(index, 1);
			}
		});

		if (appids.length === 0 && packageids.length === 0) {
			callback(response.apps, response.packages, response.unknownApps, response.unknownPackages);
		}
	});
};

SteamGameServer.prototype.getProductInfo = function(apps, packages, callback) {
	// Steam can send us the full response in multiple responses, so we need to buffer them into one callback
	var appids = [];
	var packageids = [];
	var response = {
		"apps": {},
		"packages": {},
		"unknownApps": [],
		"unknownPackages": []
	};

	apps = apps.map((app) => {
		if (typeof app === 'object') {
			appids.push(app.appid);
			return app;
		} else {
			appids.push(app);
			return {"appid": app};
		}
	});

	packages = packages.map((pkg) => {
		if (typeof pkg === 'object') {
			packageids.push(pkg.packageid);
			return pkg;
		} else {
			packageids.push(pkg);
			return {"packageid": pkg};
		}
	});
	
	this._send(SteamGameServer.EMsg.ClientPICSProductInfoRequest, {
		"apps": apps,
		"packages": packages
	}, (body) => {
		// If we're using the PICS cache, then add the items in this response to it
		if (this.options.enablePicsCache) {
			var cache = this.picsCache;
			cache.apps = cache.apps || {};
			cache.packages = cache.packages || {};

			(body.apps || []).forEach((app) => {
				var data = {
					"changenumber": app.change_number,
					"missingToken": !!app.missing_token,
					"appinfo": VDF.parse(app.buffer.toString('utf8')).appinfo
				};

				app._parsedData = data;
			});

			(body.packages || []).forEach((pkg) => {
				var data = {
					"changenumber": pkg.change_number,
					"missingToken": !!pkg.missing_token,
					"packageinfo": BinaryKVParser.parse(pkg.buffer)[pkg.packageid]
				};

				pkg._parsedData = data;
			});
		}

		if (!callback) {
			return;
		}

		(body.unknown_appids || []).forEach((appid) => {
			response.unknownApps.push(appid);
			var index = appids.indexOf(appid);
			if (index != -1) {
				appids.splice(index, 1);
			}
		});

		(body.unknown_packageids || []).forEach((packageid) => {
			response.unknownPackages.push(packageid);
			var index = packageids.indexOf(packageid);
			if (index != -1) {
				packageids.splice(index, 1);
			}
		});

		(body.apps || []).forEach((app) => {
			response.apps[app.appid] = app._parsedData || {
					"changenumber": app.change_number,
					"missingToken": !!app.missing_token,
					"appinfo": VDF.parse(app.buffer.toString('utf8')).appinfo
				};

			var index = appids.indexOf(app.appid);
			if (index != -1) {
				appids.splice(index, 1);
			}
		});

		(body.packages || []).forEach((pkg) => {
			response.packages[pkg.packageid] = pkg._parsedData || {
					"changenumber": pkg.change_number,
					"missingToken": !!pkg.missing_token,
					"packageinfo": BinaryKVParser.parse(pkg.buffer)[pkg.packageid]
				};

			var index = packageids.indexOf(pkg.packageid);
			if (index != -1) {
				packageids.splice(index, 1);
			}
		});

		if (appids.length === 0 && packageids.length === 0) {
			callback(response.apps, response.packages, response.unknownApps, response.unknownPackages);
		}
	});
};


<|start_filename|>enums/EPlatformType.js<|end_filename|>
/**
 * @enum EPlatformType
 */
module.exports = {
	"Unknown": 0,
	"Win32": 1,
	"Win64": 2,
	"Linux": 3,
	"OSX": 4,
	"PS3": 5,
	"Max": 6,

	// Value-to-name mapping for convenience
	"0": "Unknown",
	"1": "Win32",
	"2": "Win64",
	"3": "Linux",
	"4": "OSX",
	"5": "PS3",
	"6": "Max",
};


<|start_filename|>enums/EBillingType.js<|end_filename|>
/**
 * @enum EBillingType
 */
module.exports = {
	"NoCost": 0,
	"BillOnceOnly": 1,
	"BillMonthly": 2,
	"ProofOfPrepurchaseOnly": 3,
	"GuestPass": 4,
	"HardwarePromo": 5,
	"Gift": 6,
	"AutoGrant": 7,
	"OEMTicket": 8,
	"RecurringOption": 9,
	"NumBillingTypes": 10,

	// Value-to-name mapping for convenience
	"0": "NoCost",
	"1": "BillOnceOnly",
	"2": "BillMonthly",
	"3": "ProofOfPrepurchaseOnly",
	"4": "GuestPass",
	"5": "HardwarePromo",
	"6": "Gift",
	"7": "AutoGrant",
	"8": "OEMTicket",
	"9": "RecurringOption",
	"10": "NumBillingTypes",
};
