class MSBPart( BaseMSBPart, MSB_Scale, MSB_ModelName, MSB_DrawParent, MSB_DrawGroups, MSB_DisplayGroups, MSB_BackreadGroups, abc.ABC, ): PART_HEADER_STRUCT = BinaryStruct( ("__description_offset", "q"), ("__name_offset", "q"), ("_instance_index", "i"), # TK says "Unknown; appears to count up with each instance of a model added." ("__part_type", "i"), ("_part_type_index", "i"), ("_model_index", "i"), ("__sib_path_offset", "q"), ("translate", "3f"), ("rotate", "3f"), ("scale", "3f"), ("__draw_groups", "8I"), ("__display_groups", "8I"), ("__backread_groups", "8I"), "4x", ("__base_data_offset", "q"), ("__type_data_offset", "q"), ("__gparam_data_offset", "q"), ("__scene_gparam_data_offset", "q"), ) PART_BASE_DATA_STRUCT = BinaryStruct( ("entity_id", "i"), ("base_unk_x04_x05", "b"), ("base_unk_x05_x06", "b"), ("base_unk_x06_x07", "b"), ("base_unk_x07_x08", "b"), "4x", ("lantern_id", "b"), ("lod_id", "b"), ("base_unk_x0e_x0f", "b"), ("base_unk_x0f_x10", "b"), ) NAME_ENCODING = "utf-16-le" DRAW_GROUP_COUNT = 256 DISPLAY_GROUP_COUNT = 256 BACKREAD_GROUP_COUNT = 256 FIELD_INFO = BaseMSBPart.FIELD_INFO | { "sib_path": MapFieldInfo( "SIB Path", str, "", "Internal path to SIB placeholder file for part.", ), "scale": MapFieldInfo( "Scale", Vector3, Vector3.ones(), "Scale of part. Only works for Map Pieces and Objects.", ), "draw_groups": MapFieldInfo( "Draw Groups", list, set(range(DRAW_GROUP_COUNT)), "Draw groups of part. This part will be drawn when the corresponding display group is active.", ), "display_groups": MapFieldInfo( "Display Groups", list, set(range(DISPLAY_GROUP_COUNT)), "Display groups are present in all MSB Parts, but only function for collisions.", ), "backread_groups": MapFieldInfo( "Backread Groups", list, set(range(BACKREAD_GROUP_COUNT)), "Backread groups are present in all MSB Parts, but only function for collisions (probably).", ), "base_unk_x04_x05": MapFieldInfo( "Unknown Base [04-05]", int, -1, "Unknown base data integer.", ), "base_unk_x05_x06": MapFieldInfo( "Unknown Base [05-06]", int, -1, "Unknown base data integer.", ), "base_unk_x06_x07": MapFieldInfo( "Unknown Base [06-07]", int, -1, "Unknown base data integer.", ), "base_unk_x07_x08": MapFieldInfo( "Unknown Base [07-08]", int, -1, "Unknown base data integer.", ), "lantern_id": MapFieldInfo( "Lantern ID", int, 0, "Lantern param ID.", ), "lod_id": MapFieldInfo( "LoD ID", int, -1, "LoD (level of detail) param ID.", ), "base_unk_x0e_x0f": MapFieldInfo( "Unknown Base [0e-0f]", int, 20, "Unknown base data integer.", ), "base_unk_x0f_x10": MapFieldInfo( "Unknown Base [0f-10]", int, 0, "Unknown base data integer.", ), } FIELD_ORDER = ( "base_unk_x04_x05", "base_unk_x05_x06", "base_unk_x06_x07", "base_unk_x07_x08", "lantern_id", "lod_id", "base_unk_x0e_x0f", "base_unk_x0f_x10", ) sib_path: str base_unk_x04_x05: int base_unk_x05_x06: int base_unk_x06_x07: int base_unk_x07_x08: int lantern_id: int lod_id: int base_unk_x0e_x0f: int base_unk_x0f_x10: int def __init__(self, source=None, **kwargs): self._instance_index = 0 super().__init__(source, **kwargs) def unpack(self, msb_reader: BinaryReader): part_offset = msb_reader.position header = msb_reader.unpack_struct(self.PART_HEADER_STRUCT) if header["__part_type"] != self.ENTRY_SUBTYPE: raise ValueError(f"Unexpected part type enum {header['part_type']} for class {self.__class__.__name__}.") self._instance_index = header["_instance_index"] self._model_index = header["_model_index"] self._part_type_index = header["_part_type_index"] for transform in ("translate", "rotate", "scale"): setattr(self, transform, Vector3(header[transform])) self._draw_groups = int_group_to_bit_set(header["__draw_groups"], assert_size=8) self._display_groups = int_group_to_bit_set(header["__display_groups"], assert_size=8) self._backread_groups = int_group_to_bit_set(header["__backread_groups"], assert_size=8) self.description = msb_reader.unpack_string( offset=part_offset + header["__description_offset"], encoding="utf-16-le", ) self.name = msb_reader.unpack_string( offset=part_offset + header["__name_offset"], encoding="utf-16-le", ) self.sib_path = msb_reader.unpack_string( offset=part_offset + header["__sib_path_offset"], encoding="utf-16-le", ) msb_reader.seek(part_offset + header["__base_data_offset"]) base_data = msb_reader.unpack_struct(self.PART_BASE_DATA_STRUCT) self.set(**base_data) msb_reader.seek(part_offset + header["__type_data_offset"]) self.unpack_type_data(msb_reader) self._unpack_gparam_data(msb_reader, part_offset, header) self._unpack_scene_gparam_data(msb_reader, part_offset, header) def pack(self) -> bytes: """Pack to bytes, presumably as part of a full `MSB` pack.""" # Validate draw/display/backread groups before doing any real work. draw_groups = bit_set_to_int_group(self._draw_groups, group_size=8) display_groups = bit_set_to_int_group(self._display_groups, group_size=8) backread_groups = bit_set_to_int_group(self._backread_groups, group_size=8) description_offset = self.PART_HEADER_STRUCT.size packed_description = self.description.encode("utf-16-le") + b"\0\0" name_offset = description_offset + len(packed_description) packed_name = self.get_name_to_pack().encode("utf-16-le") + b"\0\0" sib_path_offset = name_offset + len(packed_name) packed_sib_path = self.sib_path.encode("utf-16-le") + b"\0\0" if self.sib_path else b"\0\0" strings_size = len(packed_description + packed_name + packed_sib_path) if strings_size <= 0x38: packed_sib_path += b"\0" * (0x3c - strings_size) else: packed_sib_path += b"\0" * 8 while len(packed_description + packed_name + packed_sib_path) % 4 != 0: packed_sib_path += b"\0" # Not done in SoulsFormats, but makes sense to me. base_data_offset = sib_path_offset + len(packed_sib_path) packed_base_data = self.PART_BASE_DATA_STRUCT.pack(self) type_data_offset = base_data_offset + len(packed_base_data) packed_type_data = self.pack_type_data() gparam_data_offset = type_data_offset + len(packed_type_data) packed_gparam_data = self._pack_gparam_data() scene_gparam_data_offset = gparam_data_offset + len(packed_gparam_data) packed_scene_gparam_data = self._pack_scene_gparam_data() try: packed_header = self.PART_HEADER_STRUCT.pack( __description_offset=description_offset, __name_offset=name_offset, _instance_index=self._instance_index, __part_type=self.ENTRY_SUBTYPE, _part_type_index=self._part_type_index, _model_index=self._model_index, __sib_path_offset=sib_path_offset, translate=list(self.translate), rotate=list(self.rotate), scale=list(self.scale), __draw_groups=draw_groups, __display_groups=display_groups, __backread_groups=backread_groups, __base_data_offset=base_data_offset, __type_data_offset=type_data_offset, __gparam_data_offset=gparam_data_offset, __scene_gparam_data_offset=scene_gparam_data_offset, ) except struct.error: raise MapError(f"Could not pack header data of MSB part '{self.name}'. See traceback.") return ( packed_header + packed_description + packed_name + packed_sib_path + packed_base_data + packed_type_data + packed_gparam_data + packed_scene_gparam_data ) def _unpack_gparam_data(self, msb_reader: BinaryReader, part_offset, header): pass def _pack_gparam_data(self): return b"" def _unpack_scene_gparam_data(self, msb_reader: BinaryReader, part_offset, header): pass def _pack_scene_gparam_data(self): return b""
class MSBPart(MSBEntryEntityCoordinates, abc.ABC): ENTRY_SUBTYPE: MSBPartSubtype = None PART_HEADER_STRUCT: BinaryStruct = None PART_BASE_DATA_STRUCT: BinaryStruct = None PART_TYPE_DATA_STRUCT: BinaryStruct = None NAME_ENCODING = "" FLAG_SET_SIZE = 128 FIELD_INFO = MSBEntryEntityCoordinates.FIELD_INFO | { "sib_path": MapFieldInfo( "SIB Path", str, "", "Internal path to SIB placeholder file for part.", ), "scale": MapFieldInfo( "Scale", Vector3, Vector3.ones(), "Scale of part. Only works for Map Pieces.", # TODO: and maybe Objects? ), # Every concrete subclass defines 'model_name', 'draw_groups', and 'display_groups'. } sib_path: str scale: Vector3 model_name: tp.Optional[str] def __init__(self, source=None, **kwargs): self._part_type_index = -1 self._model_index = -1 self._draw_groups = set() self._display_groups = set() super().__init__(source=source, **kwargs) def unpack_type_data(self, msb_buffer): """This unpacks simple attributes by default, but some Parts need to process these values more.""" self.set(**self.PART_TYPE_DATA_STRUCT.unpack(msb_buffer, exclude_asserted=True)) def pack_type_data(self): try: return self.PART_TYPE_DATA_STRUCT.pack(self) except struct.error: raise SoulstructError( f"Could not pack type data of MSB part '{self.name}'. See traceback." ) def set_indices( self, part_type_index, model_indices, local_environment_indices, region_indices, part_indices, local_collision_indices, ): self._part_type_index = part_type_index try: self._model_index = model_indices[ self.model_name] if self.model_name else -1 except KeyError: raise KeyError( f"Invalid model name for {self.ENTRY_SUBTYPE.name} {self.name} (entity ID {self.entity_id}): " f"{self.model_name}") def set_names( self, model_names, region_names, environment_names, part_names, collision_names, ): if self._model_index != -1: try: self.model_name = model_names[self._model_index] except KeyError: raise KeyError( f"Invalid model index for {self.ENTRY_SUBTYPE.name} {self.name} (entity ID {self.entity_id}): " f"{self._model_index}") else: self.model_name = None @property def draw_groups(self): return self._draw_groups @draw_groups.setter def draw_groups(self, value): """Converts value to a `set()` (possibly empty) and validates index range.""" if value is None or isinstance(value, str) and value in {"None", ""}: self._draw_groups = set() return try: draw_groups = set(value) except (TypeError, ValueError): raise TypeError( "Draw groups must be a set, sequence, `None`, 'None', or ''. Or use `set` methods like `.add()`." ) for i in draw_groups: if not isinstance(i, int) and 0 <= i < self.FLAG_SET_SIZE: raise InvalidFieldValueError( f"Invalid draw group: {i}. Must be 0 <= i < {self.FLAG_SET_SIZE}." ) self._draw_groups = draw_groups @property def display_groups(self): return self._display_groups @display_groups.setter def display_groups(self, value): """Converts value to a `set()` (possibly empty) and validates index range.""" if value is None or isinstance(value, str) and value in {"None", ""}: self._display_groups = set() return try: display_groups = set(value) except (TypeError, ValueError): raise TypeError( "Display groups must be a set, sequence, `None`, 'None', or ''. Or use `set` methods like `.add()`." ) for i in display_groups: if not isinstance(i, int) and 0 <= i < self.FLAG_SET_SIZE: raise ValueError( f"Invalid display group: {i}. Must be 0 <= i < {self.FLAG_SET_SIZE}." ) self._display_groups = display_groups