322 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			322 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | import hashlib | ||
|  | import json | ||
|  | import os | ||
|  | import zipfile | ||
|  | from typing import Optional, Any | ||
|  | 
 | ||
|  | import Utils | ||
|  | from .Locations import AdventureLocation, LocationData | ||
|  | from Utils import OptionsType | ||
|  | from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer | ||
|  | from itertools import chain | ||
|  | 
 | ||
|  | import bsdiff4 | ||
|  | 
 | ||
|  | ADVENTUREHASH: str = "157bddb7192754a45372be196797f284" | ||
|  | 
 | ||
|  | 
 | ||
|  | class AdventureAutoCollectLocation: | ||
|  |     short_location_id: int = 0 | ||
|  |     room_id: int = 0 | ||
|  | 
 | ||
|  |     def __init__(self, short_location_id: int, room_id: int): | ||
|  |         self.short_location_id = short_location_id | ||
|  |         self.room_id = room_id | ||
|  | 
 | ||
|  |     def get_dict(self): | ||
|  |         return { | ||
|  |             "short_location_id": self.short_location_id, | ||
|  |             "room_id": self.room_id, | ||
|  |         } | ||
|  | 
 | ||
|  | 
 | ||
|  | class AdventureForeignItemInfo: | ||
|  |     short_location_id: int = 0 | ||
|  |     room_id: int = 0 | ||
|  |     room_x: int = 0 | ||
|  |     room_y: int = 0 | ||
|  | 
 | ||
|  |     def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int): | ||
|  |         self.short_location_id = short_location_id | ||
|  |         self.room_id = room_id | ||
|  |         self.room_x = room_x | ||
|  |         self.room_y = room_y | ||
|  | 
 | ||
|  |     def get_dict(self): | ||
|  |         return { | ||
|  |             "short_location_id": self.short_location_id, | ||
|  |             "room_id": self.room_id, | ||
|  |             "room_x": self.room_x, | ||
|  |             "room_y": self.room_y, | ||
|  |         } | ||
|  | 
 | ||
|  | 
 | ||
|  | class BatNoTouchLocation: | ||
|  |     short_location_id: int | ||
|  |     room_id: int | ||
|  |     room_x: int | ||
|  |     room_y: int | ||
|  |     local_item: int | ||
|  | 
 | ||
|  |     def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int, local_item: int = None): | ||
|  |         self.short_location_id = short_location_id | ||
|  |         self.room_id = room_id | ||
|  |         self.room_x = room_x | ||
|  |         self.room_y = room_y | ||
|  |         self.local_item = local_item | ||
|  | 
 | ||
|  |     def get_dict(self): | ||
|  |         ret_dict = { | ||
|  |             "short_location_id": self.short_location_id, | ||
|  |             "room_id": self.room_id, | ||
|  |             "room_x": self.room_x, | ||
|  |             "room_y": self.room_y, | ||
|  |         } | ||
|  |         if self.local_item is not None: | ||
|  |             ret_dict["local_item"] = self.local_item | ||
|  |         else: | ||
|  |             ret_dict["local_item"] = 255 | ||
|  |         return ret_dict | ||
|  | 
 | ||
|  | 
 | ||
|  | class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister): | ||
|  |     hash = ADVENTUREHASH | ||
|  |     game = "Adventure" | ||
|  |     patch_file_ending = ".apadvn" | ||
|  |     zip_version: int = 2 | ||
|  | 
 | ||
|  |     # locations: [], autocollect: [], seed_name: bytes, | ||
|  |     def __init__(self, *args: Any, **kwargs: Any) -> None: | ||
|  |         patch_only = True | ||
|  |         if "autocollect" in kwargs: | ||
|  |             patch_only = False | ||
|  |             self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y) | ||
|  |                                   for loc in kwargs["locations"]] | ||
|  | 
 | ||
