fix: en fabriks inputs stjæles aldrig som kilde; stalde læses via getHusbandry* (v1.6.1.0)

In-game 2026-09-22: fire Fastrac'er med halm+TMR til to kostalde kørte med
'Rotér mål: kun aflæsning' og fast pålæsning ved 'TMR Load' -> AutoDrive læssede
halm (fabrikkens input) dér uden at modden var i spil. Med modden i spil ville
'mindst fyldte først' have valgt samme lager.

- ADSupplyPlanner.isProductionInput: vare der er input (og ikke output) i en
  fabriks produktioner udelukkes som kilde i findBestSource (mappe + behov).
- Stald som mål: niveau/kapacitet/understøttelse fra placeable:getHusbandry*
  (stationen selv siger nej til halm) -> halm rangeres og hentes.
- README: to stalde = kun pålæsnings-rotation (AD vælger pickup FØR deliver roteres).
This commit is contained in:
2026-09-22 20:45:43 +02:00
parent 795c03663c
commit 2ff0b641b0
6 changed files with 156 additions and 11 deletions
+48 -10
View File
@@ -206,6 +206,30 @@ local function getFillTypeNames(fillTypes)
return names return names
end end
local function getProductionPoint(station)
local placeable = station.owningPlaceable
if placeable ~= nil and placeable.spec_productionPoint ~= nil then
return placeable.spec_productionPoint.productionPoint
end
return nil
end
-- En fabriks læssestation udleverer også dens INPUTS (fx halm i en TMR-fabrik).
-- Dem stjæler vi aldrig — fabrikken ville løbe tør for det, den skal bruge.
-- Outputs (og varer der både er input og output i en kæde) må gerne hentes.
local function isProtectedProductionInput(station, fillTypes)
local productionPoint = getProductionPoint(station)
if productionPoint == nil or productionPoint.productions == nil then
return false
end
for _, fillType in ipairs(fillTypes) do
if ADSupplyPlanner.isProductionInput(productionPoint.productions, fillType) then
return true
end
end
return false
end
-- Bedste kilde der har varen: mindst fyldte først for lagervarer, fyldeste først for -- 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 -- 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. -- altid forud for dem der kun kan låne fra naboer. Returnerer et valg eller nil.
@@ -214,7 +238,8 @@ local function findBestSource(markerStations, fillTypes, farmId)
local best = {[true] = {}, [false] = {}} 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 ADSupplyPlanner.isBetterSource(level, best[isOwn].level, prefersFullest) then if level >= ADSmartPickup.MIN_USEFUL_LITERS and not isProtectedProductionInput(entry.station, fillTypes)
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
@@ -250,14 +275,6 @@ function ADSmartPickup.findLeastFilledPickup(adEnv, driver)
return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId) return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId)
end end
local function getProductionPoint(station)
local placeable = station.owningPlaceable
if placeable ~= nil and placeable.spec_productionPoint ~= nil then
return placeable.spec_productionPoint.productionPoint
end
return nil
end
-- Lagrene bag aflæsningsstationen; produktionspunkter uden targetStorages læses direkte. -- Lagrene bag aflæsningsstationen; produktionspunkter uden targetStorages læses direkte.
local function getTargetStorages(station) local function getTargetStorages(station)
if station.targetStorages ~= nil and next(station.targetStorages) ~= nil then if station.targetStorages ~= nil and next(station.targetStorages) ~= nil then
@@ -323,12 +340,33 @@ local function logFactoryFallbackOnce(driver, station)
end end
end end
-- Stalde (kostald m.fl.) holder foder/halm/vand i egne lagre bag én aflæsningsstation,
-- der ikke nødvendigvis siger ja til alle varer. Stalden selv svarer for alle.
local function getHusbandry(station)
local placeable = station.owningPlaceable
if placeable ~= nil and placeable.getHusbandryFillLevel ~= nil and placeable.getHusbandryCapacity ~= nil then
return placeable
end
return nil
end
local function isFillTypeAccepted(station, husbandry, fillType)
if husbandry ~= nil and husbandry.getHusbandryIsFillTypeSupported ~= nil then
return husbandry:getHusbandryIsFillTypeSupported(fillType)
end
return station.getIsFillTypeSupported == nil or station:getIsFillTypeSupported(fillType)
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 storages = getTargetStorages(station)
local husbandry = getHusbandry(station)
for _, fillType in ipairs(ingredients) do for _, fillType in ipairs(ingredients) do
if station.getIsFillTypeSupported == nil or station:getIsFillTypeSupported(fillType) then 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 local level, freeSpace = 0, 0
for _, storage in pairs(storages) do for _, storage in pairs(storages) do
level = level + (storage:getFillLevel(fillType) or 0) level = level + (storage:getFillLevel(fillType) or 0)
+15
View File
@@ -76,6 +76,21 @@ function ADSupplyPlanner.collectProductionDemand(productions, isWanted, canCarry
return demand return demand
end end
-- Er varen et INPUT til fabrikken (og ikke også et output)? Så må den aldrig
-- hentes derfra — det ville tømme fabrikken for det, den skal bruge.
function ADSupplyPlanner.isProductionInput(productions, fillType)
local isInput, isOutput = false, false
for _, production in ipairs(productions) do
for _, input in ipairs(production.inputs or {}) do
if input.type == fillType then isInput = true end
end
for _, output in ipairs(production.outputs or {}) do
if output.type == fillType then isOutput = true end
end
end
return isInput and not isOutput
end
-- Minutter før målets lager af varen er tomt ved nuværende forbrug; nil uden forbrug. -- Minutter før målets lager af varen er tomt ved nuværende forbrug; nil uden forbrug.
function ADSupplyPlanner.minutesToEmpty(level, litersPerMinute) function ADSupplyPlanner.minutesToEmpty(level, litersPerMinute)
if litersPerMinute == nil or litersPerMinute <= 0 then if litersPerMinute == nil or litersPerMinute <= 0 then
+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.6.0.0</version> <version>1.6.1.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
+16
View File
@@ -38,6 +38,22 @@ tændknappen (og det gendannes ved stop). Listen genudledes hver tur, begrænset
den vare der løber tør først hentes først, uanset relativ fyldning. Loggen viser forbrug og minutter. den vare der løber tør først hentes først, uanset relativ fyldning. Loggen viser forbrug og minutter.
- Fabrik uden ønskede produktioner, eller inputs vognen ikke kan bære → dit multi-valg som hidtil. - Fabrik uden ønskede produktioner, eller inputs vognen ikke kan bære → dit multi-valg som hidtil.
### Kilder: en fabriks inputs stjæles aldrig (v1.6.1)
En fabriks læssestation udleverer også dens **inputs** (halm i en TMR-fabrik). Sådan en vare hentes
aldrig derfra — kun fabrikkens outputs og rigtige siloer er kilder. Gælder både mappe-logik og
behovsstyring. Varer der både er input og output (kæde) må hentes.
### Stalde som mål (v1.6.1)
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.
**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,
så behovet ville blive regnet for den forkerte stald. Pålæsningsmarkøren må ikke være fast på en
fabrik ("kun aflæsning") — så kalder AutoDrive aldrig modden, og alt valgt læsses dér.
## Fælles ## Fælles
- Wrapper `ADMultipleTargetsManager.getNextPickup` (via den globale `FS25_AutoDrive`-tabel); rører ikke AutoDrive-zip'en. - Wrapper `ADMultipleTargetsManager.getNextPickup` (via den globale `FS25_AutoDrive`-tabel); rører ikke AutoDrive-zip'en.
+62
View File
@@ -355,6 +355,68 @@ fallbackPp.getIsProductionEnabled = nil
fallbackPp.activeProductions = {fallbackPp.productions[1]} fallbackPp.activeProductions = {fallbackPp.productions[1]}
check("F6 activeProductions som fallback", manager:getNextPickup(fallbackDriver, false), 4) check("F6 activeProductions som fallback", manager:getNextPickup(fallbackDriver, false), 4)
-- ==== En fabriks inputs stjæles aldrig (halm+TMR til kostald; TMR-fabrikken har halm som input) ====
local STRAW, TMR = 41, 42
FILL_TYPE_NAMES[STRAW], FILL_TYPE_NAMES[TMR] = "STRAW", "FORAGE"
local cowLevels
local function buildCowWorld()
resetWorld()
local tmrFactory = {
getName = function() return "TMR-fabrik" end,
spec_productionPoint = {productionPoint = {productions = {
{id = "tmr", cyclesPerMinute = 1, inputs = {{type = STRAW, amount = 10}, {type = SILAGE, amount = 100}}, outputs = {{type = TMR, amount = 50}}},
}}},
}
-- fabrikkens læssestation har BÅDE halm (input, mindst fyldt) og TMR (output)
addMarker(20, "TMR Load", "Fabrication", 0, 0)
table.insert(stations, makeStation(120, 0, 15, {[STRAW] = 5000, [TMR] = 30000}))
stations[#stations].owningPlaceable = tmrFactory
addMarker(21, "Halmlade", "Lader", 500, 0)
table.insert(stations, makeStation(121, 500, 15, {[STRAW] = 50000}))
addMarker(30, "Cow 1 Food", "Cow Food", 5000, 0)
cowLevels = {[STRAW] = 0, [TMR] = 40000}
table.insert(unloadStations, makeUnloadStation(130, 5000, 10, cowLevels, {[STRAW] = 100000, [TMR] = 100000}, {name = "kostald"}))
local driver = makeSupplyDriver(markerByIndex(21), markerByIndex(30), {TMR, STRAW})
driver.carriable = {TMR, STRAW}
return driver
end
-- P1: halm mangler mest -> hentes fra halmladen, IKKE fra fabrikken (selv om den er mindst fyldt)
local cowDriver = buildCowWorld()
check("P1 halm hentes fra siloen, ikke fabrikkens input", manager:getNextPickup(cowDriver, false), 21)
-- P2: halm fuld -> TMR hentes gerne fra fabrikken (det er dens output)
cowLevels[STRAW] = 99500
check("P2 fabrikkens output må hentes", manager:getNextPickup(cowDriver, false), 20)
-- P3: mappe-logik (én vare) beskytter også fabrikkens input
cowDriver = buildCowWorld()
addMarker(22, "Halmlade 2", "Fabrication", 1000, 0)
table.insert(stations, makeStation(122, 1000, 15, {[STRAW] = 60000}))
local strawDriver = makeDriver(markerByIndex(20), {STRAW})
check("P3 mappe-logik springer fabrikkens input over", manager:getNextPickup(strawDriver, false), 22)
-- P4: stald som mål — niveau/kapacitet læses fra stalden selv (getHusbandry*), ikke fra
-- stationens lagre; stationen selv "understøtter" kun foder, men stalden tager også halm
cowDriver = buildCowWorld()
local barnLevels = {[STRAW] = 45000, [TMR] = 10000}
local barnCapacity = {[STRAW] = 50000, [TMR] = 100000}
local barn = {
getName = function() return "Kostald" end,
getHusbandryFillLevel = function(_, fillType) return barnLevels[fillType] or 0 end,
getHusbandryCapacity = function(_, fillType) return barnCapacity[fillType] or 0 end,
getHusbandryFreeCapacity = function(_, fillType) return (barnCapacity[fillType] or 0) - (barnLevels[fillType] or 0) end,
getHusbandryIsFillTypeSupported = function(_, fillType) return barnCapacity[fillType] ~= nil end,
}
local barnStation = unloadStations[1]
barnStation.targetStorages = nil
barnStation.owningPlaceable = barn
barnStation.getIsFillTypeSupported = function(_, fillType) return fillType == TMR end
barnStation.getFreeCapacity = function(_, fillType) return 999999 end
check("P4 stald: TMR (90 % tom) hentes før halm (10 % tom)", manager:getNextPickup(cowDriver, false), 20)
barnLevels[TMR] = 95000
check("P4 stald: halm hentes når TMR er fuld", manager:getNextPickup(cowDriver, false), 21)
-- 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}
+14
View File
@@ -108,5 +108,19 @@ check("fuldt mål springes over trods forbrug", #ADSupplyPlanner.rankNeeds({
{fillType = STRAW, level = 99500, capacity = 100000, litersPerMinute = 20}, {fillType = STRAW, level = 99500, capacity = 100000, litersPerMinute = 20},
}), 0) }), 0)
-- isProductionInput: en fabriks input må aldrig stjæles som kilde — medmindre
-- fabrikken også selv producerer varen (så er den et output, der må hentes)
local TMR = 26
local factory = {
{id = "tmr", inputs = {{type = SILAGE, amount = 100}, {type = STRAW, amount = 10}}, outputs = {{type = TMR, amount = 50}}},
{id = "pellets", inputs = {{type = TMR, amount = 10}}, outputs = {{type = HAY, amount = 5}}},
}
check("input til fabrikken er beskyttet", ADSupplyPlanner.isProductionInput(factory, STRAW), true)
check("output må hentes", ADSupplyPlanner.isProductionInput(factory, HAY), false)
check("både input og output (kæde) -> må hentes", ADSupplyPlanner.isProductionInput(factory, TMR), false)
check("vare fabrikken ikke kender -> ikke beskyttet", ADSupplyPlanner.isProductionInput(factory, WATER), false)
check("ingen produktioner -> ikke beskyttet", ADSupplyPlanner.isProductionInput({}, STRAW), false)
check("produktion uden inputs/outputs tåles", ADSupplyPlanner.isProductionInput({{id = "x"}}, STRAW), false)
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)