diff --git a/FS25_ADSmartPickup/adLoadSwap.lua b/FS25_ADSmartPickup/adLoadSwap.lua
new file mode 100644
index 0000000..3ccd5db
--- /dev/null
+++ b/FS25_ADSmartPickup/adLoadSwap.lua
@@ -0,0 +1,107 @@
+-- AD Load Swap
+-- En traktor der venter ved et Wait-punkt med en vare målet er fuldt af, mens målet
+-- MANGLER en anden vare i traktorens liste, låser forsyningen (alle venter med halm,
+-- ingen henter TMR). Her afleveres resten i en silo der tager varen, så vognen er tom
+-- og næste tur henter det målet mangler.
+
+ADLoadSwap = {}
+
+ADLoadSwap.LOG_PREFIX = "ADSmartPickup: "
+-- Opslaget går over alle markører; ventende traktorer tjekker derfor kun så tit.
+ADLoadSwap.CHECK_INTERVAL_MS = 5000
+
+local warnedNoSilo = setmetatable({}, {__mode = "k"})
+
+-- Er en anden vare i listen så tom i målet, at et FULDT læs passer, og findes den i en silo?
+function ADLoadSwap.findMissingIngredient(adEnv, vehicle, restFillType)
+ local ingredients = ADSmartPickup.getIngredientsFor(adEnv, vehicle)
+ if ingredients == nil or #ingredients < 2 then
+ return nil
+ end
+ local choice = ADSmartPickup.findSupplyPickup(adEnv, vehicle, ingredients)
+ if choice == nil or choice.fillType == nil or choice.fillType == restFillType or choice.need.fits ~= true then
+ return nil
+ end
+ return choice
+end
+
+-- Kun rene lager-siloer: stalde og fabrikker har egne regler, og målet selv er fuldt.
+local function isSilo(station, homeStation)
+ local placeable = station.owningPlaceable
+ return placeable ~= nil and placeable.spec_silo ~= nil and placeable.getHusbandryFillLevel == nil
+ and placeable.spec_productionPoint == nil and (homeStation == nil or placeable ~= homeStation.owningPlaceable)
+end
+
+local function acceptsRest(station, fillType, rest, farmId)
+ if station.getIsFillTypeSupported ~= nil and not station:getIsFillTypeSupported(fillType) then
+ return false
+ end
+ local free = station.getFreeCapacity ~= nil and station:getFreeCapacity(fillType, farmId) or 0
+ return free >= rest
+end
+
+-- Nærmeste silo med plads til resten og vejnet til/fra; siloer der allerede har varen foretrækkes.
+function ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable)
+ local farmId = ADSmartPickup.getFarmId(vehicle)
+ local homeStation = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, homeWayPointId, farmId)
+ local x, _, z = getWorldTranslation(vehicle.rootNode)
+ local candidates = {}
+ for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
+ local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
+ if station ~= nil and isSilo(station, homeStation) and acceptsRest(station, fillType, rest, farmId) then
+ local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
+ local dx, dz = wayPoint.x - x, wayPoint.z - z
+ table.insert(candidates, {
+ marker = marker,
+ hasFillType = ADSmartPickup.getOwnStorageLevel(station, {fillType}) > 0,
+ distance = dx * dx + dz * dz,
+ })
+ end
+ end
+ table.sort(candidates, function(a, b)
+ if a.hasFillType ~= b.hasFillType then
+ return a.hasFillType
+ end
+ return a.distance < b.distance
+ end)
+ for _, candidate in ipairs(candidates) do
+ if isReachable(candidate.marker.id) then
+ return candidate.marker
+ end
+ end
+ return nil
+end
+
+-- Kaldes fra Wait-logikken hvert tick; redirect(task, id) og release(vehicle) er dens egne.
+-- true = traktoren er sendt til en silo (Wait-punktet er frigivet).
+function ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable)
+ wait.swapTimerMs = (wait.swapTimerMs or 0) + (dt or 0)
+ if wait.swapTimerMs < ADLoadSwap.CHECK_INTERVAL_MS then
+ return false
+ end
+ wait.swapTimerMs = 0
+ local vehicle = task.vehicle
+ local missing = ADLoadSwap.findMissingIngredient(adEnv, vehicle, wait.fillType)
+ if missing == nil then
+ return false
+ end
+ local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, wait.fillType, wait.rest, wait.homeDestinationID, isReachable)
+ local restName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(wait.fillType) or tostring(wait.fillType)
+ local missingName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(missing.fillType) or tostring(missing.fillType)
+ if marker == nil then
+ if not warnedNoSilo[vehicle] then
+ warnedNoSilo[vehicle] = true
+ Logging.warning(ADLoadSwap.LOG_PREFIX .. "'%s' venter med %.0f l %s, og målet mangler %s — ingen silo med plads og vejnet til/fra tager resten, så den bliver ved Wait-punktet", tostring(vehicle:getName()), wait.rest, tostring(restName), tostring(missingName))
+ end
+ return false
+ end
+ warnedNoSilo[vehicle] = nil
+ release(vehicle)
+ redirect(task, marker.id)
+ Logging.info(ADLoadSwap.LOG_PREFIX .. "'%s' bytter læs: %.0f l %s tilbage i %s (markør %s), målet mangler %s", tostring(vehicle:getName()), wait.rest, tostring(restName), tostring(marker.name), tostring(marker.markerIndex or marker.id), tostring(missingName))
+ return true
+end
+
+function ADLoadSwap.clearWarning(vehicle)
+ warnedNoSilo[vehicle] = nil
+end
diff --git a/FS25_ADSmartPickup/adUnloadWait.lua b/FS25_ADSmartPickup/adUnloadWait.lua
index 62ba880..cb54087 100644
--- a/FS25_ADSmartPickup/adUnloadWait.lua
+++ b/FS25_ADSmartPickup/adUnloadWait.lua
@@ -96,6 +96,9 @@ local function release(vehicle)
claims[wait.waitMarker.id] = nil
end
waits[vehicle] = nil
+ if ADLoadSwap ~= nil then
+ ADLoadSwap.clearWarning(vehicle)
+ end
end
-- Står køretøjet ved målet med rest-læs, som målet ikke kan tage? Så find et Wait-punkt.
@@ -136,7 +139,7 @@ local function startWaiting(adEnv, task)
end
-- Ventende køretøj: er der nu plads til hele resten, køres tilbage til målet.
-local function updateWaiting(adEnv, task, wait)
+local function updateWaiting(adEnv, task, wait, dt)
local vehicle = task.vehicle
if wait.task ~= task then
release(vehicle)
@@ -149,6 +152,12 @@ local function updateWaiting(adEnv, task, wait)
end
local free, capacity = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType)
if free == nil or not ADSupplyPlanner.canUnloadRest(free, rest, capacity) then
+ if ADLoadSwap ~= nil then
+ local isReachable = function(markerId)
+ return hasRouteBothWays(adEnv, vehicle, markerId, wait.homeDestinationID)
+ end
+ ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable)
+ end
return
end
local home = wait.homeDestinationID
@@ -157,10 +166,10 @@ local function updateWaiting(adEnv, task, wait)
Logging.info(ADUnloadWait.LOG_PREFIX .. "'%s' kører tilbage fra %s og læsser %.0f l af (målet har %.0f l fri)", tostring(vehicle:getName()), tostring(wait.waitMarker.name), rest, free)
end
-function ADUnloadWait.beforeUpdate(adEnv, task)
+function ADUnloadWait.beforeUpdate(adEnv, task, dt)
local wait = waits[task.vehicle]
if wait ~= nil then
- updateWaiting(adEnv, task, wait)
+ updateWaiting(adEnv, task, wait, dt)
else
startWaiting(adEnv, task)
end
@@ -204,7 +213,7 @@ function ADUnloadWait.install(adEnv)
local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting
task.update = function(self, dt)
- local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self)
+ local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt)
if not ok then
Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err))
release(self.vehicle)
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index 681394c..d054b10 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -16,6 +16,7 @@
+
diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua
index 9b61381..6d26360 100644
--- a/tests/test_adSmartPickup.lua
+++ b/tests/test_adSmartPickup.lua
@@ -31,10 +31,13 @@ local function makeStation(node, x, z, levels, isBroken, ownLevels)
return {
owningPlaceable = placeable,
loadTriggers = {{triggerNode = node}},
+ unloadTriggers = {{exactFillRootNode = node}},
getFillLevel = function(_, fillType)
if isBroken then error("boom") end
return levels[fillType] or 0
end,
+ getIsFillTypeSupported = function(_, fillType) return levels[fillType] ~= nil or (ownLevels ~= nil and ownLevels[fillType] ~= nil) end,
+ getFreeCapacity = function(_, fillType) return (levels.capacity or 200000) - (levels[fillType] or 0) end,
}
end
@@ -163,6 +166,7 @@ end
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adRunsController.lua")
+dofile("FS25_ADSmartPickup/adLoadSwap.lua")
dofile("FS25_ADSmartPickup/adUnloadWait.lua")
dofile("FS25_ADSmartPickup/adSmartPickup.lua")
listener:update(16) -- installerer hook'en
@@ -758,6 +762,63 @@ UnloadTask.update(tp, 16)
check("I4 kun run'ets pulje", tp.destinationID, 40)
ADRunsController.store = ADRuns.newStore()
+-- ==== S: læs-bytte fra Wait — halm-rest ved Wait, målet mangler TMR -> aflæs halm i silo ====
+local function buildSwapWorld()
+ buildWaitWorld() -- stald: halm FULD (50000/50000), TMR 10000/100000
+ -- halmladen (markør 21) er også et aflæsningssted med plads
+ table.insert(unloadStations, stations[2])
+ stations[2].unloadTriggers = {{exactFillRootNode = 121}}
+ stations[2].getIsFillTypeSupported = function(_, fillType) return fillType == STRAW end
+ stations[2].getFreeCapacity = function() return 100000 end
+ stations[2].owningPlaceable = {spec_silo = {storages = {{getFillLevel = function(_, fillType) return fillType == STRAW and 50000 or 0 end}}}}
+end
+
+-- S1: ventende traktor med halm-rest -> TMR mangler et fuldt læs (40000 l vogn, 90000 l fri) -> til halmladen
+buildSwapWorld()
+local sv, st = makeUnloadingVehicle("Fastrac Bytte", 521, 5000, 35000)
+UnloadTask.update(st, 16)
+check("S1 venter først ved Wait", st.destinationID, 40)
+UnloadTask.update(st, ADLoadSwap.CHECK_INTERVAL_MS + 1)
+check("S1 omdirigeret til halmladen", st.destinationID, 21)
+check("S1 logget bytte", countLogsSince(1, "bytter læs") >= 1, true)
+check("S1 Wait-punkt frigivet", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", sv), RT_ONLYPICKUP)
+
+-- S2: målet mangler IKKE et fuldt læs af TMR -> bliv ved Wait
+buildSwapWorld()
+waitBarnLevels[TMR] = 75000 -- 25000 fri < 40000 vogn
+local sv2, st2 = makeUnloadingVehicle("Fastrac Bliv", 522, 5000, 35000)
+UnloadTask.update(st2, 16)
+UnloadTask.update(st2, ADLoadSwap.CHECK_INTERVAL_MS + 1)
+check("S2 bliver ved Wait", st2.destinationID, 40)
+
+-- S3: ingen silo med plads/vare -> bliv ved Wait, én WARN
+buildSwapWorld()
+stations[2].getFreeCapacity = function() return 1000 end
+local sv3, st3 = makeUnloadingVehicle("Fastrac Ingen", 523, 5000, 35000)
+UnloadTask.update(st3, 16)
+local mark3 = #logLines
+UnloadTask.update(st3, ADLoadSwap.CHECK_INTERVAL_MS + 1)
+UnloadTask.update(st3, ADLoadSwap.CHECK_INTERVAL_MS + 1)
+check("S3 bliver ved Wait", st3.destinationID, 40)
+check("S3 én advarsel", countLogsSince(mark3, "ingen silo"), 1)
+
+-- S4: silo uden vejnet hjem springes over
+buildSwapWorld()
+FS25_AutoDrive.ADGraphManager.unreachable[21] = true
+local sv4, st4 = makeUnloadingVehicle("Fastrac Vej", 524, 5000, 35000)
+UnloadTask.update(st4, 16)
+UnloadTask.update(st4, ADLoadSwap.CHECK_INTERVAL_MS + 1)
+check("S4 unåelig silo -> bliv", st4.destinationID, 40)
+FS25_AutoDrive.ADGraphManager.unreachable = {}
+
+-- S5: traktor med kun én vare i listen bytter aldrig
+buildSwapWorld()
+local sv5, st5 = makeUnloadingVehicle("Fastrac Solo", 525, 5000, 35000)
+sv5.ad.stateModule.selectedFillTypes = {STRAW}
+UnloadTask.update(st5, 16)
+UnloadTask.update(st5, ADLoadSwap.CHECK_INTERVAL_MS + 1)
+check("S5 én vare -> bliver", st5.destinationID, 40)
+
-- ==== E: eksponerede opslag (bruges af runs-controller og læs-bytte) ====
local eDriver = buildCowWorld()
check("E1 aflæsningsstation ved waypoint", ADSmartPickup.getUnloadStationAtWayPointId(FS25_AutoDrive, 30, 1), unloadStations[1])