Files
fs25-adsmartpickup/FS25_ADSmartPickup/adSupplyPlanner.lua
T
masterdraco 2ff0b641b0 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).
2026-09-22 20:45:43 +02:00

160 lines
6.1 KiB
Lua

-- AD Supply Planner
-- Rene funktioner (ingen spil-API) til behovsstyret forsyning: hvilken ingrediens
-- har aflæsningsmålet mest brug for, og hvilken ingrediensliste gælder for køretøjet.
ADSupplyPlanner = {}
-- 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
-- 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)
local copy = {}
for index, value in ipairs(list) do
copy[index] = value
end
return copy
end
-- Ønsker en produktion input? Tændt, eller slukket af kortet fordi den MANGLEDE
-- input (Dakotas ExtendedProductionPoint slår den fra automatisk, men lader
-- status stå på MISSING_INPUTS). Manuelt slukket = status INACTIVE -> nej.
function ADSupplyPlanner.isProductionWanted(isEnabled, status, missingInputsStatus)
if isEnabled then
return true
end
return status ~= nil and status == missingInputsStatus
end
-- productions: produktionspunktets liste ({id, cyclesPerMinute, inputs = {{type, amount}}}).
-- isWanted(production) afgør om den tæller; canCarry[fillType] = det vognen kan bære.
-- Returnerer {[fillType] = liter pr. minut} summeret over ønskede produktioner.
function ADSupplyPlanner.collectProductionDemand(productions, isWanted, canCarry)
local demand = {}
for _, production in ipairs(productions) do
if isWanted(production) then
local cyclesPerMinute = production.cyclesPerMinute or 0
for _, input in ipairs(production.inputs or {}) do
if canCarry[input.type] then
demand[input.type] = (demand[input.type] or 0) + (input.amount or 0) * cyclesPerMinute
end
end
end
end
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
return nil
end
return level / litersPerMinute
end
-- Hvor meget trænger målet til denne ingrediens? Højere = hentes først.
-- Alle tal er liter i aflæsningsmålets lager. Standard: relativ tomhed (0..1),
-- så en lille næsten-tom tank vinder over en stor halvfuld. Kendes forbruget
-- (fabriksstyret), tæller i stedet tid til tom: score i ]1, 2], så varer med
-- forbrug altid går forud for varer uden, og kortest tid vinder.
function ADSupplyPlanner.scoreNeed(level, capacity, freeSpace, minutesToEmpty)
if minutesToEmpty ~= nil then
return 1 + 1 / (1 + minutesToEmpty)
end
if capacity <= 0 then
return 0
end
return freeSpace / capacity
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)
local ranked = {}
for _, need in ipairs(needs) do
local freeSpace = need.capacity - need.level
if freeSpace >= ADSupplyPlanner.MIN_FREE_LITERS then
local minutesToEmpty = ADSupplyPlanner.minutesToEmpty(need.level, need.litersPerMinute)
table.insert(ranked, {
fillType = need.fillType,
level = need.level,
capacity = need.capacity,
freeSpace = freeSpace,
litersPerMinute = need.litersPerMinute,
minutesToEmpty = minutesToEmpty,
score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace, minutesToEmpty),
})
end
end
table.sort(ranked, function(a, b)
if a.score ~= b.score then
return a.score > b.score
end
return a.fillType < b.fillType
end)
return ranked
end
-- state: nil eller {ingredients, narrowedTo} fra forrige tur. selected: AD's aktuelle varevalg.
-- >1 valgt = (ny) ingrediensliste. Præcis den vare vi selv indsnævrede til = gemt liste.
-- Alt andet = brugeren har valgt noget andet -> nil (behovsstyring fra).
function ADSupplyPlanner.resolveIngredients(state, selected)
if type(selected) ~= "table" then
return nil
end
if #selected > 1 then
return ADSupplyPlanner.copyList(selected)
end
if state ~= nil and #selected == 1 and selected[1] == state.narrowedTo then
return ADSupplyPlanner.copyList(state.ingredients)
end
return nil
end