From 96a14e829a6ad2267d44d41db9e996257b30de41 Mon Sep 17 00:00:00 2001 From: Nayla Date: Fri, 28 Apr 2023 19:54:14 -0400 Subject: [PATCH] Initial commit --- .github/workflows/win32.yaml | 25 ++ README.md | 4 + assets/icon.ico | Bin 0 -> 67646 bytes assets/operation/error.png | Bin 0 -> 7304 bytes assets/operation/success.png | Bin 0 -> 19666 bytes functions.py | 75 ++++ injector_lib.py | 685 +++++++++++++++++++++++++++++++++++ main.py | 100 +++++ n64crc.py | 126 +++++++ requirements.txt | 4 + version.py | 8 + 11 files changed, 1027 insertions(+) create mode 100644 .github/workflows/win32.yaml create mode 100644 README.md create mode 100644 assets/icon.ico create mode 100644 assets/operation/error.png create mode 100644 assets/operation/success.png create mode 100644 functions.py create mode 100644 injector_lib.py create mode 100644 main.py create mode 100644 n64crc.py create mode 100644 requirements.txt create mode 100644 version.py diff --git a/.github/workflows/win32.yaml b/.github/workflows/win32.yaml new file mode 100644 index 0000000..60b996f --- /dev/null +++ b/.github/workflows/win32.yaml @@ -0,0 +1,25 @@ +name: Windows CI + +on: + push: + pull_request: + +jobs: + build-win32: + name: "Windows Build" + runs-on: windows-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install PIP Modules + run: python -m pip install -r requirements.txt + + - name: Build Project (Dynamic build) + run: pyinstaller --onefile .\main.py --add-data "assets/operation/*;assets/operation/" --name="MIPS-CodeWrite" -w --icon="assets/icon.ico" + + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: MIPSCodeWrite-win32 + path: D:\a\MIPS-CodeWrite\MIPS-CodeWrite\dist \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c75740c --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# MIPS-CodeWrite +An experimental MIPS ASM to GameShark Compiler. + +![image](https://github.com/user-attachments/assets/ddd3a249-7773-4905-b6b4-ebf986ffbeab) diff --git a/assets/icon.ico b/assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7d3b0ada8baf073ce326e9be0db307a86da76775 GIT binary patch literal 67646 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 L@93^ohRyvyClR9i literal 0 HcmV?d00001 diff --git a/assets/operation/error.png b/assets/operation/error.png new file mode 100644 index 0000000000000000000000000000000000000000..aaaae4fda85fb8b2fa49d6542e923ad1b38d5b50 GIT binary patch literal 7304 zcmeHLc{o&U|38+pXBVwwLc*BcEKy;sQDaxBoY@%5%rqv3q7n*8WeF(~DIQYEQQ^;lkfbdB$ z)n(odnC$d~#!5-ayd}3vO&=?Tem?2jZnh&nStg@U<OHk z>J|gLo;&JKPxT*-ATBtg-)f((^=7L03%0Dqy&%OUJc54J%;@T_D>@&oaZxcwp@bq1 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 W1a2J+^$2>t0~~DDQA(|RqyGiZzexxH literal 0 HcmV?d00001 diff --git a/assets/operation/success.png b/assets/operation/success.png new file mode 100644 index 0000000000000000000000000000000000000000..ad11f7e95ca0b9b03f66b4a41bbacb41ba09a2f9 GIT binary patch literal 19666 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 RZNM=E22WQ%mvv4FO#tM+I#d7v literal 0 HcmV?d00001 diff --git a/functions.py b/functions.py new file mode 100644 index 0000000..5a7e805 --- /dev/null +++ b/functions.py @@ -0,0 +1,75 @@ +# ============================================ +# MIPS-CodeWrite +# Author: Nayla Hanegan (naylahanegan@gmail.com) +# Date: 6/20/2024 +# License: GPLv2 +# ============================================ + +import tkinter as tk +import customtkinter as ctk +from PIL import Image, ImageTk +from pathlib import Path +import sys +import requests +import sys +import webbrowser +import tkinter.filedialog +import os +import threading +import json +import subprocess + +def createDialog(windowTitle, warn, info, buttonTxt=None): + completeWindow = ctk.CTkToplevel() + completeWindow.title(windowTitle) + + # Load success image and display it in the success window + img = ctk.CTkImage(Image.open(fetchResource("assets/operation/" + warn + ".png")), size=(100, 100)) + imgLabel = ctk.CTkLabel(completeWindow, image=img, text="") + imgLabel.grid(row=0, column=0, padx=10, pady=10) + imgLabel.image = img # Keep a reference to the image + + if buttonTxt is not None: + try: + button = ctk.CTkButton(completeWindow, command=run_update, text=buttonTxt) + button.grid(row=1, column=0, padx=50, pady=10) + except Exception as e: + print("Error creating button:", e) + + # Adjust geometry to place the window in the bottom right corner + screen_width = completeWindow.winfo_screenwidth() + screen_height = completeWindow.winfo_screenheight() + window_width = completeWindow.winfo_reqwidth() + window_height = completeWindow.winfo_reqheight() + if sys.platform == "darwin": + x_coordinate = 15 + y_coordinate = screen_height - window_height + else: + x_coordinate = 15 + y_coordinate = screen_height - window_height - 20 + completeWindow.geometry(f"+{x_coordinate}+{y_coordinate}") + + # Configure row and column weights + completeWindow.columnconfigure(0, weight=1) + completeWindow.rowconfigure(0, weight=1) + + # Display success message in the success window + label = ctk.CTkLabel(completeWindow, text=info, font=ctk.CTkFont(size=18)) + label.grid(row=0, column=1, padx=25, pady=10) + + # Function to close the window after 2.5 seconds + def close_window(): + completeWindow.destroy() + + # Close the window after 2.5 seconds + completeWindow.after(2500, close_window) + + completeWindow.focus() + +def fetchResource(resource_path: Path) -> Path: + try: # Running as *.exe; fetch resource from temp directory + base_path = Path(sys._MEIPASS) + except AttributeError: # Running as script; return unmodified path + return resource_path + else: # Return temp resource path + return base_path.joinpath(resource_path) \ No newline at end of file diff --git a/injector_lib.py b/injector_lib.py new file mode 100644 index 0000000..4705f9a --- /dev/null +++ b/injector_lib.py @@ -0,0 +1,685 @@ + +#Import python libraries +import struct +#from numpy import * +import n64crc +from os import path + + +#Return the value of a register based off of its name +#See https://en.wikibooks.org/wiki/MIPS_Assembly/Register_File +def register(name): + if name[0] != '$': #Add $ to the register name if not there + name = '$'+name + if name == '$zero' or name == '$r0': #Always zero + return 0 + elif name == '$at': #Reserved for assembler + return 1 + elif name == '$v0': #First and second return values, respectively + return 2 + elif name == '$v1': + return 3 + elif name == '$a0': #First four return arguents to functions + return 4 + elif name == '$a1': + return 5 + elif name == '$a2': + return 6 + elif name == '$a3': + return 7 + elif name == '$t0': #Temporary registers + return 8 + elif name == '$t1': + return 9 + elif name == '$t2': + return 10 + elif name == '$t3': + return 11 + elif name == '$t4': + return 12 + elif name == '$t5': + return 13 + elif name == '$t6': + return 14 + elif name == '$t7': + return 15 + elif name == '$s0': #Saved registers + return 16 + elif name == '$s1': + return 17 + elif name == '$s2': + return 18 + elif name == '$s3': + return 19 + elif name == '$s4': + return 20 + elif name == '$s5': + return 21 + elif name == '$s6': + return 22 + elif name == '$s7': + return 23 + elif name == '$t8': #More temporary registers + return 24 + elif name == '$t9': + return 25 + elif name == '$k0': #Reserved for kernel (operating system) + return 26 + elif name == '$k1': + return 27 + elif name == '$gp': #Global pointer + return 28 + elif name == '$sp': #Stack pointer + return 29 + elif name == '$fp': #Frame pointer + return 30 + elif name == '$ra': #Return address + return 31 + + + + + +#MIPS Assembler function, feed it a command as a string and it returns the hex +#This is not really meant to be a 100% fully functional, but to help it make it easier for me to hack the game +#For commands see https://en.wikibooks.org/wiki/MIPS_Assembly/Instruction_Formats +#For information about instruction types see https://web.archive.org/web/20180101004911if_/http://www.cs.umd.edu/class/spring2003/cmsc311/Notes/Mips/format.html +def assembler(asm): + asm = asm.replace(', ', ' ').replace(')','').replace('(','').replace(',',' ') + words = asm.lower().split(' ') + n_words = len(words) + command = words[0] + opcode = 0x00 + funct = 0x00 + reg_s = 0x00 + reg_t = 0x00 + reg_d = 0x00 + imm = 0x0000 + address = 0x000000 + shift_amount = 0x00 + if command == 'nop': #Do nothing + return(0x00000000) + elif command == 'cache': #Special handling of the cache command + itype = 'I' + funct = 0x2F + reg_t = words[1] #Not a register in this case (CACHE is special) + reg_s = register(words[2]) + imm = int(words[3], 16) + elif command == 'add': #Add + itype = 'R' + funct = 0x20 + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'addi': #Add Immediate + itype = 'I' + opcode = 0x08 + reg_t = register(words[1]) + reg_s = register(words[2]) + imm = int(words[3], 16) + elif command == 'addiu': #Add Unsigned Immediate + itype = 'I' + opcode = 0x09 + reg_t = register(words[1]) + reg_s = register(words[2]) + imm = int(words[3], 16) + elif command == 'addu': #Add Unsigned + itype = 'R' + funct = 0x21 + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'and': #Bitwise AND + itype = 'R' + funct = 0x24 + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'andi': #Bitwise AND Immediate + itype = 'I' + opcode = 0x0C + reg_t = register(words[1]) + reg_s = register(words[2]) + imm = int(words[3], 16) + elif command == 'beq': #Branch if Equal + itype = 'I' + opcode = 0x04 + reg_s = register(words[1]) + reg_t = register(words[2]) + imm = int(words[3], 16) + elif command == 'beqz': #Branch if Equal to Zero + itype = 'I' + opcode = 0x04 + reg_s = register(words[1]) + reg_t = 0 + imm = int(words[2], 16) + elif command == 'blez': #Branch if Less Than or Equal to Zero + itype = 'I' + opcode = 0x06 + reg_s = register(words[1]) + imm = int(words[2], 16) + elif command == 'bne': #Branch if Not Equal + itype = 'I' + opcode = 0x05 + reg_s = register(words[1]) + reg_t = register(words[2]) + imm = int(words[3], 16) + elif command == 'bgtz': #Branch on Greater Than Zero + itype = 'I' + opcode = 0x07 + reg_s = register(words[1]) + imm = int(words[2], 16) + elif command == 'div': #Divide + itype = 'R' + funct = 0x1A + reg_s = register(words[1]) + reg_t = register(words[2]) + elif command == 'divu': #Unsigned Divide + itype = 'R' + funct = 0x1B + reg_s = register(words[1]) + reg_t = register(words[2]) + elif command == 'j': #Jump to Address + itype = 'J' + opcode = 0x02 + address = int((int(words[1], 16) - 0x80000000)/4) + elif command == 'jal': #Jump and Link + itype = 'J' + opcode = 0x03 + address = int((int(words[1], 16) - 0x80000000)/4) + elif command == 'jalr': #Jump and Link Register + itype = 'R' + funct = 0x09 + reg_s = register(words[1]) + reg_d = register(words[2]) + elif command == 'jr': #Jump to Address in Register + itype = 'R' + funct = 0x08 + reg_s = register(words[1]) + elif command == 'lb': #Load Byte + itype = 'I' + opcode = 0x20 + reg_t = register(words[1]) + imm = int(words[2], 16) + reg_s = register(words[3]) + elif command == 'lbu': #Load Byte Unsigned + itype = 'I' + opcode = 0x24 + reg_t = register(words[1]) + imm = int(words[2], 16) + reg_s = register(words[3]) + elif command == 'lhu': #Load Halfword Unsigned + itype = 'I' + opcode = 0x25 + reg_t = register(words[1]) + imm = int(words[2], 16) + reg_s = register(words[3]) + elif command == 'lh': #Load Halfword + itype = 'I' + opcode = 0x21 + reg_t = register(words[1]) + imm = int(words[2], 16) + reg_s = register(words[3]) + elif command == 'lui': #Load Upper Immediate + itype = 'I' + opcode = 0x0F + reg_t = register(words[1]) + imm = int(words[2], 16) + elif command == 'lw': #Load Word + itype = 'I' + opcode = 0x23 + reg_t = register(words[1]) + imm = int(words[2], 16) + reg_s = register(words[3]) + elif command == 'mfhi': #Move from HI Register + itype = 'R' + funct = 0x10 + reg_d = register(words[1]) + elif command == 'mthi': #Move to HI Register + itype = 'R' + funct = 0x11 + reg_s = register(words[1]) + elif command == 'mflo': #Move from LO Register + itype = 'R' + funct = 0x12 + reg_d = register(words[1]) + elif command == 'mtlo': #Move to LO Register + itype = 'R' + funct = 0x13 + reg_s = register(words[1]) + elif command == 'mfc0': #Move from Coprocessor 0 + itype = 'R' + opcode = 0x10 + elif command == 'mult': #Multiply + itype = 'R' + funct = 0x18 + reg_s = register(words[1]) + reg_t = register(words[2]) + elif command == 'multu': #Unsigned Multiply + itype = 'R' + funct = 0x19 + reg_s = register(words[1]) + reg_t = register(words[2]) + elif command == 'nor': #Bitwise NOR (NOT-OR) + itype = 'R' + funct = 0x27 + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'xor': #Bitwise XOR (Exclusive-OR) + itype = 'R' + funct = 0x26 + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'or': #Bitwise OR + itype = 'R' + funct = 0x25 + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'ori': #Bitwise OR Immediate + itype = 'I' + opcode = 0x0D + reg_t = register(words[1]) + reg_s = register(words[2]) + imm = int(words[3], 16) + elif command == 'sb': #Store Byte + itype = 'I' + opcode = 0x28 + reg_t = register(words[1]) + imm = int(words[2], 16) + reg_s = register(words[3]) + elif command == 'sh': #Store Halfword + itype = 'I' + opcode = 0x29 + reg_t = register(words[1]) + imm = int(words[2], 16) + reg_s = register(words[3]) + elif command == 'slt': #Set to 1 if Less Than + itype = 'R' + funct = 0x2A + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'slti': #Set to 1 if Less Than Immediate + itype = 'I' + opcode = 0x0A + reg_t = register(words[1]) + reg_s = register(words[2]) + imm = int(words[3], 16) + elif command == 'sltiu': #Set to 1 if Less Than Unsigned Immediate + itype = 'I' + opcode = 0x0B + reg_t = register(words[1]) + reg_s = register(words[2]) + imm = int(words[3], 16) + elif command == 'sltu': #Set to 1 if Less Than Unsigned + itype = 'R' + funct = 0x2B + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'sll': #Logical Shift Left + itype = 'R' + opcode = 0x00 + reg_d = register(words[1]) + reg_t = register(words[2]) + shift_amount = int(words[3], 16) + elif command == 'srl': #Logical Shift Right (0-extended) + itype = 'R' + funct = 0x02 + reg_d = register(words[1]) + reg_t = register(words[2]) + shift_amount = int(words[3], 16) + elif command == 'sra': #Arithmetic Shift Right (sign-extended) + itype = 'R' + funct = 0x03 + reg_d = register(words[1]) + reg_t = register(words[2]) + shift_amount = int(words[3], 16) + elif command == 'sub': #Subtract + itype = 'R' + funct = 0x22 + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'subu': #Unsigned Subtract + itype = 'R' + funct = 0x23 + reg_d = register(words[1]) + reg_s = register(words[2]) + reg_t = register(words[3]) + elif command == 'sw': #Store Word + itype = 'I' + opcode = 0x2B + reg_t = register(words[1]) + imm = int(words[2], 16) + reg_s = register(words[3]) + if itype == 'R': + return(opcode*2**26 + reg_s*2**21 + reg_t*2**16 + reg_d*2**11 + shift_amount*2**6 + funct) + elif itype == 'I': + return(opcode*2**26 + reg_s*2**21 + reg_t*2**16 + imm) + elif itype == 'J': + return(opcode*2**26 + address) + else: + print('ERROR: Something went wrong with '+asm) + return(result) + +#Convert asssembly (or hex) to gameshark code +def asm_to_gameshark(ram_address, asm): + if len(asm) == 1: #Handle single lines + asm = [asm] + gameshark_code = [] #List to hold output + current_byte = 0 #Track memory address where GS code is going + for asm_line in asm: #Loop through each ASM instruction + if type(asm_line) == str: #If ASM is a string + asm_converted_to_hex = assembler(asm_line) #Run the assembler to convert asm to hex + elif type(asm_line) == int: #If ASM already is in hex + asm_converted_to_hex = asm_line #Just use the existing hex + #asm_converted_to_hex_str = hex(asm_converted_to_hex)[2:].upper() #Convert hex for ASM instruction into a string + asm_converted_to_hex_str = hex(asm_converted_to_hex)[2:].zfill(8).upper() + if asm_converted_to_hex_str == '00000000': #Ensure NOPs are 1 line to shorten codes + gameshark_code.append('81'+hex(ram_address+current_byte)[4:].upper()+' 2400') #Write first 16 bits of 32 bit ASM instruction as an 81 type gs code + current_byte = current_byte + 4 #Increment current byte + else: + gameshark_code.append('81'+hex(ram_address+current_byte)[4:].upper()+' '+asm_converted_to_hex_str[0:4]) #Write first 16 bits of 32 bit ASM instruction as an 81 type gs code + current_byte = current_byte + 2 #Increment current byte + gameshark_code.append('81'+hex(ram_address+current_byte)[4:].upper()+' '+asm_converted_to_hex_str[4:8]) #Write last 16 bits of 32 bit ASM instruction as an 81 type gs code + current_byte = current_byte + 2 + return gameshark_code + + + +#Functions to insert instrucitons or data into the rom, shamelessly ripped from Micro500's micro mountain python scirpt +def mem_read_u32_be(addr, data): + return struct.unpack(">I",data[addr:addr+4])[0] + +def mem_read_u16_be(addr, data): + return struct.unpack(">H",data[addr:addr+2])[0] + +def u16_to_le(data): + return struct.pack("H",data) + +def s16_to_be(data): + return struct.pack(">h",data) + +def u32_to_be(data): + return struct.pack(">I",data) + +def mem_fill(data, start_addr, end_addr, value): + return data[0:start_addr] + (bytes([value] * (end_addr - start_addr))) + data[end_addr:len(data)] + +def mem_write_u32_be(addr, value, data): + return data[0:addr] + u32_to_be(value) + data[(addr+4):len(data)] + +def mem_write_u16_be(addr, value, data): + return data[0:addr] + u16_to_be(value) + data[(addr+2):len(data)] + +def mem_write_u8_be(addr, value, data): + return data[0:addr] + u8_to_be(value) + data[(addr+1):len(data)] + +def mem_write_16(addr, value, data): + return data[0:addr] + value + data[(addr+2):len(data)] + +# #Convert Gameshark like script into ASM (AKA compile) +# def script_to_asm(script): +# line = 1 +# script_to_asm = [] +# for script_line in script: +# words = script_line.split().lower() +# if words[1] == 'if': +# #do stuff +# elif words[1] == 'while': +# #do stuff +# elif words[1] == 'for': +# #do stuff + + +#Convert any 50 type gameshark codes into their many codes equivilent +def convert_50_type_gameshark_codes(gameshark_codes): + updated_gameshark_codes = [] + last_code_type = '' + for i in range(len(gameshark_codes)): + gameshark_code = gameshark_codes[i] + if gameshark_code[0:2] == '50': + next_gameshark_code = gameshark_codes[i+1] + n_addresses = int(gameshark_code[4:6], 16) + address_offset = int(gameshark_code[6:8], 16) + increment_value = int(gameshark_code[9:13], 16) + if increment_value >= 0x8000: #Handle using signed values to go negative + increment_value = 0x8000 - increment_value + code_type = next_gameshark_code[0:2] + address = int(next_gameshark_code[2:8], 16) + code_value = int(next_gameshark_code[9:13], 16) + for j in range(n_addresses): + updated_gameshark_codes.append((code_type + format(address + j*address_offset, '06x') + ' ' + format(code_value + j*increment_value, '04x')).upper()) + if gameshark_code[0:2] != '50': + if i == 0: + updated_gameshark_codes.append(gameshark_code) + elif gameshark_codes[i-1][0:2] != '50': + updated_gameshark_codes.append(gameshark_code) + return updated_gameshark_codes + + + + +#Convert gameshark codes to asm/hex +def gameshark_to_hex(gameshark_codes, boot=False): #, rom_jumps=False, end_of_rom_address=0x0): + gameshark_codes = convert_50_type_gameshark_codes(gameshark_codes) + line = 1 + last_code_type = '80' + int_last_mem_address = 000000 + code_before_last_code_type = '80' + code_value = 0000 + gameshark_hex = [] + for gameshark_code in gameshark_codes: #Build the code handler + code_type = gameshark_code[0:2] + mem_address = gameshark_code[2:8] + try: + int_mem_address = int(gameshark_code[2:8], 16) + mem_address_suffix = int(gameshark_code[4:8], 16) + if mem_address_suffix >= 0x8000: + mem_address_prefix = 0x8001 + int(gameshark_code[2:4], 16) + else: + mem_address_prefix = 0x8000 + int(gameshark_code[2:4], 16) + if code_type == '80' or code_type == 'A0' or code_type == 'F0' or code_type == 'D0' or code_type == 'D2': + code_value = int(gameshark_code[11:13], 16) + elif code_type == '81' or code_type == 'A1' or code_type == 'F1' or code_type == 'D1' or code_type == 'D3': + code_value = int(gameshark_code[9:13], 16) + if boot == False: + if (code_type == '81' or code_type == 'A1') and (last_code_type == '81' or last_code_type == 'A1') and code_type_before_last_code_type[0] != 'D' and int_mem_address - int_last_mem_address == 2 and int_last_mem_address % 4 == 0: #For two 81 type codes that combined write a word to memory, write the actual word to memory + gameshark_hex = gameshark_hex[:-3] #Remove previous 81 code + gameshark_hex.append(0x3C1A0000 + mem_address_prefix) #LUI $K0 0xXXXX + gameshark_hex.append(0x3C1B0000 + last_code_value) #LUI $K1 0xXXXX + gameshark_hex.append(0x377B0000+code_value) #ORI $K1 $K1 0x0000 + gameshark_hex.append(0xAF5B0000+last_mem_address_suffix) #SW $K1 0xXXXX ($K0) + elif code_type == '81' and code_value == '2400' and int_mem_address % 4 == 0 and last_code_type[0] != 'D': #Use $zero for NOPs + gameshark_hex.append(0x3C1A0000+mem_address_prefix) #LUI $K0 0xXXXX + gameshark_hex.append(0xAF400000+last_mem_address_suffix) #SW $zero 0xXXXX ($K0) + elif code_type == '80' or code_type == 'A0': + gameshark_hex.append(0x3C1A0000+mem_address_prefix) #LUI $K0 0xXXXX + gameshark_hex.append(0x241B0000+code_value) #ADDIU $K1 $ZERO, 0x00XX + gameshark_hex.append(0xA35B0000+mem_address_suffix) #SB $K1 0xXXXX ($K0) + if last_code_type[0] == 'D': #If last code type was a conditional, add a NOP + gameshark_hex.append(0x00000000) #Do nothing + elif code_type == '81' or code_type == 'A1': + gameshark_hex.append(0x3C1A0000+mem_address_prefix) #LUI $K0 0xXXXX + gameshark_hex.append(0x241B0000+code_value) #ADDIU $K1 $ZERO 0xXXXX + gameshark_hex.append(0xA75B0000+mem_address_suffix) #SH $K1 0xXXXX ($K0) + if last_code_type[0] == 'D': #If last code type was a conditional, add a NOP + gameshark_hex.append(0x00000000) #Do nothing + elif code_type == 'D0': + gameshark_hex.append(0x3C1A0000+mem_address_prefix) #LUI $K0 0xXXXX + gameshark_hex.append(0x935A0000+mem_address_suffix) #LBU $K0 0x0000 ($K0) + gameshark_hex.append(0x241B0000+code_value) #ADDIU $K1 $ZERO 0x00XX + gameshark_hex.append(0x175B0003) #BNE $K0 $K1 0x0004 + elif code_type == 'D1': + gameshark_hex.append(0x3C1A0000+mem_address_prefix) #LUI $K0 0xXXXX + gameshark_hex.append(0x975A0000+mem_address_suffix) #LHU $K0 0xXXXX ($K0) + gameshark_hex.append(0x241B0000+code_value) #ADDIU $K1 $ZERO 0xXXXX + gameshark_hex.append(0x175B0003) #BNE $K0 $K1 0x0004 + elif code_type == 'D2': + gameshark_hex.append(0x3C1A0000+mem_address_prefix) #LUI $K0 0xXXXX + gameshark_hex.append(0x935A0000+mem_address_suffix) #LBU $K0 0x0000 ($K0) + gameshark_hex.append(0x241B0000+code_value) #ADDIU $K1 $ZERO 0x00?? + gameshark_hex.append(0x135B0003) #BEQ $K0 $K1 0x0004 + elif code_type == 'D3': + gameshark_hex.append(0x3C1A0000+mem_address_prefix) #LUI $K0 0xXXXX + gameshark_hex.append(0x975A0000+mem_address_suffix) #LHU $K0 0x0000 ($K0) + gameshark_hex.append(0x241B0000+code_value) #ADDIU $K1, $ZERO, 0xXXXX + gameshark_hex.append(0x135B0003)#BEQ $K0, $K1, 0x0004 + elif code_type == 'F0' or code_type == 'F1' or code_type == 'EE' or code_type == 'FF' or code_type == 'DE': + 0 #Do nothing + else: + print('WARNING: Line '+str(line)+ '('+gameshark_code+') in Gameshark code list is invalid.') + elif boot == True: + if (code_type == 'F1') and (last_code_type == 'F1') and int_mem_address - int_last_mem_address == 2 and int_last_mem_address % 4 == 0: #For two F1 type codes that combined write a word to memory, write the actual word to memory + gameshark_hex = gameshark_hex[:-3] #Remove previous F1 code + gameshark_hex.append(0x3C040000+mem_address_prefix) #LUI $a0 0xXXXX + gameshark_hex.append(0x3C050000+last_code_value) #LUI $a1 0xXXXX + gameshark_hex.append(0x34A50000+code_value) #ORI $a1 $a1 0x0000 + gameshark_hex.append(0xAC850000+last_mem_address_suffix) #SW $a1 0xXXXX ($a0) + elif code_type == 'F0': #8 bit write at boot + gameshark_hex.append(0x3C040000+mem_address_prefix) #LUI $a0 0xXXXX + gameshark_hex.append(0x24050000+code_value) #ADDIU $a1 $ZERO, 0x00XX + gameshark_hex.append(0xA0850000+mem_address_suffix) #SB $a1 0xXXXX ($a0) + elif code_type == 'F1': #16 bit write at boot + gameshark_hex.append(0x3C040000+mem_address_prefix) #LUI $a0 0xXXXX + gameshark_hex.append(0x24050000+code_value) #ADDIU $a1 $ZERO 0xXXXX + gameshark_hex.append(0xA4850000+mem_address_suffix) #SH $a1 0xXXXX ($a0) + elif code_type == 'EE': #Disable expansion pak + gameshark_hex.append(0x3C048000) #LUI $a0 0x8000 + gameshark_hex.append(0x3C050040) #LUI $a1 0x0040 + gameshark_hex.append(0xAC850318) #SW $a1 0x0318 ($a0) + line = line + 1 + code_type_before_last_code_type = last_code_type + last_code_type = code_type + int_last_mem_address = int_mem_address + last_mem_address_suffix = mem_address_suffix + last_code_value = code_value + # if rom_jumps and line % 5 and line != 0: #Add in jumps if running directly from rom to force the N64 to load the codes into the N64 cache, needed to work on console when running from rom + # size_gs_codes = len(gameshark_hex)*4 #Get size of gs hex instructions + # jump_to_this_rom_address = format(end_of_rom_address + len(gameshark_hex)*4 + 0x10 + 0xB0000000, '08x') #Set up address to jump to (really just two instructions below the jump) + # code = [ + # 'LUI $k0 0x'+jump_to_this_rom_address[0:4], + # 'ORI $k0 $k0 0x'+jump_to_this_rom_address[4:8], + # 'JR $k0', + # 'NOP', + # ] + # gameshark_hex = gameshark_hex + code #Append jump to gameshark hex + except: + print('Ignoring line '+str(line)+' which does not appear to be a Gameshark code: "'+gameshark_code+'"') + + gameshark_hex.append(0x00000000) + return gameshark_hex + + + +#Class handles everything for patching a rom +class rom: + def __init__(self, file_name): + self.load(file_name) + self.rom_to_ram = mem_read_u32_be(0x8, self.data) - 0x1000 #Grab converstion of RAM to ROM (negative to go ROM to RAM) from ram address of the bootcode at 0x8 in the rom + self.ram_start = mem_read_u32_be(0x8, self.data) + print('Rom-to-ram = ', hex(self.rom_to_ram)) + print('Start ram is ', hex(self.ram_start)) + self.ram_to_rom = - self.rom_to_ram + self.rom_end = len(self.data) #Count the number of bytes in the rom + self.file_name = path.split(file_name)[-1] #Save filename of rom, only grabbing the last part of a path + def load(self, file_name): #Load rom data from file (.z64) + rom_file = open(file_name, 'rb') + self.data = rom_file.read() + rom_file.close() + def save(self, file_name, checksum=True): #Save a patched rom + if checksum: + self.fix_checksum() #Fix checksum before saving patched rom (important this is the last step before saving the rom) + patched_rom_file = open(file_name, 'wb') + patched_rom_file.write(self.data) + patched_rom_file.close() + def insert_hex(self, addr, hexcode): #Insert 32 bit MIPs instruction(s) or data in hex format into rom + if type(hexcode) == int: #If there is only one line + self.data = mem_write_u32_be(addr, hexcode, self.data)#Insert a single 32 bit value into ram + else: #If there are multiple lines + for line in hexcode: #Loop through each line + self.data = mem_write_u32_be(addr, line, self.data)#Insert a single 32 bit value into ram + addr += 4 #Go to next line + def insert_code(self, addr, code): + try: + if type(code) == int: + self.data = mem_write_u32_be(addr, code, self.data)#Insert a single 32 bit value into ram + elif type(code) == str: #If there is only one line + self.data = mem_write_u32_be(addr, assembler(code), self.data)#Insert a single 32 bit value into ram + except: + print('INSERT CODE ERROR: ', hex(code)) + else: #If there are multiple lines + line_number = 0 + for line in code: #Loop through each line + line_number += 1 + try: + if type(line) == int: + self.data = mem_write_u32_be(addr, line, self.data)#Insert a single 32 bit value into ram + #print('Inserting ', '0x'+format(line, '08x').upper(), ' into rom at ', '0x'+format(addr, '08x').upper()) + elif type(line) == str: + self.data = mem_write_u32_be(addr, assembler(line), self.data)#Insert a single 32 bit value into ram + #print('Inserting ', line, ';', '0x'+format(assembler(line), '08x').upper(), ' into rom at ', '0x'+format(addr, '08x').upper()) + except: + if type(line) == int: + print('INSERT CODE ERROR ON LINE '+str(line_number)+': ', hex(line)) + else: + print('INSERT CODE ERROR ON LINE '+str(line_number)+': ', line) + addr += 4 #Go to next line + def insert_codeblock(self, hook_addr, codeblock_addr, codeblock, jump_addr=0, offset=0, ignore_offset=False): #Insert a block of custom code with a hook (and custom jump back if you want) + hook_instructions = [0x08000000 + int((codeblock_addr + self.rom_to_ram - 0x80000000)/4),0x00000000] + self.insert_code(hook_addr, hook_instructions) + if jump_addr == 0: #If no jump address is specified, just jump back to right after the hook + jump_back = [0x08000000 + 1 + int((hook_addr + self.rom_to_ram - 0x80000000)/4), 0x00000000] + else: #Else use the specified jump address + jump_back = [0x08000000 + int((jump_addr + self.rom_to_ram - 0x80000000)/4), 0x00000000] + self.insert_code(codeblock_addr, codeblock + jump_back) + def insert_gameshark(self, hook_addr, codehandler_addr, gameshark_codes): #Insert a gameshark code handler engine along with desired codes + gameshark_hex = gameshark_to_hex(gameshark_codes) + self.insert_codeblock(hook_addr, codehandler_addr, gameshark_hex) #Insert the gameshark code handler block into the rom with a hook + def fix_checksum(self): #Fix checksum for rom, shamelessly ripped from Micro500's micro mountain python scirpt + c1, c2 = n64crc.crc(self.data) + self.data = mem_write_u32_be(0x10, c1, self.data) + self.data = mem_write_u32_be(0x14, c2, self.data) + def get_crc(self): #Get the checksum and return c1 and c2, used for checking what rom it is + c1, c2 = n64crc.crc(self.data) + return c1, c2 + def disable_internal_aa(self): #Loops through entire rom and disables internal AA from the F3DEX microcode (see https://www.assembler-games.com/threads/is-it-possible-to-disable-anti-aliasing-in-n64-games-via-gameshark-cheats.59916/page-3#post-860932) + rom_size = len(self.data) + for addr in range(0x0, rom_size, 0x4): #Loop through entire rom + instruction = mem_read_u32_be(addr, self.data) + if instruction == 0xB900031D: + print('------------') + next_instruction = mem_read_u32_be(addr + 0x4, self.data) + # if next_instruction & (1 << 3): #If 3rd bit is set (internal AA is on) + # print('Changed ', hex(next_instruction)) + # next_instruction = next_instruction - 0x8 #Turn internal AA off + # print('into ', hex(next_instruction)) + # self.data = mem_write_u32_be(addr + 0x4, next_instruction, self.data) + if (next_instruction & (1 << 4)): #If 3rd bit is set (internal AA is on) + print('Changed ', hex(next_instruction)) + next_instruction = next_instruction - 0x10 #do some random ass thing + print('into ', hex(next_instruction)) + self.data = mem_write_u32_be(addr + 0x4, next_instruction, self.data) + def get_address(self, instruction): #Grab and return the rom address of an instruction + if not type(instruction) == int: #Convert instruction to hex if not done already + intruction = assembler(instruction) + rom_size = len(self.data) + for addr in range(0x0, rom_size, 0x4): #Loop through entire rom + if instruction == mem_read_u32_be(addr, self.data): #If the instruction matches, return the address + return(addr) + def get_instruction(self, rom_address): #Returns an instruction at a given rom address + instruction = mem_read_u32_be(rom_address, self.data) + return instruction + def get_instructions(self, rom_address, size): #Returns a set of instructions given a rom address and size in number of bytes + instructions = [] + for i in range(0x0, size, 0x4): + instruction = mem_read_u32_be(rom_address + i, self.data) + instructions.append(instruction) + return instructions \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..89b444c --- /dev/null +++ b/main.py @@ -0,0 +1,100 @@ +# ============================================ +# MIPS-CodeWrite +# Author: Nayla Hanegan (naylahanegan@gmail.com) +# Date: 6/20/2024 +# License: GPLv2 +# ============================================ + +import tkinter as tk +from tkinter import scrolledtext +import subprocess +import queue +import threading +import customtkinter +import version +from functions import createDialog, fetchResource +import webbrowser +import platform +import injector_lib + +customtkinter.set_appearance_mode("Dark") +customtkinter.set_default_color_theme("blue") + +class App(customtkinter.CTk): + def __init__(self): + super().__init__() + + # configure window + self.title("MIPS CodeWrite") + self.geometry(f"{680}x{480}") + + frame = customtkinter.CTkFrame(self, fg_color=("#fcfcfc", "#2e2e2e")) + frame.pack(fill="both", expand=True, padx=20, pady=20) + frame.grid_rowconfigure(3, weight=1) + frame.grid_columnconfigure(1, weight=1) + + # Create a small text box for user input on the left side + label1 = customtkinter.CTkLabel(frame, text="Insertion Address:", font=("Arial", 14, "bold"), text_color="orange") + label1.grid(row=0, column=0, sticky="w", padx=10) + self.insertionAddress = customtkinter.CTkTextbox(frame, height=20) + self.insertionAddress.grid(row=1, column=0, padx=10, sticky="nsew") + + # Create a larger text box for user input on the left side + label2 = customtkinter.CTkLabel(frame, text="Codes:", font=("Arial", 14, "bold"), text_color="orange") + label2.grid(row=2, column=0, sticky="w", padx=10) + self.inputCode = customtkinter.CTkTextbox(frame) + self.inputCode.grid(row=3, column=0, padx=10, sticky="nsew") + + # Create a text box for displaying data on the right side + label3 = customtkinter.CTkLabel(frame, text="Output:", font=("Arial", 14, "bold"), text_color="orange") + label3.grid(row=0, column=1, sticky="w", padx=10) + self.output = customtkinter.CTkTextbox(frame) + self.output.grid(row=1, column=1, rowspan=3, padx=10, sticky="nsew") + + # Create a button at the bottom to patch + self.patchButton = customtkinter.CTkButton(frame, text="Patch", command=self.patch) + self.patchButton.grid(row=4, column=0, columnspan=2, pady=10) + + def patch(self): + insertionAddRaw = self.insertionAddress.get("1.0", "end-1c") + asmRaw = self.inputCode.get("1.0", "end-1c") + + try: + # Convert insertion address to integer + memory_address_start = int(insertionAddRaw, 16) + except ValueError: + createDialog("Error", "error", "Invalid or missing insertion address. Please enter a valid hexadecimal number.") + return + + # Preprocess ASM code: add commas between instructions if missing + asm_lines = preprocess_asm_code(asmRaw) + + #try: + gscode_lines = injector_lib.asm_to_gameshark(memory_address_start, asm_lines) + self.output.delete("1.0", tk.END) + self.output.insert(tk.END, '\n'.join(gscode_lines)) + #except Exception as e: + # createDialog("Error", "error", "Invalid ASM code.") + # return + +def preprocess_asm_code(asmRaw): + # Split by lines + lines = asmRaw.splitlines() + + processed_lines = [] + + for line in lines: + # Remove leading and trailing spaces + line = line.strip() + + # Add comma at the end if not already present + if line and not line.endswith(','): + line += ',' + + processed_lines.append(line) + + return processed_lines + +if __name__ == "__main__": + app = App() + app.mainloop() \ No newline at end of file diff --git a/n64crc.py b/n64crc.py new file mode 100644 index 0000000..ed51073 --- /dev/null +++ b/n64crc.py @@ -0,0 +1,126 @@ +# ============================================ +# MIPS-CodeWrite +# Author: Nayla Hanegan (naylahanegan@gmail.com) +# Date: 6/20/2024 +# License: GPLv2 +# ============================================ + +# Copyright (C) 2005 Parasyte +# Copyright (C) 2021 Daniel K. O. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +# This code is ported from the C code at http://n64dev.org/n64crc.html +# Based on uCON64's N64 checksum algorithm by Andreas Sterbenz + + +from binascii import crc32 +import sys + + +def read32(data : bytearray, offset): + return int.from_bytes(data[offset : offset+4], + byteorder='big') + +def write32(val): + return val.to_bytes(4, byteorder='big') + +def mask(x): + return x & 0xffffffff + + +def rotl(val, s): + return mask(val << s) | (val >> (32 - s)) + + +def get_cic(data : bytearray): + c = crc32(data[64:4096]) + if c == 0x6170a4a1: + return 6101 + if c == 0x90bb6cb5: + return 6102 + if c == 0x0b050ee0: + return 6103 + if c == 0x98bc2c86: + return 6105 + if c == 0xacc8580a: + return 6106 + if c == 0x009e9ea3: + return 7102 + return 6105 + + +def get_crc_seed(cic): + if cic in [6101, 6102, 7102]: + return 0xf8ca4ddc + if cic == 6103: + return 0xa3886759 + if cic == 6105: + return 0xdf26f436 + if cic == 6106: + return 0x1fea617a + return 1 + + +def crc(data : bytearray): + cic = get_cic(data) + seed = get_crc_seed(cic) + + t1 = t2 = t3 = t4 = t5 = t6 = seed + + # CRC1/CRC2 are calculated from offset 0x1000 to 0x101000 + + # Impl. note: XOR operations don't need to be masked to 32 bits. + + for offset in range(0x1000, 0x101000, 4): + d = read32(data, offset) + if mask(t6 + d) < t6: + t4 = mask(t4 + 1) + t6 = mask(t6 + d) + t3 ^= d + r = rotl(d, (d & 0x1f)) + t5 = mask(t5 + r) + t2 ^= r if t2 > d else t6 ^ d + + if cic == 6105: + t1 = mask(t1 + (d ^ read32(data, 0x750 + (offset & 0xff)))) + else: + t1 = mask(t1 + (d ^ t5)) + + if cic == 6103: + return mask((t6 ^ t4) + t3), mask((t5 ^ t2) + t1) + + if cic == 6106: + return mask((t6 * t4) + t3), mask((t5 * t2) + t1) + + return mask(t6 ^ t4 ^ t3), mask(t5 ^ t2 ^ t1) + + + +if __name__ == '__main__': + with open(sys.argv[1], "rb") as f: + rom = f.read(0x101000) + + cic = get_cic(rom) + + h1 = int.from_bytes(rom[16:20], byteorder='big') + h2 = int.from_bytes(rom[20:24], byteorder='big') + + c1, c2 = crc(rom) + + print("CIC:", cic) + print("header: {:08x} - {:08x}".format(h1, h2)) + print(" calc: {:08x} - {:08x}".format(c1, c2)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..649a057 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +customtkinter +tk +Pillow +pyinstaller == 5.13.2 \ No newline at end of file diff --git a/version.py b/version.py new file mode 100644 index 0000000..82c0d7e --- /dev/null +++ b/version.py @@ -0,0 +1,8 @@ +# ============================================ +# MIPS-CodeWrite +# Author: Nayla Hanegan (naylahanegan@gmail.com) +# Date: 6/20/2024 +# License: GPLv2 +# ============================================ + +appVersion = "1.0.0" \ No newline at end of file