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)
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)
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
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
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)}")