Skip to content

Story & NPC messages

Missions narrate through messages — an Admiral briefing the crew, a station replying. The reusable pattern (straight from Secret Meeting) sends one message to the server screen, every main screen, and every player's comms.

The "admiral message" helper

=== send_admiral_message
    default the_message = "You forgot to set the_message"
    face = get_face(admiral.id)

    # server screen
    sbs.send_story_dialog(0, admiral.name, the_message, face, "#444")

    # every main screen
    for c in to_object_list(role("mainscreen") & role("console")):
        sbs.send_story_dialog(c.client_id, admiral.name, the_message, face, "#444")

    # every player's comms
    comms_message(the_message, to_object_list(role("__player__") & role("tsn")), admiral.id)
    ->END

Call it from anywhere, passing the line as task data:

await task_schedule(send_admiral_message, {"the_message": "The Praetor of Peace has arrived. Escort it to the starbase."})

default the_message = ... guards against forgetting the data (see gotchas). send_story_dialog(client_id, name, text, face, color) shows the pause-screen dialog; comms_message(text, players, from_id) pushes it into comms.

Targeting a subset

Combine roles to aim precisely — e.g. only the consoles linked to one ship:

for c in to_object_list(linked_to(ship_id, "consoles") & role("comms")):
    sbs.send_story_dialog(c.client_id, name, text, face, "#444")

Audio & voice

Play a sound or voice line from your mission's media/ folder. Resolve the path with get_mission_audio_file, and let players opt out with a shared flag:

shared AUDIO_ENABLED = True     # top-level default

# later, at a story beat:
if AUDIO_ENABLED:
    sbs.play_audio_file(0, get_mission_audio_file("audio/distress_call"), 1.0, 1.0)

For music, sbs.play_music_file(0, "music/default/victory"). See the media API.

The ship's log

There is no text waterfall. It was removed everywhere in v1.4.0: the engine never wrote to it, script could not control its background, and a mission could not style it. What replaced it is a log with two halves, both fed by the same record.

The strip — one line, the newest message, sitting where the waterfall used to be on every console. It is the ambient half: always visible, no interaction, read at a glance. gui_log_tail() places it; LegendaryMissions already does on all six consoles.

The tab — the history, in the info panel: scrollable, and filtered into Log (everything), Ship and Mission. gui_info_panel_add("log", ..., gui_panel_log, ...).

Newest is first in both, so the latest line is always in the same place rather than one that moves as the log fills.

You write to it the way you always wrote to the waterfall:

comms_broadcast(ship_id, "Docking moors active", category="ship")
comms_broadcast(ship_id, "Shields critical", category="ship", severity="warning")

category picks the tab (everything shows in Log regardless, so a filter can never hide a message). severitytip, warning, danger — renders the line as a coloured callout, the same formatting gui_text_area markdown uses.

Nothing seizes the console

An urgent line does not switch the info panel to the log tab. The strip already shows it, in its severity colour, on every console — and switching away from the ship data the crew chose, with nothing to switch back, left the panel stranded. A mission that wants the interrupt can set RAISE_ON = ("danger",).

For ambient, non-blocking narrative (universe chatter, lore), the log is the surface; an info-panel card is for something the crew must act on. See the comms API.

Put reusable senders in Python

A message helper like the above is a good candidate for a .py helper function you call from MAST — see Sharing reusable Python.