aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormat <github@matdoes.dev>2022-04-30 00:20:49 -0500
committermat <github@matdoes.dev>2022-04-30 00:20:49 -0500
commit2240140136b9dfc0faeb1d29edf037609f9e8c9c (patch)
tree93419b5b0db5961ad1bf75840dffac3461a8f0fe
parent41bfebfba8eb0c2f830e01f03f8765865e8108b0 (diff)
downloadskyblock-api-2240140136b9dfc0faeb1d29edf037609f9e8c9c.tar.gz
skyblock-api-2240140136b9dfc0faeb1d29edf037609f9e8c9c.tar.bz2
skyblock-api-2240140136b9dfc0faeb1d29edf037609f9e8c9c.zip
revamp skills api
-rw-r--r--README.md2
-rw-r--r--src/cleaners/achievements.ts18
-rw-r--r--src/cleaners/player.ts12
-rw-r--r--src/cleaners/skyblock/member.ts14
-rw-r--r--src/cleaners/skyblock/skills.ts66
-rw-r--r--src/database.ts5
-rw-r--r--src/hypixel.ts4
-rw-r--r--src/hypixelCached.ts29
8 files changed, 121 insertions, 29 deletions
diff --git a/README.md b/README.md
index 59937b2..b46169a 100644
--- a/README.md
+++ b/README.md
@@ -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 []