from __future__ import annotations import abc import logging from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union from typing_extensions import TypeGuard from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components if TYPE_CHECKING: from SNIClient import SNIContext component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe")) components.append(component) def valid_patch_suffix(obj: object) -> TypeGuard[Union[str, Iterable[str]]]: """ make sure this is a valid value for the class variable `patch_suffix` """ def valid_individual(one: object) -> TypeGuard[str]: """ check an individual suffix """ # TODO: decide: len(one) > 3 and one.startswith(".ap") ? # or keep it more general? return isinstance(one, str) and len(one) > 1 and one.startswith(".") if isinstance(obj, str): return valid_individual(obj) if not isinstance(obj, Iterable): return False obj_it: Iterable[object] = obj return all(valid_individual(each) for each in obj_it) class AutoSNIClientRegister(abc.ABCMeta): game_handlers: ClassVar[Dict[str, SNIClient]] = {} def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoSNIClientRegister: # construct class new_class = super().__new__(cls, name, bases, dct) if "game" in dct: AutoSNIClientRegister.game_handlers[dct["game"]] = new_class() if "patch_suffix" in dct: patch_suffix = dct["patch_suffix"] assert valid_patch_suffix(patch_suffix), f"class {name} defining invalid {patch_suffix=}" existing_identifier = component.file_identifier assert isinstance(existing_identifier, SuffixIdentifier), f"{existing_identifier=}" new_suffixes = [*existing_identifier.suffixes] if isinstance(patch_suffix, str): new_suffixes.append(patch_suffix) else: new_suffixes.extend(patch_suffix) component.file_identifier = SuffixIdentifier(*new_suffixes) return new_class @staticmethod async def get_handler(ctx: SNIContext) -> Optional[SNIClient]: for _game, handler in AutoSNIClientRegister.game_handlers.items(): try: if await handler.validate_rom(ctx): return handler except Exception as e: text_file_logger = logging.getLogger() text_file_logger.exception(e) return None class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister): patch_suffix: ClassVar[Union[str, Iterable[str]]] = () """The file extension(s) this client is meant to open and patch (e.g. ".aplttp")""" @abc.abstractmethod async def validate_rom(self, ctx: SNIContext) -> bool: """ TODO: interface documentation here """ ... @abc.abstractmethod async def game_watcher(self, ctx: SNIContext) -> None: """ TODO: interface documentation here """ ... async def deathlink_kill_player(self, ctx: SNIContext) -> None: """ override this with implementation to kill player """ pass