💾 Archived View for tilde.pink › ~nagi › nes-utilities.gmi captured on 2024-02-05 at 10:20:02. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-11-04)
-=-=-=-=-=-=-
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.
https://github.com/theobori/nes-utils-cli
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