steam-user
Version:
Steam client for Individual and AnonUser Steam account types
418 lines (349 loc) • 13.5 kB
JavaScript
const FS = require('fs');
const HTTPS = require('https');
const URL = require('url');
const GENERATED_FILE_HEADER = `/* eslint-disable */\n// Auto-generated by generate-enums script on ${(new Date()).toString()}\n\n`;
// Sometimes Valve prefixes an enum with "k_ESomePrefix_", sometimes they prefix with "k_ETheEnumName", and sometimes
// they prefix with "k_ENotTheEnumName" (without an underscore). This third case can't be handled automatically.
const ENUMS_WITH_DIFFERENT_PREFIXES_FROM_THEIR_NAMES = {
EFrameAccumulatedStat: 'k_EFrameStat',
EHIDDeviceDisconnectMethod: 'k_EDeviceDisconnectMethod',
EHIDDeviceLocation: 'k_EDeviceLocation',
ELogFileType: 'k_ELogFile',
EPublishedFileForSaleStatus: 'k_PFFSS_',
ERemoteClientBroadcastMsg: 'k_ERemoteDevice',
ERemoteDeviceAuthorizationResult: 'k_ERemoteDeviceAuthorization',
ERemoteDeviceStreamingResult: 'k_ERemoteDeviceStreaming',
EStreamControlMessage: 'k_EStreamControl',
EStreamDataMessage: 'k_EStream',
EStreamDiscoveryMessage: 'k_EStreamDiscovery',
EStreamFrameEvent: 'k_EStream',
EStreamFramerateLimiter: 'k_EStreamFramerate',
EStreamGamepadInputType: 'k_EStreamGamepadInput',
EStreamingDataType: 'k_EStreaming',
EStreamMouseWheelDirection: 'k_EStreamMouseWheel',
EStreamQualityPreference: 'k_EStreamQuality',
EStreamStatsMessage: 'k_EStreamStats',
EChatRoomNotificationLevel: 'k_EChatroomNotificationLevel',
EPublishedFileQueryType: 'k_PublishedFileQueryType_',
EProtoAppType: 'k_EAppType',
EBluetoothDeviceType: 'k_BluetoothDeviceType_',
EPlaytestStatus: 'k_ETesterStatus',
ESystemAudioChannel: 'k_SystemAudioChannel_',
ESystemAudioDirection: 'k_SystemAudioDirection_',
ESystemAudioPortDirection: 'k_SystemAudioPortDirection_',
ESystemAudioPortType: 'k_SystemAudioPortType_',
ESystemFanControlMode: 'k_SystemFanControlMode_',
EAuthTokenRevokeAction: 'k_EAuthTokenRevoke',
EMarketingMessageAssociationType: 'k_EMarketingMessage',
EMarketingMessageLookupType: 'k_EMarketingMessageLookup',
EMarketingMessageType: 'k_EMarketingMessage',
EMarketingMessageVisibility: 'k_EMarketingMessageVisible'
};
const ENUMS_WITH_SOMETIMES_DIFFERENT_PREFIXES = {
EClientPersonaStateFlag: 'k_EClientPersonaState'
};
// Some enums are just named wrong
const ENUM_NAMES_TO_FIX = {
EChatroomNotificationLevel: 'EChatRoomNotificationLevel'
};
const ENUM_VALUES_TO_FIX = {
'2FAPrompt': 'TwoFactorPrompt'
};
// Generate enums
if (!FS.existsSync(__dirname + '/../enums')) {
FS.mkdirSync(__dirname + '/../enums');
}
let g_EnumNames = {};
let g_EnumNamesNormalized = {};
processProtobufEnums();
download('https://api.github.com/repos/SteamRE/SteamKit/contents/Resources/SteamLanguage', function(data) {
let json = JSON.parse(data);
if (!json.length) {
throw new Error('Cannot get data from GitHub');
}
let remainingFiles = 0;
json.forEach(function(file) {
if (!file.name.match(/\.steamd$/)) {
return;
}
remainingFiles++;
// Get the download URL from the github API
download(file.download_url, function(fileContents) {
// This parser isn't terribly robust, but it works as long as SteamRE doesn't change their resource format
let currentEnum = null;
fileContents.split('\n').forEach(function(line) {
// Go line-by-line
line = line.trim(); // trim whitespace
let idx = line.indexOf('//');
if (idx != -1) {
line = line.substring(0, idx).trim(); // remove line comments
}
let match;
if (!currentEnum) {
// We're not currently parsing any enum. Is this the opening of one?
if ((match = line.match(/^enum (E[a-zA-Z0-9]+)(<[a-z]+>)?( flags)?/))) {
// Okay, this is an enum assuming the next line is a bracket
currentEnum = match[1];
if (ENUM_NAMES_TO_FIX[currentEnum]) {
currentEnum = ENUM_NAMES_TO_FIX[currentEnum];
}
}
} else if (typeof currentEnum === 'string') {
if (line != '{') {
throw new Error('Syntax error parsing ' + file.name + ', bad token following ' + currentEnum);
} else {
// Okay now we're *really* parsing this enum
currentEnum = {
name: currentEnum,
values: [],
dynamicValues: []
};
}
} else {
if (line.match(/^};?$/)) {
process.stdout.write(`Generating ${currentEnum.name}.js... `);
// We're done parsing this enum, let's go ahead and generate the file
// First make sure it has actually changed
let enumFileName = `${__dirname}/../enums/${currentEnum.name}.js`;
let {changed, valuesToAdd} = validateEnum(enumFileName, currentEnum.values, currentEnum.dynamicValues);
if (!changed) {
// Enum has not changed
console.log('unchanged');
g_EnumNames[currentEnum.name] = true;
let normalized = currentEnum.name.toLowerCase();
if (g_EnumNamesNormalized[normalized] && g_EnumNamesNormalized[normalized] != currentEnum.name) {
throw new Error(`Duplicate enum ${currentEnum.name}`);
}
g_EnumNamesNormalized[normalized] = currentEnum.name;
currentEnum = null;
return;
}
process.stdout.write('\n');
currentEnum.values = currentEnum.values.concat(valuesToAdd);
currentEnum.values.sort(sortEnum);
let file = GENERATED_FILE_HEADER + `/**\n * @enum\n * @readonly\n */\nconst ${currentEnum.name} = {\n`;
currentEnum.values.forEach(function(val) {
if (ENUM_VALUES_TO_FIX[val.name]) {
val.name = ENUM_VALUES_TO_FIX[val.name];
}
file += '\t"' + val.name + '": ' + val.value + ',' + (val.comment ? ' // ' + val.comment.trim() : '') + '\n';
});
file += '\n\t// Value-to-name mapping for convenience\n';
// Put down the reverse, for simplicity in use
currentEnum.values.forEach(function(val, idx) {
if (!val.value.match(/^-?[0-9]+/)) {
return; // it's dynamic
}
// Is this the last value in this enum with this value?
if (currentEnum.values.some(function(val2, idx2) { return val2.value == val.value && idx2 > idx; })) {
return;
}
file += '\t"' + val.value + '": "' + val.name + '",\n';
});
file += `};\n\nmodule.exports = ${currentEnum.name};\n`;
if (currentEnum.dynamicValues.length > 0) {
file += '\n';
currentEnum.dynamicValues.forEach(function(val) {
file += 'module.exports.' + val.name + ' = ' + val.value + ';' + (val.comment ? ' // ' + val.comment.trim() : '') + '\n';
});
}
FS.writeFileSync(enumFileName, file);
g_EnumNames[currentEnum.name] = true;
let normalized = currentEnum.name.toLowerCase();
if (g_EnumNamesNormalized[normalized] && g_EnumNamesNormalized[normalized] != currentEnum.name) {
throw new Error(`Duplicate enum ${currentEnum.name}`);
}
g_EnumNamesNormalized[normalized] = currentEnum.name;
currentEnum = null;
} else if ((match = line.match(/^([A-Za-z0-9_]+) = ([^;]+);(.*)$/))) {
let name = match[1];
let value = match[2];
let comment = match[3];
if (value.match(/^0x[0-9A-Fa-f]+$/)) {
value = parseInt(value.substring(2), 16).toString();
}
let isDynamic = false;
let flags = value.split('|').map(function(flag) {
flag = flag.trim();
if (flag.match(/^-?[0-9]+$/)) {
return flag;
} else {
isDynamic = true;
return 'module.exports.' + flag;
}
});
value = flags.join(' | ');
(isDynamic ? currentEnum.dynamicValues : currentEnum.values).push({
name: name,
value: value,
comment: comment
});
}
}
});
if (--remainingFiles == 0) {
// All done
console.log('Finished downloading and parsing enums');
g_EnumNames = Object.keys(g_EnumNames);
g_EnumNames.sort();
let loader = GENERATED_FILE_HEADER + 'const SteamUserBase = require(\'./00-base.js\');\n\nclass SteamUserEnums extends SteamUserBase {\n}\n\n';
loader += g_EnumNames.map(name => `SteamUserEnums.${name} = require('../enums/${name}.js');`).join('\n');
loader += '\n\nmodule.exports = SteamUserEnums;\n';
FS.writeFileSync(__dirname + '/../components/01-enums.js', loader);
console.log('Wrote loader');
}
});
});
// All done
});
function processProtobufEnums() {
console.log('Processing protobuf enums...');
const Schema = require('../protobufs/generated/_load.js');
for (let enumName in Schema) {
if (!Object.hasOwnProperty.call(Schema, enumName)) {
continue;
}
if (enumName[0] != 'E' || Schema[enumName].encode || Schema[enumName].create) {
continue; // not an enum
}
process.stdout.write(`Generating ${enumName}.js... `);
let thisEnum = Schema[enumName];
if (ENUM_NAMES_TO_FIX[enumName]) {
enumName = ENUM_NAMES_TO_FIX[enumName];
}
let processed = [];
for (let i in thisEnum) {
if (!Object.hasOwnProperty.call(thisEnum, i)) {
continue;
}
let name = i.replace(new RegExp(`^k_${enumName}_?`), '').replace(/^k_E[^_]+_/, '');
if (ENUMS_WITH_DIFFERENT_PREFIXES_FROM_THEIR_NAMES[enumName]) {
name = name.replace(ENUMS_WITH_DIFFERENT_PREFIXES_FROM_THEIR_NAMES[enumName], '');
} else if (ENUMS_WITH_SOMETIMES_DIFFERENT_PREFIXES[enumName]) {
if (name.startsWith(ENUMS_WITH_SOMETIMES_DIFFERENT_PREFIXES[enumName]) && !name.startsWith(`k_${enumName}`)) {
name = name.replace(ENUMS_WITH_SOMETIMES_DIFFERENT_PREFIXES[enumName], '');
}
}
processed.push({
name,
value: thisEnum[i]
});
}
let enumFileName = `${__dirname}/../enums/${enumName}.js`;
// Check to see if the enum has changed at all
let {changed, valuesToAdd} = validateEnum(enumFileName, processed);
if (!changed) {
// Enum did not change
console.log('unchanged');
g_EnumNames[enumName] = true;
let normalized = enumName.toLowerCase();
if (g_EnumNamesNormalized[normalized] && g_EnumNamesNormalized[normalized] != enumName) {
throw new Error(`Duplicate enum ${enumName}`);
}
g_EnumNamesNormalized[normalized] = enumName;
continue;
}
process.stdout.write('\n');
processed = processed.concat(valuesToAdd);
processed.sort(sortEnum);
processed.forEach((val) => {
if (ENUM_VALUES_TO_FIX[val.name]) {
val.name = ENUM_VALUES_TO_FIX[val.name];
}
});
let enumFile = GENERATED_FILE_HEADER + `/**\n * @enum\n * @readonly\n */\nconst ${enumName} = {\n`;
enumFile += processed.map(v => `\t${quoteObjectKey(v.name)}: ${v.value},` + (v.comment ? ` // ${v.comment}` : '')).join('\n');
enumFile += '\n\n\t// Value-to-name mapping for convenience\n';
enumFile += processed.filter(v => v.comment !== 'obsolete').map(v => `\t${quoteObjectKey(v.value)}: '${v.name}',`).join('\n');
enumFile += `\n};\n\nmodule.exports = ${enumName};\n`;
FS.writeFileSync(`${__dirname}/../enums/${enumName}.js`, enumFile);
g_EnumNames[enumName] = true;
let normalized = enumName.toLowerCase();
if (g_EnumNamesNormalized[normalized] && g_EnumNamesNormalized[normalized] != enumName) {
throw new Error(`Duplicate enum ${enumName}`);
}
g_EnumNamesNormalized[normalized] = enumName;
}
console.log('Finished processing protobuf enums');
}
// Helper functions
function download(url, callback) {
let reqData = URL.parse(url);
reqData.servername = reqData.hostname;
reqData.headers = {'User-Agent': 'node-steam-user data parser'};
reqData.method = 'GET';
// This will crash if there's an error. But that's fine.
HTTPS.request(reqData, function(res) {
let data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
callback(data);
});
}).end();
}
function validateEnum(enumFileName, values, dynamicValues = []) {
let output = {
changed: true,
valuesToAdd: []
};
if (FS.existsSync(enumFileName)) {
let existingEnum = Object.assign({}, require(enumFileName)); // clone it since we're about to manipulate it
for (let i in existingEnum) {
if (i.match(/^-?\d+$/)) {
delete existingEnum[i];
continue;
}
if (dynamicValues.some(v => v.name == i)) {
// This is a dynamic value
continue;
}
let isGone = !values.some(v => v.value == existingEnum[i] && v.name == i);
let wasRenamed = isGone && values.some(v => v.value == existingEnum[i] && v.name != i);
if (isGone) {
// Looks like the name of this value has changed, or it was deleted entirely
output.valuesToAdd.push({name: i, value: existingEnum[i].toString(), comment: wasRenamed ? 'obsolete' : 'removed'});
}
}
if (values.length + output.valuesToAdd.length + dynamicValues.length == Object.keys(existingEnum).length) {
// Enum did not change
output.changed = false;
}
}
return output;
}
function sortEnum(a, b) {
let aValue = parseInt(a.value, 10);
let bValue = parseInt(b.value, 10);
if (isNaN(aValue)) {
aValue = a.value;
}
if (isNaN(bValue)) {
bValue = b.value;
}
if (aValue == bValue) {
// We want obsolete/removed values to go first
if (a.comment && !b.comment) {
return -1;
} else if (b.comment && !a.comment) {
return 1;
}
if (a.name == b.name) {
return 0;
}
if (a.name.startsWith('Base') || a.name.endsWith('Base')) {
return -1;
} else if (b.name.startsWith('Base') || b.name.endsWith('Base')) {
return 1;
}
return a.name < b.name ? -1 : 1;
}
if (aValue == bValue) {
return 0;
}
return aValue < bValue ? -1 : 1;
}
function quoteObjectKey(key) {
return key.toString().includes('-') ? `'${key}'` : key;
}