|  |             self.autocollect_items: [AdventureAutoCollectLocation] = kwargs["autocollect"] | ||
|  |             self.seedName: bytes = kwargs["seed_name"] | ||
|  |             self.local_item_locations: {} = kwargs["local_item_locations"] | ||
|  |             self.dragon_speed_reducer_info: {} = kwargs["dragon_speed_reducer_info"] | ||
|  |             self.diff_a_mode: int = kwargs["diff_a_mode"] | ||
|  |             self.diff_b_mode: int = kwargs["diff_b_mode"] | ||
|  |             self.bat_logic: int = kwargs["bat_logic"] | ||
|  |             self.bat_no_touch_locations: [LocationData] = kwargs["bat_no_touch_locations"] | ||
|  |             self.rom_deltas: {int, int} = kwargs["rom_deltas"] | ||
|  |             del kwargs["locations"] | ||
|  |             del kwargs["autocollect"] | ||
|  |             del kwargs["seed_name"] | ||
|  |             del kwargs["local_item_locations"] | ||
|  |             del kwargs["dragon_speed_reducer_info"] | ||
|  |             del kwargs["diff_a_mode"] | ||
|  |             del kwargs["diff_b_mode"] | ||
|  |             del kwargs["bat_logic"] | ||
|  |             del kwargs["bat_no_touch_locations"] | ||
|  |             del kwargs["rom_deltas"] | ||
|  |         super(AdventureDeltaPatch, self).__init__(*args, **kwargs) | ||
|  | 
 | ||
|  |     def write_contents(self, opened_zipfile: zipfile.ZipFile): | ||
|  |         super(AdventureDeltaPatch, self).write_contents(opened_zipfile) | ||
|  |         # write Delta | ||
|  |         opened_zipfile.writestr("zip_version", | ||
|  |                                 self.zip_version.to_bytes(1, "little"), | ||
|  |                                 compress_type=zipfile.ZIP_STORED) | ||
|  |         if self.foreign_items is not None: | ||
|  |             loc_bytes = [] | ||
|  |             for foreign_item in self.foreign_items: | ||
|  |                 loc_bytes.append(foreign_item.short_location_id) | ||
|  |                 loc_bytes.append(foreign_item.room_id) | ||
|  |                 loc_bytes.append(foreign_item.room_x) | ||
|  |                 loc_bytes.append(foreign_item.room_y) | ||
|  |             opened_zipfile.writestr("adventure_locations", | ||
|  |                                     bytes(loc_bytes), | ||
|  |                                     compress_type=zipfile.ZIP_LZMA) | ||
|  |         if self.autocollect_items is not None: | ||
|  |             loc_bytes = [] | ||
|  |             for item in self.autocollect_items: | ||
|  |                 loc_bytes.append(item.short_location_id) | ||
|  |                 loc_bytes.append(item.room_id) | ||
|  |             opened_zipfile.writestr("adventure_autocollect", | ||
|  |                                     bytes(loc_bytes), | ||
|  |                                     compress_type=zipfile.ZIP_LZMA) | ||
|  |         if self.player_name is not None: | ||
|  |             opened_zipfile.writestr("player", | ||
|  |                                     self.player_name,  # UTF-8 | ||
|  |                                     compress_type=zipfile.ZIP_STORED) | ||
|  |         if self.seedName is not None: | ||
|  |             opened_zipfile.writestr("seedName", | ||
|  |                                     self.seedName, | ||
|  |                                     compress_type=zipfile.ZIP_STORED) | ||
|  |         if self.local_item_locations is not None: | ||
|  |             opened_zipfile.writestr("local_item_locations", | ||
|  |                                     json.dumps(self.local_item_locations), | ||
|  |                                     compress_type=zipfile.ZIP_LZMA) | ||
|  |         if self.dragon_speed_reducer_info is not None: | ||
|  |             opened_zipfile.writestr("dragon_speed_reducer_info", | ||
|  |                                     json.dumps(self.dragon_speed_reducer_info), | ||
|  |                                     compress_type=zipfile.ZIP_LZMA) | ||
|  |         if self.diff_a_mode is not None: | ||
|  |             opened_zipfile.writestr("diff_a_mode", | ||
|  |                                     self.diff_a_mode.to_bytes(1, "little"), | ||
|  |                                     compress_type=zipfile.ZIP_STORED) | ||
|  |         if self.diff_b_mode is not None: | ||
|  |             opened_zipfile.writestr("diff_b_mode", | ||
|  |                                     self.diff_b_mode.to_bytes(1, "little"), | ||
|  |                                     compress_type=zipfile.ZIP_STORED) | ||
|  |         if self.bat_logic is not None: | ||
|  |             opened_zipfile.writestr("bat_logic", | ||
|  |                                     self.bat_logic.to_bytes(1, "little"), | ||
|  |                                     compress_type=zipfile.ZIP_STORED) | ||
|  |         if self.bat_no_touch_locations is not None: | ||
|  |             loc_bytes = [] | ||
|  |             for loc in self.bat_no_touch_locations: | ||
|  |                 loc_bytes.append(loc.short_location_id)  # used for AP items managed by script | ||
|  |                 loc_bytes.append(loc.room_id)  # used for local items placed in rom | ||
|  |                 loc_bytes.append(loc.room_x) | ||
|  |                 loc_bytes.append(loc.room_y) | ||
|  |                 loc_bytes.append(0xff if loc.local_item is None else loc.local_item) | ||
|  |             opened_zipfile.writestr("bat_no_touch_locations", | ||
|  |                                     bytes(loc_bytes), | ||
|  |                                     compress_type=zipfile.ZIP_LZMA) | ||
|  |         if self.rom_deltas is not None: | ||
|  |             # this is not an efficient way to do this AT ALL, but Adventure's data is so tiny it shouldn't matter | ||
|  |             # if you're looking at doing something like this for another game, consider encoding your rom changes | ||
|  |             # in a more efficient way | ||
|  |             opened_zipfile.writestr("rom_deltas", | ||
|  |                                     json.dumps(self.rom_deltas), | ||
|  |                                     compress_type=zipfile.ZIP_LZMA) | ||
|  | 
 | ||
