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 four filter kinds - Entity matches
entity payloads, Timer matches a timer key, 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" |
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", "spaceship", "beacon", "salvage_crate", "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, beacon, crate) | 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 |
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.
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 (shipped, Final Tally)
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.