Create / Modding reference / Filters
Filters
A filter gates a handler: when its event fires, EVERY entry in the handler's
filters list must pass (logical AND) before the actions run. An empty (or
omitted) list always passes. There are exactly six filter kinds - Entity matches
entity payloads, Timer matches a timer key, Cinematic matches a scene key,
ShipOrder matches a helm order's outcome, Expression tests scenario
variables, and Conditional combines other filters with boolean logic.
| filter | tests | typical use |
|---|---|---|
Entity |
who the event is about | "this beacon, entered by the player" |
Timer |
a timer event key | "the orbit hold timer ended" |
Cinematic |
a scene key | "the strike approach is the scene that ended" |
ShipOrder |
one helm order's outcome | "the warship reached its firing position" |
Expression |
a variable condition | "the counter is past 4 and the flag is unset" |
Conditional |
other filters, combined | "NOT the player", "picket A down OR picket B down" |
Entity
Match the identity fields the event carries. Every field is optional; every SET field must match exactly (string equality); an unset field is unconstrained.
// the beacon_1 area, entered specifically by the player ship
Entity((id: Some("beacon_1"), other_id: Some("player_spaceship")))
// any asteroid
Entity((type_name: Some("asteroid")))
// any event that carries entity data at all
Entity(())
Show explanation
| field | type | default | matches |
|---|---|---|---|
id |
Option string |
None |
the event subject's id |
type_name |
Option string |
None |
the subject's object kind ("anchor", "asteroid", "planet", "spaceship", "beacon", "light") |
other_id |
Option string |
None |
the other party's id |
other_type_name |
Option string |
None |
the other party's object kind |
Which entity is the subject and which is the other party is per-event - the payload tables in the Events reference are the source of truth. The quick map:
| event | id / type_name (subject) |
other_id / other_type_name |
|---|---|---|
OnDefeated, OnDestroyed, OnNeutralized |
the defeated / destroyed / neutralized object | (none) |
OnEnter / OnExit |
the AREA (zone or beacon) | the body that entered / left |
| Orbit lifecycle events | the well being orbited | the orbiting ship |
| travel/combat lock start/end | the locked target | the locking player ship |
OnDocked / OnUndocked |
the hull that was docked with | the hull that asked for the dock |
OnStart / OnUpdate |
(no payload - an Entity filter never matches) | (none) |
Two rules that bite:
- Absent field = never matches. A set filter field whose key the event
does not fill fails the filter -
other_idcan never pass onOnDestroyed, and anyEntityfilter fails onOnStart/OnUpdate. Constrain only the fields the event actually provides. - Filters gate; they do not bind. The matched ids are never passed to the actions. An action cannot say "spawn at whatever entered" - it acts on its own configured target id. Use the filter to decide WHETHER the handler runs, then address entities by their known scenario ids.
Timer
Match the key carried by OnTimerEnd. It fails
closed on every other event because those events carry no timer key.
Timer((key: "orbit_hold"))
Show explanation
Timer keys are scenario-local strings. This filter observes the event that already ended; it does not test whether a timer is currently running.
Cinematic
Match the cinematic key carried by
OnCinematicFinished and OnCinematicSkipped.
It fails closed on every other event because those events carry no scene key.
Cinematic((key: "strike_approach"))
Show explanation
| field | type | default | matches |
|---|---|---|---|
key |
string | required | the scene's authored key, exactly |
Scene keys are scenario-local strings. A scenario that plays two scenes and
leaves this filter off answers whichever one ends first, so put it on every
cinematic handler. A key no Cinematic action in the
scenario plays is a lint Error, and so is an empty key: both refuse the scenario
at load rather than silently dropping the handler.
ShipOrder
Match one helm order outcome, carried by any of the five ship order events. Every field is optional; every SET field must match exactly. It fails closed on every other event, which carries no order.
// the order this beat is waiting for
ShipOrder((order: Some("close_the_gap")))
// any order the warship finishes
ShipOrder((ship: Some("warship")))
// any alignment, by any ordered ship
ShipOrder((kind: Some(Align)))
Show explanation
| field | type | default | matches |
|---|---|---|---|
order |
Option string |
None |
the key the helm action named the order |
ship |
Option string |
None |
the ordered ship's id |
kind |
Option kind |
None |
Move / Align / Stop / Patrol / Orbit (bare enum, no quotes) |
The same filter reads all five outcomes - completion, interruption, resume, cancellation, failure - because the EVENT decides which outcome a handler listens for and the filter only qualifies it. Waiting on a completion and cleaning up on a failure is two handlers with the same filter.
ShipOrder(()), with nothing set, matches EVERY order outcome - a legitimate
way to hang one handler off a whole scripted sequence, and neither an error
nor a warning.
Order keys are minted by the action that installs the order, so the lint
checks them against the MoveShipTo,
ForceAlign,
StopShip,
PatrolShip and
OrbitShip actions the scenario authors: a filter
waiting on a key nothing ever issues is an Error, because that handler could
never run.
Expression
Evaluate a variable condition; pass when it is true. The single tuple field
is a condition node from the
expression grammar -
LessThan, GreaterThan or Equal over value expressions.
Expression((GreaterThan(
Term(Factor(Name("asteroids_destroyed"))),
Term(Factor(Literal(Number(4.0)))),
)))
Show explanation
An Expression filter reads ONLY the variable store - it ignores the event
payload entirely, which is why it composes with any event, including
OnUpdate.
Conditional
Boolean combinators over other filters (any kinds, nestable), written
positionally: Not(<filter>), Or(<filter>, <filter>),
And(<filter>, <filter>).
// not the player
Conditional(Not(Entity((id: Some("player_spaceship")))))
// either picket still standing
Conditional(Or(
Expression((Equal(
Term(Factor(Name("picket_a_down"))),
Term(Factor(Literal(Number(0.0)))),
))),
Expression((Equal(
Term(Factor(Name("picket_b_down"))),
Term(Factor(Literal(Number(0.0)))),
))),
))
Show explanation
| variant | arity | passes when |
|---|---|---|
Not(<filter>) |
1 | the inner filter does NOT pass |
Or(<filter>, <filter>) |
2 | either passes |
And(<filter>, <filter>) |
2 | both pass |
The filters LIST is already an AND, so a top-level Conditional(And(..))
is redundant - And exists for composing under Or and Not. Nesting
depth is bounded by RON's recursion limit (128), far past anything a sane
script needs.
Traps for the unwary
Three rules catch every new author: filters fail closed, a payload that fails to serialize reads as no-match, and repeating events re-run ungated actions.
Show explanation
- Filters fail CLOSED. Any evaluation error - an UNDEFINED variable, a
type mismatch, a division by zero - logs an error and the filter returns
false. The handler silently never fires. Consequence: seed EVERY variable
your filters read in
OnStart(variables a watch maintains are exempt). A missing seed is a soft-lock, not a crash - and the content lint warns on reads of never-set, unwatched variables. - A payload that fails to serialize also fails closed (loudly, in the
log): every
Entityfilter reads "no match" for that event. - Repeating events + an ungated filter = repeated actions.
OnUpdatefires every frame, and the lifecycle edges re-fire on every genuine transition (a lock landing again, orbit stability recovered). When the beat happens one time, mark the handleronce: trueand it retires the first time its filters pass. When the beat repeats, re-arm it in its own actions - the recipes.