Skip to content

Story missions

A Story mission is hand-authored narrative content. Unlike Generic missions (which template a kind of mission and instantiate many copies), story missions are bespoke — each script is unique, character-tied, sector-tied, and usually linear in progression.

Vanilla has 7 story scripts in md/.

FileStory arcTriggerDLC
story_paranid.xmlParanid civil war / CardinalReputation with Paranid + sector visitBase
story_buccaneers.xmlBuccaneers (Riptide) arcPirate DLC mid-gamePirate
story_ventures.xmlVentures intro / station travellerVentures DLC activeVentures
story_diplomacy_intro.xmlFirst contact with diplomacy systemFaction relation + dockKingdom End
story_research_embassy.xmlDiplomacy researchHQ research progressKingdom End
story_research_welfare_1.xmlWelfare-module researchHQ research progressBase
story_research_xen_equipment.xmlXenon-tech researchHQ + Xenon encounterBase

Story scripts share a common structure:

<mdscript name="StoryParanid" version="1">
<cues>
<!-- Setup cue: runs once on game start, sets globals -->
<cue name="Setup" version="2">
<conditions>
<event_game_started/>
</conditions>
<actions>
<set_value name="md.StoryParanid.$state" exact="0"/>
<!-- Anchor characters, mark mission groups -->
</actions>
</cue>
<!-- Phase-0 trigger: discovery / first encounter -->
<cue name="Phase0_StartTrigger" instantiate="true">
<conditions>
<check_value value="$state == 0"/>
<event_object_signalled_event/>
<!-- character / sector specific condition -->
</conditions>
<actions>
<create_mission ... />
<set_value name="$state" exact="1"/>
</actions>
</cue>
<!-- Phase-N progression cues -->
<cue name="Phase1_..." />
<cue name="Phase2_..." />
...
<!-- Cleanup cue: handles abort / completion teardown -->
<cue name="Cleanup">
<conditions>
<event_mission_aborted cue="$MissionCue"/>
</conditions>
<actions>
<!-- Remove markers, despawn spawned NPCs -->
</actions>
</cue>
</cues>
</mdscript>

Key patterns:

  • State machine via global integermd.X.$state = 0..N tracks current phase
  • Phase guards in conditions — each phase-N cue has check_value $state == N so wrong-phase events don’t fire
  • Setup runs once — see Setup runs once gotcha — patches recover state after save load
  • Cleanup unified — single cue handles abort regardless of phase

Three trigger mechanisms vanilla uses:

<conditions>
<event_object_changed_sector object="player.entity"/>
<check_value value="event.param == cluster.cluster_14.sector002"/>
</conditions>

Used by Paranid story Phase 0 — entering specific sectors fires the encounter.

<conditions>
<event_faction_relation_changed/>
<check_value value="event.param == faction.paranid"/>
<check_value value="faction.paranid.relationto.{faction.player}.uivalue ge 10"/>
</conditions>

Used for diplomacy-gated content.

<conditions>
<event_npc_talked actor="$Cardinal"/>
</conditions>

The script anchors specific NPC actors at setup, then listens for player conversation.

Story missions sometimes BYPASS the offer/accept flow:

  • Offered: create_mission action with briefing element — player sees a mission offer at the NPC, can decline
  • Forced: create_mission action with active="true" — appears already-active, no decline option

Vanilla story arcs typically use offered for hooks, forced for chains-in-progress.

Story scripts need long-lived references:

<set_value name="md.StoryParanid.$Cardinal"
exact="find_actor namespace="this" name="actor_paranid_cardinal_01"/>
<add_anchored_object
object="md.StoryParanid.$Cardinal"
reason="STORY_PARANID"/>

add_anchored_object prevents the engine from despawning the NPC during cleanup sweeps. The reason= string is for debug visibility — you can grep add_anchored_object reason=STORY_X to find what holds an object alive.

Each story registers itself with a mission group for HUD UI categorization:

<set_value name="$MissionGroup"
exact="missiongroup.MG_StoryParanid"/>
<create_mission ...
missiongroup="$MissionGroup"/>

Mission groups are declared in md/missiongroups.xml — see Mission group.

  • State machine collapses if a phase event fires twice — always increment $state BEFORE the long action sequence, or use <reset_cue cue="this"/> for guard cues
  • add_anchored_object leaks NPCs into the world forever if not paired with remove_anchored_object in cleanup
  • DLC story scripts crash at load without faction guards — see DLC handling
  • event_npc_talked requires actor= to be a specific NPC — not a class / faction filter
  • Forced missions (active=true) cannot be declined — only use for explicit player-already-committed flows
ConcernWhere to read
Cardinal anchor + civil warstory_paranid.xml:50-200
Briefing structurestory_paranid.xml:300-600
Phase progression patternAll story_*.xml files share it
Cleanup patternBottom of each story_*.xml
Save migration patchesstory_paranid.xml:1-30 (look for <patch>)

Story scripts are X4’s “bespoke content” mechanism — when your mod needs hand-authored narrative not a template, this is the pattern. If your mod templates a kind of mission and produces N variations, you want Generic missions instead.