1
0
mirror of synced 2024-12-04 19:17:55 +01:00

Split eve between dump and load files

This commit is contained in:
Stepland 2021-05-10 00:39:28 +02:00
parent 4d11287be2
commit cb2bbfe370
5 changed files with 238 additions and 98 deletions

View File

@ -1,18 +1,20 @@
# Unreleased
## Added
- [eve] 🎉 Add support for .eve as output
## Fixed
- [jubeat-analyser] Prettier rendering of decimal values
# v0.2.0
## Added
- [#mono-column] #circlefree mode accepts non-16ths notes and falls back to normal symbols when needed
- [mono-column] #circlefree mode accepts non-16ths notes and falls back to normal symbols when needed
## Fixed
- [jubeat-analyser]
- Raise exception earlier when a mono-column file is detected by the other #memo parsers (based on "--" separator lines)
- [#memo] [#memo1]
- [memo] [memo1]
- Fix incorrect handling of mid-chart `t=` and `b=` commands
- Prettify rendering by adding more blank lines between sections
- [#memo1] Fix dumping of chart with bpm changes happening on beat times that aren't multiples of 1/4
- [#memo2]
- [memo1] Fix dumping of chart with bpm changes happening on beat times that aren't multiples of 1/4
- [memo2]
- Fix parsing of BPM changes
- Fix dumping of BPM changes
- [memon]
@ -24,7 +26,7 @@
- [jubeat-analyser] Use "EXT" instead of "?" as the fallback difficulty name when loading
## Fixed
- [memon] Fix TypeError that would occur when trying to convert
- [#memo2] Fix rendering missing blank lines between blocks, while technically still valid files, this made files rendered by jubeatools absolutely fugly and very NOT human friendly
- [memo2] Fix rendering missing blank lines between blocks, while technically still valid files, this made files rendered by jubeatools absolutely fugly and very NOT human friendly
# v0.1.2
## Fixed
@ -34,7 +36,7 @@
# v0.1.1
## Fixed
- [#memo2] Loading a file that did not specify any offset (neither by `o=...`, `r=...` nor `[...]` commands) would trigger a TypeError, not anymore ! Offset now defaults to zero.
- [memo2] Loading a file that did not specify any offset (neither by `o=...`, `r=...` nor `[...]` commands) would trigger a TypeError, not anymore ! Offset now defaults to zero.
# v0.1.0
- Initial Release

View File

@ -2,4 +2,5 @@
.eve is the file format used in arcade releases of jubeat
"""
from .eve import load_eve, dump_eve
from .dump import dump_eve
from .load import load_eve

View File

@ -0,0 +1,148 @@
from __future__ import annotations
from enum import Enum
from dataclasses import dataclass, astuple
from jubeatools import song
from typing import Union
from functools import singledispatchmethod
from fractions import Fraction
from .timemap import TimeMap
AnyNote = Union[song.TapNote, song.LongNote]
DIRECTION_TO_VALUE = {
song.Direction.DOWN: 0,
song.Direction.UP: 1,
song.Direction.RIGHT: 2,
song.Direction.LEFT: 3,
}
VALUE_TO_DIRECTION = {v: k for k, v in DIRECTION_TO_VALUE.items()}
class Command(Enum):
END = 1
MEASURE = 2
HAKU = 3
PLAY = 4
LONG = 5
TEMPO = 6
@dataclass(order=True)
class Event:
"""Represents a line in an .eve file"""
time: int
command: Command
value: int
def __post_init__(self) -> None:
try:
check_func = VALUES_CHECKERS[self.command](self.value)
except KeyError:
# most likely no check function associated : forget about it
pass
except ValueError as e:
raise ValueError(f"Invalid value for the {self.command!r} command. {e}")
def dump(self) -> str:
return f"{self.time:>8},{self.command.name:<8},{self.value:>8}"
@classmethod
def from_tap_note(cls, note: song.TapNote, time_map: TimeMap) -> Event:
ticks = ticks_at_beat(note.time, time_map)
value = note.position.index
return Event(time=ticks, command=Command.PLAY, value=value)
@classmethod
def from_long_note(cls, note: song.LongNote, time_map: TimeMap) -> Event:
if not note.has_straight_tail():
raise ValueError("Diagonal tails cannot be represented in eve format")
duration = duration_in_ticks(note, time_map)
direction = DIRECTION_TO_VALUE[note.tail_direction()]
length = len(list(note.positions_covered())) - 1
if not (1 <= length <= 3):
raise ValueError(
f"Given note has a length of {length}, which is not representable "
"in the eve format"
)
position_index = note.position.index
long_note_value = duration << 8 + length << 6 + direction << 4 + position_index
ticks = ticks_at_beat(note.time, time_map)
return Event(time=ticks, command=Command.LONG, value=long_note_value)
def is_zero(value: int) -> None:
if value != 0:
raise ValueError(f"Value should be zero but {value} found")
def is_valid_button_index(value: int) -> None:
# Should raise ValueError if invalid
_ = song.NotePosition.from_index(value)
def is_valid_tail_position(value: int) -> None:
# Should raise ValueError if invalid
_ = EveLong.from_value(value)
def is_not_zero(value: int) -> None:
if value == 0:
raise ValueError(f"Value cannot be zero")
VALUES_CHECKERS = {
Command.END: is_zero,
Command.MEASURE: is_zero,
Command.HAKU: is_zero,
Command.PLAY: is_valid_button_index,
Command.LONG: is_valid_tail_position,
Command.TEMPO: is_not_zero,
}
@dataclass
class EveLong:
duration: int
length: int
direction: song.Direction
position: int
def __post_init__(self) -> None:
if self.duration < 0:
raise ValueError("Duration can't be negative")
if not 1 <= self.length < 4:
raise ValueError("Tail length must be between 1 and 3 inclusive")
pos = song.NotePosition.from_index(self.position)
step_vector = song.TAIL_DIRECTION_TO_OUTWARDS_VECTOR[self.direction]
tail_pos = pos + (self.length * step_vector)
if not ((0 <= tail_pos.x < 4) and (0 <= tail_pos.y < 4)):
raise ValueError(
f"Long note tail starts on {astuple(tail_pos)} which is "
"outside the screen"
)
@classmethod
def from_value(cls, value: int) -> EveLong:
...
def ticks_at_beat(time: song.BeatsTime, time_map: TimeMap) -> int:
seconds_time = time_map.fractional_seconds_at(time)
return seconds_to_ticks(seconds_time)
def duration_in_ticks(long: song.LongNote, time_map: TimeMap) -> int:
press_time = time_map.fractional_seconds_at(long.time)
release_time = time_map.fractional_seconds_at(long.time + long.duration)
length_in_seconds = release_time - press_time
return seconds_to_ticks(length_in_seconds)
def ticks_to_seconds(tick: int) -> Fraction:
"""Convert eve ticks (300 Hz) to seconds"""
return Fraction(tick, 300)
def seconds_to_ticks(time: Fraction) -> int:
"""Convert fractional seconds to eve ticks (300 Hz)"""
return round(time * 300)

View File

@ -1,70 +1,34 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from fractions import Fraction
from typing import List, Union
from io import StringIO
from dataclasses import dataclass
from functools import singledispatch
import ctypes
import warnings
from typing import Dict, Tuple
from pathlib import Path
from typing import List, Union
from more_itertools import numeric_range
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.adapters import make_dumper_from_chart_file_dumper
from .timemap import TimeMap
AnyNote = Union[song.TapNote, song.LongNote]
class Command(Enum):
END = 1
MEASURE = 2
HAKU = 3
PLAY = 4
LONG = 5
TEMPO = 6
@dataclass(order=True)
class Event:
"""Represents a line in an .eve file"""
time: int
command: Command
value: int
def dump(self) -> str:
return f"{self.time:>8},{self.command.name:<8},{self.value:>8}"
DIRECTION_TO_VALUE = {
song.Direction.DOWN: 0,
song.Direction.UP: 1,
song.Direction.RIGHT: 2,
song.Direction.LEFT: 3,
}
VALUE_TO_DIRECTION = {v: k for k, v in DIRECTION_TO_VALUE.items()}
from .commons import AnyNote, Command, Event, DIRECTION_TO_VALUE
def _dump_eve(song: song.Song, **kwargs: dict) -> List[ChartFile]:
res = []
for dif, chart, timing in song.iter_charts_with_timing():
chart_text = dump_chart(chart.notes, timing)
chart_bytes = chart_text.encode('ascii')
chart_bytes = chart_text.encode("ascii")
res.append(ChartFile(chart_bytes, song, dif, chart))
return res
dump_eve = make_dumper_from_chart_file_dumper(
internal_dumper=_dump_eve,
file_name_template=Path("{difficulty_index}.eve")
internal_dumper=_dump_eve, file_name_template=Path("{difficulty_index}.eve")
)
@ -77,38 +41,20 @@ def dump_chart(notes: List[AnyNote], timing: song.Timing) -> str:
def make_note_events(notes: List[AnyNote], time_map: TimeMap) -> List[Event]:
return [make_note_event(note, time_map) for note in notes]
return [Event.from_note(note, time_map) for note in notes]
@singledispatch
def make_note_event(note: AnyNote, time_map: TimeMap) -> Event:
raise NotImplementedError(f"Note of unknown type : {note}")
raise NotImplementedError(f"Unknown note type : {type(note)}")
@make_note_event.register
def make_tap_note_event(note: song.TapNote, time_map: TimeMap) -> Event:
ticks = ticks_at_beat(note.time, time_map)
value = note.position.index
return Event(time=ticks, command=Command.PLAY, value=value)
return Event.from_tap_note(note, time_map)
@make_note_event.register
def make_long_note_event(note: song.LongNote, time_map: TimeMap) -> Event:
if not note.has_straight_tail():
raise ValueError("Diagonal tails cannot be represented in eve format")
duration = duration_in_ticks(note, time_map)
direction = DIRECTION_TO_VALUE[note.tail_direction()]
length = len(list(note.positions_covered())) - 1
if not (1 <= length <= 3):
raise ValueError(
f"Given note has a length of {length}, which is not representable "
"in the eve format"
)
position_index = note.position.index
long_note_value = duration << 8 + length << 6 + direction << 4 + position_index
ticks = ticks_at_beat(note.time, time_map)
return Event(time=ticks, command=Command.LONG, value=long_note_value)
return Event.from_long_note(note, time_map)
def make_timing_events(
@ -179,29 +125,3 @@ def make_beat_events(end_beat: song.BeatsTime, time_map: TimeMap) -> List[Event]
def make_beat_event(beat: song.BeatsTime, time_map: TimeMap) -> Event:
ticks = ticks_at_beat(beat, time_map)
return Event(time=ticks, command=Command.HAKU, value=0)
def load_eve(path: Path) -> song.Song:
...
def ticks_at_beat(time: song.BeatsTime, time_map: TimeMap) -> int:
seconds_time = time_map.fractional_seconds_at(time)
return seconds_to_ticks(seconds_time)
def duration_in_ticks(long: song.LongNote, time_map: TimeMap) -> int:
press_time = time_map.fractional_seconds_at(long.time)
release_time = time_map.fractional_seconds_at(long.time + long.duration)
length_in_seconds = release_time - press_time
return seconds_to_ticks(length_in_seconds)
def ticks_to_seconds(tick: int) -> Fraction:
"""Convert eve ticks (300 Hz) to seconds"""
return Fraction(tick, 300)
def seconds_to_ticks(time: Fraction) -> int:
"""Convert fractional seconds to eve ticks (300 Hz)"""
return round(time * 300)

View File

@ -0,0 +1,69 @@
from typing import List, Iterator
from pathlib import Path
from functools import reduce
from dataclasses import dataclass
from jubeatools import song
from jubeatools.formats.load_tools import make_folder_loader
from .commons import Event, Command
def load_eve(path: Path) -> song.Song:
files = load_folder(path)
charts = [_load_eve(l, p) for p, l in files.items()]
return reduce(song.Song.merge, charts)
def load_file(path: Path) -> List[str]:
return path.read_text(encoding="ascii").split("\n")
load_folder = make_folder_loader("*.eve", load_file)
def _load_eve(lines: List[str], file_path: Path) -> song.Song:
events = list(iter_events(lines))
...
def iter_events(lines: List[str]) -> Iterator[Event]:
for i, raw_line in enumerate(lines, start=1):
line = raw_line.strip()
if not line:
continue
try:
event = parser_event(line)
except ValueError as e:
raise ValueError(
f"Error on line {i} : {e}"
)
def parse_event(line: str) -> Event:
columns = line.split(",")
if len(columns) != 3:
raise ValueError(f"Expected 3 comma-separated values but found {len(columns)}")
raw_tick, raw_command, raw_value = map(str.strip, columns)
try:
tick = int(raw_tick)
except ValueError:
raise ValueError(
f"The first column should contain an integer but {raw_tick!r} was "
f"found, which python could not understand as an integer"
)
try:
command = Command[raw_command]
except KeyError:
raise ValueError(
f"The second column should contain one of "
f"{list(Command.__members__)}, but {raw_command!r} was found"
)
try:
value = int(raw_value)
except ValueError:
raise ValueError(
f"The third column should contain an integer but {raw_tick!r} was "
f"found, which python could not understand as an integer"
)
return Event(time, command, value)