def unpack(self, esd_buffer, **kwargs): header = self.EXTERNAL_HEADER_STRUCT.unpack(esd_buffer) # Internal offsets start here, so we reset the buffer. esd_buffer = BytesIO(esd_buffer.read()) internal_header = self.INTERNAL_HEADER_STRUCT.unpack(esd_buffer) self.magic = internal_header["magic"] state_machine_headers = self.STATE_MACHINE_HEADER_STRUCT.unpack_count( esd_buffer, count=header["state_machine_count"], ) for state_machine_header in state_machine_headers: states = self.State.unpack( esd_buffer, state_machine_header["state_machine_offset"], count=state_machine_header["state_count"], ) self.state_machines[state_machine_header["state_machine_index"]] = states if internal_header["esd_name_length"] > 0: esd_name_offset = internal_header["esd_name_offset"] esd_name_length = internal_header["esd_name_length"] # Note the given length is the length of the final string. The actual UTF-16 encoded bytes are twice that. self.esd_name = read_chars_from_buffer( esd_buffer, offset=esd_name_offset, length=2 * esd_name_length, encoding="utf-16le" ) esd_buffer.seek(esd_name_offset + 2 * esd_name_length) self.file_tail = esd_buffer.read() else: self.esd_name = "" esd_buffer.seek(header["unk_offset_1"]) # after packed EZL self.file_tail = esd_buffer.read()
def unpack(self, fmg_buffer, remove_empty_entries=True): try: pre_header = self.pre_header_struct.unpack(fmg_buffer) except ValueError: raise ValueError("Could not read FMG header. Is the file/data correct?") try: self._set_version(pre_header.version) except ValueError: raise ValueError(f"Unrecognized FMG version in file content: {pre_header.version}.") header = self.header_struct.unpack(fmg_buffer) # Groups of contiguous text string IDs are defined by ranges (first ID, last ID) to save space. ranges = self.range_struct.unpack(fmg_buffer, count=header.range_count) if fmg_buffer.tell() != header.string_offsets_offset: _LOGGER.warning("Range data did not end at string data offset given in FMG header.") string_offsets = self.string_offset_struct.unpack(fmg_buffer, count=header.string_count) # Text pointer table corresponds to all the IDs (joined together) of the above ranges, in order. for string_range in ranges: i = string_range.first_index for string_id in range(string_range.first_id, string_range.last_id + 1): if string_id in self.entries: raise ValueError(f"Malformed FMG: Entry index {string_id} appeared more than once.") string_offset = string_offsets[i].offset if string_offset == 0: if not remove_empty_entries: # Empty text string. These will trigger in-game error messages, like ?PlaceName?. # Distinct from ' ', which is intentionally blank text data (e.g. the unused area subtitles). self.entries[string_id] = '' else: string = read_chars_from_buffer(fmg_buffer, offset=string_offset, encoding='utf-16le') if string or not remove_empty_entries: self.entries[string_id] = string i += 1
def unpack(self, buffer, remove_empty_entries=True): header = self.HEADER_STRUCT.unpack(buffer) # Groups of contiguous text string IDs are defined by ranges (first ID, last ID) to save space. ranges = self.RANGE_STRUCT.unpack_count(buffer, count=header["range_count"]) if buffer.tell() != header["string_offsets_offset"]: _LOGGER.warning( "Range data did not end at string data offset given in FMG header." ) string_offsets = self.STRING_OFFSET_STRUCT.unpack_count( buffer, count=header["string_count"]) # Text pointer table corresponds to all the IDs (joined together) of the above ranges, in order. for string_range in ranges: i = string_range["first_index"] for string_id in range(string_range["first_id"], string_range["last_id"] + 1): if string_id in self.entries: raise ValueError( f"Malformed FMG: Entry index {string_id} appeared more than once." ) string_offset = string_offsets[i]["offset"] if string_offset == 0: if not remove_empty_entries: # Empty text string. These will trigger in-game error messages, like ?PlaceName?. # Distinct from ' ', which is intentionally blank text data (e.g. the unused area subtitles). self.entries[string_id] = "" else: string = read_chars_from_buffer(buffer, offset=string_offset, encoding="utf-16le") if string or not remove_empty_entries: self.entries[string_id] = string i += 1
def unpack_goal(info_buffer, goal_struct): goal = goal_struct.unpack(info_buffer) name = read_chars_from_buffer(info_buffer, offset=goal.name_offset, encoding="shift-jis") if goal.logic_interrupt_name_offset > 0: logic_interrupt_name = read_chars_from_buffer( info_buffer, offset=goal.logic_interrupt_name_offset, encoding="shift-jis") else: logic_interrupt_name = "" return LuaGoal( goal_id=goal.goal_id, goal_name=name, has_battle_interrupt=goal.has_battle_interrupt, has_logic_interrupt=goal.has_logic_interrupt, logic_interrupt_name=logic_interrupt_name, )
def unpack(self, param_buffer): header = self.HEADER_STRUCT.unpack(param_buffer) self.param_name = header["param_name"] self.__magic = [header["magic0"], header["magic1"], header["magic2"]] self.__unknown = header["unknown"] # Entry data offset in header not used. (It's an unsigned short, yet doesn't limit entry count to 5461.) name_data_offset = header[ "name_data_offset"] # CANNOT BE TRUSTED IN VANILLA FILES! Off by +12 bytes. # Load entry pointer data. entry_pointers = self.ENTRY_POINTER_STRUCT.unpack_count( param_buffer, count=header["entry_count"]) entry_data_offset = param_buffer.tell() # Reliable entry data offset. # Entry size is lazily determined. TODO: Unpack entry data in sequence and associate with names separately. if len(entry_pointers) == 0: return elif len(entry_pointers) == 1: # NOTE: The only vanilla param in Dark Souls with one entry is LEVELSYNC_PARAM_ST (Remastered only), # for which the entry size is hard-coded here. Otherwise, we can trust the repacked offset from Soulstruct # (and SoulsFormats, etc.). if self.param_name == "LEVELSYNC_PARAM_ST": entry_size = 220 else: entry_size = name_data_offset - entry_data_offset else: entry_size = entry_pointers[1]["data_offset"] - entry_pointers[0][ "data_offset"] # Note that we no longer need to track buffer offset. for entry_struct in entry_pointers: param_buffer.seek(entry_struct["data_offset"]) entry_data = param_buffer.read(entry_size) if entry_struct["name_offset"] != 0: try: name = read_chars_from_buffer( param_buffer, offset=entry_struct["name_offset"], encoding="shift_jis_2004", reset_old_offset=False, # no need to reset ignore_encoding_error_for_these_chars=JUNK_ENTRY_NAMES, ) except ValueError: param_buffer.seek(entry_struct["name_offset"]) _LOGGER.error( f"Could not find null termination for entry name string in {self.param_name}.\n" f" Header: {header}\n" f" Entry Struct: {entry_struct}\n" f" 30 chrs of name data: {param_buffer.read(30)}") raise else: name = "" self.entries[entry_struct["id"]] = ParamEntry(entry_data, self.paramdef, name=name)
def unpack(self, gnl_buffer): self.big_endian, self.use_struct_64 = self._check_big_endian_and_struct_64(gnl_buffer) encoding = ("utf-16" + ("-be" if self.big_endian else "-le")) if self.use_struct_64 else "shift-jis" byte_order = ">" if self.big_endian else "<" fmt = byte_order + ('q' if self.use_struct_64 else 'i') read_size = struct.calcsize(fmt) self.names = [] offset = None while offset != 0: offset, = struct.unpack(fmt, gnl_buffer.read(read_size)) if offset != 0: self.names.append(read_chars_from_buffer(gnl_buffer, offset=offset, encoding=encoding))
def detect(cls, bnd_source): """Returns True if `bnd_source` appears to be this subclass of `BaseBND`. Does not support DCX sources.""" if isinstance(bnd_source, (str, Path)): bnd_path = Path(bnd_source) if bnd_path.is_file() and bnd_path.name == "bnd_manifest.json": bnd_path = bnd_path.parent if bnd_path.is_dir(): try: with (bnd_path / "bnd_manifest.json").open("rb") as f: return json.load( f)["version"] == cls.__name__ # "BND3" or "BND4" except FileNotFoundError: return False elif bnd_path.is_file(): with bnd_path.open("rb") as buffer: try: version = read_chars_from_buffer(buffer, length=4, encoding="utf-8") except ValueError: return False return version == cls.__name__ return False elif isinstance(bnd_source, bytes): bnd_source = io.BytesIO(bnd_source) if isinstance(bnd_source, io.BufferedIOBase): old_offset = bnd_source.tell() bnd_source.seek(0) try: version = read_chars_from_buffer(bnd_source, length=4, encoding="utf-8") except ValueError: bnd_source.seek(old_offset) return False bnd_source.seek(old_offset) return version == cls.__name__
def detect(cls, bnd_source): """ Returns True iff BND source appears to be a BND of this type. Does not support DCX sources. """ if isinstance(bnd_source, (str, Path)): bnd_path = Path(bnd_source) if bnd_path.is_file() and bnd_path.name == 'bnd_manifest.txt': bnd_path = bnd_path.parent if bnd_path.is_dir(): try: with (bnd_path / 'bnd_manifest.txt').open('rb') as f: version = cls.read_bnd_setting(f.readline(), 'version') return version.decode() == cls.__name__ except FileNotFoundError: return False elif bnd_path.is_file(): with bnd_path.open('rb') as buffer: try: version = read_chars_from_buffer(buffer, length=4, encoding='utf-8') except ValueError: return False return version == cls.__name__ return False elif isinstance(bnd_source, bytes): bnd_source = BytesIO(bnd_source) if isinstance(bnd_source, IOBase): old_offset = bnd_source.tell() bnd_source.seek(0) try: version = read_chars_from_buffer(bnd_source, length=4, encoding='utf-8') except ValueError: bnd_source.seek(old_offset) return False bnd_source.seek(old_offset) return version == cls.__name__
def unpack(cls, bnd_buffer, entry_header_struct, path_encoding, count): entry_headers = [] entry_header_dicts = entry_header_struct.unpack_count(bnd_buffer, count=count) for d in entry_header_dicts: bnd_buffer.seek(d.data_offset) path = ( read_chars_from_buffer(bnd_buffer, offset=d.path_offset, encoding=path_encoding) if "path_offset" in d else None ) data = bnd_buffer.read(d.compressed_data_size) if is_entry_compressed(d.entry_magic): data = zlib.decompressobj().decompress(data) entry_headers.append(cls(entry_id=d.get("entry_id", None), path=path, data=data, magic=d.entry_magic)) return entry_headers
def decompile(byte_sequence, esd_type, func_prefix=""): """ Input should be a sequence of bytes. """ if esd_type not in {"chr", 'talk'}: raise ValueError("esd_type must be 'chr' or 'talk'.") # _LOGGER.debug(f"Unparsed: {nice_hex_bytes(byte_sequence)}") output = [] # Used as a stack. i = 0 while i < len(byte_sequence): b = byte_sequence[i:i + 1] if b'\x00' <= b <= b'\x7f': output.append(struct.unpack('<b', b)[0] - 64) i += 1 elif b == b'\x80': # Start of a 32-bit float (single). output.append(struct.unpack('<f', byte_sequence[i + 1:i + 5])[0]) i += 5 elif b == b'\x81': # Start of a 64-bit float (double). output.append(struct.unpack('<d', byte_sequence[i + 1:i + 9])[0]) i += 9 elif b == b'\x82': # Start of a 32-bit signed integer. output.append(struct.unpack('<i', byte_sequence[i + 1:i + 5])[0]) i += 5 # b'\x83' is unknown. Possibly a 64-bit signed integer. elif b'\x84' <= b <= b'\x8a': # Function call with 0-6 arguments. output.append(format_function(output, b, esd_type, func_prefix)) i += 1 # b'\x8b' is unknown. Could simply be a function with seven arguments. elif b'\x8c' <= b <= b'\x99': # Binary operation on last two elements. Includes logical operations (and/or). output.append(format_binary_operator(output, b)) i += 1 # b'\xa0' is unknown. elif b == b'\xa1': # End of line. Not printed (and technically not that useful). i += 1 if i != len(byte_sequence): raise ValueError( "Encounter end-of-line marker \xa1 before end of line.") # Bytes b'\xa2' to b'\xa4' are unknown. Could be different types of strings. elif b == b'\xa5': # Start of a null-terminated string. I believe it is always UTF-16LE. try: string = read_chars_from_buffer(byte_sequence, offset=i + 1, encoding='utf-16le') except ValueError: _LOGGER.error( f"Could not interpret ESD string from bytes: {byte_sequence}" ) raise output.append(repr(string)) i += 2 * len(string) + 3 # includes \xa5 and null termination elif b == b'\xa6': # "Continue If False". Seems useless, as this is the default behavior. if _SHOW_INTERNAL_SYMBOLS: output[-1] += '...' i += 1 elif b'\xa7' <= b <= b'\xae': # Save (output of) last element to register. _REGISTERS[struct.unpack('<B', b)[0] - 167] = output[-1] i += 1 elif b'\xaf' <= b <= b'\xb6': # Load (output of) last element from register. loaded_expr = _REGISTERS[struct.unpack('<B', b)[0] - 175] if _SHOW_INTERNAL_SYMBOLS: loaded_expr = '&' + loaded_expr output.append(loaded_expr) i += 1 elif b == b'\xb7': # "Terminate If False". Optimizes code by ensuring that conditions that have already # failed an 'AND' logical operation only continue if needed for a register write. if _SHOW_INTERNAL_SYMBOLS: output[-1] += '!' i += 1 elif b == b'\xb8': # Get state machine argument at index. output.append(f'MACHINE_ARGS[{output.pop()}]') i += 1 elif b == b'\xb9': # Check status of state machine called (presumably just the last one called). output.append('MACHINE_CALL_STATUS') i += 1 elif b == b'\xba': # State machine status. (No other values known, or used.) output.append('ONGOING') i += 1 else: msg = f"Unknown EZL byte encountered: {b}, after output {output}" _LOGGER.error(msg) raise ValueError(msg) # _LOGGER.debug(f"Parsed: {output}") return ''.join(str(o) for o in output)