Tutorial 1: Editing a File in a ROM¶
This tutorial will explain the process of opening a ROM file, extracting a file from it, putting a modified version of the file back into the ROM, and saving the modified ROM.
Note
We’re going to do this the way I would personally do it – by making small temporary scripts that accomplish whatever we need. To make it easier to follow, I’ll post the entire script file every time it changes, instead of just the edited parts. (The modified lines will be highlighted, though.)
Opening the ROM¶
The first step will be to import the ndspy module dedicated to ROM files,
ndspy.rom
. Make a new, empty Python file (say,
rom_files_tutorial.py
), open it, and write the following:
1import ndspy.rom
Try running it at this point. If it finishes without any errors, everything’s good so far.
Of course, we need to have a ROM to use. For ease of access, it’s a good idea
to put it in the same folder as your Python script. I’ll be using New Super
Mario Bros. (nsmb.nds
), but the same technique will work on just about any
ROM.
ndspy.rom
provides a class that we can use to work with ROM files:
NintendoDSRom
. Its constructor takes a bytes
object
containing the ROM file data, so we’ll need to get one of those. This step
doesn’t really involve ndspy per se:
1import ndspy.rom
2
3with open('nsmb.nds', 'rb') as f:
4 data = f.read()
'rb'
there tells open()
to open the file for r
eading in
b
inary mode. We can print the beginning of the data to see what it looks
like:
1import ndspy.rom
2
3with open('nsmb.nds', 'rb') as f:
4 data = f.read()
5
6print(data[:50])
b'NEW MARIO\x00\x00\x00A2DE01\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x08\x00\x02\x00\x00\x00\x02\xa4\xef\x05\x00\x00\xe8'
Now we can hand it off to ndspy to get a NintendoDSRom
object we
can do interesting things with:
1import ndspy.rom
2
3with open('nsmb.nds', 'rb') as f:
4 data = f.read()
5
6rom = ndspy.rom.NintendoDSRom(data)
7
8print(rom)
<rom "NEW MARIO" (A2DE)>
Cool, we now have a NintendoDSRom
for NSMB. (NEW MARIO
is the
game’s internal name. Internal names can be helpful, but they don’t always
necessarily match up with a game’s actual name.)
Warning
I’m using rom
here as the variable name for the
NintendoDSRom
object, since it represents a ROM. Be sure not to
confuse that with ndspy.rom
, which is the module containing the
NintendoDSRom
class!
Since opening a file, reading its contents as a bytes
object, and
making a NintendoDSRom
from it is a very common thing to do, ndspy
provides a shortcut for this:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5print(rom)
<rom "NEW MARIO" (A2DE)>
As you can see, that does the same thing as what we did on our own. Many ndspy
classes have .fromFile(filename)
functions like this!
Now that we have a ROM object, what can we do with it? Lots of things! For example, we can see how many files it contains:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5print(len(rom.files))
2088
Or we can check what memory address the main ARM9 code file will be loaded to:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5print(hex(rom.arm9RamAddress))
0x2000000
But of course, what we really want to do is extract a file.
Extracting a file¶
I’m going to extract polygon_unit/evf_cloud1.nsbtx
, which is the texture
for the foreground clouds in World 7-1.
Before continuing, you need to understand the relationship between files,
filenames, and file IDs in ROMs. There’s an explanation in the introductory
material for the ndspy.fnt
module (which is used internally by
ndspy.rom
) which I recommend you read:
Filenames and file IDs. What you really need to know, though, is that
files are fundamentally accessed by ID, and IDs are indices into a list of all
files in the ROM. Filename tables are separate, exist only for convenience, and
simply map file (and folder) names to file IDs.
So we need to get the file ID for polygon_unit/evf_cloud1.nsbtx
. A
NintendoDSRom
’s filenames table is provided as a
ndspy.fnt.Folder
, in a .filenames
attribute. We can print that
out to show all of the filenames and their corresponding file IDs (warning:
this is a pretty long printout):
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5print(rom.filenames)
0131 00DUMMY
0132 BUILDTIME
0133 mgvs_sound_data.sdat
0134 sound_data.sdat
0135 ARCHIVE/
0135 ARC0.narc
0136 bomthrow.narc
0137 card.narc
[snip]
1896 pl_ttl_LZ.bin
1897 plnovs_LZ.bin
1898 polygon_unit/
1898 evf_cloud1.nsbtx
1899 evf_haze1.nsbtx
1900 evf_sea1_a.nsbtx
[snip]
2085 UI_O_menu_title_logo_o_u_ncg.bin
2086 UI_O_menu_title_logo_u.bncl
2087 UI_O_menu_title_o_d_ncg.bin
From this, we can see that the file ID for polygon_unit/evf_cloud1.nsbtx
is
1898. How would we get that programmatically, though? Pretty easily, actually:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5print(rom.filenames.idOf('polygon_unit/evf_cloud1.nsbtx'))
1898
ndspy again provides a shortcut for this: ndspy.fnt.Folder
s
support indexing syntax for converting between filenames and file IDs. We can
use that here to make the code a bit shorter:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5print(rom.filenames['polygon_unit/evf_cloud1.nsbtx'])
1898
Now we can simply get the data for that file by using that file ID as an index
into the ROM’s .files
attribute:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5cloudNSBTXFileID = rom.filenames['polygon_unit/evf_cloud1.nsbtx']
6cloudNSBTX = rom.files[cloudNSBTXFileID]
7
8print(cloudNSBTX[:50])
bytearray(b'BTX0\xff\xfe\x01\x00\x84a\x00\x00\x10\x00\x01\x00\x14\x00\x00\x00TEX0pa\x00\x00\x00\x00\x00\x00\x00\x00<\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\x00\x08')
Cool.
Note
You might be wondering what this “bytearray
” is, and why we didn’t get
a bytes
object. A bytearray
is essentially a
mutable version of bytes
, meaning it can be modified. ndspy
provides the data for files within the ROM as bytearray
s to
make it a bit more convenient to edit them.
Since it’s pretty common to want to get the data for the file with some filename, ndspy yet again has a shortcut for it:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5cloudNSBTX = rom.getFileByName('polygon_unit/evf_cloud1.nsbtx')
6
7print(cloudNSBTX[:50])
bytearray(b'BTX0\xff\xfe\x01\x00\x84a\x00\x00\x10\x00\x01\x00\x14\x00\x00\x00TEX0pa\x00\x00\x00\x00\x00\x00\x00\x00<\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\x00\x08')
Note
At this point, you’re probably wishing I would just jump straight to the
shortcut syntax in the first place. Well, I think it’s important to have
some idea of what the shortcuts are shortcuts for, especially since you
won’t always be able to use them in every situation. For example, the
.fromFile(filename)
functions aren’t very useful if you want to load a
file that you got from a ROM. ROM files are provided as bytes
objects, so you’re better off using class constructors that take those
instead.
Anyway, now we can go ahead and save evf_cloud1.nsbtx
to an actual file
outside of the ROM:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5cloudNSBTX = rom.getFileByName('polygon_unit/evf_cloud1.nsbtx')
6
7with open('evf_cloud1.nsbtx', 'wb') as f:
8 f.write(cloudNSBTX)
Now you can open that file with some other tool (such as MKDS Course Modifier) and make changes as you see fit.
Go ahead and do that now. I’ll wait.
Replacing a file¶
Done? Time to replace the file in the ROM with our new copy, then!
Let’s suppose you saved the modified NSBTX file to evf_cloud1_edited.nsbtx
.
Our goal is to get that as a bytes
object and put it into
rom.files
. One step at a time, though – let’s start by getting the new
file data:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5with open('evf_cloud1_edited.nsbtx', 'rb') as f:
6 cloudNSBTXEdited = f.read()
This gets us the NSBTX data and puts it in cloudNSBTXEdited
. We can put
said data into the files
list using the file ID:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5with open('evf_cloud1_edited.nsbtx', 'rb') as f:
6 cloudNSBTXEdited = f.read()
7
8rom.files[rom.filenames['polygon_unit/evf_cloud1.nsbtx']] = cloudNSBTXEdited
Or with ndspy’s shortcut function for accomplishing the same thing:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5with open('evf_cloud1_edited.nsbtx', 'rb') as f:
6 cloudNSBTXEdited = f.read()
7
8rom.setFileByName('polygon_unit/evf_cloud1.nsbtx', cloudNSBTXEdited)
Done. All that’s left now is to save the modified ROM so we can try it out!
Saving the ROM¶
NintendoDSRom
provides a .save()
function that returns a
bytes
object, which we can use to save the ROM:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5with open('evf_cloud1_edited.nsbtx', 'rb') as f:
6 cloudNSBTXEdited = f.read()
7
8rom.setFileByName('polygon_unit/evf_cloud1.nsbtx', cloudNSBTXEdited)
9
10with open('nsmb_edited.nds', 'wb') as f:
11 f.write(rom.save())
Naturally, though, there’s a shortcut for that:
1import ndspy.rom
2
3rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
4
5with open('evf_cloud1_edited.nsbtx', 'rb') as f:
6 cloudNSBTXEdited = f.read()
7
8rom.setFileByName('polygon_unit/evf_cloud1.nsbtx', cloudNSBTXEdited)
9
10rom.saveToFile('nsmb_edited.nds')
And that’s all there is to it! Go try your ROM out and enjoy whatever change you made.