Exemple #1
0
    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()
Exemple #2
0
    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
Exemple #3
0
    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
Exemple #4
0
 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,
     )
Exemple #5
0
    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)
Exemple #6
0
 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))
Exemple #7
0
 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__
Exemple #8
0
 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__
Exemple #9
0
    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
Exemple #10
0
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)