Initial commit

This commit is contained in:
Josh
2018-12-09 00:15:56 -05:00
commit aad3c9bb54
125 changed files with 18177 additions and 0 deletions

View File

View File

@@ -0,0 +1,222 @@
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

View File

@@ -0,0 +1,166 @@
import math
from .. import constants
from . import mathhelper
class Linear(object): #Because it made sense at the time...
def __init__(self, points):
self.pos = points
cdef class Bezier(object):
cdef public list points, pos
cdef public int order
def __init__(self, points):
self.points = points
self.order = len(self.points)
self.pos = []
self.calc_points()
cpdef calc_points(self):
if len(self.pos) != 0: #This should never happen but since im working on this I want to warn myself if I fuck up
raise Exception("Bezier was calculated twice!")
cdef list sub_points = []
for i in range(len(self.points)):
if i == len(self.points) - 1:
sub_points.append(self.points[i])
self.bezier(sub_points)
sub_points.clear()
elif len(sub_points) > 1 and self.points[i] == sub_points[-1]:
self.bezier(sub_points)
sub_points.clear()
sub_points.append(self.points[i])
cpdef bezier(self, list points):
cdef int order = len(points)
cdef float step = 0.25 / constants.SLIDER_QUALITY / order #Normaly 0.0025
cdef float i = 0
cdef int n = order - 1
cdef float x, y
cdef int p
while i < 1 + step:
x = 0
y = 0
for p in range(n + 1):
a = mathhelper.cpn(p, n) * ((1 - i) ** (n - p)) * (i ** p)
x += a * points[p].x
y += a * points[p].y
point = mathhelper.Vec2(x, y)
self.pos.append(point)
i += step
def point_at_distance(self, length):
return {
0: False,
1: self.points[0],
}.get(self.order, self.rec(length))
def rec(self, length):
return mathhelper.point_at_distance(self.pos, length)
cdef class Catmull(object): #Yes... I cry deep down on the inside aswell
cdef public list points, pos
cdef public int order
cdef public float step
def __init__(self, points):
self.points = points
self.order = len(points)
self.step = 2.5 / constants.SLIDER_QUALITY #Normaly 0.025
self.pos = []
self.calc_points()
cpdef calc_points(self):
if len(self.pos) != 0: #This should never happen but since im working on this I want to warn myself if I fuck up
raise Exception("Catmull was calculated twice!")
cdef int x
cdef float t
cdef object v1, v2, v3
for x in range(self.order - 1):
t = 0
while t < self.step + 1:
if x >= 1:
v1 = self.points[x - 1]
else:
v1 = self.points[x]
v2 = self.points[x]
if x + 1 < self.order:
v3 = self.points[x + 1]
else:
v3 = v2.calc(1, v2.calc(-1, v1))
if x + 2 < self.order:
v4 = self.points[x + 2]
else:
v4 = v3.calc(1, v3.calc(-1, v2))
point = get_point([v1, v2, v3, v4], t)
self.pos.append(point)
t += self.step
def point_at_distance(self, length):
return {
0: False,
1: self.points[0],
}.get(self.order, self.rec(length))
def rec(self, length):
return mathhelper.point_at_distance(self.pos, length)
cdef class Perfect(object):
cdef public list points
cdef float cx, cy
cdef float radius
def __init__(self, points):
self.points = points
self.cx = 0
self.cy = 0
self.radius = 0
self.setup_path()
def setup_path(self):
self.cx, self.cy, self.radius = get_circum_circle(self.points)
if is_left(self.points):
self.radius *= -1
cpdef point_at_distance(self, float length):
cdef float radians = length / self.radius
return rotate(self.cx, self.cy, self.points[0], radians)
cpdef object get_point(object p, float length):
cdef float x = mathhelper.catmull([o.x for o in p], length)
cdef float y = mathhelper.catmull([o.y for o in p], length)
return mathhelper.Vec2(x, y)
cpdef tuple get_circum_circle(list p):
cdef float d = 2 * (p[0].x * (p[1].y - p[2].y) + p[1].x * (p[2].y - p[0].y) + p[2].x * (p[0].y - p[1].y))
if d == 0:
raise Exception("Invalid circle! Unable to chose angle.")
cdef float ux = ((pow(p[0].x, 2) + pow(p[0].y, 2)) * (p[1].y - p[2].y) + (pow(p[1].x, 2) + pow(p[1].y, 2)) * (p[2].y - p[0].y) + (pow(p[2].x, 2) + pow(p[2].y, 2)) * (p[0].y - p[1].y)) / d
cdef float uy = ((pow(p[0].x, 2) + pow(p[0].y, 2)) * (p[2].x - p[1].x) + (pow(p[1].x, 2) + pow(p[1].y, 2)) * (p[0].x - p[2].x) + (pow(p[2].x, 2) + pow(p[2].y, 2)) * (p[1].x - p[0].x)) / d
cdef float px = ux - p[0].x
cdef float py = uy - p[0].y
cdef float r = pow(pow(px, 2) + pow(py, 2), 0.5)
return ux, uy, r
cpdef float is_left(object p):
return ((p[1].x - p[0].x) * (p[2].y - p[0].y) - (p[1].y - p[0].y) * (p[2].x - p[0].x)) < 0
cpdef object rotate(float cx, float cy, object p, float radians):
cdef float cos = math.cos(radians)
cdef float sin = math.sin(radians)
return mathhelper.Vec2((cos * (p.x - cx)) - (sin * (p.y - cy)) + cx, (sin * (p.x - cx)) + (cos * (p.y - cy)) + cy)

