diff --git a/BaseClasses.py b/BaseClasses.py index 28c87996..1ec1ea44 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -34,7 +34,6 @@ class World(object): self.clock_mode = 'off' self.aga_randomness = False self.lock_aga_door_in_escape = False - self.fix_door_frames = self.shuffle not in ['vanilla', 'dungeonssimple', 'dungeonsfull'] self.fix_trock_doors = self.shuffle != 'vanilla' self.save_and_quite_from_boss = False self.check_beatable_only = check_beatable_only diff --git a/Main.py b/Main.py index 0938feb2..201ec3c6 100644 --- a/Main.py +++ b/Main.py @@ -1,7 +1,7 @@ from BaseClasses import World, CollectionState, Item from Regions import create_regions from EntranceShuffle import link_entrances -from Rom import patch_rom, patch_base_rom +from Rom import patch_rom, LocalRom, JsonRom from Rules import set_rules from Dungeons import fill_dungeons from Items import ItemFactory @@ -87,11 +87,12 @@ def main(args, seed=None): outfilebase = 'ER_%s_%s_%s_%s_%s_%s' % (world.mode, world.goal, world.shuffle, world.difficulty, world.algorithm, world.seed) if not args.suppress_rom: - rom = bytearray(open(args.rom, 'rb').read()) - patch_base_rom(rom) - patched_rom = patch_rom(world, rom, bytearray(logic_hash), args.quickswap, args.heartbeep, sprite) - with open('%s.sfc' % outfilebase, 'wb') as outfile: - outfile.write(patched_rom) + if args.jsonout: + rom = JsonRom() + else: + rom = LocalRom(args.rom) + patch_rom(world, rom, bytearray(logic_hash), args.quickswap, args.heartbeep, sprite) + rom.write_to_file(args.jsonout or '%s.sfc' % outfilebase) if args.create_spoiler: with open('%s_Spoiler.txt' % outfilebase, 'w') as outfile: diff --git a/Plando.py b/Plando.py index 12826ca5..10c60434 100644 --- a/Plando.py +++ b/Plando.py @@ -1,7 +1,7 @@ from BaseClasses import World from Regions import create_regions from EntranceShuffle import link_entrances, connect_entrance, connect_two_way, connect_exit -from Rom import patch_rom, patch_base_rom, write_string_to_rom, write_credits_string_to_rom +from Rom import patch_rom, LocalRom, write_string_to_rom, write_credits_string_to_rom from Rules import set_rules from Items import ItemFactory from Main import create_playthrough @@ -79,9 +79,8 @@ def main(args, seed=None): else: sprite = None - rom = bytearray(open(args.rom, 'rb').read()) - patch_base_rom(rom) - patched_rom = patch_rom(world, rom, logic_hash, args.quickswap, args.heartbeep, sprite) + rom = LocalRom(args.rom) + patch_rom(world, rom, logic_hash, args.quickswap, args.heartbeep, sprite) for textname, texttype, text in text_patches: if texttype == 'text': @@ -91,8 +90,7 @@ def main(args, seed=None): outfilebase = 'Plando_%s_%s' % (os.path.splitext(os.path.basename(args.plando))[0], world.seed) - with open('%s.sfc' % outfilebase, 'wb') as outfile: - outfile.write(patched_rom) + rom.write_to_file('%s.sfc' % outfilebase) if args.create_spoiler: with open('%s_Spoiler.txt' % outfilebase, 'w') as outfile: outfile.write(world.spoiler) @@ -140,9 +138,6 @@ def fill_world(world, plando, text_patches): elif line.startswith('!light_cone_dw'): _, dwconestr = line.split(':', 1) world.dark_world_light_cone = dwconestr.strip().lower() == 'true' - elif line.startswith('!fix_door_frames'): - _, dfstring = line.split(':', 1) - world.fix_door_frames = dfstring.strip().lower() == 'true' elif line.startswith('!fix_trock_doors'): _, trdstr = line.split(':', 1) world.fix_trock_doors = trdstr.strip().lower() == 'true' diff --git a/Rom.py b/Rom.py index edc77443..c59c9654 100644 --- a/Rom.py +++ b/Rom.py @@ -11,6 +11,68 @@ JAP10HASH = '03a63945398191337e896e5771f77173' RANDOMIZERBASEHASH = '89fcdb48446bd858878f14e8a994d0b8' +class JsonRom(object): + + def __init__(self): + self.patches = {} + + def write_byte(self, address, value): + self.patches[str(address)] = [value] + + def write_bytes(self, startaddress, values): + self.patches[str(startaddress)] = list(values) + + def write_to_file(self, file): + json.dump([self.patches], open(file, 'w')) + + +class LocalRom(object): + + def __init__(self, file): + self.buffer = bytearray(open(file, 'rb').read()) + self.patch_base_rom() + + def write_byte(self, address, value): + self.buffer[address] = value + + def write_bytes(self, startaddress, values): + for i, value in enumerate(values): + self.write_byte(startaddress + i, value) + + def write_to_file(self, file): + with open(file, 'wb') as outfile: + outfile.write(self.buffer) + + def patch_base_rom(self): + # verify correct checksum of baserom + basemd5 = hashlib.md5() + basemd5.update(self.buffer) + if not JAP10HASH == basemd5.hexdigest(): + logging.getLogger('').warning('Supplied Base Rom does not match known MD5 for JAP(1.0) release. Will try to patch anyway.') + + # extend to 2MB + self.buffer.extend(bytearray([0x00] * (2097152 - len(self.buffer)))) + + # load randomizer patches + patches = json.load(open('base2current.json', 'r')) + for patch in patches: + if isinstance(patch, dict): + for baseaddress, values in patch.items(): + self.write_bytes(int(baseaddress), values) + + # verify md5 + patchedmd5 = hashlib.md5() + patchedmd5.update(self.buffer) + if not RANDOMIZERBASEHASH == patchedmd5.hexdigest(): + raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.') + + def write_crc(self): + # this does not seem to work + crc = sum(self.buffer[:0x7FEB] + self.buffer[0x7FF5:]) % 0xFFFF + inv = crc ^ 0xFFFF + self.write_bytes(0x7FDC, [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF]) + + def patch_rom(world, rom, hashtable, quickswap=False, beep='normal', sprite=None): # patch items for location in world.get_locations(): @@ -22,20 +84,17 @@ def patch_rom(world, rom, hashtable, quickswap=False, beep='normal', sprite=None locationaddress = location.address if not location.crystal: # regular items - write_byte(rom, locationaddress, itemid) + rom.write_byte(locationaddress, itemid) else: # crystals for address, value in zip(locationaddress, itemid): - write_byte(rom, address, value) + rom.write_byte(address, value) # patch music music_addresses = dungeon_music_addresses[location.name] music = 0x11 if 'Pendant' in location.item.name else 0x16 for music_address in music_addresses: - write_byte(rom, music_address, music) - - # store old door overlay table - door_overlays = bytearray(rom[0x15488:0x15488+0x10A]) + rom.write_byte(music_address, music) # patch entrances for region in world.regions: @@ -43,87 +102,76 @@ def patch_rom(world, rom, hashtable, quickswap=False, beep='normal', sprite=None if exit.target is not None: addresses = [exit.addresses] if isinstance(exit.addresses, int) else exit.addresses for address in addresses: - write_byte(rom, address, exit.target) - - # this does not yet seem to fix our door headaches ... - if world.fix_door_frames and exit.vanilla is not None: - # patch door overlay table. The value of where this now leads is patched into the location where this entrance would lead in vanilla - write_byte(rom, 0x15488 + (2*exit.vanilla), door_overlays[2*exit.target]) - write_byte(rom, 0x15489 + (2 * exit.vanilla), door_overlays[(2 * exit.target) + 1]) + rom.write_byte(address, exit.target) # patch medallion requirements if world.required_medallions[0] == 'Bombos': - write_byte(rom, 0x180022, 0x00) # requirement - write_byte(rom, 0x4FF2, 0x31) # sprite - write_byte(rom, 0x50D1, 0x80) - write_byte(rom, 0x51B0, 0x00) + rom.write_byte(0x180022, 0x00) # requirement + rom.write_byte(0x4FF2, 0x31) # sprite + rom.write_byte(0x50D1, 0x80) + rom.write_byte(0x51B0, 0x00) elif world.required_medallions[0] == 'Quake': - write_byte(rom, 0x180022, 0x02) # requirement - write_byte(rom, 0x4FF2, 0x31) # sprite - write_byte(rom, 0x50D1, 0x88) - write_byte(rom, 0x51B0, 0x00) + rom.write_byte(0x180022, 0x02) # requirement + rom.write_byte(0x4FF2, 0x31) # sprite + rom.write_byte(0x50D1, 0x88) + rom.write_byte(0x51B0, 0x00) if world.required_medallions[1] == 'Bombos': - write_byte(rom, 0x180023, 0x00) # requirement - write_byte(rom, 0x5020, 0x31) # sprite - write_byte(rom, 0x50FF, 0x90) - write_byte(rom, 0x51DE, 0x00) + rom.write_byte(0x180023, 0x00) # requirement + rom.write_byte(0x5020, 0x31) # sprite + rom.write_byte(0x50FF, 0x90) + rom.write_byte(0x51DE, 0x00) elif world.required_medallions[1] == 'Ether': - write_byte(rom, 0x180023, 0x01) # requirement - write_byte(rom, 0x5020, 0x31) # sprite - write_byte(rom, 0x50FF, 0x98) - write_byte(rom, 0x51DE, 0x00) + rom.write_byte(0x180023, 0x01) # requirement + rom.write_byte(0x5020, 0x31) # sprite + rom.write_byte(0x50FF, 0x98) + rom.write_byte(0x51DE, 0x00) # set open mode: if world.mode in ['open', 'swordless']: - write_byte(rom, 0x180032, 0x01) # open mode + rom.write_byte(0x180032, 0x01) # open mode # disable sword sprite from uncle - write_bytes(rom, 0x6D263, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) - write_bytes(rom, 0x6D26B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) - write_bytes(rom, 0x6D293, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) - write_bytes(rom, 0x6D29B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E]) - write_bytes(rom, 0x6D2B3, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E]) - write_bytes(rom, 0x6D2BB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E]) - write_bytes(rom, 0x6D2E3, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) - write_bytes(rom, 0x6D2EB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) - write_bytes(rom, 0x6D31B, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) - write_bytes(rom, 0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) + rom.write_bytes(0x6D263, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D26B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D293, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D29B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D2B3, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E]) + rom.write_bytes(0x6D2BB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E]) + rom.write_bytes(0x6D2E3, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) + rom.write_bytes(0x6D2EB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) + rom.write_bytes(0x6D31B, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) + rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) else: - write_byte(rom, 0x180032, 0x00) # standard mode + rom.write_byte(0x180032, 0x00) # standard mode # set light cones - write_byte(rom, 0x180038, 0x01 if world.sewer_light_cone else 0x00) - write_byte(rom, 0x180039, 0x01 if world.light_world_light_cone else 0x00) - write_byte(rom, 0x18003A, 0x01 if world.dark_world_light_cone else 0x00) - - # disable light world cane in minor glitches - if world.logic == 'minorglitches': - write_byte(rom, 0x180039, 0x00) # light world light cone disable - write_byte(rom, 0x18003A, 0x00) # dark world light cone disable + rom.write_byte(0x180038, 0x01 if world.sewer_light_cone else 0x00) + rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00) + rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00) # handle difficulty if world.difficulty == 'hard': # Spike Cave Damage - write_byte(rom, 0x180168, 0x02) + rom.write_byte(0x180168, 0x02) # Powdered Fairies Prize - write_byte(rom, 0x36DD0, 0x79) # Bee + rom.write_byte(0x36DD0, 0x79) # Bee # potion heal amount - write_byte(rom, 0x180084, 0x08) # One Heart + rom.write_byte(0x180084, 0x08) # One Heart # potion magic restore amount - write_byte(rom, 0x180085, 0x20) # Quarter Magic + rom.write_byte(0x180085, 0x20) # Quarter Magic else: # Spike Cave Damage - write_byte(rom, 0x180168, 0x08) + rom.write_byte(0x180168, 0x08) # Powdered Fairies Prize - write_byte(rom, 0x36DD0, 0xE3) # fairy + rom.write_byte(0x36DD0, 0xE3) # fairy # potion heal amount - write_byte(rom, 0x180084, 0xA0) # full + rom.write_byte(0x180084, 0xA0) # full # potion magic restore amount - write_byte(rom, 0x180085, 0x80) # full + rom.write_byte(0x180085, 0x80) # full # set up game internal RNG seed for i in range(1024): - write_byte(rom, 0x178000 + i, random.randint(0, 255)) + rom.write_byte(0x178000 + i, random.randint(0, 255)) # shuffle prize packs prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0, 0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF, @@ -132,22 +180,22 @@ def patch_rom(world, rom, hashtable, quickswap=False, beep='normal', sprite=None random.shuffle(prizes) # write tree pull prizes - write_byte(rom, 0xEFBD4, prizes.pop()) - write_byte(rom, 0xEFBD5, prizes.pop()) - write_byte(rom, 0xEFBD6, prizes.pop()) + rom.write_byte(0xEFBD4, prizes.pop()) + rom.write_byte(0xEFBD5, prizes.pop()) + rom.write_byte(0xEFBD6, prizes.pop()) # rupee crab prizes - write_byte(rom, 0x329C8, prizes.pop()) # first prize - write_byte(rom, 0x329C4, prizes.pop()) # final prize + rom.write_byte(0x329C8, prizes.pop()) # first prize + rom.write_byte(0x329C4, prizes.pop()) # final prize # stunned enemy prize - write_byte(rom, 0x37993, prizes.pop()) + rom.write_byte(0x37993, prizes.pop()) # saved fish prize - write_byte(rom, 0xE82CC, prizes.pop()) + rom.write_byte(0xE82CC, prizes.pop()) # fill enemy prize packs - write_bytes(rom, 0x37A78, prizes) + rom.write_bytes(0x37A78, prizes) # prize pack drop chances if world.difficulty == 'hard': @@ -156,140 +204,138 @@ def patch_rom(world, rom, hashtable, quickswap=False, beep='normal', sprite=None droprates = [0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01] # 50% random.shuffle(droprates) - write_bytes(rom, 0x37A62, droprates) + rom.write_bytes(0x37A62, droprates) + + vanilla_prize_pack_assignment = [131, 150, 132, 128, 128, 128, 128, 128, 2, 0, 2, 128, 160, 131, 151, 128, 128, 148, 145, 7, 0, 128, 0, 128, 146, 150, 128, 160, 0, 0, 0, 128, 4, 128, + 130, 6, 6, 0, 0, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 0, 0, 128, 128, 144, 128, 145, 145, + 145, 151, 145, 149, 149, 147, 151, 20, 145, 146, 129, 130, 130, 128, 133, 128, 128, 128, 4, 4, 128, 145, 128, 128, 128, 128, 128, 128, 128, 128, 0, 128, + 128, 130, 138, 128, 128, 128, 128, 146, 145, 128, 130, 129, 129, 128, 129, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 151, 128, 128, 128, 128, 194, + 128, 21, 21, 23, 6, 0, 128, 0, 192, 19, 64, 0, 2, 6, 16, 20, 0, 0, 64, 0, 0, 0, 0, 19, 70, 17, 128, 128, 0, 0, 0, 16, 0, 0, 0, 22, 22, 22, 129, 135, 130, + 0, 128, 128, 0, 0, 0, 0, 128, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 23, 0, 18, 0, 0, 0, 0, 0, 16, 23, 0, 64, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0] # shuffle enemies to prize packs for i in range(243): - if rom[0x6B632 + i] & 0x0F != 0x00: - rom[0x6B632 + i] = (rom[0x6B632 + i] & 0xF0) | random.randint(1, 7) + if vanilla_prize_pack_assignment[i] & 0x0F != 0x00: + rom.write_byte(0x6B632 + i, (vanilla_prize_pack_assignment[i] & 0xF0) | random.randint(1, 7)) # set bonk prizes - bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3, - 0xDA, 0x79, 0xAC, 0xAC, 0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xE3, 0x79, 0xDE, 0xE3, 0xAC, 0xDB, 0x79, 0xE3, 0xD8, 0xAC, 0x79, 0xE3, 0xDB, 0xDB, 0xE3, 0xE3, 0x79, 0xD8, 0xDD] - bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD, - 0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D, 0x4CC53, 0x4CC69, 0x4CC6F, 0x4CC7C, 0x4CCEF, 0x4CD51, - 0x4CDC0, 0x4CDC3, 0x4CDC6, 0x4CE37, 0x4D2DE, 0x4D32F, 0x4D355, 0x4D367, 0x4D384, 0x4D387, 0x4D397, 0x4D39E, 0x4D3AB, 0x4D3AE, 0x4D3D1, 0x4D3D7, - 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A] - random.shuffle(bonk_prizes) - for prize, address in zip(bonk_prizes, bonk_addresses): - write_byte(rom, address, prize) + if world.shuffle_bonk_prizes: + bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC, 0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79, 0xE3, 0xE3, + 0xDA, 0x79, 0xAC, 0xAC, 0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xE3, 0x79, 0xDE, 0xE3, 0xAC, 0xDB, 0x79, 0xE3, 0xD8, 0xAC, 0x79, 0xE3, 0xDB, 0xDB, 0xE3, 0xE3, 0x79, 0xD8, 0xDD] + bonk_addresses = [0x4CF6C, 0x4CFBA, 0x4CFE0, 0x4CFFB, 0x4D018, 0x4D01B, 0x4D028, 0x4D03C, 0x4D059, 0x4D07A, 0x4D09E, 0x4D0A8, 0x4D0AB, 0x4D0AE, 0x4D0BE, 0x4D0DD, + 0x4D16A, 0x4D1E5, 0x4D1EE, 0x4D20B, 0x4CBBF, 0x4CBBF, 0x4CC17, 0x4CC1A, 0x4CC4A, 0x4CC4D, 0x4CC53, 0x4CC69, 0x4CC6F, 0x4CC7C, 0x4CCEF, 0x4CD51, + 0x4CDC0, 0x4CDC3, 0x4CDC6, 0x4CE37, 0x4D2DE, 0x4D32F, 0x4D355, 0x4D367, 0x4D384, 0x4D387, 0x4D397, 0x4D39E, 0x4D3AB, 0x4D3AE, 0x4D3D1, 0x4D3D7, + 0x4D3F8, 0x4D416, 0x4D420, 0x4D423, 0x4D42D, 0x4D449, 0x4D48C, 0x4D4D9, 0x4D4DC, 0x4D4E3, 0x4D504, 0x4D507, 0x4D55E, 0x4D56A] + random.shuffle(bonk_prizes) + for prize, address in zip(bonk_prizes, bonk_addresses): + rom.write_byte(address, prize) # set Fountain bottle exchange items - write_byte(rom, 0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)]) - write_byte(rom, 0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)]) + rom.write_byte(0x348FF, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)]) + rom.write_byte(0x3493B, [0x16, 0x2B, 0x2C, 0x2D, 0x3C, 0x3D, 0x48][random.randint(0, 6)]) # set Fat Fairy Bow/Sword prizes to be disappointing - write_byte(rom, 0x34914, 0x3A) # Bow and Arrow - write_byte(rom, 0x180028, 0x49) # Fighter Sword + rom.write_byte(0x34914, 0x3A) # Bow and Arrow + rom.write_byte(0x180028, 0x49) # Fighter Sword # set swordless mode settings - write_byte(rom, 0x18003F, 0x01 if world.mode == 'swordless' else 0x00) # hammer can harm ganon - write_byte(rom, 0x180040, 0x01 if world.mode == 'swordless' else 0x00) # open curtains - write_byte(rom, 0x180041, 0x01 if world.mode == 'swordless' else 0x00) # swordless medallions - write_byte(rom, 0x180042, 0xFF if world.mode == 'swordless' else 0x00) # starting sword for link + rom.write_byte(0x18003F, 0x01 if world.mode == 'swordless' else 0x00) # hammer can harm ganon + rom.write_byte(0x180040, 0x01 if world.mode == 'swordless' else 0x00) # open curtains + rom.write_byte(0x180041, 0x01 if world.mode == 'swordless' else 0x00) # swordless medallions + rom.write_byte(0x180042, 0xFF if world.mode == 'swordless' else 0x00) # starting sword for link # set up clocks for timed modes if world.clock_mode == 'off': - write_bytes(rom, 0x180190, [0x00, 0x00, 0x00]) # turn off clock mode - write_bytes(rom, 0x180200, [0x00, 0x00, 0x00, 0x00]) # red clock adjustment time (in frames, sint32) - write_bytes(rom, 0x180204, [0x00, 0x00, 0x00, 0x00]) # blue clock adjustment time (in frames, sint32) - write_bytes(rom, 0x180208, [0x00, 0x00, 0x00, 0x00]) # green clock adjustment time (in frames, sint32) - write_bytes(rom, 0x18020C, [0x00, 0x00, 0x00, 0x00]) # starting time (in frames, sint32) + rom.write_bytes(0x180190, [0x00, 0x00, 0x00]) # turn off clock mode + rom.write_bytes(0x180200, [0x00, 0x00, 0x00, 0x00]) # red clock adjustment time (in frames, sint32) + rom.write_bytes(0x180204, [0x00, 0x00, 0x00, 0x00]) # blue clock adjustment time (in frames, sint32) + rom.write_bytes(0x180208, [0x00, 0x00, 0x00, 0x00]) # green clock adjustment time (in frames, sint32) + rom.write_bytes(0x18020C, [0x00, 0x00, 0x00, 0x00]) # starting time (in frames, sint32) elif world.clock_mode == 'ohko': - write_bytes(rom, 0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality - write_bytes(rom, 0x180200, [0x00, 0x00, 0x00, 0x00]) # red clock adjustment time (in frames, sint32) - write_bytes(rom, 0x180204, [0x00, 0x00, 0x00, 0x00]) # blue clock adjustment time (in frames, sint32) - write_bytes(rom, 0x180208, [0x50, 0x46, 0x00, 0x00]) # green clock adjustment time (in frames, sint32) - write_bytes(rom, 0x18020C, [0xA0, 0x8C, 0x00, 0x00]) # starting time (in frames, sint32) + rom.write_bytes(0x180190, [0x01, 0x02, 0x01]) # ohko timer with resetable timer functionality + rom.write_bytes(0x180200, [0x00, 0x00, 0x00, 0x00]) # red clock adjustment time (in frames, sint32) + rom.write_bytes(0x180204, [0x00, 0x00, 0x00, 0x00]) # blue clock adjustment time (in frames, sint32) + rom.write_bytes(0x180208, [0x50, 0x46, 0x00, 0x00]) # green clock adjustment time (in frames, sint32) + rom.write_bytes(0x18020C, [0xA0, 0x8C, 0x00, 0x00]) # starting time (in frames, sint32) if world.clock_mode == 'stopwatch': - write_bytes(rom, 0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode - write_bytes(rom, 0x180200, [0xE0, 0xE3, 0xFF, 0xFF]) # red clock adjustment time (in frames, sint32) - write_bytes(rom, 0x180204, [0x20, 0x1C, 0x00, 0x00]) # blue clock adjustment time (in frames, sint32) - write_bytes(rom, 0x180208, [0x40, 0x38, 0x00, 0x00]) # green clock adjustment time (in frames, sint32) - write_bytes(rom, 0x18020C, [0x00, 0x00, 0x00, 0x00]) # starting time (in frames, sint32) + rom.write_bytes(0x180190, [0x02, 0x01, 0x00]) # set stopwatch mode + rom.write_bytes(0x180200, [0xE0, 0xE3, 0xFF, 0xFF]) # red clock adjustment time (in frames, sint32) + rom.write_bytes(0x180204, [0x20, 0x1C, 0x00, 0x00]) # blue clock adjustment time (in frames, sint32) + rom.write_bytes(0x180208, [0x40, 0x38, 0x00, 0x00]) # green clock adjustment time (in frames, sint32) + rom.write_bytes(0x18020C, [0x00, 0x00, 0x00, 0x00]) # starting time (in frames, sint32) if world.clock_mode == 'countdown': - write_bytes(rom, 0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available - write_bytes(rom, 0x180200, [0xE0, 0xE3, 0xFF, 0xFF]) # red clock adjustment time (in frames, sint32) - write_bytes(rom, 0x180204, [0x20, 0x1C, 0x00, 0x00]) # blue clock adjustment time (in frames, sint32) - write_bytes(rom, 0x180208, [0x40, 0x38, 0x00, 0x00]) # green clock adjustment time (in frames, sint32) - write_bytes(rom, 0x18020C, [0x80, 0x32, 0x02, 0x00]) # starting time (in frames, sint32) + rom.write_bytes(0x180190, [0x01, 0x01, 0x00]) # set countdown, with no reset available + rom.write_bytes(0x180200, [0xE0, 0xE3, 0xFF, 0xFF]) # red clock adjustment time (in frames, sint32) + rom.write_bytes(0x180204, [0x20, 0x1C, 0x00, 0x00]) # blue clock adjustment time (in frames, sint32) + rom.write_bytes(0x180208, [0x40, 0x38, 0x00, 0x00]) # green clock adjustment time (in frames, sint32) + rom.write_bytes(0x18020C, [0x80, 0x32, 0x02, 0x00]) # starting time (in frames, sint32) # set up goals for treasure hunt - write_bytes(rom, 0x180165, [0x0E, 0x28] if world.treasure_hunt_icon == 'Triforce Piece' else [0x0D, 0x28]) - write_byte(rom, 0x180167, world.treasure_hunt_count % 256) + rom.write_bytes(0x180165, [0x0E, 0x28] if world.treasure_hunt_icon == 'Triforce Piece' else [0x0D, 0x28]) + rom.write_byte(0x180167, world.treasure_hunt_count % 256) # assorted fixes - write_byte(rom, 0x180030, 0x00) # Disable SRAM trace - write_byte(rom, 0x180036, 0x0A) # Rupoor negative value - write_byte(rom, 0x180169, 0x01 if world.lock_aga_door_in_escape else 0x00) # Lock or unlock aga tower door during escape sequence. - write_byte(rom, 0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness - write_byte(rom, 0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp + rom.write_byte(0x180030, 0x00) # Disable SRAM trace + rom.write_byte(0x180036, 0x0A) # Rupoor negative value + rom.write_byte(0x180169, 0x01 if world.lock_aga_door_in_escape else 0x00) # Lock or unlock aga tower door during escape sequence. + rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness + rom.write_byte(0x1800A1, 0x01) # enable overworld screen transition draining for water level inside swamp if world.goal in ['pedestal', 'starhunt', 'triforcehunt']: - write_byte(rom, 0x18003E, 0x01) # make ganon invincible + rom.write_byte(0x18003E, 0x01) # make ganon invincible elif world.goal in ['dungeons']: - write_byte(rom, 0x18003E, 0x02) # make ganon invincible until all dungeons are beat - write_byte(rom, 0x18016A, 0x00) # disable free roaming item text boxes - write_byte(rom, 0x18003B, 0x00) # disable maps showing crystals on overworld - write_byte(rom, 0x18003C, 0x00) # disable compasses showing dungeon count + rom.write_byte(0x18003E, 0x02) # make ganon invincible until all dungeons are beat + rom.write_byte(0x18016A, 0x00) # disable free roaming item text boxes + rom.write_byte(0x18003B, 0x00) # disable maps showing crystals on overworld + rom.write_byte(0x18003C, 0x00) # disable compasses showing dungeon count digging_game_rng = random.randint(1, 30) # set rng for digging game - write_byte(rom, 0x180020, digging_game_rng) - write_byte(rom, 0xEFD95, digging_game_rng) - write_byte(rom, 0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills - write_byte(rom, 0x180042, 0x01 if world.save_and_quite_from_boss else 0x00) # Allow Save and Quite after boss kill + rom.write_byte(0x180020, digging_game_rng) + rom.write_byte(0xEFD95, digging_game_rng) + rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills + rom.write_byte(0x180042, 0x01 if world.save_and_quite_from_boss else 0x00) # Allow Save and Quite after boss kill # remove shield from uncle - write_bytes(rom, 0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) - write_bytes(rom, 0x6D25B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) - write_bytes(rom, 0x6D283, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) - write_bytes(rom, 0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E]) - write_bytes(rom, 0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E]) - write_bytes(rom, 0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) - write_bytes(rom, 0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) + rom.write_bytes(0x6D253, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D25B, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D283, [0x00, 0x00, 0xf6, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D28B, [0x00, 0x00, 0xf7, 0xff, 0x00, 0x0E]) + rom.write_bytes(0x6D2CB, [0x00, 0x00, 0xf6, 0xff, 0x02, 0x0E]) + rom.write_bytes(0x6D2FB, [0x00, 0x00, 0xf7, 0xff, 0x02, 0x0E]) + rom.write_bytes(0x6D313, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E]) if world.swamp_patch_required: # patch swamp: Need to enable permanent drain of water as dam or swamp were moved - write_byte(rom, 0x18003D, 0x01) + rom.write_byte(0x18003D, 0x01) # set correct flag for hera basement item if world.get_location('[dungeon-L3-1F] Tower of Hera - Freestanding Key').item is not None and world.get_location('[dungeon-L3-1F] Tower of Hera - Freestanding Key').item.name == 'Small Key (Tower of Hera)': - write_byte(rom, 0x4E3BB, 0xE4) + rom.write_byte(0x4E3BB, 0xE4) else: - write_byte(rom, 0x4E3BB, 0xEB) - - # disable open door sprites when exiting caves - # this does not seem to work completely yet - if world.fix_door_frames: - for i in range(0x85): - write_byte(rom, 0x15274 + i, 0x00) - for i in range(0x86): - write_byte(rom, 0x15488 + i, 0x00) - # leave the entry marking tavern north a north facing exit - for i in range(0x82): - write_byte(rom, 0x15510 + i, 0x00) + rom.write_byte(0x4E3BB, 0xEB) # fix trock doors for reverse entrances if world.fix_trock_doors: - write_byte(rom, 0xFED31, 0x0E) # preopen bombable exit - write_byte(rom, 0xFEE41, 0x0E) # preopen bombable exit - write_byte(rom, 0xFE465, 0x1E) # remove small key door on backside of big key door + rom.write_byte(0xFED31, 0x0E) # preopen bombable exit + rom.write_byte(0xFEE41, 0x0E) # preopen bombable exit + rom.write_byte(0xFE465, 0x1E) # remove small key door on backside of big key door # Thanks to Zarby89 for finding these values # fix skull woods exit point if world.fix_skullwoods_exit: - write_byte(rom, 0x15E0D, 0xF8) + rom.write_byte(0x15E0D, 0xF8) # fix palace of darkness exit point if world.fix_palaceofdarkness_exit: - write_byte(rom, 0x15E03, 0x40) + rom.write_byte(0x15E03, 0x40) # fix turtle rock exit point if world.fix_trock_exit: - write_byte(rom, 0x15E1D, 0x34) + rom.write_byte(0x15E1D, 0x34) # enable quick item swapping with L and R (ported by Amazing Ampharos) if quickswap: - write_bytes(rom, 0x107fb, [0x22, 0x50, 0xFF, 0x1F]) - write_bytes(rom, 0x12451, [0x22, 0x50, 0xFF, 0x1F]) - write_bytes(rom, 0xfff50, [0x20, 0x58, 0xFF, 0xA5, 0xF6, 0x29, 0x40, 0x6B, 0xA5, 0xF6, 0x89, 0x10, 0xF0, 0x03, 0x4C, 0x69, + rom.write_bytes(0x107fb, [0x22, 0x50, 0xFF, 0x1F]) + rom.write_bytes(0x12451, [0x22, 0x50, 0xFF, 0x1F]) + rom.write_bytes(0xfff50, [0x20, 0x58, 0xFF, 0xA5, 0xF6, 0x29, 0x40, 0x6B, 0xA5, 0xF6, 0x89, 0x10, 0xF0, 0x03, 0x4C, 0x69, 0xFF, 0x89, 0x20, 0xF0, 0x03, 0x4C, 0xAA, 0xFF, 0x60, 0xAD, 0x02, 0x02, 0xF0, 0x3B, 0xDA, 0xAA, 0xE0, 0x0F, 0xF0, 0x14, 0xE0, 0x10, 0xF0, 0x14, 0xE0, 0x14, 0xD0, 0x02, 0xA2, 0x00, 0xE8, 0xBF, 0x3F, 0xF3, 0x7E, 0xF0, 0xEB, 0x4C, 0xEB, 0xFF, 0xA2, 0x01, 0x80, 0x0A, 0xAF, 0x4F, 0xF3, 0x7E, @@ -305,38 +351,46 @@ def patch_rom(world, rom, hashtable, quickswap=False, beep='normal', sprite=None # set rom name # 21 bytes - write_bytes(rom, 0x7FC0, bytearray('ER_041_%09d_' % world.seed, 'utf8') + world.option_identifier.to_bytes(4, 'big')) + rom.write_bytes(0x7FC0, bytearray('ER_041_%09d_' % world.seed, 'utf8') + world.option_identifier.to_bytes(4, 'big')) # set heart beep rate - write_byte(rom, 0x180033, {'off': 0x00, 'half': 0x40, 'quarter': 0x80, 'normal': 0x20}[beep]) + rom.write_byte(0x180033, {'off': 0x00, 'half': 0x40, 'quarter': 0x80, 'normal': 0x20}[beep]) # store hash table for main menu hash - write_bytes(rom, 0x181000, hashtable) + rom.write_bytes(0x181000, hashtable) # write link sprite if required if sprite is not None: - write_bytes(rom, 0x80000, sprite) + write_sprite(rom, sprite) + + if isinstance(rom, LocalRom): + rom.write_crc() return rom -def write_byte(rom, address, value): - rom[address] = value - - -def write_bytes(rom, startaddress, values): - for i, value in enumerate(values): - write_byte(rom, startaddress + i, value) +def write_sprite(rom, sprite): + if len(sprite) == 0x7000: + # sprite file with graphics and without palette data + rom.write_bytes(0x80000, sprite[:0x7000]) + elif len(sprite) == 0x7078: + # sprite file with graphics and palette data + rom.write_bytes(0x80000, sprite[:0x7000]) + rom.write_bytes(0xDD308, sprite[0x7000:]) + elif len(sprite) in [0x100000, 0x200000]: + # full rom with patched sprite, extract it + rom.write_bytes(0x80000, sprite[0x80000:0x87000]) + rom.write_bytes(0xDD308, sprite[0xDD308:0xDD380]) def write_string_to_rom(rom, target, string): address, maxbytes = text_addresses[target] - write_bytes(rom, address, string_to_alttp_text(string, maxbytes)) + rom.write_bytes(address, string_to_alttp_text(string, maxbytes)) def write_credits_string_to_rom(rom, target, string): address, length = credits_addresses[target] - write_bytes(rom, address, string_to_credits(string, length)) + rom.write_bytes(address, string_to_credits(string, length)) def write_strings(rom, world): @@ -396,27 +450,3 @@ def write_strings(rom, world): fluteboyitem = world.get_location('Flute Boy').item fluteboyitem_text = FluteBoy_texts[random.randint(0, len(FluteBoy_texts) - 1)] if fluteboyitem is None or fluteboyitem.fluteboy_credit_text is None else fluteboyitem.fluteboy_credit_text write_credits_string_to_rom(rom, 'FluteBoy', fluteboyitem_text) - - -def patch_base_rom(rom): - # verify correct checksum of baserom - basemd5 = hashlib.md5() - basemd5.update(rom) - if not JAP10HASH == basemd5.hexdigest(): - logging.getLogger('').warning('Supplied Base Rom does not match known MD5 for JAP(1.0) release. Will try to patch anyway.') - - # extend to 2MB - rom.extend(bytearray([0x00]*(2097152 - len(rom)))) - - # load randomizer patches - patches = json.load(open('base2current.json', 'r')) - for patch in patches: - if isinstance(patch, dict): - for baseaddress, values in patch.items(): - write_bytes(rom, int(baseaddress), values) - - # verify md5 - patchedmd5 = hashlib.md5() - patchedmd5.update(rom) - if not RANDOMIZERBASEHASH == patchedmd5.hexdigest(): - raise RuntimeError('Provided Base Rom unsuitable for patching. Please provide a JAP(1.0) "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc" rom to use as a base.')