class MSBEnvironmentEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.Environment EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("unk_x00_x04", "i"), ("unk_x04_x08", "f"), ("unk_x08_x0c", "f"), ("unk_x0c_x10", "f"), ("unk_x10_x14", "f"), ("unk_x14_x18", "f"), "8x", ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_part_name": MapFieldInfo( "Draw Parent", MapPart, None, "Environment (or 'map spot') will be drawn whenever its parent is drawn.", ), "base_region_name": MapFieldInfo( "Environment Region", Region, None, "Region (usually a Point) at which Environment appears (whatever that means).", ), "entity_id": MapFieldInfo( "Environment ID", int, -1, "Unknown index. Note that this replaces the usual Entity ID field.", ), "unk_x00_x04": MapFieldInfo( "Unknown [00-04]", int, 0, "Unknown Environment parameter (integer).", ), "unk_x04_x08": MapFieldInfo( "Unknown [04-08]", float, 1.0, "Unknown Environment parameter (floating-point number).", ), "unk_x08_x0c": MapFieldInfo( "Unknown [08-0c]", float, 1.0, "Unknown Environment parameter (floating-point number).", ), "unk_x0c_x10": MapFieldInfo( "Unknown [0c-10]", float, 1.0, "Unknown Environment parameter (floating-point number).", ), "unk_x10_x14": MapFieldInfo( "Unknown [10-14]", float, 1.0, "Unknown Environment parameter (floating-point number).", ), "unk_x14_x18": MapFieldInfo( "Unknown [14-18]", float, 1.0, "Unknown Environment parameter (floating-point number).", ), } FIELD_ORDER = ( "entity_id", "base_part_name", "base_region_name", "unk_x00_x04", "unk_x04_x08", "unk_x08_x0c", "unk_x0c_x10", "unk_x10_x14", "unk_x14_x18", ) unk_x00_x04: int unk_x04_x08: float unk_x08_x0c: float unk_x0c_x10: float unk_x10_x14: float unk_x14_x18: float
class MSBPlatoonEvent(MSBEvent): """Defines a group (platoon) of enemies.""" ENTRY_SUBTYPE = MSBEventSubtype.Platoon EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("platoon_id_script_active", "i"), ("state", "i"), "16x", ("_platoon_character_indices", "30i"), ("_platoon_parent_indices", "2i"), ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_part_name": MapFieldInfo( "Draw Parent", MapPart, None, "Probably unused for Platoon.", ), "base_region_name": MapFieldInfo( "Base Region", Region, None, "Probably unused for Platoon.", ), "entity_id": MapFieldInfo( "Entity ID", int, -1, "Probably unused for Platoon.", ), "platoon_character_names": MapFieldInfo( "Platoon Character Names", GameObjectSequence((Character, 30)), [None] * 30, "Characters in this Platoon.", ), "platoon_parent_names": MapFieldInfo( "Platoon Parent Names", GameObjectSequence((MapPart, 2)), [None] * 2, "Parent parts of this Platoon.", ), "platoon_id_script_active": MapFieldInfo( "Platoon Active Script ID", int, -1, "Unknown. Possibly an AI param ID.", ), "state": MapFieldInfo( "Platoon State", int, -1, "Unknown.", ) } FIELD_ORDER = ( "entity_id", "base_part_name", "base_region_name", "platoon_character_names", "platoon_parent_names", "platoon_id_script_active", "state", ) def __init__(self, source, **kwargs): self._platoon_character_names = [None] * 30 self._platoon_character_indices = [-1] * 30 self._platoon_parent_names = [None] * 2 self._platoon_parent_indices = [-1] * 2 super().__init__(source=source, **kwargs) @property def platoon_character_names(self): return self._platoon_character_names @platoon_character_names.setter def platoon_character_names(self, value): """Pads out to 30 names with `None`. Also replaces empty strings with `None`.""" names = [] for v in value: if v is not None and not isinstance(v, str): raise TypeError( "Platoon character names must be strings or `None`.") names.append(v if v else None) self._platoon_character_names = value while len(self._platoon_character_names) < 30: self._platoon_character_names.append(None) @property def platoon_parent_names(self): return self._platoon_parent_names @platoon_parent_names.setter def platoon_parent_names(self, value): """Pads out to 2 names with `None`. Also replaces empty strings with `None`.""" names = [] for v in value: if v is not None and not isinstance(v, str): raise TypeError( "Platoon parent names must be strings or `None`.") names.append(v if v else None) self._platoon_parent_names = value while len(self._platoon_parent_names) < 2: self._platoon_parent_names.append(None) def set_indices(self, event_index, local_event_index, region_indices, part_indices): super().set_indices(event_index, local_event_index, region_indices, part_indices) self._platoon_character_indices = [ part_indices[n] if n else -1 for n in self._platoon_character_names ] while len(self._platoon_character_indices) < 30: self._platoon_character_indices.append(-1) self._platoon_parent_indices = [ part_indices[n] if n else -1 for n in self._platoon_parent_names ] while len(self._platoon_parent_indices) < 2: self._platoon_parent_indices.append(-1) def set_names(self, region_names, part_names): super().set_names(region_names, part_names) self._platoon_character_names = [ part_names[i] if i != -1 else None for i in self._platoon_character_indices ] while len(self._platoon_character_names) < 30: self._platoon_character_names.append(None) self._platoon_parent_names = [ part_names[i] if i != -1 else None for i in self._platoon_parent_indices ] while len(self._platoon_parent_names) < 2: self._platoon_parent_names.append(None)
class MSBMultiSummonEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.MultiSummon EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("unk_x00_x04", "i"), ("unk_x04_x06", "h"), ("unk_x06_x08", "h"), ("unk_x08_x0a", "h"), ("unk_x0a_x0c", "h"), "4x", ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_part_name": MapFieldInfo( "Draw Parent", MapPart, None, "Probably unused for Multi Summon.", ), "base_region_name": MapFieldInfo( "Base Region", Region, None, "Probably unused for Multi Summon.", ), "entity_id": MapFieldInfo( "Entity ID", int, -1, "Probably unused for Multi Summon.", ), "unk_x00_x04": MapFieldInfo( "Unknown [00-04]", int, 1, "Unknown integer.", ), "unk_x04_x06": MapFieldInfo( "Unknown [04-06]", int, 1, "Unknown 16-bit integer.", ), "unk_x06_x08": MapFieldInfo( "Unknown [06-08]", int, 1, "Unknown 16-bit integer.", ), "unk_x08_x0a": MapFieldInfo( "Unknown [08-0a]", int, 1, "Unknown 16-bit integer.", ), "unk_x0a_x0c": MapFieldInfo( "Unknown [0a-0c]", int, 1, "Unknown 16-bit integer.", ), } FIELD_ORDER = ( "entity_id", "base_part_name", "base_region_name", "unk_x00_x04", "unk_x04_x06", "unk_x06_x08", "unk_x08_x0a", "unk_x0a_x0c", ) unk_x00_x04: int unk_x04_x06: int unk_x06_x08: int unk_x08_x0a: int unk_x0a_x0c: int
class MSBPart(_BaseMSBPart, 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" FLAG_SET_SIZE = 256 FIELD_INFO = _BaseMSBPart.FIELD_INFO | { "draw_groups": MapFieldInfo( "Draw Groups", list, set(range(FLAG_SET_SIZE)), "Draw groups of part. This part will be drawn when the corresponding display group is active.", ), "display_groups": MapFieldInfo( "Display Groups", list, set(range(FLAG_SET_SIZE)), "Display groups are present in all MSB Parts, but only function for collisions.", ), "backread_groups": MapFieldInfo( "Backread Groups", list, set(range(FLAG_SET_SIZE)), "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", ) 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 self._draw_groups = set() self._display_groups = set() self._backread_groups = set() 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 ) @property def backread_groups(self): return self._backread_groups @backread_groups.setter def backread_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( "Backread 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 backread group: {i}. Must be 0 <= i < {self.FLAG_SET_SIZE}.") self._display_groups = display_groups 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 MSBPatrolRouteEvent(MSBEvent): """Defines a patrol route through a sequence of up to 32 regions.""" ENTRY_SUBTYPE = MSBEventSubtype.PatrolRoute EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("unk_x00_x04", "i"), "12x", ("_patrol_region_indices", "32h"), ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_part_name": MapFieldInfo( "Draw Parent", MapPart, None, "Probably unused for Patrol Route.", ), "base_region_name": MapFieldInfo( "Base Region", Region, None, "Probably unused for Patrol Route.", ), "entity_id": MapFieldInfo( "Entity ID", int, -1, "Probably unused for Patrol Route.", ), "unk_x00_x04": MapFieldInfo( "Unknown [00-04]", int, 1, "Unknown integer.", ), "patrol_region_names": MapFieldInfo("Patrol Regions", GameObjectSequence( (Region, 32)), [None] * 32, "List of regions that define this Patrol Route."), } FIELD_ORDER = ( "entity_id", "base_part_name", "base_region_name", "unk_x00_x04", "patrol_region_names", ) unk_x00_x04: int def __init__(self, source, **kwargs): self._patrol_region_names = [None] * 32 self._patrol_region_indices = [-1] * 32 super().__init__(source=source, **kwargs) @property def patrol_region_names(self): return self._patrol_region_names @patrol_region_names.setter def patrol_region_names(self, value): """Pads out to 32 names with `None`. Also replaces empty strings with `None`.""" names = [] for v in value: if v is not None and not isinstance(v, str): raise TypeError( "Patrol point names must be strings or `None`.") names.append(v if v else None) self._patrol_region_names = value while len(self._patrol_region_names) < 32: self._patrol_region_names.append(None) def set_indices(self, event_index, local_event_index, region_indices, part_indices): super().set_indices(event_index, local_event_index, region_indices, part_indices) self._patrol_region_indices = [ region_indices[n] if n else -1 for n in self._patrol_region_names ] while len(self._patrol_region_indices) < 32: self._patrol_region_indices.append(-1) def set_names(self, region_names, part_names): super().set_names(region_names, part_names) self._patrol_region_names = [ region_names[i] if i != -1 else None for i in self._patrol_region_indices ] while len(self._patrol_region_names) < 32: self._patrol_region_names.append(None)
class MSBTreasureEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.Treasure EVENT_TYPE_DATA_STRUCT = BinaryStruct( "4x", ("_treasure_part_index", "i"), ("item_lot_1", "i"), ("minus_one_1", "i", -1), ("item_lot_2", "i"), ("minus_one_2", "i", -1), ("item_lot_3", "i"), ("minus_one_3", "i", -1), ("item_lot_4", "i"), ("minus_one_4", "i", -1), ("item_lot_5", "i"), ("minus_one_5", "i", -1), ("is_in_chest", "?"), ("is_hidden", "?"), "2x", ) FIELD_INFO = MSBEvent.FIELD_INFO | { "treasure_part_name": MapFieldInfo( "Treasure Object", Object, None, "Object on which treasure will appear (usually a corpse or chest).", ), "item_lot_1": MapFieldInfo( "Item Lot 1", ItemLotParam, -1, "First item lot of treasure. (Note that the item lots that are +1 to +5 from this ID will also be " "awarded.)", ), "item_lot_2": MapFieldInfo( "Item Lot 2", ItemLotParam, -1, "Second item lot of treasure. (Note that the item lots that are +1 to +5 from this ID will also be " "awarded.)", ), "item_lot_3": MapFieldInfo( "Item Lot 3", ItemLotParam, -1, "Third item lot of treasure. (Note that the item lots that are +1 to +5 from this ID will also be " "awarded.)", ), "is_in_chest": MapFieldInfo( "Is In Chest", bool, False, "Indicates if this treasure is inside a chest (affects appearance).", # TODO: effect? ), "is_hidden": MapFieldInfo( "Is Hidden", bool, False, "If True, this treasure will start disabled and will need to be enabled manually with an event script.", ), "item_lot_4": MapFieldInfo( "Item Lot 4", ItemLotParam, -1, "Fourth item lot of treasure. (Note that the item lots that are +1 to +5 from this ID will also be " "awarded.)", ), "item_lot_5": MapFieldInfo( "Item Lot 5", ItemLotParam, -1, "Fifth item lot of treasure. (Note that the item lots that are +1 to +5 from this ID will also be " "awarded.)", ), } FIELD_ORDER = ( "treasure_part_name", "item_lot_1", "item_lot_2", "item_lot_3", "item_lot_4", "item_lot_5", "is_in_chest", "is_hidden", ) REFERENCE_FIELDS = { "parts": ["base_part_name", "treasure_part_name"], "regions": ["base_region_name"] } treasure_part_name: tp.Optional[str] item_lot_1: int item_lot_2: int item_lot_3: int is_in_chest: bool is_hidden: bool def __init__(self, source=None, **kwargs): self._treasure_part_index = None self._treasure_part_name = None self.item_lot_1 = -1 self.item_lot_2 = -1 self.item_lot_3 = -1 self.item_lot_4 = -1 self.item_lot_5 = -1 super().__init__(source=source, **kwargs) @property def treasure_part_name(self): return self._treasure_part_name @treasure_part_name.setter def treasure_part_name(self, value: tp.Union[None, str]): if isinstance(value, str): self._treasure_part_name = value if value else None elif value is None: self._treasure_part_name = None else: raise TypeError( f"`treasure_part_name` must be a string or `None`, not {value}." ) def set_indices(self, indices: EventsIndicesData): super().set_indices(indices) self._treasure_part_index = indices.part_indices[ self._treasure_part_name] if self.treasure_part_name else -1 def set_names(self, names: EventsNamesData): super().set_names(names) self._treasure_part_name = (names.part_names[self._treasure_part_index] if self._treasure_part_index != -1 else None)
class MSBMessageEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.Message EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("text_id", "h"), ("unk_x02_x04", "h"), ("is_hidden", "?"), "3x", ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_part_name": MapFieldInfo( "Draw Parent", MapPart, None, "Message will be drawn as long as this parent (usually a Collision or Map Piece part) is drawn.", ), "base_region_name": MapFieldInfo( "Message Region", Region, None, "Region (usually a Point) at which Message appears.", ), "entity_id": MapFieldInfo( "Entity ID", int, -1, "Entity ID used to refer to the Message in other game files.", ), "text_id": MapFieldInfo( "Message Text ID", SoapstoneMessage, -1, "Soapstone Messages text ID shown when soapstone message is examined.", ), "unk_x02_x04": MapFieldInfo( "Unknown [02-04]", int, 2, "Unknown. Often set to 2.", ), "is_hidden": MapFieldInfo( "Is Hidden", bool, False, "If True, the message must be manually enabled with an event script or by using Seek Guidance.", ), } FIELD_ORDER = ( "entity_id", "base_part_name", "base_region_name", "text_id", "unk_x02_x04", "is_hidden", ) text_id: int unk_x02_x04: int is_hidden: bool
class MSBObject(_BaseMSBObject, MSBPart): PART_TYPE_DATA_STRUCT = BinaryStruct( "4x", ("_draw_parent_index", "i"), ("break_term", "b"), ("net_sync_type", "b"), "2x", ("default_animation", "h"), ("unk_x0e_x10", "h"), ("unk_x10_x14", "i"), "4x", ) FIELD_INFO = MSBPart.FIELD_INFO | _BaseMSBObject.FIELD_INFO | { "break_term": MapFieldInfo( "Break Term", int, 0, "Unknown. Related to object breakage.", ), "net_sync_type": MapFieldInfo( "Net Sync Type", int, 0, "Unknown. Related to online object synchronization.", ), "default_animation": MapFieldInfo( "Default Animation", int, # TODO: Animation 0, "Object animation ID to auto-play on map load, e.g. for different corpse poses.", ), "unk_x0e_x10": MapFieldInfo( "Unknown [0e-10]", int, 0, "Unknown.", ), "unk_x10_x14": MapFieldInfo( "Unknown [10-14]", int, 0, "Unknown.", ), } FIELD_ORDER = ( "model_name", "entity_id", "translate", "rotate", # "scale", "draw_parent_name", "draw_groups", # "display_groups", "break_term", "net_sync_type", "default_animation", # "unk_x0e_x10", # "unk_x10_x14", ) + MSBPart.LIGHTING_FIELD_ORDER + ( "is_shadow_source", "is_shadow_destination", "is_shadow_only", "draw_by_reflect_cam", "draw_only_reflect_cam", # "use_depth_bias_float", "disable_point_light_effect", ) break_term: int net_sync_type: int default_animation: int unk_x0e_x10: int unk_x10_x14: int def __init__(self, source=None, **kwargs): if source is None: # Set some defaults. kwargs.setdefault("is_shadow_source", True) kwargs.setdefault("is_shadow_destination", True) kwargs.setdefault("draw_by_reflect_cam", True) super().__init__(source=source, **kwargs)
class MSBPart(_BaseMSBPart): PART_HEADER_STRUCT = BinaryStruct( ("__name_offset", "i"), ("__part_type", "i"), ("_part_type_index", "i"), ("_model_index", "I"), ("__sib_path_offset", "i"), ("translate", "3f"), ("rotate", "3f"), ("scale", "3f"), ("__draw_groups", "4I"), ("__display_groups", "4I"), ("__base_data_offset", "i"), ("__type_data_offset", "i"), "4x", ) PART_BASE_DATA_STRUCT = BinaryStruct( ("entity_id", "i"), ("ambient_light_id", "b"), ("fog_id", "b"), ("scattered_light_id", "b"), ("lens_flare_id", "b"), ("shadow_id", "b"), ("dof_id", "b"), ("tone_map_id", "b"), ("tone_correction_id", "b"), ("point_light_id", "b"), ("lod_id", "b"), "x", ("is_shadow_source", "?"), ("is_shadow_destination", "?"), ("is_shadow_only", "?"), ("draw_by_reflect_cam", "?"), ("draw_only_reflect_cam", "?"), ("use_depth_bias_float", "?"), ("disable_point_light_effect", "?"), "2x", ) NAME_ENCODING = "shift-jis" FLAG_SET_SIZE = 128 FIELD_INFO = _BaseMSBPart.FIELD_INFO | { "draw_groups": MapFieldInfo( "Draw Groups", list, set(range(FLAG_SET_SIZE)), "Draw groups of part. This part will be drawn when the corresponding display group is active.", ), "display_groups": MapFieldInfo( "Display Groups", list, set(range(FLAG_SET_SIZE)), "Display groups are present in all MSB Parts, but only function for collisions.", ), "ambient_light_id": MapFieldInfo( "Ambient Light ID", AmbientLightParam, 0, "ID of Ambient Light parameter to use from this map's lighting parameters (DrawParam).", ), "fog_id": MapFieldInfo( "Fog ID", FogParam, 0, "ID of Fog parameter to use from this map's lighting parameters (DrawParam).", ), "scattered_light_id": MapFieldInfo( "Scattered Light ID", ScatteredLightParam, 0, "ID of Scattered Light parameter to use from this map's lighting parameters (DrawParam).", ), "lens_flare_id": MapFieldInfo( "Lens Flare ID", LensFlareParam, 0, "ID of Lens Flare parameter (both types) to use from this map's lighting parameters (DrawParam).", ), "shadow_id": MapFieldInfo( "Shadow ID", ShadowParam, 0, "ID of Shadow parameter to use from this map's lighting parameters (DrawParam).", ), "dof_id": MapFieldInfo( "Depth of Field ID", DepthOfFieldParam, 0, "ID of Depth Of Field ID parameter to use from this map's lighting parameters (DrawParam).", ), "tone_map_id": MapFieldInfo( "Tone Map ID", ToneMappingParam, 0, "ID of Tone Map parameter to use from this map's lighting parameters (DrawParam).", ), "point_light_id": MapFieldInfo( "Point Light ID", PointLightParam, 0, "ID of Point Light parameter to use from this map's lighting parameters (DrawParam).", ), "tone_correction_id": MapFieldInfo( "Tone Correction ID", ToneCorrectionParam, 0, "ID of Tone Correction parameter to use from this map's lighting parameters (DrawParam).", ), "lod_id": MapFieldInfo( "Level of Detail ID", int, 0, # only ever 0 or -1, seemingly at random "Level of Detail (LoD) parameter. Always -1 or 0, probably unused.", ), "is_shadow_source": MapFieldInfo( "Casts Shadow", bool, False, "If True, this entity will cast dynamic shadows.", ), "is_shadow_destination": MapFieldInfo( "Receives Shadow", bool, False, "If True, this entity can have dynamic shadows cast onto it.", ), "is_shadow_only": MapFieldInfo( "Only Casts Shadow", bool, False, "If True, this entity only casts shadows.", ), "draw_by_reflect_cam": MapFieldInfo( "Is Reflected", bool, False, "If True, this entity will be reflected in water, etc.", ), "draw_only_reflect_cam": MapFieldInfo( "Is Only Reflected", bool, False, "If True, this entity will only be drawn in reflections in water, etc.", ), "use_depth_bias_float": MapFieldInfo( "Use Depth Bias Float", bool, False, "Unknown.", ), "disable_point_light_effect": MapFieldInfo( "Ignore Point Lights", bool, False, "If True, this entity will not be illuminated by point lights (I think).", ), } LIGHTING_FIELD_ORDER = ( "ambient_light_id", "fog_id", "scattered_light_id", "lens_flare_id", "shadow_id", "dof_id", "tone_map_id", "point_light_id", "tone_correction_id", # "lod_id", ) entity_id: int translate: Vector3 rotate: Vector3 scale: Vector3 draw_groups: set[int] display_groups: set[int] ambient_light_id: int fog_id: int scattered_light_id: int lens_flare_id: int shadow_id: int dof_id: int tone_map_id: int point_light_id: int tone_correction_id: int lod_id: int is_shadow_source: bool is_shadow_destination: bool is_shadow_only: bool draw_by_reflect_cam: bool draw_only_reflect_cam: bool use_depth_bias_float: bool disable_point_light_effect: bool def unpack(self, msb_buffer): part_offset = msb_buffer.tell() header = self.PART_HEADER_STRUCT.unpack(msb_buffer) if header["__part_type"] != self.ENTRY_SUBTYPE: raise ValueError(f"Unexpected part type enum {header['part_type']} for class {self.__class__.__name__}.") self._model_index = header["_model_index"] self._part_type_index = header["_part_type_index"] for transform in ("translate", "rotate", "scale"): setattr(self, transform, Vector3(getattr(header, transform))) self._draw_groups = int_group_to_bit_set(header["__draw_groups"], assert_size=4) self._display_groups = int_group_to_bit_set(header["__display_groups"], assert_size=4) self.name = read_chars_from_buffer( msb_buffer, offset=part_offset + header["__name_offset"], encoding=self.NAME_ENCODING ) self.sib_path = read_chars_from_buffer( msb_buffer, offset=part_offset + header["__sib_path_offset"], encoding=self.NAME_ENCODING ) msb_buffer.seek(part_offset + header["__base_data_offset"]) base_data = self.PART_BASE_DATA_STRUCT.unpack(msb_buffer) self.set(**base_data) msb_buffer.seek(part_offset + header["__type_data_offset"]) self.unpack_type_data(msb_buffer) def pack(self): """Pack to bytes, presumably as part of a full `MSB` pack.""" # Validate draw/display groups before doing any real work. draw_groups = bit_set_to_int_group(self._draw_groups, group_size=4) display_groups = bit_set_to_int_group(self._display_groups, group_size=4) name_offset = self.PART_HEADER_STRUCT.size packed_name = self.get_name_to_pack().encode(self.NAME_ENCODING) + b"\0" # Name not padded on its own. sib_path_offset = name_offset + len(packed_name) packed_sib_path = self.sib_path.encode(self.NAME_ENCODING) + b"\0" if self.sib_path else b"\0" * 6 while len(packed_name + packed_sib_path) % 4 != 0: packed_sib_path += b"\0" 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() try: packed_header = self.PART_HEADER_STRUCT.pack( __name_offset=name_offset, __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, __base_data_offset=base_data_offset, __type_data_offset=type_data_offset, ) except struct.error: raise MapError(f"Could not pack header data of `{self.__class__.__name__}` '{self.name}'. See traceback.") return packed_header + packed_name + packed_sib_path + packed_base_data + packed_type_data
class MSBNavigationEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.Navigation EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("_navigation_region_index", "i"), "12x", ) FIELD_INFO = MSBEvent.FIELD_INFO | { "entity_id": MapFieldInfo( "Entity ID", int, -1, "Entity ID used to refer to the Navigation event in other game files.", ), "navigation_region_name": MapFieldInfo( "Navmesh Region", Region, None, "Region to which Navigation event is attached, which encloses faces of one or more Navmesh parts.", ), } FIELD_ORDER = ( "entity_id", "navigation_region_name", ) REFERENCE_FIELDS = { "parts": ["base_part_name"], "regions": ["base_region_name", "navigation_region_name"] } navigation_region_name: tp.Optional[str] def __init__(self, source=None, **kwargs): self._navigation_region_index = None self._navigation_region_name = None super().__init__(source=source, **kwargs) @property def navigation_region_name(self): return self._navigation_region_name @navigation_region_name.setter def navigation_region_name(self, value: tp.Union[None, str]): if isinstance(value, str): self._navigation_region_name = value if value else None elif value is None: self._navigation_region_name = None else: raise TypeError( f"`navigation_region_name` must be a string or `None`, not {value}." ) def set_indices(self, indices: EventsIndicesData): super().set_indices(indices) if self.navigation_region_name: self._navigation_region_index = indices.region_indices[ self._navigation_region_name] else: self._navigation_region_index = -1 def set_names(self, names: EventsNamesData): super().set_names(names) if self._navigation_region_index != -1: self._navigation_region_name = names.region_names[ self._navigation_region_index] else: self._navigation_region_name = None
class MSBTreasureEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.Treasure EVENT_TYPE_DATA_STRUCT = BinaryStruct( "8x", ("_treasure_part_index", "i"), "4x", ("item_lot_1", "i"), ("item_lot_2", "i"), ("item_lot_3", "i"), ("unknown_x1c_x20", "i"), ("unknown_x20_x24", "i"), ("unknown_x24_x28", "i"), ("unknown_x28_x2c", "i"), ("unknown_x2c_x30", "i"), ("unknown_x30_x34", "i"), ("unknown_x34_x38", "i"), ("unknown_x38_x3c", "i"), ("unknown_x3c_x40", "i"), ("is_in_chest", "?"), ("is_hidden", "?"), ("unknown_x42_x44", "h"), ("unknown_x44_x48", "i"), ("unknown_x48_x4c", "i"), "4x", ) FIELD_INFO = { "treasure_part_name": MapFieldInfo( "Treasure Object", Object, None, "Object on which treasure will appear (usually a corpse or chest).", ), "item_lot_1": MapFieldInfo( "Item Lot 1", ItemLotParam, -1, "First item lot of treasure. (Note that the item lots that are +1 to +5 from this ID will also be " "awarded.)", ), "item_lot_2": MapFieldInfo( "Item Lot 2", ItemLotParam, -1, "Second item lot of treasure. (Note that the item lots that are +1 to +5 from this ID will also be " "awarded.)", ), "item_lot_3": MapFieldInfo( "Item Lot 3", ItemLotParam, -1, "Third item lot of treasure. (Note that the item lots that are +1 to +5 from this ID will also be " "awarded.)", ), "is_in_chest": MapFieldInfo( "Is In Chest", bool, False, "Indicates if this treasure is inside a chest (affects appearance).", # TODO: effect? ), "is_hidden": MapFieldInfo( "Is Hidden", bool, False, "If True, this treasure will start disabled and will need to be enabled manually with an event script.", ), "unknown_x1c_x20": MapFieldInfo( "Unknown [1c-20]", int, -1, "Unknown integer.", ), "unknown_x20_x24": MapFieldInfo( "Unknown [20-24]", int, -1, "Unknown integer.", ), "unknown_x24_x28": MapFieldInfo( "Unknown [24-28]", int, -1, "Unknown integer.", ), "unknown_x28_x2c": MapFieldInfo( "Unknown [28-2c]", int, -1, "Unknown integer.", ), "unknown_x2c_x30": MapFieldInfo( "Unknown [2c-30]", int, -1, "Unknown integer.", ), "unknown_x30_x34": MapFieldInfo( "Unknown [30-34]", int, -1, "Unknown integer.", ), "unknown_x34_x38": MapFieldInfo( "Unknown [34-38]", int, -1, "Unknown integer.", ), "unknown_x38_x3c": MapFieldInfo( "Unknown [38-3c]", int, 0, "Unknown integer.", ), "unknown_x3c_x40": MapFieldInfo( "Unknown [3c-40]", int, -1, "Unknown integer.", ), "unknown_x42_x44": MapFieldInfo( "Unknown [42-44]", int, 0, "Unknown short.", ), "unknown_x44_x48": MapFieldInfo( "Unknown [44-48]", int, -1, "Unknown integer.", ), "unknown_x48_x4c": MapFieldInfo( "Unknown [48-4c]", int, -1, "Unknown integer.", ), } FIELD_ORDER = ( "treasure_part_name", "item_lot_1", "item_lot_2", "item_lot_3", "is_in_chest", "is_hidden", "unknown_x1c_x20", "unknown_x20_x24", "unknown_x24_x28", "unknown_x28_x2c", "unknown_x2c_x30", "unknown_x30_x34", "unknown_x34_x38", "unknown_x38_x3c", "unknown_x3c_x40", "unknown_x42_x44", "unknown_x44_x48", "unknown_x48_x4c", ) REFERENCE_FIELDS = { "parts": ["base_part_name", "treasure_part_name"], "regions": ["base_region_name"] } treasure_part_name: tp.Optional[str] item_lot_1: int item_lot_2: int item_lot_3: int is_in_chest: bool is_hidden: bool unknown_x1c_x20: int unknown_x20_x24: int unknown_x24_x28: int unknown_x28_x2c: int unknown_x2c_x30: int unknown_x30_x34: int unknown_x34_x38: int unknown_x38_x3c: int unknown_x3c_x40: int unknown_x42_x44: int unknown_x44_x48: int unknown_x48_x4c: int def __init__(self, source=None, **kwargs): self._treasure_part_index = None self._treasure_part_name = None self.item_lot_1 = -1 self.item_lot_2 = -1 self.item_lot_3 = -1 self.unknown_x1c_x20 = -1 self.unknown_x20_x24 = -1 self.unknown_x24_x28 = -1 self.unknown_x28_x2c = -1 self.unknown_x2c_x30 = -1 self.unknown_x30_x34 = -1 self.unknown_x34_x38 = -1 self.unknown_x38_x3c = -1 self.unknown_x3c_x40 = -1 self.unknown_x42_x44 = -1 self.unknown_x44_x48 = -1 self.unknown_x48_x4c = -1 super().__init__(source=source, **kwargs) @property def treasure_part_name(self): return self._treasure_part_name @treasure_part_name.setter def treasure_part_name(self, value: tp.Union[None, str]): if isinstance(value, str): self._treasure_part_name = value if value else None elif value is None: self._treasure_part_name = None else: raise TypeError( f"`treasure_part_name` must be a string or `None`, not {value}." ) def set_indices(self, indices: EventsIndicesData): super().set_indices(indices) self._treasure_part_index = indices.part_indices[ self._treasure_part_name] if self.treasure_part_name else -1 def set_names(self, names: EventsNamesData): super().set_names(names) self._treasure_part_name = (names.part_names[self._treasure_part_index] if self._treasure_part_index != -1 else None)
class MSBSpawnPointEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.SpawnPoint EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("_spawn_point_region_index", "i"), "12x", ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_part_name": MapFieldInfo( "Draw Parent", MapPart, None, "Some Spawn Points use this; unclear what it does, but it is presumably the Collision of the Spawn Point.", ), "entity_id": MapFieldInfo( "Entity ID", int, -1, "Entity ID used to refer to the Spawn Point in other game files.", ), "spawn_point_region_name": MapFieldInfo( "Spawn Point Region", Region, None, "Region where player will spawn when registered to this spawn point.", ), } FIELD_ORDER = ( "entity_id", "base_part_name", "spawn_point_region_name", ) REFERENCE_FIELDS = { "parts": ["base_part_name"], "regions": ["base_region_name", "spawn_point_region_name"] } spawn_point_region_name: tp.Optional[str] def __init__(self, source=None, **kwargs): self._spawn_point_region_index = None self._spawn_point_region_name = None super().__init__(source=source, **kwargs) @property def spawn_point_region_name(self): return self._spawn_point_region_name @spawn_point_region_name.setter def spawn_point_region_name(self, value: tp.Union[None, str]): if isinstance(value, str): self._spawn_point_region_name = value if value else None elif value is None: self._spawn_point_region_name = None else: raise TypeError( f"`spawn_point_region_name` must be a string or `None`, not {value}." ) def set_indices(self, indices: EventsIndicesData): super().set_indices(indices) if self.spawn_point_region_name: self._spawn_point_region_index = indices.region_indices[ self._spawn_point_region_name] else: self._spawn_point_region_index = -1 def set_names(self, names: EventsNamesData): super().set_names(names) if self._spawn_point_region_index != -1: self._spawn_point_region_name = names.region_names[ self._spawn_point_region_index] else: self._spawn_point_region_name = None
class MSBCollision(_BaseMSBCollision, MSBPartSceneGParam): PART_TYPE_DATA_STRUCT = BinaryStruct( ("hit_filter_id", "B"), ("sound_space_type", "B"), ("_environment_event_index", "h"), ("reflect_plane_height", "f"), ("__area_name_id", "h"), ("starts_disabled", "?"), ("unk_x0b_x0c", "B"), ("attached_bonfire", "i"), ("__play_region_id", "i"), ("camera_1_id", "h"), ("camera_2_id", "h"), ) FIELD_INFO = MSBPartSceneGParam.FIELD_INFO | _BaseMSBCollision.FIELD_INFO | { "unk_x0b_x0c": MapFieldInfo( "Unknown [0b-0c]", int, 0, "Unknown. Almost always zero (in DS1 at least).", ), "attached_bonfire": MapFieldInfo( "Attached Lantern", int, -1, "If this is set to a lantern entity ID, that lantern will be unusable if any living enemy characters are " "on this collision. Note that this also checks for enemies that are disabled by events.", ), # TODO: Confirm Bloodborne uses the same signed system for Stable Footing Flag. } FIELD_ORDER = ( "model_name", "entity_id", "translate", "rotate", "draw_groups", "display_groups", "backread_groups", "hit_filter_id", "sound_space_type", "environment_event_name", "reflect_plane_height", "area_name_id", "force_area_banner", "starts_disabled", "unk_x0b_x0c", "attached_bonfire", "play_region_id", "stable_footing_flag", "camera_1_id", "camera_2_id", ) + MSBPartSceneGParam.FIELD_ORDER unk_x0b_x0c: int def pack_type_data(self): """Pack to bytes, presumably as part of a full `MSB` pack.""" if self.area_name_id == -1 and not self._force_area_banner: raise InvalidFieldValueError("`force_area_banner` must be enabled if `area_name_id == -1` (default).") signed_area_name_id = self.area_name_id * (-1 if self.area_name_id >= 0 and self._force_area_banner else 1) if self._stable_footing_flag != 0: play_region_id = -self._stable_footing_flag - 10 else: play_region_id = self._play_region_id return self.PART_TYPE_DATA_STRUCT.pack( hit_filter_id=self.hit_filter_id, sound_space_type=self.sound_space_type, _environment_event_index=self._environment_event_index, reflect_plane_height=self.reflect_plane_height, __area_name_id=signed_area_name_id, starts_disabled=self.starts_disabled, unk_x0b_x0c=self.unk_x0b_x0c, attached_bonfire=self.attached_bonfire, __play_region_id=play_region_id, camera_1_id=self.camera_1_id, camera_2_id=self.camera_2_id, )
class MSBObject(_BaseMSBObject, MSBPartGParam): """Interactable object. Note that Bloodborne has six-digit model IDs for Objects.""" PART_TYPE_DATA_STRUCT = BinaryStruct( "4x", ("_draw_parent_index", "i"), ("break_term", "b"), ("net_sync_type", "b"), ("collision_hit_filter", "?"), ("set_main_object_structure_bools", "?"), ("animation_ids", "4h"), ("model_vfx_param_id_offsets", "4h"), ) FIELD_INFO = MSBPartGParam.FIELD_INFO | _BaseMSBObject.FIELD_INFO | { "break_term": MapFieldInfo( "Break Term", int, 0, "Unknown. Related to object breakage.", ), "net_sync_type": MapFieldInfo( "Net Sync Type", int, 0, "Unknown. Related to online object synchronization.", ), "collision_hit_filter": MapFieldInfo( "Collision Hit Filter", bool, False, "Unclear what this does when enabled.", ), "set_main_object_structure_bools": MapFieldInfo( "Set Main Object Structure Bools", bool, False, "Unknown.", ), "animation_ids": MapFieldInfo( "Animation IDs", list, [-1, -1, -1, -1], "Default animation IDs for object (e.g. corpse poses). Only the first is used, according to Pav.", ), "model_vfx_param_id_offsets": MapFieldInfo( "Model VFX Param ID Offsets", list, [0, 0, 0, 0], "Offsets for model VFX param IDs. Only the first is used, according to Pav.", ), } FIELD_ORDER = _BaseMSBObject.FIELD_ORDER + ( # "backread_groups", "break_term", "net_sync_type", "collision_hit_filter", "set_main_object_structure_bools", "animation_ids", "model_vfx_param_id_offsets", ) + MSBPartGParam.FIELD_ORDER break_term: int net_sync_type: int collision_hit_filter: bool set_main_object_structure_bools: bool animation_ids: list[int, int, int, int] model_vfx_param_id_offsets: list[int, int, int, int]
class MSBNPCInvasionEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.NPCInvasion EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("host_entity_id", "i"), ("invasion_flag_id", "i"), ("_spawn_point_region_index", "i"), "4x", ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_region_name": MapFieldInfo( "Invasion Region", Region, None, "Region in which NPC Invasion event can be triggered (e.g. with Black Eye Orb).", ), "host_entity_id": MapFieldInfo( "Host Entity ID", int, -1, "Entity ID of NPC character to be invaded.", ), "invasion_flag_id": MapFieldInfo( "Invasion Flag", Flag, -1, "Flag that is enabled while the invasion is active, which should trigger changes to the world.", ), "spawn_point_region_name": MapFieldInfo( "Spawn Point Region", Region, None, "Region where player will spawn during invasion event.", ), } FIELD_ORDER = ( "base_region_name", "host_entity_id", "invasion_flag_id", "spawn_point_region_name", ) REFERENCE_FIELDS = { "parts": ["base_part_name"], "regions": ["base_region_name", "spawn_point_region_name"] } host_entity_id: int invasion_flag_id: int spawn_point_region_name: tp.Optional[str] def __init__(self, source=None, **kwargs): self._spawn_point_region_index = None self._spawn_point_region_name = None super().__init__(source=source, **kwargs) @property def spawn_point_region_name(self): return self._spawn_point_region_name @spawn_point_region_name.setter def spawn_point_region_name(self, value: tp.Union[None, str]): if isinstance(value, str): self._spawn_point_region_name = value if value else None elif value is None: self._spawn_point_region_name = None else: raise TypeError( f"`spawn_point_region_name` must be a string or `None`, not {value}." ) def set_indices(self, indices: EventsIndicesData): super().set_indices(indices) if self.spawn_point_region_name: self._spawn_point_region_index = indices.region_indices[ self._spawn_point_region_name] else: self._spawn_point_region_index = -1 def set_names(self, names: EventsNamesData): super().set_names(names) if self._spawn_point_region_index != -1: self._spawn_point_region_name = names.region_names[ self._spawn_point_region_index] else: self._spawn_point_region_name = None
class MSBCollision(_BaseMSBCollision, MSBPart): """Dark Souls collision includes navmesh groups and Vagrants.""" PART_TYPE_DATA_STRUCT = BinaryStruct( ("hit_filter_id", "B"), ("sound_space_type", "B"), ("_environment_event_index", "h"), ("reflect_plane_height", "f"), ("__navmesh_groups", "4I"), ("vagrant_entity_ids", "3i"), ("__area_name_id", "h"), ("starts_disabled", "?"), ("unk_x27_x28", "B"), ("attached_bonfire", "i"), ("minus_ones", "3i", [-1, -1, -1]), # Never used. Possibly more bonfires? ("__play_region_id", "i"), ("camera_1_id", "h"), ("camera_2_id", "h"), "16x", ) FIELD_INFO = MSBPart.FIELD_INFO | _BaseMSBCollision.FIELD_INFO | { "navmesh_groups": MapFieldInfo( "Navmesh Groups", list, set(range(MSBPart.FLAG_SET_SIZE)), "Controls collision backread.", ), "vagrant_entity_ids": MapFieldInfo( "Vagrant Entity IDs", list, [-1, -1, -1], "Unknown.", ), "unk_x27_x28": MapFieldInfo( "Unknown [27-28]", int, 0, "Unknown. Almost always zero, but see e.g. Anor Londo spinning tower collision.", ), } FIELD_ORDER = ( "model_name", "entity_id", "translate", "rotate", # "scale", "draw_groups", "display_groups", "navmesh_groups", "hit_filter_id", "sound_space_type", "environment_event_name", "reflect_plane_height", "vagrant_entity_ids", "area_name_id", "force_area_banner", "starts_disabled", # "unk_x27_x28", "attached_bonfire", "play_region_id", "stable_footing_flag", "camera_1_id", "camera_2_id", ) + MSBPart.LIGHTING_FIELD_ORDER + ( "is_shadow_source", "is_shadow_destination", "is_shadow_only", "draw_by_reflect_cam", "draw_only_reflect_cam", # "use_depth_bias_float", "disable_point_light_effect", ) vagrant_entity_ids: list[int, int, int] unk_x27_x28: int def __init__(self, source=None, **kwargs): self._navmesh_groups = set() if source is None: kwargs.setdefault("is_shadow_source", True) kwargs.setdefault("is_shadow_destination", True) kwargs.setdefault("draw_by_reflect_cam", True) super().__init__(source=source, **kwargs) def unpack_type_data(self, msb_buffer): data = self.PART_TYPE_DATA_STRUCT.unpack(msb_buffer, exclude_asserted=True) self.set(**data) self._navmesh_groups = int_group_to_bit_set(data["__navmesh_groups"], assert_size=4) self.area_name_id = abs(data["__area_name_id"]) if data["__area_name_id"] != -1 else -1 self._force_area_banner = data["__area_name_id"] < 0 # Custom field. if data["__play_region_id"] > -10: self._play_region_id = data["__play_region_id"] self._stable_footing_flag = 0 else: self._play_region_id = 0 self._stable_footing_flag = -data["__play_region_id"] - 10 def pack_type_data(self): """Pack to bytes, presumably as part of a full `MSB` pack.""" # Validate navmesh groups before doing any real work. navmesh_groups = bit_set_to_int_group(self._navmesh_groups, group_size=4) if self.area_name_id == -1 and not self._force_area_banner: raise InvalidFieldValueError("`force_area_banner` must be enabled if `area_name_id == -1` (default).") signed_area_name_id = self.area_name_id * (-1 if self.area_name_id >= 0 and self._force_area_banner else 1) if self._stable_footing_flag != 0: play_region_id = -self._stable_footing_flag - 10 else: play_region_id = self._play_region_id return self.PART_TYPE_DATA_STRUCT.pack( hit_filter_id=self.hit_filter_id, sound_space_type=self.sound_space_type, _environment_event_index=self._environment_event_index, reflect_plane_height=self.reflect_plane_height, __navmesh_groups=navmesh_groups, vagrant_entity_ids=self.vagrant_entity_ids, __area_name_id=signed_area_name_id, starts_disabled=self.starts_disabled, unk_x27_x28=self.unk_x27_x28, attached_bonfire=self.attached_bonfire, __play_region_id=play_region_id, camera_1_id=self.camera_1_id, camera_2_id=self.camera_2_id, ) @property def navmesh_groups(self): return self._navmesh_groups @navmesh_groups.setter def navmesh_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._navmesh_groups = set() return try: navmesh_groups = set(value) except (TypeError, ValueError): raise TypeError( "Navmesh groups must be a set, sequence, `None`, 'None', or ''. Or use `set` methods like `.add()`." ) for i in navmesh_groups: if not isinstance(i, int) and 0 <= i < self.FLAG_SET_SIZE: raise ValueError(f"Invalid navmesh group: {i}. Must be 0 <= i < {self.FLAG_SET_SIZE}.") self._navmesh_groups = navmesh_groups
class MSBWindEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.Wind EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("wind_vector_min", "3f"), ("unk_x04_x08", "f"), ("wind_vector_max", "3f"), ("unk_x0c_x10", "f"), ("wind_swing_cycles", "4f"), ("wind_swing_powers", "4f"), ) FIELD_INFO = MSBEvent.FIELD_INFO | { "wind_vector_min": MapFieldInfo( "Wind Vector Min", Vector3, Vector3.zero(), "Wind vector minimum.", ), "unk_x04_x08": MapFieldInfo( "Unknown [04-08]", float, 0.0, "Unknown Wind parameter (floating-point number).", ), "wind_vector_max": MapFieldInfo( "Wind Vector Max", Vector3, Vector3.zero(), "Wind vector maximum.", ), "unk_x0c_x10": MapFieldInfo( "Unknown [0c-10]", float, 0.0, "Unknown Wind parameter (floating-point number).", ), "wind_swing_cycles": MapFieldInfo( "Wind Swing Cycles", list, [0.0, 0.0, 0.0, 0.0], "Wind swing cycles (four values).", ), "wind_swing_powers": MapFieldInfo( "Wind Swing Powers", list, [0.0, 0.0, 0.0, 0.0], "Wind swing powers (four values).", ), } FIELD_ORDER = ( "wind_vector_min", "unk_x04_x08", "wind_vector_max", "unk_x0c_x10", "wind_swing_cycles", "wind_swing_powers", ) unk_x04_x08: float unk_x0c_x10: float def __init__(self, source=None, **kwargs): """Most of these floats have been mapped out.""" self._wind_vector_min = Vector3.zero() self._wind_vector_max = Vector3.zero() self._wind_swing_cycles = [0.0, 0.0, 0.0, 0.0] self._wind_swing_powers = [0.0, 0.0, 0.0, 0.0] super().__init__(source=source, **kwargs) @property def wind_vector_min(self): return self._wind_vector_min @wind_vector_min.setter def wind_vector_min(self, value): self._wind_vector_min = Vector3(value) @property def wind_vector_max(self): return self._wind_vector_max @wind_vector_max.setter def wind_vector_max(self, value): self._wind_vector_max = Vector3(value) @property def wind_swing_cycles(self): return self._wind_swing_cycles @wind_swing_cycles.setter def wind_swing_cycles(self, value): try: value = list(value) if not len(value) == 4 or not all( isinstance(v, (int, float)) for v in value): raise ValueError except (TypeError, ValueError): raise ValueError( f"`wind_swing_cycles` must be a sequence of four numbers.") self._wind_swing_cycles = value @property def wind_swing_powers(self): return self._wind_swing_powers @wind_swing_powers.setter def wind_swing_powers(self, value): try: value = list(value) if not len(value) == 4 or not all( isinstance(v, (int, float)) for v in value): raise ValueError except (TypeError, ValueError): raise ValueError( f"`wind_swing_powers` must be a sequence of four numbers.") self._wind_swing_powers = value
class MSBTreasureEvent(_BaseMSBTreasureEvent, MSBEvent): EVENT_TYPE_DATA_STRUCT = BinaryStruct( "8x", ("_treasure_part_index", "i"), "4x", ("item_lot_1", "i"), ("item_lot_2", "i"), ("item_lot_3", "i"), ("unknown_x1c_x20", "i"), ("unknown_x20_x24", "i"), ("unknown_x24_x28", "i"), ("unknown_x28_x2c", "i"), ("unknown_x2c_x30", "i"), ("unknown_x30_x34", "i"), ("unknown_x34_x38", "i"), ("unknown_x38_x3c", "i"), ("unknown_x3c_x40", "i"), ("is_in_chest", "?"), ("is_hidden", "?"), ("unknown_x42_x44", "h"), ("unknown_x44_x48", "i"), ("unknown_x48_x4c", "i"), "4x", ) FIELD_INFO = _BaseMSBTreasureEvent.FIELD_INFO | { "unknown_x1c_x20": MapFieldInfo( "Unknown [1c-20]", int, -1, "Unknown integer.", ), "unknown_x20_x24": MapFieldInfo( "Unknown [20-24]", int, -1, "Unknown integer.", ), "unknown_x24_x28": MapFieldInfo( "Unknown [24-28]", int, -1, "Unknown integer.", ), "unknown_x28_x2c": MapFieldInfo( "Unknown [28-2c]", int, -1, "Unknown integer.", ), "unknown_x2c_x30": MapFieldInfo( "Unknown [2c-30]", int, -1, "Unknown integer.", ), "unknown_x30_x34": MapFieldInfo( "Unknown [30-34]", int, -1, "Unknown integer.", ), "unknown_x34_x38": MapFieldInfo( "Unknown [34-38]", int, -1, "Unknown integer.", ), "unknown_x38_x3c": MapFieldInfo( "Unknown [38-3c]", int, 0, "Unknown integer.", ), "unknown_x3c_x40": MapFieldInfo( "Unknown [3c-40]", int, -1, "Unknown integer.", ), "unknown_x42_x44": MapFieldInfo( "Unknown [42-44]", int, 0, "Unknown short.", ), "unknown_x44_x48": MapFieldInfo( "Unknown [44-48]", int, -1, "Unknown integer.", ), "unknown_x48_x4c": MapFieldInfo( "Unknown [48-4c]", int, -1, "Unknown integer.", ), } FIELD_ORDER = ( "treasure_part_name", "item_lot_1", "item_lot_2", "item_lot_3", "is_in_chest", "is_hidden", "unknown_x1c_x20", "unknown_x20_x24", "unknown_x24_x28", "unknown_x28_x2c", "unknown_x2c_x30", "unknown_x30_x34", "unknown_x34_x38", "unknown_x38_x3c", "unknown_x3c_x40", "unknown_x42_x44", "unknown_x44_x48", "unknown_x48_x4c", ) unknown_x1c_x20: int unknown_x20_x24: int unknown_x24_x28: int unknown_x28_x2c: int unknown_x2c_x30: int unknown_x30_x34: int unknown_x34_x38: int unknown_x38_x3c: int unknown_x3c_x40: int unknown_x42_x44: int unknown_x44_x48: int unknown_x48_x4c: int
class MSBSpawnerEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.Spawner EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("max_count", "B"), ("spawner_type", "b"), ("limit_count", "h"), ("min_spawner_count", "h"), ("max_spawner_count", "h"), ("min_interval", "f"), ("max_interval", "f"), ("initial_spawn_count", "i"), "28x", ("_spawn_region_indices", "4i"), ("_spawn_part_indices", "32i"), "64x", ) SPAWN_REGION_COUNT = 4 FIELD_INFO = MSBEvent.FIELD_INFO | { "entity_id": MapFieldInfo( "Entity ID", int, -1, "Entity ID used to refer to the Spawner in other game files.", ), "max_count": MapFieldInfo( "Max Count", int, 255, "Unsure; I suspect this is the total number of entities this spawner can produce.", ), "spawner_type": MapFieldInfo( "Spawner Type", int, 0, "Unsure what this enumeration is.", ), "limit_count": MapFieldInfo( "Limit Count", int, -1, "Unsure; I suspect this is the number of spawned entities that can be alive at once.", ), "min_spawner_count": MapFieldInfo( "Min Spawner Count", int, 1, "Unsure.", ), "max_spawner_count": MapFieldInfo( "Max Spawner Count", int, 1, "Unsure.", ), "min_interval": MapFieldInfo( "Min Interval", float, 1.0, "Minimum number of seconds between spawns.", ), "max_interval": MapFieldInfo( "Max Interval", float, 1.0, "Maximum number of seconds between spawns.", ), "initial_spawn_count": MapFieldInfo( "Initial Spawn Count", int, 1, "Unsure; I suspect this is the number of entities spawned immediately on map load.", ), "spawn_part_names": MapFieldInfo( "Spawn Characters", GameObjectSequence((Character, 32)), # TODO: ditto [None] * 32, "Entities that will be spawned at given regions.", ), "spawn_region_names": MapFieldInfo( "Spawn Regions", GameObjectSequence((Region, SPAWN_REGION_COUNT)), [None] * SPAWN_REGION_COUNT, "Regions where entities will be spawned.", ), } REFERENCE_FIELDS = { "parts": ["base_part_name", "spawn_part_names"], "regions": ["base_region_name", "spawn_region_names"] } FIELD_ORDER = ( "entity_id", "max_count", "spawner_type", "limit_count", "min_spawner_count", "max_spawner_count", "min_interval", "max_interval", "initial_spawn_count", "spawn_part_names", "spawn_region_names", ) spawn_region_names: list[tp.Union[str, None]] spawn_part_names: list[tp.Union[str, None]] def __init__(self, source=None, **kwargs): self._spawn_region_indices = [-1] * self.SPAWN_REGION_COUNT self._spawn_part_indices = [-1] * 32 super().__init__(source=source, **kwargs) def set_indices(self, indices: EventsIndicesData): super().set_indices(indices) self._spawn_region_indices = [ indices.region_indices[n] if n else -1 for n in self.spawn_region_names ] while len(self._spawn_region_indices) < self.SPAWN_REGION_COUNT: self._spawn_part_indices.append(-1) self._spawn_part_indices = [ indices.part_indices[n] if n else -1 for n in self.spawn_part_names ] while len(self._spawn_part_indices) < 32: self._spawn_part_indices.append(-1) def set_names(self, names: EventsNamesData): super().set_names(names) self.spawn_region_names = [ names.region_names[i] if i != -1 else None for i in self._spawn_region_indices ] while len(self.spawn_region_names) < self.SPAWN_REGION_COUNT: self.spawn_region_names.append(None) self.spawn_part_names = [ names.part_names[i] if i != -1 else None for i in self._spawn_part_indices ] while len(self.spawn_part_names) < 32: self.spawn_part_names.append(None)
class MSBWindVFXEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.WindVFX EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("vfx_id", "i"), ("_wind_region_index", "i"), ("unk_x08_x0c", "f"), "4x", ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_part_name": MapFieldInfo( "Draw Parent", MapPart, None, "VFX will be drawn if this parent (usually a Collision or Map Piece part) is drawn. Possibly unused.", ), "base_region_name": MapFieldInfo( "Base Region", Region, None, "Base event region. Probably unused with WindVFX (see Wind Region Name instead).", ), "wind_region_name": MapFieldInfo( "VFX Region", Region, None, "Region at or in which WindVFX appears.", ), "entity_id": MapFieldInfo( "Entity ID", int, -1, "Entity ID used to refer to this VFX in other game files. Possibly unused with WindVFX.", ), "vfx_id": MapFieldInfo( "VFX ID", int, -1, "Visual effect ID, which refers to a loaded VFX file.", ), "unk_x08_x0c": MapFieldInfo( "Unknown [08-0c]", float, 1.0, "Unknown floating-point number.", ), } FIELD_ORDER = ( "entity_id", "base_part_name", "base_region_name", "vfx_id", "unk_x08_x0c", ) wind_region_name: tp.Optional[str] vfx_id: int unk_x08_x0c: float def __init__(self, source, **kwargs): self._wind_region_index = None super().__init__(source=source, **kwargs) def set_indices(self, event_index, local_event_index, region_indices, part_indices): super().set_indices(event_index, local_event_index, region_indices, part_indices) self._wind_region_index = part_indices[ self.wind_region_name] if self.wind_region_name else -1 def set_names(self, region_names, part_names): super().set_names(region_names, part_names) self.wind_region_name = part_names[ self._wind_region_index] if self._wind_region_index != -1 else None
class MSBObjActEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.ObjAct EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("obj_act_entity_id", "i"), ("_obj_act_part_index", "i"), ("obj_act_param_id", "h"), ("obj_act_state", "B"), "x", ("obj_act_flag", "i"), ) FIELD_INFO = MSBEvent.FIELD_INFO | { "obj_act_entity_id": MapFieldInfo( "ObjAct Entity ID", int, -1, "ID that identifies this object activation event in event scripts.", ), "obj_act_part_name": MapFieldInfo( "Object", Object, None, "Object to which this object activation event is attached.", ), "obj_act_param_id": MapFieldInfo( "ObjAct Param", ObjActParam, -1, "Param entry containing information about this object activation event. If it is -1, it will " "default to the model ID of the object it is attached to.", ), "obj_act_state": MapFieldInfo( "ObjAct State", int, 0, "State of object activation. Known values include Default (0), Door (1), and Loop (2).", ), "obj_act_flag": MapFieldInfo( "ObjAct Flag", Flag, 0, "Flag that stores the persistent state (e.g. open/closed) of this object activation.", ), } FIELD_ORDER = ( "obj_act_entity_id", "obj_act_part_name", "obj_act_param_id", "obj_act_state", "obj_act_flag", ) REFERENCE_FIELDS = { "parts": ["base_part_name", "obj_act_part_name"], "regions": ["base_region_name"] } obj_act_entity_id: int obj_act_part_name: tp.Optional[str] obj_act_param_id: int obj_act_state: int obj_act_flag: int def __init__(self, source=None, **kwargs): self._obj_act_part_index = None self._obj_act_part_name = None super().__init__(source=source, **kwargs) @property def obj_act_part_name(self): return self._obj_act_part_name @obj_act_part_name.setter def obj_act_part_name(self, value: tp.Union[None, str]): if isinstance(value, str): self._obj_act_part_name = value if value else None elif value is None: self._obj_act_part_name = None else: raise TypeError( f"`obj_act_part_name` must be a string or `None`, not {value}." ) def set_indices(self, indices: EventsIndicesData): super().set_indices(indices) self._obj_act_part_index = indices.part_indices[ self._obj_act_part_name] if self.obj_act_part_name else -1 def set_names(self, names: EventsNamesData): super().set_names(names) self._obj_act_part_name = names.part_names[ self. _obj_act_part_index] if self._obj_act_part_index != -1 else None
class MSBPartSceneGParam(MSBPartGParam, abc.ABC): """Subclass of `MSBPart` that includes SceneGParam (and GParam) fields.""" PART_SCENE_GPARAM_STRUCT = BinaryStruct( ("sg_unk_x00_x04", "i"), ("sg_unk_x04_x08", "i"), ("sg_unk_x08_x0c", "i"), ("sg_unk_x0c_x10", "i"), ("sg_unk_x10_x14", "i"), ("sg_unk_x14_x18", "i"), "36x", ("event_ids", "4b"), ("sg_unk_x40_x44", "f"), "12x", ) FIELD_INFO = MSBPartGParam.FIELD_INFO | { "sg_unk_x00_x04": MapFieldInfo( "Unk SceneG [00-04]", int, 0, "Unknown integer.", ), "sg_unk_x04_x08": MapFieldInfo( "Unk SceneG [04-08]", int, 0, "Unknown integer.", ), "sg_unk_x08_x0c": MapFieldInfo( "Unk SceneG [08-0c]", int, 0, "Unknown integer.", ), "sg_unk_x0c_x10": MapFieldInfo( "Unk SceneG [0c-10]", int, 0, "Unknown integer.", ), "sg_unk_x10_x14": MapFieldInfo( "Unk SceneG [10-14]", int, 0, "Unknown integer.", ), "sg_unk_x14_x18": MapFieldInfo( "Unk SceneG [14-18]", int, 0, "Unknown integer.", ), "event_ids": MapFieldInfo( "Event IDs", list, [-1, -1, -1, -1], "List of four byte-sized event IDs.", ), "sg_unk_x40_x44": MapFieldInfo( "Unk SceneG [40-44]", float, 0.0, "Unknown floating-point number.", ), } FIELD_ORDER = MSBPartGParam.FIELD_ORDER + ( "sg_unk_x00_x04", "sg_unk_x04_x08", "sg_unk_x08_x0c", "sg_unk_x0c_x10", "sg_unk_x10_x14", "sg_unk_x14_x18", "event_ids", "sg_unk_x40_x44", ) sg_unk_x00_x04: int sg_unk_x04_x08: int sg_unk_x08_x0c: int sg_unk_x0c_x10: int sg_unk_x10_x14: int sg_unk_x14_x18: int event_ids: list[int, int, int, int] sg_unk_x40_x44: float def _unpack_scene_gparam_data(self, msb_reader: BinaryReader, part_offset, header): if header["__scene_gparam_data_offset"] == 0: raise ValueError(f"Zero SceneGParam offset found in SceneGParam-supporting part {self.name}.") msb_reader.seek(part_offset + header["__scene_gparam_data_offset"]) scene_gparam_data = msb_reader.unpack_struct(self.PART_SCENE_GPARAM_STRUCT) self.set(**scene_gparam_data) def _pack_scene_gparam_data(self): return self.PART_SCENE_GPARAM_STRUCT.pack(self)