feat: behovsstyret forsyning — én traktor holder alle input fyldt

This commit is contained in:
2026-09-21 12:52:46 +02:00
parent a7d7d7815f
commit fb87dc4d37
2 changed files with 306 additions and 40 deletions
+183 -37
View File
@@ -3,6 +3,10 @@
-- "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 = {}
@@ -13,7 +17,13 @@ ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE = 40
ADSmartPickup.MIN_USEFUL_LITERS = 1
ADSmartPickup.LOG_PREFIX = "ADSmartPickup: "
local LOAD_TRIGGER_NODE_FIELDS = {"triggerNode"}
local UNLOAD_TRIGGER_NODE_FIELDS = {"exactFillRootNode", "triggerNode", "aiNode"}
local isHooked = false
-- køretøj -> {ingredients, narrowedTo}. AutoDrive roterer selv vare ved triggeren
-- når flere er valgt, så valget indsnævres til én pr. tur; listen huskes her.
local supplyState = setmetatable({}, {__mode = "k"})
-- Cross-mod adgang: FS25-mods er sandboxede; AutoDrives globals ligger
-- i den globale tabel FS25_AutoDrive (zip-navnet).
@@ -54,24 +64,26 @@ local function getWantedFillTypes(driver)
return wanted
end
local function getDistanceToStation(station, x, z)
local function getDistanceToTriggers(triggers, nodeFields, x, z)
local closest = math.huge
for _, loadTrigger in pairs(station.loadTriggers or {}) do
local node = loadTrigger.triggerNode
if node ~= nil and entityExists(node) then
local tx, _, tz = getWorldTranslation(node)
closest = math.min(closest, MathUtil.vector2Length(x - tx, z - tz))
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 getStationAtMarker(wayPoint, farmId)
local function getClosestStation(candidateStations, triggersField, nodeFields, wayPoint, farmId)
local bestStation = nil
local bestDistance = ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE
for _, station in pairs(g_currentMission.storageSystem:getLoadingStations()) do
for _, station in pairs(candidateStations) do
if g_currentMission.accessHandler:canFarmAccess(farmId, station) then
local distance = getDistanceToStation(station, wayPoint.x, wayPoint.z)
local distance = getDistanceToTriggers(station[triggersField], nodeFields, wayPoint.x, wayPoint.z)
if distance < bestDistance then
bestDistance = distance
bestStation = station
@@ -81,6 +93,14 @@ local function getStationAtMarker(wayPoint, farmId)
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.
@@ -113,7 +133,45 @@ local function getStationFillLevel(station, fillTypes, farmId)
return total, false
end
-- Returnerer markerIndex for mappens mindst fyldte silo der har varen, ellers nil.
-- Parrer markører med deres læssestation (markører uden station udelades).
-- excludedPlaceable: aflæsningsmålets egen placeable må ikke være kilde.
local function collectMarkerStations(adEnv, candidateMarkers, farmId, excludedPlaceable)
local markerStations = {}
for _, marker in pairs(candidateMarkers) do
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
local station = wayPoint ~= nil and getLoadStationAtMarker(wayPoint, farmId) or nil
local isExcluded = station ~= nil and excludedPlaceable ~= nil and station.owningPlaceable == excludedPlaceable
if station ~= nil and not isExcluded then
table.insert(markerStations, {marker = marker, station = station})
end
end
return markerStations
end
-- Mindst fyldte silo der har varen; siloer med varen i EGET lager går altid
-- forud for dem der kun kan låne fra naboer. Returnerer et valg eller nil.
local function findLeastFilled(markerStations, fillTypes, farmId)
local best = {[true] = {level = math.huge}, [false] = {level = math.huge}}
for _, entry in ipairs(markerStations) do
local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId)
if level >= ADSmartPickup.MIN_USEFUL_LITERS and level < best[isOwn].level then
best[isOwn] = {level = level, marker = entry.marker}
end
end
local choice = best[true].marker ~= nil and best[true] or best[false]
if choice.marker == nil then
return nil
end
return {
markerIndex = choice.marker.markerIndex,
markerName = choice.marker.name,
group = choice.marker.group,
level = choice.level,
}
end
-- Mappe-logik: mappens mindst fyldte silo der har den/de valgte varer, ellers nil.
function ADSmartPickup.findLeastFilledPickup(adEnv, driver)
local firstMarker = driver.ad.stateModule:getFirstMarker()
if firstMarker == nil or firstMarker.group == nil or firstMarker.group == "All" then
@@ -124,25 +182,92 @@ function ADSmartPickup.findLeastFilledPickup(adEnv, 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
-- siloer med varen i EGET lager går altid forud for dem der kun kan låne fra naboer
local best = {[true] = {level = math.huge}, [false] = {level = math.huge}}
for _, marker in ipairs(adEnv.ADGraphManager:getMapMarkersInGroup(firstMarker.group)) do
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
local station = wayPoint ~= nil and getStationAtMarker(wayPoint, farmId) or nil
if station ~= nil then
local level, isOwn = getStationFillLevel(station, fillTypes, farmId)
if level >= ADSmartPickup.MIN_USEFUL_LITERS and level < best[isOwn].level then
best[isOwn] = {level = level, marker = marker}
-- Niveau + kapacitet pr. ingrediens i aflæsningsmålets lager.
local function getTargetNeeds(station, ingredients, farmId)
local needs = {}
for _, fillType in ipairs(ingredients) do
if station.getIsFillTypeSupported == nil or station:getIsFillTypeSupported(fillType) then
local level, freeSpace = 0, 0
for _, storage in pairs(station.targetStorages or {}) 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
local choice = best[true].marker ~= nil and best[true] or best[false]
if choice.marker == nil then
-- 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
return choice.marker.markerIndex, choice.marker.name, choice.level
local targetWayPoint = adEnv.ADGraphManager:getWayPointById(secondMarker.id)
local target = targetWayPoint ~= nil and getUnloadStationAtMarker(targetWayPoint, farmId) or nil
if target == nil then
return nil
end
local allMarkers = adEnv.ADGraphManager:getMapMarkers()
local markerStations = collectMarkerStations(adEnv, allMarkers, farmId, target.owningPlaceable)
for _, need in ipairs(ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId))) do
local choice = findLeastFilled(markerStations, {need.fillType}, farmId)
if choice ~= nil then
return {
markerIndex = choice.markerIndex,
markerName = choice.markerName,
group = choice.group,
level = choice.level,
fillType = need.fillType,
need = need,
}
end
end
return nil
end
local function applySupplyChoice(driver, ingredients, choice)
local stateModule = driver.ad.stateModule
stateModule:setFillType(choice.fillType)
stateModule.selectedFillTypes = {choice.fillType}
stateModule:raiseDirtyFlag()
supplyState[driver] = {ingredients = ingredients, narrowedTo = choice.fillType}
end
-- Samlet valg: behovsstyret når flere varer er valgt, ellers mappe-logik. nil = AutoDrives original.
function ADSmartPickup.choosePickup(adEnv, driver)
local selected = driver.ad.stateModule:getSelectedFillTypes()
local ingredients = ADSupplyPlanner.resolveIngredients(supplyState[driver], selected)
if ingredients == nil then
supplyState[driver] = nil
return ADSmartPickup.findLeastFilledPickup(adEnv, driver)
end
local choice = ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)
if choice == nil then
return ADSmartPickup.findLeastFilledPickup(adEnv, driver)
end
applySupplyChoice(driver, ingredients, choice)
return choice
end
local function describeChoice(choice)
if choice.fillType == nil then
return string.format("%.0f l", choice.level)
end
local fillTypeName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(choice.fillType) or tostring(choice.fillType)
return string.format("%s: kilde %.0f l, målet har %.0f/%.0f l", tostring(fillTypeName), choice.level, choice.need.level, choice.need.capacity)
end
local function installHook(adEnv)
@@ -150,36 +275,57 @@ local function installHook(adEnv)
local originalGetNextPickup = manager.getNextPickup
manager.getNextPickup = function(self, driver, forcedSkip)
local ok, markerIndex, markerName, level = pcall(ADSmartPickup.findLeastFilledPickup, adEnv, driver)
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(markerIndex))
Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i silo-opslag, bruger AutoDrives rotation: %s", tostring(choice))
return originalGetNextPickup(self, driver, forcedSkip)
end
if markerIndex == nil then
if choice == nil then
return originalGetNextPickup(self, driver, forcedSkip)
end
-- hold AutoDrives egen rotations-tilstand konsistent med vores valg
local groupName = driver.ad.stateModule:getFirstMarker().group
self.pickups[groupName] = self.pickups[groupName] or {}
self.pickups[groupName].lastTarget = markerIndex
self.pickups[groupName].lastVehicle = driver
self.pickups[choice.group] = self.pickups[choice.group] or {}
self.pickups[choice.group].lastTarget = choice.markerIndex
self.pickups[choice.group].lastVehicle = driver
Logging.info(ADSmartPickup.LOG_PREFIX .. "'%s' -> %s (%.0f l)", tostring(driver:getName()), tostring(markerName), level)
return markerIndex
Logging.info(ADSmartPickup.LOG_PREFIX .. "'%s' -> %s (%s)", tostring(driver:getName()), tostring(choice.markerName), describeChoice(choice))
return choice.markerIndex
end
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen vælges i mappe-rotation)")
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning)")
end
-- Når AutoDrive stopper, får køretøjet hele ingredienslisten tilbage i varevalget
-- (AutoDrive gemmer valget i savegame, så listen overlever også en genindlæsning).
local function restoreStoppedVehicles()
for vehicle, state in pairs(supplyState) do
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule == nil or not stateModule:isActive() then
supplyState[vehicle] = nil
local selected = stateModule ~= nil and stateModule:getSelectedFillTypes() or nil
if type(selected) == "table" and #selected == 1 and selected[1] == state.narrowedTo then
stateModule.selectedFillTypes = ADSupplyPlanner.copyList(state.ingredients)
stateModule:raiseDirtyFlag()
end
end
end
end
function ADSmartPickup:update(dt)
if isHooked then
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 adEnv = getAutoDriveEnv()
if adEnv ~= nil and g_currentMission ~= nil and g_currentMission.storageSystem ~= nil then
installHook(adEnv)
isHooked = true
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