feat: adOutbound — udkørsels-hooks: turvalg, hold ved Wait, objektlager-udtag, opsamlingsrunde, advarsler på skærm
This commit is contained in:
@@ -0,0 +1,548 @@
|
|||||||
|
-- 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
|
||||||
|
|
||||||
|
-- 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"})
|
||||||
|
local isInstalled = false
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
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 NÅELIGE salgssted for varen; unreachableName = navnet på det bedste der ikke kan nås.
|
||||||
|
local function findSellQuote(adEnv, vehicle, product, farmId)
|
||||||
|
local quotes, unreachableName = {}, nil
|
||||||
|
local homeId = product.sources[1] ~= nil and product.sources[1].wayPointId or nil
|
||||||
|
for _, entry in ipairs(ADSources.getSellMarkers(adEnv, farmId)) do
|
||||||
|
if ADSources.stationAccepts(entry.station, product.fillType) then
|
||||||
|
local quote = ADPrices.getQuote(entry.station, product.fillType)
|
||||||
|
if quote ~= nil then
|
||||||
|
local reachable = homeId == nil or ADWaitPool.hasRouteBothWays(adEnv, vehicle, entry.marker.id, homeId)
|
||||||
|
if reachable then
|
||||||
|
table.insert(quotes, {percent = quote.percent, price = quote.price, top = quote.top, wayPointId = entry.marker.id, stationName = tostring(entry.marker.name)})
|
||||||
|
elseif unreachableName == nil then
|
||||||
|
unreachableName = tostring(entry.marker.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ADPrices.bestQuote(quotes), unreachableName
|
||||||
|
end
|
||||||
|
|
||||||
|
local function findStoreMarker(adEnv, vehicle, product, capacity, sources)
|
||||||
|
if product.isPallet or ADLoadSwap == nil or ADLoadSwap.findDumpMarker == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local available = 0
|
||||||
|
for _, source in ipairs(product.sources) do
|
||||||
|
available = available + (source.available or 0)
|
||||||
|
end
|
||||||
|
local liters = math.max(1, math.min(available, capacity or available))
|
||||||
|
local isReachable = function(markerId)
|
||||||
|
return ADWaitPool.hasRouteTo(adEnv, vehicle, markerId)
|
||||||
|
end
|
||||||
|
local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, product.fillType, liters, nil, isReachable, ADSources.getSourcePlaceables(sources))
|
||||||
|
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
|
||||||
|
warnOnce(adEnv, vehicle, "source:" .. tostring(wayPointId), string.format("run '%s': kilde-markør %s er ikke en fabrik, silo eller objektlager (inden for 40 m)", tostring(run.name), tostring(wayPointId)))
|
||||||
|
else
|
||||||
|
clearWarning(vehicle, "source:" .. tostring(wayPointId))
|
||||||
|
table.insert(sources, source)
|
||||||
|
sourcesById[wayPointId] = source
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local byName, products = {}, {}
|
||||||
|
for _, source in ipairs(sources) do
|
||||||
|
for _, item in ipairs(ADSources.getProducts(adEnv, source, farmId)) do
|
||||||
|
local policy = ADRuns.getProductPolicy(run, item.name)
|
||||||
|
local canCarry = (item.isPallet and hasAutoload) or (not item.isPallet and carriable[item.fillType] == true)
|
||||||
|
if policy ~= "off" and canCarry then
|
||||||
|
local product = byName[item.name]
|
||||||
|
if product == nil then
|
||||||
|
product = {fillType = item.fillType, name = item.name, policy = policy, isPallet = item.isPallet, sources = {}}
|
||||||
|
byName[item.name] = product
|
||||||
|
table.insert(products, product)
|
||||||
|
end
|
||||||
|
table.insert(product.sources, {wayPointId = source.wayPointId, available = item.available, level = item.level, capacity = item.capacity})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(products, function(a, b) return a.name < b.name end)
|
||||||
|
for _, product in ipairs(products) do
|
||||||
|
if wantsSell(product.policy) then
|
||||||
|
local quote, unreachableName = findSellQuote(adEnv, vehicle, product, farmId)
|
||||||
|
product.quote = quote
|
||||||
|
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(vehicle:getName()), unreachableName, product.name))
|
||||||
|
else
|
||||||
|
clearWarning(vehicle, "sell:" .. product.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if wantsStore(product.policy) then
|
||||||
|
product.store = findStoreMarker(adEnv, vehicle, product, capacity, sources)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local input = {
|
||||||
|
capacity = capacity,
|
||||||
|
hasAutoload = hasAutoload,
|
||||||
|
sellAtPercent = run.sellAtPercent,
|
||||||
|
storeAbovePercent = run.storeAbovePercent,
|
||||||
|
collectRound = run.collectRound == true,
|
||||||
|
products = products,
|
||||||
|
}
|
||||||
|
return input, sourcesById, sources
|
||||||
|
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(vehicle:getName()), table.concat(names, "+"), table.concat(sourceNames, ", "), tostring(trip.targetName), why)
|
||||||
|
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(vehicle:getName()), 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(vehicle:getName()), 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
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
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, ": ingen silo med plads", 1, true) then
|
||||||
|
warnOnce(adEnv, vehicle, "store:" .. reason, string.format("'%s' finder ingen silo med plads (%s)", tostring(vehicle:getName()), reason))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if trip == nil then
|
||||||
|
return startHold(adEnv, vehicle, run, reasons, sourcesById)
|
||||||
|
end
|
||||||
|
releaseHold(vehicle)
|
||||||
|
runStatus.warning = nil
|
||||||
|
warned[vehicle] = nil
|
||||||
|
applyTrip(adEnv, vehicle, run, trip, sourcesById)
|
||||||
|
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
|
||||||
|
ADOutbound.choosePickup(adEnv, vehicle)
|
||||||
|
local state = trips[vehicle]
|
||||||
|
if state ~= nil then
|
||||||
|
goToSource(adEnv, state, task, state.current)
|
||||||
|
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)
|
||||||
|
if stored < spawn.pending.stored and loaded > spawn.pending.loaded 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(vehicle:getName()), 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), ms = 0}
|
||||||
|
else
|
||||||
|
warnOnce(adEnv, vehicle, "spawn", string.format("'%s': kunne ikke tage %s ud af %s; kører med det der er", tostring(vehicle:getName()), fillTypeName(fillType), tostring(source.name)))
|
||||||
|
state.done = true
|
||||||
|
end
|
||||||
|
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 state.noProgressMs > ADOutbound.NO_PROGRESS_MS and level > 0 and not spawnPending then
|
||||||
|
Logging.info(ADOutbound.LOG_PREFIX .. "'%s': ingen mere last ved %s; kører med %.0f l", tostring(vehicle:getName()), tostring(source ~= nil and source.name or state.current), level)
|
||||||
|
state.noProgressMs, state.completed = 0, true
|
||||||
|
task:finished()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
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(vehicle:getName()), 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
|
||||||
|
return originalFinished(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
-- Kør: luajit tests/test_adOutbound.lua (fra repo-roden)
|
||||||
|
-- Udkørsels-hooks: turvalg + AD-tilstand, hold ved Wait, objektlager-udtag, opsamlingsrunde,
|
||||||
|
-- advarsler på skærmen. Fake AD-miljø; adSources/adPrices/adLoadSwap er fakes, adWaitPool og
|
||||||
|
-- adOutboundPlanner er de rigtige.
|
||||||
|
|
||||||
|
local SUGAR, FORAGE, BASKET, CRATE = 5, 6, 7, 8
|
||||||
|
local NAMES = {[SUGAR] = "SUGAR", [FORAGE] = "FORAGE", [BASKET] = "STRAW_BASKET", [CRATE] = "STRAW_CRATE"}
|
||||||
|
g_fillTypeManager = {
|
||||||
|
getFillTypeNameByIndex = function(_, i) return NAMES[i] end,
|
||||||
|
getFillTypeByIndex = function(_, i) return {name = NAMES[i], palletFilename = (i == BASKET or i == CRATE) and "p.xml" or nil} end,
|
||||||
|
}
|
||||||
|
local logLines = {}
|
||||||
|
Logging = {
|
||||||
|
info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end,
|
||||||
|
warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end,
|
||||||
|
}
|
||||||
|
getWorldTranslation = function(node) return node.x, 0, node.z end
|
||||||
|
g_currentMission = {getFarmId = function() return 1 end}
|
||||||
|
|
||||||
|
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
||||||
|
dofile("FS25_ADSmartPickup/adRuns.lua")
|
||||||
|
dofile("FS25_ADSmartPickup/adWaitPool.lua")
|
||||||
|
dofile("FS25_ADSmartPickup/adOutboundPlanner.lua")
|
||||||
|
dofile("FS25_ADSmartPickup/adOutbound.lua")
|
||||||
|
|
||||||
|
local failures = 0
|
||||||
|
local function check(name, actual, expected)
|
||||||
|
if actual == expected then
|
||||||
|
print("OK " .. name)
|
||||||
|
else
|
||||||
|
failures = failures + 1
|
||||||
|
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function lastLog(pattern)
|
||||||
|
for i = #logLines, 1, -1 do
|
||||||
|
if string.find(logLines[i], pattern, 1, true) then return logLines[i] end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local function countLog(pattern)
|
||||||
|
local n = 0
|
||||||
|
for _, line in ipairs(logLines) do if string.find(line, pattern, 1, true) then n = n + 1 end end
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- AD-miljø ----
|
||||||
|
local markers, wayPoints, unreachable = {}, {}, {}
|
||||||
|
local function addMarker(id, name, group, x, z)
|
||||||
|
wayPoints[id] = {id = id, x = x, z = z}
|
||||||
|
table.insert(markers, {id = id, markerIndex = #markers + 1, name = name, group = group})
|
||||||
|
return markers[#markers]
|
||||||
|
end
|
||||||
|
local notifications = {}
|
||||||
|
local RT_NONE, RT_ONLYPICKUP = 1, 2
|
||||||
|
local adEnv = {
|
||||||
|
AutoDrive = {
|
||||||
|
RT_NONE = RT_NONE, RT_ONLYPICKUP = RT_ONLYPICKUP,
|
||||||
|
getAllUnits = function(vehicle) return vehicle.trailers end,
|
||||||
|
getAllFillLevels = function(trailers) return trailers.level, trailers.capacity, trailers.level >= trailers.capacity * 0.99, trailers.capacity - trailers.level end,
|
||||||
|
getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable end,
|
||||||
|
hasAL = function(_, trailer) return trailer.ual ~= nil end,
|
||||||
|
activateALTrailers = function(vehicle, trailers) trailers.activated = (trailers.activated or 0) + 1 end,
|
||||||
|
},
|
||||||
|
ADGraphManager = {
|
||||||
|
getMapMarkers = function() return markers end,
|
||||||
|
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
|
||||||
|
getWayPointById = function(_, id) return wayPoints[id] end,
|
||||||
|
getPathTo = function(_, vehicle, id) if unreachable[id] then return {} end return {{id = id}} end,
|
||||||
|
pathFromTo = function(_, from, to) if unreachable[to] then return {} end return {{id = to}} end,
|
||||||
|
},
|
||||||
|
AutoDriveMessageEvent = {sendMessageOrNotification = function(vehicle, kind, text, duration, ...) table.insert(notifications, {vehicle = vehicle, kind = kind, text = string.format(text, ...)}) end},
|
||||||
|
ADMessagesManager = {messageTypes = {INFO = 1, WARN = 2, ERROR = 3}},
|
||||||
|
LoadAtDestinationTask = {STATE_DRIVING = 2, update = function() end, finished = function(self) self.finishedCalls = (self.finishedCalls or 0) + 1 end},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function newVehicle(name, x, z, capacity, ual)
|
||||||
|
local v = {name = name, rootNode = {x = x, z = z}, lastSpeedReal = 0, carriable = {SUGAR, FORAGE}, farmId = 1}
|
||||||
|
v.getName = function() return name end
|
||||||
|
v.trailers = {level = 0, capacity = capacity, ual = ual and {spec = {loadedObjects = {}}} or nil}
|
||||||
|
v.trailers[1] = {spec_universalAutoload = ual and v.trailers.ual.spec or nil, ualIsFull = ual and function() return v.trailers.full == true end or nil, ual = ual and true or nil}
|
||||||
|
v.ad = {
|
||||||
|
stateModule = {isActive = function() return v.active ~= false end, selectedFillTypes = {}, fillType = nil,
|
||||||
|
setFillType = function(self, ft) self.fillType = ft end, getFillType = function(self) return self.fillType end,
|
||||||
|
setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end,
|
||||||
|
getSecondMarker = function(self) return self.secondMarkerId and {id = self.secondMarkerId} or nil end,
|
||||||
|
raiseDirtyFlag = function() end},
|
||||||
|
drivePathModule = {isTargetReached = function() return v.atTarget == true end, setPathTo = function(_, id) v.pathTo = id end},
|
||||||
|
specialDrivingModule = {stopVehicle = function() v.stopped = true end, update = function() end},
|
||||||
|
trailerModule = {isActiveAtTrigger = function() return false end},
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
local function newTask(vehicle, destinationID)
|
||||||
|
return {vehicle = vehicle, destinationID = destinationID, trailers = vehicle.trailers, state = 2, activatedUALLoading = false,
|
||||||
|
finished = function(self) return adEnv.LoadAtDestinationTask.finished(self) end}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- kilder, priser, siloer som fakes
|
||||||
|
local sources, products, quotes, dumpMarker = {}, {}, {}, nil
|
||||||
|
ADSources = {
|
||||||
|
identify = function(_, id) return sources[id] end,
|
||||||
|
getProducts = function(_, source) return products[source.wayPointId] or {} end,
|
||||||
|
getSellMarkers = function() local r = {} for _, m in ipairs(markers) do if m.group == "selling" then table.insert(r, {marker = m, station = {name = m.name}}) end end return r end,
|
||||||
|
stationAccepts = function(station, ft) return quotes[station.name] ~= nil and quotes[station.name][ft] ~= nil end,
|
||||||
|
getSourcePlaceables = function() return {} end,
|
||||||
|
getObjectInfoIndex = function(source, ft) local s = source.stored or {} return s[ft] ~= nil and s[ft] > 0 and 1 or nil, s[ft] end,
|
||||||
|
countStored = function(source, ft) return (source.stored or {})[ft] or 0 end,
|
||||||
|
spawnOne = function(source, index) source.spawnCalls = (source.spawnCalls or 0) + 1 return source.spawnFails ~= true end,
|
||||||
|
}
|
||||||
|
ADPrices = {
|
||||||
|
getQuote = function(station, ft) return quotes[station.name] and quotes[station.name][ft] end,
|
||||||
|
bestQuote = function(list) local best for _, q in ipairs(list) do if best == nil or q.percent > best.percent then best = q end end return best end,
|
||||||
|
}
|
||||||
|
ADLoadSwap = {findDumpMarker = function() return dumpMarker, "ingen silo" end}
|
||||||
|
local runs = {}
|
||||||
|
ADRunsController = {getRunForVehicle = function(vehicle) return runs[vehicle] end}
|
||||||
|
local activePickupCalls = {}
|
||||||
|
ADSmartPickup = {getFarmId = function(v) return v.farmId end, setActivePickup = function(vehicle, choice) table.insert(activePickupCalls, choice) end}
|
||||||
|
|
||||||
|
local mSugar = addMarker(1, "Suger Load", "Pickup points", 0, 0)
|
||||||
|
local mSilo = addMarker(2, "silo 4", "silo", 50, 0)
|
||||||
|
local mObj = addMarker(3, "Objektlager", "Pickup points", 100, 0)
|
||||||
|
local mSell = addMarker(10, "selling station", "selling", 200, 0)
|
||||||
|
local mSell2 = addMarker(11, "Far station", "selling", 300, 0)
|
||||||
|
local mWait = addMarker(20, "Wait 3", "General Wait", 20, 0)
|
||||||
|
local mStore = addMarker(30, "silo 6", "silo", 60, 0)
|
||||||
|
sources[1] = {kind = "factory", wayPointId = 1, station = {name = "factoryStation"}, name = "Sugar mill"}
|
||||||
|
sources[2] = {kind = "silo", wayPointId = 2, station = {name = "siloStation"}, name = "Farma"}
|
||||||
|
sources[3] = {kind = "objectStorage", wayPointId = 3, station = nil, name = "Objektlager", stored = {[BASKET] = 3, [CRATE] = 1}}
|
||||||
|
products[1] = {{fillType = SUGAR, name = "SUGAR", available = 5000, level = 5000, capacity = 180000, isPallet = false}}
|
||||||
|
products[2] = {{fillType = SUGAR, name = "SUGAR", available = 9000, level = 9000, capacity = 12000000, isPallet = false}}
|
||||||
|
products[3] = {{fillType = BASKET, name = "STRAW_BASKET", available = 3000, level = 4, capacity = 4000, isPallet = true}, {fillType = CRATE, name = "STRAW_CRATE", available = 1000, level = 4, capacity = 4000, isPallet = true}}
|
||||||
|
quotes["selling station"] = {[SUGAR] = {percent = 92, price = 480}, [BASKET] = {percent = 95, price = 1}, [CRATE] = {percent = 95, price = 1}}
|
||||||
|
quotes["Far station"] = {[SUGAR] = {percent = 99, price = 600}}
|
||||||
|
|
||||||
|
local function outboundRun(id, sourceIds, productsList, overrides)
|
||||||
|
local run = {id = id, name = "Out " .. id, kind = "outbound", waitPoolGroup = "", sourceWayPointIds = sourceIds, products = productsList, sellAtPercent = 90, storeAbovePercent = 80, collectRound = false}
|
||||||
|
for k, v in pairs(overrides or {}) do run[k] = v end
|
||||||
|
return run
|
||||||
|
end
|
||||||
|
|
||||||
|
-- O1: tur -> AD's varevalg og 2. markør sat, choice = kilden; næstbedste salgssted når bedste er uopnåeligt
|
||||||
|
unreachable[11] = true
|
||||||
|
local v1 = newVehicle("MT635", 0, 0, 20000)
|
||||||
|
runs[v1] = outboundRun(1, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||||
|
check("O1 er udkørsel", ADOutbound.isOutboundVehicle(v1), true)
|
||||||
|
local choice = ADOutbound.choosePickup(adEnv, v1)
|
||||||
|
check("O1 kilde-markør", choice.markerIndex, mSugar.markerIndex)
|
||||||
|
check("O1 station med (bulk-greb)", choice.station.name, "factoryStation")
|
||||||
|
check("O1 vare", v1.ad.stateModule.fillType, SUGAR)
|
||||||
|
check("O1 varevalg", #v1.ad.stateModule.selectedFillTypes, 1)
|
||||||
|
check("O1 2. markør = nåeligt salgssted", v1.ad.stateModule.secondMarkerId, 10)
|
||||||
|
check("O1 ingen advarsel (næstbedste kunne nås)", #notifications, 0)
|
||||||
|
check("O1 log", lastLog("udkørsel") ~= nil, true)
|
||||||
|
local trip = ADOutbound.getTripFor(v1)
|
||||||
|
check("O1 trip kind", trip.kind, "sell")
|
||||||
|
unreachable[11] = nil
|
||||||
|
|
||||||
|
-- O2: ingen tur -> Wait-markør reserveret, hold registreret
|
||||||
|
local v2 = newVehicle("ARION", 25, 0, 20000)
|
||||||
|
runs[v2] = outboundRun(2, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||||
|
quotes["selling station"][SUGAR].percent = 70
|
||||||
|
quotes["Far station"][SUGAR].percent = 60
|
||||||
|
local choice2 = ADOutbound.choosePickup(adEnv, v2)
|
||||||
|
check("O2 Wait-markør", choice2.markerIndex, mWait.markerIndex)
|
||||||
|
check("O2 uden station", choice2.station, nil)
|
||||||
|
check("O2 reserveret", ADWaitPool.getOwner(20), v2)
|
||||||
|
local hold = ADOutbound.getHoldFor(v2)
|
||||||
|
check("O2 hold", hold.markerName, "Wait 3")
|
||||||
|
check("O2 begrundelse", hold.reasons[1], "SUGAR 70 % af top (kræver 90), lager 3 %")
|
||||||
|
check("O2 run-status", ADOutbound.getRunStatus(2).reasons[1], hold.reasons[1])
|
||||||
|
check("O2 hold-log én gang", countLog("holder ved Wait 3"), 1)
|
||||||
|
ADOutbound.choosePickup(adEnv, v2)
|
||||||
|
check("O2 samme begrundelse logges ikke igen", countLog("holder ved Wait 3"), 1)
|
||||||
|
|
||||||
|
-- O3: holder ved Wait: vognen stoppes; efter 5 s med tur -> omdirigeret til kilden, hold sluppet
|
||||||
|
local task2 = newTask(v2, 20)
|
||||||
|
v2.atTarget = true
|
||||||
|
check("O3 håndteret", ADOutbound.beforeLoadUpdate(adEnv, task2, 1000), true)
|
||||||
|
check("O3 stoppet", v2.stopped, true)
|
||||||
|
quotes["selling station"][SUGAR].percent = 92
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task2, 4500)
|
||||||
|
check("O3 omdirigeret til kilden", task2.destinationID, 1)
|
||||||
|
check("O3 sti sat", v2.pathTo, 1)
|
||||||
|
check("O3 hold væk", ADOutbound.getHoldFor(v2), nil)
|
||||||
|
check("O3 reservation frigivet", ADWaitPool.getOwner(20), nil)
|
||||||
|
check("O3 activePickup opdateret", activePickupCalls[#activePickupCalls].station.name, "factoryStation")
|
||||||
|
check("O3 UAL-flag nulstillet", task2.activatedUALLoading, false)
|
||||||
|
v2.atTarget = false
|
||||||
|
check("O3 undervejs ikke håndteret", ADOutbound.beforeLoadUpdate(adEnv, task2, 100), false)
|
||||||
|
|
||||||
|
-- O4: objektlager: én palle ad gangen; stored falder + loaded stiger -> næste; ualIsFull -> finished
|
||||||
|
local v3 = newVehicle("Fendt", 100, 0, 8000, true)
|
||||||
|
runs[v3] = outboundRun(3, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}, {fillType = "STRAW_CRATE", policy = "sell"}}, {collectRound = true})
|
||||||
|
local choice3 = ADOutbound.choosePickup(adEnv, v3)
|
||||||
|
check("O4 kilde = objektlager", choice3.markerIndex, mObj.markerIndex)
|
||||||
|
check("O4 kun én vare valgt ad gangen ved objektlager", #v3.ad.stateModule.selectedFillTypes, 1)
|
||||||
|
check("O4 turen har begge varer", #ADOutbound.getTripFor(v3).fillTypes, 2)
|
||||||
|
local task3 = newTask(v3, 3)
|
||||||
|
v3.atTarget = true
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
check("O4 intet spawn før UAL er aktiveret", sources[3].spawnCalls, nil)
|
||||||
|
task3.activatedUALLoading = true
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
check("O4 første spawn", sources[3].spawnCalls, 1)
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
check("O4 venter på pallen", sources[3].spawnCalls, 1)
|
||||||
|
sources[3].stored[BASKET] = 2
|
||||||
|
v3.trailers.ual.spec.loadedObjects[{}] = 1
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
check("O4 andet spawn efter læsning", sources[3].spawnCalls, 2)
|
||||||
|
sources[3].stored[BASKET] = 0
|
||||||
|
v3.trailers.ual.spec.loadedObjects[{}] = 1
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
check("O4 skifter til kasser når kurvene er væk", v3.ad.stateModule.fillType, CRATE)
|
||||||
|
check("O4 UAL genaktiveret ved vareskift", v3.trailers.activated, 1)
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
check("O4 tredje spawn (kasse)", sources[3].spawnCalls, 3)
|
||||||
|
v3.trailers.full = true
|
||||||
|
sources[3].stored[CRATE] = 0
|
||||||
|
v3.trailers.ual.spec.loadedObjects[{}] = 1
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task3, 100)
|
||||||
|
check("O4 fuld -> finished", task3.finishedCalls, 1)
|
||||||
|
|
||||||
|
-- O5: spawn uden fremdrift i 30 s -> finished + WARN
|
||||||
|
local v4 = newVehicle("Valtra", 100, 0, 8000, true)
|
||||||
|
runs[v4] = outboundRun(4, {3}, {{fillType = "STRAW_BASKET", policy = "sell"}})
|
||||||
|
sources[3].stored[BASKET] = 5
|
||||||
|
sources[3].spawnCalls = 0
|
||||||
|
ADOutbound.choosePickup(adEnv, v4)
|
||||||
|
local task4 = newTask(v4, 3)
|
||||||
|
v4.atTarget, task4.activatedUALLoading = true, true
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task4, 100)
|
||||||
|
check("O5 spawn kaldt", sources[3].spawnCalls, 1)
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task4, 31000)
|
||||||
|
ADOutbound.beforeLoadUpdate(adEnv, task4, 100)
|
||||||
|
check("O5 finished efter stall", task4.finishedCalls, 1)
|
||||||
|
check("O5 WARN", lastLog("spawner ikke") ~= nil, true)
|
||||||
|
|
||||||
|
-- O6: opsamlingsrunde, bulk: kilde A fylder vognen -> finished uden redirect; kilde A giver 30 % -> videre til B
|
||||||
|
local v5 = newVehicle("Claas", 0, 0, 10000)
|
||||||
|
runs[v5] = outboundRun(5, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
|
||||||
|
local choice5 = ADOutbound.choosePickup(adEnv, v5)
|
||||||
|
check("O6 mest først = silo 4", choice5.markerIndex, mSilo.markerIndex)
|
||||||
|
local task5 = newTask(v5, 2)
|
||||||
|
v5.trailers.level = 10000
|
||||||
|
check("O6 fuld: ingen runde", ADOutbound.beforeLoadFinished(adEnv, task5), false)
|
||||||
|
local v6 = newVehicle("Claas2", 0, 0, 10000)
|
||||||
|
runs[v6] = outboundRun(6, {1, 2}, {{fillType = "SUGAR", policy = "sell"}}, {collectRound = true})
|
||||||
|
ADOutbound.choosePickup(adEnv, v6)
|
||||||
|
local task6 = newTask(v6, 2)
|
||||||
|
v6.trailers.level = 3000
|
||||||
|
check("O6 plads tilbage: videre til næste kilde", ADOutbound.beforeLoadFinished(adEnv, task6), true)
|
||||||
|
check("O6 destination = Suger Load", task6.destinationID, 1)
|
||||||
|
check("O6 activePickup = fabrik", activePickupCalls[#activePickupCalls].station.name, "factoryStation")
|
||||||
|
check("O6 alle besøgt: afslut", ADOutbound.beforeLoadFinished(adEnv, task6), false)
|
||||||
|
|
||||||
|
-- O7: intet nåeligt salgssted -> rød notifikation én gang
|
||||||
|
notifications = {}
|
||||||
|
unreachable[10], unreachable[11] = true, true
|
||||||
|
local v7 = newVehicle("Deutz", 0, 0, 20000)
|
||||||
|
runs[v7] = outboundRun(7, {1}, {{fillType = "SUGAR", policy = "sell"}})
|
||||||
|
local choice7 = ADOutbound.choosePickup(adEnv, v7)
|
||||||
|
check("O7 holder", ADOutbound.getHoldFor(v7) ~= nil, true)
|
||||||
|
check("O7 én notifikation", #notifications, 1)
|
||||||
|
check("O7 ERROR", notifications[1].kind, 3)
|
||||||
|
check("O7 tekst", string.find(notifications[1].text, "kan ikke nå salgssted", 1, true) ~= nil, true)
|
||||||
|
check("O7 status-advarsel", ADOutbound.getRunStatus(7).warning ~= nil, true)
|
||||||
|
ADOutbound.choosePickup(adEnv, v7)
|
||||||
|
check("O7 ikke gentaget", #notifications, 1)
|
||||||
|
unreachable[10] = nil
|
||||||
|
ADOutbound.choosePickup(adEnv, v7)
|
||||||
|
check("O7 tur når vejen er der", ADOutbound.getTripFor(v7) ~= nil, true)
|
||||||
|
check("O7 advarsel ryddet", ADOutbound.getRunStatus(7).warning, nil)
|
||||||
|
unreachable[11] = nil
|
||||||
|
|
||||||
|
-- O8: ingen silo med plads ved lagerbehov -> notifikation; med silo -> store-tur
|
||||||
|
notifications = {}
|
||||||
|
local v8 = newVehicle("JD", 0, 0, 20000)
|
||||||
|
runs[v8] = outboundRun(8, {2}, {{fillType = "SUGAR", policy = "store"}})
|
||||||
|
products[2][1].level, products[2][1].capacity = 11000000, 12000000
|
||||||
|
ADOutbound.choosePickup(adEnv, v8)
|
||||||
|
check("O8 advarsel ingen silo", #notifications, 1)
|
||||||
|
dumpMarker = mStore
|
||||||
|
ADOutbound.choosePickup(adEnv, v8)
|
||||||
|
check("O8 store-tur", ADOutbound.getTripFor(v8).kind, "store")
|
||||||
|
check("O8 mål = silo 6", v8.ad.stateModule.secondMarkerId, 30)
|
||||||
|
|
||||||
|
-- O9: releaseInactive rydder hold + reservation
|
||||||
|
local v9 = newVehicle("Case", 25, 0, 20000)
|
||||||
|
runs[v9] = outboundRun(9, {1}, {{fillType = "FORAGE", policy = "sell"}})
|
||||||
|
ADOutbound.choosePickup(adEnv, v9)
|
||||||
|
check("O9 reserveret", ADWaitPool.getOwner(20), v9)
|
||||||
|
v9.active = false
|
||||||
|
ADOutbound.releaseInactive()
|
||||||
|
check("O9 frigivet", ADWaitPool.getOwner(20), nil)
|
||||||
|
check("O9 hold væk", ADOutbound.getHoldFor(v9), nil)
|
||||||
|
|
||||||
|
-- O10: install wrapper: fejl i hook stopper ikke AD
|
||||||
|
local calls = {}
|
||||||
|
adEnv.LoadAtDestinationTask.update = function(self, dt) calls.update = (calls.update or 0) + 1 end
|
||||||
|
adEnv.LoadAtDestinationTask.finished = function(self) calls.finished = (calls.finished or 0) + 1 end
|
||||||
|
ADOutbound.install(adEnv)
|
||||||
|
local vX = newVehicle("X", 0, 0, 1000)
|
||||||
|
adEnv.LoadAtDestinationTask.update(newTask(vX, 1), 16)
|
||||||
|
check("O10 original update kaldt", calls.update, 1)
|
||||||
|
adEnv.LoadAtDestinationTask.finished(newTask(vX, 1))
|
||||||
|
check("O10 original finished kaldt", calls.finished, 1)
|
||||||
|
|
||||||
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
|
os.exit(failures == 0 and 0 or 1)
|
||||||
Reference in New Issue
Block a user