Register - Login
Views: 99794689
Main - Memberlist - Active users - Calendar - Wiki - IRC Chat - Online users
Ranks - Rules/FAQ - Stats - Latest Posts - Color Chart - Smilies
05-03-22 05:17:29 AM
Jul - General Game/ROM Hacking - F-Zero Maximum Velocity - Once More With Feeling! New poll - New thread - New reply
Next newer thread | Next older thread
GuyPerfect
Catgirl
Level: 68


Posts: 819/1096
EXP: 2665654
For next: 63146

Since: 07-23-07


Since last post: 1.7 years
Last activity: 219 days

Posted on 09-17-13 08:50:31 PM (last edited by GuyPerfect at 09-22-13 09:49:25 PM) Link | Quote
I'm working on a project right now where I needed to extract some information from F-Zero Maximum Velocity. A few select individuals might remember that I previously hacked the game in 2005. Well, after looking over some old Acmlm archives, and doing a fair amount of new reverse engineering, I've got the goods. The following are the goods. These goods apply to the North American/PAL version of the game, which has product code AFZE. Most of it still applies to the original Japanese release AFZJ though (just not the ROM offsets).

A reminder that GBA cartridge ROM is mapped to the CPU memory bus starting at address 0x08000000. So when reading a pointer from the ROM, you need to subtract that to yield the actual location in the file.

Also, a reminder that GBA is little-endian.



Course Data

There are 22 courses in the game, and they're indexed 0 to 21. Handy!

0x2C2F20 - Course Info Table A
The primary course info table contains 22 entries, each 28 bytes in size. The entries have the following format:

u16 Unknown
u16 Unknown
u32 Address of Path Coordinates
u32 Address of Path Definition
u32 Unknown address
u32 Address of Map Graphics
u8 Track Data Index
u8 Unknown
u16 Course Info Table B Index
u16 Unknown
u16 Unknown

The order of courses in this table is as follows:

0 - Knight 1
1 - Knight 2
2 - Queen 4
3 - Pawn 5
4 - Bishop 4
5 - Bishop 2
6 - Bishop 1
7 - Queen 1
8 - Pawn 2
9 - Knight 3
10 - Bishop 3
11 - Pawn 1
12 - Pawn 3
13 - Queen 3
14 - Pawn 4
15 - Knight 4
16 - Queen 2
17 - Queen 5
18 - Knight 5
19 - Bishop 5
20 - Championship
21 - Single-Pak

Path Coordinates, Path Definition
There are two paths for every course, and you can switch between them freely. They define the direction in which the course is intended to proceed, as well as give coordinates for the CPU opponents to follow. All of the coordinates are defined in a big chunk (Path Coordinates), and then individual coordinates are referenced by parallel lists of indexes into that chunk (Path Definition).

Path Coordinates has the following format:

s16 X Coordinate
s16 Y Coordinate

Both coordinates are actually bit-packed fields of the following format:

uuuuuucc cccccccc
15 8 7 0

u - Unknown
c - Multiply by 4 to yield the coordinate, in pixels

Path Definition has the following format:

u8 Primary Path Index
u8 Secondary Path Index

Both indexes are relative to the start of Path Coordinates. For any given Index, multiply it by 4 to yield the offset within Path Coordinates where the coordinates are located.

The first entry in Path Definition is the location of the starting line. The path cycles back to this first entry when an Index of 0 is read from the data. Because of this, the first 4 bytes in Path Coordinates are always zero and are never read: the first Index used by the game is always 1.

Map Graphics
These are 64 standard-issue 4-bit GBA character units. Characters are 8x8 pixels, and the first byte represents the leftmost two pixels of the top row (low bits first). Palette-wise, the map graphics only ever have the following values:

0 = Transparent
8 = Green
15 = Black

Maps are always 64 characters in size, and are arranged as 8x8 as well (for a total of 64x64 pixels). Here's a sample of what one looks like:



0x360AAC - Track Data
This is a list of 22 track data pointers (of type u32). These are the addresses within the ROM where the actual track data is located. The value in Track Info Table A is an index into this list. Multiply the Index by 4 to yield the offset relative to the start of the pointer table.

For the format of the track data itself, scroll down to the Compression Format section of this post.

0x2B4EC4 - Course Info Table B
The secondary course info table contains entries that are 76 bytes in size. The value in Track Info Table A is an index into this list. Multiply the Index by 76 to yield the offset relative tot he start of this info table. The entries have the following format:

