Switch to pathlib + mypy stuff
This commit is contained in:
parent
25e13c0936
commit
979b0e648f
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,4 +2,5 @@
|
||||
.vscode
|
||||
.hypothesis
|
||||
.pytest_cache
|
||||
.mypy_cache
|
||||
dist
|
@ -2,7 +2,7 @@
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from jubeatools.formats import DUMPERS, LOADERS
|
||||
from jubeatools.formats.enum import JUBEAT_ANALYSER_FORMATS
|
||||
|
@ -4,7 +4,7 @@ Module containing all the load/dump code for all file formats
|
||||
|
||||
from typing import IO, Any, Callable, Dict
|
||||
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from jubeatools.song import Song
|
||||
|
||||
@ -41,7 +41,7 @@ LOADERS: Dict[Format, Callable[[Path], Song]] = {
|
||||
Format.MEMO_2: load_memo2,
|
||||
}
|
||||
|
||||
DUMPERS: Dict[str, Dumper] = {
|
||||
DUMPERS: Dict[Format, Dumper] = {
|
||||
Format.MEMON_LEGACY: dump_memon_legacy,
|
||||
Format.MEMON_0_1_0: dump_memon_0_1_0,
|
||||
Format.MEMON_0_2_0: dump_memon_0_2_0,
|
||||
|
@ -2,13 +2,13 @@ import json
|
||||
import re
|
||||
from typing import Any, List
|
||||
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from .enum import Format
|
||||
|
||||
|
||||
def guess_format(path: Path) -> Format:
|
||||
if path.isdir():
|
||||
if path.is_dir():
|
||||
raise ValueError("Can't guess chart format for a folder")
|
||||
|
||||
# The file is valid json => memon
|
||||
|
@ -5,10 +5,10 @@ from dataclasses import dataclass, field
|
||||
from decimal import Decimal
|
||||
from fractions import Fraction
|
||||
from itertools import chain
|
||||
from typing import Callable, Dict, Iterator, List, Mapping, Optional, Union
|
||||
from typing import Callable, Dict, Iterator, List, Mapping, Optional, Union, TypeVar
|
||||
|
||||
from more_itertools import collapse, intersperse, mark_ends, windowed
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
from sortedcontainers import SortedDict, SortedKeyList
|
||||
|
||||
from jubeatools.formats.filetypes import ChartFile
|
||||
@ -111,15 +111,19 @@ class SortedDefaultDict(SortedDict):
|
||||
return value
|
||||
|
||||
|
||||
# Here we split dataclass and ABC stuff since mypy curently can't handle both
|
||||
# at once on a single class definition
|
||||
@dataclass
|
||||
class JubeatAnalyserDumpedSection(ABC):
|
||||
class _JubeatAnalyerDumpedSection:
|
||||
current_beat: BeatsTime
|
||||
length: Decimal = 4
|
||||
length: Decimal = Decimal(4)
|
||||
commands: Dict[str, Optional[str]] = field(default_factory=dict)
|
||||
symbol_definitions: Dict[BeatsTime, str] = field(default_factory=dict)
|
||||
symbols: Dict[BeatsTime, str] = field(default_factory=dict)
|
||||
notes: List[Union[TapNote, LongNote, LongNoteEnd]] = field(default_factory=list)
|
||||
|
||||
|
||||
class JubeatAnalyserDumpedSection(_JubeatAnalyerDumpedSection, ABC):
|
||||
def _dump_commands(self) -> Iterator[str]:
|
||||
keys = chain(COMMAND_ORDER, self.commands.keys() - set(COMMAND_ORDER))
|
||||
for key in keys:
|
||||
@ -139,14 +143,16 @@ class JubeatAnalyserDumpedSection(ABC):
|
||||
...
|
||||
|
||||
|
||||
S = TypeVar('S', bound=JubeatAnalyserDumpedSection)
|
||||
|
||||
def create_sections_from_chart(
|
||||
section_factory: Callable[[BeatsTime], JubeatAnalyserDumpedSection],
|
||||
section_factory: Callable[[BeatsTime], S],
|
||||
chart: Chart,
|
||||
difficulty: str,
|
||||
timing: Timing,
|
||||
metadata: Metadata,
|
||||
circle_free: bool,
|
||||
) -> Mapping[BeatsTime, JubeatAnalyserDumpedSection]:
|
||||
) -> Mapping[BeatsTime, S]:
|
||||
sections = SortedDefaultDict(section_factory)
|
||||
|
||||
timing_events = sorted(timing.events, key=lambda e: e.time)
|
||||
@ -225,7 +231,7 @@ def jubeat_analyser_file_dumper(
|
||||
) -> Dict[Path, bytes]:
|
||||
files = internal_dumper(song, circle_free)
|
||||
res = {}
|
||||
if path.isdir():
|
||||
if path.is_dir():
|
||||
name_format = song.metadata.title + " {difficulty}{dedup_index}.txt"
|
||||
else:
|
||||
name_format = "{base}{dedup_index}{ext}"
|
||||
@ -233,18 +239,18 @@ def jubeat_analyser_file_dumper(
|
||||
for chartfile in files:
|
||||
i = 0
|
||||
filepath = name_format.format(
|
||||
base=path.stripext(),
|
||||
base=path.parent / path.stem,
|
||||
difficulty = DIFFICULTIES.get(chartfile.difficulty, chartfile.difficulty),
|
||||
dedup_index = "" if i == 0 else f"-{i}",
|
||||
ext=path.ext,
|
||||
ext=path.suffix,
|
||||
)
|
||||
while filepath in res:
|
||||
i += 1
|
||||
filepath = name_format.format(
|
||||
base=path.stripext(),
|
||||
base=path.parent / path.stem,
|
||||
difficulty = DIFFICULTIES.get(chartfile.difficulty, chartfile.difficulty),
|
||||
dedup_index = "" if i == 0 else f"-{i}",
|
||||
ext=path.ext,
|
||||
ext=path.suffix,
|
||||
)
|
||||
|
||||
res[Path(filepath)] = chartfile.contents.getvalue().encode(
|
||||
|
@ -1,23 +1,23 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_files(path: Path) -> Dict[Path, List[str]]:
|
||||
# The vast majority of memo files you will encounter will be propely
|
||||
# decoded using shift-jis-2004. Get ready for endless fun with the small
|
||||
# portion of files that won't
|
||||
files = {}
|
||||
if path.isdir():
|
||||
for f in path.files("*.txt"):
|
||||
files: Dict[Path, List[str]] = {}
|
||||
if path.is_dir():
|
||||
for f in path.glob("*.txt"):
|
||||
_load_file(f, files)
|
||||
elif path.isfile():
|
||||
elif path.is_file():
|
||||
_load_file(path, files)
|
||||
return files
|
||||
|
||||
|
||||
def _load_file(path: Path, files: Dict[Path, List[str]]):
|
||||
try:
|
||||
files[path] = path.lines("shift-jis-2004")
|
||||
files[path] = path.read_text(encoding="shift-jis-2004").split("\n")
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
@ -6,7 +6,7 @@ from copy import deepcopy
|
||||
from dataclasses import astuple, dataclass
|
||||
from decimal import Decimal
|
||||
from itertools import product, zip_longest
|
||||
from typing import Dict, Iterator, List, Optional, Set, Tuple
|
||||
from typing import Dict, Iterator, List, Optional, Set, Tuple, AbstractSet
|
||||
|
||||
import constraint
|
||||
from parsimonious import Grammar, NodeVisitor, ParseError
|
||||
@ -164,7 +164,7 @@ class UnfinishedLongNote:
|
||||
|
||||
|
||||
def find_long_note_candidates(
|
||||
bloc: List[List[str]], note_symbols: Set[str], should_skip: Set[NotePosition]
|
||||
bloc: List[List[str]], note_symbols: AbstractSet[str], should_skip: AbstractSet[NotePosition]
|
||||
) -> Dict[NotePosition, Set[NotePosition]]:
|
||||
"Return a dict of arrow position to landing note candidates"
|
||||
arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]] = {}
|
||||
@ -178,7 +178,7 @@ def find_long_note_candidates(
|
||||
|
||||
# at this point we are sure we have a long arrow
|
||||
# we need to check in its direction for note candidates
|
||||
note_candidates: Set[Tuple[int, int]] = set()
|
||||
note_candidates = set()
|
||||
𝛿pos = LONG_DIRECTION[symbol]
|
||||
candidate = NotePosition(x, y) + 𝛿pos
|
||||
while 0 <= candidate.x < 4 and 0 <= candidate.y < 4:
|
||||
|
@ -7,10 +7,10 @@ from functools import partial
|
||||
from io import StringIO
|
||||
from itertools import chain, zip_longest
|
||||
from math import ceil
|
||||
from typing import Dict, Iterator, List, Optional, Set, Tuple, Union
|
||||
from typing import Dict, Iterator, List, Optional, Set, Tuple, Union, cast, Callable
|
||||
|
||||
from more_itertools import chunked, collapse, intersperse, mark_ends, windowed
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
from sortedcontainers import SortedKeyList
|
||||
|
||||
from jubeatools import __version__
|
||||
@ -221,7 +221,7 @@ def _dump_memo_chart(
|
||||
) -> StringIO:
|
||||
|
||||
_raise_if_unfit_for_memo(chart, timing, circle_free)
|
||||
|
||||
|
||||
sections = create_sections_from_chart(
|
||||
MemoDumpedSection, chart, difficulty, timing, metadata, circle_free
|
||||
)
|
||||
@ -259,11 +259,13 @@ def _dump_memo_chart(
|
||||
def _dump_memo_internal(song: Song, circle_free: bool) -> List[ChartFile]:
|
||||
files: List[ChartFile] = []
|
||||
for difficulty, chart in song.charts.items():
|
||||
timing = chart.timing or song.global_timing
|
||||
assert timing is not None
|
||||
contents = _dump_memo_chart(
|
||||
difficulty,
|
||||
chart,
|
||||
song.metadata,
|
||||
chart.timing or song.global_timing,
|
||||
timing,
|
||||
circle_free,
|
||||
)
|
||||
files.append(ChartFile(contents, song, difficulty, chart))
|
||||
|
@ -10,7 +10,7 @@ from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union
|
||||
import constraint
|
||||
from more_itertools import collapse, mark_ends
|
||||
from parsimonious import Grammar, NodeVisitor, ParseError
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from jubeatools.song import (
|
||||
Chart,
|
||||
@ -21,6 +21,7 @@ from jubeatools.song import (
|
||||
Song,
|
||||
TapNote,
|
||||
Timing,
|
||||
Preview,
|
||||
)
|
||||
|
||||
from ..command import is_command, parse_command
|
||||
@ -118,7 +119,10 @@ class MemoParser(JubeatAnalyserParser):
|
||||
return list(line)
|
||||
|
||||
def _frames_duration(self) -> Decimal:
|
||||
return sum(frame.duration for frame in self.frames)
|
||||
return sum(
|
||||
(frame.duration for frame in self.frames),
|
||||
start=Decimal(0)
|
||||
)
|
||||
|
||||
def _push_frame(self):
|
||||
position_part = [
|
||||
@ -198,7 +202,10 @@ class MemoParser(JubeatAnalyserParser):
|
||||
frame_starting_beat = Decimal(0)
|
||||
for i, frame in enumerate(section.frames):
|
||||
if frame.timing_part:
|
||||
frame_starting_beat = sum(f.duration for f in section.frames[:i])
|
||||
frame_starting_beat = sum(
|
||||
(f.duration for f in section.frames[:i]),
|
||||
start=Decimal(0)
|
||||
)
|
||||
local_symbols = {
|
||||
symbol: Decimal("0.25") * i + frame_starting_beat
|
||||
for i, symbol in enumerate(collapse(frame.timing_part))
|
||||
@ -315,8 +322,10 @@ def _load_memo_file(lines: List[str]) -> Song:
|
||||
cover=parser.jacket,
|
||||
)
|
||||
if parser.preview_start is not None:
|
||||
metadata.preview_start = SecondsTime(parser.preview_start) / 1000
|
||||
metadata.preview_length = SecondsTime(10)
|
||||
metadata.preview = Preview(
|
||||
start=SecondsTime(parser.preview_start) / 1000,
|
||||
length=SecondsTime(10)
|
||||
)
|
||||
|
||||
timing = Timing(
|
||||
events=parser.timing_events, beat_zero_offset=SecondsTime(parser.offset) / 1000
|
||||
|
@ -10,7 +10,7 @@ from math import ceil
|
||||
from typing import Dict, Iterator, List, Optional, Set, Tuple, Union
|
||||
|
||||
from more_itertools import chunked, collapse, intersperse, mark_ends, windowed
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
from sortedcontainers import SortedKeyList
|
||||
|
||||
from jubeatools import __version__
|
||||
@ -58,8 +58,9 @@ class Frame:
|
||||
def dump(self, length: Decimal) -> Iterator[str]:
|
||||
# Check that bars are contiguous
|
||||
for a, b in windowed(sorted(self.bars), 2):
|
||||
if b is not None and b - a != 1:
|
||||
raise ValueError("Frame has discontinuous bars")
|
||||
if a is not None and b is not None:
|
||||
if b - a != 1:
|
||||
raise ValueError("Frame has discontinuous bars")
|
||||
# Check all bars are in the same 4-bar group
|
||||
if self.bars.keys() != set(bar % 4 for bar in self.bars):
|
||||
raise ValueError("Frame contains bars from different 4-bar groups")
|
||||
@ -104,7 +105,7 @@ class Memo1DumpedSection(JubeatAnalyserDumpedSection):
|
||||
notes_by_bar[bar_index].append(note)
|
||||
|
||||
# Pre-render timing bars
|
||||
bars: Dict[int, List[str]] = defaultdict(dict)
|
||||
bars: Dict[int, List[str]] = defaultdict(list)
|
||||
chosen_symbols: Dict[BeatsTime, str] = {}
|
||||
symbols_iterator = iter(NOTE_SYMBOLS)
|
||||
for bar_index in range(ceil(self.length)):
|
||||
@ -227,7 +228,7 @@ def _dump_memo1_chart(
|
||||
)
|
||||
|
||||
# Jubeat Analyser format command
|
||||
sections[0].commands["memo1"] = None
|
||||
sections[BeatsTime(0)].commands["memo1"] = None
|
||||
|
||||
# Actual output to file
|
||||
file = StringIO()
|
||||
@ -242,11 +243,13 @@ def _dump_memo1_chart(
|
||||
def _dump_memo1_internal(song: Song, circle_free: bool) -> List[ChartFile]:
|
||||
files: List[ChartFile] = []
|
||||
for difficulty, chart in song.charts.items():
|
||||
timing = chart.timing or song.global_timing
|
||||
assert timing is not None
|
||||
contents = _dump_memo1_chart(
|
||||
difficulty,
|
||||
chart,
|
||||
song.metadata,
|
||||
chart.timing or song.global_timing,
|
||||
timing,
|
||||
circle_free,
|
||||
)
|
||||
files.append(ChartFile(contents, song, difficulty, chart))
|
||||
|
@ -10,7 +10,7 @@ from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union
|
||||
import constraint
|
||||
from more_itertools import collapse, mark_ends
|
||||
from parsimonious import Grammar, NodeVisitor, ParseError
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from jubeatools.song import (
|
||||
BeatsTime,
|
||||
@ -22,6 +22,7 @@ from jubeatools.song import (
|
||||
Song,
|
||||
TapNote,
|
||||
Timing,
|
||||
Preview
|
||||
)
|
||||
|
||||
from ..command import is_command, parse_command
|
||||
@ -112,7 +113,7 @@ class Memo1Parser(JubeatAnalyserParser):
|
||||
return list(line)
|
||||
|
||||
def _frames_duration(self) -> Decimal:
|
||||
return sum(frame.duration for frame in self.frames)
|
||||
return sum((frame.duration for frame in self.frames), start=Decimal(0))
|
||||
|
||||
def _push_frame(self):
|
||||
position_part = [
|
||||
@ -184,13 +185,13 @@ class Memo1Parser(JubeatAnalyserParser):
|
||||
]:
|
||||
"""iterate over tuples of
|
||||
currently_defined_symbols, frame, section_starting_beat, section"""
|
||||
local_symbols: Dict[str, Decimal] = {}
|
||||
local_symbols = {}
|
||||
section_starting_beat = Decimal(0)
|
||||
for section in self.sections:
|
||||
frame_starting_beat = Decimal(0)
|
||||
for i, frame in enumerate(section.frames):
|
||||
if frame.timing_part:
|
||||
frame_starting_beat = sum(f.duration for f in section.frames[:i])
|
||||
frame_starting_beat = sum((f.duration for f in section.frames[:i]), start=Decimal(0))
|
||||
local_symbols = {
|
||||
symbol: BeatsTime(symbol_index, len(bar))
|
||||
+ bar_index
|
||||
@ -309,8 +310,10 @@ def _load_memo1_file(lines: List[str]) -> Song:
|
||||
cover=parser.jacket,
|
||||
)
|
||||
if parser.preview_start is not None:
|
||||
metadata.preview_start = SecondsTime(parser.preview_start) / 1000
|
||||
metadata.preview_length = SecondsTime(10)
|
||||
metadata.preview = Preview(
|
||||
start=SecondsTime(parser.preview_start) / 1000,
|
||||
length=SecondsTime(10)
|
||||
)
|
||||
|
||||
timing = Timing(
|
||||
events=parser.timing_events, beat_zero_offset=SecondsTime(parser.offset) / 1000
|
||||
|
@ -10,7 +10,7 @@ from math import ceil
|
||||
from typing import Dict, Iterator, List, Optional, Set, Tuple, Union
|
||||
|
||||
from more_itertools import chunked, collapse, intersperse, mark_ends, windowed
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
from sortedcontainers import SortedKeyList
|
||||
|
||||
from jubeatools import __version__
|
||||
@ -38,7 +38,6 @@ from ..dump_tools import (
|
||||
DIRECTION_TO_ARROW,
|
||||
DIRECTION_TO_LINE,
|
||||
NOTE_TO_CIRCLE_FREE_SYMBOL,
|
||||
JubeatAnalyserDumpedSection,
|
||||
LongNoteEnd,
|
||||
SortedDefaultDict,
|
||||
create_sections_from_chart,
|
||||
@ -61,8 +60,9 @@ class Frame:
|
||||
def dump(self) -> Iterator[str]:
|
||||
# Check that bars are contiguous
|
||||
for a, b in windowed(sorted(self.bars), 2):
|
||||
if b is not None and b - a != 1:
|
||||
raise ValueError("Frame has discontinuous bars")
|
||||
if a is not None and b is not None:
|
||||
if b - a != 1:
|
||||
raise ValueError("Frame has discontinuous bars")
|
||||
|
||||
for pos, bar in zip_longest(self.dump_positions(), self.dump_bars()):
|
||||
if bar is None:
|
||||
@ -91,6 +91,13 @@ class StopEvent:
|
||||
duration: SecondsTime
|
||||
|
||||
|
||||
@dataclass
|
||||
class BarEvent:
|
||||
note: Optional[str] = None
|
||||
bpms: List[BPMEvent] = field(default_factory=list)
|
||||
stops: List[StopEvent] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Memo2Section:
|
||||
"""A 4-beat-long group of notes"""
|
||||
@ -115,7 +122,7 @@ class Memo2Section:
|
||||
events_by_bar[bar_index].append(event)
|
||||
|
||||
# Pre-render timing bars
|
||||
bars: Dict[int, List[str]] = defaultdict(dict)
|
||||
bars: Dict[int, List[str]] = defaultdict(list)
|
||||
chosen_symbols: Dict[BeatsTime, str] = {}
|
||||
symbols_iterator = iter(NOTE_SYMBOLS)
|
||||
for bar_index in range(4):
|
||||
@ -127,9 +134,8 @@ class Memo2Section:
|
||||
)
|
||||
if bar_length < 3:
|
||||
bar_length = 4
|
||||
bar_dict: Dict[int, List[Union[str, BPMEvent, StopEvent]]] = defaultdict(
|
||||
list
|
||||
)
|
||||
|
||||
bar_dict: Dict[int, BarEvent] = defaultdict(BarEvent)
|
||||
for note in notes:
|
||||
time_in_section = note.time % BeatsTime(4)
|
||||
time_in_bar = note.time % Fraction(1)
|
||||
@ -139,34 +145,28 @@ class Memo2Section:
|
||||
if time_index not in bar_dict:
|
||||
symbol = next(symbols_iterator)
|
||||
chosen_symbols[time_in_section] = symbol
|
||||
bar_dict[time_index].append(symbol)
|
||||
bar_dict[time_index].note = symbol
|
||||
|
||||
for event in events:
|
||||
time_in_bar = event.time % Fraction(1)
|
||||
time_index = time_in_bar.numerator * (
|
||||
bar_length / time_in_bar.denominator
|
||||
)
|
||||
bar_dict[time_index].append(event)
|
||||
if isinstance(event, StopEvent):
|
||||
bar_dict[time_index].stops.append(event)
|
||||
elif isinstance(event, BPMEvent):
|
||||
bar_dict[time_index].bpms.append(event)
|
||||
|
||||
bar = []
|
||||
for i in range(bar_length):
|
||||
events_and_note = bar_dict.get(i, [])
|
||||
stops = list(
|
||||
filter(lambda e: isinstance(e, StopEvent), events_and_note)
|
||||
)
|
||||
bpms = list(filter(lambda e: isinstance(e, BPMEvent), events_and_note))
|
||||
notes = list(filter(lambda e: isinstance(e, str), events_and_note))
|
||||
assert len(notes) <= 1
|
||||
for stop in stops:
|
||||
bar_event = bar_dict.get(i, BarEvent())
|
||||
for stop in bar_event.stops:
|
||||
bar.append(f"[{int(stop.duration * 1000)}]")
|
||||
|
||||
for bpm in bpms:
|
||||
for bpm in bar_event.bpms:
|
||||
bar.append(f"({bpm.BPM})")
|
||||
|
||||
if notes:
|
||||
note = notes[0]
|
||||
else:
|
||||
note = EMPTY_BEAT_SYMBOL
|
||||
|
||||
bar.append(note)
|
||||
bar.append(bar_event.note or EMPTY_BEAT_SYMBOL)
|
||||
|
||||
bars[bar_index] = bar
|
||||
|
||||
@ -331,11 +331,13 @@ def _dump_memo2_chart(
|
||||
def _dump_memo2_internal(song: Song, circle_free: bool = False) -> List[ChartFile]:
|
||||
files: List[ChartFile] = []
|
||||
for difficulty, chart in song.charts.items():
|
||||
timing = chart.timing or song.global_timing
|
||||
assert timing is not None
|
||||
contents = _dump_memo2_chart(
|
||||
difficulty,
|
||||
chart,
|
||||
song.metadata,
|
||||
chart.timing or song.global_timing,
|
||||
timing,
|
||||
circle_free,
|
||||
)
|
||||
files.append(ChartFile(contents, song, difficulty, chart))
|
||||
|
@ -10,7 +10,7 @@ from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union
|
||||
import constraint
|
||||
from more_itertools import collapse, mark_ends
|
||||
from parsimonious import Grammar, NodeVisitor, ParseError
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from jubeatools.song import (
|
||||
BeatsTime,
|
||||
@ -23,6 +23,7 @@ from jubeatools.song import (
|
||||
Song,
|
||||
TapNote,
|
||||
Timing,
|
||||
Preview,
|
||||
)
|
||||
|
||||
from ..command import is_command, parse_command
|
||||
@ -50,7 +51,7 @@ from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
|
||||
|
||||
|
||||
@dataclass
|
||||
class Notes:
|
||||
class NoteCluster:
|
||||
string: str
|
||||
|
||||
def dump(self) -> str:
|
||||
@ -73,7 +74,7 @@ class BPM:
|
||||
return f"({self.value})"
|
||||
|
||||
|
||||
Event = Union[Notes, Stop, BPM]
|
||||
Event = Union[NoteCluster, Stop, BPM]
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -91,7 +92,6 @@ class RawMemo2ChartLine:
|
||||
@dataclass
|
||||
class Memo2ChartLine:
|
||||
"""timing part only contains notes"""
|
||||
|
||||
position: str
|
||||
timing: Optional[List[str]]
|
||||
|
||||
@ -136,7 +136,7 @@ class Memo2ChartLineVisitor(NodeVisitor):
|
||||
self.time_part.append(BPM(Decimal(value.text)))
|
||||
|
||||
def visit_notes(self, node, visited_children):
|
||||
self.time_part.append(Notes(node.text))
|
||||
self.time_part.append(NoteCluster(node.text))
|
||||
|
||||
def generic_visit(self, node, visited_children):
|
||||
...
|
||||
@ -229,11 +229,12 @@ class Memo2Parser(JubeatAnalyserParser):
|
||||
raise SyntaxError(
|
||||
f"Invalid chart line for #bpp={self.bytes_per_panel} : {raw_line}"
|
||||
)
|
||||
|
||||
if raw_line.timing is not None and self.bytes_per_panel == 2:
|
||||
if any(
|
||||
len(event.string.encode("shift-jis-2004")) % 2 != 0
|
||||
for event in raw_line.timing
|
||||
if isinstance(event, Notes)
|
||||
len(e.string.encode("shift-jis-2004")) % 2 != 0
|
||||
for e in raw_line.timing
|
||||
if isinstance(e, NoteCluster)
|
||||
):
|
||||
raise SyntaxError(f"Invalid chart line for #bpp=2 : {raw_line}")
|
||||
|
||||
@ -241,12 +242,12 @@ class Memo2Parser(JubeatAnalyserParser):
|
||||
line = Memo2ChartLine(raw_line.position, None)
|
||||
else:
|
||||
# split notes
|
||||
bar = []
|
||||
for event in raw_line.timing:
|
||||
if isinstance(event, Notes):
|
||||
bar.extend(self._split_chart_line(event.string))
|
||||
bar: List[Union[str, Stop, BPM]] = []
|
||||
for raw_event in raw_line.timing:
|
||||
if isinstance(raw_event, NoteCluster):
|
||||
bar.extend(self._split_chart_line(raw_event.string))
|
||||
else:
|
||||
bar.append(event)
|
||||
bar.append(raw_event)
|
||||
# extract timing info
|
||||
bar_length = sum(1 for e in bar if isinstance(e, str))
|
||||
symbol_duration = BeatsTime(1, bar_length)
|
||||
@ -286,8 +287,8 @@ class Memo2Parser(JubeatAnalyserParser):
|
||||
else:
|
||||
return list(line)
|
||||
|
||||
def _frames_duration(self) -> Decimal:
|
||||
return sum(frame.duration for frame in self.frames)
|
||||
def _frames_duration(self) -> BeatsTime:
|
||||
return sum((frame.duration for frame in self.frames), start=BeatsTime(0))
|
||||
|
||||
def _push_frame(self):
|
||||
position_part = [
|
||||
@ -336,13 +337,13 @@ class Memo2Parser(JubeatAnalyserParser):
|
||||
|
||||
def _iter_frames(
|
||||
self,
|
||||
) -> Iterator[Tuple[Mapping[str, BeatsTime], Memo2Frame, BeatsTime]]:
|
||||
) -> Iterator[Tuple[Mapping[str, BeatsTime], Memo2Frame]]:
|
||||
"""iterate over tuples of (currently_defined_symbols, frame)"""
|
||||
local_symbols: Dict[str, Decimal] = {}
|
||||
local_symbols = {}
|
||||
frame_starting_beat = BeatsTime(0)
|
||||
for i, frame in enumerate(self.frames):
|
||||
if frame.timing_part:
|
||||
frame_starting_beat = sum(f.duration for f in self.frames[:i])
|
||||
frame_starting_beat = sum((f.duration for f in self.frames[:i]), start=BeatsTime(0))
|
||||
local_symbols = {
|
||||
symbol: frame_starting_beat
|
||||
+ bar_index
|
||||
@ -445,8 +446,7 @@ def _load_memo2_file(lines: List[str]) -> Song:
|
||||
cover=parser.jacket,
|
||||
)
|
||||
if parser.preview_start is not None:
|
||||
metadata.preview_start = SecondsTime(parser.preview_start) / 1000
|
||||
metadata.preview_length = SecondsTime(10)
|
||||
metadata.preview = Preview(start=SecondsTime(parser.preview_start) / 1000,length=SecondsTime(10))
|
||||
|
||||
timing = Timing(
|
||||
events=parser.timing_events, beat_zero_offset=SecondsTime(parser.offset or 0) / 1000
|
||||
|
@ -9,7 +9,7 @@ from itertools import chain
|
||||
from typing import Dict, Iterator, List, Mapping, Optional, Tuple
|
||||
|
||||
from more_itertools import collapse, intersperse, mark_ends, windowed
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
from sortedcontainers import SortedKeyList
|
||||
|
||||
from jubeatools import __version__
|
||||
@ -90,7 +90,7 @@ class MonoColumnDumpedSection(JubeatAnalyserDumpedSection):
|
||||
frame = {}
|
||||
time_in_section = note.time - self.current_beat
|
||||
if circle_free:
|
||||
symbol = CIRCLE_FREE_SYMBOLS[time_in_section]
|
||||
symbol = CIRCLE_FREE_SYMBOLS[int(time_in_section)]
|
||||
else:
|
||||
symbol = self.symbols[time_in_section]
|
||||
frame[note.position] = symbol
|
||||
@ -177,11 +177,13 @@ def _dump_mono_column_chart(
|
||||
def _dump_mono_column_internal(song: Song, circle_free: bool) -> List[ChartFile]:
|
||||
files: List[ChartFile] = []
|
||||
for difficulty, chart in song.charts.items():
|
||||
timing = chart.timing or song.global_timing
|
||||
assert timing is not None
|
||||
contents = _dump_mono_column_chart(
|
||||
difficulty,
|
||||
chart,
|
||||
song.metadata,
|
||||
chart.timing or song.global_timing,
|
||||
timing,
|
||||
circle_free,
|
||||
)
|
||||
files.append(ChartFile(contents, song, difficulty, chart))
|
||||
|
@ -7,11 +7,11 @@ from decimal import Decimal
|
||||
from enum import Enum
|
||||
from functools import reduce
|
||||
from itertools import product
|
||||
from typing import Dict, Iterator, List, Set, Tuple
|
||||
from typing import Dict, Iterator, List, Set, Tuple, Union
|
||||
|
||||
import constraint
|
||||
from parsimonious import Grammar, NodeVisitor, ParseError
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from jubeatools.song import (
|
||||
BeatsTime,
|
||||
@ -24,7 +24,7 @@ from jubeatools.song import (
|
||||
Song,
|
||||
TapNote,
|
||||
Timing,
|
||||
Union,
|
||||
Preview,
|
||||
)
|
||||
|
||||
from ..command import is_command, parse_command
|
||||
@ -296,8 +296,10 @@ def _load_mono_column_file(lines: List[str]) -> Song:
|
||||
cover=parser.jacket,
|
||||
)
|
||||
if parser.preview_start is not None:
|
||||
metadata.preview_start = SecondsTime(parser.preview_start) / 1000
|
||||
metadata.preview_length = SecondsTime(10)
|
||||
metadata.preview = Preview(
|
||||
start=SecondsTime(parser.preview_start) / 1000,
|
||||
length=SecondsTime(10)
|
||||
)
|
||||
|
||||
timing = Timing(
|
||||
events=parser.timing_events, beat_zero_offset=SecondsTime(parser.offset) / 1000
|
||||
|
@ -10,7 +10,7 @@ https://github.com/Stepland/memon
|
||||
|
||||
from io import StringIO
|
||||
from itertools import chain
|
||||
from typing import IO, Any, Dict, Iterable, List, Tuple, Union
|
||||
from typing import IO, Any, Dict, Iterable, List, Tuple, Union, Mapping
|
||||
|
||||
import simplejson as json
|
||||
from marshmallow import (
|
||||
@ -22,7 +22,7 @@ from marshmallow import (
|
||||
validate,
|
||||
validates_schema,
|
||||
)
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from jubeatools.song import *
|
||||
from jubeatools.utils import lcm
|
||||
@ -160,7 +160,7 @@ def load_memon_legacy(file: Path) -> Song:
|
||||
events=[BPMEvent(time=BeatsTime(0), BPM=memon["metadata"]["BPM"])],
|
||||
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]),
|
||||
)
|
||||
charts = MultiDict()
|
||||
charts: MultiDict[Chart] = MultiDict()
|
||||
for memon_chart in memon["data"]:
|
||||
charts.add(
|
||||
memon_chart["dif_name"],
|
||||
@ -187,7 +187,7 @@ def load_memon_0_1_0(file: Path) -> Song:
|
||||
events=[BPMEvent(time=BeatsTime(0), BPM=memon["metadata"]["BPM"])],
|
||||
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]),
|
||||
)
|
||||
charts = MultiDict()
|
||||
charts: MultiDict[Chart] = MultiDict()
|
||||
for difficulty, memon_chart in memon["data"].items():
|
||||
charts.add(
|
||||
difficulty,
|
||||
@ -221,7 +221,7 @@ def load_memon_0_2_0(file: Path) -> Song:
|
||||
events=[BPMEvent(time=BeatsTime(0), BPM=memon["metadata"]["BPM"])],
|
||||
beat_zero_offset=SecondsTime(-memon["metadata"]["offset"]),
|
||||
)
|
||||
charts = MultiDict()
|
||||
charts: MultiDict[Chart] = MultiDict()
|
||||
for difficulty, memon_chart in memon["data"].items():
|
||||
charts.add(
|
||||
difficulty,
|
||||
@ -254,7 +254,7 @@ def _get_timing(song: Song) -> Timing:
|
||||
return next(
|
||||
chart.timing
|
||||
for chart in song.charts.values()
|
||||
if chart is not None
|
||||
if chart.timing is not None
|
||||
)
|
||||
|
||||
def _raise_if_unfit_for_v0(song: Song, version: str) -> None:
|
||||
@ -337,7 +337,7 @@ def dump_memon_legacy(song: Song, path: Path) -> Dict[Path, bytes]:
|
||||
_raise_if_unfit_for_v0(song, "legacy")
|
||||
timing = _get_timing(song)
|
||||
|
||||
memon = {
|
||||
memon: Dict[str, Any] = {
|
||||
"metadata": {
|
||||
"song title": song.metadata.title,
|
||||
"artist": song.metadata.artist,
|
||||
@ -364,7 +364,7 @@ def dump_memon_legacy(song: Song, path: Path) -> Dict[Path, bytes]:
|
||||
}
|
||||
)
|
||||
|
||||
if path.isdir():
|
||||
if path.is_dir():
|
||||
filepath = path / f"{song.metadata.title}.memon"
|
||||
else:
|
||||
filepath = path
|
||||
@ -377,7 +377,7 @@ def dump_memon_0_1_0(song: Song, path: Path) -> Dict[Path, bytes]:
|
||||
_raise_if_unfit_for_v0(song, "v0.1.0")
|
||||
timing= _get_timing(song)
|
||||
|
||||
memon = {
|
||||
memon: Dict[str, Any] = {
|
||||
"version": "0.1.0",
|
||||
"metadata": {
|
||||
"song title": song.metadata.title,
|
||||
@ -400,7 +400,7 @@ def dump_memon_0_1_0(song: Song, path: Path) -> Dict[Path, bytes]:
|
||||
],
|
||||
}
|
||||
|
||||
if path.isdir():
|
||||
if path.is_dir():
|
||||
filepath = path / f"{song.metadata.title}.memon"
|
||||
else:
|
||||
filepath = path
|
||||
@ -413,7 +413,7 @@ def dump_memon_0_2_0(song: Song, path: Path) -> Dict[Path, bytes]:
|
||||
_raise_if_unfit_for_v0(song, "v0.2.0")
|
||||
timing = _get_timing(song)
|
||||
|
||||
memon = {
|
||||
memon: Dict[str, Any] = {
|
||||
"version": "0.2.0",
|
||||
"metadata": {
|
||||
"song title": song.metadata.title,
|
||||
@ -443,7 +443,7 @@ def dump_memon_0_2_0(song: Song, path: Path) -> Dict[Path, bytes]:
|
||||
],
|
||||
}
|
||||
|
||||
if path.isdir():
|
||||
if path.is_dir():
|
||||
filepath = path / f"{song.metadata.title}.memon"
|
||||
else:
|
||||
filepath = path
|
||||
|
@ -2,7 +2,7 @@ import tempfile
|
||||
|
||||
import hypothesis.strategies as st
|
||||
from hypothesis import given
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from jubeatools.testutils.strategies import NoteOption, TimingOption
|
||||
from jubeatools.testutils.strategies import song as song_strat
|
||||
|
@ -1,12 +1,8 @@
|
||||
from typing import Any, Dict, Protocol
|
||||
from typing import Any, Dict, Callable
|
||||
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
from jubeatools.song import Song
|
||||
|
||||
|
||||
class Dumper(Protocol):
|
||||
def __call__(
|
||||
self, song: Song, path: Path, **kwargs: Dict[str, Any]
|
||||
) -> Dict[Path, bytes]:
|
||||
...
|
||||
Dumper = Callable[[Song, Path], Dict[Path, bytes]]
|
@ -15,7 +15,7 @@ from functools import wraps
|
||||
from typing import Iterator, List, Mapping, Optional, Type, Union
|
||||
|
||||
from multidict import MultiDict
|
||||
from path import Path
|
||||
from pathlib import Path
|
||||
|
||||
BeatsTime = Fraction
|
||||
SecondsTime = Decimal
|
||||
@ -164,7 +164,7 @@ class Song:
|
||||
f"{self.metadata}\n"
|
||||
f"{other.metadata}"
|
||||
)
|
||||
charts = MultiDict()
|
||||
charts: MultiDict[Chart] = MultiDict()
|
||||
charts.extend(self.charts)
|
||||
charts.extend(other.charts)
|
||||
if (
|
||||
|
@ -231,7 +231,7 @@ def song(
|
||||
),
|
||||
)
|
||||
diffs = draw(st.sets(diff_name_strat, min_size=1, max_size=10))
|
||||
charts = MultiDict()
|
||||
charts: MultiDict[Chart] = MultiDict()
|
||||
for diff_name in diffs:
|
||||
chart_timing_strat = st.none()
|
||||
if TimingOption.PER_CHART in timing_options:
|
||||
|
Loading…
x
Reference in New Issue
Block a user