767 lines
38 KiB
Lua

--[[ 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