feat: fyldeste kilde først for mælk/gylle/digestat (v1.5.0.0)

Lagervarer tømmes stadig mindst-fyldte-først; varer hvis tanke løber fulde og
stopper produktionen hentes fyldest-først (ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES).
Strategien vises i loggen.

fix: en trigger foran næsen tæller aldrig som markørens egen (tolerance 2 -> 0).
Markør-drift i ensilage-rækken lod næste tanks rør (1,9 m foran) vinde.
Verificeret mod savegame: 40/40 siloer, 7/7 ensilage-tanke, 0 dropof.
This commit is contained in:
2026-09-21 16:34:19 +02:00
parent e6412d78e4
commit 8c76790ece
6 changed files with 97 additions and 11 deletions
+26 -9
View File
@@ -14,8 +14,9 @@ ADSmartPickup = {}
-- vogntog bagved. Siloer tættere end dette regnes som "markørens silo". -- vogntog bagved. Siloer tættere end dette regnes som "markørens silo".
ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE = 40 ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE = 40
-- I tætte silo-rækker ligger nabosiloens trigger FORAN næsen og er ofte nærmere -- I tætte silo-rækker ligger nabosiloens trigger FORAN næsen og er ofte nærmere
-- end markørens egen. Triggere mere end dette foran næsen hører til naboen. -- end markørens egen. Vognen holder altid BAG næsen, så en trigger foran næsen
ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER = 2 -- hører til naboen (en ensom silo med markøren ved røret fanges af nødløsningen).
ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER = 0
-- Vognen følger trækkerens spor: en trigger længere til siden end dette er en -- Vognen følger trækkerens spor: en trigger længere til siden end dette er en
-- anden bane (fx aflæsningssiden af samme tank), ikke markørens læsseplads. -- anden bane (fx aflæsningssiden af samme tank), ikke markørens læsseplads.
ADSmartPickup.MAX_TRIGGER_LATERAL_OFFSET = 6 ADSmartPickup.MAX_TRIGGER_LATERAL_OFFSET = 6
@@ -194,13 +195,26 @@ local function collectMarkerStations(adEnv, candidateMarkers, farmId, excludedPl
return markerStations return markerStations
end end
-- Mindst fyldte silo der har varen; siloer med varen i EGET lager går altid local function getFillTypeNames(fillTypes)
-- forud for dem der kun kan låne fra naboer. Returnerer et valg eller nil. local names = {}
local function findLeastFilled(markerStations, fillTypes, farmId) for _, fillType in ipairs(fillTypes) do
local best = {[true] = {level = math.huge}, [false] = {level = math.huge}} local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or nil
if name ~= nil then
table.insert(names, name)
end
end
return names
end
-- Bedste kilde der har varen: mindst fyldte først for lagervarer, fyldeste først for
-- varer hvis tanke løber fulde (ADSupplyPlanner). Siloer med varen i EGET lager går
-- altid forud for dem der kun kan låne fra naboer. Returnerer et valg eller nil.
local function findBestSource(markerStations, fillTypes, farmId)
local prefersFullest = ADSupplyPlanner.prefersFullestSource(getFillTypeNames(fillTypes))
local best = {[true] = {}, [false] = {}}
for _, entry in ipairs(markerStations) do for _, entry in ipairs(markerStations) do
local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId) local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId)
if level >= ADSmartPickup.MIN_USEFUL_LITERS and level < best[isOwn].level then if level >= ADSmartPickup.MIN_USEFUL_LITERS and ADSupplyPlanner.isBetterSource(level, best[isOwn].level, prefersFullest) then
best[isOwn] = {level = level, marker = entry.marker, station = entry.station, isOwn = isOwn} best[isOwn] = {level = level, marker = entry.marker, station = entry.station, isOwn = isOwn}
end end
end end
@@ -217,6 +231,7 @@ local function findLeastFilled(markerStations, fillTypes, farmId)
station = choice.station, station = choice.station,
isOwn = choice.isOwn, isOwn = choice.isOwn,
fillTypes = fillTypes, fillTypes = fillTypes,
prefersFullest = prefersFullest,
} }
end end
@@ -232,7 +247,7 @@ function ADSmartPickup.findLeastFilledPickup(adEnv, driver)
return nil return nil
end end
local groupMarkers = adEnv.ADGraphManager:getMapMarkersInGroup(firstMarker.group) local groupMarkers = adEnv.ADGraphManager:getMapMarkersInGroup(firstMarker.group)
return findLeastFilled(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId) return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId)
end end
-- Lagrene bag aflæsningsstationen; produktionspunkter uden targetStorages læses direkte. -- Lagrene bag aflæsningsstationen; produktionspunkter uden targetStorages læses direkte.
@@ -285,7 +300,7 @@ 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))) do for _, need in ipairs(ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId))) do
local choice = findLeastFilled(markerStations, {need.fillType}, farmId) local choice = findBestSource(markerStations, {need.fillType}, farmId)
if choice ~= nil then if choice ~= nil then
return { return {
markerIndex = choice.markerIndex, markerIndex = choice.markerIndex,
@@ -295,6 +310,7 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)
station = choice.station, station = choice.station,
isOwn = choice.isOwn, isOwn = choice.isOwn,
fillTypes = choice.fillTypes, fillTypes = choice.fillTypes,
prefersFullest = choice.prefersFullest,
fillType = need.fillType, fillType = need.fillType,
need = need, need = need,
} }
@@ -330,6 +346,7 @@ end
local function describeChoice(choice) local function describeChoice(choice)
local source = choice.isOwn and "eget lager" or "nabo-lager" local source = choice.isOwn and "eget lager" or "nabo-lager"
source = source .. (choice.prefersFullest and ", fyldeste først" or ", mindst fyldte først")
if choice.fillType == nil then if choice.fillType == nil then
return string.format("%.0f l, %s", choice.level, source) return string.format("%.0f l, %s", choice.level, source)
end end
+33
View File
@@ -7,6 +7,39 @@ ADSupplyPlanner = {}
-- Mindre fri plads end dette i målet regnes som "fuldt" (undgår ture med læs der ikke kan komme af). -- Mindre fri plads end dette i målet regnes som "fuldt" (undgår ture med læs der ikke kan komme af).
ADSupplyPlanner.MIN_FREE_LITERS = 1000 ADSupplyPlanner.MIN_FREE_LITERS = 1000
-- Varer hvis kilder LØBER FULDE (dyr/produktion fylder tanken løbende og går i stå
-- når den er fuld): her hentes den fyldeste kilde først. Alt andet er lagervarer,
-- hvor den mindst fyldte silo tømmes først så rester ryddes og siloer frigives.
ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES = {
MILK = true, GOATMILK = true, BUFFALOMILK = true, SHEEPMILK = true,
LIQUIDMANURE = true, DIGESTATE = true,
}
-- fillTypeNames: navnene på de varer der hentes på turen. Kun når ALLE er
-- "løber fuld"-varer vendes strategien; blandet eller ukendt valg = mindst først.
function ADSupplyPlanner.prefersFullestSource(fillTypeNames)
if #fillTypeNames == 0 then
return false
end
for _, name in ipairs(fillTypeNames) do
if not ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES[name] then
return false
end
end
return true
end
-- Er en kilde med dette niveau bedre end den hidtil bedste (nil = ingen endnu)?
function ADSupplyPlanner.isBetterSource(level, bestLevel, prefersFullest)
if bestLevel == nil then
return true
end
if prefersFullest then
return level > bestLevel
end
return level < bestLevel
end
function ADSupplyPlanner.copyList(list) function ADSupplyPlanner.copyList(list)
local copy = {} local copy = {}
for index, value in ipairs(list) do for index, value in ipairs(list) do
+1 -1
View File
@@ -1,7 +1,7 @@
<?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.4.0.0</version> <version>1.5.0.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
+6 -1
View File
@@ -5,7 +5,7 @@ AutoDrive-tillæg til Farming Simulator 25 med to funktioner oven på Pickup&Del
## 1. Vare-bevidst mappe-rotation (én vare valgt) ## 1. Vare-bevidst mappe-rotation (én vare valgt)
Når pålæsningsmål roteres i en AD-mappe, køres der direkte til den silo i mappen, der har den Når pålæsningsmål roteres i en AD-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. valgte vare — mindst fyldte først (mælk/gylle: fyldeste først) — i stedet for at prøve markørerne én efter én.
## 2. Behovsstyret forsyning (flere varer valgt) ## 2. Behovsstyret forsyning (flere varer valgt)
@@ -34,6 +34,11 @@ Slå fra ved at vælge en anden enkelt vare.
bagved. Markørens silo er derfor nærmeste trigger **bag næsen** set i ankomstretningen (forrige bagved. Markørens silo er derfor nærmeste trigger **bag næsen** set i ankomstretningen (forrige
waypoint), højst 2 m foran. I tætte rækker (15 m mellem siloerne) er nabosiloens trigger ellers waypoint), højst 2 m foran. I tætte rækker (15 m mellem siloerne) er nabosiloens trigger ellers
nærmere end markørens egen. Ingen trigger bag næsen/ukendt retning → nærmeste trigger som før. nærmere end markørens egen. Ingen trigger bag næsen/ukendt retning → nærmeste trigger som før.
- **Kildestrategi (v1.5):** lagervarer hentes fra den **mindst fyldte** silo først (rester ryddes,
siloer frigives). Varer hvis tanke *løber fulde* — mælk, gylle, digestat — hentes fra den **fyldeste**
kilde først, så produktionen ikke går i stå. Listen er `ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES`
(`adSupplyPlanner.lua`); tilføj fx `MANURE = true` hvis møddinger også skal tømmes fyldest-først.
Loggen viser den brugte strategi. En trigger foran næsen tæller aldrig som markørens egen (v1.5).
- **Spor-regel (v1.4):** triggeren skal ligge i trækkerens spor (højst 6 m til siden). En tank med - **Spor-regel (v1.4):** triggeren skal ligge i trækkerens spor (højst 6 m til siden). En tank med
aflæsningsbane på den ene side og pålæsningsbane på den anden giver ellers to markører til samme aflæsningsbane på den ene side og pålæsningsbane på den anden giver ellers to markører til samme
tank, og "Dropof"-markøren kunne vinde. Kendes retningen, men ligger intet i sporet (kurve ind til tank, og "Dropof"-markøren kunne vinde. Kendes retningen, men ligger intet i sporet (kurve ind til
+20
View File
@@ -3,6 +3,9 @@
local SUGARBEET, WHEAT = 10, 11 local SUGARBEET, WHEAT = 10, 11
local HAY, GRASS, SILAGE = 20, 21, 22 local HAY, GRASS, SILAGE = 20, 21, 22
local MILK = 30
local FILL_TYPE_NAMES = {[SUGARBEET] = "SUGARBEET", [WHEAT] = "WHEAT", [HAY] = "DRYGRASS_WINDROW", [GRASS] = "GRASS_WINDROW", [SILAGE] = "SILAGE", [MILK] = "MILK"}
g_fillTypeManager = {getFillTypeNameByIndex = function(_, index) return FILL_TYPE_NAMES[index] end}
local logLines = {} local logLines = {}
-- ---- FS25-mocks ---- -- ---- FS25-mocks ----
@@ -357,6 +360,15 @@ addMarker(2, "1 Silage Pickup", "Ensilage", 1247, -2278, 1247, -2266) -- køre
table.insert(stations, makeStation(101, 1247, -2271, {[SILAGE] = 4000})) table.insert(stations, makeStation(101, 1247, -2271, {[SILAGE] = 4000}))
check("G3 pickup-markøren vælges, ikke dropoff", manager:getNextPickup(makeDriver(markers[1], {SILAGE}), false), 2) check("G3 pickup-markøren vælges, ikke dropoff", manager:getNextPickup(makeDriver(markers[1], {SILAGE}), false), 2)
-- G5: markør-drift i en tank-række — næste tanks rør ligger 2 m FORAN næsen og er
-- nærmest, men vognen holder under røret 11 m bagved
resetWorld()
addMarker(1, "5 Silage Pickup", "Ensilage", 1246, -2218, 1246, -2230) -- kører +z
table.insert(stations, makeStation(101, 1246, -2216, {[SILAGE] = 9000})) -- 2 m foran næsen
table.insert(stations, makeStation(102, 1246, -2229, {[HAY] = 4000})) -- 11 m bag næsen: kun den har hø
addMarker(2, "6 Silage Pickup", "Ensilage", 1246, -2208, 1246, -2218)
check("G5 markør 5 hører til tanken bag næsen", manager:getNextPickup(makeDriver(markers[1], {HAY}), false), 1)
-- G4: kurve ind til markøren -> retningen snyder og røret ligger "foran"; tæt på = ok -- G4: kurve ind til markøren -> retningen snyder og røret ligger "foran"; tæt på = ok
resetWorld() resetWorld()
addMarker(1, "7 Silage Pickup", "Ensilage", 100, 0, 100, 20) -- kører -z addMarker(1, "7 Silage Pickup", "Ensilage", 100, 0, 100, 20) -- kører -z
@@ -380,6 +392,14 @@ check("L9 sluppet greb logges som advarsel", logLines[#logLines]:sub(1, 4), "WAR
FS25_AutoDrive.LoadAtDestinationTask.update(stuckTask, 16) FS25_AutoDrive.LoadAtDestinationTask.update(stuckTask, 16)
check("L9 grebet tages ikke igen på samme tur", stuckTask.seenRotate, RT_ONLYPICKUP) check("L9 grebet tages ikke igen på samme tur", stuckTask.seenRotate, RT_ONLYPICKUP)
-- M1: mælketanke løber fulde -> den FYLDESTE hentes først (siloer: mindst fyldte)
resetWorld()
addMarker(1, "Kostald 1", "Mælk", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[MILK] = 2000}))
addMarker(2, "Kostald 2", "Mælk", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[MILK] = 48000}))
addMarker(3, "Kostald 3", "Mælk", 1000, 0); table.insert(stations, makeStation(103, 1000, 15, {[MILK] = 30000}))
check("M1 fyldeste mælketank først", manager:getNextPickup(makeDriver(markers[1], {MILK}), false), 2)
check("M1 loggen nævner strategien", logLines[#logLines]:find("fyldeste først", 1, true) ~= nil, true)
-- L7: AD stopper -> grebet ryddes, næste opgave venter ikke på en gammel silo -- L7: AD stopper -> grebet ryddes, næste opgave venter ikke på en gammel silo
resetWorld() resetWorld()
addMarker(1, "Silo 1", "Andet", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[HAY] = 5000})) addMarker(1, "Silo 1", "Andet", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[HAY] = 5000}))
+11
View File
@@ -51,5 +51,16 @@ check("bruger valgte andet -> nil", ADSupplyPlanner.resolveIngredients(state, {H
check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2) check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2)
check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil) check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil)
-- Kildestrategi: lager-siloer tømmes mindst-først; tanke der løber fulde (mælk, gylle) fyldest-først
check("mælk hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"MILK"}), true)
check("gylle hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"LIQUIDMANURE"}), true)
check("roer hentes mindst-først", ADSupplyPlanner.prefersFullestSource({"SUGARBEET"}), false)
check("blandet valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({"MILK", "WHEAT"}), false)
check("ukendt/tomt valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({}), false)
check("mindst-først: lavere niveau er bedre", ADSupplyPlanner.isBetterSource(100, 500, false), true)
check("mindst-først: højere niveau er ikke bedre", ADSupplyPlanner.isBetterSource(900, 500, false), false)
check("fyldest-først: højere niveau er bedre", ADSupplyPlanner.isBetterSource(900, 500, true), true)
check("første kandidat er altid bedre end ingen", ADSupplyPlanner.isBetterSource(900, nil, false), true)
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)