initial yubiosi 1.0 implementation
This commit is contained in:
parent
2310fcf8e0
commit
76ecb6a26d
@ -1,5 +1,5 @@
|
||||
"""Malody is a multiplatform rhythm game that mainly lives off content created
|
||||
by its community, as is common in the rhythm game simulator scene. It support
|
||||
by its community, as is common in the rhythm game simulator scene. It supports
|
||||
many different games or "Modes", including jubeat (known as "Pad" Mode)
|
||||
|
||||
The file format it uses is not that well documented but is simple enough to
|
||||
|
11
jubeatools/formats/yubiosi/__init__.py
Normal file
11
jubeatools/formats/yubiosi/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""
|
||||
指押 (yubiosi) is a now-defunct jubeat simulator for Android
|
||||
|
||||
It supports 3x3 and 4x4 charts.
|
||||
|
||||
jubeatools only supports converting to and from 4x4 charts
|
||||
|
||||
The three versions of the format it uses are documented on a dedicated wiki :
|
||||
|
||||
https://w.atwiki.jp/yubiosi2/
|
||||
"""
|
61
jubeatools/formats/yubiosi/dump.py
Normal file
61
jubeatools/formats/yubiosi/dump.py
Normal file
@ -0,0 +1,61 @@
|
||||
from math import ceil
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from jubeatools import song
|
||||
from jubeatools.formats.dump_tools import make_dumper_from_chart_file_dumper
|
||||
from jubeatools.formats.filetypes import ChartFile
|
||||
from jubeatools.formats.timemap import TimeMap
|
||||
|
||||
from ..dump_tools import make_events_from_chart
|
||||
|
||||
from .tools import INDEX_TO_YUBIOSI_1_0
|
||||
|
||||
|
||||
def _dump_yubiosi_1_0(song: song.Song, **kwargs: dict) -> List[ChartFile]:
|
||||
res = []
|
||||
for dif, chart, timing, hakus in song.iter_charts():
|
||||
if any(isinstance(n, song.LongNote) for n in chart.notes):
|
||||
raise ValueError("yubiosi 1.0 does not support long notes")
|
||||
|
||||
if len(timing.events) != 1:
|
||||
raise ValueError("yubiosi 1.0 does not support BPM changes")
|
||||
|
||||
if any((n.time.denominator not in (1, 2, 4)) for n in chart.notes):
|
||||
raise ValueError("yubiosi 1.0 only supports 16th notes")
|
||||
|
||||
|
||||
timemap = TimeMap.from_timing(timing)
|
||||
sorted_notes = sorted(chart.notes, key=lambda n: n.time)
|
||||
last_note = sorted_notes[-1]
|
||||
header = [
|
||||
song.metadata.title or "(no title)",
|
||||
"unknown", # save data name
|
||||
timing.events[0].BPM,
|
||||
ceil(timemap.seconds_at(last_note.time) * 1000), # chart duration in ms
|
||||
int(timing.beat_zero_offset * 1000),
|
||||
len(chart.notes)
|
||||
]
|
||||
times = [
|
||||
int(note.time * 4)
|
||||
for note in sorted_notes
|
||||
]
|
||||
positions = [
|
||||
INDEX_TO_YUBIOSI_1_0[note.position.index]
|
||||
for note in sorted_notes
|
||||
]
|
||||
chart = header + times + positions
|
||||
chart_text = "\n".join(map(str, chart))
|
||||
chart_bytes = chart_text.encode("shift-jis")
|
||||
res.append(ChartFile(chart_bytes, song, dif, chart))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
dump_yubiosi_1_0 = make_dumper_from_chart_file_dumper(
|
||||
internal_dumper=_dump_yubiosi_1_0,
|
||||
file_name_template=Path("{title}.txt")
|
||||
)
|
||||
|
||||
|
||||
def
|
75
jubeatools/formats/yubiosi/load.py
Normal file
75
jubeatools/formats/yubiosi/load.py
Normal file
@ -0,0 +1,75 @@
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
from fractions import Fraction
|
||||
from functools import singledispatch
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
|
||||
from jubeatools import song
|
||||
from jubeatools.formats import timemap
|
||||
from jubeatools.formats.load_tools import FolderLoader, make_folder_loader
|
||||
from jubeatools.utils import none_or
|
||||
|
||||
from .tools import YUBIOSI_1_0_TO_INDEX
|
||||
|
||||
def load_yubiosi_1_0(path: Path, **kwargs: Any) -> song.Song:
|
||||
files = load_folder(path)
|
||||
charts = [load_yubiosi_1_0_file(d) for d in files.values()]
|
||||
return song.Song.from_monochart_instances(*charts)
|
||||
|
||||
|
||||
def load_file(path: Path) -> List[str]:
|
||||
with path.open(encoding="shift-jis") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
load_folder: FolderLoader[List[str]] = make_folder_loader("*.txt", load_file)
|
||||
|
||||
|
||||
def load_yubiosi_1_0_file(text: str) -> song.Song:
|
||||
lines = text.split("\n")
|
||||
(
|
||||
title,
|
||||
_, # save data name
|
||||
raw_bpm,
|
||||
_, # chart duration in ms
|
||||
raw_offset,
|
||||
raw_note_count,
|
||||
*raw_times_and_positions
|
||||
) = lines
|
||||
note_count = int(raw_note_count)
|
||||
if len(raw_times_and_positions) != 2 * note_count:
|
||||
raise ValueError(
|
||||
"Incorrect line count. The yubiosi file header claims this file "
|
||||
f"contains {note_count} notes, this imples the file body should "
|
||||
f"have twice as many lines ({note_count * 2}) but found {len(raw_times_and_positions)}"
|
||||
)
|
||||
|
||||
bpm = Decimal(raw_bpm)
|
||||
offset = song.SecondsTime(raw_offset) / 1000
|
||||
timing = song.Timing(
|
||||
events=[song.BPMEvent(time=song.BeatsTime(0), BPM=bpm)],
|
||||
beat_zero_offset=offset
|
||||
)
|
||||
raw_times = raw_times_and_positions[:note_count]
|
||||
raw_positions = raw_times_and_positions[note_count:]
|
||||
times = [int(t) for t in raw_times]
|
||||
positions = [int(p) for p in raw_positions]
|
||||
notes = [load_note(t, p) for t, p in zip(times, positions)]
|
||||
chart = song.Chart(
|
||||
level=Decimal(0),
|
||||
timing=timing,
|
||||
notes=notes
|
||||
)
|
||||
return song.Song(
|
||||
metadata=song.Metadata(title=title),
|
||||
charts={song.Difficulty.EXTREME.value: chart}
|
||||
)
|
||||
|
||||
def load_note(time: int, position: int) -> song.TapNote:
|
||||
time_in_beats = song.BeatsTime(time, 16)
|
||||
index = INDEX_TO_YUBIOSI_1_0[position]
|
||||
return song.TapNote(
|
||||
time=time_in_beats,
|
||||
position=song.NotePosition.from_index(index)
|
||||
)
|
8
jubeatools/formats/yubiosi/tools.py
Normal file
8
jubeatools/formats/yubiosi/tools.py
Normal file
@ -0,0 +1,8 @@
|
||||
INDEX_TO_YUBIOSI_1_0 = {
|
||||
n: 2**n
|
||||
for n in range(16)
|
||||
}
|
||||
|
||||
YUBIOSI_1_0_TO_INDEX = {
|
||||
v: k for k, v in INDEX_TO_YUBIOSI_1_0.items()
|
||||
}
|
Loading…
Reference in New Issue
Block a user