|  |     def read_contents(self, opened_zipfile: zipfile.ZipFile): | ||
|  |         super(AdventureDeltaPatch, self).read_contents(opened_zipfile) | ||
|  |         self.foreign_items = AdventureDeltaPatch.read_foreign_items(opened_zipfile) | ||
|  |         self.autocollect_items = AdventureDeltaPatch.read_autocollect_items(opened_zipfile) | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def get_source_data(cls) -> bytes: | ||
|  |         return get_base_rom_bytes() | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def check_version(cls, opened_zipfile: zipfile.ZipFile) -> bool: | ||
|  |         version_bytes = opened_zipfile.read("zip_version") | ||
|  |         version = 0 | ||
|  |         if version_bytes is not None: | ||
|  |             version = int.from_bytes(version_bytes, "little") | ||
|  |         if version != cls.zip_version: | ||
|  |             return False | ||
|  |         return True | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def read_rom_info(cls, opened_zipfile: zipfile.ZipFile) -> (bytes, bytes, str): | ||
|  |         seedbytes: bytes = opened_zipfile.read("seedName") | ||
|  |         namebytes: bytes = opened_zipfile.read("player") | ||
|  |         namestr: str = namebytes.decode("utf-8") | ||
|  |         return seedbytes, namestr | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def read_difficulty_switch_info(cls, opened_zipfile: zipfile.ZipFile) -> (int, int): | ||
|  |         diff_a_bytes = opened_zipfile.read("diff_a_mode") | ||
|  |         diff_b_bytes = opened_zipfile.read("diff_b_mode") | ||
|  |         diff_a = 0 | ||
|  |         diff_b = 0 | ||
|  |         if diff_a_bytes is not None: | ||
|  |             diff_a = int.from_bytes(diff_a_bytes, "little") | ||
|  |         if diff_b_bytes is not None: | ||
|  |             diff_b = int.from_bytes(diff_b_bytes, "little") | ||
|  |         return diff_a, diff_b | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def read_bat_logic(cls, opened_zipfile: zipfile.ZipFile) -> int: | ||
|  |         bat_logic = opened_zipfile.read("bat_logic") | ||
|  |         if bat_logic is None: | ||
|  |             return 0 | ||
|  |         return int.from_bytes(bat_logic, "little") | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def read_foreign_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]: | ||
|  |         foreign_items = [] | ||
|  |         readbytes: bytes = opened_zipfile.read("adventure_locations") | ||
|  |         bytelist = list(readbytes) | ||
|  |         for i in range(round(len(bytelist) / 4)): | ||
|  |             offset = i * 4 | ||
|  |             foreign_items.append(AdventureForeignItemInfo(bytelist[offset], | ||
|  |                                                           bytelist[offset + 1], | ||
|  |                                                           bytelist[offset + 2], | ||
|  |                                                           bytelist[offset + 3])) | ||
|  |         return foreign_items | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def read_bat_no_touch(cls, opened_zipfile: zipfile.ZipFile) -> [BatNoTouchLocation]: | ||
|  |         locations = [] | ||
|  |         readbytes: bytes = opened_zipfile.read("bat_no_touch_locations") | ||
|  |         bytelist = list(readbytes) | ||
|  |         for i in range(round(len(bytelist) / 5)): | ||
|  |             offset = i * 5 | ||
|  |             locations.append(BatNoTouchLocation(bytelist[offset], | ||
|  |                                                 bytelist[offset + 1], | ||
|  |                                                 bytelist[offset + 2], | ||
|  |                                                 bytelist[offset + 3], | ||
|  |                                                 bytelist[offset + 4])) | ||
|  |         return locations | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def read_autocollect_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]: | ||
|  |         autocollect_items = [] | ||
|  |         readbytes: bytes = opened_zipfile.read("adventure_autocollect") | ||
|  |         bytelist = list(readbytes) | ||
|  |         for i in range(round(len(bytelist) / 2)): | ||
|  |             offset = i * 2 | ||
|  |             autocollect_items.append(AdventureAutoCollectLocation(bytelist[offset], bytelist[offset + 1])) | ||
|  |         return autocollect_items | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def read_local_item_locations(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]: | ||
|  |         readbytes: bytes = opened_zipfile.read("local_item_locations") | ||
|  |         readstr: str = readbytes.decode() | ||
|  |         return json.loads(readstr) | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def read_dragon_speed_info(cls, opened_zipfile: zipfile.ZipFile) -> {}: | ||
|  |         readbytes: bytes = opened_zipfile.read("dragon_speed_reducer_info") | ||
|  |         readstr: str = readbytes.decode() | ||
|  |         return json.loads(readstr) | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def read_rom_deltas(cls, opened_zipfile: zipfile.ZipFile) -> {int, int}: | ||
|  |         readbytes: bytes = opened_zipfile.read("rom_deltas") | ||
|  |         readstr: str = readbytes.decode() | ||
|  |         return json.loads(readstr) | ||
|  | 
 | ||
