- Beatmap Parsing - Full
.osufile parsing including all sections: General, Metadata, Difficulty, Events, Timing Points, Hit Objects, Colors, and Editor - Mod Handling - Complete mod system supporting all osu! game modes (osu!, Taiko, Catch, Mania) with legacy and modern mod representations
- Star Rating & PP Calculation - Complete difficulty and performance calculation for all four rulesets (osu!, osu!taiko, osu!catch, osu!mania)
- Up to date - Implements the 2026 Q2 pp/star rating rework (osu! deploy
2026.702.1, July 2026): the new osu! Reading skill, Snap/Flow aim, deviation-based speed pp, reworked taiko rhythm, catch linear-spacing nerf - Bit-exact - Verified bit-for-bit identical to the official osu! C# implementation across a 4,400+ case test matrix (all rulesets, all mod combinations, converts, lazer & stable scores)
- Converts - Faithful osu! → taiko / catch / mania conversion, including the legacy mania pattern generator with osu!stable RNG and key mods (1K-9K)
- Lazer & Stable scores - Both scoring systems supported: lazer slider statistics (slider tail hits, large ticks) as well as classic scores incl. score-based miss estimation from
legacy_total_score - Type-safe - Fully typed codebase, mypy-checked
pip install parsecoreOr with uv:
uv add parsecoreRequires Python 3.10+
from parsecore.Beatmap import Beatmap
beatmap = Beatmap.from_path("path/to/map.osu")
print(beatmap.metadata.title)
print(beatmap.metadata.version)
print(beatmap.difficulty.approach_rate)
print(beatmap.difficulty.circle_size)
for obj in beatmap.hit_objects.hit_objects:
print(obj)
for tp in beatmap.timing_points.control_points.timing_points:
print(tp.time, 60000 / tp.beat_len) # start time, BPMfrom parsecore.Performance import Beatmap, Difficulty
bm = Beatmap.from_path("path/to/map.osu")
# NoMod
attrs = Difficulty().calculate(bm)
print(attrs.stars, attrs.max_combo)
# Mods via legacy bitflags (HD = 8, DT = 64, ...)
attrs = Difficulty().mods(8 | 64).calculate(bm)
print(attrs.stars)
# osu! attributes include the per-skill breakdown of the 2026 rework
print(attrs.aim, attrs.speed, attrs.reading, attrs.flashlight)
# Custom clock rate, difficulty overrides, partial plays
attrs = (
Difficulty()
.mods(64)
.clock_rate(1.3)
.ar(10, fixed=True) # fixed=True: use as-is; fixed=False: mods still apply
.passed_objects(500) # difficulty of the first 500 objects only
.calculate(bm)
)from parsecore.Performance import Beatmap, Performance
bm = Beatmap.from_path("path/to/map.osu")
# From accuracy (the score state is generated automatically)
result = Performance(bm).mods(64).accuracy(98.5).misses(2).calculate()
print(result.pp)
# From an explicit score state (osu!lazer score)
result = (
Performance(bm)
.lazer(True)
.n300(194).n100(0).n50(0).misses(0)
.combo(277)
.slider_end_hits(68)
.large_tick_hits(15)
.calculate()
)
print(result.pp, result.pp_aim, result.pp_speed, result.pp_acc, result.pp_reading)
# osu!(stable) / classic score, incl. score-based miss estimation
result = (
Performance(bm)
.lazer(False)
.n300(2020).n100(27).misses(4)
.combo(1699)
.legacy_total_score(31_546_804)
.calculate()
)
print(result.pp, result.effective_miss_count)from parsecore.Beatmap.beatmap import Beatmap as UserBeatmap
from parsecore.Performance import Beatmap, Difficulty, GameMode
user_map = UserBeatmap.from_path("path/to/osu_map.osu")
# Play an osu! map in another ruleset
bm = Beatmap.from_user_beatmap(user_map, override_mode=GameMode.MANIA)
attrs = Difficulty().calculate(bm)
print(attrs.stars, attrs.n_objects, attrs.n_hold_notes)
# mania key mods change the column count of converts (7K = 1 << 18)
attrs = Difficulty().mods(1 << 18).calculate(bm)from parsecore.Mods import GameMods, GameMode
# From an acronym string
mods = GameMods.from_acronyms("HDDT", GameMode.Osu)
print(mods) # DTHD
print(mods.clock_rate()) # 1.5
# Legacy bitfield conversion
legacy = mods.as_legacy()
print(legacy.bits()) # 72
# Intermode mods (not bound to a specific ruleset)
from parsecore.Mods import GameModsIntermode
intermode = GameModsIntermode.from_acronyms(["HD", "NC"])
print(intermode) # HDNCparsecore.Performance is a pure-Python port of the official osu! difficulty and
performance algorithms (ppy/osu, deploy 2026.702.1) not of a third-party
reimplementation. The pipeline:
- Parse - the
.osufile is decoded into hit objects, timing/difficulty/effect points with osu!-faithful float semantics (positions and curve math are computed in 32-bit floats exactly like the game client). - Convert - if the target ruleset differs from the map's native mode, the map is converted first (taiko drum-roll splitting with scroll-speed effect points, catch mode flag, mania legacy pattern generator with osu!stable RNG).
- Preprocess - per-ruleset difficulty objects are built (distances, angles, rhythm groupings, effective BPM, ...).
- Skills - each ruleset evaluates its skills (osu!: Aim, Speed, Reading, Flashlight; taiko: Rhythm, Reading, Colour, Stamina; catch: Movement; mania: Strain) and aggregates them into the star rating.
- Performance - the score state (either given explicitly or generated from accuracy/miss count) is combined with the difficulty attributes into pp, including miss penalties, slider-break estimation and the classic/lazer scoring differences.
Every step reproduces the C# reference including its floating-point quirks (f32 intermediates, IEEE division semantics, integer-exponent powers as explicit multiplication, C# sorting algorithms, legacy RNG), which is what makes the results bit-identical rather than merely close.
Correctness is enforced by a parity test suite that compares parsecore against an
oracle built from the official ppy.osu.Game packages:
- 4,400+ cases: star ratings and pp across all four rulesets
- extended mod matrix (EZ/HR/DT/NC/HT/HD/FL/TD/RX/AP/SO, key mods 1K-9K, combinations)
- all convert directions, native maps, edge-case and pathological maps
- randomized full score states, partial states, fails, lazer & stable scores
- result: 0 differences every value bit-identical to the official implementation
Note: After a pp rework deploys, public calculators and bots that rely on outdated libraries can disagree with parsecore. When in doubt: new scores set in-game receive exactly the values parsecore computes.
parsecore/
├── Beatmap/ # .osu file parsing and encoding
│ ├── beatmap.py
│ ├── reader.py
│ ├── encode.py
│ └── section/ # Individual section parsers
│ ├── general.py
│ ├── metadata.py
│ ├── difficulty.py
│ ├── timing_points.py
│ ├── hit_objects/
│ └── ...
├── Mods/ # Mod system
│ ├── game_mod.py
│ ├── game_mods.py
│ ├── game_mode.py
│ ├── generated_mods.py
│ └── ...
└── Performance/ # Star rating & pp calculation
├── api.py # Public API: Beatmap, Difficulty, Performance
├── utils.py # Rust/C#-faithful float & RNG helpers
├── data/ # Beatmap model, mods, score state, attributes
└── rulesets/
├── osu/ # Aim, Speed, Reading, Flashlight + pp
├── taiko/ # Rhythm, Reading, Colour, Stamina + pp
├── catch/ # Movement + pp, gradual calculation
└── mania/ # Strain + pp, legacy convert pattern generator
Contributions are welcome! Please read CONTRIBUTING.md before submitting a pull request.
To report a security vulnerability, see SECURITY.md.
parsecore's documentation is translated by the community on Crowdin. Want to see it in your language? Join the project and help translate no coding required.
Currently supported languages
| Language | |
|---|---|
| 🇺🇸 English (US) | Source |
| 🇩🇪 German | Translate → |
| 🇫🇷 French | Translate → |
| 🇱🇺 Luxembourgish | Translate → |
| 🇵🇹 Portuguese | Translate → |
The live translated site is on Read the Docs. Want to help? Join the project on Crowdin no coding required.
© 2026-Present O!Lib Team
