zaksabeast / DreamRadarCartRedirect

A patch for dream radar redirecting nds cart reading/writing to a file on the SD
GNU General Public License v3.0
74 stars 4 forks source link

Is it possible to patch the JP version of the Dream Radar? #9

Closed gpavelski closed 1 year ago

gpavelski commented 2 years ago

Hello,

Thanks for your work, that is exactly what I was looking for.

Following the instructions here, I was able to make it work for the Poke Transporter.

However, for the Dream Radar, it does not work. Actually, I have the Dream Radar downloaded from the Japanese eshop, and the Title ID is 0004000000073200, and not 00040000000AE100 as in that post. So, when I tried to add the ips code to that folder, it crashes the console.

My question is: is there a way to modify the IPS patch so that it will also work with the Japanese title?

Thank you.

zaksabeast commented 2 years ago

Yes, the patch can be modified to use offsets for the Japanese version of Dream Radar.

Since I don't have the Japanese version of Dream Radar, I won't be able to get the offsets. However, I can write some instructions to find the offsets if you or someone else with the game is willing to help.

gpavelski commented 2 years ago

If it may help, I used gm9 to dump that title into a .cia file, that can be found here (link removed). Otherwise, if you give me the instructions, I can see what I can do.

Thanks

zaksabeast commented 1 year ago

Sorry for the late response! I've been sick for the past week.

I removed the cia link since those shouldn't be shared.

As for the instructions, are you willing to use Ghidra? That would be the easiest option. If not, I can send instructions for finding the offsets using a hex editor, but that might not work as well.

gpavelski commented 1 year ago

I am sorry to hear that, hope you are getting well.

Yes, Ghidra is fine, I downloaded it just now and it seems to be working.

zaksabeast commented 1 year ago

I'm doing much better, thank you.

If you have any questions or some of these steps do not work, please let me know.

Loading the game into Ghidra

  1. Download the make_elf.py script from here
  2. Use GM9 to dump the game's exefs/.code and extheader.bin files to your computer
  3. Rename extheader.bin to exh.bin
  4. Make a directory called ExeFS and move .code to ExeFS/code.bin
    • Do not move exh.bin
  5. Run the make_elf.py script to create ExeFS.elf
  6. Load ExeFS.elf in Ghidra

Finding the addresses

Search for the value on the left, and get the address of the function using it:

Find the fs:USER file handle address:

  1. Search for the string fs:USER
  2. One of the functions that references it will look like the screenshot below
  3. The first argument of GetServiceHandleDirect is the handle
  4. Double click on the handle to find the address
Screen Shot 2022-09-13 at 7 31 22 AM

Find the wrapper function for FSUSER_CardSlotIsInserted:

  1. Search for the function using 0x8210000 - that is FSUSER_CardSlotIsInserted
  2. Get the address of the function that calls FSUSER_CardSlotIsInserted

Find the function that reads the nds card Id:

  1. Search for the function using 0x8460102 - that is FSUSER_GetLegacyRomHeader2
  2. Navigate to the function that calls FSUSER_GetLegacyRomHeader2 - that is FSUSER_GetLegacyRomHeader2's wrapper
  3. Get the address of the function that calls FSUSER_GetLegacyRomHeader2's wrapper
    • As a sanity check, it should also call the wrapper of FSUSER_GetCardType (0x8130000)

Find the function that writes nds save data:

  1. Search for the function with these bytes: 00 10 a0 e3 07 00 44 e0 04 20 8d e2 00 10 8d e5 0b 00 80 e0 41 00 82 e8
    • This function is a little tricky to find, so I'm hoping the bytes will match. If not, there are other ways to find it.

Find an address with lot of empty space at the bottom of the .text section.

gpavelski commented 1 year ago

Ok, I followed the steps until the Ghidra part, where I think something is wrong. So, just to recapitulate, the ".code" file dumped using gm9 should be renamed to "code.bin" and placed in the folder ExeFS, is that right?

When I run the python script, I get as output:

