Exemplo n.º 1
0
    def __init__(self, bnd_source=None, entry_class=None):
        """Load a BND.

        Source can be a .*bnd file, an unpacked BND directory (or the 'bnd_manifest.txt' file inside it), raw bytes,
        or an open data stream (or None to create an empty BND).

        If an entry class is given, all entry data will be passed to that class to create instances of it. The paths and
        IDs of the entries will be maintained in self.binary_entries, but the data of these entries will be overwritten
        with packed versions of the classed entry instances when the BND is packed. (Until then, any edited entry
        instances will diverge from the original binary entry data.)
        """
        self.header_struct = None
        self.entry_header_struct = None

        self.bnd_path = Path(
        )  # Always a '.bnd' file Path after the BND is loaded.
        self.bnd_version = b''
        self.bnd_signature = b''
        self.bnd_magic = None  # Can't guess this; you'll need to specify it based on your BND type.
        self.big_endian = False
        self.dcx = (
        )  # Pair of DCX magic values, or empty tuple to not use DCX.

        self._entry_class = entry_class
        self._most_recent_hash_table = b''
        self._most_recent_entry_count = 0
        self._most_recent_paths = []

        self.binary_entries = [
        ]  # Always stores binary (unpacked) entries. Only updated when pack() is called.
        self._entries = [
        ]  # List of entries, instantiated with given entry class (or left as binary).

        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():
                if bnd_path.suffix == '.unpacked':
                    self.bnd_path = bnd_path.parent.absolute() / bnd_path.stem
                else:
                    self.bnd_path = Path(bnd_path).absolute()
                self.load_unpacked_dir(bnd_path)
            else:
                self.bnd_path = bnd_path.absolute()
                if bnd_path.suffix == '.dcx':
                    bnd_dcx = DCX(bnd_path)
                    self.unpack(bnd_dcx.data)
                    self.dcx = bnd_dcx.magic
                else:
                    with open(bnd_path, 'rb') as file:
                        self.unpack(file)
        elif bnd_source is not None:
            self.unpack(bnd_source)
Exemplo n.º 2
0
    def write(self, bnd_path=None):
        if bnd_path is None:
            bnd_path = self.bnd_path
        else:
            bnd_path = Path(bnd_path)
            if self.dcx and bnd_path.suffix != '.dcx':
                bnd_path = bnd_path.with_suffix(bnd_path.suffix + '.dcx')
        bnd_path.parent.mkdir(parents=True, exist_ok=True)
        create_bak(bnd_path)
        packed = self.pack()

        if self.dcx:
            # Apply DCX compression.
            packed = DCX(packed, magic=self.dcx).pack()

        with bnd_path.open('wb') as f:
            f.write(packed)
Exemplo n.º 3
0
def BND(bnd_source=None, entry_class=None, optional_dcx=True) -> BaseBND:
    """Auto-detects BND version (BND3 or BND4) to use when opening the source, if appropriate.

    Args:
        bnd_source: path to BND file or BND file content. The BND version will be automatically detected from the data.
            If None, this function will return an empty BND4 container. (Default: None)
        entry_class: optional class to load data from each BND entry into after the BND is unpacked, which is convenient
            for any BNDs that contain file types handled by Soulstruct. (Default: None)
        optional_dcx: if 'bnd_source' is a file path and this is True, both DCX (preferred) and non-DCX versions of that
            BND path will be checked. (It doesn't matter if 'bnd_source' ends in '.dcx' already.) Set this to False to
            search only for the exact path given. (Default: True)
    """
    dcx = False
    bnd_path = None
    if isinstance(bnd_source, (str, Path)):
        if not Path(bnd_source).is_dir():
            bnd_path = Path(bnd_source).absolute()
            if optional_dcx:
                bnd_path = find_dcx(bnd_path)
            elif not bnd_path.is_file():
                raise FileNotFoundError(f"Could not find BND file: {bnd_path}")
            if bnd_path.suffix == '.dcx':
                # Must unpack DCX archive before detecting BND type.
                bnd_dcx = DCX(bnd_path)
                bnd_source = bnd_dcx.data
                dcx = bnd_dcx.magic
            else:
                bnd_source = bnd_path
    if BND3.detect(bnd_source):
        bnd = BND3(bnd_source, entry_class=entry_class)
        if dcx:
            bnd.dcx = dcx
    elif BND4.detect(bnd_source):
        bnd = BND4(bnd_source, entry_class=entry_class)
        if dcx:
            bnd.dcx = dcx
    else:
        raise TypeError("Data bytes could not be interpreted as BND3 or BND4.")
    if bnd_path is not None:
        bnd.bnd_path = bnd_path
    return bnd
