def find_raw_chunk_id(self, chunk_id): bp = ByteReader(self.data[:]) for i in range(len(self.data) - 4): bp.pos = i if bp.read_uint32() == chunk_id: return bp return None
def read_ghost(game_class, bp): uncomp_sz = bp.read_uint32() comp_sz = bp.read_uint32() comp_data = bp.read(comp_sz) data = zlib.decompress(comp_data, 0, uncomp_sz) gr = ByteReader(data) gr.skip(3 * 4) game_class.sample_period = gr.read_uint32() gr.skip(1 * 4) sample_data_sz = gr.read_uint32() sample_data_pos = gr.pos gr.skip(sample_data_sz) sample_sizes = [] num_samples = gr.read_uint32() fso = 0 if num_samples > 0: fso = gr.read_uint32() if num_samples > 1: sps = gr.read_int32() if sps == -1: sample_sizes = [] for _ in range(num_samples - 1): sample_sizes.append(gr.read_uint32()) else: sample_sizes.append(sps) gr.pos = sample_data_pos gr.skip(fso) for i in range(num_samples): sample_pos = gr.pos record = headers.GhostSampleRecord(gr.read_vec3(), gr.read_uint16(), gr.read_int16(), gr.read_int16(), gr.read_int16(), gr.read_int8(), gr.read_int8()) len_sizes = len(sample_sizes) if i >= len_sizes: if len_sizes >= 1: sample_sz = sample_sizes[0] else: sample_sz = 0 else: sample_sz = sample_sizes[i] record.raw_data = gr.read(sample_sz - (gr.pos - sample_pos)) # import binascii # print(f'{i} {binascii.hexlify(record.raw_data)}') game_class.records.append(record)
def find_raw_chunk_id(self, chunk_id): """Finds a raw chunk ID in the file, skipping through any data that does not match the chunk ID provided. It is not guaranteed that the chunk found is indeed the desired data, as it could be other unrelated chunk that bytes happened to form the chunk ID provided. Args: chunk_id (int): the chunk ID to search for Returns: ByteParser with the current position set right after the chunk ID, or None if no specified chunk ID was found """ bp = ByteReader(self.data[:]) for i in range(len(self.data) - 4): bp.pos = i if bp.read_uint32() == chunk_id: return bp return None
def __init__(self, obj): """Creates the main Gbx instance from a file path or from bytes object. Parses the Gbx file sequentially, reading all supported chunks until no more chunks have been found. Parsing can fail depending on the what classes or chunks it contains and what version of the GBX file is being parsed. Args: obj (str/bytes): a file path to the Gbx file or bytes object containing the Gbx data Raises: GbxLoadError: raised when the supplied object is not a GBX file or data """ if isinstance(obj, str): self.f = open(obj, 'rb') self.root_parser = ByteReader(self.f) else: self.root_parser = ByteReader(obj) self.magic = self.root_parser.read(3, '3s') if self.magic.decode('utf-8') != 'GBX': raise GbxLoadError( f'obj is not a valid Gbx data: magic string is incorrect') self.version = self.root_parser.read(2, 'H') self.classes = {} self.root_classes = {} self.positions = {} self.__current_class = None self.__current_waypoint = None self.__replay_header_info = {} self.root_parser.skip(3) if self.version >= 4: self.root_parser.skip(1) if self.version >= 3: self.class_id = self.root_parser.read_uint32() try: self.type = GbxType(self.class_id) except ValueError: self.type = GbxType.UNKNOWN if self.version >= 6: self._read_user_data() self.num_nodes = self.root_parser.read_uint32() self.num_external_nodes = self.root_parser.read_uint32() if self.num_external_nodes > 0: self.root_parser.read_uint32() self.__read_sub_folder() for node in range(self.num_external_nodes): flags = self.root_parser.read_uint32() if (flags & 4) == 0: self.root_parser.read_string() else: self.root_parser.read_uint32() self.root_parser.skip(4) if self.version >= 5: self.root_parser.skip(4) if (flags & 4) == 0: self.root_parser.skip(4) self.root_parser.push_info() self.positions['data_size'] = self.root_parser.pop_info() data_size = self.root_parser.read_uint32() compressed_data_size = self.root_parser.read_uint32() cdata = self.root_parser.read(compressed_data_size) self.data = bytearray(lzo.decompress(cdata, False, data_size)) bp = ByteReader(self.data) self._read_node(self.class_id, -1, bp)
def _read_node(self, class_id, depth, bp, add=True): oldcid = 0 cid = 0 if class_id == GbxType.CHALLENGE or class_id == GbxType.CHALLENGE_OLD: game_class = headers.CGameChallenge(class_id) if hasattr(self, '__community'): game_class.community = self.__community elif class_id == GbxType.REPLAY_RECORD or class_id == GbxType.REPLAY_RECORD_OLD: game_class = headers.CGameReplayRecord(class_id) if 'nickname' in self.__replay_header_info: game_class.nickname = self.__replay_header_info['nickname'] if 'driver_login' in self.__replay_header_info: game_class.driver_login = self.__replay_header_info[ 'driver_login'] elif class_id == GbxType.WAYPOINT_SPECIAL_PROP or class_id == 0x2E009000: game_class = headers.CGameWaypointSpecialProperty(class_id) self.__current_waypoint = game_class add = False elif class_id == GbxType.CTN_GHOST or class_id == GbxType.CTN_GHOST_OLD: game_class = headers.CGameCtnGhost(class_id) elif class_id == GbxType.GAME_GHOST: game_class = headers.CGameGhost(class_id) elif class_id == GbxType.COLLECTOR_LIST: game_class = headers.CGameCtnCollectorList(class_id) else: game_class = headers.CGameHeader(class_id) self.__current_class = game_class if add: self.classes[depth] = game_class while True: oldcid = cid cid = bp.read_uint32() logging.debug(f'Reading chunk {hex(cid)}') if cid == 0xFACADE01: logging.debug('Encountered chunk 0xFACADE01, stopping') break skipsize = -1 skip = bp.read_int32() if skip == 0x534B4950: skipsize = bp.read_uint32() else: bp.pos -= 4 if cid == 0x0304300D or cid == 0x2400300D: bp.read_string_lookback() bp.read_string_lookback() bp.read_string_lookback() elif cid == 0x03043011 or cid == 0x24003011: for _ in range(2): idx = bp.read_int32() if idx >= 0 and idx not in self.classes: _class_id = bp.read_int32() self._read_node(_class_id, idx, bp) bp.read_uint32() elif cid == 0x301B000 or cid == 0x2403C000: itemsct = bp.read_uint32() for _ in range(itemsct): bp.read_string_lookback() bp.read_string_lookback() bp.read_string_lookback() bp.read_uint32() elif cid == 0x0305B000 or cid == 0x2400C000: bp.skip(8 * 4) elif cid == 0x0305B001 or cid == 0x2400C001: bp.read_string() bp.read_string() bp.read_string() bp.read_string() elif cid == 0x0305B004 or cid == 0x2400C004: game_class.times = { 'bronze': bp.read_int32(), 'silver': bp.read_int32(), 'gold': bp.read_int32(), 'author': bp.read_int32() } bp.read_uint32() elif cid == 0x0305B005 or cid == 0x2400C005: bp.skip(4 * 3) elif cid == 0x0305B006 or cid == 0x2400C006: count = bp.read_uint32() bp.skip(count * 4) elif cid == 0x0305B008 or cid == 0x2400C008: bp.skip(2 * 4) elif cid == 0x0305B00A: bp.skip(9 * 4) elif cid == 0x0305B00D: bp.skip(1 * 4) elif cid == 0x03043014 or cid == 0x03043029: bp.read(16 + 4) # m = hashlib.md5() # m.update(bp.read(16)) # if isinstance(self.__current_class, headers.CGameChallenge): # self.__current_class.password_hash = m.hexdigest() # self.__current_class.password_crc = bp.read_uint32() elif cid == 0x03043017: num_cps = bp.read_uint32() for _ in range(num_cps): bp.read_uint32() bp.read_uint32() bp.read_uint32() elif cid == 0x0304301F or cid == 0x2400301F: game_class.map_uid = bp.read_string_lookback() game_class.environment = bp.read_string_lookback() game_class.map_author = bp.read_string_lookback() bp.push_info() game_class.map_name = bp.read_string() self.positions['map_name'] = bp.pop_info() bp.push_info() game_class.mood = bp.read_string_lookback() self.positions['mood'] = bp.pop_info() game_class.env_bg = bp.read_string_lookback() game_class.env_author = bp.read_string_lookback() game_class.map_size = (bp.read_int32(), bp.read_int32(), bp.read_int32()) game_class.req_unlock = bp.read_int32() game_class.flags = bp.read_int32() bp.push_info() num_blocks = bp.read_uint32() i = 0 while i < num_blocks: block = headers.MapBlock() block.name = bp.read_string_lookback() if block.name != 'Unassigned1': game_class.blocks.append(block) block.rotation = bp.read_byte() block.position = headers.Vector3(bp.read_byte(), bp.read_byte(), bp.read_byte()) if game_class.flags > 0: block.flags = bp.read_uint32() else: block.flags = bp.read_uint16() if block.flags == 0xFFFFFFFF: continue if (block.flags & 0x8000) != 0: block.skin_author = bp.read_string_lookback() if game_class.flags >= 6: # TM2 flags bp.read_string( ) # Block waypoint type {Spawn, Goal} bp.read_int32() self._read_node(0x2E009000, 0, bp, False) else: block.skin = bp.read_int32() if block.skin >= 0 and block.skin not in self.classes: _class_id = bp.read_int32() self._read_node(_class_id, block.skin, bp) if (block.flags & 0x100000) != 0: block.params = bp.read_int32() if block.params >= 0 and block.params not in self.classes: _class_id = bp.read_int32() self._read_node(_class_id, block.params, bp) i += 1 self.positions['block_data'] = bp.pop_info() elif cid == 0x03043022: bp.skip(4) elif cid == 0x03043024: version = bp.read_byte() if version >= 3: bp.skip(32) file_path = bp.read_string() if len(file_path) > 0 or version >= 3: bp.read_string() elif cid == 0x03043025: bp.skip(4 * 4) elif cid == 0x03043026: idx = bp.read_int32() if idx >= 0 and idx not in self.classes: _class_id = bp.read_int32() self._read_node(_class_id, idx, bp) # elif cid == 0x03043028: # archive_gm_cam_val = bp.read_int32() # if archive_gm_cam_val == 1: # bp.skip(1 + 4 * 7) # bp.read_string() elif cid == 0x0304302A: bp.read_int32() elif cid == 0x3043040: bp.pos -= 4 bp.push_info() bp.pos += 4 item_bp = ByteReader(bp.data) item_bp.pos = bp.pos item_bp.skip(2 * 4) item_bp.push_info() item_bp.skip(2 * 4) num_items = item_bp.read_uint32() for i in range(num_items): item_bp.skip(4 * 3) item = headers.CGameBlockItem() item.path = item_bp.read_string_lookback() item.collection = item_bp.read_string_lookback() item.author = item_bp.read_string_lookback() item.rotation = item_bp.read_float() item_bp.skip(15) item.position = item_bp.read_vec3() idx = item_bp.read_int32() if idx >= 0: self._read_node(0x2E009000, idx, item_bp) item.waypoint = self.__current_waypoint self.__current_waypoint = None item_bp.skip(4 * 4 + 2) self._read_node(0x3101004, 0, item_bp, add=False) game_class.items.append(item) item_bp.skip(4) bp.pos = item_bp.pos elif cid == 0x03059002 or cid == 0x2403A002: bp.read_string() for i in range(2): version = bp.read_byte() if version >= 3: bp.skip(32) file_path = bp.read_string() # (?) according to https://wiki.xaseco.org/wiki/GBX # we need to check if the file path is not empty # here but it crashes reading the file for TM2 challenges if len(file_path) > 0 and version >= 1: bp.read_string() elif cid == 0x03043022: bp.read_int32() elif cid == 0x03043024: bp.read_byte() bp.skip(32) bp.read_string() bp.read_string() elif cid == 0x03043025: bp.skip(16) elif cid == 0x03043026: idx = bp.read_int32() if idx >= 0 and idx not in self.classes: _class_id = bp.read_int32() self._read_node(_class_id, idx, bp) elif cid == 0x03043028: p = bp.read_int32() if p != 0: bp.skip(1 + 4 * 3 * 3 + 4 * 3 + 4 + 4 + 4) bp.read_string() elif cid == 0x0304302A: bp.read_int32() elif cid == GbxType.WAYPOINT_SPECIAL_PROP or cid == 0x2E009000: version = bp.read_uint32() if version == 1: game_class.spawn = bp.read_uint32() game_class.order = bp.read_uint32() elif version == 2: game_class.tag = bp.read_string() game_class.order = bp.read_uint32() elif cid == 0x03059000: bp.read_string() bp.read_string() elif cid == 0x0303F005: Gbx.read_ghost(game_class, bp) elif cid == 0x0303F006: bp.skip(4) Gbx.read_ghost(game_class, bp) elif cid == 0x03093002 or cid == 0x2403F002: map_gbx_size = bp.read_uint32() data = bytes(bp.read(map_gbx_size)) try: game_class.track = Gbx(data) except Exception as e: logging.error(f'Failed to parse map data: {e}') elif cid == 0x03093007: bp.skip(4) elif cid == 0x03093014 or cid == 0x2403F014: bp.skip(4) num_ghosts = bp.read_uint32() for _ in range(num_ghosts): idx = bp.read_int32() if idx >= 0 and idx not in self.classes: _class_id = bp.read_uint32() self._read_node(_class_id, idx, bp) bp.skip(4) # num_extras = bp.read_uint32() # bp.skip(num_extras * 8) elif cid == 0x3093015: idx = bp.read_int32() if idx >= 0 and idx not in self.classes: _class_id = bp.read_uint32() self._read_node(_class_id, idx, bp) elif cid == 0x03043021 or cid == 0x24003021: for _ in range(3): idx = bp.read_int32() if idx >= 0 and idx not in self.classes: _class_id = bp.read_int32() self._read_node(_class_id, idx, bp) elif cid == 0x03043022 or cid == 0x24003022: bp.skip(4) elif cid == 0x03043024 or cid == 0x24003024: version = bp.read_byte() if version >= 3: bp.skip(32) path = bp.read_string() if len(path) > 0 and version >= 1: bp.read_string() elif cid == 0x03043025 or cid == 0x24003025: bp.skip(4 * 4) elif cid == 0x03043026 or cid == 0x24003026: idx = bp.read_int32() if idx >= 0 and idx not in self.classes: _class_id = bp.read_int32() self._read_node(_class_id, idx, bp) elif cid == 0x03092005 or cid == 0x2401B005: game_class.race_time = bp.read_uint32() elif cid == 0x03092008 or cid == 0x2401B008: game_class.num_respawns = bp.read_uint32() elif cid == 0x03092009 or cid == 0x2401B009: game_class.light_trail_color = bp.read_vec3() elif cid == 0x0309200A or cid == 0x2401B00A: game_class.stunts_score = bp.read_uint32() # The GBX spec is wrong here. # 0x0309200B contains how many CP times there are # after that, there is a list of (uint32, uint32) # tuples, the first element is the time, the second # is unknown elif cid == 0x0309200B or cid == 0x2401B00B: num = bp.read_uint32() cp_times = [] for i in range(num): cp_times.append(bp.read_uint32()) bp.skip(4) game_class.cp_times = cp_times elif cid == 0x309200C or cid == 0x2401B00C: bp.skip(4) elif cid == 0x309200E or cid == 0x2401B00E: game_class.uid = bp.read_string_lookback() # For TM2 if ('version' in self.__replay_header_info and self.__replay_header_info['version'] >= 8): pos = bp.pos try: game_class.login = bp.read_string() except: bp.pos = pos elif cid == 0x309200F or cid == 0x2401B00F: game_class.login = bp.read_string() elif cid == 0x3092010 or cid == 0x2401B010: bp.read_string_lookback() elif cid == 0x3092012 or cid == 0x2401B012: bp.skip(4 + 16) # import binascii # # bp.read(4) # print(bp.read_uint32()) # print(f'{binascii.hexlify(bp.read(16))}') # print() elif cid == 0x3092013 or cid == 0x2401B013: bp.skip(4 + 4) elif cid == 0x3092014 or cid == 0x2401B014: bp.skip(4) elif cid == 0x3092015 or cid == 0x2401B015: bp.read_string_lookback() elif cid == 0x3092018 or cid == 0x2401B018: bp.read_string_lookback() bp.read_string_lookback() bp.read_string_lookback() elif cid == 0x3092019 or cid == 0x03092025 or cid == 0x2401B019 or cid == 0x2401B011: Gbx.read_ghost_events(game_class, bp, cid) elif cid == 0x309201c: bp.skip(32) elif cid == 0x03093004 or cid == 0x2403f004: bp.skip(4 * 4) elif skipsize != -1: bp.skip(skipsize) cid = oldcid else: return