Files
fs25-adsmartpickup/FS25_ADSmartPickup/adSmartPickup.lua
T
masterdraco bcc245863e fix: stå og læs ved den valgte silo — AutoDrive kørte tom videre (v1.2.0.0)
Med rotateTargets + mapper afslutter AutoDrives LoadAtDestinationTask pålæsningen
i det øjeblik markøren nås; traktoren kørte derfor lige igennem silo-markøren og
tom til aflæsning. Når moden har valgt siloen, ses rotationen som slået fra for
netop det køretøj under opgavens update, så AutoDrive står og læsser. Grebet
slippes når kilden er tom eller AD stopper.

Desuden: samme valg logges kun én gang, og loggen viser eget/nabo-lager.
2026-09-21 15:55:07 +02:00

421 lines
18 KiB
Lua

-- 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"})
-- 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
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, 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,
}
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,
station = choice.station,
isOwn = choice.isOwn,
fillTypes = choice.fillTypes,
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)
local source = choice.isOwn and "eget lager" or "nabo-lager"
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)
return string.format("%s: kilde %.0f l, %s, målet har %.0f/%.0f l", tostring(fillTypeName), choice.level, source, choice.need.level, choice.need.capacity)
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
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
task.update = function(self, dt)
local ok, shouldWait = pcall(ADSmartPickup.shouldWaitForLoading, self.vehicle)
waitingVehicle = ok and 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}
-- 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)
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()
-- 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
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)