Exemplo n.º 4
0
    def pack(self, dcx=None):
        if dcx is None:
            # Auto-detect DCX compression from source file (if applicable).
            dcx = self.dcx

        event_table_binary = b""
        instr_table_binary = b""
        argument_data_binary = b""
        arg_r_binary = b""

        current_instruction_offset = 0
        current_arg_data_offset = 0
        current_event_arg_offset = 0

        header = self.build_emevd_header()

        for e in self.events.values():
            e_bin, i_bin, a_bin, p_bin = e.to_binary(
                current_instruction_offset, current_arg_data_offset,
                current_event_arg_offset)

            event_table_binary += e_bin
            instr_table_binary += i_bin
            argument_data_binary += a_bin
            arg_r_binary += p_bin

            if len(
                    i_bin
            ) != self.Event.Instruction.STRUCT.size * e.instruction_count:
                raise ValueError(
                    f"Event ID: {e.event_id} returned packed instruction binary of size {len(i_bin)} but "
                    f"reports {e.instruction_count} total instructions (with expected size "
                    f"{self.Event.Instruction.STRUCT.size * e.instruction_count})."
                )
            if len(p_bin
                   ) != self.Event.EventArg.STRUCT.size * e.event_arg_count:
                raise ValueError(
                    f"Event ID: {e.event_id} returned packed arg replacement binary of size {len(p_bin)} "
                    f"but reports {e.event_arg_count} total replacements (with expected size "
                    f"{self.Event.EventArg.STRUCT.size * e.event_arg_count}).")
            if len(a_bin) != e.total_args_size:
                raise ValueError(
                    f"Event ID: {e.event_id} returned packed argument data binary of size {len(a_bin)} "
                    f"but reports expected size to be {e.total_args_size}).")

            current_instruction_offset += len(i_bin)
            current_arg_data_offset += len(a_bin)
            current_event_arg_offset += len(p_bin)

        linked_file_data_binary = struct.pack(
            "<" + "Q" * len(self.linked_file_offsets),
            *self.linked_file_offsets)
        event_layers_table = self.build_event_layers_table()
        event_layers_binary = b"".join(event_layers_table)

        emevd_binary = b""
        offsets = self.compute_table_offsets(event_layers_table)
        if len(header) != offsets["event"]:
            raise ValueError(
                f"Header was of size {len(header)} but expected size was {self.STRUCT.size}."
            )
        emevd_binary += header
        if len(emevd_binary) + len(
                event_table_binary) != offsets["instruction"]:
            raise ValueError(
                f"Event table was of size {len(event_table_binary)} but expected size was "
                f"{offsets['instruction'] - len(emevd_binary)}.")
        emevd_binary += event_table_binary
        if len(emevd_binary) + len(
                instr_table_binary) != offsets["event_layers"]:
            raise ValueError(
                f"Instruction table was of size {len(instr_table_binary)} but expected size was "
                f"{offsets['event_layers'] - len(emevd_binary)}.")
        emevd_binary += instr_table_binary
        if len(emevd_binary) + len(
                event_layers_binary) != offsets["base_arg_data"]:
            raise ValueError(
                f"Event layers table was of size {len(event_layers_binary)} but expected size was "
                f"{offsets['base_arg_data'] - len(emevd_binary)}.")

        emevd_binary += event_layers_binary

        # No argument data length check due to padding.
        emevd_binary += argument_data_binary
        emevd_binary = self.pad_after_base_args(emevd_binary)

        if len(emevd_binary) + len(arg_r_binary) != offsets["linked_files"]:
            raise ValueError(
                f"Argument replacement table was of size {len(linked_file_data_binary)} but expected size "
                f"was {offsets['linked_files'] - len(emevd_binary)}.")
        emevd_binary += arg_r_binary
        if len(emevd_binary) + len(
                linked_file_data_binary) != offsets["packed_strings"]:
            raise ValueError(
                f"Linked file data was of size {len(linked_file_data_binary)} but expected size was "
                f"{offsets['packed_strings'] - len(emevd_binary)}.")
        emevd_binary += linked_file_data_binary
        if len(emevd_binary) + len(
                self.packed_strings) != offsets["end_of_file"]:
            raise ValueError(
                f"Packed string data was of size {len(linked_file_data_binary)} but expected size was "
                f"{offsets['end_of_file'] - len(emevd_binary)}.")
        emevd_binary += self.packed_strings

        if dcx:
            return DCX(emevd_binary, magic=self.DCX_MAGIC).pack()
        return emevd_binary
