UNPKG

fnbr

Version:

A library to interact with Epic Games' Fortnite HTTP and XMPP services

539 lines 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.chunk = exports.resolveAuthObject = exports.resolveAuthString = exports.parseStatKey = exports.createDefaultInputTypeStats = exports.calcSTWNonSurvivorPowerLevel = exports.parseSTWHeroTemplateId = exports.calcSTWSurvivorLeadBonus = exports.calcSTWSurvivorBonus = exports.calcSTWSurvivorPowerLevel = exports.parseSTWSurvivorTemplateId = exports.buildReplay = exports.parseM3U8File = exports.parseBlurlStream = exports.createPartyInvitation = exports.getRandomDefaultCharacter = exports.makeSnakeCase = exports.makeCamelCase = exports.consoleQuestion = void 0; exports.parseSTWSchematicTemplateId = parseSTWSchematicTemplateId; const tslib_1 = require("tslib"); /* eslint-disable no-restricted-syntax */ const readline_1 = tslib_1.__importDefault(require("readline")); const zlib_1 = tslib_1.__importDefault(require("zlib")); const crypto_1 = tslib_1.__importDefault(require("crypto")); const fs_1 = require("fs"); const Enums_1 = require("../../enums/Enums"); const BinaryWriter_1 = tslib_1.__importDefault(require("./BinaryWriter")); const PowerLevelCurves_1 = tslib_1.__importDefault(require("../../resources/PowerLevelCurves")); const defaultCharacters = [ 'CID_A_272_Athena_Commando_F_Prime', 'CID_A_273_Athena_Commando_F_Prime_B', 'CID_A_274_Athena_Commando_F_Prime_C', 'CID_A_275_Athena_Commando_F_Prime_D', 'CID_A_276_Athena_Commando_F_Prime_E', 'CID_A_277_Athena_Commando_F_Prime_F', 'CID_A_278_Athena_Commando_F_Prime_G', 'CID_A_279_Athena_Commando_M_Prime', 'CID_A_280_Athena_Commando_M_Prime_B', 'CID_A_281_Athena_Commando_M_Prime_C', 'CID_A_282_Athena_Commando_M_Prime_D', 'CID_A_283_Athena_Commando_M_Prime_E', 'CID_A_284_Athena_Commando_M_Prime_F', 'CID_A_285_Athena_Commando_M_Prime_G', ]; const consoleQuestion = (question) => new Promise((resolve) => { const itf = readline_1.default.createInterface(process.stdin, process.stdout); itf.question(question, (answer) => { itf.close(); resolve(answer); }); }); exports.consoleQuestion = consoleQuestion; const makeCamelCase = (obj) => { const returnObj = {}; Object.keys(obj).forEach((k) => { returnObj[k.split('_').map((s, i) => (i > 0 ? `${s.charAt(0).toUpperCase()}${s.slice(1)}` : s)).join('')] = obj[k]; }); return returnObj; }; exports.makeCamelCase = makeCamelCase; const makeSnakeCase = (obj) => { const returnObj = {}; Object.keys(obj).forEach((k) => { returnObj[k.replace(/[A-Z]/g, (l) => `_${l.toLowerCase()}`)] = obj[k]; }); return returnObj; }; exports.makeSnakeCase = makeSnakeCase; const getRandomDefaultCharacter = () => defaultCharacters[Math.floor(Math.random() * defaultCharacters.length)]; exports.getRandomDefaultCharacter = getRandomDefaultCharacter; const createPartyInvitation = (clientUserId, pingerId, data) => { const member = data.members.find((m) => m.account_id === pingerId); const partyMeta = data.meta; const memberMeta = member.meta; const meta = { 'urn:epic:conn:type_s': 'game', 'urn:epic:cfg:build-id_s': partyMeta['urn:epic:cfg:build-id_s'], 'urn:epic:invite:platformdata_s': '', }; if (memberMeta.Platform_j) { meta.Platform_j = JSON.parse(memberMeta.Platform_j).Platform.platformStr; } if (memberMeta['urn:epic:member:dn_s']) meta['urn:epic:member:dn_s'] = memberMeta['urn:epic:member:dn_s']; return { party_id: data.id, sent_by: pingerId, sent_to: clientUserId, sent_at: data.sent, updated_at: data.sent, expires_at: data.expies_at, status: 'SENT', meta, }; }; exports.createPartyInvitation = createPartyInvitation; const parseBlurlStream = (stream) => new Promise((res) => { zlib_1.default.inflate(stream.slice(8), (err, buffer) => { const data = JSON.parse(buffer.toString()); res(data); }); }); exports.parseBlurlStream = parseBlurlStream; const parseM3U8FileLine = (line) => { const [key, value] = line.replace(/^#EXT-X-/, '').split(/:(.+)/); let output; if (value.includes(',')) { output = {}; let store = ''; let isString = false; for (const char of value.split('')) { if (char === '"') { isString = !isString; } else if (char === ',' && !isString) { const [vK, vV] = store.split(/=(.+)/); output[vK] = vV.replace(/(^"|"$)/g, ''); store = ''; } else { store += char; } } } else { output = value; } return [key, output]; }; const parseM3U8File = (data) => { const output = { streams: [], }; let streamInf; for (const line of data.split(/\n/).slice(1)) { if (line.startsWith('#EXT-X-STREAM-INF:')) { [, streamInf] = parseM3U8FileLine(line); } else if (line.startsWith('#EXT-X-')) { const [key, value] = parseM3U8FileLine(line); output[key] = value; } else if (!line.startsWith('#') && streamInf && line.length > 0) { output.streams.push({ data: streamInf, url: line, }); streamInf = undefined; } } return output; }; exports.parseM3U8File = parseM3U8File; const buildReplayMeta = (replay, replayData) => { replay .writeUInt32(480436863) .writeUInt32(6) .writeUInt32(replayData.LengthInMS) .writeUInt32(replayData.NetworkVersion) .writeUInt32(replayData.Changelist) .writeString(replayData.FriendlyName.padEnd(256), 'utf16le') .writeBool(replayData.bIsLive) .writeUInt64((BigInt(new Date(replayData.Timestamp).getTime()) * BigInt('10000')) + BigInt('621355968000000000')) .writeBool(replayData.bCompressed) .writeBool(false) .writeUInt32(0); }; const buildChunks = (replay, replayData) => { var _a, _b, _c; const chunks = [{ chunkType: 0, data: replayData.Header, }, ...((_a = replayData.DataChunks) === null || _a === void 0 ? void 0 : _a.map((c) => ({ ...c, chunkType: 1, }))) || [], ...((_b = replayData.Checkpoints) === null || _b === void 0 ? void 0 : _b.map((c) => ({ ...c, chunkType: 2, }))) || [], ...((_c = replayData.Events) === null || _c === void 0 ? void 0 : _c.map((c) => ({ ...c, chunkType: 3, }))) || []]; for (const chunk of chunks) { replay.writeUInt32(chunk.chunkType); const chunkSizeOffset = replay.offset; replay.writeInt32(0); switch (chunk.chunkType) { case 0: replay.writeBytes(chunk.data); break; case 1: replay .writeUInt32(chunk.Time1) .writeUInt32(chunk.Time2) .writeUInt32(chunk.data.length) .writeInt32(chunk.SizeInBytes) .writeBytes(chunk.data); break; case 2: replay .writeString(chunk.Id) .writeString(chunk.Group) .writeString(chunk.Metadata || '') .writeUInt32(chunk.Time1) .writeUInt32(chunk.Time2) .writeUInt32(chunk.data.length) .writeBytes(chunk.data); break; case 3: replay .writeString(chunk.Id) .writeString(chunk.Group) .writeString(chunk.Metadata || '') .writeUInt32(chunk.Time1) .writeUInt32(chunk.Time2) .writeUInt32(chunk.data.length) .writeBytes(chunk.data); break; } const chunkSize = replay.offset - (chunkSizeOffset + 4); const savedOffset = replay.offset; replay .goto(chunkSizeOffset) .writeInt32(chunkSize) .goto(savedOffset); } }; const buildReplay = (replayData, addStats) => { var _a, _b, _c; if (replayData.Events && addStats) { replayData.Events.push({ Id: `${replayData.ReplayName}_${crypto_1.default.randomBytes(16).toString('hex')}`, Group: 'AthenaReplayBrowserEvents', Metadata: 'AthenaMatchStats', data: Buffer.alloc(48), Time1: replayData.LengthInMS - 15000, Time2: replayData.LengthInMS - 15000, }); replayData.Events.push({ Id: `${replayData.ReplayName}_${crypto_1.default.randomBytes(16).toString('hex')}`, Group: 'AthenaReplayBrowserEvents', Metadata: 'AthenaMatchTeamStats', data: Buffer.alloc(12), Time1: replayData.LengthInMS - 15000, Time2: replayData.LengthInMS - 15000, }); } const finalReplayByteLength = 562 // meta + 8 + replayData.Header.length // header + (((_a = replayData.DataChunks) === null || _a === void 0 ? void 0 : _a.map((c) => 8 + 16 + c.data.length).reduce((acc, cur) => acc + cur)) || 0) // datachunks + (((_b = replayData.Events) === null || _b === void 0 ? void 0 : _b.map((e) => 8 + 12 + e.Id.length + 5 + e.Group.length + 5 + (e.Metadata ? e.Metadata.length + 5 : 5) + e.data.length).reduce((acc, cur) => acc + cur)) || 0) // events + (((_c = replayData.Checkpoints) === null || _c === void 0 ? void 0 : _c.map((c) => 8 + 12 + c.Id.length + 5 + c.Group.length + 5 + (c.Metadata ? c.Metadata.length + 5 : 5) + c.data.length).reduce((acc, cur) => acc + cur)) || 0); // checkpoints const replay = new BinaryWriter_1.default(Buffer.alloc(finalReplayByteLength)); buildReplayMeta(replay, replayData); buildChunks(replay, replayData); return replay.buffer; }; exports.buildReplay = buildReplay; const parseSTWSurvivorTemplateId = (templateId) => { const id = templateId.split(':')[1]; const fields = id.split('_'); let type; const rawType = fields.shift(); if (rawType === 'worker') type = 'special'; else if (rawType === null || rawType === void 0 ? void 0 : rawType.includes('manager')) type = 'manager'; else type = 'basic'; const tier = parseInt(fields.pop().slice(1), 10); const rarity = (type === 'manager' ? fields.shift() : fields.pop()); const name = fields[0] ? fields.join('_') : undefined; return { type, tier, rarity, name, }; }; exports.parseSTWSurvivorTemplateId = parseSTWSurvivorTemplateId; const calcSTWSurvivorPowerLevel = (rarity, isLeader, level, tier) => { /** * We use Exclude<> here because mythic lead survivors actually have SR (legendary) rarity, * and thus there are no rating curves for UR managers. */ const key = isLeader ? `manager_${rarity}_t0${tier}` : `default_${rarity}_t0${tier}`; return PowerLevelCurves_1.default.survivorItemRating[key].eval(level); }; exports.calcSTWSurvivorPowerLevel = calcSTWSurvivorPowerLevel; const calcSTWSurvivorBonus = (leaderPersonality, leaderRarity, survivorPersonality, survivorPowerLevel) => { if (survivorPersonality === leaderPersonality) { if (leaderRarity === 'sr') return 8; if (leaderRarity === 'vr') return 5; if (leaderRarity === 'r') return 4; if (leaderRarity === 'uc') return 3; if (leaderRarity === 'c') return 2; } else if (leaderRarity === 'sr') { if (survivorPowerLevel <= 2) return 0; return -2; } return 0; }; exports.calcSTWSurvivorBonus = calcSTWSurvivorBonus; const calcSTWSurvivorLeadBonus = (managerSynergy, squadName, powerLevel) => { const leaderMatch = managerSynergy.split('.')[2]; if (Enums_1.STWLeadSynergy[squadName] === leaderMatch) { return powerLevel; } return 0; }; exports.calcSTWSurvivorLeadBonus = calcSTWSurvivorLeadBonus; const parseSTWHeroTemplateId = (templateId) => { const id = templateId.split(':')[1]; const fields = id.split('_'); fields.shift(); const type = fields.shift(); const tier = parseInt(fields.pop().slice(1), 10); const rarity = fields.pop(); const name = fields[0] ? fields.join('_') : undefined; return { type, tier, rarity, name, }; }; exports.parseSTWHeroTemplateId = parseSTWHeroTemplateId; const calcSTWNonSurvivorPowerLevel = (rarity, level, tier) => (PowerLevelCurves_1.default.baseItemRating[`default_${rarity}_t0${tier}`].eval(level)); exports.calcSTWNonSurvivorPowerLevel = calcSTWNonSurvivorPowerLevel; function parseSTWSchematicTemplateId(templateId) { const id = templateId.split(':')[1]; const fields = id.split('_'); let type; let subType; let firstField = fields.shift(); if (firstField !== 'sid') { // probably ammo or something weird fields.unshift(firstField); return { type: 'other', subType: undefined, tier: undefined, evoType: undefined, rarity: undefined, name: fields[0] ? fields.join('_') : undefined, }; } firstField = fields.shift(); let nextField; let i; switch (firstField) { case 'edged': type = 'melee'; // look for "axe", "scythe", or "sword", which might occur later in the name i = fields.findIndex((f) => f === 'axe' || f === 'scythe' || f === 'sword'); if (i >= 0) { subType = `edged_${fields[i]}`; fields.splice(i, 1); } else { subType = undefined; } break; case 'blunt': type = 'melee'; // if the next field is "hammer", then the full type is "blunt_hammer", otherwise "blunt" nextField = fields.shift(); if (nextField === 'hammer') { subType = 'blunt_hammer'; } else { if (nextField !== undefined) { fields.unshift(nextField); } subType = 'blunt'; } break; case 'piercing': type = 'melee'; // "piercing" should always be followed by "spear", because spear is the only piercing weapon type nextField = fields.shift(); if (nextField === 'spear') { subType = 'piercing_spear'; } else { if (nextField !== undefined) { fields.unshift(nextField); } subType = undefined; } break; case 'ceiling': case 'floor': case 'wall': type = 'trap'; subType = firstField; break; case 'explosive': case 'launcher': // "launcher" and "explosive" are used interchangeably to refer to the same subtype, // but only some weapons of that subtype use "explosive" ammo, so we settle on "launcher" type = 'ranged'; subType = 'launcher'; break; case 'assault': type = 'ranged'; // some weapons that were originally ARs were recategorized as SMGs when that subtype // was added, and now they have both "assault" and "smg" in the name i = fields.findIndex((f) => f === 'smg'); if (i >= 0) { subType = 'smg'; fields.splice(i, 1); fields.unshift(firstField); } else { subType = 'assault'; } break; case 'pistol': case 'shotgun': case 'smg': case 'sniper': type = 'ranged'; subType = firstField; break; default: // the name doesn't fit any known pattern, so this might be a new type of schematic. // in the meantimwe, we still need to represent it somehow. type = 'other'; subType = firstField; break; } const tier = parseInt(fields.pop().slice(1), 10); const evoType = type !== 'trap' ? fields.pop() : undefined; const rarity = fields.pop(); const name = fields[0] ? fields.join('_') : undefined; return { type, subType, tier, evoType, rarity, name, }; } const defaultStats = { score: 0, scorePerMin: 0, scorePerMatch: 0, wins: 0, top3: 0, top5: 0, top6: 0, top10: 0, top12: 0, top25: 0, kills: 0, killsPerMin: 0, killsPerMatch: 0, deaths: 0, kd: 0, matches: 0, winRate: 0, minutesPlayed: 0, playersOutlived: 0, lastModified: undefined, }; const createDefaultInputTypeStats = () => ({ overall: { ...defaultStats }, solo: { ...defaultStats }, duo: { ...defaultStats }, squad: { ...defaultStats }, ltm: { ...defaultStats }, }); exports.createDefaultInputTypeStats = createDefaultInputTypeStats; const parseStatKey = (key, value) => { switch (key) { case 'lastmodified': return ['lastModified', new Date(value * 1000)]; case 'placetop25': return ['top25', value]; case 'placetop12': return ['top12', value]; case 'placetop10': return ['top10', value]; case 'placetop6': return ['top6', value]; case 'placetop5': return ['top5', value]; case 'placetop3': return ['top3', value]; case 'placetop1': return ['wins', value]; case 'playersoutlived': return ['playersOutlived', value]; case 'minutesplayed': return ['minutesPlayed', value]; case 'matchesplayed': return ['matches', value]; default: return [key, value]; } }; exports.parseStatKey = parseStatKey; const resolveAuthString = async (str) => { switch (typeof str) { case 'function': return str(); case 'string': if (str.length === 32 || str.startsWith('eg1')) { return str; } return fs_1.promises.readFile(str, 'utf8'); default: throw new TypeError(`The type "${typeof str}" does not resolve to a valid auth string`); } }; exports.resolveAuthString = resolveAuthString; const resolveAuthObject = async (obj) => { switch (typeof obj) { case 'function': return obj(); case 'string': return JSON.parse(await fs_1.promises.readFile(obj, 'utf8')); case 'object': return obj; default: throw new TypeError(`The type "${typeof obj}" does not resolve to a valid auth object`); } }; exports.resolveAuthObject = resolveAuthObject; const chunk = (array, maxSize) => { const chunkedArray = []; for (let i = 0; i < array.length; i += maxSize) { chunkedArray.push(array.slice(i, i + maxSize)); } return chunkedArray; }; exports.chunk = chunk; //# sourceMappingURL=Util.js.map