1
0
mirror of synced 2024-09-24 03:08:21 +02:00

initial yubiosi 1.0 implementation

This commit is contained in:
Stepland 2024-07-11 00:36:02 +02:00
parent 2310fcf8e0
commit 76ecb6a26d
5 changed files with 156 additions and 1 deletions

View File

@ -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

View 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/
"""

View 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

View 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)
)

View 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()
}