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 0000000..7d3b0ad Binary files /dev/null and b/assets/icon.ico differ diff --git a/assets/operation/error.png b/assets/operation/error.png new file mode 100644 index 0000000..aaaae4f Binary files /dev/null and b/assets/operation/error.png differ diff --git a/assets/operation/success.png b/assets/operation/success.png new file mode 100644 index 0000000..ad11f7e Binary files /dev/null and b/assets/operation/success.png differ 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