Initial commit
This commit is contained in:
parent
27c511688e
commit
93e26250f8
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -94,3 +94,5 @@ ENV/
|
|||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# VSC
|
||||
.vscode/
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"python.linting.pylintEnabled": true
|
||||
}
|
4
osuRepy/__init__.py
Normal file
4
osuRepy/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from . import helpers
|
||||
|
||||
from . import frame
|
||||
from . import replay
|
30
osuRepy/frame.py
Normal file
30
osuRepy/frame.py
Normal 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()
|
6
osuRepy/helpers/__init__.py
Normal file
6
osuRepy/helpers/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from . import osuModes
|
||||
from . import osuMods
|
||||
from . import osuRanks
|
||||
from . import osuButtons
|
||||
|
||||
from . import typeSerializer
|
12
osuRepy/helpers/osuButtons.py
Normal file
12
osuRepy/helpers/osuButtons.py
Normal 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
|
4
osuRepy/helpers/osuModes.py
Normal file
4
osuRepy/helpers/osuModes.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
STANDARD = 0
|
||||
TAIKO = 1
|
||||
CATCH_THE_BEAT = 2
|
||||
MANIA = 3
|
37
osuRepy/helpers/osuMods.py
Normal file
37
osuRepy/helpers/osuMods.py
Normal 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
|
10
osuRepy/helpers/osuRanks.py
Normal file
10
osuRepy/helpers/osuRanks.py
Normal 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
|
18
osuRepy/helpers/typeSerializer.py
Normal file
18
osuRepy/helpers/typeSerializer.py
Normal 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
180
osuRepy/replay.py
Normal 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
13
test.py
Normal 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")
|
Loading…
Reference in New Issue
Block a user