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.
Files changed (77) hide show
  1. parsecore-1.0.0/LICENSE +21 -0
  2. parsecore-1.0.0/PKG-INFO +293 -0
  3. parsecore-1.0.0/README.md +264 -0
  4. parsecore-1.0.0/parsecore/Beatmap/__init__.py +50 -0
  5. parsecore-1.0.0/parsecore/Beatmap/beatmap.py +316 -0
  6. parsecore-1.0.0/parsecore/Beatmap/encode.py +653 -0
  7. parsecore-1.0.0/parsecore/Beatmap/reader.py +71 -0
  8. parsecore-1.0.0/parsecore/Beatmap/section/__init__.py +84 -0
  9. parsecore-1.0.0/parsecore/Beatmap/section/colors.py +109 -0
  10. parsecore-1.0.0/parsecore/Beatmap/section/difficulty.py +111 -0
  11. parsecore-1.0.0/parsecore/Beatmap/section/editor.py +100 -0
  12. parsecore-1.0.0/parsecore/Beatmap/section/enums.py +147 -0
  13. parsecore-1.0.0/parsecore/Beatmap/section/events.py +143 -0
  14. parsecore-1.0.0/parsecore/Beatmap/section/general.py +153 -0
  15. parsecore-1.0.0/parsecore/Beatmap/section/hit_objects/__init__.py +61 -0
  16. parsecore-1.0.0/parsecore/Beatmap/section/hit_objects/hit_objects.py +434 -0
  17. parsecore-1.0.0/parsecore/Beatmap/section/hit_objects/slider.py +482 -0
  18. parsecore-1.0.0/parsecore/Beatmap/section/metadata.py +115 -0
  19. parsecore-1.0.0/parsecore/Beatmap/section/timing_points.py +402 -0
  20. parsecore-1.0.0/parsecore/Beatmap/utils.py +196 -0
  21. parsecore-1.0.0/parsecore/Mods/__init__.py +331 -0
  22. parsecore-1.0.0/parsecore/Mods/_patches.py +322 -0
  23. parsecore-1.0.0/parsecore/Mods/acronym.py +64 -0
  24. parsecore-1.0.0/parsecore/Mods/game_mod.py +639 -0
  25. parsecore-1.0.0/parsecore/Mods/game_mod_intermode.py +639 -0
  26. parsecore-1.0.0/parsecore/Mods/game_mod_kind.py +49 -0
  27. parsecore-1.0.0/parsecore/Mods/game_mod_simple.py +90 -0
  28. parsecore-1.0.0/parsecore/Mods/game_mode.py +65 -0
  29. parsecore-1.0.0/parsecore/Mods/game_mods.py +362 -0
  30. parsecore-1.0.0/parsecore/Mods/game_mods_intermode.py +241 -0
  31. parsecore-1.0.0/parsecore/Mods/game_mods_legacy.py +419 -0
  32. parsecore-1.0.0/parsecore/Mods/generated_mods.py +1737 -0
  33. parsecore-1.0.0/parsecore/Performance/__init__.py +50 -0
  34. parsecore-1.0.0/parsecore/Performance/api.py +352 -0
  35. parsecore-1.0.0/parsecore/Performance/data/__init__.py +47 -0
  36. parsecore-1.0.0/parsecore/Performance/data/attributes.py +289 -0
  37. parsecore-1.0.0/parsecore/Performance/data/beatmap.py +399 -0
  38. parsecore-1.0.0/parsecore/Performance/data/hit_objects.py +75 -0
  39. parsecore-1.0.0/parsecore/Performance/data/mode.py +31 -0
  40. parsecore-1.0.0/parsecore/Performance/data/mods.py +214 -0
  41. parsecore-1.0.0/parsecore/Performance/data/score_state.py +103 -0
  42. parsecore-1.0.0/parsecore/Performance/rulesets/__init__.py +23 -0
  43. parsecore-1.0.0/parsecore/Performance/rulesets/catch/__init__.py +23 -0
  44. parsecore-1.0.0/parsecore/Performance/rulesets/catch/convert.py +220 -0
  45. parsecore-1.0.0/parsecore/Performance/rulesets/catch/difficulty.py +152 -0
  46. parsecore-1.0.0/parsecore/Performance/rulesets/catch/gradual.py +200 -0
  47. parsecore-1.0.0/parsecore/Performance/rulesets/catch/hit_objects.py +350 -0
  48. parsecore-1.0.0/parsecore/Performance/rulesets/catch/hitresult_generator.py +337 -0
  49. parsecore-1.0.0/parsecore/Performance/rulesets/catch/performance.py +170 -0
  50. parsecore-1.0.0/parsecore/Performance/rulesets/catch/skills.py +249 -0
  51. parsecore-1.0.0/parsecore/Performance/rulesets/mania/__init__.py +23 -0
  52. parsecore-1.0.0/parsecore/Performance/rulesets/mania/convert.py +1240 -0
  53. parsecore-1.0.0/parsecore/Performance/rulesets/mania/difficulty.py +124 -0
  54. parsecore-1.0.0/parsecore/Performance/rulesets/mania/gradual.py +24 -0
  55. parsecore-1.0.0/parsecore/Performance/rulesets/mania/hit_objects.py +47 -0
  56. parsecore-1.0.0/parsecore/Performance/rulesets/mania/hitresult_generator.py +192 -0
  57. parsecore-1.0.0/parsecore/Performance/rulesets/mania/performance.py +117 -0
  58. parsecore-1.0.0/parsecore/Performance/rulesets/mania/skills.py +346 -0
  59. parsecore-1.0.0/parsecore/Performance/rulesets/osu/__init__.py +23 -0
  60. parsecore-1.0.0/parsecore/Performance/rulesets/osu/convert.py +211 -0
  61. parsecore-1.0.0/parsecore/Performance/rulesets/osu/difficulty.py +533 -0
  62. parsecore-1.0.0/parsecore/Performance/rulesets/osu/gradual.py +24 -0
  63. parsecore-1.0.0/parsecore/Performance/rulesets/osu/hit_objects.py +711 -0
  64. parsecore-1.0.0/parsecore/Performance/rulesets/osu/hitresult_generator.py +218 -0
  65. parsecore-1.0.0/parsecore/Performance/rulesets/osu/legacy_score.py +311 -0
  66. parsecore-1.0.0/parsecore/Performance/rulesets/osu/performance.py +924 -0
  67. parsecore-1.0.0/parsecore/Performance/rulesets/osu/skills.py +1813 -0
  68. parsecore-1.0.0/parsecore/Performance/rulesets/taiko/__init__.py +23 -0
  69. parsecore-1.0.0/parsecore/Performance/rulesets/taiko/convert.py +191 -0
  70. parsecore-1.0.0/parsecore/Performance/rulesets/taiko/difficulty.py +146 -0
  71. parsecore-1.0.0/parsecore/Performance/rulesets/taiko/gradual.py +24 -0
  72. parsecore-1.0.0/parsecore/Performance/rulesets/taiko/hit_objects.py +62 -0
  73. parsecore-1.0.0/parsecore/Performance/rulesets/taiko/hitresult_generator.py +105 -0
  74. parsecore-1.0.0/parsecore/Performance/rulesets/taiko/performance.py +257 -0
  75. parsecore-1.0.0/parsecore/Performance/rulesets/taiko/skills.py +1262 -0
  76. parsecore-1.0.0/parsecore/Performance/utils.py +527 -0
  77. parsecore-1.0.0/pyproject.toml +64 -0