View File

@@ -0,0 +1,169 @@
import copy
from . import mathhelper
from . import curves
cdef class SliderTick:
cdef public float x, y, time
def __init__(self, x, y, time):
self.x = x
self.y = y
self.time = time
cdef class HitObject(object):
cdef public float x, y, time, end_time, pixel_length, tick_distance, duration
cdef public int type, repeat
cdef public str slider_type
cdef public list curve_points, ticks, end_ticks, path
cdef public dict timing_point
cdef public object difficulty, end
def __init__(self, x, y, time, object_type, slider_type = None, curve_points = None, repeat = 1, pixel_length = 0, timing_point = None, difficulty = None, tick_distance = 1):
"""
HitObject params for normal hitobject and sliders
x -- x position
y -- y position
time -- timestamp
object_type -- type of object (bitmask)
[+] IF SLIDER
slider_type -- type of slider (L, P, B, C)
curve_points -- points in the curve path
repeat -- amount of repeats for the slider (+1)
pixel_length -- length of the slider
timing_point -- ref of current timing point for the timestamp
difficulty -- ref of beatmap difficulty
tick_distance -- distance betwin each slidertick
"""
self.x = x
self.y = y
self.time = time
self.end_time = 0
self.type = object_type
#isSlider?
if 2 & self.type:
self.slider_type = slider_type
self.curve_points = [mathhelper.Vec2(self.x, self.y)] + curve_points
self.repeat = repeat
self.pixel_length = pixel_length
#For slider tick calculations
self.timing_point = timing_point
self.difficulty = difficulty
self.tick_distance = tick_distance
self.duration = (int(self.timing_point["raw_bpm"]) * (pixel_length / (self.difficulty["SliderMultiplier"] * self.timing_point["spm"])) / 100) * self.repeat
self.ticks = []
self.end_ticks = []
self.path = []
self.end = None
self.calc_slider()
def calc_slider(self, calc_path = False):
#Fix broken objects
if self.slider_type == "P" and len(self.curve_points) > 3:
self.slider_type = "B"
elif len(self.curve_points) == 2:
self.slider_type = "L"
#Make curve
if self.slider_type == "P": #Perfect
try:
curve = curves.Perfect(self.curve_points)
except:
curve = curves.Bezier(self.curve_points)
self.slider_type = "B"
elif self.slider_type == "B": #Bezier
curve = curves.Bezier(self.curve_points)
elif self.slider_type == "C": #Catmull
curve = curves.Catmull(self.curve_points)
#Quickest to skip this
if calc_path: #Make path if requested (For drawing visual for testing)
if self.slider_type == "L": #Linear
self.path = curves.Linear(self.curve_points).pos
elif self.slider_type == "P": #Perfect
self.path = []
l = 0
step = 5
while l <= self.pixel_length:
self.path.append(curve.point_at_distance(l))
l += step
elif self.slider_type == "B": #Bezier
self.path = curve.pos
elif self.slider_type == "C": #Catmull
self.path = curve.pos
else:
raise Exception("Slidertype not supported! ({})".format(self.slider_type))
#Set slider ticks
current_distance = self.tick_distance
time_add = self.duration * (self.tick_distance / (self.pixel_length * self.repeat))
while current_distance < self.pixel_length - self.tick_distance / 8:
if self.slider_type == "L": #Linear
point = mathhelper.point_on_line(self.curve_points[0], self.curve_points[1], current_distance)
else: #Perfect, Bezier & Catmull uses the same function
point = curve.point_at_distance(current_distance)
self.ticks.append(SliderTick(point.x, point.y, self.time + time_add * (len(self.ticks) + 1)))
current_distance += self.tick_distance
#Adds slider_ends / repeat_points
repeat_id = 1
repeat_bonus_ticks = []
while repeat_id < self.repeat:
dist = (1 & repeat_id) * self.pixel_length
time_offset = (self.duration / self.repeat) * repeat_id
if self.slider_type == "L": #Linear
point = mathhelper.point_on_line(self.curve_points[0], self.curve_points[1], dist)
else: #Perfect, Bezier & Catmull uses the same function
point = curve.point_at_distance(dist)
self.end_ticks.append(SliderTick(point.x, point.y, self.time + time_offset))
#Adds the ticks that already exists on the slider back (but reversed)
repeat_ticks = copy.deepcopy(self.ticks)
if 1 & repeat_id: #We have to reverse the timing normalizer
repeat_ticks = list(reversed(repeat_ticks))
normalize_time_value = self.time + (self.duration / self.repeat)
else:
normalize_time_value = self.time
#Correct timing
for tick in repeat_ticks:
tick.time = self.time + time_offset + abs(tick.time - normalize_time_value)
repeat_bonus_ticks += repeat_ticks
repeat_id += 1
self.ticks += repeat_bonus_ticks
#Add endpoint for slider
dist_end = (1 & self.repeat) * self.pixel_length
if self.slider_type == "L": #Linear
point = mathhelper.point_on_line(self.curve_points[0], self.curve_points[1], dist_end)
else: #Perfect, Bezier & Catmull uses the same function
point = curve.point_at_distance(dist_end)
self.end_ticks.append(SliderTick(point.x, point.y, self.time + self.duration))
def get_combo(self):
"""
Returns the combo given by this object
1 if normal hitobject, 2+ if slider (adds sliderticks)
"""
if 2 & self.type: #Slider
val = 1 #Start of the slider
val += len(self.ticks) #The amount of sliderticks
val += self.repeat #Reverse slider
else: #Normal
val = 1 #Itself...
return val

