Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions vendor/csv_to_rkg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import csv
import binascii
from dataclasses import dataclass


# fmt: off
class FaceButtonInput:
def __init__(self, AButton: int, BButton: int, ItemButton: int, csv_line: int):
assert AButton == 0 or AButton == 1, \
f"A button input has value `{AButton}` at line {csv_line}"

assert BButton == 0 or BButton == 1, \
f"B button input has value `{BButton}` at line {csv_line}"

assert ItemButton == 0 or ItemButton == 1, \
f"Item button input has value `{ItemButton}` at line {csv_line}"
Comment on lines +9 to +16
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"<button> button input on line {csv_line} is out of range! Expected 0 - 1, got `{<button>}`"


self.AButton = AButton
self.BButton = BButton
self.ItemButton = ItemButton

def __eq__(self, other):
return (
self.AButton == other.AButton
and self.BButton == other.BButton
and self.ItemButton == other.ItemButton
)


class DirectionInput:
def __init__(self, YDirection: int, XDirection: int, csv_line: int):
assert -7 <= YDirection <= 7, \
f"Y direction input has value `{YDirection}` at line {csv_line}"

assert -7 <= XDirection <= 7, \
f"X direction input has value `{XDirection}` at line {csv_line}"
Comment on lines +32 to +36
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We (regrettably) shouldn't assume the CSV is 0-centered.

"<direction> direction input has inconsistent centering at line {csv_line}"
"<direction> direction input on line {csv_line} is out of range! Expected {range} - {range + 14}, got `{<direction>}`"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what situation would this not be the case?
How do we know then how to parse the csv if this is not always the case? A parameter that specifies the format?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still people who use 7-centered.

The most we can do is infer the center by checking unshared boundaries (> 7, < 0). If that fails, we may want to default to whichever number has the highest frequency, since neutral inputs are most likely the more common of the two. Alternatively, we can ask the user via input what center the script is.


self.YDirection = YDirection + 7
self.XDirection = XDirection + 7

def __eq__(self, other):
return (
self.YDirection == other.YDirection and self.XDirection == other.XDirection
)


class TrickInput:
def __init__(self, TrickInput: int, csv_line: int):
assert 0 <= TrickInput <= 4, \
f"Trick input has value `{TrickInput}` at line {csv_line}"
Comment on lines +49 to +50
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Trick input on line {csv_line} is out of range! Expected 0 - 4, got `{TrickInput}`"


self.TrickInput = TrickInput

def __eq__(self, other):
return self.TrickInput == other.TrickInput

# fmt: on


def read_csv_file(file_name: str):
inputs = []
with open(file_name, "r") as f:
reader = csv.reader(f)
for line in reader:
if len(line) < 6:
raise Exception(f"Malformed line at line {reader.line_num}: `{line}`")
inputs.append(
[
FaceButtonInput(
int(line[0]), int(line[1]), int(line[2]), reader.line_num
),
DirectionInput(int(line[4]), int(line[3]), reader.line_num),
TrickInput(int(line[5]), reader.line_num),
]
)
return inputs


@dataclass
class MetaData:
TrackId: int
Character: int
Vehicle: int
ManualDrift: bool


def get_dummy_data():
"""Generate dummy metadata to put in the header of the rkg file"""
return MetaData(0, 0, 0, True)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dummy metadata is invalid - this will generate a ghost with Mario on Standard Kart S.



def write_rkg(inputs: list, metadata: MetaData):
"""Writes an uncompressed RKG file with a minimal header
More information on the RKG format: https://wiki.tockdom.com/wiki/RKG_(File_Format)
"""

# The header of the rkg file
rkg_header = [0x00] * 0x88
# header magic
rkg_header[0:4] = [ord("R"), ord("K"), ord("G"), ord("D")]
# TrackId
rkg_header[0x7] = metadata.TrackId << 0x2
# Vehicle
rkg_header[0x8] = metadata.Vehicle << 0x2
# Character
rkg_header[0x8] = rkg_header[0x8] & (metadata.Character & 0x30)
rkg_header[0x9] = metadata.Character << 0x4
# Drift
drift_type = 0 if metadata.ManualDrift else 1
rkg_header[0xD] = rkg_header[0xD] | drift_type << 2

# The input section
# Lists containing tuples of the form [input, frames held]
face_buttons = [[inputs[0][0], 1]]
direction_inputs = [[inputs[0][1], 1]]
trick_inputs = [[inputs[0][2], 1]]

for input in inputs[1:]:
# Face buttons
if input[0] != face_buttons[-1][0] or face_buttons[-1][1] == 255:
face_buttons.append([input[0], 1])
else:
face_buttons[-1][1] += 1

# Direction inputs
if input[1] != direction_inputs[-1][0] or direction_inputs[-1][1] == 255:
direction_inputs.append([input[1], 1])
else:
direction_inputs[-1][1] += 1

# Trick inputs
if input[2] != trick_inputs[-1][0] or trick_inputs[-1][1] == 4096:
trick_inputs.append([input[2], 1])
else:
trick_inputs[-1][1] += 1

# The header of the input data
input_header = [0x00] * 0x8
# Face button count
input_header[0x0] = len(face_buttons) >> 0x8
input_header[0x1] = len(face_buttons) & 0xFF
# Direction input count
input_header[0x2] = len(direction_inputs) >> 0x8
input_header[0x3] = len(direction_inputs) & 0xFF
# Direction input count
input_header[0x4] = len(trick_inputs) >> 0x8
input_header[0x5] = len(trick_inputs) & 0xFF

# Input section
inputs = []
for face_button_input in face_buttons:
new_input = face_button_input[0].AButton
new_input |= face_button_input[0].BButton << 0x1
new_input |= face_button_input[0].ItemButton << 0x2
inputs.extend([new_input, face_button_input[1]])

for direction_input in direction_inputs:
new_input = direction_input[0].YDirection
new_input |= direction_input[0].XDirection << 0x4
inputs.extend([new_input, direction_input[1]])

for trick_input in trick_inputs:
new_input = (trick_input[1] - 255) >> 0x8 if trick_input[1] > 255 else 0
new_input |= trick_input[0].TrickInput << 0x4
inputs.extend([new_input, trick_input[1] if trick_input[1] <= 255 else 255])

if len(inputs) > 0x2774:
print("Too many ghost inputs for a standard RKG file")
else:
# Pad input data until it is of length 0x2774
inputs.extend([0x00] * (0x2774 - len(inputs)))
Comment on lines +167 to +171
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea for general use, though you may want to add a boolean for this project since we can rewrite the ghost parsing to extend the size.


# Calculate CRC32 of input data
crc32 = binascii.crc32(bytes(inputs))
inputs.extend(list(crc32.to_bytes(4, "big")))

# Write to file
with open("out.rkg", "wb") as f:
f.write(bytes(rkg_header))
f.write(bytes(input_header))
f.write(bytes(inputs))


if __name__ == "__main__":
inputs = read_csv_file("MKW_Player_Inputs.csv")
write_rkg(inputs, get_dummy_data())