MacroKeybindMod scripts are written in a small but capable language. This page is the complete guide to it: how source becomes instructions, the variable and expression systems, every control-flow construct, and the modern brace syntax that sits on top.
For the byte-level specification (regexes, the original engine's exact behaviour), see the DSL Reference. This page is the practical guide.
A macro is a single string bound to a trigger. It is a mix of literal chat text and embedded script blocks:
hello $${ log("hi") }$$ world
Compiling that produces three instructions: send hello , run log("hi"), send
world. There is no top-level grammar beyond "chat text, with $${ … }$$ islands of
script."
Compilation happens in two phases:
- Parameter substitution (compile time) —
$$codes like$$?(prompt) and$$<file>(include) are resolved before anything runs. See Parameters & Variables. - Compilation — the text is split into chat parts and script parts; script parts are parsed into a flat list of instructions.
%var% references, by contrast, are expanded at run time, each time an
instruction executes.
Inside a $${ … }$$ block, statements are separated by ;:
$${ log("one"); log("two"); log("three") }$$
In a standalone .txt script file, newlines separate statements too, so you can write
one per line without semicolons.
A statement is one of:
| Form | Example | Meaning |
|---|---|---|
| Action call | log("hi") |
invoke an action with arguments |
| Directive | break |
invoke an action with no arguments |
| Assignment | #x = 5 |
evaluate and store into a variable |
| Capture | &n = lcase("AB") |
run an action, store its result |
Comments use // and run to the end of the statement:
$${ // this whole line is ignored
log("real") }$$
Variables are typed by a sigil on their name:
| Sigil | Type | Default | Example |
|---|---|---|---|
# |
counter (integer) | 0 |
#count, #scores[3] |
& |
string | "" |
&name, &items[0] |
| (none) | flag (boolean) | false |
ready, automine |
@ (prefix) |
shared / global modifier | — | @#gold, @&motd |
A name is [@][#&]name[index]: an optional @ for shared scope, an optional type
sigil, a name starting with a letter (or ~), and an optional [n] array index.
- Local (no
@) — lives only for this macro's execution. The default. - Shared (
@prefix) — global, persists across macros (and, in the full mod, to disk).@#goldand#goldare different variables.
$${ #x := 1; @#x := 99;
log("local %#x%, shared %@#x%"); // local 1, shared 99
}$$
Any variable can be an array. name[3] is element 3; name[] is the whole-array
specifier used by foreach and the array actions:
$${
push(&list[], "a");
push(&list[], "b");
log("size %arraysize(&list[])%"); // wait — see "capture" below
}$$
Array operations: push (append), pop (remove last), put (first free slot),
arraysize, plus indexing with &list[0].
:=stores a literal string. The right-hand side is taken verbatim (quotes stripped).%var%references inside it are expanded lazily, when the value is later used.=evaluates an expression and stores the typed result.
$${
#total = 2 + 3 * 4; // expression → 14
&greeting := "hi %&name%"; // literal → expanded later
}$$
The stored value is coerced to the variable's type: assigning 42 to &s stores
"42"; assigning "5" to #n stores 5.
If the right-hand side of = is an action call, the action runs and its return value
is captured:
$${
&upper = ucase("hello"); // &upper = "HELLO"
#where = indexof("hello", "ll"); // #where = 2
&last = pop(&list[]); // removes + captures the last element
}$$
Expressions appear in if/while/until conditions, in = assignments, and in
calc(...). They produce a typed value (integer, string or boolean).
| Group | Operators |
|---|---|
| Arithmetic | + - * / % |
| Comparison | == (=) != < <= > >= |
| Logical | && ` |
| Grouping | ( … ) |
!!! info "Real operator precedence"
The original engine evaluated left-to-right with no arithmetic precedence, so
2+3*4 was (2+3)*4. MacroKeybindMod uses a proper precedence-climbing parser, so
2 + 3 * 4 == 14 as you'd expect. Parenthesise if you ever want the old grouping.
Precedence, lowest to highest: || → && → comparisons → + - → * / % → unary
! - → ( ).
A condition is true when: a boolean is true, an integer is non-zero, or a string is
"true" / parses to a non-zero number / is non-empty.
String literals use double quotes. Equality compares by value:
$${ if(&name == "bob"); log("hi bob"); endif }$$
Variables can be referenced bare in expressions (#count > 5) or via %count%
expansion — both resolve to the current value.
Control flow is reconstructed at run time from a single instruction pointer and an operator stack (max nesting depth 32). You don't need to know the mechanism — see Architecture if you're curious — but the constructs behave exactly as you'd expect.
$${
if(#hp < 5);
log("danger");
elseif(#hp < 10);
log("careful");
else;
log("fine");
endif;
}$$
The first matching branch runs; the rest are skipped. elseif with no argument acts
like else.
Runs forever until a break:
$${ do; key(attack); if(stop); break; endif; loop }$$
The body runs at least once; the condition is checked at the bottom:
$${ #i := 0; do; inc(#i); log("%#i%"); while(#i < 3) }$$ // 1, 2, 3
until(cond) is the negation — loop until the condition becomes true.
for(var, start, end [, step]) is inclusive and may run zero times:
$${ for(#i, 1, 5); log("%#i%"); next }$$ // 1 2 3 4 5
$${ for(#i, 10, 1, -1); log("%#i%"); next }$$ // 10 9 … 1
$${
push(&friends[], "alice"); push(&friends[], "bob");
foreach(&f, &friends[]); log("hi %&f%"); next;
}$$
Exits the innermost loop. A break inside a false branch does nothing (it is gated by
the conditional state).
Lifts the per-tick instruction throttle for a tight section. Use sparingly.
Everything above is the legacy MKB syntax. MacroKeybindMod also ships a modern brace syntax that transpiles to the legacy form, so the two are fully interoperable and run on the same VM.
| Modern | Transpiles to |
|---|---|
if cond { … } |
if(cond); … ; endif |
} elseif cond { / } else if cond { |
elseif(cond); |
} else { |
else; |
while cond { … } |
pre-check loop (do; if(!(cond)); break; endif; …; loop) |
repeat N { … } |
for(#__loop, 1, N); … ; next |
forever { … } |
do; … ; loop |
foreach v in arr { … } |
foreach(v, arr); … ; next |
name(args) / var = expr |
unchanged — same statements |
!!! note "while semantics differ by syntax"
Legacy do … while(c) is post-check (runs once, then tests). Modern
while c { … } is pre-check (tests first, may run zero times). Pick the one
that matches your intent.
A complete modern example:
#i = 0
while #i < 3 {
#i = #i + 1
log("tick %#i%")
}
foreach &f in &friends[] {
if &f == "bob" {
log("found bob")
}
}
$${
set(fishing);
#casts := 0;
do;
key(use); // cast
// (real version waits for a bite event here)
inc(#casts);
log("cast #%#casts%");
if(#casts >= 64); unset(fishing); endif;
while(fishing);
log("done after %#casts% casts");
}$$
Read on: Parameters & Variables covers the $$/%var% systems in
full, and the Catalog lists every action.