This repository has been archived on 2022-02-23. You can view files and clone it, but cannot push or open issues or pull requests.
lets/pp/catch_the_pp/osu_parser/beatmap.pyx

223 lines
8.6 KiB
Cython

from . import mathhelper
from .hitobject import HitObject
cdef class Beatmap(object):
"""
Beatmap object for beatmap parsing and handling
"""
cdef public str file_name
cdef public int version
cdef public int header
cdef public dict difficulty
cdef public dict timing_points
cdef public float slider_point_distance
cdef public list hitobjects
cdef public int max_combo
def __init__(self, file_name):
"""
file_name -- Directory for beatmap file (.osu)
"""
self.file_name = file_name
self.version = -1 #Unknown by default
self.header = -1
self.difficulty = {}
self.timing_points = {
"raw_bpm": {}, #Raw bpm modifier code
"raw_spm": {}, #Raw speed modifier code
"bpm": {}, #Beats pr minute
"spm": {} #Speed modifier
}
self.slider_point_distance = 1 #Changes after [Difficulty] is fully parsed
self.hitobjects = []
self.max_combo = 0
self.parse_beatmap()
if "ApproachRate" not in self.difficulty.keys(): #Fix old osu version
self.difficulty["ApproachRate"] = self.difficulty["OverallDifficulty"]
cpdef parse_beatmap(self):
"""
Parses beatmap file line by line by passing each line into parse_line.
"""
cdef str line
with open(self.file_name, encoding="utf8") as file_stream:
ver_line = ""
while len(ver_line) < 2: #Find the line where beatmap version is spesified (normaly first line)
ver_line = file_stream.readline()
self.version = int(''.join(list(filter(str.isdigit, ver_line)))) #Set version
for line in file_stream:
self.parse_line(line.replace("\n", ""))
cpdef parse_line(self, str line):
"""
Parse a beatmapfile line.
Handles lines that are required for our use case (Difficulty, TimingPoints & hitobjects),
everything else is skipped.
"""
if len(line) < 1:
return
if line.startswith("["):
if line == "[Difficulty]":
self.header = 0
elif line == "[TimingPoints]":
self.header = 1
elif line == "[HitObjects]":
self.header = 2
self.slider_point_distance = (100 * self.difficulty["SliderMultiplier"]) / self.difficulty["SliderTickRate"]
else:
self.header = -1
return
if self.header == -1: #We return if we are reading under a header we dont care about
return
if self.header == 0:
self.handle_difficulty_propperty(line)
elif self.header == 1:
self.handle_timing_point(line)
elif self.header == 2:
self.handle_hitobject(line)
cpdef handle_difficulty_propperty(self, str propperty):
"""
Puts the [Difficulty] propperty into the difficulty dict.
"""
prop = propperty.split(":")
self.difficulty[prop[0]] = float(prop[1])
cpdef handle_timing_point(self, str timing_point):
"""
Formats timing points used for slider velocity changes,
and store them into self.timing_points dict.
"""
timing_point_split = timing_point.split(",")
timing_point_time = int(float(timing_point_split[0])) #Fixes some special mappers special needs
timing_point_focus = timing_point_split[1]
timing_point_type = 1
if len(timing_point_split) >= 7: #Fix for old beatmaps that only stores bpm change and timestamp (only BPM change) [v3?]
timing_point_type = int(timing_point_split[6])
if timing_point_type == 0 and not timing_point_focus.startswith("-"):
timing_point_focus = "-100"
if timing_point_focus.startswith("-"): #If not then its not a slider velocity modifier
self.timing_points["spm"][timing_point_time] = -100 / float(timing_point_focus) #Convert to normalized value and store
self.timing_points["raw_spm"][timing_point_time] = float(timing_point_focus)
else:
if len(self.timing_points["bpm"]) == 0: #Fixes if hitobjects shows up before bpm is set
timing_point_time = 0
self.timing_points["bpm"][timing_point_time] = 60000 / float(timing_point_focus)#^
self.timing_points["raw_bpm"][timing_point_time] = float(timing_point_focus)
#This trash of a game resets the spm when bpm change >.>
self.timing_points["spm"][timing_point_time] = 1
self.timing_points["raw_spm"][timing_point_time] = -100
cpdef handle_hitobject(self, str line):
"""
Puts every hitobject into the hitobjects array.
Creates hitobjects, hitobject_sliders or skip depending on the given data.
We skip everything that is not important for us for our use case (Spinners)
"""
split_object = line.split(",")
time = int(split_object[2])
object_type = int(split_object[3])
if not (1 & object_type > 0 or 2 & object_type > 0): #We only want sliders and circles as spinners are random bannanas etc.
return
if 2 & object_type: #Slider
repeat = int(split_object[6])
pixel_length = float(split_object[7])
time_point = self.get_timing_point_all(time)
tick_distance = (100 * self.difficulty["SliderMultiplier"]) / self.difficulty["SliderTickRate"]
if self.version >= 8:
tick_distance /= (mathhelper.clamp(-time_point["raw_spm"], 10, 1000) / 100)
curve_split = split_object[5].split("|")
curve_points = []
for i in range(1, len(curve_split)):
vector_split = curve_split[i].split(":")
vector = mathhelper.Vec2(int(vector_split[0]), int(vector_split[1]))
curve_points.append(vector)
slider_type = curve_split[0]
if self.version <= 6 and len(curve_points) >= 2:
if slider_type == "L":
slider_type = "B"
if len(curve_points) == 2:
if (int(split_object[0]) == curve_points[0].x and int(split_object[1]) == curve_points[0].y) or (curve_points[0].x == curve_points[1].x and curve_points[0].y == curve_points[1].y):
del curve_points[0]
slider_type = "L"
if len(curve_points) == 0: #Incase of ExGon meme (Sliders that acts like hitcircles)
hitobject = HitObject(int(split_object[0]), int(split_object[1]), time, 1)
else:
hitobject = HitObject(int(split_object[0]), int(split_object[1]), time, object_type, slider_type, curve_points, repeat, pixel_length, time_point, self.difficulty, tick_distance)
else:
hitobject = HitObject(int(split_object[0]), int(split_object[1]), time, object_type)
self.hitobjects.append(hitobject)
self.max_combo += hitobject.get_combo()
def get_timing_point_all(self, time):
"""
Returns a object of all current timing types
time -- timestamp
return -- {"raw_bpm": Float, "raw_spm": Float, "bpm": Float, "spm": Float}
"""
types = {
"raw_bpm": 600,
"raw_spm": -100,
"bpm": 100,
"spm": 1
} #Will return the default value if timing point were not found
for t in types.keys():
r = self.get_timing_point(time, t)
if r is not None:
types[t] = r
#else:
#print("{} were not found for timestamp {}, using {} instead.".format(t, time, types[t]))
return types
def get_timing_point(self, time, timing_type):
"""
Returns latest timing point by timestamp (Current)
time -- timestamp
timing_type -- mpb, bmp or spm
return -- self.timing_points object
"""
r = None
try:
for key in sorted(self.timing_points[timing_type].keys(), key=lambda k: k):
if key <= time:
r = self.timing_points[timing_type][key]
else:
break
except Exception as e:
print(e)
return r
def get_object_count(self):
"""
Get the total hitobject count for the parsed beatmap (Normal hitobjects, sliders & sliderticks)
return -- total hitobjects for parsed beatmap
"""
cdef int count = 0
for hitobject in self.hitobjects:
count += hitobject.get_points()
return count