0fca6e8a9a
In-game 26/9: hak ca. hvert 5. s efter 1.27.2 = holdende vognes genplanlægning (ADOutbound/ADLoadSwap hvert 5 s) med AD-stifinding + reserve-BFS gennem 13.550 waypoints pr. kandidat. Rutesvar caches pr. mål og 10 m- område i 30 s (max 2000 poster); langsomme rutetjek logges. Æg: 'ingen mere last' markerer nu kilden som hvilende 3 min, så runden roterer mellem hønsehusene.
946 lines
44 KiB
Lua
946 lines
44 KiB
Lua
-- AD Outbound
|
||
-- Udkørsel af produkter: før hver pålæsning vurderer planlæggeren om en tur er berettiget
|
||
-- (pris, fuldt læs, lager højt), vælger vare(r) + kilde(r) + mål og skriver det i AutoDrives
|
||
-- tilstand — AutoDrive kører, læsser (bulk eller paller via Universal Autoload) og læsser af.
|
||
-- Ingen tur: traktoren holder ved et Wait-punkt og spørger igen hvert 5. sekund.
|
||
-- Ved et objektlager bedes lageret spawne én palle ad gangen, som UAL tager.
|
||
-- Opsamlingsrunde: bulk = samme vare fra flere kilder; paller = flere varer på én tur.
|
||
--
|
||
-- Hook 1: ADSmartPickup's getNextPickup-wrap kalder choosePickup for udkørsels-traktorer.
|
||
-- Hook 2: LoadAtDestinationTask.update/finished (install) — hold, udtag, fremdriftsvagt, runde.
|
||
|
||
ADOutbound = {}
|
||
|
||
ADOutbound.LOG_PREFIX = "ADSmartPickup: "
|
||
-- Holdende traktorer spørger planlæggeren så tit (opslag over alle markører).
|
||
ADOutbound.CHECK_INTERVAL_MS = 5000
|
||
-- "Kør uanset mængde": stiger vognens fyldning ikke så længe ved kilden, køres der med det der er.
|
||
ADOutbound.NO_PROGRESS_MS = 15000
|
||
-- Objektlager der ikke leverer en palle inden så længe regnes som fastlåst (kendt spilfejl).
|
||
ADOutbound.SPAWN_STALL_MS = 30000
|
||
-- UAL kan først læsse når vognen holder helt stille (AutoDrives egen grænse).
|
||
ADOutbound.MAX_SPEED_FOR_LOADING = 0.0005
|
||
ADOutbound.NOTIFICATION_MS = 8000
|
||
-- Tom vogn ved kilden så længe uden at få noget = kilden har ikke varen alligevel (paller ikke
|
||
-- spawnet, forkert farm, ...): kilden udelukkes for varen i EMPTY_SOURCE_TTL_MS og turen afsluttes.
|
||
ADOutbound.EMPTY_SOURCE_MS = 30000
|
||
ADOutbound.EMPTY_SOURCE_TTL_MS = 300000
|
||
-- kilde der gav lidt og så intet mere (fx paller uden for Universal Autoloads rækkevidde): hviler så længe,
|
||
-- så de andre kilder kommer til (in-game 26/9: æg-vognen kørte til samme hønsehus tre gange i træk)
|
||
ADOutbound.THIN_SOURCE_REST_MS = 180000
|
||
-- Autoload-bil på vej til aflæsning: stropperne løsnes så mange meter FØR markøren. Markøren står
|
||
-- hvor næsen stopper, og en lastbils lad har passeret salgstriggeren dér; AutoDrive løsner selv
|
||
-- først ved markøren ("AL unload = center"), hvilket er for sent for en lastbil.
|
||
ADOutbound.UNSTRAP_BEFORE_M = 25
|
||
|
||
-- køretøj -> {run, trip, sources, queue, visited, current, lastFill, noProgressMs, spawn, done}
|
||
local trips = setmetatable({}, {__mode = "k"})
|
||
-- køretøj -> {markerId, markerName, reasons, timerMs, lastLogged}
|
||
local holds = setmetatable({}, {__mode = "k"})
|
||
-- run-id -> {reasons, warning}
|
||
local status = {}
|
||
-- køretøj -> {[nøgle] = tekst} (advarsler vises én gang pr. ændret tekst)
|
||
local warned = setmetatable({}, {__mode = "k"})
|
||
-- "kilde-id:fillType" -> tidspunkt (ms) hvor udelukkelsen udløber
|
||
local emptyUntil = {}
|
||
local isInstalled = false
|
||
|
||
-- Glem alle "tom/hvilende kilde"-markeringer (tests; og når runs genindlæses).
|
||
function ADOutbound.resetEmptySources()
|
||
emptyUntil = {}
|
||
end
|
||
|
||
local function now()
|
||
if g_currentMission ~= nil and type(g_currentMission.time) == "number" then
|
||
return g_currentMission.time
|
||
end
|
||
return os.clock() * 1000
|
||
end
|
||
|
||
local function getRun(vehicle)
|
||
return ADRunsController ~= nil and ADRunsController.getRunForVehicle ~= nil and ADRunsController.getRunForVehicle(vehicle) or nil
|
||
end
|
||
|
||
function ADOutbound.isOutboundVehicle(vehicle)
|
||
local run = getRun(vehicle)
|
||
return run ~= nil and run.kind == "outbound"
|
||
end
|
||
|
||
local function fillTypeName(fillType)
|
||
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||
end
|
||
|
||
-- Lagrene en kildes læssestation trækker fra (FS25: sourceStorages = {[storage] = storage}); nil = ukendt.
|
||
local function sourceStorages(source)
|
||
-- kun siloer: en stalds mælke- og gyllepunkt deler også lager, men er forskellige læssepunkter
|
||
if source == nil or source.kind ~= "silo" then
|
||
return nil
|
||
end
|
||
local station = source.station
|
||
if station == nil or type(station.sourceStorages) ~= "table" then
|
||
return nil
|
||
end
|
||
local list = {}
|
||
for key, value in pairs(station.sourceStorages) do
|
||
table.insert(list, type(key) == "table" and key or value)
|
||
end
|
||
return #list > 0 and list or nil
|
||
end
|
||
|
||
local mergedLogged = {}
|
||
local function logMergedSources(vehicle, run, merged)
|
||
for keptId, ids in pairs(merged or {}) do
|
||
local key = tostring(run.id) .. ":" .. tostring(keptId) .. ":" .. table.concat(ids, ",")
|
||
if not mergedLogged[key] then
|
||
mergedLogged[key] = true
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "run '%s': kilde %s deler lager med %s — regnes som ét stop", tostring(run.name), tostring(keptId), table.concat(ids, ", "))
|
||
end
|
||
end
|
||
end
|
||
|
||
local function getStatus(run)
|
||
status[run.id] = status[run.id] or {}
|
||
return status[run.id]
|
||
end
|
||
|
||
-- ---- advarsler (log + rød AutoDrive-notifikation på skærmen, én gang pr. ændret tekst) ----
|
||
|
||
local function warnOnce(adEnv, vehicle, key, text)
|
||
warned[vehicle] = warned[vehicle] or {}
|
||
if warned[vehicle][key] == text then
|
||
return
|
||
end
|
||
warned[vehicle][key] = text
|
||
Logging.warning(ADOutbound.LOG_PREFIX .. "%s", text)
|
||
local event, messages = adEnv.AutoDriveMessageEvent, adEnv.ADMessagesManager
|
||
if event ~= nil and event.sendMessageOrNotification ~= nil and messages ~= nil and messages.messageTypes ~= nil then
|
||
pcall(event.sendMessageOrNotification, vehicle, messages.messageTypes.ERROR, "%s", ADOutbound.NOTIFICATION_MS, "ADSmartPickup: " .. text)
|
||
end
|
||
local run = getRun(vehicle)
|
||
if run ~= nil then
|
||
getStatus(run).warning = text
|
||
end
|
||
end
|
||
|
||
local function clearWarning(vehicle, key)
|
||
if warned[vehicle] ~= nil then
|
||
warned[vehicle][key] = nil
|
||
end
|
||
end
|
||
|
||
-- ---- planlægger-input fra spillet ----
|
||
|
||
local function getVehicleCapacity(adEnv, vehicle)
|
||
local trailers = adEnv.AutoDrive.getAllUnits ~= nil and adEnv.AutoDrive.getAllUnits(vehicle) or nil
|
||
if trailers == nil then
|
||
return nil, false, nil
|
||
end
|
||
local _, capacity = adEnv.AutoDrive.getAllFillLevels(trailers)
|
||
local hasAutoload = false
|
||
for _, trailer in ipairs(trailers) do
|
||
if adEnv.AutoDrive.hasAL ~= nil and adEnv.AutoDrive:hasAL(trailer) then
|
||
hasAutoload = true
|
||
end
|
||
end
|
||
-- UAL opgiver antal paller (læssede + 1), ikke liter: liter-kapaciteten er ukendt for en pallebil
|
||
if hasAutoload and (capacity == nil or capacity < 100) then
|
||
capacity = nil
|
||
end
|
||
return capacity, hasAutoload, trailers
|
||
end
|
||
|
||
local function wantsSell(policy) return policy == "sell" or policy == "sellElseStore" end
|
||
local function wantsStore(policy) return policy == "store" or policy == "sellElseStore" end
|
||
|
||
-- Bedste salgssted for varen. Rutetjek (A*) laves kun for stationer over pristærsklen, bedste
|
||
-- først, og stopper ved den første nåelige. Er ingen over tærsklen, returneres det bedste
|
||
-- (unåelighed er ligegyldig, turen kører alligevel ikke) — til begrundelse og UI.
|
||
-- unreachableName = det bedst betalende over tærsklen, når intet over tærsklen kan nås.
|
||
local function findSellQuote(adEnv, vehicle, product, sellMarkers, sellAtPercent, homeId)
|
||
local quotes = {}
|
||
for _, entry in ipairs(sellMarkers) do
|
||
if ADSources.stationAccepts(entry.station, product.fillType) then
|
||
local quote = ADPrices.getQuote(entry.station, product.fillType)
|
||
if quote ~= nil then
|
||
table.insert(quotes, {percent = quote.percent, price = quote.price, top = quote.top, wayPointId = entry.marker.id, stationName = tostring(entry.marker.name)})
|
||
end
|
||
end
|
||
end
|
||
table.sort(quotes, function(a, b)
|
||
if a.percent ~= b.percent then return a.percent > b.percent end
|
||
if a.price ~= b.price then return a.price > b.price end
|
||
return a.wayPointId < b.wayPointId
|
||
end)
|
||
local unreachableName = nil
|
||
for _, quote in ipairs(quotes) do
|
||
if quote.percent < (sellAtPercent or 90) then
|
||
break
|
||
end
|
||
if homeId == nil or ADWaitPool.hasRouteBothWays(adEnv, vehicle, quote.wayPointId, homeId) then
|
||
return quote, nil
|
||
end
|
||
unreachableName = unreachableName or quote.stationName
|
||
end
|
||
if unreachableName ~= nil then
|
||
return nil, unreachableName
|
||
end
|
||
return quotes[1], nil
|
||
end
|
||
|
||
local function findStoreMarker(adEnv, vehicle, product, capacity, sources, litersOverride, run)
|
||
local isReachable = function(markerId)
|
||
return ADWaitPool.hasRouteTo(adEnv, vehicle, markerId)
|
||
end
|
||
-- paller lagres i et palleskur (objektlager) — UAL læsser af dér
|
||
if product.isPallet then
|
||
-- "Lager til" med palleskure: første valgte der tager varen, har plads og kan nås
|
||
local forcedSheds = run ~= nil and ADRunsController ~= nil and ADRunsController.getStoreBuildings ~= nil and ADRunsController.getStoreBuildings(adEnv, run) or {}
|
||
local hasForcedShed, reasons = false, {}
|
||
for _, building in ipairs(forcedSheds) do
|
||
if building.kind == "objectStorage" then
|
||
hasForcedShed = true
|
||
local marker = building.unloadMarker or building.loadMarker
|
||
local free = ADSources.objectStorageFreeSlots(building.placeable)
|
||
local why = nil
|
||
if marker == nil then
|
||
why = "ingen markør"
|
||
elseif not ADSources.objectStorageAccepts(building.placeable, product.fillType) then
|
||
why = "palleskuret siger nej til varen"
|
||
elseif free < 1 then
|
||
why = "fuldt"
|
||
elseif not isReachable(marker.id) then
|
||
why = "ingen vej til '" .. tostring(marker.name) .. "'"
|
||
end
|
||
if why == nil then
|
||
return {wayPointId = marker.id, name = tostring(building.name), free = free}
|
||
end
|
||
table.insert(reasons, string.format("%s: %s (%d ledige pladser)", tostring(building.name), why, free == math.huge and -1 or free))
|
||
end
|
||
end
|
||
if hasForcedShed then
|
||
product.storeReason = table.concat(reasons, "; ")
|
||
warnOnce(adEnv, vehicle, "forcedShed:" .. tostring(product.name) .. ":" .. table.concat(reasons, ";"), string.format("'%s': %s kan ikke sættes på lager — %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(product.name), table.concat(reasons, "; ")))
|
||
return nil
|
||
end
|
||
if ADSources.findPalletStore == nil then
|
||
return nil
|
||
end
|
||
return ADSources.findPalletStore(adEnv, vehicle, product.fillType, ADSources.getSourcePlaceables(sources), isReachable, ADSmartPickup.getFarmId(vehicle))
|
||
end
|
||
if ADLoadSwap == nil or ADLoadSwap.findDumpMarker == nil then
|
||
return nil
|
||
end
|
||
local available = litersOverride or 0
|
||
if litersOverride == nil then
|
||
for _, source in ipairs(product.sources) do
|
||
available = available + (source.available or 0)
|
||
end
|
||
end
|
||
local liters = math.max(1, math.min(available, capacity or available))
|
||
-- "Lager til" valgt i flowet: KUN de bygninger (fx gylle -> BGA, aldrig AIO-fabrikken)
|
||
local forced = run ~= nil and ADRunsController ~= nil and ADRunsController.getStoreMarkers ~= nil and ADRunsController.getStoreMarkers(adEnv, run) or {}
|
||
if #forced > 0 and ADLoadSwap.findForcedDumpMarker ~= nil then
|
||
local chosen, why = ADLoadSwap.findForcedDumpMarker(adEnv, vehicle, product.fillType, liters, forced, isReachable)
|
||
if chosen == nil then
|
||
product.storeReason = tostring(why)
|
||
warnOnce(adEnv, vehicle, "forced:" .. tostring(product.name) .. ":" .. tostring(why), string.format("'%s': %s kan ikke læsses af — %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(product.name), tostring(why)))
|
||
return nil
|
||
end
|
||
return {wayPointId = chosen.id, name = tostring(chosen.name), free = liters}
|
||
end
|
||
local avoid = ADRunsController ~= nil and ADRunsController.getPickupMarkers ~= nil and ADRunsController.getPickupMarkers() or nil
|
||
local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, product.fillType, liters, nil, isReachable, ADSources.getSourcePlaceables(sources), avoid, true)
|
||
if marker == nil then
|
||
return nil
|
||
end
|
||
return {wayPointId = marker.id, name = tostring(marker.name), free = liters}
|
||
end
|
||
|
||
-- input til ADOutboundPlanner.plan + kilder pr. waypoint-id.
|
||
function ADOutbound.buildPlanInput(adEnv, run, vehicle)
|
||
local farmId = ADSmartPickup.getFarmId(vehicle)
|
||
local capacity, hasAutoload = getVehicleCapacity(adEnv, vehicle)
|
||
local carriable = {}
|
||
for _, fillType in ipairs(adEnv.AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically(vehicle) or {}) do
|
||
carriable[fillType] = true
|
||
end
|
||
local sources, sourcesById = {}, {}
|
||
for _, wayPointId in ipairs(run.sourceWayPointIds or {}) do
|
||
local source = ADSources.identify(adEnv, wayPointId, farmId)
|
||
if source == nil then
|
||
local explanation = ADSources.explainMarker ~= nil and ADSources.explainMarker(adEnv, wayPointId, farmId) or "?"
|
||
warnOnce(adEnv, vehicle, "source:" .. tostring(wayPointId), string.format("run '%s': kilde-markør %s er ikke en fabrik, silo, stald eller objektlager — %s", tostring(run.name), tostring(wayPointId), explanation))
|
||
else
|
||
clearWarning(vehicle, "source:" .. tostring(wayPointId))
|
||
table.insert(sources, source)
|
||
sourcesById[wayPointId] = source
|
||
end
|
||
end
|
||
-- kilder der deler lager (mælketanke som udvidelser) er ét stop — ellers kører runden mellem dem
|
||
local merged
|
||
sources, merged = ADOutboundPlanner.dedupeSharedSources(sources, sourceStorages)
|
||
logMergedSources(vehicle, run, merged)
|
||
local byName, products = {}, {}
|
||
local time = now()
|
||
for _, source in ipairs(sources) do
|
||
for _, item in ipairs(ADSources.getProducts(adEnv, source, farmId)) do
|
||
local policy = ADRuns.getProductPolicy(run, item.name)
|
||
-- bulk hvis vognen kan bære varen; ellers paller hvis varen står på paller og vognen har UAL
|
||
-- (en pallebil ved sukkerfabrikken tager sukkerpallerne, en tipvogn tager bulk)
|
||
local usePallets = item.isPallet or (carriable[item.fillType] ~= true and hasAutoload and item.canPallet == true)
|
||
local canCarry = usePallets and hasAutoload or (not usePallets and carriable[item.fillType] == true)
|
||
local available = item.available
|
||
if usePallets and not item.isPallet then
|
||
available = item.palletLiters or 0
|
||
end
|
||
local emptyKey = tostring(source.wayPointId) .. ":" .. tostring(item.fillType)
|
||
local isMarkedEmpty = emptyUntil[emptyKey] ~= nil and emptyUntil[emptyKey] > time
|
||
if policy ~= "off" and canCarry and not isMarkedEmpty then
|
||
local product = byName[item.name]
|
||
if product == nil then
|
||
product = {fillType = item.fillType, name = item.name, policy = policy, isPallet = usePallets, sources = {}}
|
||
byName[item.name] = product
|
||
table.insert(products, product)
|
||
end
|
||
table.insert(product.sources, {wayPointId = source.wayPointId, available = available, level = item.level, capacity = item.capacity, palletCount = usePallets and (item.palletCount or 0) or 0})
|
||
end
|
||
end
|
||
end
|
||
table.sort(products, function(a, b) return a.name < b.name end)
|
||
-- salgsstederne findes én gang pr. plan (opslag over alle markører × stationer)
|
||
local sellMarkers = ADSources.getSellMarkers(adEnv, farmId)
|
||
for _, product in ipairs(products) do
|
||
if wantsSell(product.policy) then
|
||
local homeId = product.sources[1] ~= nil and product.sources[1].wayPointId or nil
|
||
local quote, unreachableName = findSellQuote(adEnv, vehicle, product, sellMarkers, run.sellAtPercent, homeId)
|
||
product.quote = quote
|
||
if quote == nil and unreachableName == nil then
|
||
-- intet salgssted tager varen: sig hvilke salgsmarkører der findes, og hvad de tager
|
||
local found = {}
|
||
for _, entry in ipairs(sellMarkers) do
|
||
local accepted = ADSources.describeAccepted ~= nil and ADSources.describeAccepted(entry.station) or "?"
|
||
table.insert(found, string.format("%s (%s)", tostring(entry.marker.name), accepted))
|
||
end
|
||
local key = "nosell:" .. product.name
|
||
local text = string.format("intet salgssted tager %s — salgsmarkører: %s", product.name, #found > 0 and table.concat(found, "; ") or "ingen (sæt en markør ved et salgssted)")
|
||
if warned[vehicle] == nil or warned[vehicle][key] ~= text then
|
||
warned[vehicle] = warned[vehicle] or {}
|
||
warned[vehicle][key] = text
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s': %s", tostring(ADRuns.vehicleLabel(vehicle)), text)
|
||
end
|
||
end
|
||
-- gårdens eget behov (fabrikker/stalde) sælges ikke; kilderne selv tæller ikke som forbrugere
|
||
if ADSources.getFarmDemand ~= nil then
|
||
local demand = ADSources.getFarmDemand(adEnv, product.fillType, farmId, ADSources.getSourcePlaceables(sources))
|
||
product.reserve, product.reservedFor = demand.liters, demand.consumers
|
||
end
|
||
if quote == nil and unreachableName ~= nil then
|
||
warnOnce(adEnv, vehicle, "sell:" .. product.name, string.format("'%s' kan ikke nå salgssted '%s' for %s – tjek vejnettet", tostring(ADRuns.vehicleLabel(vehicle)), unreachableName, product.name))
|
||
else
|
||
clearWarning(vehicle, "sell:" .. product.name)
|
||
end
|
||
end
|
||
-- lagermål søges kun når kilden har noget af varen (ellers advarsler om æg før der er spawnet æg)
|
||
local available = 0
|
||
for _, source in ipairs(product.sources) do available = available + (source.available or 0) end
|
||
if wantsStore(product.policy) and available > 0 then
|
||
product.store = findStoreMarker(adEnv, vehicle, product, capacity, sources, nil, run)
|
||
end
|
||
end
|
||
local feedMode = ADRunsController ~= nil and ADRunsController.getStoreMarkers ~= nil and #ADRunsController.getStoreMarkers(adEnv, run) > 0
|
||
local input = {
|
||
feedMode = feedMode,
|
||
capacity = capacity,
|
||
hasAutoload = hasAutoload,
|
||
sellAtPercent = run.sellAtPercent,
|
||
storeAbovePercent = run.storeAbovePercent,
|
||
collectRound = run.collectRound == true,
|
||
products = products,
|
||
}
|
||
return input, sourcesById, sources
|
||
end
|
||
|
||
-- Mål for last der ALLEREDE er på vognen (ved Start efter genindlæsning/stop): salgssted hvis
|
||
-- prisen holder, ellers silo hvis politikken tillader lager. nil = intet mål (Start afvises).
|
||
function ADOutbound.planCargoTarget(adEnv, run, vehicle, fillType, liters)
|
||
local name = fillTypeName(fillType)
|
||
local policy = ADRuns.getProductPolicy(run, name)
|
||
if policy == "off" then
|
||
return nil
|
||
end
|
||
local farmId = ADSmartPickup.getFarmId(vehicle)
|
||
local capacity = getVehicleCapacity(adEnv, vehicle)
|
||
local sources = {}
|
||
for _, wayPointId in ipairs(run.sourceWayPointIds or {}) do
|
||
local source = ADSources.identify(adEnv, wayPointId, farmId)
|
||
if source ~= nil then table.insert(sources, source) end
|
||
end
|
||
local product = {fillType = fillType, name = name, policy = policy, isPallet = false, sources = {}}
|
||
if wantsSell(policy) then
|
||
local homeId = run.sourceWayPointIds ~= nil and run.sourceWayPointIds[1] or nil
|
||
local quote = findSellQuote(adEnv, vehicle, product, ADSources.getSellMarkers(adEnv, farmId), run.sellAtPercent, homeId)
|
||
if quote ~= nil and quote.percent >= (run.sellAtPercent or 90) then
|
||
return quote.wayPointId
|
||
end
|
||
end
|
||
if wantsStore(policy) then
|
||
local store = findStoreMarker(adEnv, vehicle, product, capacity, sources, liters or capacity, run)
|
||
if store ~= nil then
|
||
return store.wayPointId
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
-- ---- tur ----
|
||
|
||
local function copyList(list)
|
||
local copy = {}
|
||
for index, value in ipairs(list or {}) do copy[index] = value end
|
||
return copy
|
||
end
|
||
|
||
local function choiceFor(marker, source, trip)
|
||
return {
|
||
markerIndex = marker.markerIndex,
|
||
markerName = marker.name,
|
||
group = marker.group,
|
||
station = source ~= nil and source.station or nil,
|
||
fillTypes = trip ~= nil and copyList(trip.fillTypes) or {},
|
||
fillType = trip ~= nil and trip.fillTypes[1] or nil,
|
||
}
|
||
end
|
||
|
||
local function getMarker(adEnv, wayPointId)
|
||
return adEnv.ADGraphManager:getMapMarkerByWayPointId(wayPointId)
|
||
end
|
||
|
||
-- Ved et objektlager tager vi selv varerne én ad gangen; AD må ikke selv rotere varevalget
|
||
-- (det gør den kun med >1 valgt), så dér står kun turens første vare i AD's liste.
|
||
local function applyTrip(adEnv, vehicle, run, trip, sourcesById)
|
||
local stateModule = vehicle.ad.stateModule
|
||
local first = sourcesById[trip.sourceIds[1]]
|
||
local selected = copyList(trip.fillTypes)
|
||
if first ~= nil and first.kind == "objectStorage" then
|
||
selected = {trip.fillTypes[1]}
|
||
end
|
||
stateModule.selectedFillTypes = selected
|
||
stateModule:setFillType(trip.fillTypes[1])
|
||
if stateModule.setSecondMarkerByWayPointId ~= nil then
|
||
stateModule:setSecondMarkerByWayPointId(trip.targetWayPointId)
|
||
end
|
||
if stateModule.raiseDirtyFlag ~= nil then
|
||
stateModule:raiseDirtyFlag()
|
||
end
|
||
trips[vehicle] = {run = run, trip = trip, sources = sourcesById, queue = copyList(trip.sourceIds), visited = {}, current = trip.sourceIds[1], noProgressMs = 0}
|
||
local names = {}
|
||
for _, fillType in ipairs(trip.fillTypes) do table.insert(names, fillTypeName(fillType)) end
|
||
local sourceNames = {}
|
||
for _, id in ipairs(trip.sourceIds) do table.insert(sourceNames, sourcesById[id] ~= nil and sourcesById[id].name or tostring(id)) end
|
||
local why = trip.kind == "sell" and string.format("salg, %d %% af top", trip.percent or 0) or "lager"
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s' udkørsel: %s fra %s → %s (%s)", tostring(ADRuns.vehicleLabel(vehicle)), table.concat(names, "+"), table.concat(sourceNames, ", "), tostring(trip.targetName), why)
|
||
end
|
||
|
||
-- Lagermål i en tæt silorække: AutoDrive tipper ved den FØRSTE aflæsningstrigger inden for sin
|
||
-- globale "Max trigger distance" (standard 25 m) — nabosiloen kan så få læsset. Kan ikke sættes pr.
|
||
-- køretøj, så der advares én gang pr. mål, og brugeren sætter AD-indstillingen til 10 m.
|
||
local warnedTriggerDistance = {}
|
||
local function warnDenseSilosOnce(adEnv, vehicle, trip)
|
||
if trip.kind ~= "store" or adEnv.AutoDrive.getSetting == nil or warnedTriggerDistance[trip.targetWayPointId] then
|
||
return
|
||
end
|
||
local ok, distance = pcall(adEnv.AutoDrive.getSetting, "maxTriggerDistance")
|
||
if not ok or type(distance) ~= "number" or distance <= 10 then
|
||
return
|
||
end
|
||
local target = adEnv.ADGraphManager:getWayPointById(trip.targetWayPointId)
|
||
if target == nil then
|
||
return
|
||
end
|
||
local farmId = ADSmartPickup.getFarmId(vehicle)
|
||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||
if marker.id ~= trip.targetWayPointId then
|
||
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
||
local dx, dz = wayPoint ~= nil and (wayPoint.x - target.x) or math.huge, wayPoint ~= nil and (wayPoint.z - target.z) or math.huge
|
||
if dx * dx + dz * dz <= distance * distance and ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId) ~= nil then
|
||
warnedTriggerDistance[trip.targetWayPointId] = true
|
||
Logging.warning(ADOutbound.LOG_PREFIX .. "'%s': målet %s har en anden aflæsningsmarkør (%s) inden for AutoDrives 'Max trigger distance' (%d m) — AutoDrive kan tippe ved nabosiloen; sæt AD-indstillingen til 10 m", tostring(ADRuns.vehicleLabel(vehicle)), tostring(trip.targetName), tostring(marker.name), distance)
|
||
return
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
local function releaseHold(vehicle)
|
||
if holds[vehicle] ~= nil then
|
||
ADWaitPool.releaseVehicle(vehicle)
|
||
holds[vehicle] = nil
|
||
end
|
||
end
|
||
|
||
local function startHold(adEnv, vehicle, run, reasons, sourcesById)
|
||
local isReachable = function(candidate)
|
||
return ADWaitPool.hasRouteTo(adEnv, vehicle, candidate.marker.id)
|
||
end
|
||
local candidate = ADWaitPool.pickFree(adEnv, vehicle, run.waitPoolGroup, isReachable)
|
||
local marker, source = nil, nil
|
||
if candidate ~= nil then
|
||
ADWaitPool.claim(candidate.marker.id, vehicle)
|
||
marker = candidate.marker
|
||
clearWarning(vehicle, "noWait")
|
||
else
|
||
local firstId = run.sourceWayPointIds ~= nil and run.sourceWayPointIds[1] or nil
|
||
marker = firstId ~= nil and getMarker(adEnv, firstId) or nil
|
||
source = firstId ~= nil and sourcesById[firstId] or nil
|
||
if marker == nil then
|
||
return nil
|
||
end
|
||
warnOnce(adEnv, vehicle, "noWait", string.format("'%s' har intet ledigt Wait-punkt med vejnet (run '%s'); holder ved kilden %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(run.name), tostring(marker.name)))
|
||
end
|
||
local previous = holds[vehicle]
|
||
holds[vehicle] = {markerId = marker.id, markerName = tostring(marker.name), reasons = reasons, timerMs = 0}
|
||
local message = string.format("'%s' holder ved %s: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(marker.name), table.concat(reasons, "; "))
|
||
if previous == nil or previous.lastLogged ~= message then
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "%s", message)
|
||
end
|
||
holds[vehicle].lastLogged = message
|
||
return choiceFor(marker, source, nil)
|
||
end
|
||
|
||
-- Forsynings-traktor uden noget at hente: hold ved et Wait-punkt i run'ets pulje; replan() returnerer
|
||
-- waypoint-id for næste kilde (eller nil) og kaldes hvert CHECK_INTERVAL_MS.
|
||
function ADOutbound.holdSupplyVehicle(adEnv, vehicle, run, reasons, replan)
|
||
local choice = startHold(adEnv, vehicle, run, reasons, {})
|
||
if choice ~= nil and holds[vehicle] ~= nil then
|
||
holds[vehicle].replan = replan
|
||
end
|
||
return choice
|
||
end
|
||
|
||
-- Hook 1: AutoDrive spørger om næste pålæsning. Tur -> kilden; ingen tur -> Wait-punkt.
|
||
function ADOutbound.choosePickup(adEnv, vehicle)
|
||
local run = getRun(vehicle)
|
||
if run == nil or run.kind ~= "outbound" then
|
||
return nil
|
||
end
|
||
trips[vehicle] = nil
|
||
if ADRunsController.resolveMarkers ~= nil then
|
||
pcall(ADRunsController.resolveMarkers, adEnv, run)
|
||
end
|
||
local input, sourcesById = ADOutbound.buildPlanInput(adEnv, run, vehicle)
|
||
local trip, reasons = ADOutboundPlanner.plan(input)
|
||
local runStatus = getStatus(run)
|
||
runStatus.reasons = reasons
|
||
for _, reason in ipairs(reasons) do
|
||
if string.find(reason, ": intet lager med plads", 1, true) then
|
||
warnOnce(adEnv, vehicle, "store:" .. reason, string.format("'%s' finder intet lager med plads (%s)", tostring(ADRuns.vehicleLabel(vehicle)), reason))
|
||
end
|
||
end
|
||
if trip == nil then
|
||
return startHold(adEnv, vehicle, run, reasons, sourcesById)
|
||
end
|
||
releaseHold(vehicle)
|
||
runStatus.warning = nil
|
||
applyTrip(adEnv, vehicle, run, trip, sourcesById)
|
||
pcall(warnDenseSilosOnce, adEnv, vehicle, trip)
|
||
-- Prisgrundlaget logges, så det kan holdes op mod spillets Priser-menu (I5 i review).
|
||
if trip.kind == "sell" then
|
||
for _, product in ipairs(input.products) do
|
||
if product.fillType == trip.fillTypes[1] and product.quote ~= nil then
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s' pris %s: %.0f kr/1000 l ved %s, årets top %.0f -> %d %% (kræver %d)", tostring(ADRuns.vehicleLabel(vehicle)), product.name, (product.quote.price or 0) * 1000, tostring(product.quote.stationName), (product.quote.top or 0) * 1000, product.quote.percent or 0, run.sellAtPercent or 90)
|
||
end
|
||
end
|
||
end
|
||
local marker = getMarker(adEnv, trip.sourceIds[1])
|
||
if marker == nil then
|
||
return nil
|
||
end
|
||
return choiceFor(marker, sourcesById[trip.sourceIds[1]], trip)
|
||
end
|
||
|
||
-- ---- Hook 2: læsseopgaven ----
|
||
|
||
local function setActivePickup(vehicle, choice)
|
||
if ADSmartPickup ~= nil and ADSmartPickup.setActivePickup ~= nil then
|
||
ADSmartPickup.setActivePickup(vehicle, choice)
|
||
end
|
||
end
|
||
|
||
local function goToSource(adEnv, state, task, wayPointId)
|
||
state.current = wayPointId
|
||
state.noProgressMs, state.lastFill, state.spawn, state.done, state.completed = 0, nil, nil, false, false
|
||
ADWaitPool.redirect(task, wayPointId)
|
||
task.activatedUALLoading = false
|
||
task.spuCargoChecked = nil
|
||
local marker = getMarker(adEnv, wayPointId)
|
||
if marker ~= nil then
|
||
setActivePickup(task.vehicle, choiceFor(marker, state.sources[wayPointId], state.trip))
|
||
end
|
||
end
|
||
|
||
local function updateHold(adEnv, task, hold, dt)
|
||
local vehicle = task.vehicle
|
||
if task.destinationID ~= hold.markerId or not vehicle.ad.drivePathModule:isTargetReached() then
|
||
return false
|
||
end
|
||
vehicle.ad.specialDrivingModule:stopVehicle()
|
||
vehicle.ad.specialDrivingModule:update(dt)
|
||
hold.timerMs = hold.timerMs + (dt or 0)
|
||
if hold.timerMs < ADOutbound.CHECK_INTERVAL_MS then
|
||
return true
|
||
end
|
||
hold.timerMs = 0
|
||
-- forsynings-traktor: egen genplanlægning (ADSmartPickup) giver en kilde-markør at køre til
|
||
if hold.replan ~= nil then
|
||
local ok, targetId = pcall(hold.replan)
|
||
if ok and targetId ~= nil then
|
||
releaseHold(vehicle)
|
||
ADWaitPool.redirect(task, targetId)
|
||
task.activatedUALLoading = false
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s' kører fra Wait: der er noget at hente igen", tostring(ADRuns.vehicleLabel(vehicle)))
|
||
end
|
||
return true
|
||
end
|
||
ADOutbound.choosePickup(adEnv, vehicle)
|
||
local state = trips[vehicle]
|
||
if state ~= nil then
|
||
goToSource(adEnv, state, task, state.current)
|
||
return true
|
||
end
|
||
-- stadig hold, men ved en anden markør (et Wait-punkt blev ledigt): kør derhen
|
||
local newHold = holds[vehicle]
|
||
if newHold ~= nil and newHold.markerId ~= task.destinationID then
|
||
ADWaitPool.redirect(task, newHold.markerId)
|
||
task.activatedUALLoading = false
|
||
end
|
||
return true
|
||
end
|
||
|
||
local function countLoaded(trailers)
|
||
local count = 0
|
||
for _, trailer in ipairs(trailers or {}) do
|
||
local spec = trailer.spec_universalAutoload
|
||
for _ in pairs(spec ~= nil and spec.loadedObjects or {}) do
|
||
count = count + 1
|
||
end
|
||
end
|
||
return count
|
||
end
|
||
|
||
local function isAutoloadFull(trailers)
|
||
for _, trailer in ipairs(trailers or {}) do
|
||
if trailer.ualIsFull ~= nil and trailer:ualIsFull() then
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Objektlager: én palle ad gangen — spawn, vent til lageret er én mindre OG vognen har én
|
||
-- mere, gentag; skift vare når lageret er tomt for den aktuelle; stop ved fuld vogn.
|
||
local function updateSpawn(adEnv, state, task, source, dt)
|
||
local vehicle = task.vehicle
|
||
-- AD aktiverede selv UAL med turens første vare; kun ved vareskift genaktiveres den.
|
||
local spawn = state.spawn or {typeIndex = 1, activeType = state.trip.fillTypes[1]}
|
||
state.spawn = spawn
|
||
if not task.activatedUALLoading or (vehicle.lastSpeedReal or 0) >= ADOutbound.MAX_SPEED_FOR_LOADING then
|
||
return
|
||
end
|
||
local fillType = state.trip.fillTypes[spawn.typeIndex]
|
||
if spawn.pending ~= nil then
|
||
spawn.pending.ms = spawn.pending.ms + (dt or 0)
|
||
local stored, loaded = ADSources.countStored(source, fillType), countLoaded(task.trailers)
|
||
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
||
if stored < spawn.pending.stored and (loaded > spawn.pending.loaded or level > spawn.pending.level) then
|
||
spawn.pending = nil
|
||
elseif spawn.pending.ms > ADOutbound.SPAWN_STALL_MS then
|
||
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': objektlageret %s spawner ikke %s (30 s uden fremdrift); kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source.name), fillTypeName(fillType)))
|
||
state.done = true
|
||
return
|
||
else
|
||
return
|
||
end
|
||
end
|
||
if isAutoloadFull(task.trailers) or fillType == nil then
|
||
state.done = true
|
||
return
|
||
end
|
||
local index = ADSources.getObjectInfoIndex(source, fillType)
|
||
if index == nil then
|
||
spawn.typeIndex = spawn.typeIndex + 1
|
||
return
|
||
end
|
||
if spawn.activeType ~= fillType then
|
||
spawn.activeType = fillType
|
||
vehicle.ad.stateModule:setFillType(fillType)
|
||
if adEnv.AutoDrive.activateALTrailers ~= nil then
|
||
adEnv.AutoDrive.activateALTrailers(vehicle, task.trailers)
|
||
end
|
||
end
|
||
if ADSources.spawnOne(source, index) then
|
||
spawn.pending = {stored = ADSources.countStored(source, fillType), loaded = countLoaded(task.trailers), level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0, ms = 0}
|
||
else
|
||
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': kunne ikke tage %s ud af %s; kører med det der er", tostring(ADRuns.vehicleLabel(vehicle)), fillTypeName(fillType), tostring(source.name)))
|
||
state.done = true
|
||
end
|
||
end
|
||
|
||
-- Diagnose når intet bliver læsset: markørens navn, kildens mængde af turens varer og afstanden fra
|
||
-- vognen til nærmeste læssetrigger der udleverer varen (> ~10 m = vognen holder uden for triggeren).
|
||
local function describeLoadFailure(adEnv, vehicle, task, state, source)
|
||
local parts = {}
|
||
local marker = adEnv.ADGraphManager.getMapMarkerByWayPointId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(state.current) or nil
|
||
table.insert(parts, string.format("markør '%s' (%s)", marker ~= nil and tostring(marker.name) or "?", tostring(state.current)))
|
||
local ok, products = pcall(ADSources.getProducts, adEnv, source, g_currentMission:getFarmId())
|
||
local wanted = {}
|
||
for _, fillType in ipairs(state.trip.fillTypes) do wanted[fillType] = true end
|
||
for _, item in ipairs(ok and products or {}) do
|
||
if wanted[item.fillType] then
|
||
table.insert(parts, string.format("%s i kilden %.0f l", fillTypeName(item.fillType), item.available or item.level or 0))
|
||
end
|
||
end
|
||
local trailer = task.trailers ~= nil and task.trailers[1] or vehicle
|
||
local station = source ~= nil and source.station or nil
|
||
if trailer ~= nil and trailer.rootNode ~= nil and station ~= nil and station.loadTriggers ~= nil then
|
||
local tx, _, tz = getWorldTranslation(trailer.rootNode)
|
||
local best = nil
|
||
for _, trigger in pairs(station.loadTriggers) do
|
||
local supplies = type(trigger.fillTypes) ~= "table" or next(trigger.fillTypes) == nil
|
||
for fillType in pairs(wanted) do
|
||
if type(trigger.fillTypes) == "table" and trigger.fillTypes[fillType] then supplies = true end
|
||
end
|
||
if supplies and trigger.triggerNode ~= nil then
|
||
local x, _, z = getWorldTranslation(trigger.triggerNode)
|
||
local d = math.sqrt((x - tx) ^ 2 + (z - tz) ^ 2)
|
||
if best == nil or d < best then best = d end
|
||
end
|
||
end
|
||
table.insert(parts, best ~= nil and string.format("vogn %.0f m fra triggeren", best) or "ingen trigger med varen")
|
||
end
|
||
local selected = vehicle.ad ~= nil and vehicle.ad.stateModule ~= nil and vehicle.ad.stateModule.getFillType ~= nil and vehicle.ad.stateModule:getFillType() or nil
|
||
table.insert(parts, "AD-vare " .. fillTypeName(selected))
|
||
return table.concat(parts, ", ")
|
||
end
|
||
|
||
-- true = håndteret (original update springes over: traktoren holder ved Wait).
|
||
function ADOutbound.beforeLoadUpdate(adEnv, task, dt)
|
||
local vehicle = task.vehicle
|
||
local hold = holds[vehicle]
|
||
if hold ~= nil then
|
||
return updateHold(adEnv, task, hold, dt)
|
||
end
|
||
local state = trips[vehicle]
|
||
if state == nil or state.completed or task.destinationID ~= state.current or not vehicle.ad.drivePathModule:isTargetReached() then
|
||
return false
|
||
end
|
||
local source = state.sources[state.current]
|
||
if source ~= nil and source.kind == "objectStorage" then
|
||
updateSpawn(adEnv, state, task, source, dt)
|
||
if state.done then
|
||
state.done, state.completed = false, true
|
||
task:finished()
|
||
return true
|
||
end
|
||
end
|
||
local level = adEnv.AutoDrive.getAllFillLevels(task.trailers) or 0
|
||
if state.lastFill == nil or level > state.lastFill then
|
||
state.lastFill, state.noProgressMs = level, 0
|
||
else
|
||
state.noProgressMs = state.noProgressMs + (dt or 0)
|
||
end
|
||
local spawnPending = state.spawn ~= nil and state.spawn.pending ~= nil
|
||
if level <= 0 and state.noProgressMs > ADOutbound.EMPTY_SOURCE_MS and not spawnPending then
|
||
local expires = now() + ADOutbound.EMPTY_SOURCE_TTL_MS
|
||
for _, fillType in ipairs(state.trip.fillTypes) do
|
||
emptyUntil[tostring(state.current) .. ":" .. tostring(fillType)] = expires
|
||
end
|
||
local okDiag, diagnosis = pcall(describeLoadFailure, adEnv, vehicle, task, state, source)
|
||
Logging.warning(ADOutbound.LOG_PREFIX .. "'%s': intet at læsse ved %s efter %d s; kilden springes over i %d min (%s)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source ~= nil and source.name or state.current), math.floor(ADOutbound.EMPTY_SOURCE_MS / 1000), math.floor(ADOutbound.EMPTY_SOURCE_TTL_MS / 60000), okDiag and diagnosis or tostring(diagnosis))
|
||
state.noProgressMs, state.completed = 0, true
|
||
task:finished()
|
||
return true
|
||
end
|
||
if state.noProgressMs > ADOutbound.NO_PROGRESS_MS and level > 0 and not spawnPending then
|
||
local rests = now() + ADOutbound.THIN_SOURCE_REST_MS
|
||
for _, fillType in ipairs(state.trip.fillTypes) do
|
||
local key = tostring(state.current) .. ":" .. tostring(fillType)
|
||
if emptyUntil[key] == nil or emptyUntil[key] < rests then emptyUntil[key] = rests end
|
||
end
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s': ingen mere last ved %s; kører med %.0f l (kilden hviler %d min, så de andre kommer til)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(source ~= nil and source.name or state.current), level, math.floor(ADOutbound.THIN_SOURCE_REST_MS / 60000))
|
||
state.noProgressMs, state.completed = 0, true
|
||
task:finished()
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
|
||
local function distanceToWayPoint(adEnv, vehicle, wayPointId)
|
||
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
|
||
if wayPoint == nil or vehicle.rootNode == nil then
|
||
return nil
|
||
end
|
||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||
local dx, dz = wayPoint.x - x, wayPoint.z - z
|
||
return math.sqrt(dx * dx + dz * dz)
|
||
end
|
||
|
||
-- Aflæsningskørsel (UnloadAtDestinationTask.update): autoload-bil med tur -> stropper af
|
||
-- UNSTRAP_BEFORE_M før målet, én gang pr. tur. Ændrer aldrig opgaven (returnerer altid false).
|
||
function ADOutbound.beforeUnloadUpdate(adEnv, task, dt)
|
||
local vehicle = task.vehicle
|
||
local state = trips[vehicle]
|
||
if state == nil or state.unstrapped or task.destinationID ~= state.trip.targetWayPointId then
|
||
return false
|
||
end
|
||
local _, hasAutoload = getVehicleCapacity(adEnv, vehicle)
|
||
if not hasAutoload then
|
||
state.unstrapped = true
|
||
return false
|
||
end
|
||
local distance = distanceToWayPoint(adEnv, vehicle, task.destinationID)
|
||
if distance == nil or distance > ADOutbound.UNSTRAP_BEFORE_M then
|
||
return false
|
||
end
|
||
state.unstrapped = true
|
||
for _, unit in ipairs(task.trailers or {}) do
|
||
if unit.setAllTensionBeltsActive ~= nil then
|
||
pcall(unit.setAllTensionBeltsActive, unit, false, false)
|
||
end
|
||
end
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s': stropper løsnet %.0f m før %s", tostring(ADRuns.vehicleLabel(vehicle)), distance, tostring(state.trip.targetName))
|
||
return false
|
||
end
|
||
|
||
-- Efter pålæsning: autoload-bil med tur får stropperne spændt, så lasten ikke tabes på vejen
|
||
-- (AutoDrive/UAL sætter dem ikke selv på igen efter vores løsning før forrige aflæsning).
|
||
function ADOutbound.strapAfterLoading(adEnv, task)
|
||
local vehicle = task.vehicle
|
||
local state = trips[vehicle]
|
||
if state == nil then
|
||
return
|
||
end
|
||
local _, hasAutoload = getVehicleCapacity(adEnv, vehicle)
|
||
if not hasAutoload then
|
||
return
|
||
end
|
||
local strapped = 0
|
||
for _, unit in ipairs(task.trailers or {}) do
|
||
if unit.setAllTensionBeltsActive ~= nil then
|
||
pcall(unit.setAllTensionBeltsActive, unit, true, false)
|
||
strapped = strapped + 1
|
||
end
|
||
end
|
||
state.unstrapped = false
|
||
if strapped > 0 then
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s': stropper spændt efter pålæsning", tostring(ADRuns.vehicleLabel(vehicle)))
|
||
end
|
||
end
|
||
|
||
-- true = opsamlingsrunden fortsætter til næste kilde (original finished springes over).
|
||
function ADOutbound.beforeLoadFinished(adEnv, task)
|
||
local vehicle = task.vehicle
|
||
local state = trips[vehicle]
|
||
if state == nil or task.destinationID ~= state.current then
|
||
return false
|
||
end
|
||
state.visited[state.current] = true
|
||
local level, capacity, filledToUnload, free = adEnv.AutoDrive.getAllFillLevels(task.trailers)
|
||
if filledToUnload or capacity == nil or capacity <= 0 or (free or 0) / capacity < ADOutboundPlanner.ROUND_MIN_FREE_FRACTION or isAutoloadFull(task.trailers) then
|
||
return false
|
||
end
|
||
for _, wayPointId in ipairs(state.queue) do
|
||
if not state.visited[wayPointId] and state.sources[wayPointId] ~= nil then
|
||
Logging.info(ADOutbound.LOG_PREFIX .. "'%s' runde: %.0f l på vognen, videre til %s", tostring(ADRuns.vehicleLabel(vehicle)), level or 0, tostring(state.sources[wayPointId].name))
|
||
goToSource(adEnv, state, task, wayPointId)
|
||
return true
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- ---- oprydning og opslag ----
|
||
|
||
local function isActive(vehicle)
|
||
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||
return stateModule ~= nil and stateModule.isActive ~= nil and stateModule:isActive()
|
||
end
|
||
|
||
function ADOutbound.releaseInactive()
|
||
for vehicle in pairs(holds) do
|
||
if not isActive(vehicle) then
|
||
releaseHold(vehicle)
|
||
end
|
||
end
|
||
for vehicle in pairs(trips) do
|
||
if not isActive(vehicle) then
|
||
trips[vehicle] = nil
|
||
end
|
||
end
|
||
end
|
||
|
||
function ADOutbound.getHoldFor(vehicle)
|
||
local hold = holds[vehicle]
|
||
if hold == nil then
|
||
return nil
|
||
end
|
||
return {markerName = hold.markerName, reasons = hold.reasons}
|
||
end
|
||
|
||
function ADOutbound.getTripFor(vehicle)
|
||
local state = trips[vehicle]
|
||
return state ~= nil and state.trip or nil
|
||
end
|
||
|
||
function ADOutbound.getRunStatus(runId)
|
||
return status[runId] or {reasons = {}}
|
||
end
|
||
|
||
function ADOutbound.install(adEnv)
|
||
local task = adEnv.LoadAtDestinationTask
|
||
if isInstalled or task == nil or task.update == nil then
|
||
return
|
||
end
|
||
isInstalled = true
|
||
ADWaitPool.STATE_DRIVING = task.STATE_DRIVING or ADWaitPool.STATE_DRIVING
|
||
local originalUpdate, originalFinished = task.update, task.finished
|
||
|
||
task.update = function(self, dt)
|
||
local ok, handled = pcall(ADOutbound.beforeLoadUpdate, adEnv, self, dt)
|
||
if not ok then
|
||
Logging.warning(ADOutbound.LOG_PREFIX .. "fejl i udkørsel, AutoDrive fortsætter selv: %s", tostring(handled))
|
||
releaseHold(self.vehicle)
|
||
trips[self.vehicle] = nil
|
||
elseif handled then
|
||
return
|
||
end
|
||
return originalUpdate(self, dt)
|
||
end
|
||
|
||
if originalFinished ~= nil then
|
||
task.finished = function(self)
|
||
local ok, continueRound = pcall(ADOutbound.beforeLoadFinished, adEnv, self)
|
||
if not ok then
|
||
Logging.warning(ADOutbound.LOG_PREFIX .. "fejl i opsamlingsrunde, AutoDrive fortsætter selv: %s", tostring(continueRound))
|
||
elseif continueRound then
|
||
return
|
||
end
|
||
local result = originalFinished(self)
|
||
-- autoload-bil kører nu fra kilden: stropperne SKAL på (de løsnes 25 m før aflæsningen)
|
||
pcall(ADOutbound.strapAfterLoading, adEnv, self)
|
||
return result
|
||
end
|
||
end
|
||
|
||
local unloadTask = adEnv.UnloadAtDestinationTask
|
||
if unloadTask ~= nil and unloadTask.update ~= nil then
|
||
local originalUnloadUpdate = unloadTask.update
|
||
unloadTask.update = function(self, dt)
|
||
local okUnload, err = pcall(ADOutbound.beforeUnloadUpdate, adEnv, self, dt)
|
||
if not okUnload then
|
||
Logging.warning(ADOutbound.LOG_PREFIX .. "fejl ved strop-løsning, AutoDrive fortsætter selv: %s", tostring(err))
|
||
end
|
||
return originalUnloadUpdate(self, dt)
|
||
end
|
||
end
|
||
end
|