Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Extending

nessemble can be extended with custom pseudo-instructions written in Rhai, a small, pure-Rust scripting language. Scripts can also read and write files (see Filesystem access), so run only scripts you trust.

Usage

Pass the --pseudo flag to point at a mapping file that associates each custom directive with a script.

Example pseudo.txt:

.foo = foo.rhai

Example example.asm:

.foo 1, 2, 3

To assemble:

nessemble example.asm --pseudo pseudo.txt

A script path in the mapping file is resolved relative to the mapping file's own directory, so a pseudo.txt and the scripts it names can live together and be pointed at from anywhere. Bundled scripts installed with nessemble scripts (into ~/.nessemble/scripts) are resolved via ~/.nessemble/scripts/scripts.txt and need no --pseudo flag.

Writing a script

A script defines a function named custom that receives the directive's arguments and returns the bytes to emit:

fn custom(ints, texts) {
    // ...
}
  • ints is an array of the integer arguments.
  • texts is an array of the string arguments (quotes already removed).
  • Return the emitted bytes as an array of integers (each taken & 0xFF), a blob, or a string (its bytes are emitted). Returning () emits nothing.

Example

A .product directive that multiplies its integer arguments:

fn custom(ints, texts) {
    let product = 0;
    let first = true;
    for i in ints {
        if first { product = i; first = false; } else { product *= i; }
    }
    [product % 256]
}
.product 1, 2, 3   ; emits a single byte: 6

String arguments

String arguments arrive (with quotes removed) in texts:

.foo "easeInQuad", 0, 16
fn custom(ints, texts) {
    let name = texts[0];   // "easeInQuad"
    // ...
}

Errors

Signal an error with throw. The thrown message becomes the assembler diagnostic:

fn custom(ints, texts) {
    if texts.is_empty() {
        throw "No arguments provided";
    }
    []
}

Filesystem access

Scripts can read and write files through the rhai-fs package, so a directive can pull bytes from disk instead of only computing them. The main entry point is open_file:

  • open_file(path, "r") opens a file for reading; open_file(path) opens it for reading and writing, creating or truncating it.
  • On the returned file handle: read_blob() / read_string() return the whole file, read_blob(n) / read_string(n) read n bytes, write(blob_or_string) writes bytes and returns the count, and seek(pos) moves the cursor.

Relative paths resolve against the source file's directory — the same base as .include and the .inc* importers — while absolute paths are used as-is.

A .embed "file" directive that emits a file's bytes verbatim:

fn custom(ints, texts) {
    open_file(texts[0], "r").read_blob()
}
.embed "logo.chr"   ; emits the raw bytes of logo.chr

Filesystem access is not sandboxed. A script can read or write any path the nessemble process can. Only run pseudo-op scripts you trust, as with any build tooling.

Decoding PNGs

decode_png(blob) decodes PNG bytes (typically from open_file(...).read_blob()) into a map of the image's dimensions and its pixels:

let img = decode_png(open_file("sprite.png", "r").read_blob());

The returned map has:

  • width — the image width in pixels (integer).
  • height — the image height in pixels (integer).
  • pixels — a flat array of width * height * 4 integers, four per pixel in R, G, B, A order, row-major. Pixel (x, y) starts at index (y * width + x) * 4.

decode_png throws if the blob is not a valid PNG. For example, a directive that emits a single tile's worth of a PNG's red channel:

fn custom(ints, texts) {
    let img = decode_png(open_file(texts[0], "r").read_blob());
    let out = [];
    for y in 0..8 {
        for x in 0..8 {
            out.push(img.pixels[(y * img.width + x) * 4]);   // red channel
        }
    }
    out
}

Bundled scripts

Running nessemble scripts installs the bundled scripts. The ease script emits an easing curve as bytes:

.ease "easeInQuad"

Supported easing types include easeInQuad, easeOutQuad, easeInOutQuad, and the cubic, quint, and bounce variants.