💾 Archived View for tilde.pink › ~nagi › posts › nes-utilities.gmi captured on 2023-07-22 at 17:30:59. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-06-14)
-=-=-=-=-=-=-
By Théo Bori, edited 2022-09-22
I wanted to learn more about how the NES works, so I consulted the resources on
and I discovered that from an NES program (.nes file) you could do cool stuff like extracting some tilesets from the **CHR ROM** (**Character Read-Only Memory**) or a **PRNG** (**Pseudorandom Number Generator**) with the instructions interpreted by the NES.
I made an example binary that can run all the main features of the nes-utils library to make it more convenient to test.
A NES program include a **16 bytes** header, we can represent it like this:
const NES_HEADER_FIELDS_ORDER: [(&str, usize, usize); 9] = { [ ("magic", 0, 4), ("len_prg_rom", 4, 1), ("len_chr_rom", 5, 1), ("f6", 6, 1), ("f7", 7, 1), ("len_prg_ram", 8, 1), ("f9", 9, 1), ("f10", 10, 1), ("reserved", 11, 5) ] };
In this example, the field **magic** is at position **0** and has a lenght of **4** bytes.
The **CHR ROM** is linked to the **PPU** (**Picture Process Unit**), it means if the **CHR ROM** length is superior to zero, it contains some graphics.
There are not all the tilesets of the game in the CHR, you can find 0x2000 * len_chr_rom bytes in it, with two **banks** of 0x1000 bytes, which makes two **images** of 8 kilobytes, there is one bank for one image.
So, let's take the example of **Kirby's Adventure**, below are the graphical data of the CHR rom:
There are only four colors because the rest is calculated at runtime. Below are the main parts of code that generate this 2 images:
type Rgb = (u8, u8, u8); const BLACK_PIXEL: Rgb = (0, 0, 0); const COLOR_SCHEME: [Rgb; 4] = [ (0, 0, 0), (126, 126, 126), (189, 189, 189), (255, 255, 255) ]; fn bits_to_rgb(left: u8, right: u8) -> Rgb { let color = right << 1 | left; COLOR_SCHEME[color as usize] }
pub fn fill_with_bank(&mut self, bank: &[u8]) { let mut mem_x = 0; let mut mem_y = 0; for byte in (0..bank.len()).step_by(16) { for y in 0..8 { if mem_x >= NesImage::W { mem_y += NesImage::TILE_H; mem_x = 0; } let lower = bank[byte + y]; let upper = bank[byte + y + 8]; for bit in 0..8 { let pixel = bits_to_rgb( lower >> (7 - bit) & 1, upper >> (7 - bit) & 1 ); self.put_pixel(bit + mem_x, y + mem_y, pixel); } } mem_x += NesImage::TILE_W; } }
The bytecodes are in the **PGR ROM** (**Program Read-Only Memory**) of size 0x4000 * len_chr_rom (value in the header) bytes.
So, for **Kirby's Adventure**, the assembler code should looks like:
; Mapped registers SQ1_VOL equ $4000 SQ1_SWEEP equ $4001 PPUMASK equ $2001 SQ1_LO equ $4002 SQ1_HI equ $4003 SQ2_SWEEP equ $4005 ... ; Header hex 4e 45 53 1a hex 20 hex 20 hex 43 hex 00 hex 00 hex 00 hex 00 hex 00 00 00 00 00 ; PRG ROM and ($0f, x) ; 21 0f slo $280f ; 0f 0f 28 slo $2121 ; 0f 21 21 jsr $2020 ; 20 20 20 jsr $0221 ; 20 21 02 slo $200f ; 0f 0f 20 and ($0f, x) ; 21 0f ...
Obviously I didn't buy the enhancement cart, so I can't use the available codes. But on some emulators it is possible to access the memory and inject values.
So I made a feature that decodes the **Game Genie** codes.
For example for **Kirby's Adventure**, it is possible to have infinite energy with the following code:
$ nes-utils-cli --code SZEPSVSE Address 0x1e05 Value 0xad Compare value 0x8d