--[[ Copyright (c) 2017 Optera * Part of Logistics Train Network * * See LICENSE.md in the project directory for license information. --]] -- update global.Dispatcher.Deliveries.force when forces are removed/merged script.on_event(defines.events.on_forces_merging, function(event) for _, delivery in pairs(global.Dispatcher.Deliveries) do if delivery.force == event.source then delivery.force = event.destination end end end) ---------------------------------- MAIN LOOP ---------------------------------- function OnTick(event) local tick = event.tick -- log("DEBUG: (OnTick) "..tick.." global.tick_state: "..tostring(global.tick_state).." global.tick_stop_index: "..tostring(global.tick_stop_index).." global.tick_request_index: "..tostring(global.tick_request_index) ) if global.tick_state == 1 then -- update stops for i = 1, dispatcher_updates_per_tick, 1 do -- reset on invalid index if global.tick_stop_index and not global.LogisticTrainStops[global.tick_stop_index] then global.tick_state = 0 if message_level >= 2 then printmsg({"ltn-message.error-invalid-stop-index", global.tick_stop_index}, nil, false) end log("(OnTick) Invalid global.tick_stop_index "..tostring(global.tick_stop_index).." in global.LogisticTrainStops. Removing stop and starting over.") RemoveStop(global.tick_stop_index) return end local stopID, stop = next(global.LogisticTrainStops, global.tick_stop_index) if stopID then global.tick_stop_index = stopID if debug_log then log("(OnTick) "..tick.." updating stopID "..tostring(stopID)) end UpdateStop(stopID, stop) else -- stop updates complete, moving on global.tick_stop_index = nil global.tick_state = 2 return end end elseif global.tick_state == 2 then -- clean up and sort lists global.tick_state = 3 -- remove messages older than message_filter_age from messageBuffer for bufferedMsg, v in pairs(global.messageBuffer) do if (tick - v.tick) > message_filter_age then global.messageBuffer[bufferedMsg] = nil end end --clean up deliveries in case train was destroyed or removed local activeDeliveryTrains = "" for trainID, delivery in pairs (global.Dispatcher.Deliveries) do if not(delivery.train and delivery.train.valid) then local from_entity = global.LogisticTrainStops[delivery.from_id] and global.LogisticTrainStops[delivery.from_id].entity local to_entity = global.LogisticTrainStops[delivery.to_id] and global.LogisticTrainStops[delivery.to_id].entity if message_level >= 1 then printmsg({ "ltn-message.delivery-removed-train-invalid", Make_Stop_RichText(from_entity) or delivery.from, Make_Stop_RichText(to_entity) or delivery.to }, delivery.force, false) end if debug_log then log("(OnTick) Delivery from "..delivery.from.." to "..delivery.to.." removed. Train no longer valid.") end script.raise_event(on_delivery_failed_event, {train_id = trainID, shipment = delivery.shipment}) RemoveDelivery(trainID) elseif tick-delivery.started > delivery_timeout then local from_entity = global.LogisticTrainStops[delivery.from_id] and global.LogisticTrainStops[delivery.from_id].entity local to_entity = global.LogisticTrainStops[delivery.to_id] and global.LogisticTrainStops[delivery.to_id].entity if message_level >= 1 then printmsg({ "ltn-message.delivery-removed-timeout", Make_Stop_RichText(from_entity) or delivery.from, Make_Stop_RichText(to_entity) or delivery.to, tick-delivery.started }, delivery.force, false) end if debug_log then log("(OnTick) Delivery from "..delivery.from.." to "..delivery.to.." removed. Timed out after "..tick-delivery.started.."/"..delivery_timeout.." ticks.") end script.raise_event(on_delivery_failed_event, {train_id = trainID, shipment = delivery.shipment}) RemoveDelivery(trainID) else activeDeliveryTrains = activeDeliveryTrains.." "..trainID end end if debug_log then log("(OnTick) Trains on deliveries"..activeDeliveryTrains) end -- remove no longer active requests from global.Dispatcher.RequestAge[stopID] local newRequestAge = {} for _,request in pairs (global.Dispatcher.Requests) do local ageIndex = request.item..","..request.stopID local age = global.Dispatcher.RequestAge[ageIndex] if age then newRequestAge[ageIndex] = age end end global.Dispatcher.RequestAge = newRequestAge -- sort requests by priority and age sort(global.Dispatcher.Requests, function(a, b) if a.priority ~= b.priority then return a.priority > b.priority else return a.age < b.age end end) elseif global.tick_state == 3 then -- parse requests and dispatch trains if dispatcher_enabled then if debug_log then log("(OnTick) Available train capacity: "..global.Dispatcher.availableTrains_total_capacity.." item stacks, "..global.Dispatcher.availableTrains_total_fluid_capacity.. " fluid capacity.") end for i = 1, dispatcher_updates_per_tick, 1 do -- reset on invalid index if global.tick_request_index and not global.Dispatcher.Requests[global.tick_request_index] then global.tick_state = 0 if message_level >= 1 then printmsg({"ltn-message.error-invalid-request-index", global.tick_request_index}, nil, false) end log("(OnTick) Invalid global.tick_request_index "..tostring(global.tick_request_index).." in global.Dispatcher.Requests. Starting over.") return end local request_index, request = next(global.Dispatcher.Requests, global.tick_request_index) if request_index and request then global.tick_request_index = request_index if debug_log then log("(OnTick) "..tick.." parsing request "..tostring(request_index).."/"..tostring(#global.Dispatcher.Requests) ) end ProcessRequest(request_index, request) else -- request updates complete, moving on global.tick_request_index = nil global.tick_state = 4 return end end else if message_level >= 1 then printmsg({"ltn-message.warning-dispatcher-disabled"}, nil, true) end if debug_log then log("(OnTick) Dispatcher disabled.") end global.tick_request_index = nil global.tick_state = 4 return end elseif global.tick_state == 4 then -- raise API events global.tick_state = 0 -- raise events for mod API script.raise_event(on_stops_updated_event, { logistic_train_stops = global.LogisticTrainStops, }) script.raise_event(on_dispatcher_updated_event, { update_interval = tick - global.tick_interval_start, provided_by_stop = global.Dispatcher.Provided_by_Stop, requests_by_stop = global.Dispatcher.Requests_by_Stop, new_deliveries = global.Dispatcher.new_Deliveries, deliveries = global.Dispatcher.Deliveries, available_trains = global.Dispatcher.availableTrains, }) else -- reset global.tick_stop_index = nil global.tick_request_index = nil global.tick_state = 1 global.tick_interval_start = tick -- clear Dispatcher.Storage global.Dispatcher.Provided = {} global.Dispatcher.Requests = {} global.Dispatcher.Provided_by_Stop = {} global.Dispatcher.Requests_by_Stop = {} global.Dispatcher.new_Deliveries = {} end end ---------------------------------- DISPATCHER FUNCTIONS ---------------------------------- -- ensures removal of trainID from global.Dispatcher.Deliveries and stop.active_deliveries function RemoveDelivery(trainID) for stopID, stop in pairs(global.LogisticTrainStops) do if not stop.entity.valid or not stop.input.valid or not stop.output.valid or not stop.lamp_control.valid then RemoveStop(stopID) else for i=#stop.active_deliveries, 1, -1 do --trainID should be unique => checking matching stop name not required if stop.active_deliveries[i] == trainID then table.remove(stop.active_deliveries, i) if #stop.active_deliveries > 0 then setLamp(stop, "yellow", #stop.active_deliveries) else setLamp(stop, "green", 1) end end end end end global.Dispatcher.Deliveries[trainID] = nil end -- NewScheduleRecord: returns new schedule_record local condition_circuit_red = {type = "circuit", compare_type = "and", condition = {comparator = "=", first_signal = {type = "virtual", name = "signal-red"}, constant = 0} } local condition_circuit_green = {type = "circuit", compare_type = "or", condition = {comparator = "≥", first_signal = {type = "virtual", name = "signal-green"}, constant = 1} } local condition_wait_empty = {type = "empty", compare_type = "and" } local condition_finish_loading = {type = "inactivity", compare_type = "and", ticks = 120 } -- local condition_stop_timeout -- set in settings.lua to capture changes function NewScheduleRecord(stationName, condType, condComp, itemlist, countOverride) local record = {station = stationName, wait_conditions = {} } if condType == "time" then record.wait_conditions[#record.wait_conditions+1] = {type = condType, compare_type = "and", ticks = condComp } elseif condType == "item_count" then local waitEmpty = false -- write itemlist to conditions for i=1, #itemlist do local condFluid = nil if itemlist[i].type == "fluid" then condFluid = "fluid_count" -- workaround for leaving with fluid residue due to Factorio rounding down to 0 if condComp == "=" and countOverride == 0 then waitEmpty = true end end -- make > into >= if condComp == ">" then countOverride = itemlist[i].count - 1 end -- itemlist = {first_signal.type, first_signal.name, constant} local cond = {comparator = condComp, first_signal = {type = itemlist[i].type, name = itemlist[i].name}, constant = countOverride or itemlist[i].count} record.wait_conditions[#record.wait_conditions+1] = {type = condFluid or condType, compare_type = "and", condition = cond } end if waitEmpty then record.wait_conditions[#record.wait_conditions+1] = condition_wait_empty elseif finish_loading then -- let inserter/pumps finish record.wait_conditions[#record.wait_conditions+1] = condition_finish_loading end -- with circuit control enabled keep trains waiting until red = 0 and force them out with green ≥ 1 if schedule_cc then record.wait_conditions[#record.wait_conditions+1] = condition_circuit_red record.wait_conditions[#record.wait_conditions+1] = condition_circuit_green end if stop_timeout > 0 then -- send stuck trains away when stop_timeout is set record.wait_conditions[#record.wait_conditions+1] = condition_stop_timeout -- should it also wait for red = 0? if schedule_cc then record.wait_conditions[#record.wait_conditions+1] = condition_circuit_red end end elseif condType == "inactivity" then record.wait_conditions[#record.wait_conditions+1] = {type = condType, compare_type = "and", ticks = condComp } -- with circuit control enabled keep trains waiting until red = 0 and force them out with green ≥ 1 if schedule_cc then record.wait_conditions[#record.wait_conditions+1] = condition_circuit_red record.wait_conditions[#record.wait_conditions+1] = condition_circuit_green end end return record end local temp_wait_condition = {{type = "time", compare_type = "and", ticks = 0}} -- NewScheduleRecord: returns new schedule_record for waypoints function NewTempScheduleRecord(rail, rail_direction) local record = {wait_conditions = temp_wait_condition, rail = rail, rail_direction = rail_direction, temporary = true} return record end ---- ProcessRequest ---- -- returns the string "number1|number2" in consistent order: the smaller number is always placed first local function sorted_pair(number1, number2) return (number1 < number2) and (number1..'|'..number2) or (number2..'|'..number1) end -- return a list of matching { entity1, entity2, network_id } each connecting the two surfaces. The list will be empty if surface1 == surface2 and it will be nil if there are no matching connections. The second return value will be the number of entries in the list. local function find_surface_connections(surface1, surface2, force, network_id) if surface1 == surface2 then return {}, 0 end local surface_pair_key = sorted_pair(surface1.index, surface2.index) local surface_connections = global.ConnectedSurfaces[surface_pair_key] if not surface_connections then return nil end local matching_connections = {} local count=0 for entity_pair_key, connection in pairs(surface_connections) do if connection.entity1.valid and connection.entity2.valid then if btest(network_id, connection.network_id) and connection.entity1.force == force and connection.entity2.force == force then count = count + 1 matching_connections[count] = connection end else if debug_log then log("removing invalid surface connection "..entity_pair_key.." between surfaces "..surface_pair_key) end surface_connections[entity_pair_key] = nil end end if count > 0 then return matching_connections, count else return nil, nil end end -- return a list ordered priority > #active_deliveries > item-count of {entity, network_id, priority, activeDeliveryCount, item, count, providing_threshold, providing_threshold_stacks, min_carriages, max_carriages, locked_slots, surface_connections} local function getProviders(requestStation, item, req_count, min_length, max_length) local stations = {} local providers = global.Dispatcher.Provided[item] if not providers then return nil end local toID = requestStation.entity.unit_number local force = requestStation.entity.force local surface = requestStation.entity.surface for stopID, count in pairs (providers) do local stop = global.LogisticTrainStops[stopID] if stop and stop.entity.valid then local matched_networks = band(requestStation.network_id, stop.network_id) -- log("DEBUG: comparing 0x"..format("%x", band(requestStation.network_id)).." & 0x"..format("%x", band(stop.network_id)).." = 0x"..format("%x", band(matched_networks)) ) if stop.entity.force == force and matched_networks ~= 0 -- and count >= stop.providing_threshold and (stop.min_carriages == 0 or max_length == 0 or stop.min_carriages <= max_length) and (stop.max_carriages == 0 or min_length == 0 or stop.max_carriages >= min_length) then --check if provider can accept more trains local activeDeliveryCount = #stop.active_deliveries if activeDeliveryCount and (stop.max_trains == 0 or activeDeliveryCount < stop.max_trains) then -- check if surface transition is possible local surface_connections, surface_connections_count = find_surface_connections(surface, stop.entity.surface, force, matched_networks) if surface_connections then -- for same surfaces surface_connections = {} local from_network_id_string = format("0x%x", band(stop.network_id)) if debug_log then log("found "..count.."("..tostring(stop.providing_threshold)..")".."/"..req_count.." ".. item.." at "..stop.entity.backer_name.." {"..from_network_id_string.."}, priority: "..stop.provider_priority..", active Deliveries: "..activeDeliveryCount..", min_carriages: "..stop.min_carriages..", max_carriages: "..stop.max_carriages..", locked Slots: "..stop.locked_slots..", #surface_connections: "..(surface_connections_count)) end stations[#stations +1] = { entity = stop.entity, network_id = matched_networks, priority = stop.provider_priority, activeDeliveryCount = activeDeliveryCount, item = item, count = count, providing_threshold = stop.providing_threshold, providing_threshold_stacks = stop.providing_threshold_stacks, min_carriages = stop.min_carriages, max_carriages = stop.max_carriages, locked_slots = stop.locked_slots, surface_connections = surface_connections, surface_connections_count = surface_connections_count, } end end end end end -- sort best matching station to the top sort(stations, function(a, b) if a.priority ~= b.priority then --sort by priority, will result in train queues if trainlimit is not set return a.priority > b.priority elseif a.surface_connections_count ~= b.surface_connections_count then --sort providers without surface transition to top return min(a.surface_connections_count, 1) < min(b.surface_connections_count, 1) elseif a.activeDeliveryCount ~= b.activeDeliveryCount then --sort by #deliveries return a.activeDeliveryCount < b.activeDeliveryCount else return a.count > b.count --finally sort by item count end end) if debug_log then log ("(getProviders) sorted providers: "..serpent.block(stations)) end return stations end local function getStationDistance(stationA, stationB) local stationPair = stationA.unit_number..","..stationB.unit_number if global.StopDistances[stationPair] then --log(stationPair.." found, distance: "..global.StopDistances[stationPair]) return global.StopDistances[stationPair] else local dist = Get_Distance(stationA.position, stationB.position) global.StopDistances[stationPair] = dist --log(stationPair.." calculated, distance: "..dist) return dist end end -- returns: available trains in depots or nil -- filtered by NetworkID, carriages and surface -- sorted by priority, capacity - locked slots and distance to provider local function getFreeTrains(nextStop, min_carriages, max_carriages, type, size) local filtered_trains = {} for trainID, trainData in pairs (global.Dispatcher.availableTrains) do if trainData.train.valid and trainData.train.station and trainData.train.station.valid then local depot_network_id_string -- filled only when debug_log is enabled local dest_network_id_string -- filled only when debug_log is enabled local inventorySize if type == "item" then -- subtract locked slots from every cargo wagon inventorySize = trainData.capacity - (nextStop.locked_slots * #trainData.train.cargo_wagons) else inventorySize = trainData.fluid_capacity end if debug_log then depot_network_id_string = format("0x%x", band(trainData.network_id) ) dest_network_id_string = format("0x%x", band(nextStop.network_id) ) log("(getFreeTrain) checking train "..tostring(Get_Train_Name(trainData.train)).. ", force "..tostring(trainData.force.name).."/"..tostring(nextStop.entity.force.name).. ", network "..depot_network_id_string.."/"..dest_network_id_string.. ", priority: "..trainData.depot_priority.. ", length: "..min_carriages.."<="..#trainData.train.carriages.."<="..max_carriages.. ", inventory size: "..inventorySize.."/"..size.. ", distance: "..getStationDistance(trainData.train.station, nextStop.entity) ) end if inventorySize > 0 -- sending trains without inventory on deliveries would be pointless and trainData.force == nextStop.entity.force -- forces match and trainData.surface == nextStop.entity.surface -- pathing between surfaces is impossible and btest(trainData.network_id, nextStop.network_id) -- depot is in the same network as requester and provider and (min_carriages == 0 or #trainData.train.carriages >= min_carriages) and (max_carriages == 0 or #trainData.train.carriages <= max_carriages) -- train length fits requester and provider limitations then local distance = getStationDistance(trainData.train.station, nextStop.entity) filtered_trains[#filtered_trains+1] = { train = trainData.train, inventory_size = inventorySize, depot_priority = trainData.depot_priority, provider_distance = distance, } end else -- remove invalid train from global.Dispatcher.availableTrains global.Dispatcher.availableTrains_total_capacity = global.Dispatcher.availableTrains_total_capacity - global.Dispatcher.availableTrains[trainID].capacity global.Dispatcher.availableTrains_total_fluid_capacity = global.Dispatcher.availableTrains_total_fluid_capacity - global.Dispatcher.availableTrains[trainID].fluid_capacity global.Dispatcher.availableTrains[trainID] = nil end end -- return nil instead of empty table if next(filtered_trains) == nil then return nil end -- sort best matching train to top sort(filtered_trains, function(a, b) if a.depot_priority ~= b.depot_priority then --sort by priority return a.depot_priority > b.depot_priority elseif a.inventory_size ~= b.inventory_size and a.inventory_size >= size then --sort inventories capable of whole deliveries -- return not(b.inventory_size => size and a.inventory_size > b.inventory_size) return b.inventory_size < size or a.inventory_size < b.inventory_size elseif a.inventory_size ~= b.inventory_size and a.inventory_size < size then --sort inventories for partial deliveries -- return not(b.inventory_size >= size or b.inventory_size > a.inventory_size) return b.inventory_size < size and b.inventory_size < a.inventory_size else -- sort by distance to provider return a.provider_distance < b.provider_distance end end) if debug_log then log ("(getFreeTrain) sorted trains: "..serpent.block(filtered_trains)) end return filtered_trains end -- parse single request from global.Dispatcher.Request={stopID, item, age, count} -- returns created delivery ID or nil function ProcessRequest(reqIndex, request) -- ensure validity of request stop local toID = request.stopID local requestStation = global.LogisticTrainStops[toID] if not requestStation or not (requestStation.entity and requestStation.entity.valid) then return nil end local surface_name = requestStation.entity.surface.name local to = requestStation.entity.backer_name local to_rail = requestStation.entity.connected_rail local to_rail_direction = requestStation.entity.connected_rail_direction local to_gps = Make_Stop_RichText(requestStation.entity) or to local to_network_id_string = format("0x%x", band(requestStation.network_id)) local item = request.item local count = request.count local max_carriages = requestStation.max_carriages local min_carriages = requestStation.min_carriages local requestForce = requestStation.entity.force if debug_log then log("request "..reqIndex.."/"..#global.Dispatcher.Requests..": "..count.."("..requestStation.requesting_threshold..")".." "..item.." to "..requestStation.entity.backer_name.." {"..to_network_id_string.."} priority: "..request.priority.." min length: "..min_carriages.." max length: "..max_carriages ) end if not( global.Dispatcher.Requests_by_Stop[toID] and global.Dispatcher.Requests_by_Stop[toID][item] ) then if debug_log then log("Skipping request "..requestStation.entity.backer_name..": "..item..". Item has already been processed.") end -- goto skipRequestItem -- item has been processed already return nil end if requestStation.max_trains > 0 and #requestStation.active_deliveries >= requestStation.max_trains then if debug_log then log(requestStation.entity.backer_name.." Request station train limit reached: "..#requestStation.active_deliveries.."("..requestStation.max_trains..")" ) end -- goto skipRequestItem -- reached train limit return nil end -- find providers for requested item local itype, iname = match(item, match_string) if not (itype and iname and (game.item_prototypes[iname] or game.fluid_prototypes[iname])) then if message_level >= 1 then printmsg({"ltn-message.error-parse-item", item}, requestForce) end if debug_log then log("(ProcessRequests) could not parse "..item) end -- goto skipRequestItem return nil end local localname if itype == "fluid" then localname = game.fluid_prototypes[iname].localised_name -- skip if no trains are available if (global.Dispatcher.availableTrains_total_fluid_capacity or 0) == 0 then create_alert(requestStation.entity, "depot-empty", {"ltn-message.empty-depot-fluid"}, requestForce) if message_level >= 1 then printmsg({"ltn-message.empty-depot-fluid"}, requestForce, true) end if debug_log then log("Skipping request "..to.." {"..to_network_id_string.."}: "..item..". No trains available.") end script.raise_event(on_dispatcher_no_train_found_event, {to = to, to_id = toID, network_id = requestStation.network_id, item = item}) return nil end else localname = game.item_prototypes[iname].localised_name -- skip if no trains are available if (global.Dispatcher.availableTrains_total_capacity or 0) == 0 then create_alert(requestStation.entity, "depot-empty", {"ltn-message.empty-depot-item"}, requestForce) if message_level >= 1 then printmsg({"ltn-message.empty-depot-item"}, requestForce, true) end if debug_log then log("Skipping request "..to.." {"..to_network_id_string.."}: "..item..". No trains available.") end script.raise_event(on_dispatcher_no_train_found_event, {to = to, to_id = toID, network_id = requestStation.network_id, item = item}) return nil end end -- get providers ordered by priority local providers = getProviders(requestStation, item, count, min_carriages, max_carriages) if not providers or #providers < 1 then if requestStation.no_warnings == false and message_level >= 1 then printmsg({"ltn-message.no-provider-found", to_gps, "[" .. itype .. "=" .. iname .. "]", to_network_id_string}, requestForce, true) end if debug_log then log(format("No supply of %s found for Requester %s: surface: %s min length: %s, max length: %s, network-ID: %s", item, to, surface_name, min_carriages, max_carriages, to_network_id_string) ) end -- goto skipRequestItem return nil end local providerData = providers[1] -- only one delivery/request is created so use only the best provider local fromID = providerData.entity.unit_number local from_rail = providerData.entity.connected_rail local from_rail_direction = providerData.entity.connected_rail_direction local from = providerData.entity.backer_name local from_gps = Make_Stop_RichText(providerData.entity) or from local matched_network_id_string = format("0x%x", band(providerData.network_id)) if message_level >= 3 then printmsg({"ltn-message.provider-found", from_gps, tostring(providerData.priority), tostring(providerData.activeDeliveryCount), providerData.count, "[" .. itype .. "=" .. iname .. "]"}, requestForce, true) end -- limit deliverySize to count at provider local deliverySize = count if count > providerData.count then deliverySize = providerData.count end local stacks = deliverySize -- for fluids stack = tanker capacity if itype ~= "fluid" then stacks = ceil(deliverySize / game.item_prototypes[iname].stack_size) -- calculate amount of stacks item count will occupy end -- max_carriages = shortest set max-train-length if providerData.max_carriages > 0 and (providerData.max_carriages < requestStation.max_carriages or requestStation.max_carriages == 0) then max_carriages = providerData.max_carriages end -- min_carriages = longest set min-train-length if providerData.min_carriages > 0 and (providerData.min_carriages > requestStation.min_carriages or requestStation.min_carriages == 0) then min_carriages = providerData.min_carriages end global.Dispatcher.Requests_by_Stop[toID][item] = nil -- remove before merge so it's not added twice local loadingList = { {type=itype, name=iname, localname=localname, count=deliverySize, stacks=stacks} } local totalStacks = stacks if debug_log then log("created new order "..from.." >> "..to..": "..deliverySize.." "..item.." in "..stacks.."/"..totalStacks.." stacks, min length: "..min_carriages.." max length: "..max_carriages) end -- find possible mergeable items, fluids can't be merged in a sane way if itype ~= "fluid" then for merge_item, merge_count_req in pairs(global.Dispatcher.Requests_by_Stop[toID]) do local merge_type, merge_name = match(merge_item, match_string) if merge_type and merge_name and game.item_prototypes[merge_name] then local merge_localname = game.item_prototypes[merge_name].localised_name -- get current provider for requested item if global.Dispatcher.Provided[merge_item] and global.Dispatcher.Provided[merge_item][fromID] then -- set delivery Size and stacks local merge_count_prov = global.Dispatcher.Provided[merge_item][fromID] local merge_deliverySize = merge_count_req if merge_count_req > merge_count_prov then merge_deliverySize = merge_count_prov end local merge_stacks = ceil(merge_deliverySize / game.item_prototypes[merge_name].stack_size) -- calculate amount of stacks item count will occupy -- add to loading list loadingList[#loadingList+1] = {type=merge_type, name=merge_name, localname=merge_localname, count=merge_deliverySize, stacks=merge_stacks} totalStacks = totalStacks + merge_stacks -- order.totalStacks = order.totalStacks + merge_stacks -- order.loadingList[#order.loadingList+1] = loadingList if debug_log then log("inserted into order "..from.." >> "..to..": "..merge_deliverySize.." "..merge_item.." in "..merge_stacks.."/"..totalStacks.." stacks.") end end end end end -- find train local free_trains = getFreeTrains(providerData, min_carriages, max_carriages, itype, totalStacks) if not free_trains then create_alert(requestStation.entity, "depot-empty", {"ltn-message.no-train-found", from, to, matched_network_id_string, tostring(min_carriages), tostring(max_carriages) }, requestForce) if message_level >= 1 then printmsg({"ltn-message.no-train-found", from_gps, to_gps, matched_network_id_string, tostring(min_carriages), tostring(max_carriages) }, requestForce, true) end if debug_log then log("No train with "..tostring(min_carriages).." <= length <= "..tostring(max_carriages).." to transport "..tostring(totalStacks).." stacks from "..from.." to "..to.." in network "..matched_network_id_string.." found in Depot.") end script.raise_event(on_dispatcher_no_train_found_event, { to = to, to_id = toID, from = from, from_id = fromID, network_id = requestStation.network_id, min_carriages = min_carriages, max_carriages = max_carriages, shipment = loadingList, }) global.Dispatcher.Requests_by_Stop[toID][item] = count -- add removed item back to list of requested items. return nil end local selectedTrain = free_trains[1].train local trainInventorySize = free_trains[1].inventory_size if message_level >= 3 then printmsg({"ltn-message.train-found", from_gps, to_gps, matched_network_id_string, tostring(trainInventorySize), tostring(totalStacks) }, requestForce) end if debug_log then log("Train to transport "..tostring(trainInventorySize).."/"..tostring(totalStacks).." stacks from "..from.." to "..to.." in network "..matched_network_id_string.." found in Depot.") end -- recalculate delivery amount to fit in train if trainInventorySize < totalStacks then -- recalculate partial shipment if itype == "fluid" then -- fluids are simple loadingList[1].count = trainInventorySize else -- items need a bit more math for i=#loadingList, 1, -1 do if totalStacks - loadingList[i].stacks < trainInventorySize then -- remove stacks until it fits in train loadingList[i].stacks = loadingList[i].stacks - (totalStacks - trainInventorySize) totalStacks = trainInventorySize local newcount = loadingList[i].stacks * game.item_prototypes[loadingList[i].name].stack_size loadingList[i].count = min(newcount, loadingList[i].count) break else -- remove item and try again totalStacks = totalStacks - loadingList[i].stacks table.remove(loadingList, i) end end end end -- create delivery if message_level >= 2 then if #loadingList == 1 then printmsg({"ltn-message.creating-delivery", from_gps, to_gps, loadingList[1].count, "[" .. loadingList[1].type .. "=" .. loadingList[1].name .. "]"}, requestForce) else printmsg({"ltn-message.creating-delivery-merged", from_gps, to_gps, totalStacks}, requestForce) end end -- create schedule -- local selectedTrain = global.Dispatcher.availableTrains[trainID].train local depot = global.LogisticTrainStops[selectedTrain.station.unit_number] local schedule = {current = 1, records = {}} schedule.records[#schedule.records + 1] = NewScheduleRecord(depot.entity.backer_name, "inactivity", depot_inactivity) -- make train go to specific stations by setting a temporary waypoint on the rail the station is connected to -- schedules cannot have temporary stops on a different surface, those need to be added when the delivery is updated with a train on a different surface if from_rail and from_rail_direction and depot.entity.surface == from_rail.surface then schedule.records[#schedule.records + 1] = NewTempScheduleRecord(from_rail, from_rail_direction) else if debug_log then log("(ProcessRequest) Warning: creating schedule without temporary stop for provider.") end end schedule.records[#schedule.records + 1] = NewScheduleRecord(from, "item_count", "≥", loadingList) if to_rail and to_rail_direction and depot.entity.surface == to_rail.surface and (from_rail and to_rail.surface == from_rail.surface) then schedule.records[#schedule.records + 1] = NewTempScheduleRecord(to_rail, to_rail_direction) else if debug_log then log("(ProcessRequest) Warning: creating schedule without temporary stop for requester.") end end schedule.records[#schedule.records + 1] = NewScheduleRecord(to, "item_count", "=", loadingList, 0) -- log("DEBUG: schedule = "..serpent.block(schedule)) selectedTrain.schedule = schedule local shipment = {} if debug_log then log("Creating Delivery: "..totalStacks.." stacks, "..from.." >> "..to) end for i=1, #loadingList do local loadingListItem = loadingList[i].type..","..loadingList[i].name -- store Delivery shipment[loadingListItem] = loadingList[i].count -- subtract Delivery from Provided items and check thresholds global.Dispatcher.Provided[loadingListItem][fromID] = global.Dispatcher.Provided[loadingListItem][fromID] - loadingList[i].count local new_provided = global.Dispatcher.Provided[loadingListItem][fromID] local new_provided_stacks = 0 local useProvideStackThreshold = false if loadingList[i].type == "item" then if game.item_prototypes[loadingList[i].name] then new_provided_stacks = new_provided / game.item_prototypes[loadingList[i].name].stack_size end useProvideStackThreshold = providerData.providing_threshold_stacks > 0 end if (useProvideStackThreshold and new_provided_stacks >= providerData.providing_threshold_stacks) or (not useProvideStackThreshold and new_provided >= providerData.providing_threshold) then global.Dispatcher.Provided[loadingListItem][fromID] = new_provided global.Dispatcher.Provided_by_Stop[fromID][loadingListItem] = new_provided else global.Dispatcher.Provided[loadingListItem][fromID] = nil global.Dispatcher.Provided_by_Stop[fromID][loadingListItem] = nil end -- remove Request and reset age global.Dispatcher.Requests_by_Stop[toID][loadingListItem] = nil global.Dispatcher.RequestAge[loadingListItem..","..toID] = nil if debug_log then log(" "..loadingListItem..", "..loadingList[i].count.." in "..loadingList[i].stacks.." stacks ") end end global.Dispatcher.new_Deliveries[#global.Dispatcher.new_Deliveries+1] = selectedTrain.id global.Dispatcher.Deliveries[selectedTrain.id] = { force = requestForce, train = selectedTrain, started = game.tick, from = from, from_id = fromID, to = to, to_id = toID, network_id = providerData.network_id, surface_connections = providerData.surface_connections, shipment = shipment} global.Dispatcher.availableTrains_total_capacity = global.Dispatcher.availableTrains_total_capacity - global.Dispatcher.availableTrains[selectedTrain.id].capacity global.Dispatcher.availableTrains_total_fluid_capacity = global.Dispatcher.availableTrains_total_fluid_capacity - global.Dispatcher.availableTrains[selectedTrain.id].fluid_capacity global.Dispatcher.availableTrains[selectedTrain.id] = nil -- train is no longer available => set depot to yellow setLamp(depot, "yellow", 1) -- update delivery count and lamps on provider and requester for _, stopID in pairs({fromID, toID}) do local stop = global.LogisticTrainStops[stopID] if stop.entity.valid and (stop.entity.unit_number == fromID or stop.entity.unit_number == toID) then table.insert(stop.active_deliveries, selectedTrain.id) -- only update blue signal count; change to yellow if it wasn't blue local current_signal = stop.lamp_control.get_control_behavior().get_signal(1) if current_signal and current_signal.signal.name == "signal-blue" then setLamp(stop, "blue", #stop.active_deliveries) else setLamp(stop, "yellow", #stop.active_deliveries) end end end return selectedTrain.id -- deliveries are indexed by train.id end