예제 #1
0
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)
예제 #2
0
class MSBModelList(MSBEntryList[MSBModel]):
    INTERNAL_NAME = "MODEL_PARAM_ST"
    PLURALIZED_NAME = "Models"
    ENTRY_SUBTYPE_ENUM = MSBModelSubtype

    SUBTYPE_CLASSES = {
        MSBModelSubtype.MapPiece: MSBMapPieceModel,
        MSBModelSubtype.Object: MSBObjectModel,
        MSBModelSubtype.Character: MSBCharacterModel,
        MSBModelSubtype.Player: MSBPlayerModel,
        MSBModelSubtype.Collision: MSBCollisionModel,
        MSBModelSubtype.Navmesh: MSBNavmeshModel,
    }
    SUBTYPE_OFFSET = 4

    _entries: list[MSBModel]

    MapPieces: list[MSBMapPieceModel]
    Objects: list[MSBObjectModel]
    Characters: list[MSBCharacterModel]
    Players: list[MSBPlayerModel]
    Collisions: list[MSBCollisionModel]
    Navmeshes: list[MSBNavmeshModel]

    new = MSBEntryList.new
    new_map_piece_model: tp.Callable[..., MSBMapPieceModel] = partialmethod(new, MSBModelSubtype.MapPiece)
    new_object_model: tp.Callable[..., MSBObjectModel] = partialmethod(new, MSBModelSubtype.Object)
    new_character_model: tp.Callable[..., MSBCharacterModel] = partialmethod(new, MSBModelSubtype.Character)
    new_player_model: tp.Callable[..., MSBPlayerModel] = partialmethod(new, MSBModelSubtype.Player)
    new_collision_model: tp.Callable[..., MSBCollisionModel] = partialmethod(new, MSBModelSubtype.Collision)
    new_navmesh_model: tp.Callable[..., MSBNavmeshModel] = partialmethod(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

    def sort(self):
        """Sort all models by type, then alphabetically."""
        sorted_entries = []  # type: list[MSBModel]
        for entry_subtype in MSBModelSubtype:
            sorted_entries += list(sorted(self.get_entries(entry_subtype), key=lambda m: m.name))
        self._entries = sorted_entries
예제 #3
0
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
예제 #4
0
class MSBPartList(_BaseMSBPartList, MSBEntryList):
    SUBTYPE_CLASSES = {
        MSBPartSubtype.MapPiece: MSBMapPiece,
        MSBPartSubtype.Object: MSBObject,
        MSBPartSubtype.Character: MSBCharacter,
        MSBPartSubtype.PlayerStart: MSBPlayerStart,
        MSBPartSubtype.Collision: MSBCollision,
        MSBPartSubtype.Navmesh: MSBNavmesh,
        MSBPartSubtype.UnusedObject: MSBUnusedObject,
        MSBPartSubtype.UnusedCharacter: MSBUnusedCharacter,
        MSBPartSubtype.MapConnection: MSBMapConnection,
        MSBPartSubtype.Other: MSBOtherPart,
    }
    SUBTYPE_OFFSET = 20
    GET_MAP = staticmethod(get_map)

    _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]
    Other: tp.Sequence[MSBOtherPart]

    new_map_piece: tp.Callable[..., MSBMapPiece]
    new_object: tp.Callable[..., MSBObject]
    new_character: tp.Callable[..., MSBCharacter]
    new_player_start: tp.Callable[..., MSBPlayerStart]
    new_collision: tp.Callable[..., MSBCollision]
    new_navmesh: tp.Callable[..., MSBNavmesh]
    new_unused_object: tp.Callable[..., MSBUnusedObject]
    new_unused_character: tp.Callable[..., MSBUnusedCharacter]
    new_map_connection: tp.Callable[..., MSBMapConnection]
    new_other: tp.Callable[..., MSBOtherPart] = partialmethod(_BaseMSBPartList.new, MSBPartSubtype.Other)
