-- Leo's gemini proxy

-- Connecting to tilde.pink:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini;

NES Library

2022-09-22

I wanted to learn more about how the NES works, so I consulted the resources on

Nesdev Wiki

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.


CLI


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


Tilesets dumping


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:


Colors


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]
}

Bytes to RGB


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;
    }
}

Disassembling


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
...

Game Genie code


*The Game Genie is a enhancement cart for the NES designed by Camerica and distributed by Galoob and Cameric*

*it provides a simple interface to enter up to three cheat codes*


*Source:

NesDev Wiki


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

Links


https://github.com/theobori/nes-utils

-- Response ended

-- Page fetched on Sun May 19 15:41:26 2024