Skip to content

Work orders and maintenance

What a damage-control team has been told to go and do - and why systems now need looking after, not only fixing.

Overview

A grid node has two independent things going on.

Damage is the pair of roles the engine and every existing query already use: __damaged__ or __undamaged__. A damaged node is broken and contributes nothing.

Condition is wear on top of that, and __worn__ is its role. Wear runs 0.0 (perfect) to 1.0 (worn out), and lands a node in one of four tiers:

tier wear draws worth
damaged - the theme's damage_colors (Crimson) 0
worn >= 0.60 the theme's worn_colors (Gold) 0.75
nominal between the node's own healthy color 1.0
tuned <= 0.10 the theme's tuned_colors (cyan) 1.10

A worn node keeps __undamaged__. The two axes never merge: __worn__ only ever coexists with __undamaged__, never with __damaged__. That is what keeps the explode check, system_damage[] and every mission's own role("__undamaged__") query meaning exactly what they always meant - an all-worn ship cannot blow up.

Nothing happens until something writes wear. An untouched node reads WEAR_NOMINAL and weighs exactly 1.0, so set_damage_coefficients produces numbers identical to the undamaged / total fraction it replaced. A mission that never opts in sees no change at all.

Where the work comes from

Nothing invents maintenance. A damage-control team patches a room; it does not rebuild it - so a node a team repaired comes back __worn__ and wants a maintenance order before it is itself again. A dockyard repair (docking, or grid_repair_system_damage) restores it fully. grid_repair_grid_objects tells the two apart by whether it was given a repairer.

LegendaryMissions/damage/wear.mast adds three more sources - firing, taking hits, and a per-minute beat for time and travel. A mission can wire its own instead, or none at all.

Orders

An order is a link: link(dc, "work-order", node). That has always been the model and still is - missions outside this repo file orders that way and every one of them keeps working.

An order is a property of the TARGET; the link is the assignment.

A node carries at most one order - a kind and a priority, in its own inventory. Any number of teams link to it. A target with links but no record synthesizes one, so a bare link() reads back as an ordinary order.

Two kinds: KIND_REPAIR for a damaged node, KIND_MAINTAIN for a worn or nominal one. work_order_kind_wanted answers what a node would accept, not what it needs - a healthy system can be tuned, and that is how the tuned tier is earned at all. Only an already-tuned node answers None. Maintenance is satisfied when the node is TUNED, not merely when it stopped being worn; reading it the other way made an order on a nominal node complete on its first read and get purged before anyone moved.

A maintenance order marks its target with MAINTENANCE_ROLE (__maintenance__) so a brain can match on it. It has to: the brain picks its idle room by role, and a nominal node is not __worn__. A bare link() never carries it, which is right - a bare link has always meant a repair. If a node under a tune order breaks, the order is promoted to a repair at normal priority rather than left as a tune job on something in pieces. Priorities are plain numbers with four named rungs - PRIORITY_LOW (10), NORMAL (50), HIGH (80), CRITICAL (100) - so a mission can invent its own and work_order_bump still moves sensibly from the nearest rung.

Orders are purged as they are read

They never used to be. Agent._remove clears the role and link registries when a node dies, but not the entry in another agent's own link set - so a team's order on a deleted room outlived it forever and every count drawn from it was wrong.

work_orders_for drops an order when the target was deleted, its host is gone or exploding, it belongs to another ship (a grid rebuild replaces every id), the work is already done, the worker's ship exploded, or the worker itself is dead. It walks one team's link set - normally nought to three ids - and every caller already runs per damcon, so there is no sweep to schedule and no module state to reset.

Repair closes a job for every team on it. Dropping only the repairer's link left a second team walking to a room that was already fixed.

Quick example

# Send a team, and let the kind default to what the node needs
work_order_add(dc_id, room_id)

# Make it urgent - the brain preempts its current target once
work_order_set_priority(room_id, 100)

# What the console shows, highest priority first
for row in work_order_rows(SHIP_ID):
    print(f"{row['name']} {row['kind']} {row['priority']} {row['workers']}")
from sbs_utils.procedural.work_orders import (work_order_add, work_order_best,
                                              KIND_MAINTAIN, PRIORITY_HIGH)

work_order_add(dc_id, node_id, KIND_MAINTAIN, PRIORITY_HIGH)
target = work_order_best(dc_id, committed)   # what the team should walk to

Choosing what to do next

