-- 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") 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). 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 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 g_currentMission.accessHandler:canFarmAccess(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 -- 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. 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. 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, 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(driver:getName()), 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] 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 = getFoodPriority(station, fillType) table.insert(needs, {fillType = fillType, level = level, capacity = capacity, litersPerMinute = demand[fillType], scoreOverride = troughScore, priority = priority}) end end return needs end -- Til fanen: samme rangering som valget bruger (uden vogn-størrelse). Fulde varer sidst. function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId) local needs = getTargetNeeds(station, fillTypes, farmId, {}) 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. function ADSmartPickup.getTargetFreeCapacity(adEnv, driver, fillType) local secondMarker = driver.ad.stateModule:getSecondMarker() local farmId = getFarmId(driver) if secondMarker == nil or farmId == nil then return nil end local targetWayPoint = adEnv.ADGraphManager:getWayPointById(secondMarker.id) 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 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) local secondMarker = driver.ad.stateModule:getSecondMarker() local farmId = getFarmId(driver) if secondMarker == nil or farmId == nil then return nil end local targetWayPoint = adEnv.ADGraphManager:getWayPointById(secondMarker.id) 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) for _, need in ipairs(ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId, demand), loadCapacity)) do local choice = findBestSource(markerStations, {need.fillType}, farmId) if choice ~= nil then return { markerIndex = choice.markerIndex, 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 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 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 -- Samlet valg: behovsstyret med ingrediensliste, ellers mappe-logik. nil = AutoDrives original. function ADSmartPickup.choosePickup(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 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(driver:getName()), 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 task.update = function(self, dt) 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(self.vehicle:getName())) 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) 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] = {station = choice.station, fillTypes = choice.fillTypes, markerName = choice.markerName, fillType = choice.fillType or choice.fillTypes[1]} -- 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 logChoiceOnce(driver, choice) return choice.markerIndex end installLoadWaitHook(adEnv) if ADUnloadWait ~= nil then ADUnloadWait.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)") 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, 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) local iconFile = Utils.getFilename("icon.dds", ADSmartPickup.dir) -- icon.dds er 512×512. Uden referencestørrelsen normaliserer getUVs mod 1024×1024, -- og fanen ville kun vise ikonets øverste venstre fjerdedel. local iconUVs = GuiUtils.getUVs({0, 0, 512, 512}, {512, 512}) 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) 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)