Skip to content

Commit

Permalink
Merge pull request #25 from circleguard/osrparse
Browse files Browse the repository at this point in the history
port circleparse changes
  • Loading branch information
tybug authored Jan 26, 2021
2 parents 491cc30 + 5575343 commit 40e8ec6
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 180 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.vscode/
*.DS_Store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
86 changes: 45 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,68 @@
[![Build Status](https://travis-ci.org/kszlim/osu-replay-parser.svg?branch=master)](https://travis-ci.org/kszlim/osu-replay-parser)
# osrparse, a parser for osu replays in Python
[![PyPi version](https://badge.fury.io/py/osrparse.svg)](https://pypi.org/project/osrparse/)
[![Build Status](https://travis-ci.org/kszlim/osu-replay-parse.svg?branch=master)](https://travis-ci.org/kszlim/osu-replay-parser)

This is a parser for osu! rhythm game replays as described by https://osu.ppy.sh/wiki/Osr_(file_format)
# osrparse, a python parser for osu! replays

This is a parser for osu! replay files (.osr) as described by <https://osu.ppy.sh/wiki/en/osu%21_File_Formats/Osr_%28file_format%29>.

## Installation
To install osrparse, simply:
```
$ pip install osrparse

To install, simply:

```sh
pip install osrparse
```

## Documentation

To parse a replay from a filepath:

```python
from osrparse import parse_replay_file

#returns instance of Replay
parse_replay_file("path_to_osr.osr")
# returns a Replay object
parse_replay_file("path/to/osr.osr")
```

To parse a replay from a bytestring:
To parse a replay from an lzma string (such as the one returned from the `/get_replay` osu! api endpoint):

```python
from osrparse import parse_replay

#returns instance of Replay given the replay data encoded as a bytestring
parse_replay(byteString)
# returns a Replay object that only has a `play_data` attribute
parse_replay(lzma_string, pure_lzma=True)
```

To check for a gamemode:
```python
from osrparse.enums import GameMode
if replay.game_mode is GameMode.Standard:
print("This is GameMode Standard indeed!")
```
Note that if you use the `/get_replay` endpoint to retrieve a replay, you must decode the response before passing it to osrparse, as the response is encoded in base 64 by default.

Replay objects provide the following fields:

Replay instances provide these fields
```python
self.game_mode #GameMode enum
self.game_version #Integer
self.beatmap_hash #String
self.player_name #String
self.replay_hash #String
self.number_300s #Integer
self.number_100s #Integer
self.number_50s #Integer
self.gekis #Integer
self.katus #Integer
self.misses #Integer
self.score #Integer
self.max_combo #Integer
self.is_perfect_combo #Boolean
self.mod_combination #frozenset of Mods
self.life_bar_graph #String, unparsed as of now
self.timestamp #Python Datetime object
self.play_data #List of ReplayEvent instances
self.game_mode # GameMode enum
self.game_version # int
self.beatmap_hash # str
self.player_name # str
self.replay_hash # str
self.number_300s # int
self.number_100s # int
self.number_50s # int
self.gekis # int
self.katus # int
self.misses # int
self.score # int
self.max_combo # int
self.is_perfect_combo # bool
self.mod_combination # Mod enum
self.life_bar_graph # str, currently unparsed
self.timestamp # datetime.datetime object
self.play_data # list[ReplayEvent]
```

ReplayEvent instances provide these fields
ReplayEvent objects provide the following fields:

```python
self.time_since_previous_action #Integer representing time in milliseconds
self.x #x axis location
self.y #y axis location
self.keys_pressed #bitwise sum of keys pressed, documented in OSR format page.
self.time_since_previous_action # int (in milliseconds)
self.x # x axis location
self.y # y axis location
self.keys_pressed # bitwise sum of keys pressed, documented in OSR format page
```
10 changes: 9 additions & 1 deletion osrparse/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
from .replay import parse_replay_file, parse_replay
from osrparse.enums import GameMode, Mod
from osrparse.parse import parse_replay_file, parse_replay
from osrparse.replay import Replay, ReplayEvent

__version__ = "4.0.0"


__all__ = ["GameMode", "Mod", "parse_replay_file", "parse_replay", "Replay",
"ReplayEvent"]
77 changes: 38 additions & 39 deletions osrparse/enums.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
from enum import Enum

from enum import Enum, IntFlag

class GameMode(Enum):
Standard = 0
Taiko = 1
CatchTheBeat = 2
Osumania = 3

STD = 0
TAIKO = 1
CTB = 2
MANIA = 3

class Mod(Enum):
NoMod = 0
NoFail = 1
Easy = 2
NoVideo = 4
Hidden = 8
HardRock = 16
SuddenDeath = 32
DoubleTime = 64
Relax = 128
HalfTime = 256
Nightcore = 512
Flashlight = 1024
Autoplay = 2048
SpunOut = 4096
Autopilot = 8192
Perfect = 16384
Key4 = 32768
Key5 = 65536
Key6 = 131072
Key7 = 262144
Key8 = 524288
keyMod = 1015808
FadeIn = 1048576
Random = 2097152
LastMod = 4194304
TargetPractice = 8388608
Key9 = 16777216
Coop = 33554432
Key1 = 67108864
Key3 = 134217728
Key2 = 268435456
class Mod(IntFlag):
NoMod = 0,
NoFail = 1 << 0,
Easy = 1 << 1,
TouchDevice = 1 << 2,
Hidden = 1 << 3,
HardRock = 1 << 4,
SuddenDeath = 1 << 5,
DoubleTime = 1 << 6,
Relax = 1 << 7,
HalfTime = 1 << 8,
Nightcore = 1 << 9,
Flashlight = 1 << 10,
Autoplay = 1 << 11,
SpunOut = 1 << 12,
Autopilot = 1 << 13,
Perfect = 1 << 14,
Key4 = 1 << 15,
Key5 = 1 << 16,
Key6 = 1 << 17,
Key7 = 1 << 18,
Key8 = 1 << 19,
FadeIn = 1 << 20,
Random = 1 << 21,
Cinema = 1 << 22,
Target = 1 << 23,
Key9 = 1 << 24,
KeyCoop = 1 << 25,
Key1 = 1 << 26,
Key3 = 1 << 27,
Key2 = 1 << 28,
ScoreV2 = 1 << 29,
Mirror = 1 << 30
51 changes: 51 additions & 0 deletions osrparse/parse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import os
from typing import Union

from osrparse.replay import Replay

def parse_replay(replay_data: str, pure_lzma: bool = False, decompressed_lzma: bool = False) -> Replay:
"""
Parses a Replay from the given replay data.
Args:
String replay_data: The replay data from either parsing an osr file or from the api get_replay endpoint.
Boolean pure_lzma: Whether replay_data conatins the entirety of an osr file, or only the lzma compressed
data containing the cursor movements and keyboard presses of the player.
If replay data was loaded from an osr, this value should be False, as an osr contains
more information than just the lzma, such as username and game version (see
https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osr_(file_format)). If replay data
was retrieved from the api, this value should be True, as the api only
returns the lzma data (see https://github.com/ppy/osu-api/wiki#apiget_replay)
Boolean decompressed_lzma: Whether replay_data is compressed lzma, or decompressed
(and decoded to ascii) lzma. For example, the following calls are equivalent:
```
>>> osrparse.parse_replay(lzma_string, pure_lzma=True)
```
and
```
>>> lzma_string = lzma.decompress(lzma_string).decode("ascii")
>>> osrparse.parse_replay(lzma_string, pure_lzma=True, decompressed_lzma=True)
```
This parameter only has an affect if ``pure_lzma`` is ``True``.
Returns:
A Replay object with the fields specific in the Replay's init method. If pure_lzma is False, all fields will
be filled (nonnull). If pure_lzma is True, only the play_data will be filled.
"""

return Replay(replay_data, pure_lzma, decompressed_lzma)

def parse_replay_file(replay_path: Union[os.PathLike, str], pure_lzma: bool = False) -> Replay:
"""
Parses a Replay from the file at the given path.
Args:
[String or Path]: A pathlike object representing the absolute path to the file to parse data from.
Boolean pure_lzma: False if the file contains data equivalent to an osr file (or is itself an osr file),
and True if the file contains only lzma data. See parse_replay documentation for
more information on the difference between these two and how each affect the
fields in the final Replay object.
"""

with open(replay_path, 'rb') as f:
data = f.read()
return parse_replay(data, pure_lzma)
Loading

0 comments on commit 40e8ec6

Please sign in to comment.