feat: fabriksstyret forsyning — målets tændte produktioner styrer ingredienslisten (v1.6.0.0)

Er aflæsningsmålet et produktionspunkt, erstattes ingredienslisten af inputs fra
ønskede produktioner (tændte, eller auto-slukkede af kortet med status
MISSING_INPUTS — Dakotas ExtendedProductionPoint), begrænset til det vognen kan
bære. Brugerens multi-valg er kun tændknappen og gendannes ved stop som hidtil.

Prioritet: tid til tom = lager / (Σ amount × cyclesPerMinute); varer med kendt
forbrug scorer i ]1,2] og går altid forud for relativ tomhed [0,1]. Loggen viser
forbrug og minutter; fabrik uden ønskede produktioner logges én gang.

API-navne verificeret mod EasyDevControls + Dakota16x ExtendedProductionPoint.
This commit is contained in:
2026-09-22 20:04:08 +02:00
parent 8c76790ece
commit 795c03663c
6 changed files with 257 additions and 14 deletions
+81 -7
View File
@@ -250,21 +250,81 @@ 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
return station.targetStorages
end
local placeable = station.owningPlaceable
local productionPoint = placeable ~= nil and placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
local productionPoint = getProductionPoint(station)
if productionPoint ~= nil and productionPoint.storage ~= nil then
return {productionPoint.storage}
end
return {}
end
-- Niveau + kapacitet pr. ingrediens i aflæsningsmålets lager.
local function getTargetNeeds(station, ingredients, farmId)
local function isProductionEnabled(productionPoint, production)
if productionPoint.getIsProductionEnabled ~= nil then
return productionPoint:getIsProductionEnabled(production.id) == true
end
for _, active in ipairs(productionPoint.activeProductions or {}) do
if active == production then
return true
end
end
return false
end
-- Fabriksstyret: {[fillType] = liter/min} som målets ønskede produktioner bruger,
-- begrænset til det vognen kan bære. Tom tabel = intet produktionspunkt/intet ønsket.
local function getProductionDemand(adEnv, station, driver)
local productionPoint = getProductionPoint(station)
if productionPoint == nil or productionPoint.productions == nil then
return {}
end
local canCarry = {}
for _, fillType in ipairs(adEnv.AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically(driver) or {}) do
canCarry[fillType] = true
end
local missingInputs = ProductionPoint ~= nil and ProductionPoint.PROD_STATUS ~= nil and ProductionPoint.PROD_STATUS.MISSING_INPUTS or nil
local function isWanted(production)
return ADSupplyPlanner.isProductionWanted(isProductionEnabled(productionPoint, production), production.status, missingInputs)
end
return ADSupplyPlanner.collectProductionDemand(productionPoint.productions, isWanted, canCarry)
end
-- Fabrikkens inputs som sorteret liste (stabil rækkefølge -> stabil log/rangering).
local function getDemandFillTypes(demand)
local fillTypes = {}
for fillType in pairs(demand) do
table.insert(fillTypes, fillType)
end
table.sort(fillTypes)
return fillTypes
end
-- Fabrik som mål uden ønskede produktioner: sig det én gang, så en stille
-- tilbagefald til brugerens valg ikke ligner en fejl i moden.
local factoryFallbackLogged = setmetatable({}, {__mode = "k"})
local function logFactoryFallbackOnce(driver, station)
local placeable = station.owningPlaceable
local name = placeable ~= nil and placeable.getName ~= nil and placeable:getName() or "fabrik"
local message = string.format("'%s': %s har ingen ønskede produktioner (tændte eller auto-slukkede pga. mangel) med varer vognen kan bære; bruger dit varevalg", tostring(driver:getName()), tostring(name))
if factoryFallbackLogged[driver] ~= message then
factoryFallbackLogged[driver] = message
Logging.info(ADSmartPickup.LOG_PREFIX .. "%s", message)
end
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)
for _, fillType in ipairs(ingredients) do
@@ -278,7 +338,7 @@ local function getTargetNeeds(station, ingredients, farmId)
-- 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})
table.insert(needs, {fillType = fillType, level = level, capacity = level + freeSpace, litersPerMinute = demand[fillType]})
end
end
return needs
@@ -297,9 +357,19 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)
return nil
end
-- Er målet en fabrik med ønskede produktioner, følger listen fabrikken —
-- brugerens multi-valg er kun tændknappen (og gendannes ved stop).
local demand = getProductionDemand(adEnv, target, driver)
if next(demand) ~= nil then
ingredients = getDemandFillTypes(demand)
factoryFallbackLogged[driver] = nil
elseif getProductionPoint(target) ~= nil then
logFactoryFallbackOnce(driver, target)
end
local allMarkers = adEnv.ADGraphManager:getMapMarkers()
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, demand))) do
local choice = findBestSource(markerStations, {need.fillType}, farmId)
if choice ~= nil then
return {
@@ -351,7 +421,11 @@ local function describeChoice(choice)
return string.format("%.0f l, %s", choice.level, source)
end
local fillTypeName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(choice.fillType) or tostring(choice.fillType)
return string.format("%s: kilde %.0f l, %s, målet har %.0f/%.0f l", tostring(fillTypeName), choice.level, source, choice.need.level, choice.need.capacity)
local urgency = ""
if choice.need.minutesToEmpty ~= nil then
urgency = string.format(", fabrikken bruger %.0f l/min = %.0f min til tom", choice.need.litersPerMinute, choice.need.minutesToEmpty)
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)
end
local function logChoiceOnce(driver, choice)
+49 -5
View File
@@ -48,29 +48,73 @@ function ADSupplyPlanner.copyList(list)
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
-- 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.
function ADSupplyPlanner.scoreNeed(level, capacity, freeSpace)
-- 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}, ...}. Returnerer NY liste, mest trængende først;
-- ingredienser uden plads i målet er sorteret fra.
-- 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,
score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace),
litersPerMinute = need.litersPerMinute,
minutesToEmpty = minutesToEmpty,
score = ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace, minutesToEmpty),
})
end
end
+2 -2
View File
@@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.5.0.0</version>
<version>1.6.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
<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. 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). Kræver FS25_AutoDrive med "Brug mapper" + "Rotér mål: pålæsning".]]></en>
</description>
<iconFilename>icon.dds</iconFilename>
<multiplayer supported="true"/>