Skip to content

Commit

Permalink
Merge pull request #18 from TheOnlyZac/2025-dev
Browse files Browse the repository at this point in the history
Support outputting CLPS2C source code
  • Loading branch information
TheOnlyZac authored Jan 31, 2025
2 parents 54613cf + 9c0fa39 commit 68e3338
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 59 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Sly String Toolkit
<img src="thumb.png" alt="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." align="right" style="float: right; margin: 10px; width: 300px">

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

Expand All @@ -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 <asm_codecave>` - Change the address of the codecave where the mod's assembly code is injected.
* `-s <strings_codecave` - Change the address of the codecave where the custom strings are injected.
* `-s <strings_codecave>` - 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
Expand Down
74 changes: 37 additions & 37 deletions generator/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,33 @@ 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'
)
},
3: {
"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
)#,
Expand Down Expand Up @@ -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
"""
Expand All @@ -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:
Expand Down Expand Up @@ -213,16 +213,16 @@ 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
"""
if self.verbose:
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:
Expand All @@ -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:
Expand All @@ -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 == ""):
Expand All @@ -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)

Expand All @@ -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
"""
Expand All @@ -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}")

Expand Down
71 changes: 58 additions & 13 deletions generator/pnach.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ 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.
"""
self._address = address
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:
"""
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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]:
"""
Expand All @@ -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:
"""
Expand Down Expand Up @@ -203,21 +231,21 @@ 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.
"""
pnach_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:
Expand All @@ -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() != "":
Expand All @@ -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:
"""
Expand Down
Loading

0 comments on commit 68e3338

Please sign in to comment.