767 lines
38 KiB
Lua
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
|
|
|
|
|