The ship_data module
Load and query the ship-definition database, and let an add-on declare ships of its own.
Overview
Cosmos ships are defined in data/shipData.yaml, keyed by art ID (e.g.
"tsn_battle_cruiser"). This module loads that database, merges anything a mission
or add-on has contributed, and answers queries against the result.
get_ship_data() returns the whole merged database (a dict with a #ship-list).
get_ship_data_for(key) returns one ship's entry. get_ship_name(key) is the display
name, and filter_ship_data_by_side is how a prefab finds "a Kralien warship".
The database is loaded lazily and cached. Mission scripts do not normally need this module directly - spawn functions look ship data up for you.
Quick example
== pick_ship ==
entry = ship_data_get_ship_data_for("tsn_battle_cruiser")
display_name = entry.get("name", "Unknown")
log(f"Spawning a {display_name}")
->END
from sbs_utils.procedural.ship_data import (
get_ship_data, get_ship_data_for, get_ship_name,
)
entry = get_ship_data_for("tsn_battle_cruiser")
name = entry.get("name", "Unknown")
# every ship the database knows
for ship in get_ship_data().get("#ship-list", []):
print(ship.get("key"), ship.get("name"))
The MAST names carry a ship_data_ prefix
The prelude registers this module with a prefix, so Python's get_ship_data_for
is ship_data_get_ship_data_for in MAST, and add_extra is
ship_data_add_extra.
Adding ships from an add-on
An add-on ships a ship-data file and names it. Nothing is written:
ship_data_add_extra("turrets/extraShipData_turrets", mod="MyMod")
from sbs_utils.procedural.ship_data import add_extra
add_extra("turrets/extraShipData_turrets", mod="MyMod")
add_extra points both readers at the one file: sbs_utils' own table (which is
what filter_ship_data_by_side and get_ship_name read) and the engine (which
is what resolves artfileroot, and what makes a behav_station fire at all).
The name takes no extension - the engine tries .yaml then .json itself - and
may include a logical folder. With no path it is looked for where the media system
already looks: this mission, then each media pack it pinned.
Put the file in a media pack, not a mastlib
A mastlib is a zip, and the engine cannot read inside one. A media pack is unpacked to disk once, so the engine can be handed a real folder. That is the whole reason this used to involve writing a file.
The older generating route is broken
ship_data_merge_mod reaches the engine by generating extraShipData.json in the
mission folder. get_ship_data() then prepends that file whole on the next run -
#mod entries and all - while the add-on declares the same entries again. Measured
at 51 hulls becoming 102 from run 2 onward. Use add_extra.
extra_enable(False) turns off only the engine half, leaving the library merge in
place - useful while the engine side is in flux, since headless and the mock keep
behaving identically.
The picture of a ship
ship_art_image(id_or_key) gives the image key for a hull's flat art - the top-down
sprite the engine ships beside every mesh as <artfileroot><size>.png. It is the only
picture of a ship a GUI can draw without asking the engine for a 3d render, so it is what
a panel uses to show which ship it is talking about.
art = ship_data_ship_art_image(target_id)
gui_sub_section(f"col-width: square; background-image: {art}; background: white;")
from sbs_utils.procedural.ship_data import ship_art_image
ship_art_image("tsn_light_cruiser") # 'ships/tsn_light_cruiser1024'
ship_art_image(target_id, 256) # the small one
Takes an agent, an id, or a shipData key. artfileroot already carries the whole path
and its base is data/graphics, so the result goes straight into gui_image* or a
background-image: style - neither wants the .png. None comes back when the entry
has no art, which is ordinary for a marker or a pickup, not an error.
A background needs a color as well as an image
A layout item only draws its backdrop when background-color is set;
background-image alone paints nothing. background: white means "no tint", which
is what you want for ship art.
API
add_extra(name, path=None, mod=None)
Load another ship-data file for this mission.
name has no extension - .yaml or .json is found here, so a mod can
change format without the caller changing. It may include a logical folder
("turrets/extraShipData_turrets"). The engine now wants the fully-pathed
file WITH its suffix, so the extension search that used to be the engine's job
happens in _read_extra_ship_data and its answer is what the engine is handed
- one decision, not two that can disagree.
With no path, the file is looked for where the media system already looks:
this mission's folder first, then each media pack it pinned. That matters
because an ADD-ON cannot put a file where the engine can read it - a mastlib
is a zip - while a media pack is unpacked to disk once. So an addon ships its
hulls in its media pack and names them here, and neither it nor the library
has to write anything.
Returns True when the engine was told, False when only the library was. Missing files are not fatal, matching the engine's habit: a mod with a broken path should be a ship with no stats, not a dead mission.
add_ship_data(entry, mod=None, prepend=True)
Add a single entry to the in-memory ship data.
Inserts entry into the #ship-list so it is returned by
:func:get_ship_data, :func:get_ship_index, :func:get_ship_data_for,
:func:filter_ship_data_by_side, and the *_keys helpers -- letting a
script register a ship/terrain/pickup type at runtime without editing
shipData or shipping an extraShipData.json.
The entry is prepended by default, matching how extraShipData.json is
merged (script data ahead of built-in data). The derived caches
(ship_index and every *_keys cache) are cleared so the new entry shows
up on the next lookup; the loaded ship_data_cache itself is preserved.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry
|
dict
|
A ship data dict. Must include a |
required |
mod
|
str
|
Name of the mod/addon this entry comes from; stamped on
the entry as |
None
|
prepend
|
bool
|
Insert at the front of the list (script
priority) when |
True
|
Returns:
| Type | Description |
|---|---|
|
dict | None: The updated ship data cache, or |
alien_keys()
Return all pickup keys containing "alien" (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Alien pickup type keys. |
art_faction_for(race, role=None)
The shipData faction whose hulls should be DRAWN for race. ART ONLY.
Returns race unchanged unless a mission or profile set RACE_ART, so stock behavior
is untouched by default.
THIS DOES NOT CHANGE WHOSE SIDE ANYTHING IS ON. shipData side is a LOOKUP field - "what
kind of ship is this" - while the side handed to :func:npc_spawn is the diplomatic
faction that drives relations, comms and contact colour. The prefabs already keep the two
apart as origin and side_value; this maps the first and never touches the second, so
a Cardassian hull can spawn as a raider and the mission's diplomacy is unchanged.
WHY A MOD NEEDS THIS. Overriding a STOCK ship key with mod art works on the server and
never on a client: a client resolves a key it already knows against its own
data/shipData.yaml, so its stock artfileroot wins and the override never crosses the
wire. Pointing the lookup at the mod's OWN keys is what reaches clients, because the
client has no local record for those and renders what the server sends.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
race
|
str
|
the mission's own faction name, e.g. |
required |
role
|
str
|
if given, the mapping is only honored when the mapped faction actually has hulls in that role. Without this a typo or a partial mod would silently spawn nothing at all, which is much harder to notice than wrong art. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
str |
the faction to look hulls up under. |
art_key_for(ship_key)
The hull key to DRAW in place of ship_key. ART ONLY.
The companion to :func:art_faction_for, for the OTHER way a hull gets chosen. Some
callers do not look a ship up by faction at all - they name the key outright:
- stations (
station_type), and - fleet ladders, which list their hulls class by class so a wave keeps its shape.
A faction map cannot help those, so they get a key map instead - ART_KEYS, keyed by
the STOCK key being replaced. Mapping per key also PRESERVES THE LADDER'S CHOICES: a
battleship is replaced by a specific hull rather than by a random ship of some faction.
Returns ship_key unchanged when unset, or when the replacement is not in the ship
table - a half-written map should degrade to stock art, never to nothing spawning.
art_key_in_faction(ship_key, faction)
Pair one hull into faction by size rank. Identity when it cannot.
The single-key form of what :func:art_keys_from_theater does in bulk, for the case
where a caller wants a DIFFERENT faction than the theater's own race map gives - the
crew flying Orion hulls while the theater still re-skins tsn allies as Federation.
Falls back to ship_key when the faction has no comparable hull, because spawning
stock art is recoverable and spawning nothing is not.
art_keys_cache_clear()
Drop the generated ART_KEYS pairing. On the reset ledger with the theaters.
art_keys_from_theater()
Generate a stock-key -> mod-key map from the active theater's Art: map.
The faction map (RACE_ART) only reaches hulls that are LOOKED UP by faction. A fleet
LADDER names its hulls outright, class by class, so a wave keeps its shape - and those
never consult a faction map at all. This bridges them: for every race the theater
repoints, pair that race's stock hulls against the target faction's hulls BY SIZE RANK,
so the ladder's shape survives the re-skin (its biggest ship is still the biggest).
Cached per theater - the pairing is deterministic, and recomputing it per spawn would walk the whole ship table every time.
arvonian_ship_keys()
Return all Arvonian ship keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Arvonian ship type keys. |
arvonian_starbase_keys()
Return all Arvonian starbase keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Arvonian starbase type keys. |
asteroid_keys()
Return all asteroid ship keys from the ship data (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Asteroid type keys. |
container_keys()
Return all pickup keys containing "container" (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Container pickup type keys. |
crystal_asteroid_keys()
Return all crystal asteroid keys, excluding plain asteroids (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Crystal asteroid type keys. |
danger_keys()
Return all pickup keys containing "danger" (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Danger pickup type keys. |
extra_enable(enabled=True)
Allow or forbid the ENGINE side of add_extra.
Off, the ships are still merged into sbs_utils, so headless runs and every library lookup behave the same; only the engine is not told. Use it to take the engine path out of play without touching any caller.
extra_enabled()
Is the engine call currently allowed?
extra_loaded()
[(filename, path, reached_engine, engine_arg)] for every call so far, so a
report can say what was loaded, what exact file the ENGINE was pointed at, and
whether it heard about it. engine_arg is None when no file was found.
extra_replay()
Tell the engine again about every extra ship data file it has been given.
create_new_sim() REBUILDS the engine's ship data table (engine 1.3.4 also re-reads the
mission's extraShipData.json inside that call; later engines appear not to), and
everything add_extra_ship_data registered beforehand is gone. Nothing reports it. The library keeps its own merged copy, so the
ships still have stats everywhere sbs_utils can see, and the loss surfaces later as
MemoryError: bad allocation from a spawn, against whichever mission line asked for
one of those hulls.
Missions register at story load, which is BEFORE the first map calls sim_create(), so
this is the ordinary case rather than an edge one. LegendaryMissions declares its
monsters with a top-level shared, which by design runs once and then becomes a no-op,
so nothing ever re-issued them: every monster in the game was unspawnable from the first
map start onward, and had been for as long as anyone could remember (measured
2026-08-14 - inside LM every hull fails and re-issuing this exact call fixes all five).
Replayed from the record rather than from the files: the library merge already happened and only the engine forgot.
extra_report_untold()
Name the mods the engine does not have, while it can still be acted on.
Called from sim_create() right after extra_replay() - the point where the table has just been rebuilt and re-fed, so anything still missing is missing for the rest of the mission. Goes to debug.log too, the channel that survives an engine session.
extra_reset()
Forget the record. Called by the per-mission reset, not by missions.
extra_ship_data_enabled()
Whether extra ship data may be loaded at all.
Reads the EXTRA_SHIP_DATA setting, defaulting to False. A caller that has to
decide before settings exist - or a test - overrides it with
extra_ship_data_force.
extra_ship_data_force(on=True)
Override the setting. None hands control back to it.
extra_untold()
Which mods have hulls in the LIBRARY that the ENGINE was never (re-)told about?
[(mod, hull_count)], sorted. Empty is the healthy answer.
A mod that calls sbs.add_extra_ship_data() ITSELF is not in _extra_ship_data_loaded, so extra_replay() has nothing to replay for it - and create_new_sim() rebuilding the table is what takes its hulls away. Everything library-side keeps working, which is why it goes unnoticed: every lookup, picker, headless run and lint reports the hulls present. The bill arrives as a spawn dying inside the engine, in a mission that never mentions ship data. Three shipped mods were in exactly that state on engine 1.3.6.
Matched on the ship KEY, not on the #mod stamp: under the MOCK, add_extra_ship_data merges the file a second time and re-stamps every entry with its own name, so a name comparison reports a correctly-declared mod as untold.
filter_ship_data_by_side(test_ship_key, sides, role=None, ret_key_only=False)
Return ship data entries matching a key substring, side filter, and optional role.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
test_ship_key
|
str | None
|
Substring that must appear in the ship key,
or |
required |
sides
|
str
|
Comma-separated side names to include (case-insensitive). |
required |
role
|
str
|
Single role that must be in the ship's role list. Defaults to None (no role filter). |
None
|
ret_key_only
|
bool
|
Return a list of key strings instead of full data dicts. Defaults to False. |
False
|
Returns:
| Type | Description |
|---|---|
|
list[str | dict]: Matching ship keys or data entries. |
get_mod(key_or_entry)
Return the source mod of a ship data entry, or None if it is engine-known.
Exposed to MAST as ship_data_get_mod (the ship_data_ prelude prefix); named
get_mod here so it doesn't double-prefix to ship_data_ship_data_get_mod.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key_or_entry
|
str | dict
|
A ship key, or a ship data entry dict. |
required |
Returns:
| Type | Description |
|---|---|
|
str | None: The mod name stamped at merge time, or |
get_ship_data()
Load and cache the full ship data, merging extraShipData.json if present.
Results are cached after the first call. The mission-directory
extraShipData.json is prepended to the #ship-list so mission ships
take priority over built-in data.
Returns:
| Name | Type | Description |
|---|---|---|
dict |
The merged ship data dictionary. |
get_ship_data_for(ship_key)
Return the full ship data entry for a given key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_key
|
str
|
The ship type key. |
required |
Returns:
| Type | Description |
|---|---|
|
dict | None: Ship data dict, or |
get_ship_index()
Return ship data indexed by ship key for fast O(1) lookup.
Returns:
| Type | Description |
|---|---|
|
dict[str, dict]: Mapping of ship key → ship data entry. |
get_ship_name(ship_key)
Return the display name of a ship type by key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ship_key
|
str
|
The ship type key. |
required |
Returns:
| Type | Description |
|---|---|
|
str | None: Ship display name, or |
kralien_ship_keys()
Return all Kralien ship keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Kralien ship type keys. |
kralien_starbase_keys()
Return all Kralien starbase keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Kralien starbase type keys. |
merge_mod_ship_data(mod, file=None)
Merge a mod folder's extra ship data (YAML or JSON) into the ship data cache.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mod
|
str
|
Mod directory name (resolved via |
required |
file
|
str
|
The data file within the mod folder. Defaults to
the base name |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
The updated ship data cache. |
merge_mod_ship_yaml(content, mod=None)
Merge ship data supplied as a YAML/JSON string into the ship data cache.
Companion to :func:sbs_utils.procedural.media.media_read_relative_file, which
returns a data file's CONTENTS relative to the current addon -- working whether
the addon is a loose folder (dev) or a packaged .mastlib zip, where a plain
filesystem path can't reach the file. So an addon can ship its own ship/terrain
data next to its prefabs and load it in one line:
merge_mod_ship_yaml(media_read_relative_file("shipData_monsters.yaml"), "MyMod")
The parsed #ship-list is prepended (addon data ahead of built-in). Each entry
is stamped with mod (the #mod key) so the low-level spawn can tell these
engine-unknown entries apart and post-process them (see :func:mod_ship_data_process).
The derived caches (ship_index and the *_keys caches) are cleared so the new
entries are visible on the next lookup.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
content
|
str
|
YAML or JSON text (YAML is a JSON superset, so both parse). |
required |
mod
|
str
|
Name of the mod/addon these entries come from; stamped on
each entry as |
None
|
Returns:
| Type | Description |
|---|---|
|
dict | None: The updated ship data cache, or |
mod_ship_data_process(so, entry)
Apply a runtime-merged (mod) ship data entry to a freshly spawned object.
The engine's built-in shipData table doesn't contain entries merged at runtime
(:func:merge_mod_ship_yaml / :func:merge_mod_ship_data / :func:add_ship_data),
so create_space_object returns a bare object that never got the values the engine
normally derives from a KNOWN shipData entry. The low-level spawn (spawn_common)
calls this for such objects to reproduce that derivation.
The shipData-field -> object mapping mirrors cosmos_dev.mock.sbs's reverse-engineered
_apply_ship_data_to_object (the engine's data_set names are NOT the shipData spellings):
- Art via
set_ship_data_key(artfileroot)when the modded key differs from its art (the engine picks the mesh fromdata_tag, not a data_set field).meshscale/radarscaleare engine-internal render props with NO data_set key -- not applied. exclusionradius-> the physics attributeengine_object.exclusion_radius(not a data_set field).- 1-to-1 float scalars (
turn_rate,speed_coeff,interactionradius, ...). hullpoints->armor/armorMax(stations only; ships use another system).baycount->bay_count;tubecount->torpedo_tube_count.shieldsarray ->shield_count+shield_val/shield_max_valper facing.hull_port_setsbeams ->beamCount+beamRange/beamDamage(coeff * 6.0) /beamCycleTime/beamArcWidth/beamBarrelAngleper port.torpedostart->{Type}_NUM/_MAX/_VAL+torpedo_types_available.
Fields with no known engine mapping (and the meta key/name/side/roles/#mod) are skipped; a prefab may still set anything extra afterwards (it runs after spawn, so it wins).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
so
|
The spawned SpaceObject (exposes |
required | |
entry
|
dict
|
The ship data entry (as merged, carrying |
required |
pirate_ship_keys()
Return all pirate ship keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Pirate ship type keys. |
pirate_starbase_keys()
Return all pirate starbase keys (cached).
As of v1.2.2 no pirate starbases exist in shipData; this returns an
empty list.
Returns:
| Type | Description |
|---|---|
|
list[str]: Pirate starbase type keys. |
plain_asteroid_keys()
Return all plain asteroid keys, excluding crystal asteroids (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Plain asteroid type keys. |
reset_ship_data_caches()
Clear the DERIVED ship data caches (index and key lists), not the data itself.
Called by the merge/add functions after they change the #ship-list so the next
lookup sees the new entries. For the mission-boundary reset that also drops the
loaded data, use :func:ship_data_reset_for_mission.
ship_art_image(id_or_key, size=1024)
The image key for a ship's flat art -- e.g. ships/TSNBattleship1024.
The engine ships a top-down sprite beside every hull mesh, named
<artfileroot><size>.png: 1024 is the big one the hull mask is cut from,
256 the small one. It is the only picture of a ship a GUI can draw without
asking the engine for a 3d render, so it is what a panel uses to show WHICH
ship it is talking about.
artfileroot carries the whole path (ships/<name>) and the base for it
is data/graphics -- so what comes back here can be handed straight to
gui_image* or to a background-image: style. Neither wants the .png.
A bare root (no /) is the spelling engine 1.3.6 stopped resolving; it is
returned unchanged rather than guessed at, because _art_root_exists is
where that judgement belongs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_key
|
Agent | int | str
|
A space object, its id, or a shipData key. |
required |
size
|
int
|
Which sprite - 1024 or 256. Defaults to 1024. |
1024
|
Returns:
| Type | Description |
|---|---|
|
str | None: The image key, or |
Example
art = ship_art_image(target_id) gui_sub_section(f"col-width: square; background-image: {art}; background: white;")
ship_data_is_loaded()
Reset-ledger probe: 1 while ship data (possibly mod-merged) is held, else 0.
ship_data_reset_for_mission()
Drop the loaded ship data ENTIRELY, including any merged mod entries.
Deliberately NOT the same as :func:reset_ship_data_caches, which clears only the
DERIVED caches and is called by the merge functions themselves - clearing
ship_data_cache there would throw away the entries just merged.
This is the MISSION-BOUNDARY reset. The next mission has its own mission directory
(its own extraShipData) and its own set of mods, so a #ship-list carrying the
previous mission's merged entries must not survive into it. The engine forks a fresh
process per mission and hides this; cosmos_dev reuses one interpreter and does
not. Registered in the reset ledger as ship_data_cache.
skaraan_ship_keys()
Return all Skaraan ship keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Skaraan ship type keys. |
skaraan_starbase_keys()
Return all Skaraan starbase keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Skaraan starbase type keys. |
terran_ship_keys()
Return all TSN ship keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Terran ship type keys. |
terran_starbase_keys()
Return all USPF station (Terran starbase) keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Terran starbase type keys. |
torgoth_ship_keys()
Return all Torgoth ship keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Torgoth ship type keys. |
torgoth_starbase_keys()
Return all Torgoth starbase keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Torgoth starbase type keys. |
ximni_ship_keys()
Return all Ximni ship keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Ximni ship type keys. |
ximni_starbase_keys()
Return all Ximni starbase keys (cached).
Returns:
| Type | Description |
|---|---|
|
list[str]: Ximni starbase type keys. |