From 9c0fa394cc24c4743329cffe3dfe9c2d77cbab35 Mon Sep 17 00:00:00 2001 From: TheOnlyZac Date: Fri, 31 Jan 2025 17:18:30 -0500 Subject: [PATCH] Support outputting CLPS2C source code --- README.md | 5 +-- generator/generator.py | 74 +++++++++++++++++++++--------------------- generator/pnach.py | 71 ++++++++++++++++++++++++++++++++-------- generator/strings.py | 10 +++--- stringtoolkit.py | 11 +++++-- 5 files changed, 112 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 1ce21ef..5c5378e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Sly String Toolkit A screenshot of the Sly 2 title screen with strings replaced where game strings have been replaced with the name and link to the repository. -This is a toolkit for making string replacement mods for *Sly 2: Band of Thieves* and *Sly 3: Honor Among Thieves* for the PS2. For a complete tutorial, see [this guide](https://slymods.info/wiki/Guide:Replacing_strings). +This is a toolkit for making string replacement mods for *Sly 2: Band of Thieves* and *Sly 3: Honor Among Thieves* for the PS2. For a complete tutorial, see [this guide](https://slymods.info/wiki/Guide:Replacing_strings) on the SlyMods wiki. # Usage @@ -20,9 +20,10 @@ These arguments are optional: * Can be `en`, `fr`, `it`, `de`, `es`, `nd`, `pt`, `da`, `fi`, `no`, or `sv`. * Only one pnach can be used at a time, so if your mod supports multiple languages, you must post them as separate patches. * `-c ` - Change the address of the codecave where the mod's assembly code is injected. -* `-s ` - Change the address of the codecave where the custom strings are injected. * `--live-edit` - Enable live edit mode. This will allow you to edit the strings in the csv and the pnach will automatically update. * `--verbose` - Enable verbose output. +* `--clps2c` - Output CLPS2C source code instead of raw pnach * `-h` - Show help. # Setup diff --git a/generator/generator.py b/generator/generator.py index 0e3590d..50f07cf 100644 --- a/generator/generator.py +++ b/generator/generator.py @@ -27,21 +27,21 @@ class GameInfo: "ntsc": GameInfo( title="Sly 2: Band of Thieves (USA)", crc="07652DD9", - hook_adr=0x2013e380, + hook_adr=0x13e380, hook_delayslot="lw $v0, 0x4($a0)", lang_adr=None, - asm_adr=0x202E60B0, - strings_adr=0x203C7980, + asm_adr=0x2E60B0, + strings_adr=0x3C7980, encoding='iso-8859-1' ), "pal": GameInfo( title="Sly 2: Band of Thieves (Europe)", crc="FDA1CBF6", - hook_adr=0x2013e398, + hook_adr=0x13e398, hook_delayslot="lw $v0, 0x4($a0)", lang_adr=0x2E9254, - asm_adr=0x202ED500, - strings_adr=0x203CF190, + asm_adr=0x2ED500, + strings_adr=0x3CF190, encoding='iso-8859-1' ) }, @@ -49,11 +49,11 @@ class GameInfo: "ntsc": GameInfo( title="Sly 3: Honor Among Thieves (USA)", crc="8BC95883", - hook_adr=0x20150648, + hook_adr=0x150648, hook_delayslot="lw $v0, 0x4($v1)", lang_adr=None, - asm_adr=0x2045af00, - strings_adr=0x200F1050, + asm_adr=0x45af00, + strings_adr=0x0F1050, encoding='UTF-16' #string_table=x47A2D8 )#, @@ -163,7 +163,7 @@ def assemble(asm_code: str) -> Tuple[bytes, int]: return machine_code_bytes, count - def _gen_strings_from_csv(self, csv_file: str, csv_encoding: str = "utf-8") -> Tuple[pnach.Chunk, List[pnach.Chunk], List[Tuple[int, int]]]: + def _gen_strings_from_csv(self, csv_file: str, csv_encoding: str = "utf-8", patch_format: str = "pnach") -> Tuple[pnach.Chunk, List[pnach.Chunk], List[Tuple[int, int]]]: """ Generates the strings pnach and populate string pointers """ @@ -176,7 +176,7 @@ def _gen_strings_from_csv(self, csv_file: str, csv_encoding: str = "utf-8") -> T out_encoding = self.game_info.encoding strings_obj = strings.Strings(csv_file, self.strings_adr, csv_encoding, out_encoding) - auto_strings_chunk, manual_string_chunks, string_pointers = strings_obj.gen_pnach_chunks() + auto_strings_chunk, manual_string_chunks, string_pointers = strings_obj.gen_pnach_chunks(patch_format) # Print string pointers if verbose if self.verbose: @@ -213,7 +213,7 @@ def _gen_asm(self, string_pointers: list) -> str: return mips_code - def _gen_code_pnach(self, machine_code_bytes: bytes) -> Tuple[pnach.Chunk, pnach.Chunk]: + def _gen_code_pnach(self, machine_code_bytes: bytes, patch_format: str) -> Tuple[pnach.Chunk, pnach.Chunk]: """ Generates the pnach object for the mod and hook code """ @@ -221,8 +221,8 @@ def _gen_code_pnach(self, machine_code_bytes: bytes) -> Tuple[pnach.Chunk, pnach print("Generating pnach file...") # Generate mod pnach code - mod_chunk = pnach.Chunk(self.code_address, machine_code_bytes) - mod_chunk.set_header(f"comment=Writing {len(machine_code_bytes)} bytes of machine code at {hex(self.code_address)}") + mod_chunk = pnach.Chunk(self.code_address, machine_code_bytes, patch_format=patch_format) + mod_chunk.set_header(f"Writing {len(machine_code_bytes)} bytes of machine code at {hex(self.code_address)}") # Print mod pnach code if verbose if self.verbose: @@ -233,8 +233,8 @@ def _gen_code_pnach(self, machine_code_bytes: bytes) -> Tuple[pnach.Chunk, pnach hook_asm = f"j {self.code_address}\n" hook_code, count = self.assemble(hook_asm) - hook_chunk = pnach.Chunk(self.hook_adr, hook_code) - hook_chunk.set_header(f"comment=Hooking string load function at {hex(self.hook_adr)}") + hook_chunk = pnach.Chunk(self.hook_adr, hook_code, patch_format=patch_format) + hook_chunk.set_header(f"Hooking string load function at {hex(self.hook_adr)}") # Print hook pnach code if verbose if self.verbose: @@ -244,15 +244,15 @@ def _gen_code_pnach(self, machine_code_bytes: bytes) -> Tuple[pnach.Chunk, pnach return (mod_chunk, hook_chunk) - def generate_pnach_str(self, input_file: str, mod_name: str = None, author: str = "Sly String Toolkit", csv_encoding: str = "utf-8") -> str: + def generate_patch_str(self, input_file: str, mod_name: str = None, author: str = "Sly String Toolkit", csv_encoding: str = "utf-8", patch_format: str = "pnach") -> str: """ Generates the mod pnach text from the given input file """ # Generate the strings, asm code, and pnach files - auto_strings_chunk, manual_sting_chunks, string_pointers = self._gen_strings_from_csv(input_file, csv_encoding) + auto_strings_chunk, manual_sting_chunks, string_pointers = self._gen_strings_from_csv(input_file, csv_encoding, patch_format=patch_format) trampoline_asm = self._gen_asm(string_pointers) trampoline_binary, count = self.assemble(trampoline_asm) - mod_chunk, hook_chunk = self._gen_code_pnach(trampoline_binary) + mod_chunk, hook_chunk = self._gen_code_pnach(trampoline_binary, patch_format=patch_format) # Set the mod name (default is same as input file) if (mod_name is None or mod_name == ""): @@ -269,26 +269,26 @@ def generate_pnach_str(self, input_file: str, mod_name: str = None, author: str + f"date={timestamp}\n" # Add all mod chunks to final pnach - final_mod_pnach = pnach.Pnach(header=header_lines) - final_mod_pnach.add_chunk(hook_chunk) - final_mod_pnach.add_chunk(mod_chunk) - final_mod_pnach.add_chunk(auto_strings_chunk) + final_mod_patch = pnach.Pnach(header=header_lines, patch_format=patch_format) + final_mod_patch.add_chunk(hook_chunk) + final_mod_patch.add_chunk(mod_chunk) + final_mod_patch.add_chunk(auto_strings_chunk) for chunk in manual_sting_chunks: - final_mod_pnach.add_chunk(chunk) + final_mod_patch.add_chunk(chunk) # Print final pnach if verbose if self.verbose: print("Final mod pnach:") - print(final_mod_pnach) + print(final_mod_patch) if self.lang is None: - return str(final_mod_pnach) + return str(final_mod_patch) # Add language check conditional to final pnach - final_mod_pnach.add_conditional(self.lang_adr, self.lang, 'eq') + final_mod_patch.add_conditional(self.lang_adr, self.lang, 'eq') # Generate pnach which cancels the function hook by setting the asm back to the original - cancel_hook_pnach = pnach.Pnach() + cancel_hook_patch = pnach.Pnach(patch_format=patch_format) cancel_hook_asm = "jr $ra\nlw $v0, 0x4($a0)" cancel_hook_bytes, count = self.assemble(cancel_hook_asm) @@ -297,20 +297,20 @@ def generate_pnach_str(self, input_file: str, mod_name: str = None, author: str cancel_hook_bytes = cancel_hook_bytes[:4] + cancel_hook_bytes[8:] cancel_hook_chunk = pnach.Chunk(self.hook_adr, cancel_hook_bytes, - f"comment=Loading {len(cancel_hook_bytes)} bytes of machine code (hook cancel) at {hex(self.hook_adr)}...") + f"Loading {len(cancel_hook_bytes)} bytes of machine code (hook cancel) at {hex(self.hook_adr)}...", patch_format=patch_format) # Add chunk and conditional to pnach - cancel_hook_pnach.add_chunk(cancel_hook_chunk) + cancel_hook_patch.add_chunk(cancel_hook_chunk) # Add conditional to cancel the function hook if game is set to the wrong language - cancel_hook_pnach.add_conditional(self.lang_adr, self.lang, 'neq') + cancel_hook_patch.add_conditional(self.lang_adr, self.lang, 'neq') if self.verbose: print("Cancel hook pnach:") - print(cancel_hook_pnach) + print(cancel_hook_patch) - return str(final_mod_pnach) + str(cancel_hook_pnach) + return str(final_mod_patch) + str(cancel_hook_patch) - def generate_pnach_file(self, input_file: str, output_dir: str = "./out/", mod_name: str = None, author: str = "Sly String Toolkit", csv_encoding: str = "utf-8") -> None: + def generate_patch_file(self, input_file: str, output_dir: str = "./out/", mod_name: str = None, author: str = "Sly String Toolkit", csv_encoding: str = "utf-8", format: str = "pnach") -> None: """ Generates a mod pnach and writes it to a file """ @@ -329,12 +329,12 @@ def generate_pnach_file(self, input_file: str, output_dir: str = "./out/", mod_n mod_name = os.path.splitext(os.path.basename(input_file))[0] # Generate the pnach - pnach_lines = self.generate_pnach_str(input_file, mod_name, author, csv_encoding) + patch_lines = self.generate_patch_str(input_file, mod_name, author, csv_encoding, format) # Write the final pnach file - outfile = os.path.join(output_dir, f"{crc}.{mod_name}.pnach") + outfile = os.path.join(output_dir, f"{crc}.{mod_name}.{format}") with open(outfile, "w+", encoding="iso-8859-1") as f: - f.write(pnach_lines) + f.write(patch_lines) print(f"Wrote pnach file to {outfile}") diff --git a/generator/pnach.py b/generator/pnach.py index 1ad67f0..6952196 100644 --- a/generator/pnach.py +++ b/generator/pnach.py @@ -7,7 +7,7 @@ class Chunk: """ Chunk class, used to generate chunks of code lines for pnach files. """ - def __init__(self, address: int, data: bytes = b"", header: str = ""): + def __init__(self, address: int, data: bytes = b"", header: str = "", patch_format: str = "pnach"): """ Constructor for the Chunk class. """ @@ -15,6 +15,10 @@ def __init__(self, address: int, data: bytes = b"", header: str = ""): self._bytes = data self._header = header + if patch_format not in ["pnach", "clps2c"]: + raise ValueError(f"Invalid format specified for pnach chunk: {patch_format}") + self._format = patch_format + # Getter and setter for address def get_address(self) -> int: """ @@ -46,7 +50,12 @@ def get_header(self) -> str: """ Returns the header of the chunk. """ - return self._header + if self._format == 'pnach': + return f"comment={self._header}" + elif self._format == "clps2c": + return f"SR \"\\n{self._header}\"" + else: + raise ValueError() def set_header(self, header : str) -> None: """ @@ -76,7 +85,10 @@ def get_code_lines(self) -> List[str]: word = word[::-1] value = int.from_bytes(word, 'big') - line = f"patch=1,EE,{address:X},extended,{value:08X}" + if self._format == "pnach": + line = f"patch=1,EE,2{address:07X},extended,{value:08X}" + elif self._format == "clps2c": + line = f"W32 {address:08X} 0x{value:08X}" code_lines.append(line) return code_lines @@ -86,7 +98,7 @@ def __str__(self) -> str: """ chunk_str = "" if self._header != "": - chunk_str += self._header + '\n' + chunk_str += self.get_header() + '\n' chunk_str += '\n'.join(self.get_code_lines()) return chunk_str @@ -101,16 +113,21 @@ class Pnach: """ Pnach class, used to generate pnach files. """ - def __init__(self, address: str = "", data: bytes = b"", header: str = ""): + def __init__(self, address: str = "", data: bytes = b"", header: str = "", patch_format: str = "pnach"): """ Constructor for the Pnach class. """ self._chunks = [] self._conditionals = {} self._header = header + if data != b"": self.create_chunk(address, data, header) + if patch_format not in ["pnach", "clps2c"]: + raise ValueError(f"Invalid format specified for pnach: {patch_format}") + self._format = patch_format + # Getter for array of lines (no setter) def get_code_lines(self) -> List[str]: """ @@ -130,7 +147,18 @@ def get_header(self) -> str: """ Returns the header for the pnach file. """ - return self._header + if self._format == 'pnach': + return self._header + elif self._format == "clps2c": + lines = self._header.split('\n') + out = "" + for line in lines: + out += "SR \"\\n" + out += line + out += "\"\n" + return out + else: + raise ValueError() def set_header(self, header: str) -> None: """ @@ -203,13 +231,13 @@ def write_file(self, filename) -> None: """ with open(filename, "w+", encoding="utf-8") as f: if self._header != "": - f.write(self._header) + f.write(self.get_header()) for chunk in self._chunks: f.write(str(chunk)) f.write("\n") # String from pnach lines - def __str__(self) -> str: + def get_code_lines(self) -> str: """ Returns a string with the pnach lines. """ @@ -217,7 +245,7 @@ def __str__(self) -> str: # Write header if self._header != "": - pnach_str += self._header + "\n" + pnach_str += self.get_header() + "\n" # If there are no conditionals, write all lines if len(self._conditionals) == 0: @@ -233,7 +261,13 @@ def __str__(self) -> str: cond_address = self._conditionals['address'] cond_value = self._conditionals['value'] cond_type = self._conditionals['type'] - cond_operator = "==" if cond_type == 0 else "!=" + + cond_operator = "" + if self._format == "pnach": + cond_operator = "==" if cond_type == 0 else "!=" + elif self._format == "clps2c": + cond_operator = "=:" if cond_type == 0 else "!:" + for chunk in self._chunks: # Add chunk header if chunk.get_header() != "": @@ -250,14 +284,25 @@ def __str__(self) -> str: # Compares value at address @a to value @v, and executes next @n code llines only if condition @t is met. num_lines_remaining = num_lines - i num_lines_to_write = 0xFF if num_lines_remaining > 0xFF else num_lines_remaining + # Add conditional line - pnach_str += f"-- Conditional: if *{cond_address:X} {cond_operator} 0x{cond_value:X} do {num_lines_to_write} lines\n" - pnach_str += f"patch=1,EE,E0{num_lines_to_write:02X}{cond_value:04X},extended,{cond_type:1X}{cond_address:07X}\n" + pnach_str += f"// Conditional: if *0x{cond_address:X} {cond_operator} 0x{cond_value:X} do {num_lines_to_write} lines\n" + if self._format == "pnach": + pnach_str += f"patch=1,EE,E0{num_lines_to_write:02X}{cond_value:04X},extended,{cond_type:1X}{cond_address:07X}\n" + elif self._format == "clps2c": + pnach_str += f"IF 0x{cond_address:X} {cond_operator} 0x{cond_value:X}\n" + # Write lines to pnach - pnach_str += '\n'.join(lines[i:i + num_lines_to_write]) + "\n" + joiner = "\n" if self._format == "pnach" else "\n " + pnach_str += joiner.join(lines[i:i + num_lines_to_write]) + "\n" + if self._format == "clps2c": + pnach_str += "ENDIF\n" return pnach_str + def __str__(self) -> str: + return self.get_code_lines() + # String representation of pnach object def __repr__(self) -> str: """ diff --git a/generator/strings.py b/generator/strings.py index dbe1045..e977ba2 100644 --- a/generator/strings.py +++ b/generator/strings.py @@ -20,7 +20,7 @@ def __init__(self, csv_file: str, start_address: int, csv_encoding: str, out_enc self.out_encoding = out_encoding self.start_address = start_address - def gen_pnach_chunks(self) -> Tuple[pnach.Chunk, List[pnach.Chunk], List[Tuple[int, int]]]: + def gen_pnach_chunks(self, patch_format: str) -> Tuple[pnach.Chunk, List[pnach.Chunk], List[Tuple[int, int]]]: """ Generates a pnach file with the strings from the csv file and returns a tuple with the pnach object and the array of pointers to the strings @@ -54,12 +54,12 @@ def gen_pnach_chunks(self) -> Tuple[pnach.Chunk, List[pnach.Chunk], List[Tuple[i # gen pnach chunk for the strings that don't have a target address offset = self.start_address string_data = b''.join([string[1] for string in strings]) - auto_chunk = pnach.Chunk(offset, string_data) + auto_chunk = pnach.Chunk(offset, string_data, patch_format=patch_format) # gen pnach chunks for strings that have a target address manual_chunks = [] for string in manual_address_strings: - chunk = pnach.Chunk(string[2], string[1]) + chunk = pnach.Chunk(string[2], string[1], patch_format) manual_chunks.append(chunk) # 3 - Generate the pointers to the strings @@ -74,9 +74,9 @@ def gen_pnach_chunks(self) -> Tuple[pnach.Chunk, List[pnach.Chunk], List[Tuple[i id_string_pointer_pairs.append((string[0], string[2])) # set header for the pnach chunks - auto_chunk.set_header(f"comment=Writing {len(id_string_pointer_pairs)} strings ({len(auto_chunk.get_bytes())} bytes) at {hex(self.start_address)}") + auto_chunk.set_header(f"Writing {len(id_string_pointer_pairs)} strings ({len(auto_chunk.get_bytes())} bytes) at {hex(self.start_address)}") for chunk in manual_chunks: - chunk.set_header(f"comment=Writing 1 string ({len(chunk.get_bytes())} bytes) at {hex(self.start_address)}") + chunk.set_header(f"Writing 1 string ({len(chunk.get_bytes())} bytes) at {hex(self.start_address)}") # 4 - Return the pnach lines and the array of pointers return (auto_chunk, manual_chunks, id_string_pointer_pairs) diff --git a/stringtoolkit.py b/stringtoolkit.py index db0b512..7a328fa 100644 --- a/stringtoolkit.py +++ b/stringtoolkit.py @@ -28,6 +28,7 @@ def main(): parser.add_argument('-e', '--csv_encoding', type=str, help='Encoding of the input CSV file (default is utf-8)', default="utf-8") parser.add_argument('--verbose', action='store_true', help='Enable verbose output') parser.add_argument('--live-edit', action='store_true', help='Enable live editing of strings csv file') + parser.add_argument('--clps2c', action='store_true', help='Output CLPS2C code instead of pnach') args = parser.parse_args() # Make sure the input file exists @@ -40,6 +41,11 @@ def main(): if not os.path.isabs(args.input_file): args.input_file = os.path.abspath(args.input_file) + # Determine output format + patch_format = "pnach" + if args.clps2c: + patch_format = "clps2c" + # Set static flags on generator Generator.set_verbose(args.verbose) Generator.set_debug(DEBUG_ENABLED) @@ -51,7 +57,7 @@ def main(): # Create the observer and schedule the event handler observer = Observer() event_handler = FileSystemEventHandler() - event_handler.on_modified = lambda event: generator.generate_pnach_file(args.input_file, args.output_dir, args.name, args.author, args.csv_encoding) + event_handler.on_modified = lambda event: generator.generate_patch_file(args.input_file, args.output_dir, args.name, args.author, args.csv_encoding, patch_format) observer.schedule(event_handler, path=os.path.dirname(args.input_file), recursive=False) # Start the observer and wait for keyboard interrupt @@ -65,7 +71,8 @@ def main(): # Stop the observer observer.join() else: - generator.generate_pnach_file(args.input_file, args.output_dir, args.name, args.author) + print("FORMAT:", patch_format) + generator.generate_patch_file(args.input_file, args.output_dir, args.name, args.author, args.csv_encoding, patch_format) if __name__ == "__main__": main()