parsecore 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- parsecore-1.0.0/LICENSE +21 -0
- parsecore-1.0.0/PKG-INFO +293 -0
- parsecore-1.0.0/README.md +264 -0
- parsecore-1.0.0/parsecore/Beatmap/__init__.py +50 -0
- parsecore-1.0.0/parsecore/Beatmap/beatmap.py +316 -0
- parsecore-1.0.0/parsecore/Beatmap/encode.py +653 -0
- parsecore-1.0.0/parsecore/Beatmap/reader.py +71 -0
- parsecore-1.0.0/parsecore/Beatmap/section/__init__.py +84 -0
- parsecore-1.0.0/parsecore/Beatmap/section/colors.py +109 -0
- parsecore-1.0.0/parsecore/Beatmap/section/difficulty.py +111 -0
- parsecore-1.0.0/parsecore/Beatmap/section/editor.py +100 -0
- parsecore-1.0.0/parsecore/Beatmap/section/enums.py +147 -0
- parsecore-1.0.0/parsecore/Beatmap/section/events.py +143 -0
- parsecore-1.0.0/parsecore/Beatmap/section/general.py +153 -0
- parsecore-1.0.0/parsecore/Beatmap/section/hit_objects/__init__.py +61 -0
- parsecore-1.0.0/parsecore/Beatmap/section/hit_objects/hit_objects.py +434 -0
- parsecore-1.0.0/parsecore/Beatmap/section/hit_objects/slider.py +482 -0
- parsecore-1.0.0/parsecore/Beatmap/section/metadata.py +115 -0
- parsecore-1.0.0/parsecore/Beatmap/section/timing_points.py +402 -0
- parsecore-1.0.0/parsecore/Beatmap/utils.py +196 -0
- parsecore-1.0.0/parsecore/Mods/__init__.py +331 -0
- parsecore-1.0.0/parsecore/Mods/_patches.py +322 -0
- parsecore-1.0.0/parsecore/Mods/acronym.py +64 -0
- parsecore-1.0.0/parsecore/Mods/game_mod.py +639 -0
- parsecore-1.0.0/parsecore/Mods/game_mod_intermode.py +639 -0
- parsecore-1.0.0/parsecore/Mods/game_mod_kind.py +49 -0
- parsecore-1.0.0/parsecore/Mods/game_mod_simple.py +90 -0
- parsecore-1.0.0/parsecore/Mods/game_mode.py +65 -0
- parsecore-1.0.0/parsecore/Mods/game_mods.py +362 -0
- parsecore-1.0.0/parsecore/Mods/game_mods_intermode.py +241 -0
- parsecore-1.0.0/parsecore/Mods/game_mods_legacy.py +419 -0
- parsecore-1.0.0/parsecore/Mods/generated_mods.py +1737 -0
- parsecore-1.0.0/parsecore/Performance/__init__.py +50 -0
- parsecore-1.0.0/parsecore/Performance/api.py +352 -0
- parsecore-1.0.0/parsecore/Performance/data/__init__.py +47 -0
- parsecore-1.0.0/parsecore/Performance/data/attributes.py +289 -0
- parsecore-1.0.0/parsecore/Performance/data/beatmap.py +399 -0
- parsecore-1.0.0/parsecore/Performance/data/hit_objects.py +75 -0
- parsecore-1.0.0/parsecore/Performance/data/mode.py +31 -0
- parsecore-1.0.0/parsecore/Performance/data/mods.py +214 -0
- parsecore-1.0.0/parsecore/Performance/data/score_state.py +103 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/__init__.py +23 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/catch/__init__.py +23 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/catch/convert.py +220 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/catch/difficulty.py +152 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/catch/gradual.py +200 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/catch/hit_objects.py +350 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/catch/hitresult_generator.py +337 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/catch/performance.py +170 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/catch/skills.py +249 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/mania/__init__.py +23 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/mania/convert.py +1240 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/mania/difficulty.py +124 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/mania/gradual.py +24 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/mania/hit_objects.py +47 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/mania/hitresult_generator.py +192 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/mania/performance.py +117 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/mania/skills.py +346 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/osu/__init__.py +23 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/osu/convert.py +211 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/osu/difficulty.py +533 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/osu/gradual.py +24 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/osu/hit_objects.py +711 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/osu/hitresult_generator.py +218 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/osu/legacy_score.py +311 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/osu/performance.py +924 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/osu/skills.py +1813 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/taiko/__init__.py +23 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/taiko/convert.py +191 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/taiko/difficulty.py +146 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/taiko/gradual.py +24 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/taiko/hit_objects.py +62 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/taiko/hitresult_generator.py +105 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/taiko/performance.py +257 -0
- parsecore-1.0.0/parsecore/Performance/rulesets/taiko/skills.py +1262 -0
- parsecore-1.0.0/parsecore/Performance/utils.py +527 -0
- parsecore-1.0.0/pyproject.toml +64 -0
parsecore-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-Present O!Lib Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
parsecore-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: parsecore
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python library for osu! beatmap parsing, mod handling, and performance point calculation.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: osu,beatmap,parser,mods,performance,pp
|
|
8
|
+
Author: Error44
|
|
9
|
+
Author-email: error44@olib.dev
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Games/Entertainment
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Dist: requests
|
|
24
|
+
Project-URL: Documentation, https://github.com/O-Lib/parsecore#readme
|
|
25
|
+
Project-URL: Homepage, https://github.com/O-Lib/parsecore
|
|
26
|
+
Project-URL: Repository, https://github.com/O-Lib/parsecore
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
<div align="center">
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
**Python library for osu! beatmap parsing, mod handling, and performance point calculation.**
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/parsecore/)
|
|
36
|
+
[](https://pypi.org/project/parsecore/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](https://mypy-lang.org/)
|
|
39
|
+
[](https://pre-commit.com/)
|
|
40
|
+
[](https://docs.astral.sh/ruff/)
|
|
41
|
+
[](https://discord.gg/9p7whE7QxQ)
|
|
42
|
+
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### Features
|
|
48
|
+
|
|
49
|
+
- **Beatmap Parsing** - Full `.osu` file parsing including all sections: General, Metadata, Difficulty, Events, Timing Points, Hit Objects, Colors, and Editor
|
|
50
|
+
- **Mod Handling** - Complete mod system supporting all osu! game modes (osu!, Taiko, Catch, Mania) with legacy and modern mod representations
|
|
51
|
+
- **Star Rating & PP Calculation** - Complete difficulty and performance calculation for **all four rulesets** (osu!, osu!taiko, osu!catch, osu!mania)
|
|
52
|
+
- **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
|
|
53
|
+
- **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)
|
|
54
|
+
- **Converts** - Faithful osu! → taiko / catch / mania conversion, including the legacy mania pattern generator with osu!stable RNG and key mods (1K-9K)
|
|
55
|
+
- **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`
|
|
56
|
+
- **Type-safe** - Fully typed codebase, mypy-checked
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install parsecore
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or with [uv](https://github.com/astral-sh/uv):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv add parsecore
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Requires Python 3.10+**
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### Quick Start
|
|
77
|
+
|
|
78
|
+
### Beatmap Parsing
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from parsecore.Beatmap import Beatmap
|
|
82
|
+
|
|
83
|
+
beatmap = Beatmap.from_path("path/to/map.osu")
|
|
84
|
+
|
|
85
|
+
print(beatmap.metadata.title)
|
|
86
|
+
print(beatmap.metadata.version)
|
|
87
|
+
print(beatmap.difficulty.approach_rate)
|
|
88
|
+
print(beatmap.difficulty.circle_size)
|
|
89
|
+
|
|
90
|
+
for obj in beatmap.hit_objects.hit_objects:
|
|
91
|
+
print(obj)
|
|
92
|
+
|
|
93
|
+
for tp in beatmap.timing_points.control_points.timing_points:
|
|
94
|
+
print(tp.time, 60000 / tp.beat_len) # start time, BPM
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Star Rating (Difficulty)
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from parsecore.Performance import Beatmap, Difficulty
|
|
101
|
+
|
|
102
|
+
bm = Beatmap.from_path("path/to/map.osu")
|
|
103
|
+
|
|
104
|
+
# NoMod
|
|
105
|
+
attrs = Difficulty().calculate(bm)
|
|
106
|
+
print(attrs.stars, attrs.max_combo)
|
|
107
|
+
|
|
108
|
+
# Mods via legacy bitflags (HD = 8, DT = 64, ...)
|
|
109
|
+
attrs = Difficulty().mods(8 | 64).calculate(bm)
|
|
110
|
+
print(attrs.stars)
|
|
111
|
+
|
|
112
|
+
# osu! attributes include the per-skill breakdown of the 2026 rework
|
|
113
|
+
print(attrs.aim, attrs.speed, attrs.reading, attrs.flashlight)
|
|
114
|
+
|
|
115
|
+
# Custom clock rate, difficulty overrides, partial plays
|
|
116
|
+
attrs = (
|
|
117
|
+
Difficulty()
|
|
118
|
+
.mods(64)
|
|
119
|
+
.clock_rate(1.3)
|
|
120
|
+
.ar(10, fixed=True) # fixed=True: use as-is; fixed=False: mods still apply
|
|
121
|
+
.passed_objects(500) # difficulty of the first 500 objects only
|
|
122
|
+
.calculate(bm)
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Performance Points (PP)
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from parsecore.Performance import Beatmap, Performance
|
|
130
|
+
|
|
131
|
+
bm = Beatmap.from_path("path/to/map.osu")
|
|
132
|
+
|
|
133
|
+
# From accuracy (the score state is generated automatically)
|
|
134
|
+
result = Performance(bm).mods(64).accuracy(98.5).misses(2).calculate()
|
|
135
|
+
print(result.pp)
|
|
136
|
+
|
|
137
|
+
# From an explicit score state (osu!lazer score)
|
|
138
|
+
result = (
|
|
139
|
+
Performance(bm)
|
|
140
|
+
.lazer(True)
|
|
141
|
+
.n300(194).n100(0).n50(0).misses(0)
|
|
142
|
+
.combo(277)
|
|
143
|
+
.slider_end_hits(68)
|
|
144
|
+
.large_tick_hits(15)
|
|
145
|
+
.calculate()
|
|
146
|
+
)
|
|
147
|
+
print(result.pp, result.pp_aim, result.pp_speed, result.pp_acc, result.pp_reading)
|
|
148
|
+
|
|
149
|
+
# osu!(stable) / classic score, incl. score-based miss estimation
|
|
150
|
+
result = (
|
|
151
|
+
Performance(bm)
|
|
152
|
+
.lazer(False)
|
|
153
|
+
.n300(2020).n100(27).misses(4)
|
|
154
|
+
.combo(1699)
|
|
155
|
+
.legacy_total_score(31_546_804)
|
|
156
|
+
.calculate()
|
|
157
|
+
)
|
|
158
|
+
print(result.pp, result.effective_miss_count)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Converts (osu! → taiko / catch / mania)
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from parsecore.Beatmap.beatmap import Beatmap as UserBeatmap
|
|
165
|
+
from parsecore.Performance import Beatmap, Difficulty, GameMode
|
|
166
|
+
|
|
167
|
+
user_map = UserBeatmap.from_path("path/to/osu_map.osu")
|
|
168
|
+
|
|
169
|
+
# Play an osu! map in another ruleset
|
|
170
|
+
bm = Beatmap.from_user_beatmap(user_map, override_mode=GameMode.MANIA)
|
|
171
|
+
attrs = Difficulty().calculate(bm)
|
|
172
|
+
print(attrs.stars, attrs.n_objects, attrs.n_hold_notes)
|
|
173
|
+
|
|
174
|
+
# mania key mods change the column count of converts (7K = 1 << 18)
|
|
175
|
+
attrs = Difficulty().mods(1 << 18).calculate(bm)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Mod Handling
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from parsecore.Mods import GameMods, GameMode
|
|
182
|
+
|
|
183
|
+
# From an acronym string
|
|
184
|
+
mods = GameMods.from_acronyms("HDDT", GameMode.Osu)
|
|
185
|
+
print(mods) # DTHD
|
|
186
|
+
print(mods.clock_rate()) # 1.5
|
|
187
|
+
|
|
188
|
+
# Legacy bitfield conversion
|
|
189
|
+
legacy = mods.as_legacy()
|
|
190
|
+
print(legacy.bits()) # 72
|
|
191
|
+
|
|
192
|
+
# Intermode mods (not bound to a specific ruleset)
|
|
193
|
+
from parsecore.Mods import GameModsIntermode
|
|
194
|
+
intermode = GameModsIntermode.from_acronyms(["HD", "NC"])
|
|
195
|
+
print(intermode) # HDNC
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### How the pp calculation works
|
|
201
|
+
|
|
202
|
+
`parsecore.Performance` is a pure-Python port of the **official osu! difficulty and
|
|
203
|
+
performance algorithms** (ppy/osu, deploy `2026.702.1`) not of a third-party
|
|
204
|
+
reimplementation. The pipeline:
|
|
205
|
+
|
|
206
|
+
1. **Parse** - the `.osu` file is decoded into hit objects, timing/difficulty/effect
|
|
207
|
+
points with osu!-faithful float semantics (positions and curve math are computed
|
|
208
|
+
in 32-bit floats exactly like the game client).
|
|
209
|
+
2. **Convert** - if the target ruleset differs from the map's native mode, the map
|
|
210
|
+
is converted first (taiko drum-roll splitting with scroll-speed effect points,
|
|
211
|
+
catch mode flag, mania legacy pattern generator with osu!stable RNG).
|
|
212
|
+
3. **Preprocess** - per-ruleset difficulty objects are built (distances, angles,
|
|
213
|
+
rhythm groupings, effective BPM, ...).
|
|
214
|
+
4. **Skills** - each ruleset evaluates its skills (osu!: Aim, Speed, Reading,
|
|
215
|
+
Flashlight; taiko: Rhythm, Reading, Colour, Stamina; catch: Movement;
|
|
216
|
+
mania: Strain) and aggregates them into the star rating.
|
|
217
|
+
5. **Performance** - the score state (either given explicitly or generated from
|
|
218
|
+
accuracy/miss count) is combined with the difficulty attributes into pp,
|
|
219
|
+
including miss penalties, slider-break estimation and the classic/lazer
|
|
220
|
+
scoring differences.
|
|
221
|
+
|
|
222
|
+
Every step reproduces the C# reference including its floating-point quirks
|
|
223
|
+
(f32 intermediates, IEEE division semantics, integer-exponent powers as explicit
|
|
224
|
+
multiplication, C# sorting algorithms, legacy RNG), which is what makes the
|
|
225
|
+
results **bit-identical** rather than merely close.
|
|
226
|
+
|
|
227
|
+
#### Verification
|
|
228
|
+
|
|
229
|
+
Correctness is enforced by a parity test suite that compares parsecore against an
|
|
230
|
+
oracle built from the official `ppy.osu.Game` packages:
|
|
231
|
+
|
|
232
|
+
- 4,400+ cases: star ratings and pp across all four rulesets
|
|
233
|
+
- extended mod matrix (EZ/HR/DT/NC/HT/HD/FL/TD/RX/AP/SO, key mods 1K-9K, combinations)
|
|
234
|
+
- all convert directions, native maps, edge-case and pathological maps
|
|
235
|
+
- randomized full score states, partial states, fails, lazer & stable scores
|
|
236
|
+
- **result: 0 differences every value bit-identical to the official implementation**
|
|
237
|
+
|
|
238
|
+
> **Note:** After a pp rework deploys, public calculators and bots that rely on
|
|
239
|
+
> outdated libraries can disagree with parsecore. When in doubt: new scores set
|
|
240
|
+
> in-game receive exactly the values parsecore computes.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### Project Structure
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
parsecore/
|
|
248
|
+
├── Beatmap/ # .osu file parsing and encoding
|
|
249
|
+
│ ├── beatmap.py
|
|
250
|
+
│ ├── reader.py
|
|
251
|
+
│ ├── encode.py
|
|
252
|
+
│ └── section/ # Individual section parsers
|
|
253
|
+
│ ├── general.py
|
|
254
|
+
│ ├── metadata.py
|
|
255
|
+
│ ├── difficulty.py
|
|
256
|
+
│ ├── timing_points.py
|
|
257
|
+
│ ├── hit_objects/
|
|
258
|
+
│ └── ...
|
|
259
|
+
├── Mods/ # Mod system
|
|
260
|
+
│ ├── game_mod.py
|
|
261
|
+
│ ├── game_mods.py
|
|
262
|
+
│ ├── game_mode.py
|
|
263
|
+
│ ├── generated_mods.py
|
|
264
|
+
│ └── ...
|
|
265
|
+
└── Performance/ # Star rating & pp calculation
|
|
266
|
+
├── api.py # Public API: Beatmap, Difficulty, Performance
|
|
267
|
+
├── utils.py # Rust/C#-faithful float & RNG helpers
|
|
268
|
+
├── data/ # Beatmap model, mods, score state, attributes
|
|
269
|
+
└── rulesets/
|
|
270
|
+
├── osu/ # Aim, Speed, Reading, Flashlight + pp
|
|
271
|
+
├── taiko/ # Rhythm, Reading, Colour, Stamina + pp
|
|
272
|
+
├── catch/ # Movement + pp, gradual calculation
|
|
273
|
+
└── mania/ # Strain + pp, legacy convert pattern generator
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### Contributing
|
|
279
|
+
|
|
280
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a pull request.
|
|
281
|
+
|
|
282
|
+
### Security
|
|
283
|
+
|
|
284
|
+
To report a security vulnerability, see [SECURITY.md](SECURITY.md).
|
|
285
|
+
|
|
286
|
+
<p align="center">
|
|
287
|
+
<img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/footers/gray0_ctp_on_line.svg?sanitize=true" />
|
|
288
|
+
</p>
|
|
289
|
+
|
|
290
|
+
<p align="center">
|
|
291
|
+
<code>© 2026-Present <a href="https://github.com/O-Lib">O!Lib Team</a></code>
|
|
292
|
+
</p>
|
|
293
|
+
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**Python library for osu! beatmap parsing, mod handling, and performance point calculation.**
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/parsecore/)
|
|
8
|
+
[](https://pypi.org/project/parsecore/)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
[](https://mypy-lang.org/)
|
|
11
|
+
[](https://pre-commit.com/)
|
|
12
|
+
[](https://docs.astral.sh/ruff/)
|
|
13
|
+
[](https://discord.gg/9p7whE7QxQ)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
- **Beatmap Parsing** - Full `.osu` file parsing including all sections: General, Metadata, Difficulty, Events, Timing Points, Hit Objects, Colors, and Editor
|
|
22
|
+
- **Mod Handling** - Complete mod system supporting all osu! game modes (osu!, Taiko, Catch, Mania) with legacy and modern mod representations
|
|
23
|
+
- **Star Rating & PP Calculation** - Complete difficulty and performance calculation for **all four rulesets** (osu!, osu!taiko, osu!catch, osu!mania)
|
|
24
|
+
- **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
|
|
25
|
+
- **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)
|
|
26
|
+
- **Converts** - Faithful osu! → taiko / catch / mania conversion, including the legacy mania pattern generator with osu!stable RNG and key mods (1K-9K)
|
|
27
|
+
- **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`
|
|
28
|
+
- **Type-safe** - Fully typed codebase, mypy-checked
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install parsecore
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or with [uv](https://github.com/astral-sh/uv):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv add parsecore
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Requires Python 3.10+**
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Quick Start
|
|
49
|
+
|
|
50
|
+
### Beatmap Parsing
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from parsecore.Beatmap import Beatmap
|
|
54
|
+
|
|
55
|
+
beatmap = Beatmap.from_path("path/to/map.osu")
|
|
56
|
+
|
|
57
|
+
print(beatmap.metadata.title)
|
|
58
|
+
print(beatmap.metadata.version)
|
|
59
|
+
print(beatmap.difficulty.approach_rate)
|
|
60
|
+
print(beatmap.difficulty.circle_size)
|
|
61
|
+
|
|
62
|
+
for obj in beatmap.hit_objects.hit_objects:
|
|
63
|
+
print(obj)
|
|
64
|
+
|
|
65
|
+
for tp in beatmap.timing_points.control_points.timing_points:
|
|
66
|
+
print(tp.time, 60000 / tp.beat_len) # start time, BPM
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Star Rating (Difficulty)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from parsecore.Performance import Beatmap, Difficulty
|
|
73
|
+
|
|
74
|
+
bm = Beatmap.from_path("path/to/map.osu")
|
|
75
|
+
|
|
76
|
+
# NoMod
|
|
77
|
+
attrs = Difficulty().calculate(bm)
|
|
78
|
+
print(attrs.stars, attrs.max_combo)
|
|
79
|
+
|
|
80
|
+
# Mods via legacy bitflags (HD = 8, DT = 64, ...)
|
|
81
|
+
attrs = Difficulty().mods(8 | 64).calculate(bm)
|
|
82
|
+
print(attrs.stars)
|
|
83
|
+
|
|
84
|
+
# osu! attributes include the per-skill breakdown of the 2026 rework
|
|
85
|
+
print(attrs.aim, attrs.speed, attrs.reading, attrs.flashlight)
|
|
86
|
+
|
|
87
|
+
# Custom clock rate, difficulty overrides, partial plays
|
|
88
|
+
attrs = (
|
|
89
|
+
Difficulty()
|
|
90
|
+
.mods(64)
|
|
91
|
+
.clock_rate(1.3)
|
|
92
|
+
.ar(10, fixed=True) # fixed=True: use as-is; fixed=False: mods still apply
|
|
93
|
+
.passed_objects(500) # difficulty of the first 500 objects only
|
|
94
|
+
.calculate(bm)
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Performance Points (PP)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from parsecore.Performance import Beatmap, Performance
|
|
102
|
+
|
|
103
|
+
bm = Beatmap.from_path("path/to/map.osu")
|
|
104
|
+
|
|
105
|
+
# From accuracy (the score state is generated automatically)
|
|
106
|
+
result = Performance(bm).mods(64).accuracy(98.5).misses(2).calculate()
|
|
107
|
+
print(result.pp)
|
|
108
|
+
|
|
109
|
+
# From an explicit score state (osu!lazer score)
|
|
110
|
+
result = (
|
|
111
|
+
Performance(bm)
|
|
112
|
+
.lazer(True)
|
|
113
|
+
.n300(194).n100(0).n50(0).misses(0)
|
|
114
|
+
.combo(277)
|
|
115
|
+
.slider_end_hits(68)
|
|
116
|
+
.large_tick_hits(15)
|
|
117
|
+
.calculate()
|
|
118
|
+
)
|
|
119
|
+
print(result.pp, result.pp_aim, result.pp_speed, result.pp_acc, result.pp_reading)
|
|
120
|
+
|
|
121
|
+
# osu!(stable) / classic score, incl. score-based miss estimation
|
|
122
|
+
result = (
|
|
123
|
+
Performance(bm)
|
|
124
|
+
.lazer(False)
|
|
125
|
+
.n300(2020).n100(27).misses(4)
|
|
126
|
+
.combo(1699)
|
|
127
|
+
.legacy_total_score(31_546_804)
|
|
128
|
+
.calculate()
|
|
129
|
+
)
|
|
130
|
+
print(result.pp, result.effective_miss_count)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Converts (osu! → taiko / catch / mania)
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from parsecore.Beatmap.beatmap import Beatmap as UserBeatmap
|
|
137
|
+
from parsecore.Performance import Beatmap, Difficulty, GameMode
|
|
138
|
+
|
|
139
|
+
user_map = UserBeatmap.from_path("path/to/osu_map.osu")
|
|
140
|
+
|
|
141
|
+
# Play an osu! map in another ruleset
|
|
142
|
+
bm = Beatmap.from_user_beatmap(user_map, override_mode=GameMode.MANIA)
|
|
143
|
+
attrs = Difficulty().calculate(bm)
|
|
144
|
+
print(attrs.stars, attrs.n_objects, attrs.n_hold_notes)
|
|
145
|
+
|
|
146
|
+
# mania key mods change the column count of converts (7K = 1 << 18)
|
|
147
|
+
attrs = Difficulty().mods(1 << 18).calculate(bm)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Mod Handling
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from parsecore.Mods import GameMods, GameMode
|
|
154
|
+
|
|
155
|
+
# From an acronym string
|
|
156
|
+
mods = GameMods.from_acronyms("HDDT", GameMode.Osu)
|
|
157
|
+
print(mods) # DTHD
|
|
158
|
+
print(mods.clock_rate()) # 1.5
|
|
159
|
+
|
|
160
|
+
# Legacy bitfield conversion
|
|
161
|
+
legacy = mods.as_legacy()
|
|
162
|
+
print(legacy.bits()) # 72
|
|
163
|
+
|
|
164
|
+
# Intermode mods (not bound to a specific ruleset)
|
|
165
|
+
from parsecore.Mods import GameModsIntermode
|
|
166
|
+
intermode = GameModsIntermode.from_acronyms(["HD", "NC"])
|
|
167
|
+
print(intermode) # HDNC
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### How the pp calculation works
|
|
173
|
+
|
|
174
|
+
`parsecore.Performance` is a pure-Python port of the **official osu! difficulty and
|
|
175
|
+
performance algorithms** (ppy/osu, deploy `2026.702.1`) not of a third-party
|
|
176
|
+
reimplementation. The pipeline:
|
|
177
|
+
|
|
178
|
+
1. **Parse** - the `.osu` file is decoded into hit objects, timing/difficulty/effect
|
|
179
|
+
points with osu!-faithful float semantics (positions and curve math are computed
|
|
180
|
+
in 32-bit floats exactly like the game client).
|
|
181
|
+
2. **Convert** - if the target ruleset differs from the map's native mode, the map
|
|
182
|
+
is converted first (taiko drum-roll splitting with scroll-speed effect points,
|
|
183
|
+
catch mode flag, mania legacy pattern generator with osu!stable RNG).
|
|
184
|
+
3. **Preprocess** - per-ruleset difficulty objects are built (distances, angles,
|
|
185
|
+
rhythm groupings, effective BPM, ...).
|
|
186
|
+
4. **Skills** - each ruleset evaluates its skills (osu!: Aim, Speed, Reading,
|
|
187
|
+
Flashlight; taiko: Rhythm, Reading, Colour, Stamina; catch: Movement;
|
|
188
|
+
mania: Strain) and aggregates them into the star rating.
|
|
189
|
+
5. **Performance** - the score state (either given explicitly or generated from
|
|
190
|
+
accuracy/miss count) is combined with the difficulty attributes into pp,
|
|
191
|
+
including miss penalties, slider-break estimation and the classic/lazer
|
|
192
|
+
scoring differences.
|
|
193
|
+
|
|
194
|
+
Every step reproduces the C# reference including its floating-point quirks
|
|
195
|
+
(f32 intermediates, IEEE division semantics, integer-exponent powers as explicit
|
|
196
|
+
multiplication, C# sorting algorithms, legacy RNG), which is what makes the
|
|
197
|
+
results **bit-identical** rather than merely close.
|
|
198
|
+
|
|
199
|
+
#### Verification
|
|
200
|
+
|
|
201
|
+
Correctness is enforced by a parity test suite that compares parsecore against an
|
|
202
|
+
oracle built from the official `ppy.osu.Game` packages:
|
|
203
|
+
|
|
204
|
+
- 4,400+ cases: star ratings and pp across all four rulesets
|
|
205
|
+
- extended mod matrix (EZ/HR/DT/NC/HT/HD/FL/TD/RX/AP/SO, key mods 1K-9K, combinations)
|
|
206
|
+
- all convert directions, native maps, edge-case and pathological maps
|
|
207
|
+
- randomized full score states, partial states, fails, lazer & stable scores
|
|
208
|
+
- **result: 0 differences every value bit-identical to the official implementation**
|
|
209
|
+
|
|
210
|
+
> **Note:** After a pp rework deploys, public calculators and bots that rely on
|
|
211
|
+
> outdated libraries can disagree with parsecore. When in doubt: new scores set
|
|
212
|
+
> in-game receive exactly the values parsecore computes.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### Project Structure
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
parsecore/
|
|
220
|
+
├── Beatmap/ # .osu file parsing and encoding
|
|
221
|
+
│ ├── beatmap.py
|
|
222
|
+
│ ├── reader.py
|
|
223
|
+
│ ├── encode.py
|
|
224
|
+
│ └── section/ # Individual section parsers
|
|
225
|
+
│ ├── general.py
|
|
226
|
+
│ ├── metadata.py
|
|
227
|
+
│ ├── difficulty.py
|
|
228
|
+
│ ├── timing_points.py
|
|
229
|
+
│ ├── hit_objects/
|
|
230
|
+
│ └── ...
|
|
231
|
+
├── Mods/ # Mod system
|
|
232
|
+
│ ├── game_mod.py
|
|
233
|
+
│ ├── game_mods.py
|
|
234
|
+
│ ├── game_mode.py
|
|
235
|
+
│ ├── generated_mods.py
|
|
236
|
+
│ └── ...
|
|
237
|
+
└── Performance/ # Star rating & pp calculation
|
|
238
|
+
├── api.py # Public API: Beatmap, Difficulty, Performance
|
|
239
|
+
├── utils.py # Rust/C#-faithful float & RNG helpers
|
|
240
|
+
├── data/ # Beatmap model, mods, score state, attributes
|
|
241
|
+
└── rulesets/
|
|
242
|
+
├── osu/ # Aim, Speed, Reading, Flashlight + pp
|
|
243
|
+
├── taiko/ # Rhythm, Reading, Colour, Stamina + pp
|
|
244
|
+
├── catch/ # Movement + pp, gradual calculation
|
|
245
|
+
└── mania/ # Strain + pp, legacy convert pattern generator
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### Contributing
|
|
251
|
+
|
|
252
|
+
Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) before submitting a pull request.
|
|
253
|
+
|
|
254
|
+
### Security
|
|
255
|
+
|
|
256
|
+
To report a security vulnerability, see [SECURITY.md](SECURITY.md).
|
|
257
|
+
|
|
258
|
+
<p align="center">
|
|
259
|
+
<img src="https://raw.githubusercontent.com/catppuccin/catppuccin/main/assets/footers/gray0_ctp_on_line.svg?sanitize=true" />
|
|
260
|
+
</p>
|
|
261
|
+
|
|
262
|
+
<p align="center">
|
|
263
|
+
<code>© 2026-Present <a href="https://github.com/O-Lib">O!Lib Team</a></code>
|
|
264
|
+
</p>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026-Present O!Lib Contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
_pkg_dir = os.path.dirname(__file__)
|
|
29
|
+
if _pkg_dir not in sys.path:
|
|
30
|
+
sys.path.insert(0, _pkg_dir)
|
|
31
|
+
|
|
32
|
+
from .beatmap import Beatmap
|
|
33
|
+
from .utils import (
|
|
34
|
+
KeyValue, MAX_PARSE_VALUE, ParseNumberError, parse_with_limits, parse_int, parse_float, Pos, trim_comment,
|
|
35
|
+
to_standardized_path, clean_filename
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Beatmap",
|
|
40
|
+
"KeyValue",
|
|
41
|
+
"MAX_PARSE_VALUE",
|
|
42
|
+
"ParseNumberError",
|
|
43
|
+
"parse_with_limits",
|
|
44
|
+
"parse_int",
|
|
45
|
+
"parse_float",
|
|
46
|
+
"Pos",
|
|
47
|
+
"trim_comment",
|
|
48
|
+
"to_standardized_path",
|
|
49
|
+
"clean_filename",
|
|
50
|
+
]
|