b'Name: POKE AR' Flag: 03 [compressed][sd app] Rev.: 0000 .text addr: 00100000 .text page: 000000BE .text size: 000BD9E4 stack size: 00040000 .read addr: 001BE000 .read page: 00000015 .read size: 00014ED8 .data addr: 001D3000 .data page: 00000015 .data size: 000143FC .bss size: 00040494

The ExeFS.elf file is created, but then I open Ghidra, I create a new Project, I import the .elf file on it, and try to use the Code Browser to read it. However, in line 107 of the Decompiler, it shows a "halt_baddata();" command (Error [Bad Instruction]: Unable to resolve constructor at 0x0010c85c). So, I don't know if the problem is in the dumped files, the python script or in the Ghidra. Any ideas?

Thank you.

zaksabeast commented 1 year ago

So, just to recapitulate, the ".code" file dumped using gm9 should be renamed to "code.bin" and placed in the folder ExeFS, is that right?

Yep, that is correct. The output from the python script looks good too.

However, in line 107 of the Decompiler, it shows a "halt_baddata();" command (Error [Bad Instruction]: Unable to resolve constructor at 0x0010c85c). So, I don't know if the problem is in the dumped files, the python script or in the Ghidra. Any ideas?

halt_baddata happens when ghidra doesn't recognize an instruction, and possibly in a few other scenarios where it can't detect the correct flow. This might occasionally come up, but as long as most functions don't show this, the file was loaded correctly.

Do most functions show this, or only a few?

Are you still able to search for the values in my last comment?

gpavelski commented 1 year ago

Actually, I am not able to search for these values. When I load the ELF file into Ghidra, it displays this additional info:

----- Loading /C:/Users/Desktop/MakeElf/ExeFS.elf ----- Skipping zero-length segment [1,Loadable segment] at address ram:001be000 Skipping zero-length segment [2,Loadable segment] at address ram:001d3000 Skipping section [.fini] with invalid size 0x0 Skipping section [.rodata] with invalid size 0x0 Skipping section [.memregion] with invalid size 0x0 Skipping section [.data] with invalid size 0x0

It skips the important sections of the program for some reason and there is only one function being detected. ExeFS.zip (link removed)

zaksabeast commented 1 year ago

That's interesting. I wonder if GM9 didn't decompress the .code when it was dumped. I thought it did, but maybe I was mistaken.

Try decompressing it with ctrtool:

  1. Download ctrtool
  2. Run ctrtool -t lzss --lzssout=decompressed-code.bin code.bin
  3. Move decompressed-code.bin to ExeFS/code.bin
  4. Run make_elf.py and load the result into Ghidra

If that doesn't work, try this:

  1. Use GM9 to dump exefs.bin
  2. Run ctrtool -t exefs --decompresscode --exefsdir=ExeFS --exheader=exh.bin exefs.bin
    • This will have ctrtool extract and decompress code.bin
  3. Run make_elf.py and load the result into Ghidra
gpavelski commented 1 year ago

Yes, you are right, After I used the ctrtool, it worked.

I found all the function addresses that you asked me for, even the one with the long sequence of bytes. About the handle, I found these two statements: uVar2 = FUN_0016de1c(s_fs:USER_001c417f); iVar1 = FUN_0016dde8(&DAT_001d9a1c,s_fs:USER_001c417f,uVar2,0);

So does that mean that the handle address is 001d8ac1c?

What are the next steps?

zaksabeast commented 1 year ago

Awesome, I'm glad to hear it.

0x001d9a1c should be the fs handle. FUN_0016de1c is strlen, and FUN_0016dde8 is srvGetServiceHandleDirect.

There's one more address I forgot to have you find: the address that reads nds save data. Similar to the write function, this one is a little difficult to find, so I'm hoping some of the bytes will match. Get the address of the function that has these bytes: 60 00 95 e8 60 a0 8d e2 60 00 8a e8 10 50 82 e2 58 10 cd e5.

Next steps:

