More docs
This commit is contained in:
parent
b43f404526
commit
9b88f732de
BIN
docs/source/_static/NotoMemoSubset.woff
Normal file
BIN
docs/source/_static/NotoMemoSubset.woff
Normal file
Binary file not shown.
12
docs/source/_static/css/custom.css
Normal file
12
docs/source/_static/css/custom.css
Normal file
@ -0,0 +1,12 @@
|
||||
@font-face {
|
||||
font-family: "Noto-Memo";
|
||||
src: url("../NotoMemoSubset.woff");
|
||||
}
|
||||
|
||||
:root {
|
||||
--font-mono-jp: "Noto-Memo", monospace;
|
||||
}
|
||||
|
||||
.japanese-monospaced pre {
|
||||
font-family: var(--font-mono-jp);
|
||||
}
|
BIN
docs/source/_static/current_directory_in_cmd.exe's_prompt.png
Normal file
BIN
docs/source/_static/current_directory_in_cmd.exe's_prompt.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
docs/source/_static/jubeatools_--help.png
Normal file
BIN
docs/source/_static/jubeatools_--help.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
BIN
docs/source/_static/py_-m_pip_install_jubeatools.png
Normal file
BIN
docs/source/_static/py_-m_pip_install_jubeatools.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -1 +0,0 @@
|
||||
# API
|
8
docs/source/api/chart file formats.md
Normal file
8
docs/source/api/chart file formats.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Chart file formats
|
||||
|
||||
```{eval-rst}
|
||||
.. autofunction:: jubeatools.formats.guess::guess_format
|
||||
.. autoclass:: jubeatools.formats.format_names::Format
|
||||
.. autoclass:: jubeatools.formats.typing::Loader
|
||||
.. autoclass:: jubeatools.formats.typing::Dumper
|
||||
```
|
10
docs/source/api/index.md
Normal file
10
docs/source/api/index.md
Normal file
@ -0,0 +1,10 @@
|
||||
# API
|
||||
|
||||
Look in here if you need information about a specific class, function or object
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
song
|
||||
loaders and dumpers
|
||||
chart file formats
|
||||
```
|
139
docs/source/api/loaders and dumpers.md
Normal file
139
docs/source/api/loaders and dumpers.md
Normal file
@ -0,0 +1,139 @@
|
||||
# Loaders and dumpers
|
||||
|
||||
## Collections
|
||||
|
||||
```{eval-rst}
|
||||
.. autodata:: jubeatools.formats.loaders_and_dumpers.LOADERS
|
||||
:no-value:
|
||||
.. autodata:: jubeatools.formats.loaders_and_dumpers.DUMPERS
|
||||
:no-value:
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### jubeat analyzer
|
||||
|
||||
```{eval-rst}
|
||||
.. autofunction:: jubeatools.formats.jubeat_analyser.memo.load::load_memo
|
||||
|
||||
.. autofunction:: jubeatools.formats.jubeat_analyser.memo.dump.dump_memo
|
||||
|
||||
Default Filename Template
|
||||
``{title} {difficulty_number}.txt``
|
||||
|
||||
:kwarg bool circle_free: Use circle-free symbols for long note ends
|
||||
|
||||
.. autofunction:: jubeatools.formats.jubeat_analyser.memo1.load::load_memo1
|
||||
|
||||
.. autofunction:: jubeatools.formats.jubeat_analyser.memo1.dump::dump_memo1
|
||||
|
||||
Default Filename Template
|
||||
``{title} {difficulty_number}.txt``
|
||||
|
||||
:keyword bool circle_free: Use circle-free symbols for long note ends
|
||||
|
||||
.. autofunction:: jubeatools.formats.jubeat_analyser.memo2.load::load_memo2
|
||||
|
||||
.. autofunction:: jubeatools.formats.jubeat_analyser.memo2.dump::dump_memo2
|
||||
|
||||
Default Filename Template
|
||||
``{title} {difficulty_number}.txt``
|
||||
|
||||
:keyword bool circle_free: Use circle-free symbols for long note ends
|
||||
|
||||
.. autofunction:: jubeatools.formats.jubeat_analyser.mono_column.load::load_mono_column
|
||||
|
||||
.. autofunction:: jubeatools.formats.jubeat_analyser.mono_column.dump::dump_mono_column
|
||||
|
||||
Default Filename Template
|
||||
``{title} {difficulty_number}.txt``
|
||||
|
||||
:keyword bool circle_free: Use circle-free symbols for long note ends
|
||||
```
|
||||
|
||||
### konami
|
||||
|
||||
```{eval-rst}
|
||||
.. autofunction:: jubeatools.formats.konami.eve.load::load_eve
|
||||
|
||||
:kwarg int beat_snap: Snap all events to nearest 1/``beat_snap`` beat
|
||||
|
||||
.. autofunction:: jubeatools.formats.konami.eve.dump::dump_eve
|
||||
|
||||
Default Filename Template
|
||||
``{difficulty:l}.eve``
|
||||
|
||||
.. autofunction:: jubeatools.formats.konami.jbsq.load::load_jbsq
|
||||
|
||||
:kwarg int beat_snap: Snap all events to nearest 1/``beat_snap`` beat
|
||||
|
||||
.. autofunction:: jubeatools.formats.konami.jbsq.dump::dump_jbsq
|
||||
|
||||
Default Filename Template
|
||||
``seq_{difficulty:l}.jbsq``
|
||||
```
|
||||
|
||||
### malody
|
||||
|
||||
```{eval-rst}
|
||||
.. autofunction:: jubeatools.formats.malody.load::load_malody
|
||||
|
||||
.. autofunction:: jubeatools.formats.malody.dump::dump_malody
|
||||
|
||||
Default Filename Template
|
||||
``{difficulty:l}.mc``
|
||||
```
|
||||
|
||||
### memon
|
||||
|
||||
```{eval-rst}
|
||||
.. autofunction:: jubeatools.formats.memon.v0.load::load_memon_legacy
|
||||
|
||||
:kwarg bool merge: When called on a folder, try to merge all the .memon
|
||||
files found into a single :py:class:`Song <jubeatools.song.Song>` object
|
||||
|
||||
.. autofunction:: jubeatools.formats.memon.v0.dump::dump_memon_legacy
|
||||
|
||||
Default Filename Template
|
||||
``{title}.memon``
|
||||
|
||||
.. autofunction:: jubeatools.formats.memon.v0.load::load_memon_0_1_0
|
||||
|
||||
:kwarg bool merge: When called on a folder, try to merge all the .memon
|
||||
files found into a single :py:class:`Song <jubeatools.song.Song>` object
|
||||
|
||||
.. autofunction:: jubeatools.formats.memon.v0.dump::dump_memon_0_1_0
|
||||
|
||||
Default Filename Template
|
||||
``{title}.memon``
|
||||
|
||||
.. autofunction:: jubeatools.formats.memon.v0.load::load_memon_0_2_0
|
||||
|
||||
:kwarg bool merge: When called on a folder, try to merge all the .memon
|
||||
files found into a single :py:class:`Song <jubeatools.song.Song>` object
|
||||
|
||||
.. autofunction:: jubeatools.formats.memon.v0.dump::dump_memon_0_2_0
|
||||
|
||||
Default Filename Template
|
||||
``{title}.memon``
|
||||
|
||||
.. autofunction:: jubeatools.formats.memon.v0.load::load_memon_0_3_0
|
||||
|
||||
:kwarg bool merge: When called on a folder, try to merge all the .memon
|
||||
files found into a single :py:class:`Song <jubeatools.song.Song>` object
|
||||
|
||||
.. autofunction:: jubeatools.formats.memon.v0.dump::dump_memon_0_3_0
|
||||
|
||||
Default Filename Template
|
||||
``{title}.memon``
|
||||
|
||||
.. autofunction:: jubeatools.formats.memon.v1.load::load_memon_1_0_0
|
||||
|
||||
:kwarg bool merge: When called on a folder, try to merge all the .memon
|
||||
files found into a single :py:class:`Song <jubeatools.song.Song>` object
|
||||
|
||||
.. autofunction:: jubeatools.formats.memon.v1.dump::dump_memon_1_0_0
|
||||
|
||||
Default Filename Template
|
||||
``{title}.memon``
|
||||
```
|
7
docs/source/api/song.md
Normal file
7
docs/source/api/song.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Song and chart data model
|
||||
|
||||
## The `song` module
|
||||
|
||||
```{eval-rst}
|
||||
.. automodule:: jubeatools.song
|
||||
```
|
2
docs/source/cli.md
Normal file
2
docs/source/cli.md
Normal file
@ -0,0 +1,2 @@
|
||||
# Command-line Interface
|
||||
|
@ -3,28 +3,82 @@
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = 'jubeatools'
|
||||
copyright = '2022, Stepland'
|
||||
author = 'Stepland'
|
||||
release = '1.4.0'
|
||||
project = "jubeatools"
|
||||
copyright = "2022, Stepland"
|
||||
author = "Stepland"
|
||||
release = "1.4.0"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = ["myst_parser"]
|
||||
extensions = [
|
||||
"myst_parser",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.intersphinx"
|
||||
]
|
||||
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = []
|
||||
|
||||
nitpicky = True
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/3', None)
|
||||
}
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = 'furo'
|
||||
html_static_path = ['_static']
|
||||
html_theme = "furo"
|
||||
html_static_path = ["_static"]
|
||||
|
||||
myst_enable_extensions = ["colon_fence"]
|
||||
# These paths are either relative to html_static_path
|
||||
# or fully qualified paths (eg. https://...)
|
||||
html_css_files = [
|
||||
'css/custom.css',
|
||||
]
|
||||
|
||||
myst_enable_extensions = [
|
||||
# "amsmath",
|
||||
"colon_fence",
|
||||
# "deflist",
|
||||
# "dollarmath",
|
||||
"fieldlist",
|
||||
# "html_admonition",
|
||||
# "html_image",
|
||||
# "linkify",
|
||||
# "replacements",
|
||||
# "smartquotes",
|
||||
# "strikethrough",
|
||||
# "substitution",
|
||||
# "tasklist",
|
||||
]
|
||||
|
||||
# Autodoc options
|
||||
# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_mock_imports
|
||||
|
||||
autodoc_mock_imports = [
|
||||
"more_itertools",
|
||||
"sortedcontainers",
|
||||
"parsimonious",
|
||||
"constraint",
|
||||
"construct",
|
||||
"construct_typed",
|
||||
"simplejson",
|
||||
"marshmallow",
|
||||
"marshmallow_dataclass"
|
||||
]
|
||||
|
||||
autodoc_default_options = {
|
||||
"members": None, # document all members
|
||||
"member-order": "bysource", # respect source declaration order
|
||||
"undoc-members": True, # add an entry for members that lack docstrings
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
# Converting Charts
|
||||
|
||||
Once jubeatools is [installed](how to install jubeatools.md), you can use its command-line interface.
|
||||
|
||||
The general usage is as follows :
|
||||
|
||||
```console
|
||||
$ jubeatools (input) (output) -f (output format) (options)
|
||||
```
|
||||
|
||||
## Input
|
||||
|
134
docs/source/how to convert charts.md
Normal file
134
docs/source/how to convert charts.md
Normal file
@ -0,0 +1,134 @@
|
||||
# Converting charts with jubeatools
|
||||
|
||||
This page explains how to use jubeatools via command line for people who are
|
||||
unfamiliar with the terminal.
|
||||
|
||||
Make sure you [installed Python and jubeatools](<how to install jubeatools.md>)
|
||||
before reading this page.
|
||||
|
||||
**jubeatools has no graphical user interface**, to use it, you have to type
|
||||
commands in a terminal. If you are completely new to the terminal, this next
|
||||
section was made for you, otherwise you can skip it.
|
||||
|
||||
## Primer on the terminal
|
||||
|
||||
The terminal is a text interface to your computer, you type in a command,
|
||||
it does something in response, and maybe displays some text to tell you what
|
||||
it just did.
|
||||
|
||||
### It's basically File Explorer but in text
|
||||
|
||||
At any given time while using the terminal you will be "in" a folder, just like
|
||||
when using File Explorer on Windows.
|
||||
|
||||
On Windows, in both `cmd.exe` and PowerShell, the current directory
|
||||
(this folder you are *in*) is displayed on the left as part of the *prompt*
|
||||
|
||||
```{figure-md}
|
||||
:class: myclass
|
||||
|
||||
![](_static/current_directory_in_cmd.exe's_prompt.png)
|
||||
|
||||
Here as you can see I'm inside `C:\Users\Stepland`
|
||||
```
|
||||
|
||||
On Linux and macOS, the current directory might not be displayed in the prompt.
|
||||
You can use the `pwd` command to display it (`pwd` as in **P**rint **W**orking
|
||||
**D**irectory)
|
||||
|
||||
### Navigating with `dir`/`ls` and `cd`
|
||||
|
||||
Now that you know how to see where you are in, let's learn how to move around
|
||||
and see what things are there.
|
||||
|
||||
To see what's in the current folder, use :
|
||||
|
||||
- `dir` on Windows (**dir**ectory)
|
||||
- `ls` on macOS/Linux (**l**i**s**t)
|
||||
|
||||
To move to a different folder, use `cd` (**c**hange **d**irectory) (same on all
|
||||
3 OSes).
|
||||
|
||||
`cd` accepts two kinds of paths :
|
||||
- absolute paths (paths that start with `C:\` on Windows or `/` on macOS/Linux)
|
||||
- relative paths (relative to the current folder) : if there is a sub-folder
|
||||
named `Photos` in your home folder *and* you are already in you home folder
|
||||
on the terminal, you can just do `cd Photos` instead of giving the absolute
|
||||
path to the `Photos` folder
|
||||
|
||||
On all 3 OSes, there's a special "fake" folder called `..` (double dot). It's
|
||||
there to help you move *out* of the current folder. You can do `cd ..` from
|
||||
anywhere and it will take you one folder up the hirerarchy.
|
||||
|
||||
### Tab triggers autocomplete
|
||||
|
||||
Typing long paths is tedious, luckily the terminal can help you. Start typing
|
||||
your command (`cd ...something...`) then hit Tab before finishing, the terminal will
|
||||
try to the fill in the rest of the folder or file name you were trying to type,
|
||||
this can be chained multiple times in a row to type a longer path :
|
||||
|
||||
- Type the first few characters of the folder
|
||||
- Tab (maybe more than once if several folders match)
|
||||
- Type the first few characters of the sub-folder
|
||||
- Tab again
|
||||
|
||||
etc etc ...
|
||||
|
||||
## Using jubeatools in a terminal
|
||||
|
||||
Now that you know the commands to move around, let's learn how to use
|
||||
jubeatools itself as a command.
|
||||
|
||||
jubeatools expects arguments like this :
|
||||
|
||||
```console
|
||||
$ jubeatools (input) (output) -f (output format) (options)
|
||||
```
|
||||
|
||||
Let's break this down :
|
||||
|
||||
- `$` is a common way to represent a terminal *prompt* in computer litterature,
|
||||
it is not part of the actual command, you don't have to type it. It's just
|
||||
some sort of punctuation mark to remind you that whatever comes after it is a
|
||||
command, and can be typed in a terminal.
|
||||
- `jubeatools` is the command
|
||||
- `(input)` is the path of the chart file you want to convert
|
||||
- `(output)` is the path of the *converted* chart file you want to create
|
||||
- `-f (output format)` is the output format you want jubeatools to use
|
||||
- `(options)` are the extra options you might want to use (see [](cli.md))
|
||||
|
||||
### Example
|
||||
|
||||
Say you have a memo file called `sigsig.txt` and you want to convert it to
|
||||
memon 1.0.0, then you would open up a terminal, navigate to the folder where
|
||||
`sigsig.txt` is, then type the following command :
|
||||
|
||||
```console
|
||||
$ jubeatools sigsig.txt sigsig.memon -f memon:v1.0.0
|
||||
```
|
||||
|
||||
This will create a file called `sigsig.memon` in the same folder.
|
||||
|
||||
### Formats
|
||||
|
||||
Each format jubeatools supports has a precise name you need to use for the
|
||||
`-f` option :
|
||||
|
||||
| | | name |
|
||||
|-----------------|----------------------|----------------|
|
||||
| memon | v1.0.0 | `memon:v1.0.0` |
|
||||
| | v0.3.0 | `memon:v0.3.0` |
|
||||
| | v0.2.0 | `memon:v0.2.0` |
|
||||
| | v0.1.0 | `memon:v0.1.0` |
|
||||
| | legacy | `memon:legacy` |
|
||||
| jubeat analyser | #memo2 | `memo2` |
|
||||
| | #memo1 | `memo1` |
|
||||
| | #memo | `memo` |
|
||||
| | mono-column (1列形式) | `mono-column` |
|
||||
| jubeat (arcade) | .eve | `eve` |
|
||||
| jubeat plus | .jbsq | `jbsq` |
|
||||
| malody | .mc (Pad Mode) | `malody` |
|
||||
|
||||
### Options
|
||||
|
||||
Options are documented here : [](cli.md)
|
@ -18,13 +18,13 @@ Open the installer
|
||||
|
||||
On the first page be sure the tick the box that says `Add Python 3.(number) to PATH`
|
||||
|
||||
:::{figure-md}
|
||||
```{figure-md}
|
||||
:class: myclass
|
||||
|
||||
![](_static/Add_Python_3.10_to_PATH.png)
|
||||
|
||||
Why isn't this on by default ?
|
||||
:::
|
||||
```
|
||||
|
||||
Click `Install Now`
|
||||
|
||||
@ -32,18 +32,117 @@ Once it's done, let's check that everything went fine.
|
||||
|
||||
Open up any terminal (for instance you can search "cmd" in the start menu)
|
||||
|
||||
Once at the prompt type in `py --version` then hit enter.
|
||||
Once at the prompt type in `py --version` then hit Enter.
|
||||
|
||||
If everything went right it should answer back with the version number you just
|
||||
installed.
|
||||
|
||||
:::{figure-md}
|
||||
```{figure-md}
|
||||
:class: myclass
|
||||
|
||||
![](_static/py_--version.png)
|
||||
|
||||
Here's what it's supposed to look like in `cmd.exe`
|
||||
:::
|
||||
```
|
||||
|
||||
### macOS
|
||||
|
||||
Use [brew](https://brew.sh/) to install python (at least 3.9)
|
||||
|
||||
### Linux
|
||||
|
||||
Python most-likely already came installed on you computer, but depending on your
|
||||
chosen distribution the Python version your system ships with may be too old,
|
||||
jubeatools requires Python 3.9
|
||||
|
||||
You can check which python version you have by opening up a terminal and typing
|
||||
|
||||
```console
|
||||
$ python --version
|
||||
```
|
||||
|
||||
or, if that doesn't work
|
||||
|
||||
```console
|
||||
$ python3 --version
|
||||
```
|
||||
|
||||
To install a more recent version on python on Debian and its variants you
|
||||
should be able to do
|
||||
|
||||
```console
|
||||
$ sudo apt install python3.9
|
||||
```
|
||||
|
||||
If you use Fedora, Arch, or anything else, check your distro's documentation
|
||||
to get the precise package name, the available versions, and the actual
|
||||
command you need to execute.
|
||||
|
||||
## Installing jubeatools
|
||||
|
||||
Now that Python is on your machine, we are going to use
|
||||
[`pip`](https://en.wikipedia.org/wiki/Pip_(package_manager)), Python's own
|
||||
package manager, to download and install jubeatools.
|
||||
|
||||
Open up a terminal or Command Prompt, then type :
|
||||
|
||||
- **For Windows** : `py -m pip install jubeatools`
|
||||
- **For Linux/macOS** : `pip install jubeatools`
|
||||
|
||||
```{figure-md}
|
||||
:class: myclass
|
||||
|
||||
![](_static/py_-m_pip_install_jubeatools.png)
|
||||
|
||||
Here's how it would look like on Windows with `cmd.exe`
|
||||
```
|
||||
|
||||
Hit Enter
|
||||
|
||||
`pip` is then going to blurt out the name and version of every
|
||||
package and sub-package it's installing. You don't really need to pay much
|
||||
attention to that. Just have a quick glance at the last few lines to make sure
|
||||
`pip` says it successfully installed some packages and not that some error made
|
||||
it stop.
|
||||
|
||||
## Checking that jubeatools works
|
||||
|
||||
While in a terminal or command prompt, type `jubeatools --help`, then hit Enter.
|
||||
|
||||
jubeatools should answer with something like :
|
||||
|
||||
```none
|
||||
Usage: jubeatools [OPTIONS] SRC DST
|
||||
|
||||
Convert SRC to DST using the format specified by -f
|
||||
|
||||
Options:
|
||||
--input-format [eve|jbsq|malody|memon:legacy|memon:v0.1.0|memon:v0.2.0|memon:v0.3.0|memon:v1.0.0|mono-column|memo|memo1|memo2]
|
||||
Force jubeatools to read the input
|
||||
file/folder as the given format.If this
|
||||
option is not used jubeatools will try to
|
||||
guess the format
|
||||
-f, --format [eve|jbsq|malody|memon:legacy|memon:v0.1.0|memon:v0.2.0|memon:v0.3.0|memon:v1.0.0|mono-column|memo|memo1|memo2]
|
||||
Output file format [required]
|
||||
--circlefree Use #circlefree=1 for jubeat analyser
|
||||
formats
|
||||
--beat-snap INTEGER RANGE For compatible input formats, snap all notes
|
||||
and bpm changes to the nearest 1/beat_snap
|
||||
beat [x>=1]
|
||||
--merge For memon, if called on a folder, merge all
|
||||
the .memon files found
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
If you see this help text, jubeatools is installed !
|
||||
|
||||
Congrats !
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -2,21 +2,20 @@
|
||||
|
||||
A toolbox for jubeat file formats
|
||||
|
||||
## For Charters
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
---
|
||||
:caption: For Charters
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
how to install jubeatools
|
||||
converting_charts
|
||||
how to convert charts
|
||||
```
|
||||
|
||||
## For Developpers
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
---
|
||||
api
|
||||
:caption: For Developers
|
||||
:maxdepth: 1
|
||||
:hidden:
|
||||
library/index
|
||||
cli
|
||||
api/index
|
||||
```
|
||||
|
||||
|
||||
|
10
docs/source/library/index.md
Normal file
10
docs/source/library/index.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Using jubeatools as a library
|
||||
|
||||
Look in here if you want to use jubeatools as a library in your own python code
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
reading chart files
|
||||
song object
|
||||
writing chart files
|
||||
```
|
92
docs/source/library/reading chart files.md
Normal file
92
docs/source/library/reading chart files.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Reading charts
|
||||
|
||||
Reading a chart file is done by using a *loader*, it's a function that takes in
|
||||
a path and returns a [`Song`](Song) object.
|
||||
|
||||
Let's go over how you can import then use one.
|
||||
|
||||
## Guessing the format
|
||||
|
||||
If you need to read chart files whose format you don't know in advance, you can
|
||||
use the [`guess_format()`](guess_format) function. It's the same function the
|
||||
CLI uses under the hood when you don't specify the input format yourself.
|
||||
|
||||
```python
|
||||
>>> from pathlib import Path
|
||||
>>> from jubeatools.formats.guess import guess_format
|
||||
>>> guess_format(Path("sigsig.txt"))
|
||||
<Format.MEMO_2: 'memo2'>
|
||||
```
|
||||
|
||||
```{warning}
|
||||
[`guess_format()`](guess_format) makes an honest attempt at guessing but it
|
||||
doesn't work for all files 100% of the time. If you *know* that you will only
|
||||
ever read a single format, I **strongly** recommend you import the correct
|
||||
loader directly
|
||||
```
|
||||
|
||||
## Importing a loader
|
||||
|
||||
Loaders are all defined somewhere in the `jubeatools.formats` module. The
|
||||
precise import path of each loader is documented here :
|
||||
[](<../api/loaders and dumpers.md>)
|
||||
|
||||
For example you can import the loader for `#memo2` files like this :
|
||||
|
||||
```python
|
||||
from jubeatools.formats.jubeat_analyser import load_memo2
|
||||
```
|
||||
|
||||
:::{tip}
|
||||
If you don't want to have to write down the full import path or if you don't
|
||||
know which format you will have to read in advance, you can import
|
||||
[`LOADERS`](LOADERS) from `jubeatools.formats` to query the correct loader
|
||||
based on the format. It's a dict that maps [`Format`](Format) enum members to
|
||||
their associated loader.
|
||||
|
||||
```python
|
||||
>>> from jubeatools.formats import LOADERS, Format
|
||||
>>> load_memo2 = LOADERS[Format.MEMO_2]
|
||||
```
|
||||
:::
|
||||
|
||||
## Using a loader
|
||||
|
||||
Once you've imported or queried your loader, you can use it as follows :
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
path = Path("my_file.txt")
|
||||
song = load_thing(path)
|
||||
```
|
||||
|
||||
:::{note}
|
||||
Some loaders accept extra options as keyword arguments :
|
||||
|
||||
```python
|
||||
song = load_thing(path, splines="reticulate")
|
||||
```
|
||||
|
||||
These extra options are specific to each loader and are documented in
|
||||
[](<../api/loaders and dumpers.md>).
|
||||
:::
|
||||
|
||||
Check out [](<song object.md>) to see how the chart data is organized in a
|
||||
[`Song`](Song) object.
|
||||
|
||||
## The Loader Protocol
|
||||
|
||||
Loaders have a *uniform* interface, in other words they are all compatible with
|
||||
the same function signature. It's the Loader *Protocol* :
|
||||
|
||||
```{py:function} load(path: pathlib.Path, **kwargs: Any) -> Song
|
||||
|
||||
Read what's in `path` and turn it into a `Song` object.
|
||||
|
||||
Possibly takes in some options via the kwargs.
|
||||
|
||||
:param pathlib.Path path: path to a file or folder to be read
|
||||
:param Any **kwargs: Format-specific options
|
||||
:return: the Song instance read from the file(s)
|
||||
:rtype: Song
|
||||
```
|
346
docs/source/library/song object.md
Normal file
346
docs/source/library/song object.md
Normal file
@ -0,0 +1,346 @@
|
||||
# `Song` objects
|
||||
|
||||
The [`Song`](Song) object is the main data model of jubeatools, it holds all
|
||||
the data jubeatools can make sense of in a chart file : metadata,
|
||||
timing information, and a set of charts.
|
||||
|
||||
## Reading properties
|
||||
|
||||
Loaders all return a [`Song`](Song) object, but how is the information stored
|
||||
inside of it ?
|
||||
|
||||
### Metadata
|
||||
|
||||
The song metadata is accessible via the [`Song.metadata`](Song.metadata)
|
||||
attribute
|
||||
|
||||
```python
|
||||
>>> sigsig = load_memo2(Path("sigsig.txt"))
|
||||
>>> sigsig.metadata.title
|
||||
'SigSig'
|
||||
>>> sigsig.metadata.artist
|
||||
'kors k'
|
||||
```
|
||||
|
||||
See {py:class}`jubeatools.song.Metadata` for a complete list of the existing
|
||||
fields
|
||||
|
||||
### Charts
|
||||
|
||||
Charts are stored in the [`Song.charts`](Song.charts) attribute.
|
||||
|
||||
It's a dict that maps difficulty names (like `"ADV"` or `"EXT"`) to
|
||||
[`Chart`](Chart) objects
|
||||
|
||||
```python
|
||||
>>> sigsig.charts
|
||||
{
|
||||
'EXT': Chart(
|
||||
level=Decimal('9.1'),
|
||||
timing=Timing(
|
||||
events=(
|
||||
BPMEvent(time=Fraction(0), BPM=Decimal('179'))
|
||||
),
|
||||
beat_zero_offset=Decimal('2.32')
|
||||
),
|
||||
hakus=None,
|
||||
notes=[...]
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
### Timing
|
||||
|
||||
Timing information is split between a common timing object and per-chart timing
|
||||
objects. The common timing object acts as a fallback in case a chart doesn't
|
||||
have its own timing object.
|
||||
|
||||
The common timing object is stored in the [`common_timing`](Song.common_timing)
|
||||
attribute of [`Song`](Song) objects, while the chart timing is stored in the
|
||||
[`timing`](Chart.timing) attribute of [`Chart`](Chart) objects.
|
||||
|
||||
```python
|
||||
>>> sigsig.chart.timing
|
||||
Timing(
|
||||
events=BPMEvent(time=Fraction(0), BPM=Decimal('179')),
|
||||
beat_zero_offset=Decimal('2.32')
|
||||
)
|
||||
```
|
||||
|
||||
## Constructing `Song` objects
|
||||
|
||||
If you want to programatically create [`Song`](Song) objects directly in
|
||||
python code you have to construct a lot of different sub-objects.
|
||||
|
||||
Let's start with the most basic ones and work our way up the a full
|
||||
[`Song`](Song) object.
|
||||
|
||||
All of the classes in the following sections are defined in the
|
||||
`jubeatools.song` module. You should import them from this module.
|
||||
|
||||
### Beats
|
||||
|
||||
All musical time points and durations in jubeatools are stored as a fractional
|
||||
amount of beats in a [`BeatsTime`](BeatsTime) object.
|
||||
|
||||
[`BeatsTime`](BeatsTime) is just a renamed copy of the
|
||||
[`Fraction`](fractions.Fraction) class from python's standard library.
|
||||
|
||||
```python
|
||||
beat_zero = BeatsTime(0)
|
||||
half_a_beat = BeatsTime(1, 2)
|
||||
beat_three_and_a_quarter = BeatsTime(3) + BeatsTime(1, 4)
|
||||
```
|
||||
|
||||
jubeatools counts beats from *zero*, not one. You can think of the beat number
|
||||
like the *duration* in beats from the start.
|
||||
|
||||
### Buttons
|
||||
|
||||
jubeatools identifies the controller buttons with 0-based x and y coordinates
|
||||
with this orientation :
|
||||
|
||||
```
|
||||
x →
|
||||
0 1 2 3
|
||||
y 0 □ □ □ □
|
||||
↓ 1 □ □ □ □
|
||||
2 □ □ □ □
|
||||
3 □ □ □ □
|
||||
```
|
||||
|
||||
x goes right and y goes down, both counting from 0 to 3.
|
||||
|
||||
A button is stored as a [`NotePosition`](NotePosition) object
|
||||
|
||||
If we label the buttons this way :
|
||||
|
||||
```
|
||||
1 2 3 4
|
||||
5 6 7 8
|
||||
9 10 11 12
|
||||
13 14 15 16
|
||||
```
|
||||
|
||||
Button 5 would be stored this way :
|
||||
|
||||
```python
|
||||
button_5 = NotePosition(x=0, y=1)
|
||||
```
|
||||
|
||||
And button 12 would be stored this way :
|
||||
|
||||
```python
|
||||
button_12 = NotePosition(x=3, y=2)
|
||||
```
|
||||
|
||||
### Regular Notes
|
||||
|
||||
Regular notes are stored as [`TapNote`](TapNote) objects, these are just the
|
||||
combination of a time in beats, stored in a [`BeatsTime`](BeatsTime) object,
|
||||
and a button, stored in a [`NotePosition`](NotePosition)
|
||||
|
||||
For instance, the following [`TapNote`](TapNote) object
|
||||
|
||||
```python
|
||||
note = TapNote(
|
||||
time=BeatsTime(5, 4),
|
||||
position=NotePosition(x=1, y=2)
|
||||
)
|
||||
```
|
||||
|
||||
would be represented this way in a `#memo2` file
|
||||
|
||||
```{code}
|
||||
:class: japanese-monospaced
|
||||
□□□□ |----|
|
||||
□□□□ |-①--|
|
||||
□①□□ |----|
|
||||
□□□□ |----|
|
||||
```
|
||||
|
||||
`NotePosition(x=1, y=2)` means the note appears on button 10
|
||||
|
||||
`BeatsTime(5, 4)` means the note happens at beat {math}`\frac{5}{4}`.
|
||||
|
||||
Remember that jubeatools counts beats starting at *zero*, not one.
|
||||
Since {math}`\frac{5}{4} = 1 + \frac{1}{4}`, this means that ① happens on the
|
||||
*second* quarter note of the *second* beat.
|
||||
|
||||
### Long notes
|
||||
|
||||
Long notes are stored as [`LongNote`](LongNote) objects.
|
||||
|
||||
In addition to storing their starting time and position (just like regular
|
||||
notes), long notes store their duration expressed in beats, as well as the
|
||||
*starting* position of their tail, represented as a
|
||||
[`NotePosition`](NotePosition) object.
|
||||
|
||||
For instance, the following long note
|
||||
|
||||
```python
|
||||
long_note = LongNote(
|
||||
time=BeatsTime(1, 2),
|
||||
position=NotePosition(x=0, y=1),
|
||||
duration=BeatsTime(1),
|
||||
tail_tip=NotePosition(x=3, y=1)
|
||||
)
|
||||
```
|
||||
|
||||
would be written this way in a `#memo2` file
|
||||
|
||||
```{code}
|
||||
:class: japanese-monospaced
|
||||
□□□□ |--①-|
|
||||
①――< |--②-|
|
||||
□□□□ |----|
|
||||
□□□□ |----|
|
||||
|
||||
□□□□
|
||||
2□□□
|
||||
□□□□
|
||||
□□□□
|
||||
```
|
||||
|
||||
<small>(assuming the file uses `#circlefree=1`)</small>
|
||||
|
||||
### BPM Changes
|
||||
|
||||
A BPM Change is represented as a [`BPMEvent`](BPMEvent) object. It defines the
|
||||
BPM at a given time in beats.
|
||||
|
||||
For instance this [`BPMEvent`](BPMEvent) object
|
||||
|
||||
```python
|
||||
bpm_event = BPMEvent(time=BeatsTime(0), BPM=Decimal(120))
|
||||
```
|
||||
|
||||
Defines that the BPM at beat 0 is 120
|
||||
|
||||
:::{warning}
|
||||
Use a **string**, not a float, when storing a non-interger BPM in a
|
||||
[`Decimal`](decimal.Decimal) object
|
||||
```python
|
||||
>>> Decimal("120.1")
|
||||
Decimal('120.1')
|
||||
>>> Decimal(120.1)
|
||||
Decimal('120.099999999999994315658113919198513031005859375')
|
||||
```
|
||||
:::
|
||||
|
||||
### Timing
|
||||
|
||||
[`Timing`](Timing) objects store all the info necessary to convert between
|
||||
beats and seconds. That information boils down to two things :
|
||||
|
||||
- a list of BPM changes
|
||||
- an initial offset
|
||||
|
||||
The initial offset is the "beat zero" offset, it's the time in *seconds* at
|
||||
which beat 0 occurs in the audio file.
|
||||
|
||||
```{attention}
|
||||
If you are used to the Stepmania notion of an "offset", this is the *opposite*
|
||||
value
|
||||
```
|
||||
|
||||
The beat zero offset is stored in a [`SecondsTime`](SecondsTime), which is
|
||||
just a renamed copy of the [`Decimal`](decimal.Decimal) class from the standard
|
||||
library
|
||||
|
||||
Here's a simple example of a [`Timing`](Timing) object
|
||||
|
||||
```python
|
||||
timing = Timing(
|
||||
events=[BPMEvent(time=BeatsTime(0), BPM=Decimal("180.5"))],
|
||||
beat_zero_offset=SecondsTime("0.25")
|
||||
)
|
||||
```
|
||||
|
||||
This object means that the song's initial beat (beat *zero*) happens at time
|
||||
00:00.25 in the audio file, and that the song has a constant BPM of 180.5
|
||||
throughout
|
||||
|
||||
```{warning}
|
||||
Be sure to set the first BPM at beat 0, some parts of jubeatools won't be able
|
||||
to handle a [`Timing`](Timing) object nicely if its first BPM change isn't at
|
||||
beat 0
|
||||
```
|
||||
|
||||
### Charts
|
||||
|
||||
[`Chart`](Chart) objects store a [`Decimal`](decimal.Decimal)
|
||||
level along with a list of mixed [`TapNote`](TapNote) and
|
||||
[`LongNote`](LongNote) objects .
|
||||
|
||||
Here's a small example :
|
||||
|
||||
```python
|
||||
basic = Chart(
|
||||
level=Decimal("1.0"),
|
||||
notes=[
|
||||
TapNote(time=BeatsTime(0), position=NotePosition(x=0, y=0)),
|
||||
LongNote(
|
||||
time=BeatsTime(0),
|
||||
position=NotePosition(x=0, y=1),
|
||||
duration=BeatsTime(1),
|
||||
tail_tip=NotePosition(x=3, y=1)
|
||||
),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### Metadata
|
||||
|
||||
The [`Metadata`](Metadata) object stores all the song information that's not
|
||||
specific to any single chart.
|
||||
|
||||
Currently this includes :
|
||||
|
||||
- Song title
|
||||
- Artist
|
||||
- Path to the audio file
|
||||
- Path to the jacket file
|
||||
- Song preview segment
|
||||
- Path to a separate audio preview file (akin to BMS preview files)
|
||||
|
||||
Here's an example :
|
||||
```python
|
||||
metadata = Metadata(
|
||||
title="My great song",
|
||||
artist="Myself",
|
||||
audio=Path("my_great_song.ogg"),
|
||||
cover=Path("my_great_song.png"),
|
||||
preview=Preview(start=SecondsTime("10.5"), length=("5"))
|
||||
preview_file=Path("preview.ogg")
|
||||
)
|
||||
```
|
||||
|
||||
### Song
|
||||
|
||||
Finally, the [`Song`](Song) object combines all the previous elements.
|
||||
|
||||
It holds :
|
||||
|
||||
- a [`Metadata`](Metadata) object
|
||||
- a `dict` that maps difficulty names to [`Chart`](Chart) objects
|
||||
- a [`Timing`](Timing) object that applies to all charts
|
||||
|
||||
Here's an example :
|
||||
|
||||
```python
|
||||
song = Song(
|
||||
metadata=Metadata(
|
||||
title="My great song",
|
||||
artist="Myself",
|
||||
audio=Path("my_great_song.ogg"),
|
||||
cover=Path("my_great_song.png"),
|
||||
),
|
||||
charts={
|
||||
"BSC": basic_chart,
|
||||
"ADV": advanced_chart,
|
||||
"EXT": extreme_chart
|
||||
},
|
||||
common_timing=timing_for_all_charts
|
||||
)
|
||||
```
|
105
docs/source/library/writing chart files.md
Normal file
105
docs/source/library/writing chart files.md
Normal file
@ -0,0 +1,105 @@
|
||||
# Writing charts
|
||||
|
||||
Similar to how you can read a chart file with a loader, writing a chart file
|
||||
is done by using a *dumper*, it's a function that takes in a [`Song`](Song)
|
||||
object and an output path template and return a dict that maps path names to a
|
||||
[`bytes`](bytes) objects with the file contents.
|
||||
|
||||
Let's go over how you can import then use one.
|
||||
|
||||
## Importing a dumper
|
||||
|
||||
Much like loaders, dumpers are all defined somewhere in the `jubeatools.formats`
|
||||
module.
|
||||
|
||||
The precise import path of each dumper is documented here :
|
||||
[](<../api/loaders and dumpers.md>)
|
||||
|
||||
For example you can import the dumper for `#memo2` files like this :
|
||||
|
||||
```python
|
||||
from jubeatools.formats.jubeat_analyser import dump_memo2
|
||||
```
|
||||
|
||||
:::{tip}
|
||||
Just like for loaders, if you don't want to have to write down the full import
|
||||
path or if you don't know which formats you will have to write to in advance,
|
||||
you can import [`DUMPERS`](DUMPERS) from `jubeatools.formats` to query the
|
||||
correct dumper based on the format. It's a dict that maps [`Format`](Format)
|
||||
enum members to their associated dumper.
|
||||
|
||||
```python
|
||||
>>> from jubeatools.formats import DUMPERS, Format
|
||||
>>> dump_memo2 = DUMPERS[Format.MEMO_2]
|
||||
```
|
||||
:::
|
||||
|
||||
## Using a dumper
|
||||
|
||||
Dumpers take in a [`Song`](Song) object and an output path template
|
||||
|
||||
```python
|
||||
song = Song(...)
|
||||
path = Path("new_file")
|
||||
files = dumper(song, path)
|
||||
```
|
||||
|
||||
Some dumpers also accept extra options as keyword arguments
|
||||
|
||||
```python
|
||||
files = dumper(song, Path("new_file"), make_it_pretty=True)
|
||||
```
|
||||
|
||||
These extra options are specific to each dumper and are documented
|
||||
in [](<../api/loaders and dumpers.md>).
|
||||
|
||||
## The output path template
|
||||
|
||||
Dumpers all take in an output path template. If it doesn't point to an existing
|
||||
*folder*, the path is used as a [format string](https://docs.python.org/3/library/string.html#format-string-syntax).
|
||||
|
||||
The following parameters are available, they are all passed as `str` :
|
||||
|
||||
| name | description |
|
||||
|---------------------|----------------------------------------------------|
|
||||
| `title` | song title |
|
||||
| `difficulty` | uppercase BSC ADV EXT |
|
||||
| `difficulty_index` | 0-based difficulty index, (BSC: 0, ADV: 1, EXT: 2) |
|
||||
| `difficulty_number` | 1-based |
|
||||
| `dedup` | dedup string ("-1", "-2" etc ...) |
|
||||
|
||||
For example `"{title} {difficulty}.txt"` would generate filenames like
|
||||
`"SigSig BSC.txt"`, `"SigSig ADV.txt"`, or `"SigSig EXT.txt"`
|
||||
|
||||
jubeatools adds support for suffix `u` and `l` in the format specification
|
||||
string for uppercase and lowercase respectively. This means that for example
|
||||
`"{difficulty:l}.eve"` would generate filenames like `"bsc.eve"`, `"adv.eve"`
|
||||
or `"ext.eve"`.
|
||||
|
||||
If a generated filename points to a file that already exists, a deduplicator
|
||||
will be added right before the extension : `"SigSig EXT-1.txt"`,
|
||||
`"SigSig EXT-2.txt"`, `"SigSig EXT-3.txt"` etc ...
|
||||
|
||||
If the output path template points to an existing folder, the dumper will
|
||||
generate paths to files inside that folder. The filenames will follow a
|
||||
template preset that tries to mimick what's usual for files in the format the
|
||||
dumper outputs. These preset templates are documented in
|
||||
[](<../api/loaders and dumpers.md>).
|
||||
|
||||
## The Dumper Protocol
|
||||
|
||||
Dumpers have a *uniform* interface, in other words they are all compatible with
|
||||
the same function signature. It's the Dumper *Protocol* :
|
||||
|
||||
```{py:function} dump(song: Song, path: pathlib.Path, **kwargs: Any) -> Dict[pathlib.Path, bytes]
|
||||
|
||||
Convert the contents of `song` to files with associated name suggestions.
|
||||
|
||||
Possibly takes in some options via the kwargs.
|
||||
|
||||
:param Song song: Song object to be exported
|
||||
:param pathlib.Path path: output path template
|
||||
:param Any **kwargs: Format-specific options
|
||||
:return: A dict that maps filenames to file contents as bytes
|
||||
:rtype: Dict[pathlib.Path, bytes]
|
||||
```
|
@ -140,8 +140,7 @@ def double_braces(s: str) -> str:
|
||||
class BetterStringFormatter(string.Formatter):
|
||||
"""Enables the use of 'u' and 'l' suffixes in string format specifiers to
|
||||
convert the string to uppercase or lowercase
|
||||
Thanks stackoverflow ! https://stackoverflow.com/a/57570269/10768117
|
||||
"""
|
||||
Thanks stackoverflow ! https://stackoverflow.com/a/57570269/10768117"""
|
||||
|
||||
def format_field(self, value: Any, format_spec: str) -> str:
|
||||
if isinstance(value, str):
|
||||
|
@ -6,6 +6,8 @@ from .format_names import Format
|
||||
|
||||
|
||||
def guess_format(path: Path) -> Format:
|
||||
"""Try to guess the format of the given file, raise an exception if the
|
||||
format is unknown"""
|
||||
if path.is_dir():
|
||||
raise ValueError("Can't guess chart format for a folder")
|
||||
|
||||
|
@ -4,6 +4,7 @@ from . import jubeat_analyser, konami, malody, memon
|
||||
from .format_names import Format
|
||||
from .typing import Dumper, Loader
|
||||
|
||||
#: Maps each Format enum member to its associated loader
|
||||
LOADERS: Dict[Format, Loader] = {
|
||||
Format.EVE: konami.load_eve,
|
||||
Format.JBSQ: konami.load_jbsq,
|
||||
@ -19,6 +20,7 @@ LOADERS: Dict[Format, Loader] = {
|
||||
Format.MEMO_2: jubeat_analyser.load_memo2,
|
||||
}
|
||||
|
||||
#: Maps each Format enum member to its associated dumper
|
||||
DUMPERS: Dict[Format, Dumper] = {
|
||||
Format.EVE: konami.dump_eve,
|
||||
Format.JBSQ: konami.dump_jbsq,
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Provides the Song class, the central model for chartsets
|
||||
Every input format is converted to a Song instance
|
||||
Every output format is created from a Song instance
|
||||
"""
|
||||
Provides the :py:obj:`Song` class, the central model for chartsets
|
||||
|
||||
- Every input format is converted to a :py:obj:`Song` instance
|
||||
- Every output format is created from a :py:obj:`Song` instance
|
||||
|
||||
Most timing-related info is stored as beat fractions, otherwise a decimal
|
||||
number of seconds is used"""
|
||||
@ -30,7 +32,9 @@ from typing import (
|
||||
|
||||
from jubeatools.utils import none_or
|
||||
|
||||
#: A time measured in beats
|
||||
BeatsTime = Fraction
|
||||
#: A time measured in seconds
|
||||
SecondsTime = Decimal
|
||||
|
||||
|
||||
@ -84,15 +88,18 @@ class Position:
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class NotePosition(Position):
|
||||
"""A specific square on the controller. (0, 0) is the top-left button, x
|
||||
"""
|
||||
A specific square on the controller. (0, 0) is the top-left button, x
|
||||
goes right, y goes down.
|
||||
|
||||
::
|
||||
|
||||
x →
|
||||
0 1 2 3
|
||||
y 0 □ □ □ □
|
||||
↓ 1 □ □ □ □
|
||||
2 □ □ □ □
|
||||
3 □ □ □ □
|
||||
x →
|
||||
0 1 2 3
|
||||
y 0 □ □ □ □
|
||||
↓ 1 □ □ □ □
|
||||
2 □ □ □ □
|
||||
3 □ □ □ □
|
||||
|
||||
The main difference with Position is that x and y MUST be between 0 and 3
|
||||
"""
|
||||
@ -279,9 +286,15 @@ class Song:
|
||||
"""The abstract representation format for all jubeat chart sets.
|
||||
A Song is a set of charts with associated metadata"""
|
||||
|
||||
#: Miscellaneous information about the song
|
||||
metadata: Metadata
|
||||
#: A regular dict that maps a difficulty name to a :py:obj:`Chart`.
|
||||
#: The names for the usual jubeat difficulties should be ``"BSC"``, ``"ADV"``,
|
||||
#: and ``"EXT"``
|
||||
charts: Dict[str, Chart] = field(default_factory=dict)
|
||||
#: The optional shared timing object that applies to all charts by default
|
||||
common_timing: Optional[Timing] = None
|
||||
#: The optional shared set of HAKUs that apply to all charts by default
|
||||
common_hakus: Optional[Set[BeatsTime]] = None
|
||||
|
||||
@classmethod
|
||||
|
Loading…
Reference in New Issue
Block a user