## What is this fixing or adding? - Adds the majority of OoTR 7.0 features: - Pot shuffle, Freestanding item shuffle, Crate shuffle, Beehive shuffle - Key rings mode - Dungeon shortcuts to speed up dungeons - "Regional" shuffle for dungeon items - New options for shop pricing in shopsanity - Expanded Ganon's Boss Key shuffle options - Pre-planted beans - Improved Chest Appearance Matches Contents mode - Blue Fire Arrows - Bonk self-damage - Finer control over MQ dungeons and spawn position randomization - Several bugfixes as a result of the update: - Items recognized by the server and valid starting items are now in a 1-to-1 correspondence. In particular, starting with keys is now supported. - Entrance randomization success rate improved. Hopefully it is now at 100%. Co-authored-by: Zach Parks <zach@alliware.com>
		
			
				
	
	
		
			270 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			270 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import struct
 | 
						|
import random
 | 
						|
import io
 | 
						|
import array
 | 
						|
import zlib
 | 
						|
import copy
 | 
						|
import zipfile
 | 
						|
from .ntype import BigStream
 | 
						|
 | 
						|
 | 
						|
# get the next XOR key. Uses some location in the source rom.
 | 
						|
# This will skip of 0s, since if we hit a block of 0s, the
 | 
						|
# patch data will be raw.
 | 
						|
def key_next(rom, key_address, address_range):
 | 
						|
    key = 0
 | 
						|
    while key == 0:
 | 
						|
        key_address += 1
 | 
						|
        if key_address > address_range[1]:
 | 
						|
            key_address = address_range[0]
 | 
						|
        key = rom.original.buffer[key_address]
 | 
						|
    return key, key_address
 | 
						|
 | 
						|
 | 
						|
# creates a XOR block for the patch. This might break it up into
 | 
						|
# multiple smaller blocks if there is a concern about the XOR key
 | 
						|
# or if it is too long.
 | 
						|
def write_block(rom, xor_address, xor_range, block_start, data, patch_data):
 | 
						|
    new_data = []
 | 
						|
    key_offset = 0
 | 
						|
    continue_block = False
 | 
						|
 | 
						|
    for b in data:
 | 
						|
        if b == 0:
 | 
						|
            # Leave 0s as 0s. Do not XOR
 | 
						|
            new_data += [0]
 | 
						|
        else:
 | 
						|
            # get the next XOR key
 | 
						|
            key, xor_address = key_next(rom, xor_address, xor_range)
 | 
						|
 | 
						|
            # if the XOR would result in 0, change the key.
 | 
						|
            # This requires breaking up the block.
 | 
						|
            if b == key:
 | 
						|
                write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
 | 
						|
                new_data = []
 | 
						|
                key_offset = 0
 | 
						|
                continue_block = True
 | 
						|
 | 
						|
                # search for next safe XOR key
 | 
						|
                while b == key:
 | 
						|
                    key_offset += 1
 | 
						|
                    key, xor_address = key_next(rom, xor_address, xor_range)
 | 
						|
                    # if we aren't able to find one quickly, we may need to break again
 | 
						|
                    if key_offset == 0xFF:
 | 
						|
                        write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
 | 
						|
                        new_data = []
 | 
						|
                        key_offset = 0
 | 
						|
                        continue_block = True
 | 
						|
 | 
						|
            # XOR the key with the byte
 | 
						|
            new_data += [b ^ key]
 | 
						|
 | 
						|
            # Break the block if it's too long
 | 
						|
            if (len(new_data) == 0xFFFF):
 | 
						|
                write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
 | 
						|
                new_data = []
 | 
						|
                key_offset = 0
 | 
						|
                continue_block = True
 | 
						|
 | 
						|
    # Save the block
 | 
						|
    write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
 | 
						|
    return xor_address
 | 
						|
 | 
						|
 | 
						|
# This saves a sub-block for the XOR block. If it's the first part
 | 
						|
# then it will include the address to write to. Otherwise it will
 | 
						|
# have a number of XOR keys to skip and then continue writing after
 | 
						|
# the previous block
 | 
						|
def write_block_section(start, key_skip, in_data, patch_data, is_continue):
 | 
						|
    if not is_continue:
 | 
						|
        patch_data.append_int32(start)
 | 
						|
    else:
 | 
						|
        patch_data.append_bytes([0xFF, key_skip])
 | 
						|
    patch_data.append_int16(len(in_data))
 | 
						|
    patch_data.append_bytes(in_data)
 | 
						|
 | 
						|
 | 
						|
# This will create the patch file. Which can be applied to a source rom.
 | 
						|
# xor_range is the range the XOR key will read from. This range is not
 | 
						|
# too important, but I tried to choose from a section that didn't really
 | 
						|
# have big gaps of 0s which we want to avoid.
 | 
						|
