Skip to main content

ParseCore

Python library for osu! beatmap parsing, mod handling, and performance point calculation.

PyPI Version Python License Typing Code Style Discord Docs Translated with Crowdin


Features

  • Beatmap Parsing - Full .osu file 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

Installation

pip install parsecore

Or with uv:

uv add parsecore

Requires Python 3.10+


Quick Start

Beatmap Parsing

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, BPM

Star Rating (Difficulty)

from 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)
)

Performance Points (PP)

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)

Converts (osu! → taiko / catch / mania)

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)

Mod Handling

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)          # HDNC

How the pp calculation works

parsecore.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:

  1. Parse - the .osu file 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).
  2. 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).
  3. Preprocess - per-ruleset difficulty objects are built (distances, angles, rhythm groupings, effective BPM, ...).
  4. 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.
  5. 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.

Verification

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.


Project Structure

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

Contributing

Contributions are welcome! Please read CONTRIBUTING.md before submitting a pull request.

Security

To report a security vulnerability, see SECURITY.md.

Translations

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

parsecore-1.0.1.tar.gz (165.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

parsecore-1.0.1-py3-none-any.whl (240.8 kB view details)

Uploaded Python 3

File details

Details for the file parsecore-1.0.1.tar.gz.

File metadata

  • Download URL: parsecore-1.0.1.tar.gz
  • Upload date:
  • Size: 165.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for parsecore-1.0.1.tar.gz
Algorithm Hash digest
SHA256 f2d36ce28f77326b134d1ad6ff995da319f4542ed99c8a4a0fb74a1378e3b188
MD5 3fd350e7fbf26deea10aad0d7a26baed
BLAKE2b-256 89b43699efd7d52d0b44f7228643ca1da6784b2d9fb7def6f5a1a1627480bd19

See more details on using hashes here.

Provenance

The following attestation bundles were made for parsecore-1.0.1.tar.gz:

Publisher: publish.yml on O-Lib/parsecore

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file parsecore-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: parsecore-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 240.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for parsecore-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2c0b0e47fb6653269e5a6eb3011229dde3b33695139cc8ae6c0eed32acce416f
MD5 74e46c06c739432d15061bf6bec41c0c
BLAKE2b-256 71e1909c94f1d1446c039833f657a19f60dd29eacebd1c9cf5ff8f7b09d09c7c

See more details on using hashes here.

Provenance

The following attestation bundles were made for parsecore-1.0.1-py3-none-any.whl:

Publisher: publish.yml on O-Lib/parsecore

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page