|  |     @classmethod | ||
|  |     def apply_rom_deltas(cls, base_bytes: bytes, rom_deltas: {int, int}) -> bytearray: | ||
|  |         rom_bytes = bytearray(base_bytes) | ||
|  |         for offset, value in rom_deltas.items(): | ||
|  |             int_offset = int(offset) | ||
|  |             rom_bytes[int_offset:int_offset+1] = int.to_bytes(value, 1, "little") | ||
|  |         return rom_bytes | ||
|  | 
 | ||
|  | 
 | ||
|  | def apply_basepatch(base_rom_bytes: bytes) -> bytes: | ||
|  |     with open(os.path.join(os.path.dirname(__file__), "../../data/adventure_basepatch.bsdiff4"), "rb") as basepatch: | ||
|  |         delta: bytes = basepatch.read() | ||
|  |     return bsdiff4.patch(base_rom_bytes, delta) | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_base_rom_bytes(file_name: str = "") -> bytes: | ||
|  |     file_name = get_base_rom_path(file_name) | ||
|  |     with open(file_name, "rb") as file: | ||
|  |         base_rom_bytes = bytes(file.read()) | ||
|  |     basemd5 = hashlib.md5() | ||
|  |     basemd5.update(base_rom_bytes) | ||
|  |     if ADVENTUREHASH != basemd5.hexdigest(): | ||
|  |         raise Exception(f"Supplied Base Rom does not match known MD5 for Adventure. " | ||
|  |                         "Get the correct game and version, then dump it") | ||
|  |     return base_rom_bytes | ||
|  | 
 | ||
|  | 
 | ||
|  | def get_base_rom_path(file_name: str = "") -> str: | ||
|  |     options: OptionsType = Utils.get_options() | ||
|  |     if not file_name: | ||
|  |         file_name = options["adventure_options"]["rom_file"] | ||
|  |     if not os.path.exists(file_name): | ||
|  |         file_name = Utils.user_path(file_name) | ||
|  |     return file_name |