diff --git a/FS25_ADSmartPickup/adLoadSwap.lua b/FS25_ADSmartPickup/adLoadSwap.lua
index ab1d9d1..30533c5 100644
--- a/FS25_ADSmartPickup/adLoadSwap.lua
+++ b/FS25_ADSmartPickup/adLoadSwap.lua
@@ -43,7 +43,8 @@ end
-- Nærmeste silo med plads til resten og vejnet til/fra; siloer der allerede har varen foretrækkes.
-- Nærmeste silo med plads til resten og vejnet fra traktoren; siloer der allerede har varen
-- foretrækkes. Returnerer marker — eller nil + en forklaring (til loggen).
-function ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable)
+-- excludedPlaceables (valgfri): {[placeable] = true} der aldrig må vælges (fx udkørslens kilder).
+function ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable, excludedPlaceables)
if vehicle.rootNode == nil then
return nil, "køretøjet har ingen position"
end
@@ -54,7 +55,8 @@ function ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointI
local silos, withSpace = 0, 0
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
- if station ~= nil and isSilo(station, homeStation) then
+ local isExcluded = excludedPlaceables ~= nil and excludedPlaceables[station ~= nil and station.owningPlaceable or false] == true
+ if station ~= nil and isSilo(station, homeStation) and not isExcluded then
silos = silos + 1
if acceptsRest(station, fillType, rest, farmId) then
withSpace = withSpace + 1
diff --git a/FS25_ADSmartPickup/adUnloadWait.lua b/FS25_ADSmartPickup/adUnloadWait.lua
index 3d22155..5eb985b 100644
--- a/FS25_ADSmartPickup/adUnloadWait.lua
+++ b/FS25_ADSmartPickup/adUnloadWait.lua
@@ -18,8 +18,6 @@ ADUnloadWait.LOG_PREFIX = "ADSmartPickup: "
-- køretøj -> {task, homeDestinationID, waitMarker, fillType, rest}
local waits = setmetatable({}, {__mode = "k"})
--- markør-id -> køretøj der har reserveret Wait-punktet
-local claims = setmetatable({}, {__mode = "v"})
local warnedNoWaitPoint = setmetatable({}, {__mode = "k"})
local isInstalled = false
@@ -39,72 +37,23 @@ local function getRestLoad(adEnv, vehicle)
return bestFillType, bestLevel
end
--- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper.
-local function getWaitCandidates(adEnv, poolGroup)
- local candidates = {}
- for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
- local isInPool = poolGroup == nil or poolGroup == "" or marker.group == poolGroup
- if isInPool and ADSupplyPlanner.isWaitMarker(marker) then
- local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
- if wayPoint ~= nil then
- table.insert(candidates, {marker = marker, x = wayPoint.x, z = wayPoint.z})
- end
- end
- end
- return candidates
-end
-
local function getWaitPoolGroup(vehicle)
local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(vehicle) or nil
return run ~= nil and run.waitPoolGroup or nil
end
-local function getVehiclePosition(vehicle)
- if vehicle.rootNode == nil then
- return nil
- end
- local x, _, z = getWorldTranslation(vehicle.rootNode)
- return x, z
-end
-
--- Ender ruten i målet? (AutoDrive returnerer tom/afkortet liste når der ingen vej er.)
-local function pathReaches(path, wayPointId)
- return path ~= nil and #path > 0 and path[#path].id == wayPointId
-end
-
--- Findes der vejnet fra køretøjet til Wait-punktet OG fra Wait-punktet hjem til målet?
--- Sendes AutoDrive mod et punkt uden rute, viser den "cannot reach" og slukker sig selv.
--- getPathTo/pathFromTo er rene opslag uden bivirkninger.
--- Kun "til punktet": ved aflæsning af (forkert) last i en silo er næste tur AD's egen
--- rute fra siloen, så "fra siloen hjem" er ikke et krav.
+-- Rutetjek, reservationer og omdirigering deles med udkørsels-modulet (adWaitPool).
+-- Kun "til punktet" ved silo-aflæsning: næste tur er AD's egen rute fra siloen.
local function hasRouteTo(adEnv, vehicle, wayPointId)
- local graph = adEnv.ADGraphManager
- if graph.getPathTo == nil then
- return true
- end
- return pathReaches(graph:getPathTo(vehicle, wayPointId), wayPointId)
+ return ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
end
local function hasRouteBothWays(adEnv, vehicle, waitWayPointId, homeWayPointId)
- local graph = adEnv.ADGraphManager
- if graph.getPathTo == nil or graph.pathFromTo == nil then
- return true
- end
- return pathReaches(graph:getPathTo(vehicle, waitWayPointId), waitWayPointId)
- and pathReaches(graph:pathFromTo(waitWayPointId, homeWayPointId), homeWayPointId)
+ return ADWaitPool.hasRouteBothWays(adEnv, vehicle, waitWayPointId, homeWayPointId)
end
--- Står AD midt i en (blokeret) aflæsning, stoppes den pænt først — ellers kører traktoren
--- væk med tippet vogn.
local function redirect(task, destinationID)
- local trailerModule = task.vehicle.ad.trailerModule
- if trailerModule ~= nil and trailerModule.isActiveAtTrigger ~= nil and trailerModule:isActiveAtTrigger() then
- if trailerModule.stopUnloading ~= nil then trailerModule:stopUnloading() end
- if trailerModule.stopLoading ~= nil then trailerModule:stopLoading() end
- end
- task.destinationID = destinationID
- task.state = ADUnloadWait.STATE_DRIVING
- task.vehicle.ad.drivePathModule:setPathTo(destinationID)
+ ADWaitPool.redirect(task, destinationID)
end
-- køretøj -> AD's oprindelige 2. markør (waypoint-id) mens der læsses af i en silo
@@ -137,9 +86,8 @@ local function restoreTarget(vehicle)
end
local function release(vehicle)
- local wait = waits[vehicle]
- if wait ~= nil and claims[wait.waitMarker.id] == vehicle then
- claims[wait.waitMarker.id] = nil
+ if waits[vehicle] ~= nil then
+ ADWaitPool.releaseVehicle(vehicle)
end
waits[vehicle] = nil
if ADLoadSwap ~= nil then
@@ -199,12 +147,11 @@ local function startWaiting(adEnv, task)
return
end
- local x, z = getVehiclePosition(vehicle)
local isReachable = function(candidate)
return hasRouteBothWays(adEnv, vehicle, candidate.marker.id, task.destinationID)
end
local pool = getWaitPoolGroup(vehicle)
- local candidate = x ~= nil and ADSupplyPlanner.pickNearestFreeWaitPoint(getWaitCandidates(adEnv, pool), claims, x, z, vehicle, isReachable) or nil
+ local candidate = ADWaitPool.pickFree(adEnv, vehicle, pool, isReachable)
if candidate == nil then
if not warnedNoWaitPoint[vehicle] then
warnedNoWaitPoint[vehicle] = true
@@ -214,7 +161,7 @@ local function startWaiting(adEnv, task)
return
end
warnedNoWaitPoint[vehicle] = nil
- claims[candidate.marker.id] = vehicle
+ ADWaitPool.claim(candidate.marker.id, vehicle)
waits[vehicle] = {task = task, homeDestinationID = task.destinationID, waitMarker = candidate.marker, fillType = fillType, rest = rest}
-- AD tipper (og holder vognen stille) så længe den står nær sit EGET mål — også i små
-- portioner, når målet forbruger løbende. Derfor peger AD's mål på Wait-punktet imens.
@@ -366,20 +313,9 @@ function ADUnloadWait.getWaitFor(vehicle)
return {markerName = wait.waitMarker ~= nil and wait.waitMarker.name or nil, fillType = wait.fillType, rest = wait.rest}
end
-function ADUnloadWait.getClaims()
- local result = {}
- for _, wait in pairs(waits) do
- if wait.waitMarker ~= nil and claims[wait.waitMarker.id] ~= nil then
- table.insert(result, {marker = wait.waitMarker, vehicle = claims[wait.waitMarker.id]})
- end
- end
- table.sort(result, function(a, b)
- if a.marker.group ~= b.marker.group then
- return tostring(a.marker.group) < tostring(b.marker.group)
- end
- return a.marker.id < b.marker.id
- end)
- return result
+-- Alle reservationer i puljen (også udkørsels-traktorer der holder).
+function ADUnloadWait.getClaims(adEnv)
+ return ADWaitPool.getClaims(adEnv)
end
function ADUnloadWait.install(adEnv)
@@ -389,6 +325,7 @@ function ADUnloadWait.install(adEnv)
end
isInstalled = true
ADUnloadWait.STATE_DRIVING = task.STATE_DRIVING or 2
+ ADWaitPool.STATE_DRIVING = ADUnloadWait.STATE_DRIVING
local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting
diff --git a/FS25_ADSmartPickup/adWaitPool.lua b/FS25_ADSmartPickup/adWaitPool.lua
new file mode 100644
index 0000000..8a6c1a4
--- /dev/null
+++ b/FS25_ADSmartPickup/adWaitPool.lua
@@ -0,0 +1,118 @@
+-- AD Wait Pool
+-- Fælles pulje af Wait-punkter (markører i mapper med ordet "Wait" i navnet) med
+-- reservationer, nærmeste-ledige-opslag, rutetjek og omdirigering af en AutoDrive-opgave.
+-- Bruges af adUnloadWait (rest-læs ved fuldt mål) og adOutbound (hold uden tur).
+
+ADWaitPool = {}
+
+ADWaitPool.LOG_PREFIX = "ADSmartPickup: "
+-- sættes af den der installerer opgave-hooks (task.STATE_DRIVING); 2 er AD's værdi
+ADWaitPool.STATE_DRIVING = 2
+
+-- markør-id -> køretøj der har reserveret Wait-punktet
+ADWaitPool.claims = setmetatable({}, {__mode = "v"})
+
+-- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper.
+function ADWaitPool.getCandidates(adEnv, poolGroup)
+ local candidates = {}
+ for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
+ local isInPool = poolGroup == nil or poolGroup == "" or marker.group == poolGroup
+ if isInPool and ADSupplyPlanner.isWaitMarker(marker) then
+ local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
+ if wayPoint ~= nil then
+ table.insert(candidates, {marker = marker, x = wayPoint.x, z = wayPoint.z})
+ end
+ end
+ end
+ return candidates
+end
+
+function ADWaitPool.claim(markerId, vehicle)
+ ADWaitPool.claims[markerId] = vehicle
+end
+
+function ADWaitPool.getOwner(markerId)
+ return ADWaitPool.claims[markerId]
+end
+
+function ADWaitPool.releaseVehicle(vehicle)
+ for markerId, owner in pairs(ADWaitPool.claims) do
+ if owner == vehicle then
+ ADWaitPool.claims[markerId] = nil
+ end
+ end
+end
+
+local function getVehiclePosition(vehicle)
+ if vehicle.rootNode == nil then
+ return nil
+ end
+ local x, _, z = getWorldTranslation(vehicle.rootNode)
+ return x, z
+end
+
+-- Nærmeste ledige (eller eget reserverede) Wait-punkt fra vognens position; isReachable(candidate)
+-- valgfri. nil uden position eller uden brugbart punkt. Reserverer IKKE selv.
+function ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable)
+ local x, z = getVehiclePosition(vehicle)
+ if x == nil then
+ return nil
+ end
+ return ADSupplyPlanner.pickNearestFreeWaitPoint(ADWaitPool.getCandidates(adEnv, poolGroup), ADWaitPool.claims, x, z, vehicle, isReachable)
+end
+
+-- Ender ruten i målet? (AutoDrive returnerer tom/afkortet liste når der ingen vej er.)
+local function pathReaches(path, wayPointId)
+ return path ~= nil and #path > 0 and path[#path].id == wayPointId
+end
+
+-- Sendes AutoDrive mod et punkt uden rute, viser den "cannot reach" og slukker sig selv.
+-- getPathTo/pathFromTo er rene opslag uden bivirkninger.
+function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
+ local graph = adEnv.ADGraphManager
+ if graph.getPathTo == nil then
+ return true
+ end
+ return pathReaches(graph:getPathTo(vehicle, wayPointId), wayPointId)
+end
+
+function ADWaitPool.hasRouteBothWays(adEnv, vehicle, toWayPointId, homeWayPointId)
+ local graph = adEnv.ADGraphManager
+ if graph.getPathTo == nil or graph.pathFromTo == nil then
+ return true
+ end
+ return pathReaches(graph:getPathTo(vehicle, toWayPointId), toWayPointId)
+ and pathReaches(graph:pathFromTo(toWayPointId, homeWayPointId), homeWayPointId)
+end
+
+-- Står AD midt i en (blokeret) af-/pålæsning, stoppes den pænt først — ellers kører
+-- traktoren væk med tippet vogn. Kun opgavens destination ændres.
+function ADWaitPool.redirect(task, destinationID)
+ local trailerModule = task.vehicle.ad.trailerModule
+ if trailerModule ~= nil and trailerModule.isActiveAtTrigger ~= nil and trailerModule:isActiveAtTrigger() then
+ if trailerModule.stopUnloading ~= nil then trailerModule:stopUnloading() end
+ if trailerModule.stopLoading ~= nil then trailerModule:stopLoading() end
+ end
+ task.destinationID = destinationID
+ task.state = ADWaitPool.STATE_DRIVING
+ task.vehicle.ad.drivePathModule:setPathTo(destinationID)
+end
+
+-- Hvem holder hvor (GUI'ets Wait-puljer-visning). Sorteret på mappe, dernæst markør-id.
+function ADWaitPool.getClaims(adEnv)
+ local result = {}
+ local graph = adEnv ~= nil and adEnv.ADGraphManager or (FS25_AutoDrive ~= nil and FS25_AutoDrive.ADGraphManager or nil)
+ for markerId, vehicle in pairs(ADWaitPool.claims) do
+ local marker = graph ~= nil and graph.getMapMarkerByWayPointId ~= nil and graph:getMapMarkerByWayPointId(markerId) or nil
+ if marker ~= nil then
+ table.insert(result, {marker = marker, vehicle = vehicle})
+ end
+ end
+ table.sort(result, function(a, b)
+ if a.marker.group ~= b.marker.group then
+ return tostring(a.marker.group) < tostring(b.marker.group)
+ end
+ return a.marker.id < b.marker.id
+ end)
+ return result
+end
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index 6f6515d..c1eae3e 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -17,6 +17,7 @@
+
diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua
index 3cc7685..16ec510 100644
--- a/tests/test_adSmartPickup.lua
+++ b/tests/test_adSmartPickup.lua
@@ -168,6 +168,7 @@ dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adRuns.lua")
dofile("FS25_ADSmartPickup/adRunsController.lua")
dofile("FS25_ADSmartPickup/adLoadSwap.lua")
+dofile("FS25_ADSmartPickup/adWaitPool.lua")
dofile("FS25_ADSmartPickup/adUnloadWait.lua")
dofile("FS25_ADSmartPickup/adSmartPickup.lua")
listener:update(16) -- installerer hook'en
diff --git a/tests/test_adWaitPool.lua b/tests/test_adWaitPool.lua
new file mode 100644
index 0000000..dd5c0b2
--- /dev/null
+++ b/tests/test_adWaitPool.lua
@@ -0,0 +1,96 @@
+-- Kør: luajit tests/test_adWaitPool.lua (fra repo-roden)
+-- Wait-puljen: kandidater pr. mappe, reservationer, nærmeste ledige, rutetjek, redirect.
+
+dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
+dofile("FS25_ADSmartPickup/adWaitPool.lua")
+
+local failures = 0
+local function check(name, actual, expected)
+ if actual == expected then
+ print("OK " .. name)
+ else
+ failures = failures + 1
+ print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
+ end
+end
+
+local markers, wayPoints = {}, {}
+local function addMarker(id, name, group, x, z)
+ wayPoints[id] = {id = id, x = x, z = z, incoming = {}}
+ local marker = {id = id, markerIndex = #markers + 1, name = name, group = group}
+ table.insert(markers, marker)
+ return marker
+end
+local unreachable = {}
+local adEnv = {
+ ADGraphManager = {
+ getMapMarkers = function() return markers end,
+ getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
+ getWayPointById = function(_, id) return wayPoints[id] end,
+ getPathTo = function(_, vehicle, id) if unreachable[id] then return {} end return {{id = id}} end,
+ pathFromTo = function(_, from, to) if unreachable[to] then return {} end return {{id = to}} end,
+ },
+}
+getWorldTranslation = function(node) return node.x, 0, node.z end
+
+addMarker(10, "cow wait 1", "cow wait", 0, 0)
+addMarker(11, "cow wait 2", "cow wait", 50, 0)
+addMarker(20, "Wait 3", "General Wait", 10, 0)
+addMarker(30, "silo 1", "silo", 5, 0)
+
+-- W1: kandidater
+check("W1 kun mappen", #ADWaitPool.getCandidates(adEnv, "cow wait"), 2)
+check("W1 tom pulje = alle Wait-mapper", #ADWaitPool.getCandidates(adEnv, ""), 3)
+check("W1 nil = alle", #ADWaitPool.getCandidates(adEnv, nil), 3)
+
+-- W2: nærmeste ledige fra vognens position; reserverede springes over; egen reservation OK
+local v1, v2 = {name = "v1", rootNode = {x = 45, z = 0}}, {name = "v2", rootNode = {x = 45, z = 0}}
+check("W2 nærmeste", ADWaitPool.pickFree(adEnv, v1, "cow wait").marker.id, 11)
+ADWaitPool.claim(11, v1)
+check("W2 reserveret springes over", ADWaitPool.pickFree(adEnv, v2, "cow wait").marker.id, 10)
+check("W2 egen reservation genbruges", ADWaitPool.pickFree(adEnv, v1, "cow wait").marker.id, 11)
+
+-- W3: claim / owner / release
+check("W3 ejer", ADWaitPool.getOwner(11), v1)
+ADWaitPool.releaseVehicle(v1)
+check("W3 frigivet", ADWaitPool.getOwner(11), nil)
+check("W3 nu ledig for v2", ADWaitPool.pickFree(adEnv, v2, "cow wait").marker.id, 11)
+
+-- W4: nåelighed
+unreachable[11] = true
+check("W4 uopnåelig nærmeste -> næste", ADWaitPool.pickFree(adEnv, v2, "cow wait", function(c) return ADWaitPool.hasRouteTo(adEnv, v2, c.marker.id) end).marker.id, 10)
+check("W4 hasRouteTo false", ADWaitPool.hasRouteTo(adEnv, v2, 11), false)
+check("W4 hasRouteBothWays true", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 30), true)
+check("W4 hasRouteBothWays hjem uopnåelig", ADWaitPool.hasRouteBothWays(adEnv, v2, 10, 11), false)
+unreachable[11] = nil
+check("W4 intet punkt -> nil", ADWaitPool.pickFree(adEnv, v2, "cow wait", function() return false end), nil)
+
+-- W5: getClaims sorteret på mappe, dernæst id
+ADWaitPool.claim(20, v2)
+ADWaitPool.claim(10, v1)
+local claims = ADWaitPool.getClaims(adEnv)
+check("W5 to reservationer", #claims, 2)
+check("W5 første = General Wait", claims[1].marker.group, "General Wait")
+check("W5 anden = cow wait 1", claims[2].marker.id, 10)
+ADWaitPool.releaseVehicle(v1)
+ADWaitPool.releaseVehicle(v2)
+check("W5 alt frigivet", #ADWaitPool.getClaims(adEnv), 0)
+
+-- W6: redirect stopper trigger-aktivitet og sætter sti
+local stopped = {}
+local task = {
+ destinationID = 99, state = 1,
+ vehicle = {ad = {
+ trailerModule = {isActiveAtTrigger = function() return true end, stopUnloading = function() stopped.unload = true end, stopLoading = function() stopped.load = true end},
+ drivePathModule = {setPathTo = function(_, id) stopped.pathTo = id end},
+ }},
+}
+ADWaitPool.STATE_DRIVING = 2
+ADWaitPool.redirect(task, 10)
+check("W6 destination", task.destinationID, 10)
+check("W6 state driving", task.state, 2)
+check("W6 setPathTo", stopped.pathTo, 10)
+check("W6 stopUnloading", stopped.unload, true)
+
+print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
+os.exit(failures == 0 and 0 or 1)