Skip to content

NPC orders

How does an Argon transport know to fly to a station, dock, sell its cargo, and return for more? It’s not driven by MD missions β€” those are for missions. NPC ship behaviour runs through aiscript orders: declarative XML files in aiscripts/order.*.xml that define what a ship does and what it reacts to.

This overview explains how 81 vanilla order files compose into the runtime behaviour the player sees.

For the runtime data side (cancel orders, query state) see Order. For the language schema see Order definition.

aiscripts/
β”œβ”€β”€ order.attack.xml
β”œβ”€β”€ order.assist.xml
β”œβ”€β”€ order.build.deploy.xml
β”œβ”€β”€ order.collect.ship.xml
β”œβ”€β”€ order.flee.xml
β”œβ”€β”€ order.mining.routine.xml
β”œβ”€β”€ order.patrol.xml
β”œβ”€β”€ order.protect.position.xml
β”œβ”€β”€ order.wait.xml
β”œβ”€β”€ ... 72 more
β”œβ”€β”€ interrupt.attacked.xml
β”œβ”€β”€ interrupt.scanned.xml
β”œβ”€β”€ ... more interrupts
β”œβ”€β”€ lib.fleet.organize.defences.xml
β”œβ”€β”€ lib.find.sectors.inrange.xml
β”œβ”€β”€ ... lib helpers
└── orders.base.xml ← shared lifecycle code
  • 81 order files (order.X.xml)
  • 15 interrupt files (interrupt.X.xml) β€” see Interrupt
  • Many lib files β€” shared utilities (lib.X.xml)
  • orders.base.xml (394 lines) β€” base lifecycle code referenced by all orders
create_order (MD or engine)
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Order queue β”‚
β”‚ [active] β†’ [next] β†’ ... β†’ [end] β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
Active order's <actions> running
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Interrupts evaluated each tick β”‚
β”‚ (per-order subscription list) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ - Order completes naturally β”‚
β”‚ β†’ next order activates β”‚
β”‚ - Order interrupted β”‚
β”‚ β†’ handler runs, order may end β”‚
β”‚ - Order cancelled β”‚
β”‚ β†’ cleanup, next runs β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↓
Queue empty β†’ fallback to .defaultorder
(or idle if no default)

Most orders share lifecycle code via orders.base.xml. It handles:

  • Setup at order start β€” register the order with the entity, claim resources, etc.
  • Cleanup at order end β€” release resources, reset state
  • Player vs NPC dispatch β€” different UI / voice for player ships
  • Order parameter normalisation β€” defaults, type coercion
  • Skipwait logic β€” when a new override order arrives, base detects via this.$skipwait

Custom orders include base via:

<include_actions ref="orders.base"/>

Without this, your custom order is missing standard X4 niceties.

Every aiscript order file has:

<aiscript>
β”œβ”€β”€ <order id="X" .../>
β”‚ β”œβ”€β”€ <params> ...
β”‚ └── <requires> ...
β”œβ”€β”€ <actions> ← main execution
β”œβ”€β”€ <on_abort> ← cancellation cleanup
β”œβ”€β”€ <on_finish> ← natural completion
β”œβ”€β”€ <attention min="...">← attention-level branching
└── <interrupts> ← reactive handlers
β”œβ”€β”€ <handler ref="AttackHandler"/>
└── <handler ref="..."/>
</aiscript>

See Order definition for the schema details.

A ship has:

  • .defaultorder β€” the fallback that fires when queue is empty
  • .orders β€” queue of explicitly scheduled orders
  • .order β€” currently active (front of queue, or default if queue empty)

Typical patterns:

PatternWhat runs
Queue is empty, default = 'Wait'Ship waits in place
Queue is empty, default = 'Patrol'Ship patrols the sector
Queue is empty, default = 'ProtectPosition'Ship guards a position
Queue has 'Attack' queuedShip attacks first; queue advances

The vanilla canonical pattern for β€œset this ship’s default behaviour”:

<cancel_all_orders object="$ship"/>
<create_order id="'ProtectPosition'" object="$ship" default="true">
<param name="destination" value="[$sector, $position]"/>
</create_order>

cancel_all_orders clears queue + default; create_order default="true" sets the new default. See Order β†’ Cancel and swap orders.

Each order subscribes to interrupts in its <interrupts> block:

<interrupts>
<handler ref="AttackHandler"/>
<handler ref="MissileLockHandler"/>
<handler ref="ScannedHandler"/>
<handler ref="InspectedHandler"/>
</interrupts>

When any of these events fires during the order, the handler runs and may:

  • Modify order state (set $flag)
  • Force the order to end (set this.$skipwait)
  • Issue a new override order
  • Just log without acting (consume="false")

See Interrupt for the handler reference.

Aiscript orders use <attention> blocks to skip expensive logic when the player isn’t watching:

<actions>
<wait min="30s" max="120s"/> <!-- runs at all attentions -->
</actions>
<attention min="insector">
<actions>
<!-- detailed movements only when player in sector -->
<move_to ... />
</actions>
</attention>

See Attention.

Most order files split player-ship vs NPC-ship handling:

<do_if value="this.isplayerowned">
<!-- player UI prompts, voice lines, camera framing -->
</do_if>
<do_else>
<!-- silent NPC execution -->
</do_else>

Player paths are heavier β€” UI feedback, voice lines, sometimes auto-engage prompts. NPC paths just run the math. This is partially why orders.base.xml is 394 lines.

MD missions / cues create orders, but the actual behaviour runs in aiscript:

MD cue Aiscript order
────────── ─────────────
<create_order order.protect.position.xml
id="'ProtectPosition'" reads $destination
object="$ship"> runs <actions> block
<param subscribes to interrupts
name="destination"
value="..."/>
</create_order>

Mission cues observe outcomes via signal_objects emitted from the aiscript’s <on_finish> block:

<!-- inside aiscript -->
<on_finish>
<signal_objects
object="this.$mission_cue"
param="'order_done'"/>
</on_finish>
<!-- MD cue listens -->
<cue name="WatchOrderDone" instantiate="true">
<conditions>
<event_object_signalled
object="this.$mission_cue"
param="'order_done'"/>
</conditions>
...
</cue>

This is the canonical mission ↔ ship-behaviour bridge.

Adding a new order requires:

  1. New aiscripts/order.X.xml file (see Order definition)
  2. Test load β€” engine errors at load if XML invalid
  3. Reference order id from MD with <create_order id="'YourId'"/>

Adding new interrupt handlers (see Interrupt) lets you create reusable reaction logic that multiple orders can import.

To understand β€œwhat does the AI do when X”, grep aiscripts/order.X.xml. The 81 files cover most NPC behaviour; the lib files cover shared utilities.

Orders run continuously while active. Interrupts evaluate each tick. Mods that add expensive logic to a popular interrupt (or a popular order) impact CPU for every NPC ship that uses it.

  • Patrol coordination β€” how patrol orders integrate with galaxy combat bus
  • Boarding β€” boarding operations use specialised orders
  • Fleet reconstitution β€” replacing destroyed ships uses build orders