Here's my attempt at the problem. This first post contains all of control.lua, the second post contains explanations of the changes:
----------------------------------------------------------------
-- SomeTrainStationsAreOptional v1.0.1 control.lua
----------------------------------------------------------------
-- TODO
-- on_train_schedule_changed ?
-- make some options controlled via mods settings (dbg, pat)
----------------------------------------------------------------
local dbg = false
local pat = '?'
----------------------------------------------------------------
local function myTrainChangedStateHandler(event)
local idx = event.train.schedule.current -- idx, which line of the schedule we're on
if defines.train_state.wait_station == event.train.state
then
storage.arrivals[event.train.id] = idx -- save idx of where this train last arrived
else
local nbr = #event.train.schedule.records -- number of stations in this train's schedule
local lw = storage.arrivals[event.train.id] -- idx of where this train last waited at a station
if nbr > 1 and -- at least two stops in the entire schedule
lw and -- true if we know the station idx where this train last waited
(defines.train_state.no_path == event.train.state or -- true if NO_PATH
defines.train_state.destination_full == event.train.state) -- true if DESTINATION_FULL
then -- we might want to skip this stop
local reachable = false
local target = idx -- idx of station where this train presently wants to go
local name = event.train.schedule.records[target].station -- name of station where this train presently wants to go
local tmp = event.train.schedule.records[target].temporary -- true if presently wanted station is a temporary stop
local at_start = (pat == string.sub(name,1,1)) -- true if start of station name matches our pattern
local at_end = (pat == string.sub(name,-1,-1)) -- true if end of station name matches our pattern
local optional = at_start or at_end -- true if wanted station is optional
local wrapped = (target == lw) -- true if we wrap around to where we are now
while optional and not wrapped and not reachable and not tmp do
if dbg then game.print("skipping optional station " .. name) end
target = (target+1) > nbr and 1 or (target+1) -- this is where we try next
event.train.go_to_station(target)
reachable = event.train.recalculate_path(true)
name = event.train.schedule.records[target].station -- name of station where this train presently wants to go
tmp = event.train.schedule.records[target].temporary -- true if presently wanted station is a temporary stop
at_start = ('?' == string.sub(name,1,1)) -- true if start of station name matches our pattern
at_end = ('?' == string.sub(name,-1,-1)) -- true if end of station name matches our pattern
optional = at_start or at_end -- true if wanted station is optional
wrapped = (target == lw) -- true if we wrap around to where we are now
end
-- SHEMP EDIT: Add a temp stop to alleviate schedule oscillation
if wrapped then
local sched = event.train.get_schedule()
sched.add_record{
index = {schedule_index = target + 1},
station = name,
temporary = true,
wait_conditions = {{
type = "time",
ticks = 300,
}},
}
event.train.go_to_station(target + 1)
end
end
end
end
----------------------------------------------------------------
local function initStorage()
if not storage.arrivals then
storage.arrivals = {}
end
end
script.on_init(initStorage)
script.on_configuration_changed(initStorage)
script.on_event(defines.events.on_train_changed_state, myTrainChangedStateHandler)
----------------------------------------------------------------
----------------------------------------------------------------