828 lines
37 KiB
Lua
828 lines
37 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
|
||
|
||
-- 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
|
||
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
|
||
for _, fillType in pairs(group.fillTypes or {}) do
|
||
fillTypes[fillType] = true
|
||
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] = true
|
||
end
|
||
end
|
||
return fillTypes
|
||
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
|
||
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.
|
||
-- 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)
|