..
Copyright 2019 RoadrunnerWMC
This file is part of ndspy.
ndspy is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ndspy is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with ndspy. If not, see .
Tutorial 1: Editing a File in a ROM
===================================
.. py:currentmodule:: ndspy.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,
:py:mod:`ndspy.rom`. Make a new, empty Python file (say,
``rom_files_tutorial.py``), open it, and write the following:
.. code-block:: python
:linenos:
import 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.
:py:mod:`ndspy.rom` provides a class that we can use to work with ROM files:
:py:class:`NintendoDSRom`. Its constructor takes a :py:class:`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:
.. code-block:: python
:emphasize-lines: 3-4
:linenos:
import ndspy.rom
with open('nsmb.nds', 'rb') as f:
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:
.. code-block:: python
:emphasize-lines: 6
:linenos:
import ndspy.rom
with open('nsmb.nds', 'rb') as f:
data = f.read()
print(data[:50])
.. code-block:: text
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 :py:class:`NintendoDSRom` object we
can do interesting things with:
.. code-block:: python
:emphasize-lines: 6-8
:linenos:
import ndspy.rom
with open('nsmb.nds', 'rb') as f:
data = f.read()
rom = ndspy.rom.NintendoDSRom(data)
print(rom)
.. code-block:: text
Cool, we now have a :py:class:`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
:py:class:`NintendoDSRom` object, since it represents a ROM. Be sure not to
confuse that with :py:mod:`ndspy.rom`, which is the module containing the
:py:class:`NintendoDSRom` class!
Since opening a file, reading its contents as a :py:class:`bytes` object, and
making a :py:class:`NintendoDSRom` from it is a very common thing to do, ndspy
provides a shortcut for this:
.. code-block:: python
:emphasize-lines: 3
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
print(rom)
.. code-block:: text
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:
.. code-block:: python
:emphasize-lines: 5
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
print(len(rom.files))
.. code-block:: text
2088
Or we can check what memory address the main ARM9 code file will be loaded to:
.. code-block:: python
:emphasize-lines: 5
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
print(hex(rom.arm9RamAddress))
.. code-block:: text
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.
.. figure:: images/rom-before.png
:scale: 30%
:align: center
What World 7-1 looks like in regular *New Super Mario Bros.* You can see
one-and-a-half foreground clouds in this screenshot.
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 :py:mod:`ndspy.fnt` module (which is used internally by
:py:mod:`ndspy.rom`) which I recommend you read:
:ref:`file-names-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
:py:class:`NintendoDSRom`'s filenames table is provided as a
:py:class:`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):
.. code-block:: python
:emphasize-lines: 5
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
print(rom.filenames)
.. code-block:: text
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:
.. code-block:: python
:emphasize-lines: 5
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
print(rom.filenames.idOf('polygon_unit/evf_cloud1.nsbtx'))
.. code-block:: text
1898
ndspy again provides a shortcut for this: :py:class:`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:
.. code-block:: python
:emphasize-lines: 5
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
print(rom.filenames['polygon_unit/evf_cloud1.nsbtx'])
.. code-block:: text
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:
.. code-block:: python
:emphasize-lines: 5-8
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
cloudNSBTXFileID = rom.filenames['polygon_unit/evf_cloud1.nsbtx']
cloudNSBTX = rom.files[cloudNSBTXFileID]
print(cloudNSBTX[:50])
.. code-block:: text
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 :py:class:`bytes` object. A :py:class:`bytearray` is essentially a
mutable version of :py:class:`bytes`, meaning it can be modified. ndspy
provides the data for files within the ROM as :py:class:`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:
.. code-block:: python
:emphasize-lines: 5
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
cloudNSBTX = rom.getFileByName('polygon_unit/evf_cloud1.nsbtx')
print(cloudNSBTX[:50])
.. code-block:: text
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 :py:class:`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:
.. code-block:: python
:emphasize-lines: 7-8
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
cloudNSBTX = rom.getFileByName('polygon_unit/evf_cloud1.nsbtx')
with open('evf_cloud1.nsbtx', 'wb') as f:
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 :py:class:`bytes` object and put it into
``rom.files``. One step at a time, though -- let's start by getting the new
file data:
.. code-block:: python
:emphasize-lines: 5-6
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
with open('evf_cloud1_edited.nsbtx', 'rb') as f:
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:
.. code-block:: python
:emphasize-lines: 8
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
with open('evf_cloud1_edited.nsbtx', 'rb') as f:
cloudNSBTXEdited = f.read()
rom.files[rom.filenames['polygon_unit/evf_cloud1.nsbtx']] = cloudNSBTXEdited
Or with ndspy's shortcut function for accomplishing the same thing:
.. code-block:: python
:emphasize-lines: 8
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
with open('evf_cloud1_edited.nsbtx', 'rb') as f:
cloudNSBTXEdited = f.read()
rom.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
--------------
:py:class:`NintendoDSRom` provides a ``.save()`` function that returns a
:py:class:`bytes` object, which we can use to save the ROM:
.. code-block:: python
:emphasize-lines: 10-11
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
with open('evf_cloud1_edited.nsbtx', 'rb') as f:
cloudNSBTXEdited = f.read()
rom.setFileByName('polygon_unit/evf_cloud1.nsbtx', cloudNSBTXEdited)
with open('nsmb_edited.nds', 'wb') as f:
f.write(rom.save())
Naturally, though, there's a shortcut for that:
.. code-block:: python
:emphasize-lines: 10
:linenos:
import ndspy.rom
rom = ndspy.rom.NintendoDSRom.fromFile('nsmb.nds')
with open('evf_cloud1_edited.nsbtx', 'rb') as f:
cloudNSBTXEdited = f.read()
rom.setFileByName('polygon_unit/evf_cloud1.nsbtx', cloudNSBTXEdited)
rom.saveToFile('nsmb_edited.nds')
And that's all there is to it! Go try your ROM out and enjoy whatever change
you made.
.. figure:: images/rom-after.png
:scale: 30%
:align: center
Perfect.