Files
fs25-adsmartpickup/FS25_ADSmartPickup/adSmartPickup.lua
T

774 lines
34 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 = {}
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
local function isFillTypeAccepted(station, husbandry, fillType)
if husbandry ~= nil and husbandry.getHusbandryIsFillTypeSupported ~= nil then
return husbandry:getHusbandryIsFillTypeSupported(fillType)
end
return station.getIsFillTypeSupported == nil or station:getIsFillTypeSupported(fillType)
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 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
table.insert(needs, {fillType = fillType, level = level, capacity = capacity, litersPerMinute = demand[fillType]})
end
end
return needs
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.
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}
-- 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
-- Indsætter fanen i ESC-menuen foran Statistik/Priser (mønster fra FS25_FarmOperationsDashboard).
local function insertInGameMenuPage(frame, pageName, iconFile, uvs, position, 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 then
return false
end
local insertAt = position
for i = 1, #inGameMenu.pagingElement.elements do
local child = inGameMenu.pagingElement.elements[i]
if child == inGameMenu.pageStatistics or child == inGameMenu.pagePrices then
insertAt = i
break
end
end
if inGameMenu.controlIDs ~= nil then
inGameMenu.controlIDs[pageName] = nil
end
inGameMenu[pageName] = frame
inGameMenu.pagingElement:addElement(frame)
if inGameMenu.exposeControlsAsFields ~= nil then
inGameMenu:exposeControlsAsFields(pageName)
end
for _, list in ipairs({inGameMenu.pagingElement.elements, inGameMenu.pageFrames}) do
for i = 1, #list do
if list[i] == frame then
table.remove(list, i)
table.insert(list, insertAt, frame)
break
end
end
end
for i = 1, #inGameMenu.pagingElement.pages do
if inGameMenu.pagingElement.pages[i].element == frame then
local page = table.remove(inGameMenu.pagingElement.pages, i)
table.insert(inGameMenu.pagingElement.pages, insertAt, page)
break
end
end
inGameMenu.pagingElement:updateAbsolutePosition()
inGameMenu.pagingElement:updatePageMapping()
inGameMenu:registerPage(frame, position, predicate)
inGameMenu:addPageTab(frame, iconFile, GuiUtils.getUVs(uvs))
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)
if not insertInGameMenuPage(frame, "smartPickupFrame", iconFile, {0, 0, 512, 512}, 2, 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)