work_order_best(worker, committed, room) picks the closest order in the highest live priority band - but keeps the commit it already made unless something strictly outranks it. That is not a nicety: recomputing the straight-line closest every tick makes the choice flip as a team walks the corridor between two orders. With every order at the default priority it behaves exactly as the plain nearest-first pick it replaced.

Preemption happens once, because the new choice is committed too. Two equal orders can never trade a team back and forth.

Tuning

Every threshold and arrival rate is a module global, and grid_set_wear_tuning moves them at runtime:

grid_set_wear_tuning(tuned_bonus=0.0)      # maintenance, but no over-unity
grid_set_wear_tuning(upkeep_rate=0)        # no time-based wear at all
grid_set_wear_tuning(beam_hit=0.0005, warp_minute=0.05)

Rates are named by their short name (beam_hit -> WEAR_PER_BEAM_HIT). An unknown name warns rather than silently doing nothing - a typo in a dial otherwise reads exactly like "the dial has no effect".

dial default events to wear a node out
beam_hit 0.002 ~175 hits landed
tube_shot 0.01 ~35 launches
shield_hit 0.004 ~88 hits taken on that facing
impulse_minute 0.004 ~88 min at full impulse
warp_minute 0.02 ~18 min at warp 1
upkeep_rate 0.005 ~70 min, every node

Themes

worn_colors and tuned_colors are optional theme maps beside colors and damage_colors. No shipped theme has them, and every lookup falls back to GRID_WORN_COLOR / GRID_TUNED_COLOR, so the tiers work out of the box. A mission re-skins them the usual way - extra_grid_theme.json, or grid_merge_mod_theme:

{"name": "cosmos",
 "worn_colors":  {"default": "#FFC83C"},
 "tuned_colors": {"default": "#40E0E0"}}

Gotchas

  • work_order_kind_wanted answers None only for an already-tuned node. Gate a menu on that, not on __damaged__, or maintenance can never be offered at all.
  • A brain node that filters on room: __damaged__ will never see a maintenance order. ai_lifeform_move_to_work_order defaults to no filter for that reason, and the idle room matches __maintenance__ rather than __worn__.
  • Only a tier change recomputes coefficients. Wear moving within a band costs one dict write, which is what makes a per-minute sweep over every node cheap.
  • grid_wear_system picks nodes at random rather than spreading wear evenly. Even spreading would move a whole pool across the threshold together, so a ship would go from fine to fully worn in one tick with nothing in between.

Work orders: what a damage-control team has been told to go and do.

The model was, and still is, a LINK - link(dc, "work-order", node). This module does not replace that; it is a superset layered on the same link, because missions outside this repo already file orders with a bare link() and must keep working.

An order is a property of the TARGET; the link is the assignment.

A node has at most one order - a KIND and a PRIORITY, kept in the node's own inventory. Any number of teams can be linked to it. A target that has links but no record gets a synthesized one, which is exactly what a bare link() produces, so an old mission's orders read back as ordinary orders with sensible defaults.

There is deliberately no __work_order__ role. It would make "every node with an order" a cheap set intersection, but a bare link() would not carry it, so the query would silently under-report precisely the missions this design exists to protect. has_link(WORK_ORDER_LINK) already gives the sources for free.

Orders are purged as they are read. They never were before: Agent._remove clears the role and link REGISTRIES when a node dies, but not the entries in another agent's own link set, so a team's link to a deleted node survived forever and the count on the console was permanently wrong. work_orders_for walks one team's link set - normally nought to three ids - and drops what is no longer real, and every caller already runs per damcon. No timer, no module-level state, nothing to register with the reset ledger.

work_order_add(worker, id_or_obj, kind=None, priority=None)

Send a team to a node, filing (or refreshing) the order on it.

Accepts sets on either side, exactly as link does.

Parameters:

Name Type Description Default
worker

the damcon team (or a set of them).

required
id_or_obj

the target node (or a set of them).

required
kind str

KIND_REPAIR / KIND_MAINTAIN. Defaults to what the node currently needs.

None
priority int

defaults to NORMAL for a repair, LOW for maintenance. An existing priority is KEPT unless one is passed - a second team joining a job must not quietly demote it.

None

Returns:

Type Description

dict | None: the order on the last target touched.

work_order_best(worker, committed=None, room=None)

Which order this team should be walking to right now.

Highest live priority band, closest within it - and the team STAYS COMMITTED to what it already chose unless something strictly outranks it. That commit is the anti-oscillation property: recomputing the straight-line closest every tick makes the choice flip as the team walks the corridor between two orders. With every order at the default priority this is exactly the old behavior.

Parameters:

Name Type Description Default
worker

the damcon team.

