diff options
author | mat <github@matdoes.dev> | 2022-04-30 00:20:49 -0500 |
---|---|---|
committer | mat <github@matdoes.dev> | 2022-04-30 00:20:49 -0500 |
commit | 2240140136b9dfc0faeb1d29edf037609f9e8c9c (patch) | |
tree | 93419b5b0db5961ad1bf75840dffac3461a8f0fe | |
parent | 41bfebfba8eb0c2f830e01f03f8765865e8108b0 (diff) | |
download | skyblock-api-2240140136b9dfc0faeb1d29edf037609f9e8c9c.tar.gz skyblock-api-2240140136b9dfc0faeb1d29edf037609f9e8c9c.tar.bz2 skyblock-api-2240140136b9dfc0faeb1d29edf037609f9e8c9c.zip |
revamp skills api
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | src/cleaners/achievements.ts | 18 | ||||
-rw-r--r-- | src/cleaners/player.ts | 12 | ||||
-rw-r--r-- | src/cleaners/skyblock/member.ts | 14 | ||||
-rw-r--r-- | src/cleaners/skyblock/skills.ts | 66 | ||||
-rw-r--r-- | src/database.ts | 5 | ||||
-rw-r--r-- | src/hypixel.ts | 4 | ||||
-rw-r--r-- | src/hypixelCached.ts | 29 |
8 files changed, 121 insertions, 29 deletions
@@ -21,7 +21,7 @@ If you (this is really just here for myself so I don't forget) are adding a new - Use camelCase for keys. - Use snake_case for values. - Prefer arrays over dictionaries when the keys aren't static. For example `[ { name: "asdf", value: "dsfasg" } ]` rather than `{ "asdf": "dsfasg" }`. -- Dates are done with milliseconds since epoch +- Dates are milliseconds since epoch. - Fields that contain a snake_case ID should be called `id`. At the moment some of them are called `name`, this will be changed soon. ## Development diff --git a/src/cleaners/achievements.ts b/src/cleaners/achievements.ts new file mode 100644 index 0000000..9e4c7ae --- /dev/null +++ b/src/cleaners/achievements.ts @@ -0,0 +1,18 @@ +import typedHypixelApi from 'typed-hypixel-api' + +export interface Achievements { + skyblock: Record<string, number> +} + +export function cleanPlayerAchievements(data: typedHypixelApi.PlayerDataResponse['player']): Achievements { + const achievements: Achievements = { + skyblock: {} + } + + for (const [id, value] of Object.entries(data.achievements)) { + if (id.startsWith('skyblock_')) + achievements.skyblock[id.substring(9)] = value + } + + return achievements +} diff --git a/src/cleaners/player.ts b/src/cleaners/player.ts index 2d819e9..8facdad 100644 --- a/src/cleaners/player.ts +++ b/src/cleaners/player.ts @@ -1,6 +1,7 @@ import { cleanSocialMedia, CleanSocialMedia } from './socialmedia.js' import { cleanPlayerSkyblockProfiles } from './skyblock/profiles.js' import { cleanPlayerSkyblockClaimed } from './skyblock/claimed.js' +import { cleanPlayerAchievements, Achievements } from './achievements.js' import { CleanBasicProfile } from './skyblock/profile.js' import { cleanRank, CleanRank } from './rank.js' import typedHypixelApi from 'typed-hypixel-api' @@ -28,16 +29,21 @@ export interface CleanPlayer extends CleanBasicPlayer { claimed?: ClaimedSkyBlockItem[] } -export async function cleanPlayerResponse(data: typedHypixelApi.PlayerDataResponse['player']): Promise<CleanPlayer | null> { +export interface CleanFullPlayer extends CleanPlayer { + achievements: Achievements +} + +export async function cleanPlayerResponse(data: typedHypixelApi.PlayerDataResponse['player']): Promise<CleanFullPlayer | null> { // Cleans up a 'player' api response if (!data) - return null // bruh + return null return { uuid: undashUuid(data.uuid), username: data.displayname, rank: cleanRank(data), socials: cleanSocialMedia(data), profiles: cleanPlayerSkyblockProfiles(data.stats?.SkyBlock?.profiles), - claimed: cleanPlayerSkyblockClaimed(data) + claimed: cleanPlayerSkyblockClaimed(data), + achievements: cleanPlayerAchievements(data) } } diff --git a/src/cleaners/skyblock/member.ts b/src/cleaners/skyblock/member.ts index 57462ed..6b2f452 100644 --- a/src/cleaners/skyblock/member.ts +++ b/src/cleaners/skyblock/member.ts @@ -10,7 +10,7 @@ import { CleanMinion, cleanMinions } from './minions.js' import { cleanSlayers, SlayerData } from './slayers.js' import { AccountCustomization } from '../../database.js' import { cleanVisitedZones, Zone } from './zones.js' -import { cleanSkills, Skill } from './skills.js' +import { cleanSkills, Skills } from './skills.js' import * as cached from '../../hypixelCached.js' import typedHypixelApi from 'typed-hypixel-api' import { cleanPets, PetsData } from './pets.js' @@ -37,7 +37,7 @@ export interface CleanMember extends CleanBasicMember { fairySouls: FairySouls inventories?: Inventories objectives: Objective[] - skills: Skill[] + skills: Skills zones: Zone[] collections: Collection[] slayers: SlayerData @@ -50,7 +50,7 @@ export interface CleanMember extends CleanBasicMember { } export async function cleanSkyBlockProfileMemberResponseBasic(member: typedHypixelApi.SkyBlockProfileMember & { uuid: string }): Promise<CleanBasicMember | null> { - const player = await cached.fetchPlayer(member.uuid) + const player = await cached.fetchPlayer(member.uuid, false) if (!player) return null return { uuid: member.uuid, @@ -64,7 +64,7 @@ export async function cleanSkyBlockProfileMemberResponseBasic(member: typedHypix /** Cleans up a member (from skyblock/profile) */ export async function cleanSkyBlockProfileMemberResponse(member: typedHypixelApi.SkyBlockProfileMember & { uuid: string }, profileId?: string, included: Included[] | undefined = undefined): Promise<CleanMember | null> { const inventoriesIncluded = included === undefined || included.includes('inventories') - const player = await cached.fetchPlayer(member.uuid) + const player = await cached.fetchPlayer(member.uuid, true) if (!player) return null const fairySouls = await cleanFairySouls(member) @@ -74,7 +74,7 @@ export async function cleanSkyBlockProfileMemberResponse(member: typedHypixelApi const coopInvitationPromise = cleanCoopInvitation(member, member.uuid) const minionsPromise = cleanMinions(member) - const skillsPromise = cleanSkills(member) + const skillsPromise = cleanSkills(member, player) const zonesPromise = cleanVisitedZones(member) const petsPromise = cleanPets(member) const harpPromise = cleanHarp(member) @@ -100,7 +100,9 @@ export async function cleanSkyBlockProfileMemberResponse(member: typedHypixelApi fairySouls: fairySouls, inventories: inventoriesPromise ? await inventoriesPromise : undefined, objectives: cleanObjectives(member), + skills: await skillsPromise, + zones: await zonesPromise, collections: cleanCollections(member), slayers: cleanSlayers(member), @@ -126,7 +128,7 @@ export interface CleanMemberProfilePlayer extends CleanPlayer { fairySouls: FairySouls inventories?: Inventories objectives: Objective[] - skills: Skill[] + skills: Skills zones: Zone[] collections: Collection[] slayers: SlayerData diff --git a/src/cleaners/skyblock/skills.ts b/src/cleaners/skyblock/skills.ts index d035a96..8d0f16d 100644 --- a/src/cleaners/skyblock/skills.ts +++ b/src/cleaners/skyblock/skills.ts @@ -2,9 +2,10 @@ import typedHypixelApi from 'typed-hypixel-api' import { fetchSkills } from '../../constants.js' import { levelFromXpTable } from '../../util.js' import * as constants from '../../constants.js' +import { CleanFullPlayer } from '../player.js' export interface Skill { - name: string + id: string xp: number level: number @@ -14,6 +15,16 @@ export interface Skill { levelXpRequired: number } +export interface Skills { + list: Skill[] + /** + * Whether the player has their skills API enabled. If this is off, that + * means the data doesn't include xp and is per-player. You should show a + * warning to the user. + */ + apiEnabled: boolean +} + // the highest level you can have in each skill // numbers taken from https://hypixel-skyblock.fandom.com/wiki/Skills const skillsMaxLevel: { [key: string]: number } = { @@ -137,7 +148,39 @@ export function levelForSkillXp(xp: number, maxLevel: number) { return levelFromXpTable(xp, xpTable) } -export async function cleanSkills(data: typedHypixelApi.SkyBlockProfileMember): Promise<Skill[]> { +function skillFromLevel(id: string, level: number): Skill { + const maxLevel = skillsMaxLevel[id] ?? skillsDefaultMaxLevel + const xpTable = (maxLevel <= 25 ? skillXpTableEasier : skillXpTable).slice(0, maxLevel) + const xp = level > 0 ? xpTable[level - 1] ?? 0 : 0 + + return { + id, + level, + levelXp: 0, + levelXpRequired: xpTable[level], + maxLevel: maxLevel, + xp + } +} + +function skillsFromSkyBlockAchievements(achievements: CleanFullPlayer['achievements']['skyblock']): Skills { + return { + apiEnabled: false, + list: [ + skillFromLevel('fishing', achievements['angler']), + skillFromLevel('enchanting', achievements['augmentation']), + skillFromLevel('combat', achievements['combat']), + skillFromLevel('alchemy', achievements['concoctor']), + skillFromLevel('taming', achievements['domesticator']), + skillFromLevel('dungeoneering', achievements['dungeoneer']), + skillFromLevel('mining', achievements['excavator']), + skillFromLevel('foraging', achievements['gatherer']), + skillFromLevel('farming', achievements['harvester']) + ] + } +} + +export async function cleanSkills(data: typedHypixelApi.SkyBlockProfileMember, player: CleanFullPlayer): Promise<Skills> { const allSkillNames = await fetchSkills() const skills: Skill[] = [] @@ -168,7 +211,7 @@ export async function cleanSkills(data: typedHypixelApi.SkyBlockProfileMember): const skillLevelXpRequired = xpTable[skillLevel] - previousLevelXp skills.push({ - name: skillName, + id: skillName, xp: skillXp, level: skillLevel, maxLevel: skillMaxLevel, @@ -178,16 +221,22 @@ export async function cleanSkills(data: typedHypixelApi.SkyBlockProfileMember): } } + // if the player has no skills but has kills, we can assume they have the skills api off + // (we check kills to know whether the profile is actually used, this is kinda arbitrary) + if (skills.length === 0 && Object.keys(data.stats).includes('kills')) { + return skillsFromSkyBlockAchievements(player.achievements.skyblock) + } + constants.addSkills(skillNamesFound) // add missing skills - const missingSkillNames = allSkillNames.filter(skillName => !skills.some(skill => skill.name === skillName)) + const missingSkillNames = allSkillNames.filter(skillName => !skills.some(skill => skill.id === skillName)) for (const skillName of missingSkillNames) { const skillMaxLevel = skillsMaxLevel[skillName] ?? skillsDefaultMaxLevel const xpTable = (skillMaxLevel <= 25 ? skillXpTableEasier : skillXpTable).slice(0, skillMaxLevel) skills.push({ - name: skillName, + id: skillName, xp: 0, level: 0, maxLevel: skillMaxLevel, @@ -197,7 +246,10 @@ export async function cleanSkills(data: typedHypixelApi.SkyBlockProfileMember): } // sort skills by name - skills.sort((a, b) => a.name.localeCompare(b.name)) + skills.sort((a, b) => a.id.localeCompare(b.id)) - return skills + return { + apiEnabled: true, + list: skills, + } } diff --git a/src/database.ts b/src/database.ts index 8b606d4..04b3825 100644 --- a/src/database.ts +++ b/src/database.ts @@ -159,9 +159,10 @@ function getMemberCollectionAttributes(member: CleanMember): StringNumber { } function getMemberSkillAttributes(member: CleanMember): StringNumber { + if (!member.skills.apiEnabled) return {} const skillAttributes = {} - for (const collection of member.skills) { - const skillLeaderboardName = `skill_${collection.name}` + for (const collection of member.skills.list) { + const skillLeaderboardName = `skill_${collection.id}` skillAttributes[skillLeaderboardName] = collection.xp } return skillAttributes diff --git a/src/hypixel.ts b/src/hypixel.ts index fb0edb3..7bc5ed3 100644 --- a/src/hypixel.ts +++ b/src/hypixel.ts @@ -123,7 +123,7 @@ export async function fetchUser({ user, uuid, username }: UserAny, included: Inc let playerData: CleanPlayer | null = null if (includePlayers) { - playerData = await cached.fetchPlayer(uuid) + playerData = await cached.fetchPlayer(uuid, true) // if not including profiles, include lightweight profiles just in case if (!includeProfiles) basicProfilesData = playerData?.profiles @@ -182,7 +182,7 @@ export async function fetchMemberProfile(user: string, profile: string, customiz if (!profileUuid) return null if (!playerUuid) return null - const player: CleanPlayer | null = await cached.fetchPlayer(playerUuid) + const player: CleanPlayer | null = await cached.fetchPlayer(playerUuid, true) if (!player) return null // this should never happen, but if it does just return null diff --git a/src/hypixelCached.ts b/src/hypixelCached.ts index d92ba75..164cf63 100644 --- a/src/hypixelCached.ts +++ b/src/hypixelCached.ts @@ -4,7 +4,7 @@ import { CleanProfile, CleanFullProfile, CleanBasicProfile } from './cleaners/skyblock/profile.js' import { isUuid, sleep, undashUuid } from './util.js' -import { CleanPlayer } from './cleaners/player.js' +import { CleanFullPlayer, CleanPlayer } from './cleaners/player.js' import * as hypixel from './hypixel.js' import * as mojang from './mojang.js' import NodeCache from 'node-cache' @@ -157,19 +157,32 @@ export async function usernameFromUser(user: string): Promise<string | null> { let fetchingPlayers: Set<string> = new Set() -export async function fetchPlayer(user: string): Promise<CleanPlayer | null> { +function cleanFullPlayerToCleanPlayer(player: CleanFullPlayer): CleanPlayer { + return { + rank: player.rank, + socials: player.socials, + username: player.username, + uuid: player.uuid, + claimed: player.claimed, + profiles: player.profiles + } +} + +export async function fetchPlayer<Full extends boolean = true>(user: string, full: Full): Promise<(Full extends true ? CleanFullPlayer : CleanPlayer) | null> { const playerUuid = await uuidFromUser(user) if (!playerUuid) return null - if (playerCache.has(playerUuid)) - return playerCache.get(playerUuid)! + if (playerCache.has(playerUuid)) { + const player: CleanFullPlayer = playerCache.get(playerUuid)! + return full ? player : cleanFullPlayerToCleanPlayer(player) as any + } // if it's already in the process of fetching, check every 100ms until it's not fetching the player anymore and fetch it again, since it'll be cached now if (fetchingPlayers.has(playerUuid)) { while (fetchingPlayers.has(playerUuid)) { await sleep(100) } - return await fetchPlayer(user) + return await fetchPlayer(user, full) } fetchingPlayers.add(playerUuid) @@ -194,7 +207,7 @@ export async function fetchPlayer(user: string): Promise<CleanPlayer | null> { } basicPlayerCache.set(playerUuid, cleanBasicPlayer) - return cleanPlayer + return full ? cleanPlayer : cleanFullPlayerToCleanPlayer(cleanPlayer) as any } /** Fetch a player without their profiles. This is heavily cached. */ @@ -211,7 +224,7 @@ export async function fetchBasicPlayer(user: string, includeClaimed: boolean = t return player } - const player = await fetchPlayer(playerUuid) + const player = await fetchPlayer(playerUuid, false) if (!player) { console.debug('no player? this should never happen, perhaps the uuid is invalid or the player hasn\'t played hypixel', playerUuid) return null @@ -283,7 +296,7 @@ async function fetchBasicProfiles(user: string): Promise<CleanBasicProfile[] | n if (debug) console.debug('Cache miss: fetchBasicProfiles', user) - const player = await fetchPlayer(playerUuid) + const player = await fetchPlayer(playerUuid, false) if (!player) { // this happens when the player changed their name recently and the old name is cached on hypixel return [] |