.. 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 2: Editing a File in a NARC Archive ============================================ .. py:currentmodule:: ndspy.narc 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 (:doc:`rom-editing`), this will feel extremely familiar. *NARC*\s are more-or-less like miniature ROMs that only have files and filenames, so the ndspy APIs for ROMs and *NARC*\s are very similar. .. seealso:: If you haven't tried the first two tutorials yet, I recommend you do so: * :doc:`getting-started`: This tutorial helps you check that you have ndspy installed and set up correctly. * :doc:`rom-editing`: 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 :py:mod:`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: .. code-block:: python :linenos: import 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 :py:class:`NARC` class. If your *NARC* is within a ROM file, you can use :py:mod:`ndspy.rom` to get the *NARC* data and pass it to :py:class:`NARC`\'s default constructor, like so: .. code-block:: python :emphasize-lines: 2, 4-6 :linenos: import ndspy.narc import ndspy.rom rom = ndspy.rom.NintendoDSRom('nsmb.nds') narcData = rom.files[169] narc = 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 :py:func:`fromFile` class method to load it in a single line instead: .. code-block:: python :emphasize-lines: 3 :linenos: import ndspy.narc narc = 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 :py:class:`NARC` object, since it represents a *NARC*. Be sure not to confuse that with :py:mod:`ndspy.narc`, which is the module containing the :py:class:`NARC` class! And the same goes for the variable name ``rom`` versus the module :py:mod:`ndspy.rom`, in a few places. Either way, now we have a :py:class:`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: .. code-block:: python :emphasize-lines: 4 :linenos: import ndspy.narc narc = ndspy.narc.NARC.fromFile('vs_escape.narc') print(narc) .. code-block:: text .. 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 :py:class:`NARC` from scratch for a game that uses *NARC*\s with these nonstandard headers, you'll have to remember to set :py:attr:`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. .. figure:: images/narc-before.png :scale: 30% :align: center What the minigame looks like in regular *New Super Mario Bros.* You use the touch screen to drag the Bob-Omb around, and dodge the fire columns and fireballs. I wonder if there's a way all of this excess heat could be put to good use... .. note:: If you're not yet familiar with the relationship between files, filenames, and file IDs in *NARC*\s, I recommend you read the documentation on this topic in the introductory material for the :py:mod:`ndspy.fnt` module (which is used internally by :py:mod:`ndspy.narc`): :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 *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: .. code-block:: python :emphasize-lines: 5-6 :linenos: import ndspy.narc narc = ndspy.narc.NARC.fromFile('vs_escape.narc') ncgData = narc.files[8] nclData = narc.files[9] Or we could use the :py:class:`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: .. code-block:: python :emphasize-lines: 5-6 :linenos: import ndspy.narc narc = ndspy.narc.NARC.fromFile('vs_escape.narc') ncgData = narc.getFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin') nclData = narc.getFileByName('vs_escape/d_2d_mgvs_escape_ncl.bin') Either way, we've now got :py:class:`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 :py:mod:`ndspy.lz10` and :py:mod:`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: .. code-block:: python :emphasize-lines: 8-11 :linenos: import ndspy.narc narc = ndspy.narc.NARC.fromFile('vs_escape.narc') ncgData = narc.getFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin') nclData = narc.getFileByName('vs_escape/d_2d_mgvs_escape_ncl.bin') with open('d_2d_mgvs_escape_ncg.bin', 'wb') as f: f.write(ncgData) with open('d_2d_mgvs_escape_ncl.bin', 'wb') as f: 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 :py:class:`bytes` objects from the *NARC* using other ndspy modules, you'll already have the modified file data as :py:class:`bytes` objects. But if you instead saved your file externally to edit with something else, you'll need to load its data back in: .. code-block:: python :emphasize-lines: 5-8 :linenos: import ndspy.narc narc = ndspy.narc.NARC.fromFile('vs_escape.narc') with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f: ncgData = f.read() with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f: nclData = f.read() Now we can put the data back into the *NARC*, either by file ID: .. code-block:: python :emphasize-lines: 10-11 :linenos: import ndspy.narc narc = ndspy.narc.NARC.fromFile('vs_escape.narc') with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f: ncgData = f.read() with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f: nclData = f.read() narc.files[8] = ncgData narc.files[9] = nclData Or by filename: .. code-block:: python :emphasize-lines: 10-11 :linenos: import ndspy.narc narc = ndspy.narc.NARC.fromFile('vs_escape.narc') with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f: ncgData = f.read() with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f: nclData = f.read() narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin', ncgData) narc.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 :py:class:`bytes`, suitable for the ROM's ``.files`` list: .. code-block:: python :emphasize-lines: 2, 4, 16 :linenos: import ndspy.narc import ndspy.rom rom = ndspy.rom.NintendoDSRom('nsmb.nds') narc = ndspy.narc.NARC.fromFile('vs_escape.narc') with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f: ncgData = f.read() with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f: nclData = f.read() narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin', ncgData) narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncl.bin', nclData) rom.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: .. code-block:: python :emphasize-lines: 13 :linenos: import ndspy.narc narc = ndspy.narc.NARC.fromFile('vs_escape.narc') with open('d_2d_mgvs_escape_ncg_edited.bin', 'rb') as f: ncgData = f.read() with open('d_2d_mgvs_escape_ncl_edited.bin', 'rb') as f: nclData = f.read() narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncg.bin', ncgData) narc.setFileByName('vs_escape/d_2d_mgvs_escape_ncl.bin', nclData) narc.saveToFile('vs_escape_edited.narc') And, well, that's it! Have fun with your newly modified *NARC* file. .. figure:: images/narc-after.png :scale: 30% :align: center It's supposed to be a pizza, but I'm no good at drawing. At least I tried!