From 3f4b7969aaa83becb3f2bbec2cf5d9b8b90e7b2e Mon Sep 17 00:00:00 2001 From: Helge Neumann Date: Sat, 4 Jul 2026 13:24:22 +0200 Subject: [PATCH 1/4] Add reverseOnStuck: recover instead of waiting forever when blocked When the collision detection stops the vehicle in front of a static obstacle (tree, lamp post, parked vehicle), it previously waited forever, because the existing stuck detection is suspended while the vehicle is intentionally stopped. With the new global setting 'reverseOnStuck' (default 10s, can be disabled), the driver now: - backs up ~12m, - drives around the obstacle with an increasing lateral offset, choosing the side by the front corner sensors and alternating it on repeated attempts, - rejoins the current route at the first waypoint more than 20m away (no mode restart, which would just replan the same network route), - gives up with the existing 'got stuck' error after 3 failed attempts in the same area. Purely AD-internal waiting (right of way) uses 3x the timeout so normal junction waits are unaffected, and a small per-vehicle jitter prevents two vehicles blocking each other from moving simultaneously. When stuck within 20m of the end of the route in 'Drive to' mode or with a CP/AIVE/AI handover configured, the target is declared reached instead, so the helper still starts when another vehicle is already parked on the target point. Includes setting UI, tooltips and labels for all 18 languages. Co-Authored-By: Claude Fable 5 --- gui/globalSettingsPage.xml | 6 + scripts/Modules/CollisionDetectionModule.lua | 5 + scripts/Modules/DrivePathModule.lua | 195 ++++++++++++++++++- scripts/Settings.lua | 11 ++ scripts/Tasks/ReverseFromBadLocationTask.lua | 10 +- translations/translation_br.xml | 2 + translations/translation_cs.xml | 2 + translations/translation_ct.xml | 2 + translations/translation_cz.xml | 2 + translations/translation_da.xml | 2 + translations/translation_de.xml | 2 + translations/translation_ea.xml | 2 + translations/translation_en.xml | 2 + translations/translation_es.xml | 2 + translations/translation_fr.xml | 2 + translations/translation_hu.xml | 2 + translations/translation_it.xml | 16 +- translations/translation_nl.xml | 2 + translations/translation_pl.xml | 2 + translations/translation_pt.xml | 2 + translations/translation_ru.xml | 2 + translations/translation_tr.xml | 2 + translations/translation_uk.xml | 2 + 23 files changed, 266 insertions(+), 11 deletions(-) diff --git a/gui/globalSettingsPage.xml b/gui/globalSettingsPage.xml index 0f8a6ed..ec9fadb 100644 --- a/gui/globalSettingsPage.xml +++ b/gui/globalSettingsPage.xml @@ -51,6 +51,12 @@ + + + + + + diff --git a/scripts/Modules/CollisionDetectionModule.lua b/scripts/Modules/CollisionDetectionModule.lua index d48bee4..54d9e6e 100644 --- a/scripts/Modules/CollisionDetectionModule.lua +++ b/scripts/Modules/CollisionDetectionModule.lua @@ -6,6 +6,7 @@ function ADCollisionDetectionModule:new(vehicle) self.__index = self o.vehicle = vehicle o.detectedObstable = false + o.detectedPhysicalObstacle = false o.reverseSectionClear = AutoDriveTON:new() o.reverseSectionClear.elapsedTime = 20000 o.detectedCollision = false @@ -32,6 +33,10 @@ function ADCollisionDetectionModule:hasDetectedObstable(dt) ) end + -- physically sensed obstacle (sensor box / vehicle collision), as opposed to + -- AD-internal right-of-way waiting - only the former justifies a stuck-recovery maneuver + self.detectedPhysicalObstacle = detectObstacle + self.detectedObstable = detectObstacle or detectAdTrafficOnRoute or not self.reverseSectionClear:done() return self.detectedObstable end diff --git a/scripts/Modules/DrivePathModule.lua b/scripts/Modules/DrivePathModule.lua index d7822b3..aacd6f6 100644 --- a/scripts/Modules/DrivePathModule.lua +++ b/scripts/Modules/DrivePathModule.lua @@ -7,6 +7,16 @@ ADDrivePathModule.MAX_STEERING_ANGLE = 30 ADDrivePathModule.PAUSE_TIMEOUT = 3000 ADDrivePathModule.BLINK_TIMEOUT = 1000 +-- obstacle avoidance (panic mode) parameters +ADDrivePathModule.AVOIDANCE_REVERSE = 1 +ADDrivePathModule.AVOIDANCE_FORWARD = 2 +ADDrivePathModule.AVOIDANCE_REVERSE_DISTANCE = 12 +ADDrivePathModule.AVOIDANCE_SKIP_DISTANCE = 20 +ADDrivePathModule.AVOIDANCE_FORWARD_SPEED = 8 +ADDrivePathModule.AVOIDANCE_REVERSE_SPEED = 6 +-- if stuck closer to the end of the route than this, declare the target reached instead of maneuvering +ADDrivePathModule.STUCK_HANDOVER_DISTANCE = 20 + function ADDrivePathModule:new(vehicle) local o = {} setmetatable(o, self) @@ -14,6 +24,9 @@ function ADDrivePathModule:new(vehicle) o.vehicle = vehicle o.min_distance = AutoDrive.defineMinDistanceByVehicleType(vehicle) o.minDistanceTimer = AutoDriveTON:new() + o.blockedStuckTimer = AutoDriveTON:new() + o.stuckRecoveryCounter = 0 + o.lastStuckPosition = nil o.waitTimer = AutoDriveTON:new() o.blinkTimer = AutoDriveTON:new() o.brakeHysteresisActive = false @@ -36,6 +49,8 @@ function ADDrivePathModule:reset() self.onRoadNetwork = true self.minDistanceToNextWp = math.huge self.minDistanceTimer:timer(false, 5000, 0) + self.blockedStuckTimer:timer(false, 10000, 0) + self.obstacleAvoidanceState = nil self.waitTimer:timer(false, ADDrivePathModule.PAUSE_TIMEOUT, 0) self.blinkTimer:timer(false, ADDrivePathModule.BLINK_TIMEOUT, 0) self.vehicle.ad.stateModule:setCurrentWayPointId(-1) @@ -168,6 +183,15 @@ function ADDrivePathModule:update(dt) return end + if self.obstacleAvoidanceState ~= nil then + if self.wayPoints ~= nil and self:getCurrentWayPointIndex() <= #self.wayPoints then + self:updateObstacleAvoidance(dt) + return + else + self.obstacleAvoidanceState = nil + end + end + if self.wayPoints ~= nil and self:getCurrentWayPointIndex() <= #self.wayPoints then if self.isReversing then self.vehicle.ad.specialDrivingModule:handleReverseDriving(dt) @@ -788,9 +812,40 @@ function ADDrivePathModule:checkIfStuck(dt) if self.minDistanceTimer:done() then self:handleBeingStuck() end + self.blockedStuckTimer:timer(false) else self.minDistanceTimer:timer(false) + self:checkIfBlockedByObstacle(dt) + end + end +end + +--- The collision detection stops the vehicle in front of an obstacle, but a static obstacle +--- (tree, lamp post, parked vehicle) never clears - so the vehicle would wait forever. +--- Run a separate timer while being stopped by a physically detected obstacle and treat it +--- as being stuck once the configured time has passed. +function ADDrivePathModule:checkIfBlockedByObstacle(dt) + local reverseTime = AutoDrive.getSetting("reverseOnStuck") or 0 + if reverseTime > 0 then + local standingWhileStopped = self.vehicle.ad.specialDrivingModule:isStoppingVehicle() + and math.abs(self.vehicle.lastSpeedReal) <= 0.0005 + -- small per-vehicle offset so two AD vehicles blocking each other do not start + -- their avoidance maneuver at the same moment + local jitter = (NetworkUtil.getObjectId(self.vehicle) or 0) % 5 + local timeout = (reverseTime + jitter) * 1000 + if self.vehicle.ad.collisionDetectionModule.detectedPhysicalObstacle ~= true then + -- stopped by AD-internal logic only (right of way / reverse section wait): + -- normally this clears on its own, so allow much more time before treating + -- it as a mutual deadlock + timeout = timeout * 3 end + self.blockedStuckTimer:timer(standingWhileStopped, timeout, dt) + if self.blockedStuckTimer:done() then + self.blockedStuckTimer:timer(false) + self:handleBeingStuck() + end + else + self.blockedStuckTimer:timer(false) end end @@ -799,11 +854,147 @@ function ADDrivePathModule:handleBeingStuck() AutoDrive.debugPrint(self.vehicle, AutoDrive.DC_VEHICLEINFO, "handleBeingStuck") end if self.vehicle.isServer then - AutoDriveMessageEvent.sendMessageOrNotification(self.vehicle, ADMessagesManager.messageTypes.ERROR, "$l10n_AD_Driver_of; %s $l10n_AD_got_stuck;", 5000, self.vehicle.ad.stateModule:getName()) - self.vehicle.ad.taskModule:stopAndRestartAD() + local reverseTime = AutoDrive.getSetting("reverseOnStuck") or 0 + + if reverseTime > 0 and self:isStuckCloseToTarget() then + -- stuck within a few meters of the destination - most likely another vehicle is + -- already parked on the target point. Declare the target reached so the route ends + -- normally, including a possible handover to CP/AIVE/AI helper + AutoDriveMessageEvent.sendMessageOrNotification(self.vehicle, ADMessagesManager.messageTypes.WARN, "$l10n_AD_Driver_of; %s $l10n_AD_got_stuck;", 5000, self.vehicle.ad.stateModule:getName()) + self.stuckRecoveryCounter = 0 + self.atTarget = true + return + end + + -- reset the recovery counter once the vehicle got well away from the last stuck location + local x, _, z = getWorldTranslation(self.vehicle.components[1].node) + if self.lastStuckPosition ~= nil and MathUtil.vector2Length(x - self.lastStuckPosition.x, z - self.lastStuckPosition.z) > 50 then + self.stuckRecoveryCounter = 0 + end + self.lastStuckPosition = {x = x, z = z} + self.stuckRecoveryCounter = self.stuckRecoveryCounter + 1 + + if reverseTime > 0 and self.stuckRecoveryCounter <= 3 and self:startObstacleAvoidance() then + -- panic mode: back up, drive around the obstacle with a side offset and + -- continue the current route at a waypoint beyond it (restarting the mode + -- would just replan the same route on the waypoint network) + AutoDriveMessageEvent.sendMessageOrNotification(self.vehicle, ADMessagesManager.messageTypes.WARN, "$l10n_AD_Driver_of; %s $l10n_AD_got_stuck;", 5000, self.vehicle.ad.stateModule:getName()) + elseif reverseTime > 0 and self.stuckRecoveryCounter > 3 then + -- several recovery attempts in the same spot failed - give up for good instead of looping + self.stuckRecoveryCounter = 0 + AutoDriveMessageEvent.sendMessageOrNotification(self.vehicle, ADMessagesManager.messageTypes.ERROR, "$l10n_AD_Driver_of; %s $l10n_AD_got_stuck;", 5000, self.vehicle.ad.stateModule:getName()) + self.vehicle.ad.isStoppingWithError = true + self.vehicle.ad.taskModule:abortAllTasks() + self.vehicle.ad.taskModule:addTask(StopAndDisableADTask:new(self.vehicle)) + else + AutoDriveMessageEvent.sendMessageOrNotification(self.vehicle, ADMessagesManager.messageTypes.ERROR, "$l10n_AD_Driver_of; %s $l10n_AD_got_stuck;", 5000, self.vehicle.ad.stateModule:getName()) + self.vehicle.ad.taskModule:stopAndRestartAD() + end end end +--- Stuck close to the end of the route: most likely another vehicle is already parked on the +--- target point. In 'drive to' mode, or whenever a helper (CP/AIVE/AI) is supposed to take over +--- at the destination, it is better to declare the target reached than to maneuver around or +--- give up right next to it. +function ADDrivePathModule:isStuckCloseToTarget() + local distance = self:getDistanceToLastWaypoint(40) + if distance == nil or distance > ADDrivePathModule.STUCK_HANDOVER_DISTANCE then + return false + end + local usesHelper = self.vehicle.ad.stateModule.usedHelper ~= nil and + self.vehicle.ad.stateModule.usedHelper ~= ADStateModule.HELPER_NONE + return self.vehicle.ad.stateModule:getMode() == AutoDrive.MODE_DRIVETO or usesHelper +end + +--- Start the obstacle avoidance maneuver: back up a bit, then drive around the obstacle +--- with a lateral offset and rejoin the current route at a waypoint beyond it. +--- Returns false when there is no waypoint far enough ahead to rejoin at. +function ADDrivePathModule:startObstacleAvoidance() + if self.wayPoints == nil then + return false + end + local x, y, z = getWorldTranslation(self.vehicle.components[1].node) + local skipIx = nil + for i = math.max(self:getCurrentWayPointIndex(), 1), #self.wayPoints do + local wp = self.wayPoints[i] + if MathUtil.vector2Length(wp.x - x, wp.z - z) > ADDrivePathModule.AVOIDANCE_SKIP_DISTANCE then + skipIx = i + break + end + end + if skipIx == nil then + return false + end + + -- pass the obstacle on the side where the front corner sensors report more space, + -- alternate the side and go wider with every further attempt in the same spot + local leftBlocked = self.vehicle.ad.sensors.leftFrontSensor:pollInfo() + local rightBlocked = self.vehicle.ad.sensors.rightFrontSensor:pollInfo() + local side = 1 + if rightBlocked and not leftBlocked then + side = -1 + end + if self.stuckRecoveryCounter % 2 == 0 then + side = -side + end + local lateralOffset = 4 + 1.5 * (self.stuckRecoveryCounter - 1) + + local rx, ry, rz = AutoDrive.localToWorld(self.vehicle, 0, 0, -100) + self.avoidanceReverseTarget = {x = rx, y = ry, z = rz} + local fx, fy, fz = AutoDrive.localToWorld(self.vehicle, side * lateralOffset, 0, ADDrivePathModule.AVOIDANCE_SKIP_DISTANCE + 2) + self.avoidanceForwardTarget = {x = fx, y = fy, z = fz} + self.avoidanceStartPosition = {x = x, y = y, z = z} + self.avoidanceSkipIx = skipIx + self.avoidanceTimer = 0 + self.obstacleAvoidanceState = ADDrivePathModule.AVOIDANCE_REVERSE + if AutoDrive.getDebugChannelIsSet(AutoDrive.DC_PATHINFO) then + AutoDrive.debugPrint(self.vehicle, AutoDrive.DC_PATHINFO, "startObstacleAvoidance side %d offset %.1f skipIx %d", side, lateralOffset, skipIx) + end + return true +end + +function ADDrivePathModule:updateObstacleAvoidance(dt) + self.avoidanceTimer = self.avoidanceTimer + dt + local x, _, z = getWorldTranslation(self.vehicle.components[1].node) + if self.obstacleAvoidanceState == ADDrivePathModule.AVOIDANCE_REVERSE then + local distanceReversed = MathUtil.vector2Length(x - self.avoidanceStartPosition.x, z - self.avoidanceStartPosition.z) + if distanceReversed >= ADDrivePathModule.AVOIDANCE_REVERSE_DISTANCE or self.avoidanceTimer > 15000 then + self.vehicle.ad.specialDrivingModule:releaseVehicle() + self.obstacleAvoidanceState = ADDrivePathModule.AVOIDANCE_FORWARD + self.avoidanceTimer = 0 + else + self.vehicle.ad.specialDrivingModule:reverseToTargetLocation(dt, self.avoidanceReverseTarget, ADDrivePathModule.AVOIDANCE_REVERSE_SPEED) + end + elseif self.obstacleAvoidanceState == ADDrivePathModule.AVOIDANCE_FORWARD then + local distanceToTarget = MathUtil.vector2Length(x - self.avoidanceForwardTarget.x, z - self.avoidanceForwardTarget.z) + if distanceToTarget < 3 or self.avoidanceTimer > 20000 then + self:finishObstacleAvoidance() + else + -- deliberately no collision stop here: we are skirting an obstacle the sensors + -- would stop for anyway, at limited speed + local lx, lz = AutoDrive.getDriveDirection(self.vehicle, self.avoidanceForwardTarget.x, self.avoidanceForwardTarget.y, self.avoidanceForwardTarget.z) + self.vehicle.ad.trailerModule:handleTrailerReversing(false) + AutoDrive.driveInDirection(self.vehicle, dt, 30, 0.75, 0.2, 20, true, true, lx, lz, ADDrivePathModule.AVOIDANCE_FORWARD_SPEED, 0.3) + end + else + self:finishObstacleAvoidance() + end +end + +--- Rejoin the route at the waypoint beyond the obstacle and hand control back to the +--- normal waypoint following +function ADDrivePathModule:finishObstacleAvoidance() + self.vehicle.ad.specialDrivingModule:releaseVehicle() + if self.wayPoints ~= nil and self.avoidanceSkipIx ~= nil and self.avoidanceSkipIx <= #self.wayPoints then + self:setCurrentWayPointIndex(self.avoidanceSkipIx) + end + self.minDistanceToNextWp = math.huge + self.minDistanceTimer:timer(false) + self.blockedStuckTimer:timer(false) + self.obstacleAvoidanceState = nil +end + function ADDrivePathModule:checkForReverseSection() -- returns [current segment is reversed], [current segment is the last forward segment], [current segment is the last reverse segment] if AutoDrive.getDebugChannelIsSet(AutoDrive.DC_PATHINFO) then diff --git a/scripts/Settings.lua b/scripts/Settings.lua index 00fa638..c2a935d 100644 --- a/scripts/Settings.lua +++ b/scripts/Settings.lua @@ -606,6 +606,17 @@ AutoDrive.settings.enableTrafficDetection = { isVehicleSpecific = false } +AutoDrive.settings.reverseOnStuck = { + values = {0, 5, 10, 15, 20, 30}, + texts = {"-", "5s", "10s", "15s", "20s", "30s"}, + default = 3, + current = 3, + text = "gui_ad_reverseOnStuck", + tooltip = "gui_ad_reverseOnStuck_tooltip", + translate = false, + isVehicleSpecific = false +} + AutoDrive.settings.shovelWidth = { values = {0, 0.2, 0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0}, texts = {"0m", "0.2m", "0.4m", "0.6m", "0.8m", "1.0m", "1.2m", "1.4m", "1.6m", "1.8m", "2.0m", "2.2m", "2.4m", "2.6m", "2.8m", "3.0m", "3.2m", "3.4m", "3.6m", "3.8m", "4.0m"}, diff --git a/scripts/Tasks/ReverseFromBadLocationTask.lua b/scripts/Tasks/ReverseFromBadLocationTask.lua index 11a24f8..d98c832 100644 --- a/scripts/Tasks/ReverseFromBadLocationTask.lua +++ b/scripts/Tasks/ReverseFromBadLocationTask.lua @@ -2,9 +2,10 @@ ReverseFromBadLocationTask = ADInheritsFrom(AbstractTask) ReverseFromBadLocationTask.STATE_REVERSING = 1 -function ReverseFromBadLocationTask:new(vehicle) +function ReverseFromBadLocationTask:new(vehicle, dontPropagate) local o = ReverseFromBadLocationTask:create() o.vehicle = vehicle + o.dontPropagate = dontPropagate o.trailers = nil return o end @@ -72,7 +73,12 @@ function ReverseFromBadLocationTask:abort() end function ReverseFromBadLocationTask:finished() - self.vehicle.ad.taskModule:setCurrentTaskFinished() + if self.dontPropagate then + -- used by the generic stuck recovery: the mode must not treat this as a finished mode task + self.vehicle.ad.taskModule:setCurrentTaskFinished(ADTaskModule.DONT_PROPAGATE) + else + self.vehicle.ad.taskModule:setCurrentTaskFinished() + end end function ReverseFromBadLocationTask:getInfoText() diff --git a/translations/translation_br.xml b/translations/translation_br.xml index c2774f4..d01f5f8 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index 29ee2b0..0ff1287 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index 4a246c1..7057bf2 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index d31b92b..40cc04b 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index eff93b5..c62c875 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index 5b35468..0656867 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index 7e51b8c..f3c49ba 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index 0298f12..2f421cb 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index 5c51685..826d4f6 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index c31efc5..7639fae 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index 15bb18c..97ef442 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 2a3845f..5975f2a 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -165,12 +165,14 @@ + + - - + + @@ -346,10 +348,10 @@ - - - - + + + + @@ -397,4 +399,4 @@ - + \ No newline at end of file diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index 61e9845..0dbcc51 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 958f9fe..ebfcb6a 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index bea5c95..7449abd 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index 9a59ce7..b6cca78 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index a3de9c3..bddab76 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -165,6 +165,8 @@ + + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index 2c466c6..ba497bd 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -165,6 +165,8 @@ + + From 677c58678caa434b98f53623d58609e6d932314a Mon Sep 17 00:00:00 2001 From: Helge Neumann Date: Sat, 4 Jul 2026 13:41:01 +0200 Subject: [PATCH 2/4] Make near-target handover distance configurable (default 30m) 20m was too tight for some target layouts. New global setting 'stuckHandoverDistance' (10-50m, default 30m), used by isStuckCloseToTarget() instead of the fixed constant. Co-Authored-By: Claude Fable 5 --- gui/globalSettingsPage.xml | 6 ++++++ scripts/Modules/DrivePathModule.lua | 10 ++++++---- scripts/Settings.lua | 11 +++++++++++ translations/translation_br.xml | 2 ++ translations/translation_cs.xml | 2 ++ translations/translation_ct.xml | 2 ++ translations/translation_cz.xml | 2 ++ translations/translation_da.xml | 2 ++ translations/translation_de.xml | 2 ++ translations/translation_ea.xml | 2 ++ translations/translation_en.xml | 2 ++ translations/translation_es.xml | 2 ++ translations/translation_fr.xml | 2 ++ translations/translation_hu.xml | 2 ++ translations/translation_it.xml | 2 ++ translations/translation_nl.xml | 2 ++ translations/translation_pl.xml | 2 ++ translations/translation_pt.xml | 2 ++ translations/translation_ru.xml | 2 ++ translations/translation_tr.xml | 2 ++ translations/translation_uk.xml | 2 ++ 21 files changed, 59 insertions(+), 4 deletions(-) diff --git a/gui/globalSettingsPage.xml b/gui/globalSettingsPage.xml index ec9fadb..0eeff6a 100644 --- a/gui/globalSettingsPage.xml +++ b/gui/globalSettingsPage.xml @@ -57,6 +57,12 @@ + + + + + + diff --git a/scripts/Modules/DrivePathModule.lua b/scripts/Modules/DrivePathModule.lua index aacd6f6..e0ea095 100644 --- a/scripts/Modules/DrivePathModule.lua +++ b/scripts/Modules/DrivePathModule.lua @@ -14,8 +14,9 @@ ADDrivePathModule.AVOIDANCE_REVERSE_DISTANCE = 12 ADDrivePathModule.AVOIDANCE_SKIP_DISTANCE = 20 ADDrivePathModule.AVOIDANCE_FORWARD_SPEED = 8 ADDrivePathModule.AVOIDANCE_REVERSE_SPEED = 6 --- if stuck closer to the end of the route than this, declare the target reached instead of maneuvering -ADDrivePathModule.STUCK_HANDOVER_DISTANCE = 20 +-- if stuck closer to the end of the route than this, declare the target reached instead of +-- maneuvering (fallback, configurable via the stuckHandoverDistance setting) +ADDrivePathModule.STUCK_HANDOVER_DISTANCE = 30 function ADDrivePathModule:new(vehicle) local o = {} @@ -898,8 +899,9 @@ end --- at the destination, it is better to declare the target reached than to maneuver around or --- give up right next to it. function ADDrivePathModule:isStuckCloseToTarget() - local distance = self:getDistanceToLastWaypoint(40) - if distance == nil or distance > ADDrivePathModule.STUCK_HANDOVER_DISTANCE then + local maxDistance = AutoDrive.getSetting("stuckHandoverDistance") or ADDrivePathModule.STUCK_HANDOVER_DISTANCE + local distance = self:getDistanceToLastWaypoint(60) + if distance == nil or distance > maxDistance then return false end local usesHelper = self.vehicle.ad.stateModule.usedHelper ~= nil and diff --git a/scripts/Settings.lua b/scripts/Settings.lua index c2a935d..e027af7 100644 --- a/scripts/Settings.lua +++ b/scripts/Settings.lua @@ -617,6 +617,17 @@ AutoDrive.settings.reverseOnStuck = { isVehicleSpecific = false } +AutoDrive.settings.stuckHandoverDistance = { + values = {10, 15, 20, 25, 30, 40, 50}, + texts = {"10m", "15m", "20m", "25m", "30m", "40m", "50m"}, + default = 5, + current = 5, + text = "gui_ad_stuckHandoverDistance", + tooltip = "gui_ad_stuckHandoverDistance_tooltip", + translate = false, + isVehicleSpecific = false +} + AutoDrive.settings.shovelWidth = { values = {0, 0.2, 0.4, 0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0}, texts = {"0m", "0.2m", "0.4m", "0.6m", "0.8m", "1.0m", "1.2m", "1.4m", "1.6m", "1.8m", "2.0m", "2.2m", "2.4m", "2.6m", "2.8m", "3.0m", "3.2m", "3.4m", "3.6m", "3.8m", "4.0m"}, diff --git a/translations/translation_br.xml b/translations/translation_br.xml index d01f5f8..f8a0287 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index 0ff1287..eb6b9d4 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index 7057bf2..cb36b20 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index 40cc04b..f071951 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index c62c875..a444d9b 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index 0656867..7333a6d 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index f3c49ba..6351a91 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index 2f421cb..309f144 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index 826d4f6..c72e985 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index 7639fae..c69d4d4 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index 97ef442..8c6663a 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 5975f2a..93c3759 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index 0dbcc51..663c79f 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index ebfcb6a..d8274bc 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index 7449abd..d366d44 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index b6cca78..0df477b 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index bddab76..799e9e5 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -167,6 +167,8 @@ + + diff --git a/translations/translation_uk.xml b/translations/translation_uk.xml index ba497bd..24be029 100644 --- a/translations/translation_uk.xml +++ b/translations/translation_uk.xml @@ -167,6 +167,8 @@ + + From 360fcbc4be4110f76a4301f60241d8be0a69a756 Mon Sep 17 00:00:00 2001 From: helgehelge123 Date: Tue, 14 Jul 2026 18:45:58 +0200 Subject: [PATCH 3/4] Use the real A* pathfinder for obstacle avoidance, fix detour driving and a stuck-handover crash Several issues found and fixed during in-game testing: - Obstacle avoidance previously drove a blind fixed side-offset target (localToWorld) instead of a real route. Replace it with AD's own PathFinderModule (the one ExitFieldTask/DriveToVehicleTask use) to plan an actual path around the obstacle to the rejoin waypoint, including field-boundary/fruit avoidance. New AVOIDANCE_PATHPLANNING state polls the pathfinder per frame (15s timeout, no rejoin on failure so the attempt counts for real). - The detour itself was then driven with naive per-waypoint driveInDirection ("point and go"), which cannot follow the curves a real A* route contains - two vehicles reliably hit the 45s forward timeout without completing a multi-waypoint detour. Fix: AVOIDANCE_FORWARD now temporarily swaps in the pathfinder's waypoints for the duration of each update() call and drives them with AD's real followWaypoints()/isCloseToWaypoint() (proper curves/speed/lookahead), restoring the real route immediately after - atTarget/reachedTarget() are never touched while this runs. - Pre-existing crash (not caused by the above): "attempt to index nil with number" in getNextWayPoint. checkIfStuck -> handleBeingStuck (stuck close to target, helper restartable) -> vehicle:stopAutoDrive() -> reset() nils self.wayPoints -> passToExternalMod_CP rejects the handover (distance > max) -> update() still calls isCloseToWaypoint()/getNextWayPoint() against the now-nil wayPoints. Fix: re-check self.wayPoints ~= nil after checkIfStuck before calling either. - isStuckCloseToTarget() now also considers the beeline distance to the last waypoint (path distance via getDistanceToLastWaypoint(60) returns math.huge with >60 remaining waypoints, which could mask a vehicle that is physically right next to the target). - Added "[AutoDrive stuck-recovery]" Logging.info calls at the key decision points (stuck-close-to-target branch, avoidance attempts, pathfinding start/success/failure, CP handover accept/reject) so behavior is greppable from log.txt without enabling AD debug channels. - MODE_LOAD handover fix: LoadAtDestinationTask only hands off to CP at MODE_PICKUPANDDELIVER, so a stuck-near-target vehicle in MODE_LOAD would just set atTarget and then wait forever at a load trigger that never comes. handleBeingStuck now calls vehicle:stopAutoDrive() directly (routing through passToExternalMod_CP) when the helper is restartable and mode isn't MODE_DRIVETO. passToExternalMod_CP's fixed 30m handover gate is now max(30, stuckHandoverDistance) to match. Verified in-game 2026-07-14 (multiple vehicles): pathfinder finds a route around the obstacle, detour is driven correctly following curves, route rejoins normally, no crash, no timeout. Co-Authored-By: Claude Fable 5 --- scripts/Modules/DrivePathModule.lua | 147 +++++++++++++++++++++------- scripts/Specialization.lua | 17 +++- 2 files changed, 123 insertions(+), 41 deletions(-) diff --git a/scripts/Modules/DrivePathModule.lua b/scripts/Modules/DrivePathModule.lua index e0ea095..f01a94e 100644 --- a/scripts/Modules/DrivePathModule.lua +++ b/scripts/Modules/DrivePathModule.lua @@ -9,11 +9,13 @@ ADDrivePathModule.BLINK_TIMEOUT = 1000 -- obstacle avoidance (panic mode) parameters ADDrivePathModule.AVOIDANCE_REVERSE = 1 -ADDrivePathModule.AVOIDANCE_FORWARD = 2 +ADDrivePathModule.AVOIDANCE_PATHPLANNING = 2 +ADDrivePathModule.AVOIDANCE_FORWARD = 3 ADDrivePathModule.AVOIDANCE_REVERSE_DISTANCE = 12 ADDrivePathModule.AVOIDANCE_SKIP_DISTANCE = 20 -ADDrivePathModule.AVOIDANCE_FORWARD_SPEED = 8 ADDrivePathModule.AVOIDANCE_REVERSE_SPEED = 6 +ADDrivePathModule.AVOIDANCE_PATHPLANNING_TIMEOUT = 15000 +ADDrivePathModule.AVOIDANCE_FORWARD_TIMEOUT = 45000 -- if stuck closer to the end of the route than this, declare the target reached instead of -- maneuvering (fallback, configurable via the stuckHandoverDistance setting) ADDrivePathModule.STUCK_HANDOVER_DISTANCE = 30 @@ -200,7 +202,10 @@ function ADDrivePathModule:update(dt) self:followWaypoints(dt) self:checkIfStuck(dt) - if self:isCloseToWaypoint() then + -- checkIfStuck can hand off control (e.g. stopAutoDrive -> passToExternalMod_*) + -- which resets this module and nils self.wayPoints; isCloseToWaypoint()/ + -- handleReachedWayPoint() would then index a nil wayPoints table + if self.wayPoints ~= nil and self:isCloseToWaypoint() then self:handleReachedWayPoint() end end @@ -859,11 +864,21 @@ function ADDrivePathModule:handleBeingStuck() if reverseTime > 0 and self:isStuckCloseToTarget() then -- stuck within a few meters of the destination - most likely another vehicle is - -- already parked on the target point. Declare the target reached so the route ends - -- normally, including a possible handover to CP/AIVE/AI helper + -- already parked on the target point AutoDriveMessageEvent.sendMessageOrNotification(self.vehicle, ADMessagesManager.messageTypes.WARN, "$l10n_AD_Driver_of; %s $l10n_AD_got_stuck;", 5000, self.vehicle.ad.stateModule:getName()) self.stuckRecoveryCounter = 0 - self.atTarget = true + if self.vehicle.ad.stateModule:getCanRestartHelper() and self.vehicle.ad.stateModule:getMode() ~= AutoDrive.MODE_DRIVETO then + -- in modes like LOAD/UNLOAD the helper (CP/AI) is only started via stopAutoDrive + -- at the end of the task chain - the load/unload tasks would swallow atTarget + -- and wait at a (non-existing) trigger forever. Stop AD directly instead, + -- which passes control to the helper (passToExternalMod_*) + Logging.info("[AutoDrive stuck-recovery] '%s': stuck close to target -> stopping AD to pass control to helper", tostring(self.vehicle.ad.stateModule:getName())) + self.vehicle:stopAutoDrive() + else + -- declare the target reached so the route ends normally + Logging.info("[AutoDrive stuck-recovery] '%s': stuck close to target -> declaring target reached", tostring(self.vehicle.ad.stateModule:getName())) + self.atTarget = true + end return end @@ -879,6 +894,7 @@ function ADDrivePathModule:handleBeingStuck() -- panic mode: back up, drive around the obstacle with a side offset and -- continue the current route at a waypoint beyond it (restarting the mode -- would just replan the same route on the waypoint network) + Logging.info("[AutoDrive stuck-recovery] '%s': starting obstacle avoidance, attempt %d", tostring(self.vehicle.ad.stateModule:getName()), self.stuckRecoveryCounter) AutoDriveMessageEvent.sendMessageOrNotification(self.vehicle, ADMessagesManager.messageTypes.WARN, "$l10n_AD_Driver_of; %s $l10n_AD_got_stuck;", 5000, self.vehicle.ad.stateModule:getName()) elseif reverseTime > 0 and self.stuckRecoveryCounter > 3 then -- several recovery attempts in the same spot failed - give up for good instead of looping @@ -900,17 +916,31 @@ end --- give up right next to it. function ADDrivePathModule:isStuckCloseToTarget() local maxDistance = AutoDrive.getSetting("stuckHandoverDistance") or ADDrivePathModule.STUCK_HANDOVER_DISTANCE + -- path distance along the remaining waypoints (math.huge when more than 60 waypoints remain) local distance = self:getDistanceToLastWaypoint(60) - if distance == nil or distance > maxDistance then - return false + -- beeline to the final waypoint as fallback: dense waypoints or loops in the last route + -- section must not prevent the handover when the vehicle is physically next to the target + if self.wayPoints ~= nil and #self.wayPoints > 0 then + local target = self.wayPoints[#self.wayPoints] + local x, _, z = getWorldTranslation(self.vehicle.components[1].node) + local beeline = MathUtil.vector2Length(target.x - x, target.z - z) + if distance == nil or beeline < distance then + distance = beeline + end end local usesHelper = self.vehicle.ad.stateModule.usedHelper ~= nil and self.vehicle.ad.stateModule.usedHelper ~= ADStateModule.HELPER_NONE - return self.vehicle.ad.stateModule:getMode() == AutoDrive.MODE_DRIVETO or usesHelper + local modeOk = self.vehicle.ad.stateModule:getMode() == AutoDrive.MODE_DRIVETO or usesHelper + local result = distance ~= nil and distance <= maxDistance and modeOk + Logging.info("[AutoDrive stuck-recovery] '%s': distanceToTarget=%s max=%d mode=%s usedHelper=%s -> handover=%s", + tostring(self.vehicle.ad.stateModule:getName()), tostring(distance), maxDistance, + tostring(self.vehicle.ad.stateModule:getMode()), tostring(self.vehicle.ad.stateModule.usedHelper), tostring(result)) + return result end ---- Start the obstacle avoidance maneuver: back up a bit, then drive around the obstacle ---- with a lateral offset and rejoin the current route at a waypoint beyond it. +--- Start the obstacle avoidance maneuver: back up a bit, then let AD's own A* pathfinder +--- (the one ExitFieldTask/DriveToVehicleTask etc. use for off-network driving) plan a way +--- around the obstacle to a waypoint beyond it on the current route. --- Returns false when there is no waypoint far enough ahead to rejoin at. function ADDrivePathModule:startObstacleAvoidance() if self.wayPoints == nil then @@ -929,29 +959,26 @@ function ADDrivePathModule:startObstacleAvoidance() return false end - -- pass the obstacle on the side where the front corner sensors report more space, - -- alternate the side and go wider with every further attempt in the same spot - local leftBlocked = self.vehicle.ad.sensors.leftFrontSensor:pollInfo() - local rightBlocked = self.vehicle.ad.sensors.rightFrontSensor:pollInfo() - local side = 1 - if rightBlocked and not leftBlocked then - side = -1 - end - if self.stuckRecoveryCounter % 2 == 0 then - side = -side + local targetNode = self.wayPoints[skipIx] + local afterNode = self.wayPoints[skipIx + 1] + local vecToNextPoint + if afterNode ~= nil then + vecToNextPoint = {x = afterNode.x - targetNode.x, z = afterNode.z - targetNode.z} + else + local beforeNode = self.wayPoints[skipIx - 1] or targetNode + vecToNextPoint = {x = targetNode.x - beforeNode.x, z = targetNode.z - beforeNode.z} end - local lateralOffset = 4 + 1.5 * (self.stuckRecoveryCounter - 1) + self.avoidanceTargetNode = targetNode + self.avoidanceTargetVector = vecToNextPoint local rx, ry, rz = AutoDrive.localToWorld(self.vehicle, 0, 0, -100) self.avoidanceReverseTarget = {x = rx, y = ry, z = rz} - local fx, fy, fz = AutoDrive.localToWorld(self.vehicle, side * lateralOffset, 0, ADDrivePathModule.AVOIDANCE_SKIP_DISTANCE + 2) - self.avoidanceForwardTarget = {x = fx, y = fy, z = fz} self.avoidanceStartPosition = {x = x, y = y, z = z} self.avoidanceSkipIx = skipIx self.avoidanceTimer = 0 self.obstacleAvoidanceState = ADDrivePathModule.AVOIDANCE_REVERSE if AutoDrive.getDebugChannelIsSet(AutoDrive.DC_PATHINFO) then - AutoDrive.debugPrint(self.vehicle, AutoDrive.DC_PATHINFO, "startObstacleAvoidance side %d offset %.1f skipIx %d", side, lateralOffset, skipIx) + AutoDrive.debugPrint(self.vehicle, AutoDrive.DC_PATHINFO, "startObstacleAvoidance skipIx %d", skipIx) end return true end @@ -963,38 +990,82 @@ function ADDrivePathModule:updateObstacleAvoidance(dt) local distanceReversed = MathUtil.vector2Length(x - self.avoidanceStartPosition.x, z - self.avoidanceStartPosition.z) if distanceReversed >= ADDrivePathModule.AVOIDANCE_REVERSE_DISTANCE or self.avoidanceTimer > 15000 then self.vehicle.ad.specialDrivingModule:releaseVehicle() - self.obstacleAvoidanceState = ADDrivePathModule.AVOIDANCE_FORWARD + self.vehicle.ad.pathFinderModule:reset() + self.vehicle.ad.pathFinderModule:startPathPlanningTo(self.avoidanceTargetNode, self.avoidanceTargetVector) + Logging.info("[AutoDrive stuck-recovery] '%s': reversed clear of obstacle, starting pathfinder around it", tostring(self.vehicle.ad.stateModule:getName())) + self.obstacleAvoidanceState = ADDrivePathModule.AVOIDANCE_PATHPLANNING self.avoidanceTimer = 0 else self.vehicle.ad.specialDrivingModule:reverseToTargetLocation(dt, self.avoidanceReverseTarget, ADDrivePathModule.AVOIDANCE_REVERSE_SPEED) end + elseif self.obstacleAvoidanceState == ADDrivePathModule.AVOIDANCE_PATHPLANNING then + if self.vehicle.ad.pathFinderModule:hasFinished() then + local path = self.vehicle.ad.pathFinderModule:getPath() + if path == nil or #path == 0 then + Logging.info("[AutoDrive stuck-recovery] '%s': pathfinder found no way around the obstacle, aborting this attempt", tostring(self.vehicle.ad.stateModule:getName())) + self:finishObstacleAvoidance(false) + else + Logging.info("[AutoDrive stuck-recovery] '%s': pathfinder found a way around the obstacle, %d waypoints", tostring(self.vehicle.ad.stateModule:getName()), #path) + self.avoidancePath = path + self.avoidancePathIndex = 1 + self.obstacleAvoidanceState = ADDrivePathModule.AVOIDANCE_FORWARD + self.avoidanceTimer = 0 + end + elseif self.avoidanceTimer > ADDrivePathModule.AVOIDANCE_PATHPLANNING_TIMEOUT then + Logging.info("[AutoDrive stuck-recovery] '%s': pathfinder timed out, aborting this attempt", tostring(self.vehicle.ad.stateModule:getName())) + self.vehicle.ad.pathFinderModule:abort() + self:finishObstacleAvoidance(false) + else + self.vehicle.ad.pathFinderModule:update(dt) + self.vehicle.ad.specialDrivingModule:stopVehicle() + self.vehicle.ad.specialDrivingModule:update(dt) + end elseif self.obstacleAvoidanceState == ADDrivePathModule.AVOIDANCE_FORWARD then - local distanceToTarget = MathUtil.vector2Length(x - self.avoidanceForwardTarget.x, z - self.avoidanceForwardTarget.z) - if distanceToTarget < 3 or self.avoidanceTimer > 20000 then - self:finishObstacleAvoidance() + if self.avoidancePathIndex > #self.avoidancePath then + -- drove through every pathfinder waypoint - rejoin the original route + Logging.info("[AutoDrive stuck-recovery] '%s': detour around obstacle completed, rejoining route", tostring(self.vehicle.ad.stateModule:getName())) + self:finishObstacleAvoidance(true) + elseif self.avoidanceTimer > ADDrivePathModule.AVOIDANCE_FORWARD_TIMEOUT then + -- taking too long (e.g. stuck again on the detour) - fall back to the original + -- route; checkIfStuck will trigger a fresh avoidance attempt if still blocked + Logging.info("[AutoDrive stuck-recovery] '%s': detour took too long, giving up and rejoining route anyway", tostring(self.vehicle.ad.stateModule:getName())) + self:finishObstacleAvoidance(true) else - -- deliberately no collision stop here: we are skirting an obstacle the sensors - -- would stop for anyway, at limited speed - local lx, lz = AutoDrive.getDriveDirection(self.vehicle, self.avoidanceForwardTarget.x, self.avoidanceForwardTarget.y, self.avoidanceForwardTarget.z) + -- Drive the pathfinder-planned detour with AD's own waypoint-follower + -- (handles curvature/speed/lookahead properly) instead of blindly aiming at + -- each point - a naive point-chase could not actually traverse the turns the + -- plan needed and always ran into the timeout above. self.wayPoints/currentWayPoint + -- are only swapped for the duration of this call and restored right after, so + -- the real route (and atTarget/reachedTarget()) are never touched by this. + local savedWayPoints, savedIndex = self.wayPoints, self.currentWayPoint + self.wayPoints = self.avoidancePath + self.currentWayPoint = self.avoidancePathIndex self.vehicle.ad.trailerModule:handleTrailerReversing(false) - AutoDrive.driveInDirection(self.vehicle, dt, 30, 0.75, 0.2, 20, true, true, lx, lz, ADDrivePathModule.AVOIDANCE_FORWARD_SPEED, 0.3) + self:followWaypoints(dt) + if self:isCloseToWaypoint() then + self.avoidancePathIndex = self.avoidancePathIndex + 1 + end + self.wayPoints = savedWayPoints + self.currentWayPoint = savedIndex end else - self:finishObstacleAvoidance() + self:finishObstacleAvoidance(false) end end ---- Rejoin the route at the waypoint beyond the obstacle and hand control back to the ---- normal waypoint following -function ADDrivePathModule:finishObstacleAvoidance() +--- Ends the avoidance maneuver. When reachedSkip is true the pathfinder-planned detour was +--- driven successfully, so rejoin the route at the waypoint beyond the obstacle; otherwise +--- just hand control back at the current position and let checkIfStuck retry or give up. +function ADDrivePathModule:finishObstacleAvoidance(reachedSkip) self.vehicle.ad.specialDrivingModule:releaseVehicle() - if self.wayPoints ~= nil and self.avoidanceSkipIx ~= nil and self.avoidanceSkipIx <= #self.wayPoints then + if reachedSkip and self.wayPoints ~= nil and self.avoidanceSkipIx ~= nil and self.avoidanceSkipIx <= #self.wayPoints then self:setCurrentWayPointIndex(self.avoidanceSkipIx) end self.minDistanceToNextWp = math.huge self.minDistanceTimer:timer(false) self.blockedStuckTimer:timer(false) self.obstacleAvoidanceState = nil + self.avoidancePath = nil end function ADDrivePathModule:checkForReverseSection() diff --git a/scripts/Specialization.lua b/scripts/Specialization.lua index f9cc91f..2c6c1d0 100644 --- a/scripts/Specialization.lua +++ b/scripts/Specialization.lua @@ -1413,7 +1413,16 @@ function AutoDrive.passToExternalMod_CP(vehicle) -- TODO: check if this dirty hack works in future! local isControlled = vehicle:getIsControlled() - if not vehicle.ad.isStoppingWithError and distanceToStart < 30 then + -- allow the handover a bit further away from the marker when the user raised the + -- stuck handover distance - the vehicle may have been stopped short of the target + -- by another vehicle parked on it + local maxDistance = 30 + local handoverDistance = AutoDrive.getSetting("stuckHandoverDistance") + if handoverDistance ~= nil and handoverDistance > maxDistance then + maxDistance = handoverDistance + end + + if not vehicle.ad.isStoppingWithError and distanceToStart < maxDistance then AutoDrive.debugPrint(vehicle, AutoDrive.DC_EXTERNALINTERFACEINFO, "AutoDrive.passToExternalMod_CP") -- CP button enabled if (vehicle.cpStartStopDriver ~= nil and vehicle.ad.stateModule:getUsedHelper() == ADStateModule.HELPER_CP) then @@ -1422,15 +1431,17 @@ function AutoDrive.passToExternalMod_CP(vehicle) if vehicle.ad.restartCP == true then -- restart CP to continue vehicle.ad.restartCP = false - AutoDrive.debugPrint(vehicle, AutoDrive.DC_EXTERNALINTERFACEINFO, "AutoDrive.passToExternalMod_CP pass control to CP with restart") + Logging.info("[AutoDrive stuck-recovery] '%s': passing control to CP (restart), distanceToStart=%.1f", tostring(vehicle.ad.stateModule:getName()), distanceToStart) AutoDrive:RestartCP(vehicle) else -- start CP from beginning - AutoDrive.debugPrint(vehicle, AutoDrive.DC_EXTERNALINTERFACEINFO, "AutoDrive.passToExternalMod_CP pass control to CP with start") + Logging.info("[AutoDrive stuck-recovery] '%s': passing control to CP (start), distanceToStart=%.1f", tostring(vehicle.ad.stateModule:getName()), distanceToStart) AutoDrive:StartCP(vehicle) end vehicle.spec_enterable.isControlled = isControlled end + elseif not vehicle.ad.isStoppingWithError then + Logging.info("[AutoDrive stuck-recovery] '%s': NOT passing control to CP, distanceToStart=%.1f > max=%d", tostring(vehicle.ad.stateModule:getName()), distanceToStart, maxDistance) end end From 25bd0387ef8737c650219d98e26fe47686073b1f Mon Sep 17 00:00:00 2001 From: helgehelge123 Date: Tue, 14 Jul 2026 18:58:51 +0200 Subject: [PATCH 4/4] Fix off-by-one crash when the detour's first waypoint has no predecessor checkForReverseSection()'s bounds guard only checked getCurrentWayPointIndex() < 1, which does not protect index 1: at index 1, wp_ref = wayPoints[index - 1] = wayPoints[0], which is nil in a 1-indexed table, and the subsequent angle calculation crashes with "attempt to index nil with 'x'". This is latent in the base game code (present identically in the unmodified 3.0.1.2 release), but never triggered in normal driving. Our new AVOIDANCE_FORWARD detour-following (previous commit) does trigger it reliably: avoidancePathIndex starts at 1, and the very first isCloseToWaypoint() call after the pathfinder succeeds runs checkForReverseSection() against index 1 of the swapped-in avoidancePath, crashing every frame until the vehicle is manually stopped. Fix: require index < 2 (i.e. index > 1) before indexing wayPoints[idx - 1], matching the guard's own comment ("first or last segment") - index 1 has no previous waypoint and should be treated the same as the existing "last segment" case. Found via live in-game reproduction 2026-07-14 (repeated crash-loop on '1042 Vario' immediately after a successful pathfinder plan). Co-Authored-By: Claude Fable 5 --- scripts/Modules/DrivePathModule.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Modules/DrivePathModule.lua b/scripts/Modules/DrivePathModule.lua index f01a94e..5fdc2c1 100644 --- a/scripts/Modules/DrivePathModule.lua +++ b/scripts/Modules/DrivePathModule.lua @@ -1074,7 +1074,7 @@ function ADDrivePathModule:checkForReverseSection() AutoDrive.debugPrint(self.vehicle, AutoDrive.DC_VEHICLEINFO, "checkForReverseSection start") end - if self.wayPoints == nil or self:getCurrentWayPointIndex() < 1 or #self.wayPoints <= self:getCurrentWayPointIndex() + 1 then + if self.wayPoints == nil or self:getCurrentWayPointIndex() < 2 or #self.wayPoints <= self:getCurrentWayPointIndex() + 1 then if AutoDrive.getDebugChannelIsSet(AutoDrive.DC_PATHINFO) then AutoDrive.debugPrint(self.vehicle, AutoDrive.DC_PATHINFO, "ADDrivePathModule:checkForReverseSection wpIdx=%d - first or last segment" , self:getCurrentWayPointIndex())