diff --git a/Patch.py b/Patch.py index 9b49876bb72d..dfb04a9d8afd 100644 --- a/Patch.py +++ b/Patch.py @@ -17,10 +17,20 @@ class RomMeta(TypedDict): player_name: str +class IncompatiblePatchError(Exception): + """ + Used to report a version mismatch between a patch's world version and + a user's installed world version that is too important to be compatible + """ + pass + + def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]: auto_handler = AutoPatchRegister.get_handler(patch_file) if auto_handler: handler: APAutoPatchInterface = auto_handler(patch_file) + handler.read() + handler.verify_version() target = os.path.splitext(patch_file)[0]+handler.result_file_ending handler.patch(target) return {"server": handler.server, diff --git a/worlds/Files.py b/worlds/Files.py index ddd1f4e1ce92..42c04a731dd3 100644 --- a/worlds/Files.py +++ b/worlds/Files.py @@ -221,6 +221,7 @@ def get_manifest(self) -> Dict[str, Any]: class APPlayerContainer(APContainer): """A zipfile containing at least archipelago.json meant for a player""" game: ClassVar[Optional[str]] = None + world_version: "Version | None" = None patch_file_ending: str = "" player: Optional[int] @@ -235,10 +236,13 @@ def __init__(self, path: Optional[str] = None, player: Optional[int] = None, self.server = server def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]: + from Utils import tuplize_version manifest = super().read_contents(opened_zipfile) self.player = manifest["player"] self.server = manifest["server"] self.player_name = manifest["player_name"] + if "world_version" in manifest: + self.world_version = tuplize_version(manifest["world_version"]) return manifest def get_manifest(self) -> Dict[str, Any]: @@ -250,6 +254,9 @@ def get_manifest(self) -> Dict[str, Any]: "game": self.game, "patch_file_ending": self.patch_file_ending, }) + if self.game: + from .AutoWorld import AutoWorldRegister + manifest["world_version"] = AutoWorldRegister.world_types[self.game].world_version.as_simple_string() return manifest @@ -281,6 +288,22 @@ class APAutoPatchInterface(APPatch, abc.ABC, metaclass=AutoPatchRegister): def patch(self, target: str) -> None: """ create the output file with the file name `target` """ + def verify_version(self) -> None: + """ + Verify compatibility between a game's currently installed + world version and the version used for generation. + Warns the user or raises an IncompatiblePatchError if the versions are too different. + """ + from Utils import messagebox + from .AutoWorld import AutoWorldRegister + game_version = AutoWorldRegister.world_types[self.game].world_version if self.game else None + if game_version and self.world_version and game_version != self.world_version: + info_msg = "This patch was generated with " \ + f"{self.game} version {self.world_version.as_simple_string()}, " \ + f"but its currently installed version is {game_version.as_simple_string()}. " \ + "You may encounter errors while patching or connecting." + messagebox("APWorld version mismatch", info_msg, False) + class APProcedurePatch(APAutoPatchInterface): """ @@ -343,7 +366,6 @@ def write_file(self, file_name: str, file: bytes) -> None: self.files[file_name] = file def patch(self, target: str) -> None: - self.read() base_data = self.get_source_data_with_cache() patch_extender = AutoPatchExtensionRegister.get_handler(self.game) assert not isinstance(self.procedure, str), f"{type(self)} must define procedures"