├── .gitignore ├── README.md ├── notes.txt └── rl_replay_parser.py /.gitignore: -------------------------------------------------------------------------------- 1 | temp 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rl_replay_parser 2 | RocketLeague Replay File Parser 3 | 4 | This project is abandoned. For alternatives, check out [this wiki page](https://github.com/rocket-league-replays/rocket-league-replays/wiki/Rocket-League-Replay-Parsers). For Python parsers, I recommend [pyrope](https://github.com/Galile0/pyrope). 5 | -------------------------------------------------------------------------------- /notes.txt: -------------------------------------------------------------------------------- 1 | General Notes: 2 | 3 | Integers appear to be little endian. 4 | 5 | Byte Offsets (examples from 22Cf): 6 | 7 | 0-20: Data that seems to change often. Perhaps a checksum? 8 | 21-44: "TAGame.Replay_Soccar_TA" + null terminator 9 | 45-?: Seems to be a collection of properties, with the name, type, and value itself. For example: 10 | 11 | . . . . T e a m S i z e . . . . . I n t P r o p e r t y . . . . . . . . . . . . . 12 | 0900 0000 5465 616d 5369 7a65 000c 0000 0049 6e74 5072 6f70 6572 7479 0004 0000 0000 0000 0001 0000 00 13 | ^ Length of "TeamSize" + terminator 14 | ^ Length of "IntProperty" + terminator 15 | ^ Length (stored in 8-bytes) of value size (4 bytes) 16 | ^ Team size was one 17 | 18 | 19 | . . . . P r i m a r y P l a y e r T e a m . . . . . I n t P r o p e r t y . . . . . . . . . . . . . 20 | 12 0000 0050 7269 6d61 7279 506c 6179 6572 5465 616d 000c 0000 0049 6e74 5072 6f70 6572 7479 0004 0000 0000 0000 0001 0000 00 21 | ^ Length of "PrimaryPlayerTeam" + null terminator (0x12 = 18) 22 | ^ Length of "IntProperty" + terminator 23 | ^ Byte size? 24 | ^ Primary team was one 25 | 26 | . . . . T e a m 1 S c o r e . . . . . I n t P r o p e r t y . . . . . . . . . . . . . 27 | 0b 0000 0054 6561 6d31 5363 6f72 6500 0c00 0000 496e 7450 726f 7065 7274 7900 0400 0000 0000 0000 0100 0000 28 | ^ Length of "Team1Score" + null terminator 29 | ^ Length of "IntProperty" + terminator 30 | ^ Byte size (in eight bytes) 31 | ^ Team score was 1 32 | 33 | 34 | . . . . G o a l s . . . . . A r r a y P r o p e r t y . . . . . . . . . 35 | 0600 0000 476f 616c 7300 0e00 0000 4172 7261 7950 726f 7065 7274 7900 8e00 0000 0000 0000 36 | ^ Length of "Goals" + terminator 37 | ^ Length of "ArrayProperty" + terminator 38 | ^ 8e: 8-byte unsigned integer specifying the number of bytes after this value that the "array" takes up. 39 | In this case, 0x8e (142) bytes. 40 | 41 | 42 | An array consists of a 4-byte integer specifying the number of elements in the array. Each element is the same name/type/value that is used above. 43 | The very last element of the array is just a name of 'None', which does not have data for the type or value. 44 | -------------------------------------------------------------------------------- /rl_replay_parser.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import sys 3 | import struct 4 | import math 5 | 6 | import bitstring 7 | 8 | UINT32 = 'uintle:32' 9 | UINT64 = 'uintle:64' 10 | FLOAT32 = 'floatle:32' 11 | 12 | class ReplayParser: 13 | def parse(self, replay_file): 14 | data = {} 15 | # TODO: CRC, version info, other stuff 16 | crc = replay_file.read('bytes:20') 17 | header_start = replay_file.read('bytes:24') 18 | 19 | data['header'] = self._read_properties(replay_file) 20 | unknown = replay_file.read('bytes:8') 21 | data['level_info'] = self._read_level_info(replay_file) 22 | data['key_frames'] = self._read_key_frames(replay_file) 23 | data['network_frames'] = self._read_network_frames(replay_file) 24 | data['debug_logs'] = self._read_debug_logs(replay_file) 25 | data['goal_frame_info'] = self._read_goal_frame_infos(replay_file) 26 | data['packages'] = self._read_packages(replay_file) 27 | data['objects'] = self._read_objects(replay_file) 28 | data['names'] = self._read_names(replay_file) 29 | data['class_index'] = self._read_class_index(replay_file) 30 | data['class_net_cache'] = self._read_class_net_cache(replay_file) 31 | 32 | return data 33 | 34 | def _read_properties(self, replay_file): 35 | results = {} 36 | 37 | while True: 38 | property_info = self._read_property(replay_file) 39 | if property_info: 40 | results[property_info['name']] = property_info['value'] 41 | else: 42 | return results 43 | 44 | def _read_property(self, replay_file): 45 | name_length = replay_file.read(UINT32) 46 | property_name = self._read_string(replay_file, name_length) 47 | 48 | if property_name == 'None': 49 | return None 50 | 51 | type_length = replay_file.read(UINT32) 52 | type_name = self._read_string(replay_file, type_length) 53 | length_of_data = replay_file.read(UINT64) 54 | 55 | if type_name == 'IntProperty': 56 | value = replay_file.read(UINT32) 57 | elif type_name == 'StrProperty': 58 | length = replay_file.read(UINT32) 59 | value = self._read_string(replay_file, length) 60 | elif type_name == 'FloatProperty': 61 | value = replay_file.read(FLOAT32) 62 | elif type_name == 'NameProperty': 63 | length = replay_file.read(UINT32) 64 | value = self._read_string(replay_file, length) 65 | elif type_name == 'ArrayProperty': 66 | array_length = replay_file.read(UINT32) 67 | value = [ 68 | self._read_properties(replay_file) 69 | for x in range(array_length) 70 | ] 71 | 72 | print('{}: {}'.format(property_name, value)) 73 | return { 'name' : property_name, 'value': value} 74 | 75 | def _read_level_info(self, replay_file): 76 | map_names = [] 77 | number_of_maps = replay_file.read(UINT32) 78 | for x in range(number_of_maps): 79 | map_name_length = replay_file.read(UINT32) 80 | map_name = self._read_string(replay_file, map_name_length) 81 | map_names.append(map_name) 82 | 83 | return map_names 84 | 85 | def _read_key_frames(self, replay_file): 86 | number_of_key_frames = replay_file.read(UINT32) 87 | key_frames = [ 88 | self._read_key_frame(replay_file) 89 | for x in range(number_of_key_frames) 90 | ] 91 | return key_frames 92 | 93 | def _read_key_frame(self, replay_file): 94 | time = replay_file.read(FLOAT32) 95 | frame = replay_file.read(UINT32) 96 | file_position = replay_file.read(UINT32) 97 | return { 98 | 'time' : time, 99 | 'frame' : frame, 100 | 'file_position' : file_position 101 | } 102 | 103 | def _read_network_frames(self, replay_file): 104 | stream_byte_length = replay_file.read(UINT32) 105 | # TODO: Figure this out at a later time 106 | replay_file.bytepos += stream_byte_length 107 | 108 | def _read_debug_logs(self, replay_file): 109 | log_size = replay_file.read(UINT32) 110 | return [ 111 | self._read_debug_log(replay_file) 112 | for x in range(log_size) 113 | ] 114 | 115 | def _read_debug_log(self, replay_file): 116 | frame = replay_file.read(UINT32) 117 | name_length = replay_file.read(UINT32) 118 | name = self._read_string(replay_file, name_length) 119 | msg_length = replay_file.read(UINT32) 120 | msg = self._read_string(replay_file, msg_length) 121 | return { 122 | 'frame': frame, 123 | 'name' : name, 124 | 'message' : msg 125 | } 126 | 127 | def _read_goal_frame_infos(self, replay_file): 128 | number_goal_frame_infos = replay_file.read(UINT32) 129 | return [ 130 | self._read_goal_frame_info(replay_file) 131 | for x in range(number_goal_frame_infos) 132 | ] 133 | 134 | def _read_goal_frame_info(self, replay_file): 135 | type_length = replay_file.read(UINT32) 136 | type_name = self._read_string(replay_file, type_length) 137 | frame_number = replay_file.read(UINT32) 138 | return { 139 | 'type' : type_name, 140 | 'frame_number': frame_number, 141 | } 142 | 143 | def _read_packages(self, replay_file): 144 | number_of_packages = replay_file.read(UINT32) 145 | return [ 146 | self._read_package(replay_file) 147 | for x in range(number_of_packages) 148 | ] 149 | 150 | def _read_package(self, replay_file): 151 | package_length = replay_file.read(UINT32) 152 | return self._read_string(replay_file, package_length) 153 | 154 | def _read_objects(self, replay_file): 155 | number_of_objects = replay_file.read(UINT32) 156 | return [ 157 | self._read_object(replay_file) 158 | for x in range(number_of_objects) 159 | ] 160 | 161 | def _read_object(self, replay_file): 162 | object_length = replay_file.read(UINT32) 163 | return self._read_string(replay_file, object_length) 164 | 165 | def _read_names(self, replay_file): 166 | number_of_names = replay_file.read(UINT32) 167 | return [ 168 | self._read_name(replay_file) 169 | for x in range(number_of_names) 170 | ] 171 | 172 | def _read_name(self, replay_file): 173 | name_length = replay_file.read(UINT32) 174 | return self._read_string(replay_file, name_length) 175 | 176 | def _read_class_index(self, replay_file): 177 | number_of_classes = replay_file.read(UINT32) 178 | return dict( 179 | self._read_class_index_item(replay_file) 180 | for x in range(number_of_classes) 181 | ) 182 | 183 | def _read_class_index_item(self, replay_file): 184 | class_name_size = replay_file.read(UINT32) 185 | class_name = self._read_string(replay_file, class_name_size) 186 | class_id = replay_file.read(UINT32) 187 | return (class_id, class_name) 188 | 189 | def _read_class_net_cache(self, replay_file): 190 | array_length = replay_file.read(UINT32) 191 | return dict( 192 | self._read_class_net_cache_item(replay_file) 193 | for x in range(array_length) 194 | ) 195 | 196 | def _read_class_net_cache_item(self, replay_file): 197 | # Corresponds to the 'id' value in the class index. 198 | class_id = replay_file.read(UINT32) 199 | # start/end represent range of the corresponding property elements in the 200 | # 'objects' array. 201 | class_index_start = replay_file.read(UINT32) 202 | class_index_end = replay_file.read(UINT32) 203 | length = replay_file.read(UINT32) 204 | data = { 205 | 'class_index_start': class_index_start, 206 | 'class_index_end': class_index_end, 207 | 'properties' : dict( 208 | self._read_class_net_cache_item_property_map(replay_file) 209 | for x in range(length) 210 | ) 211 | } 212 | return (class_id, data) 213 | 214 | def _read_class_net_cache_item_property_map(self, replay_file): 215 | property_index = replay_file.read(UINT32) 216 | property_mapped_id = replay_file.read(UINT32) 217 | return (property_mapped_id, property_index) 218 | 219 | def _pretty_byte_string(self, bytes_read): 220 | return ':'.join(format(ord(x), '#04x') for x in bytes_read) 221 | 222 | def _print_bytes(self, bytes_read): 223 | print('Hex read: {}'.format(self._pretty_byte_string(bytes_read))) 224 | 225 | def _read_string(self, replay_file, length): 226 | # NOTE(mhildreth): Strip off the final byte, since it will be a zero (null terminator) 227 | return replay_file.read(8 * length).bytes[:-1] 228 | 229 | def _sniff_bits(self, replay_file, size): 230 | print('****** Sniff Results *******') 231 | b = replay_file.read(size) 232 | print("Binary: {}".format(b.bin)) 233 | if size % 4 == 0: 234 | print("Hex: {}".format(b.hex)) 235 | if size >= 8: 236 | print("Int: {}".format(b.intle)) 237 | print("Uint: {}".format(b.uintle)) 238 | if size in [32, 64]: 239 | print("Float: {}".format(b.floatle)) 240 | 241 | if __name__ == '__main__': 242 | filename = sys.argv[1] 243 | if not filename.endswith('.replay'): 244 | sys.exit('Filename {} does not appear to be a valid replay file'.format(filename)) 245 | 246 | with open(filename, 'rb') as replay_file: 247 | replay_bit_stream = bitstring.ConstBitStream(replay_file) 248 | results = ReplayParser().parse(replay_bit_stream) 249 | try: 250 | pprint.pprint(results) 251 | except IOError as e: 252 | pass 253 | --------------------------------------------------------------------------------