Files
fs25-adsmartpickup/FS25_ADSmartPickup/adSmartPickup.lua
T

1140 lines
54 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- AD Smart Pickup
-- Gør AutoDrives mappe-rotation for pålæsning vare-bevidst: i stedet for
-- "næste markør i mappen" vælges den markør, hvis silo har den valgte vare —
-- og blandt dem den med MINDST i (så rester tømmes først og siloer frigives).
-- Findes ingen silo med varen, bruges AutoDrives originale rotation uændret.
--
-- Behovsstyret forsyning: er der valgt FLERE varer i AutoDrive, ses de som
-- ingredienser til aflæsningsmålet (fx en TMR-blander). Pr. tur hentes den
-- ingrediens målet mangler mest, fra en silo hvor som helst på kortet.
ADSmartPickup = {}
ADSmartPickup.dir = g_currentModDirectory
if ADSmartPickup.dir ~= nil and source ~= nil then
source(ADSmartPickup.dir .. "gui/SmartPickupFrame.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupSupplyTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupOutboundTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
end
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
-- vogntog bagved. Siloer tættere end dette regnes som "markørens silo".
ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE = 40
-- I tætte silo-rækker ligger nabosiloens trigger FORAN næsen og er ofte nærmere
-- end markørens egen. Vognen holder altid BAG næsen, så en trigger foran næsen
-- 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
-- anden bane (fx aflæsningssiden af samme tank), ikke markørens læsseplads.
ADSmartPickup.MAX_TRIGGER_LATERAL_OFFSET = 6
-- Kendes køreretningen, men ligger ingen trigger i sporet bag næsen (kurve ind til
-- markøren), accepteres kun en trigger helt tæt på.
ADSmartPickup.MAX_FALLBACK_TRIGGER_DISTANCE = 12
-- Under dette niveau regnes siloen som tom (undgår ture efter støv-rester).
ADSmartPickup.MIN_USEFUL_LITERS = 1
-- Sikkerhedsventil: står køretøjet ved markøren så længe uden at vognen fyldes
-- (forkert markør↔silo-kobling, røret når ikke vognen), slippes grebet.
ADSmartPickup.MAX_WAIT_WITHOUT_LOADING_MS = 30000
ADSmartPickup.LOG_PREFIX = "ADSmartPickup: "
local LOAD_TRIGGER_NODE_FIELDS = {"triggerNode"}
local UNLOAD_TRIGGER_NODE_FIELDS = {"exactFillRootNode", "triggerNode", "aiNode"}
local isHooked = false
-- køretøj -> {ingredients, narrowedTo}. AutoDrive roterer selv vare ved triggeren
-- når flere er valgt, så valget indsnævres til én pr. tur; listen huskes her.
local supplyState = setmetatable({}, {__mode = "k"})
-- køretøj -> {station, fillTypes}: siloen moden sendte køretøjet til på denne tur.
local activePickups = setmetatable({}, {__mode = "k"})
-- køretøj -> senest loggede valg (AutoDrive spørger flere gange pr. start).
-- køretøj -> {wayPointId, fillType, liters}: læs traktoren er på vej med (fordeler flere traktorer til
-- samme mål; ryddes ved aflæsning, stop eller ny tur)
local enRoute = setmetatable({}, {__mode = "k"})
local lastLogged = setmetatable({}, {__mode = "k"})
-- Cross-mod adgang: FS25-mods er sandboxede; AutoDrives globals ligger
-- i den globale tabel FS25_AutoDrive (zip-navnet).
local function getAutoDriveEnv()
if FS25_AutoDrive ~= nil and FS25_AutoDrive.ADMultipleTargetsManager ~= nil then
return FS25_AutoDrive
end
return nil
end
-- Adgang til en station: spillets adgangstjek, eller stationens bygning er gårdens efter den fælles
-- ejerregel (jord-bundne kortanlæg som Dakotas BGA står med ejer 15 selv på gårdens jord).
function ADSmartPickup.canUseStation(farmId, station)
local access = g_currentMission ~= nil and g_currentMission.accessHandler or nil
if access == nil or access:canFarmAccess(farmId, station) then
return true
end
return station.owningPlaceable ~= nil and ADPrices ~= nil and ADPrices.isOwnedBy ~= nil and ADPrices.isOwnedBy(station.owningPlaceable, farmId)
end
function ADSmartPickup.getFarmId(vehicle)
if vehicle.getOwnerFarmId ~= nil then
local farmId = vehicle:getOwnerFarmId()
if farmId ~= nil and farmId > 0 then
return farmId
end
end
if vehicle.spec_aiVehicle ~= nil and vehicle.spec_aiVehicle.startedFarmId ~= nil and vehicle.spec_aiVehicle.startedFarmId > 0 then
return vehicle.spec_aiVehicle.startedFarmId
end
return nil
end
local getFarmId = ADSmartPickup.getFarmId
local function getWantedFillTypes(driver)
local wanted = {}
local selected = driver.ad.stateModule:getSelectedFillTypes()
if type(selected) == "table" then
for _, fillType in pairs(selected) do
table.insert(wanted, fillType)
end
end
if #wanted == 0 then
local fillType = driver.ad.stateModule:getFillType()
if fillType ~= nil then
table.insert(wanted, fillType)
end
end
return wanted
end
-- Ankomstretning ved markøren (enhedsvektor) ud fra forrige waypoint, ellers nil.
local function getArrivalDirection(adEnv, wayPoint)
local previousId = wayPoint.incoming ~= nil and wayPoint.incoming[1] or nil
local previous = previousId ~= nil and adEnv.ADGraphManager:getWayPointById(previousId) or nil
if previous == nil then
return nil
end
local dx, dz = wayPoint.x - previous.x, wayPoint.z - previous.z
local length = math.sqrt(dx * dx + dz * dz)
if length < 0.01 then
return nil
end
return {x = dx / length, z = dz / length}
end
-- Nærmeste trigger-afstand for en station, samlet og blandt triggere BAG næsen.
local function getDistancesToTriggers(triggers, nodeFields, wayPoint, direction)
local closest, closestBehind = math.huge, math.huge
for _, trigger in pairs(triggers or {}) do
for _, field in ipairs(nodeFields) do
local node = trigger[field]
if node ~= nil and entityExists(node) then
local tx, _, tz = getWorldTranslation(node)
local distance = MathUtil.vector2Length(wayPoint.x - tx, wayPoint.z - tz)
closest = math.min(closest, distance)
if direction ~= nil then
-- behind > 0: bag næsen set i køreretningen; lateral: afstand fra sporet
local behind = (wayPoint.x - tx) * direction.x + (wayPoint.z - tz) * direction.z
local lateral = math.abs((wayPoint.x - tx) * direction.z - (wayPoint.z - tz) * direction.x)
if behind >= -ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER and lateral <= ADSmartPickup.MAX_TRIGGER_LATERAL_OFFSET then
closestBehind = math.min(closestBehind, distance)
end
end
end
end
end
return closest, closestBehind
end
-- Markørens station: nærmeste trigger i sporet bag næsen. Findes ingen, bruges
-- nærmeste trigger overhovedet — helt tæt på hvis retningen kendes, ellers som før.
local function getClosestStation(adEnv, candidateStations, triggersField, nodeFields, wayPoint, farmId)
local direction = getArrivalDirection(adEnv, wayPoint)
local maxFallbackDistance = direction ~= nil and ADSmartPickup.MAX_FALLBACK_TRIGGER_DISTANCE or ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE
local best = {any = {distance = maxFallbackDistance}, behind = {distance = ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE}}
for _, station in pairs(candidateStations) do
if ADSmartPickup.canUseStation(farmId, station) then
local closest, closestBehind = getDistancesToTriggers(station[triggersField], nodeFields, wayPoint, direction)
if closest < best.any.distance then
best.any = {distance = closest, station = station}
end
if closestBehind < best.behind.distance then
best.behind = {distance = closestBehind, station = station}
end
end
end
return best.behind.station or best.any.station
end
local function getLoadStationAtMarker(adEnv, wayPoint, farmId)
return getClosestStation(adEnv, g_currentMission.storageSystem:getLoadingStations(), "loadTriggers", LOAD_TRIGGER_NODE_FIELDS, wayPoint, farmId)
end
local function getUnloadStationAtMarker(adEnv, wayPoint, farmId)
return getClosestStation(adEnv, g_currentMission.storageSystem:getUnloadingStations(), "unloadTriggers", UNLOAD_TRIGGER_NODE_FIELDS, wayPoint, farmId)
end
-- Station ved et AutoDrive-waypoint (bruges af runs-controlleren og læs-byttet); nil uden station.
function ADSmartPickup.getLoadStationAtWayPointId(adEnv, wayPointId, farmId)
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
return wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
end
function ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId)
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
return wayPoint ~= nil and getUnloadStationAtMarker(adEnv, wayPoint, farmId) or nil
end
-- Løst opslag: nærmeste læssestation inden for maxDistance, uanset køreretning og "bag næsen".
-- Til stalde og fabrikker (mælketank ved siden af markøren); siloer skal bruge det strenge
-- opslag, ellers rammes nabosiloen i tætte rækker.
function ADSmartPickup.getLoadStationNearWayPointId(adEnv, wayPointId, farmId, maxDistance)
local wayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
if wayPoint == nil then
return nil
end
local best, bestDistance = nil, maxDistance or ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE
for _, station in pairs(g_currentMission.storageSystem:getLoadingStations()) do
if ADSmartPickup.canUseStation(farmId, station) then
local closest = getDistancesToTriggers(station.loadTriggers, LOAD_TRIGGER_NODE_FIELDS, wayPoint, nil)
if closest < bestDistance then
best, bestDistance = station, closest
end
end
end
return best
end
-- Siloens EGET indhold. Udvidelses-siloer (isExtension) deler lager med naboer
-- inden for storageRadius, så stationens niveau er en nabo-sum — eget lager er
-- det tal spilleren ser på siloen og giver en præcis "mindst først"-rangering.
function ADSmartPickup.getOwnStorageLevel(station, fillTypes)
local placeable = station.owningPlaceable
local storages = placeable ~= nil and placeable.spec_silo ~= nil and placeable.spec_silo.storages or nil
if storages == nil then
return 0
end
local total = 0
for _, storage in pairs(storages) do
for _, fillType in ipairs(fillTypes) do
total = total + (storage:getFillLevel(fillType) or 0)
end
end
return total
end
local getOwnStorageLevel = ADSmartPickup.getOwnStorageLevel
-- Returnerer niveau + om det er siloens eget lager (true) eller kun nabo-lagre (false).
local function getStationFillLevel(station, fillTypes, farmId)
local ownLevel = getOwnStorageLevel(station, fillTypes)
if ownLevel >= ADSmartPickup.MIN_USEFUL_LITERS then
return ownLevel, true
end
-- intet i eget lager: stationen kan stadig læsse fra nabo-lagre
local total = 0
for _, fillType in ipairs(fillTypes) do
total = total + (station:getFillLevel(fillType, farmId) or 0)
end
return total, false
end
-- Parrer markører med deres læssestation (markører uden station udelades).
-- excludedPlaceable: aflæsningsmålets egen placeable må ikke være kilde.
local function collectMarkerStations(adEnv, candidateMarkers, farmId, excludedPlaceable)
local markerStations = {}
for _, marker in pairs(candidateMarkers) do
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
local station = wayPoint ~= nil and getLoadStationAtMarker(adEnv, wayPoint, farmId) or nil
local isExcluded = station ~= nil and excludedPlaceable ~= nil and station.owningPlaceable == excludedPlaceable
if station ~= nil and not isExcluded then
table.insert(markerStations, {marker = marker, station = station})
end
end
return markerStations
end
local function getFillTypeNames(fillTypes)
local names = {}
for _, fillType in ipairs(fillTypes) do
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
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.
-- En STALDS læssestation udleverer kun staldens produktion. Den kan dog "have" halm/foder/vand
-- (strøelse, trug, udvidelseslagre inden for storageRadius) — dem henter vi aldrig dér.
local HUSBANDRY_OUTPUTS = {MILK = true, GOATMILK = true, BUFFALOMILK = true, SHEEPMILK = true, LIQUIDMANURE = true, MANURE = true, DIGESTATE = true, EGG = true, WOOL = true}
local function isProtectedHusbandryInput(station, fillTypes)
local placeable = station.owningPlaceable
if placeable == nil or placeable.getHusbandryFillLevel == nil or g_fillTypeManager == nil then
return false
end
for _, fillType in ipairs(fillTypes) do
if not HUSBANDRY_OUTPUTS[tostring(g_fillTypeManager:getFillTypeNameByIndex(fillType))] then
return true
end
end
return false
end
local function isProtectedProductionInput(station, fillTypes)
if isProtectedHusbandryInput(station, fillTypes) then
return true
end
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.
local function findBestSource(markerStations, fillTypes, farmId)
local prefersFullest = ADSupplyPlanner.prefersFullestSource(getFillTypeNames(fillTypes))
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 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
local choice = best[true].marker ~= nil and best[true] or best[false]
if choice.marker == nil then
return nil
end
return {
markerIndex = choice.marker.markerIndex,
markerId = choice.marker.id,
markerName = choice.marker.name,
group = choice.marker.group,
level = choice.level,
station = choice.station,
isOwn = choice.isOwn,
fillTypes = fillTypes,
prefersFullest = prefersFullest,
}
end
-- Mappe-logik: mappens mindst fyldte silo der har den/de valgte varer, ellers nil.
function ADSmartPickup.findLeastFilledPickup(adEnv, driver)
local firstMarker = driver.ad.stateModule:getFirstMarker()
if firstMarker == nil or firstMarker.group == nil or firstMarker.group == "All" then
return nil
end
local farmId = getFarmId(driver)
local fillTypes = getWantedFillTypes(driver)
if farmId == nil or #fillTypes == 0 then
return nil
end
local groupMarkers = adEnv.ADGraphManager:getMapMarkersInGroup(firstMarker.group)
return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId)
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 productionPoint = getProductionPoint(station)
if productionPoint ~= nil and productionPoint.storage ~= nil then
return {productionPoint.storage}
end
return {}
end
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(ADRuns.vehicleLabel(driver)), tostring(name))
if factoryFallbackLogged[driver] ~= message then
factoryFallbackLogged[driver] = message
Logging.info(ADSmartPickup.LOG_PREFIX .. "%s", message)
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
-- FS25-staldes foder ligger i spec_husbandryFood: ét fælles trug (capacity) med niveau pr.
-- vare (fillLevels). Hvilke varer der er foder, ved animalFoodSystem (grupper + blandinger).
-- Returnerer {[fillType] = true} eller nil når stalden ikke har foder-spec.
local function getHusbandryFoodFillTypes(husbandry)
local foodSpec = husbandry.spec_husbandryFood
local foodSystem = g_currentMission ~= nil and g_currentMission.animalFoodSystem or nil
if foodSpec == nil or foodSystem == nil or foodSpec.animalTypeIndex == nil then
return nil
end
-- værdien er foderets vægt (spillets productionWeight/eatWeight: TMR 1,0, hø/ensilage
-- 0,8, græs 0,4) — bruges som prioritet inden for truget; ukendt = 1
local fillTypes = {}
local animalFood = foodSystem.getAnimalFood ~= nil and foodSystem:getAnimalFood(foodSpec.animalTypeIndex) or nil
for _, group in pairs(animalFood ~= nil and animalFood.groups or {}) do
local weight = tonumber(group.productionWeight) or tonumber(group.eatWeight) or 1
for _, fillType in pairs(group.fillTypes or {}) do
fillTypes[fillType] = math.max(fillTypes[fillType] or 0, weight)
end
end
local mixtures = foodSystem.getMixturesByAnimalTypeIndex ~= nil and foodSystem:getMixturesByAnimalTypeIndex(foodSpec.animalTypeIndex) or nil
for _, mixture in pairs(mixtures or {}) do
local fillType = type(mixture) == "table" and (mixture.fillType or mixture.fillTypeIndex) or mixture
if type(fillType) == "number" then
fillTypes[fillType] = math.max(fillTypes[fillType] or 0, 1)
end
end
return fillTypes
end
-- Trugets fælles trang (fri/kapacitet) og foderets vægt for en vare; nil når varen ikke er foder.
local function getFoodPriority(station, fillType)
local husbandry = getHusbandry(station)
local foodFillTypes = husbandry ~= nil and getHusbandryFoodFillTypes(husbandry) or nil
if foodFillTypes == nil or foodFillTypes[fillType] == nil then
return nil
end
local foodSpec = husbandry.spec_husbandryFood
local total = 0
for _, level in pairs(foodSpec.fillLevels or {}) do
total = total + (level or 0)
end
local capacity = foodSpec.capacity or 0
local troughScore = capacity > 0 and math.max(0, capacity - total) / capacity or 0
return troughScore, foodFillTypes[fillType], capacity
end
-- En stald kan MODTAGE foder, halm og vand — ikke sine egne outputs (mælk, gødning, gylle),
-- selv om placeable'n "understøtter" dem som lager. Uden foder-spec (ældre/mod-stalde)
-- spørges staldens egen understøttelse.
local function isFillTypeAccepted(station, husbandry, fillType)
if husbandry ~= nil then
local foodFillTypes = getHusbandryFoodFillTypes(husbandry)
if foodFillTypes ~= nil then
if foodFillTypes[fillType] then
return true
end
local strawSpec, waterSpec = husbandry.spec_husbandryStraw, husbandry.spec_husbandryWater
local isStraw = strawSpec ~= nil and (strawSpec.fillType == nil or strawSpec.fillType == fillType)
and g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) == "STRAW"
local isWater = waterSpec ~= nil and waterSpec.automaticWaterSupply ~= true
and (waterSpec.fillType == fillType or g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) == "WATER")
return isStraw or isWater
end
if husbandry.getHusbandryIsFillTypeSupported ~= nil then
return husbandry:getHusbandryIsFillTypeSupported(fillType)
end
end
return station.getIsFillTypeSupported == nil or station:getIsFillTypeSupported(fillType)
end
-- Foder deler ét trug: varens eget niveau + trugets fri plads = den kapacitet varen kan nå.
local function getHusbandryFoodLevelAndCapacity(husbandry, fillType)
local foodSpec = husbandry.spec_husbandryFood
local total = 0
for _, level in pairs(foodSpec.fillLevels or {}) do
total = total + (level or 0)
end
local level = (foodSpec.fillLevels or {})[fillType] or 0
local freeSpace = math.max(0, (foodSpec.capacity or 0) - total)
return level, level + freeSpace
end
-- Niveau + kapacitet for én vare i aflæsningsmålet; nil hvis målet ikke tager varen.
function ADSmartPickup.getStationLevelAndCapacity(station, fillType, farmId)
local husbandry = getHusbandry(station)
if not isFillTypeAccepted(station, husbandry, fillType) then
return nil
end
if husbandry ~= nil then
local foodFillTypes = getHusbandryFoodFillTypes(husbandry)
if foodFillTypes ~= nil and foodFillTypes[fillType] then
return getHusbandryFoodLevelAndCapacity(husbandry, fillType)
end
local level = husbandry:getHusbandryFillLevel(fillType) or 0
return level, husbandry:getHusbandryCapacity(fillType) or level
end
local level, freeSpace = 0, 0
for _, storage in pairs(getTargetStorages(station)) do
level = level + (storage:getFillLevel(fillType) or 0)
freeSpace = freeSpace + (storage:getFreeCapacity(fillType) or 0)
end
if level + freeSpace <= 0 and station.getFreeCapacity ~= nil then
-- ukendt lagertype: brug stationens egen opgørelse af fri plads
level, freeSpace = 0, station:getFreeCapacity(fillType, farmId) or 0
end
return level, level + freeSpace
end
local getTargetLevelAndCapacity = ADSmartPickup.getStationLevelAndCapacity
-- Niveau + kapacitet pr. ingrediens i aflæsningsmålets lager; demand giver forbrug/min.
local function getTargetNeeds(station, ingredients, farmId, demand)
local needs = {}
for _, fillType in ipairs(ingredients) do
local level, capacity = getTargetLevelAndCapacity(station, fillType, farmId)
if level ~= nil then
local troughScore, priority, troughCapacity = getFoodPriority(station, fillType)
table.insert(needs, {fillType = fillType, level = level, capacity = capacity, litersPerMinute = demand[fillType], scoreOverride = troughScore, priority = priority, troughCapacity = troughCapacity})
end
end
return needs
end
-- Til fanen: samme rangering som valget bruger (uden vogn-størrelse). Fulde varer sidst.
-- ---- læs på vej ----
function ADSmartPickup.setEnRoute(vehicle, wayPointId, fillType, liters)
if wayPointId == nil or fillType == nil or liters == nil or liters <= 0 then
enRoute[vehicle] = nil
return
end
enRoute[vehicle] = {wayPointId = wayPointId, fillType = fillType, liters = liters}
end
function ADSmartPickup.clearEnRoute(vehicle)
enRoute[vehicle] = nil
end
function ADSmartPickup.resetEnRoute()
enRoute = setmetatable({}, {__mode = "k"})
end
-- {[fillType] = liter} som ANDRE (aktive) traktorer er på vej med til målet.
function ADSmartPickup.getPending(wayPointId, excludeVehicle)
local pending = {}
for vehicle, entry in pairs(enRoute) do
if vehicle ~= excludeVehicle and entry.wayPointId == wayPointId then
pending[entry.fillType] = (pending[entry.fillType] or 0) + entry.liters
end
end
return pending
end
-- pending (valgfri): læs på vej til målet, lægges oven i (fanen og mål-valget mellem stalde).
function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId, pending)
local needs = getTargetNeeds(station, fillTypes, farmId, {})
if pending ~= nil and next(pending) ~= nil then
needs = ADSupplyPlanner.applyPending(needs, pending)
end
local ranked = ADSupplyPlanner.rankNeeds(needs)
local seen = {}
for _, need in ipairs(ranked) do seen[need.fillType] = true end
for _, need in ipairs(needs) do
if not seen[need.fillType] then
table.insert(ranked, {fillType = need.fillType, level = need.level, capacity = need.capacity, freeSpace = need.capacity - need.level, fits = false, score = 0, priority = need.priority or 0})
end
end
return ranked
end
-- Vognens samlede kapacitet (liter); nil hvis AutoDrive ikke kan opgøre den.
local function getLoadCapacity(adEnv, driver)
if adEnv.AutoDrive.getAllUnits == nil or adEnv.AutoDrive.getAllFillLevels == nil then
return nil
end
local trailers = adEnv.AutoDrive.getAllUnits(driver)
local _, capacity = adEnv.AutoDrive.getAllFillLevels(trailers)
return capacity ~= nil and capacity > 0 and capacity or nil
end
-- Fri plads + kapacitet for varen i køretøjets aflæsningsmål (Wait-punkt-logikken);
-- nil hvis målet ikke kendes eller ikke tager varen.
-- askAccepted=true: returnerer i stedet true/false for "tager målet varen?" (nil = mål ukendt).
-- targetWayPointId (valgfri): brug dette mål i stedet for AD's 2. markør (mens en traktor venter
-- ved et Wait-punkt, peger AD's markør på Wait-punktet, så AD ikke tipper).
function ADSmartPickup.getTargetFreeCapacity(adEnv, driver, fillType, askAccepted, targetWayPointId)
local secondMarker = driver.ad.stateModule:getSecondMarker()
local farmId = getFarmId(driver)
local wayPointId = targetWayPointId or (secondMarker ~= nil and secondMarker.id or nil)
if wayPointId == nil or farmId == nil then
return nil
end
local targetWayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
local target = targetWayPoint ~= nil and getUnloadStationAtMarker(adEnv, targetWayPoint, farmId) or nil
if target == nil then
return nil
end
local level, capacity = getTargetLevelAndCapacity(target, fillType, farmId)
if askAccepted then
return level ~= nil
end
if level == nil then
return nil
end
return capacity - level, capacity
end
-- Behovsstyret: den ingrediens aflæsningsmålet mangler mest, som også findes i en silo.
function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients, targetWayPointId)
local secondMarker = driver.ad.stateModule:getSecondMarker()
local farmId = getFarmId(driver)
local wayPointId = targetWayPointId or (secondMarker ~= nil and secondMarker.id or nil)
if wayPointId == nil or farmId == nil then
return nil
end
local targetWayPoint = adEnv.ADGraphManager:getWayPointById(wayPointId)
local target = targetWayPoint ~= nil and getUnloadStationAtMarker(adEnv, targetWayPoint, farmId) or nil
if target == nil then
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)
local loadCapacity = getLoadCapacity(adEnv, driver)
local needs = ADSupplyPlanner.applyPending(getTargetNeeds(target, ingredients, farmId, demand), ADSmartPickup.getPending(wayPointId, driver))
local ranked = ADSupplyPlanner.rankNeeds(needs, loadCapacity)
-- bedste foder først: lavere foder kun når truget er nødlidende (ADSupplyPlanner.isFoodAllowed)
local bestFood = ADSupplyPlanner.bestFoodPriority ~= nil and ADSupplyPlanner.bestFoodPriority(ranked) or nil
for _, need in ipairs(ranked) do
local choice = (ADSupplyPlanner.isFoodAllowed == nil or ADSupplyPlanner.isFoodAllowed(need, bestFood)) and findBestSource(markerStations, {need.fillType}, farmId) or nil
if choice ~= nil then
return {
markerIndex = choice.markerIndex,
markerId = choice.markerId,
targetWayPointId = wayPointId,
-- det der reelt kommer af: vognen, kildens beholdning og målets fri plads sætter grænsen
pendingLiters = math.min(loadCapacity or math.huge, choice.level or math.huge, need.freeSpace or math.huge),
markerName = choice.markerName,
group = choice.group,
level = choice.level,
station = choice.station,
isOwn = choice.isOwn,
fillTypes = choice.fillTypes,
prefersFullest = choice.prefersFullest,
fillType = need.fillType,
need = need,
}
end
end
return nil
end
local function applySupplyChoice(driver, ingredients, choice)
local stateModule = driver.ad.stateModule
ADSmartPickup.setEnRoute(driver, choice.targetWayPointId, choice.fillType, choice.pendingLiters)
stateModule:setFillType(choice.fillType)
stateModule.selectedFillTypes = {choice.fillType}
stateModule:raiseDirtyFlag()
supplyState[driver] = {ingredients = ingredients, narrowedTo = choice.fillType}
end
-- Ingredienslisten for køretøjet: run'ets liste når det er i et run (ingen tændknap
-- nødvendig), ellers AD's multi-valg (>1 vare). nil = ingen behovsstyring.
-- Den silo og vare moden sidst sendte køretøjet efter (til overblikket); nil = ingen.
function ADSmartPickup.getActivePickup(vehicle)
local pickup = activePickups[vehicle]
if pickup == nil then
return nil
end
return {markerName = pickup.markerName, fillType = pickup.fillType}
end
-- Udkørslens opsamlingsrunde skifter kilde midt i en tur: grebet ("stå og læs ved markøren")
-- følger med til den nye kilde. choice = {station, fillTypes, markerName, fillType} eller nil.
function ADSmartPickup.setActivePickup(vehicle, choice)
if choice == nil or choice.station == nil then
activePickups[vehicle] = nil
return
end
activePickups[vehicle] = {station = choice.station, fillTypes = choice.fillTypes or {}, markerName = choice.markerName, fillType = choice.fillType or (choice.fillTypes or {})[1]}
end
function ADSmartPickup.getIngredientsFor(adEnv, driver)
local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(driver) or nil
if run ~= nil then
local ingredients = ADRunsController.getRunIngredients(adEnv, run, driver)
if #ingredients > 0 then
return ingredients
end
return nil
end
return ADSupplyPlanner.resolveIngredients(supplyState[driver], driver.ad.stateModule:getSelectedFillTypes())
end
-- Ved turens start sættes AD's aflæsningsmål til run'ets mål — ved flere mål den stald der
-- mangler mest; ved ét mål gendannes det (fx efter at forkert last er læsset af i en silo).
-- AD læser først målet når vognen er læsset, så skiftet her gælder for hele turen.
local function chooseRunTarget(adEnv, driver)
local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(driver) or nil
if run ~= nil and ADRunsController.resolveMarkers ~= nil then
pcall(ADRunsController.resolveMarkers, adEnv, run)
end
if run == nil or ADRunsController.chooseTarget == nil or #ADRunsController.getRunTargets(run) < 1 then
return
end
-- Er AD's mål midlertidigt en silo (forkert last læsses af), må det ikke røres nu.
if ADUnloadWait ~= nil and ADUnloadWait.getPendingTargetRestore ~= nil and ADUnloadWait.getPendingTargetRestore(driver) ~= nil then
return
end
local stateModule = driver.ad.stateModule
local targetId = ADRunsController.chooseTarget(adEnv, run, driver)
local current = stateModule.getSecondMarker ~= nil and stateModule:getSecondMarker() or nil
if targetId == nil or stateModule.setSecondMarkerByWayPointId == nil or (current ~= nil and current.id == targetId) then
return
end
stateModule:setSecondMarkerByWayPointId(targetId)
local marker = adEnv.ADGraphManager.getMapMarkerByWayPointId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(targetId) or nil
local why = #ADRunsController.getRunTargets(run) > 1 and "mangler mest af run'ets mål" or "run'ets mål"
Logging.info(ADSmartPickup.LOG_PREFIX .. "'%s' kører til %s (%s)", tostring(ADRuns.vehicleLabel(driver)), marker ~= nil and tostring(marker.name) or tostring(targetId), why)
end
local tryOtherRunTargets
-- Det valgte mål har intet der kan hentes (kilden er tom, eller andre traktorer har allerede læs på vej
-- til det): prøv run'ets ANDRE mål, og skift AD's aflæsningsmål til det første der kan forsynes.
-- Ellers holdt traktoren ved Wait mens fx fire andre DOB'er ved samme biogasanlæg stod tomme.
tryOtherRunTargets = function(adEnv, driver, ingredients)
local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(driver) or nil
if run == nil or (run.kind or "supply") ~= "supply" then
return nil
end
-- AD's mål er midlertidigt en silo (forkert last læsses af): rør det ikke
if ADUnloadWait ~= nil and ADUnloadWait.getPendingTargetRestore ~= nil and ADUnloadWait.getPendingTargetRestore(driver) ~= nil then
return nil
end
local stateModule = driver.ad.stateModule
local current = stateModule.getSecondMarker ~= nil and stateModule:getSecondMarker() or nil
local currentId = current ~= nil and current.id or nil
for _, targetId in ipairs(ADRunsController.getRunTargets(run)) do
if targetId ~= currentId then
local choice = ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients, targetId)
if choice ~= nil and stateModule.setSecondMarkerByWayPointId ~= nil then
stateModule:setSecondMarkerByWayPointId(targetId)
local marker = adEnv.ADGraphManager.getMapMarkerByWayPointId ~= nil and adEnv.ADGraphManager:getMapMarkerByWayPointId(targetId) or nil
Logging.info(ADSmartPickup.LOG_PREFIX .. "'%s' kører til %s (intet at hente til %s)", tostring(ADRuns.vehicleLabel(driver)),
marker ~= nil and tostring(marker.name) or tostring(targetId), current ~= nil and tostring(current.name) or "?")
return choice
end
end
end
return nil
end
-- Samlet valg: behovsstyret med ingrediensliste, ellers mappe-logik. nil = AutoDrives original.
function ADSmartPickup.choosePickup(adEnv, driver)
enRoute[driver] = nil -- ny tur: forrige læs er leveret (eller opgivet)
chooseRunTarget(adEnv, driver)
local ingredients = ADSmartPickup.getIngredientsFor(adEnv, driver)
if ingredients == nil then
supplyState[driver] = nil
return ADSmartPickup.findLeastFilledPickup(adEnv, driver)
end
local choice = ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)
if choice == nil then
choice = tryOtherRunTargets(adEnv, driver, ingredients)
end
if choice == nil then
-- Traktor i et forsynings-run uden noget (tilladt) at hente: hold ved Wait og prøv igen, i stedet
-- for at falde tilbage til mappe-logikken (den henter hvad som helst, fx ensilage i et halvfuldt trug).
local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(driver) or nil
if run ~= nil and (run.kind or "supply") == "supply" and ADOutbound ~= nil and ADOutbound.holdSupplyVehicle ~= nil then
local replan = function()
local list = ADSmartPickup.getIngredientsFor(adEnv, driver)
local next = list ~= nil and ADSmartPickup.findSupplyPickup(adEnv, driver, list) or nil
if next == nil then
return nil
end
applySupplyChoice(driver, list, next)
activePickups[driver] = {station = next.station, fillTypes = next.fillTypes, markerName = next.markerName, fillType = next.fillType}
return next.markerId
end
local held = ADOutbound.holdSupplyVehicle(adEnv, driver, run, {"intet at hente til nogen af run'ets mål (tom kilde, læs på vej, eller bedste foder mangler)"}, replan)
if held ~= nil then
return held
end
end
return ADSmartPickup.findLeastFilledPickup(adEnv, driver)
end
applySupplyChoice(driver, ingredients, choice)
return choice
end
local function describeChoice(choice)
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
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)
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
if choice.need.fits == false then
urgency = urgency .. ", et fuldt læs passer IKKE (rest ventes ved Wait-punkt)"
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)
local message = string.format("'%s' -> %s (%s)", tostring(ADRuns.vehicleLabel(driver)), tostring(choice.markerName), describeChoice(choice))
if lastLogged[driver] ~= message then
lastLogged[driver] = message
Logging.info(ADSmartPickup.LOG_PREFIX .. "%s", message)
end
end
-- Sendte moden køretøjet til en silo der stadig har varen? Så skal AutoDrive
-- stå og læsse dér; løber kilden tør, slippes grebet og turen fortsætter.
function ADSmartPickup.shouldWaitForLoading(vehicle)
local pickup = activePickups[vehicle]
if pickup == nil then
return false
end
-- kun FREMME ved markøren: undervejs ruller vognen gennem nabosiloers triggere,
-- og dér skal AutoDrives rotation gælde (kør videre ved tom trigger).
local drivePathModule = vehicle.ad ~= nil and vehicle.ad.drivePathModule or nil
if drivePathModule == nil or not drivePathModule:isTargetReached() then
return false
end
local level = getStationFillLevel(pickup.station, pickup.fillTypes, getFarmId(vehicle))
return level >= ADSmartPickup.MIN_USEFUL_LITERS
end
-- Med "Rotér mål" + mapper afslutter AutoDrive pålæsningen i det øjeblik markøren
-- nås (LoadAtDestinationTask: rotation = "prøv næste silo", aldrig vente). Moden
-- har valgt en silo der HAR varen, så under opgavens update ser AutoDrive
-- rotationen som slået fra for netop det køretøj -> den står og læsser som normalt.
local function installLoadWaitHook(adEnv)
local task, autoDrive = adEnv.LoadAtDestinationTask, adEnv.AutoDrive
if task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke koble på pålæsningsopgaven; traktoren venter ikke ved siloen")
return
end
local originalGetSetting, originalUpdate = autoDrive.getSetting, task.update
local waitingVehicle = nil
autoDrive.getSetting = function(settingName, vehicle)
if waitingVehicle ~= nil and vehicle == waitingVehicle and settingName == "rotateTargets" then
return autoDrive.RT_NONE
end
return originalGetSetting(settingName, vehicle)
end
-- true når vognen ikke er blevet fyldt i MAX_WAIT_WITHOUT_LOADING_MS ved markøren
local function hasLoadingStalled(loadTask, dt)
local pickup = activePickups[loadTask.vehicle]
local fillLevel = autoDrive.getAllFillLevels ~= nil and autoDrive.getAllFillLevels(loadTask.trailers) or 0
if pickup.lastFillLevel == nil or fillLevel > pickup.lastFillLevel then
pickup.lastFillLevel, pickup.stalledMs = fillLevel, 0
return false
end
pickup.stalledMs = pickup.stalledMs + dt
return pickup.stalledMs > ADSmartPickup.MAX_WAIT_WITHOUT_LOADING_MS
end
-- Vognen kan kun læsse oven i SAMME vare. Er der en anden vare på vognen (rest fra et
-- tidligere run), afsluttes pålæsningen med det samme, så AD kører til aflæsning med
-- det den har — dér tager Wait-/bytte-logikken over, hvis målet ikke tager varen.
local function hasWrongCargo(loadTask)
if loadTask.spuCargoChecked or ADUnloadWait == nil or ADUnloadWait.getRestLoad == nil then
return false
end
loadTask.spuCargoChecked = true
local pickup = activePickups[loadTask.vehicle]
if pickup == nil or pickup.fillType == nil then
return false
end
local fillType, liters = ADUnloadWait.getRestLoad(adEnv, loadTask.vehicle)
if fillType == nil or liters < 100 or fillType == pickup.fillType then
return false
end
local cargoName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
local wantedName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(pickup.fillType) or tostring(pickup.fillType)
Logging.info(ADSmartPickup.LOG_PREFIX .. "'%s' har %.0f l %s på vognen og skulle hente %s — leverer først det den har", tostring(ADRuns.vehicleLabel(loadTask.vehicle)), liters, tostring(cargoName), tostring(wantedName))
-- hører lasten ikke til run'et (fx gødning i et ensilage-run), køres den til en silo der tager den
local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(loadTask.vehicle) or nil
if run ~= nil and ADRunsController.findWrongCargoDump ~= nil and ADUnloadWait.setDumpTarget ~= nil then
local stateModule = loadTask.vehicle.ad.stateModule
local current = stateModule.getSecondMarker ~= nil and stateModule:getSecondMarker() or nil
local ok, dump = pcall(ADRunsController.findWrongCargoDump, adEnv, run, loadTask.vehicle, current ~= nil and current.id or nil)
if ok and dump ~= nil then
ADUnloadWait.setDumpTarget(loadTask.vehicle, dump.marker.id)
end
end
return true
end
task.update = function(self, dt)
local wrongOk, isWrong = pcall(hasWrongCargo, self)
if wrongOk and isWrong then
activePickups[self.vehicle] = nil
if self.finished ~= nil then
self:finished()
return
end
end
local ok, shouldWait = pcall(ADSmartPickup.shouldWaitForLoading, self.vehicle)
shouldWait = ok and shouldWait
if shouldWait then
local stalledOk, isStalled = pcall(hasLoadingStalled, self, dt)
if not stalledOk or isStalled then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "'%s' læsser ikke ved markøren; slipper grebet så AutoDrive kører videre", tostring(ADRuns.vehicleLabel(self.vehicle)))
activePickups[self.vehicle] = nil
shouldWait = false
end
end
waitingVehicle = shouldWait and self.vehicle or nil
local updateOk, err = pcall(originalUpdate, self, dt)
waitingVehicle = nil
if not updateOk then
error(err, 0)
end
end
end
local function installHook(adEnv)
local manager = adEnv.ADMultipleTargetsManager
local originalGetNextPickup = manager.getNextPickup
manager.getNextPickup = function(self, driver, forcedSkip)
-- Udkørsel: turvalg (kilde + mål + varer) eller Wait-punkt fra ADOutbound.
if ADOutbound ~= nil and ADOutbound.isOutboundVehicle(driver) then
local okOut, outChoice = pcall(ADOutbound.choosePickup, adEnv, driver)
if not okOut then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i udkørsel, bruger AutoDrives rotation: %s", tostring(outChoice))
activePickups[driver] = nil
return originalGetNextPickup(self, driver, forcedSkip)
end
if outChoice == nil then
activePickups[driver] = nil
return originalGetNextPickup(self, driver, forcedSkip)
end
ADSmartPickup.setActivePickup(driver, outChoice)
self.pickups[outChoice.group] = self.pickups[outChoice.group] or {}
self.pickups[outChoice.group].lastTarget = outChoice.markerIndex
self.pickups[outChoice.group].lastVehicle = driver
return outChoice.markerIndex
end
local ok, choice = pcall(ADSmartPickup.choosePickup, adEnv, driver)
if not ok then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i silo-opslag, bruger AutoDrives rotation: %s", tostring(choice))
activePickups[driver] = nil
return originalGetNextPickup(self, driver, forcedSkip)
end
if choice == nil then
activePickups[driver] = nil
return originalGetNextPickup(self, driver, forcedSkip)
end
activePickups[driver] = choice.station ~= nil and {station = choice.station, fillTypes = choice.fillTypes, markerName = choice.markerName, fillType = choice.fillType or choice.fillTypes[1]} or nil
-- hold AutoDrives egen rotations-tilstand konsistent med vores valg
self.pickups[choice.group] = self.pickups[choice.group] or {}
self.pickups[choice.group].lastTarget = choice.markerIndex
self.pickups[choice.group].lastVehicle = driver
if choice.level ~= nil then
logChoiceOnce(driver, choice) -- et hold ved Wait logges af ADOutbound
end
return choice.markerIndex
end
installLoadWaitHook(adEnv)
if ADUnloadWait ~= nil then
ADUnloadWait.install(adEnv)
end
if ADOutbound ~= nil and ADOutbound.install ~= nil then
ADOutbound.install(adEnv)
end
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning; Wait-punkter for rest-læs; udkørsel til silo/salg)")
end
-- Når AutoDrive stopper, får køretøjet hele ingredienslisten tilbage i varevalget
-- (AutoDrive gemmer valget i savegame, så listen overlever også en genindlæsning).
local function restoreStoppedVehicles()
-- en standset traktor må ikke tage "stå og læs"-grebet med over i næste opgave
for vehicle in pairs(activePickups) do
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule == nil or stateModule.isActive == nil or not stateModule:isActive() then
activePickups[vehicle] = nil
end
end
for vehicle in pairs(enRoute) do
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule == nil or stateModule.isActive == nil or not stateModule:isActive() then
enRoute[vehicle] = nil
end
end
for vehicle, state in pairs(supplyState) do
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule == nil or not stateModule:isActive() then
supplyState[vehicle] = nil
local selected = stateModule ~= nil and stateModule:getSelectedFillTypes() or nil
if type(selected) == "table" and #selected == 1 and selected[1] == state.narrowedTo then
stateModule.selectedFillTypes = ADSupplyPlanner.copyList(state.ingredients)
stateModule:raiseDirtyFlag()
end
end
end
end
-- Tilføjer fanen SIDST i ESC-menuen (mønster fra FS25_FarmManager/Courseplay). Siden skal
-- først være barn af PagingElement, ellers mangler den i idPageHash og rebuildTabList
-- fejler. Omrokering af siderne forskyder fane↔side-koblingen (sidebjælkens markering
-- hoppede), så positionen er den samme overalt: efter de eksisterende sider.
local function insertInGameMenuPage(frame, pageName, iconFile, iconUVs, predicate)
local inGameMenu = g_gui ~= nil and g_gui.screenControllers ~= nil and g_gui.screenControllers[InGameMenu] or nil
if inGameMenu == nil or inGameMenu.pagingElement == nil or inGameMenu.pageFrames == nil then
return false
end
local position = #inGameMenu.pageFrames + 1
if inGameMenu.controlIDs ~= nil then
inGameMenu.controlIDs[pageName] = nil
end
if inGameMenu.exposeControlsAsFields ~= nil then
inGameMenu:exposeControlsAsFields({pageName})
end
inGameMenu[pageName] = frame
inGameMenu.pagingElement:addElement(frame)
inGameMenu.pagingElement:updateAbsolutePosition()
inGameMenu.pagingElement:updatePageMapping()
inGameMenu:registerPage(frame, position, predicate)
inGameMenu:addPageTab(frame, iconFile, iconUVs)
if frame.updateAbsolutePosition ~= nil then
frame:updateAbsolutePosition()
end
inGameMenu:rebuildTabList()
return true
end
local function loadGui()
if g_gui == nil or SmartPickupFrame == nil or ADSmartPickup.dir == nil then
return
end
g_gui:loadProfiles(ADSmartPickup.dir .. "gui/guiProfiles.xml")
local frame = SmartPickupFrame.new(g_i18n, getAutoDriveEnv)
g_gui:loadGui(ADSmartPickup.dir .. "gui/SmartPickupFrame.xml", "smartPickupFrame", frame, true)
-- Eget faneikon i spillets stil (hvid streg på gennemsigtig baggrund, 1024×1024, DXT5).
local iconFile = Utils.getFilename("gui/menuIcon.dds", ADSmartPickup.dir)
local iconUVs = GuiUtils.getUVs({0, 0, 1024, 1024}, {1024, 1024})
if not insertInGameMenuPage(frame, "smartPickupFrame", iconFile, iconUVs, function() return g_currentMission ~= nil end) then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke indsætte fanen i ESC-menuen")
return
end
frame:initialize()
ADSmartPickup.guiFrame = frame
Logging.info(ADSmartPickup.LOG_PREFIX .. "ESC-fane 'Smart Pickup' registreret")
end
function ADSmartPickup:loadMap()
local ok, err = pcall(loadGui)
if not ok then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "GUI kunne ikke indlæses (resten af moden virker): %s", tostring(err))
end
end
function ADSmartPickup:update(dt)
if not isHooked then
local adEnv = getAutoDriveEnv()
if adEnv ~= nil and g_currentMission ~= nil and g_currentMission.storageSystem ~= nil then
installHook(adEnv)
isHooked = true
if ADRunsController ~= nil then
pcall(ADRunsController.load, adEnv)
pcall(ADRunsController.installSaveHook)
end
end
return
end
if ADUnloadWait ~= nil then
pcall(ADUnloadWait.releaseInactive)
if ADUnloadWait.adoptOrphansThrottled ~= nil then
ADUnloadWait.adoptOrphansThrottled(getAutoDriveEnv(), dt)
end
end
if ADOutbound ~= nil and ADOutbound.releaseInactive ~= nil then
pcall(ADOutbound.releaseInactive)
end
if ADRunsController ~= nil and ADRunsController.applyVehicleNamesThrottled ~= nil then
pcall(ADRunsController.applyVehicleNamesThrottled, getAutoDriveEnv(), dt)
end
local ok, err = pcall(restoreStoppedVehicles)
if not ok then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
supplyState = setmetatable({}, {__mode = "k"})
end
end
addModEventListener(ADSmartPickup)