diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index 7923361..9ce546c 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -206,6 +206,30 @@ local function getFillTypeNames(fillTypes) return names 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 -- 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. @@ -214,7 +238,8 @@ local function findBestSource(markerStations, fillTypes, farmId) local best = {[true] = {}, [false] = {}} for _, entry in ipairs(markerStations) do 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} end end @@ -250,14 +275,6 @@ function ADSmartPickup.findLeastFilledPickup(adEnv, driver) return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId) 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. local function getTargetStorages(station) if station.targetStorages ~= nil and next(station.targetStorages) ~= nil then @@ -323,12 +340,33 @@ local function logFactoryFallbackOnce(driver, station) 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. local function getTargetNeeds(station, ingredients, farmId, demand) local needs = {} local storages = getTargetStorages(station) + local husbandry = getHusbandry(station) 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 for _, storage in pairs(storages) do level = level + (storage:getFillLevel(fillType) or 0) diff --git a/FS25_ADSmartPickup/adSupplyPlanner.lua b/FS25_ADSmartPickup/adSupplyPlanner.lua index 1c44d48..53a026d 100644 --- a/FS25_ADSmartPickup/adSupplyPlanner.lua +++ b/FS25_ADSmartPickup/adSupplyPlanner.lua @@ -76,6 +76,21 @@ function ADSupplyPlanner.collectProductionDemand(productions, isWanted, canCarry return demand 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. function ADSupplyPlanner.minutesToEmpty(level, litersPerMinute) if litersPerMinute == nil or litersPerMinute <= 0 then diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 9de90c1..cb1a711 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.6.0.0 + 1.6.1.0 <en>AD Smart Pickup</en> diff --git a/README.md b/README.md index b21a6ed..e02acf0 100644 --- a/README.md +++ b/README.md @@ -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. - 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 - Wrapper `ADMultipleTargetsManager.getNextPickup` (via den globale `FS25_AutoDrive`-tabel); rører ikke AutoDrive-zip'en. diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index b66b99b..f84e3cb 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -355,6 +355,68 @@ fallbackPp.getIsProductionEnabled = nil fallbackPp.activeProductions = {fallbackPp.productions[1]} 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) resetWorld() local loadLevels = {[SUGARBEET] = 5000} diff --git a/tests/test_adSupplyPlanner.lua b/tests/test_adSupplyPlanner.lua index 123d91f..e33e1ea 100644 --- a/tests/test_adSupplyPlanner.lua +++ b/tests/test_adSupplyPlanner.lua @@ -108,5 +108,19 @@ check("fuldt mål springes over trods forbrug", #ADSupplyPlanner.rankNeeds({ {fillType = STRAW, level = 99500, capacity = 100000, litersPerMinute = 20}, }), 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")) os.exit(failures == 0 and 0 or 1)