Files
project-cybersyn/cybersyn/scripts/central-planning.lua
Monica Moniot f30ce4e08f Merge pull request #41 from matthew-sharp/threshold-override
Provider threshold override fixes
2023-03-13 18:21:08 -04:00

828 lines
27 KiB
Lua

--By Mami
local min = math.min
local max = math.max
local abs = math.abs
local ceil = math.ceil
local INF = math.huge
local btest = bit32.btest
local band = bit32.band
local table_remove = table.remove
local random = math.random
---@param map_data MapData
---@param station Station
---@param manifest Manifest
---@param sign -1|1
function remove_manifest(map_data, station, manifest, sign)
local deliveries = station.deliveries
for i, item in ipairs(manifest) do
deliveries[item.name] = deliveries[item.name] + sign*item.count
if deliveries[item.name] == 0 then
deliveries[item.name] = nil
end
end
set_comb2(map_data, station)
station.deliveries_total = station.deliveries_total - 1
if station.deliveries_total == 0 and band(station.display_state, 1) > 0 then
station.display_state = station.display_state - 1
update_display(map_data, station)
end
end
---@param map_data MapData
---@param r_station_id uint
---@param p_station_id uint
---@param train_id uint
---@param manifest Manifest
function create_delivery(map_data, r_station_id, p_station_id, train_id, manifest)
local economy = map_data.economy
local r_station = map_data.stations[r_station_id]
local p_station = map_data.stations[p_station_id]
local train = map_data.trains[train_id]
local depot = map_data.depots[train.depot_id]
if not train.entity.valid then
on_train_broken(map_data, train_id, train)
interface_raise_train_dispatch_failed(train_id)
return
end
if not depot.entity_stop.valid then
on_depot_broken(map_data, train.depot_id, depot)
interface_raise_train_dispatch_failed(train_id)
return
end
if not p_station.entity_stop.valid then
on_station_broken(map_data, p_station_id, p_station)
interface_raise_train_dispatch_failed(train_id)
return
end
if not r_station.entity_stop.valid then
on_station_broken(map_data, r_station_id, r_station)
interface_raise_train_dispatch_failed(train_id)
return
end
local is_at_depot = remove_available_train(map_data, train_id, train)
--NOTE: we assume that the train is not being teleported at this time
--NOTE: set_manifest_schedule is allowed to cancel the delivery at the last second if applying the schedule to the train makes it lost and is_at_depot == false
local r_enable_inactive = mod_settings.allow_cargo_in_depot and r_station.enable_inactive--[[@as boolean]]
if set_manifest_schedule(map_data, train.entity, depot.entity_stop, not train.use_any_depot, p_station.entity_stop, p_station.enable_inactive, r_station.entity_stop, r_enable_inactive, manifest, is_at_depot) then
local old_status = train.status
train.status = STATUS_TO_P
train.p_station_id = p_station_id
train.r_station_id = r_station_id
train.manifest = manifest
train.last_manifest_tick = map_data.total_ticks
r_station.last_delivery_tick = map_data.total_ticks
p_station.last_delivery_tick = map_data.total_ticks
r_station.deliveries_total = r_station.deliveries_total + 1
p_station.deliveries_total = p_station.deliveries_total + 1
local r_is_each = r_station.network_name == NETWORK_EACH
local p_is_each = p_station.network_name == NETWORK_EACH
for item_i, item in ipairs(manifest) do
assert(item.count > 0, "main.lua error, transfer amount was not positive")
r_station.deliveries[item.name] = (r_station.deliveries[item.name] or 0) + item.count
p_station.deliveries[item.name] = (p_station.deliveries[item.name] or 0) - item.count
if item_i > 1 or r_is_each or p_is_each then
local f, a
if r_is_each then
f, a = pairs(r_station.network_flag--[[@as {[string]: int}]])
if p_is_each then
for network_name, _ in f, a do
local item_network_name = network_name..":"..item.name
economy.all_r_stations[item_network_name] = nil
economy.all_p_stations[item_network_name] = nil
end
f, a = pairs(p_station.network_flag--[[@as {[string]: int}]])
end
elseif p_is_each then
f, a = pairs(p_station.network_flag--[[@as {[string]: int}]])
else
f, a = once, r_station.network_name
end
--prevent deliveries from being processed for these items until their stations are re-polled
--if we don't wait until they are repolled a duplicate delivery might be generated for stations that share inventories
for network_name, _ in f, a do
local item_network_name = network_name..":"..item.name
economy.all_r_stations[item_network_name] = nil
economy.all_p_stations[item_network_name] = nil
end
end
end
set_comb2(map_data, p_station)
set_comb2(map_data, r_station)
p_station.display_state = 1
update_display(map_data, p_station)
r_station.display_state = 1
update_display(map_data, r_station)
interface_raise_train_status_changed(train_id, old_status, STATUS_TO_P)
else
interface_raise_train_dispatch_failed(train_id)
end
end
---@param map_data MapData
---@param r_station_id uint
---@param p_station_id uint
---@param train_id uint
---@param primary_item_name string?
function create_manifest(map_data, r_station_id, p_station_id, train_id, primary_item_name)
--trains and stations expected to be of the same network
local r_station = map_data.stations[r_station_id]
local p_station = map_data.stations[p_station_id]
local train = map_data.trains[train_id]
---@type Manifest
local manifest = {}
for k, v in pairs(r_station.tick_signals) do
---@type string
local item_name = v.signal.name
local item_type = v.signal.type
local r_item_count = v.count
local r_effective_item_count = r_item_count + (r_station.deliveries[item_name] or 0)
if r_effective_item_count < 0 and r_item_count < 0 then
local r_threshold = r_station.item_thresholds and r_station.item_thresholds[item_name] or r_station.r_threshold
if r_station.is_stack and item_type == "item" then
r_threshold = r_threshold*get_stack_size(map_data, item_name)
end
local p_effective_item_count = p_station.item_p_counts[item_name]
--could be an item that is not present at the station
local override_threshold = p_station.item_thresholds and p_station.item_thresholds[item_name]
if override_threshold and p_station.is_stack and item_type == "item" then
override_threshold = override_threshold*get_stack_size(map_data, item_name)
end
if p_effective_item_count and p_effective_item_count >= (override_threshold or r_threshold) then
local item = {name = item_name, type = item_type, count = min(-r_effective_item_count, p_effective_item_count)}
if item_name == primary_item_name then
manifest[#manifest + 1] = manifest[1]
manifest[1] = item
else
manifest[#manifest + 1] = item
end
end
end
end
--locked slots is only taken into account after the train is already approved for dispatch
local locked_slots = p_station.locked_slots
local total_item_slots = train.item_slot_capacity
if locked_slots > 0 and total_item_slots > 0 then
local total_cargo_wagons = #train.entity.cargo_wagons
total_item_slots = max(total_item_slots - total_cargo_wagons*locked_slots, 1)
end
local total_liquid_left = train.fluid_capacity
local i = 1
while i <= #manifest do
local item = manifest[i]
local keep_item = false
if item.type == "fluid" then
if total_liquid_left > 0 then
if item.count > total_liquid_left then
item.count = total_liquid_left
end
total_liquid_left = 0--no liquid merging
keep_item = true
end
elseif total_item_slots > 0 then
local stack_size = get_stack_size(map_data, item.name)
local slots = ceil(item.count/stack_size)
if slots > total_item_slots then
item.count = total_item_slots*stack_size
end
total_item_slots = total_item_slots - slots
keep_item = true
end
if keep_item then
i = i + 1
else--swap remove
manifest[i] = manifest[#manifest]
manifest[#manifest] = nil
end
end
return manifest
end
---@param map_data MapData
---@param mod_settings CybersynModSettings
local function tick_dispatch(map_data, mod_settings)
--we do not dispatch more than one train per tick
--psuedo-randomize what item (and what station) to check first so if trains available is low they choose orders psuedo-randomly
--NOTE: this is an approximation algorithm for solving the assignment problem (bipartite graph weighted matching), the true solution would be to implement the simplex algorithm but I strongly believe most factorio players would prefer run-time efficiency over perfect train routing logic
--NOTE: the above isn't even the full story, we can only use one edge per item per tick, which might break the assumptions of the simplex algorithm causing it to give imperfect solutions.
local all_r_stations = map_data.economy.all_r_stations
local all_p_stations = map_data.economy.all_p_stations
local all_names = map_data.economy.all_names
local stations = map_data.stations
local r_stations
local p_stations
local item_name
local item_type
local item_network_name
while true do
local size = #all_names
if size == 0 then
map_data.tick_state = STATE_INIT
return true
end
--randomizing the ordering should only matter if we run out of available trains
local name_i = size <= 2 and 2 or 2*random(size/2)
item_network_name = all_names[name_i - 1]--[[@as string]]
local signal = all_names[name_i]--[[@as SignalID]]
--swap remove
all_names[name_i - 1] = all_names[size - 1]
all_names[name_i] = all_names[size]
all_names[size] = nil
all_names[size - 1] = nil
r_stations = all_r_stations[item_network_name]
p_stations = all_p_stations[item_network_name]
if r_stations then
if p_stations then
item_name = signal.name--[[@as string]]
item_type = signal.type
break
else
for i, id in ipairs(r_stations) do
local station = stations[id]
if station and band(station.display_state, 2) == 0 then
station.display_state = station.display_state + 2
update_display(map_data, station)
end
end
end
end
end
while true do
local r_station_i = nil
local r_threshold = nil
local best_r_prior = -INF
local best_timestamp = INF
for i, id in ipairs(r_stations) do
local station = stations[id]
--NOTE: the station at r_station_id could have been deleted and reregistered since last poll, this check here prevents it from being processed for a delivery in that case
if not station or station.deliveries_total >= station.trains_limit then
goto continue
end
local threshold = station.r_threshold
local prior = station.priority
local item_threshold = station.item_thresholds and station.item_thresholds[item_name] or nil
if item_threshold then
threshold = item_threshold
if station.item_priority then
prior = station.item_priority--[[@as int]]
end
end
if prior < best_r_prior then
goto continue
end
if prior == best_r_prior and station.last_delivery_tick > best_timestamp then
goto continue
end
r_station_i = i
r_threshold = threshold
best_r_prior = prior
best_timestamp = station.last_delivery_tick
::continue::
end
if not r_station_i then
for _, id in ipairs(r_stations) do
local station = stations[id]
if station and band(station.display_state, 2) == 0 then
station.display_state = station.display_state + 2
update_display(map_data, station)
end
end
return false
end
local r_station_id = r_stations[r_station_i]
local r_station = stations[r_station_id]
---@type string
local network_name
if r_station.network_name == NETWORK_EACH then
_, _, network_name = string.find(item_network_name, "^(.*):")
else
network_name = r_station.network_name
end
local trains = map_data.available_trains[network_name]
local is_fluid = item_type == "fluid"
if not is_fluid and r_station.is_stack then
r_threshold = r_threshold*get_stack_size(map_data, item_name)
end
--no train exists with layout accepted by both provide and request stations
local correctness = 0
local closest_to_correct_p_station = nil
---@type uint?
local p_station_i = nil
local best_train_id = nil
local best_p_prior = -INF
local best_dist = INF
--if no available trains in the network, skip search
---@type uint
local j = 1
while j <= #p_stations do
local p_flag, r_flag, netand, best_p_train_id, best_t_prior, best_capacity, best_t_to_p_dist, effective_count, override_threshold, p_prior, best_p_to_r_dist, effective_threshold, slot_threshold
local p_station_id = p_stations[j]
local p_station = stations[p_station_id]
if not p_station or p_station.deliveries_total >= p_station.trains_limit then
goto p_continue
end
p_flag = get_network_flag(p_station, network_name)
r_flag = get_network_flag(r_station, network_name)
netand = band(p_flag, r_flag)
if netand == 0 then
goto p_continue
end
effective_count = p_station.item_p_counts[item_name]
override_threshold = p_station.item_thresholds and p_station.item_thresholds[item_name]
if override_threshold and p_station.is_stack and not is_fluid then
override_threshold = override_threshold*get_stack_size(map_data, item_name)
end
if override_threshold and override_threshold <= r_threshold then
effective_threshold = override_threshold
else
effective_threshold = r_threshold
end
if effective_count < effective_threshold then
--this p station should have serviced the current r station, lock it so it can't serve any others
--this will lock stations even when the r station manages to find a p station, this not a problem because all stations will be unlocked before it could be an issue
table_remove(p_stations, j)
if band(p_station.display_state, 4) == 0 then
p_station.display_state = p_station.display_state + 4
update_display(map_data, p_station)
end
goto p_continue_remove
end
p_prior = p_station.priority
if override_threshold and p_station.item_priority then
p_prior = p_station.item_priority--[[@as int]]
end
if p_prior < best_p_prior then
goto p_continue
end
best_p_to_r_dist = p_station.entity_stop.valid and r_station.entity_stop.valid and get_dist(p_station.entity_stop, r_station.entity_stop) or INF
if p_prior == best_p_prior and best_p_to_r_dist > best_dist then
goto p_continue
end
if is_fluid then
slot_threshold = effective_threshold
else
slot_threshold = ceil(effective_threshold/get_stack_size(map_data, item_name))
end
if correctness < 1 then
correctness = 1
closest_to_correct_p_station = p_station
end
----------------------------------------------------------------
-- check for valid train
----------------------------------------------------------------
---@type uint?
best_p_train_id = nil
best_t_prior = -INF
best_capacity = 0
best_t_to_p_dist = INF
if trains then
for train_id, _ in pairs(trains) do
local train = map_data.trains[train_id]
local train_flag = get_network_flag(train, network_name)
if not btest(netand, train_flag) or train.se_is_being_teleported then
goto train_continue
end
if correctness < 2 then
correctness = 2
closest_to_correct_p_station = p_station
end
--check cargo capabilities
local capacity = (is_fluid and train.fluid_capacity) or train.item_slot_capacity
if capacity < slot_threshold then
--no train with high enough capacity is available
goto train_continue
end
if correctness < 3 then
correctness = 3
closest_to_correct_p_station = p_station
end
--check layout validity for both stations
local layout_id = train.layout_id
if not (r_station.allows_all_trains or r_station.accepted_layouts[layout_id]) then
goto train_continue
end
if correctness < 4 then
correctness = 4
closest_to_correct_p_station = p_station
end
if not (p_station.allows_all_trains or p_station.accepted_layouts[layout_id]) then
goto train_continue
end
if correctness < 5 then
correctness = 5
closest_to_correct_p_station = p_station
end
if train.priority < best_t_prior then
goto train_continue
end
if train.priority == best_t_prior and capacity < best_capacity then
goto train_continue
end
--check if path is shortest so we prioritize locality
local t = get_any_train_entity(train.entity)
local t_to_p_dist = t and p_station.entity_stop.valid and (get_dist(t, p_station.entity_stop) - DEPOT_PRIORITY_MULT*train.priority) or INF
if capacity == best_capacity and t_to_p_dist > best_t_to_p_dist then
goto train_continue
end
best_p_train_id = train_id
best_capacity = capacity
best_t_prior = train.priority
best_t_to_p_dist = t_to_p_dist
::train_continue::
end
end
if not best_p_train_id then
goto p_continue
end
p_station_i = j
best_train_id = best_p_train_id
best_p_prior = p_prior
best_dist = best_p_to_r_dist
::p_continue::
j = j + 1
::p_continue_remove::
end
if best_train_id then
local p_station_id = table_remove(p_stations, p_station_i)
local manifest = create_manifest(map_data, r_station_id, p_station_id, best_train_id, item_name)
create_delivery(map_data, r_station_id, p_station_id, best_train_id, manifest)
return false
else
if correctness == 1 then
send_alert_missing_train(r_station.entity_stop, closest_to_correct_p_station.entity_stop)
elseif correctness == 2 then
send_alert_no_train_has_capacity(r_station.entity_stop, closest_to_correct_p_station.entity_stop)
elseif correctness == 3 then
send_alert_no_train_matches_r_layout(r_station.entity_stop, closest_to_correct_p_station.entity_stop)
elseif correctness == 4 then
send_alert_no_train_matches_p_layout(r_station.entity_stop, closest_to_correct_p_station.entity_stop)
end
if band(r_station.display_state, 2) == 0 then
r_station.display_state = r_station.display_state + 2
update_display(map_data, r_station)
end
end
table_remove(r_stations, r_station_i)
end
end
---@param map_data MapData
---@param mod_settings CybersynModSettings
local function tick_poll_station(map_data, mod_settings)
local tick_data = map_data.tick_data
local all_r_stations = map_data.economy.all_r_stations
local all_p_stations = map_data.economy.all_p_stations
local all_names = map_data.economy.all_names
local station_id
local station
while true do--choose a station
tick_data.i = (tick_data.i or 0) + 1
if tick_data.i > #map_data.active_station_ids then
tick_data.i = nil
map_data.tick_state = STATE_DISPATCH
return true
end
station_id = map_data.active_station_ids[tick_data.i]
station = map_data.stations[station_id]
if station and not station.is_warming_up then
if station.network_name then
break
end
else
--lazy delete removed stations
table_remove(map_data.active_station_ids, tick_data.i)
tick_data.i = tick_data.i - 1
end
end
if station.entity_stop.valid and station.entity_comb1.valid and (not station.entity_comb2 or station.entity_comb2.valid) then
station.trains_limit = station.entity_stop.trains_limit
else
on_station_broken(map_data, station_id, station)
return false
end
station.r_threshold = mod_settings.r_threshold
station.priority = mod_settings.priority
station.item_priority = nil
station.locked_slots = mod_settings.locked_slots
local is_each = station.network_name == NETWORK_EACH
if is_each then
station.network_flag = {}
else
station.network_flag = mod_settings.network_flag
end
local comb1_signals, comb2_signals = get_signals(station)
station.tick_signals = comb1_signals
station.item_p_counts = {}
local is_requesting_nothing = true
if comb1_signals then
if comb2_signals then
station.item_thresholds = {}
for k, v in pairs(comb2_signals) do
local item_name = v.signal.name
local item_count = v.count
local item_type = v.signal.type
if item_name then
if item_type == "virtual" then
if item_name == SIGNAL_PRIORITY then
station.item_priority = item_count
end
else
station.item_thresholds[item_name] = abs(item_count)
end
end
end
else
station.item_thresholds = nil
end
for k, v in pairs(comb1_signals) do
local item_name = v.signal.name
local item_count = v.count
local item_type = v.signal.type
if item_name then
if item_type == "virtual" then
if item_name == SIGNAL_PRIORITY then
station.priority = item_count
elseif item_name == REQUEST_THRESHOLD then
--NOTE: thresholds must be >0 or they can cause a crash
station.r_threshold = abs(item_count)
elseif item_name == LOCKED_SLOTS then
station.locked_slots = max(item_count, 0)
elseif is_each then
station.network_flag[item_name] = item_count
end
comb1_signals[k] = nil
end
if item_name == station.network_name then
station.network_flag = item_count
comb1_signals[k] = nil
end
else
comb1_signals[k] = nil
end
end
for k, v in pairs(comb1_signals) do
---@type string
local item_name = v.signal.name
local item_type = v.signal.type
local item_count = v.count
local effective_item_count = item_count + (station.deliveries[item_name] or 0)
local is_not_requesting = true
if station.is_r then
local r_threshold = station.item_thresholds and station.item_thresholds[item_name] or station.r_threshold
if station.is_stack and item_type == "item" then
r_threshold = r_threshold*get_stack_size(map_data, item_name)
end
if -effective_item_count >= r_threshold and -item_count >= r_threshold then
is_not_requesting = false
is_requesting_nothing = false
local f, a
if station.network_name == NETWORK_EACH then
f, a = pairs(station.network_flag--[[@as {[string]: int}]])
else
f, a = once, station.network_name
end
for network_name, _ in f, a do
local item_network_name = network_name..":"..item_name
local stations = all_r_stations[item_network_name]
if stations == nil then
stations = {}
all_r_stations[item_network_name] = stations
all_names[#all_names + 1] = item_network_name
all_names[#all_names + 1] = v.signal
end
stations[#stations + 1] = station_id
end
end
end
if is_not_requesting then
if station.is_p and effective_item_count > 0 and item_count > 0 then
local f, a
if station.network_name == NETWORK_EACH then
f, a = pairs(station.network_flag--[[@as {[string]: int}]])
else
f, a = once, station.network_name
end
for network_name, _ in f, a do
local item_network_name = network_name..":"..item_name
local stations = all_p_stations[item_network_name]
if stations == nil then
stations = {}
all_p_stations[item_network_name] = stations
end
stations[#stations + 1] = station_id
station.item_p_counts[item_name] = effective_item_count
end
else
comb1_signals[k] = nil
end
end
end
end
if station.display_state > 1 then
if is_requesting_nothing and band(station.display_state, 2) > 0 then
station.display_state = station.display_state - 2
update_display(map_data, station)
end
if band(station.display_state, 8) > 0 then
if band(station.display_state, 4) > 0 then
station.display_state = station.display_state - 4
else
station.display_state = station.display_state - 8
update_display(map_data, station)
end
elseif band(station.display_state, 4) > 0 then
station.display_state = station.display_state + 4
end
end
return false
end
---@param map_data MapData
---@param mod_settings CybersynModSettings
function tick_poll_entities(map_data, mod_settings)
local tick_data = map_data.tick_data
if map_data.total_ticks%5 == 0 then
if tick_data.last_train == nil or map_data.trains[tick_data.last_train] then
local train_id, train = next(map_data.trains, tick_data.last_train)
tick_data.last_train = train_id
if train then
if train.manifest and not train.se_is_being_teleported and train.last_manifest_tick + mod_settings.stuck_train_time*mod_settings.tps < map_data.total_ticks then
if mod_settings.stuck_train_alert_enabled then
send_alert_stuck_train(map_data, train.entity)
end
interface_raise_train_stuck(train_id)
end
end
else
tick_data.last_train = nil
end
if tick_data.last_refueler == nil or map_data.each_refuelers[tick_data.last_refueler] then
local refueler_id, _ = next(map_data.each_refuelers, tick_data.last_refueler)
tick_data.last_refueler = refueler_id
if refueler_id then
local refueler = map_data.refuelers[refueler_id]
if refueler.entity_stop.valid and refueler.entity_comb.valid then
set_refueler_from_comb(map_data, mod_settings, refueler_id, refueler)
else
on_refueler_broken(map_data, refueler_id, refueler)
end
end
else
tick_data.last_refueler = nil
end
else
if tick_data.last_comb == nil or map_data.to_comb[tick_data.last_comb] then
local comb_id, comb = next(map_data.to_comb, tick_data.last_comb)
tick_data.last_comb = comb_id
if comb then
if comb.valid then
combinator_update(map_data, comb, true)
else
map_data.to_comb[comb_id] = nil
end
end
else
tick_data.last_comb = nil
end
end
end
---@param map_data MapData
---@param mod_settings CybersynModSettings
function tick_init(map_data, mod_settings)
map_data.economy.all_p_stations = {}
map_data.economy.all_r_stations = {}
map_data.economy.all_names = {}
while #map_data.warmup_station_ids > 0 do
local id = map_data.warmup_station_ids[1]
local station = map_data.stations[id]
if station then
local cycles = map_data.warmup_station_cycles[id]
--force a station to wait at least 1 cycle so we can be sure active_station_ids was flushed of duplicates
if cycles > 0 then
if station.last_delivery_tick + mod_settings.warmup_time*mod_settings.tps < map_data.total_ticks then
station.is_warming_up = nil
map_data.active_station_ids[#map_data.active_station_ids + 1] = id
table_remove(map_data.warmup_station_ids, 1)
map_data.warmup_station_cycles[id] = nil
if station.entity_comb1.valid then
combinator_update(map_data, station.entity_comb1)
else
on_station_broken(map_data, id, station)
end
else
break
end
else
map_data.warmup_station_cycles[id] = cycles + 1
break
end
else
table_remove(map_data.warmup_station_ids, 1)
map_data.warmup_station_cycles[id] = nil
end
end
if map_data.queue_station_update then
for id, _ in pairs(map_data.queue_station_update) do
local station = map_data.stations[id]
if station then
local pre = station.allows_all_trains
if station.entity_comb1.valid then
set_station_from_comb(station)
if station.allows_all_trains ~= pre then
update_stop_if_auto(map_data, station, true)
end
else
on_station_broken(map_data, id, station)
end
end
end
map_data.queue_station_update = nil
end
map_data.tick_state = STATE_POLL_STATIONS
interface_raise_tick_init()
end
---@param map_data MapData
---@param mod_settings CybersynModSettings
function tick(map_data, mod_settings)
map_data.total_ticks = map_data.total_ticks + 1
if map_data.active_alerts then
if map_data.total_ticks%(8*mod_settings.tps) < 1 then
process_active_alerts(map_data)
end
end
tick_poll_entities(map_data, mod_settings)
if mod_settings.enable_planner then
if map_data.tick_state == STATE_INIT then
tick_init(map_data, mod_settings)
end
if map_data.tick_state == STATE_POLL_STATIONS then
for i = 1, mod_settings.update_rate do
if tick_poll_station(map_data, mod_settings) then break end
end
elseif map_data.tick_state == STATE_DISPATCH then
for i = 1, mod_settings.update_rate do
if tick_dispatch(map_data, mod_settings) then break end
end
end
else
map_data.tick_state = STATE_INIT
end
end