Skip to content

API Add on trips

Marcel1853 edited this page Oct 21, 2026 · 6 revisions

🇩🇪 Deutsche Version · ⬅ For mod authors

Add-ons: trips

Note

New in 0.0.20.

An add-on can send trains off itself (jobs), start normal UTL deliveries, insert stops into running deliveries and have a say in choosing the train. The planning happens in the add-on – UTL writes the stops into the schedule and reports what happens.

Jobs

A job has nothing to do with goods: it is a list of stops with any wait conditions – to the siding, to the workshop, to the rebuild station, a round trip …

Function Effect
send_job(train_id, mod, stops) Send a train over own stops. Returns the job id or nil and a reason: "unknown-train", "bad-mod", "busy" (has a delivery or a job), "held-by-other", "owned-by-other", "no-stops", "bad-target", "station-full" or "unreachable" (each plus the number of the stop), "no-schedule".
cancel_job(id) Cancel a job; its stops disappear, the train carries on with its schedule. Returns true if it existed.
get_job(id) { id, mod, train_id, stops (count), started (tick) } or nil

Each entry in stops is one of these, each with wait = wait conditions (as in LuaSchedule; without wait the train passes through):

Target Meaning
{ station = unit } a UTL station
{ stop = train_stop } any train stop (LuaEntity or its unit_number) – also one without UTL
{ rail = rail, rail_direction = … } a piece of rail
{ position = { x, y }, surface = … } a waypoint by position: UTL looks for the rail there (radius 2 tiles; surface defaults to the train's)

Without rail_direction UTL takes the direction in which the train reaches the rail according to path finding. All of this is optional: for stations and train stops UTL sets the waypoint in front of them by itself – an add-on only needs own waypoints if it wants to set the route more precisely (e.g. over a certain track).

Several add-ons: a train belongs to the add-on that holds it (also during a job) or in whose depot it stands free (own_trains). Other add-ons do not get it: send_job rejects with "held-by-other" or "owned-by-other", hold_train returns false. Free UTL trains may be taken by any add-on – they leave UTL's depot pool until the job is done.

Separate rail networks: UTL checks by path finding that the first stop is reachable from the train and every further one from the previous stop (the train may reverse at a stop). Otherwise "unreachable" with the number of the stop. skip_path_check = true on a stop skips the check. Path finding takes some time – it only runs when a job is created.

Train limit: if a target train stop is full (the stop's train limit or "max trains" of the UTL station), send_job rejects with "station-full" and the number of the stop – just as UTL checks for deliveries. ignore_limit = true on a stop skips this. UTL drives to train stops via a waypoint in front of them and reserves the trip, so other trains count the stop as taken. If the train limit is set only after the train is on its way, it drives up to the stop and waits there – the add-on handles that itself (e.g. with cancel_job).

local job = remote.call("utl", "send_job", train_id, "my-mod", {
  { station = siding, wait = { { type = "time", ticks = 600 } } },
  { stop = workshop, wait = { { type = "inactivity", ticks = 300 } } },
})

What happens:

  1. UTL removes old temporary UTL stops (e.g. the waypoint into the depot), writes the job's stops after the current stop and sends the train off (manual mode is switched off).
  2. During the job the train is held: no dispatcher, no chained delivery, no refuelling, no cancelling (holding trains).
  3. At UTL stations on_train_arrived/on_train_departed come with job_id.
  4. When all stops are passed, on_job_finished comes; the train is free again and carries on with its own schedule, usually back to the depot.

If the train is rebuilt without the add-on announcing it, the job ends with reason = "rebuilt"; if the train disappears, with "train-lost".

Starting deliveries

Function Effect
create_delivery{ provider, requester, type, name, quality, amount, train } create a normal UTL delivery – with reservations, loading filters, statistics and events like any other.
  • provider, requester = stations (unit); both need a train stop with rail.
  • type = "item" (default) or "fluid", quality default "normal", amount = amount.
  • train = train id of a free UTL train; without train UTL takes the nearest free train that fits (network, length, cargo space, fuel).
  • More than the train holds is not loaded.
  • Returns the delivery id or nil and a reason: "bad-spec", "bad-station", "not-enough" (the provider does not have that much free), "no-train", "unreachable", "schedule".

Own schedule entries

An add-on may also change its trains' schedules itself:

  • Permanent stops, train groups and interrupts are never touched by UTL – it only works with temporary entries after the current stop.
  • Own temporary entries (added past UTL with LuaSchedule.add_record) stay when UTL cleans up (cancelling, a new job, a rebuild): UTL remembers which temporary entries it wrote itself and only removes those.
-- interrupt "low on fuel → workshop" – UTL leaves it alone
local schedule = train.get_schedule()
schedule.add_interrupt({ name = "Workshop", conditions = { { type = "fuel_item_count_any", condition = {
  first_signal = { type = "item", name = "coal" },
  comparator = "<", constant = 5 } } }, targets = { { station = "Workshop",
  wait_conditions = { { type = "time", ticks = 600 } } } }, inside_interrupt = false })

Inserting stops into deliveries

Function Effect
add_delivery_stop(delivery_id, spec) Insert a stop into a running delivery. Returns true or false and a reason ("unknown-delivery", "bad-position", "bad-target", "unreachable", "no-schedule", "stop-not-found").

spec = target as for jobs (station, stop or rail), wait = wait conditions and where:

where allowed while the delivery …
"before_provider" is still on its way to the provider
"after_provider" (default) is on its way to the provider or loading there
"after_requester" is not finished yet

Inserted stops do not count for the course of the delivery and disappear with it if it is cancelled. get_delivery shows their number in extra_stops.

Train filter

Function Effect
register_train_filter{ mod, interface, filter } have a say in choosing the train (register in on_init and on_configuration_changed)

Only when the provider or requester of a trip has an add-on role, UTL calls it – once per dispatch with all candidates, best first, also for the chained delivery right after a delivery:

remote.add_interface("my-mod", {
  filter = function(train_ids, info)
    -- info = { provider, requester, key, provider_role, requester_role }
    local allowed = {}
    for _, id in ipairs(train_ids) do
      if fits(id, info) then allowed[#allowed + 1] = id end
    end
    return allowed   -- empty list = none; nil = all as before
  end,
})
remote.call("utl", "register_train_filter", { mod = "my-mod", interface = "my-mod", filter = "filter" })

The order of the result is the preferred order. If the filter rejects all, UTL tries again on the next run – so keep the filter fast (Performance).

Clone this wiki locally