Skip to content

What's New ✨

What has landed in Artemis Cosmos, newest release first β€” across the LegendaryMissions content and the sbs_utils library that powers it.


v1.4.0

Start at the top and stop whenever you like: each part runs from the new things to play, through the things you already do that now work better, to mission writing, the tools, and finally the library changes. Links go to the relevant docs.

The short version

What it is
The ePADD One button on every console opens a screen of apps β€” mail, quests, cargo, the boarding party β€” each with a live badge you can read without opening it.
"On screen" The captain says it and science can finally do it: hand the main screen a shot of whatever science has selected.
Incoming hails Comms stops being something only the crew start. The mission calls you, and waits until somebody answers.
Starbases launch fighters Bases hold named fighter wings you can launch, reassign and recall - and a base with no crew to command it flies them itself, enemy bases included.
Command your allies Every friendly ship, turret and starbase takes the orders it can actually carry out: escort, patrol, retreat, investigate, return to base, hold fire.
The old missions fly again All 27 Artemis 2.8 missions in our archive crossed over, and they play β€” same fleets, same tempers, same voice over comms.
A living bestiary Seven new species over one behavior, each one aging from Young to Ancient. Scan it before you shoot it.
Grav-tether Reel, tow and lock β€” on a beam that now feels the weight, and tells you when it is struggling.
Engineering has something to do Systems wear, and a well-run ship can be tuned above spec. Work orders, a four-tab panel rebuilt for touch, and eight efficiency numbers nobody could see before.
New faces Every portrait in the game redrawn - faces that frown, glare, blink and talk, skin in any color you like, and a Randomize button.
The console picker The first screen of the night now tells you who you are about to be.
Beacons & the Fabricator Engineering builds ordnance over a timer and hands it to the tube. The coordination is the gameplay.
Readouts at a glance Efficiency, shields, quest progress and what a build still needs are drawn as bars, the way the engine's own panels are - and Help pages link to each other.
Write the mission, don't program it Quests, dialogue, drops, markers and whole missions as readable AMD documents.
The Casino Eight games, a bar with rumors, and a grey market for anyone the regulars come to trust.
The Director Cut your stream like a broadcast: rundowns, preview and program, titles that write themselves.

Take it for a spin

The new things to do β€” whole systems that were not there before. Start here.


πŸ“Ÿ The ePADD β€” every console gets apps

The ePADD home screen on Engineering
Engineering's ePADD. The counts on the tiles are live.

A console used to offer three tabs along the top β€” help, library, upgrade β€” each labeled with the raw lowercase name of whatever wrote it, and nowhere to put anything else.

Now one button opens a whole device. ePADD is a screen of app tiles, named and described and grouped, with only the ones this station should see:

[ ePADD ][ back ]                                    Artemis        T+00:14:22

  Ship
   [ Boarding Party ]   [ Status  2 ]   [ Messages  3 ]   [ Cargo ]
   [ Fabricate 1 ] [ Upgrades ]

  Mission
   [ Quests  2 ]   [ Library ]     [ Help ]
  • A tile tells you whether it is worth opening. The number on it is live: three unread, two things building, the quest count. Previously the only way to find out was to go and look, on a bar that did not have room to say.
  • A badge above the ship data names who is sitting there and how many apps have something waiting β€” Lt Marek (2) β€” on every console, without opening anything.
  • Messages. The crew can text each other, because it is a bridge simulator full of people sitting at separate screens who cannot see one another, and passing a note is half of what they would do if they could. The same inbox carries mail from the story β€” a letter from your sister, a note from the admiral β€” and a letter can ask a question, with up to four things you can say back. Replies land in the thread, so it reads as a conversation rather than a form you filled in.
  • A mission's mail is a document a writer can be handed. One heading per letter, a From:, a To: naming a console, and After: for how far into the mission it arrives. No code.
  • The boarding party carries it down. A landing party's own apps travel with the party, and the scene they are playing is mirrored into the inbox, so the surface crew read their story where they read everything else β€” one device, wherever the person is.
  • It fits the screen it is on. Thirty apps at 1024x768 is a scrolling list; the same thirty at 1920x1080 is a grid. Nothing is ever cut off the bottom.

For authors, an app is one route and one line of registration β€” //gui/app/cargo plus gui_app_register("cargo", title="Cargo", consoles="engineering", status=hold_count) β€” and the route keeps its own if, which is still what decides whether the app is offered at all.

Docs: The ePADD.


πŸ–₯️ "On screen" β€” science can finally answer the captain

The captain says "on screen" and, until now, nobody could make it happen. A drop-down beside the science console's Follow checkbox hands the ship's main screen a shot of whatever science has selected.

[x] Follow   [ On Screen - Orbit   v ]
                Off
                On Screen - Dolly
                On Screen - Orbit
                Tactical 2D

Dolly pushes slowly in and out, Orbit turns around the contact, and Tactical 2D puts the radar on it. Change the selection and the shot follows; destroy the contact and the viewer stands down.

Beside the picture is a data column carrying what science actually knows: vitals (range, bearing, shields, hull), every scanned tab together β€” Scan, Status, Intel, Materials, Bio β€” recent comms with that contact, and any quest bound to it. It pages itself when there is more than one screenful, and skips pages that have nothing to say. A mission adds its own page with viewscreen_page_register.

Nothing has to be co-ordinated with helm: the viewer writes the same main-screen state helm's own control does, so last writer wins β€” helm reaching for the control simply takes the screen back, and the drop-down falls back to Off. It is scoped per ship, so science on one bridge cannot change what another's crew is looking at.

Writing your own main-screen console?

While a shot runs, the console is assigned to the subject β€” the engine only honors a camera change when the console and the lens ride the same object. So sbs.get_ship_of_client() on a main screen answers with the contact being filmed. Use viewscreen_home_ship(client_id) for "this console's own ship".

See On screen.


πŸ“ž Incoming hails β€” the mission calls you

A hail open on the comms console, with three replies
The admiral calls. Comms picks a reply - and the last hail is logged above, with the answer it got.

Comms has always been something the crew starts: pick a contact, a menu opens. An incoming hail is the other direction. It arrives in an Incoming Hails list on the comms console, newest first, and waits until somebody answers it.

Answering opens a short conversation with up to four things the crew can say back. Back steps out without answering, so comms can read a hail through and present it later, when the captain is ready. Answered conversations stay in the info panel and can be replayed β€” a replay can never change what was chosen.

Beside the list is a dial that decides where the conversation is drawn β€” Off, This Console, Main Screen, or Both β€” and an Audio checkbox. Each console carries its own dial, so putting a hail on the main screen is a decision one officer makes, not something the mission forces on the bridge.

While a hail is placed on a comms console, that console's 2D radar follows whoever is calling, the way science points it at a scan target. It obeys the crew's existing Follow checkbox, and it hands the radar back afterwards.

Here There Be Monsters is written this way end to end: twenty scenes in one document, each named by one line of the story.

Incoming hails


πŸ›©οΈ Starbases launch fighters

A starbase with hangar bays now holds wings of fighters - Red, Gold, Blue - and each one is yours to command from the base's comms menu:

  • Launch Red wing (4/4) at a hostile, or to protect an ally.
  • Once it is out: Reassign Red wing (2 out) onto a new target, or Recall it.
  • A command starbase flies two wings, so one can cover the station while the other strikes.

Fighters launch with a few minutes of fuel. At bingo they break off and head home on their own, and a fighter that lands spends a minute refueling and rearming before it can go again. Losses are permanent: the count on the menu is what the wing has left, and a science scan's new hangar tab shows every wing - ready, out, refitting and lost.

A base with nobody to give it orders fights for itself. It launches at whatever comes near, keeps a reserve wing home until the station itself is hit, covers allies under fire, and recalls its fighters when the sky is clear. That includes enemy bases - fly into range of one with bays and expect company. On your own side, tell a base Act on your own and it runs itself until a bridge gives it an order.

Enemy maps build a real base network. Deep Strike and Border War now field a mix of Command, Industrial, Science and Civil bases for whichever race you are fighting, each with the fighters its kind carries - and the smaller kinds draw as smaller copies of the race's base when the host has extra ship data on.

Play guide: Giving orders, markers & fighter wings.


πŸŽ–οΈ Command your allies

Comms can order anything on your side: warships, freighters, turrets and starbases. Each one is offered only what it can carry out - a freighter is never told to attack, a turret is never told to move, and a base with no hangar is not on the list at all.

New order What it does
Escort stays with an ally and fights off anything that gets near it
Patrol flies between where it is and a marker, engaging on the way
Guard here holds its position and engages whatever comes close
Investigate flies out to a contact and scans it for your side
Return to base docks at the nearest friendly station and recharges
Retreat holds fire and gets clear of the nearest threats
Fire on holds position and shoots one target - turrets too
Hold fire / Weapons free stops or resumes choosing its own targets, without cancelling its orders

Drag to order: drag a ship onto its target on the comms map and comms opens with just the orders that fit that pair.

Point at a place. Science right-clicks empty space and drops a named marker - Alpha, Bravo, Charlie - for the whole side. Drag a ship onto Bravo to send it there, guard it or patrol to it. The comms map gains Can order and Markers chips.

Deployable turrets arrive wherever the install supports them: with EXTRA_SHIP_DATA on, a crew buys a kit, tows it into place and unfolds a defense tower that holds the ground it is left on β€” and takes Fire on and Hold fire from comms like any other unit. Peacetime's Picket Line job is built on them.

Play guide: Giving orders.


πŸ›Έ The old missions fly again

Every mission your crew ever flew in Artemis 2.8 can come back into service. Hand one over and it returns as a Cosmos mission you can host tonight: the same fleets waiting in the same corners of the map, the same enemies with the same tempers, the same voice cutting in over comms.

All 27 missions in our archive made the crossing β€” and not just far enough to load. They play: hostiles pick fights, stations answer hails, the timers still run out on you, and each mission still ends the way it always did.

  • The briefing is no longer a memory test. The mission's goals arrive as real Cosmos quests, ticking over in the crew's quest log as you go β€” or, if you'd rather have the original exactly as it was written, you can have that instead.
  • Enemies remember they're enemies. Old missions never spelled out who hated whom; that gets worked out and declared, so hostiles open fire, Science sorts friend from foe, and contacts show in the right colors.
  • The Game Master keeps the con. Menus stay nested the way they were laid out, shortcut actions survive, and a spawn lands where the GM is pointing rather than in the corner of the map.
  • The details survive. Elite Skaraan tricks, captains with a temper, fighters and shuttles in the hangar, science scans, hail text β€” the flavor comes with the fleet, not just the ships.

Anything the old script left genuinely ambiguous is written down for whoever finishes the port, rather than quietly guessed at. β†’ Porting from Artemis 2.x


πŸ™ A Living Bestiary

The seven new species, one frame each
The seven new species. Each frame is scaled to its subject, so sizes are not comparable - the Bulwark is the biggest thing here.

