Tutorial 2: Editing a File in a NARC Archive¶
This tutorial will explain the process of opening a NARC archive file, extracting a file from it, putting a modified version of the file back into the NARC archive, and saving the modified ROM.
If you’ve completed the first tutorial (Tutorial 1: Editing a File in a ROM), this will feel extremely familiar. NARCs are more-or-less like miniature ROMs that only have files and filenames, so the ndspy APIs for ROMs and NARCs are very similar.
See also
If you haven’t tried the first two tutorials yet, I recommend you do so:
Tutorial 0: Getting Started: This tutorial helps you check that you have ndspy installed and set up correctly.
Tutorial 1: Editing a File in a ROM: This one is written at a slower pace than the other tutorials, and serves as a gentle introduction to help you get a feel for how to use ndspy in general.
Opening the NARC¶
We’ll begin by importing ndspy.narc
, the ndspy module for NARC
archives. Make a new, empty Python file (say,
narc_files_tutorial.py
), and put the following in it:
1import ndspy.narc
You can test it at this point if you want to check that it runs correctly, but it won’t do a whole lot!
Next, we need to open a NARC using the NARC
class. If your NARC
is within a ROM file, you can use ndspy.rom
to get the NARC data
and pass it to NARC
's default constructor, like so:
1import ndspy.narc
2import ndspy.rom
3
4rom = ndspy.rom.NintendoDSRom('nsmb.nds')
5narcData = rom.files[169]
6narc = ndspy.narc.NARC(narcData)
However, for brevity, I’ll be assuming from here on that your NARC is a
standalone file, so that I can use the fromFile()
class method to load
it in a single line instead:
1import ndspy.narc
2
3narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
You can substitute the ROM-loading code if you prefer, though, of course.
Warning
I’m using narc
here as the variable name for the
NARC
object, since it represents a NARC. Be sure not to
confuse that with ndspy.narc
, which is the module containing the
NARC
class!
And the same goes for the variable name rom
versus the module
ndspy.rom
, in a few places.
Either way, now we have a NARC
object. We can print it out to see
all of its file IDs, folders and filenames, and the first few bytes of each
file:
1import ndspy.narc
2
3narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
4print(narc)
<narc endiannessOfBeginning='>'
0000 vs_escape/
0000 d_2d_mgvs_bg_escape_down_ncg.bin b'\x10\0\x80\0?\0\0\xf0\1\xf0\x13\xf0%\xf07\xf0'...
0001 d_2d_mgvs_bg_escape_down_ncl.bin b'\x1f\03\tU\t:\1\x97\tz\5\x9b\5\xb9\r'...
0002 d_2d_mgvs_bg_escape_down_nsc.bin b'\x10\0\x08\0\0\x90\x82\x91\xe2\x92\x82\x93\x82\0\x94\xe2'...
0003 d_2d_mgvs_bg_escape_up1_nsc.bin b'\x10\0\x08\0\0\xc7\xa1\xc8\xa1\xc9\xb1\xca\x91\0\xcb\x81'...
0004 d_2d_mgvs_bg_escape_up2_nsc.bin b'\x10\0\x08\0 \x7f3\xf0\1\xfb5\xfa5\xf9\05'...
0005 d_2d_mgvs_bg_escape_up3_nsc.bin b'\x10\0\x08\00\x7f3\xf0\1p\1\x96\xd5\x96\xd1\xe0'...
0006 d_2d_mgvs_bg_escape_up_ncg.bin b'\x10\0\x80\0?\0\0\xf0\1\xf0\x13\xf0%\xf07\xf0'...
0007 d_2d_mgvs_bg_escape_up_ncl.bin b'\x94z\0\0\0\0\0\0\0\0\0\0\0\0\0\0'...
0008 d_2d_mgvs_escape_ncg.bin b'\x10\0@\06\0\0\xf0\1\x80\x130\xf0\x1fp1'...
0009 d_2d_mgvs_escape_ncl.bin b'\xa7}\0\0\xff\x7f\x18c\x10B\x08!\xff\3\xbc\2'...
>
Note
Don’t worry about the endiannessOfBeginning
part too much; it just
means that ndspy noticed that the NARC file data had a slightly
nonstandard header, which it will match when we resave the file.
If, however, you’re planning on making a new NARC
from scratch
for a game that uses NARCs with these nonstandard headers, you’ll have
to remember to set NARC.endiannessOfBeginning
to '>'
yourself!
Extracting a file¶
I’m going to extract d_2d_mgvs_escape_ncg.bin
and
d_2d_mgvs_escape_ncl.bin
, which are the sprite graphics and palette for
New Super Mario Bros.’s “Danger, Bob-omb! Danger!” minigame.
Note
If you’re not yet familiar with the relationship between files,
filenames, and file IDs in NARCs, I recommend you read the documentation
on this topic in the introductory material for the ndspy.fnt
module (which is used internally by ndspy.narc
):
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 NARC. Filename tables are separate, exist only for convenience, and simply map file (and folder) names to file IDs.
It’s also worth mentioning that, while NARC file IDs work the same way as ROM file IDs, they’re completely separate from the file IDs of the enclosing ROM, if there is one.
We can see from the NARC printout we made earlier that these two files have file IDs 8 and 9 respectively; thus, we can access their data directly using those indices:
1import ndspy.narc
2
3narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
4
5ncgData = narc.files[8]
6nclData = narc.files[9]
Or we could use the NARC
class’s helper function that looks up a
filename in the filenames table to get a file ID, and retrieves the
corresponding file data directly:
1import ndspy.narc
2
3narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
4
5ncgData = narc.getFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin')
6nclData = narc.getFileByName('vs_escape/d_2d_mgvs_escape_ncl.bin')
Either way, we’ve now got bytes
objects containing the data for the
files we’re interested in.
Now it’s time to edit the files. If ndspy supports editing the file formats in
question, you can just import the appropriate modules and edit the files that
way. (In this particular example case, the NCL and NCG files can be edited
using the ndspy.lz10
and ndspy.graphics2D
modules, which is
what I did at this point.) If you instead want to save the files externally to
edit using some other tool, you can of course do that as well:
1import ndspy.narc
2
3narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
4
5ncgData = narc.getFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin')
6nclData = narc.getFileByName('vs_escape/d_2d_mgvs_escape_ncl.bin')
7
8with open('d_2d_mgvs_escape_ncg.bin', 'wb') as f:
9 f.write(ncgData)
10with open('d_2d_mgvs_escape_ncl.bin', 'wb') as f:
11 f.write(nclData)
Replacing a file¶
Once you’re done editing your file, we can go ahead and put it back in the NARC.
If (like me) you modified the bytes
objects from the NARC using
other ndspy modules, you’ll already have the modified file data as
bytes
objects. But if you instead saved your file externally to
edit with something else, you’ll need to load its data back in:
1import ndspy.narc
2
3narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
4
5with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f:
6 ncgData = f.read()
7with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f:
8 nclData = f.read()
Now we can put the data back into the NARC, either by file ID:
1import ndspy.narc
2
3narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
4
5with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f:
6 ncgData = f.read()
7with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f:
8 nclData = f.read()
9
10narc.files[8] = ncgData
11narc.files[9] = nclData
Or by filename:
1import ndspy.narc
2
3narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
4
5with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f:
6 ncgData = f.read()
7with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f:
8 nclData = f.read()
9
10narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin', ncgData)
11narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncl.bin', nclData)
Saving the NARC¶
All that’s left now is to resave the NARC. If you want to put it into a ROM,
you should use the NARC’s .save()
function to get a bytes
,
suitable for the ROM’s .files
list:
1import ndspy.narc
2import ndspy.rom
3
4rom = ndspy.rom.NintendoDSRom('nsmb.nds')
5
6narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
7
8with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f:
9 ncgData = f.read()
10with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f:
11 nclData = f.read()
12
13narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin', ncgData)
14narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncl.bin', nclData)
15
16rom.files[169] = narc.save()
(Don’t forget to save the ROM itself when you’re done with it, too!)
Or if you just want to save it as its own file, that can be done using the
.saveToFile()
function:
1import ndspy.narc
2
3narc = ndspy.narc.NARC.fromFile('vs_escape.narc')
4
5with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f:
6 ncgData = f.read()
7with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f:
8 nclData = f.read()
9
10narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin', ncgData)
11narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncl.bin', nclData)
12
13narc.saveToFile('vs_escape_edited.narc')
And, well, that’s it! Have fun with your newly modified NARC file.