From 76ecb6a26d0b2d816ad939fcb026a65df716e31b Mon Sep 17 00:00:00 2001 From: Stepland <10530295-Buggyroom@users.noreply.gitlab.com> Date: Thu, 11 Jul 2024 00:36:02 +0200 Subject: [PATCH] initial yubiosi 1.0 implementation --- jubeatools/formats/malody/__init__.py | 2 +- jubeatools/formats/yubiosi/__init__.py | 11 ++++ jubeatools/formats/yubiosi/dump.py | 61 +++++++++++++++++++++ jubeatools/formats/yubiosi/load.py | 75 ++++++++++++++++++++++++++ jubeatools/formats/yubiosi/tools.py | 8 +++ 5 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 jubeatools/formats/yubiosi/__init__.py create mode 100644 jubeatools/formats/yubiosi/dump.py create mode 100644 jubeatools/formats/yubiosi/load.py create mode 100644 jubeatools/formats/yubiosi/tools.py diff --git a/jubeatools/formats/malody/__init__.py b/jubeatools/formats/malody/__init__.py index d9f4090..3cc5cae 100644 --- a/jubeatools/formats/malody/__init__.py +++ b/jubeatools/formats/malody/__init__.py @@ -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 diff --git a/jubeatools/formats/yubiosi/__init__.py b/jubeatools/formats/yubiosi/__init__.py new file mode 100644 index 0000000..7859ca1 --- /dev/null +++ b/jubeatools/formats/yubiosi/__init__.py @@ -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/ +""" \ No newline at end of file diff --git a/jubeatools/formats/yubiosi/dump.py b/jubeatools/formats/yubiosi/dump.py new file mode 100644 index 0000000..e2ade11 --- /dev/null +++ b/jubeatools/formats/yubiosi/dump.py @@ -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 \ No newline at end of file diff --git a/jubeatools/formats/yubiosi/load.py b/jubeatools/formats/yubiosi/load.py new file mode 100644 index 0000000..4239bf1 --- /dev/null +++ b/jubeatools/formats/yubiosi/load.py @@ -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) + ) \ No newline at end of file diff --git a/jubeatools/formats/yubiosi/tools.py b/jubeatools/formats/yubiosi/tools.py new file mode 100644 index 0000000..40811ea --- /dev/null +++ b/jubeatools/formats/yubiosi/tools.py @@ -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() +} \ No newline at end of file