class FMG0(BaseFMG): """Used only in Demon's Souls. Big-endian.""" HEADER_STRUCT = BinaryStruct( "x", ("big_endian", "?", True), ("version", "b", 0), "x", ("file_size", "i"), ("one", "b", 1), ("unknown1", "b", -1), "2x", ("range_count", "i"), ("string_count", "i"), ("string_offsets_offset", "i"), ("zero", "i", 0), byte_order=">", ) RANGE_STRUCT = BinaryStruct( ("first_index", "i"), ("first_id", "i"), ("last_id", "i"), byte_order=">", ) STRING_OFFSET_STRUCT = BinaryStruct( ("offset", "i"), byte_order=">", ) BIG_ENDIAN = True VERSION = 0 MAX_LINES = None # TODO: Don't know for Demon's Souls.
class FMG1(BaseFMG): """Used in Dark Souls (both versions) and Dark Souls 2.""" HEADER_STRUCT = BinaryStruct( "x", ("big_endian", "?", False), ("version", "b", 1), "x", ("file_size", "i"), ("one", "b", 1), ("unknown1", "b", 0), "2x", ("range_count", "i"), ("string_count", "i"), ("string_offsets_offset", "i"), ("zero", "i", 0), ) RANGE_STRUCT = BinaryStruct( ("first_index", "i"), ("first_id", "i"), ("last_id", "i"), ) STRING_OFFSET_STRUCT = BinaryStruct(("offset", "i"), ) BIG_ENDIAN = False VERSION = 1 MAX_LINES = 11 # TODO: Correct for DS1, not sure about DS2.
class FMG2(BaseFMG): """Used in Bloodborne, Dark Souls 3, and Sekiro.""" HEADER_STRUCT = BinaryStruct( "x", ("big_endian", "?", False), ("version", "b", 2), "x", ("file_size", "i"), ("one", "b", 1), ("unknown1", "b", 0), "2x", ("range_count", "i"), ("string_count", "i"), ("unknown2", "i", 255), ("string_offsets_offset", "q"), ("zero", "q", 0), ) RANGE_STRUCT = BinaryStruct( ("first_index", "i"), ("first_id", "i"), ("last_id", "i"), "4x", ) STRING_OFFSET_STRUCT = BinaryStruct(("offset", "q"), ) BIG_ENDIAN = False VERSION = 2 MAX_LINES = None # TODO: Don't know for Bloodborne or DS3.
class Command(_BaseCommand, abc.ABC): STRUCT = BinaryStruct( ("bank", "i"), # Values of 1, 5, 6, 7 have been encountered. ("index", "i"), ("args_offset", "q"), ("args_count", "q"), ) ARG_STRUCT = BinaryStruct(("arg_ezl_offset", "q"), ("arg_ezl_size", "q"), )
class MSBEntryList(_BaseMSBEntryList, abc.ABC): MAP_ENTITY_LIST_HEADER = BinaryStruct( ("version", "i", 3), ("entry_offset_count", "i"), ("name_offset", "q"), ) MAP_ENTITY_ENTRY_OFFSET = BinaryStruct(("entry_offset", "q"), ) MAP_ENTITY_LIST_TAIL = BinaryStruct(("next_entry_list_offset", "q"), ) NAME_ENCODING = "utf-16-le"
class Condition(_BaseCondition, abc.ABC): STRUCT = BinaryStruct( ("next_state_offset", "q"), ("pass_commands_offset", "q"), ("pass_commands_count", "q"), ("subcondition_pointers_offset", "q"), ("subcondition_pointers_count", "q"), ("test_ezl_offset", "q"), ("test_ezl_size", "q"), ) POINTER_STRUCT = BinaryStruct(("condition_offset", "q"), ) STATE_ID_STRUCT = BinaryStruct(("state_id", "q"), )
class ESD(DarkSoulsDSRType, _BaseESD, abc.ABC): DCX_TYPE = DCXType.DCX_DFLT_10000_24_9 EXTERNAL_HEADER_STRUCT = BinaryStruct( ("version", "4s", b"fsSL"), # Note specific case. ("one", "i", 1), ("game_version", "2i", [GAME_VERSION, GAME_VERSION]), ("table_size_offset", "i", 84), ("internal_data_size", "i"), # excludes this external header ("unk", "i", 6), ("internal_header_size", "i", 72), ("internal_header_count", "i", 1), ("state_machine_header_size", "i", 32), ("state_machine_count", "i"), ("state_size", "i", 72), ("state_count", "i"), ("condition_size", "i", 56), ("condition_count", "i"), ("command_size", "i", 24), ("command_count", "i"), ("command_arg_size", "i", 16), ("command_arg_count", "i"), ("condition_pointers_offset", "i"), ("condition_pointers_count", "i"), ("esd_name_offset_minus_1", "i"), # Not sure why this is minus 1. Use the internal header for the real offset. ("esd_name_length", "i"), # equal to internal header ("unk_offset_1", "i"), ("unk_size_1", "i", 0), ("unk_offset_2", "i"), ("unk_size_2", "i", 0), ) INTERNAL_HEADER_STRUCT = BinaryStruct( ("one", "i", 1), ("magic", "4i"), "4x", ("state_machine_headers_offset", "q", 72), ("state_machine_count", "q"), ("esd_name_offset", "q"), # accurate, unlike external header ("esd_name_length", "q"), # identical to external header entry; note this is only *half* the name byte size due to UTF-16 "16x", ) STATE_MACHINE_HEADER_STRUCT = BinaryStruct( ("state_machine_index", "q"), ("state_machine_offset", "q"), ("state_count", "q"), # number of states ("state_machine_offset_2", "q"), # duplicate )
class MSBEvent(BaseMSBEvent, abc.ABC): """MSB event entry in Bloodborne.""" EVENT_HEADER_STRUCT = BinaryStruct( ("__name_offset", "q"), ("_event_index", "i"), ("__event_type", "i"), ("_local_event_index", "i"), "4x", ("__base_data_offset", "q"), ("__type_data_offset", "q"), ) EVENT_TYPE_OFFSET = 12 EVENT_BASE_DATA_STRUCT = BinaryStruct( ("_base_part_index", "i"), ("_base_region_index", "i"), ("entity_id", "i"), ("_unknowns", "4b"), ) NAME_ENCODING = "utf-16-le" FIELD_INFO = BaseMSBEvent.FIELD_INFO | { "entity_id": MapFieldInfo( # definition overridden when used "Entity ID", int, -1, "Entity ID for event. Unused for this event type.", ), "base_part_name": MapFieldInfo( # definition overridden when used "Base Part Name", MapPart, None, "Map Part name related to event. Unused for this event type.", ), "base_region_name": MapFieldInfo( # definition overridden when used "Base Region Name", Region, None, "Map Region name related to event. Unused for this event type.", ), } def __init__(self, source=None, **kwargs): self._unknowns = [0, 0, 0, 0] super().__init__(source=source, **kwargs)
class MSBRegionCylinder(MSBRegion, abc.ABC): ENTRY_SUBTYPE = MSBRegionSubtype.Cylinder REGION_TYPE_DATA_STRUCT = BinaryStruct( ("radius", "f"), ("height", "f"), ) FIELD_INFO = MSBRegion.FIELD_INFO | { "radius": MapFieldInfo( "Radius", float, 1.0, "Radius (in xz-plane) of cylinder-shaped region.", ), "height": MapFieldInfo( "Height", float, 1.0, "Height (along y-axis) of cylinder-shaped region.", ), } FIELD_ORDER = ( "entity_id", "translate", "rotate", "radius", "height", ) radius: float height: float
class GXItem(BinaryObject): """Item that sets various material rendering properties.""" STRUCT = BinaryStruct( ("gx_id", "4s"), # actually "i" prior to Dark Souls 2 (0x20010) but always stored as bytes here for consistency ("unk_x04", "i"), ("__size", "i"), # includes header ) gx_id: bytes unk_x04: int data: bytes def unpack(self, reader: BinaryReader): gx_item = reader.unpack_struct(self.STRUCT) self.data = reader.read(gx_item.pop("__size") - self.STRUCT.size) self.set(**gx_item) def pack(self, writer: BinaryWriter): writer.pack_struct( self.STRUCT, self, __size=len(self.data) + self.STRUCT.size, ) writer.append(self.data) def __repr__(self): return f"GXItem(gx_id = {self.gx_id}, unk_x04 = {self.unk_x04}, data = {self.data})"
class BaseMSBRegionRect(BaseMSBRegion, abc.ABC): """Almost never used (no volume).""" REGION_TYPE_DATA_STRUCT = BinaryStruct( ("width", "f"), ("depth", "f"), ) FIELD_INFO = BaseMSBRegion.FIELD_INFO | { "width": MapFieldInfo( "Width", float, 1.0, "Width (along x-axis) of rectangle-shaped region.", ), "height": MapFieldInfo( "Height", float, 1.0, "Height (along y-axis) of rectangle-shaped region.", ), } FIELD_ORDER = ( "entity_id", "translate", "rotate", "width", "height", ) width: float height: float
class EventArg(_BaseEventArg): HEADER_STRUCT = BinaryStruct( ("instruction_line", "Q"), ("write_from_byte", "Q"), ("read_from_byte", "Q"), ("bytes_to_write", "Q"), )
class MSBPlayerStart(MSBPart, abc.ABC): """Starting point for the player character (e.g. a warp point). No additional data. Note that these are distinct from Spawn Point events, which are used much more often (e.g. bonfires). If the player's position within a given map is lost by the game, they will respawn at the Player Start with entity ID -1. """ ENTRY_SUBTYPE = MSBPartSubtype.PlayerStart PART_TYPE_DATA_STRUCT = BinaryStruct("16x", ) FIELD_INFO = { "model_name": MapFieldInfo( "Model Name", CharacterModel, "c0000", "Name of character model to use for this PlayerStart. This should always be c0000.", ), } FIELD_ORDER = ( "model_name", "entity_id", "translate", "rotate", ) def __init__(self, source=None, **kwargs): if source is None: kwargs.setdefault("model_name", "c0000") super().__init__(source=source, **kwargs)
class MSBMapPiece(MSBPart, abc.ABC): """Just a textured, visible mesh asset. Does not include any collision.""" ENTRY_SUBTYPE = MSBPartSubtype.MapPiece PART_TYPE_DATA_STRUCT = BinaryStruct("8x", ) FIELD_INFO = { "model_name": MapFieldInfo( "Model Name", MapPieceModel, None, "Name of map piece model to use for this map piece.", ), } FIELD_ORDER = ( "model_name", "entity_id", "translate", "rotate", "scale", "draw_groups", "display_groups", )
class Texture(BinaryObject): STRUCT = BinaryStruct( ("__path__z", "i"), ("__texture_type__z", "i"), ("scale", "2f"), ("unk_x10", "B"), # 0, 1, or 2 ("unk_x11", "?"), "2x", ("unk_x14", "f"), ("unk_x18", "f"), ("unk_x1C", "f"), ) path: str texture_type: str scale: Vector2 unk_x10: int unk_x11: bool unk_x14: float unk_x18: float unk_x1C: float DEFAULTS = { "scale": Vector2.ones(), "unk_x10": 1, "unk_x11": True, "unk_x14": 0.0, "unk_x18": 0.0, "unk_x1C": 0.0, } unpack = BinaryObject.default_unpack pack = BinaryObject.default_pack def set_name(self, name: str): """Set '.tga' name of `path`.""" name = name.removesuffix(".tga").removesuffix(".tpf") + ".tga" self.path = str(Path(self.path).with_name(name)) def __repr__(self): lines = [ f"Texture(", f" path = {repr(self.path)}", f" texture_type = {repr(self.texture_type)}", ] if self.scale != (1.0, 1.0): lines.append(f" scale = {self.scale}") if self.unk_x10 != 1: lines.append(f" unk_x10 = {self.unk_x10}") if not self.unk_x11: lines.append(f" unk_x11 = {self.unk_x11}") if self.unk_x14 != 0.0: lines.append(f" unk_x14 = {self.unk_x14}") if self.unk_x18 != 0.0: lines.append(f" unk_x18 = {self.unk_x18}") if self.unk_x1C != 0.0: lines.append(f" unk_x1C = {self.unk_x1C}") lines.append(")") return "\n".join(lines)
class MSBLightEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.Light EVENT_TYPE_DATA_STRUCT = BinaryStruct(("point_light_id", "i"), ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_part_name": MapFieldInfo( "Draw Parent", MapPart, None, "Light will be drawn as long as this parent (usually a Collision or Map Piece part) is drawn.", ), "base_region_name": MapFieldInfo( "Light Region", Region, None, "Region (usually a Point) at which Light appears.", ), "point_light_id": MapFieldInfo( "Point Light", PointLightParam, 0, "Point Light lighting parameter ID to use for this light.", ), } FIELD_ORDER = ( "base_part_name", "base_region_name", "point_light_id", )
def GET_HEADER_STRUCT(flags1: ParamFlags1, byte_order) -> BinaryStruct: fields = [ ("name_data_offset", "I"), "2x" if (flags1[0] and flags1.IntDataOffset) or flags1.LongDataOffset else ("row_data_offset", "H"), ("unknown", "H"), # 0 or 1 ("paramdef_data_version", "H"), ("row_count", "H"), ] if flags1.OffsetParam: fields += [ "4x", ("param_type_offset", "q"), "20x", ] else: fields.append(("param_type", "32j")) fields += [ ("big_endian", "b", 255 if byte_order == ">" else 0), ("flags1", "b"), ("flags2", "b"), ("paramdef_format_version", "b"), ] if flags1[0] and flags1.IntDataOffset: fields += [ ("row_data_offset", "i"), "12x", ] elif flags1.LongDataOffset: fields += [ ("row_data_offset", "q"), "8x", ] return BinaryStruct(*fields, byte_order=byte_order)
class MSBPlayerStart(MSBPart): ENTRY_SUBTYPE = MSBPartSubtype.PlayerStart PART_TYPE_DATA_STRUCT = BinaryStruct( "16x", ) FIELD_INFO = MSBPart.FIELD_INFO | { "model_name": MapFieldInfo( "Model Name", CharacterModel, "c0000", "Name of character model to use for this PlayerStart. This should always be c0000.", ), } FIELD_ORDER = ( "model_name", "entity_id", "translate", "rotate", ) + MSBPart.FIELD_ORDER def __init__(self, source=None, **kwargs): if source is None: # Set some different defaults. kwargs.setdefault("model_name", "c0000") super().__init__(source=source, **kwargs)
class MSBSpawnerEvent(_BaseMSBSpawnerEvent, MSBEvent): """Attributes are identical to base event, except there are eight spawn region slots.""" 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", "B"), "31x", ("_spawn_region_indices", "8i"), ("_spawn_part_indices", "32i"), "64x", ) SPAWN_REGION_COUNT = 8 FIELD_INFO = _BaseMSBSpawnerEvent.FIELD_INFO | { "spawn_region_names": MapFieldInfo( "Spawn Regions", GameObjectSequence((Region, SPAWN_REGION_COUNT)), [None] * SPAWN_REGION_COUNT, "Regions where entities will be spawned.", ), }
class ParamDefField(_BaseParamDefField): STRUCT = BinaryStruct( ("display_name", "64j"), ("display_type", "8j"), ("display_format", "8j"), # %i, %u, %d, etc. ("default", "f"), ("minimum", "f"), ("maximum", "f"), ("increment", "f"), ("edit_type", "i"), ("size", "i"), ("description_offset", "i"), ("internal_type", "32j"), # could be an enum name (see params.enums) ("name", "32j"), ("sort_id", "i"), ) def get_display_info(self, row: ParamRow): try: field_info = get_param_info_field(self.param_name, self.name) except ValueError: raise ValueError(f"No display information given for field '{self.name}'.") return field_info(row) def get_default_value(self): v = DEFAULTS[self.param_name].get(self.name, self.default) if self.bit_count == 1 and self.internal_type != "dummy8": return bool(v) elif self.internal_type not in {"f32", "f64"}: return int(v) return v
class FBX(GameFile): """Not actually a "game" file, but interchangeable with `FLVER` game files.""" MAX_VERSION = 7700 HEADER_STRUCT = BinaryStruct( ("magic", "23s", b"Kaydara FBX Binary \x00\x1a\x00"), ("version", "I"), ) # Constant 176 byte footer at the end of every FBX file. FOOTER = ( b"\xfa\xbc\xab\x09\xd0\xc8\xd4\x66\xb1\x76\xfb\x83\x1c\xf7\x26\x7e" + b"\0" * 20 + b"\xe8\x1c" + b"\0" * 122 + b"\xf8\x5a\x8c\x6a\xde\xf5\xd9\x7e\xec\xe9\x0c\xe3\x75\x8f\x29\x0b") def __init__( self, file_source: tp.Union[None, str, Path, bytes, io.BufferedIOBase, BinaryReader] = None, dcx_magic: tuple[int, int] = (), **kwargs, ): self.root_nodes = [ ] # type: tp.Union[list[FBXNode32, ...], list[FBXNode64, ...]] self.node_class = FBXNode64 # defaults to 64-bit offsets super().__init__(file_source, dcx_magic, **kwargs) def unpack(self, reader: BinaryReader, **kwargs): header = reader.unpack_struct(self.HEADER_STRUCT) self.node_class = FBXNode64 if header["version"] >= 7700 else FBXNode32 if header["version"] > self.MAX_VERSION: raise NotImplementedError( f"Cannot unpack FBX version {header['version']}. Last supported version is {self.MAX_VERSION}." ) start_offset = self.HEADER_STRUCT.size self.root_nodes = [] while 1: node = self.node_class(reader, start_offset=start_offset) start_offset += node.size if node.is_empty: break # empty node is not kept self.root_nodes.append(node) # Constant FBX footer is ignored. def pack(self): raise NotImplementedError("FBX cannot be packed yet.") def __getitem__(self, root_node_name: str): try: return next(n for n in self.root_nodes if n.name == root_node_name) except StopIteration: raise KeyError(f"No root FBX node named {root_node_name}.") def to_string(self) -> str: return "\n".join(node.to_string() for node in self.root_nodes)
class EventLayers(_BaseEventLayers): HEADER_STRUCT = BinaryStruct( ("two", "I", 2), ("event_layers", "I"), # 32-bit bit field ("zero", "Q", 0), ("minus_one", "q", -1), ("one", "Q", 1), )
class MSBPartGParam(MSBPart, abc.ABC): """Subclass of `MSBPart` that includes GParam fields.""" PART_GPARAM_STRUCT = BinaryStruct( ("light_set_id", "i"), ("fog_id", "i"), ("light_scattering_id", "i"), ("environment_map_id", "i"), "16x", ) FIELD_INFO = MSBPart.FIELD_INFO | { "light_set_id": MapFieldInfo( "Light Set ID", int, # TODO: GParam support. 0, "Light set GParam ID.", ), "fog_id": MapFieldInfo( "Fog Param ID", int, # TODO: GParam support. 0, "Fog GParam ID.", ), "light_scattering_id": MapFieldInfo( "Light Scattering ID", int, # TODO: GParam support. 0, "Light scattering GParam ID.", ), "environment_map_id": MapFieldInfo( "Environment Map ID", int, # TODO: GParam support. 0, "Environment map GParam ID.", ), } FIELD_ORDER = MSBPart.FIELD_ORDER + ( "light_set_id", "fog_id", "light_scattering_id", "environment_map_id", ) light_set_id: int fog_id: int light_scattering_id: int environment_map_id: int def _unpack_gparam_data(self, msb_reader: BinaryReader, part_offset, header): if header["__gparam_data_offset"] == 0: raise ValueError(f"Zero GParam offset found in GParam-supporting part {self.name}.") msb_reader.seek(part_offset + header["__gparam_data_offset"]) gparam_data = msb_reader.unpack_struct(self.PART_GPARAM_STRUCT) self.set(**gparam_data) def _pack_gparam_data(self): return self.PART_GPARAM_STRUCT.pack(self)
class ESD(_BaseESD, DarkSoulsPTDEType, abc.ABC): EXTERNAL_HEADER_STRUCT = BinaryStruct( ("version", "4s", b"fSSL"), # Note specific case. ("one", "i", 1), ("game_version", "2i", [GAME_VERSION, GAME_VERSION]), ("table_size_offset", "i", 84), ("internal_data_size", "i"), # excludes header size ("unk", "i", 6), ("internal_header_size", "i", 44), ("internal_header_count", "i", 1), ("state_machine_header_size", "i", 16), ("state_machine_count", "i"), ("state_size", "i", 36), ("state_count", "i"), ("condition_size", "i", 28), ("condition_count", "i"), ("command_size", "i", 16), ("command_count", "i"), ("command_arg_size", "i", 8), ("command_arg_count", "i"), ("condition_pointers_offset", "i"), ("condition_pointers_count", "i"), ("esd_name_offset_minus_1", "i"), ("esd_name_length", "i"), ("unk_offset_1", "i"), ("unk_size_1", "i", 0), ("unk_offset_2", "i"), ("unk_size_2", "i", 0), ) INTERNAL_HEADER_STRUCT = BinaryStruct( ("one", "i", 1), ("magic", "4i"), # TODO: constant within games, at least? ("state_machine_headers_offset", "i", 44), ("state_machine_count", "i"), ("esd_name_offset", "i"), # accurate, unlike external header ("esd_name_length", "i"), "8x", ) STATE_MACHINE_HEADER_STRUCT = BinaryStruct( ("state_machine_index", "i"), ("state_machine_offset", "i"), ("state_count", "i"), # number of states ("state_machine_offset_2", "i"), # duplicate )
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 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) if self.navigation_region_name: self._navigation_region_index = region_indices[ self.navigation_region_name] else: self._navigation_region_index = -1 def set_names(self, region_names, part_names): super().set_names(region_names, part_names) if self._navigation_region_index != -1: self.navigation_region_name = region_names[ self._navigation_region_index] else: self.navigation_region_name = None
class FBXNode64(FBXNodeBase): STRUCT = BinaryStruct( ("__end_offset", "q"), ("__property_count", "q"), ("__property_list_size", "q"), ("__name_length", "B"), ) children: list[FBXNode64]
class MSBEvent(_BaseMSBEvent, abc.ABC): EVENT_HEADER_STRUCT = BinaryStruct( ("__name_offset", "i"), ("_event_index", "i"), ("__event_type", "i"), ("_local_event_index", "i"), ("__base_data_offset", "i"), ("__type_data_offset", "i"), "4x", ) EVENT_BASE_DATA_STRUCT = BinaryStruct( ("_base_part_index", "i"), ("_base_region_index", "i"), ("entity_id", "i"), "4x", ) NAME_ENCODING = "shift_jis_2004"
class MSBNavmesh(_BaseMSBNavmesh, MSBPart): PART_TYPE_DATA_STRUCT = BinaryStruct( ("__navmesh_groups", "4I"), "16x", ) FIELD_INFO = MSBPart.FIELD_INFO | _BaseMSBNavmesh.FIELD_INFO | { "navmesh_groups": MapFieldInfo( "Navmesh Groups", list, set(range(MSBPart.FLAG_SET_SIZE)), "Controls collision backread.", ), } FIELD_ORDER = _BaseMSBNavmesh.FIELD_ORDER + ("navmesh_groups", ) def __init__(self, source=None, **kwargs): self._navmesh_groups = set() if source is None: kwargs.setdefault("is_shadow_source", True) super().__init__(source=source, **kwargs) def unpack_type_data(self, msb_reader: BinaryReader): data = msb_reader.unpack_struct(self.PART_TYPE_DATA_STRUCT, exclude_asserted=True) self._navmesh_groups = int_group_to_bit_set(data["__navmesh_groups"], assert_size=4) def pack_type_data(self): return self.PART_TYPE_DATA_STRUCT.pack( __navmesh_groups=bit_set_to_int_group(self._navmesh_groups, group_size=4), ) @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 MSBCharacter(_BaseMSBCharacter, MSBPart): PART_TYPE_DATA_STRUCT = BinaryStruct( "8x", ("ai_id", "i"), ("character_id", "i"), ("talk_id", "i"), ("patrol_type", "B"), "x", ("platoon_id", "H"), ("player_id", "i"), ("_draw_parent_index", "i"), "8x", ("_patrol_region_indices", "8h"), ("default_animation", "i"), ("damage_animation", "i"), ) FIELD_INFO = MSBPart.FIELD_INFO | _BaseMSBCharacter.FIELD_INFO | { "patrol_type": MapFieldInfo( "Patrol Type", int, 0, "Patrol behavior type. (Effects unknown.)", ), "platoon_id": MapFieldInfo( "Platoon ID", int, 0, "Unused 'platoon' ID value.", ), } FIELD_ORDER = _BaseMSBCharacter.FIELD_ORDER + ( "patrol_type", # "platoon_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", ) patrol_type: int platoon_id: int def __init__(self, source=None, **kwargs): if source is None: # Set some different 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 MSBVFXEvent(MSBEvent): ENTRY_SUBTYPE = MSBEventSubtype.VFX EVENT_TYPE_DATA_STRUCT = BinaryStruct( ("vfx_id", "i"), ("starts_disabled", "i"), # 32-bit bool ) FIELD_INFO = MSBEvent.FIELD_INFO | { "base_part_name": MapFieldInfo( "Draw Parent", MapPart, None, "VFX will be drawn as long as this parent (usually a Collision or Map Piece part) is drawn.", ), "base_region_name": MapFieldInfo( "VFX Region", Region, None, "Region (usually a Point) at which VFX appears.", ), "entity_id": MapFieldInfo( "Entity ID", int, -1, "Entity ID used to refer to this VFX in other game files.", ), "vfx_id": MapFieldInfo( "VFX ID", int, 0, "Visual effect ID, which refers to a loaded VFX file.", ), "starts_disabled": MapFieldInfo( "Starts Disabled", bool, False, "VFX will not be automatically created on map load (requires event script).", ) } FIELD_ORDER = ( "entity_id", "base_part_name", "base_region_name", "vfx_id", "starts_disabled", ) vfx_id: int starts_disabled: bool