This document explains how pointers work in Metroid and how to convert from a pointer value to a ROM offset and vice-versa.
An NES pointer is usually written as a four-digit hex number preceded by a dollar sign, e.g. $ABCD. NES pointers are little-endian. A pointer with the value $1234 would appear as 34 12 in a hex editor.
The table below lists the memory addresses a pointer may refer to in Metroid.
$0000 - $00FF RAM (zero-page) $0100 - $01FF RAM (stack) $0200 - $07FF RAM - $2000 - $401F Hardware Registers - $8000 - $BFFF Swappable ROM bank $C000 - $FFFF Fixed ROM bank
Zero page is a special section of memory that can be accessed with 8-bit pointers. The reason for this is that instructions that use 8-bit pointers execute faster and take less space in the ROM. This section of memory is often referred to with a two-digit value, such as $76, rather than a four-digit value like $0076, but $76 and $0076 refer to the same memory location.
Metroid is divided into banks of 16 KB. The original game contains 8 banks of data, numbered 0 through 7. An expanded ROM contains 16 banks of data, numbered 0 through F. Metroid always has two banks loaded at a time. One bank is loaded at $8000-$BFFF and another is loaded at $C000-$FFFF.
This table shows what each bank is used for in an unexpanded ROM
0 - Title Screen, Password Entry, Game Over, Ending 1 - Brinstar 2 - Norfair 3 - Tourian 4 - Kraid's Hideout 5 - Ridley's Hideout 6 - Graphics 7 - Game Engine
This table shows what each bank is used for in an expanded ROM
0 - Title Screen, Password Entry, Game Over, Ending 1 - Brinstar 2 - Norfair 3 - Tourian 4 - Kraid's Hideout 5 - Ridley's Hideout 6 - ???? 7 - ???? 8 - Brinstar screen data 9 - Norfair screen data A - Tourian screen data B - Kraid screen data C - Ridley screen data D - Not used E - Used by Editroid F - Game Engine
The bank at $8000-$BFFF is swappable. Metroid stores the data for each level in a different bank, and keeps the appropriate bank loaded for the level the player is in. So, for instance, when the player is in Brinstar, bank 1 will always be accessible at $8000-$BFFF.
The bank at $C000-$FFFF is fixed. The last bank in the ROM is always loaded here. This is where Metroid stores data and code common to every level (i.e. the game engine). In the original game, bank 7 will always be loaded here. In an expanded ROM, bank F will always be loaded here.
ROM offsets (the location of a piece of data in the ROM file) are usually represented in hex. While a pointer is usually written as $1234, a ROM offset can be written $1234, 0x1234, or for bonus confusion, sometimes just 1234. In this article, ROM offsets will always use the 0x representation.
An NES ROM contains a sixteen ($10) byte header which is followed by all the game's banks. Metroid uses 16 KB ($4000 byte) banks, so the first bank starts at 0x10, the second bank starts an 0x4010, the third bank starts at 0x8010, and so on. The table below shows the offset of all the banks.
0 - 0x10 1 - 0x4010 2 - 0x8010 3 - 0xC010 4 - 0x10010 5 - 0x14010 6 - 0x18010 7 - 0x1C010 8 - 0x20010 9 - 0x24010 A - 0x28010 B - 0x2C010 C - 0x30010 D - 0x34010 E - 0x38010 F - 0x3C010
To convert a pointer to a ROM offset, subtract the bank's base address from the pointer, and add the ROM offset of the bank. The base address is $8000 for pointers between $8000 and $BFFF. The base address is $C000 for pointers between $C000 and $FFFF.
Rom Offset = Pointer - Base Address + Bank Offset
For example, a pointer with the value of $ABCD falls between $8000 and $BFFF, so base address is $8000. Assuming this is a pointer for Brinstar, we would be working with bank 1, which is at 0x4010. The pointer $ABCD would point to 0x6BDD in the ROM.
Rom Offset = Pointer - Base Address + Bank Offset = ABCD - 8000 + 4010 = 6BDD
If the pointer were for Norfair instead, we would be using Bank 2 at 0x8010. The pointer would point to 0xABDD in the ROM.
Rom Offset = Pointer - Base Address + Bank Offset = ABCD - 8000 + 8010 = ABDD
A pointer with the value of $DEAD falls between $C000 and $FFFF, which is the fixed bank. The base address of the fixed bank is $C000. Pointers between $C000 and $FFFF will always point to the last bank. In an unexpanded ROM this is bank 7, located at 0x1C010 in the ROM, so $DEAD would point to 0x1DEBD.
Rom Offset = Pointer - Base Address + Bank Offset = DEAD - C000 + 1C010 = 1DEBD
In an expanded ROM, the last bank is bank F, which is located at 0x3C010, so $DEAD would point to 0x3DEBD in the ROM.
Rom Offset = Pointer - Base Address + Bank Offset = DEAD - C000 + 3C010 = 3DEBD
With some practice, the conversions can be done in your head, especially when it comes to the fixed bank.
A pointer with a value of less than $8000 points to RAM or a hardware register, and can't be mapped to a ROM offset.
To convert a ROM offset to a pointer, simply reverse the process above.
Pointer = Rom Offset - Bank Offset + Base Address
Suppose you put some new data at 0x8775 in the Norfair bank and want to change a pointer to point to this data. The bank offset is 0x8010. This bank would be loaded as the swappable bank, so the base address is $8000.
Pointer = Rom Offset - Bank Offset + Base Address = 8775 - 8010 + 8000 = 8765
To reference the data at 0x8775 in the ROM, we would use the pointer $8765.
Suppose you need a pointer to $1F01D (in an unexpanded ROM). This falls within the game engine bank, which is loaded as the fixed bank at $C000-$FFFF, so the base address is $C000.
Pointer = Rom Offset - Bank Offset + Base Address = 1F01D - 1C010 + C000 = F00D
A pointer by itself doesn't tell you which bank it refers to. $B00B could refer to any bank, and you have to guess based on the circumstances. To make things clearer, a pointer can be paired with a bank number using the format BB:AAAA (BB = bank, AAAA = address). 01:B00B refers to the address $B00B and bank 1. You still convert this pointer to and from a ROM offset the same way.
Rom Offset = Pointer - Base Address + Bank Offset = B00B - 8000 + 4010 = 701B