Introduction
Well, I didn't really intend on this being 3 parts, but here we are. So if you haven't read the previous parts, you probably should. Here are some links:
Following Part 2, I went from having a frozen white screen "brick" of an Action Replay DS cartridge to having a working cartridge, just like how things were in 2007. I now have full access to working hardware. And I am able to flash any kind of firmware I want onto a physical ARDS cartridge utilising a trick with CFW. So I think it's now time to go and finish off the remaining research on how the thing works.
In Part 1, a lot of the recovery steps taken were based on guesses and looking at bytes in a hex editor. While I'm sure a lot of that research and guessing was correct (as I was able to recover all of my old codes), it would be nice to simplify it all down and organise it. Documentation, I guess. So, where do we start?
Game Address Lookup
In Part 1, I wrote and introduced some tools in a suite. One of them was
ards_game_ls
,
which can be used to list the addresses of games that are on an Action
Replay DS cartridge. Let's talk a little more about it, because there's
things to improve.
The way this program searched for games was brute-force, or exhaustive. It
started at 0x00054000
on the cartridge and just went through
each byte searching for a
magic number.
Once it found one, it knows it found a game. Then it just figures out the
code binary and names from there. While this does work, it is for
more of a "data rescue" procedure. I genuinely doubt the actual cartridge
will casually search through its entire memory to show a list of games.
It's just too slow. There should be a lookup table somewhere. At least
that's my hunch.
This is where 0x00044000
comes in. Behold:
Well, ASME-AEA63749
shows up as the first one, and it's
obviously Super Mario 64 DS. The rest of them are also valid game IDs.
Google them up (except for NDS-Action Replay Opts
). There is a
pattern here. Each game is a field of 32 bytes. Hence, there is a 32 byte
struct that stores game data and location. And this is an array of structs,
so treat it as one literally. These structs can be proposed as:
/* List Node Struct */
typedef struct AR_GAME_LIST_NODE {
// Bytes Description
uint32_t magic; // 00 - 03. Always "00 00 00 00" if game.
union {
char raw[24]; // 04 - 27. Entire Game ID as C-String
struct { // -----------------------------------------
char ID_LEFT[4]; // 04 - 07. 4 characters on cartridge
char sep; // 08 - 08. Always "-" (0x2D). Separator
char ID_NCRC32[8]; // 09 - 16. ~CRC32(first 512 bytes of ROM)
char null_term; // 17 - 17. Terminates ID C-String above.
char extra[10]; // 18 - 27. Remaining Buffer
} segment;
} ID;
uint16_t location; // 28 - 29. 0x40000 + (location << 8)
uint16_t chunks; // 30 - 31. Number of 0x100 byte chunks
} ar_game_list_node;
/* List Node Wrapper */
typedef struct AR_GAME_LIST_T {
size_t num_games;
ar_game_list_node *games;
} ar_game_list_t;
Maybe it's a little excessive that there's a union and struct there. But I
want to give a programmer the option to use node.ID.raw
for
printing out the raw C-String, or access individual parts at will via
node.ID.segment.ID_LEFT
, etc. That's why it's formatted that
way. Code readability. Also, this is because the 24 bytes are indeed a
buffer. Otherwise, we will run into errors accounting for
NDS-Action Replay Opts
, which is a valid input.
Anyways, those location
and chunks
values at the
very end are our focus. They jump straight to the location of the header
for a game, which is followed by code binary, and then finally the code
text and note strings. And since every single game can be put into chunks
of 0x100
(256) bytes, we can optimise
ards_game_ls
for casual use, while keeping it's original
brute-force mode as a sort of "data rescue" mode.
There really is an opportunity for some code elegance here. Build the
array, jump to that point of memory, then use the knowledge of
ar_game_info_t
from
this segment of Part 1's post
to get the location of the game name. Jump to that and read it. Thus, you
have a proper game lister in a few lines of code. Elegant. And since it
jumps around memory rather than searching byte-for-byte, it will be
extremely fast too.
It works btw.
And the time difference is substantial, for obvious reasons:
This isn't even a proper benchmark. But I don't think I need to do one to show that it's clearly faster.
NDS ROM Structure
Since that code list was the very last thing I needed to know, in generics, I can document the overall structure of the ROM file. It becomes simple once you see it.
Happens at every 0x00100000 until 0x00FFFFFF
0x00000 - 0x3FFFF - Firmware
0x40000 - 0x43FFF - ???
0x44000 - 0x53FFF - Game List
0x54000 - 0xFFFFF - Game Data and Codes
Well, okay. I don't know what that "???" section is. But whatever. This is the overall structure of the ROM file. It's what all of the tools written for the 3 blog posts have relied on.
I can go into some detail on each part, linking to parts within the 3 blog posts where relevant information is helpful.
Segment 1: Firmware
This segment ranges from 0x00000
to 0x3FFFF
. But
it can vary in size. For the details and research, the section
What is the "firmware" file anyways?
in Part 2 breaks everything down.
The firmware file has an 8 byte header, as well as the bytes of this
section in the NDS ROM. What the Code Manager does is validate the firmware
with a CRC16 checksum stored in bytes 4 through 8 in the firmware file.
Then it copies everything else to the cartridge over USB, flashing it.
Everything that comes after the firmware's file size is FF
up
until (and including) 0x3FFFF
.
This means you can extract the firmware easily from a ROM dump. But the
first copy of the ROM has some modified data between
0x00002000
and 0x00004800
. So to extract the
firmware, simply jump to 0x001047FF
. Then go back until
you find a byte that isn't a FF
. Then copy from
0x00100000
to that point. That's your firmware. Details on
that are
here.
And if you just want a program to extract it for you,
ards_firm_extract
will do it easily.
Segment 2: ???
idk. It's the segment that ranges from 0x40000
to
0x43FFF
. So naturally I nuked it and set everything to
00
to see if it still boots. It does.
Segment 3: Game List
This segment ranges from 0x44000
to 0x53FFF
. It
just stores a lookup array. This serves as a way for the cartridge to
look up what games are stored on it, and then jump to that spot in memory
instantly. Each game in this lookup array takes up 32 bytes. After the list
ends, all bytes are FF
up until (and including)
0x53FFF
.
See Game Address Lookup up above for details on this. But it really is that simple.
Segment 4: Game Data and Codes
This segment requires it's own section, as it's the most complex. This
segment ranges from 0x54000
all the way until the end of the
current copy of the ROM at 0xFFFFF
. The memory here is
constantly overwritten. If you wipe out the memory of the Action Replay DS
cartridge to remove all codes, it doesn't set memory here to zero. Instead,
something overwrites it. Kind of like how deleting files on a hard drive
works.
The location of games is determined by the lookup array defined in the
previous segment at 0x44000
. From there, jump to a specific
address in this segment and you will find game data stored in the following
format:
32 bytes - Header
?? bytes - Code Binary
?? bytes - String Position Data
?? bytes - String Data
The reason why 3 of the 4 sections have "??" as the byte count is because it is dynamic. But each section after the first relies on data from the previous ones. The code binary segment will read codes based on the number of codes specified in the header. The code text/note data will read text, and positional data is specified in the "String Position Data" segment. It is dynamic based on, obviously, how many codes are present.
Header
To break this down, first is the header. Thus, the following struct is useful:
typedef struct AR_GAME_INFO_T {
// Bytes Description
uint32_t magic; // 00 - 03. Always "01 00 1C 00".
uint16_t num_codes; // 04 - 05. Number of codes present
uint16_t nx20; // 06 - 07. Always "20 00".
uint32_t offset_text; // 08 - 11. Bytes from game_info_t to text - 1
uint32_t offset_strlen; // 12 - 15. Bytes from game_info_t to strlen
uint16_t wDosDate; // 16 - 17. DOS date (?)
uint16_t wDosTime; // 18 - 19. DOS time (?)
char ID[4]; // 20 - 23. 4 characters that appear on cartridge
uint32_t idk; // 24 - 27
uint32_t N_CRC32; // 28 - 31. ~CRC32(first 512 bytes of ROM)
} ar_game_info_t;
This is straight from part 1, here. With it, we have the first 32 bytes of a game's code data.
Deciphering Code Binary - Codes
Now for the code binary. Before each code or folder, there are 2 bytes which tell you what type the next few bytes will be. Then there is 2 bytes for quantity. After that is the body of the code, assuming it's a code. If it's a folder, a code will follow. Let's break it down.
The first 2 bytes are to tell the type. This can be one of the following values:
01 00
is a code.11 00
is a master code.01 80
is a on by default code.11 80
is a master code on by default.19 80
is a master code always on.09 80
is a always on code.02 00
is a regular folder.-
06 00
is a radio button folder (only one code can be on at once).
For simplicity, we'll talk about a code first. So if I had something like this:
01 00 02 00 94 F1 17 12 22 00 00 00 00 00 00 D2
00 00 00 00
The first 2 bytes are 01 00
. Therefore, this is a code. The
next 2 bytes are for the number of lines the code has. Thus, this
tells us that the code has 2 lines. A line is like
XXXXXXXX YYYYYYYY
. In the case of Action Replay, this is
optimally represented as 8 bytes per line. Each line is also stored
as little-endian. This means that 01234567 89ABCDEF
would be stored in memory as 67 45 23 01 EF CD AB 89
.
With this in mind, the example up above has 2 lines. If we want to decode
it, it will have 94 F1 17 12 22 00 00 00
and
00 00 00 D2 00 00 00 00
to decode. This becomes:
1217F194 00000022
D2000000 00000000
The binary storage is useful because it stores this in 8 bytes. If it was stored as text, it would take up 32 bytes. An Action Replay DS cartridge can only store 1 MB of codes compact. That isn't a lot. So if you stored the codes as text, you would only have 1/4th of 1 MB. 256 KB. That is 5.625x less than a standard floppy disk. Binary is important.
Oh, also if this is a Master Code, the format is exactly the same,
except the first 2 bytes are 11 00
rather than
01 00
. In a codelist XML file, this is indicated
master
before the actual code such as:
<cheat>
<name>(M)</name>
<codes>master 01234567 89ABCDEF</codes>
</cheat>
For a On by default or Always On code, these can be done by
specifying on
or always_on
respectively before
the actual code values.
<cheat>
<name>On by default</name>
<codes>on 01234567 89ABCDEF</codes>
</cheat>
<cheat>
<name>Always On</name>
<codes>always_on 01234567 89ABCDEF</codes>
</cheat>
Lastly, for some extremely specific situations where you will want
combinations of the master
, on
, and
always_on
, you can specify multiple at once:
<cheat>
<name>Master and On by Default</name>
<codes>master on 01234567 89ABCDEF</codes>
</cheat>
<cheat>
<name>Master and Always On</name>
<codes>master always_on 01234567 89ABCDEF</codes>
</cheat>
<cheat>
<name>On by Default and Always On</name>
<codes>on always_on 01234567 89ABCDEF</codes>
</cheat>
<cheat>
<name>Master and On by Default and Always On</name>
<codes>master on always_on 01234567 89ABCDEF</codes>
</cheat>
Whenever you put this into the Code Manager on PC, it will only show the
first modifier. But in the XML file, it will preserve all flags given. And
when pushed to the ARDS cartridge, it will also retain all flags. However,
always_on
will assume on
by default. So it is
useless to have all three tacked onto a single code. More on why down
below.
Deciphering Code Binary - Folders
As I said earlier, 02 00
or 06 00
is a folder. If
this is the case, then the 2 bytes after will be the number of codes the
folder has. The codes inside the folder retain the same format
discussed above. So here's an example:
02 00 02 00 01 00 02 00 F8 9D 16 22 11 00 00 00
F9 9D 16 22 0D 00 00 00 01 00 02 00 F8 9D 16 22
19 00 00 00 F9 9D 16 22 17 00 00 00
The first 2 bytes are 02 00
. Therefore, this is a folder. The
next 2 bytes tells us that this folder has 2 codes in it. From here, it's
easy. The remaining parts are just codes, which can be figured out from the
procedure up above.
The first code has 2 lines, as it's 01 00 02 00
. This resolves
to:
22169DF8 00000011
22169DF9 0000000D
Then the next bytes show another 2 line code, as it's also
01 00 02 00
. This code resolves to:
22169DF8 00000019
22169DF9 00000017
As mentioned, both 02 00
and 06 00
are folders.
The difference is that 02 00
is a regular folder of a
square shape. Any codes in it can be enabled in any combination.
06 00
indicates that the folder is a radio folder. This
means that only one code in that folder can be on at once. In a codelist
XML file, this is indicated allowedon
such as:
<folder>
<name>Radio Folder</name>
<allowedon>1</allowedon>
...
</folder>
The allowedon
value can only be set to 1. If set to a higher
value with force (via XML), it will still transfer to the DS with the same
flag set. So even if you set it to 2, 3, or higher, only 1 code will be
allowed to be active at once.
But what if we force it?
<folder>
<name>Radio Folder Always On</name>
<allowedon>1</allowedon>
<cheat>
<name>Cheat A (Always On)</name>
<codes>always_on AAAAAAAA AAAAAAAA</codes>
</cheat>
<cheat>
<name>Cheat B (Always On)</name>
<codes>always_on BBBBBBBB BBBBBBBB</codes>
</cheat>
<cheat>
<name>Cheat C (On by Default)</name>
<codes>on CCCCCCCC CCCCCCCC</codes>
</cheat>
<cheat>
<name>Cheat D (Normal)</name>
<codes>DDDDDDDD DDDDDDDD</codes>
</cheat>
</folder>
Hilariously, this works. So you can open up the folder and it will just have 3 of those cheats checked off, even though they shouldn't be:
If you try to enable or disable any of them, it will go back to default
behaviour, where it will only allow 1 code to be enabled at a time in the
folder. It bypasses the always_on
in favour of 1 code at a
time in this case.
And of course, nested folders don't work. My tools were written with nested folders in consideration though via recursion. This was from before I had actual hardware to test it with. If a recursive folder was forced via XML, it will not be recognised by the Code Manager, and will not be put into the cartridge. No binary of any cheats inside the nested folder exist in a ROM dump with a nested folder.
Deciphering Code Binary - Misc
This procedure goes on until we try to read a code and get a
00 00
. This indicates the end of the code binary segment. At
least that's how it looks. There is another way to tell. In the
ar_game_info_t
struct, there is an integer called
code_bytes_size
. If you exceed this, you know you're at the
end too. That header is really useful.
I was tempted to believe that the first 2 bits of the first byte would
be the type. This would make sense, considering that 01
/
11
(in hex) are both codes, and 02
/
06
are folders. Here's what I mean:
Codes:
0x0001 = 00000000 00000001 Normal Code
0x0011 = 00000000 00010001 Master Code
0x8001 = 10000000 00000001 Normal Code + On by Default
0x8009 = 10000000 00001001 Normal Code + Always On (Assumes "On by Default")
0x8011 = 10000000 00010001 Master Code + On by Default
0x8019 = 10000000 00011001 Master Code + Always On (Assumes "On by Default")
Folders:
0x0002 = 00000000 00000010 Normal Folder
0x0006 = 00000000 00000110 Radio Folder (Only 1 code active)
So the bits are the same. It's just that there's extra bits tacked on.
They could have extra meaning. Let's say that they do. If so, then we
can make the initial flag be like a AR_FLAG_TERMINATE = 0
,
AR_FLAG_CODE = 1
, AR_FLAG_FOLDER = 2
. Then
higher bit values can be modifiers that go on top of them. For instance,
AR_FLAG_MASTER = 0x10
and AR_FLAG_ONLYONE = 0x04
.
So just like in Part 2, I threw the Code Manager into IDA Pro and figured
out all of the possible ones the easy way. Thus,
typedef enum AR_FLAG_T {
AR_FLAG_TERMINATE = 0x0000, /* 0000 0000 0000 0000 */
AR_FLAG_CODE = 0x0001, /* 0000 0000 0000 0001 */
AR_FLAG_FOLDER = 0x0002, /* 0000 0000 0000 0010 */
AR_FLAG_ONLYONE = 0x0004, /* 0000 0000 0000 0100 */
AR_FLAG_ON_ALWAYS = 0x0008, /* 0000 0000 0000 1000 */
AR_FLAG_MASTER = 0x0010, /* 0000 0000 0001 0000 */
AR_FLAG_ON_DEFAULT = 0x8000 /* 1000 0000 0000 0000 */
} ar_flag_t;
ar_flag_t value;
/* To get a radio folder... 0x02 | 0x04 = 0x06 */
value = AR_FLAG_FOLDER | AR_FLAG_ONLYONE;
/* To get a master code... 0x01 | 0x10 = 0x11 */
value = AR_FLAG_CODE | AR_FLAG_MASTER;
/* To get a code on by default... 0x01 | 0x8000 = 0x8001 */
value = AR_FLAG_CODE | AR_FLAG_ON_DEFAULT;
/* To get a code on always... 0x01 | 0x8000 | 0x08 = 0x8009 */
value = AR_FLAG_CODE | AR_FLAG_ON_DEFAULT | AR_FLAG_ON_ALWAYS;
Flexibility is a C programmer's dream. Though I'm not sure if it's practical here. It's always going to be one of those values. It does make my code cleaner though, because then I only have to check for the first 2 bits of the flag. Originally, I had a switch statement that had 6 different cases to account for all of the combinations. So this does simplify it down, seriously.
String Position Data & String Data
Following the binary of codes are a series of incrementing 16-bit integers. These integers grow with every text string (cheat, folder, game name, notes) that is with the game. All it does is store the position of the beginning of each string in the next section, which is "String Data". As such, I have to put both of them into a single section to explain it properly.
I teach by example. So here's an example:
So the first 32 bytes are the string position data. Everything is the actual text. I was nice and put the text representation on the side just like in previous pictures. It will be easier to use that for position.
So let's make this as simple as possible. Every 2 bytes is a position.
So looking at 01 00
, considering string index values are
0-based, this means the first string starts at the 2nd byte
in the text section. The next 2 bytes says 06 00
. So the
next string is at 7th byte. Then the next one is 07 00
,
which means it's at the 8th byte. So let's highlight these three in
the diagram:
Let's put it into a single row.
Each string goes until a 00
byte is hit. This is because this
is how C-Strings work. Strings are
null-terminated,
and the 00
is the null terminator. Thus, a blank string is a
single byte, being 00
. This is why you see blanks, like at
06
and at 0D
. And it's why strings with actual
text have an extra byte at the end, also being 00
.
This is how the entirety of the string section goes. Each code, folder, and game name have 2 strings that go with them. This is to account for how codes can have notes alongside them. The first string is always the game's name. The next string is always blank, as a game doesn't have a note. Then the third string and fourth string will be the first cheat or folder with a binary representation in the code binary section above. It just goes from there.
This example does have a proper layout, by the way.
Excuse the extremely dramatic names. It's intentional. Anyways, "Radio" is a folder. It has no note. Then the next instance is a cheat, which is "Press SELECT to Annihilate". This is a code inside "Radio". It has no note. This would also be a good time to mention that the names are stored in reverse order. If you look at that example, everything is in reverse order. It starts with the "Radio" folder, which is the last item. And it recursively goes in and starts with "Press SELECT to Annihilate", which is the last code in that folder. Going further down, the "(M)" is the last code, being the first in the layout up above.
Japanese Text Encoding
Since I put the effort into flashing a Japanese firmware onto my cartridge, I'll write about the details of text encoding here. Whenever you are able to input text, you are given an extra keyboard to type text. This is a カタカナ (Katakana) keyboard.
This kind of came as a surprise at first. But then I looked into how the text was encoded. It's JIS X 0201. If you look at the table there, there is no way to represent ひらがな (Hiragana) or Kanji at all. Hence why they aren't giving the option to type it into the Action Replay code editor shown above.
Speaking of the code editor, you can see a comparison between the USA and Japanese versions of text entry down below. Use the dropdown boxes to configure which side shows what.
|
|
The Japanese keyboard has an error. It's missing 「
. It has
」
, as shown on the symbols page. The English keyboards are
identical. But the letter sprites are remade to be slightly bigger on the
Japanese release. The text at the top is, being
アタラシイゲームタイトルノニュウリョク, is 新しいゲームタイトルの入力
(Enter new game title). Other than the text getting translation, those
keyboard options, and how they are encoded on the cartridge, there isn't
that much difference between the two releases.
Action Replay in Japan (Pro Action Replay, Ez, MAX, MAX2, and MAX3)
In Japan, Action Replay is called プロアクションリプレイ (Pro Action Replay). They got some other models too, like the MAX2 and MAX3. You can find out about more of their releases here. There are links to the firmwares for those in Part 2. They have microSD card slots on them. Kind of makes me think of the Action Replay DSi. But, what if we flashed the firmware onto a regular Action Replay DS? My old hardware surely wouldn't work with it, right?
Well, I actually tried it. It does boot. But you lose access to the PC, which means no Code Manager. Additionally, if you try to add a game or a code, it will fail to proceed because it says "Insert Action Replay", as if the cartridge isn't inserted at all. So it's missing things and will not work properly. It boots, but that's about it. It can easily be flashed back with the trick mentioned in Part 2 with CFW or a Flashcart.
This doesn't stop me from comparing the versions I have booted with each other though.
Main Menu
|
|
|
|
There is an extra option on the screen for MAX2 and MAX3. This is もどる (戻る / Go back). The reason for this is because the there is a menu prior to this on the official carts, as shown in step 9 on this page [Archive]. The other option is マックスドライブ (MAX DRIVE), which is a way to back up your saves.
There is a text change in the sliding green/red part. The first one was 「ゲームカードに差し替えてください」 (Replace with a game card). This is actually because the ROM dump probably was of one of the versions of the Pro Action Replay that didn't have an extra cartridge slot on it. So it literally required you to swap out the cartridge for your game. The v1.62 version is from the Pro Action Replay MAX, which does have an extra slot, so you can insert your games. Hence, it says 「ゲームカードを挿入してください」 (Insert a game card). Lastly, the last part was changed from 「ください」 to 「下さい」 in MAX2 and MAX3. They are pronounced the same. The attention to detail here is impressive.
Game Card Inserted
...So let's do what it says and insert a game card. I'm thinking 流星のロックマン ドラゴン ("Mega Man Star Force: Dragon" in the USA). Thus,
|
|
|
|
Decided to throw 1.21 in here too. 「ゲームタイトルフメイ」 (ゲームタイトル不明 / Game Title Unknown) shows whenever the game isn't in the code list on the cartridge. This is unchanged between all 4 versions. But the font size is notably larger in 1.21. The start button has definitely changed though. From being square to being round. And the text changed from 「スタート」 (Start) to 「ゲームを始める」 (Start the game). It isn't showing in 1.21, probably because it wants a codelist for that game to be present before it boots, or something. Now I'm just guessing.
For some reason, screenshots of these are hard to find. Manuals on the archived Datel Japan page are a good source, obviously. So is the Amazon page for these products. Anyways, you may have noticed that MAX3 is a lot brighter. This might not be intentional, but that's just how that firmware is. It's like it tried to fade in from a white screen and it stopped midway. This carries over to every other menu, kind of. I tried booting up v3.02 as well. It has the same thing happen to it.
Game List Menu
The game lists look like this. Interestingly, the English text
<Add New Game>
is present.
|
|
|
Keyboard
The second keyboard's name changed from 「カタカナ」 (Katakana) to 「ニホンゴ」 (日本語 / Japanese). Additionally, 「エイスウ」 (英数 / Alphanumeric) got changed to 「エイゴ」 (英語 / English). I think that's a weird change. The first version is more accurate.
|
|
|
Options Menu
The options menus are interesting. All 3 versions all have different options menus.
|
|
|
To save you from trying to translate that yourself, here you go:
Raw Text | Proper | Translated |
---|---|---|
コードリストノセーブ | コードリストのセーブ | Save the codelist |
オートPCリンク | オートPCリンク | Auto PC Link |
コードリストヲカキダス | コードリストを書き出す | Export codelist |
コードリストヲヨミコム | コードリストを読み込む | Load codelist |
That's about all that I can do from an emulator, and even physical hardware. For the time being, I think I'll stick to having Japanese v1.50 on my physical cartridge. Because that one actually works with the Code Manager and has no problems. I'm pretty sure that Datel Japan's product is just the same thing as the USA's but with a different stock firmware installed on it, as well as distributing a Japanese version of the Code Manager, which uses the same font style you see in the v1.50 screenshots.
What's next?
You may have noticed that I didn't go into any details on how the Action Replay injects code into games. I guess the intent of this was to figure out one big problem. And that one problem is how the devices brick themselves. And to show a way to preserve your own data if you happen to have the right hardware. I guess I didn't really think about how the cheats actually work. It was fun to figure out how the codes are stored. That being said, writing Action Replay codes is very easy if you have documentation. So I'm sure the hardware is able to decode that and intercept game data. It's all just conditionals, memory writes, etc. But, that's for another time. Or for someone more bored than me.