From 203bb40c8a5d4f97b48379266b9dfe69330d6099 Mon Sep 17 00:00:00 2001 From: kionell Date: Sat, 10 Sep 2022 00:17:23 +0300 Subject: [PATCH] Remove build from repository --- lib/index.cjs | 1153 ------------------------------------------------ lib/index.d.ts | 593 ------------------------- lib/index.mjs | 1127 ---------------------------------------------- package.json | 2 +- 4 files changed, 1 insertion(+), 2874 deletions(-) delete mode 100644 lib/index.cjs delete mode 100644 lib/index.d.ts delete mode 100644 lib/index.mjs diff --git a/lib/index.cjs b/lib/index.cjs deleted file mode 100644 index 0709c4d..0000000 --- a/lib/index.cjs +++ /dev/null @@ -1,1153 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, '__esModule', { value: true }); - -let osuClasses = require('osu-classes'); -let osuStandardStable = require('osu-standard-stable'); -let osuTaikoStable = require('osu-taiko-stable'); -let osuCatchStable = require('osu-catch-stable'); -let osuManiaStable = require('osu-mania-stable'); -let fs = require('fs'); -let osuDownloader = require('osu-downloader'); -let osuParsers = require('osu-parsers'); - -function calculateDifficulty(options) { - const { beatmap, ruleset, mods } = options; - - if (!beatmap || !ruleset) { - throw new Error('Cannot calculate difficulty attributes'); - } - - const calculator = options.calculator - ?? ruleset.createDifficultyCalculator(beatmap); - - if (typeof mods !== 'string' && typeof mods !== 'number') { - return calculator.calculateAt(options.totalHits); - } - - const combination = ruleset.createModCombination(mods); - - return calculator.calculateWithModsAt(combination, options.totalHits); -} - -function calculatePerformance(options) { - const { difficulty, scoreInfo, ruleset } = options; - - if (!difficulty || !scoreInfo || !ruleset) { - throw new Error('Cannot calculate performance attributes'); - } - - const castedDifficulty = difficulty; - const calculator = ruleset.createPerformanceCalculator(castedDifficulty, scoreInfo); - - return calculator.calculateAttributes(); -} - -exports.GameMode = void 0; - -(function(GameMode) { - GameMode[GameMode['Osu'] = 0] = 'Osu'; - GameMode[GameMode['Taiko'] = 1] = 'Taiko'; - GameMode[GameMode['Fruits'] = 2] = 'Fruits'; - GameMode[GameMode['Mania'] = 3] = 'Mania'; -})(exports.GameMode || (exports.GameMode = {})); - -class ExtendedStandardDifficultyCalculator extends osuStandardStable.StandardDifficultyCalculator { - constructor() { - super(...arguments); - this._skills = []; - } - getSkills() { - return this._skills; - } - _createDifficultyAttributes(beatmap, mods, skills) { - this._skills = skills; - - return super._createDifficultyAttributes(beatmap, mods, skills); - } -} - -class ExtendedTaikoDifficultyCalculator extends osuTaikoStable.TaikoDifficultyCalculator { - constructor() { - super(...arguments); - this._skills = []; - } - getSkills() { - return this._skills; - } - _createDifficultyAttributes(beatmap, mods, skills) { - this._skills = skills; - - return super._createDifficultyAttributes(beatmap, mods, skills); - } -} - -class ExtendedCatchDifficultyCalculator extends osuCatchStable.CatchDifficultyCalculator { - constructor() { - super(...arguments); - this._skills = []; - } - getSkills() { - return this._skills; - } - _createDifficultyAttributes(beatmap, mods, skills) { - this._skills = skills; - - return super._createDifficultyAttributes(beatmap, mods, skills); - } -} - -class ExtendedManiaDifficultyCalculator extends osuManiaStable.ManiaDifficultyCalculator { - constructor() { - super(...arguments); - this._skills = []; - } - getSkills() { - return this._skills; - } - _createDifficultyAttributes(beatmap, mods, skills) { - this._skills = skills; - - return super._createDifficultyAttributes(beatmap, mods, skills); - } -} - -function createDifficultyCalculator(beatmap, ruleset) { - switch (ruleset.id) { - case exports.GameMode.Osu: - return new ExtendedStandardDifficultyCalculator(beatmap, ruleset); - case exports.GameMode.Taiko: - return new ExtendedTaikoDifficultyCalculator(beatmap, ruleset); - case exports.GameMode.Fruits: - return new ExtendedCatchDifficultyCalculator(beatmap, ruleset); - case exports.GameMode.Mania: - return new ExtendedManiaDifficultyCalculator(beatmap, ruleset); - } - - throw new Error('This ruleset does not support strain output!'); -} - -function createBeatmapInfo(beatmap, hash) { - return new osuClasses.BeatmapInfo({ - id: beatmap?.metadata.beatmapId, - beatmapsetId: beatmap?.metadata.beatmapSetId, - creator: beatmap?.metadata.creator, - title: beatmap?.metadata.title, - artist: beatmap?.metadata.artist, - version: beatmap?.metadata.version, - hittable: countObjects(osuClasses.HitType.Normal, beatmap), - slidable: countObjects(osuClasses.HitType.Slider, beatmap), - spinnable: countObjects(osuClasses.HitType.Spinner, beatmap), - holdable: countObjects(osuClasses.HitType.Hold, beatmap), - length: (beatmap?.length ?? 0) / 1000, - bpmMin: beatmap?.bpmMin, - bpmMax: beatmap?.bpmMax, - bpmMode: beatmap?.bpmMode, - circleSize: beatmap?.difficulty.circleSize, - approachRate: beatmap?.difficulty.approachRate, - overallDifficulty: beatmap?.difficulty.overallDifficulty, - drainRate: beatmap?.difficulty.drainRate, - rulesetId: beatmap?.mode, - mods: getMods(beatmap), - maxCombo: getMaxCombo(beatmap), - isConvert: beatmap?.originalMode !== beatmap?.mode, - md5: hash ?? '', - }); -} - -function createBeatmapAttributes(beatmap) { - const hittable = countObjects(osuClasses.HitType.Normal, beatmap); - const maxTinyDroplets = countTinyDroplets(beatmap); - const maxDroplets = countDroplets(beatmap) - maxTinyDroplets; - const maxFruits = countFruits(beatmap) + hittable; - const totalHits = beatmap?.mode === exports.GameMode.Fruits - ? maxFruits + maxDroplets + maxTinyDroplets - : getTotalHits(beatmap); - - return { - beatmapId: beatmap?.metadata.beatmapId, - rulesetId: beatmap?.mode, - mods: getMods(beatmap)?.toString() ?? 'NM', - maxCombo: getMaxCombo(beatmap), - totalHits, - maxFruits, - maxDroplets, - maxTinyDroplets, - }; -} - -function countObjects(hitType, beatmap) { - if (!beatmap) { - return 0; - } - - return beatmap.hitObjects.reduce((sum, obj) => { - return sum + (obj.hitType & hitType ? 1 : 0); - }, 0); -} - -function countFruits(beatmap) { - return countNested(osuCatchStable.JuiceFruit, beatmap); -} - -function countDroplets(beatmap) { - return countNested(osuCatchStable.JuiceDroplet, beatmap); -} - -function countTinyDroplets(beatmap) { - return countNested(osuCatchStable.JuiceTinyDroplet, beatmap); -} - -function countNested(Class, beatmap) { - const rulesetBeatmap = beatmap; - - return rulesetBeatmap.hitObjects.reduce((sum, obj) => { - const nestedSum = obj.nestedHitObjects?.reduce((sum, obj) => { - return sum + (obj instanceof Class ? 1 : 0); - }, 0); - - return sum + (nestedSum ?? 0); - }, 0); -} - -function getTotalHits(beatmap) { - if (!beatmap) { - return 0; - } - - switch (beatmap.mode) { - case exports.GameMode.Osu: { - const circles = countObjects(osuClasses.HitType.Normal, beatmap); - const sliders = countObjects(osuClasses.HitType.Slider, beatmap); - const spinners = countObjects(osuClasses.HitType.Spinner, beatmap); - - return circles + sliders + spinners; - } - case exports.GameMode.Taiko: { - return countObjects(osuClasses.HitType.Normal, beatmap); - } - case exports.GameMode.Fruits: { - const hittable = countObjects(osuClasses.HitType.Normal, beatmap); - const tinyDroplets = countTinyDroplets(beatmap); - const droplets = countDroplets(beatmap) - tinyDroplets; - const fruits = countFruits(beatmap) + hittable; - - return fruits + droplets + tinyDroplets; - } - case exports.GameMode.Mania: { - const notes = countObjects(osuClasses.HitType.Normal, beatmap); - const holds = countObjects(osuClasses.HitType.Hold, beatmap); - - return notes + holds; - } - } - - const hittable = countObjects(osuClasses.HitType.Normal, beatmap); - const slidable = countObjects(osuClasses.HitType.Slider, beatmap); - const spinnable = countObjects(osuClasses.HitType.Spinner, beatmap); - const holdable = countObjects(osuClasses.HitType.Hold, beatmap); - - return hittable + slidable + spinnable + holdable; -} - -function getMaxCombo(beatmap) { - return beatmap?.maxCombo ?? 0; -} - -function getMods(beatmap) { - return beatmap?.mods ?? null; -} - -function getRulesetIdByName(rulesetName) { - switch (rulesetName?.toLowerCase()) { - case 'standard': - case 'std': - case 'osu': return exports.GameMode.Osu; - case 'taiko': return exports.GameMode.Taiko; - case 'ctb': - case 'catch': - case 'fruits': return exports.GameMode.Fruits; - case 'mania': return exports.GameMode.Mania; - } - - throw new Error('Unknown ruleset!'); -} - -function getRulesetById(rulesetId) { - switch (rulesetId) { - case exports.GameMode.Osu: return new osuStandardStable.StandardRuleset(); - case exports.GameMode.Taiko: return new osuTaikoStable.TaikoRuleset(); - case exports.GameMode.Fruits: return new osuCatchStable.CatchRuleset(); - case exports.GameMode.Mania: return new osuManiaStable.ManiaRuleset(); - } - - throw new Error('Unknown ruleset!'); -} - -function toDifficultyMods(mods, rulesetId) { - const ruleset = getRulesetById(rulesetId ?? exports.GameMode.Osu); - const difficultyCalculator = ruleset.createDifficultyCalculator(new osuClasses.Beatmap()); - const difficultyMods = difficultyCalculator.difficultyMods; - const combination = ruleset.createModCombination(mods); - const difficultyBitwise = combination.all.reduce((bitwise, mod) => { - const found = difficultyMods.find((m) => { - if (m.bitwise === mod.bitwise) { - return true; - } - - return m.acronym === 'DT' && mod.acronym === 'NC'; - }); - - return bitwise + (found?.bitwise ?? 0); - }, 0); - - return ruleset.createModCombination(difficultyBitwise); -} - -function toCombination(input, rulesetId) { - const ruleset = getRulesetById(rulesetId ?? exports.GameMode.Osu); - - return ruleset.createModCombination(input); -} - -function toDifficultyAttributes(difficulty, rulesetId) { - const attributes = createAttributes(rulesetId, difficulty?.mods); - - if (typeof difficulty !== 'object') { - return attributes; - } - - for (const key in difficulty) { - if (key in attributes) { - attributes[key] = difficulty[key]; - } - } - - return attributes; -} - -function toScoreInfo(data) { - const scoreInfo = new osuClasses.ScoreInfo(); - - if (data?.toJSON) { - return scoreInfo; - } - - const jsonable = data; - - scoreInfo.id = jsonable?.id; - scoreInfo.totalScore = jsonable?.totalScore; - scoreInfo.pp = jsonable?.pp; - scoreInfo.maxCombo = jsonable?.maxCombo; - scoreInfo.passed = jsonable?.passed; - scoreInfo.perfect = jsonable?.perfect; - scoreInfo.rank = jsonable?.rank; - scoreInfo.accuracy = jsonable?.accuracy; - scoreInfo.username = jsonable?.username; - scoreInfo.userId = jsonable?.userId; - scoreInfo.beatmapId = jsonable?.beatmapId; - scoreInfo.date = jsonable?.date; - scoreInfo.beatmapHashMD5 = jsonable?.beatmapHashMD5; - scoreInfo.rulesetId = jsonable?.rulesetId; - scoreInfo.mods = toCombination(jsonable.mods, jsonable.rulesetId); - scoreInfo.countGeki = jsonable?.countGeki; - scoreInfo.count300 = jsonable?.count300; - scoreInfo.countKatu = jsonable?.countKatu; - scoreInfo.count100 = jsonable?.count100; - scoreInfo.count50 = jsonable?.count50; - scoreInfo.countMiss = jsonable?.countMiss; - - return scoreInfo; -} - -function createAttributes(rulesetId, mods) { - const ruleset = getRulesetById(rulesetId ?? exports.GameMode.Osu); - const combination = ruleset.createModCombination(mods); - - switch (ruleset.id) { - case exports.GameMode.Taiko: return new osuTaikoStable.TaikoDifficultyAttributes(combination, 0); - case exports.GameMode.Fruits: return new osuCatchStable.CatchDifficultyAttributes(combination, 0); - case exports.GameMode.Mania: return new osuManiaStable.ManiaDifficultyAttributes(combination, 0); - } - - return new osuStandardStable.StandardDifficultyAttributes(combination, 0); -} - -async function downloadFile(path, options) { - const downloader = new osuDownloader.Downloader({ rootPath: path }); - const entry = new osuDownloader.DownloadEntry(options); - - downloader.addSingleEntry(entry); - - return downloader.downloadSingle(); -} - -function generateHitStatistics(attributes, accuracy = 1, countMiss = 0, count50, count100) { - if (accuracy > 1) { - accuracy /= 100; - } - - switch (attributes.rulesetId) { - case exports.GameMode.Taiko: - return generateTaikoHitStatistics(attributes, accuracy, countMiss, count100); - case exports.GameMode.Fruits: - return generateCatchHitStatistics(attributes, accuracy, countMiss, count50, count100); - case exports.GameMode.Mania: - return generateManiaHitStatistics(attributes); - } - - return generateOsuHitStatistics(attributes, accuracy, countMiss, count50, count100); -} - -function generateOsuHitStatistics(attributes, accuracy = 1, countMiss = 0, count50, count100) { - const totalHits = attributes.totalHits ?? 0; - - countMiss = osuClasses.MathUtils.clamp(countMiss, 0, totalHits); - count50 = count50 ? osuClasses.MathUtils.clamp(count50, 0, totalHits - countMiss) : 0; - - if (typeof count100 !== 'number') { - count100 = Math.round((totalHits - totalHits * accuracy) * 1.5); - } - else { - count100 = osuClasses.MathUtils.clamp(count100, 0, totalHits - count50 - countMiss); - } - - const count300 = totalHits - count100 - count50 - countMiss; - - return { - great: count300, - ok: count100, - meh: count50 ?? 0, - miss: countMiss, - }; -} - -function generateTaikoHitStatistics(attributes, accuracy = 1, countMiss = 0, count100) { - const totalHits = attributes.totalHits ?? 0; - - countMiss = osuClasses.MathUtils.clamp(countMiss, 0, totalHits); - - let count300; - - if (typeof count100 !== 'number') { - const targetTotal = Math.round(accuracy * totalHits * 2); - - count300 = targetTotal - (totalHits - countMiss); - count100 = totalHits - count300 - countMiss; - } - else { - count100 = osuClasses.MathUtils.clamp(count100, 0, totalHits - countMiss); - count300 = totalHits - count100 - countMiss; - } - - return { - great: count300, - ok: count100, - miss: countMiss, - }; -} - -function generateCatchHitStatistics(attributes, accuracy = 1, countMiss = 0, count50, count100) { - const maxCombo = attributes.maxCombo ?? 0; - const maxFruits = attributes.maxFruits ?? 0; - const maxDroplets = attributes.maxDroplets ?? 0; - const maxTinyDroplets = attributes.maxTinyDroplets ?? 0; - - if (typeof count100 === 'number') { - countMiss += maxDroplets - count100; - } - - countMiss = osuClasses.MathUtils.clamp(countMiss, 0, maxDroplets + maxFruits); - - let droplets = count100 ?? Math.max(0, maxDroplets - countMiss); - - droplets = osuClasses.MathUtils.clamp(droplets, 0, maxDroplets); - - const fruits = maxFruits - (countMiss - (maxDroplets - droplets)); - let tinyDroplets = Math.round(accuracy * (maxCombo + maxTinyDroplets)); - - tinyDroplets = count50 ?? tinyDroplets - fruits - droplets; - - const tinyMisses = maxTinyDroplets - tinyDroplets; - - return { - great: osuClasses.MathUtils.clamp(fruits, 0, maxFruits), - largeTickHit: osuClasses.MathUtils.clamp(droplets, 0, maxDroplets), - smallTickHit: tinyDroplets, - smallTickMiss: tinyMisses, - miss: countMiss, - }; -} - -function generateManiaHitStatistics(attributes) { - return { - perfect: attributes.totalHits ?? 0, - }; -} - -function getValidHitStatistics(original) { - return { - perfect: original?.perfect ?? 0, - great: original?.great ?? 0, - good: original?.good ?? 0, - ok: original?.ok ?? 0, - meh: original?.meh ?? 0, - largeTickHit: original?.largeTickHit ?? 0, - smallTickMiss: original?.smallTickMiss ?? 0, - smallTickHit: original?.smallTickHit ?? 0, - miss: original?.miss ?? 0, - largeBonus: 0, - largeTickMiss: 0, - smallBonus: 0, - ignoreHit: 0, - ignoreMiss: 0, - none: 0, - }; -} - -function calculateAccuracy(scoreInfo) { - const geki = scoreInfo.countGeki; - const katu = scoreInfo.countKatu; - const c300 = scoreInfo.count300; - const c100 = scoreInfo.count100; - const c50 = scoreInfo.count50; - const total = scoreInfo.totalHits || calculateTotalHits(scoreInfo); - - if (total <= 0) { - return 1; - } - - switch (scoreInfo.rulesetId) { - case exports.GameMode.Osu: - return Math.max(0, (c50 / 6 + c100 / 3 + c300) / total); - case exports.GameMode.Taiko: - return Math.max(0, (c100 / 2 + c300) / total); - case exports.GameMode.Fruits: - return Math.max(0, (c50 + c100 + c300) / total); - case exports.GameMode.Mania: - return Math.max(0, (c50 / 6 + c100 / 3 + katu / 1.5 + (c300 + geki)) / total); - } - - return 1; -} - -function calculateTotalHits(scoreInfo) { - const geki = scoreInfo.countGeki; - const katu = scoreInfo.countKatu; - const c300 = scoreInfo.count300; - const c100 = scoreInfo.count100; - const c50 = scoreInfo.count50; - const misses = scoreInfo.countMiss; - - switch (scoreInfo.rulesetId) { - case exports.GameMode.Osu: - return c300 + c100 + c50 + misses; - case exports.GameMode.Taiko: - return c300 + c100 + c50 + misses; - case exports.GameMode.Fruits: - return c300 + c100 + c50 + misses + katu; - case exports.GameMode.Mania: - return c300 + c100 + c50 + misses + geki + katu; - } - - return c300 + c100 + c50 + misses + geki + katu; -} - -function scaleTotalScore(totalScore, mods) { - const difficultyReduction = mods?.all - .filter((m) => m.type === osuClasses.ModType.DifficultyReduction) ?? []; - - return difficultyReduction - .reduce((score, mod) => score * mod.multiplier, totalScore); -} - -function calculateRank(scoreInfo) { - if (!scoreInfo.passed) { - return osuClasses.ScoreRank.F; - } - - switch (scoreInfo.rulesetId) { - case exports.GameMode.Osu: return calculateOsuRank(scoreInfo); - case exports.GameMode.Taiko: return calculateTaikoRank(scoreInfo); - case exports.GameMode.Fruits: return calculateCatchRank(scoreInfo); - case exports.GameMode.Mania: return calculateManiaRank(scoreInfo); - } - - return osuClasses.ScoreRank.F; -} - -function calculateOsuRank(scoreInfo) { - const hasFL = scoreInfo.mods?.has('FL') ?? false; - const hasHD = scoreInfo.mods?.has('HD') ?? false; - const ratio300 = Math.fround(scoreInfo.count300 / scoreInfo.totalHits); - const ratio50 = Math.fround(scoreInfo.count50 / scoreInfo.totalHits); - - if (ratio300 === 1) { - return hasHD || hasFL ? osuClasses.ScoreRank.XH : osuClasses.ScoreRank.X; - } - - if (ratio300 > 0.9 && ratio50 <= 0.01 && scoreInfo.countMiss === 0) { - return hasHD || hasFL ? osuClasses.ScoreRank.SH : osuClasses.ScoreRank.S; - } - - if ((ratio300 > 0.8 && scoreInfo.countMiss === 0) || ratio300 > 0.9) { - return osuClasses.ScoreRank.A; - } - - if ((ratio300 > 0.7 && scoreInfo.countMiss === 0) || ratio300 > 0.8) { - return osuClasses.ScoreRank.B; - } - - return ratio300 > 0.6 ? osuClasses.ScoreRank.C : osuClasses.ScoreRank.D; -} - -function calculateTaikoRank(scoreInfo) { - const hasFL = scoreInfo.mods?.has('FL') ?? false; - const hasHD = scoreInfo.mods?.has('HD') ?? false; - const ratio300 = Math.fround(scoreInfo.count300 / scoreInfo.totalHits); - const ratio50 = Math.fround(scoreInfo.count50 / scoreInfo.totalHits); - - if (ratio300 === 1) { - return hasHD || hasFL ? osuClasses.ScoreRank.XH : osuClasses.ScoreRank.X; - } - - if (ratio300 > 0.9 && ratio50 <= 0.01 && scoreInfo.countMiss === 0) { - return hasHD || hasFL ? osuClasses.ScoreRank.SH : osuClasses.ScoreRank.S; - } - - if ((ratio300 > 0.8 && scoreInfo.countMiss === 0) || ratio300 > 0.9) { - return osuClasses.ScoreRank.A; - } - - if ((ratio300 > 0.7 && scoreInfo.countMiss === 0) || ratio300 > 0.8) { - return osuClasses.ScoreRank.B; - } - - return ratio300 > 0.6 ? osuClasses.ScoreRank.C : osuClasses.ScoreRank.D; -} - -function calculateCatchRank(scoreInfo) { - const hasFL = scoreInfo.mods?.has('FL') ?? false; - const hasHD = scoreInfo.mods?.has('HD') ?? false; - const accuracy = scoreInfo.accuracy; - - if (accuracy === 1) { - return hasHD || hasFL ? osuClasses.ScoreRank.XH : osuClasses.ScoreRank.X; - } - - if (accuracy > 0.98) { - return hasHD || hasFL ? osuClasses.ScoreRank.SH : osuClasses.ScoreRank.S; - } - - if (accuracy > 0.94) { - return osuClasses.ScoreRank.A; - } - - if (accuracy > 0.90) { - return osuClasses.ScoreRank.B; - } - - if (accuracy > 0.85) { - return osuClasses.ScoreRank.C; - } - - return osuClasses.ScoreRank.D; -} - -function calculateManiaRank(scoreInfo) { - const hasFL = scoreInfo.mods?.has('FL') ?? false; - const hasHD = scoreInfo.mods?.has('HD') ?? false; - const accuracy = scoreInfo.accuracy; - - if (accuracy === 1) { - return hasHD || hasFL ? osuClasses.ScoreRank.XH : osuClasses.ScoreRank.X; - } - - if (accuracy > 0.95) { - return hasHD || hasFL ? osuClasses.ScoreRank.SH : osuClasses.ScoreRank.S; - } - - if (accuracy > 0.9) { - return osuClasses.ScoreRank.A; - } - - if (accuracy > 0.8) { - return osuClasses.ScoreRank.B; - } - - if (accuracy > 0.7) { - return osuClasses.ScoreRank.C; - } - - return osuClasses.ScoreRank.D; -} - -async function parseBeatmap(options) { - const { beatmapId, fileURL, hash, savePath } = options; - - if (typeof beatmapId === 'string' || typeof beatmapId === 'number') { - return parseBeatmapById(beatmapId, hash, savePath); - } - - if (typeof fileURL === 'string') { - return parseCustomBeatmap(fileURL, hash, savePath); - } - - throw new Error('No beatmap ID or beatmap URL was specified!'); -} - -async function parseBeatmapById(id, hash, savePath) { - let _a; - const result = await downloadFile(savePath, { - save: typeof savePath === 'string', - id, - }); - - if (!result.isSuccessful || (!savePath && !result.buffer)) { - throw new Error(`Beatmap with ID "${id}" failed to download: "${result.statusText}"`); - } - - if (hash && hash !== result.md5) { - throw new Error('Beatmap MD5 hash missmatch!'); - } - - const data = savePath - ? fs.readFileSync(result.filePath) - : result.buffer; - const parsed = parseBeatmapData(data); - - (_a = parsed.metadata).beatmapId || (_a.beatmapId = parseInt(id)); - - return { - hash: result.md5, - data: parsed, - }; -} - -async function parseCustomBeatmap(url, hash, savePath) { - const result = await downloadFile(savePath, { - save: typeof savePath === 'string', - url, - }); - - if (!result.isSuccessful || (!savePath && !result.buffer)) { - throw new Error(`Beatmap from "${url}" failed to download: ${result.statusText}`); - } - - if (hash && hash !== result.md5) { - throw new Error('Beatmap MD5 hash missmatch!'); - } - - const data = savePath - ? fs.readFileSync(result.filePath) - : result.buffer; - - return { - data: parseBeatmapData(data), - hash: result.md5, - }; -} - -function parseBeatmapData(data) { - const stringified = data.toString(); - const decoder = new osuParsers.BeatmapDecoder(); - const parseStoryboard = false; - - return decoder.decodeFromString(stringified, parseStoryboard); -} - -async function parseScore(options) { - const { replayURL, hash, lifeBar } = options; - - if (typeof replayURL === 'string') { - return parseCustomScore(replayURL, hash, lifeBar); - } - - throw new Error('No replay URL was specified!'); -} - -async function parseCustomScore(url, hash, parseReplay = false) { - const result = await downloadFile('', { - type: osuDownloader.DownloadType.Replay, - save: false, - url, - }); - - if (!result.isSuccessful || !result.buffer) { - throw new Error('Replay failed to download!'); - } - - if (hash && hash !== result.md5) { - throw new Error('Replay MD5 hash missmatch!'); - } - - return { - data: await parseScoreData(result.buffer, parseReplay), - hash: result.md5, - }; -} - -async function parseScoreData(data, parseReplay = false) { - return await new osuParsers.ScoreDecoder().decodeFromBuffer(data, parseReplay); -} - -class ScoreSimulator { - async completeReplay(score, attributes) { - const scoreInfo = score.info; - const beatmapCombo = attributes.maxCombo ?? 0; - - return this._generateScoreInfo({ - ...scoreInfo, - beatmapId: attributes.beatmapId, - rulesetId: attributes.rulesetId, - totalHits: attributes.totalHits, - mods: toCombination(attributes.mods, attributes.rulesetId), - perfect: scoreInfo.maxCombo >= beatmapCombo, - }); - } - simulate(options) { - const statistics = generateHitStatistics(options.attributes, options.accuracy, options.countMiss, options.count50, options.count100); - const attributes = options.attributes; - const beatmapCombo = attributes.maxCombo ?? 0; - const percentage = options.percentCombo ?? 100; - const multiplier = osuClasses.MathUtils.clamp(percentage, 0, 100) / 100; - const scoreCombo = options.maxCombo ?? Math.round(beatmapCombo * multiplier); - const misses = statistics.miss ?? 0; - const limitedCombo = Math.min(scoreCombo, beatmapCombo - misses); - const maxCombo = Math.max(0, limitedCombo); - - return this._generateScoreInfo({ - beatmapId: attributes.beatmapId, - rulesetId: attributes.rulesetId, - totalHits: attributes.totalHits, - mods: toCombination(attributes.mods, attributes.rulesetId), - totalScore: options.totalScore, - perfect: maxCombo >= beatmapCombo, - statistics, - maxCombo, - }); - } - simulateFC(scoreInfo, attributes) { - if (scoreInfo.rulesetId === exports.GameMode.Mania) { - return this.simulateMax(attributes); - } - - const statistics = getValidHitStatistics(scoreInfo.statistics); - const totalHits = attributes.totalHits ?? 0; - - switch (scoreInfo.rulesetId) { - case exports.GameMode.Fruits: - statistics.great = totalHits - statistics.largeTickHit - - statistics.smallTickHit - statistics.smallTickMiss - statistics.miss; - - statistics.largeTickHit += statistics.miss; - break; - case exports.GameMode.Mania: - statistics.perfect = totalHits - statistics.great - - statistics.good - statistics.ok - statistics.meh; - - break; - default: - statistics.great = totalHits - statistics.ok - statistics.meh; - } - - statistics.miss = 0; - - return this._generateScoreInfo({ - ...scoreInfo, - mods: scoreInfo.mods ?? toCombination(attributes.mods, attributes.rulesetId), - beatmapId: attributes.beatmapId, - rulesetId: attributes.rulesetId, - maxCombo: attributes.maxCombo, - perfect: true, - statistics, - totalHits, - }); - } - simulateMax(attributes) { - const statistics = generateHitStatistics(attributes); - const totalHits = attributes.totalHits ?? 0; - const score = this._generateScoreInfo({ - beatmapId: attributes.beatmapId, - rulesetId: attributes.rulesetId, - maxCombo: attributes.maxCombo, - mods: toCombination(attributes.mods, attributes.rulesetId), - perfect: true, - statistics, - totalHits, - }); - - if (attributes.rulesetId === exports.GameMode.Mania) { - score.totalScore = 1e6; - } - - return score; - } - _generateScoreInfo(options) { - const scoreInfo = new osuClasses.ScoreInfo({ - id: options?.id, - beatmapId: options?.beatmapId, - userId: options?.userId, - username: options?.username ?? 'osu!', - maxCombo: options?.maxCombo, - statistics: getValidHitStatistics(options?.statistics), - rawMods: options?.rawMods, - rulesetId: options?.rulesetId, - perfect: options?.perfect, - beatmapHashMD5: options?.beatmapHashMD5, - date: options?.date, - pp: options?.pp, - }); - - if (options?.mods) { - scoreInfo.mods = options.mods; - } - - if (scoreInfo.rulesetId === exports.GameMode.Mania) { - scoreInfo.totalScore = options?.totalScore || scaleTotalScore(1e6, scoreInfo.mods); - } - - scoreInfo.passed = scoreInfo.totalHits >= (options?.totalHits ?? 0); - scoreInfo.accuracy = options.accuracy || calculateAccuracy(scoreInfo); - scoreInfo.rank = osuClasses.ScoreRank[calculateRank(scoreInfo)]; - - return scoreInfo; - } -} - -class BeatmapCalculator { - constructor() { - this._scoreSimulator = new ScoreSimulator(); - } - async calculate(options) { - if (this._checkPrecalculated(options)) { - return this._processPrecalculated(options); - } - - const { data: parsed, hash: beatmapMD5 } = await parseBeatmap(options); - const ruleset = options.ruleset ?? getRulesetById(options.rulesetId ?? parsed.mode); - const combination = ruleset.createModCombination(options.mods); - const beatmap = ruleset.applyToBeatmapWithMods(parsed, combination); - const beatmapInfo = options.beatmapInfo ?? createBeatmapInfo(beatmap, beatmapMD5); - const attributes = options.attributes ?? createBeatmapAttributes(beatmap); - const totalHits = options.totalHits; - const calculator = createDifficultyCalculator(beatmap, ruleset); - const difficulty = options.difficulty && !options.strains - ? toDifficultyAttributes(options.difficulty, ruleset.id) - : calculateDifficulty({ beatmap, ruleset, calculator, totalHits }); - const skills = options.strains ? this._getSkillsOutput(calculator) : null; - const scores = this._simulateScores(attributes, options); - const performance = scores.map((scoreInfo) => calculatePerformance({ - difficulty, - ruleset, - scoreInfo, - })); - - return { - beatmapInfo: beatmapInfo?.toJSON() ?? beatmapInfo, - attributes, - skills, - difficulty, - performance, - }; - } - _processPrecalculated(options) { - const { beatmapInfo, attributes } = options; - const ruleset = options.ruleset ?? getRulesetById(attributes.rulesetId); - const difficulty = toDifficultyAttributes(options.difficulty, ruleset.id); - const scores = this._simulateScores(attributes, options); - const performance = scores.map((scoreInfo) => calculatePerformance({ - difficulty, - ruleset, - scoreInfo, - })); - - return { - beatmapInfo: beatmapInfo?.toJSON() ?? beatmapInfo, - skills: null, - attributes, - difficulty, - performance, - }; - } - _checkPrecalculated(options) { - const isValid = !!options.beatmapInfo - && !!options.attributes - && !!options.difficulty - && !options.strains; - - if (options.attributes && typeof options.totalHits === 'number') { - return isValid && options.attributes.totalHits === options.totalHits; - } - - return isValid; - } - _getSkillsOutput(calculator) { - const skills = calculator.getSkills(); - const strainSkills = skills.filter((s) => s instanceof osuClasses.StrainSkill); - const output = strainSkills.map((skill) => { - return { - title: skill.constructor.name, - strainPeaks: [...skill.getCurrentStrainPeaks()], - }; - }); - - if (output[0]?.title === 'Aim' && output[1]?.title === 'Aim') { - output[1].title = 'Aim (No Sliders)'; - } - - if (output[2]?.title === 'Stamina' && output[3]?.title === 'Stamina') { - output[2].title = 'Stamina (Left)'; - output[3].title = 'Stamina (Right)'; - } - - return output; - } - _simulateScores(attributes, options) { - return attributes.rulesetId === exports.GameMode.Mania - ? this._simulateManiaScores(attributes, options.totalScores) - : this._simulateOtherScores(attributes, options.accuracy); - } - _simulateOtherScores(attributes, accuracy) { - accuracy ?? (accuracy = [95, 99, 100]); - - return accuracy.map((accuracy) => this._scoreSimulator.simulate({ - attributes, - accuracy, - })); - } - _simulateManiaScores(attributes, totalScores) { - const mods = toCombination(attributes.mods, attributes.rulesetId); - - totalScores ?? (totalScores = [ - scaleTotalScore(8e5, mods), - scaleTotalScore(9e5, mods), - scaleTotalScore(1e6, mods), - ]); - - return totalScores.map((totalScore) => this._scoreSimulator.simulate({ - attributes, - totalScore, - })); - } -} - -class ScoreCalculator { - constructor() { - this._scoreSimulator = new ScoreSimulator(); - } - async calculate(options) { - let attributes = options.attributes; - let beatmapMD5 = options.hash ?? options.attributes?.hash; - let rulesetId = options.rulesetId ?? options.attributes?.rulesetId; - let ruleset = options.ruleset ?? getRulesetById(rulesetId); - let difficulty = options.difficulty && ruleset - ? toDifficultyAttributes(options.difficulty, ruleset.id) - : null; - let score = attributes ? await this._createScore(options, attributes) : null; - const beatmapTotalHits = attributes?.totalHits ?? 0; - const scoreTotalHits = score?.info.totalHits ?? 0; - const isPartialDifficulty = beatmapTotalHits > scoreTotalHits; - - if (!attributes || !beatmapMD5 || !ruleset || !score || !difficulty || (isPartialDifficulty && !options.fix)) { - const { data, hash } = await parseBeatmap(options); - - beatmapMD5 ?? (beatmapMD5 = hash); - rulesetId ?? (rulesetId = data.mode); - ruleset ?? (ruleset = getRulesetById(rulesetId)); - - const combination = ruleset.createModCombination(options.mods); - const beatmap = ruleset.applyToBeatmapWithMods(data, combination); - - attributes ?? (attributes = createBeatmapAttributes(beatmap)); - score ?? (score = await this._createScore(options, attributes)); - - if (!difficulty || isPartialDifficulty) { - difficulty = calculateDifficulty({ - totalHits: scoreTotalHits, - beatmap, - ruleset, - }); - } - } - - const scoreBeatmapMD5 = score.info.beatmapHashMD5; - - if (beatmapMD5 && scoreBeatmapMD5 && beatmapMD5 !== scoreBeatmapMD5) { - throw new Error('Beatmap & replay missmatch!'); - } - - if (beatmapMD5 && !scoreBeatmapMD5) { - score.info.beatmapHashMD5 = beatmapMD5; - } - - const performance = calculatePerformance({ - scoreInfo: score.info, - difficulty, - ruleset, - }); - - return { - scoreInfo: score.info.toJSON(), - lifeBar: score.replay?.lifeBar, - difficulty, - performance, - }; - } - async _createScore(options, attributes) { - const score = await this._parseOrSimulateScore(options, attributes); - - if (options.fix) { - score.info = this._scoreSimulator.simulateFC(score.info, attributes); - } - - return score; - } - async _parseOrSimulateScore(options, attributes) { - const { scoreInfo, replayURL } = options; - - if (scoreInfo) { - const info = toScoreInfo(scoreInfo); - const replay = null; - - return new osuClasses.Score(info, replay); - } - - if (!replayURL) { - const info = this._scoreSimulator.simulate({ ...options, attributes }); - const replay = null; - - return new osuClasses.Score(info, replay); - } - - const { data: score } = await parseScore(options); - - score.info = await this._scoreSimulator.completeReplay(score, attributes); - - return score; - } -} - -exports.BeatmapCalculator = BeatmapCalculator; -exports.ScoreCalculator = ScoreCalculator; -exports.ScoreSimulator = ScoreSimulator; -exports.calculateAccuracy = calculateAccuracy; -exports.calculateDifficulty = calculateDifficulty; -exports.calculatePerformance = calculatePerformance; -exports.calculateRank = calculateRank; -exports.calculateTotalHits = calculateTotalHits; -exports.createBeatmapAttributes = createBeatmapAttributes; -exports.createBeatmapInfo = createBeatmapInfo; -exports.createDifficultyCalculator = createDifficultyCalculator; -exports.downloadFile = downloadFile; -exports.generateHitStatistics = generateHitStatistics; -exports.getRulesetById = getRulesetById; -exports.getRulesetIdByName = getRulesetIdByName; -exports.getValidHitStatistics = getValidHitStatistics; -exports.parseBeatmap = parseBeatmap; -exports.parseScore = parseScore; -exports.scaleTotalScore = scaleTotalScore; -exports.toCombination = toCombination; -exports.toDifficultyAttributes = toDifficultyAttributes; -exports.toDifficultyMods = toDifficultyMods; -exports.toScoreInfo = toScoreInfo; diff --git a/lib/index.d.ts b/lib/index.d.ts deleted file mode 100644 index 6c05377..0000000 --- a/lib/index.d.ts +++ /dev/null @@ -1,593 +0,0 @@ -import { IBeatmap, IRuleset, DifficultyCalculator, DifficultyAttributes, IScoreInfo, PerformanceAttributes, Skill, IScore, ScoreInfo, IBeatmapInfo, IJsonableScoreInfo, IHitStatistics, ModCombination, ScoreRank, IJsonableBeatmapInfo, ILifeBarFrame } from 'osu-classes'; -import { IDownloadEntryOptions, DownloadResult } from 'osu-downloader'; - -/** - * Beatmap attributes that will be used to simulate scores. - */ -interface IBeatmapAttributes { - /** - * Beatmap ID. - */ - beatmapId?: number; - /** - * Beatmap MD5 hash. - */ - hash?: string; - /** - * Beatmap ruleset ID. - */ - rulesetId?: number; - /** - * Mod combination or bitwise. - */ - mods?: string | number; - /** - * Beatmap total hits. - */ - totalHits?: number; - /** - * Beatmap max combo. - */ - maxCombo?: number; - /** - * The number of fruits in osu!catch beatmap. - */ - maxFruits?: number; - /** - * The number of droplets in osu!catch beatmap. - */ - maxDroplets?: number; - /** - * The number of tiny droplets in osu!catch beatmap. - */ - maxTinyDroplets?: number; -} - -/** - * Options for beatmap parsing. - */ -interface IBeatmapParsingOptions { - /** - * ID of the target beatmap. - */ - beatmapId?: string | number; - /** - * Custom file URL of the target beatmap. - */ - fileURL?: string; - /** - * Path to the beatmap file save location. - */ - savePath?: string; - /** - * Hash of the target beatmap. Used to validate beatmap files. - * If wasn't specified then file will not be validated. - */ - hash?: string; -} - -/** - * Beatmap skill data. - */ -interface IBeatmapSkill { - /** - * Skill name. - */ - title: string; - /** - * Strain peaks of this skill. - */ - strainPeaks: number[]; -} - -/** - * Raw difficulty attributes with no methods. - */ -interface IDifficultyAttributes { - /** - * The combined star rating of all skill. - */ - starRating: number; - /** - * The maximum achievable combo. - */ - maxCombo: number; - /** - * Mod combination or bitwise. - */ - mods: string | number; -} - -interface IDifficultyCalculationOptions { - /** - * An instance of any beatmap. - */ - beatmap?: IBeatmap; - /** - * An instance of any ruleset. - */ - ruleset?: IRuleset; - /** - * Custom difficulty calculator. - */ - calculator?: DifficultyCalculator; - /** - * Mod combination or bitwise. Default is NM. - */ - mods?: string | number; - /** - * Total hits for gradual beatmap difficulty calculation. - */ - totalHits?: number; -} - -/** - * Options for performance calculation of a score. - */ -interface IPerformanceCalculationOptions { - /** - * Difficulty attributes of the target beatmap. - */ - difficulty: DifficultyAttributes; - /** - * Target score information. - */ - scoreInfo: IScoreInfo; - /** - * An instance of any ruleset. - */ - ruleset: IRuleset; -} - -/** - * Options for score parsing. - */ -interface IScoreParsingOptions { - /** - * Custom replay file URL. - */ - replayURL?: string; - /** - * Output replay life bar if replay file is present? - */ - lifeBar?: boolean; - /** - * Path to the replay file save location. - */ - savePath?: string; - /** - * Hash of the target replay. Used to validate beatmap files. - * If wasn't specified then file will not be validated. - */ - hash?: string; -} - -/** - * Options for score simulation. - */ -interface IScoreSimulationOptions { - /** - * Beatmap attributes for score simulation. - */ - attributes: IBeatmapAttributes; - /** - * Target score misses. - */ - countMiss?: number; - /** - * Target score 50's. - */ - count50?: number; - /** - * Target score 100's. - */ - count100?: number; - /** - * Target score accuracy. - */ - accuracy?: number; - /** - * Target total score. - */ - totalScore?: number; - /** - * Target max combo of a score. - */ - maxCombo?: number; - /** - * Target percent of max combo of a score. - */ - percentCombo?: number; -} - -/** - * Calculates difficulty attributes by ID, custom file or IBeatmap object. - * @param options Difficulty attributes request options. - * @returns Calculated difficulty attributes. - */ -declare function calculateDifficulty(options: IDifficultyCalculationOptions): DifficultyAttributes; -/** - * Calculates difficulty attributes by ID, custom file or IBeatmap object. - * @param options Difficulty attributes request options. - * @returns Calculated difficulty attributes. - */ -declare function calculatePerformance(options: IPerformanceCalculationOptions): PerformanceAttributes; - -declare enum GameMode { - Osu = 0, - Taiko = 1, - Fruits = 2, - Mania = 3 -} - -/** - * Difficulty calculator that can return skill data. - */ -interface IExtendedDifficultyCalculator extends DifficultyCalculator { - /** - * Get current skill list. - */ - getSkills(): Skill[]; -} -/** - * Factory of extended difficulty calculators. - * @param beatmap IBeatmap object. - * @param ruleset Ruleset instance. - * @returns Instance of extended difficulty calculator. - */ -declare function createDifficultyCalculator(beatmap: IBeatmap, ruleset: IRuleset): IExtendedDifficultyCalculator; - -declare type BeatmapParsingResult = { - data: IBeatmap; - hash: string; -}; -declare type ScoreParsingResult = { - data: IScore; - hash: string; -}; -/** - * Tries to parse beatmap by beatmap ID or custom file URL. - * @param options Beatmap parsing options. - * @returns Parsed beatmap. - */ -declare function parseBeatmap(options: IBeatmapParsingOptions): Promise; -/** - * Downloads replay file and tries to parse a score from it. - * Returns null if parsing was not successful. - * @param options Score parsing options. - * @returns Parsed score. - */ -declare function parseScore(options: IScoreParsingOptions): Promise; - -/** - * A score simulator. - */ -declare class ScoreSimulator { - /** - * Adds missing properties to a score parsed from a replay file. - * @param score Parsed score from the replay file. - * @param attributes Beatmap attributes of this score. - * @returns Completed score info. - */ - completeReplay(score: IScore, attributes: IBeatmapAttributes): Promise; - /** - * Simulates a score by score simulation options. - * @param options Score simulation options. - * @returns Simulated score. - */ - simulate(options: IScoreSimulationOptions): ScoreInfo; - /** - * Simulates a new score with full combo. - * @param scoreInfo Original score. - * @param attributes Beatmap attributes of this score. - * @returns Simulated FC score. - */ - simulateFC(scoreInfo: IScoreInfo, attributes: IBeatmapAttributes): ScoreInfo; - /** - * Simulates a new score with max possible performance. - * @param attributes Beatmap attributes of this score. - * @returns Simulated SS score. - */ - simulateMax(attributes: IBeatmapAttributes): ScoreInfo; - private _generateScoreInfo; -} - -/** - * Converts IBeatmap object to beatmap information. - * @param beatmap IBeatmap object. - * @param hash Beatmap MD5 hash. - * @returns Converted beatmap info. - */ -declare function createBeatmapInfo(beatmap?: IBeatmap, hash?: string): IBeatmapInfo; -/** - * Converts IBeatmap object to beatmap attributes. - * @param beatmap IBeatmap object. - * @returns Converted beatmap attributes. - */ -declare function createBeatmapAttributes(beatmap?: IBeatmap): IBeatmapAttributes; - -/** - * Converts raw difficulty attributes to real difficulty attributes. - * @param difficulty Raw difficulty attributes. - * @returns Difficulty attributes instance. - */ -declare function toDifficultyAttributes(difficulty?: IDifficultyAttributes, rulesetId?: GameMode): DifficultyAttributes; -/** - * Converts score information object to score information instance. - * @param jsonable Raw score info data. - * @returns Converted score information. - */ -declare function toScoreInfo(data?: IScoreInfo | IJsonableScoreInfo): ScoreInfo; - -/** - * Downloads an osu! file by ID or URL. - * @param path Path to the file save location. - * @param options Download options. - * @returns Download result. - */ -declare function downloadFile(path?: string, options?: IDownloadEntryOptions): Promise; - -declare function generateHitStatistics(attributes: IBeatmapAttributes, accuracy?: number, countMiss?: number, count50?: number, count100?: number): Partial; -declare function getValidHitStatistics(original?: Partial): IHitStatistics; - -/** - * Filters mods from combination to get only difficulty mods. - * @param mods Original mods. - * @param rulesetId Target ruleset ID. - * @returns Difficulty mods. - */ -declare function toDifficultyMods(mods?: string | number, rulesetId?: number): ModCombination; -/** - * Converts unknown input to mod combination. - * @param input Original input. - * @param rulesetId Target ruleset ID. - * @returns Mod combination. - */ -declare function toCombination(input?: string | number, rulesetId?: number): ModCombination; - -/** - * Converts ruleset name to ruleset ID. - * @param rulesetName Ruleset name. - * @returns Ruleset ID. - */ -declare function getRulesetIdByName(rulesetName?: string): GameMode; -/** - * Creates a new ruleset instance by its ID. - * @param rulesetId Ruleset ID. - * @returns Ruleset instance. - */ -declare function getRulesetById(rulesetId?: number): IRuleset; - -/** - * Calculates accuracy of a score. - * @param scoreInfo Score information. - * @returns Calculated accuracy. - */ -declare function calculateAccuracy(scoreInfo: IScoreInfo): number; -/** - * Calculates total hits of a score. - * @param scoreInfo Score information. - * @returns Calculated total hits. - */ -declare function calculateTotalHits(scoreInfo: IScoreInfo): number; -/** - * Scales total score of a play with mod multipliers. - * @param totalScore Original total score. - * @returns Scaled total score. - */ -declare function scaleTotalScore(totalScore: number, mods?: ModCombination | null): number; -/** - * Calculates rank of a score. - * @param scoreInfo Score information. - * @returns Calculated score rank. - */ -declare function calculateRank(scoreInfo: IScoreInfo): ScoreRank; - -/** - * Options for beatmap calculation. - */ -interface IBeatmapCalculationOptions extends IBeatmapParsingOptions { - /** - * Precalculated beatmap information. - */ - beatmapInfo?: IBeatmapInfo | IJsonableBeatmapInfo; - /** - * Beatmap attributes for score simulation. - */ - attributes?: IBeatmapAttributes; - /** - * Ruleset ID. - */ - rulesetId?: GameMode; - /** - * Custom ruleset instance. - */ - ruleset?: IRuleset; - /** - * Mod combination or bitwise. - */ - mods?: string | number; - /** - * Precalculated difficulty attributes. - */ - difficulty?: IDifficultyAttributes; - /** - * Total hits for gradual beatmap difficulty calculation. - * If it differs from the hit object count of - * a full beatmap then it will force difficulty calculation. - */ - totalHits?: number; - /** - * Output strain peaks or not. - */ - strains?: boolean; - /** - * List of accuracy for all game modes except osu!mania. - */ - accuracy?: number[]; - /** - * List of total scores for osu!mania game mode. - */ - totalScores?: number[]; -} - -/** - * Calculated beatmap. - */ -interface ICalculatedBeatmap { - /** - * Beatmap information. - */ - beatmapInfo: IJsonableBeatmapInfo; - /** - * Beatmap missing attributes. - */ - attributes: IBeatmapAttributes; - /** - * Beatmap skill data. - */ - skills: IBeatmapSkill[] | null; - /** - * Difficulty attributes of calculated beatmap. - */ - difficulty: DifficultyAttributes; - /** - * List of performance attributes of calculated beatmap. - */ - performance: PerformanceAttributes[]; -} - -/** - * Calculated score. - */ -interface ICalculatedScore { - /** - * Score information. - */ - scoreInfo: IJsonableScoreInfo; - /** - * Difficulty attributes of calculated beatmap. - */ - difficulty: DifficultyAttributes; - /** - * List of performance attributes of calculated beatmap. - */ - performance: PerformanceAttributes; - /** - * Replay life bar. - */ - lifeBar?: ILifeBarFrame[]; -} - -/** - * Options for score calculation. - */ -interface IScoreCalculationOptions extends IScoreParsingOptions, Partial { - /** - * Beatmap ID of this score. - */ - beatmapId?: number; - /** - * Custom beatmap file URL of this score. - */ - fileURL?: string; - /** - * Ruleset ID. - */ - rulesetId?: GameMode; - /** - * Custom ruleset instance. - */ - ruleset?: IRuleset; - /** - * Mod combination or bitwise. - */ - mods?: string | number; - /** - * Precalculated difficulty attributes. - */ - difficulty?: IDifficultyAttributes; - /** - * Target score. - */ - scoreInfo?: IScoreInfo | IJsonableScoreInfo; - /** - * Should this score be unchoked or not? - */ - fix?: boolean; -} - -/** - * A beatmap calculator. - */ -declare class BeatmapCalculator { - /** - * Instance of a score simulator. - */ - private _scoreSimulator; - /** - * Calculates difficulty and performance of a beatmap. - * @param options Beatmap calculation options. - * @returns Calculated beatmap. - */ - calculate(options: IBeatmapCalculationOptions): Promise; - /** - * This is the special case in which all precalculated stuff is present. - * @param options Beatmap calculation options. - * @returns Calculated beatmap. - */ - private _processPrecalculated; - /** - * Tests these beatmap calculation options for the possibility of skipping beatmap parsing. - * @param options Beatmap calculation options. - * @returns If these options enough to skip beatmap parsing. - */ - private _checkPrecalculated; - /** - * Transforms skill data to get strain peaks. - * @param calculator Extended difficulty calculator. - * @returns Skill output data. - */ - private _getSkillsOutput; - /** - * Simulates custom scores by accuracy or total score values. - * @param attributes Beatmap attributes. - * @param options Beatmap calculation options. - * @returns Simulated scores. - */ - private _simulateScores; - /** - * Simulates custom scores by accuracy values. - * @param attributes Beatmap attributes. - * @param accuracy Accuracy values. - * @returns Simulated scores. - */ - private _simulateOtherScores; - /** - * Simulates custom osu!mania scores by total score values. - * @param attributes Beatmap attributes. - * @param totalScores Total score values. - * @returns Simulated osu!mania scores. - */ - private _simulateManiaScores; -} - -/** - * A score calculator. - */ -declare class ScoreCalculator { - /** - * Instance of a score simulator. - */ - private _scoreSimulator; - /** - * Calculates difficulty and performance of a score. - * @param options Score calculation options. - * @returns Calculated score. - */ - calculate(options: IScoreCalculationOptions): Promise; - private _createScore; - private _parseOrSimulateScore; -} - -export { BeatmapCalculator, GameMode, IBeatmapAttributes, IBeatmapCalculationOptions, IBeatmapParsingOptions, IBeatmapSkill, ICalculatedBeatmap, ICalculatedScore, IDifficultyAttributes, IDifficultyCalculationOptions, IExtendedDifficultyCalculator, IPerformanceCalculationOptions, IScoreCalculationOptions, IScoreParsingOptions, IScoreSimulationOptions, ScoreCalculator, ScoreSimulator, calculateAccuracy, calculateDifficulty, calculatePerformance, calculateRank, calculateTotalHits, createBeatmapAttributes, createBeatmapInfo, createDifficultyCalculator, downloadFile, generateHitStatistics, getRulesetById, getRulesetIdByName, getValidHitStatistics, parseBeatmap, parseScore, scaleTotalScore, toCombination, toDifficultyAttributes, toDifficultyMods, toScoreInfo }; diff --git a/lib/index.mjs b/lib/index.mjs deleted file mode 100644 index be32d6b..0000000 --- a/lib/index.mjs +++ /dev/null @@ -1,1127 +0,0 @@ -import { BeatmapInfo, HitType, Beatmap, ScoreInfo, MathUtils, ModType, ScoreRank, StrainSkill, Score } from 'osu-classes'; -import { StandardDifficultyCalculator, StandardRuleset, StandardDifficultyAttributes } from 'osu-standard-stable'; -import { TaikoDifficultyCalculator, TaikoRuleset, TaikoDifficultyAttributes } from 'osu-taiko-stable'; -import { CatchDifficultyCalculator, JuiceDroplet, JuiceFruit, JuiceTinyDroplet, CatchRuleset, CatchDifficultyAttributes } from 'osu-catch-stable'; -import { ManiaDifficultyCalculator, ManiaRuleset, ManiaDifficultyAttributes } from 'osu-mania-stable'; -import { readFileSync } from 'fs'; -import { Downloader, DownloadEntry, DownloadType } from 'osu-downloader'; -import { BeatmapDecoder, ScoreDecoder } from 'osu-parsers'; - -function calculateDifficulty(options) { - const { beatmap, ruleset, mods } = options; - - if (!beatmap || !ruleset) { - throw new Error('Cannot calculate difficulty attributes'); - } - - const calculator = options.calculator - ?? ruleset.createDifficultyCalculator(beatmap); - - if (typeof mods !== 'string' && typeof mods !== 'number') { - return calculator.calculateAt(options.totalHits); - } - - const combination = ruleset.createModCombination(mods); - - return calculator.calculateWithModsAt(combination, options.totalHits); -} - -function calculatePerformance(options) { - const { difficulty, scoreInfo, ruleset } = options; - - if (!difficulty || !scoreInfo || !ruleset) { - throw new Error('Cannot calculate performance attributes'); - } - - const castedDifficulty = difficulty; - const calculator = ruleset.createPerformanceCalculator(castedDifficulty, scoreInfo); - - return calculator.calculateAttributes(); -} - -let GameMode; - -(function(GameMode) { - GameMode[GameMode['Osu'] = 0] = 'Osu'; - GameMode[GameMode['Taiko'] = 1] = 'Taiko'; - GameMode[GameMode['Fruits'] = 2] = 'Fruits'; - GameMode[GameMode['Mania'] = 3] = 'Mania'; -})(GameMode || (GameMode = {})); - -class ExtendedStandardDifficultyCalculator extends StandardDifficultyCalculator { - constructor() { - super(...arguments); - this._skills = []; - } - getSkills() { - return this._skills; - } - _createDifficultyAttributes(beatmap, mods, skills) { - this._skills = skills; - - return super._createDifficultyAttributes(beatmap, mods, skills); - } -} - -class ExtendedTaikoDifficultyCalculator extends TaikoDifficultyCalculator { - constructor() { - super(...arguments); - this._skills = []; - } - getSkills() { - return this._skills; - } - _createDifficultyAttributes(beatmap, mods, skills) { - this._skills = skills; - - return super._createDifficultyAttributes(beatmap, mods, skills); - } -} - -class ExtendedCatchDifficultyCalculator extends CatchDifficultyCalculator { - constructor() { - super(...arguments); - this._skills = []; - } - getSkills() { - return this._skills; - } - _createDifficultyAttributes(beatmap, mods, skills) { - this._skills = skills; - - return super._createDifficultyAttributes(beatmap, mods, skills); - } -} - -class ExtendedManiaDifficultyCalculator extends ManiaDifficultyCalculator { - constructor() { - super(...arguments); - this._skills = []; - } - getSkills() { - return this._skills; - } - _createDifficultyAttributes(beatmap, mods, skills) { - this._skills = skills; - - return super._createDifficultyAttributes(beatmap, mods, skills); - } -} - -function createDifficultyCalculator(beatmap, ruleset) { - switch (ruleset.id) { - case GameMode.Osu: - return new ExtendedStandardDifficultyCalculator(beatmap, ruleset); - case GameMode.Taiko: - return new ExtendedTaikoDifficultyCalculator(beatmap, ruleset); - case GameMode.Fruits: - return new ExtendedCatchDifficultyCalculator(beatmap, ruleset); - case GameMode.Mania: - return new ExtendedManiaDifficultyCalculator(beatmap, ruleset); - } - - throw new Error('This ruleset does not support strain output!'); -} - -function createBeatmapInfo(beatmap, hash) { - return new BeatmapInfo({ - id: beatmap?.metadata.beatmapId, - beatmapsetId: beatmap?.metadata.beatmapSetId, - creator: beatmap?.metadata.creator, - title: beatmap?.metadata.title, - artist: beatmap?.metadata.artist, - version: beatmap?.metadata.version, - hittable: countObjects(HitType.Normal, beatmap), - slidable: countObjects(HitType.Slider, beatmap), - spinnable: countObjects(HitType.Spinner, beatmap), - holdable: countObjects(HitType.Hold, beatmap), - length: (beatmap?.length ?? 0) / 1000, - bpmMin: beatmap?.bpmMin, - bpmMax: beatmap?.bpmMax, - bpmMode: beatmap?.bpmMode, - circleSize: beatmap?.difficulty.circleSize, - approachRate: beatmap?.difficulty.approachRate, - overallDifficulty: beatmap?.difficulty.overallDifficulty, - drainRate: beatmap?.difficulty.drainRate, - rulesetId: beatmap?.mode, - mods: getMods(beatmap), - maxCombo: getMaxCombo(beatmap), - isConvert: beatmap?.originalMode !== beatmap?.mode, - md5: hash ?? '', - }); -} - -function createBeatmapAttributes(beatmap) { - const hittable = countObjects(HitType.Normal, beatmap); - const maxTinyDroplets = countTinyDroplets(beatmap); - const maxDroplets = countDroplets(beatmap) - maxTinyDroplets; - const maxFruits = countFruits(beatmap) + hittable; - const totalHits = beatmap?.mode === GameMode.Fruits - ? maxFruits + maxDroplets + maxTinyDroplets - : getTotalHits(beatmap); - - return { - beatmapId: beatmap?.metadata.beatmapId, - rulesetId: beatmap?.mode, - mods: getMods(beatmap)?.toString() ?? 'NM', - maxCombo: getMaxCombo(beatmap), - totalHits, - maxFruits, - maxDroplets, - maxTinyDroplets, - }; -} - -function countObjects(hitType, beatmap) { - if (!beatmap) { - return 0; - } - - return beatmap.hitObjects.reduce((sum, obj) => { - return sum + (obj.hitType & hitType ? 1 : 0); - }, 0); -} - -function countFruits(beatmap) { - return countNested(JuiceFruit, beatmap); -} - -function countDroplets(beatmap) { - return countNested(JuiceDroplet, beatmap); -} - -function countTinyDroplets(beatmap) { - return countNested(JuiceTinyDroplet, beatmap); -} - -function countNested(Class, beatmap) { - const rulesetBeatmap = beatmap; - - return rulesetBeatmap.hitObjects.reduce((sum, obj) => { - const nestedSum = obj.nestedHitObjects?.reduce((sum, obj) => { - return sum + (obj instanceof Class ? 1 : 0); - }, 0); - - return sum + (nestedSum ?? 0); - }, 0); -} - -function getTotalHits(beatmap) { - if (!beatmap) { - return 0; - } - - switch (beatmap.mode) { - case GameMode.Osu: { - const circles = countObjects(HitType.Normal, beatmap); - const sliders = countObjects(HitType.Slider, beatmap); - const spinners = countObjects(HitType.Spinner, beatmap); - - return circles + sliders + spinners; - } - case GameMode.Taiko: { - return countObjects(HitType.Normal, beatmap); - } - case GameMode.Fruits: { - const hittable = countObjects(HitType.Normal, beatmap); - const tinyDroplets = countTinyDroplets(beatmap); - const droplets = countDroplets(beatmap) - tinyDroplets; - const fruits = countFruits(beatmap) + hittable; - - return fruits + droplets + tinyDroplets; - } - case GameMode.Mania: { - const notes = countObjects(HitType.Normal, beatmap); - const holds = countObjects(HitType.Hold, beatmap); - - return notes + holds; - } - } - - const hittable = countObjects(HitType.Normal, beatmap); - const slidable = countObjects(HitType.Slider, beatmap); - const spinnable = countObjects(HitType.Spinner, beatmap); - const holdable = countObjects(HitType.Hold, beatmap); - - return hittable + slidable + spinnable + holdable; -} - -function getMaxCombo(beatmap) { - return beatmap?.maxCombo ?? 0; -} - -function getMods(beatmap) { - return beatmap?.mods ?? null; -} - -function getRulesetIdByName(rulesetName) { - switch (rulesetName?.toLowerCase()) { - case 'standard': - case 'std': - case 'osu': return GameMode.Osu; - case 'taiko': return GameMode.Taiko; - case 'ctb': - case 'catch': - case 'fruits': return GameMode.Fruits; - case 'mania': return GameMode.Mania; - } - - throw new Error('Unknown ruleset!'); -} - -function getRulesetById(rulesetId) { - switch (rulesetId) { - case GameMode.Osu: return new StandardRuleset(); - case GameMode.Taiko: return new TaikoRuleset(); - case GameMode.Fruits: return new CatchRuleset(); - case GameMode.Mania: return new ManiaRuleset(); - } - - throw new Error('Unknown ruleset!'); -} - -function toDifficultyMods(mods, rulesetId) { - const ruleset = getRulesetById(rulesetId ?? GameMode.Osu); - const difficultyCalculator = ruleset.createDifficultyCalculator(new Beatmap()); - const difficultyMods = difficultyCalculator.difficultyMods; - const combination = ruleset.createModCombination(mods); - const difficultyBitwise = combination.all.reduce((bitwise, mod) => { - const found = difficultyMods.find((m) => { - if (m.bitwise === mod.bitwise) { - return true; - } - - return m.acronym === 'DT' && mod.acronym === 'NC'; - }); - - return bitwise + (found?.bitwise ?? 0); - }, 0); - - return ruleset.createModCombination(difficultyBitwise); -} - -function toCombination(input, rulesetId) { - const ruleset = getRulesetById(rulesetId ?? GameMode.Osu); - - return ruleset.createModCombination(input); -} - -function toDifficultyAttributes(difficulty, rulesetId) { - const attributes = createAttributes(rulesetId, difficulty?.mods); - - if (typeof difficulty !== 'object') { - return attributes; - } - - for (const key in difficulty) { - if (key in attributes) { - attributes[key] = difficulty[key]; - } - } - - return attributes; -} - -function toScoreInfo(data) { - const scoreInfo = new ScoreInfo(); - - if (data?.toJSON) { - return scoreInfo; - } - - const jsonable = data; - - scoreInfo.id = jsonable?.id; - scoreInfo.totalScore = jsonable?.totalScore; - scoreInfo.pp = jsonable?.pp; - scoreInfo.maxCombo = jsonable?.maxCombo; - scoreInfo.passed = jsonable?.passed; - scoreInfo.perfect = jsonable?.perfect; - scoreInfo.rank = jsonable?.rank; - scoreInfo.accuracy = jsonable?.accuracy; - scoreInfo.username = jsonable?.username; - scoreInfo.userId = jsonable?.userId; - scoreInfo.beatmapId = jsonable?.beatmapId; - scoreInfo.date = jsonable?.date; - scoreInfo.beatmapHashMD5 = jsonable?.beatmapHashMD5; - scoreInfo.rulesetId = jsonable?.rulesetId; - scoreInfo.mods = toCombination(jsonable.mods, jsonable.rulesetId); - scoreInfo.countGeki = jsonable?.countGeki; - scoreInfo.count300 = jsonable?.count300; - scoreInfo.countKatu = jsonable?.countKatu; - scoreInfo.count100 = jsonable?.count100; - scoreInfo.count50 = jsonable?.count50; - scoreInfo.countMiss = jsonable?.countMiss; - - return scoreInfo; -} - -function createAttributes(rulesetId, mods) { - const ruleset = getRulesetById(rulesetId ?? GameMode.Osu); - const combination = ruleset.createModCombination(mods); - - switch (ruleset.id) { - case GameMode.Taiko: return new TaikoDifficultyAttributes(combination, 0); - case GameMode.Fruits: return new CatchDifficultyAttributes(combination, 0); - case GameMode.Mania: return new ManiaDifficultyAttributes(combination, 0); - } - - return new StandardDifficultyAttributes(combination, 0); -} - -async function downloadFile(path, options) { - const downloader = new Downloader({ rootPath: path }); - const entry = new DownloadEntry(options); - - downloader.addSingleEntry(entry); - - return downloader.downloadSingle(); -} - -function generateHitStatistics(attributes, accuracy = 1, countMiss = 0, count50, count100) { - if (accuracy > 1) { - accuracy /= 100; - } - - switch (attributes.rulesetId) { - case GameMode.Taiko: - return generateTaikoHitStatistics(attributes, accuracy, countMiss, count100); - case GameMode.Fruits: - return generateCatchHitStatistics(attributes, accuracy, countMiss, count50, count100); - case GameMode.Mania: - return generateManiaHitStatistics(attributes); - } - - return generateOsuHitStatistics(attributes, accuracy, countMiss, count50, count100); -} - -function generateOsuHitStatistics(attributes, accuracy = 1, countMiss = 0, count50, count100) { - const totalHits = attributes.totalHits ?? 0; - - countMiss = MathUtils.clamp(countMiss, 0, totalHits); - count50 = count50 ? MathUtils.clamp(count50, 0, totalHits - countMiss) : 0; - - if (typeof count100 !== 'number') { - count100 = Math.round((totalHits - totalHits * accuracy) * 1.5); - } - else { - count100 = MathUtils.clamp(count100, 0, totalHits - count50 - countMiss); - } - - const count300 = totalHits - count100 - count50 - countMiss; - - return { - great: count300, - ok: count100, - meh: count50 ?? 0, - miss: countMiss, - }; -} - -function generateTaikoHitStatistics(attributes, accuracy = 1, countMiss = 0, count100) { - const totalHits = attributes.totalHits ?? 0; - - countMiss = MathUtils.clamp(countMiss, 0, totalHits); - - let count300; - - if (typeof count100 !== 'number') { - const targetTotal = Math.round(accuracy * totalHits * 2); - - count300 = targetTotal - (totalHits - countMiss); - count100 = totalHits - count300 - countMiss; - } - else { - count100 = MathUtils.clamp(count100, 0, totalHits - countMiss); - count300 = totalHits - count100 - countMiss; - } - - return { - great: count300, - ok: count100, - miss: countMiss, - }; -} - -function generateCatchHitStatistics(attributes, accuracy = 1, countMiss = 0, count50, count100) { - const maxCombo = attributes.maxCombo ?? 0; - const maxFruits = attributes.maxFruits ?? 0; - const maxDroplets = attributes.maxDroplets ?? 0; - const maxTinyDroplets = attributes.maxTinyDroplets ?? 0; - - if (typeof count100 === 'number') { - countMiss += maxDroplets - count100; - } - - countMiss = MathUtils.clamp(countMiss, 0, maxDroplets + maxFruits); - - let droplets = count100 ?? Math.max(0, maxDroplets - countMiss); - - droplets = MathUtils.clamp(droplets, 0, maxDroplets); - - const fruits = maxFruits - (countMiss - (maxDroplets - droplets)); - let tinyDroplets = Math.round(accuracy * (maxCombo + maxTinyDroplets)); - - tinyDroplets = count50 ?? tinyDroplets - fruits - droplets; - - const tinyMisses = maxTinyDroplets - tinyDroplets; - - return { - great: MathUtils.clamp(fruits, 0, maxFruits), - largeTickHit: MathUtils.clamp(droplets, 0, maxDroplets), - smallTickHit: tinyDroplets, - smallTickMiss: tinyMisses, - miss: countMiss, - }; -} - -function generateManiaHitStatistics(attributes) { - return { - perfect: attributes.totalHits ?? 0, - }; -} - -function getValidHitStatistics(original) { - return { - perfect: original?.perfect ?? 0, - great: original?.great ?? 0, - good: original?.good ?? 0, - ok: original?.ok ?? 0, - meh: original?.meh ?? 0, - largeTickHit: original?.largeTickHit ?? 0, - smallTickMiss: original?.smallTickMiss ?? 0, - smallTickHit: original?.smallTickHit ?? 0, - miss: original?.miss ?? 0, - largeBonus: 0, - largeTickMiss: 0, - smallBonus: 0, - ignoreHit: 0, - ignoreMiss: 0, - none: 0, - }; -} - -function calculateAccuracy(scoreInfo) { - const geki = scoreInfo.countGeki; - const katu = scoreInfo.countKatu; - const c300 = scoreInfo.count300; - const c100 = scoreInfo.count100; - const c50 = scoreInfo.count50; - const total = scoreInfo.totalHits || calculateTotalHits(scoreInfo); - - if (total <= 0) { - return 1; - } - - switch (scoreInfo.rulesetId) { - case GameMode.Osu: - return Math.max(0, (c50 / 6 + c100 / 3 + c300) / total); - case GameMode.Taiko: - return Math.max(0, (c100 / 2 + c300) / total); - case GameMode.Fruits: - return Math.max(0, (c50 + c100 + c300) / total); - case GameMode.Mania: - return Math.max(0, (c50 / 6 + c100 / 3 + katu / 1.5 + (c300 + geki)) / total); - } - - return 1; -} - -function calculateTotalHits(scoreInfo) { - const geki = scoreInfo.countGeki; - const katu = scoreInfo.countKatu; - const c300 = scoreInfo.count300; - const c100 = scoreInfo.count100; - const c50 = scoreInfo.count50; - const misses = scoreInfo.countMiss; - - switch (scoreInfo.rulesetId) { - case GameMode.Osu: - return c300 + c100 + c50 + misses; - case GameMode.Taiko: - return c300 + c100 + c50 + misses; - case GameMode.Fruits: - return c300 + c100 + c50 + misses + katu; - case GameMode.Mania: - return c300 + c100 + c50 + misses + geki + katu; - } - - return c300 + c100 + c50 + misses + geki + katu; -} - -function scaleTotalScore(totalScore, mods) { - const difficultyReduction = mods?.all - .filter((m) => m.type === ModType.DifficultyReduction) ?? []; - - return difficultyReduction - .reduce((score, mod) => score * mod.multiplier, totalScore); -} - -function calculateRank(scoreInfo) { - if (!scoreInfo.passed) { - return ScoreRank.F; - } - - switch (scoreInfo.rulesetId) { - case GameMode.Osu: return calculateOsuRank(scoreInfo); - case GameMode.Taiko: return calculateTaikoRank(scoreInfo); - case GameMode.Fruits: return calculateCatchRank(scoreInfo); - case GameMode.Mania: return calculateManiaRank(scoreInfo); - } - - return ScoreRank.F; -} - -function calculateOsuRank(scoreInfo) { - const hasFL = scoreInfo.mods?.has('FL') ?? false; - const hasHD = scoreInfo.mods?.has('HD') ?? false; - const ratio300 = Math.fround(scoreInfo.count300 / scoreInfo.totalHits); - const ratio50 = Math.fround(scoreInfo.count50 / scoreInfo.totalHits); - - if (ratio300 === 1) { - return hasHD || hasFL ? ScoreRank.XH : ScoreRank.X; - } - - if (ratio300 > 0.9 && ratio50 <= 0.01 && scoreInfo.countMiss === 0) { - return hasHD || hasFL ? ScoreRank.SH : ScoreRank.S; - } - - if ((ratio300 > 0.8 && scoreInfo.countMiss === 0) || ratio300 > 0.9) { - return ScoreRank.A; - } - - if ((ratio300 > 0.7 && scoreInfo.countMiss === 0) || ratio300 > 0.8) { - return ScoreRank.B; - } - - return ratio300 > 0.6 ? ScoreRank.C : ScoreRank.D; -} - -function calculateTaikoRank(scoreInfo) { - const hasFL = scoreInfo.mods?.has('FL') ?? false; - const hasHD = scoreInfo.mods?.has('HD') ?? false; - const ratio300 = Math.fround(scoreInfo.count300 / scoreInfo.totalHits); - const ratio50 = Math.fround(scoreInfo.count50 / scoreInfo.totalHits); - - if (ratio300 === 1) { - return hasHD || hasFL ? ScoreRank.XH : ScoreRank.X; - } - - if (ratio300 > 0.9 && ratio50 <= 0.01 && scoreInfo.countMiss === 0) { - return hasHD || hasFL ? ScoreRank.SH : ScoreRank.S; - } - - if ((ratio300 > 0.8 && scoreInfo.countMiss === 0) || ratio300 > 0.9) { - return ScoreRank.A; - } - - if ((ratio300 > 0.7 && scoreInfo.countMiss === 0) || ratio300 > 0.8) { - return ScoreRank.B; - } - - return ratio300 > 0.6 ? ScoreRank.C : ScoreRank.D; -} - -function calculateCatchRank(scoreInfo) { - const hasFL = scoreInfo.mods?.has('FL') ?? false; - const hasHD = scoreInfo.mods?.has('HD') ?? false; - const accuracy = scoreInfo.accuracy; - - if (accuracy === 1) { - return hasHD || hasFL ? ScoreRank.XH : ScoreRank.X; - } - - if (accuracy > 0.98) { - return hasHD || hasFL ? ScoreRank.SH : ScoreRank.S; - } - - if (accuracy > 0.94) { - return ScoreRank.A; - } - - if (accuracy > 0.90) { - return ScoreRank.B; - } - - if (accuracy > 0.85) { - return ScoreRank.C; - } - - return ScoreRank.D; -} - -function calculateManiaRank(scoreInfo) { - const hasFL = scoreInfo.mods?.has('FL') ?? false; - const hasHD = scoreInfo.mods?.has('HD') ?? false; - const accuracy = scoreInfo.accuracy; - - if (accuracy === 1) { - return hasHD || hasFL ? ScoreRank.XH : ScoreRank.X; - } - - if (accuracy > 0.95) { - return hasHD || hasFL ? ScoreRank.SH : ScoreRank.S; - } - - if (accuracy > 0.9) { - return ScoreRank.A; - } - - if (accuracy > 0.8) { - return ScoreRank.B; - } - - if (accuracy > 0.7) { - return ScoreRank.C; - } - - return ScoreRank.D; -} - -async function parseBeatmap(options) { - const { beatmapId, fileURL, hash, savePath } = options; - - if (typeof beatmapId === 'string' || typeof beatmapId === 'number') { - return parseBeatmapById(beatmapId, hash, savePath); - } - - if (typeof fileURL === 'string') { - return parseCustomBeatmap(fileURL, hash, savePath); - } - - throw new Error('No beatmap ID or beatmap URL was specified!'); -} - -async function parseBeatmapById(id, hash, savePath) { - let _a; - const result = await downloadFile(savePath, { - save: typeof savePath === 'string', - id, - }); - - if (!result.isSuccessful || (!savePath && !result.buffer)) { - throw new Error(`Beatmap with ID "${id}" failed to download: "${result.statusText}"`); - } - - if (hash && hash !== result.md5) { - throw new Error('Beatmap MD5 hash missmatch!'); - } - - const data = savePath - ? readFileSync(result.filePath) - : result.buffer; - const parsed = parseBeatmapData(data); - - (_a = parsed.metadata).beatmapId || (_a.beatmapId = parseInt(id)); - - return { - hash: result.md5, - data: parsed, - }; -} - -async function parseCustomBeatmap(url, hash, savePath) { - const result = await downloadFile(savePath, { - save: typeof savePath === 'string', - url, - }); - - if (!result.isSuccessful || (!savePath && !result.buffer)) { - throw new Error(`Beatmap from "${url}" failed to download: ${result.statusText}`); - } - - if (hash && hash !== result.md5) { - throw new Error('Beatmap MD5 hash missmatch!'); - } - - const data = savePath - ? readFileSync(result.filePath) - : result.buffer; - - return { - data: parseBeatmapData(data), - hash: result.md5, - }; -} - -function parseBeatmapData(data) { - const stringified = data.toString(); - const decoder = new BeatmapDecoder(); - const parseStoryboard = false; - - return decoder.decodeFromString(stringified, parseStoryboard); -} - -async function parseScore(options) { - const { replayURL, hash, lifeBar } = options; - - if (typeof replayURL === 'string') { - return parseCustomScore(replayURL, hash, lifeBar); - } - - throw new Error('No replay URL was specified!'); -} - -async function parseCustomScore(url, hash, parseReplay = false) { - const result = await downloadFile('', { - type: DownloadType.Replay, - save: false, - url, - }); - - if (!result.isSuccessful || !result.buffer) { - throw new Error('Replay failed to download!'); - } - - if (hash && hash !== result.md5) { - throw new Error('Replay MD5 hash missmatch!'); - } - - return { - data: await parseScoreData(result.buffer, parseReplay), - hash: result.md5, - }; -} - -async function parseScoreData(data, parseReplay = false) { - return await new ScoreDecoder().decodeFromBuffer(data, parseReplay); -} - -class ScoreSimulator { - async completeReplay(score, attributes) { - const scoreInfo = score.info; - const beatmapCombo = attributes.maxCombo ?? 0; - - return this._generateScoreInfo({ - ...scoreInfo, - beatmapId: attributes.beatmapId, - rulesetId: attributes.rulesetId, - totalHits: attributes.totalHits, - mods: toCombination(attributes.mods, attributes.rulesetId), - perfect: scoreInfo.maxCombo >= beatmapCombo, - }); - } - simulate(options) { - const statistics = generateHitStatistics(options.attributes, options.accuracy, options.countMiss, options.count50, options.count100); - const attributes = options.attributes; - const beatmapCombo = attributes.maxCombo ?? 0; - const percentage = options.percentCombo ?? 100; - const multiplier = MathUtils.clamp(percentage, 0, 100) / 100; - const scoreCombo = options.maxCombo ?? Math.round(beatmapCombo * multiplier); - const misses = statistics.miss ?? 0; - const limitedCombo = Math.min(scoreCombo, beatmapCombo - misses); - const maxCombo = Math.max(0, limitedCombo); - - return this._generateScoreInfo({ - beatmapId: attributes.beatmapId, - rulesetId: attributes.rulesetId, - totalHits: attributes.totalHits, - mods: toCombination(attributes.mods, attributes.rulesetId), - totalScore: options.totalScore, - perfect: maxCombo >= beatmapCombo, - statistics, - maxCombo, - }); - } - simulateFC(scoreInfo, attributes) { - if (scoreInfo.rulesetId === GameMode.Mania) { - return this.simulateMax(attributes); - } - - const statistics = getValidHitStatistics(scoreInfo.statistics); - const totalHits = attributes.totalHits ?? 0; - - switch (scoreInfo.rulesetId) { - case GameMode.Fruits: - statistics.great = totalHits - statistics.largeTickHit - - statistics.smallTickHit - statistics.smallTickMiss - statistics.miss; - - statistics.largeTickHit += statistics.miss; - break; - case GameMode.Mania: - statistics.perfect = totalHits - statistics.great - - statistics.good - statistics.ok - statistics.meh; - - break; - default: - statistics.great = totalHits - statistics.ok - statistics.meh; - } - - statistics.miss = 0; - - return this._generateScoreInfo({ - ...scoreInfo, - mods: scoreInfo.mods ?? toCombination(attributes.mods, attributes.rulesetId), - beatmapId: attributes.beatmapId, - rulesetId: attributes.rulesetId, - maxCombo: attributes.maxCombo, - perfect: true, - statistics, - totalHits, - }); - } - simulateMax(attributes) { - const statistics = generateHitStatistics(attributes); - const totalHits = attributes.totalHits ?? 0; - const score = this._generateScoreInfo({ - beatmapId: attributes.beatmapId, - rulesetId: attributes.rulesetId, - maxCombo: attributes.maxCombo, - mods: toCombination(attributes.mods, attributes.rulesetId), - perfect: true, - statistics, - totalHits, - }); - - if (attributes.rulesetId === GameMode.Mania) { - score.totalScore = 1e6; - } - - return score; - } - _generateScoreInfo(options) { - const scoreInfo = new ScoreInfo({ - id: options?.id, - beatmapId: options?.beatmapId, - userId: options?.userId, - username: options?.username ?? 'osu!', - maxCombo: options?.maxCombo, - statistics: getValidHitStatistics(options?.statistics), - rawMods: options?.rawMods, - rulesetId: options?.rulesetId, - perfect: options?.perfect, - beatmapHashMD5: options?.beatmapHashMD5, - date: options?.date, - pp: options?.pp, - }); - - if (options?.mods) { - scoreInfo.mods = options.mods; - } - - if (scoreInfo.rulesetId === GameMode.Mania) { - scoreInfo.totalScore = options?.totalScore || scaleTotalScore(1e6, scoreInfo.mods); - } - - scoreInfo.passed = scoreInfo.totalHits >= (options?.totalHits ?? 0); - scoreInfo.accuracy = options.accuracy || calculateAccuracy(scoreInfo); - scoreInfo.rank = ScoreRank[calculateRank(scoreInfo)]; - - return scoreInfo; - } -} - -class BeatmapCalculator { - constructor() { - this._scoreSimulator = new ScoreSimulator(); - } - async calculate(options) { - if (this._checkPrecalculated(options)) { - return this._processPrecalculated(options); - } - - const { data: parsed, hash: beatmapMD5 } = await parseBeatmap(options); - const ruleset = options.ruleset ?? getRulesetById(options.rulesetId ?? parsed.mode); - const combination = ruleset.createModCombination(options.mods); - const beatmap = ruleset.applyToBeatmapWithMods(parsed, combination); - const beatmapInfo = options.beatmapInfo ?? createBeatmapInfo(beatmap, beatmapMD5); - const attributes = options.attributes ?? createBeatmapAttributes(beatmap); - const totalHits = options.totalHits; - const calculator = createDifficultyCalculator(beatmap, ruleset); - const difficulty = options.difficulty && !options.strains - ? toDifficultyAttributes(options.difficulty, ruleset.id) - : calculateDifficulty({ beatmap, ruleset, calculator, totalHits }); - const skills = options.strains ? this._getSkillsOutput(calculator) : null; - const scores = this._simulateScores(attributes, options); - const performance = scores.map((scoreInfo) => calculatePerformance({ - difficulty, - ruleset, - scoreInfo, - })); - - return { - beatmapInfo: beatmapInfo?.toJSON() ?? beatmapInfo, - attributes, - skills, - difficulty, - performance, - }; - } - _processPrecalculated(options) { - const { beatmapInfo, attributes } = options; - const ruleset = options.ruleset ?? getRulesetById(attributes.rulesetId); - const difficulty = toDifficultyAttributes(options.difficulty, ruleset.id); - const scores = this._simulateScores(attributes, options); - const performance = scores.map((scoreInfo) => calculatePerformance({ - difficulty, - ruleset, - scoreInfo, - })); - - return { - beatmapInfo: beatmapInfo?.toJSON() ?? beatmapInfo, - skills: null, - attributes, - difficulty, - performance, - }; - } - _checkPrecalculated(options) { - const isValid = !!options.beatmapInfo - && !!options.attributes - && !!options.difficulty - && !options.strains; - - if (options.attributes && typeof options.totalHits === 'number') { - return isValid && options.attributes.totalHits === options.totalHits; - } - - return isValid; - } - _getSkillsOutput(calculator) { - const skills = calculator.getSkills(); - const strainSkills = skills.filter((s) => s instanceof StrainSkill); - const output = strainSkills.map((skill) => { - return { - title: skill.constructor.name, - strainPeaks: [...skill.getCurrentStrainPeaks()], - }; - }); - - if (output[0]?.title === 'Aim' && output[1]?.title === 'Aim') { - output[1].title = 'Aim (No Sliders)'; - } - - if (output[2]?.title === 'Stamina' && output[3]?.title === 'Stamina') { - output[2].title = 'Stamina (Left)'; - output[3].title = 'Stamina (Right)'; - } - - return output; - } - _simulateScores(attributes, options) { - return attributes.rulesetId === GameMode.Mania - ? this._simulateManiaScores(attributes, options.totalScores) - : this._simulateOtherScores(attributes, options.accuracy); - } - _simulateOtherScores(attributes, accuracy) { - accuracy ?? (accuracy = [95, 99, 100]); - - return accuracy.map((accuracy) => this._scoreSimulator.simulate({ - attributes, - accuracy, - })); - } - _simulateManiaScores(attributes, totalScores) { - const mods = toCombination(attributes.mods, attributes.rulesetId); - - totalScores ?? (totalScores = [ - scaleTotalScore(8e5, mods), - scaleTotalScore(9e5, mods), - scaleTotalScore(1e6, mods), - ]); - - return totalScores.map((totalScore) => this._scoreSimulator.simulate({ - attributes, - totalScore, - })); - } -} - -class ScoreCalculator { - constructor() { - this._scoreSimulator = new ScoreSimulator(); - } - async calculate(options) { - let attributes = options.attributes; - let beatmapMD5 = options.hash ?? options.attributes?.hash; - let rulesetId = options.rulesetId ?? options.attributes?.rulesetId; - let ruleset = options.ruleset ?? getRulesetById(rulesetId); - let difficulty = options.difficulty && ruleset - ? toDifficultyAttributes(options.difficulty, ruleset.id) - : null; - let score = attributes ? await this._createScore(options, attributes) : null; - const beatmapTotalHits = attributes?.totalHits ?? 0; - const scoreTotalHits = score?.info.totalHits ?? 0; - const isPartialDifficulty = beatmapTotalHits > scoreTotalHits; - - if (!attributes || !beatmapMD5 || !ruleset || !score || !difficulty || (isPartialDifficulty && !options.fix)) { - const { data, hash } = await parseBeatmap(options); - - beatmapMD5 ?? (beatmapMD5 = hash); - rulesetId ?? (rulesetId = data.mode); - ruleset ?? (ruleset = getRulesetById(rulesetId)); - - const combination = ruleset.createModCombination(options.mods); - const beatmap = ruleset.applyToBeatmapWithMods(data, combination); - - attributes ?? (attributes = createBeatmapAttributes(beatmap)); - score ?? (score = await this._createScore(options, attributes)); - - if (!difficulty || isPartialDifficulty) { - difficulty = calculateDifficulty({ - totalHits: scoreTotalHits, - beatmap, - ruleset, - }); - } - } - - const scoreBeatmapMD5 = score.info.beatmapHashMD5; - - if (beatmapMD5 && scoreBeatmapMD5 && beatmapMD5 !== scoreBeatmapMD5) { - throw new Error('Beatmap & replay missmatch!'); - } - - if (beatmapMD5 && !scoreBeatmapMD5) { - score.info.beatmapHashMD5 = beatmapMD5; - } - - const performance = calculatePerformance({ - scoreInfo: score.info, - difficulty, - ruleset, - }); - - return { - scoreInfo: score.info.toJSON(), - lifeBar: score.replay?.lifeBar, - difficulty, - performance, - }; - } - async _createScore(options, attributes) { - const score = await this._parseOrSimulateScore(options, attributes); - - if (options.fix) { - score.info = this._scoreSimulator.simulateFC(score.info, attributes); - } - - return score; - } - async _parseOrSimulateScore(options, attributes) { - const { scoreInfo, replayURL } = options; - - if (scoreInfo) { - const info = toScoreInfo(scoreInfo); - const replay = null; - - return new Score(info, replay); - } - - if (!replayURL) { - const info = this._scoreSimulator.simulate({ ...options, attributes }); - const replay = null; - - return new Score(info, replay); - } - - const { data: score } = await parseScore(options); - - score.info = await this._scoreSimulator.completeReplay(score, attributes); - - return score; - } -} - -export { BeatmapCalculator, GameMode, ScoreCalculator, ScoreSimulator, calculateAccuracy, calculateDifficulty, calculatePerformance, calculateRank, calculateTotalHits, createBeatmapAttributes, createBeatmapInfo, createDifficultyCalculator, downloadFile, generateHitStatistics, getRulesetById, getRulesetIdByName, getValidHitStatistics, parseBeatmap, parseScore, scaleTotalScore, toCombination, toDifficultyAttributes, toDifficultyMods, toScoreInfo }; diff --git a/package.json b/package.json index fc1da12..99c4de4 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "test": "jest --verbose --passWithNoTests", "fix": "eslint --fix ./src", "format": "eslint --fix ./lib/** --no-ignore", - "prepublishOnly": "npm run build && npm run test", + "prepublishOnly": "npm run build", "docs": "npx typedoc src/index.ts" }, "author": "Kionell",