Skip to content

Syntax reference

The building blocks of MAST: variables, labels, flow control, tasks, and the rest. MAST evaluates expressions with Python, so Python types and expressions work inside MAST statements — but not identically; see Common gotchas.

Variables

Assign with name = value:

enemy_count = 20
name = "Artemis"

For values the MAST parser struggles with (nested lists, dict/set literals), wrap the expression in inline python — two or more tildes (~~) on each side:

inventory = ~~ [[2, 3], [4, 5]] ~~

Scope

Variables are scoped to the task by default. Two modifiers change that:

shared enemy_count = 20      # visible to every task in the story
default difficulty = 5       # set only if it isn't already defined

Reading is automatic — you only mark scope at assignment:

shared beer_count = 8
my_beer = beer_count         # read the shared value
shared beer_count = 0        # update the shared value

Labels

A label starts at column 0 with two or more = signs. The trailing = is optional, and the count doesn't matter:

== goto_bar ==
    ...
=== show_helm
    ...

Two labels are implicit and don't need defining: main (where every script starts) and END (ends the current task).

Labels are not functions — execution falls through from one into the next:

== one ==
    log("one")
== two ==
    log("two")
== three ==
    log("three")
one
two
three

Jumps and ending

jump label (or the shortcut -> label) redirects flow. ->END ends the task:

== start ==
    log("first")
    -> here

== skipped ==
    log("got here later")
    -> done

== here ==
    log("second")
    -> skipped

== done ==
    log("done")
    ->END
first
second
got here later
done

Ending the last remaining task ends the story.

Conditionals

MAST supports Python-style if / elif / else (and they can nest):

if value < 300:
    log("less")
elif value > 300:
    log("more")
else:
    log("equal")

...and match / case:

match value:
    case 200:
        log("200")
    case 300:
        log("300")
    case _:
        log("something else")

Conditions also work inline on many statements:

->END if obj is None
jump loop if not is_timer_finished(0, "warmup")

Loops

Standard for ... in, plus a MAST-specific for ... while that loops until a condition goes false. break and continue work as in Python:

for x in range(3):
    log(f"{x}")

y = 10
for z while y < 30:
    log(f"{z} {y}")
    y += 10
0
1
2
0 10
1 20
2 30

Interpolating variables in log

log() is a function call, so use an f-string to interpolate: log(f"{x}"). (Plain log("{x}") prints the literal text {x}.) GUI and comms text — """{x} ships""" — interpolate without the f.

Tasks

Start a background task with task_schedule; the current task keeps running:

== start ==
    log("before")
    task_schedule(a_task)
    log("after")

== a_task ==
    log("in task")
    ->END
before
after
in task

Passing data

Data passed to a task becomes variables in it, separate from the caller's:

== start ==
    message = "caller"
    task_schedule(a_task, {"message": "Hello"})
    log(f"{message}")
    ->END

== a_task ==
    log(f"{message}")     # "Hello" - from the passed data
    ->END

Metadata (label defaults)

A label can carry a metadata: block — a fenced YAML section whose keys are injected as task variables when a task enters the label. They are defaults: a variable already in scope (passed data, live state) wins.

== patrol ==
metadata: ``` yaml
speed: 0.5
radius: 4000
```
    # `speed` and `radius` are now variables in this task
    set_throttle(ship_id, speed)
    ->END
  • Injected on entry — at task creation for spawned tasks (brains, objectives, prefabs) and on a jump/reroute into the label.
  • Column-0 rule: the metadata: line, the top-level YAML keys, and the closing fence must sit at column 0 — only the label's code is indented.
  • Works on any label type, including // routes and @ decorator labels.

Brain and objective labels lean on this for tunables — see brains and objectives.

Waiting for tasks

Capture a task in a variable and await it, or race/join several with promise_any / promise_all:

t = task_schedule(a_task)
await t                                   # wait for one task

a = task_schedule(worker, {"say": "A"})
b = task_schedule(worker, {"say": "B"})
await promise_all(a, b)                    # wait for both
# await promise_any(a, b)                  # wait for the first to finish

Cancelling

t = task_schedule(a_task)
task_cancel(t)

Comments

Line comments use #. MAST also supports C-style block comments:

fred = 10   # set fred to 10

/*
  A block comment.
  Handy for disabling several lines at once.
*/

// is not a comment

A line starting with // is a route label, not a comment. Use #.

Importing

Split a mission across files and import them — including from a zip (the basis of shareable add-ons):

import story_two.mast
from my_lib.zip import bar.mast

Delays

Delays need a clock. delay_app uses the real-time clock (always running); delay_sim uses sim time (paused when the game is). Both accept minutes and seconds:

await delay_app(seconds=10)
await delay_app(minutes=1, seconds=5)
await delay_sim(5)            # 5 sim-seconds

for x in range(3):
    log(f"{x}")
    await delay_sim(1)

Logging

logger() enables logging; log() writes to it. Logging can go to stdout, a string variable, and/or a file, and you can have several named loggers.

logger()                                    # enable stdout logging
logger(file="{mission_dir}/my.log")         # also to a file
logger(name="tonnage", var="tonnage")       # a second, named logger

log("Hello, world")                         # default logger
log(f"Tonnage: {tonnage}", name="tonnage")  # named logger
log("Careful", level="warning")             # with a level

log(message, name=None, level=None) is preferred over print(). See the execution API.