Str*32 Name
u32 Address of Music
u32 Address of Track Graphics
u32 Address of Track Palette
u32 Address of Horizon Graphics
u32 Address of Horizon Back Layer
u32 Address of Horizon Front Layer
u32 Address of Horizon Palette
u32 Address of Floor Graphics
u32 Address of Floor Palette
u32 Address of Floor Mosaic
s8 Index of Floor Layout
u8 Unknown
u16 Unknown

Name
An ASCII name for the course, which is used to display the course name in game. Course names less than 32 characters in size are padded with zeroes. As these are ASCII, the Track Info Table B can pretty easily be found by searching the file for "ANCIENT MESA"

Music
An AGB MP2000/M4A audio track. I don't know the details of the format, but it's all over the place out there. Just do a web search for "gba sappy"

Track Graphics, Horizon Graphics, Floor Graphics
These are all standard GBA character units. The formats and number of tiles for each data element are as follows:

Track: 8bpp, 256 characters
Horizon: 4bpp, 240 characters
Floor: 8bpp, 192 characters

Track Palette, Horizon Palette, Floor Palette
These are all standard GBA palette entry definitions. Palette entries are 16 bits with the following format:

ubbbbbgg gggrrrrr
15 8 7 0

u - Unused
b - Blue intensity (0-31)
g - Green intensity (0-31)
r - Red intensity (0-31)

The base color indexes to load into, as well as the number of colors to load, are as follows:

Track: 128 entries starting at 0x00
Horizon: 128 entries starting at 0x80
Floor: 256 entries starting at 0x00

As you can see, the floor palette shares the same indexes as the track and horizon palettes. The game cleverly dances around the issue to produce a 512-color image.

Horizon Layers
Horizons are two-layer images that scroll left and right depending on what direction the player is facing in the game. They are some number of characters wide and 4 characters tall. The background layer is 96 characters wide and the foreground layer is 128 characters wide.

The data that defines the layers are simple 96x4- and 128x4-entry lists, starting with the leftmost character of the top row, and proceeding to the right for each row. Entries are standard GBA 4-bit BG cell entries and have the following format:

ppppvhcc cccccccc
15 8 7 0

p - Palette index (or, if you prefer, the upper 4 bits of color indexes)
v - If set, the character is flipped vertically
h - If set, the character is flipped horizontally
c - The character index

It's important to note that at runtime, the horizon characters begin at index 0x0180 relative to their background's character base, so that amount should be subtracted from c to yield the offset relative to the first byte of horizon character data.

Here's a sample of what one set of horizon layers looks like:




0x3BB20C - Floor Mosaic, Floor Layout
Some courses have full character maps for "floors", the graphics off to the sides of the track, and other courses have a repeating pattern that may or may not be animated. These are mutually exclusive, and whichever one is not in use will be stored as NULL in the ROM data.

When a repeating mosaic is present, the data represents the character indexes for a 16x16-character image. Depending on the level, the image might scroll horizontally beneath the track. When a mosaic is in use, up to 256 characters can be used, rather than the 192 used by all maps.

When a layout map is present, it represents a signed index relative to 0x3BB20C. Multiply the Index by 4 to yield the location of the layout address (of type u32). The layout is stored in the same format as the Track Data from Track Info Table A. For the format, scroll down to the Compression Format section of this post.



Compression Format

Tracks and floor layouts are both stored as 512x512-character images, for a total of 4096x4096 pixels each. Data is stored in an orders-of-magnitude hierarchy:

• A number of 8x8-pixel characters are loaded into character memory.
• A number of 8x8-character "panels" are stored in the ROM data.
• A BG map is a 64x64-panel arrangement.

Panels are stored as 64 bytes, each a character index. The first 8 bytes of each panel represent the top row, starting on the left, and so-on for each group of 8 pixels for each subsequent row.

Similarly, the BG map is stored as 4096 16-bit indexes referencing panels. The first 64 indexes are the top row, starting on the left, etc.

Track panels are located at 0x1D43FC, and there are 3,070 of them.
Floor panels are located at 0x2815B4, and there are 3,297 of them.

Note - The X and Y "pixel coordinates" from the pathing information refer to pixels in the final 4096x4096 map, relative to the top-left corner.