Exemplo n.º 5
0
    def __init__(self, emevd_source, script_path=None):

        if not self.GAME_MODULE:
            raise NotImplementedError(
                "You cannot instantiate BaseEMEVD. Use a game-specific child, e.g. "
                "`from soulstruct.events.darksoul1 import EMEVD`.")

        self.events = OrderedDict()
        self.packed_strings = b""
        self.linked_file_offsets = []  # Offsets into packed strings.
        self.dcx = False

        if isinstance(emevd_source, EvsParser):
            self.map_name = emevd_source.map_name
            events, linked_file_offsets, packed_strings = build_numeric(
                emevd_source.numeric_emevd, self.Event)
            self.events.update(events)
            self.linked_file_offsets = linked_file_offsets
            self.packed_strings = packed_strings

        elif isinstance(emevd_source, dict):
            self.map_name = None
            try:
                self.linked_file_offsets = emevd_source.pop("linked")
            except KeyError:
                _LOGGER.warning(
                    "No linked file offsets found in EMEVD source.")
            try:
                self.packed_strings = emevd_source.pop("strings")
            except KeyError:
                _LOGGER.warning("No strings found in EMEVD source.")
            self.events.update(OrderedDict(emevd_source))

        elif isinstance(emevd_source, str) and "\n" in emevd_source:
            parsed = EvsParser(emevd_source,
                               game_module=self.GAME_MODULE,
                               script_path=script_path)
            self.map_name = parsed.map_name
            events, self.linked_file_offsets, self.packed_strings = build_numeric(
                parsed.numeric_emevd, self.Event)
            self.events.update(events)

        elif isinstance(emevd_source, Path) or (isinstance(emevd_source, str)
                                                and "\n" not in emevd_source):
            emevd_path = Path(emevd_source)
            self.map_name = emevd_path.stem

            if emevd_path.suffix in {".evs", ".py"}:
                parsed = EvsParser(emevd_path,
                                   game_module=self.GAME_MODULE,
                                   script_path=script_path)
                self.map_name = parsed.map_name
                events, self.linked_file_offsets, self.packed_strings = build_numeric(
                    parsed.numeric_emevd, self.Event)
                self.events.update(events)

            elif emevd_path.suffix == ".txt":
                try:
                    self.build_from_numeric_path(emevd_path)
                except Exception:
                    raise IOError(
                        f"Could not interpret file '{str(emevd_path)}' as numeric-style EMEVD.\n"
                        f"(Note that you cannot use verbose-style text files as EMEVD input.)\n"
                        f"If your file is an EVS script, change the extension to '.py' or '.evs'."
                    )

            elif emevd_path.name.endswith(".emevd.dcx"):
                emevd_data = DCX(emevd_path).data
                self.dcx = True  # DCX magic of EMEVD is applied automatically at pack.
                self.map_name = emevd_path.name.split(".")[
                    0]  # Strip all extensions.
                try:
                    self.unpack(BytesIO(emevd_data))
                except Exception:
                    raise IOError(
                        f"Could not interpret file '{str(emevd_path)}' as binary EMEVD data.\n"
                        f"You should only use the '.emevd[.dcx]' extension for actual game-ready\n"
                        f"EMEVD, which you can create with the `pack()` method of this class."
                    )

            elif emevd_path.suffix == ".emevd":
                try:
                    with emevd_path.open("rb") as f:
                        self.unpack(f)
                except Exception:
                    raise IOError(
                        f"Could not interpret file '{str(emevd_source)}' as binary EMEVD data.\n"
                        f"You should only use the '.emevd' extension for actual game-ready\n"
                        f"EMEVD, which you can create with the `pack()` method of this class."
                    )

            else:
                raise TypeError(
                    f"Cannot open EMEVD from source {emevd_source} with type {type(emevd_source)}."
                )

        elif isinstance(emevd_source, bytes):
            self.map_name = None
            self.unpack(BytesIO(emevd_source))

        else:
            raise TypeError(
                f"Cannot open EMEVD from source type: {type(emevd_source)}")