Hello
I have a PSP game that I want to apply a "patch" for, by reading a file I have inserted into its ISO. The file contains the script (dialogues and choices since it is a visual novel) of the game, decompressed and then edited. Which is supposed to be read and inserted into a specific memory location where the game's script resides in RAM (0x08DD9A80 ->). This has been confirmed to work by externally injecting bytes into that location, using a modifed version of PPSSPP (hardcoded function calls into it when a button combination is pressed, to insert all the bytes of the file into that specific memory location). (The game would not respond to cheat scripts, so went with a less common option for verifying that changing the script worked without issue).
The process has been this:
1. Identify where in the game I can insert my patch safely by checking instructions with PPSSPP, as well as Ghidra.
2. Successfully found a spot, and inserted my new function into it (by manually inserting it into the EBOOT using a hex editor after compiling the instructions, and hooking a function call early when the game starts up).
3. The function is executed and the game safely returns back to normal operation, no issue here.
Now then, the issue is that the file I am trying to read from the ISO simply cannot be "read". I am able to open it using sceIoOpen which provides me with a valid FD, I can then use sceIoLseek the file to get the file size (which works). But I am unable to read the file, all attempts lead to the error code 80020323, despite a valid FD (that was able to be used with seeking the file). PPSSPP does not log this error though (BADF log for example), but it does if I give it an invalid FD.
What I have tried:
1. Insert running my own thread. No change.
2. Use async reading instead, if anything may block the synchronous reading. No change (async read returns v0 = 0x0, a successful queue, but async wait also returns v0 = 0x0, thus no reading was performed).
3. Recreate the setup that the game originally uses for reading files (from what I could tell: Open the file, changeAsyncPriority(?), Lseek to the end then rewind, syncRead). This still did not work.
4. Began writing to uncached RAM instead of cached RAM (0x48DD9A80, which later on will be moved over to its cached counterpart.) I used to try and write to the cached RAM section, however since that failed I switched to uncached instead. No change was observed for reading though.
The game does read files right up to the point where I insert my function, well of course one would think that there is something blocking my read perhaps but not at the moment where my function is being executed. I hijacked the call to "install game on memory stick" to call my patched function instead. Doesn't matter if I place my function right at the start of the game either, as IoOpen & Lseek succeeds but any reading fails.
I will provide the function I currently use (it has a looping issue right now but that is not what I need to be fixed, that is something I can work on later. I need assistance with figuring out why reads fail). I apologize if the instructions are quite messy, I am not well knowledgable in writing 32 MIPS myself so had to consult chatGPT for most of the generating of instructions.
The registers contain the correct values all throughout the function, they do not get overwritten elsewhere during execution (for example FD is valid until I close it, it is not overwritten). Due to a whole lot of testing and messing around with the instructions, there may be other issues you can find but if someone could see where the reading issue lies that would be greatly appreciated.
Function:
.set noreorder
.text
# Trampolines
IO_CHG_PRIO = 0x0887ED9c # sceIoChangeAsyncPriority
IO_LSEEK = 0x0887EDb4 # sceIoLseek
IO_READ = 0x0887ED74 # sceIoRead
IO_OPEN = 0x0887EDAC
IO_READ_AS = 0x0887ED94
IO_WAIT_AS = 0x0887EDA4
IO_CLOSE = 0x0887ED8C
ERR_BUSY = 0x800201A7
.global _start
_start:
# FILE 1 : sn_decomp.bin
li $a0, 0x08890F34 # path_script, changed to hardcoded RAM offset where the string is inserted in RAM
li $a1, 0 # O_RDONLY
li $a2, 0x01FF # mode 0777
li $t9, IO_OPEN
jalr $t9
nop
move $s2, $v0 # keep fd in s2 (fd > 0)
# sceIoLseek(fd, 0, SEEK_END)
move $a0, $s2 # fd
li $a1, 0 # not used
li $a2, 0 # offset (low)
li $a3, 0 # offset (high)
li $t0, 2 # SEEK_END
li $t9, IO_LSEEK
jalr $t9
nop
move $s1, $v0 # bytes left
# second sceIoLseek(fd, 0, SEEK_SET)
move $a0, $s2 # fd
li $a1, 0 # not used
li $a2, 0 # offset (low)
li $a3, 0 # offset (high)
li $t0, 0 # SEEK_SET
li $t9, IO_LSEEK
jalr $t9
nop
jalr $t9
nop
li $t0, 0x48DD9A80 # uncached buffer
r1_loop:
beqz $t1, r1_done
nop
move $a0, $s2 # fd
move $a1, $t0 # buf
move $a2, $s1 # left
li $t9, IO_READ
jalr $t9
nop
blez $v0, r1_done
subu $t1, $t1, $v0
addu $t0, $t0, $v0
b r1_loop
nop
r1_done:
move $a0, $s2
li $t9, IO_CLOSE
jalr $t9
nop
# Return to original game flow
lui $ra, 0x0886
ori $ra, $ra, 0x4ADC # 0x08864ADC
jr $ra
nop
.section .rodata
path_script: .asciz "disc0:/PSP_GAME/USRDIR/patch/sn_decomp.bin"
.align 2
The reason why I do not simply hijack where the game originally reads the script file is due to the file being compressed with LZSS (LZ77 variant). The game then needs to decompress it, which means that I either need to fix a working compression script that managed to compress the decompressed file back into its original state (which never worked in previous attempts), or remove decompressing from the game. Removing the decompression could lead to issues further down the line if following behavior of the game depended on what it performed more than just the script itself. I chose to play it safe by letting the game start up normally and then just injecting my edited script directly into the "decompressed script" location in RAM. However, due to failing reads this seems to be an issue. I am also constrained by not being able to edit much in the EBOOT, except for free space, since if what I add is different in length then all offsets become misaligned (shorted edits can be padded without issue though).
I have a PSP game that I want to apply a "patch" for, by reading a file I have inserted into its ISO. The file contains the script (dialogues and choices since it is a visual novel) of the game, decompressed and then edited. Which is supposed to be read and inserted into a specific memory location where the game's script resides in RAM (0x08DD9A80 ->). This has been confirmed to work by externally injecting bytes into that location, using a modifed version of PPSSPP (hardcoded function calls into it when a button combination is pressed, to insert all the bytes of the file into that specific memory location). (The game would not respond to cheat scripts, so went with a less common option for verifying that changing the script worked without issue).
The process has been this:
1. Identify where in the game I can insert my patch safely by checking instructions with PPSSPP, as well as Ghidra.
2. Successfully found a spot, and inserted my new function into it (by manually inserting it into the EBOOT using a hex editor after compiling the instructions, and hooking a function call early when the game starts up).
3. The function is executed and the game safely returns back to normal operation, no issue here.
Now then, the issue is that the file I am trying to read from the ISO simply cannot be "read". I am able to open it using sceIoOpen which provides me with a valid FD, I can then use sceIoLseek the file to get the file size (which works). But I am unable to read the file, all attempts lead to the error code 80020323, despite a valid FD (that was able to be used with seeking the file). PPSSPP does not log this error though (BADF log for example), but it does if I give it an invalid FD.
What I have tried:
1. Insert running my own thread. No change.
2. Use async reading instead, if anything may block the synchronous reading. No change (async read returns v0 = 0x0, a successful queue, but async wait also returns v0 = 0x0, thus no reading was performed).
3. Recreate the setup that the game originally uses for reading files (from what I could tell: Open the file, changeAsyncPriority(?), Lseek to the end then rewind, syncRead). This still did not work.
4. Began writing to uncached RAM instead of cached RAM (0x48DD9A80, which later on will be moved over to its cached counterpart.) I used to try and write to the cached RAM section, however since that failed I switched to uncached instead. No change was observed for reading though.
The game does read files right up to the point where I insert my function, well of course one would think that there is something blocking my read perhaps but not at the moment where my function is being executed. I hijacked the call to "install game on memory stick" to call my patched function instead. Doesn't matter if I place my function right at the start of the game either, as IoOpen & Lseek succeeds but any reading fails.
I will provide the function I currently use (it has a looping issue right now but that is not what I need to be fixed, that is something I can work on later. I need assistance with figuring out why reads fail). I apologize if the instructions are quite messy, I am not well knowledgable in writing 32 MIPS myself so had to consult chatGPT for most of the generating of instructions.
The registers contain the correct values all throughout the function, they do not get overwritten elsewhere during execution (for example FD is valid until I close it, it is not overwritten). Due to a whole lot of testing and messing around with the instructions, there may be other issues you can find but if someone could see where the reading issue lies that would be greatly appreciated.
Function:
.set noreorder
.text
# Trampolines
IO_CHG_PRIO = 0x0887ED9c # sceIoChangeAsyncPriority
IO_LSEEK = 0x0887EDb4 # sceIoLseek
IO_READ = 0x0887ED74 # sceIoRead
IO_OPEN = 0x0887EDAC
IO_READ_AS = 0x0887ED94
IO_WAIT_AS = 0x0887EDA4
IO_CLOSE = 0x0887ED8C
ERR_BUSY = 0x800201A7
.global _start
_start:
# FILE 1 : sn_decomp.bin
li $a0, 0x08890F34 # path_script, changed to hardcoded RAM offset where the string is inserted in RAM
li $a1, 0 # O_RDONLY
li $a2, 0x01FF # mode 0777
li $t9, IO_OPEN
jalr $t9
nop
move $s2, $v0 # keep fd in s2 (fd > 0)
# sceIoLseek(fd, 0, SEEK_END)
move $a0, $s2 # fd
li $a1, 0 # not used
li $a2, 0 # offset (low)
li $a3, 0 # offset (high)
li $t0, 2 # SEEK_END
li $t9, IO_LSEEK
jalr $t9
nop
move $s1, $v0 # bytes left
# second sceIoLseek(fd, 0, SEEK_SET)
move $a0, $s2 # fd
li $a1, 0 # not used
li $a2, 0 # offset (low)
li $a3, 0 # offset (high)
li $t0, 0 # SEEK_SET
li $t9, IO_LSEEK
jalr $t9
nop
jalr $t9
nop
li $t0, 0x48DD9A80 # uncached buffer
r1_loop:
beqz $t1, r1_done
nop
move $a0, $s2 # fd
move $a1, $t0 # buf
move $a2, $s1 # left
li $t9, IO_READ
jalr $t9
nop
blez $v0, r1_done
subu $t1, $t1, $v0
addu $t0, $t0, $v0
b r1_loop
nop
r1_done:
move $a0, $s2
li $t9, IO_CLOSE
jalr $t9
nop
# Return to original game flow
lui $ra, 0x0886
ori $ra, $ra, 0x4ADC # 0x08864ADC
jr $ra
nop
.section .rodata
path_script: .asciz "disc0:/PSP_GAME/USRDIR/patch/sn_decomp.bin"
.align 2
The reason why I do not simply hijack where the game originally reads the script file is due to the file being compressed with LZSS (LZ77 variant). The game then needs to decompress it, which means that I either need to fix a working compression script that managed to compress the decompressed file back into its original state (which never worked in previous attempts), or remove decompressing from the game. Removing the decompression could lead to issues further down the line if following behavior of the game depended on what it performed more than just the script itself. I chose to play it safe by letting the game start up normally and then just injecting my edited script directly into the "decompressed script" location in RAM. However, due to failing reads this seems to be an issue. I am also constrained by not being able to edit much in the EBOOT, except for free space, since if what I add is different in length then all offsets become misaligned (shorted edits can be padded without issue though).







