From b8da94a254a545a462d1eff649345a923afe3a80 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Tue, 22 Sep 2026 21:44:55 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20rute-tjek=20begge=20veje=20f=C3=B8r=20Wa?= =?UTF-8?q?it-omdirigering;=20hjem=20ved=20halv-tomt=20m=C3=A5l=20(v1.7.2.?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In-game 2026-09-22 21:37: Fastrac omdirigeret til 'Cow Wait' uden vejnet dertil -> AutoDrive 'cannot reach' + StopAndDisableADTask (AD slukket i stalden). Nu: ADGraphManager:getPathTo + pathFromTo (rene opslag) skal ende i hhv. Wait-punkt og mål, ellers næste punkt; intet nåeligt = som i dag + advarsel. 700.000 l-vogn i 300.000 l-stald kunne aldrig opfylde 'hele resten passer': ADSupplyPlanner.canUnloadRest = fri >= min(rest, 50 % af kapacitet). --- FS25_ADSmartPickup/adSmartPickup.lua | 4 +-- FS25_ADSmartPickup/adSupplyPlanner.lua | 35 ++++++++++++++++----- FS25_ADSmartPickup/adUnloadWait.lua | 28 ++++++++++++++--- FS25_ADSmartPickup/modDesc.xml | 2 +- README.md | 6 +++- tests/test_adSmartPickup.lua | 43 ++++++++++++++++++++++++-- tests/test_adSupplyPlanner.lua | 12 +++++++ 7 files changed, 112 insertions(+), 18 deletions(-) diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index 3376380..55bf209 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -401,7 +401,7 @@ local function getLoadCapacity(adEnv, driver) return capacity ~= nil and capacity > 0 and capacity or nil end --- Fri plads til varen i køretøjets aflæsningsmål (bruges af Wait-punkt-logikken); +-- Fri plads + kapacitet for varen i køretøjets aflæsningsmål (Wait-punkt-logikken); -- nil hvis målet ikke kendes eller ikke tager varen. function ADSmartPickup.getTargetFreeCapacity(adEnv, driver, fillType) local secondMarker = driver.ad.stateModule:getSecondMarker() @@ -418,7 +418,7 @@ function ADSmartPickup.getTargetFreeCapacity(adEnv, driver, fillType) if level == nil then return nil end - return capacity - level + return capacity - level, capacity end -- Behovsstyret: den ingrediens aflæsningsmålet mangler mest, som også findes i en silo. diff --git a/FS25_ADSmartPickup/adSupplyPlanner.lua b/FS25_ADSmartPickup/adSupplyPlanner.lua index 91a7ea7..c8ce7ad 100644 --- a/FS25_ADSmartPickup/adSupplyPlanner.lua +++ b/FS25_ADSmartPickup/adSupplyPlanner.lua @@ -132,20 +132,39 @@ function ADSupplyPlanner.isWaitMarker(marker) end -- candidates: {{marker, x, z}}; claims: {[marker.id] = køretøj}. Nærmeste punkt der er --- ledigt eller allerede reserveret af dette køretøj. nil hvis alle er optaget. -function ADSupplyPlanner.pickNearestFreeWaitPoint(candidates, claims, x, z, vehicle) - local best, bestDistance = nil, nil +-- ledigt (eller allerede reserveret af dette køretøj) og som isReachable(candidate) +-- godkender (valgfri; AutoDrive slukker sig selv, hvis den sendes mod et punkt uden rute). +-- nil hvis intet punkt duer. Nåelighed tjekkes kun for de punkter der er i spil, nærmeste først. +function ADSupplyPlanner.pickNearestFreeWaitPoint(candidates, claims, x, z, vehicle, isReachable) + local free = {} for _, candidate in ipairs(candidates) do local owner = claims[candidate.marker.id] if owner == nil or owner == vehicle then local dx, dz = candidate.x - x, candidate.z - z - local distance = dx * dx + dz * dz - if bestDistance == nil or distance < bestDistance then - best, bestDistance = candidate, distance - end + table.insert(free, {candidate = candidate, distance = dx * dx + dz * dz}) end end - return best + table.sort(free, function(a, b) return a.distance < b.distance end) + for _, entry in ipairs(free) do + if isReachable == nil or isReachable(entry.candidate) then + return entry.candidate + end + end + return nil +end + +-- Så stor en andel af målets kapacitet skal være fri, før en rest der er større end +-- det, kan afleveres (en 700.000 l-vogn kan aldrig tømme HELE resten i en 300.000 l-stald). +ADSupplyPlanner.WAIT_RETURN_FREE_FRACTION = 0.5 + +-- Kan den ventende køre hjem? Ja når hele resten passer, eller når målet er mindst +-- WAIT_RETURN_FREE_FRACTION tomt. capacity nil = kun hele resten tæller. +function ADSupplyPlanner.canUnloadRest(freeSpace, rest, capacity) + local needed = rest + if capacity ~= nil and capacity > 0 then + needed = math.min(rest, capacity * ADSupplyPlanner.WAIT_RETURN_FREE_FRACTION) + end + return freeSpace >= needed end -- needs: {{fillType, level, capacity[, litersPerMinute]}, ...}. Returnerer NY liste, diff --git a/FS25_ADSmartPickup/adUnloadWait.lua b/FS25_ADSmartPickup/adUnloadWait.lua index 4b2eafe..99aa28c 100644 --- a/FS25_ADSmartPickup/adUnloadWait.lua +++ b/FS25_ADSmartPickup/adUnloadWait.lua @@ -60,6 +60,23 @@ local function getVehiclePosition(vehicle) 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. +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) +end + local function redirect(task, destinationID) task.destinationID = destinationID task.state = ADUnloadWait.STATE_DRIVING @@ -90,11 +107,14 @@ local function startWaiting(adEnv, task) end local x, z = getVehiclePosition(vehicle) - local candidate = x ~= nil and ADSupplyPlanner.pickNearestFreeWaitPoint(getWaitCandidates(adEnv), claims, x, z, vehicle) or nil + local isReachable = function(candidate) + return hasRouteBothWays(adEnv, vehicle, candidate.marker.id, task.destinationID) + end + local candidate = x ~= nil and ADSupplyPlanner.pickNearestFreeWaitPoint(getWaitCandidates(adEnv), claims, x, z, vehicle, isReachable) or nil if candidate == nil then if not warnedNoWaitPoint[vehicle] then warnedNoWaitPoint[vehicle] = true - Logging.warning(ADUnloadWait.LOG_PREFIX .. "'%s' har %.0f l rest, og målet er fuldt — intet ledigt Wait-punkt (markør i en mappe med 'Wait' i navnet), så AutoDrive venter ved målet", tostring(vehicle:getName()), rest) + Logging.warning(ADUnloadWait.LOG_PREFIX .. "'%s' har %.0f l rest, og målet er fuldt — intet ledigt Wait-punkt med vejnet til OG fra målet (markør i en mappe med 'Wait' i navnet), så AutoDrive venter ved målet", tostring(vehicle:getName()), rest) end return end @@ -118,8 +138,8 @@ local function updateWaiting(adEnv, task, wait) release(vehicle) return end - local free = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType) - if free == nil or free < rest then + local free, capacity = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType) + if free == nil or not ADSupplyPlanner.canUnloadRest(free, rest, capacity) then return end local home = wait.homeDestinationID diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 662514d..0183d76 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.7.1.0 + 1.7.2.0 <en>AD Smart Pickup</en> diff --git a/README.md b/README.md index 132421b..cd9bac8 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,11 @@ spærrer for de næste i køen. Med Wait-punkter: dine mark-ventepladser ("F91 Wait" i mark-mapperne) rører den ikke. 2. Kan målet ikke tage mere af vognens vare, kører traktoren til det **nærmeste ledige** Wait-punkt (modden holder styr på reservationer) og venter dér. -3. Når målet igen har plads til **hele** resten, kører den tilbage og læsser af. Derefter normal tur. +3. Når målet igen har plads til **hele** resten — eller er mindst **halvt tomt** (kæmpe vogne kan aldrig + tømme hele resten i en lille stald) — kører den tilbage og læsser af. Derefter normal tur. +4. Wait-punkterne SKAL være forbundet til vejnettet både fra målets udkørsel og tilbage til målet. + Modden tjekker ruten begge veje før den omdirigerer; uden rute prøves næste punkt (AutoDrive + ville ellers vise "cannot reach" og slukke sig selv). Kun den aktuelle aflæsningsopgaves destination ændres — mål, rotation og loop-tæller røres ikke. Ventende traktorer ser rotationen som slået fra, så AutoDrive ikke kører videre fra Wait-punktet. diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index 5a22bbe..7db2a54 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -109,6 +109,10 @@ FS25_AutoDrive = { end, getWayPointById = function(_, id) return wayPoints[id] end, getMapMarkers = function() return markers end, + -- ruter: alt nåeligt medmindre testen sætter unreachable[id] (til eller fra) + unreachable = {}, + getPathTo = function(self, vehicle, id) if self.unreachable[id] then return {} end; return {{id = id}} end, + pathFromTo = function(self, fromId, toId) if self.unreachable[fromId] or self.unreachable[toId] then return {} end; return {{id = fromId}, {id = toId}} end, }, } @@ -429,7 +433,12 @@ check("P4 stald: halm hentes når TMR er fuld", manager:getNextPickup(cowDriver, -- ==== Wait-punkter: rest-læs + fuld stald -> vent ved nærmeste ledige Wait, tilbage når resten passer ==== local UnloadTask = FS25_AutoDrive.UnloadAtDestinationTask local waitBarnLevels, waitBarnCapacity = {}, {[STRAW] = 50000, [TMR] = 100000} +local waitVehicles = {} local function buildWaitWorld() + -- tidligere testkøretøjer slipper deres Wait-punkter (AD "stoppes" på dem) + for _, vehicle in ipairs(waitVehicles) do vehicle.ad.stateModule.active = false end + listener:update(16) + waitVehicles = {} buildCowWorld() waitBarnLevels = {[STRAW] = 50000, [TMR] = 10000} -- halm FULD local barn2 = { @@ -462,6 +471,7 @@ local function makeUnloadingVehicle(name, node, x, restStraw) } vehicle.ad.trailerModule = {isActiveAtTrigger = function() return false end} local task = {vehicle = vehicle, destinationID = 30, state = UnloadTask.STATE_DRIVING, trailers = vehicle.trailers} + table.insert(waitVehicles, vehicle) return vehicle, task end @@ -519,12 +529,15 @@ UnloadTask.update(t7, 16); UnloadTask.update(t7, 16) check("W7 uden Wait-punkter -> uændret mål", t7.destinationID, 30) check("W7 én advarsel", countLogsSince(warnMark, "WARN"), 1) --- W8: B og C venter stadig og holder begge punkter; stoppes de i AD, frigives punkterne +-- W8: to andre holder begge punkter; stoppes de i AD, frigives punkterne buildWaitWorld() +local vA, tA = makeUnloadingVehicle("Fastrac A2", 521, 5000, 35000) +local vB, tB = makeUnloadingVehicle("Fastrac B2", 522, 5000, 35000) +UnloadTask.update(tA, 16); UnloadTask.update(tB, 16) local v8, t8 = makeUnloadingVehicle("Fastrac F", 508, 5000, 35000) UnloadTask.update(t8, 16) check("W8 alle Wait-punkter optaget -> uændret", t8.destinationID, 30) -v2.ad.stateModule.active, v3.ad.stateModule.active = false, false +vA.ad.stateModule.active, vB.ad.stateModule.active = false, false listener:update(16) UnloadTask.update(t8, 16) check("W8 venter ved Wait 1 efter frigivelse", t8.destinationID, 40) @@ -535,6 +548,32 @@ UnloadTask.update(t9, 16) check("W8 Wait 1 frigivet efter finished", t9.destinationID, 40) check("W8 rotation gendannet efter finished", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v8), RT_ONLYPICKUP) +-- W9: intet vejnet til nærmeste Wait -> næste; intet net til nogen -> uændret (AD slukker ellers sig selv) +buildWaitWorld() +FS25_AutoDrive.ADGraphManager.unreachable = {[40] = true} +local v10, t10 = makeUnloadingVehicle("Fastrac H", 510, 5000, 35000) +UnloadTask.update(t10, 16) +check("W9 unåeligt Wait 1 -> Wait 2", t10.destinationID, 41) +FS25_AutoDrive.ADGraphManager.unreachable = {[40] = true, [41] = true} +local v11, t11 = makeUnloadingVehicle("Fastrac I", 511, 5000, 35000) +local unreachableMark = #logLines +UnloadTask.update(t11, 16) +check("W9 intet nåeligt -> uændret mål", t11.destinationID, 30) +check("W9 advarsel om manglende vejnet", countLogsSince(unreachableMark, "WARN"), 1) +FS25_AutoDrive.ADGraphManager.unreachable = {} + +-- W10: kæmpe vogn (700.000 l rest) i 50.000 l-stald -> hjem når halvdelen af stalden er fri +buildWaitWorld() +local v12, t12 = makeUnloadingVehicle("Fastrac J", 512, 5000, 700000) +UnloadTask.update(t12, 16) +check("W10 venter", t12.destinationID, 40) +waitBarnLevels[STRAW] = 30000 -- 20000 fri < 25000 +UnloadTask.update(t12, 16) +check("W10 under halv stald fri -> venter stadig", t12.destinationID, 40) +waitBarnLevels[STRAW] = 20000 -- 30000 fri >= 25000 +UnloadTask.update(t12, 16) +check("W10 halv stald fri -> hjem", t12.destinationID, 30) + -- L1: moden valgte siloen -> AutoDrive skal stå og læsse (rotation skjules under opgaven) resetWorld() local loadLevels = {[SUGARBEET] = 5000} diff --git a/tests/test_adSupplyPlanner.lua b/tests/test_adSupplyPlanner.lua index 4a191df..037be1b 100644 --- a/tests/test_adSupplyPlanner.lua +++ b/tests/test_adSupplyPlanner.lua @@ -159,6 +159,18 @@ check("reserveret springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitP check("egen reservation tæller som ledig", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1).marker.id, 1) check("alle optaget -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2, [2] = V2, [3] = V2}, 10, 0, V1), nil) check("ingen punkter -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint({}, {}, 0, 0, V1), nil) +-- nåelighed: et punkt AD ikke kan finde vej til/fra springes over (ellers slukker AD sig selv) +local unreachableA = function(candidate) return candidate.marker.id ~= 1 end +check("unåeligt nærmeste springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, unreachableA).marker.id, 2) +check("intet nåeligt -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, function() return false end), nil) + +-- Hjem fra Wait: fri plads >= min(rest, halvdelen af målets kapacitet) +-- (en 700.000 l-vogn kan aldrig tømme HELE resten i en 300.000 l-stald) +check("hele resten passer -> hjem", ADSupplyPlanner.canUnloadRest(35000, 35000, 300000), true) +check("resten passer ikke endnu -> vent", ADSupplyPlanner.canUnloadRest(5000, 20000, 50000), false) +check("kæmpe rest: halv stald fri -> hjem", ADSupplyPlanner.canUnloadRest(150000, 700000, 300000), true) +check("kæmpe rest: under halv stald fri -> vent", ADSupplyPlanner.canUnloadRest(100000, 700000, 300000), false) +check("ukendt kapacitet -> kun hele resten tæller", ADSupplyPlanner.canUnloadRest(100000, 700000, nil), false) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)