272 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			272 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, file, 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) | ||
|  | 
 | ||
|  |     # save the patch file | ||
|  |     with open(file, 'wb') as outfile: | ||
|  |         outfile.write(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 | ||
|  | 
 |