feat: Wait-punkter for rest-læs + læs der passer foretrækkes (v1.7.0.0)

Rest-læs ved fuldt mål: AutoDrive ventede ved aflæsningspunktet 'til tømt' og
spærrede køen. Nu omdirigeres den aktuelle UnloadAtDestinationTask (destinationID +
drivePathModule:setPathTo) til nærmeste ledige markør med 'Wait' i navnet / mappen
Wait (fælles pulje, reservationer pr. køretøj); tilbage når målet har plads til
hele resten. Ventende ser rotateTargets=RT_NONE så 'rotér aflæsning' ikke kører
videre fra Wait-punktet. Ingen ledige punkter = som i dag + én advarsel.

Rangering: varer hvor et fuldt læs (AutoDrive.getAllFillLevels-kapacitet) passer i
målet går forud; passer intet, mest trængende. Nyt modul adUnloadWait.lua;
ADSmartPickup.getTargetFreeCapacity delt hjælper.
This commit is contained in:
2026-09-22 21:12:55 +02:00
parent 2ff0b641b0
commit 39fccefa40
7 changed files with 469 additions and 22 deletions
+67 -18
View File
@@ -357,31 +357,70 @@ local function isFillTypeAccepted(station, husbandry, fillType)
return station.getIsFillTypeSupported == nil or station:getIsFillTypeSupported(fillType)
end
-- Niveau + kapacitet for én vare i aflæsningsmålet; nil hvis målet ikke tager varen.
local function getTargetLevelAndCapacity(station, fillType, farmId)
local husbandry = getHusbandry(station)
if not isFillTypeAccepted(station, husbandry, fillType) then
return nil
end
if husbandry ~= nil then
local level = husbandry:getHusbandryFillLevel(fillType) or 0
return level, husbandry:getHusbandryCapacity(fillType) or level
end
local level, freeSpace = 0, 0
for _, storage in pairs(getTargetStorages(station)) do
level = level + (storage:getFillLevel(fillType) or 0)
freeSpace = freeSpace + (storage:getFreeCapacity(fillType) or 0)
end
if level + freeSpace <= 0 and station.getFreeCapacity ~= nil then
-- ukendt lagertype: brug stationens egen opgørelse af fri plads
level, freeSpace = 0, station:getFreeCapacity(fillType, farmId) or 0
end
return level, level + freeSpace
end
-- Niveau + kapacitet pr. ingrediens i aflæsningsmålets lager; demand giver forbrug/min.
local function getTargetNeeds(station, ingredients, farmId, demand)
local needs = {}
local storages = getTargetStorages(station)
local husbandry = getHusbandry(station)
for _, fillType in ipairs(ingredients) do
if husbandry ~= nil and isFillTypeAccepted(station, husbandry, fillType) then
local level = husbandry:getHusbandryFillLevel(fillType) or 0
table.insert(needs, {fillType = fillType, level = level, capacity = husbandry:getHusbandryCapacity(fillType) or level, litersPerMinute = demand[fillType]})
elseif isFillTypeAccepted(station, husbandry, fillType) then
local level, freeSpace = 0, 0
for _, storage in pairs(storages) do
level = level + (storage:getFillLevel(fillType) or 0)
freeSpace = freeSpace + (storage:getFreeCapacity(fillType) or 0)
end
if level + freeSpace <= 0 and station.getFreeCapacity ~= nil then
-- ukendt lagertype: brug stationens egen opgørelse af fri plads
level, freeSpace = 0, station:getFreeCapacity(fillType, farmId) or 0
end
table.insert(needs, {fillType = fillType, level = level, capacity = level + freeSpace, litersPerMinute = demand[fillType]})
local level, capacity = getTargetLevelAndCapacity(station, fillType, farmId)
if level ~= nil then
table.insert(needs, {fillType = fillType, level = level, capacity = capacity, litersPerMinute = demand[fillType]})
end
end
return needs
end
-- Vognens samlede kapacitet (liter); nil hvis AutoDrive ikke kan opgøre den.
local function getLoadCapacity(adEnv, driver)
if adEnv.AutoDrive.getAllUnits == nil or adEnv.AutoDrive.getAllFillLevels == nil then
return nil
end
local trailers = adEnv.AutoDrive.getAllUnits(driver)
local _, capacity = adEnv.AutoDrive.getAllFillLevels(trailers)
return capacity ~= nil and capacity > 0 and capacity or nil
end
-- Fri plads til varen i køretøjets aflæsningsmål (bruges af Wait-punkt-logikken);
-- nil hvis målet ikke kendes eller ikke tager varen.
function ADSmartPickup.getTargetFreeCapacity(adEnv, driver, fillType)
local secondMarker = driver.ad.stateModule:getSecondMarker()
local farmId = getFarmId(driver)
if secondMarker == nil or farmId == nil then
return nil
end
local targetWayPoint = adEnv.ADGraphManager:getWayPointById(secondMarker.id)
local target = targetWayPoint ~= nil and getUnloadStationAtMarker(adEnv, targetWayPoint, farmId) or nil
if target == nil then
return nil
end
local level, capacity = getTargetLevelAndCapacity(target, fillType, farmId)
if level == nil then
return nil
end
return capacity - level
end
-- Behovsstyret: den ingrediens aflæsningsmålet mangler mest, som også findes i en silo.
function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)
local secondMarker = driver.ad.stateModule:getSecondMarker()
@@ -407,7 +446,8 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)
local allMarkers = adEnv.ADGraphManager:getMapMarkers()
local markerStations = collectMarkerStations(adEnv, allMarkers, farmId, target.owningPlaceable)
for _, need in ipairs(ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId, demand))) do
local loadCapacity = getLoadCapacity(adEnv, driver)
for _, need in ipairs(ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId, demand), loadCapacity)) do
local choice = findBestSource(markerStations, {need.fillType}, farmId)
if choice ~= nil then
return {
@@ -463,6 +503,9 @@ local function describeChoice(choice)
if choice.need.minutesToEmpty ~= nil then
urgency = string.format(", fabrikken bruger %.0f l/min = %.0f min til tom", choice.need.litersPerMinute, choice.need.minutesToEmpty)
end
if choice.need.fits == false then
urgency = urgency .. ", et fuldt læs passer IKKE (rest ventes ved Wait-punkt)"
end
return string.format("%s: kilde %.0f l, %s, målet har %.0f/%.0f l%s", tostring(fillTypeName), choice.level, source, choice.need.level, choice.need.capacity, urgency)
end
@@ -571,8 +614,11 @@ local function installHook(adEnv)
end
installLoadWaitHook(adEnv)
if ADUnloadWait ~= nil then
ADUnloadWait.install(adEnv)
end
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning)")
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs)")
end
-- Når AutoDrive stopper, får køretøjet hele ingredienslisten tilbage i varevalget
@@ -608,6 +654,9 @@ function ADSmartPickup:update(dt)
return
end
if ADUnloadWait ~= nil then
pcall(ADUnloadWait.releaseInactive)
end
local ok, err = pcall(restoreStoppedVehicles)
if not ok then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
+42 -1
View File
@@ -114,9 +114,46 @@ function ADSupplyPlanner.scoreNeed(level, capacity, freeSpace, minutesToEmpty)
return freeSpace / capacity
end
-- nil når læs-størrelsen er ukendt, ellers om et fuldt læs kan komme af i målet.
function ADSupplyPlanner.fullLoadFits(freeSpace, loadCapacity)
if loadCapacity == nil or loadCapacity <= 0 then
return nil
end
return freeSpace >= loadCapacity
end
-- Wait-punkter: markører med ordet "Wait" i navnet (uanset store/små) eller i mappen "Wait".
-- Her venter en traktor med rest-læs, til der igen er plads i målet.
function ADSupplyPlanner.isWaitMarker(marker)
if marker.group == "Wait" then
return true
end
local name = string.lower(tostring(marker.name or ""))
return string.find(name, "%f[%a]wait%f[%A]") ~= nil
end
-- candidates: {{marker, x, z}}; claims: {[marker.id] = køretøj}. Nærmeste punkt der er
-- ledigt eller allerede reserveret af dette køretøj. nil hvis alle er optaget.
function ADSupplyPlanner.pickNearestFreeWaitPoint(candidates, claims, x, z, vehicle)
local best, bestDistance = nil, nil
for _, candidate in ipairs(candidates) do
local owner = claims[candidate.marker.id]
if owner == nil or owner == vehicle then
local dx, dz = candidate.x - x, candidate.z - z
local distance = dx * dx + dz * dz
if bestDistance == nil or distance < bestDistance then
best, bestDistance = candidate, distance
end
end
end
return best
end
-- needs: {{fillType, level, capacity[, litersPerMinute]}, ...}. Returnerer NY liste,
-- mest trængende først; ingredienser uden plads i målet er sorteret fra.
function ADSupplyPlanner.rankNeeds(needs)
-- loadCapacity (valgfri): vognens kapacitet — varer hvor et FULDT læs kan komme af
-- går forud (så vognen ikke ender med rest-læs); passer intet, gælder ren trang.
function ADSupplyPlanner.rankNeeds(needs, loadCapacity)
local ranked = {}
for _, need in ipairs(needs) do
local freeSpace = need.capacity - need.level
@@ -129,11 +166,15 @@ function ADSupplyPlanner.rankNeeds(needs)
freeSpace = freeSpace,
litersPerMinute = need.litersPerMinute,
minutesToEmpty = minutesToEmpty,
fits = ADSupplyPlanner.fullLoadFits(freeSpace, loadCapacity),
score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace, minutesToEmpty),
})
end
end
table.sort(ranked, function(a, b)
if a.fits ~= b.fits then
return a.fits == true
end
if a.score ~= b.score then
return a.score > b.score
end
+184
View File
@@ -0,0 +1,184 @@
-- 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 med ordet "Wait" i navnet eller i mappen "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
local function getWaitCandidates(adEnv)
local candidates = {}
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
if 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 getVehiclePosition(vehicle)
if vehicle.rootNode == nil then
return nil
end
local x, _, z = getWorldTranslation(vehicle.rootNode)
return x, z
end
local function redirect(task, destinationID)
task.destinationID = destinationID
task.state = ADUnloadWait.STATE_DRIVING
task.vehicle.ad.drivePathModule:setPathTo(destinationID)
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
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
if not vehicle.ad.drivePathModule:isTargetReached() or vehicle.ad.trailerModule:isActiveAtTrigger() then
return
end
local fillType, rest = getRestLoad(adEnv, vehicle)
if fillType == nil or rest < ADUnloadWait.MIN_REST_LITERS then
return
end
local free = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType)
if free == nil or free >= ADUnloadWait.MIN_FREE_TO_UNLOAD then
return
end
local x, z = getVehiclePosition(vehicle)
local candidate = x ~= nil and ADSupplyPlanner.pickNearestFreeWaitPoint(getWaitCandidates(adEnv), claims, x, z, vehicle) or nil
if candidate == nil then
if not warnedNoWaitPoint[vehicle] then
warnedNoWaitPoint[vehicle] = true
Logging.warning(ADUnloadWait.LOG_PREFIX .. "'%s' har %.0f l rest, og målet er fuldt — intet ledigt Wait-punkt (markør med 'Wait' i navnet), så AutoDrive venter ved målet", tostring(vehicle:getName()), rest)
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: %.0f l %s rest, målet har kun %.0f l fri", tostring(vehicle:getName()), tostring(candidate.marker.name), 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)
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
local free = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType)
if free == nil or free < rest then
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)
local wait = waits[task.vehicle]
if wait ~= nil then
updateWaiting(adEnv, task, wait)
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
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)
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)
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
+3 -2
View File
@@ -1,17 +1,18 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.6.1.0</version>
<version>1.7.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
<description>
<en><![CDATA[AutoDrive-tillæg: når pålæsningsmål roteres i en mappe, køres der direkte til den silo i mappen, der har den valgte vare (mindst fyldte først), i stedet for at prøve markørerne én efter én. Vælges FLERE varer i AutoDrive, ses de som ingredienser til aflæsningsmålet (fx en TMR-blander): én traktor henter pr. tur den vare målet mangler mest, fra en silo hvor som helst på kortet. Er målet en fabrik, følger listen automatisk fabrikkens tændte produktioner (tid-til-tom først). Kræver FS25_AutoDrive med "Brug mapper" + "Rotér mål: pålæsning".]]></en>
<en><![CDATA[AutoDrive-tillæg: når pålæsningsmål roteres i en mappe, køres der direkte til den silo i mappen, der har den valgte vare (mindst fyldte først), i stedet for at prøve markørerne én efter én. Vælges FLERE varer i AutoDrive, ses de som ingredienser til aflæsningsmålet (fx en TMR-blander): én traktor henter pr. tur den vare målet mangler mest, fra en silo hvor som helst på kortet. Er målet en fabrik, følger listen automatisk fabrikkens tændte produktioner (tid-til-tom først). Rest-læs ved fuldt mål venter ved nærmeste ledige "Wait"-markør til der er plads. Kræver FS25_AutoDrive med "Brug mapper" + "Rotér mål: pålæsning".]]></en>
</description>
<iconFilename>icon.dds</iconFilename>
<multiplayer supported="true"/>
<extraSourceFiles>
<sourceFile filename="adSupplyPlanner.lua"/>
<sourceFile filename="adUnloadWait.lua"/>
<sourceFile filename="adSmartPickup.lua"/>
</extraSourceFiles>
</modDesc>