diff --git a/pages/api/beatleader/stars.js b/pages/api/beatleader/stars.js index 56ace1a..54c7b58 100644 --- a/pages/api/beatleader/stars.js +++ b/pages/api/beatleader/stars.js @@ -30,9 +30,7 @@ export default async function handler(req, res) { const before = Date.now(); const data = await fetch( - WebsiteTypes.BeatLeader.ApiUrl.MapData.replace("%h", mapHash) - .replace("%d", difficulty) - .replace("%m", characteristic), + WebsiteTypes.BeatLeader.ApiUrl.MapData.replace("%h", mapHash), { headers: { "X-Requested-With": "BeatSaber Overlay", diff --git a/pages/api/scoresaber/stars.js b/pages/api/scoresaber/stars.js new file mode 100644 index 0000000..1b4bbee --- /dev/null +++ b/pages/api/scoresaber/stars.js @@ -0,0 +1,70 @@ +import fetch from "node-fetch"; +import WebsiteTypes from "../../../src/consts/LeaderboardType"; +import RedisUtils from "../../../src/utils/redisUtils"; +import { diffToScoreSaberDiff } from "../../../src/utils/scoreSaberUtils"; + +const KEY = "SS_MAP_STAR_"; + +/** + * + * @param {Request} req + * @param {Response} res + * @returns + */ +export default async function handler(req, res) { + const mapHash = req.query.hash.replace("custom_level_", "").toLowerCase(); + const difficulty = req.query.difficulty.replace(" ", ""); + const characteristic = req.query.characteristic; + + const key = `${KEY}${difficulty}-${characteristic}-${mapHash}`; + const exists = await RedisUtils.exists(key); + if (exists) { + const data = await RedisUtils.getValue(key); + res.setHeader("Cache-Status", "hit"); + + return res.status(200).json({ + status: "OK", + stars: Number.parseFloat(data), + difficulty: difficulty, + }); + } + + const before = Date.now(); + const data = await fetch( + WebsiteTypes.ScoreSaber.ApiUrl.MapData.replace("%h", mapHash).replace( + "%d", + diffToScoreSaberDiff(difficulty) + ), + { + headers: { + "X-Requested-With": "BeatSaber Overlay", + }, + } + ); + if (data.status === 404) { + return res.status(404).json({ + status: 404, + message: "Unknown Map Hash", + }); + } + const json = await data.json(); + console.log(json); + let starCount = json.stars; + if (starCount === undefined) { + return res.status(404).json({ + status: 404, + message: "Unknown Map Hash", + }); + } + await RedisUtils.setValue(key, starCount); + console.log( + `[Cache]: Cached SS Star Count for hash ${mapHash} in ${ + Date.now() - before + }ms` + ); + res.setHeader("Cache-Status", "miss"); + return res.status(200).json({ + status: "OK", + stars: starCount, + }); +} diff --git a/pages/overlay.js b/pages/overlay.js index f41f5e6..62ce21a 100644 --- a/pages/overlay.js +++ b/pages/overlay.js @@ -266,16 +266,24 @@ export default class Overlay extends Component { const json = await data.json(); this.setState({ beatSaverData: json }); - if (this.state.websiteType == "BeatLeader") { - const { characteristic, levelId, difficulty } = songData; - let mapHash = levelId.replace("custom_level_", ""); - const mapStars = await LeaderboardType.BeatLeader.getMapStarCount( + const { characteristic, levelId, difficulty } = songData; + let mapHash = levelId.replace("custom_level_", ""); + let mapStars = undefined; + if (this.state.websiteType === "BeatLeader") { + mapStars = await LeaderboardType.BeatLeader.getMapStarCount( mapHash, difficulty.replace("+", "Plus"), characteristic ); - this.setState({ mapStarCount: mapStars }); } + if (this.state.websiteType === "ScoreSaber") { + mapStars = await LeaderboardType.ScoreSaber.getMapStarCount( + mapHash, + difficulty.replace("+", "Plus"), + characteristic + ); + } + this.setState({ mapStarCount: mapStars }); } /** diff --git a/src/consts/LeaderboardType.js b/src/consts/LeaderboardType.js index d852248..3967fe4 100644 --- a/src/consts/LeaderboardType.js +++ b/src/consts/LeaderboardType.js @@ -4,6 +4,15 @@ const WebsiteTypes = { PlayerData: process.env.NEXT_PUBLIC_HTTP_PROXY + "/https://scoresaber.com/api/player/%s/basic", + MapData: + "https://scoresaber.com/api/leaderboard/by-hash/%h/info?difficulty=%d", + }, + async getMapStarCount(mapHash, mapDiff, characteristic) { + const data = await fetch( + `/api/scoresaber/stars?hash=${mapHash}&difficulty=${mapDiff}&characteristic=${characteristic}` + ); + const json = await data.json(); + return json.stars || undefined; }, }, BeatLeader: { @@ -20,20 +29,6 @@ const WebsiteTypes = { const json = await data.json(); return json.stars || undefined; }, - curve(acc, stars) { - var l = 1 - (0.03 * (stars - 3.0)) / 11.0; - var a = 0.96 * l; - var f = 1.2 - (0.6 * stars) / 14.0; - - return Math.pow(Math.log10(l / (l - acc)) / Math.log10(l / (l - a)), f); - }, - ppFromAcc(acc, stars) { - if (stars === undefined || acc === undefined) { - return undefined; - } - const pp = this.curve(acc / 100, stars - 0.5) * (stars + 0.5) * 42; - return Number.isNaN(pp) ? undefined : pp; - }, }, }; diff --git a/src/curve/BeatLeaderCurve.js b/src/curve/BeatLeaderCurve.js new file mode 100644 index 0000000..2c6fc1b --- /dev/null +++ b/src/curve/BeatLeaderCurve.js @@ -0,0 +1,18 @@ +export default class BeatLeaderCurve { + static curve(acc, stars) { + var l = 1 - (0.03 * (stars - 3.0)) / 11.0; + var a = 0.96 * l; + var f = 1.2 - (0.6 * stars) / 14.0; + + return Math.pow(Math.log10(l / (l - acc)) / Math.log10(l / (l - a)), f); + } + + static getPP(acc, stars) { + if (stars === undefined || acc === undefined) { + return undefined; + } + const pp = + BeatLeaderCurve.curve(acc / 100, stars - 0.5) * (stars + 0.5) * 42; + return Number.isNaN(pp) ? undefined : pp; + } +} diff --git a/src/curve/ScoreSaberCurve.js b/src/curve/ScoreSaberCurve.js new file mode 100644 index 0000000..0c91d9d --- /dev/null +++ b/src/curve/ScoreSaberCurve.js @@ -0,0 +1,82 @@ +// Yoinked from https://github.com/Shurdoof/pp-calculator/blob/c24b5ca452119339928831d74e6d603fb17fd5ef/src/lib/pp/calculator.ts +// Thank for for this I have no fucking idea what the maths is doing but it works! +export default class ScoreSaberCurve { + static starMultiplier = 42.11; + static ppCurve = [ + [1, 7], + [0.999, 5.8], + [0.9975, 4.7], + [0.995, 3.76], + [0.9925, 3.17], + [0.99, 2.73], + [0.9875, 2.38], + [0.985, 2.1], + [0.9825, 1.88], + [0.98, 1.71], + [0.9775, 1.57], + [0.975, 1.45], + [0.9725, 1.37], + [0.97, 1.31], + [0.965, 1.2], + [0.96, 1.11], + [0.955, 1.045], + [0.95, 1], + [0.94, 0.94], + [0.93, 0.885], + [0.92, 0.835], + [0.91, 0.79], + [0.9, 0.75], + [0.875, 0.655], + [0.85, 0.57], + [0.825, 0.51], + [0.8, 0.47], + [0.75, 0.4], + [0.7, 0.34], + [0.65, 0.29], + [0.6, 0.25], + [0.0, 0.0], + ]; + + static clamp(value, min, max) { + if (min !== null && value < min) { + return min; + } + + if (max !== null && value > max) { + return max; + } + + return value; + } + + static lerp(v0, v1, t) { + return v0 + t * (v1 - v0); + } + + static calculatePPModifier(c1, c2, acc) { + const distance = (c2[0] - acc) / (c2[0] - c1[0]); + return ScoreSaberCurve.lerp(c2[1], c1[1], distance); + } + + static findPPModifier(acc, curve) { + acc = ScoreSaberCurve.clamp(acc, 0, 100) / 100; + + let prev = curve[1]; + for (const item of curve) { + if (item[0] <= acc) { + return ScoreSaberCurve.calculatePPModifier(item, prev, acc); + } + prev = item; + } + } + + static getPP(acc, stars) { + const ppValue = stars * ScoreSaberCurve.starMultiplier; + const modifier = ScoreSaberCurve.findPPModifier( + acc, + ScoreSaberCurve.ppCurve + ); + + return modifier * ppValue; + } +} diff --git a/src/utils/scoreSaberUtils.js b/src/utils/scoreSaberUtils.js new file mode 100644 index 0000000..9e16849 --- /dev/null +++ b/src/utils/scoreSaberUtils.js @@ -0,0 +1,30 @@ +function diffToScoreSaberDiff(diff) { + console.log( + "🚀 ~ file: scoreSaberUtils.js ~ line 2 ~ diffToScoreSaberDiff ~ diff", + diff + ); + switch (diff) { + case "Easy": { + return 1; + } + case "Normal": { + return 3; + } + case "Hard": { + return 5; + } + case "Expert": { + return 7; + } + case "Expert+": { + return 9; + } + case "ExpertPlus": { + return 9; + } + } +} + +module.exports = { + diffToScoreSaberDiff, +}; diff --git a/src/utils/utils.js b/src/utils/utils.js index fd2f615..5928b21 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,11 +1,8 @@ -import { - default as LeaderboardType, - default as WebsiteTypes, -} from "../consts/LeaderboardType"; +import { default as LeaderboardType } from "../consts/LeaderboardType"; +import BeatLeaderCurve from "../curve/BeatLeaderCurve"; +import ScoreSaberCurve from "../curve/ScoreSaberCurve"; export default class Utils { - constructor() {} - /** * Returns the information for the given website type. * @@ -35,7 +32,10 @@ export default class Utils { static calculatePP(stars, acc, type) { if (type === "BeatLeader") { - return WebsiteTypes.BeatLeader.ppFromAcc(acc, stars); + return BeatLeaderCurve.getPP(acc, stars); + } + if (type === "ScoreSaber") { + return ScoreSaberCurve.getPP(acc, stars); } return undefined; }