The 16-bit indexes of the panel arrangement are compressed. To unpack the data:
  • Initialize a 16-bit LastOut variable to 0. Every time a value is outputted, LastOut inherits its value.
  • As long as fewer than 4096 indexes have been outputted...
    • Read 16 bits as a bit packed value: (15) ffffcccc cccccccc (0)
      • f - Function
      • c - Count
    • Process input according to the value of Function...
      • Function = 0: Read (Count) 16-bit values from input and write them as output.
      • Function = 1 and Count = 0: Read a 16-bit value into Count, then read (Count + 1) values from input and write as output.
      • Function = 1 and Count != 0: Read (Count) signed 8-bit values, adding them to LastOut each time, then writing LastOut as output for each one. If an odd number of bytes was read, read one more dummy byte.
      • Function = 4: Increment LastOut by 1 (Count + 1) times, writing LastOut as output each time.
      • Function = 8: Read a 16-bit value into LastOut, then write LastOut as output (Count + 1) times.
      • Function = 12: Reset LastOut to 0, then write LastOut as output (Count + 1) times.
No other values for Function are used in the game, so I don't know if they have any behavior associated with them.



Vermillion Code

The game has 10 racers, though only four are available at the start. You unlock additional machines by completing Grand Prix under various circumsances. The tenth machine, Jet Vermillion, is unlocked by completing every series (Pawn, Knight, Bishop and Queen) with all nine of the other machines, each on the hardest difficulty Master. Alternately, you can complete the Championship circuit 255 times. Either way, you've got a lot of racing ahead of you.

Due to the nature of the machine, most players will never see it. So Nintendo implemented a secret code to unlock it: on the machine select screen in Grand Prix mode, press L, R, Start, R, L, Select. This brings up a password screen, and doesn't say what it's for. If you enter the correct password, you unlock the Jet Vermillion early.

