diff --git a/.gitignore b/.gitignore index 2c0ddba..438d0f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /scripts/MKW_Inputs/*.csv +scripts/AGC_Data/ghost.data diff --git a/scripts/AGC_Data/save your AGC data here.txt b/scripts/AGC_Data/save your AGC data here.txt new file mode 100644 index 0000000..e69de29 diff --git a/scripts/Modules/TimeDifference information.txt b/scripts/Modules/TimeDifference information.txt new file mode 100644 index 0000000..7b66c9b --- /dev/null +++ b/scripts/Modules/TimeDifference information.txt @@ -0,0 +1,58 @@ +This file contain some information about the TimeDifference scripts in infodisplay. + +In infodisplay.ini, you can chose to display or not each time difference calculation. + +In infodisplay.ini, "timediff setting" is a setting with 4 possible value : + "player" which will use the TimeDifference (Player -> Ghost) + "ghost" which will use the TimeDifference (Ghost -> Player) + "ahead" which will use the TimeDifference (the one ahead -> the one behind) + "behind" which will use the TimeDifference (the one behind -> the one ahead) + any other value will default to "player". + +In infodisplay.ini "history size" is a setting used for the TimeDifference RaceComp. +history size = 200 means the TimeDiff RaceComp can at best detect a timedifference of 200 frames or less. +It uses memory, so don't use an unecessary large number. + +Some TimeDifference calculations are not symmetrical. It means this calculation gives different result +for the time difference between the ghost and the player, and between the player and the ghost. +Here's an example : For the TimeDifference Absolute (Player1 -> Player2) : + We take Player1's speed, we take the distance between both players. + And we simply define the TimeDiff as the distance divided by the speed. + Player1 and Player2 have asymmetrical roles in the calculation. + Therefore : we talk about the timedifference from Player1 to Player2 (and not the timedifference between Player1 and Player2) + +This is how each one is calculated : + +-TimeDifference Absolute (P1 -> P2) (Not very relevant imo) + Take S1 the speed of P1 + Take D the distance between P1 and P2 + Return D / S1 + +-TimeDifference Relative (P1 -> P2) (A bit more relevant maybe) + Take S1 the speed of P1 directed "toward" P2. (mathematically, it's a dot product) + Take D the distance between P1 and P2 + Return D / S1 + +-TimeDifference Projected (P1 -> P2) (A good one for short distances) + Take S1 the speed of P1 + Take D the distance represented here : https://blounard.s-ul.eu/iMDYhZDI.png + Return D / S1 + +-TimeDifference CrossPath (P1 -> P2) (Another good one for short distances) +this one is symmetrical + With the notation here : https://blounard.s-ul.eu/WYbotlks.png + Calculate t1 = TimeDifference (P1 -> C) (in this case, all 3 above timedifference formula will give the same result) + Calculate t2 = TimeDifference (P2 -> C) (--------------------------------------------------------------------------) + Return t1-t2 + +-TimeDifference ToFinish (P1 -> P2) (Perfectly precise when both player are going straight to the finish line at constant speed. Useless otherwise) +this one is symmetrical + Calculate t1, the time needed for P1 to cross the finish line if P1 keep going at a constant speed. + Calculate t2, the time needed for P2 to cross the finish line if P2 keep going at a constant speed. + Return t1-t2 + +-TimeDifference RaceComp (P1 -> P2) (Useful even for long distances. Based on RaceCompletion data. Has several flaws) +this one is symmetrical + Store in memory the racecompletion data for both players for the last few frames. + Make the player ahead go "back in time" until it's behind the other player. + How much frame you had to go "back in time" is how much frame the player was ahead. \ No newline at end of file diff --git a/scripts/Modules/agc_lib.py b/scripts/Modules/agc_lib.py new file mode 100644 index 0000000..e814100 --- /dev/null +++ b/scripts/Modules/agc_lib.py @@ -0,0 +1,320 @@ +from dolphin import gui, memory +from .mkw_classes import vec3 +from .mkw_classes import VehiclePhysics, KartMove, RaceConfig, RaceManagerPlayer +import math + + +class FrameData: + """Class to represent a set of value accessible each frame in the memory""" + + def __init__(self, addrlist=None, string=None, usedefault=False): + self.values = [] # List of bytearray + if string is not None: + self.read_from_string(string) + + elif addrlist is not None: + if not usedefault: + for addr, size in addrlist: + self.values.append(memory.read_bytes(addr, size)) + else: + for addr, size in addrlist: + self.values.append(bytearray(size)) + + def __str__(self): + text = '' + for array in self.values: + for byte in array: + text += str(byte)+',' + if len(array) > 0: + text = text[:-1] + text += ';' + if len(self.values) > 0: + text = text[:-1] + return text+'\n' + + def read_from_string(self, string): + values = string.split(';') + for value in values: + self.values.append(bytearray([int(s) for s in value.split(',')])) + + def interpolate(self, other, selfi, otheri): + # Call only if self.value[0] represent a vec3 + v1 = vec3.from_bytes(self.values[0]) + v2 = vec3.from_bytes(other.values[0]) + v = (v1*selfi)+(v2*otheri) + self.values[0] = v.to_bytes() + + def write(self, addrlist): + for index in range(len(self.values)): + addr = addrlist[index][0] + val = self.values[index] + memory.write_bytes(addr, val) + + +def float_to_str(f): + ms = round((f % 1)*1000) + s = math.floor(f) % 60 + m = math.floor(f)//60 + return f"{m},{s},{ms}" + + +def floats_to_str(fs): + return f"{float_to_str(fs[0])};{float_to_str(fs[1])};{float_to_str(fs[2])}" + + +class Split: + """Class for a lap split. Contain just a float, representing the split in s""" + + def __init__(self, f): + self.val = f + + def __str__(self): + return f"{self.val:.3f}" + + def __add__(self, other): + return Split(max(0, self.val+other.val)) + + @staticmethod + def from_string(string): + return Split(float(string)) + + @staticmethod + def from_time_format(m, s, ms): + return Split(m*60+s+ms/1000) + + @staticmethod + def from_bytes(b): + data_int = b[0]*256*256+b[1]*256+b[2] + ms = data_int % 1024 + data_int = data_int//1024 + s = data_int % 128 + data_int = data_int//128 + m = data_int % 128 + return Split(m*60+s+ms/1000) + + def time_format(self): + # return m,s,ms corresponding + f = self.val + ms = round((f % 1)*1000) + s = math.floor(f) % 60 + m = math.floor(f)//60 + return m, s, ms + + def bytes_format(self): + # return a bytearray of size 3 for rkg format + m, s, ms = self.time_format() + data_int = ms+s*1024+m*1024*128 + b3 = data_int % 256 + data_int = data_int//256 + b2 = data_int % 256 + data_int = data_int//256 + b1 = data_int % 256 + return bytearray((b1, b2, b3)) + + +class TimerData: + """Class for the laps splits, both in RKG and Timer format + Cumulative convention (lap2 split is stored as lap1+lap2)""" + + def __init__(self, string=None, readid=0, splits=None): + # Call with a string OR when the race is finished + if string is None: + if splits is None: + self.splits = [] # List of Split (size 3) + timerlist = [RaceManagerPlayer.lap_finish_time( + readid, lap) for lap in range(3)] + for timer in timerlist: + self.splits.append(Split.from_time_format( + timer.minutes(), timer.seconds(), timer.milliseconds())) + else: + self.splits = splits + else: + self.splits = [] + laps = string.split(';') + for lap in laps: + self.splits.append(Split.from_string(lap)) + + @staticmethod + def from_sliced_rkg(rkg_metadata): + sliced_bytes = rkg_metadata.values[3] + l1 = Split.from_bytes(sliced_bytes[1:4]) + l2 = Split.from_bytes(sliced_bytes[4:7])+l1 + l3 = Split.from_bytes(sliced_bytes[7:10])+l2 + return TimerData(splits=[l1, l2, l3]) + + def __str__(self): + text = 't' + for split in self.splits: + text += str(split)+";" + text = text[:-1] + return text+'\n' + + def add_delay(self, delay): + s = -delay/59.94 + for i in range(len(self.splits)): + self.splits[i] = Split(max(self.splits[i].val+s, 0)) + + def to_bytes(self): + # A lap split is 3 bytes, so there is 9 bytes total + # Non cumulative format, ready to be written in a rkg + r = bytearray() + prev = 0 + for split in self.splits: + r = r + Split(split.val - prev).bytes_format() + prev = split.val + return r + + def write_rkg(self): + r = rkg_addr() + memory.write_bytes(r+0x11, self.to_bytes()) + + +def metadata_to_file(filename, read_id): + # Should be called before the countdown + metadata = FrameData(get_metadata_addr(read_id)) + file = open(filename, 'w') + if file is None: + gui.add_osd_message("Error : could not create the data file") + else: + file.write(str(metadata)) + file.close() + gui.add_osd_message(f"{filename} successfully opened") + + +def get_metadata(read_id): + return FrameData(get_metadata_addr(read_id)) + + +def get_rkg_metadata(): + return FrameData(get_rkg_metadata_addr()) + + +def rkg_metadata_to_file(filename): + rkg_metadata = get_rkg_metadata() + file = open(filename, 'w') + if file is None: + gui.add_osd_message("Error : could not create the data file") + else: + file.write("r"+str(rkg_metadata)) + file.close() + gui.add_osd_message(f"{filename} successfully opened") + + +def frame_to_file(filename, read_id): + frame = FrameData(get_addr(read_id)) + file = open(filename, 'a') + if file is None: + gui.add_osd_message("Error : could not create the data file") + else: + file.write(str(frame)) + file.close() + + +def get_framedata(read_id): + return FrameData(get_addr(read_id)) + + +def timerdata_to_file(filename, read_id): + timerdata = TimerData(read_id) + file = open(filename, 'a') + if file is None: + gui.add_osd_message("Error : could not create the data file") + else: + file.write(str(timerdata)) + file.close() + + +def get_timerdata(read_id): + return TimerData(read_id) + + +def file_to_framedatalist(filename): + datalist = [] + file = open(filename, 'r') + if file is None: + gui.add_osd_message("Error : could not load the data file") + else: + timerdata = None + metadata = None + rkg_metadata = None + listlines = file.readlines() + if listlines[0][0] == 'r': + rkg_metadata = FrameData(string=listlines[0][1:]) + timerdata = TimerData.from_sliced_rkg(rkg_metadata) + else: + metadata = FrameData(string=listlines[0]) + if listlines[-1][0] == 't': + timerdata = TimerData(string=listlines.pop()[1:]) + for i in range(1, len(listlines)): + datalist.append(FrameData(string=listlines[i])) + file.close() + gui.add_osd_message(f"Data successfully loaded from {filename}") + return metadata, datalist, timerdata, rkg_metadata + + +def framedatalist_to_file(filename, datalist, read_id): + metadata = get_metadata(read_id) + timerdata = get_timerdata(read_id) + file = open(filename, 'w') + if file is None: + gui.add_osd_message("Error : could not create the data file") + else: + file.write(str(metadata)) + for frame in range(max(datalist.keys())+1): + if frame in datalist.keys(): + file.write(str(datalist[frame])) + else: + file.write(str(FrameData(get_addr(read_id), usedefault=True))) + file.write(str(timerdata)) + file.close() + + +def framedatalist_to_file_rkg(filename, datalist): + metadata = get_rkg_metadata() + file = open(filename, 'w') + if file is None: + gui.add_osd_message("Error : could not create the data file") + else: + file.write('r'+str(metadata)) + for frame in range(max(datalist.keys())+1): + if frame in datalist.keys(): + file.write(str(datalist[frame])) + else: + file.write(str(FrameData(get_addr(1), usedefault=True))) + file.close() + + +def get_addr(player_id): + a = VehiclePhysics.chain(player_id) + b = KartMove.chain(player_id) + return [(a+0x68, 12), # Position + (a+0xF0, 16), # Rotation + (a+0x74, 12), # EV + (a+0x14C, 12), # IV + (b+0x18, 4), # MaxEngineSpd + (b+0x20, 4), # EngineSpd + (b+0x9C, 4), # OutsideDriftAngle + (b+0x5C, 12)] # Dir + + +def get_metadata_addr(player_id): + a = RaceConfig.chain() + player_id*0xF0 + return [(a+0x30, 8)] # CharacterID and VehicleID + + +def rkg_addr(): + return memory.read_u32(RaceConfig.chain() + 0xC0C) + + +def get_rkg_metadata_addr(): + r = rkg_addr() + return [(r+0x4, 3), # Skipping track ID + (r+0x8, 4), # Skipping Compression flag + (r+0xD, 1), # Skipping Input Data Length + (r+0x10, 0x78)] + + +def is_rkg(): + s = bytearray('RKGD', 'ASCII') + r = rkg_addr() + return s == memory.read_bytes(r, 4) diff --git a/scripts/Modules/framesequence.py b/scripts/Modules/framesequence.py index 99c48fc..139ce5a 100644 --- a/scripts/Modules/framesequence.py +++ b/scripts/Modules/framesequence.py @@ -22,6 +22,8 @@ class Frame: accel: bool brake: bool item: bool + drift: bool + brakedrift: bool stick_x: int stick_y: int @@ -43,10 +45,11 @@ def __init__(self, raw: List): * raw[0] (str) - A * raw[1] (str) - B/R * raw[2] (str) - L - * raw[3] (str) - Horizontal stick - * raw[4] (str) - Vertical stick - * raw[5] (str) - Dpad - + * raw[5] (str) - Horizontal stick + * raw[6] (str) - Vertical stick + * raw[7] (str) - Dpad + * raw[3] (str) - Drift "ghost" button + * raw[4] (str) - BrakeDrift "ghost" button Args: raw (List): CSV line to be read """ @@ -55,9 +58,12 @@ def __init__(self, raw: List): self.accel = self.read_button(raw[0]) self.brake = self.read_button(raw[1]) self.item = self.read_button(raw[2]) - self.stick_x = self.read_stick(raw[3]) - self.stick_y = self.read_stick(raw[4]) - self.read_dpad(raw[5]) + self.drift = self.read_button(raw[3]) + self.brakedrift = self.read_button(raw[4]) + self.stick_x = self.read_stick(raw[5]) + self.stick_y = self.read_stick(raw[6]) + self.read_dpad(raw[7]) + def __iter__(self): self.iter_idx = 0 @@ -73,11 +79,16 @@ def __next__(self): if (self.iter_idx == 3): return int(self.item) if (self.iter_idx == 4): - return self.stick_x + return int(self.drift) if (self.iter_idx == 5): - return self.stick_y + return int(self.brakedrift) if (self.iter_idx == 6): + return self.stick_x + if (self.iter_idx == 7): + return self.stick_y + if (self.iter_idx == 8): return self.dpad_raw() + raise StopIteration def read_button(self, button: str) -> bool: @@ -142,7 +153,80 @@ def dpad_raw(self) -> int: return 4 return 0 +def compressInputList(rawInputList): + """A function that convert Raw Input List (List of Frames) to + Compressed Input List (List of 7-int-list, TTK .csv format)""" + compressedInputList = [] + prevInputRaw = Frame(["0"]*8) + prevInputCompressed = [0,0,0,0,0,0,-1] + for rawInput in rawInputList: + compressedInput = [int(rawInput.accel), + 0, + int(rawInput.item), + rawInput.stick_x, + rawInput.stick_y, + rawInput.dpad_raw(), + -1] + if not rawInput.brake: + compressedInput[1] = 0 + elif rawInput.brakedrift: + compressedInput[1] = 3 + elif not prevInputRaw.brake: + compressedInput[1] = 1 + elif rawInput.drift and not prevInputRaw.drift: + compressedInput[1] = 3-prevInputCompressed + else: + compressedInput[1] = prevInputCompressed[1] + + if rawInput.accel and rawInput.brake and (not rawInput.drift): + if prevInputRaw.accel and prevInputRaw.brake and prevInputRaw.drift: + compressedInput[6] = 0 + elif not prevInputRaw.brake: + compressedInput[6] = 0 + + if ((not rawInput.accel) or (not rawInput.brake)) and rawInput.drift: + compressedInput[6] = 1 + prevInputRaw = rawInput + prevInputCompressed = compressedInput + compressedInputList.append(compressedInput) + return compressedInputList + +def decompressInputList(compressedInputList): + """A function that convert Compressed Input List (List of 7-int-list, TTK .csv format) to + Raw Input List (List of Frames)""" + prevInputRaw = Frame(["0"]*8) + prevInputCompressed = [0,0,0,0,0,0,-1] + rawInputList = [] + for compressedInput in compressedInputList: + accel = compressedInput[0] + brake = int(compressedInput[1]>0) + item = compressedInput[2] + X = compressedInput[3] + Y = compressedInput[4] + dpad = compressedInput[5] + brakedrift = int(compressedInput[1]==3) + + if accel + brake < 2: + drift = 0 + elif prevInputRaw.drift: + drift = 1 + elif prevInputCompressed[1] == compressedInput[1]: + drift = 0 + else: + drift = 1 + + if compressedInput[6] != -1: + drift = compressedInput[6] + + rawInput = Frame(list(map(str, (accel, brake, item, drift, brakedrift, X, Y, dpad)))) + prevInputRaw = rawInput + prevInputCompressed = compressedInput + rawInputList.append(rawInput) + return rawInputList + + + class FrameSequence: """ A class representing a sequence of inputs, indexed by frames. @@ -205,13 +289,11 @@ def read_from_file(self) -> None: try: with open(self.filename, 'r') as f: reader = csv.reader(f) + compressedInputList = [] for row in reader: - frame = self.process(row) - if not frame: - # TODO: Handle error - pass - - self.frames.append(frame) + compressedInputList.append(list(map(int,row))) + for frame in decompressInputList(compressedInputList): + self.frames.append(frame) except IOError: return @@ -227,7 +309,7 @@ def write_to_file(self, filename: str) -> bool: try: with open(filename, 'w', newline='') as f: writer = csv.writer(f, delimiter=',') - writer.writerows(self.frames) + writer.writerows(compressInputList(self.frames)) except IOError: return False return True @@ -244,7 +326,8 @@ def process(self, raw_frame: List) -> Optional[Frame]: A new Frame object initialized with the raw frame, or None if the frame is invalid. """ - if len(raw_frame) != 6: + assert len(raw_frame) == 8 + if len(raw_frame) != 8: return None frame = Frame(raw_frame) diff --git a/scripts/Modules/mkw_classes/__init__.py b/scripts/Modules/mkw_classes/__init__.py index cae5be1..507f099 100644 --- a/scripts/Modules/mkw_classes/__init__.py +++ b/scripts/Modules/mkw_classes/__init__.py @@ -8,7 +8,7 @@ # noqa: F401 from .common import RegionError -from .common import vec2, vec3, mat34, quatf +from .common import vec2, vec3, mat34, quatf, eulerAngle from .common import ExactTimer from .common import CupId, CourseId, VehicleId, CharacterId, WheelCount, VehicleType from .common import SpecialFloor, TrickType, SurfaceProperties, RaceConfigPlayerType @@ -56,4 +56,4 @@ from .race_manager import RaceManager, RaceState from .time_manager import TimerManager from .timer import Timer -from .race_manager_player import RaceManagerPlayer \ No newline at end of file +from .race_manager_player import RaceManagerPlayer diff --git a/scripts/Modules/mkw_classes/common.py b/scripts/Modules/mkw_classes/common.py index 1df46d7..a71e9b8 100644 --- a/scripts/Modules/mkw_classes/common.py +++ b/scripts/Modules/mkw_classes/common.py @@ -38,17 +38,55 @@ def __add__(self, other): def __sub__(self, other): return vec3(self.x - other.x, self.y - other.y, self.z - other.z) + def __mul__(self, other): + """ vec3 * vec3 -> float (dot product) + vec3 * float -> vec3 (scalar multiplication)""" + if type(other) == vec3: + return self.x * other.x + self.y * other.y + self.z * other.z + else: + return vec3(self.x * other, self.y * other, self.z * other) + + def __matmul__(self, other): + """ vec3 @ vec3 -> vec3 (cross product) + vec3 @ float -> vec3 (scalar multiplication)""" + if type(other) == vec3: + x = self.y*other.z - self.z*other.y + y = self.z*other.x - self.x*other.z + z = self.x*other.y - self.y*other.x + return vec3(x,y,z) + else: + return vec3(self.x * other, self.y * other, self.z * other) + def length(self) -> float: return math.sqrt(self.x**2 + self.y**2 + self.z**2) def length_xz(self) -> float: return math.sqrt(self.x**2 + self.z**2) + def forward(self, facing_yaw) -> float: + speed_yaw = -180/math.pi * math.atan2(self.x, self.z) + diff_angle_rad = (facing_yaw - speed_yaw)*math.pi/180 + return math.sqrt(self.x**2 + self.z**2)*math.cos(diff_angle_rad) + + def sideway(self, facing_yaw) -> float: + speed_yaw = -180/math.pi * math.atan2(self.x, self.z) + diff_angle_rad = (facing_yaw - speed_yaw)*math.pi/180 + return math.sqrt(self.x**2 + self.z**2)*math.sin(diff_angle_rad) + @staticmethod def read(ptr) -> "vec3": bytes = memory.read_bytes(ptr, 0xC) return vec3(*struct.unpack('>' + 'f'*3, bytes)) + @staticmethod + def from_bytes(bts) -> "vec3": + return vec3(*struct.unpack('>' + 'f'*3, bts)) + + def to_bytes(self) -> bytearray: + return bytearray(struct.pack('>fff', self.x, self.y, self.z)) + + + @dataclass class mat34: e00: float = 0.0 @@ -81,6 +119,29 @@ def read(ptr) -> "quatf": bytes = memory.read_bytes(ptr, 0x10) return quatf(*struct.unpack('>' + 'f'*4, bytes)) +def angle_degree_format(angle): + return ((angle+180)%360) - 180 + +class eulerAngle: + """A class for Euler Angles. + Angles in degrees, between -180 and 180""" + def __init__(self, pitch=0, yaw=0, roll=0): + self.pitch = angle_degree_format(pitch) + self.yaw = angle_degree_format(yaw) + self.roll = angle_degree_format(roll) + + def __add__(self, other): + pitch = self.pitch + other.pitch + yaw = self.yaw + other.yaw + roll = self.roll + other.roll + return eulerAngle(pitch, yaw, roll) + + def __sub__(self, other): + pitch = self.pitch - other.pitch + yaw = self.yaw - other.yaw + roll = self.roll - other.roll + return eulerAngle(pitch, yaw, roll) + @dataclass class ExactTimer: """This is used in conjunction with the Exact Finish Code. diff --git a/scripts/Modules/mkw_classes/race_manager.py b/scripts/Modules/mkw_classes/race_manager.py index 61a0ef4..507782e 100644 --- a/scripts/Modules/mkw_classes/race_manager.py +++ b/scripts/Modules/mkw_classes/race_manager.py @@ -7,7 +7,7 @@ class RaceState(Enum): INTRO_CAMERA = 0 # Course preview COUNTDOWN = 1 # including starting pan RACE = 2 - FINISHED_RACE = 3 + FINISHED_RACE = 4 class RaceManager: def __init__(self): @@ -262,4 +262,4 @@ def disable_lower_respawns() -> bool: def inst_disable_lower_respawns(self) -> bool: """Delfino Plaza?""" disable_lower_respawns_ref = self.addr + 0x48 - return memory.read_u8(disable_lower_respawns_ref) > 0 \ No newline at end of file + return memory.read_u8(disable_lower_respawns_ref) > 0 diff --git a/scripts/Modules/mkw_utils.py b/scripts/Modules/mkw_utils.py index d2d2dfd..f4f132e 100644 --- a/scripts/Modules/mkw_utils.py +++ b/scripts/Modules/mkw_utils.py @@ -1,7 +1,9 @@ from dolphin import memory, utils -from .mkw_classes import mat34, quatf, vec3, ExactTimer -from .mkw_classes import VehicleDynamics, VehiclePhysics, RaceManagerPlayer +from .mkw_classes import mat34, quatf, vec3, ExactTimer, eulerAngle +from .mkw_classes import VehicleDynamics, VehiclePhysics, RaceManagerPlayer, KartObjectManager, RaceManager, RaceState + +import math # These are helper functions that don't quite fit in common.py # This file also contains getter functions for a few global variables. @@ -9,6 +11,43 @@ # NOTE (xi): wait for get_game_id() to be put in dolphin.memory before clearing # these commented-out lines: + +class FrameData: + def __init__(self): + # Default values + self.prc = 0 # PlayerRaceCompletion + self.grc = 0 # GhostRaceCompletion + self.euler = eulerAngle() + self.movangle = eulerAngle() + + # Value for current frame (if available) + if RaceManager().state().value >= RaceState.COUNTDOWN.value: + self.prc = RaceManagerPlayer(0).race_completion() + self.euler = get_facing_angle(0) + self.movangle = get_moving_angle(0) + if not is_single_player(): + self.grc = RaceManagerPlayer(1).race_completion() + + +class History: + def __init__(self, size): + self.size = size + self.array = [] + self.index = 0 + for _ in range(size): + self.array.append(FrameData()) + + def update(self): + """Add a new frameData to the array, and move the index""" + self.index = (self.index+1) % self.size + self.array[self.index] = FrameData() + + def get_older_frame(self, i): + """Return the FrameData i frame older""" + assert i < self.size + return self.array[(self.index-i) % self.size] + + def chase_pointer(base_address, offsets, data_type): """This is a helper function to allow multiple ptr dereferences in quick succession. base_address is dereferenced first, and then @@ -42,14 +81,20 @@ def frame_of_input(): "RMCJ01": 0x809C2920, "RMCK01": 0x809B1F00} return memory.read_u32(address[id]) + def delta_position(playerIdx=0): dynamics_ref = VehicleDynamics(playerIdx) physics_ref = VehiclePhysics(addr=dynamics_ref.vehicle_physics()) return physics_ref.position() - dynamics_ref.position() + +def is_single_player(): + return KartObjectManager().player_count() == 1 + # Next 3 functions are used for exact finish display + def get_igt(lap, player): if player == 0: address = 0x800001B0 @@ -57,7 +102,7 @@ def get_igt(lap, player): address = 0x800001F8 else: return ExactTimer(0, 0, 0) - + race_manager_player_inst = RaceManagerPlayer(player) timer_inst = race_manager_player_inst.lap_finish_time(lap-1) mins = timer_inst.minutes() @@ -65,6 +110,7 @@ def get_igt(lap, player): mils = memory.read_f32(address + (lap-1)*0x4) / 1000 % 1 return ExactTimer(mins, secs, mils) + def update_exact_finish(lap, player): currentLapTime = get_igt(lap, player) if lap > 1: @@ -74,6 +120,7 @@ def update_exact_finish(lap, player): return currentLapTime - pastLapTime + def get_unrounded_time(lap, player): t = ExactTimer(0, 0, 0) for i in range(lap): @@ -82,4 +129,231 @@ def get_unrounded_time(lap, player): # TODO: Rotation display helper functions -# TODO: Time difference display helper functions \ No newline at end of file + +def quaternion_to_euler_angle(q): + """Param : quatf + Return : eulerAngle """ + x1, x2 = 2*q.x*q.w-2*q.y*q.z, 1-2*q.x*q.x-2*q.z*q.z + y1, y2 = 2*q.y*q.w-2*q.x*q.z, 1-2*q.y*q.y-2*q.z*q.z + z = 2*q.x*q.y + 2*q.z*q.w + roll = 180/math.pi * math.asin(z) + pitch = -180/math.pi * math.atan2(x1, x2) + yaw = -180/math.pi * math.atan2(y1, y2) + return eulerAngle(pitch, yaw, roll) + + +def get_facing_angle(player): + """Param : int player_id + Return : eulerAngle , correspond to facing angles""" + quaternion = VehiclePhysics(player).main_rotation() + return quaternion_to_euler_angle(quaternion) + + +def speed_to_euler_angle(speed): + """Param : vec3 speed + Return : eulerAngle""" + s = speed + pitch = 180/math.pi * math.atan2(s.z, s.y) # unsure and unused + yaw = -180/math.pi * math.atan2(s.x, s.z) + roll = -180/math.pi * math.atan2(s.y, s.x) # unsure and unused + return eulerAngle(pitch, yaw, roll) + + +def get_moving_angle(player): + """Param : int player_id + Return : eulerAngle , correspond to moving angles""" + speed = delta_position(player) + return speed_to_euler_angle(speed) + + +# TODO: Time difference display helper functions +"""The time difference functions. +time_difference_[name](P1, S1, P2, S2) is a function that takes as arguments +P1,S1 : Player1's Position and Speed vec3. +P2,S2 : Player2's Position and Speed vec3 +Return the time it would take for Player1 to catch Player2 (not always symmetric) + +get_time_difference_[name](Player1, Player2) takes as arguments +Player1 : Player1 ID +Player2 : Player2 ID +Return the time it would take for Player1 to catch Player2 (not always symmetric) +It's the function called in draw_infodisplay.py +""" + + +def get_physics(player1, player2): + """Take the Player1 and Player2 ID's, return their + P1, S1, P2, S2 data""" + P1, S1 = VehiclePhysics(player1).position(), delta_position(player1) + P2, S2 = VehiclePhysics(player2).position(), delta_position(player2) + return P1, S1, P2, S2 + + +def get_distance_ghost_vec(): + """Give the distance (vec3) between the player and the ghost + Player to ghost vec""" + player_position = VehiclePhysics(0).position() + ghost_position = VehiclePhysics(1).position() + return (ghost_position - player_position) + + +def get_distance_ghost(): + """Give the distance(float) between the player and the ghost""" + return get_distance_ghost_vec().length() + + +def time_difference_absolute(P1, P2, S1, S2): + s = S1.length() + if s != 0: + return (P2-P1).length() / s + return float('inf') + + +def get_time_difference_absolute(player1, player2): + """Time difference "Absolute" (simple and bad) + Simply takes the distance player-ghost, and divide it by raw speed (always positive)""" + P1, S1, P2, S2 = get_physics(player1, player2) + return time_difference_absolute(P1, P2, S1, S2) + + +def time_difference_relative(P1, P2, S1, S2): + L = (P2 - P1).length() + if L == 0: + return 0 + s = S1*(P2-P1)/L + if s == 0: + return float('inf') + return (P2-P1).length() / s + + +def get_time_difference_relative(player1, player2): + """Time difference "Relative" + Take distance player-ghost. Divide it by the player's speed "toward" the ghost (dot product)""" + P1, S1, P2, S2 = get_physics(player1, player2) + return time_difference_relative(P1, P2, S1, S2) + + +def time_difference_projected(P1, P2, S1, S2): + s = S1.length() + if s == 0: + return float('inf') + return (P2-P1)*S1/(s**2) + + +def get_time_difference_projected(player1, player2): + """ Time difference "Projected" + Take the distance between the player and the plane oriented by the player speed, covering the ghost. + Then divide it by the player raw speed + This is the 2D version because no numpy""" + P1, S1, P2, S2 = get_physics(player1, player2) + return time_difference_projected(P1, P2, S1, S2) + + +def time_to_cross(A, S, B, C): + """If A is going at a constant speed S, how many frame will it take + to cross the vertical plan containing B and C + Param : A, S, B, C : (vec3),(vec3),(vec3),(vec3) + Return t (float) """ + N = (B-C)@vec3(0, 1, 0) # normal vector to the plan containing B,C + ns = N*S + if ns != 0: + return N*(B-A)/ns + return float('inf') + + +def time_difference_crosspath(P1, P2, S1, S2): + t1 = time_to_cross(P1, S1, P2, P2+S2) + t2 = time_to_cross(P2, S2, P1, P1+S1) + return t1-t2 + + +def get_time_difference_crosspath(player1, player2): + """Time difference "CrossPath" + Take both XZ trajectories of the player and the ghost + Calculate how much time it takes them to reach the crosspoint. (2D only) + Return the difference.""" + P1, S1, P2, S2 = get_physics(player1, player2) + return time_difference_crosspath(P1, P2, S1, S2) + + +def get_finish_line_coordinate(): + """pointA is the position of the left side of the finish line (vec3) + point B ---------------------right------------------------------ + both have 0 as their Y coordinate.""" + game_id = utils.get_game_id() + address = {"RMCE01": 0x809B8F28, "RMCP01": 0x809BD6E8, + "RMCJ01": 0x809BC748, "RMCK01": 0x809ABD28} + kmp_ref = chase_pointer(address[game_id], [0x4, 0x0], 'u32') + offset = memory.read_u32(kmp_ref+0x24) + pointA = vec3(memory.read_f32(kmp_ref+0x4C+offset+0x8+0x0), + 0, memory.read_f32(kmp_ref+0x4C+offset+0x8+0x4)) + pointB = vec3(memory.read_f32(kmp_ref+0x4C+offset+0x8+0x8), + 0, memory.read_f32(kmp_ref+0x4C+offset+0x8+0xC)) + return pointA, pointB + + +def time_difference_tofinish(P1, P2, S1, S2): + A, B = get_finish_line_coordinate() + t1 = time_to_cross(P1, S1, A, B) + t2 = time_to_cross(P2, S2, A, B) + return t1-t2 + + +def get_time_difference_tofinish(player1, player2): + """Assume player and ghost are not accelerated. + Calculate the time to the finish line for both, and takes the difference.""" + P1, S1, P2, S2 = get_physics(player1, player2) + return time_difference_tofinish(P1, P2, S1, S2) + + +def find_index(value, value_list): + """Find the index i so value_list[i]>=value>value_list[i+1] + We suppose value_list[i+1] < value_list[i] + and value_list[0]>= value>=value_list[-1]""" + n = len(value_list) + if n == 1: + return 0 + h = n//2 + if value <= value_list[h]: + return h+find_index(value, value_list[h:]) + return find_index(value, value_list[:h]) + + +def get_time_difference_racecompletion(history): + """Use RaceCompletionData History to calculate the frame difference + The function assume that RaceCompletion is increasing every frames""" + curframe = history.get_older_frame(0) + lastframe = history.get_older_frame(-1) + inf = float('inf') + if curframe.prc >= curframe.grc: + if curframe.grc > lastframe.prc: + length = [history.get_older_frame( + k).prc for k in range(history.size)] + i = find_index(curframe.grc, length) + time = i + (curframe.grc - length[i]) / (length[i+1] - length[i]) + return -time + return -inf + else: + if curframe.prc > lastframe.grc: + length = [history.get_older_frame( + k).grc for k in range(history.size)] + i = find_index(curframe.prc, length) + time = i + (curframe.prc - length[i]) / (length[i+1] - length[i]) + return time + return inf + + +def get_timediff_settings(string): + if string == 'player': + return 0, 1 + if string == 'ghost': + return 1, 0 + pp, sp, pg, sg = get_physics(0, 1) + player_is_ahead = int(sp*(pg-pp) > 0) + if string == 'ahead': + return 1-player_is_ahead, player_is_ahead + if string == 'behind': + return player_is_ahead, 1-player_is_ahead + else: + print('TimeDiff setting value not recognized. Default to "player"') + return 0, 1 diff --git a/scripts/Modules/ttk_lib.py b/scripts/Modules/ttk_lib.py index bd9540a..925dfce 100644 --- a/scripts/Modules/ttk_lib.py +++ b/scripts/Modules/ttk_lib.py @@ -28,8 +28,10 @@ def decode_face_button(input): A = input % 0x2 B = (input >> 1) % 0x2 L = (input >> 2) % 0x2 + D = (input >> 3) % 0x2 #drift button + BD = (input >> 4) % 0x2 #breakdrift button - return [A, B, L] + return [A, B, L, D, BD] def decode_direction_input(input): X = input >> 4 @@ -421,8 +423,7 @@ def set_buttons(inputs, controller : Controller): """This writes button data to addresses with implicit padding in structs. This must be called only after controller_patch()""" addr = controller.addr - memory.write_u8(addr + 0x4d, inputs.accel + (inputs.brake << 1) + - (inputs.item << 2) | ((inputs.accel & inputs.brake) << 3) + 0x80) + memory.write_u8(addr + 0x4d, inputs.accel + (inputs.brake << 1) + (inputs.item << 2) + (inputs.drift << 3) + (inputs.brakedrift << 4) + 0x80) memory.write_u8(addr + 0x4e, inputs.stick_x + 7) memory.write_u8(addr + 0x4f, inputs.stick_y + 7) memory.write_u8(addr + 0x52, inputs.dpad_raw()) diff --git a/scripts/RMC/Adv. Ghost Comp/Load_data.py b/scripts/RMC/Adv. Ghost Comp/Load_data.py new file mode 100644 index 0000000..8de974a --- /dev/null +++ b/scripts/RMC/Adv. Ghost Comp/Load_data.py @@ -0,0 +1,54 @@ +from dolphin import event, utils +import Modules.agc_lib as lib +from Modules.mkw_classes import RaceManager, RaceState +import Modules.mkw_utils as mkw_utils +import os +from math import floor + +def main(): + global filename + filename = os.path.join(utils.get_script_dir(), r'AGC_Data\ghost.data') + + global delay + delay = 0 + + global framedatalist + global timerdata + global metadata + global rkg_metadata + + metadata, framedatalist, timerdata, rkg_metadata = lib.file_to_framedatalist(filename) + + if timerdata is not None: + timerdata.add_delay(delay) + + +if __name__ == '__main__': + main() + + +@event.on_frameadvance +def on_frame_advance(): + racestate = RaceManager().state().value + frame = mkw_utils.frame_of_input() + delayed_frame = floor(delay)+frame + decimal_delay = delay - floor(delay) + + if metadata is not None: + metadata.write(lib.get_metadata_addr(1)) + + if lib.is_rkg(): + if rkg_metadata is not None: + rkg_metadata.write(lib.get_rkg_metadata_addr()) + #if not timerdata is None: + #timerdata.write_rkg() + + if 0 < delayed_frame+1 < len(framedatalist) and racestate >= RaceState.COUNTDOWN.value and not mkw_utils.is_single_player(): + + #print(timerdata) + f1 = lib.FrameData(string = str(framedatalist[delayed_frame])) #Makes a copy so you can modify f1 without affecting the framedatalist + f2 = framedatalist[delayed_frame+1] + f1.interpolate(f2, 1-decimal_delay, decimal_delay) + f1.write(lib.get_addr(1)) + + diff --git a/scripts/RMC/Adv. Ghost Comp/Save_to_file.py b/scripts/RMC/Adv. Ghost Comp/Save_to_file.py new file mode 100644 index 0000000..0718c68 --- /dev/null +++ b/scripts/RMC/Adv. Ghost Comp/Save_to_file.py @@ -0,0 +1,46 @@ +from dolphin import event, utils +import Modules.agc_lib as lib +from Modules.mkw_classes import RaceManager, RaceState +import Modules.mkw_utils as mkw_utils +import os + + +def main(): + global filename + filename = os.path.join(utils.get_script_dir(), r'AGC_Data\ghost.data') + + global framedatalist + framedatalist = {} + + global end + end = False + + global metadata_saved + metadata_saved = False + +if __name__ == '__main__': + main() + + +@event.on_frameadvance +def on_frame_advance(): + global framedatalist + global end + global metadata_saved + + racestate = RaceManager().state().value + frame = mkw_utils.frame_of_input() + + if (not metadata_saved) and racestate >= RaceState.COUNTDOWN.value: + lib.metadata_to_file(filename, 0) + metadata_saved = True + + if (not end) and RaceState.RACE.value >= racestate >= RaceState.COUNTDOWN.value: + framedatalist[frame] = lib.get_framedata(0) + lib.frame_to_file(filename, 0) + + if (not end) and racestate == RaceState.FINISHED_RACE.value: + lib.framedatalist_to_file(filename, framedatalist, 0) + end = True + + diff --git a/scripts/RMC/_draw_info_display.py b/scripts/RMC/_draw_info_display.py index b460731..70f7ee4 100644 --- a/scripts/RMC/_draw_info_display.py +++ b/scripts/RMC/_draw_info_display.py @@ -4,6 +4,7 @@ import os from Modules.mkw_classes.common import SurfaceProperties +from Modules.mkw_utils import History import Modules.mkw_utils as mkw_utils from Modules.mkw_classes import RaceManager, RaceManagerPlayer, RaceState @@ -12,23 +13,29 @@ from Modules.mkw_classes import VehicleDynamics, VehiclePhysics, KartBoost, KartJump from Modules.mkw_classes import KartState, KartCollide, KartInput, RaceInputState + def populate_default_config(file_path): config = configparser.ConfigParser() - + config['DEBUG'] = {} config['DEBUG']['Debug'] = "False" - + config['INFO DISPLAY'] = {} config['INFO DISPLAY']["Frame Count"] = "True" config['INFO DISPLAY']["Lap Splits"] = "False" config['INFO DISPLAY']["Speed"] = "True" + config['INFO DISPLAY']["Oriented Speed"] = "False" config['INFO DISPLAY']["Internal Velocity (X, Y, Z)"] = "False" + config['INFO DISPLAY']["Oriented Internal Velocity"] = "False" config['INFO DISPLAY']["Internal Velocity (XYZ)"] = "False" config['INFO DISPLAY']["External Velocity (X, Y, Z)"] = "False" + config['INFO DISPLAY']["Oriented External Velocity"] = "False" config['INFO DISPLAY']["External Velocity (XYZ)"] = "True" config['INFO DISPLAY']["Moving Road Velocity (X, Y, Z)"] = "False" + config['INFO DISPLAY']["Oriented Moving Road Velocity"] = "False" config['INFO DISPLAY']["Moving Road Velocity (XYZ)"] = "False" config['INFO DISPLAY']["Moving Water Velocity (X, Y, Z)"] = "False" + config['INFO DISPLAY']["Oriented Moving Water Velocity"] = "False" config['INFO DISPLAY']["Moving Water Velocity (XYZ)"] = "False" config['INFO DISPLAY']["Charges and Boosts"] = "True" config['INFO DISPLAY']["Checkpoints and Completion"] = "True" @@ -36,66 +43,159 @@ def populate_default_config(file_path): config['INFO DISPLAY']["Miscellaneous"] = "False" config['INFO DISPLAY']["Surface Properties"] = "False" config['INFO DISPLAY']["Position"] = "False" + config['INFO DISPLAY']["Rotation"] = "True" config['INFO DISPLAY']["Stick"] = "True" config['INFO DISPLAY']["Text Color (ARGB)"] = "0xFFFFFFFF" config['INFO DISPLAY']["Digits (to round to)"] = "6" - + config['INFO DISPLAY']["TimeDiff Absolute"] = "False" + config['INFO DISPLAY']["TimeDiff Relative"] = "False" + config['INFO DISPLAY']["TimeDiff Projected"] = "False" + config['INFO DISPLAY']["TimeDiff CrossPath"] = "False" + config['INFO DISPLAY']["TimeDiff ToFinish"] = "False" + config['INFO DISPLAY']["TimeDiff RaceComp"] = "False" + config['INFO DISPLAY']["TimeDiff Setting"] = "behind" + config['INFO DISPLAY']["History Size"] = "200" + with open(file_path, 'w') as f: config.write(f) - + return config + class ConfigInstance(): - def __init__(self, config : configparser.ConfigParser): + def __init__(self, config: configparser.ConfigParser): self.debug = config['DEBUG'].getboolean('Debug') self.frame_count = config['INFO DISPLAY'].getboolean('Frame Count') self.lap_splits = config['INFO DISPLAY'].getboolean('Lap Splits') self.speed = config['INFO DISPLAY'].getboolean('Speed') - self.iv = config['INFO DISPLAY'].getboolean('Internal Velocity (X, Y, Z)') - self.iv_xyz = config['INFO DISPLAY'].getboolean('Internal Velocity (XYZ)') - self.ev = config['INFO DISPLAY'].getboolean('External Velocity (X, Y, Z)') - self.ev_xyz = config['INFO DISPLAY'].getboolean('External Velocity (XYZ)') - self.mrv = config['INFO DISPLAY'].getboolean('Moving Road Velocity (X, Y, Z)') - self.mrv_xyz = config['INFO DISPLAY'].getboolean('Moving Road Velocity (XYZ)') - self.mwv = config['INFO DISPLAY'].getboolean('Moving Water Velocity (X, Y, Z)') - self.mwv_xyz = config['INFO DISPLAY'].getboolean('Moving Water Velocity (XYZ)') + self.speed_oriented = config['INFO DISPLAY'].getboolean( + 'Oriented Speed') + self.iv = config['INFO DISPLAY'].getboolean( + 'Internal Velocity (X, Y, Z)') + self.iv_oriented = config['INFO DISPLAY'].getboolean( + 'Oriented Internal Velocity') + self.iv_xyz = config['INFO DISPLAY'].getboolean( + 'Internal Velocity (XYZ)') + self.ev = config['INFO DISPLAY'].getboolean( + 'External Velocity (X, Y, Z)') + self.ev_oriented = config['INFO DISPLAY'].getboolean( + 'Oriented External Velocity') + self.ev_xyz = config['INFO DISPLAY'].getboolean( + 'External Velocity (XYZ)') + self.mrv = config['INFO DISPLAY'].getboolean( + 'Moving Road Velocity (X, Y, Z)') + self.mrv_oriented = config['INFO DISPLAY'].getboolean( + 'Oriented Moving Road Velocity') + self.mrv_xyz = config['INFO DISPLAY'].getboolean( + 'Moving Road Velocity (XYZ)') + self.mwv = config['INFO DISPLAY'].getboolean( + 'Moving Water Velocity (X, Y, Z)') + self.mwv_oriented = config['INFO DISPLAY'].getboolean( + 'Oriented Moving Water Velocity') + self.mwv_xyz = config['INFO DISPLAY'].getboolean( + 'Moving Water Velocity (XYZ)') self.charges = config['INFO DISPLAY'].getboolean('Charges and Boosts') - self.cps = config['INFO DISPLAY'].getboolean('Checkpoints and Completion') + self.cps = config['INFO DISPLAY'].getboolean( + 'Checkpoints and Completion') self.air = config['INFO DISPLAY'].getboolean('Airtime') self.misc = config['INFO DISPLAY'].getboolean('Miscellaneous') self.surfaces = config['INFO DISPLAY'].getboolean('Surface Properties') self.position = config['INFO DISPLAY'].getboolean('Position') + self.rotation = config['INFO DISPLAY'].getboolean('Rotation') + self.td_absolute = config['INFO DISPLAY'].getboolean( + 'TimeDiff Absolute') + self.td_relative = config['INFO DISPLAY'].getboolean( + 'TimeDiff Relative') + self.td_projected = config['INFO DISPLAY'].getboolean( + 'TimeDiff Projected') + self.td_crosspath = config['INFO DISPLAY'].getboolean( + 'TimeDiff CrossPath') + self.td_tofinish = config['INFO DISPLAY'].getboolean( + 'TimeDiff ToFinish') + self.td_racecomp = config['INFO DISPLAY'].getboolean( + 'TimeDiff RaceComp') + self.td_set = config['INFO DISPLAY']['TimeDiff Setting'] + self.td = self.td_absolute or self.td_relative or self.td_projected or self.td_crosspath or self.td_tofinish or self.td_racecomp self.stick = config['INFO DISPLAY'].getboolean('Stick') self.color = int(config['INFO DISPLAY']['Text Color (ARGB)'], 16) - self.digits = min(7, config['INFO DISPLAY'].getint('Digits (to round to)')) - -def main(): - config = configparser.ConfigParser() + self.digits = min( + 7, config['INFO DISPLAY'].getint('Digits (to round to)')) + self.history_size = config['INFO DISPLAY'].getint('History Size') + + +def make_line_text_speed(left_text_prefix, left_text_suffix, size, speed): + """Function to generate a line of text + It has "left_text" as a str on the left, + enough spaces to make the text on the left exactly size length + then it has ":" followed by the speed, finished with a \n. + Param: str left_text + int size + float speed + Return str text""" + return left_text_prefix+" "*(size - len(left_text_prefix+left_text_suffix))+left_text_suffix + f"{speed:.{c.digits}f}\n" + + +def make_text_speed(speed, speedname, player, boolspd, boolspdoriented, boolspdxyz): + """Function to generate the text for a certain speed + Parameters : vec3 speed : the speed to generate the text for. + str speedname : the string to write before each line + int player : ID of the player (used for oriented speed, 0 if player) + bool boolspd : True if we draw the (X, Y, Z) speed + bool boolspdoriented : True if we draw (Forward, Sideway, Y) + bool boolspdxyz : True if we draw (XZ, XYZ) + Return str text ready to be displayed""" + text = "" + facing_yaw = mkw_utils.get_facing_angle(player).yaw + offset_size = 13 + if boolspd and boolspdoriented: + text += make_line_text_speed(speedname, "X: ", offset_size, speed.x) + text += make_line_text_speed(speedname, "Y: ", offset_size, speed.y) + text += make_line_text_speed(speedname, "Z: ", offset_size, speed.z) + text += make_line_text_speed(speedname, "Forward: ", + offset_size, speed.forward(facing_yaw)) + text += make_line_text_speed(speedname, "Sideway: ", + offset_size, speed.sideway(facing_yaw)) + elif boolspd: + text += make_line_text_speed(speedname, "X: ", offset_size, speed.x) + text += make_line_text_speed(speedname, "Y: ", offset_size, speed.y) + text += make_line_text_speed(speedname, "Z: ", offset_size, speed.z) + elif boolspdoriented: + text += make_line_text_speed(speedname, "Forward: ", + offset_size, speed.forward(facing_yaw)) + text += make_line_text_speed(speedname, "Sideway: ", + offset_size, speed.sideway(facing_yaw)) + text += make_line_text_speed(speedname, "Y: ", offset_size, speed.y) + if boolspdxyz: + text += make_line_text_speed(speedname, + "XZ: ", offset_size, speed.length_xz()) + text += make_line_text_speed(speedname, + "XYZ: ", offset_size, speed.length()) + return text - file_path = os.path.join(utils.get_script_dir(), 'modules', 'infodisplay.ini') - config.read(file_path) - if not config.sections(): - config = populate_default_config(file_path) - - global c - c = ConfigInstance(config) +def make_text_timediff(timediff, prefix_text, prefix_size, timesize): + timediffms = timediff/59.94 + ms = f"{timediffms:.{c.digits}f}" + frame = f"{timediff:.{c.digits}f}" + ms += " "*(timesize - len(ms)) + ms = ms[:timesize] + frame = frame[:timesize]+"f" + return prefix_text+":"+" "*(prefix_size - len(prefix_text))+ms+"| "+frame+"\n" -if __name__ == '__main__': - main() +def make_text_rotation(rot, rotspd, prefix_text, prefix_size, rotsize): + rot_text = f"{rot:.{c.digits}f}" + rotspd_text = f"{rotspd:.{c.digits}f}" + rot_text += " "*(rotsize - len(rot_text)) + rot_text = rot_text[:rotsize] + rotspd_text = rotspd_text[:rotsize] + return prefix_text+":"+" "*(prefix_size - len(prefix_text))+rot_text+"| "+rotspd_text+"\n" # draw information to the screen + def create_infodisplay(): text = "" - if c.debug: - # test values here - text += f"{utils.get_game_id()}\n\n" - - if c.frame_count: - text += f"Frame: {mkw_utils.frame_of_input()}\n\n" - race_mgr_player = RaceManagerPlayer() race_scenario = RaceConfigScenario(addr=RaceConfig.race_scenario()) race_settings = RaceConfigSettings(race_scenario.settings()) @@ -106,6 +206,12 @@ def create_infodisplay(): vehicle_dynamics = VehicleDynamics(addr=kart_body.vehicle_dynamics()) vehicle_physics = VehiclePhysics(addr=vehicle_dynamics.vehicle_physics()) + if c.debug: + value = mkw_utils.delta_position(0) - VehiclePhysics.speed(0) + text += f"Debug : {value.length()}\n" + + if c.frame_count: + text += f"Frame: {mkw_utils.frame_of_input()}\n\n" if c.lap_splits: # The actual max lap address does not update when crossing the finish line @@ -117,76 +223,50 @@ def create_infodisplay(): if player_max_lap >= 2 and lap_count > 1: for lap in range(1, player_max_lap): - text += "Lap {}: {}\n".format(lap, mkw_utils.update_exact_finish(lap, 0)) + text += "Lap {}: {}\n".format(lap, + mkw_utils.update_exact_finish(lap, 0)) if player_max_lap > lap_count: - text += "Final: {}\n".format(mkw_utils.get_unrounded_time(lap_count, 0)) + text += "Final: {}\n".format( + mkw_utils.get_unrounded_time(lap_count, 0)) text += "\n" if c.speed: speed = mkw_utils.delta_position(playerIdx=0) engine_speed = kart_move.speed() cap = kart_move.soft_speed_limit() - text += f" XZ: {round(speed.length_xz(), c.digits)}\n" - text += f" XYZ: {round(speed.length(), c.digits)}\n" - text += f" Y: {round(speed.y, c.digits)}\n" - text += f" Engine: {round(engine_speed, c.digits)} / {round(cap, c.digits)}" - text += "\n\n" + text += make_text_speed(speed, "", 0, False, c.speed_oriented, c.speed) + text += f" Engine: {round(engine_speed, c.digits)} / {round(cap, c.digits)}\n" + text += "\n" - if (c.iv or c.iv_xyz): + if (c.iv or c.iv_xyz or c.iv_oriented): iv = vehicle_physics.internal_velocity() + text += make_text_speed(iv, "IV ", 0, c.iv, c.iv_oriented, c.iv_xyz) + text += "\n" - if c.iv: - text += f" IV X: {round(iv.x,c.digits)}\n" - text += f" IV Y: {round(iv.y,c.digits)}\n" - text += f" IV Z: {round(iv.z,c.digits)}\n\n" - - if c.iv_xyz: - text += f" IV XZ: {round(iv.length_xz(),c.digits)}\n" - text += f" IV XYZ: {round(iv.length(),c.digits)}\n\n" - - if (c.ev or c.ev_xyz): + if (c.ev or c.ev_xyz or c.ev_oriented): ev = vehicle_physics.external_velocity() + text += make_text_speed(ev, "EV ", 0, c.ev, c.ev_oriented, c.ev_xyz) + text += "\n" - if c.ev: - text += f" EV X: {round(ev.x,c.digits)}\n" - text += f" EV Y: {round(ev.y,c.digits)}\n" - text += f" EV Z: {round(ev.z,c.digits)}\n\n" - - if c.ev_xyz: - text += f" EV XZ: {round(ev.length_xz(),c.digits)}\n" - text += f" EV XYZ: {round(ev.length(),c.digits)}\n\n" - - if (c.mrv or c.mrv_xyz): + if (c.mrv or c.mrv_xyz or c.mrv_oriented): mrv = vehicle_physics.moving_road_velocity() + text += make_text_speed(mrv, "MRV ", 0, c.mrv, + c.mrv_oriented, c.mrv_xyz) + text += "\n" - if c.mrv: - text += f" MRV X: {round(mrv.x,c.digits)}\n" - text += f" MRV Y: {round(mrv.y,c.digits)}\n" - text += f" MRV Z: {round(mrv.z,c.digits)}\n\n" - - if c.mrv_xyz: - text += f" MRV XZ: {round(mrv.length_xz(),c.digits)}\n" - text += f" MRV XYZ: {round(mrv.length(),c.digits)}\n\n" - - if (c.mwv or c.mwv_xyz): + if (c.mwv or c.mwv_xyz or c.mwv_oriented): mwv = vehicle_physics.moving_water_velocity() - - if c.mwv: - text += f" MWV X: {round(mwv.x,c.digits)}\n" - text += f" MWV Y: {round(mwv.y,c.digits)}\n" - text += f" MWV Z: {round(mwv.z,c.digits)}\n\n" - - if c.mwv_xyz: - text += f" MWV XZ: {round(mwv.length_xz(),c.digits)}\n" - text += f" MWV XYZ: {round(mwv.length(),c.digits)}\n\n" + text += make_text_speed(mwv, "MWV ", 0, c.mwv, + c.mwv_oriented, c.mwv_xyz) + text += "\n" if c.charges or c.misc: kart_settings = KartSettings(addr=kart_object.kart_settings()) if c.charges: kart_boost = KartBoost(addr=kart_move.kart_boost()) - + mt = kart_move.mt_charge() smt = kart_move.smt_charge() ssmt = kart_move.ssmt_charge() @@ -197,7 +277,7 @@ def create_infodisplay(): text += f"MT Charge: {mt} | SSMT Charge: {ssmt}\n" else: text += f"MT Charge: {mt} ({smt}) | SSMT Charge: {ssmt}\n" - + text += f"MT: {mt_boost} | Trick: {trick_boost} | Mushroom: {shroom_boost}\n\n" if c.cps: @@ -235,7 +315,8 @@ def create_infodisplay(): if c.surfaces: surface_properties = kart_collide.surface_properties() is_offroad = (surface_properties.value & SurfaceProperties.OFFROAD) > 0 - is_trickable = (surface_properties.value & SurfaceProperties.TRICKABLE) > 0 + is_trickable = (surface_properties.value & + SurfaceProperties.TRICKABLE) > 0 kcl_speed_mod = kart_move.kcl_speed_factor() text += f" Offroad: {is_offroad}\n" text += f"Trickable: {is_trickable}\n" @@ -247,28 +328,113 @@ def create_infodisplay(): text += f"Y Pos: {pos.y}\n" text += f"Z Pos: {pos.z}\n\n" - # TODO: figure out why classes.RaceInfoPlayer.stick_x() and + if c.rotation: + fac = mkw_utils.get_facing_angle(0) + mov = mkw_utils.get_moving_angle(0) + prevfac = Memory_History.get_older_frame(1).euler + prevmov = Memory_History.get_older_frame(1).movangle + facdiff = fac - prevfac + movdiff = mov - prevmov + prefix_size = 10 + rotsize = c.digits+4 + text += " "*(prefix_size+1)+"Rotation"+" "*(rotsize - 8)+"| Speed\n" + text += make_text_rotation(fac.pitch, facdiff.pitch, + "Pitch", prefix_size, rotsize) + text += make_text_rotation(fac.yaw, facdiff.yaw, + "Yaw", prefix_size, rotsize) + text += make_text_rotation(mov.yaw, movdiff.yaw, + "Moving Y", prefix_size, rotsize) + text += make_text_rotation(fac.roll, facdiff.roll, + "Roll", prefix_size, rotsize) + text += "\n" + + if c.td and not mkw_utils.is_single_player(): + size = 10 + timesize = c.digits+4 + p1, p2 = mkw_utils.get_timediff_settings(c.td_set) + s = 1 if 1-p1 else -1 + text += "TimeDiff:"+" "*(timesize+size-16)+"Seconds | Frames\n" + if c.td_absolute: + absolute = mkw_utils.get_time_difference_absolute(p1, p2) + text += make_text_timediff(absolute, "Absolute", size, timesize) + if c.td_relative: + relative = s*mkw_utils.get_time_difference_relative(p1, p2) + text += make_text_timediff(relative, "Relative", size, timesize) + if c.td_projected: + projected = s*mkw_utils.get_time_difference_projected(p1, p2) + text += make_text_timediff(projected, "Projected", size, timesize) + if c.td_crosspath: + crosspath = s*mkw_utils.get_time_difference_crosspath(p1, p2) + text += make_text_timediff(crosspath, "CrossPath", size, timesize) + if c.td_tofinish: + tofinish = s*mkw_utils.get_time_difference_tofinish(p1, p2) + text += make_text_timediff(tofinish, "ToFinish", size, timesize) + if c.td_racecomp: + racecomp = mkw_utils.get_time_difference_racecompletion( + Memory_History) + text += make_text_timediff(racecomp, "RaceComp", size, timesize) + text += "\n" + + # TODO: figure out why classes.RaceInfoPlayer.stick_x() and # classes.RaceInfoPlayer.stick_y() do not update # (using these as placeholders until further notice) if c.stick: kart_input = KartInput(addr=race_mgr_player.kart_input()) - current_input_state = RaceInputState(addr=kart_input.current_input_state()) + current_input_state = RaceInputState( + addr=kart_input.current_input_state()) stick_x = current_input_state.raw_stick_x() - 7 stick_y = current_input_state.raw_stick_y() - 7 - text += f"X: {stick_x} | Y: {stick_y}\n\n" + text += f"X: {stick_x} | Y: {stick_y}\n\n" return text +""" @event.on_savestateload def on_state_load(fromSlot: bool, slot: int): race_mgr = RaceManager() if race_mgr.state().value >= RaceState.COUNTDOWN.value: gui.draw_text((10, 10), c.color, create_infodisplay()) +""" + + +def main(): + config = configparser.ConfigParser() + + file_path = os.path.join(utils.get_script_dir(), + 'modules', 'infodisplay.ini') + config.read(file_path) + + if not config.sections(): + config = populate_default_config(file_path) + + global c + c = ConfigInstance(config) + + # Those 2 variables are used to store some parameters from previous frames + global Frame_of_input + Frame_of_input = 0 + global Memory_History + size = max(c.history_size, int(c.rotation)+1) + Memory_History = History(size) + + +if __name__ == '__main__': + main() + @event.on_frameadvance def on_frame_advance(): + global Frame_of_input + global Memory_History + race_mgr = RaceManager() - if race_mgr.state().value >= RaceState.COUNTDOWN.value: + newframe = Frame_of_input != mkw_utils.frame_of_input() + draw = race_mgr.state().value >= RaceState.COUNTDOWN.value + if newframe: + Frame_of_input = mkw_utils.frame_of_input() + Memory_History.update() + + if draw: gui.draw_text((10, 10), c.color, create_infodisplay())