A few weeks ago I made a branch called armips-needs-testing that rewrites this patch to use armips. I did this because I think it's easier to read and make changes. This is the perfect chance to use it.

To build the patches, you'll need armips, which creates a patched code.bin, and flips, which creates a code.ips from the original and patched code.bins.

Once you have armips and flips:

  1. Make a copy of radar.s
  2. Replace these addresses with the ones you found:
    • These lines with the addresses for the FSUSER functions, FSFILE functions, and the fs handle
    • This line with the FSUSER_CardSlotIsInserted wrapper address
    • This line with the address that reads nds save data
    • This line with the address that reads the nds cart Id
    • This line with the address that writes nds save data
    • This line is for the large amount of empty space in .text
  3. Run these commands, but replace radar.s with your copied file
  4. Test the patch and let me know if it works
gpavelski commented 1 year ago

Actually, I still have one question concerning the "large amount of empty space in .text". Is there a more specific way to find this address?

After I use flips to generate the ips patch, I noticed that the string "/roms/nds/saves/white2.sav" appears is in the wrong location:

image

As you can see in the picture, it should appear exactly before the EOF, but it is appearing much earlier. And besides, on Ghidra, the last instruction of .text is 001bd9e3 e1 ?? E1h, then .rodata starts with DAT_001be000, should this address be in this interval?

Thank you

zaksabeast commented 1 year ago

Yeah, you have the right idea. .text is where executable instructions are stored, so we want to find empty space in between the last instruction of .text and the first byte of .rodata. The address that is used needs to be a multiple of 4.

These lines will fill the empty space with the sd save utils and check_if_save_exists. It looks like that uses 58 instructions, two words, and the save file name, so ~270 bytes are needed.

After I use flips to generate the ips patch, I noticed that the string "/roms/nds/saves/white2.sav" appears is in the wrong location

The armips-needs-testing branch makes a few size optimizations and moves things around, so it won't look identical to the currently released patches. In the new branch, the save path is at the end of the sd save utils, which is included before check_if_save_exists.

Is the patch not working?

If it isn't, you might try modifying these values in the python script instead.

gpavelski commented 1 year ago

After I apply the patch, I boot the game, then when I press the second button (to transfer the data), it gives me an error.

Error type: generic
Process ID: 41
Process name: POKE AR
Process title ID: 0x0004000000073200
Address: 0x00137b48
Errror code: 0xfff8e41c

If I send you my radar.s and my ExeFS.elf, could you help me to check if the addresses are correct? Files.zip (link removed)

Thanks again.

zaksabeast commented 1 year ago

I found a few bugs in my patch rewrite. I tested and pushed fixes to the armips-needs-testing branch. Pull the latest changes and use the updated radar.s file to make your patch.

I also wrote a python script that finds and prints the addresses you need. Run that script with ghidra and update radar.s with the addresses the script prints. After that, build and test the new patch.

Here is the script:

# Prints addresses for the Pokemon Radar Cart Redirect patch
# @category 3ds
#

from array import array
from ghidra.app.decompiler import DecompInterface

# ----------------------------------------
# Globals
# ----------------------------------------

memory = currentProgram.getMemory()
decomp = DecompInterface()
decomp.openProgram(currentProgram)

# ----------------------------------------
# Utils
# ----------------------------------------

def bytesToByteStr(bytes):
  encoded_bytes = map(lambda byte : chr(byte), bytes)
  return ''.join(encoded_bytes)

def bytesToByteArr(bytes):
  return array('b', bytesToByteStr(bytes))

def findBytes(start_addr, byte_arr):
  return memory.findBytes(start_addr, byte_arr, None, True, monitor)

def findFirstByteArray(bytes):
  byte_arr = bytesToByteArr(bytes)
  current_addr = findBytes(toAddr(0), byte_arr)
  while current_addr.getOffset() % 4 != 0:
    current_addr = findBytes(current_addr.add(4), byte_arr)
  return current_addr

def findFirstByteString(byte_str):
  return findFirstByteArray(array('B', byte_str.decode('hex')))

