bcc245863e
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.
421 lines
18 KiB
Lua
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)
|