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) return station.getIsFillTypeSupported == nil or station:getIsFillTypeSupported(fillType)
end 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. -- Niveau + kapacitet pr. ingrediens i aflæsningsmålets lager; demand giver forbrug/min.
local function getTargetNeeds(station, ingredients, farmId, demand) local function getTargetNeeds(station, ingredients, farmId, demand)
local needs = {} local needs = {}
local storages = getTargetStorages(station)
local husbandry = getHusbandry(station)
for _, fillType in ipairs(ingredients) do for _, fillType in ipairs(ingredients) do
if husbandry ~= nil and isFillTypeAccepted(station, husbandry, fillType) then local level, capacity = getTargetLevelAndCapacity(station, fillType, farmId)
local level = husbandry:getHusbandryFillLevel(fillType) or 0 if level ~= nil then
table.insert(needs, {fillType = fillType, level = level, capacity = husbandry:getHusbandryCapacity(fillType) or level, litersPerMinute = demand[fillType]}) table.insert(needs, {fillType = fillType, level = level, capacity = capacity, 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]})
end end
end end
return needs return needs
end 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. -- Behovsstyret: den ingrediens aflæsningsmålet mangler mest, som også findes i en silo.
function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients) function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)
local secondMarker = driver.ad.stateModule:getSecondMarker() local secondMarker = driver.ad.stateModule:getSecondMarker()
@@ -407,7 +446,8 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)
local allMarkers = adEnv.ADGraphManager:getMapMarkers() local allMarkers = adEnv.ADGraphManager:getMapMarkers()
local markerStations = collectMarkerStations(adEnv, allMarkers, farmId, target.owningPlaceable) 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) local choice = findBestSource(markerStations, {need.fillType}, farmId)
if choice ~= nil then if choice ~= nil then
return { return {
@@ -463,6 +503,9 @@ local function describeChoice(choice)
if choice.need.minutesToEmpty ~= nil then if choice.need.minutesToEmpty ~= nil then
urgency = string.format(", fabrikken bruger %.0f l/min = %.0f min til tom", choice.need.litersPerMinute, choice.need.minutesToEmpty) urgency = string.format(", fabrikken bruger %.0f l/min = %.0f min til tom", choice.need.litersPerMinute, choice.need.minutesToEmpty)
end 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) 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 end
@@ -571,8 +614,11 @@ local function installHook(adEnv)
end end
installLoadWaitHook(adEnv) 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 end
-- Når AutoDrive stopper, får køretøjet hele ingredienslisten tilbage i varevalget -- Når AutoDrive stopper, får køretøjet hele ingredienslisten tilbage i varevalget
@@ -608,6 +654,9 @@ function ADSmartPickup:update(dt)
return return
end end
if ADUnloadWait ~= nil then
pcall(ADUnloadWait.releaseInactive)
end
local ok, err = pcall(restoreStoppedVehicles) local ok, err = pcall(restoreStoppedVehicles)
if not ok then if not ok then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err)) 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 return freeSpace / capacity
end 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, -- needs: {{fillType, level, capacity[, litersPerMinute]}, ...}. Returnerer NY liste,
-- mest trængende først; ingredienser uden plads i målet er sorteret fra. -- 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 = {} local ranked = {}
for _, need in ipairs(needs) do for _, need in ipairs(needs) do
local freeSpace = need.capacity - need.level local freeSpace = need.capacity - need.level
@@ -129,11 +166,15 @@ function ADSupplyPlanner.rankNeeds(needs)
freeSpace = freeSpace, freeSpace = freeSpace,
litersPerMinute = need.litersPerMinute, litersPerMinute = need.litersPerMinute,
minutesToEmpty = minutesToEmpty, minutesToEmpty = minutesToEmpty,
fits = ADSupplyPlanner.fullLoadFits(freeSpace, loadCapacity),
score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace, minutesToEmpty), score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace, minutesToEmpty),
}) })
end end
end end
table.sort(ranked, function(a, b) table.sort(ranked, function(a, b)
if a.fits ~= b.fits then
return a.fits == true
end
if a.score ~= b.score then if a.score ~= b.score then
return a.score > b.score return a.score > b.score
end 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"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107"> <modDesc descVersion="107">
<author>masterdraco</author> <author>masterdraco</author>
<version>1.6.1.0</version> <version>1.7.0.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
<description> <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> </description>
<iconFilename>icon.dds</iconFilename> <iconFilename>icon.dds</iconFilename>
<multiplayer supported="true"/> <multiplayer supported="true"/>
<extraSourceFiles> <extraSourceFiles>
<sourceFile filename="adSupplyPlanner.lua"/> <sourceFile filename="adSupplyPlanner.lua"/>
<sourceFile filename="adUnloadWait.lua"/>
<sourceFile filename="adSmartPickup.lua"/> <sourceFile filename="adSmartPickup.lua"/>
</extraSourceFiles> </extraSourceFiles>
</modDesc> </modDesc>
+18
View File
@@ -49,6 +49,24 @@ behovsstyring. Varer der både er input og output (kæde) må hentes.
Kostald m.fl. svarer selv for niveau/kapacitet (`getHusbandryFillLevel/Capacity`), så halm+TMR til en Kostald m.fl. svarer selv for niveau/kapacitet (`getHusbandryFillLevel/Capacity`), så halm+TMR til en
stald rangeres korrekt, selv om staldens aflæsningsstation kun "kender" foder. stald rangeres korrekt, selv om staldens aflæsningsstation kun "kender" foder.
### Wait-punkter: rest-læs ved fuld stald (v1.7)
AutoDrive holder ellers en traktor med rest-læs stille ved aflæsningspunktet, til den er tømt — og
spærrer for de næste i køen. Med Wait-punkter:
1. Sæt markører med ordet **`Wait`** i navnet (fx `Wait 1`, `Cow Wait 2`) eller i en mappe kaldet
`Wait` — så mange du har plads til; de er en fælles pulje for alle traktorer.
2. Kan målet ikke tage mere af vognens vare, kører traktoren til det **nærmeste ledige** Wait-punkt
(modden holder styr på reservationer) og venter dér.
3. Når målet igen har plads til **hele** resten, kører den tilbage og læsser af. Derefter normal tur.
Kun den aktuelle aflæsningsopgaves destination ændres — mål, rotation og loop-tæller røres ikke.
Ventende traktorer ser rotationen som slået fra, så AutoDrive ikke kører videre fra Wait-punktet.
Ingen ledige Wait-punkter → AutoDrive venter ved målet som i dag (én advarsel i loggen).
**Læs der passer, foretrækkes (v1.7):** ved valg af næste tur går varer, hvor et fuldt læs kan komme af
i målet, forud; passer intet fuldt læs, tages den mest trængende alligevel (og Wait-punktet bruges).
**Opsætning med flere stalde:** én traktor (eller flere) pr. stald med *Rotér mål: kun pålæsning*. **Opsætning med flere stalde:** én traktor (eller flere) pr. stald med *Rotér mål: kun pålæsning*.
Brug IKKE "pålæsning og aflæsning": AutoDrive vælger næste pålæsning FØR den roterer aflæsningsmålet, Brug IKKE "pålæsning og aflæsning": AutoDrive vælger næste pålæsning FØR den roterer aflæsningsmålet,
så behovet ville blive regnet for den forkerte stald. Pålæsningsmarkøren må ikke være fast på en så behovet ville blive regnet for den forkerte stald. Pålæsningsmarkøren må ikke være fast på en
+119 -1
View File
@@ -84,7 +84,9 @@ FS25_AutoDrive = {
AutoDrive = { AutoDrive = {
RT_NONE = RT_NONE, RT_NONE = RT_NONE,
getSetting = function(settingName) return settingName == "rotateTargets" and RT_ONLYPICKUP or "andet" end, getSetting = function(settingName) return settingName == "rotateTargets" and RT_ONLYPICKUP or "andet" end,
getAllFillLevels = function(trailers) return trailers.level end, getAllFillLevels = function(trailers) return trailers.level, trailers.capacity or 0, false, 0 end,
getAllUnits = function(vehicle) return vehicle.trailers or {level = 0} end,
getAllDischargeableUnits = function(vehicle) return vehicle.units or {} end,
}, },
-- opgaven noterer hvad AutoDrive "ser" af indstillinger mens den opdaterer -- opgaven noterer hvad AutoDrive "ser" af indstillinger mens den opdaterer
LoadAtDestinationTask = {update = function(self) LoadAtDestinationTask = {update = function(self)
@@ -93,6 +95,12 @@ FS25_AutoDrive = {
if self.shouldFail then error("task boom") end if self.shouldFail then error("task boom") end
end}, end},
ADMultipleTargetsManager = {pickups = {}, getNextPickup = function() return "ORIGINAL" end}, ADMultipleTargetsManager = {pickups = {}, getNextPickup = function() return "ORIGINAL" end},
-- aflæsningsopgaven: original update tæller bare kald; finished markerer
UnloadAtDestinationTask = {
STATE_PATHPLANNING = 1, STATE_DRIVING = 2,
update = function(self) self.updates = (self.updates or 0) + 1 end,
finished = function(self) self.isFinished = true end,
},
ADGraphManager = { ADGraphManager = {
getMapMarkersInGroup = function(_, group) getMapMarkersInGroup = function(_, group)
local result = {} local result = {}
@@ -142,6 +150,7 @@ local function makeSupplyDriver(firstMarker, secondMarker, fillTypes)
end end
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua") dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adUnloadWait.lua")
dofile("FS25_ADSmartPickup/adSmartPickup.lua") dofile("FS25_ADSmartPickup/adSmartPickup.lua")
listener:update(16) -- installerer hook'en listener:update(16) -- installerer hook'en
local manager = FS25_AutoDrive.ADMultipleTargetsManager local manager = FS25_AutoDrive.ADMultipleTargetsManager
@@ -417,6 +426,115 @@ check("P4 stald: TMR (90 % tom) hentes før halm (10 % tom)", manager:getNextPic
barnLevels[TMR] = 95000 barnLevels[TMR] = 95000
check("P4 stald: halm hentes når TMR er fuld", manager:getNextPickup(cowDriver, false), 21) check("P4 stald: halm hentes når TMR er fuld", manager:getNextPickup(cowDriver, false), 21)
-- ==== Wait-punkter: rest-læs + fuld stald -> vent ved nærmeste ledige Wait, tilbage når resten passer ====
local UnloadTask = FS25_AutoDrive.UnloadAtDestinationTask
local waitBarnLevels, waitBarnCapacity = {}, {[STRAW] = 50000, [TMR] = 100000}
local function buildWaitWorld()
buildCowWorld()
waitBarnLevels = {[STRAW] = 50000, [TMR] = 10000} -- halm FULD
local barn2 = {
getName = function() return "Kostald" end,
getHusbandryFillLevel = function(_, fillType) return waitBarnLevels[fillType] or 0 end,
getHusbandryCapacity = function(_, fillType) return waitBarnCapacity[fillType] or 0 end,
getHusbandryFreeCapacity = function(_, fillType) return (waitBarnCapacity[fillType] or 0) - (waitBarnLevels[fillType] or 0) end,
getHusbandryIsFillTypeSupported = function(_, fillType) return waitBarnCapacity[fillType] ~= nil end,
}
unloadStations[1].targetStorages = nil
unloadStations[1].owningPlaceable = barn2
addMarker(40, "Wait 1", "Cow Food", 5100, 0)
addMarker(41, "Wait 2", "Cow Food", 5300, 0)
end
-- traktor der holder ved stalden (markør 30) med rest-læs halm på vognen
local function makeUnloadingVehicle(name, node, x, restStraw)
local vehicle = makeSupplyDriver(markerByIndex(21), markerByIndex(30), {TMR, STRAW})
vehicle.getName = function() return name end
vehicle.rootNode = node
nodePositions[node] = {x = x, z = 10}
vehicle.units = {{object = {
getFillUnitFillLevel = function() return restStraw end,
getFillUnitFillType = function() return STRAW end,
}, fillUnitIndex = 1}}
vehicle.trailers = {level = restStraw, capacity = 40000}
vehicle.targetReached = true
vehicle.ad.drivePathModule = {
isTargetReached = function() return vehicle.targetReached end,
setPathTo = function(self, id) self.pathTo = id end,
}
vehicle.ad.trailerModule = {isActiveAtTrigger = function() return false end}
local task = {vehicle = vehicle, destinationID = 30, state = UnloadTask.STATE_DRIVING, trailers = vehicle.trailers}
return vehicle, task
end
-- W1: fuld stald + rest-læs -> omdirigeres til nærmeste ledige Wait; AD's original kaldes stadig
buildWaitWorld()
local v1, t1 = makeUnloadingVehicle("Fastrac A", 501, 5000, 35000)
local waitLogMark = #logLines
UnloadTask.update(t1, 16)
check("W1 omdirigeret til nærmeste Wait", t1.destinationID, 40)
check("W1 rute lagt til Wait", v1.ad.drivePathModule.pathTo, 40)
check("W1 original update kaldt", t1.updates, 1)
check("W1 log fortæller om ventning", countLogsSince(waitLogMark, "Wait 1"), 1)
-- W2: anden traktor i samme situation -> Wait 1 er optaget -> Wait 2
local v2, t2 = makeUnloadingVehicle("Fastrac B", 502, 5010, 20000)
UnloadTask.update(t2, 16)
check("W2 optaget Wait springes over", t2.destinationID, 41)
-- W3: mens A venter, skjules rotationen (ellers kører AD videre fra Wait-punktet ved "rotér aflæsning")
check("W3 rotation skjult for ventende", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v1), RT_NONE)
check("W3 andre indstillinger urørt", FS25_AutoDrive.AutoDrive.getSetting("useFolders", v1), "andet")
-- W4: stalden får plads til HELE resten -> A sendes hjem, Wait 1 frigives, rotation tilbage
waitBarnLevels[STRAW] = 10000 -- 40000 fri >= 35000
v1.targetReached = true -- står ved Wait 1
UnloadTask.update(t1, 16)
check("W4 sendt tilbage til stalden", t1.destinationID, 30)
check("W4 rute lagt hjem", v1.ad.drivePathModule.pathTo, 30)
check("W4 rotation gendannet", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v1), RT_ONLYPICKUP)
local v3, t3 = makeUnloadingVehicle("Fastrac C", 503, 5020, 30000)
waitBarnLevels[STRAW] = 50000
UnloadTask.update(t3, 16)
check("W4 Wait 1 er ledig igen", t3.destinationID, 40)
-- W5: plads til noget, men ikke hele resten -> B bliver ved Wait (kun hele læs kommer af)
waitBarnLevels[STRAW] = 45000 -- 5000 fri < 20000
UnloadTask.update(t2, 16)
check("W5 delvis plads -> venter stadig", t2.destinationID, 41)
-- W6: stalden har plads ved ankomst -> ingen omdirigering (AD læsser af som normalt)
buildWaitWorld()
waitBarnLevels[STRAW] = 10000
local v6, t6 = makeUnloadingVehicle("Fastrac D", 506, 5000, 35000)
UnloadTask.update(t6, 16)
check("W6 plads ved ankomst -> uændret mål", t6.destinationID, 30)
-- W7: ingen Wait-punkter -> uændret (AD venter som i dag) + én advarsel
buildCowWorld()
unloadStations[1].targetStorages = nil
unloadStations[1].owningPlaceable = { getHusbandryFillLevel = function() return 50000 end, getHusbandryCapacity = function() return 50000 end,
getHusbandryFreeCapacity = function() return 0 end, getHusbandryIsFillTypeSupported = function() return true end }
local v7, t7 = makeUnloadingVehicle("Fastrac E", 507, 5000, 35000)
local warnMark = #logLines
UnloadTask.update(t7, 16); UnloadTask.update(t7, 16)
check("W7 uden Wait-punkter -> uændret mål", t7.destinationID, 30)
check("W7 én advarsel", countLogsSince(warnMark, "WARN"), 1)
-- W8: B og C venter stadig og holder begge punkter; stoppes de i AD, frigives punkterne
buildWaitWorld()
local v8, t8 = makeUnloadingVehicle("Fastrac F", 508, 5000, 35000)
UnloadTask.update(t8, 16)
check("W8 alle Wait-punkter optaget -> uændret", t8.destinationID, 30)
v2.ad.stateModule.active, v3.ad.stateModule.active = false, false
listener:update(16)
UnloadTask.update(t8, 16)
check("W8 venter ved Wait 1 efter frigivelse", t8.destinationID, 40)
UnloadTask.finished(t8)
check("W8 original finished kaldt", t8.isFinished, true)
local v9, t9 = makeUnloadingVehicle("Fastrac G", 509, 5000, 35000)
UnloadTask.update(t9, 16)
check("W8 Wait 1 frigivet efter finished", t9.destinationID, 40)
check("W8 rotation gendannet efter finished", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v8), RT_ONLYPICKUP)
-- L1: moden valgte siloen -> AutoDrive skal stå og læsse (rotation skjules under opgaven) -- L1: moden valgte siloen -> AutoDrive skal stå og læsse (rotation skjules under opgaven)
resetWorld() resetWorld()
local loadLevels = {[SUGARBEET] = 5000} local loadLevels = {[SUGARBEET] = 5000}
+36
View File
@@ -122,5 +122,41 @@ check("vare fabrikken ikke kender -> ikke beskyttet", ADSupplyPlanner.isProducti
check("ingen produktioner -> ikke beskyttet", ADSupplyPlanner.isProductionInput({}, STRAW), false) check("ingen produktioner -> ikke beskyttet", ADSupplyPlanner.isProductionInput({}, STRAW), false)
check("produktion uden inputs/outputs tåles", ADSupplyPlanner.isProductionInput({{id = "x"}}, STRAW), false) check("produktion uden inputs/outputs tåles", ADSupplyPlanner.isProductionInput({{id = "x"}}, STRAW), false)
-- rankNeeds med læs-størrelse: varer hvor et FULDT læs passer går forud, ellers mest trængende
local sized = ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500}, -- 80 min, kun 10000 fri
{fillType = TMR, level = 20000, capacity = 100000, litersPerMinute = 50}, -- 400 min, 80000 fri
}, 40000)
check("fuldt læs der passer vinder over mere trængende der ikke passer", sized[1].fillType, TMR)
check("den der ikke passer bevares som reserve", sized[2].fillType, STRAW)
check("fits markeres", sized[1].fits, true)
check("ikke-fits markeres", sized[2].fits, false)
local noneFit = ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 40000, capacity = 50000, litersPerMinute = 500},
{fillType = TMR, level = 90000, capacity = 100000, litersPerMinute = 50},
}, 40000)
check("passer intet -> mest trængende først", noneFit[1].fillType, STRAW)
check("uden læs-størrelse -> som før", ADSupplyPlanner.rankNeeds({{fillType = STRAW, level = 40000, capacity = 50000}})[1].fits, nil)
-- Wait-punkter: navn med ordet "Wait" (uanset store/små) eller mappen "Wait"
check("'Cow 1 Food Wait' er wait-punkt", ADSupplyPlanner.isWaitMarker({name = "Cow 1 Food Wait", group = "Cow Food"}), true)
check("'wait 3' er wait-punkt", ADSupplyPlanner.isWaitMarker({name = "wait 3", group = "Andet"}), true)
check("mappen Wait tæller", ADSupplyPlanner.isWaitMarker({name = "P2", group = "Wait"}), true)
check("'Waiter' er ikke wait-punkt", ADSupplyPlanner.isWaitMarker({name = "Waiter", group = "X"}), false)
check("'Cow 1 Food' er ikke wait-punkt", ADSupplyPlanner.isWaitMarker({name = "Cow 1 Food", group = "Cow Food"}), false)
-- nærmeste ledige wait-punkt; candidates = {{marker, x, z}}, claims = {[markerId] = vehicle}
local V1, V2 = {}, {}
local waitPoints = {
{marker = {id = 1, name = "Wait A"}, x = 0, z = 0},
{marker = {id = 2, name = "Wait B"}, x = 100, z = 0},
{marker = {id = 3, name = "Wait C"}, x = 300, z = 0},
}
check("nærmeste ledige vælges", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1).marker.id, 1)
check("reserveret springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2}, 10, 0, V1).marker.id, 2)
check("egen reservation tæller som ledig", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1).marker.id, 1)
check("alle optaget -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2, [2] = V2, [3] = V2}, 10, 0, V1), nil)
check("ingen punkter -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint({}, {}, 0, 0, V1), nil)
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)