-- 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 = {} -- 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 -- Under dette niveau regnes siloen som tom (undgår ture efter støv-rester). ADSmartPickup.MIN_USEFUL_LITERS = 1 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"}) -- 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 local function 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 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 local function getDistanceToTriggers(triggers, nodeFields, x, z) local closest = 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) closest = math.min(closest, MathUtil.vector2Length(x - tx, z - tz)) end end end return closest end local function getClosestStation(candidateStations, triggersField, nodeFields, wayPoint, farmId) local bestStation = nil local bestDistance = ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE for _, station in pairs(candidateStations) do if g_currentMission.accessHandler:canFarmAccess(farmId, station) then local distance = getDistanceToTriggers(station[triggersField], nodeFields, wayPoint.x, wayPoint.z) if distance < bestDistance then bestDistance = distance bestStation = station end end end return bestStation end local function getLoadStationAtMarker(wayPoint, farmId) return getClosestStation(g_currentMission.storageSystem:getLoadingStations(), "loadTriggers", LOAD_TRIGGER_NODE_FIELDS, wayPoint, farmId) end local function getUnloadStationAtMarker(wayPoint, farmId) return getClosestStation(g_currentMission.storageSystem:getUnloadingStations(), "unloadTriggers", UNLOAD_TRIGGER_NODE_FIELDS, wayPoint, farmId) 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. local function 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 -- 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(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 -- Mindst fyldte silo der har varen; 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 findLeastFilled(markerStations, fillTypes, farmId) local best = {[true] = {level = math.huge}, [false] = {level = math.huge}} for _, entry in ipairs(markerStations) do local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId) if level >= ADSmartPickup.MIN_USEFUL_LITERS and level < best[isOwn].level then best[isOwn] = {level = level, marker = entry.marker} 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, } 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 findLeastFilled(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 placeable = station.owningPlaceable local productionPoint = placeable ~= nil and placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil 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 needs = {} local storages = getTargetStorages(station) for _, fillType in ipairs(ingredients) do if station.getIsFillTypeSupported == nil or station:getIsFillTypeSupported(fillType) then local level, freeSpace = 0, 0 for _, storage in pairs(storages) 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 table.insert(needs, {fillType = fillType, level = level, capacity = level + freeSpace}) end end return needs 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(targetWayPoint, farmId) or nil if target == nil then return nil 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 local choice = findLeastFilled(markerStations, {need.fillType}, farmId) if choice ~= nil then return { markerIndex = choice.markerIndex, markerName = choice.markerName, group = choice.group, level = choice.level, 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 -- Samlet valg: behovsstyret når flere varer er valgt, ellers mappe-logik. nil = AutoDrives original. function ADSmartPickup.choosePickup(adEnv, driver) local selected = driver.ad.stateModule:getSelectedFillTypes() local ingredients = ADSupplyPlanner.resolveIngredients(supplyState[driver], selected) 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) if choice.fillType == nil then return string.format("%.0f l", choice.level) end local fillTypeName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(choice.fillType) or tostring(choice.fillType) return string.format("%s: kilde %.0f l, målet har %.0f/%.0f l", tostring(fillTypeName), choice.level, choice.need.level, choice.need.capacity) 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)) return originalGetNextPickup(self, driver, forcedSkip) end if choice == nil then return originalGetNextPickup(self, driver, forcedSkip) end -- 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 Logging.info(ADSmartPickup.LOG_PREFIX .. "'%s' -> %s (%s)", tostring(driver:getName()), tostring(choice.markerName), describeChoice(choice)) return choice.markerIndex end Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning)") 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() 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 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 end return 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)