The password is officially known as the Vermillion Code, and it was only distributed in Japan for a time. The code is dependent on the name of the save file. On the Japanese website, you could enter your player name and get the corresponding Vermillion Code. Internationally, only the code for the name YAZOO JR (the name of the Jet Vermillion's pilot) was distributed, along with instructions to start a new save file with that name.

For whatever reason, the game decrypts the Vermillion Code to produce the corresponding player name, which it then checks against the name of the save file. This makes the game's programming less than helpful for producing the code in the first place. So I spent hours and hours in front of that Japanese web form, analyzing it until I finally found a simple algorithm to produce a Vermillion Code given the name of a save file.

Player names can be anywhere from 1 to 8 characters in length. There are 46 valid characters for player names, as follows (including the space character):

!&',-./0123456789?ABCDEFGHIJKLMNOPQRSTUVWXYZ_

Vermillion Codes contain characters from a different set of 32, as follows:

0123456789A?CEFHJKLMNPQRTVWXY=-+

Each character in the Vermillion Code corresponds with a 5-bit numerical value, in the order listed above. For example, "0" represents value 0, "?" represents value 11, and "+" represents value 31.

Vermillion Codes are 12 characters long, but only the first 10 are generated algorithmically. The initial value of the first 10 correspond with the characters for the code "9+7N-6V9A-" and have the following hexadecimal values:

09 1F 07 14 1E 06 19 09 0A 1E

To calculate the Vermillion Code, the player name and code characters are both processed as numbers. Let each of the 8 characters of the player name be indexed left-to-right, from 0 to 7. Let the 10 characters of the Vermillion Code be 0 to 9. Lastly, let binary digits be numbered with the least significant bit being bit 0.

For each character in the player name, take the ASCII value of the character and subtract 32. This will yield a 6-bit number in the range of 0 for " " and 63 for "_". For each bit in that value, zeroes do not modify the Vermillion Code, but ones do. Each bit of each character in the player name can only modify one character in the Vermillion Code by a certain amount.

The characters modified and the amount by which they are modified are listed in the table below. The "In" column represents the character and bit from the player name (that is, "0.3" means character 0, bit 3). The "Char" column represents the index of the character in the Vermillion Code that will be modified if the bit is a one. The "Mod" column denotes the amount by which to change the character in the Vermillion Code for that bit. As the Vermillion Code characters are 5-bit, they can only contain values from 0 to 31.

In Char Mod In Char Mod In Char Mod In Char Mod
0.0 6 - 1 1.0 2 + 8 2.0 3 - 4 3.0 4 - 2
0.1 5 + 1 1.1 2 +16 2.1 3 + 8 3.1 4 - 4
0.2 4 + 1 1.2 0 +16 2.2 3 -16 3.2 4 - 8
0.3 3 + 1 1.3 9 + 1 2.3 0 - 8 3.3 4 -16
0.4 2 - 1 1.4 8 + 1 2.4 2 - 2 3.4 0 + 4
0.5 1 - 1 1.5 7 - 1 2.5 2 - 4 3.5 3 + 2

In Char Mod In Char Mod In Char Mod In Char Mod
4.0 0 - 1 5.0 7 +16 6.0 8 - 8 7.0 9 - 4
4.1 5 - 2 5.1 1 -16 6.1 8 +16 7.1 9 - 8
4.2 5 - 4 5.2 6 + 2 6.2 1 - 8 7.2 9 -16
4.3 5 + 8 5.3 6 + 4 6.3 7 + 2 7.3 1 - 4
4.4 5 +16 5.4 6 - 8 6.4 7 + 4 7.4 8 - 2
4.5 0 + 2 5.5 6 -16 6.5 7 - 8 7.5 8 + 4

After all characters from the player name have been processed, the final Vermillion Code string can be constructed using the 32-character alphabet from above. Lastly, all Vermillion Codes end with "30", which needs to be appended after calculation is complete.

To help illustrate the algorithm, let's put it against the player name NINTENDO:

Char Text ASCII Minus 32 Modifications
0 N 01001110 101110 - 1 + 1 + 1 + 1
1 I 01001001 101001 + 8 - 1 + 1
2 N 01001110 101110 - 8 - 4 - 8
3 T 01010100 110100 + 4 + 2 - 8
4 E 01000101 100101 + 1 - 4
5 N 01001110 101110 -16 -10
6 D 01000100 100100 - 8 - 8
7 O 01001111 101111 - 4 + 4 -28
-------------------------------------------------
Total Modifications: - 3 -29 + 4 - 5 - 7 - 3 -10 - 9 + 4 -27

9 - 7 N - 6 V 9 A -
Password Seed: 0x09 0x1F 0x07 0x14 0x1E 0x06 0x19 0x09 0x0A 0x1E

6 2 ? H R 3 H 0 F 3
Modified Password: 0x06 0x02 0x0B 0x0F 0x17 0x03 0x0F 0x00 0x0E 0x03

Final concatenated password: 62?HR3H0F330

Only the first 11 characters of the password are validated: anything the user enters starting at and including the last "0" will have no effect on whether the Vermillion Code is accepted.

Additionally, here's a JavaScript function to do all the heavy lifting:

// Generate the Vermillion Code for a given player name
// Returns the code on success, a zero-length string on error

function VermillionCode(name) {

// Valid characters for player names
var valid =
" !&',-./0123456789?ABCDEFGHIJKLMNOPQRSTUVWXYZ_";

// The numeric-to-text mapping for the password characters
var passkey = "0123456789A?CEFHJKLMNPQRTVWXY=-+";

// Password register modifications by bit by name character
var bits = [
[[6, - 1], [5, + 1], [4, + 1], [3, + 1], [2, - 1], [1, - 1]],
[[2, + 8], [2, +16], [0, +16], [9, + 1], [8, + 1], [7, - 1]],
[[3, - 4], [3, + 8], [3, -16], [0, - 8], [2, - 2], [2, - 4]],
[[4, - 2], [4, - 4], [4, - 8], [4, -16], [0, + 4], [3, + 2]],
[[0, - 1], [5, - 2], [5, - 4], [5, + 8], [5, +16], [0, + 2]],
[[7, +16], [1, -16], [6, + 2], [6, + 4], [6, - 8], [6, -16]],
[[8, - 8], [8, +16], [1, - 8], [7, + 2], [7, + 4], [7, - 8]],
[[9, - 4], [9, - 8], [9, -16], [1, - 4], [8, - 2], [8, + 4]]
];

// Password value register; seeded with "9+7N-6V9A-"
var password =
[0x09, 0x1F, 0x07, 0x14, 0x1E, 0x06, 0x19, 0x09, 0x0A, 0x1E];

// Working variables
var x, a, b;

// Input validation
if (!name || name.length > 8 || !name.trim()) return "";
for (x = 0, name = name.toUpperCase(); x < name.length; x++)
if (valid.indexOf(name.charAt(x)) < 0) return "";

// Password register modifications
for (x = 0; x < name.length; x++)
for (a = name.charCodeAt(x) - 32, b = 0; b < 6; b++)
if (a & (1 << b))
password[bits[x][b][0]] += bits[x][b][1];

// Convert the password to a string and return it
for (x = 0, a = ""; x < 10; x++)
a += passkey.charAt(password[x] & 0x1F);
return a + "30";
}
Next newer thread | Next older thread
Jul - General Game/ROM Hacking - F-Zero Maximum Velocity - Once More With Feeling! New poll - New thread - New reply


Rusted Logic

Acmlmboard - commit 47be4dc [2021-08-23]
©2000-2022 Acmlm, Xkeeper, Kaito Sinclaire, et al.

26 database queries.
Query execution time: 0.088481 seconds
Script execution time: 0.026286 seconds
Total render time: 0.114767 seconds