Skip to content

Your first script

Let's draw something. By the end of this page you'll have a complete mod that paints a square on top of a running game — and you'll understand every line.

The whole program

munos
// A 32×32 solid block of palette index 1.
var block: image = image_literal(32, 32,
    "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB…")  // (truncated for the page)

// Index 0 is transparent; index 1 is bright magenta.
var palette: u32[] = [0x00000000, 0x00FF00FF]

event frame(frame_num: i32) {
    draw_image(block, palette, 40, 40)
}

That's it. No imports, no main, no configuration, and — note — no wrapper of any kind. Let's read it top to bottom.

No boilerplate

You might expect a "this is the client" declaration somewhere. There isn't one. A Munos script with no server section is a client-only program by default — there's always a player running it, so the whole file just is the client. You write your overlay at the top level and it runs.

The client { … } / server { … } blocks exist for multiplayer: when your mod also has a server half (relaying positions, holding shared state), the server-only code goes in a server { … } block and the client-only code in a client { … } block. A purely local overlay like this one needs neither. We'll meet them in Talking to the server.

Making an image

munos
var block: image = image_literal(32, 32, "AQEB…")

image is a built-in type: a 2D grid of palette indices. Each pixel is a small number (a u8) that says "use color #N from the palette" — not an RGB color directly. This is exactly how the original consoles worked, and it's what makes recoloring cheap (more on that in Drawing sprites).

image_literal(width, height, data) builds an image from base64-encoded pixel data, decoded when your script compiles. Here every pixel is the byte 1, so the whole 32×32 block is "color #1."

The palette

munos
var palette: u32[] = [0x00000000, 0x00FF00FF]

A palette is just an array of colors — u32[]. Each entry holds a 24-bit 0xRRGGBB color in its low bytes. Index by index:

  • palette[0] = 0x00000000 — slot 0. Palette index 0 is always transparent, no matter what color you put there. Pixels that reference it simply aren't drawn.
  • palette[1] = 0x00FF00FF — magenta (red FF, green 00, blue FF).

Our image is full of index-1 pixels, so it draws as a solid magenta square with nothing else.

Why does index 0 not draw?

Transparency in Munos is by palette slot, not by an alpha channel. Slot 0 is the see-through one. You'll see scripts write its color as 0x00000000 by convention, but any value there is still transparent.

The frame event

munos
event frame(frame_num: i32) {
    draw_image(block, palette, 40, 40)
}

event frame is the heartbeat of a client mod. The runtime calls it once per emulator frame — about 60 times a second for an NTSC NES game. The frame_num parameter counts up from 0, so it doubles as a clock.

Inside, we call draw_image(image, palette, x, y) to paint our block with its top-left corner at screen pixel (40, 40). We pass no conditions, so it draws every single frame and the square is rock-steady on screen.

draw_image has many more options — flipping, clipping, sub-regions — but (image, palette, x, y) is the form you'll use most. We'll meet the rest in Drawing sprites.

Running it

To try a mod locally, start the web player from the repository root:

sh
npm run dev -w @multinostalgia/web

That serves the player at http://localhost:5173/. Load a ROM, point the player at your script, and your magenta square appears over the game.

This square is a debugging tool

A fixed, unconditional block like this is the classic "is my draw path working at all?" probe. If you can see it, the emulator → draw_image → framebuffer chain is healthy, and any other invisibility (a ghost that won't show up, say) is a bug in your coordinates or your network logic — not in drawing itself.

What you learned

  • A script with no server section is a client-only program — no wrapper needed.
  • image is indexed pixels; a u32[] palette turns indices into colors.
  • Palette index 0 is transparent.
  • event frame(frame_num) runs once per frame; that's where you draw.
  • draw_image(img, pal, x, y) paints onto the screen.

Next

A static square is nice, but the point of a mod is to react to the game. Next we'll read the game's own memory to find out what's happening inside it.

Reading the game →

Part of the MultiNostalgia project.