From b4bb88fcf7986859bb9efb2d6a159fcebcbab105 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 7 Mar 2024 03:18:22 -0600 Subject: [PATCH] SNIClient: dynamically generate patch file identifier (#2870) Co-authored-by: beauxq --- worlds/AutoSNIClient.py | 45 +++++++++++++++++++++++++++++++++++- worlds/LauncherComponents.py | 4 ---- worlds/alttp/Client.py | 1 + worlds/dkc3/Client.py | 1 + worlds/lufia2ac/Client.py | 1 + worlds/sm/Client.py | 1 + worlds/smw/Client.py | 1 + worlds/smz3/Client.py | 1 + 8 files changed, 50 insertions(+), 5 deletions(-) diff --git a/worlds/AutoSNIClient.py b/worlds/AutoSNIClient.py index a30dbbb4..2b984d9c 100644 --- a/worlds/AutoSNIClient.py +++ b/worlds/AutoSNIClient.py @@ -1,11 +1,35 @@ from __future__ import annotations import abc -from typing import TYPE_CHECKING, ClassVar, Dict, Tuple, Any, Optional +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]] = {} @@ -15,6 +39,22 @@ class AutoSNIClientRegister(abc.ABCMeta): 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 @@ -27,6 +67,9 @@ class AutoSNIClientRegister(abc.ABCMeta): 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 """ diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 7814aac5..41c0bb83 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -85,10 +85,6 @@ components: List[Component] = [ file_identifier=SuffixIdentifier('.archipelago', '.zip')), Component('Generate', 'Generate', cli=True), Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient), - # SNI - Component('SNI Client', 'SNIClient', - file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', - '.apsmw', '.apl2ac', '.apkdl3')), Component('Links Awakening DX Client', 'LinksAwakeningClient', file_identifier=SuffixIdentifier('.apladx')), Component('LttP Adjuster', 'LttPAdjuster'), diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index edc68473..5b27f559 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -471,6 +471,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: class ALTTPSNIClient(SNIClient): game = "A Link to the Past" + patch_suffix = [".aplttp", ".apz3"] async def deathlink_kill_player(self, ctx): from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index 77ed51fe..efa199e1 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -24,6 +24,7 @@ DEATH_LINK_ACTIVE_ADDR = DKC3_ROMNAME_START + 0x15 # DKC3_TODO: Find a perma class DKC3SNIClient(SNIClient): game = "Donkey Kong Country 3" + patch_suffix = ".apdkc3" async def deathlink_kill_player(self, ctx): pass diff --git a/worlds/lufia2ac/Client.py b/worlds/lufia2ac/Client.py index ac0de19b..9025a113 100644 --- a/worlds/lufia2ac/Client.py +++ b/worlds/lufia2ac/Client.py @@ -30,6 +30,7 @@ L2AC_RX_ADDR: int = SRAM_START + 0x2800 class L2ACSNIClient(SNIClient): game: str = "Lufia II Ancient Cave" + patch_suffix = ".apl2ac" async def validate_rom(self, ctx: SNIContext) -> bool: from SNIClient import snes_read diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index 756fd4bf..ed3f2d5b 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -37,6 +37,7 @@ SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277F06 # 1 byte class SMSNIClient(SNIClient): game = "Super Metroid" + patch_suffix = [".apsm", ".apm3"] async def deathlink_kill_player(self, ctx): from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py index 92aeac4d..50899abe 100644 --- a/worlds/smw/Client.py +++ b/worlds/smw/Client.py @@ -57,6 +57,7 @@ SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A class SMWSNIClient(SNIClient): game = "Super Mario World" + patch_suffix = ".apsmw" async def deathlink_kill_player(self, ctx): from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index b07aa850..0a248aa5 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -32,6 +32,7 @@ SMZ3_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte class SMZ3SNIClient(SNIClient): game = "SMZ3" + patch_suffix = ".apsmz3" async def validate_rom(self, ctx): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read