Skip to content

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.
  • ->END ends 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 delays
  • await gui() — a GUI interaction
  • await signal_next("docked") — the next time a signal fires
  • await 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 finish
  • sub_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 story
  • default x = 5 — set only if x doesn'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.)