class MSBEventList(MSBEntryList[MSBEvent]): ENTRY_LIST_NAME = "Events" ENTRY_SUBTYPE_ENUM = MSBEventSubtype SUBTYPE_CLASSES = {} # type: dict[MSBEventSubtype, type] SUBTYPE_OFFSET = None # type: int _entries: list[MSBEvent] Sounds: tp.Sequence[MSBSoundEvent] VFX: tp.Sequence[MSBVFXEvent] Treasure: tp.Sequence[MSBTreasureEvent] Spawners: tp.Sequence[MSBSpawnerEvent] Messages: tp.Sequence[MSBMessageEvent] ObjActs: tp.Sequence[MSBObjActEvent] SpawnPoints: tp.Sequence[MSBSpawnPointEvent] MapOffsets: tp.Sequence[MSBMapOffsetEvent] Navigation: tp.Sequence[MSBNavigationEvent] Environment: tp.Sequence[MSBEnvironmentEvent] new = MSBEntryList.new new_sound: tp.Callable[..., MSBSoundEvent] = partialmethod(new, MSBEventSubtype.Sound) new_vfx: tp.Callable[..., MSBVFXEvent] = partialmethod(new, MSBEventSubtype.VFX) new_treasure: tp.Callable[..., MSBTreasureEvent] = partialmethod(new, MSBEventSubtype.Treasure) new_spawner: tp.Callable[..., MSBSpawnerEvent] = partialmethod(new, MSBEventSubtype.Spawner) new_message: tp.Callable[..., MSBMessageEvent] = partialmethod(new, MSBEventSubtype.Message) new_obj_act: tp.Callable[..., MSBObjActEvent] = partialmethod(new, MSBEventSubtype.ObjAct) new_spawn_point: tp.Callable[..., MSBSpawnPointEvent] = partialmethod(new, MSBEventSubtype.SpawnPoint) new_map_offset: tp.Callable[..., MSBMapOffsetEvent] = partialmethod(new, MSBEventSubtype.MapOffset) new_navigation: tp.Callable[..., MSBNavigationEvent] = partialmethod(new, MSBEventSubtype.Navigation) new_environment: tp.Callable[..., MSBEnvironmentEvent] = partialmethod(new, MSBEventSubtype.Environment) def pack_entry(self, index: int, entry: MSBEvent): return entry.pack() def set_names(self, region_names, part_names): for entry in self._entries: entry.set_names(region_names, part_names) def set_indices(self, region_indices, part_indices): """Global and subtype-specific indices both set. (Unclear if either of them do anything.)""" subtype_indices = {} for i, entry in enumerate(self._entries): try: entry.set_indices( event_index=i, local_event_index=subtype_indices.setdefault(entry.ENTRY_SUBTYPE, 0), region_indices=region_indices, part_indices=part_indices, ) except KeyError as e: raise MapEventError( f"Invalid map component name for {entry.ENTRY_SUBTYPE.name} event '{entry.name}': {e}" ) else: subtype_indices[entry.ENTRY_SUBTYPE] += 1
class MSBEventList(_BaseMSBEventList, MSBEntryList): SUBTYPE_CLASSES = { MSBEventSubtype.Light: MSBLightEvent, MSBEventSubtype.Sound: MSBSoundEvent, MSBEventSubtype.VFX: MSBVFXEvent, MSBEventSubtype.Wind: MSBWindEvent, MSBEventSubtype.Treasure: MSBTreasureEvent, MSBEventSubtype.Spawner: MSBSpawnerEvent, MSBEventSubtype.Message: MSBMessageEvent, MSBEventSubtype.ObjAct: MSBObjActEvent, MSBEventSubtype.SpawnPoint: MSBSpawnPointEvent, MSBEventSubtype.MapOffset: MSBMapOffsetEvent, MSBEventSubtype.Navigation: MSBNavigationEvent, MSBEventSubtype.Environment: MSBEnvironmentEvent, MSBEventSubtype.NPCInvasion: MSBNPCInvasionEvent, } SUBTYPE_OFFSET = 8 Lights: tp.Sequence[MSBLightEvent] Sounds: tp.Sequence[MSBSoundEvent] VFX: tp.Sequence[MSBVFXEvent] Wind: tp.Sequence[MSBWindEvent] Treasure: tp.Sequence[MSBTreasureEvent] Spawners: tp.Sequence[MSBSpawnerEvent] Messages: tp.Sequence[MSBMessageEvent] ObjActs: tp.Sequence[MSBObjActEvent] SpawnPoints: tp.Sequence[MSBSpawnPointEvent] MapOffsets: tp.Sequence[MSBMapOffsetEvent] Navigation: tp.Sequence[MSBNavigationEvent] Environment: tp.Sequence[MSBEnvironmentEvent] NPCInvasion: tp.Sequence[MSBNPCInvasionEvent] new = _BaseMSBEventList.new new_light: tp.Callable[..., MSBLightEvent] = partialmethod( new, MSBEventSubtype.Light) new_wind: tp.Callable[..., MSBWindEvent] = partialmethod( new, MSBEventSubtype.Wind) new_npc_invasion: tp.Callable[..., MSBNPCInvasionEvent] = partialmethod( new, MSBEventSubtype.NPCInvasion)
class MSBModelList(MSBEntryList[MSBModel]): PLURALIZED_NAME = "Models" ENTRY_SUBTYPE_ENUM = MSBModelSubtype SUBTYPE_CLASSES = {} # type: dict[MSBModelSubtype, tp.Callable] ENTRY_CLASS: tp.Type[MSBModel] = None _entries: list[MSBModel] MapPieces: list[MSBModel] Objects: list[MSBModel] Characters: list[MSBModel] Items: list[MSBModel] Players: list[MSBModel] Collisions: list[MSBModel] Navmeshes: list[MSBModel] new_map_piece_model: tp.Callable[..., MSBModel] = partialmethod( MSBEntryList.new, MSBModelSubtype.MapPiece) new_object_model: tp.Callable[..., MSBModel] = partialmethod( MSBEntryList.new, MSBModelSubtype.Object) new_character_model: tp.Callable[..., MSBModel] = partialmethod( MSBEntryList.new, MSBModelSubtype.Character) new_item_model: tp.Callable[..., MSBModel] = partialmethod( MSBEntryList.new, MSBModelSubtype.Item) new_player_model: tp.Callable[..., MSBModel] = partialmethod( MSBEntryList.new, MSBModelSubtype.Player) new_collision_model: tp.Callable[..., MSBModel] = partialmethod( MSBEntryList.new, MSBModelSubtype.Collision) new_navmesh_model: tp.Callable[..., MSBModel] = partialmethod( MSBEntryList.new, MSBModelSubtype.Navmesh) def pack_entry(self, index: int, entry: MSBModel): return entry.pack() def set_indices(self, part_instance_counts): """Local type-specific index only. (Note that global entry index is still used by Parts.)""" type_indices = {} for entry in self._entries: try: entry.set_indices( model_type_index=type_indices.setdefault( entry.ENTRY_SUBTYPE, 0), instance_count=part_instance_counts.get(entry.name, 0), ) except KeyError as e: raise SoulstructError( f"Invalid map component name for {entry.ENTRY_SUBTYPE.name} model {entry.name}: {e}" ) else: type_indices[entry.ENTRY_SUBTYPE] += 1
class MSBRegionList(MSBEntryList[MSBRegion], abc.ABC): ENTRY_LIST_NAME = "Regions" ENTRY_SUBTYPE_ENUM = MSBRegionSubtype SUBTYPE_CLASSES = {} # type: dict[MSBRegionSubtype, tp.Type[MSBRegion]] SUBTYPE_OFFSET = -1 # type: int _entries: list[MSBRegion] Points: tp.Sequence[MSBRegionPoint] Circles: tp.Sequence[MSBRegionCircle] Spheres: tp.Sequence[MSBRegionSphere] Cylinders: tp.Sequence[MSBRegionCylinder] Rectangles: tp.Sequence[MSBRegionRect] Boxes: tp.Sequence[MSBRegionBox] new_point: tp.Callable[..., MSBRegionPoint] = partialmethod( MSBEntryList.new, MSBRegionSubtype.Point) new_circle: tp.Callable[..., MSBRegionCircle] = partialmethod( MSBEntryList.new, MSBRegionSubtype.Circle) new_sphere: tp.Callable[..., MSBRegionSphere] = partialmethod( MSBEntryList.new, MSBRegionSubtype.Sphere) new_cylinder: tp.Callable[..., MSBRegionCylinder] = partialmethod( MSBEntryList.new, MSBRegionSubtype.Cylinder) new_rect: tp.Callable[..., MSBRegionRect] = partialmethod( MSBEntryList.new, MSBRegionSubtype.Rect) new_box: tp.Callable[..., MSBRegionBox] = partialmethod(MSBEntryList.new, MSBRegionSubtype.Box) def pack_entry(self, index: int, entry: MSBRegion): return entry.pack(index) def set_indices(self): """Global region index only.""" for i, entry in enumerate(self._entries): entry.set_indices(region_index=i)
class MSBEventList(_BaseMSBEventList, MSBEntryList): SUBTYPE_CLASSES = { # MSBEventSubtype.Light: MSBLightEvent, MSBEventSubtype.Sound: MSBSoundEvent, MSBEventSubtype.VFX: MSBVFXEvent, # MSBEventSubtype.Wind: MSBWindEvent, MSBEventSubtype.Treasure: MSBTreasureEvent, MSBEventSubtype.Spawner: MSBSpawnerEvent, MSBEventSubtype.Message: MSBMessageEvent, MSBEventSubtype.ObjAct: MSBObjActEvent, MSBEventSubtype.SpawnPoint: MSBSpawnPointEvent, MSBEventSubtype.MapOffset: MSBMapOffsetEvent, MSBEventSubtype.Navigation: MSBNavigationEvent, MSBEventSubtype.Environment: MSBEnvironmentEvent, # MSBEventSubtype.NPCInvasion: MSBNPCInvasionEvent, MSBEventSubtype.WindVFX: MSBWindVFXEvent, MSBEventSubtype.PatrolRoute: MSBPatrolRouteEvent, MSBEventSubtype.DarkLock: MSBDarkLockEvent, MSBEventSubtype.Platoon: MSBPlatoonEvent, MSBEventSubtype.MultiSummon: MSBMultiSummonEvent, MSBEventSubtype.Other: MSBOtherEvent, } SUBTYPE_OFFSET = 12 Sounds: tp.Sequence[MSBSoundEvent] VFX: tp.Sequence[MSBVFXEvent] Treasure: tp.Sequence[MSBTreasureEvent] Spawners: tp.Sequence[MSBSpawnerEvent] Messages: tp.Sequence[MSBMessageEvent] ObjActs: tp.Sequence[MSBObjActEvent] SpawnPoints: tp.Sequence[MSBSpawnPointEvent] MapOffsets: tp.Sequence[MSBMapOffsetEvent] Navigation: tp.Sequence[MSBNavigationEvent] Environment: tp.Sequence[MSBEnvironmentEvent] WindVFX: tp.Sequence[MSBWindVFXEvent] PatrolRoutes: tp.Sequence[MSBPatrolRouteEvent] DarkLocks: tp.Sequence[MSBDarkLockEvent] MultiSummons: tp.Sequence[MSBMultiSummonEvent] Other: tp.Sequence[MSBOtherEvent] new = _BaseMSBEventList.new new_sound: tp.Callable[..., MSBSoundEvent] new_vfx: tp.Callable[..., MSBVFXEvent] new_treasure: tp.Callable[..., MSBTreasureEvent] new_spawner: tp.Callable[..., MSBSpawnerEvent] new_message: tp.Callable[..., MSBMessageEvent] new_obj_act: tp.Callable[..., MSBObjActEvent] new_spawn_point: tp.Callable[..., MSBSpawnPointEvent] new_map_offset: tp.Callable[..., MSBMapOffsetEvent] new_navigation: tp.Callable[..., MSBNavigationEvent] new_environment: tp.Callable[..., MSBEnvironmentEvent] new_wind_vfx: tp.Callable[..., MSBWindVFXEvent] = partialmethod( new, MSBEventSubtype.WindVFX) new_patrol_route: tp.Callable[..., MSBPatrolRouteEvent] = partialmethod( new, MSBEventSubtype.PatrolRoute) new_dark_lock: tp.Callable[..., MSBDarkLockEvent] = partialmethod( new, MSBEventSubtype.DarkLock) new_multi_summon: tp.Callable[..., MSBMultiSummonEvent] = partialmethod( new, MSBEventSubtype.MultiSummon) new_other: tp.Callable[..., MSBOtherEvent] = partialmethod( new, MSBEventSubtype.Other)
class MSBPartList(MSBEntryList[MSBPart], abc.ABC): ENTRY_LIST_NAME = "Parts" ENTRY_SUBTYPE_ENUM = MSBPartSubtype SUBTYPE_CLASSES: dict[MSBPartSubtype, tp.Type[MSBPart]] = {} SUBTYPE_OFFSET = -1 # type: int GET_MAP = None # type: tp.Callable _entries: list[MSBPart] MapPieces: tp.Sequence[MSBMapPiece] Objects: tp.Sequence[MSBObject] Characters: tp.Sequence[MSBCharacter] PlayerStarts: tp.Sequence[MSBPlayerStart] Collisions: tp.Sequence[MSBCollision] Navmeshes: tp.Sequence[MSBNavmesh] UnusedObjects: tp.Sequence[MSBUnusedObject] UnusedCharacters: tp.Sequence[MSBUnusedCharacter] MapConnections: tp.Sequence[MSBMapConnection] new = MSBEntryList.new new_map_piece: tp.Callable[..., MSBMapPiece] = partialmethod( new, MSBPartSubtype.MapPiece) new_object: tp.Callable[..., MSBObject] = partialmethod(new, MSBPartSubtype.Object) new_character: tp.Callable[..., MSBCharacter] = partialmethod( new, MSBPartSubtype.Character) new_player_start: tp.Callable[..., MSBPlayerStart] = partialmethod( new, MSBPartSubtype.PlayerStart) new_collision: tp.Callable[..., MSBCollision] = partialmethod( new, MSBPartSubtype.Collision) new_navmesh: tp.Callable[..., MSBNavmesh] = partialmethod( new, MSBPartSubtype.Navmesh) new_unused_object: tp.Callable[..., MSBUnusedObject] = partialmethod( new, MSBPartSubtype.UnusedObject) new_unused_character: tp.Callable[..., MSBUnusedCharacter] = partialmethod( new, MSBPartSubtype.UnusedCharacter) new_map_connection: tp.Callable[..., MSBMapConnection] = partialmethod( new, MSBPartSubtype.MapConnection) def pack_entry(self, index: int, entry: MSBPart): return entry.pack() def set_indices( self, model_indices, local_environment_indices, region_indices, part_indices, local_collision_indices, ): """Local type-specific index only. Events and other Parts may point to Parts by global entry index, but it seems the local index still matters, as ObjAct Events seem to break when the local object index is changed. It's possible this was just an idiosyncrasy of Wulf's MSB Editor. Either way, this method should ensure the global and local indices are consistent. Remember that Navmesh indices are hard-coded into MCP and MCG files. Also note that cutscene files (remo) access MSB parts by index as well, which is why map mods tend to break them so often. `local_environment_indices` are needed for Collisions and `local_collision_indices` are needed for Map Load Triggers. No other MSB entry type requires local subtype indices. """ type_indices = {} for entry in self._entries: try: entry.set_indices( part_type_index=type_indices.setdefault( entry.ENTRY_SUBTYPE, 0), model_indices=model_indices, local_environment_indices=local_environment_indices, region_indices=region_indices, part_indices=part_indices, local_collision_indices=local_collision_indices, ) except KeyError as e: raise SoulstructError( f"Missing name referenced by {entry.name}: {str(e)}") else: type_indices[entry.ENTRY_SUBTYPE] += 1 def set_names( self, model_names, environment_names, region_names, part_names, collision_names, ): for entry in self._entries: entry.set_names( model_names, region_names, environment_names, part_names, collision_names, ) def get_instance_counts(self): """Returns a dictionary mapping model names to part instance counts.""" instance_counts = {} for entry in self._entries: instance_counts.setdefault(entry.model_name, 0) instance_counts[entry.model_name] += 1 return instance_counts # ------------------------------------- # # Additional special creation functions # # ------------------------------------- # def duplicate_collision_with_environment_event( self, collision, msb: MSB, insert_below_original=True, **kwargs, ) -> MSBCollision: """Duplicate a Collision and any attached `MSBEnvironment` instance and its region.""" if "name" not in kwargs: raise ValueError( f"Must pass `name` to Collision duplication call to duplicate attached environment event." ) new_collision = self.new_collision( copy_entry=collision, insert_below_original=insert_below_original, **kwargs, ) if new_collision.environment_event_name is None: return new_collision try: environment_event: MSBEnvironmentEvent = msb.events.get_entry_by_name( new_collision.environment_event_name, "Environment", ) except KeyError: raise KeyError( f"Could not find environment event '{new_collision.environment_event_name}' in MSB." ) if not environment_event.base_region_name: raise AttributeError( f"Environment event '{environment_event.name}' has no anchor Region." ) try: environment_region: MSBRegion = msb.regions.get_entry_by_name( environment_event.base_region_name) except KeyError: raise KeyError( f"Could not find environment region '{environment_event.base_region_name}' in MSB." ) new_region = msb.regions.duplicate_entry( environment_region, name=f"GI Region ({kwargs['name']})") new_event = msb.events.duplicate_entry( environment_event, name=f"GI Event ({kwargs['name']})") new_event.base_region_name = new_region.name new_collision.environment_event_name = new_event.name return new_collision def create_map_connection_from_collision(self, collision, connected_map, name=None, draw_groups=None, display_groups=None): """Creates a new `MapConnection` that references and copies the transform of the given `collision`. The `name` and `map_id` of the new `MapConnection` must be given. You can also specify its `draw_groups` and `display_groups`. Otherwise, it will leave them as the extensive default values: [0, ..., 127]. """ if not isinstance(collision, MSBCollision): collision = self.get_entry_by_name(collision, "Collision") if name is None: game_map = self.GET_MAP(connected_map) name = collision.name + f"_[{game_map.area_id:02d}_{game_map.block_id:02d}]" if name in self.get_entry_names("MapConnection"): raise ValueError( f"{repr(name)} is already the name of an existing MapConnection." ) map_connection = self.SUBTYPE_CLASSES[MSBPartSubtype.MapConnection]( name=name, connected_map=connected_map, collision_name=collision.name, translate=collision.translate.copy(), rotate=collision.rotate.copy(), scale=collision.scale.copy(), # for completion's sake model_name=collision.model_name, ) if draw_groups is not None: # otherwise keep same draw groups map_connection.draw_groups = draw_groups if display_groups is not None: # otherwise keep same display groups map_connection.display_groups = display_groups self.add_entry(map_connection) return map_connection def new_c1000(self, name, **kwargs) -> MSBCharacter: """Useful to create basic c1000 instances as debug warp points.""" return self.new_character(name=name, model_name="c1000", **kwargs)