예제 #5
0
class MSBRegionList(MSBEntryList[MSBRegion]):
    INTERNAL_NAME = "POINT_PARAM_ST"
    ENTRY_LIST_NAME = "Regions"
    ENTRY_SUBTYPE_ENUM = MSBRegionSubtype

    SUBTYPE_CLASSES = {
        MSBRegionSubtype.Point: MSBRegionPoint,
        MSBRegionSubtype.Circle: MSBRegionCircle,
        MSBRegionSubtype.Sphere: MSBRegionSphere,
        MSBRegionSubtype.Cylinder: MSBRegionCylinder,
        MSBRegionSubtype.Rect: MSBRegionRect,
        MSBRegionSubtype.Box: MSBRegionBox,
    }
    SUBTYPE_OFFSET = 12

    _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)

    _entries: list[MSBRegion]
예제 #6
0
class MSBPartList(MSBEntryList[MSBPart]):
    INTERNAL_NAME = "PARTS_PARAM_ST"
    ENTRY_LIST_NAME = "Parts"
    ENTRY_SUBTYPE_ENUM = MSBPartSubtype
    SUBTYPE_CLASSES: dict[MSBPartSubtype, tp.Type[MSBPart]] = {
        MSBPartSubtype.MapPiece: MSBMapPiece,
        MSBPartSubtype.Object: MSBObject,
        MSBPartSubtype.Character: MSBCharacter,
        MSBPartSubtype.PlayerStart: MSBPlayerStart,
        MSBPartSubtype.Collision: MSBCollision,
        MSBPartSubtype.Navmesh: MSBNavmesh,
        MSBPartSubtype.UnusedObject: MSBUnusedObject,
        MSBPartSubtype.UnusedCharacter: MSBUnusedCharacter,
        MSBPartSubtype.MapConnection: MSBMapConnection,
        MSBPartSubtype.Other: MSBOtherPart,
    }
    SUBTYPE_OFFSET = 20
    GET_MAP = staticmethod(get_map)

    _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]
    Other: tp.Sequence[MSBOtherPart]

    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)
    new_other: tp.Callable[..., MSBOtherPart] = partialmethod(new, MSBPartSubtype.Other)

    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 idiosyncracy
        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:
                indices = PartIndicesData(
                    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,
                )
                entry.set_indices(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:
            names = PartNamesData(
                model_names=model_names,
                region_names=region_names,
                environment_names=environment_names,
                part_names=part_names,
                collision_names=collision_names,
            )
            entry.set_names(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: tp.Union[str, MSBCollision], 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)
예제 #7
0
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)
예제 #8
0
class MSBEventList(BaseMSBEventList, MSBEntryList[MSBEvent]):
    INTERNAL_NAME = "EVENT_PARAM_ST"
    ENTRY_LIST_NAME = "Events"
    ENTRY_SUBTYPE_ENUM = MSBEventSubtype

    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]

    _entries: list[MSBEvent]

    new = MSBEntryList.new
    new_light: tp.Callable[..., MSBLightEvent] = partialmethod(
        new, MSBEventSubtype.Light)
    new_sound: tp.Callable[..., MSBSoundEvent] = partialmethod(
        new, MSBEventSubtype.Sound)
    new_vfx: tp.Callable[...,
                         MSBVFXEvent] = partialmethod(new, MSBEventSubtype.VFX)
    new_wind: tp.Callable[..., MSBWindEvent] = partialmethod(
        new, MSBEventSubtype.Wind)
    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)
    new_npc_invasion: tp.Callable[..., MSBNPCInvasionEvent] = partialmethod(
        new, MSBEventSubtype.NPCInvasion)

    def pack_entry(self, index: int, entry: MSBEvent):
        return entry.pack()

    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:
                indices = EventsIndicesData(
                    event_index=i,
                    local_event_index=subtype_indices.setdefault(
                        entry.ENTRY_SUBTYPE, 0),
                    region_indices=region_indices,
                    part_indices=part_indices,
                )
                entry.set_indices(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

    def set_names(self, region_names, part_names):
        for entry in self._entries:
            names = EventsNamesData(region_names, part_names)
            entry.set_names(names)