Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added event support (except for storyboards) #69

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 216 additions & 2 deletions slider/beatmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,181 @@ def _get(cs, ix, default=no_default):
return default


class EventType(IntEnum):
Background = 0
Video = 1
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):
return {
"Background": EventType.Background,
"Video": EventType.Video,
"Break": EventType.Break,
"Sprite": EventType.Sprite,
"Animation": EventType.Animation,
"Sample": EventType.Sample
}[value]


class Event:
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 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:
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)
except ValueError:
raise ValueError(f'Invalid start_time provided, got {start_time}')

if event_type is EventType.Background:
return Background.parse(event_params)
if event_type is EventType.Video:
return Video.parse(start_time, event_params)
if event_type is EventType.Break:
return Break.parse(start_time, event_params)

# make sure we've handled all event types.
raise ValueError(f'Unimplemented event type: {event_type}')


class Background(Event):

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, 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:
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)


class Break(Event):
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 = int(event_params[0])
except ValueError:
raise ValueError(f'Invalid end_time provided, got {end_time}')

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)
self.filename = filename
self.x_offset = x_offset
self.y_offset = y_offset

@classmethod
def parse(cls, start_time, event_params):
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(
"expected no more than 3 params for Video, "
f"but got params {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)


# TODO implement these events
class Sprite:
pass


class Animation:
pass


class Sample:
pass


class TimingPoint:
"""A timing point assigns properties to an offset into a beatmap.

Expand Down Expand Up @@ -1611,6 +1786,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
-----
Expand Down Expand Up @@ -1652,7 +1829,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
Expand Down Expand Up @@ -1685,6 +1863,8 @@ def __init__(self,
self.slider_multiplier = slider_multiplier
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 = {}
Expand Down Expand Up @@ -1871,6 +2051,24 @@ def ar(self,

return ar

@lazyval
def breaks(self):
"""The breaks of this beatmap.
"""
return tuple(e for e in self.events if isinstance(e, Break))

@lazyval
def backgrounds(self):
"""The backgrounds of this beatmap.
"""
return tuple(e for e in self.events if isinstance(e, Background))
tybug marked this conversation as resolved.
Show resolved Hide resolved

@lazyval
def videos(self):
"""The videos of this beatmap.
"""
return tuple(e for e in self.events if isinstance(e, Video))

def hit_objects(self,
*,
circles=True,
Expand Down Expand Up @@ -2454,6 +2652,22 @@ def parse(cls, data):
parent = timing_point
timing_points.append(timing_point)

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)

slider_multiplier = _get_as_float(
groups,
'Difficulty',
Expand Down Expand Up @@ -2566,7 +2780,7 @@ def parse(cls, data):
),
groups['HitObjects'],
)),

events=events,
)

def pack(self):
Expand Down
12 changes: 11 additions & 1 deletion slider/tests/test_beatmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -239,6 +240,15 @@ def test_od(beatmap):
assert beatmap.od() == 9


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
assert background.y_offset == 0
assert background.start_time == timedelta(milliseconds=0)


def test_pack(beatmap):
# Pack the beatmap and parse it again to see if there is difference.
packed_str = beatmap.pack()
Expand Down