From c9dec5bd1a84b23449b16bffe00933ddbb2b1c41 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Fri, 5 Apr 2024 20:24:46 -0700 Subject: [PATCH] 0.2.0 changes --- worlds/mmx3/Names/ItemName.py | 4 +- worlds/mmx3/Options.py | 23 ++++ worlds/mmx3/Rom.py | 174 ++++++++++++------------ worlds/mmx3/__init__.py | 31 ++--- worlds/mmx3/data/mmx3_basepatch.bsdiff4 | Bin 4177 -> 4125 bytes worlds/mmx3/data/mmx3_legacy.bsdiff4 | Bin 4464 -> 0 bytes 6 files changed, 124 insertions(+), 108 deletions(-) delete mode 100644 worlds/mmx3/data/mmx3_legacy.bsdiff4 diff --git a/worlds/mmx3/Names/ItemName.py b/worlds/mmx3/Names/ItemName.py index 8d6b7ae7f401..1e7b41dcb41a 100644 --- a/worlds/mmx3/Names/ItemName.py +++ b/worlds/mmx3/Names/ItemName.py @@ -1,7 +1,7 @@ # Stages stage_toxic_seahorse = "Toxic Seahorse Access Codes" -stage_volt_catfish = "Volt Catfish Stage Access Codes" -stage_tunnel_rhino = "Tunnel Rhino Stage Access Codes" +stage_volt_catfish = "Volt Catfish Access Codes" +stage_tunnel_rhino = "Tunnel Rhino Access Codes" stage_blizzard_buffalo = "Blizzard Buffalo Access Codes" stage_crush_crawfish = "Crush Crawfish Access Codes" stage_neon_tiger = "Neon Tiger Access Codes" diff --git a/worlds/mmx3/Options.py b/worlds/mmx3/Options.py index 61c2714b574a..784295d45738 100644 --- a/worlds/mmx3/Options.py +++ b/worlds/mmx3/Options.py @@ -3,6 +3,28 @@ from Options import Choice, Range, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool +class ROMVersion(Choice): + """ + Which ROM version will be used to generate a patch. + Note: Legacy collection users must dump the game themselves from the collection. + """ + display_name = "ROM Version" + option_snes = 0 + option_legacy_collection = 1 + default = 1 + +class LogicZSaber(Choice): + """ + Adds the Z-Saber to the game's logic. + """ + display_name = "Z-Saber Logic" + option_not_required = 5 + option_required_for_lab_1 = 0 + option_required_for_lab_2 = 1 + option_required_for_lab_3 = 2 + option_required_for_doppler = 3 + option_only_sigma = 4 + default = 2 class EnergyLink(DefaultOnToggle): """ @@ -231,6 +253,7 @@ class MMX3Options(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool death_link: DeathLink energy_link: EnergyLink + rom_version: ROMVersion disable_charge_freeze: DisableChargeFreeze starting_life_count: StartingLifeCount pickupsanity: PickupSanity diff --git a/worlds/mmx3/Rom.py b/worlds/mmx3/Rom.py index 3d7c91c4c5a7..26cc40591c57 100644 --- a/worlds/mmx3/Rom.py +++ b/worlds/mmx3/Rom.py @@ -1,12 +1,14 @@ -import Utils -from worlds.AutoWorld import World -from worlds.Files import APDeltaPatch - +import typing import bsdiff4 +import Utils import hashlib import os +from typing import Optional, TYPE_CHECKING from pkgutil import get_data +from worlds.AutoWorld import World +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes + HASH_US = 'cfe8c11f0dce19e4fa5f3fd75775e47c' HASH_LEGACY = 'ff683b75e75e9b59f0c713c7512a016b' @@ -56,96 +58,96 @@ #0xBD0033: ["large weapon refill"] } -class MMX3DeltaPatch(APDeltaPatch): +class MMX3ProcedurePatch(APProcedurePatch, APTokenMixin): hash = [HASH_US, HASH_LEGACY] game = "Mega Man X3" patch_file_ending = ".apmmx3" + result_file_ending = ".sfc" + name: bytearray + procedure = [ + ("apply_tokens", ["token_patch.bin"]), + ("apply_bsdiff4", ["mmx3_basepatch.bsdiff4"]), + ] @classmethod def get_source_data(cls) -> bytes: return get_base_rom_bytes() -class LocalRom: - - def __init__(self, file, patch=True, vanillaRom=None, name=None, hash=None): - self.name = name - self.hash = hash - self.orig_buffer = None - - with open(file, 'rb') as stream: - self.buffer = Utils.read_snes_rom(stream) - - def read_bit(self, address: int, bit_number: int) -> bool: - bitflag = (1 << bit_number) - return ((self.buffer[address] & bitflag) != 0) - - def read_byte(self, address: int) -> int: - return self.buffer[address] - - def read_bytes(self, startaddress: int, length: int) -> bytes: - return self.buffer[startaddress:startaddress + length] - - def write_byte(self, address: int, value: int): - self.buffer[address] = value - - def write_bytes(self, startaddress: int, values): - self.buffer[startaddress:startaddress + len(values)] = values - - def write_to_file(self, file): - with open(file, 'wb') as outfile: - outfile.write(self.buffer) - - def read_from_file(self, file): - with open(file, 'rb') as stream: - self.buffer = bytearray(stream.read()) - - def apply_patch(self, patch: bytes): - self.buffer = bytearray(bsdiff4.patch(bytes(self.buffer), patch)) - - def write_crc(self): - crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF - inv = crc ^ 0xFFFF - self.write_bytes(0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF]) + def write_byte(self, offset, value): + self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little")) + def write_bytes(self, offset, value: typing.Iterable[int]): + self.write_token(APTokenTypes.WRITE, offset, bytes(value)) -def patch_rom(world: World, rom, player): +def patch_rom(world: World, patch: MMX3ProcedurePatch): from Utils import __version__ - # Apply base patch - rom.apply_patch(get_data(__name__, os.path.join("data", "mmx3_basepatch.bsdiff4"))) + # Prepare some ROM locations to receive the basepatch output + patch.write_bytes(0x00638, bytearray([0x85,0xB4,0x8A])) + patch.write_bytes(0x0065A, bytearray([0x85,0xB4,0x8A])) + patch.write_bytes(0x00EFD, bytearray([0xA9,0x10,0x20,0x91,0x86])) + patch.write_bytes(0x00F36, bytearray([0xA5,0xAD,0x89,0x08,0xF0,0x09,0xA5,0x3C, + 0x3A,0x10,0x11,0xA9,0x02,0x80,0x0D,0x89, + 0x24,0xF0,0x24,0xA5,0x3C,0x1A,0xC9,0x03, + 0xD0,0x02,0xA9,0x00,0x85,0x3C,0xAA,0xBD, + 0xF8,0x87,0x8D,0xE0,0x09,0xA5,0x3C,0x18, + 0x69,0x10,0x20,0x91,0x86,0xA9,0xF0,0x85, + 0x3B,0xA9,0x1C,0x22,0x2B,0x80,0x01])) + patch.write_bytes(0x00FF2, bytearray([0x9C,0xD9,0x09,0x9C,0xDA,0x09])) + patch.write_bytes(0x01034, bytearray([0x64,0x38,0x64,0x39])) + patch.write_bytes(0x03118, bytearray([0xA9,0x08,0x85,0xD5])) + patch.write_bytes(0x06A0B, bytearray([0x62,0x81])) + patch.write_bytes(0x06C4C, bytearray([0x85,0x00,0x0A])) + patch.write_bytes(0x06E76, bytearray([0x9F,0xCB,0xFF,0x7E])) + patch.write_bytes(0x06F28, bytearray([0x41,0x88])) + patch.write_bytes(0x0F242, bytearray([0xA9,0x02,0x85])) + patch.write_bytes(0x16900, bytearray([0xA9,0x04,0x85,0x01])) + patch.write_bytes(0x19604, bytearray([0xA9,0x01,0x0C,0xD7,0x1F])) + patch.write_bytes(0x1B34D, bytearray([0xA9,0xC0,0x0C,0xB2,0x1F])) + patch.write_bytes(0x24E01, bytearray([0xED,0x00,0x00,0x8D,0xFF,0x09])) + patch.write_bytes(0x24F44, bytearray([0x85,0x27,0xA9,0x20])) + patch.write_bytes(0x24F5C, bytearray([0x64,0x27,0xA9,0x06])) + patch.write_bytes(0x25095, bytearray([0xED,0x00,0x00,0x8D,0xFF,0x09])) + patch.write_bytes(0x29B83, bytearray([0xA9,0x04,0x85,0x01])) + patch.write_bytes(0x2C81D, bytearray([0xBD,0xFD,0xBB,0x0C,0xD1,0x1F,0x60,0xA9, + 0xFF,0x0C,0xCC,0x1F])) + patch.write_bytes(0x30E4A, bytearray([0xAD,0x97,0xAD,0x97])) + patch.write_bytes(0x395EA, bytearray([0xA9,0x04,0x85,0x01])) + patch.write_bytes(0x0FF84, bytearray([0xFF for _ in range(0x007C)])) + patch.write_bytes(0x1FA80, bytearray([0xFF for _ in range(0x0580)])) # Edit the ROM header - rom.name = bytearray(f'MMX3{__version__.replace(".", "")[0:3]}_{player}_{world.multiworld.seed:11}\0', 'utf8')[:21] - rom.name.extend([0] * (21 - len(rom.name))) - rom.write_bytes(0x7FC0, rom.name) + patch.name = bytearray(f'MMX3{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0', 'utf8')[:21] + patch.name.extend([0] * (21 - len(patch.name))) + patch.write_bytes(0x7FC0, patch.name) # Write options to the ROM - rom.write_byte(0x17FFE0, world.options.doppler_open.value) - rom.write_byte(0x17FFE1, world.options.doppler_medal_count.value) - rom.write_byte(0x17FFE2, world.options.doppler_weapon_count.value) - rom.write_byte(0x17FFE3, world.options.doppler_upgrade_count.value) - rom.write_byte(0x17FFE4, world.options.doppler_heart_tank_count.value) - rom.write_byte(0x17FFE5, world.options.doppler_sub_tank_count.value) - rom.write_byte(0x17FFE6, world.options.starting_life_count.value) + patch.write_byte(0x17FFE0, world.options.doppler_open.value) + patch.write_byte(0x17FFE1, world.options.doppler_medal_count.value) + patch.write_byte(0x17FFE2, world.options.doppler_weapon_count.value) + patch.write_byte(0x17FFE3, world.options.doppler_upgrade_count.value) + patch.write_byte(0x17FFE4, world.options.doppler_heart_tank_count.value) + patch.write_byte(0x17FFE5, world.options.doppler_sub_tank_count.value) + patch.write_byte(0x17FFE6, world.options.starting_life_count.value) if world.options.pickupsanity.value: - rom.write_byte(0x17FFE7, 0x01) + patch.write_byte(0x17FFE7, 0x01) else: - rom.write_byte(0x17FFE7, 0x00) - rom.write_byte(0x17FFE8, world.options.vile_open.value) - rom.write_byte(0x17FFE9, world.options.vile_medal_count.value) - rom.write_byte(0x17FFEA, world.options.vile_weapon_count.value) - rom.write_byte(0x17FFEB, world.options.vile_upgrade_count.value) - rom.write_byte(0x17FFEC, world.options.vile_heart_tank_count.value) - rom.write_byte(0x17FFED, world.options.vile_sub_tank_count.value) - - rom.write_byte(0x17FFEE, world.options.logic_boss_weakness.value) - rom.write_byte(0x17FFEF, world.options.logic_vile_required.value) - rom.write_byte(0x17FFF0, world.options.logic_z_saber.value) + patch.write_byte(0x17FFE7, 0x00) + patch.write_byte(0x17FFE8, world.options.vile_open.value) + patch.write_byte(0x17FFE9, world.options.vile_medal_count.value) + patch.write_byte(0x17FFEA, world.options.vile_weapon_count.value) + patch.write_byte(0x17FFEB, world.options.vile_upgrade_count.value) + patch.write_byte(0x17FFEC, world.options.vile_heart_tank_count.value) + patch.write_byte(0x17FFED, world.options.vile_sub_tank_count.value) + + patch.write_byte(0x17FFEE, world.options.logic_boss_weakness.value) + patch.write_byte(0x17FFEF, world.options.logic_vile_required.value) + patch.write_byte(0x17FFF0, world.options.logic_z_saber.value) - #rom.write_byte(0x17FFF1, world.options.doppler_lab_1_boss.value) - rom.write_byte(0x17FFF1, 0x00) - rom.write_byte(0x17FFF2, world.options.doppler_lab_2_boss.value) - rom.write_byte(0x17FFF3, world.options.doppler_lab_3_boss_rematch_count.value) + #patch.write_byte(0x17FFF1, world.options.doppler_lab_1_boss.value) + patch.write_byte(0x17FFF1, 0x00) + patch.write_byte(0x17FFF2, world.options.doppler_lab_2_boss.value) + patch.write_byte(0x17FFF3, world.options.doppler_lab_3_boss_rematch_count.value) bit_medal_count = world.options.bit_medal_count.value byte_medal_count = world.options.byte_medal_count.value @@ -155,27 +157,27 @@ def patch_rom(world: World, rom, player): if bit_medal_count == 7: bit_medal_count = 6 byte_medal_count = bit_medal_count + 1 - rom.write_byte(0x17FFF4, bit_medal_count) - rom.write_byte(0x17FFF5, byte_medal_count) + patch.write_byte(0x17FFF4, bit_medal_count) + patch.write_byte(0x17FFF5, byte_medal_count) # QoL - rom.write_byte(0x17FFF6, world.options.disable_charge_freeze.value) + patch.write_byte(0x17FFF6, world.options.disable_charge_freeze.value) # EnergyLink - rom.write_byte(0x17FFF7, world.options.energy_link.value) + patch.write_byte(0x17FFF7, world.options.energy_link.value) # DeathLink - rom.write_byte(0x17FFF8, world.options.death_link.value) + patch.write_byte(0x17FFF8, world.options.death_link.value) # Setup starting life count - rom.write_byte(0x0019B1, world.options.starting_life_count.value) - rom.write_byte(0x0072C3, world.options.starting_life_count.value) - rom.write_byte(0x0021BE, world.options.starting_life_count.value) + patch.write_byte(0x0019B1, world.options.starting_life_count.value) + patch.write_byte(0x0072C3, world.options.starting_life_count.value) + patch.write_byte(0x0021BE, world.options.starting_life_count.value) # Debug option - rom.write_byte(0x17FFFF, 0x00) + patch.write_byte(0x17FFFF, 0x00) - rom.write_crc() + patch.write_file("token_patch.bin", patch.get_token_binary()) def get_base_rom_bytes(file_name: str = "") -> bytes: @@ -186,7 +188,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) - if basemd5.hexdigest() not in {HASH_US}: + if basemd5.hexdigest() not in {HASH_US, HASH_LEGACY}: raise Exception('Supplied Base Rom does not match known MD5 for US or LC release. ' 'Get the correct game and version, then dump it') get_base_rom_bytes.base_rom_bytes = base_rom_bytes diff --git a/worlds/mmx3/__init__.py b/worlds/mmx3/__init__.py index b46988cb08ce..780f067d2c97 100644 --- a/worlds/mmx3/__init__.py +++ b/worlds/mmx3/__init__.py @@ -5,6 +5,7 @@ import settings import hashlib import threading +import pkgutil from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification from Options import PerGameCommonOptions @@ -15,15 +16,14 @@ from .Names import ItemName, LocationName, EventName from .Options import MMX3Options from .Client import MMX3SNIClient -from .Rom import LocalRom, patch_rom, get_base_rom_path, MMX3DeltaPatch, HASH_US, HASH_LEGACY -from worlds.generic.Rules import add_rule, exclusion_rules +from .Rom import patch_rom, MMX3ProcedurePatch, HASH_US, HASH_LEGACY class MMX3Settings(settings.Group): class RomFile(settings.SNESRomPath): - """File name of the SMW US rom""" + """File name of the MMX3 US rom""" description = "Mega Man X3 (USA) ROM File" copy_to = "Mega Man X3 (USA).sfc" - md5s = [HASH_US] + md5s = [HASH_US, HASH_LEGACY] rom_file: RomFile = RomFile(RomFile.copy_to) @@ -194,28 +194,19 @@ def get_filler_item_name(self) -> str: return self.random.choice(list(junk_table.keys())) def generate_output(self, output_directory: str): - rompath = "" # if variable is not declared finally clause may fail try: - multiworld = self.multiworld - player = self.player + patch = MMX3ProcedurePatch() + patch.write_file("mmx3_basepatch.bsdiff4", pkgutil.get_data(__name__, "data/mmx3_basepatch.bsdiff4")) + patch_rom(self, patch) - rom = LocalRom(get_base_rom_path()) - patch_rom(self, rom, self.player) + self.rom_name = patch.name - rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") - rom.write_to_file(rompath) - self.rom_name = rom.name - - patch = MMX3DeltaPatch(os.path.splitext(rompath)[0]+MMX3DeltaPatch.patch_file_ending, player=player, - player_name=multiworld.player_name[player], patched_path=rompath) - patch.write() - except: + patch.write(os.path.join(output_directory, + f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}")) + except Exception: raise finally: self.rom_name_available_event.set() # make sure threading continues and errors are collected - if os.path.exists(rompath): - os.unlink(rompath) - def modify_multidata(self, multidata: dict): import base64 diff --git a/worlds/mmx3/data/mmx3_basepatch.bsdiff4 b/worlds/mmx3/data/mmx3_basepatch.bsdiff4 index 03bf92e7bd14237cd2dc95b497c77931018e9365..6d108e61d1ba2b8c74fe074799d685d5bbfa4705 100644 GIT binary patch delta 1245 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 HD+clj`3o{E diff --git a/worlds/mmx3/data/mmx3_legacy.bsdiff4 b/worlds/mmx3/data/mmx3_legacy.bsdiff4 deleted file mode 100644 index 95851cec78b0c9f7e6f429f3ce3ef65f69447784..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4464 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