About
This module is published for my own convenience so that I can use it across multiple projects of mine without having to manually transfer files, namely:
Features
- Beatmap parser
- osu! difficulty calculator
- Two versions of difficulty calculators are available:
- Live calculator (based on the current state of difficulty algorithm)
- Local calculator (based on the most recent difficulty algorithm in osu!lazer)
- Includes a strain graph generator.
- Two versions of difficulty calculators are available:
- osu! performance calculator
- Two versions of performance calculators are available:
- Live calculator (based on the current state of performance algorithm)
- Local calculator (based on the most recent performance algorithm in osu!lazer)
- Two versions of performance calculators are available:
- osu!droid replay analyzer
- Allows parsing osu!droid replay files (
.odr
) to get replay information. - Three finger/two hand detection
- Uses cursor movement and beatmap difficulty data to detect.
- Two hand detection is not practical as it's still a WIP.
- Allows parsing osu!droid replay files (
An error margin should be expected from difficulty and performance calculator due to differences between C# and TypeScript.
All features that the module offers are interchangeable with two gamemodes where they are applicable (such as difficulty and performance calculator):
- osu!droid
- osu!standard
Requirements
You need Node v16 or newer to use this module.
Online features may require the following:
- For osu!droid online-related features, you need to have an osu!droid API key specified as
DROID_API_KEY
environment variable. - For osu!standard online-related features, you need to have an osu! API key specified as
OSU_API_KEY
environment variable.
The table below lists all classes along with their methods that require an osu!droid API key or osu! API key.
- osu!droid API key
Class Method(s) MapInfo
fetchDroidLeaderboard()
Player
static getInformation()
hasPlayedVerificationMap()
Score
static getFromHash()
- osu! API key
Class Method(s) MapInfo
static getInformation()
Examples
Below are some examples on how to use the features offered by this module.
The beatmap parser is the most important part of the module as it is required to obtain a Beatmap
instance, which is required for all features of the module.
While the beatmap parser provides ways to obtain a Beatmap
instance with and without osu! API, every examples after the beatmap parser will obtain the Beatmap
instance using osu! API.
Beatmap Parser
Parsing with osu! API
import { MapInfo } from "osu-droid";
// MD5 hash is also supported
const beatmapInfo = await MapInfo.getInformation({ beatmapID: 901854 });
if (!beatmapInfo.title) {
return console.log("Beatmap not found");
}
// Parsed beatmap can be accessed via the `map` field
// Note that the parsed beatmap will be cloned every time this is called. This allows caching of the original instance when needed
console.log(beatmapInfo.map);
Parsing without osu! API
import { readFile } from "fs";
import { Parser } from "osu-droid";
readFile("path/to/file.osu", { encoding: "utf-8" }, (err, data) => {
if (err) throw err;
const parser = new Parser().parse(data);
// Parsed beatmap can be accessed via the `map` field
console.log(parser.map);
});
Both examples return a Beatmap
instance, which is necessary for some features of the module.
osu! difficulty calculator
All examples use live difficulty calculator. For local difficulty calculator, prefix calculation-related classes with Rebalance
:
DroidStarRating
→RebalanceDroidStarRating
OsuStarRating
→RebalanceOsuStarRating
MapStars
→RebalanceMapStars
General difficulty calculator usage
import { DroidStarRating, MapInfo, MapStars, OsuStarRating } from "osu-droid";
const beatmapInfo = await MapInfo.getInformation({ beatmapID: 901854 });
if (!beatmapInfo.title) {
return console.log("Beatmap not found");
}
// Calculate osu!droid difficulty
const droidRating = new DroidStarRating().calculate({
map: beatmapInfo.map,
});
console.log(droidRating);
// Calculate osu!standard difficulty
const osuRating = new OsuStarRating().calculate({
map: beatmapInfo.map,
});
console.log(osuRating);
// Calculate both osu!droid and osu!standard difficulty
const rating = new MapStars().calculate({
map: beatmapInfo.map,
});
// osu!droid difficulty
console.log(rating.droidStars);
// osu!standard difficulty
console.log(rating.pcStars);
Specifying difficulty calculation parameters
Parameters can be applied to alter the result of the calculation:
- Mods: Modifications that will be considered when calculating the difficulty of a beatmap. Defaults to none.
- Custom statistics: Used to apply a custom speed multiplier and force AR. Defaults to none.
import { MapInfo, MapStars, MapStats, ModUtil } from "osu-droid";
const beatmapInfo = await MapInfo.getInformation({ beatmapID: 901854 });
if (!beatmapInfo.title) {
return console.log("Beatmap not found");
}
const mods = ModUtil.pcStringToMods("HDHR");
const stats = new MapStats({
ar: 9,
isForceAR: true,
speedMultiplier: 1.5,
});
// Also available for `DroidStarRating` and `OsuStarRating`
const rating = new MapStars().calculate({
map: beatmapInfo.map,
mods: mods,
stats: stats,
});
// osu!droid difficulty
console.log(rating.droidStars);
// osu!standard difficulty
console.log(rating.pcStars);
Generating a strain graph
The following example generates strain graph for the osu!standard gamemode. For osu!droid, replace OsuStarRating
with DroidStarRating
.
The strain graph is returned as a Buffer
.
import { MapInfo, OsuStarRating } from "osu-droid";
const beatmapInfo = await MapInfo.getInformation({ beatmapID: 901854 });
if (!beatmapInfo.title) {
return console.log("Beatmap not found");
}
const rating = new OsuStarRating().calculate({
map: beatmapInfo.map,
});
// Generate a graph without a background and black graph color
console.log(await rating.getStrainChart());
// Generate a graph using the beatmap set's banner and black graph color
// For usage without osu! API, use `<Parser>.beatmapsetId`
console.log(await rating.getStrainChart(beatmapInfo.beatmapsetID));
// Generate a graph using the beatmap set's banner and a specific graph color
console.log(await rating.getStrainChart(beatmapInfo.beatmapsetID, "#fcba03"));
osu! performance calculator
All examples use live performance calculator. For local performance calculator, prefix calculation-related classes with Rebalance
:
DroidPerformanceCalculator
→RebalanceDroidPerformanceCalculator
OsuPerformanceCalculator
→RebalanceOsuPerformanceCalculator
General performance calculator usage
import {
DroidPerformanceCalculator,
MapInfo,
MapStars,
OsuPerformanceCalculator,
} from "osu-droid";
const beatmapInfo = await MapInfo.getInformation({ beatmapID: 901854 });
if (!beatmapInfo.title) {
return console.log("Beatmap not found");
}
const rating = new MapStars().calculate({
map: beatmapInfo.map,
});
// osu!droid performance
const droidPerformance = new DroidPerformanceCalculator().calculate({
stars: rating.droidStars,
});
console.log(droidPerformance);
// osu!standard performance
const osuPerformance = new OsuPerformanceCalculator().calculate({
stars: rating.pcStars,
});
console.log(osuPerformance);
Specifying performance calculation parameters
Parameters can be passed to alter the result of the calculation:
- Combo: The maximum combo achieved. Defaults to the beatmap's maximum combo.
- Accuracy: The accuracy achieved. Defaults to 100%.
- Misses: The amount of misses achieved.
- Tap penalty: Penalty given from three-finger detection. Only applied for osu!droid gamemode. Defaults to 1.
- Custom statistics: Used to apply a custom speed multiplier and force AR. Defaults to none.
The following example uses osu! API key to obtain a beatmap and calculates for the osu!standard gamemode. See this section for more methods on obtaining a beatmap. For osu!droid, replace OsuPerformanceCalculator
with DroidPerformanceCalculator
and OsuStarRating
with DroidStarRating
.
import {
Accuracy,
MapInfo,
MapStats,
OsuPerformanceCalculator,
OsuStarRating,
} from "osu-droid";
const beatmapInfo = await MapInfo.getInformation({ beatmapID: 901854 });
if (!beatmapInfo.title) {
return console.log("Beatmap not found");
}
const rating = new OsuStarRating().calculate({
map: beatmapInfo.map,
});
const accuracy = new Accuracy({
// Specify your misses here
nmiss: 1,
// The module provides a convenient way to specify accuracy based on the data that you have
// Remove the codes below as you see fit
// If you have hit data (amount of 300s, 100s, and 50s)
n300: 1000,
n100: 0,
n50: 0,
// If you have accuracy percentage
// While this method is more convenient to use, the amount of 300s, 100s, and 50s will be estimated
// This will lead to values being off when calculating for specific accuracies
percent: 100,
nobjects: beatmapInfo.objects,
});
const stats = new MapStats({
ar: 9.5,
isForceAR: true,
speedMultiplier: 1.25,
});
const performance = new OsuPerformanceCalculator().calculate({
stars: rating,
combo: 1250,
accPercent: accuracy,
// The tap penalty can be properly obtained by checking a replay for three finger usage
// However, a custom value can also be provided
tapPenalty: 1.5,
stats: stats,
});
console.log(performance);
osu!droid replay analyzer
To use the replay analyzer, you need a replay file (.odr
).
Using replay analyzer with osu!droid API key
If you have an osu!droid API key, you can get a score's replay from a beatmap:
import { DroidStarRating, MapInfo, MapStats, Score } from "osu-droid";
const beatmapInfo = await MapInfo.getInformation({ beatmapID: 901854 });
if (!beatmapInfo.title) {
return console.log("Beatmap not found");
}
const score = await Score.getFromHash({
uid: 51076,
hash: beatmapInfo.hash,
});
if (!score.title) {
return console.log("Score not found");
}
await score.downloadReplay();
// A `ReplayAnalyzer` instance that contains the replay
const { replay } = score;
// The data of the replay
const { data } = replay;
if (!data) {
return console.log("Replay not found");
}
const stats = new MapStats({
ar: data.forcedAR,
speedMultiplier: data.speedModification,
isForceAR: !isNaN(data.forcedAR),
// In droid version 1.6.7 and below, there exists a bug where NC is slower than DT in a few beatmaps
// This option checks for said condition
oldStatistics: data.replayVersion <= 3,
});
replay.map = new DroidStarRating().calculate({
map: beatmapInfo.map,
mods: data.convertedMods,
stats: stats,
});
// More data can be obtained after specifying beatmap. We can call this method again to do so
await replay.analyze();
// Check for three-finger usage
replay.checkFor3Finger();
// Check for two-hand usage
replay.checkFor2Hand();
Using replay analyzer with a score ID
If you know a score's ID, you can use it to directly retrieve a replay:
import { DroidStarRating, MapInfo, MapStats, ReplayAnalyzer } from "osu-droid";
const beatmapInfo = await MapInfo.getInformation({ beatmapID: 901854 });
if (!beatmapInfo.title) {
return console.log("Beatmap not found");
}
// A `ReplayAnalyzer` instance that contains the replay
const replay = await new ReplayAnalyzer({ scoreID: 12948732 }).analyze();
// The data of the replay
const { data } = replay;
if (!data) {
return console.log("Replay not found");
}
const stats = new MapStats({
ar: data.forcedAR,
speedMultiplier: data.speedModification,
isForceAR: !isNaN(data.forcedAR),
// In droid version 1.6.7 and below, there exists a bug where NC is slower than DT in a few beatmaps
// This option checks for said condition
oldStatistics: data.replayVersion <= 3,
});
replay.map = new DroidStarRating().calculate({
map: beatmapInfo.map,
mods: data.convertedMods,
stats: stats,
});
// More data can be obtained after specifying beatmap. We can call this method again to do so
await replay.analyze();
// Check for three-finger usage
replay.checkFor3Finger();
// Check for two-hand usage
replay.checkFor2Hand();
Using replay analyzer with a local replay file
import { readFile } from "fs";
import { DroidStarRating, MapInfo, MapStats, ReplayAnalyzer } from "osu-droid";
readFile("path/to/file.odr", async (err, replayData) => {
if (err) throw err;
const beatmapInfo = await MapInfo.getInformation({ beatmapID: 901854 });
if (!beatmapInfo.title) {
return console.log("Beatmap not found");
}
// A `ReplayAnalyzer` instance that contains the replay
const replay = new ReplayAnalyzer({ scoreID: 0 });
replay.originalODR = replayData;
await replay.analyze();
// The data of the replay
const { data } = replay;
const stats = new MapStats({
ar: data.forcedAR,
speedMultiplier: data.speedModification,
isForceAR: !isNaN(data.forcedAR),
// In droid version 1.6.7 and below, there exists a bug where NC is slower than DT in a few beatmaps
// This option checks for said condition
oldStatistics: data.replayVersion <= 3,
});
replay.map = new DroidStarRating().calculate({
map: beatmapInfo.map,
mods: data.convertedMods,
stats: stats,
});
// More data can be obtained after specifying beatmap. We can call this method again to do so
await replay.analyze();
// Check for three-finger usage
replay.checkFor3Finger();
// Check for two-hand usage
replay.checkFor2Hand();
});