def unpack(self, buffer, **kwargs): self.header_struct = BinaryStruct(*self.HEADER_STRUCT_START, byte_order="<") header = self.header_struct.unpack(buffer) self._check_version(header["version"].decode()) self.signature = header["signature"].rstrip(b"\0").decode() self.magic = header["magic"] self.big_endian = header["big_endian"] or is_big_endian(self.magic) byte_order = ">" if self.big_endian else "<" header.update( self.header_struct.unpack(buffer, *self.HEADER_STRUCT_ENDIAN, byte_order=byte_order)) self.unknown = header["unknown"] self.entry_header_struct = BinaryStruct(*self.BND_ENTRY_HEADER, byte_order=byte_order) if has_id(self.magic): self.entry_header_struct.add_fields(self.ENTRY_ID, byte_order=byte_order) if has_path(self.magic): self.entry_header_struct.add_fields(self.NAME_OFFSET, byte_order=byte_order) if has_uncompressed_size(self.magic): self.entry_header_struct.add_fields(self.UNCOMPRESSED_DATA_SIZE, byte_order=byte_order) # NOTE: BND paths are *not* encoded in `shift_jis_2004`, unlike most other strings! They are `shift-jis`. # The main annoyance here is that escaped backslashes are encoded as the yen symbol in `shift_jis_2004`. for entry in BNDEntry.unpack(buffer, self.entry_header_struct, path_encoding="shift-jis", count=header["entry_count"]): self.add_entry(entry)
def pack(self): header = self.header_struct.pack(goal_count=len(self.goals)) packed_goals = b"" packed_strings = b"" goal_struct = BinaryStruct( *(self.GOAL_STRUCT_64 if self.use_struct_64 else self.GOAL_STRUCT_32), byte_order=">" if self.big_endian else "<", ) packed_strings_offset = len(header) + len( self.goals) * goal_struct.size encoding = self.encoding z_term = b"\0\0" if self.use_struct_64 else b"\0" for goal in self.goals: name_offset = packed_strings_offset + len(packed_strings) packed_strings += goal.goal_name.encode(encoding=encoding) + z_term goal_kwargs = goal.get_interrupt_details() logic_interrupt_name = goal_kwargs.pop("logic_interrupt_name") if logic_interrupt_name: logic_interrupt_name_offset = packed_strings_offset + len( packed_strings) packed_strings += logic_interrupt_name.encode( encoding=encoding) + z_term else: logic_interrupt_name_offset = 0 packed_goals += goal_struct.pack( goal_id=goal.goal_id, name_offset=name_offset, logic_interrupt_name_offset=logic_interrupt_name_offset, **goal_kwargs, ) return header + packed_goals + packed_strings
def create_header_structs(self): self._most_recent_hash_table = b"" # Hash table will need to be built on first pack. self._most_recent_entry_count = len(self._entries) self._most_recent_paths = [entry.path for entry in self._entries] self.header_struct = BinaryStruct(*self.HEADER_STRUCT_START, byte_order="<") byte_order = ">" if self.big_endian else "<" self.header_struct.add_fields(*self.HEADER_STRUCT_ENDIAN, byte_order=byte_order) self.entry_header_struct = BinaryStruct(*self.BND_ENTRY_HEADER, byte_order=byte_order) if has_uncompressed_size(self.magic): self.entry_header_struct.add_fields(self.UNCOMPRESSED_DATA_SIZE, byte_order=byte_order) self.entry_header_struct.add_fields(self.DATA_OFFSET, byte_order=byte_order) if has_id(self.magic): self.entry_header_struct.add_fields(self.ENTRY_ID, byte_order=byte_order) if has_path(self.magic): self.entry_header_struct.add_fields(self.NAME_OFFSET, byte_order=byte_order) if self.magic == 0x20: # Extra pad. self.entry_header_struct.add_fields("8x")
def __init__(self, luainfo_source=None, big_endian=False, use_struct_64=False): self.big_endian = big_endian self.use_struct_64 = use_struct_64 self.luainfo_path = None self.header_struct = BinaryStruct( *self.HEADER_STRUCT, byte_order=">" if self.big_endian else "<") self.goals = [] # type: List[LuaGoal] if luainfo_source is None: return if isinstance(luainfo_source, (list, tuple)): self.goals = luainfo_source # type: List[LuaGoal] return if isinstance(luainfo_source, (str, Path)): self.luainfo_path = Path(luainfo_source) with self.luainfo_path.open("rb") as f: self.unpack(f) return if hasattr(luainfo_source, "data"): luainfo_source = luainfo_source.data if isinstance(luainfo_source, bytes): luainfo_source = io.BytesIO(luainfo_source) if isinstance(luainfo_source, io.BufferedIOBase): self.unpack(luainfo_source)
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 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 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 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( "4x", ("name_offset", "i"), ("entry_offset_count", "i"), ) MAP_ENTITY_ENTRY_OFFSET = BinaryStruct(("entry_offset", "i"), ) MAP_ENTITY_LIST_TAIL = BinaryStruct(("next_entry_list_offset", "i"), ) NAME_ENCODING = "utf-8"
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(_BaseESD, abc.ABC): GAME = DARK_SOULS_DSR 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 )
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 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 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 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 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", )
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 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 EventArg(_BaseEventArg): HEADER_STRUCT = BinaryStruct( ("instruction_line", "Q"), ("write_from_byte", "Q"), ("read_from_byte", "Q"), ("bytes_to_write", "Q"), )
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 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 ESD(_BaseESD, abc.ABC): GAME = DARK_SOULS_PTDE 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 )
def create_header_structs(self): self.header_struct = BinaryStruct(*self.HEADER_STRUCT_START, byte_order="<") byte_order = ">" if self.big_endian else "<" self.header_struct.add_fields(*self.HEADER_STRUCT_ENDIAN, byte_order=byte_order) self.entry_header_struct = BinaryStruct(*self.BND_ENTRY_HEADER, byte_order=byte_order) if has_id(self.magic): self.entry_header_struct.add_fields(self.ENTRY_ID, byte_order=byte_order) if has_path(self.magic): self.entry_header_struct.add_fields(self.NAME_OFFSET, byte_order=byte_order) if has_uncompressed_size(self.magic): self.entry_header_struct.add_fields(self.UNCOMPRESSED_DATA_SIZE, byte_order=byte_order)
class MSBEvent(_BaseMSBEvent): 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 EventLayers(_BaseEventLayers): """Never used in DS1 and very probably not actually supported by the engine.""" HEADER_STRUCT = BinaryStruct( ("two", "I", 2), ("event_layers", "I"), # 32-bit bit field ("zero", "I", 0), # format is a guess ("minus_one", "i", -1), # format is a guess ("one", "I", 1), # format is a guess )
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 MSBTreasureEvent(_BaseMSBTreasureEvent, MSBEvent): 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 = _BaseMSBTreasureEvent.FIELD_INFO | MSBEvent.FIELD_INFO | { # base_part, base_region, and entity_id are unused for Treasure. "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", ) def __init__(self, source=None, **kwargs): self.item_lot_4 = -1 self.item_lot_5 = -1 super().__init__(source, **kwargs)
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_buffer): data = self.PART_TYPE_DATA_STRUCT.unpack(msb_buffer, 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 Instruction(_BaseInstruction): DECOMPILER = InstructionDecompiler() INSTRUCTION_ARG_TYPES = INSTRUCTION_ARG_TYPES EventLayers = EventLayers HEADER_STRUCT = BinaryStruct( ("instruction_class", "I"), ("instruction_index", "I"), ("base_args_size", "Q"), ("first_base_arg_offset", "i"), "4x", ("first_event_layers_offset", "q"), )
class State(_BaseState, abc.ABC): STRUCT = BinaryStruct( ("index", "q"), ("condition_pointers_offset", "q"), ("condition_pointers_count", "q"), ("enter_commands_offset", "q"), ("enter_commands_count", "q"), ("exit_commands_offset", "q"), ("exit_commands_count", "q"), ("ongoing_commands_offset", "q"), ("ongoing_commands_count", "q"), )