Skip to content

Reading the game

A mod becomes interesting the moment it knows what's happening inside the game. In Munos you find that out by reading the console's memory — the same RAM the game itself reads and writes every frame.

Memory is an address space

A retro console keeps its live state — player position, score, lives, which enemies are alive — at fixed numeric addresses in RAM. If you know that Super Mario Bros. stores Mario's on-screen X position at address 0x0086, you can read it:

munos
var mario_x = read_u8(0x0086)

read_u8 reads one unsigned byte at the given address. The result is a u8 (0–255).

Naming your addresses

Magic numbers scattered through code are hard to read. The convention is to name every address as a const at the top of the file:

munos
const PLAYER_X: u32 = 0x0086   // Mario's screen X
const PLAYER_Y: u32 = 0x00CE   // Mario's screen Y
const LIVES:    u32 = 0x075A   // remaining lives

Addresses are u32 — wide enough to point anywhere in any console's address space. A const can't be reassigned, which is exactly what you want for a fixed hardware address.

Now the reads explain themselves:

munos
event frame(frame_num: i32) {
    var x = read_u8(PLAYER_X)
    var y = read_u8(PLAYER_Y)
    var lives = read_u8(LIVES)
    print("Mario is at ", x, ",", y, " with ", lives, " lives")
}

Where do the addresses come from?

Memory maps are game- and even region-specific. Modders find them in community RAM maps (the Data Crystal wiki is a common source), in disassemblies, or by watching memory change as they play. Each script keeps its own table of the addresses it cares about.

Reading wider values

A single byte only holds 0–255. Many values — a 16-bit X coordinate, a score — span several bytes. Munos has a read builtin for each width and signedness:

munos
read_u8(addr)    read_u16(addr)   read_u32(addr)   // unsigned
read_i8(addr)    read_i16(addr)   read_i32(addr)   // signed

Multi-byte reads use the console's native byte order, so you don't have to assemble bytes yourself:

munos
const SCORE: u32 = 0x07DD
var score = read_u16(SCORE)   // both bytes, correctly ordered

Pick the width that matches how the game stores the value, and the signedness that matches what it means (a velocity that can go negative wants read_i8).

Integer types, briefly

Every value in Munos has a fixed-width integer type:

TypeBitsRange
u880 … 255
u16160 … 65 535
u32320 … ~4.3 billion
i88−128 … 127
i1616−32 768 … 32 767
i3232~−2.1 … 2.1 billion

There are no floats — integers only. And there's a rule that trips up newcomers: Munos never converts between types automatically. You can't add a u8 to an i32 directly. When you need a value in a different type, you cast explicitly:

munos
var raw: u8  = read_u8(PLAYER_X)   // a u8
var x:   i32 = i32(raw)            // cast it to i32 to do i32 math

The cast functions are named after their target type: i8(x), i16(x), i32(x), u8(x), u16(x), u32(x). You'll cast often — most reads come back as u8, but most arithmetic and most builtins want i32. That's normal Munos style, not a smell.

Writing memory, too

The same family exists for writing. Writing game memory lets a mod change the game live — a randomizer, a difficulty tweak, an infinite-lives cheat:

munos
write_u8(LIVES, 99)   // set lives to 99

There's a write_* for each width: write_u8/u16/u32 and write_i8/i16/i32. Use writes with care — you're reaching into the running game's state.

Putting it together

Here's a mod that watches the player's position and prints it only when it changes, using a shared variable to remember the last value:

munos
const PLAYER_X: u32 = 0x0086

var last_x: i32 = -1

event frame(frame_num: i32) {
    var x = i32(read_u8(PLAYER_X))
    if x != last_x {
        print("x changed to ", x)
        last_x = x
    }
}

Note last_x is declared at the top level but outside the event — so it persists across frames. A var inside the event body would reset every frame.

What you learned

  • Game state lives at fixed addresses; read_u8(addr) reads a byte.
  • Name addresses as const … : u32 at the top of the file.
  • There's a read/write builtin per width and sign: read_u8read_i32, write_u8write_i32.
  • Munos is integer-only and never auto-converts; cast with i32(x) etc.
  • A var outside an event persists; one inside resets each call.

Next

Now you can find out where the player is. Let's draw something meaningful there.

Drawing sprites →

Part of the MultiNostalgia project.