smoke-machine-js
Version:
A collection of helper functions often used in my projects.
603 lines (550 loc) • 16.8 kB
JavaScript
/**
* @author Owen Rossi-Keen
* https://owenrossikeen.com/
*/
// []==============[]
// [] DEPENDENCIES []
// []==============[]
// Dependencies
const ByteBuffer = require('bytebuffer');
const CryptoJS = require('crypto-js');
const readline = require('readline');
const fs = require('fs');
const recursive = require('recursive-readdir');
// []============[]
// [] NAMESPACES []
// []============[]
/** @namespace Definitions */
/** @namespace helperFunctions */
/** @namespace launchFunctions */
// []=============[]
// [] DEFINITIONS []
// []=============[]
/**
* @memberof Definitions
* The size of the prefix required to denote the size of a TobComm encoded string.
*/
const STRING_LENGTH_PREFIX_SIZE = 2;
/**
* @memberof Definitions
* A string containing TobComm valid characters. Currently unused.
*/
const SANCTIFIED_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
// []==================[]
// [] HELPER FUNCTIONS []
// []==================[]
/**
* @param {string} str
* @returns ByteBuffer containing SHA256 encoded str
* @memberof helperFunctions
*/
const stringToSHA256 = (str) => {
return new ByteBuffer(32)
.append(Buffer.from(CryptoJS.SHA256(str).toString(CryptoJS.enc.Hex), 'hex'))
.reset();
};
/**
* @param {string} str
* @returns Byte Array containing encoded str
* @memberof helperFunctions
*/
const stringToByteBuffer = (str) => {
const bb = new ByteBuffer(str.length);
bb.writeString(str);
return bb.buffer;
};
/**
* @param {bytearray} bytearray
* @returns Decoded 64-bit Floating Point number
* @memberof helperFunctions
*/
const intFromBytes = (byteArray) => {
if (byteArray.length !== 4) return console.log('Yikes!');
const bb = new ByteBuffer(4).append(byteArray);
return (
(bb.buffer[0] << 24) +
(bb.buffer[1] << 16) +
(bb.buffer[2] << 8) +
bb.buffer[3]
);
};
/**
* Useful for rounding single-precision floats (such as those encoded in a Java application)
* @param {number} float
* @returns Number rounded to the 100ths place.
* @memberof helperFunctions
*/
const roundFloat = (float) => Math.round(float * 100) / 100;
/**
* @param {boolean} full - Whether or not function returns seconds. default: false.
* @returns Formatted string time
* @memberof helperFunctions
*/
const getTime = (full = false) => {
// This is a hacky way to do it, but it works 🤷♀️
let currentDate = new Date();
if (full) return currentDate.toJSON().slice(0, 19).replace('T', ' || ');
return currentDate.toJSON().slice(0, 19).split('T')[1];
};
/**
* Iterates over all files (excluding node_modules and the like) to determine the (roughly estimated) number of lines of code in a given project
* Asynchronous
* @memberof helperFunctions
*/
const projectAudit = async () => {
const countLines = async (filteredPaths) =>
new Promise((res, rej) => {
var totalLines = 0;
var filesRead = 0;
filteredPaths.forEach((filePath, idx) => {
let linesCount = 0;
readline
.createInterface({
input: fs.createReadStream(filePath),
output: process.stdout,
terminal: false,
})
.on('line', () => {
linesCount++;
})
.on('close', () => {
filesRead++;
totalLines = totalLines + linesCount;
if (filesRead == filteredPaths.length) res(totalLines);
});
});
});
const remove = (array, phrase) =>
array.filter((item) => item.indexOf(phrase) == -1);
// Get array of all file paths
const filePaths = await recursive('../../', [
(path) => {
var a = true;
if (
path.indexOf(
'node_modules' &&
path.indexOf('android') &&
path.indexOf('package') &&
path.indexOf('build') &&
path.indexOf('message') &&
path.indexOf('build') &&
path.indexOf('.mext')
) == -1
) {
a = false;
}
if (path.indexOf('node_modules')) return false;
},
]);
// Filter out unwanted files
const filtered = remove(
remove(
remove(
remove(
remove(
remove(
filePaths.filter(
(path, idx) =>
path.slice(-3) == 'tml' ||
path.slice(-3) == 'jsx' ||
path.slice(-3) == '.js' ||
path.slice(-3) == 'son' ||
path.slice(-3) == 'tsx' ||
path.slice(-3) == 'css' ||
path.slice(-3) == 'txt' ||
path.slice(-3) == '.md'
),
'node_modules'
),
'android'
),
'package'
),
'build'
),
'.next'
),
'message'
);
// Pass array of filtered files to counting function
let finalCount = await countLines(filtered);
// Log the final count
return finalCount;
};
/**
* Generates a crc32 checksum from a string
* @param {string} str - The stringified data to be converted.
* @returns crc32 checksum number
* @memberof helperFunctions
*/
const crc32 = (str) => {
for (var a, o = [], c = 0; c < 256; c++) {
a = c;
for (var f = 0; f < 8; f++) a = 1 & a ? 3988292384 ^ (a >>> 1) : a >>> 1;
o[c] = a;
}
for (var n = -1, t = 0; t < str.length; t++)
n = (n >>> 8) ^ o[255 & (n ^ str.charCodeAt(t))];
return (-1 ^ n) >>> 0;
};
/**
* Takes a parsed JSON object which contains numbers read as strings and converts those strings to numbers.
* e.g. userID: "1234" would become userID: 1234.
* Also has the nasty tendency to turn empty arrays into zeroes.
* @param {object} object - The stringified data to be converted.
* @returns Object with numbers, rather than strings.
* @memberof helperFunctions
*/
const parsedJsonStringsToInt = (object) => {
if (Array.isArray(object)) {
return object.map((data) => parsedJsonStringsToInt(data));
}
var obj = object;
for (var prop in obj) {
if (Array.isArray(obj[prop])) {
this.parsedJsonStringsToInt(obj[prop]);
} else {
switch (obj[prop]) {
case 'true':
obj[prop] = true;
break;
case 'false':
obj[prop] = false;
break;
default:
if (
obj.hasOwnProperty(prop) &&
obj[prop] !== null &&
!isNaN(obj[prop])
) {
obj[prop] = +obj[prop];
}
break;
}
}
}
return obj;
};
/**
* Converts degrees to radians. Simple as.
* @param {number} deg
* @memberof helperFunctions
*/
const degreesToRadians = (deg) => {
return deg * (Math.PI / 180);
};
/**
* Converts radians to degrees. Simple as.
* @param {number} rad
* @memberof helperFunctions
*/
const radiansToDegrees = (rad) => {
return rad * (180 / Math.PI);
};
/**
* Converts miles to kilometers. Simple as.
* @param {number} miles
* @memberof helperFunctions
*/
const milesToKilometers = (miles) => {
return miles / 1.609;
};
/**
* Converts kilometers to miles. Simple as.
* @param {number} kilometers
* @memberof helperFunctions
*/
const kilometersToMiles = (kilometers) => {
return kilometers * 1.609;
};
/**
* Converts a byte array into a hexadecimal string.
* @param {bytearray} data - Data to be encoded
* @param {number} length - The maximum number of bytes which will be read (left justified). Default: 32.
* @memberof helperFunctions
*/
const bytesToHexString = (data, length = 32) => {
let bb = new ByteBuffer(length).append(data).reset();
return bb.toHex();
};
/**
* Selects a pseudorandom value from the provided array.
* Because I was sick of typing 'Math.' 200 times.
* @param {array} arr - Array to be selected from.
* @memberof helperFunctions
*/
const arrayRandom = (arr) => arr[Math.floor(Math.random() * arr.length)];
/**
* Capitalizes a given string.
* @param {string} str - String to be capitalized
* @memberof helperFunctions
*/
const capitalizeString = (str) => str.replace(/^\w/, (c) => c.toUpperCase());
/**
* Generates an ordinal suffix for a number.
* e.g. 1 becomes 1st, 9 becomes 9th.
* @param {number} i - The number for which an ordinal will be generated.
* @returns The number concatenated with its ordinal.
* @memberof helperFunctions
*/
const generateOrdinal = (i) => {
var j = i % 10,
k = i % 100;
if (j == 1 && k != 11) {
return i + 'st';
}
if (j == 2 && k != 12) {
return i + 'nd';
}
if (j == 3 && k != 13) {
return i + 'rd';
}
return i + 'th';
};
/**
* Coarsely removes markdown tags from a string.
* e.g. *Hello __world__* becomes Hello world
* @param {string} str - The string with Markdown formatting
* @returns The number without markdown tags
* @memberof helperFunctions
*/
const removeMarkdownTags = (str) => {
if (str === null || str === '') {
return false;
} else {
str = str.toString();
return str.replace(/(<([^>]+)>)/gi, '');
}
};
/**
* Formats a string to camelCase
* e.g. Hello World becomes helloWorld.
* @param {string} str
* @returns camelCase string
* @memberof helperFunctions
*/
const camelCase = (str) => {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index == 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/\s+/g, '');
};
// []==================[]
// [] LAUNCH FUNCTIONS []
// []==================[]
/**
* Reads a Short at the provided ByteBuffer's offset, reads a string of the length denoted by the read short.
* This DOES iterate the ByteBuffer in the namespace where the function is called.
* @param {ByteBuffer} bb - The ByteBuffer from which the string will be read.
* @returns The string which was read.
* @memberof launchFunctions
*/
const stringFromData = (bb) => {
let lengthToRead = bb.readShort();
const cString = bb.readString(lengthToRead);
return cString;
};
/**
* Converts a string to a ByteBuffer containing a short (representing the length of the converted string), followed by the converted string.
* @param {string} str - The string which will be converted.
* @returns ByteArray
* @memberof launchFunctions
*/
const getStringData = (str) => {
const bb = new ByteBuffer.allocate(getStringDataSize(str));
cString = stringToBytesBB(str);
bb.writeShort(cString.length);
bb.append(cString);
return bb.buffer;
};
/**
* Returns the byte-size of a short (representing the length of the string), followed by a byte-encoded string
* @param {string} str - The string which will be measured.
* @returns Number
* @memberof launchFunctions
*/
const getStringDataSize = (str) => {
return STRING_LENGTH_PREFIX_SIZE + stringToBytesBB(str).length;
};
/**
* Sanitized the given name for profanity, as well as dangerous characters. Not currently functional.
* @param {string} str - The string which will be sanitized.
* @returns The sanitized string.
* @memberof launchFunctions
*/
const sanitizeName = (str) => {
// https://github.com/2Toad/Profanity
let strName = str.trim();
if (strName.length > 32) {
strName = `NTL${Math.random()}-${Math.random() * 100 * Math.pi}`.slice(
0,
32
);
} else {
if (strName.match('^.*[a-xzA-Z0-9][a-zA-Z0-9][a-zA-Z0-9].*$')) {
strName = `RGX${Math.random()}-${Math.random() * 100 * Math.pi}`.slice(
0,
32
);
}
}
return strName;
};
/**
* Cleans coarsely converted JSON game save (from XML) for use in Counterforce.
* This is an ultra-specific function for Counterforce.
* @param {object} json - The parsed JSON object.
* @returns Cleaned object.
* @memberof launchFunctions
*/
//
const cleanJSONGameSave = (json) => {
// This is assuming you manually removed the top-level Game: {} and Ctrl+F find-and-replaced all underscores with nothing.
let parsedGame = this.parsedJsonStringsToInt(json);
let userTest;
let playerTest;
let mSiteTest;
let sSiteTest;
// I'm aware this is some of the worst code ever written. It works, and I (hopefully) won't have to use it frequently.
Object.keys(parsedGame).forEach((key, idx) => {
if (key == 'Users') {
let fixedUser = parsedGame[key].map((user) => {
// for each user
let faux = { ...user };
if (user?.Reports?.Report) {
if (Array.isArray(user?.Reports?.Report)) {
faux.Reports = [...user.Reports.Report];
} else {
faux.Reports = [user.Reports.Report];
}
}
return faux;
});
userTest = fixedUser;
return;
}
if (key == 'Players') {
let fixedPlayer = parsedGame[key].map((player) => {
let faux = { ...player };
if (player?.MissileSystem?.Slots?.Slot) {
if (player?.MissileSystem?.Slots?.Slot == '') {
faux.MissileSystem.Slots = [];
} else {
if (Array.isArray(player?.MissileSystem?.Slots?.Slot)) {
faux.MissileSystem.Slots = [...player.MissileSystem.Slots.Slot];
} else {
faux.MissileSystem.Slots = [player.MissileSystem.Slots.Slot];
}
}
}
if (player?.InterceptorSystem?.Slots?.Slot) {
if (player?.InterceptorSystem?.Slots?.Slot == '') {
faux.InterceptorSystem.Slots = [];
} else {
if (Array.isArray(player?.InterceptorSystem?.Slots?.Slot)) {
faux.InterceptorSystem.Slots = [
...player.InterceptorSystem.Slots.Slot,
];
} else {
faux.InterceptorSystem.Slots = [
player.InterceptorSystem.Slots.Slot,
];
}
}
}
return faux;
});
playerTest = fixedPlayer;
return;
}
if (key == 'MissileSites') {
let fixedSite = parsedGame[key].map((site) => {
let faux = { ...site };
if (site?.MissileSystem?.Slots?.Slot) {
if (site?.MissileSystem?.Slots?.Slot == '') {
faux.MissileSystem.Slots = [];
} else {
if (Array.isArray(site?.MissileSystem?.Slots?.Slot)) {
faux.MissileSystem.Slots = [...site.MissileSystem.Slots.Slot];
} else {
faux.MissileSystem.Slots = [site.MissileSystem.Slots.Slot];
}
}
}
return faux;
});
mSiteTest = fixedSite;
return;
}
if (key == 'SAMSites') {
let fixedSite = parsedGame[key].map((site) => {
let faux = { ...site };
if (site?.InterceptorSystem?.Slots?.Slot) {
if (site?.InterceptorSystem?.Slots?.Slot == '') {
faux.InterceptorSystem.Slots = [];
} else {
if (Array.isArray(site?.InterceptorSystem?.Slots?.Slot)) {
faux.InterceptorSystem.Slots = [
...site.InterceptorSystem.Slots.Slot,
];
} else {
faux.InterceptorSystem.Slots = [
site.InterceptorSystem.Slots.Slot,
];
}
}
}
return faux;
});
sSiteTest = fixedSite;
return;
}
});
fs.writeFileSync('./output.json', JSON.stringify(output));
let output = {
Users: userTest,
Alliances: parsedGame.AlliancesplayerTest,
Treaties: parsedGame.Treaties,
Players: playerTest,
Missiles: parsedGame.Missiles,
Interceptors: parsedGame.Interceptors,
MissileSites: mSiteTest,
SAMSites: sSiteTest,
SentryGuns: parsedGame.SentryGuns,
OreMines: parsedGame.OreMines,
Loots: parsedGame.Loots,
Radiations: parsedGame.Radiations,
};
return output;
};
// []=========[]
// [] EXPORTS []
// []=========[]
module.exports.helperFunctions = {
stringToSHA256,
stringToByteBuffer,
intFromBytes,
roundFloat,
getTime,
projectAudit,
crc32,
parsedJsonStringsToInt,
degreesToRadians,
radiansToDegrees,
milesToKilometers,
kilometersToMiles,
bytesToHexString,
arrayRandom,
capitalizeString,
generateOrdinal,
removeMarkdownTags,
camelCase,
};
module.exports.launchFunctions = {
cleanJSONGameSave,
getStringDataSize,
getStringData,
stringFromData,
sanitizeName,
};