Timers and counters
Wait for a duration, and measure how long something has been running.
Overview
There are two separate things on this page.
Delays are awaitable promises: the task stops at the await and resumes when the
time is up. Two time bases:
- Simulation time (
delay_sim) - scaled by the engine's simulation clock, so it stops while the sim is paused. This is the one a mission wants. - Real time (
delay_app) - wall-clock seconds, unaffected by simulation speed.
timeout_sim / timeout are the same two clocks in the form promise_any expects, for
racing a delay against something else.
Timers and counters are not awaitable. Each one is a single value in an agent's
inventory and nothing runs on its behalf, which is why a mission can hold hundreds of
them for free - a script asks about them, or arms one with a signal (see below).
- A timer counts DOWN to a deadline:
set_timer, thenis_timer_finished,get_time_remaining,format_time_remaining. - A counter counts UP from a start:
start_counter, thenget_counter_elapsed_secondsorformat_counter_elapsed_seconds.clear_counterstops it.
await delay_sim(seconds=5)
set_timer(SHIP_ID, "repair", seconds=30)
start_counter(SHIP_ID, "in_combat")
0 means the server
Every timer and counter takes an agent, and id 0 is the server - the usual
place to hang a mission-wide clock. set_timer(0, "mission_clock", minutes=20) and
start_counter(0, "Mission_Elapsed_Time") are ordinary usage.
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
== mission_clock ==
# id 0 is the server, so this is the whole mission's clock.
start_counter(0, "Mission_Elapsed_Time")
->END
== show_elapsed ==
elapsed = get_counter_elapsed_seconds(0, "Mission_Elapsed_Time")
log(f"{int(elapsed)} seconds into the mission")
# Or ready-formatted. Only the units you name are filled, and the
# largest one present carries the overflow: "mm:ss" gives "90:00",
# never "00:00", for an hour and a half.
gui_text(format_counter_elapsed_seconds(0, "Mission_Elapsed_Time"))
->END
from sbs_utils.procedural.timers import (delay_sim, delay_app, set_timer,
is_timer_finished, start_counter,
get_counter_elapsed_seconds, clear_counter)
# Wait 10 simulation seconds before continuing
await delay_sim(seconds=10)
# Wait 5 real seconds
await delay_app(seconds=5)
# Count DOWN to a deadline
set_timer(ship_id, "cooldown", seconds=30)
if is_timer_finished(ship_id, "cooldown"):
fire_again(ship_id)
# Count UP from a start. 0 is the server.
start_counter(0, "Mission_Elapsed_Time")
elapsed = get_counter_elapsed_seconds(0, "Mission_Elapsed_Time")
clear_counter(0, "Mission_Elapsed_Time")
The mission clock
"How long have we been out here" is asked by the crew, not by one script, so the library
keeps that one counter itself. map_start stamps it - which every mission goes through -
so a mission has a clock without starting one:
| function | answers |
|---|---|
mission_elapsed_seconds() |
seconds since the mission started, as a float |
mission_elapsed_text(display="hh:mm:ss") |
the same, formatted and rounded |
mission_clock_start() |
restart it, when the real beginning is later |
== show_mission_time ==
gui_text(f"$text:{gui_text_escape(mission_elapsed_text())};")
log(f"{int(mission_elapsed_seconds())} seconds in")
->END
It is sim time, like everything else here, so a paused sim does not age the mission. A
mission whose real beginning is the end of a cutscene calls mission_clock_start() there
and the clock re-zeros. Nothing reads None: with no start stamped the clock answers with
the sim's own age.
The ePADD home screen draws it at the right end of its bar as T+hh:mm:ss, in the
wordmark's own size, and moves it on with gui_app_home_tick() from an on change - the
widget, never a page repaint.
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? | Use for |
|---|---|---|---|
delay_sim(seconds) |
Simulation clock | Yes | Anything in the mission |
delay_app(seconds) |
Wall clock | No | UI pacing, outside the sim |
timeout_sim(seconds) |
Simulation clock | Yes | Racing with promise_any |
timeout(seconds) |
Wall clock | No | Racing with promise_any |
Timers and counters run on the simulation clock as well, so a paused sim neither expires a timer nor advances a counter.
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_counter_elapsed_seconds(id_or_obj, name, display='hh:mm:ss')
Return a counter's elapsed time as a formatted string, rounded to the second.
The counterpart to format_time_remaining for counters, with the layout
left to the caller: hh is hours, mm minutes, ss seconds. Any other
text in display is kept as-is, so "mm minutes, ss seconds" works.
Only the units NAMED in display are used, and the largest one present
absorbs everything above it - "mm:ss" reports 90 minutes as "90:00"
rather than silently dropping the hour, which is the same convention
format_time_remaining follows with its M:SS.
A counter that was never started reads as zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id_or_obj
|
Agent | int
|
Agent ID or object. |
required |
name
|
str
|
Counter name. |
required |
display
|
str
|
Layout to fill in. Use |
'hh:mm:ss'
|
Returns:
| Name | Type | Description |
|---|---|---|
str |
The formatted elapsed time. |
Example
gui_text("Elapsed: {format_counter_elapsed_seconds(SHIP_ID, 'mission')}")
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!"
mission_clock_start()
Stamp now as the start of the mission.
Called by map_start - the one door every mission goes through - so a
mission gets a clock without asking for one. Call it directly to restart the
clock at a moment the mission thinks is the real beginning (the end of a
cutscene, say).
Example
mission_clock_start()
mission_elapsed_seconds()
Sim seconds since the mission started.
Falls back to the sim's own clock when nothing stamped a start - a mission
that never calls map_start has been running for as long as its sim has,
which is the honest answer and never None.
Returns:
| Name | Type | Description |
|---|---|---|
float |
Seconds since the mission started. |
Example
if mission_elapsed_seconds() > 600: "You have been out here ten minutes."
mission_elapsed_text(display='hh:mm:ss')
The mission clock as text, rounded to the second.
Same layout tokens as format_counter_elapsed_seconds: hh, mm,
ss, with the largest one present absorbing everything above it. Fixed
width by default, so a readout drawn with it does not twitch as the digits
roll over.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
display
|
str
|
Layout to fill in. Defaults to |
'hh:mm:ss'
|
Returns:
| Name | Type | Description |
|---|---|---|
str |
The formatted elapsed time. |
Example
gui_text("$text:{gui_text_escape(mission_elapsed_text())};")
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).