def findFirstUint(num):
  num_bytes = [num & 0xff, (num >> 8) & 0xff, (num >> 16) & 0xff, (num >> 24) & 0xff]
  return findFirstByteArray(num_bytes)

def findFirstFunctionUsingUint(num):
  num_addr = findFirstUint(num)
  num_ref = getReferencesTo(num_addr)[0]
  num_use = num_ref.getFromAddress()
  func = getFunctionContaining(num_use)
  return func.getEntryPoint()

def findFirstFunctionWithBytes(byte_str):
  bytes_addr = findFirstByteString(byte_str)
  func = getFunctionContaining(bytes_addr)
  return func.getEntryPoint()

def getFirstCallingFunction(func_addr):
  callers_refs = getReferencesTo(func_addr)
  first_caller_addr = callers_refs[0].getFromAddress()
  return getFunctionContaining(first_caller_addr).getEntryPoint()

def findFirstString(find_str):
  find_bytes = array('b', find_str)
  return findBytes(toAddr(0), find_bytes)

def findNextFunctionCallRef(addr):
  while addr < currentProgram.getMaxAddress():
    refs = getReferencesFrom(addr)
    for ref in refs:
      if ref.getReferenceType().isCall():
        return ref
    addr = addr.add(4)
  return None

def getCallArgs(caller_func, callee_addr):
    decompiled_func = decomp.decompileFunction(caller_func, 60, monitor)
    high_func = decompiled_func.getHighFunction()

    if not high_func:
      return []

    opiter = high_func.getPcodeOps()
    while opiter.hasNext():
      op = opiter.next()
      mnemonic = str(op.getMnemonic())
      if mnemonic == 'CALL':
        inputs = op.getInputs()
        addr = inputs[0].getAddress()
        if addr == callee_addr:
          return inputs[1:]

    return []

def getCallArgsFromRef(call_ref):
    from_addr = call_ref.getFromAddress()
    to_addr = call_ref.getToAddress()
    func = getFunctionContaining(from_addr)
    return getCallArgs(func, to_addr)

# ----------------------------------------
# Find radar addresses
# ----------------------------------------

# Get base addresses for most functions
fsuser_openfiledirectly = findFirstFunctionUsingUint(0x8030204)
fsfile_read = findFirstFunctionUsingUint(0x80200C2)
fsfile_write = findFirstFunctionUsingUint(0x8030102)
fsfile_close = findFirstFunctionUsingUint(0x8080000)
fsuser_cardslotisinserted = findFirstFunctionUsingUint(0x8210000)
fsuser_getlegacyromheader2 = findFirstFunctionUsingUint(0x8460102)
read_nds_save = findFirstFunctionWithBytes('600095e860a08de260008ae8105082e25810cde5')
write_nds_save = findFirstFunctionWithBytes('0010a0e3070044e004208de200108de50b0080e0410082e8')

# Get wrapper and calling function addresses
read_nds_save_wrapper = getFirstCallingFunction(read_nds_save)
fsuser_cardslotisinserted_wrapper = getFirstCallingFunction(fsuser_cardslotisinserted)
fsuser_getlegacyromheader2_wrapper = getFirstCallingFunction(fsuser_getlegacyromheader2)
read_nds_cart_id = getFirstCallingFunction(fsuser_getlegacyromheader2_wrapper)

# Find fs handle
fs_user_str = findFirstString('fs:USER')
# First ref is typically strlen
# Second ref should be with GetServiceHandleDirect
fs_user_ref = getReferencesTo(fs_user_str)[1].getFromAddress()
get_fs_handle_call = findNextFunctionCallRef(fs_user_ref)
get_fs_handle_args = getCallArgsFromRef(get_fs_handle_call)
fs_handle_ref_offset = get_fs_handle_args[0].getAddress()
fs_handle = memory.getInt(fs_handle_ref_offset)

