--[[ Copyright (c) 2017 Optera * Part of Logistics Train Network * * See LICENSE.md in the project directory for license information. --]] -- return true if stop, output, lamp are on same logic network local function detectShortCircuit(checkStop) local scdetected = false local networks = {} local entities = {checkStop.entity, checkStop.output, checkStop.input} for k, entity in pairs(entities) do local greenWire = entity.get_circuit_network(defines.wire_type.green) if greenWire then if networks[greenWire.network_id] then scdetected = true else networks[greenWire.network_id] = entity.unit_number end end local redWire = entity.get_circuit_network(defines.wire_type.red) if redWire then if networks[redWire.network_id] then scdetected = true else networks[redWire.network_id] = entity.unit_number end end end return scdetected end local function remove_available_train(trainID) if debug_log then log("(UpdateStop) removing available train "..tostring(trainID).." from depot." ) end 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 -- update stop input signals function UpdateStop(stopID, stop) global.Dispatcher.Requests_by_Stop[stopID] = nil -- remove invalid stops if not stop or not stop.entity.valid or not stop.input.valid or not stop.output.valid or not stop.lamp_control.valid then if message_level >= 1 then printmsg({"ltn-message.error-invalid-stop", stopID}) end if debug_log then log(format("(UpdateStop) Removing invalid stop: [%d]", stopID )) end RemoveStop(stopID) return end -- remove invalid trains if stop.parked_train and not stop.parked_train.valid then global.LogisticTrainStops[stopID].parked_train = nil global.LogisticTrainStops[stopID].parked_train_id = nil end -- remove invalid active_deliveries -- shouldn't be necessary for i=#stop.active_deliveries, 1, -1 do if not global.Dispatcher.Deliveries[stop.active_deliveries[i]] then table.remove(stop.active_deliveries, i) if message_level >= 1 then printmsg({"ltn-message.error-invalid-delivery", stop.entity.backer_name}) end if debug_log then log("(UpdateStop) Removing invalid delivery from stop '"..tostring(stop.entity.backer_name).."': "..tostring(stop.active_deliveries[i])) end end end -- reset stop parameters in case something goes wrong stop.min_carriages = 0 stop.max_carriages = 0 stop.max_trains = 0 stop.requesting_threshold = min_requested stop.requester_priority = 0 stop.no_warnings = false stop.providing_threshold = min_provided stop.provider_priority = 0 stop.locked_slots = 0 stop.depot_priority = 0 -- skip short circuited stops if detectShortCircuit(stop) then stop.error_code = 1 if stop.parked_train_id and global.Dispatcher.availableTrains[stop.parked_train_id] then remove_available_train(stop.parked_train_id) end setLamp(stop, ErrorCodes[stop.error_code], 1) if debug_log then log("(UpdateStop) Short circuit error: "..stop.entity.backer_name) end return end -- skip deactivated stops local stopCB = stop.entity.get_control_behavior() if stopCB and stopCB.disabled then stop.error_code = 1 if stop.parked_train_id and global.Dispatcher.availableTrains[stop.parked_train_id] then remove_available_train(stop.parked_train_id) end setLamp(stop, ErrorCodes[stop.error_code], 2) if debug_log then log("(UpdateStop) Circuit deactivated stop: "..stop.entity.backer_name) end return end -- initialize control signal values to defaults local is_depot = false local depot_priority = 0 local network_id = default_network local min_carriages = 0 local max_carriages = 0 local max_trains = 0 local requesting_threshold = min_requested local requesting_threshold_stacks = 0 local requester_priority = 0 local no_warnings = false local providing_threshold = min_provided local providing_threshold_stacks = 0 local provider_priority = 0 local locked_slots = 0 -- get circuit values 0.16.24 local signals = stop.input.get_merged_signals() if not signals then return end -- either lamp and lampctrl are not connected or lampctrl has no output signal local signals_filtered = {} local signal_type_virtual = "virtual" local abs = math.abs for _,v in pairs(signals) do if v.signal.name and v.signal.type then if v.signal.type ~= signal_type_virtual then -- add item and fluid signals to new array signals_filtered[v.signal] = v.count elseif ControlSignals[v.signal.name] then -- read out control signals if v.signal.name == ISDEPOT and v.count > 0 then is_depot = true elseif v.signal.name == DEPOT_PRIORITY then depot_priority = v.count elseif v.signal.name == NETWORKID then network_id = v.count elseif v.signal.name == MINTRAINLENGTH and v.count > 0 then min_carriages = v.count elseif v.signal.name == MAXTRAINLENGTH and v.count > 0 then max_carriages = v.count elseif v.signal.name == MAXTRAINS and v.count > 0 then max_trains = v.count elseif v.signal.name == REQUESTED_THRESHOLD then requesting_threshold = abs(v.count) elseif v.signal.name == REQUESTED_STACK_THRESHOLD then requesting_threshold_stacks = abs(v.count) elseif v.signal.name == REQUESTED_PRIORITY then requester_priority = v.count elseif v.signal.name == NOWARN and v.count > 0 then no_warnings = true elseif v.signal.name == PROVIDED_THRESHOLD then providing_threshold = abs(v.count) elseif v.signal.name == PROVIDED_STACK_THRESHOLD then providing_threshold_stacks = abs(v.count) elseif v.signal.name == PROVIDED_PRIORITY then provider_priority = v.count elseif v.signal.name == LOCKEDSLOTS and v.count > 0 then locked_slots = v.count end end end end local network_id_string = format("0x%x", band(network_id)) --update lamp colors when error_code or is_depot changed state if stop.error_code ~=0 or stop.is_depot ~= is_depot then stop.error_code = 0 -- we are error free here if is_depot then if stop.parked_train_id and stop.parked_train.valid then if global.Dispatcher.Deliveries[stop.parked_train_id] then setLamp(stop, "yellow", 1) else setLamp(stop, "blue", 1) end else setLamp(stop, "green", 1) end else if #stop.active_deliveries > 0 then if stop.parked_train_id and stop.parked_train.valid then setLamp(stop, "blue", #stop.active_deliveries) else setLamp(stop, "yellow", #stop.active_deliveries) end else setLamp(stop, "green", 1) end end end -- check if it's a depot if is_depot then stop.is_depot = true stop.depot_priority = depot_priority stop.network_id = network_id -- add parked train to available trains if stop.parked_train_id and stop.parked_train.valid then if global.Dispatcher.Deliveries[stop.parked_train_id] then if debug_log then log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."}".. ", depot priority: ".. depot_priority.. ", assigned train.id: "..stop.parked_train_id ) end else if not global.Dispatcher.availableTrains[stop.parked_train_id] then -- full arrival handling in case ltn-depot signal was turned on with an already parked train TrainArrives(stop.parked_train) else -- update properties from depot global.Dispatcher.availableTrains[stop.parked_train_id].network_id = network_id global.Dispatcher.availableTrains[stop.parked_train_id].depot_priority = depot_priority end if debug_log then log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."}".. ", depot priority: ".. depot_priority.. ", available train.id: "..stop.parked_train_id ) end end else if debug_log then log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."}".. ", depot priority: ".. depot_priority.. ", no available train" ) end end -- not a depot > check if the name is unique else stop.is_depot = false if stop.parked_train_id and global.Dispatcher.availableTrains[stop.parked_train_id] then remove_available_train(stop.parked_train_id) end for signal, count in pairs(signals_filtered) do local signal_type = signal.type local signal_name = signal.name local item = signal_type..","..signal_name for trainID, delivery in pairs (global.Dispatcher.Deliveries) do local deliverycount = delivery.shipment[item] if deliverycount then if stop.parked_train and stop.parked_train_id == trainID then -- calculate items +- train inventory local traincount = 0 if signal_type == "fluid" then traincount = stop.parked_train.get_fluid_count(signal_name) else traincount = stop.parked_train.get_item_count(signal_name) end if delivery.to_id == stop.entity.unit_number then local newcount = count + traincount if newcount > 0 then newcount = 0 end --make sure we don't turn it into a provider if debug_log then log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."} updating requested count with train "..trainID.." inventory: "..item.." "..count.."+"..traincount.."="..newcount) end count = newcount elseif delivery.from_id == stop.entity.unit_number then if traincount <= deliverycount then local newcount = count - (deliverycount - traincount) if newcount < 0 then newcount = 0 end --make sure we don't turn it into a request if debug_log then log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."} updating provided count with train "..trainID.." inventory: "..item.." "..count.."-"..deliverycount - traincount.."="..newcount) end count = newcount else --train loaded more than delivery if debug_log then log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."} updating delivery count with overloaded train "..trainID.." inventory: "..item.." "..traincount) end -- update delivery to new size global.Dispatcher.Deliveries[trainID].shipment[item] = traincount end end else -- calculate items +- deliveries if delivery.to_id == stop.entity.unit_number then local newcount = count + deliverycount if newcount > 0 then newcount = 0 end --make sure we don't turn it into a provider if debug_log then log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."} updating requested count with delivery: "..item.." "..count.."+"..deliverycount.."="..newcount) end count = newcount elseif delivery.from_id == stop.entity.unit_number and not delivery.pickupDone then local newcount = count - deliverycount if newcount < 0 then newcount = 0 end --make sure we don't turn it into a request if debug_log then log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."} updating provided count with delivery: "..item.." "..count.."-"..deliverycount.."="..newcount) end count = newcount end end end end -- for delivery local useProvideStackThreshold = false local useRequestStackThreshold = false local stack_count = 0 if signal_type == "item" then useProvideStackThreshold = providing_threshold_stacks > 0 useRequestStackThreshold = requesting_threshold_stacks > 0 if game.item_prototypes[signal_name] then stack_count = count / game.item_prototypes[signal_name].stack_size end end -- update Dispatcher Storage -- Providers are used when above Provider Threshold -- Requests are handled when above Requester Threshold if (useProvideStackThreshold and stack_count >= providing_threshold_stacks) or (not useProvideStackThreshold and count >= providing_threshold) then global.Dispatcher.Provided[item] = global.Dispatcher.Provided[item] or {} global.Dispatcher.Provided[item][stopID] = count global.Dispatcher.Provided_by_Stop[stopID] = global.Dispatcher.Provided_by_Stop[stopID] or {} global.Dispatcher.Provided_by_Stop[stopID][item] = count if debug_log then local trainsEnRoute = ""; for k,v in pairs(stop.active_deliveries) do trainsEnRoute=trainsEnRoute.." "..v end log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."} provides "..item.." "..count.."("..providing_threshold..")".." stacks: "..stack_count.."("..providing_threshold_stacks..")"..", priority: "..provider_priority..", min length: "..min_carriages..", max length: "..max_carriages..", trains en route: "..trainsEnRoute) end elseif (useRequestStackThreshold and stack_count*-1 >= requesting_threshold_stacks) or (not useRequestStackThreshold and count*-1 >= requesting_threshold) then count = count * -1 local ageIndex = item..","..stopID global.Dispatcher.RequestAge[ageIndex] = global.Dispatcher.RequestAge[ageIndex] or game.tick global.Dispatcher.Requests[#global.Dispatcher.Requests+1] = {age = global.Dispatcher.RequestAge[ageIndex], stopID = stopID, priority = requester_priority, item = item, count = count} global.Dispatcher.Requests_by_Stop[stopID] = global.Dispatcher.Requests_by_Stop[stopID] or {} global.Dispatcher.Requests_by_Stop[stopID][item] = count if debug_log then local trainsEnRoute = ""; for k,v in pairs(stop.active_deliveries) do trainsEnRoute=trainsEnRoute.." "..v end log("(UpdateStop) "..stop.entity.backer_name.." {"..network_id_string.."} requests "..item.." "..count.."("..requesting_threshold..")".." stacks: "..tostring(stack_count*-1).."("..requesting_threshold_stacks..")"..", priority: "..requester_priority..", min length: "..min_carriages..", max length: "..max_carriages..", age: "..global.Dispatcher.RequestAge[ageIndex].."/"..game.tick..", trains en route: "..trainsEnRoute) end end end -- for circuitValues stop.network_id = network_id stop.providing_threshold = providing_threshold stop.providing_threshold_stacks = providing_threshold_stacks stop.provider_priority = provider_priority stop.requesting_threshold = requesting_threshold stop.requesting_threshold_stacks = requesting_threshold_stacks stop.requester_priority = requester_priority stop.min_carriages = min_carriages stop.max_carriages = max_carriages stop.max_trains = max_trains stop.locked_slots = locked_slots stop.no_warnings = no_warnings end end function setLamp(trainStop, color, count) -- skip invalid stops and colors if trainStop and trainStop.lamp_control.valid and ColorLookup[color] then trainStop.lamp_control.get_control_behavior().parameters = {{index = 1, signal = {type="virtual",name=ColorLookup[color]}, count = count }} return true end return false end function UpdateStopOutput(trainStop, ignore_existing_cargo) -- skip invalid stop outputs if not trainStop.output.valid then return end local signals = {} local index = 0 if trainStop.parked_train and trainStop.parked_train.valid then -- get train composition local carriages = trainStop.parked_train.carriages local encoded_positions_by_name = {} local encoded_positions_by_type = {} local inventory = not(ignore_existing_cargo) and trainStop.parked_train.get_contents() or {} local fluidInventory = not(ignore_existing_cargo) and trainStop.parked_train.get_fluid_contents() or {} if #carriages < 32 then --prevent circuit network integer overflow error if trainStop.parked_train_faces_stop then --train faces forwards >> iterate normal for i=1, #carriages do local signal_type = format("ltn-position-any-%s", carriages[i].type) if game.virtual_signal_prototypes[signal_type] then if encoded_positions_by_type[signal_type] then encoded_positions_by_type[signal_type] = encoded_positions_by_type[signal_type] + 2^(i-1) else encoded_positions_by_type[signal_type] = 2^(i-1) end else if message_level >= 1 then printmsg({"ltn-message.error-invalid-position-signal", signal_type}) end log(format("Error: signal \"%s\" not found!", signal_type)) end local signal_name = format("ltn-position-%s", carriages[i].name) if game.virtual_signal_prototypes[signal_name] then if encoded_positions_by_name[signal_name] then encoded_positions_by_name[signal_name] = encoded_positions_by_name[signal_name] + 2^(i-1) else encoded_positions_by_name[signal_name] = 2^(i-1) end else if message_level >= 1 then printmsg({"ltn-message.error-invalid-position-signal", signal_name}) end log(format("Error: signal \"%s\" not found!", signal_name)) end end else --train faces backwards >> iterate backwards n = 0 for i=#carriages, 1, -1 do local signal_type = format("ltn-position-any-%s", carriages[i].type) if game.virtual_signal_prototypes[signal_type] then if encoded_positions_by_type[signal_type] then encoded_positions_by_type[signal_type] = encoded_positions_by_type[signal_type] + 2^n else encoded_positions_by_type[signal_type] = 2^n end else if message_level >= 1 then printmsg({"ltn-message.error-invalid-position-signal", signal_type}) end log(format("Error: signal \"%s\" not found!", signal_type)) end local signal_name = format("ltn-position-%s", carriages[i].name) if game.virtual_signal_prototypes[signal_name] then if encoded_positions_by_name[signal_name] then encoded_positions_by_name[signal_name] = encoded_positions_by_name[signal_name] + 2^n else encoded_positions_by_name[signal_name] = 2^n end else if message_level >= 1 then printmsg({"ltn-message.error-invalid-position-signal", signal_name}) end log(format("Error: signal \"%s\" not found!", signal_name)) end n=n+1 end end for k ,v in pairs (encoded_positions_by_type) do index = index+1 table.insert(signals, {index = index, signal = {type="virtual",name=k}, count = v }) end for k ,v in pairs (encoded_positions_by_name) do index = index+1 table.insert(signals, {index = index, signal = {type="virtual",name=k}, count = v }) end end if not trainStop.is_depot then -- Update normal stations local conditions = trainStop.parked_train.schedule.records[trainStop.parked_train.schedule.current].wait_conditions if conditions ~= nil then for _, c in pairs(conditions) do if c.condition and c.condition.first_signal then -- loading without mods can make first signal nil? if c.type == "item_count" then if (c.condition.comparator == "=" and c.condition.constant == 0) then --train expects to be unloaded of each of this item inventory[c.condition.first_signal.name] = nil elseif c.condition.comparator == "≥" then --train expects to be loaded to x of this item inventory[c.condition.first_signal.name] = c.condition.constant end elseif c.type == "fluid_count" then if (c.condition.comparator == "=" and c.condition.constant == 0) then --train expects to be unloaded of each of this fluid fluidInventory[c.condition.first_signal.name] = -1 elseif c.condition.comparator == "≥" then --train expects to be loaded to x of this fluid fluidInventory[c.condition.first_signal.name] = c.condition.constant end end end end end -- output expected inventory contents for k,v in pairs(inventory) do index = index+1 table.insert(signals, {index = index, signal = {type="item", name=k}, count = v}) end for k,v in pairs(fluidInventory) do index = index+1 table.insert(signals, {index = index, signal = {type="fluid", name=k}, count = v}) end end -- not trainStop.is_depot end -- will reset if called with no parked train if index > 0 then -- log("[LTN] "..tostring(trainStop.entity.backer_name).. " displaying "..#signals.."/"..tostring(trainStop.output.get_control_behavior().signals_count).." signals.") while #signals > trainStop.output.get_control_behavior().signals_count do -- log("[LTN] removing signal "..tostring(signals[#signals].signal.name)) table.remove(signals) end if index ~= #signals then if message_level >= 1 then printmsg({"ltn-message.error-stop-output-truncated", tostring(trainStop.entity.backer_name), tostring(trainStop.parked_train), trainStop.output.get_control_behavior().signals_count, index-#signals}, trainStop.entity.force) end if debug_log then log("(UpdateStopOutput) Inventory of train "..tostring(trainStop.parked_train.id).." at stop "..tostring(trainStop.entity.backer_name).." exceeds stop output limit of "..trainStop.output.get_control_behavior().signals_count.." by "..index-#signals.." signals.") end end trainStop.output.get_control_behavior().parameters = signals if debug_log then log("(UpdateStopOutput) Updating signals for "..tostring(trainStop.entity.backer_name)..": train "..tostring(trainStop.parked_train.id)..": "..index.." signals") end else trainStop.output.get_control_behavior().parameters = nil if debug_log then log("(UpdateStopOutput) Resetting signals for "..tostring(trainStop.entity.backer_name)..".") end end end