fnbr
Version:
A library to interact with Epic Games' Fortnite HTTP and XMPP services
539 lines • 19.6 kB
JavaScript
;
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