# Print results
print('fsuser_openfiledirectly {}'.format(fsuser_openfiledirectly))
print('fsfile_read {}'.format(fsfile_read))
print('fsfile_write {}'.format(fsfile_write))
print('fsfile_close {}'.format(fsfile_close))
print('fsuser_cardslotisinserted_wrapper {}'.format(fsuser_cardslotisinserted_wrapper))
print('read_nds_save_wrapper {}'.format(read_nds_save_wrapper))
print('read_nds_cart_id {}'.format(read_nds_cart_id))
print('write_nds_save {}'.format(write_nds_save))
print('fs_handle {:x}'.format(fs_handle))
gpavelski commented 1 year ago

Well, it's almost working now. Actually, running the Python script showed that I got some addresses wrong. Now the patch detects my white2.sav and even prompts to send the data. However, for some reason, the data is not sent. It displays that animation of sending data, but it fails in the end. I tried to create a new save data, but it still didn't finish the transfer.

Do you think it may be related to the region of the game?

zaksabeast commented 1 year ago

I don't think the region would cause any issues.

Just to make sure, are you using the Nintendo 3ds link after launching White 2?

Have you tried replacing these values in the python script and making a patch that way?

gpavelski commented 1 year ago

Just to make sure, are you using the Nintendo 3ds link after launching White 2?

Yes, when I open the Unova 3DS Link, it says there is no data to be transferred from Dream Radar.

Have you tried replacing these values in the python script and making a patch that way?

