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)