def create_patch_file(rom, xor_range=(0x00B8AD30, 0x00F029A0)):
 | 
						|
    dma_start, dma_end = rom.get_dma_table_range()
 | 
						|
 | 
						|
    # add header
 | 
						|
    patch_data = BigStream([])
 | 
						|
    patch_data.append_bytes(list(map(ord, 'ZPFv1')))
 | 
						|
    patch_data.append_int32(dma_start)
 | 
						|
    patch_data.append_int32(xor_range[0])
 | 
						|
    patch_data.append_int32(xor_range[1])
 | 
						|
 | 
						|
    # get random xor key. This range is chosen because it generally
 | 
						|
    # doesn't have many sections of 0s
 | 
						|
    xor_address = random.Random().randint(*xor_range)
 | 
						|
    patch_data.append_int32(xor_address)
 | 
						|
 | 
						|
    new_buffer = copy.copy(rom.original.buffer)
 | 
						|
 | 
						|
    # write every changed DMA entry
 | 
						|
    for dma_index, (from_file, start, size) in rom.changed_dma.items():
 | 
						|
        patch_data.append_int16(dma_index)
 | 
						|
        patch_data.append_int32(from_file)
 | 
						|
        patch_data.append_int32(start)
 | 
						|
        patch_data.append_int24(size)
 | 
						|
 | 
						|
        # We don't trust files that have modified DMA to have their
 | 
						|
        # changed addresses tracked correctly, so we invalidate the
 | 
						|
        # entire file
 | 
						|
        for address in range(start, start + size):
 | 
						|
            rom.changed_address[address] = rom.buffer[address]
 | 
						|
 | 
						|
        # Simulate moving the files to know which addresses have changed
 | 
						|
        if from_file >= 0:
 | 
						|
            old_dma_start, old_dma_end, old_size = rom.original.get_dmadata_record_by_key(from_file)
 | 
						|
            copy_size = min(size, old_size)
 | 
						|
            new_buffer[start:start+copy_size] = rom.original.read_bytes(from_file, copy_size)
 | 
						|
            new_buffer[start+copy_size:start+size] = [0] * (size - copy_size)
 | 
						|
        else:
 | 
						|
            # this is a new file, so we just fill with null data
 | 
						|
            new_buffer[start:start+size] = [0] * size
 | 
						|
 | 
						|
    # end of DMA entries
 | 
						|
    patch_data.append_int16(0xFFFF)
 | 
						|
 | 
						|
    # filter down the addresses that will actually need to change.
 | 
						|
    # Make sure to not include any of the DMA table addresses
 | 
						|
    changed_addresses = [address for address,value in rom.changed_address.items() \
 | 
						|
        if (address >= dma_end or address < dma_start) and \
 | 
						|
            (address in rom.force_patch or new_buffer[address] != value)]
 | 
						|
    changed_addresses.sort()
 | 
						|
 | 
						|
    # Write the address changes. We'll store the data with XOR so that
 | 
						|
    # the patch data won't be raw data from the patched rom.
 | 
						|
    data = []
 | 
						|
    block_start = None
 | 
						|
    BLOCK_HEADER_SIZE = 7 # this is used to break up gaps
 | 
						|
    for address in changed_addresses:
 | 
						|
        # if there's a block to write and there's a gap, write it
 | 
						|
        if block_start:
 | 
						|
            block_end = block_start + len(data) - 1
 | 
						|
            if address > block_end + BLOCK_HEADER_SIZE:
 | 
						|
                xor_address = write_block(rom, xor_address, xor_range, block_start, data, patch_data)
 | 
						|
                data = []
 | 
						|
                block_start = None
 | 
						|
                block_end = None
 | 
						|
 | 
						|
        # start a new block
 | 
						|
        if not block_start:
 | 
						|
            block_start = address
 | 
						|
            block_end = address - 1             
 | 
						|
 | 
						|
        # save the new data
 | 
						|
        data += rom.buffer[block_end+1:address+1]
 | 
						|
 | 
						|
    # if there was any left over blocks, write them out
 | 
						|
    if block_start:
 | 
						|
        xor_address = write_block(rom, xor_address, xor_range, block_start, data, patch_data)
 | 
						|
 | 
						|
    # compress the patch file
 | 
						|
    patch_data = bytes(patch_data.buffer)
 | 
						|
    patch_data = zlib.compress(patch_data)
 | 
						|
 | 
						|
    return patch_data
 | 
						|
 | 
						|
 | 
						|
# This will apply a patch file to a source rom to generate a patched rom.
 | 
						|
