2019-03-13 05:43:26 +01:00
|
|
|
import struct
|
|
|
|
import string
|
2019-03-17 02:57:48 +01:00
|
|
|
#import lzma
|
|
|
|
import pylzma # Required cause pythons standard library lzma has missing support
|
2019-03-13 05:43:26 +01:00
|
|
|
|
2019-03-13 10:59:06 +01:00
|
|
|
from .frame import ReplayFrame
|
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
from .helpers import osuModes
|
|
|
|
from .helpers import osuMods
|
|
|
|
from .helpers import osuRanks
|
|
|
|
|
2019-03-13 09:07:46 +01:00
|
|
|
from .helpers.typeSerializer import *
|
|
|
|
|
2019-03-13 06:32:21 +01:00
|
|
|
from io import BufferedReader
|
2019-03-13 06:04:29 +01:00
|
|
|
from os.path import isfile
|
2019-03-13 05:43:26 +01:00
|
|
|
from hashlib import md5 as _md5
|
|
|
|
|
2019-03-17 02:57:48 +01:00
|
|
|
import traceback
|
|
|
|
|
2019-03-13 06:32:21 +01:00
|
|
|
def md5_str(data):
|
|
|
|
if type(data) is str:
|
|
|
|
data = data.encode("ascii")
|
|
|
|
return _md5(data).hexdigest().encode()
|
|
|
|
|
|
|
|
def md5_file(file):
|
|
|
|
if type(file) is not BufferedReader:
|
|
|
|
raise Exception("File is not a BufferedReader")
|
|
|
|
|
|
|
|
if file.mode != "rb":
|
|
|
|
raise Exception("File is not in ReadBytes mode")
|
|
|
|
|
|
|
|
hash = _md5()
|
|
|
|
for chunk in iter(lambda: file.read(4096), b""):
|
|
|
|
hash.update(chunk)
|
|
|
|
return hash.hexdigest()
|
2019-03-13 05:43:26 +01:00
|
|
|
|
2019-03-17 02:57:48 +01:00
|
|
|
def append_and_compress(data):
|
|
|
|
return lzma_compress(data + b"-12345|0|0|1337,")
|
|
|
|
|
2019-03-13 09:07:46 +01:00
|
|
|
def lzma_compress(data):
|
2019-03-17 02:57:48 +01:00
|
|
|
comp = pylzma.compress(data, dictionary = 21, fastBytes = 255, eos = False)
|
|
|
|
comp = comp[:5] + struct.pack(b"<Q", len(data)) + comp[5:] # Append uncompressed length
|
2019-03-19 14:10:14 +01:00
|
|
|
return comp
|
2019-03-13 09:07:46 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
class Replay:
|
2019-03-18 08:00:18 +01:00
|
|
|
_mode = Serializable(osuModes.STANDARD, TYPE_BYTE)
|
|
|
|
_osu_version = Serializable(20181216, TYPE_INT)
|
2019-03-19 14:10:14 +01:00
|
|
|
#_beatmap_hash_length = Serializable(32, TYPE_ULEB)
|
|
|
|
_beatmap_hash = Serializable(b"d41d8cd98f00b204e9800998ecf8427e", TYPE_STRING, length = TYPE_ULEB, prefix = b"\x0b")
|
|
|
|
#_player_name_length = Serializable(4, TYPE_ULEB)
|
|
|
|
_player_name = Serializable(b"osu!", TYPE_STRING, length = TYPE_ULEB, prefix = b"\x0b")
|
|
|
|
#_score_hash_length = Serializable(32, TYPE_ULEB)
|
|
|
|
_score_hash = Serializable(b"d41d8cd98f00b204e9800998ecf8427e", TYPE_STRING, length = TYPE_ULEB, prefix = b"\x0b")
|
2019-03-18 08:00:18 +01:00
|
|
|
|
|
|
|
_score_300s = Serializable(0, TYPE_USHORT)
|
|
|
|
_score_100s = Serializable(0, TYPE_USHORT)
|
|
|
|
_score_50s = Serializable(0, TYPE_USHORT)
|
|
|
|
_score_gekis = Serializable(0, TYPE_USHORT)
|
|
|
|
_score_katus = Serializable(0, TYPE_USHORT)
|
|
|
|
_score_miss = Serializable(0, TYPE_USHORT)
|
|
|
|
_score = Serializable(0, TYPE_INT)
|
|
|
|
_combo = Serializable(0, TYPE_USHORT)
|
|
|
|
_perfect = Serializable(True, TYPE_BOOL)
|
|
|
|
|
|
|
|
_mods = Serializable(osuMods.NOMOD, TYPE_INT)
|
|
|
|
|
2019-03-19 14:10:14 +01:00
|
|
|
#_lifebar_graph_length = Serializable(4, TYPE_ULEB)
|
|
|
|
_lifebar_graph = Serializable(b"0|1,", TYPE_STRING, length = TYPE_ULEB, prefix = b"\x0b")
|
2019-03-18 08:00:18 +01:00
|
|
|
_timestamp = Serializable(0, TYPE_ULLONG)
|
|
|
|
|
2019-03-19 14:10:14 +01:00
|
|
|
#_replay_data_length = Serializable(0, TYPE_UINT)
|
|
|
|
_replay_data = Serializable(b"", TYPE_STRING, length = TYPE_UINT, filter = append_and_compress)
|
2019-03-18 08:00:18 +01:00
|
|
|
|
|
|
|
_online_score_id = Serializable(0, TYPE_ULLONG)
|
2019-03-13 05:43:26 +01:00
|
|
|
|
2019-03-13 09:07:46 +01:00
|
|
|
# Marker (Only used as attribute section marker for auto-serializing)
|
2019-03-13 09:55:17 +01:00
|
|
|
_end_of_attributes = None
|
2019-03-13 05:43:26 +01:00
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
allowed_kwargs = {
|
|
|
|
"mode": self.set_mode,
|
|
|
|
"mods": self.set_mods,
|
|
|
|
"osu_version": self.set_osu_version,
|
2019-03-13 11:13:55 +01:00
|
|
|
"beatmap_hash": self.set_beatmap_hash,
|
2019-03-13 05:43:26 +01:00
|
|
|
"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
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 09:55:17 +01:00
|
|
|
# --------------------------------------------------------------------------
|
2019-03-13 05:43:26 +01:00
|
|
|
# Set validators -----------------------------------------------------------
|
|
|
|
|
2019-03-13 09:07:46 +01:00
|
|
|
def set_mode(self, mode):
|
|
|
|
if mode > 3 or mode < 0:
|
2019-03-13 05:43:26 +01:00
|
|
|
raise Exception("Invalid mode")
|
2019-03-13 09:55:17 +01:00
|
|
|
self._mode.value = mode
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
def set_osu_version(self, osu_version):
|
|
|
|
if type(osu_version) is not int:
|
|
|
|
raise Exception("osu! version must be an int")
|
2019-03-13 09:55:17 +01:00
|
|
|
self._osu_version.value = osu_version
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
def set_beatmap_hash(self, md5_hash):
|
2019-03-13 11:13:55 +01:00
|
|
|
if type(md5_hash) is bytes:
|
|
|
|
md5_hash = md5_hash.decode()
|
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
if len(md5_hash) != 32 or len([x for x in md5_hash if x in string.hexdigits]) != 32:
|
|
|
|
raise Exception("Invalid beatmap hash")
|
2019-03-13 11:13:55 +01:00
|
|
|
self._beatmap_hash.value = md5_hash.encode()
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 09:55:17 +01:00
|
|
|
def set_beatmap_file(self, filepath):
|
2019-03-13 06:32:21 +01:00
|
|
|
if not isfile(filepath):
|
|
|
|
raise Exception("Beatmap file not found")
|
|
|
|
with open(filepath, "rb") as f:
|
2019-03-13 09:55:17 +01:00
|
|
|
self._beatmap_hash.value = md5_file(f)
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
def set_player_name(self, player_name):
|
2019-03-17 02:57:48 +01:00
|
|
|
if type(player_name) is str:
|
|
|
|
player_name = player_name.encode()
|
2019-03-13 09:55:17 +01:00
|
|
|
self._player_name.value = player_name
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
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")
|
2019-03-13 09:55:17 +01:00
|
|
|
self._score_hash.value = md5_hash.encode()
|
|
|
|
|
|
|
|
def set_score(self, score = None, combo = None, s300 = None, s100 = None, s50 = None, sgekis = None, skatus = None, miss = None):
|
|
|
|
if type(score) is int: self._score.value = score
|
|
|
|
if type(combo) is int: self._combo.value = combo
|
|
|
|
if type(s300) is int: self._score_300s.value = s300
|
|
|
|
if type(s100) is int: self._score_100s.value = s100
|
|
|
|
if type(s50) is int: self._score_50s.value = s50
|
|
|
|
if type(sgekis) is int: self._score_gekis.value = sgekis
|
|
|
|
if type(skatus) is int: self._score_katus.value = skatus
|
|
|
|
if type(miss) is int: self._score_miss.value = miss
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
def set_mods(self, mods):
|
|
|
|
if mods < 0 or mods > (1 << 30) - 1:
|
|
|
|
raise Exception("Mods are out of range")
|
2019-03-13 09:55:17 +01:00
|
|
|
self._mods.value = mods
|
|
|
|
|
|
|
|
def set_lifebar_graph(self, graph):
|
|
|
|
t_graph = type(graph)
|
|
|
|
if t_graph is list:
|
|
|
|
self._lifebar_graph.value = b"".join(graph)
|
|
|
|
elif t_graph is str:
|
|
|
|
self._lifebar_graph.value = graph.encode()
|
|
|
|
elif t_graph is bytes:
|
|
|
|
self._lifebar_graph.value = graph
|
|
|
|
else:
|
|
|
|
raise Exception("Invalid lifebar data")
|
|
|
|
|
|
|
|
def set_timestamp(self, timestamp, use_unix = True):
|
|
|
|
if use_unix:
|
|
|
|
timestamp += 62135599380000 # offset
|
|
|
|
timestamp *= 10 ** 4
|
|
|
|
|
|
|
|
self._timestamp.value = timestamp
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-17 02:57:48 +01:00
|
|
|
def set_online_score_id(self, online_score_id):
|
|
|
|
self._online_score_id.value = online_score_id
|
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
# Update variables ---------------------------------------------------------
|
|
|
|
|
|
|
|
def update_perfect(self):
|
2019-03-13 09:55:17 +01:00
|
|
|
m = [self._score_100s.value, self._score_50s.value, self._score_katus.value, self._score_miss.value]
|
|
|
|
self._perfect.value = sum(m) == 0
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
def update_score_hash(self):
|
|
|
|
self.set_score_hash(
|
2019-03-17 02:57:48 +01:00
|
|
|
md5_str(b"%d%b%b%b%d%b" % (
|
|
|
|
self._combo.value, b"osu", self._player_name.value,
|
2019-03-13 09:55:17 +01:00
|
|
|
self._beatmap_hash.value, self._score.value, self.get_rank()
|
2019-03-13 05:43:26 +01:00
|
|
|
))
|
|
|
|
)
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
def update(self):
|
|
|
|
self.update_perfect()
|
|
|
|
self.update_score_hash()
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
# Get / Helpers ------------------------------------------------------------
|
|
|
|
|
|
|
|
def get_hits(self):
|
2019-03-13 09:55:17 +01:00
|
|
|
return sum([self._score_300s.value, self._score_100s.value, self._score_50s.value])
|
2019-03-13 05:43:26 +01:00
|
|
|
|
|
|
|
def get_possible_hits(self):
|
2019-03-13 09:55:17 +01:00
|
|
|
return self.get_hits() + self._score_miss.value
|
2019-03-13 05:43:26 +01:00
|
|
|
|
2019-03-13 11:01:54 +01:00
|
|
|
def get_rank(self):
|
2019-03-13 05:43:26 +01:00
|
|
|
hits = self.get_possible_hits()
|
|
|
|
if hits == 0:
|
2019-03-13 11:01:54 +01:00
|
|
|
print("Can not calculate rank without any score data (Defaulting to Fail)")
|
|
|
|
return osuRanks.F
|
2019-03-13 05:43:26 +01:00
|
|
|
|
2019-03-13 09:55:17 +01:00
|
|
|
r300 = self._score_300s.value / hits
|
|
|
|
r50 = self._score_50s.value / hits
|
|
|
|
h = osuMods.any_enabled(self._mods.value, osuMods.HIDDEN | osuMods.FLASHLIGHT)
|
2019-03-13 05:43:26 +01:00
|
|
|
|
|
|
|
if r300 == 1:
|
|
|
|
return osuRanks.SSH if h else osuRanks.SS
|
2019-03-13 09:55:17 +01:00
|
|
|
if r300 > .9 and r50 <= .01 and self._score_miss.value == 0:
|
2019-03-13 05:43:26 +01:00
|
|
|
return osuRanks.SH if h else osuRanks.S
|
2019-03-13 09:55:17 +01:00
|
|
|
if r300 > .8 and self._score_miss.value == 0 or r300 > .9:
|
2019-03-13 05:43:26 +01:00
|
|
|
return osuRanks.A
|
2019-03-13 09:55:17 +01:00
|
|
|
if r300 > .7 and self._score_miss.value == 0 or r300 > .8:
|
2019-03-13 05:43:26 +01:00
|
|
|
return osuRanks.B
|
|
|
|
if r300 > .6:
|
|
|
|
return osuRanks.C
|
|
|
|
return osuRanks.D
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
# Write replay data --------------------------------------------------------
|
|
|
|
|
|
|
|
def write(self, frame):
|
2019-03-13 10:59:06 +01:00
|
|
|
t_frame = type(frame)
|
|
|
|
if t_frame is list:
|
2019-03-13 05:43:26 +01:00
|
|
|
for _frame in frame: # (frames)
|
|
|
|
self.write(_frame)
|
|
|
|
return
|
2019-03-13 10:59:06 +01:00
|
|
|
if t_frame is ReplayFrame:
|
|
|
|
self._replay_data.value += bytes(frame)
|
|
|
|
elif t_frame is bytes:
|
|
|
|
self._replay_data.value += frame
|
|
|
|
elif t_frame is str:
|
|
|
|
self._replay_data.value += frame.encode()
|
|
|
|
else:
|
|
|
|
raise Exception("Invalid frame data")
|
2019-03-13 06:32:21 +01:00
|
|
|
|
2019-03-13 05:43:26 +01:00
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
# IO replay data -----------------------------------------------------------
|
|
|
|
|
|
|
|
def save(self, filename):
|
|
|
|
self.update()
|
|
|
|
|
2019-03-13 09:07:46 +01:00
|
|
|
serializer = Serializer()
|
|
|
|
|
|
|
|
attribs = [ a for a in self.__dir__() if not a.startswith("__") ]
|
|
|
|
for attrib in attribs:
|
2019-03-13 09:55:17 +01:00
|
|
|
if attrib == "_end_of_attributes":
|
2019-03-13 09:07:46 +01:00
|
|
|
break
|
2019-03-13 05:43:26 +01:00
|
|
|
|
2019-03-13 09:07:46 +01:00
|
|
|
serializer.add( self.__getattribute__(attrib) ) # Add value to serializer
|
2019-03-13 05:43:26 +01:00
|
|
|
|
|
|
|
with open(filename, "wb") as f:
|
2019-03-13 09:07:46 +01:00
|
|
|
f.write( serializer.flush() )
|
2019-03-18 08:00:18 +01:00
|
|
|
|
|
|
|
def load(self, filename):
|
|
|
|
attribs = [ a for a in self.__dir__() if not a.startswith("__") ]
|
|
|
|
for attrib in attribs:
|
|
|
|
if attrib == "_end_of_attributes":
|
|
|
|
break
|