Create / Modding reference / Events
Events
Everything that can fire a handler. A handler's name: field names one of
the FORTY-FIVE event kinds below, written bare (they are unit variants):
name: OnStart, name: OnEnter, and so on. When the event fires, the
handler's filters gate it and its actions run.
A handler that describes a beat happening ONCE says
once: true beside its
name, and the engine retires it the first time its filters pass. Every
repeating event below composes with it.
An event name also appears inside a
Sequence step's until gate, where it names the
event a paced beat waits for. The filters that qualify it are the same ones.
The whole vocabulary at a glance:
| event | payload | fires when |
|---|---|---|
OnStart |
none | once, right after the scenario loads |
OnResume |
none | once, right after a saved world loads, instead of OnStart |
OnUpdate |
none | every frame while live, unpaused and fully spawned |
OnTimerEnd |
key |
a keyed scenario timer ends |
OnCinematicFinished |
cinematic |
a scene ends, by ANY path |
OnCinematicSkipped |
cinematic |
the player leaves a scene early |
OnDefeated |
id, type_name |
a ship is neutralized or directly destroyed |
OnDestroyed |
id, type_name |
a scenario object is physically destroyed |
OnNeutralized |
id, type_name |
an armed ship loses ALL weapons, or the flight computer it had |
OnEnter |
id, other_id, other_type_name |
a body enters a trigger area |
OnExit |
id, other_id, other_type_name |
a body leaves a trigger area |
OnGotoComplete |
id, other_id, other_type_name |
the player's GOTO reaches its target and stops |
OnStopComplete |
id, type_name |
the player's STOP ends |
OnOrbitStart |
id, other_id, other_type_name |
an ORBIT maneuver starts |
OnOrbitStable |
id, other_id, other_type_name |
ORBIT enters stable station-keeping |
OnOrbitLap |
id, other_id, other_type_name |
one net revolution of the ring completes |
OnOrbitUnstable |
id, other_id, other_type_name |
stable station-keeping is lost |
OnOrbitEnd |
id, other_id, other_type_name |
a surviving ship ends ORBIT |
OnTravelLockStart |
id, other_id, other_type_name |
the player's travel lock lands |
OnTravelLockEnd |
id, other_id, other_type_name |
the player's travel lock leaves |
OnCombatLockStart |
id, other_id, other_type_name |
the player's combat lock lands |
OnCombatLockEnd |
id, other_id, other_type_name |
the player's combat lock leaves |
OnDocked |
id, type_name, other_id, other_type_name |
two hulls clamp together |
OnUndocked |
id, type_name, other_id, other_type_name |
a dock comes off, by any ending |
OnShipOrderComplete |
order, kind, id, type_name |
a helm order reaches what it was told to reach |
OnShipOrderInterrupted |
order, kind, id, type_name |
an AI ship breaks off its order to fight |
OnShipOrderResumed |
order, kind, id, type_name |
that ship picks the same order back up |
OnShipOrderCanceled |
order, kind, id, type_name |
an unfinished order is cleared or replaced |
OnShipOrderFailed |
order, kind, id, type_name |
an order can no longer be flown |
OnMined |
id, other_id, other_type_name, item, count |
a mining pulse frees ore from a rock |
OnPickedUp |
id, type_name, canister, item, count |
a cargo intake takes whole items of a canister stack into the hold |
OnJettisoned |
id, type_name, canister, item, count |
a cargo intake drops a stack out as a canister |
OnTraded |
id, other_id, other_type_name, trade, item, count, price_cr |
the player buys or sells a stack across a dock |
OnCreditsChanged |
id, type_name, previous_cr, current_cr, delta_cr, cause |
a ship's credits move |
OnRepaired |
id, type_name, section, item, count |
the Ship pane spends plates on a section |
OnRebuilt |
id, type_name, section, parts, price_cr |
the Ship pane rebuilds a lost section at a docked station |
OnSwapped |
id, type_name, section, from, to, parts, price_cr |
the Ship pane swaps a section for another part at a docked station |
OnAdded |
id, type_name, section, part, at, point, parts, price_cr |
the Ship pane adds a part at a free socket at a docked station |
OnShipBought |
id, type_name, hull, price_cr |
the Station pane buys a whole hull from a docked station |
OnEscortHired |
id, type_name, escort, hull, price_cr |
the Ship pane hires an escort from a docked station into the fleet |
OnStationBought |
id, type_name, station, price_cr |
the Ship pane buys the docked station from its civilization |
OnStripped |
id, type_name, hull, section, parts |
an Inventory pane strip takes a section off a docked wreck, once per section, the berth port last; or a Ship pane delete takes a section off the ship itself |
OnStored |
id, other_id, other_type_name, item, count |
the player stores a stack in a docked station's storage |
OnWithdrawn |
id, other_id, other_type_name, item, count |
the player withdraws a stack from a docked station's storage, or Take all or Strip takes one from a dead station's |
OnJobAccepted |
id |
the Jobs pane accepts a board offer; id is the job's id; a refused accept and a Load fire none |
Entity payload fields are what an Entity filter can match: id /
type_name name the event's SUBJECT, other_id / other_type_name its other
party. OnTimerEnd instead carries key, matched by a Timer filter; the two
cinematic endings carry cinematic, matched by a
Cinematic filter; and the five
ship-order events carry order / kind for a
ShipOrder filter beside the ship's own id /
type_name - the same four fields on all five, so one filter matches
whichever of them you hang it on. Which entity is which is per-event and listed below. A filter
field the event does not fill NEVER matches - other_id on an OnDestroyed
handler can never pass.
OnStart
Fires exactly once, right after the scenario loads - after every handler entity exists, so no handler can miss it. Carries no payload; this is where a scenario seeds its world: spawns, lights, variable seeds, the first objective.
(
name: OnStart,
actions: [
VariableSet((key: "beat", expression: Term(Factor(Literal(Number(0.0)))))),
// ... spawns, lights, first objective ...
],
),
Show explanation
An Entity filter never matches OnStart (no payload). Seed every variable
your expression filters will read (they
fail closed on unset variables), and post
the first objective here.
Its spawns arrive over the next few frames, and the scenario is HELD until they
have: no other handler runs, the scenario clock does not tick, and the LOADING
panel stays up. So the first OnUpdate after OnStart already sees every
object OnStart asked for - a count gate cannot read a half-built world.
OnResume
Fires exactly once, instead of OnStart, when a saved world loads. By then
the saved scenario state is already back: the variables, the objectives, the
story log, the readouts, the running timers and the generated marks. A once
handler that fired before the save is not rebuilt, so what it did is not
done twice. Carries no payload.
Only a scenario with world: Generated((seed: None)) saves, and lint
requires its OnResume to spawn exactly one player ship. Hang the same
spawn list on both openings - the ship and the lights - and keep the seeds
and the first objective on OnStart alone: the save brings those back.
(
name: OnResume,
once: true,
actions: [
// ... the same player spawn and lights as OnStart ...
],
),
Show explanation
The save does not keep the player ship as a spawn: the open world streams
around the ship OnResume spawns, and the save's ship state lands on it
once it stands. A VariableSet here would overwrite the saved value, and an
Objective would post a second copy of one the save already lists.
OnResume on a scenario that cannot be saved is a lint warning: it never
fires there.
OnUpdate
Fires every frame while the scenario is live and UNPAUSED (frozen behind the
pause menu and the outcome overlay) and every object it asked for exists.
Carries no payload - and an unfiltered OnUpdate handler runs its actions
EVERY frame, so always gate it.
Show explanation
The chain order is guaranteed: the scenario clock ticks, typed queries and
watches update, ended timers fire, then OnUpdate fires. Query-backed gates
see one coherent frame snapshot.
This is the workhorse for clock-driven beats and count thresholds that must
not depend on handler order. Gate it with Expression filters, and add
once: true when the beat
happens one time - then the only filter left is the one about the game:
(
name: OnUpdate,
once: true,
filters: [
Expression((GreaterThan(
Term(Factor(Name("scenario_elapsed"))),
Term(Factor(Literal(Number(10.0)))),
))),
],
actions: [
NarrativeCue((
speaker: "Control",
text: "Ten seconds elapsed.",
)),
],
),
A beat that genuinely repeats leaves once off and re-arms itself in its own
actions - see the recipes.
OnTimerEnd
Fires exactly once when a keyed scenario timer reaches its deadline. Payload:
key is the scenario-local timer key - match it with a
Timer filter.
(
name: OnTimerEnd,
filters: [Timer((key: "briefing_delay"))],
actions: [NarrativeCue((speaker: "Control", text: "Proceed."))],
),
Show explanation
Timer-end events queue before that frame's OnUpdate pulse.
Start or restart the delay with TimerStart. Cancel
it with TimerCancel. Timers use live, unpaused
scenario time and clear on retry or teardown.
Cinematic endings
A Cinematic does not carry an on-finish action list.
It REPORTS its ending, and the handlers that answer live beside it. Payload:
cinematic is the scene's authored key - match it with a
Cinematic filter.
// Every path out: give back what the scene took.
(
name: OnCinematicFinished,
filters: [Cinematic((key: "strike_approach"))],
actions: [
ReleaseCamera(()),
ResumePlayerControl(()),
],
),
// Only the skip: what the beats that never played would have left behind.
(
name: OnCinematicSkipped,
filters: [Cinematic((key: "strike_approach"))],
actions: [
DespawnScenarioObject((target_id: "warship")),
VariableSet((key: "beat", expression: Term(Factor(Literal(Number(6.0)))))),
],
),
Show explanation
OnCinematicFinished fires on EVERY way out of the scene - its last beat ran,
the player skipped it, a CancelCinematic
stopped it, or one of its steps blew its deadline. Author the post-state there,
once, and no path can forget it.
OnCinematicSkipped fires FIRST on a skip, immediately before that scene's
OnCinematicFinished. It carries only the catch-up: the actors the unplayed
beats would have spawned, moved or destroyed, and the variables they would have
latched. It is not a second copy of the scene - a skip means the player asked
not to watch, so nothing presentational belongs here.
Always filter by key. A scenario with two scenes and an unfiltered handler
answers whichever one ends first. A Cinematic filter naming a key no scene
plays is a lint Error, so a typo costs the whole scenario, not one handler.
Ship order lifecycle
The five events below report what became of a
helm order. They carry the SAME four fields -
order is the key the action named the order, kind is which of the five it
was (Move / Align / Stop / Patrol / Orbit), and id / type_name
are the ship - so one ShipOrder filter matches
whichever of them a handler listens for, and an Entity filter still works on
all of them.
(
name: OnShipOrderComplete,
filters: [ShipOrder((order: Some("close_the_gap")))],
actions: [ForceRailgunFire((ship: "warship", section: "spinal"))],
),
Show explanation
An order that is REFUSED when it is issued - a dangling ship id, a player's ship, an empty patrol route, an impossible tolerance - fires nothing at all. It was never installed, so there is nothing to report on. The lint catches each of those before the scenario runs.
A key is reusable: give the same order again and the lifecycle runs again.
OnShipOrderComplete
Fires exactly once when an order reaches what it was told to reach.
Show explanation
What "reached" means is per order:
MoveShipToandStopShipcomplete when the ship's autopilot lets go - inside the standoff, or at rest.ForceAligncompletes when the aim is inside its authored tolerance and steady there.PatrolShipcompletes at the END of its one loop, back where it started - not at each waypoint.OrbitShipcompletes when the ring is ESTABLISHED, and then keeps holding it.
Completion says the CONDITION was met, not that the behavior stopped. Move,
Stop and Patrol release the helm as they report; Align and Orbit keep
holding until something else takes it.
OnShipOrderInterrupted
Fires when an AI ship breaks off its order to fight. Only an AI ship can, and
only one whose order_interruption policy says so (see
Spaceship); the default policy is never, and a
None-controller ship has no bot to break off in the first place.
(
name: OnShipOrderInterrupted,
filters: [ShipOrder((ship: Some("picket")))],
actions: [NarrativeCue((speaker: "Picket", text: "Contact. Breaking off."))],
),
Show explanation
The order is NOT lost. It is parked: the ship keeps the key, the kind and its
place in the route, the AI takes the helm back and fights, and when the signal
clears the same order picks up from where it stopped - see
OnShipOrderResumed.
A beat gated on the COMPLETION still waits, correctly: an interrupted patrol has not swept the belt yet.
OnShipOrderResumed
Fires when an interrupted order is picked back up - the hostile is gone, or the damage has stopped, depending on the ship's policy.
Show explanation
Pairs one-for-one with
OnShipOrderInterrupted, and can pair with it
several times over one order's life. The resumed order flies its OWN
directive, from the leg it was on - not a fresh copy from the top.
OnShipOrderCanceled
Fires when an unfinished order is called off: either
ClearShipOrder, or a second helm order
replacing it.
Show explanation
Cancellation is not completion, and a beat gated on the completion correctly never runs. This is the event for the OTHER side of that: cleaning up a readout, or telling the player the approach was called off.
An order that had already reported a terminal outcome says nothing more, so
clearing a ship that is holding a finished
ForceAlign is silent - the align already
completed.
OnShipOrderFailed
Fires when an order can no longer be flown. The hull lost the flight computer
or the drives the order runs on, or no single loaded well answers an
OrbitShip target, or the well is lost or will not
hold a ring before the ring is established.
Show explanation
This is the event that keeps a Sequence gate from waiting forever on a beat
that can never land. A wreck does not arrive.
It reports the ORDER, not the ship: a hull that is destroyed outright is
OnDestroyed and OnDefeated, which is what a
kill objective should be waiting on. Failure is for the ship that is still
there and can no longer do the job.
Weapon actions have no lifecycle at all - what a shot produces is the weapon's own event chain.
OnDefeated
Fires exactly once when a ship leaves combat through neutralization or direct
physical destruction. Payload: id and type_name of the defeated ship - the
event for kill objectives and encounter progression that do not care whether a
wreck remains.
(
name: OnDefeated,
filters: [Entity((id: Some("raider")))],
actions: [ /* complete the encounter once */ ],
),
Show explanation
Ordering is fixed:
- Neutralization:
OnDefeated, thenOnNeutralized. - Direct ship destruction:
OnDefeated, thenOnDestroyed. - Later destruction of an already-neutralized wreck:
OnDestroyedonly.
Scripted despawn, scenario teardown, and boundary cleanup fire none of these edges.
OnDestroyed
Fires when a scenario object is physically destroyed: an asteroid breaks, or a
ship dies through the section-explosion pipeline. Payload: id and
type_name of the DESTROYED object; there is no other party.
(
name: OnDestroyed,
filters: [ Entity((type_name: Some("asteroid"))) ],
actions: [ /* bump a counter */ ],
),
Show explanation
Type names are the object-kind constants: "anchor", "asteroid",
"planet", "spaceship", "beacon", "light" - see
Scenario objects.
OnNeutralized
Fires when a ship that was ARMED loses all working weapons, or loses the
flight computer it once had - combat-dead, hull possibly intact, still in the
world. Payload: id, type_name of the neutralized ship; no other party.
(
name: OnNeutralized,
filters: [Entity((id: Some("derelict_gunship")))],
actions: [
ObjectiveComplete((id: "disarm_gunship")),
NarrativeCue((speaker: "Control", text: "Guns down. The wreck is yours.")),
],
),
Show explanation
A brain-dead ship cannot aim or fly, whatever else survives; thrusters play
no part in the rule. The ship is NOT despawned, so no OnDestroyed fires
with it. A ship that never had a computer (a bare emplacement) only
neutralizes by losing its guns.
Use OnNeutralized only when the persistent-wreck distinction matters. Use
OnDefeated for the shared combat outcome.
OnEnter
Fires when a body's FIRST collider makes contact with a trigger area
(occupancy is refcounted per body, so a multi-section ship fires it once, on
the 0-to-1 transition). Payload: id is the AREA; other_id /
other_type_name are the ENTERING body.
Match one area and one specific entering ship:
(
name: OnEnter,
filters: [
Entity((
id: Some("safe_zone"),
other_id: Some("player_spaceship"),
)),
],
actions: [ /* player arrived */ ],
),
Show explanation
Two things produce trigger areas: the
CreateScenarioArea action and a
Beacon with area_radius set. Both report under their
own id. Entering an area moves no cargo.
Or accept any spaceship that enters that area by filtering the other party's type:
(
name: OnEnter,
filters: [
Entity((
id: Some("repair_zone"),
other_type_name: Some("spaceship"),
)),
],
actions: [ /* a ship entered */ ],
),
Notes:
- The entering body must itself be a scenario object (carry an id and type name) to be reported.
- An area created AROUND a body already inside it still fires
OnEnter- the fresh overlapping pair counts as an entry. So arming a zone late, or spawning it on top of the player, works. (This was once not true; do not design around the old behavior.) OnEnteris a plain event, not a state: if the handler's variable gate is not open yet when the body enters, the entry is consumed and will NOT re-fire when the gate opens later. Place areas so the entry happens after the gate opens, or gate on a variable the repeat pulses can re-check (OnUpdate+ occupancy variables you maintain yourself).
OnExit
The complement: fires when a body's LAST collider leaves the area (the
1-to-0 transition). Same payload shape as OnEnter.
(
name: OnExit,
filters: [
Entity((
id: Some("repair_zone"),
other_id: Some("player_spaceship"),
)),
],
actions: [ /* player left the repair zone */ ],
),
Show explanation
A body despawned while inside an area fires NO OnExit for itself - its
occupancy rows are pruned silently.
Use other_type_name: Some("spaceship") instead of other_id when every ship
leaving the area should match.
A generated world's bounds block (see World) is an
area named bounds the loader spawns before OnStart, so id: Some("bounds") here is how a scenario answers the player leaving it.
In a generated world the entering body may be one the generator placed - a
rock, a derelict - whose id (sector_n2_0_0_background_rock_3) exists only
at runtime. You cannot write that id in a filter or an action (lint refuses
it); pass it on from the event instead with
ObjectiveMarkerAttach((target_id: "$other_id", ..)).
Player maneuver completion
OnGotoComplete and OnStopComplete report successful terminal conditions
from the player's real autopilot. They do not fire when manual input cancels a
maneuver, a target disappears, or the ship loses the ability to continue.
Scripted and AI ship orders use the separate
OnShipOrderComplete lifecycle.
OnGotoComplete carries the reached target as id and the player ship as
other_id / other_type_name. The target still exists while the handler runs,
so this is the safe event on which to remove a temporary navigation beacon:
(
name: OnGotoComplete,
once: true,
filters: [Entity((
id: Some("transit_mark"),
other_id: Some("cutter"),
))],
actions: [
ObjectiveComplete((id: "transit")),
DespawnScenarioObject((id: "transit_mark")),
],
),
OnStopComplete carries the braked player ship as id / type_name. It
reports the maneuver ending, which is a deadband and not a dead stop - a drift
the drive is not pointing at is released at up to 7.5 m/s - so a beat that needs
the hull genuinely still should gate on a
Speed query as well:
(
name: OnStopComplete,
once: true,
filters: [Entity((id: Some("cutter")))],
actions: [ObjectiveComplete((id: "come_to_rest"))],
),
A GOTO that reaches a gravity well and transitions directly into a viable ORBIT does not complete as GOTO; use the orbit lifecycle for that maneuver.
Orbit lifecycle
Five events describe ORBIT without hidden timing: OnOrbitStart,
OnOrbitStable, OnOrbitLap, OnOrbitUnstable, OnOrbitEnd. All five carry
id = well and other_id / other_type_name = orbiting ship.
Show explanation
-
OnOrbitStart: the maneuver engages for a well. The ship may still be aligning or burning toward its ring. -
OnOrbitStable: velocity error enters the autopilot's stable Hold band. It can fire again after stability is recovered. -
OnOrbitLap: the ship accumulates one net revolution in the planned travel direction. It fires again for each later complete lap.Counting starts when the ship first reaches the ring, not when the verb engages: the approach curves around the well and would otherwise bank most of a lap before the orbit began. From there it survives everything the autopilot does on the ring - an ORBIT that drops out of the stable band is correcting, not leaving - and only an absence of five seconds or more writes the partial lap off and starts it again at the ring. A lap objective therefore does not punish a player for one nudge.
-
OnOrbitUnstable: velocity error leaves Hold while ORBIT stays engaged. -
OnOrbitEnd: a surviving ship cancels ORBIT, changes verb, loses flight capability, loses the well, or switches wells. Ship destruction emits onlyOnDestroyed, consistent with area despawn not emittingOnExit.
Switching wells queues OnOrbitEnd for the old well, then OnOrbitStart for
the new one. Ending a stable orbit emits only OnOrbitEnd, not an unstable
edge first.
A mission that requires one physical lap listens directly:
(
name: OnOrbitLap,
once: true,
filters: [Entity((id: Some("planetoid"), other_id: Some("player_spaceship")))],
actions: [ObjectiveComplete((id: "orbit_once"))],
),
A continuous eight-second stable hold uses a timer instead:
(
name: OnOrbitStable,
filters: [Entity((id: Some("planetoid"), other_id: Some("player_spaceship")))],
actions: [TimerStart((key: "orbit_hold", seconds: Term(Factor(Literal(Number(8.0))))))],
),
(
name: OnOrbitUnstable,
filters: [Entity((id: Some("planetoid"), other_id: Some("player_spaceship")))],
actions: [TimerCancel((key: "orbit_hold"))],
),
(
name: OnOrbitEnd,
filters: [Entity((id: Some("planetoid"), other_id: Some("player_spaceship")))],
actions: [TimerCancel((key: "orbit_hold"))],
),
(
name: OnTimerEnd,
filters: [Timer((key: "orbit_hold"))],
actions: [ObjectiveComplete((id: "hold_orbit"))],
),
Lock lifecycle
Player locks expose four one-shot edges: OnTravelLockStart /
OnTravelLockEnd for the travel (white, navigation) lock landing on and
leaving its target, OnCombatLockStart / OnCombatLockEnd for the combat
(red) lock. All four carry the locked target as id and the locking player
ship as other_id / other_type_name.
Show explanation
A held lock stays quiet. AI locks do not fire scenario events. A direct target switch queues end for the old target, then start for the new target.
(
name: OnTravelLockStart,
filters: [Entity((
id: Some("anchorage"),
other_id: Some("player_spaceship"),
))],
actions: [
VariableSet((key: "surveyed", expression: Term(Factor(Literal(Number(1.0)))))),
// ... the survey beat ...
],
),
(
name: OnTravelLockEnd,
filters: [Entity((
id: Some("anchorage"),
other_id: Some("player_spaceship"),
))],
actions: [
// ... react to losing the survey target ...
],
),
Use the combat pair for the red lock. To react to any locked target, omit
id; keep other_id when the locking ship must be player_spaceship.
Docking lifecycle
Two edges describe a dock: OnDocked when the clamp is made and OnUndocked
when it comes off. Both carry the hull that was docked WITH as id /
type_name and the ship that ASKED for the dock as other_id /
other_type_name, and the release keeps the roles the capture set - so one
pair of ids filters both edges.
Show explanation
DOCK is a player verb, so other_id is the player's ship and id is
whatever it flew up to. Its type_name tells a station from a ship: a living
station reads living_station, a dead one derelict_station (spawned dead,
or a living one that lost its last turret), and any other hull spaceship.
That is how a beat asks for "any living station" in a generated world, where
no author can write the station's id:
(
name: OnUndocked,
once: true,
filters: [Entity((
type_name: Some("living_station"),
other_id: Some("player"),
))],
actions: [
Objective((id: "find_help", message: "Find another living station")),
],
),
OnDocked fires when the clamp exists, which is before the sleeves finish
reaching across the gap - the joint is what holds the pair, and the tubes are
art that follows it.
OnUndocked fires for EVERY ending, and carries no reason code: either pilot
pressing DOCK again, a shot-off docking port, or one of the two ships dying. What a beat owes the pair
back - the objective, the next order, the clearance to leave - is the same
whichever way the clamp came off, and a scenario that must tell the endings
apart has OnDestroyed to do it with.
Only a pair that dies WHOLE stays quiet, the way OnExit and OnOrbitEnd do
for a despawned body. One hull dying is a release like any other: the survivor
is free, and it is told so.
A delivery beat that opens when the player clamps on and closes when it lets go:
(
name: OnDocked,
once: true,
filters: [Entity((
id: Some("supply_tender"),
other_id: Some("player_spaceship"),
))],
actions: [
ObjectiveComplete((id: "make_contact")),
Objective((id: "carry_cargo", message: "Objective: Run the cargo home")),
],
),
(
name: OnUndocked,
filters: [Entity((
id: Some("supply_tender"),
other_id: Some("player_spaceship"),
))],
actions: [
VariableSet((key: "cargo_aboard", expression: Term(Factor(Literal(Number(1.0)))))),
],
),
To react to a dock with anything, omit id and keep other_id.
Hold events
Fifteen edges report what a ship's hold and purse do. Each fires ONCE per occurrence from the system that did the work, never from a poll of the inventory, so a handler counts occurrences, not frames.
| event | id is |
other_id is |
extra fields |
|---|---|---|---|
OnMined |
the rock | the mining ship | item, count (corners the pulse freed) |
OnPickedUp |
the ship whose intake took the stack | none | canister, item, count |
OnJettisoned |
the ship whose intake dropped the stack | none | canister, item, count |
OnTraded |
the ship traded WITH | the player's ship | trade (Buy or Sell, from the player's side), item, count, price_cr |
OnCreditsChanged |
the ship whose purse moved | none | previous_cr, current_cr, delta_cr (signed), cause (Trade, Take, Rebuild, Swap, Add, ShipBuy, EscortHire, StationBuy, JobReward, Settle or Cheat) |
OnRepaired |
the repaired ship | none | section (the Ship pane code, CTL-1), item, count (plates spent) |
OnRebuilt |
the rebuilt ship | none | section (the code the lost section had, PDC-1), parts (every recipe part, item id to count), price_cr (work plus bought parts) |
OnSwapped |
the refitted ship | none | section (the Ship pane code, PDC-1), from and to (the old and new section prototype ids), parts (every part of the new part's recipe), price_cr (work plus bought parts) |
OnAdded |
the refitted ship | none | section (the new section's Ship pane code, PDC-3), part (its section prototype id), at and point (the code and socket id it mounts on), parts (every part of its recipe), price_cr (work plus bought parts) |
OnShipBought |
the ship that bought | none | hull (the parked hull's name, Vel civilian Mk 1), price_cr (every section's recipe at ask, paid in full). A free Board of a ship the player owns fires none |
OnEscortHired |
the ship that hired | none | escort (the escort's id, sector_1_0_0_cluster_1_0_0_station_0_escort_0), hull (the catalogue name, Vel armored escort 1), price_cr (the hull's price times 3/2) |
OnStationBought |
the ship that bought | none | station (the station's id, sector_0_n1_0_cluster_0_n1_0_station_0), price_cr (its sections, hold and credits at ask, times 2) |
OnStripped |
the ship that stripped | none | hull (the wreck's id, or the ship's own id for a delete), section (the stripped section's code on that hull, PDC-1; a strip's last is the port the player docked through), parts (every part the strip put in the hold, item id to count) |
OnStored |
the station whose storage took the stack | the player's ship | item, count |
OnWithdrawn |
the station whose storage gave the stack, a dead one looted by Take all or Strip included | the player's ship | item, count |
Show explanation
The pair events follow the docking rule: the PASSIVE party is id and the
ship that acted is other_id. A rock is mined, a tender is traded with, a
station is stored at. The
single-ship events name the ship the hold or purse belongs to.
OnMined fires per paying pulse: a pulse that flips no corner is silent, and
count is the corners it did flip, which is the ore that lands in the hold
only if the intake catches the chips. OnPickedUp and OnJettisoned fire
once per STACK: a canister holding two item kinds raises two events with the
same canister. A hold with room for only part of a canister takes whole
items and raises OnPickedUp with the count it took; the rest is picked up
later under the same canister.
OnTraded fires once per confirmed trade on the player's queue, so a buy of
four rations is one event with count: 4. The credits move it causes raises
one OnCreditsChanged per party, cause: Trade, in the same frame; the
loot-take of a dead ship's purse raises one with cause: Take; a rebuild
raises one per party with cause: Rebuild, the ship paying and the station
paid, a swap one per party with cause: Swap, an add one per party
with cause: Add, a hull buy one per party with cause: ShipBuy, and an
escort hire one per party with cause: EscortHire. A station buy raises
one, for the player only, with cause: StationBuy: the price goes to the
civilization, so the station's purse does not move. An armed credits give
or credits set command raises one with cause: Cheat. A move that
leaves the purse unchanged raises nothing. OnStored and OnWithdrawn fire
once per confirmed move and move no credits, so they raise no
OnCreditsChanged.
Trades, stores, withdraws, repairs, rebuilds, swaps, adds, hull buys, escort hires and station buys come from the interface panes, which run while the world is paused: their events arrive on the first simulation tick after the pane closes. Intake and mining events arrive the frame the hold changes.
A prospecting berth that wants one of each:
(
name: OnTraded,
once: true,
filters: [Entity((id: Some("example_tender"), other_id: Some("player_spaceship")))],
actions: [ObjectiveComplete((id: "prospect_trade"))],
),
(
name: OnMined,
once: true,
filters: [Entity((id: Some("example_ore_rock"), other_id: Some("player_spaceship")))],
actions: [ObjectiveComplete((id: "prospect_mine"))],
),
(
name: OnRepaired,
once: true,
filters: [Entity((id: Some("player_spaceship")))],
actions: [ObjectiveComplete((id: "prospect_repair"))],
),
The example mod's example_prospect scenario runs all but OnRebuilt,
OnSwapped, OnAdded, OnShipBought, OnEscortHired, OnStationBought,
OnStripped, OnStored and OnWithdrawn.
OnJobAccepted
Fires once per successful accept from the Jobs pane. Payload: id only, and
it is the JOB's id, not an entity id. There is no type_name and no
other_id.
(
name: OnJobAccepted,
once: true,
actions: [ObjectiveComplete((id: "take_a_job"))],
),
Show explanation
An Entity filter on id matches a job id here, not a scenario object. A
job id reads <station id>#<slot>.<posting> for a slot job and
<station id>#<visit id>.<index> for a visit posting.
A refused accept fires nothing, and neither does a Load that restores held jobs, so a handler on this event never runs twice for one job.
Dispatch order (what you can rely on)
- Events queue and drain FIFO within a frame; for each event, handlers run in AUTHORED order; within a handler, actions run in authored order.
- Actions mutate the event world immediately (a later filter in the same
frame sees the new variable values), but their WORLD effects (spawns,
despawns) land together at the end-of-frame sync - and an
ObjectiveMarkerAttachordered after aSpawnScenarioObjectin the same handler does see the fresh object. - Never depend on handler order across DIFFERENT events; gate on variables.
OnTradedand its twoOnCreditsChangedland on the same tick, trade first; which party's credits event runs first is not promised.