| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  | from __future__ import annotations | 
					
						
							| 
									
										
										
										
											2021-11-13 20:52:30 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-20 04:19:58 +01:00
										 |  |  | import shutil | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  | import json | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | import bsdiff4 | 
					
						
							|  |  |  | import yaml | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import lzma | 
					
						
							| 
									
										
										
										
											2020-04-15 01:03:04 -07:00
										 |  |  | import threading | 
					
						
							|  |  |  | import concurrent.futures | 
					
						
							|  |  |  | import zipfile | 
					
						
							| 
									
										
										
										
											2020-04-15 10:11:47 +02:00
										 |  |  | import sys | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  | from typing import Tuple, Optional, Dict, Any, Union, BinaryIO | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-07 10:19:18 +02:00
										 |  |  | import ModuleUpdate | 
					
						
							|  |  |  | ModuleUpdate.update() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | import Utils | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-09-02 17:37:37 -04:00
										 |  |  | current_patch_version = 5 | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class AutoPatchRegister(type): | 
					
						
							|  |  |  |     patch_types: Dict[str, APDeltaPatch] = {} | 
					
						
							|  |  |  |     file_endings: Dict[str, APDeltaPatch] = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __new__(cls, name: str, bases, dct: Dict[str, Any]): | 
					
						
							|  |  |  |         # construct class | 
					
						
							|  |  |  |         new_class = super().__new__(cls, name, bases, dct) | 
					
						
							|  |  |  |         if "game" in dct: | 
					
						
							|  |  |  |             AutoPatchRegister.patch_types[dct["game"]] = new_class | 
					
						
							|  |  |  |             if not dct["patch_file_ending"]: | 
					
						
							|  |  |  |                 raise Exception(f"Need an expected file ending for {name}") | 
					
						
							|  |  |  |             AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class | 
					
						
							|  |  |  |         return new_class | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @staticmethod | 
					
						
							|  |  |  |     def get_handler(file: str) -> Optional[type(APDeltaPatch)]: | 
					
						
							|  |  |  |         for file_ending, handler in AutoPatchRegister.file_endings.items(): | 
					
						
							|  |  |  |             if file.endswith(file_ending): | 
					
						
							|  |  |  |                 return handler | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class APContainer: | 
					
						
							|  |  |  |     """A zipfile containing at least archipelago.json""" | 
					
						
							|  |  |  |     version: int = current_patch_version | 
					
						
							|  |  |  |     compression_level: int = 9 | 
					
						
							|  |  |  |     compression_method: int = zipfile.ZIP_DEFLATED | 
					
						
							|  |  |  |     game: Optional[str] = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # instance attributes: | 
					
						
							|  |  |  |     path: Optional[str] | 
					
						
							|  |  |  |     player: Optional[int] | 
					
						
							|  |  |  |     player_name: str | 
					
						
							|  |  |  |     server: str | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, path: Optional[str] = None, player: Optional[int] = None, | 
					
						
							|  |  |  |                  player_name: str = "", server: str = ""): | 
					
						
							|  |  |  |         self.path = path | 
					
						
							|  |  |  |         self.player = player | 
					
						
							|  |  |  |         self.player_name = player_name | 
					
						
							|  |  |  |         self.server = server | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def write(self, file: Optional[Union[str, BinaryIO]] = None): | 
					
						
							|  |  |  |         if not self.path and not file: | 
					
						
							|  |  |  |             raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.") | 
					
						
							|  |  |  |         with zipfile.ZipFile(file if file else self.path, "w", self.compression_method, True, self.compression_level) \ | 
					
						
							|  |  |  |                 as zf: | 
					
						
							|  |  |  |             if file: | 
					
						
							|  |  |  |                 self.path = zf.filename | 
					
						
							|  |  |  |             self.write_contents(zf) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def write_contents(self, opened_zipfile: zipfile.ZipFile): | 
					
						
							|  |  |  |         manifest = self.get_manifest() | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             manifest = json.dumps(manifest) | 
					
						
							|  |  |  |         except Exception as e: | 
					
						
							|  |  |  |             raise Exception(f"Manifest {manifest} did not convert to json.") from e | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             opened_zipfile.writestr("archipelago.json", manifest) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def read(self, file: Optional[Union[str, BinaryIO]] = None): | 
					
						
							|  |  |  |         """Read data into patch object. file can be file-like, such as an outer zip file's stream.""" | 
					
						
							|  |  |  |         if not self.path and not file: | 
					
						
							|  |  |  |             raise FileNotFoundError(f"Cannot read {self.__class__.__name__} due to no path provided.") | 
					
						
							|  |  |  |         with zipfile.ZipFile(file if file else self.path, "r") as zf: | 
					
						
							|  |  |  |             if file: | 
					
						
							|  |  |  |                 self.path = zf.filename | 
					
						
							|  |  |  |             self.read_contents(zf) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def read_contents(self, opened_zipfile: zipfile.ZipFile): | 
					
						
							|  |  |  |         with opened_zipfile.open("archipelago.json", "r") as f: | 
					
						
							|  |  |  |             manifest = json.load(f) | 
					
						
							|  |  |  |         if manifest["compatible_version"] > self.version: | 
					
						
							|  |  |  |             raise Exception(f"File (version: {manifest['compatible_version']}) too new " | 
					
						
							|  |  |  |                             f"for this handler (version: {self.version})") | 
					
						
							|  |  |  |         self.player = manifest["player"] | 
					
						
							|  |  |  |         self.server = manifest["server"] | 
					
						
							|  |  |  |         self.player_name = manifest["player_name"] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_manifest(self) -> dict: | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |             "server": self.server,  # allow immediate connection to server in multiworld. Empty string otherwise | 
					
						
							|  |  |  |             "player": self.player, | 
					
						
							|  |  |  |             "player_name": self.player_name, | 
					
						
							|  |  |  |             "game": self.game, | 
					
						
							|  |  |  |             # minimum version of patch system expected for patching to be successful | 
					
						
							|  |  |  |             "compatible_version": 4, | 
					
						
							|  |  |  |             "version": current_patch_version, | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class APDeltaPatch(APContainer, metaclass=AutoPatchRegister): | 
					
						
							|  |  |  |     """An APContainer that additionally has delta.bsdiff4
 | 
					
						
							|  |  |  |     containing a delta patch to get the desired file, often a rom."""
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     hash = Optional[str]  # base checksum of source file | 
					
						
							|  |  |  |     patch_file_ending: str = "" | 
					
						
							|  |  |  |     delta: Optional[bytes] = None | 
					
						
							|  |  |  |     result_file_ending: str = ".sfc" | 
					
						
							|  |  |  |     source_data: bytes | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, *args, patched_path: str = "", **kwargs): | 
					
						
							|  |  |  |         self.patched_path = patched_path | 
					
						
							|  |  |  |         super(APDeltaPatch, self).__init__(*args, **kwargs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_manifest(self) -> dict: | 
					
						
							|  |  |  |         manifest = super(APDeltaPatch, self).get_manifest() | 
					
						
							|  |  |  |         manifest["base_checksum"] = self.hash | 
					
						
							|  |  |  |         manifest["result_file_ending"] = self.result_file_ending | 
					
						
							| 
									
										
										
										
											2022-09-02 17:37:37 -04:00
										 |  |  |         manifest["patch_file_ending"] = self.patch_file_ending | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  |         return manifest | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def get_source_data(cls) -> bytes: | 
					
						
							|  |  |  |         """Get Base data""" | 
					
						
							|  |  |  |         raise NotImplementedError() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @classmethod | 
					
						
							|  |  |  |     def get_source_data_with_cache(cls) -> bytes: | 
					
						
							|  |  |  |         if not hasattr(cls, "source_data"): | 
					
						
							|  |  |  |             cls.source_data = cls.get_source_data() | 
					
						
							|  |  |  |         return cls.source_data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def write_contents(self, opened_zipfile: zipfile.ZipFile): | 
					
						
							|  |  |  |         super(APDeltaPatch, self).write_contents(opened_zipfile) | 
					
						
							|  |  |  |         # write Delta | 
					
						
							|  |  |  |         opened_zipfile.writestr("delta.bsdiff4", | 
					
						
							|  |  |  |                                 bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()), | 
					
						
							|  |  |  |                                 compress_type=zipfile.ZIP_STORED)  # bsdiff4 is a format with integrated compression | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def read_contents(self, opened_zipfile: zipfile.ZipFile): | 
					
						
							|  |  |  |         super(APDeltaPatch, self).read_contents(opened_zipfile) | 
					
						
							|  |  |  |         self.delta = opened_zipfile.read("delta.bsdiff4") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def patch(self, target: str): | 
					
						
							|  |  |  |         """Base + Delta -> Patched""" | 
					
						
							|  |  |  |         if not self.delta: | 
					
						
							|  |  |  |             self.read() | 
					
						
							|  |  |  |         result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta) | 
					
						
							|  |  |  |         with open(target, "wb") as f: | 
					
						
							|  |  |  |             f.write(result) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # legacy patch handling follows: | 
					
						
							| 
									
										
										
										
											2021-11-12 14:36:34 +01:00
										 |  |  | GAME_ALTTP = "A Link to the Past" | 
					
						
							|  |  |  | GAME_SM = "Super Metroid" | 
					
						
							| 
									
										
										
										
											2021-11-13 20:52:30 +01:00
										 |  |  | GAME_SOE = "Secret of Evermore" | 
					
						
							| 
									
										
										
										
											2022-03-15 08:55:57 -04:00
										 |  |  | GAME_SMZ3 = "SMZ3" | 
					
						
							| 
									
										
										
										
											2022-07-22 01:02:25 -04:00
										 |  |  | GAME_DKC3 = "Donkey Kong Country 3" | 
					
						
							|  |  |  | supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore", "SMZ3", "Donkey Kong Country 3"} | 
					
						
							| 
									
										
										
										
											2021-11-13 20:52:30 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | preferred_endings = { | 
					
						
							|  |  |  |     GAME_ALTTP: "apbp", | 
					
						
							|  |  |  |     GAME_SM: "apm3", | 
					
						
							| 
									
										
										
										
											2022-03-15 08:55:57 -04:00
										 |  |  |     GAME_SOE: "apsoe", | 
					
						
							| 
									
										
										
										
											2022-07-22 01:02:25 -04:00
										 |  |  |     GAME_SMZ3: "apsmz", | 
					
						
							|  |  |  |     GAME_DKC3: "apdkc3" | 
					
						
							| 
									
										
										
										
											2021-11-13 20:52:30 +01:00
										 |  |  | } | 
					
						
							| 
									
										
										
										
											2021-11-12 14:36:34 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-12 14:36:34 +01:00
										 |  |  | def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes: | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |     if game == GAME_ALTTP: | 
					
						
							| 
									
										
										
										
											2022-06-28 00:43:05 +00:00
										 |  |  |         from worlds.alttp.Rom import LTTPJPN10HASH as HASH | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |     elif game == GAME_SM: | 
					
						
							| 
									
										
										
										
											2022-06-28 00:43:05 +00:00
										 |  |  |         from worlds.sm.Rom import SMJUHASH as HASH | 
					
						
							| 
									
										
										
										
											2021-11-13 20:52:30 +01:00
										 |  |  |     elif game == GAME_SOE: | 
					
						
							|  |  |  |         from worlds.soe.Patch import USHASH as HASH | 
					
						
							| 
									
										
										
										
											2022-03-15 08:55:57 -04:00
										 |  |  |     elif game == GAME_SMZ3: | 
					
						
							| 
									
										
										
										
											2022-06-28 00:43:05 +00:00
										 |  |  |         from worlds.alttp.Rom import LTTPJPN10HASH as ALTTPHASH | 
					
						
							|  |  |  |         from worlds.sm.Rom import SMJUHASH as SMHASH | 
					
						
							| 
									
										
										
										
											2022-03-15 08:55:57 -04:00
										 |  |  |         HASH = ALTTPHASH + SMHASH | 
					
						
							| 
									
										
										
										
											2022-07-22 01:02:25 -04:00
										 |  |  |     elif game == GAME_DKC3: | 
					
						
							|  |  |  |         from worlds.dkc3.Rom import USHASH as HASH | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |     else: | 
					
						
							| 
									
										
										
										
											2021-11-13 20:52:30 +01:00
										 |  |  |         raise RuntimeError(f"Selected game {game} for base rom not found.") | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-16 09:05:11 -07:00
										 |  |  |     patch = yaml.dump({"meta": metadata, | 
					
						
							| 
									
										
										
										
											2020-07-05 02:06:00 +02:00
										 |  |  |                        "patch": patch, | 
					
						
							| 
									
										
										
										
											2021-11-12 14:36:34 +01:00
										 |  |  |                        "game": game, | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  |                        # minimum version of patch system expected for patching to be successful | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |                        "compatible_version": 3, | 
					
						
							| 
									
										
										
										
											2020-10-24 05:38:56 +02:00
										 |  |  |                        "version": current_patch_version, | 
					
						
							| 
									
										
										
										
											2021-11-13 20:52:30 +01:00
										 |  |  |                        "base_checksum": HASH}) | 
					
						
							| 
									
										
										
										
											2020-04-16 09:05:11 -07:00
										 |  |  |     return patch.encode(encoding="utf-8-sig") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-12 14:36:34 +01:00
										 |  |  | def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes: | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |     if metadata is None: | 
					
						
							|  |  |  |         metadata = {} | 
					
						
							| 
									
										
										
										
											2021-11-14 21:03:17 +01:00
										 |  |  |     patch = bsdiff4.diff(get_base_rom_data(game), rom) | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |     return generate_yaml(patch, metadata, game) | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-14 15:25:57 +02:00
										 |  |  | def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None, | 
					
						
							| 
									
										
										
										
											2021-11-12 14:36:34 +01:00
										 |  |  |                       player: int = 0, player_name: str = "", game: str = GAME_ALTTP) -> str: | 
					
						
							|  |  |  |     meta = {"server": server,  # allow immediate connection to server in multiworld. Empty string otherwise | 
					
						
							| 
									
										
										
										
											2021-05-14 15:25:57 +02:00
										 |  |  |             "player_id": player, | 
					
						
							|  |  |  |             "player_name": player_name} | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |     bytes = generate_patch(load_bytes(rom_file_to_patch), | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |                            meta, | 
					
						
							|  |  |  |                            game) | 
					
						
							| 
									
										
										
										
											2021-11-12 14:36:34 +01:00
										 |  |  |     target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ( | 
					
						
							| 
									
										
										
										
											2022-07-22 01:02:25 -04:00
										 |  |  |         ".apbp" if game == GAME_ALTTP | 
					
						
							|  |  |  |         else ".apsmz" if game == GAME_SMZ3 | 
					
						
							|  |  |  |         else ".apdkc3" if game == GAME_DKC3 | 
					
						
							|  |  |  |         else ".apm3") | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |     write_lzma(bytes, target) | 
					
						
							|  |  |  |     return target | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]: | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |     data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig")) | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |     game_name = data["game"] | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  |     if not ignore_version and data["compatible_version"] > current_patch_version: | 
					
						
							| 
									
										
										
										
											2020-10-24 05:38:56 +02:00
										 |  |  |         raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.") | 
					
						
							| 
									
										
										
										
											2021-11-14 21:03:17 +01:00
										 |  |  |     patched_data = bsdiff4.patch(get_base_rom_data(game_name), data["patch"]) | 
					
						
							| 
									
										
										
										
											2020-06-07 12:04:33 -07:00
										 |  |  |     rom_hash = patched_data[int(0x7FC0):int(0x7FD5)] | 
					
						
							|  |  |  |     data["meta"]["hash"] = "".join(chr(x) for x in rom_hash) | 
					
						
							| 
									
										
										
										
											2020-03-10 00:38:29 +01:00
										 |  |  |     target = os.path.splitext(patch_file)[0] + ".sfc" | 
					
						
							| 
									
										
										
										
											2020-06-09 12:18:48 -07:00
										 |  |  |     return data["meta"], target, patched_data | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-14 21:03:17 +01:00
										 |  |  | def get_base_rom_data(game: str): | 
					
						
							|  |  |  |     if game == GAME_ALTTP: | 
					
						
							|  |  |  |         from worlds.alttp.Rom import get_base_rom_bytes | 
					
						
							|  |  |  |     elif game == "alttp":  # old version for A Link to the Past | 
					
						
							|  |  |  |         from worlds.alttp.Rom import get_base_rom_bytes | 
					
						
							|  |  |  |     elif game == GAME_SM: | 
					
						
							|  |  |  |         from worlds.sm.Rom import get_base_rom_bytes | 
					
						
							|  |  |  |     elif game == GAME_SOE: | 
					
						
							| 
									
										
										
										
											2022-03-31 05:08:15 +02:00
										 |  |  |         from worlds.soe.Patch import get_base_rom_path | 
					
						
							|  |  |  |         get_base_rom_bytes = lambda: bytes(read_rom(open(get_base_rom_path(), "rb"))) | 
					
						
							| 
									
										
										
										
											2022-03-15 08:55:57 -04:00
										 |  |  |     elif game == GAME_SMZ3: | 
					
						
							|  |  |  |         from worlds.smz3.Rom import get_base_rom_bytes | 
					
						
							| 
									
										
										
										
											2022-07-22 01:02:25 -04:00
										 |  |  |     elif game == GAME_DKC3: | 
					
						
							|  |  |  |         from worlds.dkc3.Rom import get_base_rom_bytes | 
					
						
							| 
									
										
										
										
											2021-11-14 21:03:17 +01:00
										 |  |  |     else: | 
					
						
							|  |  |  |         raise RuntimeError("Selected game for base rom not found.") | 
					
						
							|  |  |  |     return get_base_rom_bytes() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-09 12:18:48 -07:00
										 |  |  | def create_rom_file(patch_file: str) -> Tuple[dict, str]: | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  |     auto_handler = AutoPatchRegister.get_handler(patch_file) | 
					
						
							|  |  |  |     if auto_handler: | 
					
						
							|  |  |  |         handler: APDeltaPatch = auto_handler(patch_file) | 
					
						
							|  |  |  |         target = os.path.splitext(patch_file)[0]+handler.result_file_ending | 
					
						
							|  |  |  |         handler.patch(target) | 
					
						
							|  |  |  |         return {"server": handler.server, | 
					
						
							|  |  |  |                 "player": handler.player, | 
					
						
							|  |  |  |                 "player_name": handler.player_name}, target | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         data, target, patched_data = create_rom_bytes(patch_file) | 
					
						
							|  |  |  |         with open(target, "wb") as f: | 
					
						
							|  |  |  |             f.write(patched_data) | 
					
						
							|  |  |  |         return data, target | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-15 01:03:04 -07:00
										 |  |  | def update_patch_data(patch_data: bytes, server: str = "") -> bytes: | 
					
						
							|  |  |  |     data = Utils.parse_yaml(lzma.decompress(patch_data).decode("utf-8-sig")) | 
					
						
							|  |  |  |     data["meta"]["server"] = server | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |     bytes = generate_yaml(data["patch"], data["meta"], data["game"]) | 
					
						
							| 
									
										
										
										
											2020-04-15 01:03:04 -07:00
										 |  |  |     return lzma.compress(bytes) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-15 10:11:47 +02:00
										 |  |  | def load_bytes(path: str) -> bytes: | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |     with open(path, "rb") as f: | 
					
						
							|  |  |  |         return f.read() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def write_lzma(data: bytes, path: str): | 
					
						
							|  |  |  |     with lzma.LZMAFile(path, 'wb') as f: | 
					
						
							|  |  |  |         f.write(data) | 
					
						
							| 
									
										
										
										
											2020-03-17 19:16:11 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-16 09:05:11 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-12 14:36:34 +01:00
										 |  |  | def read_rom(stream, strip_header=True) -> bytearray: | 
					
						
							|  |  |  |     """Reads rom into bytearray and optionally strips off any smc header""" | 
					
						
							|  |  |  |     buffer = bytearray(stream.read()) | 
					
						
							|  |  |  |     if strip_header and len(buffer) % 0x400 == 0x200: | 
					
						
							|  |  |  |         return buffer[0x200:] | 
					
						
							|  |  |  |     return buffer | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-17 19:16:11 +01:00
										 |  |  | if __name__ == "__main__": | 
					
						
							| 
									
										
										
										
											2020-04-15 01:03:04 -07:00
										 |  |  |     host = Utils.get_public_ipv4() | 
					
						
							| 
									
										
										
										
											2020-04-15 10:11:47 +02:00
										 |  |  |     options = Utils.get_options()['server_options'] | 
					
						
							|  |  |  |     if options['host']: | 
					
						
							|  |  |  |         host = options['host'] | 
					
						
							| 
									
										
										
										
											2020-04-15 01:03:04 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-15 10:11:47 +02:00
										 |  |  |     address = f"{host}:{options['port']}" | 
					
						
							| 
									
										
										
										
											2020-04-15 01:03:04 -07:00
										 |  |  |     ziplock = threading.Lock() | 
					
						
							| 
									
										
										
										
											2020-04-15 10:11:47 +02:00
										 |  |  |     print(f"Host for patches to be created is {address}") | 
					
						
							| 
									
										
										
										
											2020-05-02 13:01:30 +02:00
										 |  |  |     with concurrent.futures.ThreadPoolExecutor() as pool: | 
					
						
							|  |  |  |         for rom in sys.argv: | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 if rom.endswith(".sfc"): | 
					
						
							|  |  |  |                     print(f"Creating patch for {rom}") | 
					
						
							|  |  |  |                     result = pool.submit(create_patch_file, rom, address) | 
					
						
							|  |  |  |                     result.add_done_callback(lambda task: print(f"Created patch {task.result()}")) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-19 08:26:31 +02:00
										 |  |  |                 elif rom.endswith(".apbp"): | 
					
						
							| 
									
										
										
										
											2020-05-02 13:01:30 +02:00
										 |  |  |                     print(f"Applying patch {rom}") | 
					
						
							|  |  |  |                     data, target = create_rom_file(rom) | 
					
						
							| 
									
										
										
										
											2022-01-20 04:19:58 +01:00
										 |  |  |                     #romfile, adjusted = Utils.get_adjuster_settings(target) | 
					
						
							|  |  |  |                     adjuster_settings = Utils.get_adjuster_settings(GAME_ALTTP) | 
					
						
							|  |  |  |                     adjusted = False | 
					
						
							|  |  |  |                     if adjuster_settings: | 
					
						
							|  |  |  |                         import pprint | 
					
						
							|  |  |  |                         from worlds.alttp.Rom import get_base_rom_path | 
					
						
							|  |  |  |                         adjuster_settings.rom = target | 
					
						
							|  |  |  |                         adjuster_settings.baserom = get_base_rom_path() | 
					
						
							|  |  |  |                         adjuster_settings.world = None | 
					
						
							|  |  |  |                         whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap", | 
					
						
							|  |  |  |                                         "uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes", | 
					
						
							|  |  |  |                                         "reduceflashing", "deathlink"} | 
					
						
							|  |  |  |                         printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist} | 
					
						
							|  |  |  |                         if hasattr(adjuster_settings, "sprite_pool"): | 
					
						
							|  |  |  |                             sprite_pool = {} | 
					
						
							|  |  |  |                             for sprite in getattr(adjuster_settings, "sprite_pool"): | 
					
						
							|  |  |  |                                 if sprite in sprite_pool: | 
					
						
							|  |  |  |                                     sprite_pool[sprite] += 1 | 
					
						
							|  |  |  |                                 else: | 
					
						
							|  |  |  |                                     sprite_pool[sprite] = 1 | 
					
						
							|  |  |  |                             if sprite_pool: | 
					
						
							|  |  |  |                                 printed_options["sprite_pool"] = sprite_pool | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                         adjust_wanted = str('no') | 
					
						
							|  |  |  |                         if not hasattr(adjuster_settings, 'auto_apply') or 'ask' in adjuster_settings.auto_apply: | 
					
						
							|  |  |  |                             adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n" | 
					
						
							|  |  |  |                                                   f"{pprint.pformat(printed_options)}\n" | 
					
						
							|  |  |  |                                                   f"Enter yes, no, always or never: ") | 
					
						
							|  |  |  |                         if adjuster_settings.auto_apply == 'never':  # never adjust, per user request | 
					
						
							|  |  |  |                             adjust_wanted = 'no' | 
					
						
							|  |  |  |                         elif adjuster_settings.auto_apply == 'always': | 
					
						
							|  |  |  |                             adjust_wanted = 'yes' | 
					
						
							|  |  |  |                          | 
					
						
							|  |  |  |                         if adjust_wanted and "never" in adjust_wanted: | 
					
						
							|  |  |  |                             adjuster_settings.auto_apply = 'never' | 
					
						
							|  |  |  |                             Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                         elif adjust_wanted and "always" in adjust_wanted: | 
					
						
							|  |  |  |                             adjuster_settings.auto_apply = 'always' | 
					
						
							|  |  |  |                             Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                         if adjust_wanted and adjust_wanted.startswith("y"): | 
					
						
							|  |  |  |                             if hasattr(adjuster_settings, "sprite_pool"): | 
					
						
							|  |  |  |                                 from LttPAdjuster import AdjusterWorld | 
					
						
							|  |  |  |                                 adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool")) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                             adjusted = True | 
					
						
							|  |  |  |                             import LttPAdjuster | 
					
						
							|  |  |  |                             _, romfile = LttPAdjuster.adjust(adjuster_settings) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                             if hasattr(adjuster_settings, "world"): | 
					
						
							|  |  |  |                                 delattr(adjuster_settings, "world") | 
					
						
							|  |  |  |                         else: | 
					
						
							|  |  |  |                             adjusted = False | 
					
						
							| 
									
										
										
										
											2020-06-07 12:04:33 -07:00
										 |  |  |                     if adjusted: | 
					
						
							|  |  |  |                         try: | 
					
						
							| 
									
										
										
										
											2022-01-20 04:19:58 +01:00
										 |  |  |                             shutil.move(romfile, target) | 
					
						
							| 
									
										
										
										
											2020-06-07 12:04:33 -07:00
										 |  |  |                             romfile = target | 
					
						
							|  |  |  |                         except Exception as e: | 
					
						
							|  |  |  |                             print(e) | 
					
						
							|  |  |  |                     print(f"Created rom {romfile if adjusted else target}.") | 
					
						
							| 
									
										
										
										
											2020-05-02 13:01:30 +02:00
										 |  |  |                     if 'server' in data: | 
					
						
							| 
									
										
										
										
											2020-06-07 12:04:33 -07:00
										 |  |  |                         Utils.persistent_store("servers", data['hash'], data['server']) | 
					
						
							| 
									
										
										
										
											2020-05-02 13:01:30 +02:00
										 |  |  |                         print(f"Host is {data['server']}") | 
					
						
							| 
									
										
										
										
											2021-11-12 08:00:11 -05:00
										 |  |  |                 elif rom.endswith(".apm3"): | 
					
						
							|  |  |  |                     print(f"Applying patch {rom}") | 
					
						
							|  |  |  |                     data, target = create_rom_file(rom) | 
					
						
							|  |  |  |                     print(f"Created rom {target}.") | 
					
						
							|  |  |  |                     if 'server' in data: | 
					
						
							|  |  |  |                         Utils.persistent_store("servers", data['hash'], data['server']) | 
					
						
							|  |  |  |                         print(f"Host is {data['server']}") | 
					
						
							| 
									
										
										
										
											2022-03-15 08:55:57 -04:00
										 |  |  |                 elif rom.endswith(".apsmz"): | 
					
						
							|  |  |  |                     print(f"Applying patch {rom}") | 
					
						
							|  |  |  |                     data, target = create_rom_file(rom) | 
					
						
							|  |  |  |                     print(f"Created rom {target}.") | 
					
						
							|  |  |  |                     if 'server' in data: | 
					
						
							|  |  |  |                         Utils.persistent_store("servers", data['hash'], data['server']) | 
					
						
							|  |  |  |                         print(f"Host is {data['server']}") | 
					
						
							| 
									
										
										
										
											2022-07-22 01:02:25 -04:00
										 |  |  |                 elif rom.endswith(".apdkc3"): | 
					
						
							|  |  |  |                     print(f"Applying patch {rom}") | 
					
						
							|  |  |  |                     data, target = create_rom_file(rom) | 
					
						
							|  |  |  |                     print(f"Created rom {target}.") | 
					
						
							|  |  |  |                     if 'server' in data: | 
					
						
							|  |  |  |                         Utils.persistent_store("servers", data['hash'], data['server']) | 
					
						
							|  |  |  |                         print(f"Host is {data['server']}") | 
					
						
							| 
									
										
										
										
											2020-06-07 12:04:33 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-02 13:01:30 +02:00
										 |  |  |                 elif rom.endswith(".zip"): | 
					
						
							|  |  |  |                     print(f"Updating host in patch files contained in {rom}") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |                     def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str): | 
					
						
							| 
									
										
										
										
											2020-05-02 13:01:30 +02:00
										 |  |  |                         data = zfr.read(zfinfo) | 
					
						
							| 
									
										
										
										
											2022-07-22 01:02:25 -04:00
										 |  |  |                         if zfinfo.filename.endswith(".apbp") or \ | 
					
						
							|  |  |  |                            zfinfo.filename.endswith(".apm3") or \ | 
					
						
							|  |  |  |                            zfinfo.filename.endswith(".apdkc3"): | 
					
						
							| 
									
										
										
										
											2020-05-02 13:01:30 +02:00
										 |  |  |                             data = update_patch_data(data, server) | 
					
						
							|  |  |  |                         with ziplock: | 
					
						
							|  |  |  |                             zfw.writestr(zfinfo, data) | 
					
						
							|  |  |  |                         return zfinfo.filename | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-03-17 19:16:11 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-15 01:03:04 -07:00
										 |  |  |                     futures = [] | 
					
						
							|  |  |  |                     with zipfile.ZipFile(rom, "r") as zfr: | 
					
						
							|  |  |  |                         updated_zip = os.path.splitext(rom)[0] + "_updated.zip" | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  |                         with zipfile.ZipFile(updated_zip, "w", compression=zipfile.ZIP_DEFLATED, | 
					
						
							|  |  |  |                                              compresslevel=9) as zfw: | 
					
						
							| 
									
										
										
										
											2020-04-15 01:03:04 -07:00
										 |  |  |                             for zfname in zfr.namelist(): | 
					
						
							| 
									
										
										
										
											2020-04-15 10:11:47 +02:00
										 |  |  |                                 futures.append(pool.submit(_handle_zip_file_entry, zfr.getinfo(zfname), address)) | 
					
						
							| 
									
										
										
										
											2020-04-15 01:03:04 -07:00
										 |  |  |                             for future in futures: | 
					
						
							|  |  |  |                                 print(f"File {future.result()} added to {os.path.split(updated_zip)[1]}") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-02 13:01:30 +02:00
										 |  |  |             except: | 
					
						
							|  |  |  |                 import traceback | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-02 13:01:30 +02:00
										 |  |  |                 traceback.print_exc() | 
					
						
							| 
									
										
										
										
											2022-03-18 04:53:09 +01:00
										 |  |  |                 input("Press enter to close.") |