View File

@@ -0,0 +1,124 @@
import math
cpdef float clamp(float value, float mn, float mx):
return min(max(mn, value), mx)
cpdef sign(float value):
if value == 0:
return 0
elif value > 0:
return 1
else:
return -1
cpdef cpn(int p, int n):
if p < 0 or p > n:
return 0
p = min(p, n - p)
out = 1
for i in range(1, p + 1):
out = out * (n - p + i) / i
return out
cpdef float catmull(p, t): # WARNING: Worst math formula incomming
return 0.5 * (
(2 * p[1]) +
(-p[0] + p[2]) * t +
(2 * p[0] - 5 * p[1] + 4 * p[2] - p[3]) * (t ** 2) +
(-p[0] + 3 * p[1] - 3 * p[2] + p[3]) * (t ** 3))
cpdef Vec2 point_on_line(Vec2 p0, Vec2 p1, float length):
cdef float full_length = (((p1.x - p0.x) ** 2) + ((p1.y - p0.y) ** 2)) ** 0.5
cdef float n = full_length - length
if full_length == 0: #Fix for something that seems unknown... (We warn if this happens)
full_length = 1
cdef float x = (n * p0.x + length * p1.x) / full_length
cdef float y = (n * p0.y + length * p1.y) / full_length
return Vec2(x, y)
cpdef float angle_from_points(Vec2 p0, Vec2 p1):
return math.atan2(p1.y - p0.y, p1.x - p0.x)
cpdef float distance_from_points(array):
cdef float distance = 0
cdef int i
for i in range(1, len(array)):
distance += array[i].distance(array[i - 1])
return distance
cpdef Vec2 cart_from_pol(r, t):
cdef float x = (r * math.cos(t))
cdef float y = (r * math.sin(t))
return Vec2(x, y)
cpdef point_at_distance(array, float distance): #TODO: Optimize...
cdef int i = 0
cdef float x, y, current_distance = 0, new_distance = 0, angle
cdef Vec2 coord, cart
if len(array) < 2:
return Vec2(0, 0)
if distance == 0:
return array[0]
if distance_from_points(array) <= distance:
return array[len(array) - 1]
for i in range(len(array) - 2):
x = (array[i].x - array[i + 1].x)
y = (array[i].y - array[i + 1].y)
new_distance = math.sqrt(x * x + y * y)
current_distance += new_distance
if distance <= current_distance:
break
current_distance -= new_distance
if distance == current_distance:
return array[i]
else:
angle = angle_from_points(array[i], array[i + 1])
cart = cart_from_pol((distance - current_distance), angle)
if array[i].x > array[i + 1].x:
coord = Vec2((array[i].x - cart.x), (array[i].y - cart.y))
else:
coord = Vec2((array[i].x + cart.y), (array[i].y + cart.y))
return coord
cdef class Vec2(object):
cdef public float x
cdef public float y
def __init__(self, x, y):
self.x = x
self.y = y
def __richcmp__(x, y, op):
if op == 2:#Py_EQ
return x.__is_equal(y)
else:#Py_NE
return not x.__is_equal(y)
def __is_equal(self, other):
return self.x == other.x and self.y == other.y
cpdef float distance(Vec2 self, Vec2 other):
cdef float x = self.x - other.x
cdef float y = self.y - other.y
return (x*x + y*y) ** 0.5 #sqrt, lol
cpdef Vec2 calc(Vec2 self, float value, Vec2 other): #I dont know what to call this function yet
cdef float x = self.x + value * other.x
cdef float y = self.y + value * other.y
return Vec2(x, y)