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) {
// ...
}
intsis an array of the integer arguments.textsis 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)readnbytes,write(blob_or_string)writes bytes and returns the count, andseek(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
nessembleprocess 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 ofwidth * height * 4integers, four per pixel inR, G, B, Aorder, 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.