def apply_patch_file(rom, file, sub_file=None):
 | 
						|
    # load the patch file and decompress
 | 
						|
    if sub_file:
 | 
						|
        with zipfile.ZipFile(file, 'r') as patch_archive:
 | 
						|
            try:
 | 
						|
                with patch_archive.open(sub_file, 'r') as stream:
 | 
						|
                    patch_data = stream.read()
 | 
						|
            except KeyError as ex:
 | 
						|
                raise FileNotFoundError('Patch file missing from archive. Invalid Player ID.')
 | 
						|
    else:
 | 
						|
        with open(file, 'rb') as stream:
 | 
						|
            patch_data = stream.read()
 | 
						|
    patch_data = BigStream(zlib.decompress(patch_data))
 | 
						|
 | 
						|
    # make sure the header is correct
 | 
						|
    if patch_data.read_bytes(length=4) != b'ZPFv':
 | 
						|
        raise Exception("File is not in a Zelda Patch Format")
 | 
						|
    if patch_data.read_byte() != ord('1'):
 | 
						|
        # in the future we might want to have revisions for this format
 | 
						|
        raise Exception("Unsupported patch version.")
 | 
						|
 | 
						|
    # load the patch configuration info. The fact that the DMA Table is
 | 
						|
    # included in the patch is so that this might be able to work with
 | 
						|
    # other N64 games.
 | 
						|
    dma_start = patch_data.read_int32()
 | 
						|
    xor_range = (patch_data.read_int32(), patch_data.read_int32())
 | 
						|
    xor_address = patch_data.read_int32()
 | 
						|
 | 
						|
    # Load all the DMA table updates. This will move the files around.
 | 
						|
    # A key thing is that some of these entries will list a source file
 | 
						|
    # that they are from, so we know where to copy from, no matter where
 | 
						|
    # in the DMA table this file has been moved to. Also important if a file
 | 
						|
    # is copied. This list is terminated with 0xFFFF
 | 
						|
    while True:
 | 
						|
        # Load DMA update
 | 
						|
        dma_index = patch_data.read_int16()
 | 
						|
        if dma_index == 0xFFFF:
 | 
						|
            break
 | 
						|
 | 
						|
        from_file = patch_data.read_int32()
 | 
						|
        start = patch_data.read_int32()
 | 
						|
        size = patch_data.read_int24()
 | 
						|
 | 
						|
        # Save new DMA Table entry
 | 
						|
        dma_entry = dma_start + (dma_index * 0x10)
 | 
						|
        end = start + size
 | 
						|
        rom.write_int32(dma_entry, start)
 | 
						|
        rom.write_int32(None,      end)
 | 
						|
        rom.write_int32(None,      start)
 | 
						|
        rom.write_int32(None,      0)
 | 
						|
 | 
						|
        if from_file != 0xFFFFFFFF:
 | 
						|
            # If a source file is listed, copy from there
 | 
						|
            old_dma_start, old_dma_end, old_size = rom.original.get_dmadata_record_by_key(from_file)
 | 
						|
            copy_size = min(size, old_size)
 | 
						|
            rom.write_bytes(start, rom.original.read_bytes(from_file, copy_size))
 | 
						|
            rom.buffer[start+copy_size:start+size] = [0] * (size - copy_size)
 | 
						|
        else:
 | 
						|
            # if it's a new file, fill with 0s
 | 
						|
            rom.buffer[start:start+size] = [0] * size
 | 
						|
 | 
						|
    # Read in the XOR data blocks. This goes to the end of the file.
 | 
						|
    block_start = None
 | 
						|
    while not patch_data.eof():
 | 
						|
        is_new_block = patch_data.read_byte() != 0xFF
 | 
						|
 | 
						|
        if is_new_block:
 | 
						|
            # start writing a new block
 | 
						|
            patch_data.seek_address(delta=-1)
 | 
						|
            block_start = patch_data.read_int32()
 | 
						|
            block_size = patch_data.read_int16()
 | 
						|
        else:
 | 
						|
            # continue writing from previous block
 | 
						|
            key_skip = patch_data.read_byte()
 | 
						|
            block_size = patch_data.read_int16()
 | 
						|
            # skip specified XOR keys
 | 
						|
            for _ in range(key_skip):
 | 
						|
                key, xor_address = key_next(rom, xor_address, xor_range)
 | 
						|
 | 
						|
        # read in the new data
 | 
						|
        data = []
 | 
						|
        for b in patch_data.read_bytes(length=block_size):
 | 
						|
            if b == 0:
 | 
						|
                # keep 0s as 0s
 | 
						|
                data += [0]
 | 
						|
            else:
 | 
						|
                # The XOR will always be safe and will never produce 0
 | 
						|
                key, xor_address = key_next(rom, xor_address, xor_range)
 | 
						|
                data += [b ^ key]
 | 
						|
 | 
						|
        # Save the new data to rom
 | 
						|
        rom.write_bytes(block_start, data)
 | 
						|
        block_start = block_start+block_size
 | 
						|
 |