diff --git a/N64-database.txt b/N64-database.txt index 6eb901a..4ac8e65 100644 --- a/N64-database.txt +++ b/N64-database.txt @@ -858,7 +858,8 @@ f25ad011df54065768c3a3cabf41d4b3 ntsc|cic6102 # V64Jr Backup Tool by RedBoX (V0. ; Source: https://misterfpga.org/viewtopic.php?t=8078 a5ee8a6c34863e3d0eb8c06ae8668b30 pal|cic7101|rpak|p4:080d0000|p5:5f81bb9d|p1cab0:84020000 # Batman Beyond - Return of the Joker [Batman of the Future - Return of the Joker] (Europe) (En,Fr,De) a08676124b326b1035b202c83a97468f ntsc|cic6102|rpak|p5:6a8d2e60|pb45f:8c010000 # Batman Beyond - Return of the Joker (USA) -997fd8f79cd6f3cd1c1c1fd21e358717 ntsc|cic6102|cpak|rpak|p4:42008400|p5:2b1d7193|p2778:6120e700 # Blues Brothers 2000 (USA) +31b4a8ed52b48e756b344c9f22736e50 pal|cic7101|cpak|rpak|p16:0140705c75713f86|p43841:e72061|p44549:372b37 # Blues Brothers 2000 (Europe) (En,Fr,De,It,Nl,Es) +997fd8f79cd6f3cd1c1c1fd21e358717 ntsc|cic6102|cpak|rpak|p17:60704ce7364a4b|p40417:e72061|p41125:372b37 # Blues Brothers 2000 (USA) baaf237e71aa7526c9b2f01c08b68a53 pal|cic7105|flash128k|rpak|p4:85035a65|p5:a06de792|p28043:1f006315|p28047:1b008314|p2804a:1800c314|p2804d:15004004|p2804f:1300401c|p28056:0c006014|p2805c:03006014|p2805f:03004010 # Jet Force Gemini (Europe) (En,Fr,De,Es) ca28a3645fc7ad969ebd75c5d6506e7a ntsc|cic6105|flash128k|rpak|p4:9a005af5|p5:6f0b382b|p27f57:1f006315|p27f5b:1b008314|p27f5e:1800c314|p27f61:15004004|p27f63:1300401c|p27f6a:0c006014|p27f70:03006014|p27f73:03004010 # Jet Force Gemini [Star Twins] (Japan) 772cc6eab2620d2d3cdc17bbc26c4f68 ntsc|cic6105|flash128k|rpak|p4:853f30b5|p5:2e13d8f9|p27fa7:1f006315|p27fab:1b008314|p27fae:1800c314|p27faf:021b0500|p27fb1:15004004|p27fb3:1300401c|p27fba:0c006014|p27fc0:03006014|p27fc3:03004010 # Jet Force Gemini (USA) diff --git a/patchgen.py b/patchgen.py new file mode 100644 index 0000000..f2ad8e4 --- /dev/null +++ b/patchgen.py @@ -0,0 +1,190 @@ +import sys +import os +import hashlib +import struct + +def decode_bps_number(data, offset): + """Decodes a variable-length integer from BPS data.""" + result = 0 + shift = 1 + while True: + byte = data[offset] + offset += 1 + result += (byte & 0x7F) * shift + if byte & 0x80: + return result, offset + shift <<= 7 + result += shift + +def apply_bps_patch(source_data, patch_data): + """Applies a BPS patch to source_data and returns the target bytearray.""" + if patch_data[:4] != b'BPS1': + raise ValueError("Invalid BPS header.") + + offset = 4 + source_len, offset = decode_bps_number(patch_data, offset) + target_len, offset = decode_bps_number(patch_data, offset) + metadata_len, offset = decode_bps_number(patch_data, offset) + offset += metadata_len # Skip metadata + + target_data = bytearray(target_len) + + output_offset = 0 + source_offset = 0 + target_read_offset = 0 + + while output_offset < target_len: + action_code, offset = decode_bps_number(patch_data, offset) + action = action_code & 3 + length = (action_code >> 2) + 1 + + if action == 0: # SourceRead + # Copy bytes from source ROM to target ROM + chunk = source_data[output_offset : output_offset + length] + target_data[output_offset : output_offset + length] = chunk + output_offset += length + + elif action == 1: # TargetRead + # Read bytes directly from the patch file + chunk = patch_data[offset : offset + length] + target_data[output_offset : output_offset + length] = chunk + output_offset += length + offset += length + + elif action == 2: # SourceCopy + # Copy data from anywhere in source + data, offset = decode_bps_number(patch_data, offset) + # Map encoded int to positive/negative shift + shift = (data >> 1) + if data & 1: + shift = -shift + source_offset += shift + + chunk = source_data[source_offset : source_offset + length] + target_data[output_offset : output_offset + length] = chunk + + output_offset += length + source_offset += length + + elif action == 3: # TargetCopy + # Copy data from already written parts of target + data, offset = decode_bps_number(patch_data, offset) + shift = (data >> 1) + if data & 1: + shift = -shift + target_read_offset += shift + + # Since we might copy from overlapping regions, we do it byte by byte + # or careful slicing. Python slices create copies, so safe for simple overlaps. + # But specific run-length patterns might require loops. + for i in range(length): + target_data[output_offset + i] = target_data[target_read_offset + i] + + output_offset += length + target_read_offset += length + + return target_data + +def get_xor_diff(source, target, max_diff=256): + """Compares two bytearrays and returns XOR deltas.""" + + # Handle size mismatches by padding the shorter one with nulls for comparison + max_len = max(len(source), len(target)) + diffs = [] + + diff_count = 0 + + i = 0 + while i < max_len: + b_src = source[i] if i < len(source) else 0 + b_tgt = target[i] if i < len(target) else 0 + + if b_src != b_tgt: + xor_val = b_src ^ b_tgt + + # Start a contiguous block + block_start = i + block_vals = [xor_val] + diff_count += 1 + + # Look ahead for contiguous differences + j = i + 1 + while j < max_len: + b_src_next = source[j] if j < len(source) else 0 + b_tgt_next = target[j] if j < len(target) else 0 + + if b_src_next != b_tgt_next: + block_vals.append(b_src_next ^ b_tgt_next) + diff_count += 1 + if diff_count > max_diff: + raise ValueError(f"Too many differences (>{max_diff}). Aborting.") + j += 1 + else: + break + + # Store the block + diffs.append((block_start, block_vals)) + i = j + else: + i += 1 + + return diffs + +def main(): + if len(sys.argv) < 3: + print("Usage: python xordiff.py ") + sys.exit(1) + + rom_path = sys.argv[1] + file2_path = sys.argv[2] + + if not os.path.exists(rom_path) or not os.path.exists(file2_path): + print("Error: Input files not found.") + sys.exit(1) + + # 1. Load Original ROM + with open(rom_path, 'rb') as f: + source_bytes = bytearray(f.read()) + + # 2. Calculate MD5 of Original ROM + rom_md5 = hashlib.md5(source_bytes).hexdigest() + + # 3. Load File 2 (Check if BPS or ROM) + with open(file2_path, 'rb') as f: + file2_bytes = bytearray(f.read()) + + target_bytes = None + + # Check for BPS Header "BPS1" + if file2_bytes[:4] == b'BPS1': + try: + target_bytes = apply_bps_patch(source_bytes, file2_bytes) + except Exception as e: + print(f"Error applying BPS patch: {e}") + sys.exit(1) + else: + # Assume it's a pre-patched ROM + target_bytes = file2_bytes + + # 4. Calculate Differences + try: + diffs = get_xor_diff(source_bytes, target_bytes, max_diff=1024) + except ValueError as e: + print(f"Error: {e}") + sys.exit(1) + + # 5. Format Output + # Format: pOFFSET:HEXVAL + output_parts = [] + + for offset, vals in diffs: + hex_str = "".join(f"{b:02x}" for b in vals) + output_parts.append(f"p{offset}:{hex_str}") + + diff_str = "|".join(output_parts) + filename = os.path.basename(rom_path) + + print(f"{rom_md5} {diff_str} # {filename}") + +if __name__ == "__main__": + main() \ No newline at end of file