Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

osrparse 6.0.0 #30

Merged
merged 25 commits into from
Jan 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = docs
BUILDDIR = build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
155 changes: 31 additions & 124 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# 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>.
This is a parser for the ``.osr`` format for osu! replay files, as described by [the wiki](https://osu.ppy.sh/wiki/en/Client/File_formats/Osr_%28file_format%29).

## Installation

Expand All @@ -15,140 +15,47 @@ pip install osrparse

## Documentation

### Parsing
Please see the full documentation for a comprehensive guide: <https://kevin-lim.ca/osu-replay-parser/>. A quickstart follows below for the impatient, but you should read the full documentation if you are at all confused.

To parse a replay from a filepath:
### Quickstart

```python
from osrparse import parse_replay_file
from osrparse import Replay, parse_replay_data
# parse from a path
replay = Replay.from_path("path/to/osr.osr")

# returns a Replay object
replay = parse_replay_file("path/to/osr.osr")
```

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 a Replay object that only has a `play_data` attribute
replay = parse_replay(lzma_string, pure_lzma=True)
```
# or from an opened file object
with open("path/to/osr.osr") as f:
replay = Replay.from_file(f)

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.
# or from a string
with open("path/to/osr.osr") as f:
replay_string = f.read()
replay = Replay.from_string(replay_string)

### Dumping
# a replay has various attributes
r = replay
print(r.mode, r.game_version, r.beatmap_hash, r.username,
r.r_hash, r.count_300, r.count_100, r.count_50, r.count_geki,
r.count_miss, r.score, r.max_combo, r.perfect, r.mods,
r.life_bar_graph, r.timestamp, r.r_data, r.r_id, r.rng_seed)

Existing `Replay` objects can be "dumped" back to a `.osr` file:
# parse the replay data from api v1's /get_replay endpoint
lzma_string = retrieve_from_api()
replay_data = parse_replay_data(lzma_string)
# replay_data is a list of ReplayEvents

```python
# write a replay back to a path
replay.write_path("path/to/osr.osr")

replay.dump("path/to/osr.osr")
# or to an opened file object
with open("path/to/osr.osr") as f:
replay.dump(f)
```

You can also edit osr files by parsing a replay, editing an attribute, and dumping it back to its file:

```python
replay = parse_replay_file("path/to/osr.osr")
replay.player_name = "fake username"
replay.dump(""path/to/osr.osr")
```

### Attributes

`Replay` objects have the following attibutes:

```python
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
# list of either ReplayEventOsu, ReplayEventTaiko, ReplayEventCatch,
# or ReplayEventMania objects, depending on self.game_mode
self.play_data
```

`ReplayEventOsu` objects have the following attributes:

```python
self.time_delta # int, time since previous event in milliseconds
self.x # float, x axis location
self.y # float, y axis location
self.keys # Key enum, keys pressed
```

`ReplayEventTaiko` objects have the following attributes:

```python
self.time_delta # int, time since previous event in milliseconds
self.x # float, x axis location
self.keys # KeyTaiko enum, keys pressed
```
replay.write_file(f)

`ReplayEventCatch` objects have the following attributes:
# or to a string
packed = replay.pack()

```python
self.time_delta # int, time since previous event in milliseconds
self.x # float, x axis location
self.dashing # bool, whether the player was dashing or not
```

`ReplayEventMania` objects have the following attributes:

```python
self.time_delta # int, time since previous event in milliseconds
self.keys # KeyMania enum
```

The `Key` enums used in the above `ReplayEvent`s are defined as follows:

```python
class Key(IntFlag):
M1 = 1 << 0
M2 = 1 << 1
K1 = 1 << 2
K2 = 1 << 3
SMOKE = 1 << 4

class KeyTaiko(IntFlag):
LEFT_DON = 1 << 0
LEFT_KAT = 1 << 1
RIGHT_DON = 1 << 2
RIGHT_KAT = 1 << 3

class KeyMania(IntFlag):
K1 = 1 << 0
K2 = 1 << 1
K3 = 1 << 2
K4 = 1 << 3
K5 = 1 << 4
K6 = 1 << 5
K7 = 1 << 6
K8 = 1 << 7
K9 = 1 << 8
K10 = 1 << 9
K11 = 1 << 10
K12 = 1 << 11
K13 = 1 << 12
K14 = 1 << 13
K15 = 1 << 14
K16 = 1 << 15
K17 = 1 << 16
K18 = 1 << 17
# edited attributes are saved
replay.username = "fake username"
replay.write_path("path/to/new_osr.osr")
```
12 changes: 12 additions & 0 deletions docs/appendix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Appendix
========

Replay
------
.. automodule:: osrparse.replay
:members:

Utils
-----
.. automodule:: osrparse.utils
:members:
66 changes: 66 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

from osrparse import __version__

project = "osrparse"
copyright = "2022, Kevin Lim, Liam DeVoe"
author = "Kevin Lim, Liam DeVoe"
release = "v" + __version__
version = "v" + __version__
master_doc = 'index'

# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_show_copyright
html_show_copyright = False
# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_show_sphinx
html_show_sphinx = False

extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
"sphinx.ext.todo"
]

intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
# https://stackoverflow.com/a/37210251
autodoc_member_order = "bysource"

html_theme = "furo"

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []


# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ["_static"]

# references that we want to use easily in any file
rst_prolog = """
.. |Replay| replace:: :class:`~osrparse.replay.Replay`
.. |from_path| replace:: :func:`Replay.from_path() <osrparse.replay.Replay.from_path>`
.. |from_file| replace:: :func:`Replay.from_file() <osrparse.replay.Replay.from_file>`
.. |from_string| replace:: :func:`Replay.from_string() <osrparse.replay.Replay.from_string>`
.. |write_path| replace:: :func:`Replay.write_path() <osrparse.replay.Replay.write_path>`
.. |write_file| replace:: :func:`Replay.write_file() <osrparse.replay.Replay.write_file>`
.. |pack| replace:: :func:`Replay.pack() <osrparse.replay.Replay.pack>`
.. |parse_replay_data| replace:: :func:`parse_replay_data() <osrparse.replay.parse_replay_data>`

.. |br| raw:: html

<br />
"""

# linebreak workaround documented here
# https://stackoverflow.com/a/9664844/12164878
50 changes: 50 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
osrparse
==========

osrparse is a parser for the ``.osr`` format, as described `on the osu! wiki <https://osu.ppy.sh/wiki/en/Client/File_formats/Osr_%28file_format%29>`__.

osrparse is maintained by:

* `tybug <https://github.com/tybug>`__
* `kszlim <https://github.com/kszlim>`__

Installation
------------

osrparse can be installed from pip:

.. code-block:: console

$ pip install osrparse

Links
-----

| Github: https://github.com/kszlim/osu-replay-parser
| Documentation: https://kevin-lim.ca/osu-replay-parser/


..
couple notes about these toctrees - the first toctree is so our sidebar has
a link back to the index page. the ``self`` keyword comes with its share of
issues (https://github.com/sphinx-doc/sphinx/issues/2103), but none that matter
that much to us. It's better than using ``index`` which works but generates
many warnings when building.

Hidden toctrees appear on the sidebar but not as text on the table of contents
displayed on this page.

Contents
--------

.. toctree::
:hidden:

self

.. toctree::
:maxdepth: 2

parsing-replays
writing-replays
appendix
52 changes: 52 additions & 0 deletions docs/parsing-replays.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Parsing Replays
===============

Creating a Replay
-----------------

Depending on the type of data you have, a |Replay| can be created multiple ways, using either one of |from_path|, |from_file|, or |from_string|:

.. code-block:: python

from osrparse import Replay
# from a path
replay = Replay.from_path("path/to/osr.osr")

# or from an opened file object
with open("path/to/osr.osr") as f:
replay = Replay.from_file(f)

# or from a string
with open("path/to/osr.osr") as f:
replay_string = f.read()
replay = Replay.from_string(replay_string)

Most likely, you will be using |from_path| to create a |Replay|.

Parsing Just Replay Data
------------------------

Unfortunately, the `/get_replay <https://github.com/ppy/osu-api/wiki#apiget_replay>`__ endpoint of `osu!api v1 <https://github.com/ppy/osu-api/wiki>`__ does not return the full contents of a replay, but only the replay data potion. This means that you cannot create a full replay from the response of this endpoint.

For this, we provide |parse_replay_data|, a function that takes the response of this endpoint and returns List[:class:`~osrparse.utils.ReplayEvent`] (ie, the parsed replay data):

.. code-block:: python

from osrparse import parse_replay_data
import base64
import lzma

lzma_string = retrieve_from_api()
replay_data = parse_replay_data(lzma_string)
assert isinstance(replay_data[0], ReplayEvent)

# or parse an already decoded lzma string
lzma_string = retrieve_from_api()
lzma_string = base64.b64decode(lzma_string)
replay_data = parse_replay_data(lzma_string, decoded=True)

# or parse an already decoded and decompressed lzma string
lzma_string = retrieve_from_api()
lzma_string = base64.b64decode(lzma_string)
lzma_string = lzma.decompress(lzma_string).decode("ascii")
replay_data = parse_replay_data(lzma_string, decompressed=True)
Loading