Files
fs25-adsmartpickup/FS25_ADSmartPickup/adUnloadWait.lua
T

355 lines
15 KiB
Lua

-- AD Unload Wait
-- Holder en traktor med rest-læs ved en fuld stald AutoDrive i dag stille ved
-- aflæsningspunktet "til den er tømt på en eller anden måde" — og spærrer for
-- de næste i køen. Her omdirigeres den i stedet til nærmeste ledige Wait-punkt
-- (markør i en mappe med ordet "Wait" i navnet, fx "Cow Wait"), og når målet igen
-- har plads til HELE resten, sendes den tilbage og læsser af.
--
-- Kun den aktuelle aflæsningsopgaves destination ændres; AutoDrives mål,
-- rotation og loop-tæller røres ikke.
ADUnloadWait = {}
-- Under dette regnes vognen som tom (AutoDrive selv bruger 0,1 l).
ADUnloadWait.MIN_REST_LITERS = 100
-- Kan målet tage mindre end dette, regnes det som fuldt (aflæsning starter ikke).
ADUnloadWait.MIN_FREE_TO_UNLOAD = 100
ADUnloadWait.LOG_PREFIX = "ADSmartPickup: "
-- køretøj -> {task, homeDestinationID, waitMarker, fillType, rest}
local waits = setmetatable({}, {__mode = "k"})
-- markør-id -> køretøj der har reserveret Wait-punktet
local claims = setmetatable({}, {__mode = "v"})
local warnedNoWaitPoint = setmetatable({}, {__mode = "k"})
local isInstalled = false
-- Den vare og mængde der er tilbage på vognen (største fyldte aflæsselige enhed).
local function getRestLoad(adEnv, vehicle)
local bestFillType, bestLevel = nil, 0
for _, unit in ipairs(adEnv.AutoDrive.getAllDischargeableUnits(vehicle) or {}) do
local object = unit.object
if object ~= nil and object.getFillUnitFillLevel ~= nil then
local level = object:getFillUnitFillLevel(unit.fillUnitIndex) or 0
if level > bestLevel then
bestLevel = level
bestFillType = object.getFillUnitFillType ~= nil and object:getFillUnitFillType(unit.fillUnitIndex) or nil
end
end
end
return bestFillType, bestLevel
end
-- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper.
local function getWaitCandidates(adEnv, poolGroup)
local candidates = {}
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
local isInPool = poolGroup == nil or poolGroup == "" or marker.group == poolGroup
if isInPool and ADSupplyPlanner.isWaitMarker(marker) then
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
if wayPoint ~= nil then
table.insert(candidates, {marker = marker, x = wayPoint.x, z = wayPoint.z})
end
end
end
return candidates
end
local function getWaitPoolGroup(vehicle)
local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(vehicle) or nil
return run ~= nil and run.waitPoolGroup or nil
end
local function getVehiclePosition(vehicle)
if vehicle.rootNode == nil then
return nil
end
local x, _, z = getWorldTranslation(vehicle.rootNode)
return x, z
end
-- Ender ruten i målet? (AutoDrive returnerer tom/afkortet liste når der ingen vej er.)
local function pathReaches(path, wayPointId)
return path ~= nil and #path > 0 and path[#path].id == wayPointId
end
-- Findes der vejnet fra køretøjet til Wait-punktet OG fra Wait-punktet hjem til målet?
-- Sendes AutoDrive mod et punkt uden rute, viser den "cannot reach" og slukker sig selv.
-- getPathTo/pathFromTo er rene opslag uden bivirkninger.
-- Kun "til punktet": ved aflæsning af (forkert) last i en silo er næste tur AD's egen
-- rute fra siloen, så "fra siloen hjem" er ikke et krav.
local function hasRouteTo(adEnv, vehicle, wayPointId)
local graph = adEnv.ADGraphManager
if graph.getPathTo == nil then
return true
end
return pathReaches(graph:getPathTo(vehicle, wayPointId), wayPointId)
end
local function hasRouteBothWays(adEnv, vehicle, waitWayPointId, homeWayPointId)
local graph = adEnv.ADGraphManager
if graph.getPathTo == nil or graph.pathFromTo == nil then
return true
end
return pathReaches(graph:getPathTo(vehicle, waitWayPointId), waitWayPointId)
and pathReaches(graph:pathFromTo(waitWayPointId, homeWayPointId), homeWayPointId)
end
-- Står AD midt i en (blokeret) aflæsning, stoppes den pænt først — ellers kører traktoren
-- væk med tippet vogn.
local function redirect(task, destinationID)
local trailerModule = task.vehicle.ad.trailerModule
if trailerModule ~= nil and trailerModule.isActiveAtTrigger ~= nil and trailerModule:isActiveAtTrigger() then
if trailerModule.stopUnloading ~= nil then trailerModule:stopUnloading() end
if trailerModule.stopLoading ~= nil then trailerModule:stopLoading() end
end
task.destinationID = destinationID
task.state = ADUnloadWait.STATE_DRIVING
task.vehicle.ad.drivePathModule:setPathTo(destinationID)
end
-- køretøj -> AD's oprindelige 2. markør (waypoint-id) mens der læsses af i en silo
local pendingTargetRestore = setmetatable({}, {__mode = "k"})
-- Aflæsning ved en ANDEN destination end AD's mål: AD tipper kun når vognen er tæt på
-- AD's egen 2. markør (getDistanceToUnloadPosition), så den skiftes midlertidigt til
-- siloen og gendannes når opgaven er færdig.
local function redirectForUnload(task, destinationID)
local vehicle = task.vehicle
local stateModule = vehicle.ad.stateModule
local current = stateModule.getSecondMarker ~= nil and stateModule:getSecondMarker() or nil
if current ~= nil and current.id ~= destinationID and stateModule.setSecondMarkerByWayPointId ~= nil then
pendingTargetRestore[vehicle] = pendingTargetRestore[vehicle] or current.id
stateModule:setSecondMarkerByWayPointId(destinationID)
end
redirect(task, destinationID)
end
local function restoreTarget(vehicle)
local homeId = pendingTargetRestore[vehicle]
if homeId == nil then
return
end
pendingTargetRestore[vehicle] = nil
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule ~= nil and stateModule.setSecondMarkerByWayPointId ~= nil then
stateModule:setSecondMarkerByWayPointId(homeId)
end
end
local function release(vehicle)
local wait = waits[vehicle]
if wait ~= nil and claims[wait.waitMarker.id] == vehicle then
claims[wait.waitMarker.id] = nil
end
waits[vehicle] = nil
if ADLoadSwap ~= nil then
ADLoadSwap.clearWarning(vehicle)
end
end
-- Står køretøjet ved målet med rest-læs, som målet ikke kan tage? Så find et Wait-punkt.
local function startWaiting(adEnv, task)
local vehicle = task.vehicle
-- Kun "fremme ved målet". At AD stadig regner vognen som aflæssende (isActiveAtTrigger)
-- tæller IKKE med: ved et fuldt fodertrug bliver isUnloading stående for evigt, selv om
-- intet løber — og så blokerede traktoren stalden i stedet for at køre til Wait.
if not vehicle.ad.drivePathModule:isTargetReached() then
return
end
-- Efter et bytte er opgavens mål en silo, ikke AD's rigtige aflæsningsmål (anden markør);
-- der skal AD bare læsse af som normalt, uden Wait-logik.
local target = vehicle.ad.stateModule.getSecondMarker ~= nil and vehicle.ad.stateModule:getSecondMarker() or nil
if target == nil or task.destinationID ~= target.id then
return
end
local fillType, rest = getRestLoad(adEnv, vehicle)
if fillType == nil or rest < ADUnloadWait.MIN_REST_LITERS then
return
end
local free, capacity = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType)
-- nil = målet tager slet ikke varen (forkert last fra et tidligere run): som "fuldt"
local isRejected = free == nil and ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType, true) == false
if isRejected then
free = 0
end
if free == nil or free >= ADUnloadWait.MIN_FREE_TO_UNLOAD then
return
end
local x, z = getVehiclePosition(vehicle)
local isReachable = function(candidate)
return hasRouteBothWays(adEnv, vehicle, candidate.marker.id, task.destinationID)
end
local pool = getWaitPoolGroup(vehicle)
local candidate = x ~= nil and ADSupplyPlanner.pickNearestFreeWaitPoint(getWaitCandidates(adEnv, pool), claims, x, z, vehicle, isReachable) or nil
if candidate == nil then
if not warnedNoWaitPoint[vehicle] then
warnedNoWaitPoint[vehicle] = true
local poolText = pool ~= nil and pool ~= "" and (" i puljen '" .. pool .. "'") or ""
Logging.warning(ADUnloadWait.LOG_PREFIX .. "'%s' har %.0f l rest, og målet er fuldt — intet ledigt Wait-punkt%s med vejnet til OG fra målet (markør i en mappe med 'Wait' i navnet), så AutoDrive venter ved målet", tostring(vehicle:getName()), rest, poolText)
end
return
end
warnedNoWaitPoint[vehicle] = nil
claims[candidate.marker.id] = vehicle
waits[vehicle] = {task = task, homeDestinationID = task.destinationID, waitMarker = candidate.marker, fillType = fillType, rest = rest}
redirect(task, candidate.marker.id)
local fillTypeName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
Logging.info(ADUnloadWait.LOG_PREFIX .. "'%s' venter ved %s (markør %s): %.0f l %s rest, målet har kun %.0f l fri", tostring(vehicle:getName()), tostring(candidate.marker.name), tostring(candidate.marker.markerIndex or candidate.marker.id), rest, tostring(fillTypeName), free)
end
-- Ventende køretøj: er der nu plads til hele resten, køres tilbage til målet.
local function updateWaiting(adEnv, task, wait, dt)
local vehicle = task.vehicle
if wait.task ~= task then
release(vehicle)
return
end
local fillType, rest = getRestLoad(adEnv, vehicle)
if fillType == nil or rest < ADUnloadWait.MIN_REST_LITERS then
release(vehicle)
return
end
-- Friske tal til evt. bytte og til tilbage-loggen — ikke dem fra da ventningen startede.
wait.fillType, wait.rest = fillType, rest
local free, capacity = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType)
local isRejected = free == nil and ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType, true) == false
if isRejected then
free = 0
end
wait.isRejected = isRejected
-- run'ets "kør hjem under X %" (fri ≥ (100-X) % af kapaciteten), ellers standardreglen
local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(vehicle) or nil
local freeFraction = run ~= nil and run.returnBelowPercent ~= nil and (1 - run.returnBelowPercent / 100) or nil
if free == nil or not ADSupplyPlanner.canUnloadRest(free, rest, capacity, freeFraction) then
if ADLoadSwap ~= nil then
local isReachable = function(markerId)
return hasRouteTo(adEnv, vehicle, markerId)
end
ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirectForUnload, release, isReachable)
end
return
end
local home = wait.homeDestinationID
release(vehicle)
redirect(task, home)
Logging.info(ADUnloadWait.LOG_PREFIX .. "'%s' kører tilbage fra %s og læsser %.0f l af (målet har %.0f l fri)", tostring(vehicle:getName()), tostring(wait.waitMarker.name), rest, free)
end
function ADUnloadWait.beforeUpdate(adEnv, task, dt)
local wait = waits[task.vehicle]
if wait ~= nil then
updateWaiting(adEnv, task, wait, dt)
else
startWaiting(adEnv, task)
end
end
-- Kaldes fra ADSmartPickup:update — standsede køretøjer slipper deres Wait-punkt.
function ADUnloadWait.releaseInactive()
for vehicle in pairs(waits) do
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule == nil or stateModule.isActive == nil or not stateModule:isActive() then
release(vehicle)
end
end
for vehicle in pairs(pendingTargetRestore) do
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule == nil or stateModule.isActive == nil or not stateModule:isActive() then
restoreTarget(vehicle)
end
end
end
function ADUnloadWait.getPendingTargetRestore(vehicle)
return pendingTargetRestore[vehicle]
end
-- Ved Start med forkert last: AD's mål peger på siloen indtil aflæsningen er færdig;
-- det oprindelige mål gendannes af finished-hook'en.
function ADUnloadWait.setDumpTarget(vehicle, dumpWayPointId)
local stateModule = vehicle.ad.stateModule
local current = stateModule.getSecondMarker ~= nil and stateModule:getSecondMarker() or nil
if current ~= nil and current.id ~= dumpWayPointId then
pendingTargetRestore[vehicle] = pendingTargetRestore[vehicle] or current.id
end
if stateModule.setSecondMarkerByWayPointId ~= nil then
stateModule:setSecondMarkerByWayPointId(dumpWayPointId)
end
end
-- Hvem holder hvor (GUI'ets Wait-puljer-visning). Sorteret på mappe, dernæst markør-id.
-- Vognens indhold (største enhed) og evt. Wait-tilstand — til overblikket i fanen.
function ADUnloadWait.hasRouteBothWays(adEnv, vehicle, toWayPointId, homeWayPointId)
return hasRouteBothWays(adEnv, vehicle, toWayPointId, homeWayPointId)
end
function ADUnloadWait.hasRouteTo(adEnv, vehicle, wayPointId)
return hasRouteTo(adEnv, vehicle, wayPointId)
end
function ADUnloadWait.getRestLoad(adEnv, vehicle)
return getRestLoad(adEnv, vehicle)
end
function ADUnloadWait.getWaitFor(vehicle)
local wait = waits[vehicle]
if wait == nil then
return nil
end
return {markerName = wait.waitMarker ~= nil and wait.waitMarker.name or nil, fillType = wait.fillType, rest = wait.rest}
end
function ADUnloadWait.getClaims()
local result = {}
for _, wait in pairs(waits) do
if wait.waitMarker ~= nil and claims[wait.waitMarker.id] ~= nil then
table.insert(result, {marker = wait.waitMarker, vehicle = claims[wait.waitMarker.id]})
end
end
table.sort(result, function(a, b)
if a.marker.group ~= b.marker.group then
return tostring(a.marker.group) < tostring(b.marker.group)
end
return a.marker.id < b.marker.id
end)
return result
end
function ADUnloadWait.install(adEnv)
local task, autoDrive = adEnv.UnloadAtDestinationTask, adEnv.AutoDrive
if isInstalled or task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then
return
end
isInstalled = true
ADUnloadWait.STATE_DRIVING = task.STATE_DRIVING or 2
local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting
task.update = function(self, dt)
local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt)
if not ok then
Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err))
release(self.vehicle)
end
return originalUpdate(self, dt)
end
if originalFinished ~= nil then
task.finished = function(self)
release(self.vehicle)
restoreTarget(self.vehicle)
return originalFinished(self)
end
end
-- Ved "rotér aflæsning" kører AutoDrive straks videre fra et mål den ikke kan læsse
-- af ved — også fra Wait-punktet. Ventende køretøjer ser derfor rotationen som slået fra.
autoDrive.getSetting = function(settingName, vehicle)
if settingName == "rotateTargets" and vehicle ~= nil and waits[vehicle] ~= nil then
return autoDrive.RT_NONE
end
return originalGetSetting(settingName, vehicle)
end
end