Profiles
A profile is one named file that decides how a mission runs — its settings, and which add-ons and art packs it loads. You select it by name and nothing else changes:
Artemis3-x64-release.exe autostartserver defaultmission=LegendaryMissions profile=a28_skies
python -m cosmos_dev.mission_runner . --test 20 --map 0 --profile a28_skies
It lives in one of two places, searched in this order:
| path | authored by | scope |
|---|---|---|
<mission>/profiles/<name>.yaml |
whoever wrote the mission; ships with it | that mission |
<missions>/common_data/profiles/<name>.yaml |
the operator; not inside any mission | every mission |
Both are full profiles - settings, addons: and media: alike. The mission's own wins a
name collision, so a mission can always ship a definitive profile under a name an operator
also uses. Which one answered is logged.
The shared tier exists because an operator's house setup is not mission content: written
into a mission folder it needs a .gitignore line and does not survive a re-extract, and
common_data sits beside the missions instead. A shared profile can select content,
which is most of the point - addons: include: falls back to __lib__ and a media pack
matches the packs there, and both of those are shared, so "the Artemis 2.8 skies in
whatever I am running tonight" is one file rather than one copy per mission:
sbs run server,science,comms profile=a28_skies -m WalkTheLine
Only exclude: names something a particular mission declared, and an exclude that
matches nothing is a no-op filter rather than an error - so the same file is safe to point
at a mission that never had the add-on you are replacing.
Why a file and not more arguments
A Windows command line is capped at 8191 characters, shortcuts truncate it, quoting around
spaces and = is awkward, and none of it can be commented, reviewed or diffed. A file is
all of those things. Put the twenty settings in a profile and let the command line name
it — plus a var. or two for whatever you are changing right now:
... profile=soak var.DIFFICULTY=3
Settings
The plain case: any settings key, exactly as it would appear in settings.yaml.
DIFFICULTY: 11
MAP_SIZE: Huge
AUTO_PLAY:
enable: true
Merged in this order, each beating the one before:
built-in defaults
< a mod's settings_set_mod_default()
< the mission's settings.yaml
< profile=<name>
< the COSMOS_SETTINGS environment variable
< var.NAME= on the command line
Whole-key replace, not a deep merge: a profile's AUTO_PLAY replaces the entire
AUTO_PLAY entry rather than merging into it.
Add-ons and media packs
A profile can also add and remove add-ons and art packs relative to story.json.
This is what turns a profile from "a bundle of settings" into "a way to run the same
mission with different content".
addons:
exclude:
- basic_random_skybox # the eight stock skies
include:
- a28_skyboxes # the thirty Artemis 2.8 ones
media:
include:
- A28-Skybox-Mod.media # ...and their art
PLAYABLE_RACES: "USFP" # settings keys still work, in the same file
Name the folder, never the version. a28_skyboxes, not
artemis-sbs.A28-Skybox-Mod.a28_skyboxes.v1.0.0.mastlib. Versions belong in story.json;
a profile that spelled one would break on the next release of the pack. An include
resolves the way a declared lib does — a mission-local source folder first, then the newest
matching .mastlib in __lib__. Media packs match on a substring, for the same reason.
Names are matched case-insensitively, and a section may be a single string instead of a list.
Add versus replace
The two shipped LegendaryMissions profiles differ by one exclude: block, which is the
clearest statement of what the feature does. Measured, seed 7, map 0:
| profile | stock skies | A28 skies |
|---|---|---|
| (none) | 7 | 0 |
a28_add |
7 | 30 |
a28_skies |
0 | 30 |
Everything is logged by name
[runner] profile: a28_skies
profile: dropped 1 addon(s): basic_random_skybox
profile: added addon 'a28_skyboxes' -> artemis-sbs.A28-Skybox-Mod.a28_skyboxes.v1.0.0.mastlib
profile: dropped 1 mission addon folder(s): basic_random_skybox
profile: added media pack artemis-sbs.A28-Skybox-Mod.media.v1.0.0
That is not decoration. An add-on that vanished silently is indistinguishable from one the
mission never declared, and the symptom of getting it wrong is a story that compiles to
zero labels while still reporting PASS.
There are two drop lines because there are two ways an add-on loads. A repo running from
its own clone — LegendaryMissions itself — resolves its declared libs to source folders
and loads them through the directory walk rather than the mastlib list. Both are filtered.
Gotchas
Excluding an add-on that another one requires empties the whole story
requires is a hard compile barrier, so dropping gamemaster while gamemaster_comms
is loaded compiles nothing. The error names the profile and what it removed, so the
cause is in the message rather than something to deduce.
An add-on may own more than its name suggests
The quiet version of the same trap has no error at all. LegendaryMissions' stock music
banks used to be declared inside basic_random_skybox, so a profile that swapped the
skies turned the soundtrack off. (They now live in the consoles add-on, beside the
console that picks one.) If a profile makes something unrelated disappear, look at what
else the excluded add-on declared.
Art needs its pack
addons: include: gets you the labels; media: include: gets you the files. Without
the pack every @media/skybox label fails its file test and is dropped from the random
pick — silently, with no error, leaving you the stock set.
A mission's own profiles are per mission
A mission's profiles/ folder is read from that mission only. Two missions that want
the same content profile need a copy each - addons:/media: cannot be shared,
because they name things one story.json declares. A settings profile can be
shared: put it in common_data/profiles/ and every mission sees it.
It follows a restart, not a mission switch
A profile applies to every restart of the mission you launched, for as long as the
engine is running - the launch arguments do not change and the file is re-read each
time. Restart into a different mission and it is dropped instead, with a line in
the log: profiles/ is per mission, so the same name there would be a different
file meaning something else entirely. See
Switching missions.
A profile is not a saved setup
A profile is written by a person, in an editor, and it is input. What the game writes -
a named preset from the server console, or the last-used setup behind
RESTORE_LAST_SETUP - is output, and lives under
<missions>/common_data/game_codes/<mission>.yaml. Reach for a profile when you have
several standing configurations to choose between at launch; reach for a preset when you
want to keep the setup you just built on screen.
Player ships in particular. A profile's PLAYER_LIST is the roster: name, side, hull
and face, per slot. A saved setup carries only names and hulls, as a capture of what
the crew actually flew. So a standing roster belongs in a profile; "the ships we flew last
time" belongs in a saved setup. A code you share carries neither - ship names are not
part of the match.
What a profile cannot do
resources and shared_media packs are filtered, and mastlib add-ons are filtered — all
three are read by Python before anything compiles. The sbslib list is not: it is read
by PyAddons/sbslibs.py at import script, before a profile has been looked at, so the
library a mission runs against is fixed by story.json.
Where it happens
Nothing about this involves the engine — it has never heard of story.json. The add-on
list is assembled by Mast.find_add_ons() and find_imports() in ordinary Python, inside
a live frame, before a single .mastlib zip is opened, which is why a profile can filter
it at all. Confirmed in the real engine on 2026-08-16.