I can try, but how do I make sure what is my card_id? How do I know if it is IRDO or other value? I tried to replace the values and run this command on a CMD window: python poke_redirect.py "pokear" "IRDO" "/nds/saves/white2.sav" However, it only printed that as output: �����☺ ��☻ ��♠��� ♂�|�O-�4 �� �� 0��☺@��(P��☺��♥p�� ���▬���♥��� ����☼-�t����☼������∟�↔ /nds/saves/white2.sav EOF Shouldn't it create a file somewhere?

zaksabeast commented 1 year ago

You need to pipe the output somewhere. If you're on *nix/mac, you can use:

python poke_redirect.py "pokear" "IRDO" "/nds/saves/white2.sav" > code.ips

IRDO is the English version.

Here is a table of the language associated with each game id: Language White 2 game id Black 2 game id
German IRDD IRED
French IRDF IREF
Italian IRDI IREI
Japanese IRDJ IREJ
Korean IRDK IREK
English IRDO IREO
Spanish IRDS IRES

In the armips branch, you can change that at the end of this line. In the future it should be configurable with a default instead of hardcoded.

gpavelski commented 1 year ago

Running the poke_redirect.py script as you described generates the patch file. However, when I try to apply it to the game, it crashes before loading the 3DS logo. What is curious is that I installed a .cia of the European Dream Radar and repeated the steps above to create the patch from the radar.s file, and it worked pretty well, as it also works with the latest release version of the .ips file. That makes me think that there may be some small differences between the JP and EUR version that should be considered. Otherwise, the only address that may still be wrong in the radar.s file is the one after the end of .text, I am using 0x1bdec4.

Anyway, it is also fine if it doesn't work with the Japanese version, I could use the other version for transferring the Pokémon instead.

zaksabeast commented 1 year ago

Sorry to hear it's still not working.

I'm skeptical it would be handling save writing differently, which makes me think the address used might be incorrect.

I modified the script to adjust how it searches for the nds write save function. If you're still interested in getting this to work, here's the updated version (and if not, no worries!):

# Prints addresses for the Pokemon Radar Cart Redirect patch
# @category 3ds
#

from array import array
from ghidra.app.decompiler import DecompInterface

# ----------------------------------------
# Globals
# ----------------------------------------

memory = currentProgram.getMemory()
decomp = DecompInterface()
decomp.openProgram(currentProgram)

# ----------------------------------------
# Utils
# ----------------------------------------

def bytesToByteStr(bytes):
  encoded_bytes = map(lambda byte : chr(byte), bytes)
  return ''.join(encoded_bytes)

def bytesToByteArr(bytes):
  return array('b', bytesToByteStr(bytes))

def findBytes(start_addr, byte_arr):
  return memory.findBytes(start_addr, byte_arr, None, True, monitor)

def findFirstByteArray(bytes):
  byte_arr = bytesToByteArr(bytes)
  current_addr = findBytes(toAddr(0), byte_arr)
  while current_addr.getOffset() % 4 != 0:
    current_addr = findBytes(current_addr.add(4), byte_arr)
  return current_addr

def findFirstByteString(byte_str):
  return findFirstByteArray(array('B', byte_str.decode('hex')))

def findFirstUint(num):
  num_bytes = [num & 0xff, (num >> 8) & 0xff, (num >> 16) & 0xff, (num >> 24) & 0xff]
  return findFirstByteArray(num_bytes)

def findFirstFunctionUsingUint(num):
  num_addr = findFirstUint(num)
  num_ref = getReferencesTo(num_addr)[0]
  num_use = num_ref.getFromAddress()
  func = getFunctionContaining(num_use)
  return func.getEntryPoint()

def findFirstFunctionWithBytes(byte_str):
  bytes_addr = findFirstByteString(byte_str)
  func = getFunctionContaining(bytes_addr)
  return func.getEntryPoint()

def getFirstCallingFunction(func_addr):
  callers_refs = getReferencesTo(func_addr)
  first_caller_addr = callers_refs[0].getFromAddress()
  return getFunctionContaining(first_caller_addr).getEntryPoint()

def findFirstString(find_str):
  find_bytes = array('b', find_str)
  return findBytes(toAddr(0), find_bytes)

def findNextFunctionCallRef(addr):
  while addr < currentProgram.getMaxAddress():
    refs = getReferencesFrom(addr)
    for ref in refs:
      if ref.getReferenceType().isCall():
        return ref
    addr = addr.add(4)
  return None

def getCallArgs(caller_func, callee_addr):
    decompiled_func = decomp.decompileFunction(caller_func, 60, monitor)
    high_func = decompiled_func.getHighFunction()

    if not high_func:
      return []

    opiter = high_func.getPcodeOps()
    while opiter.hasNext():
      op = opiter.next()
      mnemonic = str(op.getMnemonic())
      if mnemonic == 'CALL':
        inputs = op.getInputs()
        addr = inputs[0].getAddress()
        if addr == callee_addr:
          return inputs[1:]

    return []

def getCallArgsFromRef(call_ref):
    from_addr = call_ref.getFromAddress()
    to_addr = call_ref.getToAddress()
    func = getFunctionContaining(from_addr)
    return getCallArgs(func, to_addr)

# ----------------------------------------
# Find radar addresses
# ----------------------------------------

# Get base addresses for most functions
fsuser_openfiledirectly = findFirstFunctionUsingUint(0x8030204)
fsfile_read = findFirstFunctionUsingUint(0x80200C2)
fsfile_write = findFirstFunctionUsingUint(0x8030102)
fsfile_close = findFirstFunctionUsingUint(0x8080000)
fsuser_cardslotisinserted = findFirstFunctionUsingUint(0x8210000)
fsuser_getlegacyromheader2 = findFirstFunctionUsingUint(0x8460102)
read_nds_save = findFirstFunctionWithBytes('600095e860a08de260008ae8105082e25810cde5')
write_nds_save = findFirstFunctionWithBytes('0400dce50430a0e10520a0e1080050e3')

# Get wrapper and calling function addresses
read_nds_save_wrapper = getFirstCallingFunction(read_nds_save)
fsuser_cardslotisinserted_wrapper = getFirstCallingFunction(fsuser_cardslotisinserted)
fsuser_getlegacyromheader2_wrapper = getFirstCallingFunction(fsuser_getlegacyromheader2)
read_nds_cart_id = getFirstCallingFunction(fsuser_getlegacyromheader2_wrapper)

# Find fs handle
fs_user_str = findFirstString('fs:USER')
# First ref is typically strlen
# Second ref should be with GetServiceHandleDirect
fs_user_ref = getReferencesTo(fs_user_str)[1].getFromAddress()
get_fs_handle_call = findNextFunctionCallRef(fs_user_ref)
get_fs_handle_args = getCallArgsFromRef(get_fs_handle_call)
fs_handle_ref_offset = get_fs_handle_args[0].getAddress()
fs_handle = memory.getInt(fs_handle_ref_offset)

# Print results
print('fsuser_openfiledirectly {}'.format(fsuser_openfiledirectly))
print('fsfile_read {}'.format(fsfile_read))
print('fsfile_write {}'.format(fsfile_write))
print('fsfile_close {}'.format(fsfile_close))
print('fsuser_cardslotisinserted_wrapper {}'.format(fsuser_cardslotisinserted_wrapper))
print('read_nds_save_wrapper {}'.format(read_nds_save_wrapper))
print('read_nds_cart_id {}'.format(read_nds_cart_id))
print('write_nds_save {}'.format(write_nds_save))
print('fs_handle {:x}'.format(fs_handle))
gpavelski commented 1 year ago

It worked! With the new write save address it transfers the Pokemon from the JP Dream Radar. That's awesome, thanks a lot!

zaksabeast commented 1 year ago

Awesome! I'm glad it worked!

Did you use the armips branch or the python script?

Would you be able to post the offsets here or make a pull request with the offsets? I'd love to make sure other people can use this too.

gpavelski commented 1 year ago

Did you use the armips branch or the python script?

I used the armips branch.

Would you be able to post the offsets here or make a pull request with the offsets?

Sure, here is the radar.s file that I used to create the patch:

.3ds
.open "radar.bin", "radar_patched.bin", 0x100000

fs_handle equ 0x1d9a1c
FSUSER_OpenFileDirectly equ 0x124d88
FSFILE_ReadFile equ 0x138dd8
FSFILE_WriteFile equ 0x138e5c
FSFILE_Close equ 0x138e30

.org 0x137b24
check_if_cart_is_inserted:
  b check_if_save_exists

.org 0x13d7ec
read_save:
  b read_save_from_sd

.org 0x13da18
.include "read-cart-id.s"

.org 0x187ebc
write_save:
  b write_save_to_sd

.org 0x1bdec4
.include "sd-save-utils.s"

.align 4
check_if_save_exists:
  stmdb sp!, { r1, r2, lr }
  stmdb sp!, { r0 }           ; push extra stack location to store file handle
  mov r1, sp                  ; set r1 to sp to store the file handle
  bl open_sd_save             ; store openSDSaveFile result in r0
  movs r2, r0, lsr #0x1f      ; get the most significant bit of the result - 0 if success, 1 if error
  eor r2, r2, #0x1            ; flip the bit so true if success, false if error
  mov r0, sp
  bl FSFILE_Close             ; FSFILE_Close is located at 0x1390b0
  ldmia sp!, { r0 }           ; pop extra stack location
  mov r0, r2                  ; set r0 to result
  ldmia sp!, { r1, r2, pc }

.close

Just one more question. My White 2 ROM in the Twilight Menu++ has a different name, that is quite long. For transferring data between the game and the Dream Radar, I simply make a copy of the save file and rename it to white2.sav. However, then after I transfer the data, I need to rename it again to the ROM name in order to use the Unova Link. So, for saving time I was thinking about generating the patch with the original name of the ROM. I simply run the command armips radar.s -strequ SD_SAVE_PATH "/roms/nds/saves/Pokemon White 2 (Experience + Trade Evolution Patched) (NoSSL).sav" -strequ GAME_ID "IRDO" instead of the traditional armips radar.s -strequ SD_SAVE_PATH "/roms/nds/saves/white2.sav" -strequ GAME_ID "IRDO" and keep all the other files unchanged. The patch is created as expected, but then when I try to boot the game, it just crashes. Do you think that it may be due to the length of the name or is it because it contains spaces?