@@ -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.
@@ -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
+ ![ParseCore](https://i.imgur.com/35asYBQ.jpeg)
32
+
33
+ **Python library for osu! beatmap parsing, mod handling, and performance point calculation.**
34
+
35
+ [![PyPI Version](https://img.shields.io/pypi/v/parsecore?style=for-the-badge&color=pink)](https://pypi.org/project/parsecore/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/parsecore?style=for-the-badge&color=blue)](https://pypi.org/project/parsecore/)
37
+ [![License](https://img.shields.io/github/license/O-Lib/parsecore?style=for-the-badge&color=green)](LICENSE)
38
+ [![Typing](https://img.shields.io/badge/typing-checked-blue?style=for-the-badge)](https://mypy-lang.org/)
39
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?style=for-the-badge&logo=pre-commit)](https://pre-commit.com/)
40
+ [![Code Style](https://img.shields.io/badge/code%20style-ruff-orange?style=for-the-badge)](https://docs.astral.sh/ruff/)
41
+ [![Discord](https://img.shields.io/discord/1499516844711608350?style=for-the-badge&logo=discord&label=discord&color=5865F2)](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>&copy 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
+ ![ParseCore](https://i.imgur.com/35asYBQ.jpeg)
4
+
5
+ **Python library for osu! beatmap parsing, mod handling, and performance point calculation.**
6
+
7
+ [![PyPI Version](https://img.shields.io/pypi/v/parsecore?style=for-the-badge&color=pink)](https://pypi.org/project/parsecore/)
8
+ [![Python](https://img.shields.io/pypi/pyversions/parsecore?style=for-the-badge&color=blue)](https://pypi.org/project/parsecore/)
9
+ [![License](https://img.shields.io/github/license/O-Lib/parsecore?style=for-the-badge&color=green)](LICENSE)
10
+ [![Typing](https://img.shields.io/badge/typing-checked-blue?style=for-the-badge)](https://mypy-lang.org/)
11
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?style=for-the-badge&logo=pre-commit)](https://pre-commit.com/)
12
+ [![Code Style](https://img.shields.io/badge/code%20style-ruff-orange?style=for-the-badge)](https://docs.astral.sh/ruff/)
13
+ [![Discord](https://img.shields.io/discord/1499516844711608350?style=for-the-badge&logo=discord&label=discord&color=5865F2)](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>&copy 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
+ ]