The MAST language
MAST reads like a screenplay more than a program. Execution moves forward through a file, jumping between labels, pausing when it needs to wait, and ending when the story is done. If you've seen BASIC, Ink, or ChoiceScript, this will feel familiar.
This page covers the mechanics of the language. For why MAST exists and how it fits Artemis: Cosmos, see What is MAST. Coming from Python? Skim Common gotchas — the handful of ways MAST will surprise you.
Labels and flow
A label starts at column 0 with two or more = signs. Code under a label is
indented. Execution runs forward and, by default, falls through into the next
label unless you jump or end.
jump label_name(or the shortcut-> label_name) jumps to a label.->ENDends the current task.
== start ==
log("Hello, world")
-> goodbye
== skipped ==
log("I get jumped over")
== goodbye ==
log("Goodbye")
->END
Hello, world
Goodbye
The implicit main
Top-level code (before the first label) is automatically combined into one
main label that every mission starts at. Don't write == main == yourself
— use descriptive labels like == setup == instead.
Pausing the flow with await
MAST runs inside the game engine, which gives it only a sliver of time each tick. If MAST ran without stopping it would freeze the game. So when a story can't continue — it's waiting for a timer, a button, a scan — MAST yields control back to the engine and resumes later.
await is how you wait. It suspends the task until the condition is met, then
continues on the next line:
== start ==
log("Hello, world")
await delay_sim(5) # wait 5 sim-seconds; the engine keeps running
log("Goodbye")
@label()
def start():
log("Hello, world")
yield AWAIT(delay_sim(5))
log("Goodbye")
You'll await many things:
await delay_sim(5)/await delay_app(5)— sim-time / real-time delaysawait gui()— a GUI interactionawait signal_next("docked")— the next time a signal firesawait task_schedule(other_label)— another task to finish
To force a single yield without waiting for anything, use a bare yield.
Tasks: storylines in parallel
MAST is Multiple Agent Story Telling — many storylines run at once. Each is a task: an independent thread of the story that runs until it ends. They're not truly parallel (the engine is single-threaded), but the scheduler advances all of them each tick, so they run "at the same time."
A player ship might run one task for its comms, another for science, and another for a side quest.
task_schedule(label, data)— start a task and keep going (fire and forget)await task_schedule(label, data)— start a task and wait for it to finishsub_task_schedule(label)— start a child task tied to the current one
== start ==
await task_schedule(count_to_three)
log("done")
== count_to_three ==
for x in range(3):
log(f"{x}")
await delay_sim(1)
->END
0
1
2
done
Variables and data
Variables are scoped to the task by default. A few modifiers change that:
shared x = 5— visible to every task in the storydefault x = 5— set only ifxdoesn't already exist (great for values a task may be given when scheduled)
Passing a data dict to task_schedule makes those values variables in the new
task — so the same label can be scheduled many times with different data:
== start ==
shared greeting = "Hello"
task_schedule(greet, {"name": "World"})
task_schedule(greet, {"name": "Cosmos"})
->END
== greet ==
log(f"{greeting}, {name}") # greeting is shared; name came from the data
->END
@label()
def start():
set_shared_variable("greeting", "Hello")
task_schedule(greet, {"name": "World"})
task_schedule(greet, {"name": "Cosmos"})
@label()
def greet():
log(f"{get_shared_variable('greeting')}, {name}")
Multiline expressions
A Python expression can span several lines as long as it stays inside
brackets ( ) [ ] { } — just like Python. Reach for it when a dict or a call
argument list gets long, instead of cramming it onto one line or wrapping it in
~~ … ~~:
prefab_spawn("prefab_fleet_raider", {
"race": "skaraan",
"fleet_difficulty": 2,
"START_X": fleet_pos.x,
"START_Y": fleet_pos.y,
"START_Z": fleet_pos.z,
})
It works for any bracketed expression — dict/list/call literals and multiline
if conditions alike. Error line numbers stay accurate for everything after the
block (an error inside a multiline expression is reported at the expression's
first line). The lines are only joined inside brackets, so ordinary
indentation and block structure are unaffected.
Note
A quoted string that itself spans real newlines inside the brackets isn't
joined (its newlines are text, not layout) — keep multiline string
content in a triple-quoted """…""" block or a ~~ … ~~ fence.
Route labels
Route labels run automatically when an engine event matches their condition
— the system schedules a task to run the label. They start with //:
//spawn if has_roles(SPAWNED_ID, "tsn, player")
log("A new TSN player spawned")
->END
Routes cover comms, science, damage, spawning, signals, and more. See Routes.
Sub-labels
A sub-label starts with three or more dashes (---) at column 0. It's a jump
target local to the enclosing == label, so names can't collide across labels
— ideal for loops and re-entry points:
== patrol ==
set_up_patrol()
--- loop
await delay_sim(2)
if still_patrolling():
-> loop
->END
Delays and timers
delay_sim / delay_app handle one-off waits. For named countdowns, use timers:
set_timer(0, "meeting", minutes=20)
...
jump meeting_over if is_timer_finished(0, "meeting")
A common pattern — show pages of text a few seconds apart — is just a sequence of awaits:
== show_credits ==
"""The first page of credits"""
await gui(timeout=delay_sim(10))
"""The second page of credits"""
await gui(timeout=delay_sim(10))
->END
Coming from Artemis 2.x (XML)?
XML <event> tags always ran, every tick, forever. MAST tasks are the
modern equivalent but better: they only run when scheduled, they can end, and
they can be cancelled. Route labels are tasks that schedule themselves when
their condition is met. And unlike XML variables (all global), task variables
are scoped — so a label can be reused with different data instead of being
copy-pasted. (XML is not supported in Artemis: Cosmos.)