Timers and counters
Delay execution and count events with awaitable promises.
Overview
Timers let MAST scripts wait for a real-time or simulation-time duration before continuing. Two time bases are available:
- Real time (
delay) — wall-clock seconds; unaffected by simulation speed. - Simulation time (
delay_sim) — scaled by the engine's simulation clock.
Counters (count_goal) are awaitable promises that resolve once a named counter reaches a target value. Increment the counter with increment_count; reset it with reset_count.
All timer and counter functions are designed to be used with await in MAST:
await delay_sim(seconds=5)
await count_goal("enemies_killed", 10)
Quick example
== timed_event ==
log("Reactor will detonate in 30 seconds!")
await delay_sim(seconds=30)
log("The reactor has detonated!")
explode_player_ship(station_id)
->END
== count_kills ==
await count_goal("kills", 5)
log("You have destroyed 5 enemies.")
->END
//signal/enemy_destroyed
increment_count("kills")
from sbs_utils.procedural.timers import delay_sim, delay, count_goal, increment_count, reset_count
# Wait 10 simulation seconds before continuing
await delay_sim(seconds=10)
# Wait 5 real seconds
await delay(seconds=5)
# Count-based gate
await count_goal("patrols_complete", 3)
# Increment from signal handlers or events
increment_count("patrols_complete")
reset_count("patrols_complete")
Signals instead of polling
A timer is one value in an agent's inventory and nothing runs on its behalf, which is
why a mission can hold hundreds of them for free — but it also means a script has to
ask whether one is finished. Pass signal to set_timer and the library emits that
signal once, when it expires, so a route can react instead:
== start_repairs ==
set_timer(SHIP_ID, "repair", seconds=30, signal="repair_done")
->END
//shared/signal/repair_done
repair_ship(TIMER_AGENT_ID)
->END
set_interval is the repeating sibling — it emits every so often until it is cleared:
== begin_patrol ==
set_interval(SHIP_ID, "patrol", "patrol_beat", seconds=30)
->END
//shared/signal/patrol_beat
pick_new_patrol_point(TIMER_AGENT_ID)
->END
== stand_down ==
clear_interval(SHIP_ID, "patrol")
->END
Every emit carries three variables:
| Variable | Meaning |
|---|---|
TIMER_AGENT_ID |
The agent the timer or interval is on |
TIMER_NAME |
The timer or interval name |
TIMER_COUNT |
Which beat this is — always 1 for a set_timer completion |
Use //shared/signal for anything that acts
A plain //signal/<name> route runs once per connected console, so a five-console
bridge repairs the ship five times — and an interval does it five times a beat. Only
per-console display belongs in //signal. See Signals.
Why it is worth using. The alternative — a watcher task per timer — costs a task resumption every tick, forever, for each one. An armed timer knows its deadline as a number, so the library keeps only the earliest and a tick costs a single comparison. A mission that arms none schedules nothing at all.
What does and does not fire
- Nothing fires early. The signal lands on the same tick
is_timer_finishedstarts answeringTrue. - The timer is untouched.
is_timer_set_and_finished,get_time_remainingandformat_time_remainingall behave exactly as they do without a signal, so a countdown widget and a route can share one timer. - Cleared, re-set without a signal, or its agent deleted → no signal. Re-setting with
signalagain re-arms it. timer_add_timemoves the signal with the deadline — extend a repair and the completion follows; shorten it past zero and it fires at once.- A paused sim does not expire timers, and interval beats missed while paused are skipped rather than delivered in a burst on resume.
- Beats do not drift. Each one is scheduled from the interval's start, not from when the last one happened to be noticed.
Real time vs simulation time
| Function | Time base | Pauses with sim? |
|---|---|---|
delay(seconds) |
Wall clock | No |
delay_sim(seconds) |
Simulation clock | Yes |
API
clear_counter(id_or_obj, name)
Remove a named counter from an agent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Counter name. |
required |
Example
clear_counter(SHIP_ID, "docked")
clear_interval(id_or_obj, name)
Stop an interval started by set_interval.
Identical to clear_counter - an interval IS a counter - and named for
symmetry so a script that starts one can stop it by the same word.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Interval name. |
required |
Example
clear_interval(SHIP_ID, "patrol")
clear_timer(id_or_obj, name)
Clear a named timer so it is no longer set.
After clearing, is_timer_set returns False and
is_timer_finished returns True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Timer name. |
required |
Example
clear_timer(SHIP_ID, "cooldown")
delay_app(seconds=0, minutes=0)
Suspend the current task for a duration measured in real application time.
Application time is not affected by game pause.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seconds
|
int
|
Duration in seconds. Defaults to 0. |
0
|
minutes
|
int
|
Additional duration in minutes. Defaults to 0. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
Delay |
Delay
|
A promise that resolves when the time has elapsed. |
Example
await delay_app(seconds=3) "Three real seconds have passed (even if paused)."
delay_sim(seconds=0, minutes=0)
Suspend the current task for a duration measured in simulation time.
Simulation time can be paused (e.g. when the game is paused).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seconds
|
int
|
Duration in seconds. Defaults to 0. |
0
|
minutes
|
int
|
Additional duration in minutes. Defaults to 0. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
Delay |
Delay
|
A promise that resolves when the time has elapsed. |
Example
await delay_sim(seconds=5) "Five simulation seconds have passed."
delay_test(seconds=0, minutes=0)
Suspend a task for use in unit tests (not real-time).
Uses DelayForTests which counts poll iterations rather than wall or sim
time, so tests run fast without sleeping.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seconds
|
int
|
Simulated duration in seconds. Defaults to 0. |
0
|
minutes
|
int
|
Additional simulated minutes. Defaults to 0. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
DelayForTests |
A promise that resolves after enough poll ticks. |
format_time_remaining(id_or_obj, name)
Return the time remaining on a timer as a M:SS string.
Returns an empty string when the timer has expired or is not set.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Timer name. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
str |
Formatted remaining time, e.g. |
Example
gui_text("Time: {format_time_remaining(SHIP_ID, 'mission')}")
get_counter_elapsed_seconds(id_or_obj, name, default_value=None)
Return the number of seconds elapsed since a counter was started.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Counter name. |
required |
default_value
|
optional
|
Value returned if the counter was never started. Defaults to None. |
None
|
Returns:
| Type | Description |
|---|---|
|
float | None: Seconds elapsed, or |
Example
elapsed = get_counter_elapsed_seconds(SHIP_ID, "docked", 0) if elapsed > 60: "Docking complete."
get_time_remaining(id_or_obj, name)
Return the number of whole seconds remaining on a timer.
Returns 0 when the timer has expired or is not set.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Timer name. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
int |
Seconds remaining, or |
Example
secs = get_time_remaining(SHIP_ID, "mission") if secs < 60: "Less than a minute remaining!"
is_timer_finished(id_or_obj, name)
Return whether a timer has expired. Returns True if the timer is not set.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Timer name. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
bool |
|
Example
if is_timer_finished(SHIP_ID, "repair"): "Repair bay ready."
is_timer_set(id_or_obj, name)
Return whether a named timer exists on an agent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Timer name. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
bool |
|
Example
if not is_timer_set(SHIP_ID, "cooldown"): set_timer(SHIP_ID, "cooldown", seconds=10)
is_timer_set_and_finished(id_or_obj, name)
Return whether a timer was explicitly set and has since expired.
Unlike is_timer_finished, returns False when the timer was never
set. Use this to distinguish "timer done" from "timer never started".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Timer name. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
bool |
|
Example
if is_timer_set_and_finished(SHIP_ID, "cooldown"): clear_timer(SHIP_ID, "cooldown") "Weapons ready!"
set_interval(id_or_obj, name, signal, seconds=0, minutes=0)
Emit a signal on an agent every seconds, until it is cleared.
The repeating sibling of set_timer. Beats are scheduled from the start,
not from when the last one fired, so the period does not drift. Each emit
carries TIMER_AGENT_ID, TIMER_NAME and TIMER_COUNT (1 for the
first beat). Handle it with //shared/signal/<name> for anything with a
side effect - a plain //signal/<name> runs once per console, so a beat
becomes one per console per beat (see SIGNAL_ROUTING.md).
Runs on a counter, so get_counter_elapsed_seconds(id, name) reads the
time since it started and clear_interval (or clear_counter) stops it.
Stops on its own if the agent is deleted. A paused sim does not advance the
beat, and beats missed while paused are skipped rather than caught up on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The agent to run the interval on. |
required |
name
|
str
|
Unique interval name for this agent. |
required |
signal
|
str
|
Signal to emit on every beat. |
required |
seconds
|
int
|
Seconds between beats. Defaults to 0. |
0
|
minutes
|
int
|
Additional minutes between beats. Defaults to 0. |
0
|
Example
set_interval(SHIP_ID, "patrol", "patrol_beat", seconds=30)
//shared/signal/patrol_beat runs on the server every 30 seconds
clear_interval(SHIP_ID, "patrol")
set_timer(id_or_obj, name, seconds=0, minutes=0, signal=None)
Start a named countdown timer on an agent.
Records the expiry tick in the agent's inventory. Use is_timer_finished
or get_time_remaining to check progress.
Pass signal to have the library emit that signal once, when the timer
expires, instead of polling for it. The emit carries TIMER_AGENT_ID and
TIMER_NAME. It is purely additive - the timer is still an ordinary timer
afterwards, so is_timer_set_and_finished and format_time_remaining
behave exactly as they do without it. Handle it with
//shared/signal/<name> for anything with a side effect; a plain
//signal/<name> runs once per console (see SIGNAL_ROUTING.md).
No signal is emitted if the timer is cleared, re-set without signal, or
its agent is deleted before it expires. A paused sim does not advance the
timer, so it does not expire while paused.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
The agent to set the timer on. |
required |
name
|
str
|
Unique timer name for this agent. |
required |
seconds
|
int
|
Duration in seconds. Defaults to 0. |
0
|
minutes
|
int
|
Additional duration in minutes. Defaults to 0. |
0
|
signal
|
str
|
Signal to emit once when the timer expires. Defaults to None (no signal - poll it instead). |
None
|
Example
set_timer(SHIP_ID, "repair", seconds=30) if is_timer_finished(SHIP_ID, "repair"): "Repairs complete!"
set_timer(SHIP_ID, "repair", seconds=30, signal="repair_done")
//shared/signal/repair_done runs on the server when it expires
start_counter(id_or_obj, name)
Record the current sim tick as the start of a named counter.
Use get_counter_elapsed_seconds to read how many seconds have passed
since the counter was started. Use set_interval for a counter that emits
a signal every so often instead of being read.
Restarting a counter that set_interval armed restarts its beat too - the
next one lands a full interval from now.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Counter name. |
required |
Example
start_counter(SHIP_ID, "docked")
later...
secs = get_counter_elapsed_seconds(SHIP_ID, "docked")
timeout(seconds=0, minutes=0)
Create a timeout promise measured in real application time.
Identical to delay_app. Typically passed to await comms(timeout=…)
or similar constructs that accept a timeout promise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seconds
|
int
|
Duration in seconds. Defaults to 0. |
0
|
minutes
|
int
|
Additional duration in minutes. Defaults to 0. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
Delay |
Delay
|
A promise that resolves when the time has elapsed. |
Example
await comms(timeout=timeout(seconds=30))
timeout_sim(seconds=0, minutes=0)
Create a timeout promise measured in simulation time.
Identical to delay_sim. Simulation time can be paused.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seconds
|
int
|
Duration in seconds. Defaults to 0. |
0
|
minutes
|
int
|
Additional duration in minutes. Defaults to 0. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
Delay |
Delay
|
A promise that resolves when the time has elapsed. |
Example
await comms(timeout=timeout_sim(minutes=2))
timer_add_time(id_or_obj, name, seconds=0, minutes=0)
Add (or subtract) time on a timer that is currently running.
A no-op when the timer was never set or has already expired - use set_timer
to start a fresh one. Times may be negative, which shortens the timer and can
expire it outright.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Timer name. |
required |
seconds
|
int
|
Seconds to add. Negative shortens. Defaults to 0. |
0
|
minutes
|
int
|
Additional minutes to add. Defaults to 0. |
0
|
Returns:
| Name | Type | Description |
|---|---|---|
bool |
|
Example
set_timer(SHIP_ID, "repair", seconds=30) timer_add_time(SHIP_ID, "repair", seconds=15) # damaged mid-repair
timer_signals_clear()
Drop every armed timer/counter signal (mission reset).
timer_signals_count()
How many timers/counters are armed to emit a signal (reset audit).