Initial commit

This commit is contained in:
Emily 2019-03-13 05:43:26 +01:00
parent 27c511688e
commit 93e26250f8
12 changed files with 319 additions and 0 deletions

2
.gitignore vendored
View File

@ -94,3 +94,5 @@ ENV/
# Rope project settings # Rope project settings
.ropeproject .ropeproject
# VSC
.vscode/

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.linting.pylintEnabled": true
}

4
osuRepy/__init__.py Normal file
View File

@ -0,0 +1,4 @@
from . import helpers
from . import frame
from . import replay

30
osuRepy/frame.py Normal file
View File

@ -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()

View File

@ -0,0 +1,6 @@
from . import osuModes
from . import osuMods
from . import osuRanks
from . import osuButtons
from . import typeSerializer

View File

@ -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

View File

@ -0,0 +1,4 @@
STANDARD = 0
TAIKO = 1
CATCH_THE_BEAT = 2
MANIA = 3

View File

@ -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

View File

@ -0,0 +1,10 @@
SSH = 0
SH = 1
SS = 2
S = 3
A = 4
B = 5
C = 6
D = 7
F = 8
N = 9

View File

@ -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])

180
osuRepy/replay.py Normal file
View File

@ -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"<BiBB32sB%dsBB32s6HiH?iB%dsQ%dsQ" % (len(_player_name), len(_lifebar_graph), len(_compressed_replay)),
self.mode,
self.osu_version,
11, len(self.beatmap_hash), self.beatmap_hash,
11, _player_name,
11, len(self.score_hash), self.score_hash,
self.score_300s, self.score_100s, self.score_50s, self.score_gekis, self.score_katus, self.score_miss,
self.score,
self.combo,
self.perfect,
self.mods,
11, _lifebar_graph,
self.timestamp,
_compressed_replay,
self.online_score_id
)
f.write(data)

13
test.py Normal file
View File

@ -0,0 +1,13 @@
from osuRepy.frame import ReplayFrame
from osuRepy.replay import Replay
replay = Replay()
replay.write( ReplayFrame(2, 3, 4, 4) )
replay.score_300s = 20
replay.timestamp = 635592412124124124 # Wed Feb 11 2015 09:03:52 GMT+0100 (Central European Standard Time)
replay.timestamp = 635570498982876124 # Invalid timestamp?!
# "2|3|4|4,-12345|0|0|1337,"
# ]\0\0 \0\u0018\0\0\0\0\0\0\0\0\u0019\u001f\u0002g<32>L<EFBFBD>_<EFBFBD>F<EFBFBD><46><EFBFBD>6m<36><6D>\u0003<30>,<2C>7<EFBFBD><37>Í
replay.save("test.osr")