Space monsters are no longer just one hostile Typhon. The Monsters map option now seeds a weighted mix of species β€” some deadly, some harmless, some that actually help you β€” and a Game Master can drop any of them from the spawn menu. Scan an unknown creature and its science readout tells you what it is before you decide to shoot.

  • Seven new species over one behavior: Reaver (fast hunter that enrages β€” faster and redder with every wound), Ravener (an apex predator that feeds on weapon fire β€” beams and torpedoes only heal it; the one thing it can't eat is a black hole), Grazer (a placid drifter that's tame unless provoked, then turns on you), Bulwark (colossal, inert living reef β€” harmless unless you ram it), Sparkfeeder (a docile creature that recharges your ship's energy), Siphon Leech (its parasitic twin β€” drains energy, non-lethal), and the Warden (a friendly guardian that hunts raiders, never you).
  • Every monster has an age. Individuals spawn Young, Mature or Ancient β€” older ones are tougher and larger (age never changes the damage they deal), and an aged Ancient eventually seeks out a black hole to die.
  • Black holes bite again. Player ships and fighters that drift into a black hole's pull are now reliably destroyed β€” no more bobbing at the edge or warping free (fixes the long-standing "black holes don't kill" bug).

Authors add a species by dropping in a prefab file over behav_typhon; the roster and mix live in one weight table.


πŸͺ Grav-tether

A tractor beam for Weapons and fighters β€” one system, scaled by hull.

  • Weapons hold-click any contact for a context menu: Reel cargo in (it's collected on contact), Tow a ship or derelict at distance, Lock for a rigid grab, or Release. The mode is chosen by what you grabbed. You do not have to target it first - the menu acts on whatever you held the button on, so you can hook a derelict while staying locked on the raider shooting at you.
  • Weapons shows what you have hold of. The called-shot square doubles as the tether readout: the load's own silhouette, the mode (TOW / REEL / LOCK), its name and its range. When the called-shot panel is busy with a target, the tow is named along the bottom strip instead - it is never left unsaid. And it says so when the beam is on you: a hostile tether reads TOWED, a fighter on a rock reads SWING.
  • Fighters get a cockpit button with nose-aim targeting (it grabs what you're pointing at): reel salvage, or swing around an asteroid on a tether that holds your radius while you orbit. The button glows cyan while tethered.
  • Impulse only (the canonical rule): a tether can't hold at warp β€” it caps you back to impulse, or optionally snaps and drops the load.
The beam now feels the weight
  • Tow a starbase. It is slow, it drinks your reserves, and one ship will not get it far β€” but it moves. A crate still comes in like a crate; a freighter is a real load you feel on the helm; a station is a job.
  • Call for help, and mean it. Every ship on the same beam pulls harder and takes a share of the power bill, so four hulls haul a station further than one ever will β€” not four times harder, but four times longer before anyone runs dry. Weapons counts the crew on the readout: TOW Γ—4.
  • The beam tells you it is struggling. A haul reports light, heavy or overloaded, and an overloaded one says outright that it wants another ship on it. No more wondering why the helm feels like treacle.
  • Grav Lock on something enormous pulls you over. Lock a station and the station wins: it winches you across β€” a steady haul you can watch, not a jump cut β€” and holds you there, which is a fast way to park.
πŸ”§ Tug rigs

Two ways to make a ship better at hauling, and they stack.

  • Heavy Tug Rig β€” the ship hauls as if it were four. Permanent once fitted, and bought at a station market, not found floating in space.
  • Tug Rig Mk I β€” an early-pattern rig you find out in the world. Two and a half ships' worth of pull, and it burns itself out after ten minutes. Good for one delivery.

Fit both and they add up. Neither changes what your ship weighs, so a rig will not make you harder to grab or make your own wreck worth more.

Weight is honest

Everything on the beam has a mass, and the mass is what a crate looks like it weighs. A canister barely slows you while you reel it in, a fighter can pick one up without being dragged onto it, and a turret kit tows behind a cruiser instead of pulling the cruiser around. The heavy things β€” stations, capital hulls β€” are the ones that fight back.

Built on the engine's native tractor.


πŸ›°οΈ Sensor Beacons & the Fabricator

Engineering has a new job. A Fabricate tab turns materials into gear over a build timer, and its headline product is Beacons β€” a fabricate-only ordnance the crew builds, hands to the tube, fires, and later flies over to recover.

  • A two-console loop, on purpose. Beacons don't come pre-loaded β€” a ship spawns with zero rounds. Weapons tells Engineering what to build, Engineering fabricates it (spending inputs over a timer) and delivers it to the tube, and only then can Weapons fire it. The coordination is the gameplay.
  • Bio Beacons herd the bestiary. Program one to attract or repel a chosen space monster, fire it, and it broadcasts across the sector β€” baiting a Reaver into a minefield or shooing a Grazer off your six.
  • Sensor Beacons β€” the old Probe, brought forward. The passive sensor-relay Sensor Beacon folds in Artemis 2.8's Probe concept. Ported 2.8 missions that stocked Probes come across as Sensor Beacons the crew can build and deploy.
  • Recover and reprogram. Fly over a deployed beacon to add the round back and keep its program; Science can scan any beacon to read what it's broadcasting.
  • Recipes are data. Every beacon (and any other craftable) is an AMD recipe β€” inputs, build time, and program β€” so a mission adds its own without touching the addon.

Authors: Fabrication & Beacons addon Β· Porting Probes: Porting from Artemis 2.x.


🎰 The Casino is open

A KoraTa hand in progress
KoraTa, one round in: your run below, the apprentice's above - and now you pick which gate to drop on theirs.

Dock in the hangar bay and step into the Casino β€” a self-contained hangout that adds a Casino tab to the bay (leave it out and nothing changes). It runs on an Arvonian bit-card deck, dealt in some games by the Understander, their revered master computer.

  • Play with chips. Start with a stack; the cage lets you Buy more or Cash Out β€” buying dips into the crew's shared credits so you can always get back in, and a hot session earns comp chips.
  • Eight games, from a ten-second flutter to a full poker duel: Parity (quick XOR bet), Blackjack, Nibble, Gates (bit-card baccarat), Choga, Video Poker, and the new KoraTa β€” Ghost-Writing, a five-round head-to-head where you play cards as values to build your run or as opcodes to corrupt your rival's (3-bit and 4-bit tables).
  • The bar. Toast the room, buy a regular a drink, and ask for a rumor β€” act on it to see if it pans out. Reliable patrons earn your trust; enough trust opens the grey market.
  • Pilot market. Spend your winnings on real ship upgrades and gear β€” chips first, then side credits.

Play guide: How to play the Casino Β· Authors: The Casino addon.


πŸ›©οΈ Hangar

  • Sorties for pilots. Pick a fighter or a shuttle and its sortie quests appear under Available Quests, ready to accept before you launch.
  • Loadouts as upgrades. Craft loadouts are now deltas over the ship hull β€” shields and ammo applied through the item/upgrade system β€” with an image-based cockpit overlay.
  • Rearming takes time. A craft's refit now runs longer the more torpedoes it has to reload β€” every empty tube adds time (tunable per dock), so a bomber that spent its payload turns around slower than one that never fired. Fit the new Torpedo Autoloader upgrade to cut that per-torpedo cost.

See the LegendaryMissions addon reference.


πŸ•΅οΈ Peacetime Remastered β€” a mission written in AMD

Peacetime Remastered is a border-patrol shift that doubles as a worked example of authoring a whole mission as data. Almost everything the crew sees β€” the story, the job board, the cast, the clues, even the chatter β€” is declared in one .amd fact-sheet; MAST holds only the logic that reacts to it.

What the crew sees Authored as
The Ambassador Florbin kidnapping a quest tree β€” take the case, identify the kidnapper, subdue, recover
The cast (a deck chief, the Admiral, the Ambassador…) lifeforms β€” hosted contacts appear as comms badges you hail
The 40 allergy clues generic AMD records β€” each heading is a container the ambassador could hide in, its body the clue
The Ambassador's passenger complaints a chatter line-pool β€” one picked at random
Briefings, cargo manifests, interview reports prose templates filled in at send time
Object scans (cargo ships, anomalies) dialogue-native scans (Scan of: / Tab: / % variants)
The patrol quests β€” gunnery, rocks, poacher, mercy, customs, survey quests with goals, rewards, and fail triggers

Pick the quests you want. Each one waits under Available Quests until the crew Accepts it. Accepting is when a quest's clock starts and when its targets spawn, so a timed rescue gives you the full window (the Mayday arrives, then the shuttle) and nothing clutters space for work nobody took. Weapons stays busy in peacetime: qualify on drones, break hazard rocks, and disable β€” don't destroy a poacher.

Two dials. Quests Offered (none / few / some / max, default some) deals a random hand of quests, so no two shifts offer the same work β€” and a seed deals the same hand every time. Quest Size sets how big each one is.

The mystery is different every time, and always solvable. One kidnapper, the Ambassador hidden in exactly one cargo hold, and a clue trail laced with decoys β€” dealt fresh each game, and never dealt into a dead end.

Authoring reference: Quests Β· Sides, lifeforms & faces.


πŸ“‹ Available Quests β€” everything you can take on, in one place

Quests you could take on and quests you have taken on now live side by side, on the same screen:

  • Available Quests lists every quest on offer β€” a mission's patrol work, the sorties for the craft a pilot has picked, and in Open Universe the station work, cargo runs and passengers. Pick one and read it in full, then press Accept, on the consoles that may. Station work in Open Universe says to hail the station instead.
  • Quests is what you have taken on β€” active, done and failed. A quest moves there the moment you accept it.

The Available Quests tile shows how many are waiting, and only appears when there is one to take.


🀝 Peacetime, with more than one ship

Bring a second ship to a peacetime patrol and a single Quest Mode (set on the map panel) decides whether you cooperate or compete for the same board of jobs:

  • Co-op β€” nothing is claimed. Deliver a barge and every ship holding that job is paid, and the multi-step arcs run for the whole crew together.
  • Protected (default) β€” the moment you work a target it's locked to you. A rival's grav-tether is refused ("claimed by another ship") and only you are paid. Friendly by default, nobody can spoil your job.
  • Claim-jump β€” claims are stealable: the grav-tether becomes the competitive tool, letting you tow a rival's salvage out from under them. Payment follows whoever delivers, and each ship banks its own earnings for a top-earner readout.

Who owns a job, who gets paid, and who gets credit for a kill are firm rules rather than best effort β€” they hold however many ships are flying.

Player guide: Multiplayer jobs.


πŸ›‘οΈ The Siege Map, Refined

  • Pick your battlefield size with a new Map Size option.
  • Consistent, repeatable maps from phased, keyed seeding (no more surprise spawns inside an asteroid).
  • Share the exact setup with per-map seed options and a shareable game code.
  • Optional bonus objectives for skilled crews.
  • Quests between waves. Peacetime's patrol quests come to the Siege: set Quests Offered to few, some or max and a random hand of rescues, tows, surveys and gunnery drills appears under Available Quests, to take on between assaults. Off by default; the same seed deals the same quests. Quests offered.
  • Survive Clock option. Choose what the time limit means: Win = outlast the clock to hold the line, or Loss = break the siege before time runs out or you lose. One dropdown flips a defensive hold into a race against the clock.
πŸ’€ Bosses

The Siege can now escalate into a boss that warps in when the raiders thin out β€” picked from a new Boss dropdown:

  • Warlord β€” a named enemy flagship and honor-guard reinforcements.
  • Continuous β€” endless waves until the clock nears its end, then the attackers break off in retreat for a hard-won defender victory.
  • Ragnarok β€” the renegade "42 Fleet" led by a Terran juggernaut. Beat it outright, or have your comms officer hail XORN and turn one of its captains to your side.
  • Infestation β€” a BioMech swarm that drifts neutral and feeds on asteroids until you provoke it, then wakes as a collective, evolves through four stages, and breeds β€” with the sentient Stage 4 hailable to calm or enrage. BioMechs are a reusable addon you can drop into any mission.

Bosses are data-driven and folder-scanned β€” each is a small file in maps/bosses/, so authoring a new one is just dropping in a file.

Playing & hosting: LegendaryMissions Β· Authors: Writing a Siege boss.


πŸ—£οΈ Characters who ask, and leave if nobody comes

A deadline you cannot hear is just a number expiring somewhere. An urge gives it a voice: a condition, a cadence, and a pool of authored lines. Anyone can hold one β€” a passenger, a station, a whole side β€” and the closer the clock runs, the more insistent they get.

  • The stakes stay in the quest. An urge declares no consequence of its own; it is the voice of a quest that is already counting down. One clock, one place to tune, and deleting the urge costs the drama but not the mechanics.
  • The countdown IS the drama curve. Write % while there is time, %% as it runs short, %%% at the end, and Escalates: with deadline reads the quest's own clock. The number of markers is the curve; Fails when: is the tempo. Nothing has to agree with anything else.
  • They know when to shut up. A per-actor floor stops one character monologuing, and a global floor stops five of them piping up the moment a jump makes them all eligible β€” shared with mission dispatch, so nobody talks over the Admiral. Only something urgent (Weight: 90) jumps that queue.
  • A whole station can hold a quest, so a resupply run has a deadline and a cost that lands on the world rather than on whichever ship happened to fly past.
  • Standing is a consequence. Reward: and Penalty: take earns <faction> <pole> <n>, so finishing a job β€” or abandoning one β€” moves how a faction feels about you, not only what it charges you.

In Open Universe, Doctor Voss waits on the docking ring at her pickup, asks more insistently as her window closes, and takes a berth on someone else's freighter if nobody comes. In Legendary Missions, Ambassador Florbin's famous passenger requests are the same character, now five lines of data instead of a hand-written loop.


πŸ€– Attract Mode β€” the ship flies itself

Flip on Auto Play (AUTO_PLAY: enable: true) and every bridge runs itself β€” a great lobby / attract screen or hands-free demo. The autoplayer doesn't just wander and shoot; it plays like a coordinated crew:

  • Stand-off alpha strike. Against an enemy at range it holds at 5000u, opens with an EMP to strip shields, then a single Nuke, waits for the blast to clear, and only then closes to mop up with beams and homing torps β€” so it never catches itself in its own explosion.
  • Fires only what it has. It reads real magazine counts and launches only loaded types β€” no more phantom Homing torpedoes once the tubes run dry β€” and it won't friendly-fire: area torps are held whenever an ally sits in the blast, and wrecks get beams only, never torpedoes. A PShock finishes a target whose shields are down.
  • Talks like a crew. It hails enemies to taunt them with the right line β€” the one an intel scan reveals β€” demands their surrender once shields drop, and at the start of a match asks friendly stations to build Nukes to keep itself armed.
  • Plays engineer. It overpowers drive and weapons for a faster, harder-hitting ship and balances the heat with coolant, easing off when energy runs low.
  • Survives sensibly. It flees lethal terrain β€” steering clear of a black hole's pull well before it's caught in the well β€” docks to repair when hurt, and if its maneuvering is shot out it holds station to keep fighting instead of burning off into deep space.
  • Goes home when the tank runs low. A ship short on energy breaks off, flies to a friendly station, docks, and comes back out fully fueled with its shields up β€” so a lobby left running all evening still has ships flying at the end of it.

Turn it on β€” and tune its stand-off range and engineering overpower β€” in LegendaryMissions settings.


🎬 The Director β€” stream your game like a broadcast

Cosmos looks best from outside the ship, and a stream of it is only as good as the shot it is on. The Director is a console for cutting that stream live: build a list of shots, see the next one before it goes out, take it to air when it is right.

It never takes a crew seat or a mainscreen. You open extra clients and tell each one what it is β€” Program (what the stream shows), Preview (the shot before it goes out), or the Director itself. Each screen names itself PROG01, PRE01, DIR01, so four windows are told apart at a glance.

  • Two feeds. Every Program screen shows the same thing; every Preview screen shows the same thing; the two are different. Open a second Program window and it stays in step with the first, so you can capture one and project the other. Preview is where you check a shot before the audience sees it β€” and it follows the editor live as you build.
  • Rundowns are the running order. Four are built from the live game every time they are used, so they never go stale: Bridge wall (a multiview of every console), Player ships, The action (whatever is most exciting right now), and Stations & terrain. Build your own beside them.
  • The shot on air is green, and so is the rundown holding it, so a collapsed branch still says where the show is.
  • Two ways to cut. Tick rundowns and press Send to Program to let it run on a dwell β€” or flip to Items and click a shot to put it on air the instant you pick it, like a clip launcher.
  • The same shots the bridge has. Dolly, Orbit, Chase and Tactical are the science and weapons On Screen vocabulary, framed off the ship's own hull size β€” so a Director shot and a bridge shot of the same ship are the same shot. Chase is the Director's own addition: a third person that rides behind a ship as it turns.
  • A shot can hold for as long as it deserves. An establishing shot of a starbase can ask for ten seconds where a chase in a firefight wants three; everything else follows the dwell.
  • Let the game direct. The auto-director ranks by the same signal the engine's own cinematic camera follows, holds its choice through noise, and falls back to your running order in a lull.

Titles that write themselves. Lower thirds, hero cards, a top status bar and a letterbox β€” and because a generated rundown makes one item per ship, the text is a template filled in from whatever the shot is pointed at:

name:  <<name>>          ->   Artemis
line:  <<class>>         ->   Light Cruiser

<<side>>, <<role>>, <<race>>, <<comms_id>>, <<hull>> and <<shields>> too, with <<class|contact>> to say what to fall back to when there is nothing to give. Each row has presets, and a Save to add your own.

Docs: The Director.


πŸŽ›οΈ Tablet control panels β€” a real throttle lever (experimental)

Put a tablet beside the helm and give it a throttle you can grab. With TouchOSC (or any app that speaks Open Sound Control), a phone or tablet becomes an extra panel for a console: an impulse lever with a REV switch and warp in four steps, a big red-alert button, shield and dock buttons, power faders for engineering, torpedo fire buttons for weapons β€” and live gauges for shields, energy, heat and torpedoes left.

  • It follows the ship. Move the throttle at the real helm and the lever on the tablet moves with it. The gauges update a few times a second.
  • It sits beside the consoles, not instead of them. The consoles stay the main screens; the tablet adds the physical feel.
  • Ready-made panels. sbs osc layout all writes a TouchOSC layout for each of the five consoles.
  • Off unless you turn it on - it opens a network port. Launch LegendaryMissions with profile=osc, point the tablet at the server machine, and play.

Experimental: only the helm panel has been tried on a tablet so far, and a few console controls β€” loading tubes, starting a scan, jumping β€” are out of a panel's reach for now.

Docs: OSC control panels.


The things you already do, done better

Everything in this part is something you already had, met again. A few change what your game looks like whether you ask for them or not.


😐 New faces, and faces that move

Six rows of redrawn portraits, one per species
Terran, Skaraan, Kralien, Torgoth, Ximni and Arvonian - each one rolled at random.

Every character portrait in the game has been redrawn. All six species - Terran, Torgoth, Skaraan, Kralien, Ximni, Arvonian - are new art: more hairstyles, far more clothing, glasses, headsets, rank insignia, breathing masks, and eight painted Arvonian busts instead of one.

Faces have expressions now. A Terran comes with fourteen sets of eyes and fourteen mouths - angry, worried, a sideways glance, an eye-roll, gritted teeth, a smile, eyes shut - so the officer on your comms screen can look like they mean it. A mission can ask for one by name, and everything else about the person stays exactly as it was.

One officer in seven expressions
The same officer, seven ways.

And they can talk. The mouths include the shapes speech is made of, so a face can be animated while a line is delivered, and blink while it is not.

Eye color is real. Green, amber and blue eyes are drawn rather than tinted - on Terrans, Kraliens, Skaraans, Torgoth and Ximni alike - and the avatar editor offers them by name. Hair is drawn in neutral gray, which means hair color finally works across its whole range instead of fighting whatever color the hair was already painted.

Skin comes in more than one color. Every species now varies, and the range runs well past human: emerald, jade, ice blue, cobalt, rust, crimson, ashen and amber sit alongside the flesh tones, and any of them can land on any race. Hair has its own range the same way.

The avatar editor says what you are looking at. Short lists - which body, which mask - are dropdowns; the long ones are a slider with the name written beside it, so dragging through thirty-nine skin tones tells you which one you are on. A new face starts with a neutral expression.

And there is a Randomize button. It keeps the species you picked and rolls everything else - face, hair, clothes, color. Hit it until you see somebody you like. If the mission says your console wears a uniform, a roll will not put you in a suit and tie.

One thing to know: the new sheets replace the old ones, and the old art is gone. Faces written into missions have been translated to the nearest equivalent, and a face you built yourself is translated once, automatically, the next time you play. It will be the same character - same species, same build - redrawn. It will not be pixel-for-pixel the person you last saw, because the art underneath it is different.

Docs: Faces.

πŸͺͺ The console picker says who you are about to be

The first screen of every session used to offer an empty Crew person Name box. Almost nobody filled it in β€” and a name was only worked out after the console was chosen, so the one screen where you decide what to be was the one screen that could not tell you.

It shows you instead. The face and the name sit on one line and follow whichever station is highlighted in the list: run down helm, science, engineering and the line changes with you. Highlight engineering on a hull whose mod brought its own cast and it names that ship's engineer. Nothing is written while you look.

A name belongs to the seat, not to whoever sat down. Helm on the Artemis is the same officer every time anything asks β€” before anybody connects, while you are only looking at it, and after whoever was there gets up. So what the picker shows you is what the console will actually get, and a station nobody is at still has somebody's name on it. One consequence worth knowing: moving from helm to weapons renames an automatically named player β€” you stopped being the helmsman and became the weapons officer, which is what a cast list has always done. A name you typed yourself is yours, and is never overwritten.

One button, marked "Edit", opens the whole person: your name, your avatar, and the option to be somebody out of the ship's roster.

Editing one thing keeps the others. What you choose is laid over the identity the ship had for you, one field at a time: naming yourself keeps the face the ship gave you, building a face keeps your name, and renaming a cast member keeps their rank and their station β€” the same officer under another name, not a stranger who took the chair. A face and a photograph still displace each other; they are two answers to the same question.

Faces agree with their names, and wear a uniform. A face is rolled for anybody who has none of their own β€” from the gender their name declares, and in the ship's uniform, so the bridge looks like a crew rather than a room of passengers.

The TSN Watch is forty people. Seven name a station and the rest float, so a ship with a dozen consoles open, a second science station and a hangar full of pilots stays inside the cast instead of falling through to automatic names half way down the ship.

The ship list on that screen also offers only the ships that will actually fly β€” picking a hull that was about to be parked out from under your console is no longer possible.

Docs: Crew rosters.


πŸ”§ Engineering has something to do between the hits

Engineering's interior view beside the Systems tab
After a patch-up: every repaired system is back in service, but worn (gold), and the Systems tab says by how much.

Damage control was binary: a room was broken or it was fine, and the only thing that ever changed it was taking a hit. Systems now have condition as well as damage β€” and Engineering has a panel that shows it.

Tier Drawn Worth
Damaged crimson nothing
Worn gold 75%
Nominal its own color 100%
Tuned cyan 110%

A damage-control team patches a room; it does not rebuild it. A room your teams put out comes back worn β€” in service, drawing gold, giving three quarters of what it used to. Only a dockyard repair makes it new again. So the ship that leaves the second fight is not the ship that went into the first, and that is now something the crew can act on rather than something they just live with.

Wear does not only come from being shot. Firing a beam wears the beam that fired; launching a torpedo wears the tube β€” charged on the shot, because a torpedo that missed wore it just the same; a hit wears the shield facing that ate it. And everything ages on a slow clock, the drive you are actually running included. Warp is about five times harder on a ship than impulse.

Tuning is what a well-run ship gets for it β€” and somebody has to order it. Send a team to a system that is fine and they bring it above spec: a tuned beam pool fires at 110 percent. Nothing tunes itself, and nothing is tuned by a team merely standing in the room. It is a job you assigned.

The order is two clicks now. Picking a damaged or worn room in comms used to grow two buttons per damage-control team β€” eight or more on a three-team ship, with the one you wanted buried in them. It is one button, Repair or Tune, carrying how many teams are already on it, opening a submenu of who to send, who to call off, and how urgent it is. Raise a job to critical and the team turns around once: the choice is committed, so two orders can never trade a team back and forth.

Engineering's right column is a four-tab panel, in place of the oversized crew face that used to hold it (and that was also the thing that collapsed at 1280x720):

  • Selected β€” the room or team you picked on the interior view, described in full.
  • Orders β€” every work order on the ship, highest priority first, with raise and close on each row. Only live jobs are listed: one another console has finished is off the list before you reach for it.
  • Systems β€” the four pools, and the eight efficiency numbers the grid has always derived and Engineering has never been able to see: beam, tube, impulse, warp, turn, sensor, and both shield facings. Each one wears its tier's color, so the pool that is hurting is findable without reading all eight.
  • View β€” how the interior draws its rooms and systems, icons or shapes, one press each. These used to live on a pad you had to find on the map and open two menus deep; they sit beside the view they change now.

And the whole console is built for a finger. Every control you actually press is a real target: the orders under your selection, the preset bank along the bottom, the panel's own tabs. The orders list scrolls, so a room that offers nine things to do shows you nine β€” where a long menu used to simply stop at the bottom of its box with nothing to say so. Ship data, the heat bank and the power sliders all grew, because the band of empty console between them is gone.

Your damage-control teams are a row of chips above the interior view β€” name, and what each one is doing right now, in the team's own color. Tap one to select it; it is the same selection as clicking the figure on the map, so the panel and the orders follow either way.

Engineering reads its own screen, so a short display shows three orders at a time and a tall one shows five, and the panel above takes whatever is left.

The cockpit's system lights read the same model, so they show the wear tiers too.

A mission that never opts in sees no change at all. An untouched system reads as nominal and weighs exactly 1.0, so effectiveness comes out identical to the undamaged / total fraction it replaced β€” and because wear sits on top of undamaged rather than replacing it, an all-worn ship still cannot explode. Every threshold and rate is a dial: grid_set_wear_tuning(tuned_bonus=0.0) gives you maintenance with no over-unity, upkeep_rate=0 turns off time-based wear entirely.

The small print, so none of it surprises you. A gym, cargo bay, hatch or airlock is not a system: it never wears and never needs tuning, but it can still be hit, and a repaired one goes back to its own color. A tune that has worn off stops being paid for, so beams sit at 110 percent only while the tune lasts. A team sent to a job walks to it, and any console can take that job over. And the sick bay now heals a team all the way β€” a team that had taken a single hit healed nothing at all, and Engineering said "visit sickbay" forever β€” while an uninjured team is left alone rather than parked there.

Docs: Work orders and maintenance, Damage.


πŸ“» The text waterfall grew up into the ship's log

Every console gets this, with nothing to opt into. The waterfall did one job β€” show the last few lines β€” and did it with its hands tied: the engine never wrote to it, script could not control its background, and no mission could style it. It is now a proper log, in two halves fed by one record:

  • The strip β€” one line, the newest message, right where the waterfall used to sit. Always visible, no interaction, read at a glance.
  • The tab β€” everything the waterfall used to lose. The full history in the info panel: scrollable, and filtered into Log, Ship and Mission.

Newest is first in both, so the latest line never moves.

You write to it exactly as before β€” comms_broadcast(...) β€” with two new optional arguments: category picks the tab (everything still shows in Log, so a filter can never hide a message), and severity (tip / warning / danger) renders the line as a colored callout.

Everything a mission says is kept. overlay_toast(), the toast <text> quest directive and announce(level="status") / level="minor" all write a log line, so a console that connects a second later can still read what was said. Only the levels that are meant to interrupt draw over the view.

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, everywhere. A mission that wants the interrupt sets RAISE_ON = ("danger",).

A message names the person who spoke, not the ship they are standing on. When one of your officers hails another bridge, it arrives from that officer β€” by name, with their face β€” rather than from the hull they happen to be aboard. The log files each exchange under the person at the other end, so two crew members on the same ship are two conversations instead of one pile filed under the ship, and the direction of each line is drawn for you instead of being spelled out in the title.

Docs: Messages & the ship's log, Overlays.


πŸ’Ύ Your setup survives a restart

Restarting the mission put every option back to settings.yaml β€” player ships, difficulty, terrain, the lot. After an early death, or with one setting to change, that meant retyping the whole screen. Two ways not to, for two different needs.

Save a preset. Build the setup you want on the server console, type a name beside the Presets dropdown, press the save icon. It is in the dropdown afterwards, restart or no restart β€” and it brings back the crew's ship names and hulls as well as the options. Presets are per map.

Or let it remember. RESTORE_LAST_SETUP: true brings back whatever the last game started with, per mission and per map, with nothing to press. It saves when a game starts, so a crash or a quit still leaves it recorded.

It is off by default, and that is deliberate: a convention or venue machine wants every group to start from the same known state, which is exactly what it already does. Turn it on for a crew replaying the same match.

Loading a saved setup puts those ships straight on helm's picker, so helm can still change one and the change sticks.

Where it all lives now

Saved setups are written to data/missions/common_data/, beside the missions rather than inside one β€” so updating or re-extracting a mission keeps them.


πŸ“œ The Quest Log says something new

A quest log row says two things in one glyph: the shape is what the thing is β€” job, objective, beat, arc β€” and the color is what state it's in, so a caption never has to repeat what the icon already showed.

The second line has to earn its place. It says how far along you are (2 of 6), what a job pays while it's still a choice (Reward: 120 credits), or how long is left (1:30 left) β€” and falls back to the state only for Done and Failed, where the state is the news.

Quests written before kinds existed still render; they just say less. And because every glyph is asked for by name, a mission that ships its own icon sheet re-skins the whole log without touching a line of it.


πŸ“Š Readouts you can read at a glance

Numbers that were really levels are now drawn as levels: a label, the value, and a bar under both, in the same look as the engine's own ship-status panel.

  • Engineering's Efficiency is eight bars, in the grid's own colors - cyan when a system is tuned above 100, green, gold, then crimson - so the system that is hurting is the short red one, not the fifth number down.
  • Science (the new Science console) shows shields as bars that still say the number you read out loud, the enemy's four systems as a two-by-two block of bars, and each shield frequency as its own bar - with the weakest still called WEAK in words.
  • Quests. Pick one and its facts come first: how far along as a bar, its state, what it pays, how long is left - then the story. A story arc's steps are ticked, crossed or marked in progress, and steps the story has not revealed still stay hidden.
  • Help links to itself. "Ask Engineering" now goes to Engineering: follow a link in a Help page and the page and the topic list both move there.
  • Fabricate says why you can't build. Each material shows what you have against what the recipe needs - red when you are short - and a running build has a progress bar.
  • An upgrade's effect drains as a bar while it lasts, labelled with the time left.
  • The handheld's Scan reads a room's condition as a colored pip and its wear as a bar, and the same reading lands in the Survey log. Crew shows the face of whoever you pick and how hurt they are, as a health bar, and the "On the surface" list puts a face beside every name.
  • Letters come from someone. A message from a character in the story - the admiral, a crewmate - opens with that person's face beside their name.

Mission writers get all of this in their own text: see Gauges and Rich text areas.


🧭 Quests & Stories

  • A signal-driven quest engine with kill / collect / scan / dock / reach / arrive triggers and real rewards β€” all authored in simple AMD files.
  • A proper Quest Log you can accept and abandon quests from.
  • Multi-step bridge stories that unfold a step at a time.
  • Quest-driven end-game. A mission is now a tree of quests β€” a parent mission with children marked required (must finish to win) or fatal (fail one and the game is lost). Quests carry a Win: / Lose: outcome and a Fails when: trigger (a signal fires, a target dies, a timer expires), so the whole win/lose condition is authored in AMD, not hand-wired in script. The Siege bosses hang their objectives on the siege's mission tree with Part of:.
  • A quest log that isn't a spoiler. Show: says when a quest is listed, apart from when it runs β€” so a story beat can drive its event unseen and appear once it has happened. A converted 2.8 mission went from 48 rows of story to 9: the six things the crew can act on, and history accruing underneath.

Docs: Quests.


πŸ’± Items & Upgrades

One pickup can be worth several units. item_spawn(key, x, y, z, qty=24) stamps a quantity on the pickup and collecting it credits the lot β€” so a job wanting 24 salvage is a crate to fetch, not 24 collectibles scattered across the map. Pickups that don't ask for a quantity are unchanged.

  • Discoverable items and upgrades driven by a data registry β€” collect them in space and activate them through a generic Upgrades GUI.

Docs: Items & Upgrades.


πŸ† Game Results & Scorekeeping

The results board's Summary tab
Sixteen minutes of Siege, scored: kills, tonnage and damage, with Fleet, Air Wing, Quests and Enemies a tab away.

The end-of-game screen is now a tabbed results board with real scorekeeping β€” built for bragging rights, and for running a scored event.

  • Five tabs: Summary, Fleet, Air Wing, Quests, and Enemies.
  • Objective scoring. Every kill is credited to the ship (or pilot) that landed the final blow, and tallied three ways:
    • Kills β€” enemies destroyed.
    • Tonnage β€” naval-style "tonnage sunk," scaled by each hull's size, so bigger kills are worth more than swatting fighters.
    • Damage dealt β€” raw impact, so crews who soften targets without stealing the kill still show up.
  • Fleet ranks each bridge ship (kills / tonnage / damage / hull remaining); Air Wing ranks each fighter & shuttle pilot by call sign (sorties / kills / tonnage / objectives). Bridge and cockpit credit never double-count.
  • Every game is saved to a rolling history β€” with an even denser per-ship / per-pilot / per-quest breakdown than the screen shows.

Running a tournament? β€” for Convention Operators

Pair the scoreboard with repeatable seeds and a shareable game code: hand every crew the same code and they all play the identical map and enemy layout, then rank them by their Fleet / Air Wing boards (kills, tonnage, damage) for an objective result. The Director gives you a spectator / big-screen view for the venue floor.

Details: LegendaryMissions › Game features.


🀝 Friend or Foe β€” decided by Diplomacy, not by labels

A deep pass reworked how the game answers one deceptively simple question: "is this ship a friend or an enemy?" It used to be answered by hardcoded role labels β€” raider meant "the bad guys" and tsn meant "the good guys" β€” an assumption quietly baked into targeting, victory checks, docking, comms, loot, and quests. Now those questions are answered by diplomacy: the actual side relationships. A side's name no longer decides its allegiance.

What that unlocks:

  • Runtime ceasefires & alliances that actually stick. Negotiate peace with a hostile faction and it really stops being a target β€” your fleets break off, its ships stop counting as "enemies remaining," you can claim its systems, and killing it no longer pays a bounty. Ally with a faction and its bases count as your friendly bases.
  • Many enemy factions, not one lump. Enemies can field their own faction sides, each independently at war or at peace β€” instead of everyone sharing a single "raider" side.
  • More than one player side. Co-op or rival player factions work without assuming everyone is tsn: friendly stations, escorts, and win/lose conditions all derive from who the players actually are.
  • Quests that mean "enemies," not "raiders." A kill objective can be authored as destroy N enemies β€” faction-agnostic and ceasefire-safe β€” right alongside the familiar destroy N raiders.

For authors it arrives as a symmetric sides & diplomacy vocabulary β€” ask side_are_enemies / side_are_friendly, or grab the whole set of what's hostile (or allied) to a ship or to the players β€” so you never hardcode role("raider") or role("tsn") again. Every stock single-side mission plays exactly as before; the new reach is there when you want it.

Docs: Sides & Diplomacy.


🚒 Fleets, written down at last

Fleet ladders became per-race data in v1.4.0, but how you actually spawn one was folklore. Now documented: Fleets & raiding β€” prefab_fleet_raider, what a variant is, per-faction sides via faction_side, and the difficulty encoding, which has two forms nobody guessed:

You pass You get
0 the mission's DIFFICULTY setting
200 DIFFICULTY + 2 β€” a relative offset

🏷️ Icons have names now

gui_icon("icon_index:111;") asked you to remember that 111 is a wanted poster. Every one of the built-in sheet's 176 glyphs now has a name, and there is one call that takes one:

gui_icon_name("quest.job", "#cc0")

Two kinds of name, and the difference is the point. A look β€” square, bell, wanted β€” is what a glyph is. A meaning β€” quest.job, quest.state, list.expand β€” is what it's for, and points at a look. Ask for the meaning, and re-pointing it changes every screen at once.

The same names reach your own art. Register a cell of your sheet under a look and it wins over the built-in one:

gui_icon_add_atlas("wanted", media_shared("icons/quest-sheet"), 0, 0, 64, 64)

Every gui_icon_name("quest.job") in the game now draws your art β€” with no edit to the code that draws it. Screens can be written before their art exists, and an add-on can re-skin screens it doesn't own. An unknown name draws nothing rather than a plausible wrong glyph, because a wrong icon looks deliberate.

Claiming a look is deliberate: only the icon domain re-skins, so an ordinary image that happens to be called square or flag can't silently change every state pip in the game.

Or write the sheet as a fact sheet. An ## [Icons] section registers the same keys with no Python β€” the sheet, the cell size and the domain are written once on the section, so an entry is a single line:

## [Icons](icons)
---
icons
Sheet: icons/quest-sheet
Cell: 64
---

### [Job](wanted)
---
At: 0, 0
---

The same section type registers any atlas, not just icons β€” a card deck or a set of console backdrops is the same format with a different word (images, art) and its own Domain:. sbs lint checks them: a sheet that isn't on disk, an At: with no Cell: to measure against, a cell off the edge of the sheet. Each of those draws a blank widget in the game and says nothing, so the lint is where you find out.

The whole named set, with pictures: Icons by name.


πŸ—‚οΈ Console tabs stop drawing over each other

The tab strip divided a fixed width evenly, so more tabs only ever meant narrower tabs β€” and the engine does not clip text, it draws it anyway, over the neighbour. Every tab was present, clickable and illegible, which is why adding a tab broke ones that already worked.

Eight tabs are shown; the rest go behind a More (N) menu that behaves exactly like clicking a tab. BACK is never overflowed. Nothing to configure.

See The console tab strip.


⚑ Smoother with big fleets

Large battles used to stutter on a beat. Every few seconds the game processed every NPC brain in a single frame β€” then, on another beat, every mission objective β€” so the frame time spiked periodically. The bigger the fleet, the bigger the hitch.

That work is now spread evenly across frames. Each brain and each objective still updates on the same schedule as before β€” the AI is exactly as sharp and reacts just as fast β€” but instead of one big burst, the load is smoothed out tick by tick. On a busy Siege those periodic frame spikes are largely gone, and it scales far better as fleets grow. Nothing to configure; every mission gets it automatically.

For tinkerers β€” how it works

Per-tick work that iterated a whole set at once (all brains, all objectives) now runs a rolling slice each tick, sized by a shared RollingSlicer so a full pass completes in its declared period regardless of set size or tick rate β€” no batch spike. The slice is sized by the elapsed tick count, not by how often the host happens to call it (the engine makes about 15 calls a sim-second, the headless runner 6), which is how TickTask has always measured its own delay. If you're chasing a mission_tick that overruns its budget, the engine's Elapsed time log line now prints a per-phase breakdown (dispatch_tick / gui_present / gc / …) so you can see exactly which subsystem caused a given spike.

A fleet whose last ship is gone stops being a fleet. Every wave, every fleet a gamemaster puts together and every fleet-of-one a spawned raider builds used to leave an empty one behind when its ships died β€” for the rest of the run, each still thinking away with nothing left to command. They are cleared up now, a few seconds after the last ship goes. A mission that deliberately holds an empty fleet keeps it.


🎬 Quality-of-Life & Presentation

  • Shareable game codes and per-map seed options so crews can replay the exact same setup.
  • A nebula is the color it is named. Three of the seven stock colors were not: yellow came out chartreuse and read as a second green, and red and orange were a single indistinguishable dot wherever a map carried both. The clouds themselves were retuned rather than just their radar icons β€” the icon is drawn from the cloud, and the two are not allowed to disagree β€” so nebulae genuinely look different now in any mission using the stock colors.

Details: LegendaryMissions › Game features.


πŸ—‚οΈ One profile for every mission

A profile is one named file that decides how a mission runs β€” its settings, its add-ons, its art packs. Put it in data/missions/common_data/profiles/ and it applies to whatever you launch, so one file serves every mission and survives updating any of them:

sbs run server,science,comms profile=a28_skies -m WalkTheLine

That is a full profile, add-ons and art packs included β€” so the Artemis 2.8 skies follow you into any mission, from one file. A mission's own profiles/ folder still wins if both have the same name, so a mission can ship a definitive one.

Launch arguments know which mission they were for. profile=, map=, console= and var.NAME= belong to the mission you launched: restart into a different one and they stop applying, and the log says so, rather than a same-named profile over there meaning something else entirely. seed=, run=, record= and test= describe the launch itself and still apply. See Switching missions.

Sharing a game code does not share your ship names

A game code is for replaying an identical match, so it carries the map, size, seed and options β€” and not your crew's ship names, which are not part of the match and made the code several times longer. A setup you save does carry them.


Writing a mission

For anyone building one. The short version: far more of a mission is now something you write down rather than something you program.


✍️ AMD β€” write the mission, don't program it

AMD is the new way to author content: a mission's jobs, characters, places and story beats are written as plain fact sheets, in the words you would use describing them to someone. No script, no wiring β€” you say what a thing is, and it behaves that way.

A record says what it is, and that decides how it behaves. A Beat is a moment the crew lives through β€” it runs unseen and appears in the log once it has happened, as history. A Cue is a stage direction: it fires and is never listed. An Arc is the heading over a run of beats. A Job is taken by a ship; an Objective is the crew's. The word you choose carries the rest:

# [The Coils Overheat](ramscoop/coils)
---
Beat
Done when: signal ramscoop_online
---
Engineering reports the coils running hot.

One grammar answers three questions. Starts when: / Done when: / Fails when: all take the same thing β€” signal X, destroy 6 raiders, reach 6, 4, 5 minutes, all dead convoy, accepted, revealed. Learn it once and you can say when anything opens, finishes or fails.

Reward: and Penalty: say what a job pays and what failing it costs β€” so a timed job with something at stake is two lines.

Traits β€” what a record ALSO does. A worldlet is a Landmark that happens to yield ore; that second half is a trait, not a new kind of thing:

Landmark
Also: economy
Yields: ore 8
Reserve: 4000

Two traits ship: economy (Yields: Reserve: Price: Costs: Time:) and reputation (Values: Standing: Reliability: Rival when:). A Side and a Character are always regarded some way, so they carry reputation without asking; Also: is for the optional half.

See The AMD file format.


🎞️ AMD prose reads like a screenplay

The fact sheets were only half the file. The other half is the prose, and it learned six marks β€” borrowed from Fountain, the screenplay format, and Obsidian, where linked notes come from. A line the format does not recognize is still prose, always, so nothing you have already written changes.

A scene can hold a conversation. The speaker's cue goes in the body, the way a script writes it, so one scene carries as many voices as it needs:

@Ashfang
% You're a long way from friends, captain.

@Vell (comms)
(shaken)
He means it, captain.

(shaken) is how the line is delivered β€” write anything you like. (comms) is where it is delivered, so who-says-what-and-where is decided in the script instead of in code.

Prose can point at things. Talk to [[cmdr_vell]] before you reach [[ds1|the station]]. These are real references, so Find All References, Rename and the Story Graph all see them β€” and a link to something you have not written is not an error:

$ sbs lint <mission> --missing
3 thing(s) referenced but not written yet:
  cmdr_vell
      linked from `brief`   story.amd:6

Draft the whole mission as prose, link freely to scenes that do not exist yet, and let the linter hand you the list of what to write next. In the editor each one gets a Create this record quick fix.

And three smaller ones. = a note to yourself records what a beat is for β€” it shows in hover and the outline and never reaches a player. /* ... */ cuts a scene without deleting it. > [!WARNING] gives an in-fiction document in-fiction formatting. In a cutscene, FADE IN: and > CUT TO: say how a shot arrives without appearing on screen β€” so a cutscene file reads as a screenplay and is still the shot list the engine plays.

See Writing the body.


πŸ’€ Drops: β€” what a kill leaves behind

Loot is authored now, keyed by role, because what a ship drops follows from what it is:

### [Raider](raider)
---
Drops: salvage x2-4, contraband 20%
---

The defaults stay the defaults β€” a mission that authors nothing behaves exactly as before. What changes is that an author can see the answer and change it, instead of a condemned hulk on a live-fire range leaving contraband because it happens to be spawned hostile. Drops: none means this one drops nothing, which is not the same as having no table at all, and sbs lint flags a drop key that names no item.

See What a kill leaves behind.


πŸ“ Markers β€” naming a place

"Clear the asteroids in the shipping lane" needed the crew to know which asteroids, and prose written at authoring time could only name coordinates that go stale. So put the place in the world instead: marker_area for a region, marker_point for a spot, and marker_object for a spot the crew has to select.

See Naming a place.


⏳ A deadline you can hear coming

A job with a Fails when: clock calls in as it runs down, on comms, rather than counting itself out silently on a tab nobody is looking at. Marks are absolute β€” 5:00, 2:00, 1:00, 0:30 β€” filtered to the ones that fit under the deadline, so a six-minute job gets four reminders and a forty-five-second one gets two.

Who speaks is whoever the job asked for: Speaker: outright, else Held by: when it can talk (a station's job speaks with the station's face for free), else the mission's registered dispatch voice β€” and silence if none of those resolve, because a reminder from nobody is worse than no reminder. Signal says: gives it words, with {time} interpolated, so an automated beacon sounds like one:

Speaker: shuttle_pilot
Fails when: 6 minutes
Signal says: LIFE SUPPORT CRITICAL. {time} TO FAILURE.

See Deadline reminders.


πŸ“ž A beat that opens with a call

The reason hails matter for authors: somebody calling is a natural way to hand out work, and taking the job is what you say back. Both halves are AMD.

#### [Take the Case](brief)
---
Scope: shared
Starts when: revealed
Action:
  - ds1 hails ds1_brief
Then: reveal florbin/trail
---
### [DS 1 Briefing](ds1_brief)
---
Speaker: ds1
When: hail
Title: Ambassador Kidnapped
---
Ambassador Florbin was taken off this station inside a cargo container.

- [Take the case]() ; completes florbin/brief
- [Not now]()

The scene carries the words, the title, the portrait and the recorded line, so the beat only names it. Written bare β€” ds1 hails β€” it opens that speaker's When: hail scene. Who gets called follows Scope: and you never say: a shared beat calls every player ship, a per-ship beat calls its own.

What an answer means is written on the answer: accepts, completes or fails a quest, or signal for anything those three do not cover. It is resolved on the server, so it happens exactly once however many consoles are connected β€” no //shared/signal route to write, and no guarding against two officers pressing at the same moment.

Peacetime's Florbin case opens this way β€” the whole exchange is authored, with no hand-written MAST behind it.

Three smaller things came with it. At start: posting lists a quest without a working Accept button β€” a board you take by answering the call that offers it, not by pressing a button. Then: signal X now reaches other quests' Done when: signal X, which is what those two lines always looked like they did. And a mistyped scene key, an unknown outcome verb or an unrecognized Then: word are all lint findings now, instead of a beat that quietly does nothing.

Writing hails in AMD


🎭 Overlays β€” cards, HUDs & cutscenes on top of the view

A whole new overlay system for drawing on top of a console's page and its live 3D view β€” chapter cards, notifications, a modal choice, a live HUD β€” that update without repainting the page underneath.

  • One-liners for the whole toolkit. overlay_hero("CHAPTER TWO", subtitle="…") for a scene title (with a face, ship, icon, or image); overlay_lower_third(name, line) for someone speaking over the action; overlay_banner, overlay_toast (toasts stack β€” several coexist, each clearing on its own), overlay_letterbox and overlay_flash for a cutscene, overlay_credits(..., roll=) for rolling credits.
  • Aim it anywhere. Every overlay takes to β€” the current console, one client, or a role set: overlay_hero("FLEET ALERT", to=role("mainscreen")). Server story logic pushes straight to the players, or fires it with a one-line //shared/signal bridge.
  • A modal that returns a value. result = await overlay_choice("Fire?", ["Yes","No"]) β€” a full-screen card you can await, resolving to the button they pressed.
  • A live HUD over the view. overlay_hud(rows=…, controls=…) floats a sticky panel over the 3D view; overlay_hud_update refreshes just its values, no repaint.
  • Author overlays as data. Declare them in an .amd file and fire by key (overlay_amd("ch2")), and a quest fires overlays on accept / complete / fail to its participants β€” Complete Overlay: convoy_saved or an inline On complete: hero CONVOY SAVED, no wiring.
  • Your own cards, in pure MAST. A //overlay/<kind> route builds a custom card with the usual gui_* verbs and registers itself β€” no Python:
    //overlay/briefing
        gui_face(face)
        gui_text(f"$text:`{name}`;font:gui-5")
    

πŸ“‹ Richer GUI β€” tables, and text that does more

  • Gauges β€” the engine's own status-panel look. A label, a value and a bar that goes green, yellow, then red as it drains, and turns the engineering console's tuned cyan when a system runs over 100%. One line of MAST - gui_gauge(45, 120, "FRNT SHLD") - and set .value to watch it move. They work in text areas too, so a whole ship-status panel, ENGN / WEAP / SHLD / SENS grid included, can be written as a few lines of markdown:

    [Energy](gauge://946?max=1000)
    [FRNT SHLD](gauge://45?max=120)
    

  • gui_table. Describe a table as rows + column specs and get back a real, selectable, scrollable list box with the columns auto-sized to their content. Cells aren't just text β€” a column type can be a checkbox, dropdown, input, or button, and interactive cells write straight back to the row and fire an on_cell_change callback. An editable data grid in one call.

  • gui_list β€” design your own rows, in pure MAST. When a row needs more than columns (a picture, a button, two lines), write the row once as a with block and it repeats for every item β€” scrolling and selectable, no Python helper required:

    with gui_list(ships, select=True) as ship:
        gui_text("{ship.name}")
        gui_button("Hail"):
            jump hail
    
    Keep the handle to read what they picked: ship_list.get_selected().

  • gui_grid. with gui_grid(3): arranges items in even rows of N β€” a palette of buttons or a board of tiles β€” breaking to the next row for you. Great with a for loop.

  • Pipe tables, links & rules in gui_text_area. The rich-text area learned a few new tricks in its mini-markdown:

    • GFM pipe tables β€” | Ship | Hull | with a |:--|--:| alignment row β€” render as a real grid, columns sized to fit.
    • Hyperlinks β€” a [Torgoth](ref://torgoth) line, or a table cell, becomes a clickable link that navigates within the same document (give the area a link_resolver), so a Kralien entry can link straight to the Torgoth one. Perfect for a codex.
    • Icons β€” ![](icon://wanted) Bounty posted puts an icon at the start of a line or a table cell, and at the start of a list item the icon is the bullet: a checklist of ticks and crosses is just - ![](icon://check.on) Done, or declare [](bullet://check.on) once and the whole list follows. A face, image or ship can lead a line the same way, so a crew roster with a portrait beside every name is just - ![](face://...) Lt Marek - injured.
    • Sections that fold β€” ##+ Weapons is a heading you click to open; ##- Hull starts open. A long damage report or codex entry shows its headings first and opens what the reader asks for.
    • Grids β€” a table with an empty first row has no header, which is how the gauge grid above is written.
    • <hr> draws a horizontal rule.
  • Text areas use their full width. A long-standing measuring bug made gui_text_area wrap text at roughly 60% of the available width; it now measures properly and fills the space.

  • Rows and columns can size themselves. Add row-height: content or col-width: content and a row is as tall as its text, a label as wide as its word β€” at every window size, with no percentages to maintain. min-content and max-content give finer control, and 1fr is now the default: a column still shares the leftover space but is never squeezed below the widest word it has to show. (1fr is what CSS calls an equal share with a minimum; the older spelling auto still works.)

  • 2fr β€” ask for more than an even share. A flex row takes one share of the leftover space; write 2fr and it takes two, 0.5fr half, exactly as CSS grid means it. Rows of 1fr, 2fr and 3fr divide what is left 1/6, 2/6, 3/6 β€” so a panel whose list matters more than its prose can simply say so, instead of doing the percentages by hand and redoing them at every window size. Everything weighs 1 unless it says otherwise, so nothing you have already written changes.

    gui_row("row-height: 1fr;")   # the message
    gui_text_area(line)
    gui_row("row-height: 2fr;")   # the replies get twice as much
    gui_list_box(choices)
    
    Worth knowing if you are reading an older script: 2fr used to mean 2% of the screen, silently. 1fr was a whole keyword rather than a number and a unit, so 2fr fell through and was read as the plain number 2 β€” about 15 pixels. It never warned. Now it means what it says.

  • A Python button handler can read its data. gui_button(..., on_press=fn, data={...}) used to call fn() with nothing, so the data reached MAST variables and a Python handler had no way to see it. Declare a parameter and you are handed it. Handlers that take no arguments β€” the usual lambda _id=thing: go(_id) β€” are called exactly as before.

  • col-width: square. A column as wide as the row is tall. The other keywords derive a width from the column's own content; this one derives it from the other axis, which is what a portrait, an icon, a ship render or a badge nearly always wants. gui_face and gui_icon were already square; gui_ship and the image widgets were not β€” so a ship placed beside text used to flex and take half the row. Now they all say the same thing.

    Behavior change: square and an explicit width are mutually exclusive, and setting either clears the other. A square column that also carried a width used to be counted twice when the row was divided up, so the row reserved its space twice over and β€” since the engine never clips β€” drew the surplus over and outside its neighbours. If a screen of yours puts a col-width on a face or an icon, it now gets the width it asked for instead of that double-count.

  • overflow: for text that cannot fit. Because the engine never clips, text too big for its box is drawn over its neighbours. When there is genuinely no space to give β€” a user-entered name, a fixed strip β€” a widget can now say shrink (step the font down), ellipsis (truncate with ...) or hide. The default is unchanged, so nothing moves unless you ask.

  • layer: β€” paint order you can set. The other half of the same problem. overflow: changes the text; layer: changes what is drawn on top. Raise a row or section's background above its neighbour's content and an overflowing string is simply covered β€” not clipped (the engine cannot clip), but invisible, which is what a player cares about. It cascades like color and font, so one declaration can raise a whole panel. Two fixes came with it: a background could not be lifted above content at all before, and gui_image was silently discarding any draw_layer you gave it. Opt-in β€” a layout that never says layer: is byte-for-byte unchanged.

  • Text no longer overlaps itself in a scrolling text area. A gui_text_area measured its wrapping against the full width but drew 20px narrower to leave room for its scrollbar, so roughly one paragraph in eight gained a line the layout had not counted β€” and drew it on top of the next one. It also broke lines by estimating characters from an average glyph width, which ended lines short of the edge for no visible reason; it now wraps on measured words.

  • Sizes add up. row-height and col-width accept full arithmetic β€” 1em+10px, 62-25px. A + or - term used to be silently dropped, so a layout could have been running with a size it never asked for.

  • Text no longer draws on top of the row below it. Three separate causes, all now fixed: a content-sized column got the exact width its text needed and no more, so rounding tipped it into an extra line; raising one row to fit its text could starve its neighbours to zero height, and a zero-height row still draws; and a nested panel measured its own rows unwrapped, so it asked for less room than its content needed.

  • A list box no longer shrinks around its tallest row. Slot budgeting divided the available space by the largest item, so a single tall row could halve how many rows a list showed and leave half the box empty. Rows are now packed by their real heights; a list with equal rows is unchanged.


⏰ Timers can tell you when they are done

A timer was always something you had to ask about. You set one, then wrote a loop that woke up to check it, or an await delay_sim in a task that existed for no other reason than to hold the wait. Now a timer can just say so:

== start_repairs ==
    set_timer(SHIP_ID, "repair", seconds=30, signal="repair_done")
    ->END

//shared/signal/repair_done
    repair_ship(TIMER_AGENT_ID)
    ->END

And set_interval is the same idea on a loop β€” a heartbeat that keeps its period until you call clear_interval:

set_interval(SHIP_ID, "patrol", "patrol_beat", seconds=30)

Each emit carries TIMER_AGENT_ID, TIMER_NAME and TIMER_COUNT. Handle it with //shared/signal if it does anything β€” a plain //signal route runs once per console, which for an interval means one beat per console per beat.

It is opt-in on purpose, and that is what makes it cheap. Timers stay what they always were β€” one number in an agent's inventory, costing nothing β€” and only the ones you ask to speak are watched. Because an armed timer knows its deadline as a number, the library keeps just the earliest one and a tick costs a single comparison, no matter how many are running. That is less work than the watcher task it replaces, which had to be resumed every tick for as long as it waited.

The timer itself is unchanged, so a countdown widget reading format_time_remaining and a route waiting on the signal can share one timer. Clearing it, re-setting it without a signal, or deleting the agent all cancel quietly; timer_add_time carries the signal along with the deadline.

See Timers and counters.


πŸ—‚οΈ Art that lives once

Shared art used to be copied into every mission that used it β€” LegendaryMissions' 27 MB of backdrops and card decks became 314 MB across twelve missions, and a re-release left every copy stale. Now a media pack is unpacked once beside the libraries, and missions read it there.

{ "shared_media": ["artemis-sbs.LegendaryMissions.media.v1.4.0.zip"] }
gui_image_add_atlas("card_back", media_shared("casino/terran_back"))

media_shared() looks in your mission's own media/ first, then in each pack you declare β€” so overriding one file is dropping your own copy in, and nothing hardcodes the unpacked path (which carries the version). Publishing a pack is a zip entry in __lib__.json; export-ignore then keeps the art out of the source archive consumers download, so a fetch no longer drags along 27 MB nothing reads.

272 MB reclaimed. Guide: Shared media.


🌐 Web pages, written in MAST

  • Author browser pages with //web/<path> routes using the same gui_* layout you already know, and open them in a browser while a mission runs β€” live pages that update in place and are parameterized by the URL query (/web/scores?title=Hi).
  • Living pages (leaderboards, dashboards) update during the game and are kept as a snapshot for after it.
  • Bake a read-only page to a standalone HTML file, front many engines from one address (/web/<engine>/…), or run it from a non-engine MAST tool β€” all with no engine changes.

Docs: Web pages Β· Serving web pages.


πŸ§ͺ Mods β€” ships, races and art from an add-on (experimental)

An add-on can now add ships the game does not have β€” with their stats, interiors, fleet ladder and race β€” without editing a single file in your Cosmos install.

The idea that makes it work is a split by who opens the file: MAST reads happily out of a .mastlib, but the engine needs real files on disk, so a mod's ship data and art travel in a shared media pack and everything else in the mastlib.

your ships, in the media pack
'#ship-list':
- key: dw_scrapper           # YOUR key - never one the game already owns
  side: Driftwake
  origin: Driftwake
your add-on
sbs.add_extra_ship_data("dw_ships", pack_folder)     # the engine learns your hulls
grid_merge_ascii(media_read_relative_file("dw_scrapper.grid"), "dw_races")
fleet_table_load_yaml(media_read_relative_file("dw_fleets.yaml"), "dw_races")

Add your race to PLAYABLE_RACES / NPC_RACES rather than replacing them and your ships fight alongside the shipped ones instead of replacing them. Two mods that pick distinct key prefixes can be installed together.

What works: ship data, interiors, fleet ladders, races, sides β€” all verified in the engine. What does not, yet: hull art. The path mechanism is in place, but generating derived art from a bare .obj crashes the engine, so for now a mod points artfileroot at art the game already owns. There is also a trap worth knowing before you start: never commit a .paxmesh β€” it bakes its texture paths, so a committed one points at its author's disk.

Marked experimental because the engine side is still moving. Full walkthrough, including publishing on GitHub or as a plain zip: Making a mod.


⚠️ The races add-on β€” add it to your story.json

This one needs action. Ship interiors and fleet compositions used to be built into the game; in v1.4.0 they ship as per-race add-ons, and a mission has to ask for them.

If your mission loads LegendaryMissions' ai or fleets add-on, add one line:

story.json
"artemis-sbs.LegendaryMissions.races.v1.4.0.mastlib"

Leave them out and two things break quietly. Your player ship gets a dead Engineering console β€” no system nodes, no damcons, no internal damage β€” and fleet_create finds no ladder for any race, so nothing raids you. Neither failure prints an error, which is exactly why it is worth checking now.

New missions from sbs create already include it.

What you get in return is content that was previously unreachable. Interiors lived in the engine's own data/grid_data.json; fleet ladders were Python literals behind an if race == "..." chain. Now a race owns both, "random" picks from the races that actually registered, and a new race joins the rotation by existing. Two settings β€” PLAYABLE_RACES and NPC_RACES β€” turn races off per mission.

Every hull that declares an interior now has one, so ships that were never really flyable β€” Kralien, Torgoth, Skaraan, Biomech and the pirates β€” have a working Engineering console for the first time.

See The races add-on.


Stop guessing what a widget looks like. The Control Gallery is a mission you start: 61 entries in six categories, each one live on screen with the code that built it directly underneath.

  • The snippet cannot be out of date, because it is not a copy. It is sliced out of the mission's own file at runtime between two comment markers β€” so what you read is literally what drew the thing above it. Copy puts the real line on your clipboard.
  • It opens on the server screen. No console to pick, no ship needed. Start the map and browse.
  • A Traps category, which may be the most useful part: mistakes that produce a plausible screen and therefore survive review β€” a 1em row under a bigger font, padding eaten out of the row height, a starved content row, update() quietly dropping the rest of your style string, a handler built in a for loop that captures the wrong item. Each runs broken and fixed side by side.
  • A layout playground where dropdowns set row-height, col-width and the font and the boxes move under you β€” sizing is one of those things you have to push around rather than read about.
  • "Take the tour" walks all 54 entries, narrating each through the overlay system's own lower third β€” the gallery introducing itself with the feature it was originally built to demo.

One entry is a record in a .amd file plus a marked span in the code, joined by a key, so adding to it costs nothing.

sbs debug control_gallery --map 0     # then http://localhost:8765/server

Docs: The Control Gallery Β· Repo: artemis-sbs/control_gallery.


The tools around it

Launching, editing, testing β€” the things you use around the game rather than in it.


πŸš€ Launch it without clicking β€” sbs run and command-line arguments

Cosmos can now be started with arguments, and a mission can read them. A shortcut, a batch file or a CI job can bring up a full bridge on a particular map with particular settings, untouched by human hands.

sbs run                                        server + five consoles, nothing to click
sbs run comms,weapons                          just those two
sbs run -m LM_TestRange map=sandbox            a mission and a map
sbs run --dry-run                              show the command lines, launch nothing

The mission comes from -m (default LegendaryMissions) and is passed as defaultmission=, so preferences.json is no longer edited to choose one. Consoles are selected per process instead of by rewriting a file inside the game install β€” two runs at once no longer fight over it, and a crash cannot leave it changed.

Anything you add on the end reaches the mission, because the engine hands unrecognized key=value arguments straight to script:

map= console= start a map, open a console
profile= var.NAME= settings, in bulk or one at a time
seed= run= reproducible runs, labelled
record= transcribe what you click
test= a pass/fail verdict from the real engine

Settings merge defaults < settings.yaml < profile= < COSMOS_SETTINGS < var.NAME=, so a profile file carries the bulk and the command line names it:

sbs run -m MyMission profile=soak var.DIFFICULTY=3 var.AUTO_PLAY.enable=true

test=30 is the one worth knowing about if you automate anything: the mission plays for that long and writes records/verdict.json, which means the real engine can be checked by a script rather than by someone watching it. It counts runtime errors, not MAST coverage β€” sbs debug . --test remains the stronger check for whether your mission actually did anything.

Anything that matches nothing says so, rather than quietly doing nothing.

β†’ Command-line arguments


πŸš€ Start a mission in one command

Writing your first mission used to begin with "download this repository, rename the folder, edit these four files, then work out which libraries you need." Now:

sbs templates              # see what you can start from
sbs create MyMission       # make one β€” libraries and all

There are five boilerplates, and they are whole missions rather than empty shells:

Template What you get
minimal One @map and a line of narration β€” the smallest thing that runs.
sandbox Two sides, a station in an asteroid field, player ships, and raider waves that ramp with the difficulty setting.
addon A shareable add-on β€” provides / requires, packaged as a .mastlib β€” plus a harness map to run it.
amd Quests and science scans authored as data in a .amd fact sheet, with MAST holding only the logic that reacts.
ou A whole procedurally generated universe, built on the OpenUniverse engine.

It won't hand you a mission your game can't launch. Missions are pinned to a release line, and everything a mission loads comes from the same one. sbs create picks the newest line your install already has libraries for β€” capped by your version of Cosmos β€” tells you which it chose and why, and never reaches for a newer one just because it exists. Not every template is on every line, either: addon, amd and ou need v1.4.0 language and library features.

The templates live in the mast_starter repository and are read straight from it, so new ones show up without updating the tool. β†’ Creating a mission Β· The sbs CLI


🧰 The AMD editor knows the format

The VS Code extension reads AMD as a format rather than as colored text:

  • "This is a" picker β€” a record's word is visible and settable, grouped Story / Work / Content, and it tells you what choosing it means ("scope: shared, show: when done").
  • Completion answers where you are: nouns on the fence's first line, field labels on a fence line, that field's values after the colon.
  • Hover explains the field β€” the format's own words for it and its allowed values.
  • Your mission's own vocabulary is learned, by reading the Python that declares it (never running it), so a mission's private fields get the same widgets and lint as the core ones.
  • Edits are safe β€” changing one field leaves the kind line, // notes and list continuations exactly as you wrote them.

🎨 Design a screen without writing the code β€” the GUI Editor

Lay out a console screen visually and it writes the MAST for you. Drag sections, rows, buttons, lists and tables from a palette, size a section by dragging its corner, rearrange the tree, and watch a live preview β€” then Copy the generated code or Insert it into your .mast (it updates just a # <gui-designer> block if you have one).

  • *.gui.mast files open as the editor β€” the whole file is the screen, edited visually and saved back as MAST, with a one-click toggle to the raw text and back.
  • Round-trips β€” reopen a screen and keep editing; your comments and anything the editor doesn't model are left untouched.

Docs: The GUI Editor.


🐞 Debug your mission β€” pause it and look inside

Ever chased a bug by sprinkling print lines and replaying? Now you can pause your mission while it plays and look inside it β€” right in VS Code. Put a pause point on any .mast line, play your mission, and it stops there: the exact line, every value your story is tracking, and how it got there. Then step through one line at a time and watch it unfold.

  • One click. Pick "MAST: Debug mission (one click)" and press play β€” the mission launches with its game window, you set pause points and debug, and pressing Stop shuts it all down. No terminals, no leftover processes to hunt for.
  • Proper debugging tools. Pause points that stop only when a condition is true (hp < 20), on the Nth hit, or that just log a note without pausing; step over / into / out; a live Variables panel (Task / Shared / Global) you can edit while paused; a call stack; and watch expressions.
  • Step into the engine. On a line that calls a built-in like terrain_to_value(...), Step Into drops you into the engine's own Python β€” real source and its values β€” then Step Out brings you back to your story. Even library code that ships zipped shows its source.

It costs nothing when you're not debugging β€” normal mission runs and the shipped game are completely unaffected.

Docs: Debugging your mission.


πŸ“„ Read your mission on paper β€” sbs docs

Your .amd files hold the quests, the dialogue, the cast, the lore. Until now the only thing that could read them was the game, so reviewing a script meant opening a code editor.

sbs docs . --lens all --pdf

Four editions, because an .amd file is a different document depending on who is reading it: prose (a manual or story book), catalog (a sourcebook, where every field is rendered as its type β€” a color as a swatch, a reference as a working link), screenplay (a script you could read aloud), and bible (the quest spine, its triggers, and a map of the branches).

--profile player leaves out everything a player should never see β€” author notes, the condition on a dialogue choice, what a choice secretly costs β€” and genuinely leaves it out of the file, so you can hand the PDF to someone. The bible has no player profile and says so: the bible is the spoiler.

PDFs need no extra install; --pdf drives the headless browser you already have. Character faces composite properly into the page. See Printing a mission.


🩺 sbs doctor β€” check your setup

Reports what is installed, what a mission expects, and what is missing β€” with the command that fixes each one. It checks your setup, never your writing; for that there is still sbs lint. See Checking your setup.

It ends with the arithmetic, so you can tell at a glance whether anything needs doing without reading forty rows:

17 checks: 14 ok, 3 optional absent, 0 problems

and when something is wrong, a second line names which parts of the report to look in. An optional library that is not installed is counted separately from a problem β€” it is not a fault, and one combined number would make a healthy machine look broken.


🎨 sbs art β€” the art the game builds for itself

Some of a ship's art is not drawn by an artist. The engine builds it the first time it shows that hull β€” a .paxmesh and the flat 1024/256 sprites β€” and saves it beside the original. It is never packaged, because a built mesh remembers where its textures were and shipping one points somebody else's install at a folder that is not there.

If the game is interrupted while it is doing that, it leaves the job half-finished β€” and a half-finished hull crashes the client every single time anyone looks at it. The engine tries again, fails in the same place, leaves the same mess, so it never recovers on its own. Three hulls were stuck like that in one install and it took two separate crash hunts to find them; the fix was throwing the partial files away and letting the engine start over.

sbs art check              # anything half-finished?
sbs art clear              # throw the half-finished bits away
sbs art bake               # ...and drive the engine to rebuild them
sbs art bake --undrawn     # also build art nobody has looked at yet

clear touches only what the engine made β€” your .obj and textures are never removed. bake runs the engine once per hull, so a crash costs one hull instead of the whole batch, and finishes with the list of hulls that still will not build: "the server keeps dying" becomes three names.

--undrawn builds ahead of time, which is the real defence β€” every build that has already happened is one that cannot go wrong mid-match.

A hull listed as not yet drawn is normal, not a fault: it is simply art nobody has looked at. sbs doctor runs the same check as part of its report. See the CLI.


🚒 Add-on ships: a file that exists, not a file that gets written

An add-on that adds hulls ships its ship data in its media pack and names it β€” a file that exists, never one the library writes into your mission folder:

ship_data_add_extra("turrets/extraShipData_turrets", mod="MyMod")

One call points both readers at that one file: the engine (which resolves artfileroot, and is what makes a behav_station fire at all) and sbs_utils' own table (which is what filter_ship_data_by_side and headless use). Nothing is written.

It has to be a media pack rather than a mastlib because a mastlib is a zip and the engine cannot read inside one β€” which is exactly what the writing was working around.

See Making a mod.


Under the hood

Library and API changes. Nothing here needs your attention unless a mission of yours misbehaves in a way one of them explains.


πŸŽ›οΈ A console can draw the grid buttons itself

The engineering grid_control widget is a rectangle the engine fills: no scroll, no row height, no styling, and a menu taller than the box loses the rest. The buttons in it are not the engine's, though β€” they are your //comms/grid routes. Three accessors let a console draw them:

comms_grid_buttons(origin, selected)    # [{index, label, color, icon}]
comms_grid_press(origin, selected, index, client_id=None)
comms_grid_revision(origin, selected)   # for an `on change`

Grid comms is unchanged β€” the routes, the expansion, the one-shot and sticky rules and the press handling all behave exactly as they did, and the widget keeps being fed for any console still showing it.

index is the position in the unfiltered list, because that is what the press path looks a button up by. A button hidden by its if, or a * already used, is skipped when you draw but still consumes one β€” so pass back the index from the row, never the row's position.

A button can name an icon in its own format block, which needs no new syntax:

+ [red icon:wrench] "Fix now"
+ [icon:person] "Workout"

It is icon: rather than a comma slot because =$raider red, white already means a two-colour format. The engine widget never sees the marker.

Two more for building consoles that fit the screen they are on:

  • gui_cycle_button(states, value=, label=) β€” one target that shows the current state and advances on each press, for a setting with a handful of values. Read .state. Inside a tabbed panel or a listbox row it does not repaint itself β€” the container does, which for a tab means returning a redraw from its tick.
  • gui_screen_revision(client_id) β€” what an on change watches to rebuild a console after a resize. A console that decides anything from the screen reads it while building, and screen_size re-presents without re-running the builder, so those decisions used to stay as they were. Banded to 100px so dragging a window rebuilds once per band rather than once per reported size.

Docs: Making add-ons, Damage.


πŸ”’ _private helpers in an add-on's Python are private to their file

Two add-ons can both define _label, _CACHE or class _Row and never meet.

A leading underscore now scopes a top-level name to the .py file that defines it. Only names without one are published for the rest of the mission's Python to call by bare name, so a private helper needs no prefix and can't be taken over by a same-named helper in an add-on you didn't write. Ask for one explicitly and you still get it:

import hangar_bays
hangar_bays._pick_bay(craft)

Public names are unchanged β€” shared across the mission's .py files and resolved when the call runs, so the cross-file calls add-ons rely on keep working in both directions whatever the load order. sbs lint's ns-duplicate-function is still what tells you two add-ons have claimed the same public name.

Docs: Making add-ons.


πŸŽ–οΈ Orders by capability - one module every menu asks

An order is a MAST objective label under objective/orders/ that says what it needs and what it may be aimed at:

requires: move, weapons
valid_for: hostile

orders_available / orders_can_take answer for the comms chip, the right-click menu and drag alike, so they can never disagree. Capabilities come from what the object is (a behav_station does not move; a stock station does not fire) plus addon providers, with per-object overrides and the no_orders role for story ships. An order can list instances: to appear once per wing, bay or whatever else the provider returns. Order markers (marker_order_drop) give "go there" orders something to aim at.

Also in the library: every spawn records its behave_id, and every behav_selection object carries the __selection__ role; and a Vec3 compared with a non-vector is simply unequal, which fixed default x = ... stopping a route when x already held a position.

Docs: Orders, Naming a place, Turrets.


πŸ› οΈ For Mission Makers & Tinkerers

For testing only β€” not an engine replacement

The mock GUI and headless runner exist to test and debug missions outside Cosmos. They are not a reimplementation of the engine, and 100% parity is not a goal β€” many engine behaviors are approximated or absent by design. Please don't file issues or feature requests asking for more engine parity. If something must behave exactly like the engine, verify it in Cosmos.

  • Run missions outside Cosmos. A full headless test mode plays a mission for N seconds and reports a pass/fail verdict with coverage β€” great for CI β€” plus a browser-based mock GUI with a 3D cinematic view and 2D radar. β†’ Testing missions
  • Web pages, written in MAST β€” see above. β†’ Web pages
  • A coverage "exerciser" that drives real gameplay (scans, torpedoes, docking, comms) to shake out routes automatically. β†’ Testing missions
  • A faithful mock simulation calibrated to the real engine: ship speeds, 3D steering, per-facing shields, heat, energy, and a full weapons model (beams, torpedoes, drones, mines, EMP). β†’ Testing missions
  • --use-working-tree to smoke-test local library edits, and --seed for reproducible runs. β†’ The sbs CLI
  • sbs compile can gate a build. It used to print an error and exit 0, so any script or CI reading its exit code saw success on a compile that never happened. It now reports the errors and exits non-zero. (It still can't see a multi-line { } β€” only a --test run finds that.) β†’ The sbs CLI
  • An in-game Avatar Editor β€” an opt-in addon that customizes a character face inside Cosmos, with a live gui_face preview that updates as you move the sliders (unlike the extension's blind builder). Pick a race, tweak each feature, and the face is copied to your clipboard on every change β€” paste it straight into a .amd Face: field. β†’ Avatar Editor addon
  • sbs swap β€” keep several mission sets side by side and switch between them without copying anything (see below). β†’ The sbs CLI
  • signal_next β€” one-shot await of the next signal. β†’ Signals
  • once routes, and creates that can't duplicate. //shared/signal runs a route server-once per emit β€” it never promised the signal is only emitted once, so setup that got emitted twice used to build everything twice. Now mark a route //shared/signal/give_starting_cash once and it runs at most once a mission. Better still, where the thing being made has a natural name, creating is idempotent: player_ensure(slot, …) and AMD landmarks and characters key off the slot or the record's own key, so asking twice gets you the same ship, station or person β€” while a deliberate rebuild after a reset still works. β†’ Signals
  • Bring an old Artemis 2.8 mission forward. pip install arme2cosmos, point it at the old XML, and it writes the Cosmos mission for you β€” as a quest tree, or as plain MAST you can edit β€” plus a notes file listing anything worth a second look. It's ordinary Python, so you don't need Cosmos to run it. Porting by hand instead? The a2x_* helpers are waiting in MAST, no import needed. We tried it on 27 of the old missions and they all run. β†’ Porting from Artemis 2.x
  • A faster, friendlier MAST compiler β€” quicker parsing, all errors reported at once, sturdier crash handling, more Python built-ins available in scripts, and now multiline expressions (see below). β†’ The MAST language
  • Declare addon dependencies β€” provides / requires / suggests make cross-addon contracts explicit and compile-checked (see below).
  • Deterministic building blocks β€” keyed terrain fields, position-keyed scatter, and reusable game-code encode/decode.

✨ New β€” sbs swap, switch mission sets in place

Cosmos loads exactly one data/missions folder. If you keep more than one set β€” a converted 2.8 port, a work-in-progress copy, the stock missions β€” you have been copying folders around. Instead, park each set beside it as data/missions_<name> and let data/missions be a link:

sbs swap            # which set is active, and what else is available
sbs swap amd        # load data/missions_amd
sbs swap mast       # load data/missions_mast

Any folder named missions_<name> is a valid target, so adding a set is just creating the folder. If your data/missions is a normal folder, the first swap renames it to missions_cos rather than deleting it, so sbs swap cos puts you back on the stock missions. Close Cosmos first β€” a running client holds files open under the link. β†’ The sbs CLI

Back up your missions first

This moves and re-creates the data/missions link, and renames a real data/missions folder aside. It is written to never delete a mission folder β€” only the link β€” but it is rearranging the folder that holds all your work. Take a backup of data/missions before your first swap, especially if you have edits in there that aren't in source control.

✨ New β€” multiline expressions in MAST

Python dicts, lists, and function calls can now span multiple lines, just like Python. No more cramming a spawn onto one giant line or reaching for ~~ ... ~~ fences β€” write it the natural way:

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,
})

Works for any bracketed expression β€” dict/list/call literals and multiline if conditions alike. Error line numbers stay accurate, and every existing script compiles exactly as before. β†’ The MAST language

✨ New β€” declare addon dependencies

Addons share one global namespace and load in no fixed order, so "this addon needs that one" used to be an unwritten rule that failed with a cryptic NameError. Now an addon states its contract at the top of its __init__.mast, checked at compile time:

provides hangar.sortie_board
suggests hangar        # optional: warn if absent, never fail
requires gamemaster    # hard: fail the build if it's not loaded

A requires not satisfied by some loaded addon's provides fails the compile (caught by sbs lint / --test, and shown as a runtime error screen); suggests only logs a warning. Checking is order-independent and fully backward compatible. β†’ Making add-ons

✨ New β€” colons inside quoted strings

A : inside a quoted string no longer confuses the parser, so you can write handler labels naturally β€” no more assigning the button to a variable first:

on gui_message(gui_button("Test Upgrades:")):
    show_upgrades()

Previously the : in "Test Upgrades:" was mistaken for the end of the on ...: header and the line failed to compile. The same fix applies to await ...: blocks, and button/text labels with colons (gui_button("Score: 5")) now render as text instead of being misread as a style key.

⚠️ Don't call sbs.delete_object β€” use delete_object instead

Deleting an object with the raw engine call sbs.delete_object(id) is a known way to crash Cosmos to the desktop. Delete through the procedural delete_object(id) (or obj.delete_object()) instead:

# βœ— risky β€” frees the object immediately
sbs.delete_object(DAMAGE_TARGET_ID)

# βœ“ safe β€” tombstoned now, freed safely later
delete_object(DAMAGE_TARGET_ID)

Why it crashes. sbs.delete_object frees the native C++ object β€” and its engine_object/data_set pointers β€” the instant you call it. But MAST tasks run interleaved across a tick: another task (or even the next line, in a different task) may still be holding that object. When it touches the freed object it reads dead memory β€” a use-after-free that crashes to the desktop, or, if the memory slot has been reused for a new object, silently corrupts that unrelated object and crashes later somewhere else.

Why delete_object is safe. It tombstones the object immediately β€” object_exists() and to_object() report it gone at once, and it stops ticking β€” but defers the actual native free to the end of the event handler, once every task for that tick has finished. So a reference held elsewhere this tick still points at valid memory instead of crashing. It also does nothing if the object is already gone, so a double delete can't double-free. Scripts that already use delete_object / obj.delete_object() need no change β€” only direct sbs.delete_object calls should be swapped.


🧡 A handler no longer depends on which task built the widget

on gui_message, on change and on_press= all belonged to the task that built the widget. That is invisible while the builder is the console's own GUI task, and confusing the moment it is not β€” which is exactly what gui_sub_task_schedule is for. Three changes, all on by default:

  • await gui() reached off the GUI task used to hang the calling task forever on a promise the page never adopted, print a bare print() nothing reads, and strand the task in gui_task.sub_tasks. The screen still drew, so it looked like the flow died rather than like a hang. It now sends the GUI task to the screen the handler just built, superseding β€” never resolving β€” the promise the GUI task was parked on.
  • on_press=<label> can now be a sub-task by default, which is what is_sub_task=True always meant. is_sub_task is deprecated; an explicit False still works. The knock-on is the answer to the question everyone asks: a handler label now just ends with ->END. Under the old jump form ->END ended the console.
  • on change inside a scheduled label now fires. It belongs to the GUI build, like every other handler, instead of dying with the task that registered it.

A fourth fix came out of the same work: on_new_gui ended every hosted handler hanging off the GUI task β€” including the one that was currently painting. A handler that repainted killed itself on its own first tagged widget and never reached its await gui(). It could not tell "the GUI that owned me is being replaced" from "I am the one replacing it".

New reference page: Handler lifetime β€” which task runs each form, how long it lives, how it should end, and when you still need gui_task_jump.


πŸ”˜ A button's handler no longer dies with the task that built it

A widget's handler belongs to the task that built the widget: on gui_message(w): compiles to an inline block inside that task's label, and on_press=<label> is a jump on that task. So a builder that was scheduled and then ended took its handlers with it β€” the button drew, the click did nothing, and nothing was logged anywhere. That silence was most of the problem.

    await task_schedule(build_panel)     # this REQUIRES the builder to end

Handlers now run in that case, and if one still cannot, a warning naming the source site goes to mast.runtime.log.

A second fix rides along and reaches further: an on ...: block whose body falls off its end (no jump, no ->END) never gave the task back, so it never returned to its await. The visible face of that was a self-repainting panel β€” a live countdown, a refreshing status β€” freezing permanently on the first press. If a block of yours ends in jump, it always escaped this and is unchanged.

gui_message_callback(widget, fn) remains the option with no task in the path at all, and is still the safest choice for a handler built somewhere awkward.


πŸ’₯ A broken expression stops, instead of quietly becoming None

None is a perfectly good MAST value β€” default ship_art = None is ordinary β€” so when an expression blew up, the runtime had no way to say so. It logged the error and then handed the line None anyway, and the line ran with it:

    range_to_base = ship.pos.distance(base_pos)   # `ship` is a typo
    if range_to_base < 5000:                      # None -> takes the ELSE
        ...

The assignment wrote None β€” and if it was shared, every other task in the story now read None too. An if slid into its else:. Worst of all, for x in <broken>: went on to do iter(None) and reported a second error, on the same line, about a NoneType β€” and that second one is the one people saw. The real cause was three messages up, if it was still on screen at all.

Now the line simply does not run. The assignment does not happen, the if does not choose a branch, the loop does not iterate, jump X if <broken> neither jumps nor falls through. The task ends there, and the first error in mast.runtime.log is the real one.

The message improved too. It names the exception, quotes your expression, and no longer prints the word None where the offending code should be:

mast RUNTIME ERROR
      line: 12 in file: story.mast
      label: patrol_logic

NameError in expression:
     ship.pos.distance(base_pos)
name 'ship' is not defined

And it knows about the trap that reads as "my variable vanished":

    assigned = sbs.get_ship_of_client(client_id)   # assigns to NOTHING
    ok = assigned == foe_id                        # NameError, reported HERE

shared, assigned, client and temp are scope keywords, so assigned = x parses as the assigned scope with an empty target. The error now says so by name instead of leaving you to find it. (Rename the variable β€” assigned_ship is fine.)

Nothing to change in your scripts. A mission with no broken expressions behaves exactly as it did; a mission with one now tells you which one.


⚠️ List boxes: row-height is the row's height

This one may move your layouts. On a list box, row-height used to be the gap added after each item β€” the height came from whatever the item template opened. It now means what it says everywhere else: the height of one item row. The spacing between items is a new key, item-gap.

lb = gui_list_box(items, "row-height: 1.6em; item-gap: 0.1em;", item_template=row)

If you declared row-height and meant spacing, rename it to item-gap and nothing moves. If you declared it and the template already set its own row height, the list was rendering at double pitch and will now tighten up β€” including every gui_table, which handed one style string to both.

It is a floor, not a cap: a two-line item still grows past it. Declare neither and nothing changes at all.

Worth the churn because the old meaning was not only confusing, it was wrong in a way you could not see: an item's click region comes from its row box, so a list could draw text you could barely click, and a template whose rows declared no height collapsed to nothing and took its hit area with it.


πŸ—ΊοΈ @map works without LegendaryMissions β€” a map picker in the library

@map is how a mission offers several entry points, and until now it only worked if you loaded LegendaryMissions. Both halves lived there: LM's server console drew the selection screen, so a mission loading only sbs_utils came up with an empty server page and no way in, and the headless runner's --map had nothing to start.

Two functions close that, and a mission front-end is now two lines:

== main ==
    chosen = await gui_map_picker()
    map_start(chosen)

gui_map_picker() is a carousel of your story's maps β€” name, description, next/prev β€” together with that map's Properties: panel and a Start button. map_start() launches what you were handed: applies the map's Defaults:, resumes the sim, schedules the map, sets GAME_STARTED and emits game_started. That launch sequence previously existed twice, in LM's console and in the runner, and the two had quietly drifted apart β€” down to whether the sim resumes before or after the map is scheduled. There is one definition now.

The properties panel is on by default, because a map declaring a PLAYER_COUNT slider expects that variable to exist when it runs. Skip the panel and it starts unset β€” which looks like a mission bug, not a missing feature. Pass properties=False if you really do want just a carousel and a button.

Conditional maps are now honoured. @map/secret "Secret" if UNLOCKED was previously offered whether or not UNLOCKED was true; maps_get_list() evaluates the condition and hides it. Two carve-outs: with no task in context (the headless runner) maps are shown, and maps_get_list(include_hidden=True) still returns everything, so a saved game code naming a currently-hidden map keeps resolving. If you rely on --map 0, note the index counts the filtered list β€” prefer --map <name>.

Do not ->END straight after map_start

The task running the picker is usually main, and main is the page's GUI task. The started map calls gui_task_jump to show its own page, and that call is silently discarded if the GUI task has already finished β€” no error, blank screen. Park the task instead:

---running
    await delay_sim(1)
    jump running

This is the light tier on purpose. LM's console also does game codes and presets, music, the player roster, operator mode and difficulty-scaled beam damage; it keeps all of that. The picker is for missions that must stand on sbs_utils alone β€” which is exactly what an add-on's own test mission needs, so a mod repo can now ship a mission that runs from a fresh clone with nothing else installed.

See Map picker.


Thanks for playing, building, and tinkering. There's more under the hood than ever β€” go build something great. πŸš€