The objective module
The tick scheduler that drives brains, docking, and other background systems.
Overview
The objective module manages a recurring tick task that runs registered background systems once per second. Most library systems that need periodic evaluation (brains, docking, extra scan sources) piggyback on this tick rather than creating their own TickDispatcher intervals.
Mission scripts rarely call objective functions directly — brain_schedule, docking_schedule, and similar functions call objective_schedule automatically. The one exception is objective_add / objective_remove, which let you register your own callback to be called every objective tick.
Quick example
== setup ==
objective_add(check_mission_state)
== teardown ==
objective_remove(check_mission_state)
from sbs_utils.procedural.objective import objective_add, objective_remove, objective_schedule
def my_tick(tick_task):
# Called every ~1 second
check_mission_state()
# Register your tick callback
objective_add(my_tick)
# Remove when done
objective_remove(my_tick)
API
Manage all objective
Objective
Bases: Agent
done
property
Is the objective completed? Returns: bool: True if the objective is complete.
result
property
writable
Get the result of the objective. Returns: PollResults: The result.
__init__(agent, label, data, client_id)
Create an Objective. Args: agent (Agent | int): The agent or id for this objective label (str | Label): The objective label to run data (dict): Data to associate with this objective. May not be used. client_id (int): The client ID for this objective. May not be used.
force_clear()
Clear this objective from its agent and undesignates it as an objective.
run()
Run the objective label.
run_sub_label(loc)
Run the sublabel with the specified index. Args: loc (int): The index of the sublabel. Returns: PollResults: The result of the sublabel task.
stop_and_leave(result=PollResults.FAIL_END)
Stop the label run with a result. Args: result (PollResults): The result of the objective label.
game_end_condition_add(promise, message, is_win, music=None, signal=None)
Register a promise that ends the game when it resolves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
promise
|
Promise
|
Resolving this promise triggers the end condition. |
required |
message
|
str
|
Message displayed on the results screen. |
required |
is_win
|
bool
|
|
required |
music
|
str
|
Music file to play at end. Defaults to None (uses the default victory/failure track). |
None
|
signal
|
str
|
Signal to emit instead of
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
Handle ID that can be passed to |
game_end_condition_remove(id)
Remove a registered game end condition.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
id
|
int
|
Handle returned by |
required |
game_end_conditions_count()
How many end-game conditions are registered.
Exists so the restart soak's reset audit can probe this container by name. An unregistered container is invisible to that audit, which is why a --runs soak reported STABLE while stale conditions were ending every replay early.
game_end_run_all(tt)
Poll all registered game end conditions and trigger the end screen if any resolve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tt
|
Task
|
Tick task (unused). |
required |
objective_add(agent_id_or_set, label, data=None, client_id=0)
Add an objective label to one or more agents.
label may be a label name, a Label object, a dict with "label" and
"data" keys, or a list of any of these. One Objective is created
per (agent, label) pair.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent_id_or_set
|
Agent | int | set[Agent | int]
|
Agent(s) to attach the objective to. |
required |
label
|
str | Label | dict | list
|
The objective label(s) to run. |
required |
data
|
dict
|
Variables passed into the objective label. Defaults to None. |
None
|
client_id
|
int
|
Console client ID (reserved for future use). Defaults to 0. |
0
|
Returns:
| Type | Description |
|---|---|
|
Objective | list[Objective]: A single Objective when exactly one is created, otherwise a list. |
objective_clear(agent_id_or_set)
Remove all active objectives from one or more agents.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
agent_id_or_set
|
Agent | int | set[Agent | int]
|
Agent(s) whose objectives should be cleared. |
required |
objective_extends(label, data=None)
Run an objective label as a sub-task of the current task.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str | Label
|
The label to execute. |
required |
data
|
dict
|
Variables to pass into the sub-task. Defaults to None. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
MastAsyncTask |
The scheduled sub-task. |
objective_reset()
Forget the scheduled tick tasks so the next mission re-registers them.
These three module globals are "already scheduled" latches. A mission restart calls TickDispatcher.clear(), which throws the tasks away - but the latches stayed set, so objective_schedule() decided there was nothing to do and NOTHING EVER DROVE BRAINS OR OBJECTIVES AGAIN. NPCs still spawn and still get brains attached; they simply never think, never move, and never report an error.
The engine forks a fresh process per mission and so never hits this. The dev
runner reuses the interpreter, so there it means every mission after the first
plays with dead AI - which is how it turned up: a --runs soak showed moving
dropping 41 -> 6 while npcs and brains stayed put.
objective_schedule()
Ensure the background tick task that drives objectives is running.
objective_ticks_stale()
True if we think the tick tasks are scheduled but the dispatcher has lost them.
Exactly the state above: latched but not running. Registered with the reset ledger so a restart soak names it instead of leaving an AI-less mission.
objectives_run_all(tick_task, pass_seconds=None)
Poll active OBJECTIVE_RUN objectives; skip any whose agent is gone.
pass_seconds controls batching (mirrors brains_run_all):
None(default) - run all objectives this call (original behavior).- a number - run only a rolling slice this call, sized so a full pass
over every objective completes in ~
pass_secondsof sim time, spreading a large goal list across ticks instead of one batch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tick_task
|
Task
|
Tick task (unused). |
required |
pass_seconds
|
If set, spread a full pass over ~this many seconds. |
None
|
objectives_run_everything(tick_task)
Run one slice of the per-tick work: objectives, brains, scan sources, or game-end checks.
Work is spread across three alternating states to avoid all processing
happening in the same tick. tick_task.state selects which slice runs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tick_task
|
Task
|
The repeating tick task — |
required |