| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | import bsdiff4 | 
					
						
							|  |  |  | import yaml | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import lzma | 
					
						
							| 
									
										
										
										
											2020-03-07 00:07:45 +01:00
										 |  |  | import hashlib | 
					
						
							| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2020-03-28 21:55:41 +01:00
										 |  |  | from typing import Tuple, Optional | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | import Utils | 
					
						
							| 
									
										
										
										
											2020-10-24 05:38:56 +02:00
										 |  |  | from worlds.alttp.Rom import JAP10HASH | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-05-14 15:25:57 +02:00
										 |  |  | current_patch_version = 2 | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-26 15:14:30 +02:00
										 |  |  | def get_base_rom_path(file_name: str = "") -> str: | 
					
						
							|  |  |  |     options = Utils.get_options() | 
					
						
							|  |  |  |     if not file_name: | 
					
						
							| 
									
										
										
										
											2021-04-01 11:40:58 +02:00
										 |  |  |         file_name = options["lttp_options"]["rom_file"] | 
					
						
							| 
									
										
										
										
											2020-04-26 15:14:30 +02:00
										 |  |  |     if not os.path.exists(file_name): | 
					
						
							|  |  |  |         file_name = Utils.local_path(file_name) | 
					
						
							|  |  |  |     return file_name | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-05 02:06:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-15 10:11:47 +02:00
										 |  |  | def get_base_rom_bytes(file_name: str = "") -> bytes: | 
					
						
							| 
									
										
										
										
											2020-10-24 05:38:56 +02:00
										 |  |  |     from worlds.alttp.Rom import read_rom | 
					
						
							| 
									
										
										
										
											2020-04-26 15:14:30 +02:00
										 |  |  |     base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |     if not base_rom_bytes: | 
					
						
							| 
									
										
										
										
											2020-04-26 15:14:30 +02:00
										 |  |  |         file_name = get_base_rom_path(file_name) | 
					
						
							| 
									
										
										
										
											2020-03-07 00:30:14 +01:00
										 |  |  |         base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) | 
					
						
							| 
									
										
										
										
											2020-03-07 00:07:45 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         basemd5 = hashlib.md5() | 
					
						
							|  |  |  |         basemd5.update(base_rom_bytes) | 
					
						
							|  |  |  |         if JAP10HASH != basemd5.hexdigest(): | 
					
						
							| 
									
										
										
										
											2020-03-08 02:18:55 +01:00
										 |  |  |             raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. ' | 
					
						
							|  |  |  |                             'Get the correct game and version, then dump it') | 
					
						
							| 
									
										
										
										
											2020-04-26 15:14:30 +02:00
										 |  |  |         get_base_rom_bytes.base_rom_bytes = base_rom_bytes | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |     return base_rom_bytes | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-16 09:05:11 -07:00
										 |  |  | def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: | 
					
						
							|  |  |  |     patch = yaml.dump({"meta": metadata, | 
					
						
							| 
									
										
										
										
											2020-07-05 02:06:00 +02:00
										 |  |  |                        "patch": patch, | 
					
						
							| 
									
										
										
										
											2021-05-14 15:25:57 +02:00
										 |  |  |                        "game": "A Link to the Past", | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  |                        # minimum version of patch system expected for patching to be successful | 
					
						
							| 
									
										
										
										
											2021-05-14 15:25:57 +02:00
										 |  |  |                        "compatible_version": 1, | 
					
						
							| 
									
										
										
										
											2020-10-24 05:38:56 +02:00
										 |  |  |                        "version": current_patch_version, | 
					
						
							| 
									
										
										
										
											2020-07-05 02:06:00 +02:00
										 |  |  |                        "base_checksum": JAP10HASH}) | 
					
						
							| 
									
										
										
										
											2020-04-16 09:05:11 -07:00
										 |  |  |     return patch.encode(encoding="utf-8-sig") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-15 10:11:47 +02:00
										 |  |  | def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes: | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |     if metadata is None: | 
					
						
							|  |  |  |         metadata = {} | 
					
						
							|  |  |  |     patch = bsdiff4.diff(get_base_rom_bytes(), rom) | 
					
						
							| 
									
										
										
										
											2020-04-16 09:05:11 -07:00
										 |  |  |     return generate_yaml(patch, metadata) | 
					
						
							| 
									
										
										
										
											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, | 
					
						
							|  |  |  |                       player: int = 0, player_name: str = "") -> str: | 
					
						
							|  |  |  |     meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise | 
					
						
							|  |  |  |             "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-05-14 15:25:57 +02:00
										 |  |  |                            meta) | 
					
						
							| 
									
										
										
										
											2020-10-19 08:26:31 +02:00
										 |  |  |     target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp" | 
					
						
							| 
									
										
										
										
											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-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.") | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |     patched_data = bsdiff4.patch(get_base_rom_bytes(), 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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-09 12:18:48 -07:00
										 |  |  | def create_rom_file(patch_file: str) -> Tuple[dict, str]: | 
					
						
							|  |  |  |     data, target, patched_data = create_rom_bytes(patch_file) | 
					
						
							| 
									
										
										
										
											2020-03-10 00:38:29 +01:00
										 |  |  |     with open(target, "wb") as f: | 
					
						
							| 
									
										
										
										
											2020-03-06 00:48:23 +01:00
										 |  |  |         f.write(patched_data) | 
					
						
							| 
									
										
										
										
											2020-06-09 12:18:48 -07:00
										 |  |  |     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 | 
					
						
							| 
									
										
										
										
											2020-04-16 09:05:11 -07:00
										 |  |  |     bytes = generate_yaml(data["patch"], data["meta"]) | 
					
						
							| 
									
										
										
										
											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
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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) | 
					
						
							| 
									
										
										
										
											2020-06-07 12:04:33 -07:00
										 |  |  |                     romfile, adjusted = Utils.get_adjuster_settings(target) | 
					
						
							|  |  |  |                     if adjusted: | 
					
						
							|  |  |  |                         try: | 
					
						
							|  |  |  |                             os.replace(romfile, target) | 
					
						
							|  |  |  |                             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-01-03 14:32:32 +01:00
										 |  |  |                 elif rom.endswith(".archipelago"): | 
					
						
							| 
									
										
										
										
											2020-06-07 12:04:33 -07:00
										 |  |  |                     import json | 
					
						
							|  |  |  |                     import zlib | 
					
						
							| 
									
										
										
										
											2021-01-03 14:32:32 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-07 12:04:33 -07:00
										 |  |  |                     with open(rom, 'rb') as fr: | 
					
						
							| 
									
										
										
										
											2020-07-25 22:40:24 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-07 12:04:33 -07:00
										 |  |  |                         multidata = zlib.decompress(fr.read()).decode("utf-8") | 
					
						
							|  |  |  |                         with open(rom + '.txt', 'w') as fw: | 
					
						
							|  |  |  |                             fw.write(multidata) | 
					
						
							|  |  |  |                         multidata = json.loads(multidata) | 
					
						
							| 
									
										
										
										
											2020-07-25 22:40:24 +02:00
										 |  |  |                         for romname in multidata['roms']: | 
					
						
							|  |  |  |                             Utils.persistent_store("servers", "".join(chr(byte) for byte in romname[2]), address) | 
					
						
							|  |  |  |                         from Utils import get_options | 
					
						
							| 
									
										
										
										
											2021-01-17 06:54:38 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-25 22:40:24 +02:00
										 |  |  |                         multidata["server_options"] = get_options()["server_options"] | 
					
						
							|  |  |  |                         multidata = zlib.compress(json.dumps(multidata).encode("utf-8"), 9) | 
					
						
							| 
									
										
										
										
											2021-01-03 14:32:32 +01:00
										 |  |  |                         with open(rom + "_updated.archipelago", 'wb') as f: | 
					
						
							| 
									
										
										
										
											2020-07-25 22:40:24 +02:00
										 |  |  |                             f.write(multidata) | 
					
						
							| 
									
										
										
										
											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) | 
					
						
							| 
									
										
										
										
											2020-10-19 08:26:31 +02:00
										 |  |  |                         if zfinfo.filename.endswith(".apbp"): | 
					
						
							| 
									
										
										
										
											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() | 
					
						
							|  |  |  |                 input("Press enter to close.") |