Skip to content

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, then is_timer_finished, get_time_remaining, format_time_remaining.
  • A counter counts UP from a start: start_counter, then get_counter_elapsed_seconds or format_counter_elapsed_seconds. clear_counter stops 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_finished starts answering True.
  • The timer is untouched. is_timer_set_and_finished, get_time_remaining and format_time_remaining all 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 signal again re-arms it.
  • timer_add_time moves 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 for hours, mm for minutes and ss for seconds. Defaults to "hh:mm:ss".

'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. "1:30", or "" if expired.

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 default_value if not set.

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 0 if expired or not set.

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

True if the timer has expired or was never set.

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

True if the timer has been set (even if already expired).

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

True only if the timer was set and has now expired.

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".

'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

True if a running timer was adjusted.

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).