--By Mami local get_distance = require("__flib__.misc").get_distance local math = math local INF = math.huge local create_loading_order_condition = {type = "inactivity", compare_type = "and", ticks = 120} function create_loading_order(stop, manifest) local condition = {} for _, item in ipairs(manifest) do local cond_type if item.type == "fluid" then cond_type = "fluid_count" else cond_type = "item_count" end condition[#condition + 1] = { type = cond_type, compare_type = "and", condition = {comparator = "≥", first_signal = {type = item.type, name = item.name}, constant = item.count} } end condition[#condition + 1] = create_loading_order_condition return {station = stop.backer_name, wait_conditions = condition} end local create_unloading_order_condition = {{type = "empty", compare_type = "and"}} function create_unloading_order(stop) return {station = stop.backer_name, wait_conditions = create_unloading_order_condition} end local create_inactivity_order_condition = {{type = "inactivity", compare_type = "and", ticks = 120}} function create_inactivity_order(depot_name) return {station = depot_name, wait_conditions = create_inactivity_order_condition} end local create_direct_to_station_order_condition = {{type = "time", compare_type = "and", ticks = 0}} local function create_direct_to_station_order(stop) return {rail = stop.connected_rail, rail_direction = stop.connected_rail_direction} end function create_depot_schedule(depot_name) return {current = 1, records = {create_inactivity_order(depot_name)}} end function create_manifest_schedule(depot_name, p_stop, r_stop, manifest) return {current = 1, records = { create_inactivity_order(depot_name), create_direct_to_station_order(p_stop), create_loading_order(p_stop, manifest), create_direct_to_station_order(r_stop), create_unloading_order(r_stop), }} end local function get_signals(station) if station.comb1.valid then local signals = station.comb1.get_merged_signals(defines.circuit_connector_id.combinator_input) return signals else return nil end end function set_combinator_output(map_data, comb, signals) if comb.valid then local out = map_data.to_output[comb.unit_number] if out.valid then out.get_or_create_control_behavior().parameters = signals else --TODO: error logging? end else --TODO: error logging? end end local function set_comb2(map_data, station) if station.comb2 then local deliveries = station.deliveries local signals = {} for item_name, count in pairs(deliveries) do local i = #signals + 1 local item_type = game.item_prototypes[item_name].type signals[i] = {index = i, signal = {type = item_type, name = item_name}, count = count} end set_combinator_output(map_data, station.comb2, signals) end end 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 end local function get_thresholds(map_data, station, signal) local comb2 = station.comb2 if comb2 and comb2.valid then local count = comb2.get_merged_signal(signal, defines.circuit_connector_id.combinator_input) if count > 0 then return station.r_threshold, count elseif count < 0 then return -count, station.p_threshold end end return station.r_threshold, station.p_threshold end local function get_stop_dist(stop0, stop1) return get_distance(stop0.position, stop1.position) end local function station_accepts_layout(station, layout_id) return true end local function get_valid_train(map_data, r_station_id, p_station_id, item_type) --NOTE: this code is the critical section for run-time optimization local r_station = map_data.stations[r_station_id] local p_station = map_data.stations[p_station_id] local p_to_r_dist = get_stop_dist(p_station.entity, r_station.entity) if p_to_r_dist == INF then return nil, INF end local best_train = nil local best_dist = INF local valid_train_exists = false local is_fluid = item_type == "fluid" for train_id, _ in pairs(map_data.trains_available) do local train = map_data.trains[train_id] --check cargo capabilities --check layout validity for both stations if ((is_fluid and train.fluid_capacity > 0) or (not is_fluid and train.item_slot_capacity > 0)) and station_accepts_layout(r_station, train.layout_id) and station_accepts_layout(p_station, train.layout_id) and train.entity.station then valid_train_exists = true --check if exists valid path --check if path is shortest so we prioritize locality local d_to_p_dist = get_stop_dist(train.entity.station, p_station.entity) local dist = d_to_p_dist if dist < best_dist then best_dist = dist best_train = train end end end if valid_train_exists then return best_train, best_dist + p_to_r_dist else return nil, p_to_r_dist end end local function send_train_between(map_data, r_station_id, p_station_id, train, primary_item_name, economy) local r_station = map_data.stations[r_station_id] local p_station = map_data.stations[p_station_id] local requests = {} local manifest = {} local r_signals = get_signals(r_station) if r_signals then for k, v in pairs(r_signals) do local item_name = v.signal.name local item_count = v.count local item_type = v.signal.type if item_name and item_type and item_type ~= "virtual" then local effective_item_count = item_count + (r_station.deliveries[item_name] or 0) local r_threshold, p_threshold = get_thresholds(map_data, r_station, v) if -effective_item_count >= r_threshold then requests[item_name] = -effective_item_count end end end end local p_signals = get_signals(p_station) if p_signals then for k, v in pairs(p_signals) do local item_name = v.signal.name local item_count = v.count local item_type = v.signal.type if item_name and item_type and item_type ~= "virtual" then local effective_item_count = item_count + (p_station.deliveries[item_name] or 0) local r_threshold, p_threshold = get_thresholds(map_data, r_station, v) if effective_item_count >= p_threshold then local r = requests[item_name] if r then local item = {name = item_name, count = math.min(r, effective_item_count), type = item_type} if item_name == primary_item_name then manifest[#manifest + 1] = manifest[1] manifest[1] = item else manifest[#manifest + 1] = item end end end end end end local locked_slots = math.max(p_station.locked_slots, r_station.locked_slots) local total_slots_left = train.item_slot_capacity if locked_slots > 0 then total_slots_left = math.max(total_slots_left - #train.entity.cargo_wagons*locked_slots, math.min(total_slots_left, #train.entity.cargo_wagons)) 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_slots_left > 0 then local stack_size = game.item_prototypes[item.name].stack_size local slots = math.ceil(item.count/stack_size) if slots > total_slots_left then item.count = total_slots_left*stack_size end total_slots_left = total_slots_left - slots keep_item = true end if keep_item then i = i + 1 else--swap remove manifest[i] = manifest[#manifest] manifest[#manifest] = nil end end r_station.last_delivery_tick = economy.total_ticks p_station.last_delivery_tick = economy.total_ticks r_station.deliveries_total = r_station.deliveries_total + 1 p_station.deliveries_total = p_station.deliveries_total + 1 for _, 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 local r_stations = economy.r_stations_all[item.name] local p_stations = economy.p_stations_all[item.name] for i, id in ipairs(r_stations) do if id == r_station_id then table.remove(r_stations, i) break end end for i, id in ipairs(p_stations) do if id == p_station_id then table.remove(p_stations, i) break end end end map_data.trains_available[train.entity.id] = nil train.status = STATUS_D_TO_P train.p_station_id = p_station_id train.r_station_id = r_station_id train.manifest = manifest train.entity.schedule = create_manifest_schedule(train.depot_name, p_station.entity, r_station.entity, manifest) set_comb2(map_data, p_station) set_comb2(map_data, r_station) end function tick(map_data, mod_settings) local total_ticks = map_data.total_ticks local stations = map_data.stations local economy = { r_stations_all = {}, p_stations_all = {}, all_items = {}, total_ticks = total_ticks, } local r_stations_all = economy.r_stations_all local p_stations_all = economy.p_stations_all local all_items = economy.all_items for station_id, station in pairs(stations) do if station.deliveries_total < station.entity.trains_limit then station.r_threshold = mod_settings.r_threshold station.p_threshold = mod_settings.p_threshold station.priority = 0 station.locked_slots = 0 local signals = get_signals(station) if signals then for k, v in pairs(signals) do local item_name = v.signal.name local item_count = v.count local item_type = v.signal.type if item_name and item_type then if item_type == "virtual" then if item_name == SIGNAL_PRIORITY then station.priority = item_count elseif item_name == REQUEST_THRESHOLD then station.r_threshold = math.abs(item_count) elseif item_name == PROVIDE_THRESHOLD then station.p_threshold = math.abs(item_count) elseif item_name == LOCKED_SLOTS then station.locked_slots = math.max(item_count, 0) end signals[k] = nil end else signals[k] = nil end end for k, v in pairs(signals) do local item_name = v.signal.name local item_count = v.count local effective_item_count = item_count + (station.deliveries[item_name] or 0) local r_threshold, p_threshold = get_thresholds(map_data, station, v) if -effective_item_count >= r_threshold then if r_stations_all[item_name] == nil then r_stations_all[item_name] = {} p_stations_all[item_name] = {} all_items[#all_items + 1] = item_name all_items[#all_items + 1] = v.signal.type end table.insert(r_stations_all[item_name], station_id) elseif effective_item_count >= p_threshold then if r_stations_all[item_name] == nil then r_stations_all[item_name] = {} p_stations_all[item_name] = {} all_items[#all_items + 1] = item_name all_items[#all_items + 1] = v.signal.type end table.insert(p_stations_all[item_name], station_id) end end end end end local failed_because_missing_trains_total = 0 --we do not dispatch more than one train per station per tick --psuedo-randomize what item (and what station) to check first so if trains available is low they choose orders psuedo-randomly local start_i = 2*(total_ticks%(#all_items/2)) + 1 for item_i = 0, #all_items - 1, 2 do local item_name = all_items[(start_i + item_i - 1)%#all_items + 1] local item_type = all_items[(start_i + item_i)%#all_items + 1] local r_stations = r_stations_all[item_name] local p_stations = p_stations_all[item_name] --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 (and run it twice to compare the locality solution to the round-robin solution) but I strongly believe most factorio players would prefer run-time efficiency over perfect train routing logic if #r_stations > 0 and #p_stations > 0 then if #r_stations <= #p_stations then --probably backpressure, prioritize locality repeat local i = total_ticks%#r_stations + 1 local r_station_id = table.remove(r_stations, i) local best = 0 local best_train = nil local best_dist = INF local highest_prior = -INF local could_have_been_serviced = false for j, p_station_id in ipairs(p_stations) do local train, d = get_valid_train(map_data, r_station_id, p_station_id, item_type) local prior = stations[p_station_id].priority if prior > highest_prior or (prior == highest_prior and d < best_dist) then if train then best = j best_dist = d best_train = train highest_prior = prior elseif d < INF then could_have_been_serviced = true end end end if best > 0 then send_train_between(map_data, r_station_id, p_stations[best], best_train, item_name, economy) elseif could_have_been_serviced then failed_because_missing_trains_total = failed_because_missing_trains_total + 1 end until #r_stations == 0 else --prioritize round robin repeat local j = total_ticks%#p_stations + 1 local p_station_id = table.remove(p_stations, j) local best = 0 local best_train = nil local lowest_tick = INF local highest_prior = -INF local could_have_been_serviced = false for i, r_station_id in ipairs(r_stations) do local r_station = stations[r_station_id] local prior = r_station.priority if prior > highest_prior or (prior == highest_prior and r_station.last_delivery_tick < lowest_tick) then local train, d = get_valid_train(map_data, r_station_id, p_station_id, item_type) if train then best = i best_train = train lowest_tick = r_station.last_delivery_tick highest_prior = prior elseif d < INF then could_have_been_serviced = true end end end if best > 0 then send_train_between(map_data, r_stations[best], p_station_id, best_train, item_name, economy) elseif could_have_been_serviced then failed_because_missing_trains_total = failed_because_missing_trains_total + 1 end until #p_stations == 0 end end end --TODO: add alert for missing trains end