From 93e26250f8f2bcc036efb019328aebf385bd332c Mon Sep 17 00:00:00 2001 From: Sunpy Date: Wed, 13 Mar 2019 05:43:26 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + .vscode/settings.json | 3 + osuRepy/__init__.py | 4 + osuRepy/frame.py | 30 +++++ osuRepy/helpers/__init__.py | 6 + osuRepy/helpers/osuButtons.py | 12 ++ osuRepy/helpers/osuModes.py | 4 + osuRepy/helpers/osuMods.py | 37 ++++++ osuRepy/helpers/osuRanks.py | 10 ++ osuRepy/helpers/typeSerializer.py | 18 +++ osuRepy/replay.py | 180 ++++++++++++++++++++++++++++++ test.py | 13 +++ 12 files changed, 319 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 osuRepy/__init__.py create mode 100644 osuRepy/frame.py create mode 100644 osuRepy/helpers/__init__.py create mode 100644 osuRepy/helpers/osuButtons.py create mode 100644 osuRepy/helpers/osuModes.py create mode 100644 osuRepy/helpers/osuMods.py create mode 100644 osuRepy/helpers/osuRanks.py create mode 100644 osuRepy/helpers/typeSerializer.py create mode 100644 osuRepy/replay.py create mode 100644 test.py diff --git a/.gitignore b/.gitignore index 6a18ad4..661e241 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,5 @@ ENV/ # Rope project settings .ropeproject +# VSC +.vscode/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..41fac95 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.linting.pylintEnabled": true +} \ No newline at end of file diff --git a/osuRepy/__init__.py b/osuRepy/__init__.py new file mode 100644 index 0000000..8cb4fdf --- /dev/null +++ b/osuRepy/__init__.py @@ -0,0 +1,4 @@ +from . import helpers + +from . import frame +from . import replay diff --git a/osuRepy/frame.py b/osuRepy/frame.py new file mode 100644 index 0000000..ae7dabd --- /dev/null +++ b/osuRepy/frame.py @@ -0,0 +1,30 @@ +from .helpers import osuButtons + +class ReplayFrame: + delta = 0 # Int + x = 0.0 # Float + y = 0.0 # Float + buttons = 0 # Int + def __init__(self, delta, x, y, buttons): + self.set_delta(delta) + self.set_position(x, y) + self.set_buttons(buttons) + + def set_delta(self, delta): + if type(delta) is not int: + raise Exception("Delta is not type of int") + + def set_position(self, x, y): + self.x = x + self.y = y + + def set_buttons(self, buttons): + if buttons < 0 or buttons > (1 << 5) - 1: + raise Exception("Buttons are out of range") + self.buttons = buttons + + def __str__(self): + return "%s|%s|%s|%s," % (self.delta, self.x, self.y, self.buttons) + + def __bytes__(self): + return str(self).encode() \ No newline at end of file diff --git a/osuRepy/helpers/__init__.py b/osuRepy/helpers/__init__.py new file mode 100644 index 0000000..e55edb4 --- /dev/null +++ b/osuRepy/helpers/__init__.py @@ -0,0 +1,6 @@ +from . import osuModes +from . import osuMods +from . import osuRanks +from . import osuButtons + +from . import typeSerializer diff --git a/osuRepy/helpers/osuButtons.py b/osuRepy/helpers/osuButtons.py new file mode 100644 index 0000000..b4c71fd --- /dev/null +++ b/osuRepy/helpers/osuButtons.py @@ -0,0 +1,12 @@ +NONE = 0 +LEFTMOUSE = 1 << 0 +RIGHTMOUSE = 1 << 1 +LEFTKEY = 1 << 2 +RIGHTKEY = 1 << 3 +SMOKE = 1 << 4 + +# How a normal human should read it... +M1 = LEFTMOUSE +M2 = RIGHTMOUSE +K1 = LEFTKEY | LEFTMOUSE +K2 = RIGHTKEY | RIGHTMOUSE diff --git a/osuRepy/helpers/osuModes.py b/osuRepy/helpers/osuModes.py new file mode 100644 index 0000000..dc5ce27 --- /dev/null +++ b/osuRepy/helpers/osuModes.py @@ -0,0 +1,4 @@ +STANDARD = 0 +TAIKO = 1 +CATCH_THE_BEAT = 2 +MANIA = 3 diff --git a/osuRepy/helpers/osuMods.py b/osuRepy/helpers/osuMods.py new file mode 100644 index 0000000..30cc777 --- /dev/null +++ b/osuRepy/helpers/osuMods.py @@ -0,0 +1,37 @@ +NOMOD = 0 +NOFAIL = 1 << 0 +EASY = 1 << 1 +TOUCHSCREEN = 1 << 2 +HIDDEN = 1 << 3 +HARDROCK = 1 << 4 +SUDDENDEATH = 1 << 5 +DOUBLETIME = 1 << 6 +RELAX = 1 << 7 +HALFTIME = 1 << 8 +NIGHTCORE = 1 << 9 +FLASHLIGHT = 1 << 10 +AUTOPLAY = 1 << 11 +SPUNOUT = 1 << 12 +RELAX2 = 1 << 13 +PERFECT = 1 << 14 +KEY4 = 1 << 15 +KEY5 = 1 << 16 +KEY6 = 1 << 17 +KEY7 = 1 << 18 +KEY8 = 1 << 19 +KEYMOD = 1 << 20 +FADEIN = 1 << 21 +RANDOM = 1 << 22 +LASTMOD = 1 << 23 +KEY9 = 1 << 24 +KEY10 = 1 << 25 +KEY1 = 1 << 26 +KEY3 = 1 << 27 +KEY2 = 1 << 28 +SCOREV2 = 1 << 29 + +def all_enabled(modmask, mods): + return modmask & mods == mods + +def any_enabled(modmask, mods): + return modmask & mods > 0 diff --git a/osuRepy/helpers/osuRanks.py b/osuRepy/helpers/osuRanks.py new file mode 100644 index 0000000..f4a2d0c --- /dev/null +++ b/osuRepy/helpers/osuRanks.py @@ -0,0 +1,10 @@ +SSH = 0 +SH = 1 +SS = 2 +S = 3 +A = 4 +B = 5 +C = 6 +D = 7 +F = 8 +N = 9 \ No newline at end of file diff --git a/osuRepy/helpers/typeSerializer.py b/osuRepy/helpers/typeSerializer.py new file mode 100644 index 0000000..d4685ed --- /dev/null +++ b/osuRepy/helpers/typeSerializer.py @@ -0,0 +1,18 @@ +NULL = bytes([0]) +BOOL = bytes([1]) +BYTE = bytes([2]) +UINT16 = bytes([3]) +UINT32 = bytes([4]) +UINT64 = bytes([5]) +SBYTE = bytes([6]) +INT16 = bytes([7]) +INT32 = bytes([8]) +INT64 = bytes([9]) +CHAR = bytes([10]) +STRING = bytes([11]) +SINGLE = bytes([12]) +DOUBLE = bytes([13]) +DECIMAL = bytes([14]) +DATE = bytes([15]) +BYTEARRAY = bytes([16]) +CHARARRAY = bytes([17]) diff --git a/osuRepy/replay.py b/osuRepy/replay.py new file mode 100644 index 0000000..c786e45 --- /dev/null +++ b/osuRepy/replay.py @@ -0,0 +1,180 @@ +import struct +import string +import lzma + +from .helpers import osuModes +from .helpers import osuMods +from .helpers import osuRanks +from .helpers import typeSerializer + +from hashlib import md5 as _md5 + +def md5(str): + return _md5(str.encode("ascii")).hexdigest().encode() + +class Replay: + mode = osuModes.STANDARD # Byte + osu_version = 20131216 # Int + beatmap_hash = b"d41d8cd98f00b204e9800998ecf8427e" # Byte[] + player_name = b"osu!" # Byte[] + score_hash = b"d41d8cd98f00b204e9800998ecf8427e" # Byte[] + + score_300s = 0 # uShort + score_100s = 0 # uShort + score_50s = 0 # uShort + score_gekis = 0 # uShort + score_katus = 0 # uShort + score_miss = 0 # uShort + score = 0 # Int + combo = 0 # uShort + perfect = True # Byte + + mods = osuMods.NOMOD # Int + + lifebar_graph = b"0|1," # Byte[] + timestamp = 0 # Long + + replay_data = b"" # Byte[] + + online_score_id = 0 # Long + + def __init__(self, **kwargs): + allowed_kwargs = { + "mode": self.set_mode, + "mods": self.set_mods, + "osu_version": self.set_osu_version, + "player_name": self.set_player_name, + "replay_data": self.write + } + for k, v in kwargs.items(): + if k in allowed_kwargs.keys(): + allowed_kwargs[k](v) # Run callback func + + # Set validators ----------------------------------------------------------- + + def set_mode(self, mode_id): + if mode_id > 3 or mode_id < 0: + raise Exception("Invalid mode") + self.mode_id = mode_id + + def set_osu_version(self, osu_version): + if type(osu_version) is not int: + raise Exception("osu! version must be an int") + self.osu_version = osu_version + + def set_beatmap_hash(self, md5_hash): + if len(md5_hash) != 32 or len([x for x in md5_hash if x in string.hexdigits]) != 32: + raise Exception("Invalid beatmap hash") + self.beatmap_hash = md5_hash + + def set_player_name(self, player_name): + self.player_name = player_name + + def set_score_hash(self, md5_hash): + if type(md5_hash) is bytes: + md5_hash = md5_hash.decode() + if len(md5_hash) != 32 or len([x for x in md5_hash if x in string.hexdigits]) != 32: + raise Exception("Invalid replay hash") + self.score_hash = md5_hash.encode() + + def set_mods(self, mods): + if mods < 0 or mods > (1 << 30) - 1: + raise Exception("Mods are out of range") + + # -------------------------------------------------------------------------- + # Update variables --------------------------------------------------------- + + def update_perfect(self): + m = [self.score_100s, self.score_50s, self.score_katus, self.score_miss] + self.perfect = sum(m) == 0 + + def update_score_hash(self): + self.set_score_hash( + md5("%d%s%s%s%d%d" % ( + self.combo, "osu", self.player_name, + self.beatmap_hash, self.score, self.get_rank() + )) + ) + + def update(self): + self.update_perfect() + self.update_score_hash() + + # -------------------------------------------------------------------------- + # Get / Helpers ------------------------------------------------------------ + + def get_hits(self): + return sum([self.score_300s, self.score_100s, self.score_50s]) + + def get_possible_hits(self): + return self.get_hits() + self.score_miss + + def get_rank(self): # We dont give out F ranks around here + hits = self.get_possible_hits() + if hits == 0: + raise Exception("Can not calculate rank without any score data") + + r300 = self.score_300s / hits + r50 = self.score_50s / hits + h = osuMods.any_enabled(self.mods, osuMods.HIDDEN | osuMods.FLASHLIGHT) + + if r300 == 1: + return osuRanks.SSH if h else osuRanks.SS + if r300 > .9 and r50 <= .01 and self.score_miss == 0: + return osuRanks.SH if h else osuRanks.S + if r300 > .8 and self.score_miss == 0 or r300 > .9: + return osuRanks.A + if r300 > .7 and self.score_miss == 0 or r300 > .8: + return osuRanks.B + if r300 > .6: + return osuRanks.C + return osuRanks.D + + # -------------------------------------------------------------------------- + # Write replay data -------------------------------------------------------- + + def write(self, frame): + if type(frame) is list: + for _frame in frame: # (frames) + self.write(_frame) + return + + self.replay_data += bytes(frame) + + # -------------------------------------------------------------------------- + # IO replay data ----------------------------------------------------------- + + def save(self, filename): + self.update() + + compressed_replay = lzma.compress(self.replay_data + b"-12345|0|0|1337,", format = lzma.FORMAT_ALONE, filters = [ + { + "id": lzma.FILTER_LZMA1, + "preset": lzma.PRESET_DEFAULT, + "dict_size": 1 << 21, # !important + } + ]) + + _player_name = bytes([len(self.player_name)]) + self.player_name + _lifebar_graph = bytes([len(self.lifebar_graph)]) + self.lifebar_graph + _compressed_replay = bytes([len(compressed_replay)]) + compressed_replay + + with open(filename, "wb") as f: + # Yes... Please never do this + data = struct.pack(b"