From 59bbe5598a76483cca6913da9b3987ca2b93ffc3 Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Mon, 4 May 2020 23:44:19 -0700 Subject: [PATCH 01/23] Added event support (except for storyboards) --- slider/beatmap.py | 133 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 400e4e9..4ce7747 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -1,5 +1,5 @@ from datetime import timedelta -from enum import unique, IntEnum +from enum import unique, IntEnum, Enum from functools import partial import inspect from itertools import chain, islice, cycle @@ -30,6 +30,132 @@ def _get(cs, ix, default=no_default): raise return default +class Event: + + EventType = Enum('EventType', {'Background': 0, 'Video': 1, 'Break': 2, 'Sprite': 3, 'Animation': 4}) + + @property + def start_time(self): + return self.start_time if self.start_time != 0 else None + + @classmethod + def parse(cls, data): + event_type, start_time, *event_params = data.split(',') + try: + start_time = int(start_time) + start_time = timedelta(milliseconds = start_time) + except ValueError: + return Storyboard.parse() + try: + event_type = cls.EventType(int(event_type)) + except ValueError: + try: + event_type = cls.EventType[event_type] + except KeyError: + raise ValueError(f'Invalid event type, got {event_type}') + if event_type == cls.EventType.Background: + pass + elif event_type == cls.EventType.Video: + pass + elif event_type == cls.EventType.Break: + return Break.parse(start_time, event_params) + else: + raise ValueError(f'Unimplemented event type: {event_type}, this state should not be reachable') + +class Background(Event): + + @property + def start_time(self): + return self.start_time + + @property + def event_type(self): + return self.EventType.Background + + @classmethod + def parse(cls, start_time, event_params): + try: + filename, x_offset, y_offset = event_params + except ValueError: + raise ValueError(f'Missing one or more of filename, x_offset, y_offset, received {event_params}') + try: + x_offset = int(x_offset) + except ValueError: + raise ValueError(f'x_offset is invalid, got {x_offset}') + try: + y_offset = int(y_offset) + except ValueError: + raise ValueError(f'y_offset is invalid, got {y_offset}') + + def __init__(self, filename, x_offset, y_offset): + self.start_time = timedelta(milliseconds=0) + self.filename = filename + self.x_offset = x_offset + self.y_offset = y_offset + +class Break(Event): + + @property + def event_type(self): + return self.EventType.Break + + @classmethod + def parse(cls, start_time, event_params): + try: + end_time = event_params[0] + end_time = timedelta(milliseconds=int(end_time)) + except IndexError: + raise ValueError(f'Beatmap is invalid, no end_time received') + except ValueError: + raise ValueError(f'Invalid end_time provided, got {end_time}') + return cls(start_time, end_time) + + def __init__(self, start_time, end_time): + self.start_time = start_time + self.end_time = end_time + + +class Video(Event): + @property + def event_type(self): + return self.EventType.Video + + @classmethod + def parse(cls, start_time, event_params): + try: + filename, x_offset, y_offset = event_params + except ValueError: + raise ValueError(f'Missing one or more of filename, x_offset, y_offset, received {event_params}') + try: + x_offset = int(x_offset) + except ValueError: + raise ValueError(f'x_offset is invalid, got {x_offset}') + try: + y_offset = int(y_offset) + except ValueError: + raise ValueError(f'y_offset is invalid, got {y_offset}') + return cls(start_time, filename, x_offset, y_offset) + + def __init__(self, start_time, filename, x_offset, y_offset): + self.start_time = start_time + self.filename = filename + self.x_offset = x_offset + self.y_offset = y_offset + +class Storyboard(Event): + + @property + def start_time(self): + raise ValueError('Currently unimplemented') + + @property + def event_type(self): + return self.EventType.Storyboard + + @classmethod + def parse(cls): + return cls() + class TimingPoint: """A timing point assigns properties to an offset into a beatmap. @@ -1661,6 +1787,11 @@ def parse(cls, data): parent = timing_point timing_points.append(timing_point) + events = [] + for raw_event in groups['Events']: + event = Event.parse(raw_event) + events.append(event) + slider_multiplier = _get_as_float( groups, 'Difficulty', From 8962c1af828cf765bc04cfdf26da8814625898f2 Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Tue, 5 May 2020 00:01:38 -0700 Subject: [PATCH 02/23] added events to beatmap class --- slider/beatmap.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 4ce7747..33e1cf0 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -1283,6 +1283,8 @@ class Beatmap: The timing points the the map. hit_objects : list[HitObject] The hit objects in the map. + events : list[Event] + The events in the map. Notes ----- @@ -1323,7 +1325,8 @@ def __init__(self, slider_multiplier, slider_tick_rate, timing_points, - hit_objects): + hit_objects, + events): self.format_version = format_version self.audio_filename = audio_filename self.audio_lead_in = audio_lead_in @@ -1357,6 +1360,7 @@ def __init__(self, self.slider_tick_rate = slider_tick_rate self.timing_points = timing_points self.hit_objects = hit_objects + self.events = events # cache the stars with different mod combinations self._stars_cache = {} @@ -1540,6 +1544,24 @@ def ar(self, return ar + @lazyval + def breaks(self): + """The breaks with all other events filtered out. + """ + return tuple(e for e in self.events if not isinstance(e, Break)) + + @lazyval + def background(self): + """The background, if it exists, otherwise returns None. + """ + return next((e for e in self.events if not isinstance(e, Background)), None) + + @lazyval + def videos(self): + """The videos with all other events filtered out. + """ + return tuple(e for e in self.events if not isinstance(e, Break)) + @lazyval def hit_objects_no_spinners(self): """The hit objects with spinners filtered out. @@ -1904,7 +1926,7 @@ def parse(cls, data): ), groups['HitObjects'], )), - + events=events, ) def timing_point_at(self, time): From 54d13b9621751717dff87dc057ed005421c18506 Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Tue, 5 May 2020 00:16:55 -0700 Subject: [PATCH 03/23] fix flake8 --- slider/beatmap.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 33e1cf0..04e414b 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -30,9 +30,15 @@ def _get(cs, ix, default=no_default): raise return default + class Event: - EventType = Enum('EventType', {'Background': 0, 'Video': 1, 'Break': 2, 'Sprite': 3, 'Animation': 4}) + EventType = Enum('EventType', + {'Background': 0, + 'Video': 1, + 'Break': 2, + 'Sprite': 3, + 'Animation': 4}) @property def start_time(self): @@ -43,7 +49,7 @@ def parse(cls, data): event_type, start_time, *event_params = data.split(',') try: start_time = int(start_time) - start_time = timedelta(milliseconds = start_time) + start_time = timedelta(milliseconds=start_time) except ValueError: return Storyboard.parse() try: @@ -59,8 +65,11 @@ def parse(cls, data): pass elif event_type == cls.EventType.Break: return Break.parse(start_time, event_params) + # should be unreachable, added to enforce explicit handling of enums. else: - raise ValueError(f'Unimplemented event type: {event_type}, this state should not be reachable') + raise ValueError( + f'Unimplemented event type: {event_type}') + class Background(Event): @@ -77,7 +86,8 @@ def parse(cls, start_time, event_params): try: filename, x_offset, y_offset = event_params except ValueError: - raise ValueError(f'Missing one or more of filename, x_offset, y_offset, received {event_params}') + raise ValueError( + f'Missing param for Background, received {event_params}') try: x_offset = int(x_offset) except ValueError: @@ -93,6 +103,7 @@ def __init__(self, filename, x_offset, y_offset): self.x_offset = x_offset self.y_offset = y_offset + class Break(Event): @property @@ -109,7 +120,7 @@ def parse(cls, start_time, event_params): except ValueError: raise ValueError(f'Invalid end_time provided, got {end_time}') return cls(start_time, end_time) - + def __init__(self, start_time, end_time): self.start_time = start_time self.end_time = end_time @@ -125,7 +136,8 @@ def parse(cls, start_time, event_params): try: filename, x_offset, y_offset = event_params except ValueError: - raise ValueError(f'Missing one or more of filename, x_offset, y_offset, received {event_params}') + raise ValueError( + f'Missing param for video, received {event_params}') try: x_offset = int(x_offset) except ValueError: @@ -142,6 +154,7 @@ def __init__(self, start_time, filename, x_offset, y_offset): self.x_offset = x_offset self.y_offset = y_offset + class Storyboard(Event): @property @@ -185,6 +198,7 @@ class TimingPoint: kiai_mode : bool Wheter or not kiai time effects are active. """ + def __init__(self, offset, ms_per_beat, @@ -1554,7 +1568,8 @@ def breaks(self): def background(self): """The background, if it exists, otherwise returns None. """ - return next((e for e in self.events if not isinstance(e, Background)), None) + return next( + (e for e in self.events if not isinstance(e, Background)), None) @lazyval def videos(self): From d68553c0e8fb1517c4109747b2e40827031bbdb7 Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Tue, 5 May 2020 18:38:03 -0700 Subject: [PATCH 04/23] Address some CR issues, added logic to ignore storyboard event params --- slider/beatmap.py | 92 +++++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 04e414b..dfb7341 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -31,39 +31,45 @@ def _get(cs, ix, default=no_default): return default -class Event: +EventType = Enum('EventType', + {'Background': 0, + 'Video': 1, + 'Break': 2, + 'Sprite': 3, + 'Animation': 4}) - EventType = Enum('EventType', - {'Background': 0, - 'Video': 1, - 'Break': 2, - 'Sprite': 3, - 'Animation': 4}) +class Event: @property def start_time(self): - return self.start_time if self.start_time != 0 else None + return self._start_time if self._start_time != 0 else None @classmethod def parse(cls, data): - event_type, start_time, *event_params = data.split(',') - try: - start_time = int(start_time) - start_time = timedelta(milliseconds=start_time) - except ValueError: - return Storyboard.parse() + event_type, start_time_or_layer, *event_params = data.split(',') try: - event_type = cls.EventType(int(event_type)) + event_type = EventType(int(event_type)) except ValueError: try: - event_type = cls.EventType[event_type] + event_type = EventType[event_type] except KeyError: raise ValueError(f'Invalid event type, got {event_type}') - if event_type == cls.EventType.Background: - pass - elif event_type == cls.EventType.Video: - pass - elif event_type == cls.EventType.Break: + if event_type == EventType.Sprite: + layer = start_time_or_layer + return Sprite.parse(layer, event_params) + elif event_type == EventType.Animation: + layer = start_time_or_layer + return Animation.parse(layer, event_params) + try: + start_time = int(start_time_or_layer) + start_time = timedelta(milliseconds=start_time) + except ValueError: + raise ValueError(f'Invalid start_time provided, got {start_time}') + if event_type == EventType.Background: + return Background.parse(start_time, event_params) + elif event_type == EventType.Video: + return Video.parse(start_time, event_params) + elif event_type == EventType.Break: return Break.parse(start_time, event_params) # should be unreachable, added to enforce explicit handling of enums. else: @@ -75,11 +81,11 @@ class Background(Event): @property def start_time(self): - return self.start_time + return self._start_time @property def event_type(self): - return self.EventType.Background + return EventType.Background @classmethod def parse(cls, start_time, event_params): @@ -96,9 +102,10 @@ def parse(cls, start_time, event_params): y_offset = int(y_offset) except ValueError: raise ValueError(f'y_offset is invalid, got {y_offset}') + return cls(filename, x_offset, y_offset) def __init__(self, filename, x_offset, y_offset): - self.start_time = timedelta(milliseconds=0) + self._start_time = None self.filename = filename self.x_offset = x_offset self.y_offset = y_offset @@ -108,7 +115,7 @@ class Break(Event): @property def event_type(self): - return self.EventType.Break + return EventType.Break @classmethod def parse(cls, start_time, event_params): @@ -122,14 +129,14 @@ def parse(cls, start_time, event_params): return cls(start_time, end_time) def __init__(self, start_time, end_time): - self.start_time = start_time + self._start_time = start_time self.end_time = end_time class Video(Event): @property def event_type(self): - return self.EventType.Video + return EventType.Video @classmethod def parse(cls, start_time, event_params): @@ -149,13 +156,26 @@ def parse(cls, start_time, event_params): return cls(start_time, filename, x_offset, y_offset) def __init__(self, start_time, filename, x_offset, y_offset): - self.start_time = start_time + self._start_time = start_time self.filename = filename self.x_offset = x_offset self.y_offset = y_offset +class Sprite(Event): + + @property + def start_time(self): + raise ValueError('Currently unimplemented') + + @property + def event_type(self): + return EventType.Sprite + + @classmethod + def parse(cls, layer, params): + return cls() -class Storyboard(Event): +class Animation(Event): @property def start_time(self): @@ -163,10 +183,10 @@ def start_time(self): @property def event_type(self): - return self.EventType.Storyboard + return EventType.Animation @classmethod - def parse(cls): + def parse(cls, layer, params): return cls() @@ -1562,20 +1582,20 @@ def ar(self, def breaks(self): """The breaks with all other events filtered out. """ - return tuple(e for e in self.events if not isinstance(e, Break)) + return tuple(e for e in self.events if isinstance(e, Break)) @lazyval def background(self): """The background, if it exists, otherwise returns None. """ return next( - (e for e in self.events if not isinstance(e, Background)), None) + (e for e in self.events if isinstance(e, Background)), None) @lazyval def videos(self): """The videos with all other events filtered out. """ - return tuple(e for e in self.events if not isinstance(e, Break)) + return tuple(e for e in self.events if isinstance(e, Break)) @lazyval def hit_objects_no_spinners(self): @@ -1769,7 +1789,9 @@ def commit_group(): commit_group() current_group = line[1:-1] else: - group_buffer.append(line) + is_storyboard_param = line[0] == '_' or line[0] == ' ' + if not is_storyboard_param: + group_buffer.append(line) # commit the final group commit_group() From a26fd48b82ee25f319099bef491c293a3a0ba64c Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Tue, 5 May 2020 18:41:32 -0700 Subject: [PATCH 05/23] Fixed flake 8 lints --- slider/beatmap.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index dfb7341..73e026d 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -31,12 +31,16 @@ def _get(cs, ix, default=no_default): return default -EventType = Enum('EventType', - {'Background': 0, - 'Video': 1, - 'Break': 2, - 'Sprite': 3, - 'Animation': 4}) +_event_map = { + 'Background': 0, + 'Video': 1, + 'Break': 2, + 'Sprite': 3, + 'Animation': 4 + } + +EventType = Enum('EventType', _event_map) + class Event: @@ -55,11 +59,11 @@ def parse(cls, data): except KeyError: raise ValueError(f'Invalid event type, got {event_type}') if event_type == EventType.Sprite: - layer = start_time_or_layer + layer = start_time_or_layer return Sprite.parse(layer, event_params) elif event_type == EventType.Animation: layer = start_time_or_layer - return Animation.parse(layer, event_params) + return Animation.parse(layer, event_params) try: start_time = int(start_time_or_layer) start_time = timedelta(milliseconds=start_time) @@ -161,6 +165,7 @@ def __init__(self, start_time, filename, x_offset, y_offset): self.x_offset = x_offset self.y_offset = y_offset + class Sprite(Event): @property @@ -175,6 +180,7 @@ def event_type(self): def parse(cls, layer, params): return cls() + class Animation(Event): @property From 30116c59a58556e6be946eb281ef1bc085b8d417 Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Tue, 5 May 2020 18:57:28 -0700 Subject: [PATCH 06/23] Add simple test case for background --- slider/beatmap.py | 8 +++++++- slider/tests/test_beatmap.py | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 73e026d..d16ef40 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -48,6 +48,10 @@ class Event: def start_time(self): return self._start_time if self._start_time != 0 else None + @classmethod + def strip_quotes_from_filename(cls, filename): + return filename[1:-1] + @classmethod def parse(cls, data): event_type, start_time_or_layer, *event_params = data.split(',') @@ -95,6 +99,7 @@ def event_type(self): def parse(cls, start_time, event_params): try: filename, x_offset, y_offset = event_params + filename = cls.strip_quotes_from_filename(filename) except ValueError: raise ValueError( f'Missing param for Background, received {event_params}') @@ -109,7 +114,7 @@ def parse(cls, start_time, event_params): return cls(filename, x_offset, y_offset) def __init__(self, filename, x_offset, y_offset): - self._start_time = None + self._start_time = timedelta(milliseconds=0) self.filename = filename self.x_offset = x_offset self.y_offset = y_offset @@ -146,6 +151,7 @@ def event_type(self): def parse(cls, start_time, event_params): try: filename, x_offset, y_offset = event_params + filename = cls.strip_quotes_from_filename(filename) except ValueError: raise ValueError( f'Missing param for video, received {event_params}') diff --git a/slider/tests/test_beatmap.py b/slider/tests/test_beatmap.py index 8d2294a..86c8b47 100644 --- a/slider/tests/test_beatmap.py +++ b/slider/tests/test_beatmap.py @@ -153,3 +153,11 @@ def test_hp(beatmap): def test_od(beatmap): assert beatmap.od() == 9 + + +def test_background(beatmap): + background = beatmap.background + assert background.filename == 'miiro_no_scenario.png' + assert background.x_offset == 0 + assert background.y_offset == 0 + assert background.start_time == timedelta(milliseconds=0) From b370bf4bd2acb6fd981048ff11912feff8e81f9b Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Tue, 5 May 2020 19:04:14 -0700 Subject: [PATCH 07/23] Used strip instead vs method for stripping quotes off filename --- slider/beatmap.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index d16ef40..85eeaeb 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -48,10 +48,6 @@ class Event: def start_time(self): return self._start_time if self._start_time != 0 else None - @classmethod - def strip_quotes_from_filename(cls, filename): - return filename[1:-1] - @classmethod def parse(cls, data): event_type, start_time_or_layer, *event_params = data.split(',') @@ -99,7 +95,7 @@ def event_type(self): def parse(cls, start_time, event_params): try: filename, x_offset, y_offset = event_params - filename = cls.strip_quotes_from_filename(filename) + filename = filename.strip('"') except ValueError: raise ValueError( f'Missing param for Background, received {event_params}') @@ -151,7 +147,7 @@ def event_type(self): def parse(cls, start_time, event_params): try: filename, x_offset, y_offset = event_params - filename = cls.strip_quotes_from_filename(filename) + filename = filename.strip('"') except ValueError: raise ValueError( f'Missing param for video, received {event_params}') From 610b14eb5d340bc42a580fbbfb923e5924cdbea3 Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Tue, 5 May 2020 19:12:19 -0700 Subject: [PATCH 08/23] Pull Enum into class --- slider/beatmap.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 85eeaeb..f7d9673 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -39,8 +39,23 @@ def _get(cs, ix, default=no_default): 'Animation': 4 } -EventType = Enum('EventType', _event_map) +class EventType(Enum): + Background = 0 + Video = 1 + Break = 2 + Sprite = 3 + Animation = 4 + @staticmethod + def parse(event_type): + try: + event_type = EventType(int(event_type)) + except ValueError: + try: + event_type = EventType[event_type] + except KeyError: + raise ValueError(f'Invalid event type, got {event_type}') + return event_type class Event: @@ -51,13 +66,7 @@ def start_time(self): @classmethod def parse(cls, data): event_type, start_time_or_layer, *event_params = data.split(',') - try: - event_type = EventType(int(event_type)) - except ValueError: - try: - event_type = EventType[event_type] - except KeyError: - raise ValueError(f'Invalid event type, got {event_type}') + event_type = EventType.parse(event_type) if event_type == EventType.Sprite: layer = start_time_or_layer return Sprite.parse(layer, event_params) From 895bccb73b0c4b0f5e7c24e260ef509c5ac42a49 Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Tue, 5 May 2020 19:12:55 -0700 Subject: [PATCH 09/23] Fix flake 8 --- slider/beatmap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/slider/beatmap.py b/slider/beatmap.py index f7d9673..3e4da52 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -39,6 +39,7 @@ def _get(cs, ix, default=no_default): 'Animation': 4 } + class EventType(Enum): Background = 0 Video = 1 @@ -57,6 +58,7 @@ def parse(event_type): raise ValueError(f'Invalid event type, got {event_type}') return event_type + class Event: @property From d4190962c31f37e9822909ed130a39be7ad69a46 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 12 May 2022 00:15:20 -0400 Subject: [PATCH 10/23] pass events in test --- slider/tests/test_beatmap.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/slider/tests/test_beatmap.py b/slider/tests/test_beatmap.py index 409d0c4..2966967 100644 --- a/slider/tests/test_beatmap.py +++ b/slider/tests/test_beatmap.py @@ -177,7 +177,8 @@ def test_hit_objects_stacking(): slider_multiplier=1, slider_tick_rate=1, timing_points=[], - hit_objects=hit_objects + hit_objects=hit_objects, + events=[] ) radius = slider.beatmap.circle_radius(5) stack_offset = radius / 10 From dd2fe2336b62ddff2c1bfb7a377b217b58821a1b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 18 May 2022 16:06:14 -0400 Subject: [PATCH 11/23] rewrite structure and remove some methods --- slider/beatmap.py | 174 +++++++++++++---------------------- slider/tests/test_beatmap.py | 2 +- 2 files changed, 64 insertions(+), 112 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 673f4f7..4dd92aa 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -1,5 +1,5 @@ from datetime import timedelta -from enum import unique, IntEnum, Enum +from enum import unique, IntEnum from functools import partial import inspect from itertools import chain, islice, cycle @@ -32,79 +32,56 @@ def _get(cs, ix, default=no_default): return default -_event_map = { - 'Background': 0, - 'Video': 1, - 'Break': 2, - 'Sprite': 3, - 'Animation': 4 - } - - -class EventType(Enum): +class EventType(IntEnum): Background = 0 Video = 1 Break = 2 Sprite = 3 Animation = 4 - @staticmethod - def parse(event_type): - try: - event_type = EventType(int(event_type)) - except ValueError: - try: - event_type = EventType[event_type] - except KeyError: - raise ValueError(f'Invalid event type, got {event_type}') - return event_type - class Event: - - @property - def start_time(self): - return self._start_time if self._start_time != 0 else None + def __init__(self, event_type, start_time): + self.event_type = event_type + self.start_time = timedelta(milliseconds=start_time) @classmethod def parse(cls, data): event_type, start_time_or_layer, *event_params = data.split(',') - event_type = EventType.parse(event_type) - if event_type == EventType.Sprite: - layer = start_time_or_layer - return Sprite.parse(layer, event_params) - elif event_type == EventType.Animation: - layer = start_time_or_layer - return Animation.parse(layer, event_params) + event_type = EventType(int(event_type)) + + # TODO implement storyboarding events + if event_type is EventType.Sprite: + pass + if event_type is EventType.Animation: + pass + try: start_time = int(start_time_or_layer) - start_time = timedelta(milliseconds=start_time) except ValueError: raise ValueError(f'Invalid start_time provided, got {start_time}') - if event_type == EventType.Background: - return Background.parse(start_time, event_params) - elif event_type == EventType.Video: + + if event_type is EventType.Background: + return Background.parse(event_params) + if event_type is EventType.Video: return Video.parse(start_time, event_params) - elif event_type == EventType.Break: + if event_type is EventType.Break: return Break.parse(start_time, event_params) - # should be unreachable, added to enforce explicit handling of enums. - else: - raise ValueError( - f'Unimplemented event type: {event_type}') + # make sure we've handled all event types. + raise ValueError(f'Unimplemented event type: {event_type}') -class Background(Event): - @property - def start_time(self): - return self._start_time +class Background(Event): - @property - def event_type(self): - return EventType.Background + def __init__(self, filename, x_offset, y_offset): + super().__init__(EventType.Background, 0) + self.filename = filename + self.x_offset = x_offset + self.y_offset = y_offset @classmethod - def parse(cls, start_time, event_params): + def parse(cls, event_params): try: filename, x_offset, y_offset = event_params filename = filename.strip('"') @@ -115,45 +92,40 @@ def parse(cls, start_time, event_params): x_offset = int(x_offset) except ValueError: raise ValueError(f'x_offset is invalid, got {x_offset}') + try: y_offset = int(y_offset) except ValueError: raise ValueError(f'y_offset is invalid, got {y_offset}') - return cls(filename, x_offset, y_offset) - def __init__(self, filename, x_offset, y_offset): - self._start_time = timedelta(milliseconds=0) - self.filename = filename - self.x_offset = x_offset - self.y_offset = y_offset + return cls(filename, x_offset, y_offset) class Break(Event): - - @property - def event_type(self): - return EventType.Break + def __init__(self, start_time, end_time): + super().__init__(EventType.Break, start_time) + self.end_time = timedelta(milliseconds=end_time) @classmethod def parse(cls, start_time, event_params): + if not event_params: + raise ValueError('expected end_time paramter for Break') + try: - end_time = event_params[0] - end_time = timedelta(milliseconds=int(end_time)) - except IndexError: - raise ValueError(f'Beatmap is invalid, no end_time received') + end_time = int(event_params[0]) except ValueError: raise ValueError(f'Invalid end_time provided, got {end_time}') + return cls(start_time, end_time) - def __init__(self, start_time, end_time): - self._start_time = start_time - self.end_time = end_time class Video(Event): - @property - def event_type(self): - return EventType.Video + def __init__(self, start_time, filename, x_offset, y_offset): + super().__init__(EventType.Video, start_time) + self.filename = filename + self.x_offset = x_offset + self.y_offset = y_offset @classmethod def parse(cls, start_time, event_params): @@ -163,51 +135,20 @@ def parse(cls, start_time, event_params): except ValueError: raise ValueError( f'Missing param for video, received {event_params}') + try: x_offset = int(x_offset) except ValueError: raise ValueError(f'x_offset is invalid, got {x_offset}') + try: y_offset = int(y_offset) except ValueError: raise ValueError(f'y_offset is invalid, got {y_offset}') - return cls(start_time, filename, x_offset, y_offset) - - def __init__(self, start_time, filename, x_offset, y_offset): - self._start_time = start_time - self.filename = filename - self.x_offset = x_offset - self.y_offset = y_offset - - -class Sprite(Event): - - @property - def start_time(self): - raise ValueError('Currently unimplemented') - - @property - def event_type(self): - return EventType.Sprite - - @classmethod - def parse(cls, layer, params): - return cls() + return cls(start_time, filename, x_offset, y_offset) -class Animation(Event): - - @property - def start_time(self): - raise ValueError('Currently unimplemented') - - @property - def event_type(self): - return EventType.Animation - @classmethod - def parse(cls, layer, params): - return cls() class TimingPoint: @@ -1868,6 +1809,7 @@ def __init__(self, self.slider_tick_rate = slider_tick_rate self.timing_points = timing_points self.events = events + self._hit_objects = hit_objects # cache hit object stacking at different ar and cs values self._hit_objects_with_stacking = {} @@ -2056,22 +1998,32 @@ def ar(self, @lazyval def breaks(self): - """The breaks with all other events filtered out. + """The breaks of this beatmap. """ return tuple(e for e in self.events if isinstance(e, Break)) @lazyval - def background(self): - """The background, if it exists, otherwise returns None. + def backgrounds(self): + """The backgrounds of this beatmap. """ - return next( - (e for e in self.events if isinstance(e, Background)), None) + return tuple(e for e in self.events if isinstance(e, Background)) @lazyval def videos(self): - """The videos with all other events filtered out. + """The videos of this beatmap. """ - return tuple(e for e in self.events if isinstance(e, Break)) + return tuple(e for e in self.events if isinstance(e, Video)) + + def sprites(self): + """The sprites of this beatmap. + """ + return tuple(e for e in self.events if isinstance(e, Sprite)) + + def animations(self): + """The animations of this beatmap. + """ + return tuple(e for e in self.events if isinstance(e, Animation)) + def hit_objects(self, *, diff --git a/slider/tests/test_beatmap.py b/slider/tests/test_beatmap.py index 2966967..06c4095 100644 --- a/slider/tests/test_beatmap.py +++ b/slider/tests/test_beatmap.py @@ -241,7 +241,7 @@ def test_od(beatmap): def test_background(beatmap): - background = beatmap.background + background = beatmap.backgrounds[0] assert background.filename == 'miiro_no_scenario.png' assert background.x_offset == 0 assert background.y_offset == 0 From 05bcc3a30a8eb5a39f5688eaedef0b4b310eff97 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 18 May 2022 16:17:14 -0400 Subject: [PATCH 12/23] linting fixes --- slider/beatmap.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 4dd92aa..5ef63f9 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -119,7 +119,6 @@ def parse(cls, start_time, event_params): return cls(start_time, end_time) - class Video(Event): def __init__(self, start_time, filename, x_offset, y_offset): super().__init__(EventType.Video, start_time) @@ -149,8 +148,6 @@ def parse(cls, start_time, event_params): return cls(start_time, filename, x_offset, y_offset) - - class TimingPoint: """A timing point assigns properties to an offset into a beatmap. @@ -179,7 +176,6 @@ class TimingPoint: kiai_mode : bool Wheter or not kiai time effects are active. """ - def __init__(self, offset, ms_per_beat, From d0e8a27edc5cfe50935ea8e99326c562bb777ff5 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 18 May 2022 16:17:21 -0400 Subject: [PATCH 13/23] remove unimplemented methods --- slider/beatmap.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 5ef63f9..eca3e89 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -2010,17 +2010,6 @@ def videos(self): """ return tuple(e for e in self.events if isinstance(e, Video)) - def sprites(self): - """The sprites of this beatmap. - """ - return tuple(e for e in self.events if isinstance(e, Sprite)) - - def animations(self): - """The animations of this beatmap. - """ - return tuple(e for e in self.events if isinstance(e, Animation)) - - def hit_objects(self, *, circles=True, From 17c00c4f159b7c695b8f3a5e4b7b1b8be925fbf9 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 19 May 2022 11:54:04 -0400 Subject: [PATCH 14/23] don't require background x and y offset --- slider/beatmap.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index eca3e89..eeeb2f7 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -82,12 +82,22 @@ def __init__(self, filename, x_offset, y_offset): @classmethod def parse(cls, event_params): - try: - filename, x_offset, y_offset = event_params - filename = filename.strip('"') - except ValueError: - raise ValueError( - f'Missing param for Background, received {event_params}') + if len(event_params) == 0: + raise ValueError('expected filename parameter for Background') + + filename = event_params[0].strip('"') + x_offset = 0 + y_offset = 0 + + # x_offset and y_offset are optional, default to 0 + if len(event_params) > 1: + x_offset = event_params[1] + if len(event_params) > 2: + y_offset = event_params[2] + if len(event_params) > 3: + raise ValueError("expected no more than 3 params for Background, " + f"but got params {event_params}") + try: x_offset = int(x_offset) except ValueError: From 522be3a6e650032424937960c273b8fffeeb2de3 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 19 May 2022 11:54:19 -0400 Subject: [PATCH 15/23] fix error on sprite/animation parsing --- slider/beatmap.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index eeeb2f7..975510f 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -52,9 +52,9 @@ def parse(cls, data): # TODO implement storyboarding events if event_type is EventType.Sprite: - pass + return Sprite() if event_type is EventType.Animation: - pass + return Animation() try: start_time = int(start_time_or_layer) @@ -157,6 +157,11 @@ def parse(cls, start_time, event_params): return cls(start_time, filename, x_offset, y_offset) +# TODO implement these events +class Sprite: + pass +class Animation: + pass class TimingPoint: """A timing point assigns properties to an offset into a beatmap. From 5b2dbf376f1acc0d7b183b5109b2d19629ed9496 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 19 May 2022 12:04:17 -0400 Subject: [PATCH 16/23] linting fixes --- slider/beatmap.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 975510f..ab4c247 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -95,8 +95,10 @@ def parse(cls, event_params): if len(event_params) > 2: y_offset = event_params[2] if len(event_params) > 3: - raise ValueError("expected no more than 3 params for Background, " - f"but got params {event_params}") + raise ValueError( + "expected no more than 3 params for Background, " + f"but got params {event_params}" + ) try: x_offset = int(x_offset) @@ -157,12 +159,16 @@ def parse(cls, start_time, event_params): return cls(start_time, filename, x_offset, y_offset) + # TODO implement these events class Sprite: pass + + class Animation: pass + class TimingPoint: """A timing point assigns properties to an offset into a beatmap. From 6c7e3d76fbaf4d20fb30b4d957270e59b1a22d1d Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 19 May 2022 14:12:24 -0400 Subject: [PATCH 17/23] remove weird storyboard check --- slider/beatmap.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index ab4c247..2cf8dc4 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -2559,9 +2559,7 @@ def commit_group(): commit_group() current_group = line[1:-1] else: - is_storyboard_param = line[0] == '_' or line[0] == ' ' - if not is_storyboard_param: - group_buffer.append(line) + group_buffer.append(line) # commit the final group commit_group() From 1d97a0cbaf8cde4dc73ce8abf46f07a8389cf26f Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Thu, 19 May 2022 14:14:15 -0400 Subject: [PATCH 18/23] assert exactly one background in test --- slider/tests/test_beatmap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/slider/tests/test_beatmap.py b/slider/tests/test_beatmap.py index 06c4095..058493b 100644 --- a/slider/tests/test_beatmap.py +++ b/slider/tests/test_beatmap.py @@ -241,6 +241,7 @@ def test_od(beatmap): def test_background(beatmap): + assert len(beatmap.backgrounds) == 1 background = beatmap.backgrounds[0] assert background.filename == 'miiro_no_scenario.png' assert background.x_offset == 0 From f60fb9c6155c19a692c185dbb440f87fb8f72446 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 May 2022 17:01:12 -0400 Subject: [PATCH 19/23] allow string values for EventType --- slider/beatmap.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 2cf8dc4..3ea326b 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -39,6 +39,16 @@ class EventType(IntEnum): Sprite = 3 Animation = 4 + @classmethod + def _missing_(cls, value): + return { + "Background": EventType.Background, + "Video": EventType.Video, + "Break": EventType.Break, + "Sprite": EventType.Sprite, + "Animation": EventType.Animation + }[value] + class Event: def __init__(self, event_type, start_time): @@ -48,7 +58,13 @@ def __init__(self, event_type, start_time): @classmethod def parse(cls, data): event_type, start_time_or_layer, *event_params = data.split(',') - event_type = EventType(int(event_type)) + + # event types are allowed to be specified as either integers or strings. + # try parsing as an int first, and just leave it alone otherwise (our + # enum instantiation will take care of validation). + if event_type.isdigit(): + event_type = int(event_type) + event_type = EventType(event_type) # TODO implement storyboarding events if event_type is EventType.Sprite: From d7b9b0b33a342642959b76bff5eb305df5164053 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 May 2022 17:01:21 -0400 Subject: [PATCH 20/23] fix video parsing --- slider/beatmap.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 3ea326b..6a322f5 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -156,12 +156,23 @@ def __init__(self, start_time, filename, x_offset, y_offset): @classmethod def parse(cls, start_time, event_params): - try: - filename, x_offset, y_offset = event_params - filename = filename.strip('"') - except ValueError: + if len(event_params) == 0: + raise ValueError('expected filename parameter for Video') + + filename = event_params[0].strip('"') + x_offset = 0 + y_offset = 0 + + # x_offset and y_offset are optional, default to 0 + if len(event_params) > 1: + x_offset = event_params[1] + if len(event_params) > 2: + y_offset = event_params[2] + if len(event_params) > 3: raise ValueError( - f'Missing param for video, received {event_params}') + "expected no more than 3 params for Video, " + f"but got params {event_params}" + ) try: x_offset = int(x_offset) From b982d9dd02b1b3a1931ed2bbb6261ca93897f090 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 May 2022 17:10:38 -0400 Subject: [PATCH 21/23] ignore storyboard layer events --- slider/beatmap.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/slider/beatmap.py b/slider/beatmap.py index 6a322f5..7ddcd98 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -2643,6 +2643,14 @@ def parse(cls, data): events = [] for raw_event in groups['Events']: + # storyboard elements (sprites and animations) are followed by a + # list of layers and positions that they appear on. This list is + # indented with a space. We'll want to parse these properly + # eventually, but they're not Events and we'll error if we try to + # parse them as such right now, so just ignore them for now. + if raw_event[0] in ["F", "M", "S", "L", "R"] and raw_event[1] == ",": + # TODO implement storyboard layers / events + continue event = Event.parse(raw_event) events.append(event) From ec0bb37e64b74a81b6b494d76a5443caefbd81cb Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 May 2022 17:11:41 -0400 Subject: [PATCH 22/23] linting fixes --- slider/beatmap.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 7ddcd98..46cd7c2 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -59,9 +59,9 @@ def __init__(self, event_type, start_time): def parse(cls, data): event_type, start_time_or_layer, *event_params = data.split(',') - # event types are allowed to be specified as either integers or strings. - # try parsing as an int first, and just leave it alone otherwise (our - # enum instantiation will take care of validation). + # event types are allowed to be specified as either integers or + # strings. try parsing as an int first, and just leave it alone + # otherwise (our enum instantiation will take care of validation). if event_type.isdigit(): event_type = int(event_type) event_type = EventType(event_type) @@ -2648,7 +2648,10 @@ def parse(cls, data): # indented with a space. We'll want to parse these properly # eventually, but they're not Events and we'll error if we try to # parse them as such right now, so just ignore them for now. - if raw_event[0] in ["F", "M", "S", "L", "R"] and raw_event[1] == ",": + if( + raw_event[0] in ["F", "M", "S", "L", "R"] and + raw_event[1] == "," + ): # TODO implement storyboard layers / events continue event = Event.parse(raw_event) From 48bad0cfe3afc6bd3e8327a8576ca4f675361593 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 May 2022 20:12:21 -0400 Subject: [PATCH 23/23] add placeholder sample class --- slider/beatmap.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/slider/beatmap.py b/slider/beatmap.py index 46cd7c2..d33e1a9 100644 --- a/slider/beatmap.py +++ b/slider/beatmap.py @@ -38,6 +38,10 @@ class EventType(IntEnum): Break = 2 Sprite = 3 Animation = 4 + # TODO I have absolutely no idea if Sample is really supposed to be 5. + # See https://osu.ppy.sh/beatmapsets/14902#osu/54879 for a map with a + # sample event + Sample = 5 @classmethod def _missing_(cls, value): @@ -46,7 +50,8 @@ def _missing_(cls, value): "Video": EventType.Video, "Break": EventType.Break, "Sprite": EventType.Sprite, - "Animation": EventType.Animation + "Animation": EventType.Animation, + "Sample": EventType.Sample }[value] @@ -71,6 +76,8 @@ def parse(cls, data): return Sprite() if event_type is EventType.Animation: return Animation() + if event_type is EventType.Sample: + return Sample() try: start_time = int(start_time_or_layer) @@ -196,6 +203,10 @@ class Animation: pass +class Sample: + pass + + class TimingPoint: """A timing point assigns properties to an offset into a beatmap.