From 4b78c2fbdfcdd9dcc794e4b0c17cfb6d88a1006b Mon Sep 17 00:00:00 2001 From: mat <27899617+mat-1@users.noreply.github.com> Date: Sat, 1 May 2021 19:04:52 -0500 Subject: Show minions that haven't been unlocked in response (#20) * add minions that haven't been unlocked yet to response, and slightly optimize constants * remove most console.logs with console.debugs * mock constants --- build/cleaners/skyblock/member.js | 2 +- build/cleaners/skyblock/minions.js | 44 +- build/constants.js | 147 ++-- build/database.js | 28 +- build/hypixel.js | 2 +- build/hypixelCached.js | 30 +- build/index.js | 2 +- src/cleaners/skyblock/member.ts | 2 +- src/cleaners/skyblock/minions.ts | 30 +- src/constants.ts | 136 ++-- src/database.ts | 24 +- src/hypixel.ts | 2 +- src/hypixelCached.ts | 30 +- src/index.ts | 3 +- test-data-generator/index.ts | 14 + test/data/constants/collections.json | 62 ++ test/data/constants/minions.json | 55 ++ test/data/constants/skills.json | 12 + test/data/constants/slayers.json | 5 + test/data/constants/stats.json | 773 +++++++++++++++++++++ test/data/constants/zones.json | 95 +++ .../player/6536bfed869548fd83a1ecd24cf2a0fd.json | 366 ++++------ .../player/ef3bb867eec048a1a9b92b451f0ffc66.json | 749 +++++++++----------- .../profiles/6536bfed869548fd83a1ecd24cf2a0fd.json | 48 +- .../profiles/ef3bb867eec048a1a9b92b451f0ffc66.json | 172 +++-- test/test.js | 17 +- 26 files changed, 1910 insertions(+), 940 deletions(-) create mode 100644 test/data/constants/collections.json create mode 100644 test/data/constants/minions.json create mode 100644 test/data/constants/skills.json create mode 100644 test/data/constants/slayers.json create mode 100644 test/data/constants/stats.json create mode 100644 test/data/constants/zones.json diff --git a/build/cleaners/skyblock/member.js b/build/cleaners/skyblock/member.js index ce322bc..218518d 100644 --- a/build/cleaners/skyblock/member.js +++ b/build/cleaners/skyblock/member.js @@ -59,7 +59,7 @@ async function cleanSkyBlockProfileMemberResponse(member, included = null) { stats: stats_1.cleanProfileStats(member), // this is used for leaderboards rawHypixelStats: (_a = member.stats) !== null && _a !== void 0 ? _a : {}, - minions: minions_1.cleanMinions(member), + minions: await minions_1.cleanMinions(member), fairy_souls: fairysouls_1.cleanFairySouls(member), inventories: inventoriesIncluded ? await inventory_1.cleanInventories(member) : undefined, objectives: objectives_1.cleanObjectives(member), diff --git a/build/cleaners/skyblock/minions.js b/build/cleaners/skyblock/minions.js index 0543e92..fae3c96 100644 --- a/build/cleaners/skyblock/minions.js +++ b/build/cleaners/skyblock/minions.js @@ -1,15 +1,36 @@ "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.countUniqueMinions = exports.combineMinionArrays = exports.cleanMinions = void 0; const hypixel_1 = require("../../hypixel"); +const constants = __importStar(require("../../constants")); /** * Clean the minions provided by Hypixel * @param minionsRaw The minion data provided by the Hypixel API */ -function cleanMinions(data) { +async function cleanMinions(member) { var _a; const minions = []; - for (const minionRaw of (_a = data === null || data === void 0 ? void 0 : data.crafted_generators) !== null && _a !== void 0 ? _a : []) { + const processedMinionNames = new Set(); + for (const minionRaw of (_a = member === null || member === void 0 ? void 0 : member.crafted_generators) !== null && _a !== void 0 ? _a : []) { // do some regex magic to get the minion name and level // examples of potential minion names: CLAY_11, PIG_1, MAGMA_CUBE_4 const minionName = minionRaw.split(/_\d/)[0].toLowerCase(); @@ -28,8 +49,25 @@ function cleanMinions(data) { matchingMinion.levels.push(false); // set the minion at that level to true matchingMinion.levels[minionLevel - 1] = true; + processedMinionNames.add(minionName); } - return minions; + const allMinionNames = new Set(await constants.fetchMinions()); + for (const minionName of processedMinionNames) { + if (!allMinionNames.has(minionName)) { + constants.addMinions(Array.from(processedMinionNames)); + break; + } + } + for (const minionName of allMinionNames) { + if (!processedMinionNames.has(minionName)) { + processedMinionNames.add(minionName); + minions.push({ + name: minionName, + levels: new Array(hypixel_1.maxMinion).fill(false) + }); + } + } + return minions.sort((a, b) => a.name > b.name ? 1 : (a.name < b.name ? -1 : 0)); } exports.cleanMinions = cleanMinions; /** diff --git a/build/constants.js b/build/constants.js index 96ae2e7..861eae6 100644 --- a/build/constants.js +++ b/build/constants.js @@ -2,25 +2,47 @@ /** * Fetch and edit constants from the skyblock-constants repo */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.addSlayers = exports.fetchSlayers = exports.addZones = exports.fetchZones = exports.addSkills = exports.fetchSkills = exports.addCollections = exports.fetchCollections = exports.addStats = exports.fetchStats = exports.addJSONConstants = void 0; -const node_fetch_1 = __importDefault(require("node-fetch")); -const https_1 = require("https"); +exports.addMinions = exports.fetchMinions = exports.addSlayers = exports.fetchSlayers = exports.addZones = exports.fetchZones = exports.addSkills = exports.fetchSkills = exports.addCollections = exports.fetchCollections = exports.addStats = exports.fetchStats = exports.addJSONConstants = exports.fetchJSONConstant = void 0; +// we have to do this so we can mock the function from the tests properly +const constants = __importStar(require("./constants")); const node_cache_1 = __importDefault(require("node-cache")); const queue_promise_1 = __importDefault(require("queue-promise")); +const node_fetch_1 = __importDefault(require("node-fetch")); +const https_1 = require("https"); +const _1 = require("."); const httpsAgent = new https_1.Agent({ keepAlive: true }); const githubApiBase = 'https://api.github.com'; const owner = 'skyblockstats'; const repo = 'skyblock-constants'; -// we use a queue for editing so it doesnt hit the github ratelimit as much +// we use a queue for editing so it always utilizes the cache if possible, and to avoid hitting the github rateimit const queue = new queue_promise_1.default({ concurrent: 1, - interval: 500 + interval: 10 }); /** * Send a request to the GitHub API @@ -31,6 +53,8 @@ const queue = new queue_promise_1.default({ */ async function fetchGithubApi(method, route, headers, json) { try { + if (_1.debug) + console.debug('fetching github api', method, route); return await node_fetch_1.default(githubApiBase + route, { agent: () => httpsAgent, body: json ? JSON.stringify(json) : null, @@ -56,20 +80,24 @@ const fileCache = new node_cache_1.default({ * Fetch a file from skyblock-constants * @param path The file path, for example stats.json */ -async function fetchFile(path) { - if (fileCache.has(path)) - return fileCache.get(path); - const r = await fetchGithubApi('GET', `/repos/${owner}/${repo}/contents/${path}`, { - 'Accept': 'application/vnd.github.v3+json', +function fetchFile(path) { + return new Promise(resolve => { + queue.enqueue(async () => { + if (fileCache.has(path)) + return resolve(fileCache.get(path)); + const r = await fetchGithubApi('GET', `/repos/${owner}/${repo}/contents/${path}`, { + 'Accept': 'application/vnd.github.v3+json', + }); + const data = await r.json(); + const file = { + path: data.path, + content: Buffer.from(data.content, data.encoding).toString(), + sha: data.sha + }; + fileCache.set(path, file); + resolve(file); + }); }); - const data = await r.json(); - const file = { - path: data.path, - content: Buffer.from(data.content, data.encoding).toString(), - sha: data.sha - }; - fileCache.set(path, file); - return file; } /** * Edit a file on skyblock-constants @@ -92,6 +120,7 @@ async function editFile(file, message, newContent) { }); } async function fetchJSONConstant(filename) { + console.log('actually fetchJSONConstant'); const file = await fetchFile(filename); try { return JSON.parse(file.content); @@ -101,83 +130,99 @@ async function fetchJSONConstant(filename) { return []; } } +exports.fetchJSONConstant = fetchJSONConstant; /** Add stats to skyblock-constants. This has caching so it's fine to call many times */ async function addJSONConstants(filename, addingValues, unit = 'stat') { if (addingValues.length === 0) return; // no stats provided, just return - queue.enqueue(async () => { - const file = await fetchFile(filename); - if (!file.path) - return; - let oldStats; - try { - oldStats = JSON.parse(file.content); - } - catch { - // invalid json, set it as an empty array - oldStats = []; - } - const updatedStats = oldStats - .concat(addingValues) - // remove duplicates - .filter((value, index, array) => array.indexOf(value) === index) - .sort((a, b) => a.localeCompare(b)); - const newStats = updatedStats.filter(value => !oldStats.includes(value)); - // there's not actually any new stats, just return - if (newStats.length === 0) - return; - const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new ${unit}s` : `Add '${newStats[0]}' ${unit}`; + let file = await fetchFile(filename); + if (!file.path) + return; + let oldStats; + try { + oldStats = JSON.parse(file.content); + } + catch { + // invalid json, set it as an empty array + oldStats = []; + } + const updatedStats = oldStats + .concat(addingValues) + // remove duplicates + .filter((value, index, array) => array.indexOf(value) === index) + .sort((a, b) => a.localeCompare(b)); + const newStats = updatedStats.filter(value => !oldStats.includes(value)); + // there's not actually any new stats, just return + if (newStats.length === 0) + return; + const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new ${unit}s` : `Add '${newStats[0]}' ${unit}`; + try { await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2)); - }); + } + catch { + // the file probably changed or something, try again + file = await fetchFile(filename); + await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2)); + } } exports.addJSONConstants = addJSONConstants; /** Fetch all the known SkyBlock stats as an array of strings */ async function fetchStats() { - return await fetchJSONConstant('stats.json'); + return await constants.fetchJSONConstant('stats.json'); } exports.fetchStats = fetchStats; /** Add stats to skyblock-constants. This has caching so it's fine to call many times */ async function addStats(addingStats) { - await addJSONConstants('stats.json', addingStats, 'stat'); + await constants.addJSONConstants('stats.json', addingStats, 'stat'); } exports.addStats = addStats; /** Fetch all the known SkyBlock collections as an array of strings */ async function fetchCollections() { - return await fetchJSONConstant('collections.json'); + return await constants.fetchJSONConstant('collections.json'); } exports.fetchCollections = fetchCollections; /** Add collections to skyblock-constants. This has caching so it's fine to call many times */ async function addCollections(addingCollections) { - await addJSONConstants('collections.json', addingCollections, 'collection'); + await constants.addJSONConstants('collections.json', addingCollections, 'collection'); } exports.addCollections = addCollections; /** Fetch all the known SkyBlock collections as an array of strings */ async function fetchSkills() { - return await fetchJSONConstant('skills.json'); + return await constants.fetchJSONConstant('skills.json'); } exports.fetchSkills = fetchSkills; /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ async function addSkills(addingSkills) { - await addJSONConstants('skills.json', addingSkills, 'skill'); + await constants.addJSONConstants('skills.json', addingSkills, 'skill'); } exports.addSkills = addSkills; /** Fetch all the known SkyBlock collections as an array of strings */ async function fetchZones() { - return await fetchJSONConstant('zones.json'); + return await constants.fetchJSONConstant('zones.json'); } exports.fetchZones = fetchZones; /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ async function addZones(addingZones) { - await addJSONConstants('zones.json', addingZones, 'zone'); + await constants.addJSONConstants('zones.json', addingZones, 'zone'); } exports.addZones = addZones; /** Fetch all the known SkyBlock slayer names as an array of strings */ async function fetchSlayers() { - return await fetchJSONConstant('slayers.json'); + return await constants.fetchJSONConstant('slayers.json'); } exports.fetchSlayers = fetchSlayers; /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ async function addSlayers(addingSlayers) { - await addJSONConstants('slayers.json', addingSlayers, 'slayer'); + await constants.addJSONConstants('slayers.json', addingSlayers, 'slayer'); } exports.addSlayers = addSlayers; +/** Fetch all the known SkyBlock slayer names as an array of strings */ +async function fetchMinions() { + return await constants.fetchJSONConstant('minions.json'); +} +exports.fetchMinions = fetchMinions; +/** Add skills to skyblock-constants. This has caching so it's fine to call many times */ +async function addMinions(addingMinions) { + await constants.addJSONConstants('minions.json', addingMinions, 'minion'); +} +exports.addMinions = addMinions; diff --git a/build/database.js b/build/database.js index 45b0179..9557668 100644 --- a/build/database.js +++ b/build/database.js @@ -355,27 +355,27 @@ async function getApplicableProfileLeaderboardAttributes(profile) { } /** Update the member's leaderboard data on the server if applicable */ async function updateDatabaseMember(member, profile) { - if (_1.debug) - console.log('updateDatabaseMember', member.username); if (!client) return; // the db client hasn't been initialized + if (_1.debug) + console.debug('updateDatabaseMember', member.username); // the member's been updated too recently, just return if (recentlyUpdated.get(profile.uuid + member.uuid)) return; // store the member in recentlyUpdated so it cant update for 3 more minutes recentlyUpdated.set(profile.uuid + member.uuid, true); if (_1.debug) - console.log('adding member to leaderboards', member.username); + console.debug('adding member to leaderboards', member.username); await constants.addStats(Object.keys(member.rawHypixelStats)); await constants.addCollections(member.collections.map(coll => coll.name)); await constants.addSkills(member.skills.map(skill => skill.name)); await constants.addZones(member.visited_zones.map(zone => zone.name)); await constants.addSlayers(member.slayers.bosses.map(s => s.raw_name)); if (_1.debug) - console.log('done constants..'); + console.debug('done constants..'); const leaderboardAttributes = await getApplicableMemberLeaderboardAttributes(member); if (_1.debug) - console.log('done getApplicableMemberLeaderboardAttributes..', leaderboardAttributes, member.username, profile.name); + console.debug('done getApplicableMemberLeaderboardAttributes..', leaderboardAttributes, member.username, profile.name); await memberLeaderboardsCollection.updateOne({ uuid: member.uuid, profile: profile.uuid @@ -402,7 +402,7 @@ async function updateDatabaseMember(member, profile) { cachedRawLeaderboards.set(attributeName, newRawLeaderboard); } if (_1.debug) - console.log('added member to leaderboards', member.username, leaderboardAttributes); + console.debug('added member to leaderboards', member.username, leaderboardAttributes); } exports.updateDatabaseMember = updateDatabaseMember; /** @@ -410,20 +410,20 @@ exports.updateDatabaseMember = updateDatabaseMember; * This will not also update the members, you have to call updateDatabaseMember separately for that */ async function updateDatabaseProfile(profile) { - if (_1.debug) - console.log('updateDatabaseProfile', profile.name); if (!client) return; // the db client hasn't been initialized + if (_1.debug) + console.debug('updateDatabaseProfile', profile.name); // the profile's been updated too recently, just return if (recentlyUpdated.get(profile.uuid + 'profile')) return; // store the profile in recentlyUpdated so it cant update for 3 more minutes recentlyUpdated.set(profile.uuid + 'profile', true); if (_1.debug) - console.log('adding profile to leaderboards', profile.name); + console.debug('adding profile to leaderboards', profile.name); const leaderboardAttributes = await getApplicableProfileLeaderboardAttributes(profile); if (_1.debug) - console.log('done getApplicableProfileLeaderboardAttributes..', leaderboardAttributes, profile.name); + console.debug('done getApplicableProfileLeaderboardAttributes..', leaderboardAttributes, profile.name); await profileLeaderboardsCollection.updateOne({ uuid: profile.uuid }, { @@ -451,7 +451,7 @@ async function updateDatabaseProfile(profile) { cachedRawLeaderboards.set(attributeName, newRawLeaderboard); } if (_1.debug) - console.log('added profile to leaderboards', profile.name, leaderboardAttributes); + console.debug('added profile to leaderboards', profile.name, leaderboardAttributes); } exports.updateDatabaseProfile = updateDatabaseProfile; const leaderboardUpdateMemberQueue = new queue_promise_1.default({ @@ -500,7 +500,7 @@ async function fetchAllLeaderboards(fast) { const leaderboards = await fetchAllMemberLeaderboardAttributes(); // shuffle so if the application is restarting many times itll still be useful if (_1.debug) - console.log('Caching leaderboards!'); + console.debug('Caching leaderboards!'); for (const leaderboard of util_1.shuffle(leaderboards)) { if (!fast) // wait 2 seconds so it doesnt use as much ram @@ -508,10 +508,10 @@ async function fetchAllLeaderboards(fast) { await fetchMemberLeaderboard(leaderboard); } if (_1.debug) - console.log('Finished caching leaderboards!'); + console.debug('Finished caching leaderboards!'); } // make sure it's not in a test -if (typeof global.it !== 'function') { +if (!globalThis.isTest) { connect().then(() => { // when it connects, cache the leaderboards and remove bad members removeBadMemberLeaderboardAttributes(); diff --git a/build/hypixel.js b/build/hypixel.js index 9e9007e..d77e1ae 100644 --- a/build/hypixel.js +++ b/build/hypixel.js @@ -69,7 +69,7 @@ async function fetchUser({ user, uuid, username }, included = ['player']) { if (!uuid) { // the user doesn't exist. if (_1.debug) - console.log('error:', user, 'doesnt exist'); + console.debug('error:', user, 'doesnt exist'); return null; } const includePlayers = included.includes('player'); diff --git a/build/hypixelCached.js b/build/hypixelCached.js index c9e4876..c5a027c 100644 --- a/build/hypixelCached.js +++ b/build/hypixelCached.js @@ -110,7 +110,7 @@ async function uuidFromUser(user) { return uuid; } if (_1.debug) - console.log('Cache miss: uuidFromUser', user); + console.debug('Cache miss: uuidFromUser', user); // set it as waitForCacheSet (a promise) in case uuidFromUser gets called while its fetching mojang exports.usernameCache.set(util_1.undashUuid(user), waitForCacheSet(exports.usernameCache, user, user)); // not cached, actually fetch mojang api now @@ -134,11 +134,11 @@ exports.uuidFromUser = uuidFromUser; async function usernameFromUser(user) { if (exports.usernameCache.has(util_1.undashUuid(user))) { if (_1.debug) - console.log('Cache hit! usernameFromUser', user); + console.debug('Cache hit! usernameFromUser', user); return exports.usernameCache.get(util_1.undashUuid(user)); } if (_1.debug) - console.log('Cache miss: usernameFromUser', user); + console.debug('Cache miss: usernameFromUser', user); let { uuid, username } = await mojang.profileFromUser(user); uuid = util_1.undashUuid(uuid); exports.usernameCache.set(uuid, username); @@ -181,7 +181,7 @@ async function fetchBasicPlayer(user) { return exports.basicPlayerCache.get(playerUuid); const player = await fetchPlayer(playerUuid); if (!player) - console.log('no player? this should never happen', user); + console.debug('no player? this should never happen', user); delete player.profiles; return player; } @@ -189,11 +189,11 @@ exports.fetchBasicPlayer = fetchBasicPlayer; async function fetchSkyblockProfiles(playerUuid) { if (exports.profilesCache.has(playerUuid)) { if (_1.debug) - console.log('Cache hit! fetchSkyblockProfiles', playerUuid); + console.debug('Cache hit! fetchSkyblockProfiles', playerUuid); return exports.profilesCache.get(playerUuid); } if (_1.debug) - console.log('Cache miss: fetchSkyblockProfiles', playerUuid); + console.debug('Cache miss: fetchSkyblockProfiles', playerUuid); const profiles = await hypixel.fetchMemberProfilesUncached(playerUuid); const basicProfiles = []; // create the basicProfiles array @@ -225,11 +225,11 @@ async function fetchBasicProfiles(user) { return; // invalid player, just return if (exports.basicProfilesCache.has(playerUuid)) { if (_1.debug) - console.log('Cache hit! fetchBasicProfiles', playerUuid); + console.debug('Cache hit! fetchBasicProfiles', playerUuid); return exports.basicProfilesCache.get(playerUuid); } if (_1.debug) - console.log('Cache miss: fetchBasicProfiles', user); + console.debug('Cache miss: fetchBasicProfiles', user); const player = await fetchPlayer(playerUuid); const profiles = player.profiles; exports.basicProfilesCache.set(playerUuid, profiles); @@ -247,11 +247,11 @@ async function fetchProfileUuid(user, profile) { // if a profile wasn't provided, return if (!profile) { if (_1.debug) - console.log('no profile provided?', user, profile); + console.debug('no profile provided?', user, profile); return null; } if (_1.debug) - console.log('Cache miss: fetchProfileUuid', user); + console.debug('Cache miss: fetchProfileUuid', user); const profiles = await fetchBasicProfiles(user); if (!profiles) return; // user probably doesnt exist @@ -275,11 +275,11 @@ async function fetchProfile(user, profile) { if (exports.profileCache.has(profileUuid)) { // we have the profile cached, return it :) if (_1.debug) - console.log('Cache hit! fetchProfile', profileUuid); + console.debug('Cache hit! fetchProfile', profileUuid); return exports.profileCache.get(profileUuid); } if (_1.debug) - console.log('Cache miss: fetchProfile', user, profile); + console.debug('Cache miss: fetchProfile', user, profile); const profileName = await fetchProfileName(user, profile); const cleanProfile = await hypixel.fetchMemberProfileUncached(playerUuid, profileUuid); // we know the name from fetchProfileName, so set it here @@ -296,7 +296,7 @@ async function fetchBasicProfileFromUuid(profileUuid) { if (exports.profileCache.has(profileUuid)) { // we have the profile cached, return it :) if (_1.debug) - console.log('Cache hit! fetchBasicProfileFromUuid', profileUuid); + console.debug('Cache hit! fetchBasicProfileFromUuid', profileUuid); const profile = exports.profileCache.get(profileUuid); return { uuid: profile.uuid, @@ -326,11 +326,11 @@ async function fetchProfileName(user, profile) { if (exports.profileNameCache.has(`${playerUuid}.${profileUuid}`)) { // Return the profile name if it's cached if (_1.debug) - console.log('Cache hit! fetchProfileName', profileUuid); + console.debug('Cache hit! fetchProfileName', profileUuid); return exports.profileNameCache.get(`${playerUuid}.${profileUuid}`); } if (_1.debug) - console.log('Cache miss: fetchProfileName', user, profile); + console.debug('Cache miss: fetchProfileName', user, profile); const basicProfiles = await fetchBasicProfiles(playerUuid); let profileName; for (const basicProfile of basicProfiles) diff --git a/build/index.js b/build/index.js index 0a6944c..f499c21 100644 --- a/build/index.js +++ b/build/index.js @@ -52,5 +52,5 @@ app.get('/leaderboards', async (req, res) => { res.json(await database_1.fetchAllLeaderboardsCategorized()); }); // only run the server if it's not doing tests -if (typeof global.it !== 'function') +if (!globalThis.isTest) app.listen(8080, () => console.log('App started :)')); diff --git a/src/cleaners/skyblock/member.ts b/src/cleaners/skyblock/member.ts index 5d8d4f9..cfa9a71 100644 --- a/src/cleaners/skyblock/member.ts +++ b/src/cleaners/skyblock/member.ts @@ -67,7 +67,7 @@ export async function cleanSkyBlockProfileMemberResponse(member, included: Inclu // this is used for leaderboards rawHypixelStats: member.stats ?? {}, - minions: cleanMinions(member), + minions: await cleanMinions(member), fairy_souls: cleanFairySouls(member), inventories: inventoriesIncluded ? await cleanInventories(member) : undefined, objectives: cleanObjectives(member), diff --git a/src/cleaners/skyblock/minions.ts b/src/cleaners/skyblock/minions.ts index 06e7752..21f7b66 100644 --- a/src/cleaners/skyblock/minions.ts +++ b/src/cleaners/skyblock/minions.ts @@ -1,4 +1,5 @@ import { maxMinion } from '../../hypixel' +import * as constants from '../../constants' export interface CleanMinion { name: string, @@ -10,9 +11,11 @@ export interface CleanMinion { * Clean the minions provided by Hypixel * @param minionsRaw The minion data provided by the Hypixel API */ -export function cleanMinions(data: any): CleanMinion[] { +export async function cleanMinions(member: any): Promise { const minions: CleanMinion[] = [] - for (const minionRaw of data?.crafted_generators ?? []) { + const processedMinionNames: Set = new Set() + + for (const minionRaw of member?.crafted_generators ?? []) { // do some regex magic to get the minion name and level // examples of potential minion names: CLAY_11, PIG_1, MAGMA_CUBE_4 const minionName = minionRaw.split(/_\d/)[0].toLowerCase() @@ -32,8 +35,29 @@ export function cleanMinions(data: any): CleanMinion[] { // set the minion at that level to true matchingMinion.levels[minionLevel - 1] = true + processedMinionNames.add(minionName) + } + + const allMinionNames = new Set(await constants.fetchMinions()) + + for (const minionName of processedMinionNames) { + if (!allMinionNames.has(minionName)) { + constants.addMinions(Array.from(processedMinionNames)) + break + } } - return minions + + for (const minionName of allMinionNames) { + if (!processedMinionNames.has(minionName)) { + processedMinionNames.add(minionName) + minions.push({ + name: minionName, + levels: new Array(maxMinion).fill(false) + }) + } + } + + return minions.sort((a, b) => a.name > b.name ? 1 : (a.name < b.name ? -1 : 0)) } /** diff --git a/src/constants.ts b/src/constants.ts index 5f20147..1652b6b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,11 +2,15 @@ * Fetch and edit constants from the skyblock-constants repo */ -import fetch from 'node-fetch' +// we have to do this so we can mock the function from the tests properly +import * as constants from './constants' + import * as nodeFetch from 'node-fetch' -import { Agent } from 'https' import NodeCache from 'node-cache' import Queue from 'queue-promise' +import fetch from 'node-fetch' +import { Agent } from 'https' +import { debug } from '.' const httpsAgent = new Agent({ keepAlive: true @@ -16,10 +20,10 @@ const githubApiBase = 'https://api.github.com' const owner = 'skyblockstats' const repo = 'skyblock-constants' -// we use a queue for editing so it doesnt hit the github ratelimit as much +// we use a queue for editing so it always utilizes the cache if possible, and to avoid hitting the github rateimit const queue = new Queue({ concurrent: 1, - interval: 500 + interval: 10 }) /** @@ -31,6 +35,7 @@ const queue = new Queue({ */ async function fetchGithubApi(method: string, route: string, headers?: any, json?: any): Promise { try { + if (debug) console.debug('fetching github api', method, route) return await fetch( githubApiBase + route, { @@ -67,26 +72,30 @@ const fileCache = new NodeCache({ * Fetch a file from skyblock-constants * @param path The file path, for example stats.json */ -async function fetchFile(path: string): Promise { - if (fileCache.has(path)) - return fileCache.get(path) - - const r = await fetchGithubApi( - 'GET', - `/repos/${owner}/${repo}/contents/${path}`, - { - 'Accept': 'application/vnd.github.v3+json', - }, - ) - const data = await r.json() - - const file = { - path: data.path, - content: Buffer.from(data.content, data.encoding).toString(), - sha: data.sha - } - fileCache.set(path, file) - return file +function fetchFile(path: string): Promise { + return new Promise(resolve => { + queue.enqueue(async() => { + if (fileCache.has(path)) + return resolve(fileCache.get(path)) + + const r = await fetchGithubApi( + 'GET', + `/repos/${owner}/${repo}/contents/${path}`, + { + 'Accept': 'application/vnd.github.v3+json', + }, + ) + const data = await r.json() + + const file = { + path: data.path, + content: Buffer.from(data.content, data.encoding).toString(), + sha: data.sha + } + fileCache.set(path, file) + resolve(file) + }) + }) } /** @@ -115,7 +124,8 @@ async function editFile(file: GithubFile, message: string, newContent: string): }) } -async function fetchJSONConstant(filename: string): Promise { +export async function fetchJSONConstant(filename: string): Promise { + console.log('actually fetchJSONConstant') const file = await fetchFile(filename) try { return JSON.parse(file.content) @@ -129,80 +139,94 @@ async function fetchJSONConstant(filename: string): Promise { export async function addJSONConstants(filename: string, addingValues: string[], unit: string='stat'): Promise { if (addingValues.length === 0) return // no stats provided, just return - queue.enqueue(async() => { - const file = await fetchFile(filename) - if (!file.path) - return - let oldStats: string[] - try { - oldStats = JSON.parse(file.content) - } catch { - // invalid json, set it as an empty array - oldStats = [] - } - const updatedStats = oldStats - .concat(addingValues) - // remove duplicates - .filter((value, index, array) => array.indexOf(value) === index) - .sort((a, b) => a.localeCompare(b)) - const newStats = updatedStats.filter(value => !oldStats.includes(value)) + let file: GithubFile = await fetchFile(filename) + if (!file.path) + return + let oldStats: string[] + try { + oldStats = JSON.parse(file.content) + } catch { + // invalid json, set it as an empty array + oldStats = [] + } + const updatedStats = oldStats + .concat(addingValues) + // remove duplicates + .filter((value, index, array) => array.indexOf(value) === index) + .sort((a, b) => a.localeCompare(b)) + const newStats = updatedStats.filter(value => !oldStats.includes(value)) - // there's not actually any new stats, just return - if (newStats.length === 0) return + // there's not actually any new stats, just return + if (newStats.length === 0) return - const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new ${unit}s` : `Add '${newStats[0]}' ${unit}` + const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new ${unit}s` : `Add '${newStats[0]}' ${unit}` + try { await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2)) - }) + } catch { + // the file probably changed or something, try again + file = await fetchFile(filename) + await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2)) + } } /** Fetch all the known SkyBlock stats as an array of strings */ export async function fetchStats(): Promise { - return await fetchJSONConstant('stats.json') + return await constants.fetchJSONConstant('stats.json') } /** Add stats to skyblock-constants. This has caching so it's fine to call many times */ export async function addStats(addingStats: string[]): Promise { - await addJSONConstants('stats.json', addingStats, 'stat') + await constants.addJSONConstants('stats.json', addingStats, 'stat') } /** Fetch all the known SkyBlock collections as an array of strings */ export async function fetchCollections(): Promise { - return await fetchJSONConstant('collections.json') + return await constants.fetchJSONConstant('collections.json') } /** Add collections to skyblock-constants. This has caching so it's fine to call many times */ export async function addCollections(addingCollections: string[]): Promise { - await addJSONConstants('collections.json', addingCollections, 'collection') + await constants.addJSONConstants('collections.json', addingCollections, 'collection') } /** Fetch all the known SkyBlock collections as an array of strings */ export async function fetchSkills(): Promise { - return await fetchJSONConstant('skills.json') + return await constants.fetchJSONConstant('skills.json') } /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ export async function addSkills(addingSkills: string[]): Promise { - await addJSONConstants('skills.json', addingSkills, 'skill') + await constants.addJSONConstants('skills.json', addingSkills, 'skill') } /** Fetch all the known SkyBlock collections as an array of strings */ export async function fetchZones(): Promise { - return await fetchJSONConstant('zones.json') + return await constants.fetchJSONConstant('zones.json') } /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ export async function addZones(addingZones: string[]): Promise { - await addJSONConstants('zones.json', addingZones, 'zone') + await constants.addJSONConstants('zones.json', addingZones, 'zone') } /** Fetch all the known SkyBlock slayer names as an array of strings */ export async function fetchSlayers(): Promise { - return await fetchJSONConstant('slayers.json') + return await constants.fetchJSONConstant('slayers.json') } /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ export async function addSlayers(addingSlayers: string[]): Promise { - await addJSONConstants('slayers.json', addingSlayers, 'slayer') + await constants.addJSONConstants('slayers.json', addingSlayers, 'slayer') +} + +/** Fetch all the known SkyBlock slayer names as an array of strings */ +export async function fetchMinions(): Promise { + return await constants.fetchJSONConstant('minions.json') +} + +/** Add skills to skyblock-constants. This has caching so it's fine to call many times */ +export async function addMinions(addingMinions: string[]): Promise { + await constants.addJSONConstants('minions.json', addingMinions, 'minion') } diff --git a/src/database.ts b/src/database.ts index 8aa334a..4c6dadf 100644 --- a/src/database.ts +++ b/src/database.ts @@ -442,15 +442,15 @@ async function getApplicableProfileLeaderboardAttributes(profile: CleanFullProfi /** Update the member's leaderboard data on the server if applicable */ export async function updateDatabaseMember(member: CleanMember, profile: CleanFullProfile): Promise { - if (debug) console.log('updateDatabaseMember', member.username) if (!client) return // the db client hasn't been initialized + if (debug) console.debug('updateDatabaseMember', member.username) // the member's been updated too recently, just return if (recentlyUpdated.get(profile.uuid + member.uuid)) return // store the member in recentlyUpdated so it cant update for 3 more minutes recentlyUpdated.set(profile.uuid + member.uuid, true) - if (debug) console.log('adding member to leaderboards', member.username) + if (debug) console.debug('adding member to leaderboards', member.username) await constants.addStats(Object.keys(member.rawHypixelStats)) await constants.addCollections(member.collections.map(coll => coll.name)) @@ -458,11 +458,11 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu await constants.addZones(member.visited_zones.map(zone => zone.name)) await constants.addSlayers(member.slayers.bosses.map(s => s.raw_name)) - if (debug) console.log('done constants..') + if (debug) console.debug('done constants..') const leaderboardAttributes = await getApplicableMemberLeaderboardAttributes(member) - if (debug) console.log('done getApplicableMemberLeaderboardAttributes..', leaderboardAttributes, member.username, profile.name) + if (debug) console.debug('done getApplicableMemberLeaderboardAttributes..', leaderboardAttributes, member.username, profile.name) await memberLeaderboardsCollection.updateOne( { @@ -495,7 +495,7 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu cachedRawLeaderboards.set(attributeName, newRawLeaderboard) } - if (debug) console.log('added member to leaderboards', member.username, leaderboardAttributes) + if (debug) console.debug('added member to leaderboards', member.username, leaderboardAttributes) } /** @@ -503,8 +503,8 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu * This will not also update the members, you have to call updateDatabaseMember separately for that */ export async function updateDatabaseProfile(profile: CleanFullProfile): Promise { - if (debug) console.log('updateDatabaseProfile', profile.name) if (!client) return // the db client hasn't been initialized + if (debug) console.debug('updateDatabaseProfile', profile.name) // the profile's been updated too recently, just return if (recentlyUpdated.get(profile.uuid + 'profile')) @@ -512,11 +512,11 @@ export async function updateDatabaseProfile(profile: CleanFullProfile): Promise< // store the profile in recentlyUpdated so it cant update for 3 more minutes recentlyUpdated.set(profile.uuid + 'profile', true) - if (debug) console.log('adding profile to leaderboards', profile.name) + if (debug) console.debug('adding profile to leaderboards', profile.name) const leaderboardAttributes = await getApplicableProfileLeaderboardAttributes(profile) - if (debug) console.log('done getApplicableProfileLeaderboardAttributes..', leaderboardAttributes, profile.name) + if (debug) console.debug('done getApplicableProfileLeaderboardAttributes..', leaderboardAttributes, profile.name) await profileLeaderboardsCollection.updateOne( { @@ -550,7 +550,7 @@ export async function updateDatabaseProfile(profile: CleanFullProfile): Promise< cachedRawLeaderboards.set(attributeName, newRawLeaderboard) } - if (debug) console.log('added profile to leaderboards', profile.name, leaderboardAttributes) + if (debug) console.debug('added profile to leaderboards', profile.name, leaderboardAttributes) } const leaderboardUpdateMemberQueue = new Queue({ @@ -607,7 +607,7 @@ async function fetchAllLeaderboards(fast?: boolean): Promise { const leaderboards: string[] = await fetchAllMemberLeaderboardAttributes() // shuffle so if the application is restarting many times itll still be useful - if (debug) console.log('Caching leaderboards!') + if (debug) console.debug('Caching leaderboards!') for (const leaderboard of shuffle(leaderboards)) { if (!fast) // wait 2 seconds so it doesnt use as much ram @@ -615,11 +615,11 @@ async function fetchAllLeaderboards(fast?: boolean): Promise { await fetchMemberLeaderboard(leaderboard) } - if (debug) console.log('Finished caching leaderboards!') + if (debug) console.debug('Finished caching leaderboards!') } // make sure it's not in a test -if (typeof global.it !== 'function') { +if (!globalThis.isTest) { connect().then(() => { // when it connects, cache the leaderboards and remove bad members removeBadMemberLeaderboardAttributes() diff --git a/src/hypixel.ts b/src/hypixel.ts index 599a7e1..464f3dd 100644 --- a/src/hypixel.ts +++ b/src/hypixel.ts @@ -83,7 +83,7 @@ export async function fetchUser({ user, uuid, username }: UserAny, included: Inc } if (!uuid) { // the user doesn't exist. - if (debug) console.log('error:', user, 'doesnt exist') + if (debug) console.debug('error:', user, 'doesnt exist') return null } diff --git a/src/hypixelCached.ts b/src/hypixelCached.ts index a5a5909..f7b96ef 100644 --- a/src/hypixelCached.ts +++ b/src/hypixelCached.ts @@ -105,7 +105,7 @@ export async function uuidFromUser(user: string): Promise { return uuid } - if (debug) console.log('Cache miss: uuidFromUser', user) + if (debug) console.debug('Cache miss: uuidFromUser', user) // set it as waitForCacheSet (a promise) in case uuidFromUser gets called while its fetching mojang usernameCache.set(undashUuid(user), waitForCacheSet(usernameCache, user, user)) @@ -132,11 +132,11 @@ export async function uuidFromUser(user: string): Promise { */ export async function usernameFromUser(user: string): Promise { if (usernameCache.has(undashUuid(user))) { - if (debug) console.log('Cache hit! usernameFromUser', user) + if (debug) console.debug('Cache hit! usernameFromUser', user) return usernameCache.get(undashUuid(user)) } - if (debug) console.log('Cache miss: usernameFromUser', user) + if (debug) console.debug('Cache miss: usernameFromUser', user) let { uuid, username } = await mojang.profileFromUser(user) uuid = undashUuid(uuid) @@ -191,7 +191,7 @@ export async function fetchBasicPlayer(user: string): Promise { return basicPlayerCache.get(playerUuid) const player = await fetchPlayer(playerUuid) - if (!player) console.log('no player? this should never happen', user) + if (!player) console.debug('no player? this should never happen', user) delete player.profiles return player @@ -199,11 +199,11 @@ export async function fetchBasicPlayer(user: string): Promise { export async function fetchSkyblockProfiles(playerUuid: string): Promise { if (profilesCache.has(playerUuid)) { - if (debug) console.log('Cache hit! fetchSkyblockProfiles', playerUuid) + if (debug) console.debug('Cache hit! fetchSkyblockProfiles', playerUuid) return profilesCache.get(playerUuid) } - if (debug) console.log('Cache miss: fetchSkyblockProfiles', playerUuid) + if (debug) console.debug('Cache miss: fetchSkyblockProfiles', playerUuid) const profiles: CleanProfile[] = await hypixel.fetchMemberProfilesUncached(playerUuid) @@ -240,11 +240,11 @@ async function fetchBasicProfiles(user: string): Promise { if (!playerUuid) return // invalid player, just return if (basicProfilesCache.has(playerUuid)) { - if (debug) console.log('Cache hit! fetchBasicProfiles', playerUuid) + if (debug) console.debug('Cache hit! fetchBasicProfiles', playerUuid) return basicProfilesCache.get(playerUuid) } - if (debug) console.log('Cache miss: fetchBasicProfiles', user) + if (debug) console.debug('Cache miss: fetchBasicProfiles', user) const player = await fetchPlayer(playerUuid) const profiles = player.profiles @@ -265,11 +265,11 @@ async function fetchBasicProfiles(user: string): Promise { export async function fetchProfileUuid(user: string, profile: string): Promise { // if a profile wasn't provided, return if (!profile) { - if (debug) console.log('no profile provided?', user, profile) + if (debug) console.debug('no profile provided?', user, profile) return null } - if (debug) console.log('Cache miss: fetchProfileUuid', user) + if (debug) console.debug('Cache miss: fetchProfileUuid', user) const profiles = await fetchBasicProfiles(user) if (!profiles) return // user probably doesnt exist @@ -295,11 +295,11 @@ export async function fetchProfile(user: string, profile: string): Promise { if (profileCache.has(profileUuid)) { // we have the profile cached, return it :) - if (debug) console.log('Cache hit! fetchBasicProfileFromUuid', profileUuid) + if (debug) console.debug('Cache hit! fetchBasicProfileFromUuid', profileUuid) const profile: CleanFullProfile = profileCache.get(profileUuid) return { uuid: profile.uuid, @@ -351,11 +351,11 @@ export async function fetchProfileName(user: string, profile: string): Promise { // only run the server if it's not doing tests -if (typeof global.it !== 'function') +if (!globalThis.isTest) app.listen(8080, () => console.log('App started :)')) diff --git a/test-data-generator/index.ts b/test-data-generator/index.ts index c2b1361..45a5991 100644 --- a/test-data-generator/index.ts +++ b/test-data-generator/index.ts @@ -2,11 +2,15 @@ * Automatically generate Hypixel API responses for the unit tests */ +globalThis.isTest = true + import * as hypixelApi from '../src/hypixelApi' +import * as constants from '../src/constants' import * as mojang from '../src/mojang' import fs from 'fs/promises' import path from 'path' + const playerUuids = [ '6536bfed869548fd83a1ecd24cf2a0fd', '4133cab5a7534f3f9bb636fc06a1f0fd', @@ -29,6 +33,15 @@ async function addResponse(requestPath: string, args: { [ key: string ]: string await writeTestData(requestPath, name, response) } + +async function addConstants() { + const constantNames = ['collections', 'minions', 'skills', 'slayers', 'stats', 'zones'] + for (const constantName of constantNames) { + const constantData = await constants.fetchJSONConstant(constantName + '.json') + await writeTestData('constants', constantName, constantData) + } +} + async function main() { const uuidsToUsername = {} for (const playerUuid of playerUuids) { @@ -39,6 +52,7 @@ async function main() { } await writeTestData('', 'mojang', uuidsToUsername) + await addConstants() } main() \ No newline at end of file diff --git a/test/data/constants/collections.json b/test/data/constants/collections.json new file mode 100644 index 0000000..efab904 --- /dev/null +++ b/test/data/constants/collections.json @@ -0,0 +1,62 @@ +[ + "acacia_log", + "birch_log", + "blaze_rod", + "bone", + "cactus", + "carrot", + "chicken", + "clay_ball", + "coal", + "cobblestone", + "cocoa_beans", + "cod", + "dark_oak_log", + "diamond", + "emerald", + "end_stone", + "ender_pearl", + "feather", + "ghast_tear", + "glowstone_dust", + "gold_ingot", + "gravel", + "gunpowder", + "ice", + "ink_sac", + "iron_ingot", + "jungle_log", + "lapis_lazuli", + "leather", + "lily_pad", + "magma_cream", + "melon_slice", + "mithril_ore", + "mutton", + "nether_wart", + "netherrack", + "oak_log", + "obsidian", + "porkchop", + "potato", + "prismarine_crystals", + "prismarine_shard", + "pufferfish", + "pumpkin", + "quartz", + "rabbit", + "red_mushroom", + "redstone", + "rotten_flesh", + "salmon", + "sand", + "slime_ball", + "spider_eye", + "sponge", + "spruce_log", + "string", + "sugar_cane", + "tropical_fish", + "wheat", + "wheat_seeds" +] \ No newline at end of file diff --git a/test/data/constants/minions.json b/test/data/constants/minions.json new file mode 100644 index 0000000..5291a34 --- /dev/null +++ b/test/data/constants/minions.json @@ -0,0 +1,55 @@ +[ + "acacia", + "birch", + "blaze", + "cactus", + "carrot", + "cavespider", + "chicken", + "clay", + "coal", + "cobblestone", + "cocoa", + "cow", + "creeper", + "dark_oak", + "diamond", + "emerald", + "ender_stone", + "enderman", + "fishing", + "flower", + "ghast", + "glowstone", + "gold", + "gravel", + "ice", + "iron", + "jungle", + "lapis", + "magma_cube", + "melon", + "mithril", + "mushroom", + "nether_warts", + "oak", + "obsidian", + "pig", + "potato", + "pumpkin", + "quartz", + "rabbit", + "redstone", + "revenant", + "sand", + "sheep", + "skeleton", + "slime", + "snow", + "spider", + "spruce", + "sugar_cane", + "tarantula", + "wheat", + "zombie" +] \ No newline at end of file diff --git a/test/data/constants/skills.json b/test/data/constants/skills.json new file mode 100644 index 0000000..ab04362 --- /dev/null +++ b/test/data/constants/skills.json @@ -0,0 +1,12 @@ +[ + "alchemy", + "carpentry", + "combat", + "enchanting", + "farming", + "fishing", + "foraging", + "mining", + "runecrafting", + "taming" +] \ No newline at end of file diff --git a/test/data/constants/slayers.json b/test/data/constants/slayers.json new file mode 100644 index 0000000..f9bc036 --- /dev/null +++ b/test/data/constants/slayers.json @@ -0,0 +1,5 @@ +[ + "spider", + "wolf", + "zombie" +] \ No newline at end of file diff --git a/test/data/constants/stats.json b/test/data/constants/stats.json new file mode 100644 index 0000000..89685bd --- /dev/null +++ b/test/data/constants/stats.json @@ -0,0 +1,773 @@ +[ + "auctions_bids", + "auctions_bought_common", + "auctions_bought_epic", + "auctions_bought_legendary", + "auctions_bought_mythic", + "auctions_bought_rare", + "auctions_bought_special", + "auctions_bought_uncommon", + "auctions_completed", + "auctions_created", + "auctions_fees", + "auctions_gold_earned", + "auctions_gold_spent", + "auctions_highest_bid", + "auctions_no_bids", + "auctions_sold_common", + "auctions_sold_epic", + "auctions_sold_legendary", + "auctions_sold_mythic", + "auctions_sold_rare", + "auctions_sold_special", + "auctions_sold_uncommon", + "auctions_won", + "chicken_race_best_time", + "chicken_race_best_time_2", + "deaths", + "deaths_alpha_wolf", + "deaths_ancient_one", + "deaths_arachne", + "deaths_arachne_brood", + "deaths_arachne_keeper", + "deaths_armor_stand", + "deaths_blaze", + "deaths_blue_shark", + "deaths_bonzo", + "deaths_bonzo_summon_undead", + "deaths_brood_mother_cave_spider", + "deaths_brood_mother_spider", + "deaths_cactus", + "deaths_carrot_king", + "deaths_catfish", + "deaths_cave_spider", + "deaths_caverns_ghost", + "deaths_cellar_spider", + "deaths_chicken", + "deaths_chicken_deep", + "deaths_corrupted_protector", + "deaths_creeper", + "deaths_crushed", + "deaths_crypt_dreadlord", + "deaths_crypt_lurker", + "deaths_crypt_souleater", + "deaths_crypt_tank_zombie", + "deaths_crypt_undead", + "deaths_crypt_undead_agentk", + "deaths_crypt_undead_alexander", + "deaths_crypt_undead_apunch", + "deaths_crypt_undead_bernhard", + "deaths_crypt_undead_cecer", + "deaths_crypt_undead_chilynn", + "deaths_crypt_undead_christian", + "deaths_crypt_undead_codename_b", + "deaths_crypt_undead_connorlinfoot", + "deaths_crypt_undead_dctr", + "deaths_crypt_undead_erosemberg", + "deaths_crypt_undead_externalizable", + "deaths_crypt_undead_flameboy101", + "deaths_crypt_undead_friedrich", + "deaths_crypt_undead_hypixel", + "deaths_crypt_undead_jamiethegeek", + "deaths_crypt_undead_jayavarmen", + "deaths_crypt_undead_judg3", + "deaths_crypt_undead_likaos", + "deaths_crypt_undead_luckykessie", + "deaths_crypt_undead_marius", + "deaths_crypt_undead_minikloon", + "deaths_crypt_undead_nicholas", + "deaths_crypt_undead_nitroholic_", + "deaths_crypt_undead_orangemarshall", + "deaths_crypt_undead_pieter", + "deaths_crypt_undead_pjoke1", + "deaths_crypt_undead_plancke", + "deaths_crypt_undead_relenter", + "deaths_crypt_undead_revengeee", + "deaths_crypt_undead_rezzus", + "deaths_crypt_undead_sfarnham", + "deaths_crypt_undead_sylent", + "deaths_crypt_undead_thorlon", + "deaths_crypt_undead_valentin", + "deaths_crypt_undead_williamtiger", + "deaths_crypt_witherskeleton", + "deaths_crystal_sentry", + "deaths_dante_goon", + "deaths_dante_slime", + "deaths_dante_slime_goon", + "deaths_dasher_spider", + "deaths_deathmite", + "deaths_deep_sea_protector", + "deaths_diamond_guy", + "deaths_diamond_skeleton", + "deaths_diamond_zombie", + "deaths_drowning", + "deaths_dungeon_respawning_skeleton", + "deaths_dungeon_respawning_skeleton_skull", + "deaths_emerald_slime", + "deaths_ender_dragon", + "deaths_enderman", + "deaths_endermite", + "deaths_entity", + "deaths_fall", + "deaths_fall_kill", + "deaths_fire", + "deaths_fireball_magma_cube", + "deaths_frosty_the_snowman", + "deaths_frozen_steve", + "deaths_gaia_construct", + "deaths_generator_ghast", + "deaths_generator_magma_cube", + "deaths_generator_slime", + "deaths_ghast", + "deaths_goblin_battler", + "deaths_goblin_creeper", + "deaths_goblin_golem", + "deaths_goblin_knife_thrower", + "deaths_goblin_murderlover", + "deaths_goblin_weakling_bow", + "deaths_goblin_weakling_melee", + "deaths_great_white_shark", + "deaths_grim_reaper", + "deaths_guardian", + "deaths_guardian_defender", + "deaths_guardian_emperor", + "deaths_headless_horseman", + "deaths_horseman_bat", + "deaths_horseman_horse", + "deaths_horseman_zombie", + "deaths_howling_spirit", + "deaths_ice_walker", + "deaths_invisible_creeper", + "deaths_iron_golem", + "deaths_jockey_shot_silverfish", + "deaths_jockey_skeleton", + "deaths_kill", + "deaths_kill_fire", + "deaths_king_midas", + "deaths_lapis_zombie", + "deaths_liquid_hot_magma", + "deaths_livid", + "deaths_livid_clone", + "deaths_lonely_spider", + "deaths_lost_adventurer", + "deaths_magma_cube", + "deaths_magma_cube_boss", + "deaths_master_bonzo", + "deaths_master_bonzo_summon_undead", + "deaths_master_cellar_spider", + "deaths_master_crypt_dreadlord", + "deaths_master_crypt_lurker", + "deaths_master_crypt_souleater", + "deaths_master_crypt_tank_zombie", + "deaths_master_crypt_undead", + "deaths_master_crypt_undead__fudgiethewhale", + "deaths_master_crypt_undead_adamwho", + "deaths_master_crypt_undead_agentk", + "deaths_master_crypt_undead_apunch", + "deaths_master_crypt_undead_codename_b", + "deaths_master_crypt_undead_connorlinfoot", + "deaths_master_crypt_undead_dctr", + "deaths_master_crypt_undead_donpireso", + "deaths_master_crypt_undead_hypixel", + "deaths_master_crypt_undead_jamiethegeek", + "deaths_master_crypt_undead_jayavarmen", + "deaths_master_crypt_undead_judg3", + "deaths_master_crypt_undead_likaos", + "deaths_master_crypt_undead_minikloon", + "deaths_master_crypt_undead_nicholas", + "deaths_master_crypt_undead_nitroholic_", + "deaths_master_crypt_undead_orangemarshall", + "deaths_master_crypt_undead_plummel", + "deaths_master_crypt_undead_relenter", + "deaths_master_crypt_undead_rezzus", + "deaths_master_crypt_undead_sfarnham", + "deaths_master_crypt_undead_themgrf", + "deaths_master_crypt_undead_thorlon", + "deaths_master_crypt_undead_vinny8ball666", + "deaths_master_crypt_witherskeleton", + "deaths_master_deathmite", + "deaths_master_diamond_guy", + "deaths_master_dungeon_respawning_skeleton", + "deaths_master_dungeon_respawning_skeleton_skull", + "deaths_master_king_midas", + "deaths_master_livid", + "deaths_master_livid_clone", + "deaths_master_lonely_spider", + "deaths_master_lost_adventurer", + "deaths_master_mimic", + "deaths_master_parasite", + "deaths_master_professor", + "deaths_master_professor_archer_guardian", + "deaths_master_professor_guardian_summon", + "deaths_master_professor_mage_guardian", + "deaths_master_professor_priest_guardian", + "deaths_master_professor_warrior_guardian", + "deaths_master_sadan", + "deaths_master_sadan_giant", + "deaths_master_sadan_golem", + "deaths_master_sadan_statue", + "deaths_master_scared_skeleton", + "deaths_master_scarf", + "deaths_master_scarf_archer", + "deaths_master_scarf_mage", + "deaths_master_scarf_warrior", + "deaths_master_shadow_assassin", + "deaths_master_skeleton_grunt", + "deaths_master_skeleton_master", + "deaths_master_skeleton_soldier", + "deaths_master_skeletor", + "deaths_master_skeletor_prime", + "deaths_master_sniper_skeleton", + "deaths_master_spirit_bat", + "deaths_master_spirit_chicken", + "deaths_master_spirit_miniboss", + "deaths_master_spirit_rabbit", + "deaths_master_spirit_sheep", + "deaths_master_spirit_wolf", + "deaths_master_super_archer", + "deaths_master_super_tank_zombie", + "deaths_master_tentaclees", + "deaths_master_watcher_bonzo", + "deaths_master_watcher_guardian", + "deaths_master_watcher_livid", + "deaths_master_watcher_scarf", + "deaths_master_watcher_summon_undead", + "deaths_master_zombie_commander", + "deaths_master_zombie_grunt", + "deaths_master_zombie_knight", + "deaths_master_zombie_soldier", + "deaths_maxor", + "deaths_mimic", + "deaths_minos_champion", + "deaths_minos_hunter", + "deaths_minos_inquisitor", + "deaths_minotaur", + "deaths_necron_guard", + "deaths_night_respawning_skeleton", + "deaths_nightmare", + "deaths_nurse_shark", + "deaths_obsidian_wither", + "deaths_ocelot", + "deaths_old_dragon", + "deaths_old_wolf", + "deaths_pack_spirit", + "deaths_pack_wolf", + "deaths_parasite", + "deaths_phantom_fisherman", + "deaths_phantom_spirit", + "deaths_pig_zombie", + "deaths_pigman", + "deaths_player", + "deaths_professor", + "deaths_professor_archer_guardian", + "deaths_professor_guardian_summon", + "deaths_professor_mage_guardian", + "deaths_professor_priest_guardian", + "deaths_professor_warrior_guardian", + "deaths_protector_dragon", + "deaths_random_slime", + "deaths_redstone_pigman", + "deaths_respawning_skeleton", + "deaths_revenant_zombie", + "deaths_ruin_wolf", + "deaths_runic_zombie", + "deaths_sadan", + "deaths_sadan_giant", + "deaths_sadan_golem", + "deaths_sadan_statue", + "deaths_scarecrow", + "deaths_scared_skeleton", + "deaths_scarf", + "deaths_scarf_archer", + "deaths_scarf_mage", + "deaths_scarf_warrior", + "deaths_scary_jerry", + "deaths_sea_archer", + "deaths_sea_guardian", + "deaths_sea_leech", + "deaths_sea_walker", + "deaths_sea_witch", + "deaths_shadow_assassin", + "deaths_sheep", + "deaths_siamese_lynx", + "deaths_silverfish", + "deaths_skeleton", + "deaths_skeleton_emperor", + "deaths_skeleton_grunt", + "deaths_skeleton_lord", + "deaths_skeleton_master", + "deaths_skeleton_soldier", + "deaths_skeletor", + "deaths_skeletor_prime", + "deaths_slime", + "deaths_sniper_skeleton", + "deaths_snowman", + "deaths_soul_of_the_alpha", + "deaths_spider", + "deaths_spider_jockey", + "deaths_spirit_bat", + "deaths_spirit_bull", + "deaths_spirit_chicken", + "deaths_spirit_miniboss", + "deaths_spirit_rabbit", + "deaths_spirit_sheep", + "deaths_spirit_wolf", + "deaths_splitter_spider", + "deaths_splitter_spider_silverfish", + "deaths_strong_dragon", + "deaths_suffocation", + "deaths_super_archer", + "deaths_super_tank_zombie", + "deaths_superior_dragon", + "deaths_tarantula_spider", + "deaths_tentaclees", + "deaths_the_watcher", + "deaths_tiger_shark", + "deaths_trap", + "deaths_treasure_hoarder", + "deaths_trick_or_treater", + "deaths_unburried_zombie", + "deaths_unknown", + "deaths_unstable_dragon", + "deaths_void", +