required
committed int

the target already chosen, from the blackboard.

None
room str

restrict to nodes with this role. Falsy means no filter - which is what lets a maintenance order be picked at all.

None

Returns:

Type Description

int | None: the chosen target id.

work_order_bump(id_or_obj, step=1)

Move an order along PRIORITY_STEPS, clamped at both ends.

Parameters:

Name Type Description Default
id_or_obj

a grid node.

required
step int

rungs to move; negative lowers. Defaults to 1.

1

Returns:

Type Description

int | None: the new priority, or None if the node has no order.

work_order_cancel(worker, id_or_obj)

Take one team off a node. The order survives while anyone is still on it.

work_order_cancel_all(id_or_obj)

Close a node's order for EVERY team on it.

What repair does. Dropping only the repairer's own link left a second team walking to a room that was already fixed - the role("__damaged__") filter stopped them acting on it, silently, but the link itself never went away.

work_order_get(id_or_obj, ensure=False)

The order on a node, synthesizing one for a bare link().

Parameters:

Name Type Description Default
id_or_obj

a grid node.

required
ensure bool

persist a synthesized record. Defaults to False, which keeps this a pure read.

False

Returns:

Type Description

dict | None: {"kind": ..., "priority": ...}, or None when nothing is

assigned to this node at all.

work_order_is_satisfied(id_or_obj)

Whether the work this order asked for has been done.

A repair is satisfied when the node is no longer damaged. Maintenance is satisfied when the node is tuned - not merely when it stopped being worn. Reading it as "no longer __worn__" made an order on a nominal node instantly complete, so it was purged on the first read and a healthy system could never be tuned at all.

A node with no order is trivially satisfied.

work_order_kind(id_or_obj)

The order's kind, or None when the node has no order.

work_order_kind_wanted(id_or_obj)

What kind of order this node would ACCEPT, or None if there is nothing to gain.

Note "accept", not "need". A nominal node takes a maintenance order too - tuning it is how a crew earns the tuned tier at all. Gating this on __worn__ made cyan reachable only by neglecting a system and then fixing it, which is the exact opposite of rewarding a well-run ship.

But only a SYSTEM node can be tuned. Tuning is what moves a system's effectiveness, and a gymnasium has none to move - offering a damage-control team to go and tune one was the tell that the model had been applied a room too wide. A crew space still takes a REPAIR: a fire in the galley is a real fire.

An already-tuned system answers None too: there is genuinely nothing left to do.

Parameters:

Name Type Description Default
id_or_obj

a grid node.

required

Returns:

Type Description

str | None: KIND_REPAIR for a damaged node, KIND_MAINTAIN for a worn or

nominal SYSTEM, None for a crew space in one piece or a system already at

spec.

work_order_priority(id_or_obj)

The order's priority, or 0 when the node has no order.

work_order_purge_ship(id_or_obj)

Sweep every team on a ship. Returns how many orders were dropped.

For the events that invalidate every id at once - a ship destroyed, an interior rebuilt - where waiting for each team's next read would leave the console reporting orders on nodes that no longer exist.

work_order_purge_worker(worker)

Sweep one team's orders. Returns how many were dropped.

work_order_rows(id_or_obj)

Every order on a ship as display rows, highest priority first.

Parameters:

Name Type Description Default
id_or_obj

the ship.

required

Returns:

Type Description

list[dict]: target, name, kind, priority, state and a

sorted workers list of team names.

work_order_set_priority(id_or_obj, priority)

Set an order's priority. No-op on a node with no order.

work_order_targets(id_or_obj)

Every node on this ship that has an order, whoever it is assigned to.

work_order_workers(id_or_obj)

Every team currently assigned to this node.

Parameters:

Name Type Description Default
id_or_obj

a grid node.

required

Returns:

Type Description

set[int]: the damcon ids linked to it.

work_orders_for(worker, purge=True)

This team's live work orders, dropping any that are no longer real.

THE purge point. Six ways an order stops being real, every one of which used to leave a link behind forever:

  • the target was deleted (Agent._remove clears the registries, not this set)
  • the target's host is gone or exploding
  • the target is not on the worker's ship any more - a grid rebuild replaces every id, so an old id can even collide with a new node
  • somebody else already did the work
  • the worker's ship exploded
  • the worker itself is dead

Parameters:

Name Type Description Default
worker

the damcon team.

required
purge bool

actually unlink what is dropped. Defaults to True; pass False for a read that must not write (a signature, a probe).

True

Returns:

Type Description

set[int]: the still-valid target ids.