Skip to content

Commit

Permalink
Merge pull request #30 from kszlim/restructure
Browse files Browse the repository at this point in the history
osrparse 6.0.0
  • Loading branch information
tybug authored Jan 12, 2022
2 parents 39a1b88 + 70dee37 commit 64e1c93
Show file tree
Hide file tree
Showing 14 changed files with 863 additions and 529 deletions.
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

0 comments on commit 64e1c93

Please sign in to comment.