From 166a10f6da0732fd4544c68104922b16ae423963 Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Thu, 17 Dec 2020 07:08:56 -0600 Subject: [PATCH] 6.03.00020 Mode2/3 improvements - Wait before starting the next row if the rendezvous point is close to the row end. - use A* to calculate distance to combine before the rendezvous. - combine notifies unloader on missed rendezvous, unloader re-plans route - try to approach combine waiting after backing out of the fruit from the rear so we won't cut in front of it. - don't slow down around rendezvous when discharging already - don't ask for a rendezvous when the combine is not willing to, for example when unload is disabled on the first headland. - won't initiate new rendezvous until the combine cancels the current one - move up rendezvous points close to row end - pipe in fruit map changed to have a 20 m buffer at each end of the row to account for non-perpendicular headlands. - fixed offsets when calculating target to combine, sometimes it used the side offset as front offset. - make sure to reset 95% full limit after making pocket - added safety margin to the calculation of the distance until full All this to avoid the situation where the combine stops before reaching the rendezvous point because it thinks it is full (although only 95%) - only considers unloaders actually waiting for assignment, ignore the ones on unload course for example when a combine is looking for an unloader - if an unloader is already assigned to a combine, only it is waiting for the combine to become ready for unload (for example due to fruit in pipe), assign the same combine to it. This should probably be refactored, adding a new state for the unloader, like WAITING_FOR_COMBINE_TO_BECOME_READY or so. --- CombineAIDriver.lua | 309 +++++++++++++++++++--------- CombineUnloadAIDriver.lua | 231 +++++++++++++++------ CombineUnloadManager.lua | 45 ++-- Events/UnloaderEvents.lua | 2 +- GlobalSettings.lua | 2 +- TrafficCollision.lua | 6 +- Waypoint.lua | 22 +- course-generator/HybridAStar.lua | 9 +- course-generator/PathfinderUtil.lua | 54 ++++- modDesc.xml | 2 +- settings.lua | 23 +++ translations/translation_br.xml | 4 +- translations/translation_cs.xml | 4 +- translations/translation_cz.xml | 8 +- translations/translation_de.xml | 4 +- translations/translation_en.xml | 6 +- translations/translation_es.xml | 4 +- translations/translation_fr.xml | 4 +- translations/translation_hu.xml | 4 +- translations/translation_it.xml | 6 +- translations/translation_jp.xml | 4 +- translations/translation_nl.xml | 4 +- translations/translation_pl.xml | 8 +- translations/translation_pt.xml | 6 +- translations/translation_ru.xml | 8 +- translations/translation_sl.xml | 4 +- 26 files changed, 564 insertions(+), 219 deletions(-) diff --git a/CombineAIDriver.lua b/CombineAIDriver.lua index 15b8a5e22..d6493372b 100644 --- a/CombineAIDriver.lua +++ b/CombineAIDriver.lua @@ -22,6 +22,7 @@ CombineAIDriver = CpObject(UnloadableFieldworkAIDriver) -- fill level when we start making a pocket to unload if we are on the outermost headland CombineAIDriver.pocketFillLevelFullPercentage = 95 +CombineAIDriver.safeUnloadDistanceBeforeEndOfRow = 40 CombineAIDriver.myStates = { PULLING_BACK_FOR_UNLOAD = {}, @@ -30,6 +31,8 @@ CombineAIDriver.myStates = { REVERSING_TO_MAKE_A_POCKET = {}, MAKING_POCKET = {}, WAITING_FOR_UNLOAD_IN_POCKET = {}, + WAITING_FOR_UNLOAD_BEFORE_STARTING_NEXT_ROW = {}, + UNLOADING_BEFORE_STARTING_NEXT_ROW = {}, WAITING_FOR_UNLOAD_AFTER_FIELDWORK_ENDED = {}, WAITING_FOR_UNLOADER_TO_LEAVE = {}, RETURNING_FROM_POCKET = {}, @@ -47,6 +50,12 @@ CombineAIDriver.turnTypes = { UP_DOWN_NORMAL = {} } +-- Developer hack: to check the class of an object one should use the is_a() defined in CpObject.lua. +-- However, when we reload classes on the fly during the development, the is_a() calls in other modules still +-- have the old class definition (for example CombineUnloadManager.lua) of this class and thus, is_a() fails. +-- Therefore, use this instead, this is safe after a reload. +CombineAIDriver.isACombineAIDriver = true + function CombineAIDriver:init(vehicle) courseplay.debugVehicle(11, vehicle, 'CombineAIDriver:init()') UnloadableFieldworkAIDriver.init(self, vehicle) @@ -155,6 +164,7 @@ function CombineAIDriver:init(vehicle) g_combineUnloadManager:addCombineToList(self.vehicle, self) self:measureBackDistance() self.vehicleIgnoredByFrontProximitySensor = CpTemporaryObject() + self.waitingForUnloaderAtEndOfRow = CpTemporaryObject() end --- Get the combine object, this can be different from the vehicle in case of tools towed or mounted on a tractor @@ -237,6 +247,8 @@ function CombineAIDriver:onWaypointPassed(ix) -- harvesting fruit while making the pocket unless we have self unload turned on if self:shouldMakePocket() and self.vehicle.cp.settings.selfUnload:is(false) then self.fillLevelFullPercentage = self.pocketFillLevelFullPercentage + else + self.fillLevelFullPercentage = self.normalFillLevelFullPercentage end self:shouldStrawSwathBeOn(ix) @@ -264,7 +276,7 @@ function CombineAIDriver:onWaypointPassed(ix) end function CombineAIDriver:isWaitingInPocket() - return self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_IN_POCKET + return self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_IN_POCKET end function CombineAIDriver:changeToFieldworkUnloadOrRefill() @@ -317,21 +329,18 @@ function CombineAIDriver:changeToFieldworkUnloadOrRefill() end function CombineAIDriver:driveFieldwork(dt) - if self.fieldworkState == self.states.WORKING then - if self.agreedUnloaderRendezvousWaypointIx then - local d = self.fieldworkCourse:getDistanceBetweenWaypoints(self.fieldworkCourse:getCurrentWaypointIx(), - self.agreedUnloaderRendezvousWaypointIx) - if d < 10 then - self:debugSparse('Slow down around the unloader rendezvous waypoint %d to let the unloader catch up', - self.agreedUnloaderRendezvousWaypointIx) - self:setSpeed(self:getWorkSpeed() / 2) - end - end - end + self:checkRendezvous() self:checkBlockingUnloader() return UnloadableFieldworkAIDriver.driveFieldwork(self, dt) end +function CombineAIDriver:startWaitingForUnloadBeforeNextRow() + self:debug('Waiting for unload before starting the next row') + self.waitingForUnloaderAtEndOfRow:set(true, 30000) + self.fieldworkState = self.states.UNLOAD_OR_REFILL_ON_FIELD + self.fieldworkUnloadOrRefillState = self.states.WAITING_FOR_UNLOAD_BEFORE_STARTING_NEXT_ROW +end + --- Stop for unload/refill while driving the fieldwork course function CombineAIDriver:driveFieldworkUnloadOrRefill() if self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_STOP then @@ -351,10 +360,12 @@ function CombineAIDriver:driveFieldworkUnloadOrRefill() elseif self.fieldworkUnloadOrRefillState == self.states.RETURNING_FROM_PULL_BACK then self:setSpeed(self.vehicle.cp.speeds.turn) elseif self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_IN_POCKET or - self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_AFTER_PULLED_BACK then + self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_AFTER_PULLED_BACK or + self.fieldworkUnloadOrRefillState == self.states.UNLOADING_BEFORE_STARTING_NEXT_ROW then if self:unloadFinished() then -- reset offset to return to the original up/down row after we unloaded in the pocket self.aiDriverOffsetX = 0 + self:clearInfoText(self:getFillLevelInfoText()) -- wait a bit after the unload finished to give a chance to the unloader to move away self.stateBeforeWaitingForUnloaderToLeave = self.fieldworkUnloadOrRefillState @@ -364,6 +375,20 @@ function CombineAIDriver:driveFieldworkUnloadOrRefill() else self:setSpeed(0) end + elseif self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_BEFORE_STARTING_NEXT_ROW then + self:setSpeed(0) + if self:isDischarging() then + self:cancelRendezvous() + self.fieldworkUnloadOrRefillState = self.states.UNLOADING_BEFORE_STARTING_NEXT_ROW + self:debug('Unloading started at end of row') + end + if not self.waitingForUnloaderAtEndOfRow:get() then + local unloaderWhoDidNotShowUp = self.unloadAIDriverToRendezvous + self:cancelRendezvous() + unloaderWhoDidNotShowUp:onMissedRendezvous(self) + self:debug('Waited for unloader at the end of the row but it did not show up, try to continue') + self:changeToFieldwork() + end elseif self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_AFTER_FIELDWORK_ENDED then local fillLevel = self.vehicle:getFillUnitFillLevel(self.combine.fillUnitIndex) if fillLevel < 0.01 then @@ -393,6 +418,12 @@ function CombineAIDriver:driveFieldworkUnloadOrRefill() self:debug('Unloading in pocket finished, returning to fieldwork') self.fillLevelFullPercentage = self.normalFillLevelFullPercentage self:changeToFieldwork() + elseif self.stateBeforeWaitingForUnloaderToLeave == self.states.UNLOADING_BEFORE_STARTING_NEXT_ROW then + self:debug('Unloading before next row finished, returning to fieldwork') + self:changeToFieldwork() + else + self:debug('Unloading finished, previous state not known, returning to fieldwork') + self:changeToFieldwork() end end elseif self.fieldworkUnloadOrRefillState == self.states.DRIVING_TO_SELF_UNLOAD then @@ -553,50 +584,103 @@ function CombineAIDriver:checkDistanceUntilFull(ix) self:debug('Fill rate is %.1f l/m, %.1f l/s', self.litersPerMeter, self.litersPerSecond) end local litersUntilFull = self.combine:getFillUnitCapacity(self.combine.fillUnitIndex) - fillLevel - local dUntilFull = litersUntilFull / self.litersPerMeter + local dUntilFull = litersUntilFull / self.litersPerMeter * 0.9 -- safety margin self.secondsUntilFull = self.litersPerSecond > 0 and (litersUntilFull / self.litersPerSecond) or nil self.waypointIxWhenFull = self.course:getNextWaypointIxWithinDistance(ix, dUntilFull) or self.course:getNumberOfWaypoints() - self.waypointIxWhenFull = self:getSafeUnloaderDestinationWaypoint(self.waypointIxWhenFull) self.distanceToWaypointWhenFull = self.course:getDistanceBetweenWaypoints(self.waypointIxWhenFull, self.course:getCurrentWaypointIx()) self:debug('Will be full at waypoint %d in %d m', self.waypointIxWhenFull or -1, self.distanceToWaypointWhenFull) end +function CombineAIDriver:checkRendezvous() + if self.fieldworkState == self.states.WORKING then + if self.agreedUnloaderRendezvousWaypointIx then + local d = self.fieldworkCourse:getDistanceBetweenWaypoints(self.fieldworkCourse:getCurrentWaypointIx(), + self.agreedUnloaderRendezvousWaypointIx) + if d < 10 then + self:debugSparse('Slow down around the unloader rendezvous waypoint %d to let the unloader catch up', + self.agreedUnloaderRendezvousWaypointIx) + self:setSpeed(self:getWorkSpeed() / 2) + local dToTurn = self.fieldworkCourse:getDistanceToNextTurn(self.agreedUnloaderRendezvousWaypointIx) + if dToTurn < 20 then + self:debug('Unloader rendezvous waypoint %d is before a turn, waiting for the unloader here', + self.agreedUnloaderRendezvousWaypointIx) + self:startWaitingForUnloadBeforeNextRow() + end + elseif self.fieldworkCourse:getCurrentWaypointIx() > self.agreedUnloaderRendezvousWaypointIx then + self:debug('Unloader missed the rendezvous at %d', self.agreedUnloaderRendezvousWaypointIx) + if self.unloadAIDriverToRendezvous then + local unloaderWhoDidNotShowUp = self.unloadAIDriverToRendezvous + -- need to call this before onMissedRendezvous as the unloader will call back to set up a new rendezvous + -- and we don't want to cancel that right away + self:cancelRendezvous() + unloaderWhoDidNotShowUp:onMissedRendezvous(self) + end + end + if self:isDischarging() then + self:debug('Discharging, cancelling unloader rendezvous') + self:cancelRendezvous() + end + end + end +end + +function CombineAIDriver:hasRendezvousWith(unloadAIDriver) + return self.unloadAIDriverToRendezvous == unloadAIDriver +end + +function CombineAIDriver:cancelRendezvous() + self:debug('Rendezvous with %s at waypoint %d cancelled', nameNum(self.unloadAIDriverToRendezvous), + self.agreedUnloaderRendezvousWaypointIx or -1) + self.agreedUnloaderRendezvousWaypointIx = nil + self.unloadAIDriverToRendezvous = nil +end + +--- Before the unloader asks for a rendezvous (which may result in a lengthy pathfinding to figure out +--- the distance), it should check if the combine is willing to rendezvous. +function CombineAIDriver:isWillingToRendezvous() + if self.state ~= self.states.ON_FIELDWORK_COURSE then + self:debug('not on fieldwork course, will not rendezvous') + return nil + elseif self.vehicle.cp.settings.allowUnloadOnFirstHeadland:is(false) and + self.fieldworkCourse:isOnHeadland(self.fieldworkCourse:getCurrentWaypointIx(), 1) then + self:debug('on first headland and unload not allowed on first headland, will not rendezvous') + return nil + end + return true +end + +--- When the unloader asks us for a rendezvous, provide him with a waypoint index to meet us. +--- This waypoint should be a good location to unload (pipe not in fruit, not in a turn, etc.) +--- If no such waypoint found, reject the rendezvous. ---@param unloaderEstimatedSecondsEnroute number minimum time the unloader needs to get to the combine +---@param unloadAIDriver CombineUnloadAIDriver the driver requesting the rendezvous ---@return Waypoint, number, number waypoint to meet the unloader, index of waypoint, time we need to reach that waypoint -function CombineAIDriver:getUnloaderRendezvousWaypoint(unloaderEstimatedSecondsEnroute) +function CombineAIDriver:getUnloaderRendezvousWaypoint(unloaderEstimatedSecondsEnroute, unloadAIDriver) local dToUnloaderRendezvous = unloaderEstimatedSecondsEnroute * self:getWorkSpeed() / 3.6 - local unloaderRendezvousWaypointIx = self.fieldworkCourse:getNextWaypointIxWithinDistance(self.fieldworkCourse:getCurrentWaypointIx(), - dToUnloaderRendezvous) or self.fieldworkCourse:getNumberOfWaypoints() - - self:debug('Seconds until full: %d, unloader ETE: %d', self.secondsUntilFull or -1, unloaderEstimatedSecondsEnroute) - - if not self.secondsUntilFull or (self.secondsUntilFull and self.secondsUntilFull > unloaderEstimatedSecondsEnroute) then - -- unloader will reach us before we are full, or we don't know where we'll be full, guess at which waypoint we will be by then - unloaderRendezvousWaypointIx = self:getSafeUnloaderDestinationWaypoint(unloaderRendezvousWaypointIx) - if self:canUnloadWhileMovingAtWaypoint(unloaderRendezvousWaypointIx) then - self.agreedUnloaderRendezvousWaypointIx = unloaderRendezvousWaypointIx - self:debug('Rendezvous with unloader at waypoint %d in %d m', unloaderRendezvousWaypointIx, dToUnloaderRendezvous) - return self.fieldworkCourse:getWaypoint(unloaderRendezvousWaypointIx), unloaderRendezvousWaypointIx, unloaderEstimatedSecondsEnroute - else - return nil, 0, 0 - end - elseif self.waypointIxWhenFull then - self:debug('We don\'t know when exactly we\'ll be full, but it will be at waypoint %d in %d m, reject rendezvous', - self.waypointIxWhenFull, self.distanceToWaypointWhenFull) - if self:canUnloadWhileMovingAtWaypoint(unloaderRendezvousWaypointIx) then - self.agreedUnloaderRendezvousWaypointIx = self.waypointIxWhenFull - -- TODO: figure out what to do in this case, it does not seem to make sense to send the unloader to - -- a distant waypoint - return nil, 0, 0 - -- return self.fieldworkCourse:getWaypoint(self.waypointIxWhenFull), self.waypointIxWhenFull, self.distanceToWaypointWhenFull / (self:getWorkSpeed() / 3.6) - else - return nil, 0, 0 - end + -- this is where we'll be when the unloader gets here + local unloaderRendezvousWaypointIx = self.fieldworkCourse:getNextWaypointIxWithinDistance( + self.fieldworkCourse:getCurrentWaypointIx(), dToUnloaderRendezvous) or + self.fieldworkCourse:getNumberOfWaypoints() + + self:debug('Rendezvous request: seconds until full: %d, unloader ETE: %d (around my wp %d, in %d meters), full at waypoint %d, ', + self.secondsUntilFull or -1, unloaderEstimatedSecondsEnroute, unloaderRendezvousWaypointIx, dToUnloaderRendezvous, + self.waypointIxWhenFull or -1) + + -- rendezvous at whichever is closer + unloaderRendezvousWaypointIx = math.min(unloaderRendezvousWaypointIx, self.waypointIxWhenFull or unloaderRendezvousWaypointIx) + -- now check if this is a good idea + self.agreedUnloaderRendezvousWaypointIx = self:findBestWaypointToUnload(unloaderRendezvousWaypointIx) + if self.agreedUnloaderRendezvousWaypointIx then + self.unloadAIDriverToRendezvous = unloadAIDriver + self:debug('Rendezvous with unloader at waypoint %d in %d m', self.agreedUnloaderRendezvousWaypointIx, dToUnloaderRendezvous) + return self.fieldworkCourse:getWaypoint(self.agreedUnloaderRendezvousWaypointIx), + self.agreedUnloaderRendezvousWaypointIx, unloaderEstimatedSecondsEnroute else - self:debug('We don\t know when exactly we\'ll be full, reject rendezvous') + self:cancelRendezvous() + self:debug('Rendezvous with unloader rejected') return nil, 0, 0 end end @@ -613,34 +697,6 @@ function CombineAIDriver:canUnloadWhileMovingAtWaypoint(ix) return true end ---- Check if ix is a safe destination for an unloader, return an adjusted ix if not ----@param ix number waypoint index to check ----@return number waypoint index adjusted if needed -function CombineAIDriver:getSafeUnloaderDestinationWaypoint(ix) - local newWpIx = ix - if self.fieldworkCourse:isTurnStartAtIx(ix) then - if self.fieldworkCourse:isOnHeadland(ix) then - -- on the headland, use the wp after the turn, the one before may be very far, especially on a - -- transition from headland to up/down rows. - newWpIx = ix + 1 - else - -- turn start waypoints usually aren't safe as they point to the turn end direction in 180 turns - -- so use the one before - newWpIx = ix - 1 - end - else - - end - -- if we ended up on a turn start WP and the row is long enough, move it a bit forward so the unloader does - -- not drive much off the field to align with it - if self.fieldworkCourse:isTurnStartAtIx(newWpIx) and self.fieldworkCourse:getDistanceToNextTurn(newWpIx) > 20 then - -- TODO: get the guess factor out of this (2 wp distance < 20 m) - newWpIx = newWpIx + 2 - end - - return newWpIx -end - -- TODO: put this in onBlocked()? function CombineAIDriver:checkBlockingUnloader() if not self.backwardLookingProximitySensorPack then return end @@ -671,46 +727,87 @@ function CombineAIDriver:isPipeInFruitAtWaypointNow(course, ix) end --- Find the best waypoint to unload. ----@param waypointIxWhenFull number estimated waypoint index when full based on current fruit flow and distance ----@return number best waypoint to unload. What is a good point to unload: -function CombineAIDriver:findBestWaypointToUnload(waypointIxWhenFull) - if self.course:isOnHeadland(waypointIxWhenFull) then - return self:findBestWaypointToUnloadOnHeadland(waypointIxWhenFull) +---@param ix number waypoint index we want to start unloading, either because that's about where +--- we'll rendezvous the unloader or we'll be full there. +---@return number best waypoint to unload, ix may be adjusted to make sure it isn't in a turn or +--- the fruit is not in the pipe. +function CombineAIDriver:findBestWaypointToUnload(ix) + if self.fieldworkCourse:isOnHeadland(ix) then + return self:findBestWaypointToUnloadOnHeadland(ix) else - return self:findBestWaypointToUnloadOnUpDownRows(waypointIxWhenFull) + return self:findBestWaypointToUnloadOnUpDownRows(ix) end end -function CombineAIDriver:findBestWaypointToUnloadOnHeadland(waypointIxWhenFull) - return waypointIxWhenFull +function CombineAIDriver:findBestWaypointToUnloadOnHeadland(ix) + if self.vehicle.cp.settings.allowUnloadOnFirstHeadland:is(false) and + self.fieldworkCourse:isOnHeadland(ix, 1) then + self:debug('planned rendezvous waypoint %d is on first headland, no unloading of moving combine there', ix) + return nil + end + if self.fieldworkCourse:isTurnStartAtIx(ix) then + -- on the headland, use the wp after the turn, the one before may be very far, especially on a + -- transition from headland to up/down rows. + return ix + 1 + else + return ix + end end -function CombineAIDriver:findBestWaypointToUnloadOnUpDownRows(waypointIxWhenFull) - local dToNextTurn = self.course:getDistanceToNextTurn(waypointIxWhenFull) or 0 - local lRow, ixAtTurnEnd = self.course:getRowLength(waypointIxWhenFull) - local pipeInFruit, _ = self:isPipeInFruitAtWaypoint(self.course, waypointIxWhenFull) - self:debug('Estimated waypoint when full: %d on up/down row, pipe in fruit %s, dToNextTurn: %d m, lRow = %d m', - waypointIxWhenFull, tostring(pipeInFruit), dToNextTurn, lRow or 0) +--- We calculated a waypoint to meet the unloader (either because it asked for it or we think we'll need +--- to unload. Now make sure that this location is not around a turn or the pipe isn't in the fruit by +--- trying to move it up or down a bit. If that's not possible, just leave it and see what happens :) +function CombineAIDriver:findBestWaypointToUnloadOnUpDownRows(ix) + local dToNextTurn = self.fieldworkCourse:getDistanceToNextTurn(ix) or 0 + local lRow, ixAtRowStart = self.fieldworkCourse:getRowLength(ix) + local pipeInFruit = self.fieldworkCourse:isPipeInFruitAt(ix) + local currentIx = self.fieldworkCourse:getCurrentWaypointIx() + local newWpIx = ix + self:debug('Looking for a waypoint to unload around %d on up/down row, pipe in fruit %s, dToNextTurn: %d m, lRow = %d m', + ix, tostring(pipeInFruit), dToNextTurn, lRow or 0) if pipeInFruit then - self:debug('Pipe would be in fruit where we will be full. Check previous row') - if ixAtTurnEnd and ixAtTurnEnd > self.course:getCurrentWaypointIx() then - pipeInFruit, _ = self:isPipeInFruitAtWaypoint(self.course, ixAtTurnEnd - 1) - if not pipeInFruit then - local lPreviousRow = self.course:getRowLength(ixAtTurnEnd - 1) - self:debug('pipe not in fruit in the previous row (%d m, ending at wp %d), so unload there if long enough', - lPreviousRow, ixAtTurnEnd - 1) - return ixAtTurnEnd - 3 + if ixAtRowStart then + if ixAtRowStart > currentIx then + -- have not started the previous row yet + self:debug('Pipe would be in fruit at waypoint %d. Check previous row', ix) + pipeInFruit, _ = self.fieldworkCourse:isPipeInFruitAt(ixAtRowStart - 2) -- wp before the turn start + if not pipeInFruit then + local lPreviousRow, ixAtPreviousRowStart = self.fieldworkCourse:getRowLength(ixAtRowStart - 1) + self:debug('pipe not in fruit in the previous row (%d m, ending at wp %d), rendezvous at %d', + lPreviousRow, ixAtRowStart - 1, newWpIx) + newWpIx = math.max(ixAtRowStart - 3, ixAtPreviousRowStart, currentIx) + else + self:debug('Pipe in fruit in previous row too, rejecting rendezvous') + newWpIx = nil + end + else + -- previous row already started. Could check next row but that means the rendezvous would be after + -- the combine turns, and we'd be in the way during the turn, so rather not worry about the next row + -- until the combine gets there. + self:debug('Pipe would be in fruit at waypoint %d. Previous row is already started, no rendezvous', ix) + newWpIx = nil end + else + self:debug('Could not determine row length, rejecting rendezvous') + newWpIx = nil end else - self:debug('pipe is not in fruit where we are full. If it is towards the end of the row, bring it up a bit') + self:debug('pipe is not in fruit at %d. If it is towards the end of the row, bring it up a bit', ix) -- so we'll have some distance for unloading - if ixAtTurnEnd and dToNextTurn < lRow / 2 then - return ixAtTurnEnd + 1 + if ixAtRowStart and dToNextTurn < CombineAIDriver.safeUnloadDistanceBeforeEndOfRow then + local safeIx = self.fieldworkCourse:getPreviousWaypointIxWithinDistance(ix, + CombineAIDriver.safeUnloadDistanceBeforeEndOfRow) + newWpIx = math.max(ixAtRowStart + 1, safeIx or -1, ix - 4) end end - -- no better idea, just use the original estimated - return waypointIxWhenFull + -- no better idea, just use the original estimated, making sure we avoid turn start waypoints + if newWpIx and self.fieldworkCourse:isTurnStartAtIx(newWpIx) then + self:debug('Calculated rendezvous waypoint is at turn start, moving it up') + -- make sure it is not on the turn start waypoint + return math.max(newWpIx - 1, currentIx) + else + return newWpIx + end end function CombineAIDriver:updateLightsOnField() @@ -849,6 +946,14 @@ function CombineAIDriver:isWaitingForUnloadAfterCourseEnded() self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_AFTER_FIELDWORK_ENDED end +--- Interface for Mode 2 +---@return boolean true when the combine is waiting to after it pulled back. +function CombineAIDriver:isWaitingForUnloadAfterPulledBack() + return self.state == self.states.ON_FIELDWORK_COURSE and + self.fieldworkState == self.states.UNLOAD_OR_REFILL_ON_FIELD and + self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_AFTER_PULLED_BACK +end + function CombineAIDriver:createTurnCourse() return CombineCourseTurn(self.vehicle, self, self.turnContext, self.fieldworkCourse) end @@ -1330,7 +1435,7 @@ end --- events are for the low level coordination between the combine and its unloader(s). CombineUnloadManager --- takes care about coordinating the work between multiple combines. function CombineAIDriver:clearAllUnloaderInformation() - self.agreedUnloaderRendezvousWaypointIx = nil + self:cancelRendezvous() -- the unloaders table hold all registered unloaders, key and value are both the unloader AIDriver self.unloaders = {} end @@ -1410,13 +1515,15 @@ function CombineAIDriver:initUnloadStates() self.states.WAITING_FOR_UNLOAD_AFTER_FIELDWORK_ENDED, self.states.WAITING_FOR_UNLOAD_OR_REFILL, self.states.WAITING_FOR_UNLOAD_AFTER_PULLED_BACK, - self.states.WAITING_FOR_UNLOAD_IN_POCKET + self.states.WAITING_FOR_UNLOAD_IN_POCKET, + self.states.WAITING_FOR_UNLOAD_BEFORE_STARTING_NEXT_ROW } self.willWaitForUnloadToFinishFieldworkStates = { self.states.WAITING_FOR_UNLOAD_AFTER_PULLED_BACK, self.states.WAITING_FOR_UNLOAD_IN_POCKET, self.states.WAITING_FOR_UNLOAD_AFTER_FIELDWORK_ENDED, + self.states.WAITING_FOR_UNLOAD_BEFORE_STARTING_NEXT_ROW } end diff --git a/CombineUnloadAIDriver.lua b/CombineUnloadAIDriver.lua index 20d5524b0..10ab0d136 100644 --- a/CombineUnloadAIDriver.lua +++ b/CombineUnloadAIDriver.lua @@ -72,6 +72,12 @@ CombineUnloadAIDriver.unloaderFollowingDistance = 30 -- distance to keep between CombineUnloadAIDriver.pathfindingRange = 5 -- won't do pathfinding if target is closer than this CombineUnloadAIDriver.proximitySensorRange = 15 +-- Developer hack: to check the class of an object one should use the is_a() defined in CpObject.lua. +-- However, when we reload classes on the fly during the development, the is_a() calls in other modules still +-- have the old class definition (for example CombineUnloadManager.lua) of this class and thus, is_a() fails. +-- Therefore, use this instead, this is safe after a reload. +CombineUnloadAIDriver.isACombineUnloadAIDriver = true + CombineUnloadAIDriver.myStates = { ON_FIELD = {}, ON_UNLOAD_COURSE = @@ -120,6 +126,7 @@ function CombineUnloadAIDriver:init(vehicle) self.distanceToFront = 0 self.combineToUnloadReversing = 0 self.doNotSwerveForVehicle = CpTemporaryObject() + self.justFinishedPathfindingForDistance = CpTemporaryObject() end function CombineUnloadAIDriver:getAssignedCombines() @@ -272,6 +279,11 @@ function CombineUnloadAIDriver:isProximitySpeedControlEnabled() (self.state == self.states.ON_FIELD and self.onFieldState.properties.enableProximitySpeedControl) end +function CombineUnloadAIDriver:isWaitingForAssignment() + return self.state == self.states.ON_FIELD and self.onFieldState == self.states.WAITING_FOR_COMBINE_TO_CALL +end + + function CombineUnloadAIDriver:startWaitingForCombine() -- to always have a valid course (for the traffic conflict detector mainly) self:startCourse(self:getStraightForwardCourse(25), 1) @@ -316,6 +328,7 @@ function CombineUnloadAIDriver:driveOnField(dt) -- check for an available combine but not in every loop, not needed if g_updateLoopIndex % 100 == 0 then + self:debug('Check if there\'s a combine to unload, %s', self:getAssignedCombinesSetting()) self.combineToUnload, combineToWaitFor = g_combineUnloadManager:giveMeACombineToUnload(self.vehicle) if self.combineToUnload ~= nil then self:refreshHUD() @@ -364,27 +377,7 @@ function CombineUnloadAIDriver:driveOnField(dt) elseif self.onFieldState == self.states.DRIVE_TO_COMBINE then - -- do not swerve for our combine, otherwise we won't be able to align with it when coming from - -- the wrong angle - self.doNotSwerveForVehicle:set(self.combineToUnload, 2000) - - courseplay:setInfoText(self.vehicle, "COURSEPLAY_DRIVE_TO_COMBINE"); - - self:setFieldSpeed() - - -- stop when too close to a combine not ready to unload (wait until it is done with turning for example) - if self:isWithinSafeManeuveringDistance(self.combineToUnload) then - self:debugSparse('Too close to maneuvering combine, stop.') - --self:hold() - else - self:setFieldSpeed() - end - - if self:isOkToStartUnloadingCombine() then - self:startUnloadingCombine() - elseif self:isOkToStartFollowingChopper() then - self:startFollowingChopper() - end + self:driveToCombine() elseif self.onFieldState == self.states.DRIVE_TO_MOVING_COMBINE then @@ -403,7 +396,6 @@ function CombineUnloadAIDriver:driveOnField(dt) self:moveOutOfWay() elseif self.onFieldState == self.states.UNLOADING_MOVING_COMBINE then - self:disableProximitySpeedControl() self:disableProximitySwerve() @@ -651,10 +643,6 @@ function CombineUnloadAIDriver:getTrailersTargetNode() return self.vehicle.cp.workTools[1].rootNode, allTrailersFull end -function CombineUnloadAIDriver:getZOffsetToBehindCombine() - return -self:getCombinesMeasuredBackDistance() - 2 -end - function CombineUnloadAIDriver:getSpeedBesideChopper(targetNode) local allowedToDrive = true local baseNode = self:getPipesBaseNode(self.combineToUnload) @@ -1204,7 +1192,8 @@ function CombineUnloadAIDriver:startCourseFollowingCombine() self:setNewOnFieldState(self.states.UNLOADING_MOVING_COMBINE) end -function CombineUnloadAIDriver:isPathFound(path, goalNodeInvalid, goalDescriptor) +---@param dontRelax boolean do not relax pathfinder constraint on failure +function CombineUnloadAIDriver:isPathFound(path, goalNodeInvalid, goalDescriptor, dontRelax) if path and #path > 2 then self:debug('Found path (%d waypoints, %d ms)', #path, self.vehicle.timer - (self.pathfindingStartedAt or 0)) self:resetPathfinder() @@ -1213,7 +1202,7 @@ function CombineUnloadAIDriver:isPathFound(path, goalNodeInvalid, goalDescriptor if goalNodeInvalid then self:error('No path found to %s, goal occupied by a vehicle, waiting...', goalDescriptor) return false - else + elseif not dontRelax then self.pathfinderFailureCount = self.pathfinderFailureCount + 1 if self.pathfinderFailureCount > 1 then self:error('No path found to %s in %d ms, pathfinder failed at least twice, trying a path through crop and relaxing pathfinder field constraint...', @@ -1262,30 +1251,26 @@ end function CombineUnloadAIDriver:startDrivingToCombine() if self.combineToUnload.cp.driver:isWaitingForUnload() then self:debug('Combine is waiting for unload, start finding path to combine') - self:startPathfindingToCombine(self.onPathfindingDoneToCombine, nil, self:getZOffsetToBehindCombine()) + local zOffset + if self.combineToUnload.cp.driver:isWaitingForUnloadAfterPulledBack() then + -- combine pulled back so it's pipe is now out of the fruit. In this case, if the unloader is in front + -- of the combine, it sometimes finds a path between the combine and the fruit to the pipe, we are trying to + -- fix it here: the target is behind the combine, not under the pipe. When we get there, we may need another + -- (short) pathfinding to get under the pipe. + zOffset = - self:getCombinesMeasuredBackDistance() - 10 + else + zOffset = - self:getCombinesMeasuredBackDistance() - 2 + end + self:startPathfindingToCombine(self.onPathfindingDoneToCombine, nil, zOffset) else - -- combine is moving, agree on a rendezvous - -- for now, just use the Eucledian distance. This should rather be the length of a pathfinder generated - -- path, using the simple A* should be good enough for estimation, the hybrid A* would be too slow - local d = self:getDistanceFromCombine() - local estimatedSecondsEnroute = d / (self:getFieldSpeed() / 3.6) + 3 -- add a few seconds to allow for starting the engine/accelerating - local rendezvousWaypoint, rendezvousWaypointIx = self.combineToUnload.cp.driver:getUnloaderRendezvousWaypoint(estimatedSecondsEnroute) - local xOffset = self:getPipeOffset(self.combineToUnload) - local zOffset = self:getZOffsetToBehindCombine() - if rendezvousWaypoint then - if self:isPathfindingNeeded(self.vehicle, rendezvousWaypoint, xOffset, zOffset, 25) then - self:setNewOnFieldState(self.states.WAITING_FOR_PATHFINDER) - self:debug('Start pathfinding to moving combine, %d m, ETE: %d s, meet combine at waypoint %d, xOffset = %.1f, zOffset = %.1f', - d, estimatedSecondsEnroute, rendezvousWaypointIx, xOffset, zOffset) - self:startPathfinding(rendezvousWaypoint, xOffset, zOffset, - PathfinderUtil.getFieldNumUnderVehicle(self.combineToUnload), - {self.combineToUnload}, self.onPathfindingDoneToMovingCombine) - else - self:debug('Rendezvous waypoint %d to moving combine too close, wait a bit', rendezvousWaypointIx) - self:startWaitingForCombine() - end + -- combine is moving, agree on a rendezvous, for that, we need to know the driving distance to the + -- combine first, so find a simple A* path (no hybrid A* needed here as all we need is an approximate distance + -- avoiding fruit) + self:debug('Combine is moving, find path to determine driving distance first') + if self.combineToUnload.cp.driver:isWillingToRendezvous() then + self:startPathfindingForDistance() else - self:debug('can\'t find rendezvous waypoint to combine, waiting') + self:debug('Combine is not willing to rendezvous, wait a bit') self:startWaitingForCombine() end end @@ -1303,6 +1288,52 @@ function CombineUnloadAIDriver:onPathfindingDoneToMovingCombine(path, goalNodeIn end end +------------------------------------------------------------------------------------------------------------------------ +-- Start a simple A* pathfinding to the combine to find out the driving distance while avoiding fruit +-- (which may be considerably longer than a direct path between the unloader and the combine) +------------------------------------------------------------------------------------------------------------------------ +function CombineUnloadAIDriver:startPathfindingForDistance() + if self.justFinishedPathfindingForDistance:get() then + self:debug('just finished another pathfinding for distance, wait a bit before starting another') + self:startWaitingForCombine() + return + end + if self:isPathfindingNeeded(self.vehicle, self:getCombineRootNode(), 0, 0) then + self:setNewOnFieldState(self.states.WAITING_FOR_PATHFINDER) + local done, path, goalNodeInvalid + self.pathfindingStartedAt = self.vehicle.timer + self.pathfinder, done, path, goalNodeInvalid = PathfinderUtil.startAStarPathfindingFromVehicleToNode( + self.vehicle, AIDriverUtil.getDirectionNode(self.combineToUnload), 0, 0, + PathfinderUtil.getFieldNumUnderVehicle(self.combineToUnload), {self.combineToUnload}) + if done then + self:onPathfindingDoneForDistance(path, goalNodeInvalid) + return + else + self:setPathfindingDoneCallback(self, self.onPathfindingDoneForDistance) + return + end + else + local d = self:getDistanceFromCombine() + self:arrangeRendezvousWithCombine(d) + return + end +end + +function CombineUnloadAIDriver:onPathfindingDoneForDistance(path, goalNodeInvalid) + self.justFinishedPathfindingForDistance:set(true, 15000) + if self:isPathFound(path, goalNodeInvalid, nameNum(self.combineToUnload), true) and + self.onFieldState == self.states.WAITING_FOR_PATHFINDER then + local driveToCombineCourse = Course(self.vehicle, courseGenerator.pointsToXzInPlace(path), true) + self:arrangeRendezvousWithCombine(driveToCombineCourse:getLength()) + return true + else + self:debug('pathfinding to find distance to combine did not work out, no rendezvous.') + self:startWaitingForCombine() + return true + end +end + + ------------------------------------------------------------------------------------------------------------------------ -- Pathfinding to combine ------------------------------------------------------------------------------------------------------------------------ @@ -1319,7 +1350,7 @@ function CombineUnloadAIDriver:startPathfindingToCombine(onPathfindingDoneFunc, PathfinderUtil.getFieldNumUnderVehicle(self.combineToUnload), {}, onPathfindingDoneFunc) else self:debug('Can\'t start pathfinding, too close?') - self:startWorking() + self:startWaitingForCombine() end end @@ -1335,6 +1366,43 @@ function CombineUnloadAIDriver:onPathfindingDoneToCombine(path, goalNodeInvalid) end end +------------------------------------------------------------------------------------------------------------------------ +-- With the driving distance known, arrange an unload rendezvous with the combine +------------------------------------------------------------------------------------------------------------------------ +---@param d number distance in meters to drive to the combine, preferably the pathfinder route around the crop +function CombineUnloadAIDriver:arrangeRendezvousWithCombine(d) + if self.combineToUnload.cp.driver:hasRendezvousWith(self) then + self:debug('Have a pending rendezvous, wait a bit') + self:startWaitingForCombine() + return + end + local estimatedSecondsEnroute = d / (self:getFieldSpeed() / 3.6) + 3 -- add a few seconds to allow for starting the engine/accelerating + local rendezvousWaypoint, rendezvousWaypointIx = + self.combineToUnload.cp.driver:getUnloaderRendezvousWaypoint(estimatedSecondsEnroute, self) + if rendezvousWaypoint then + local xOffset, zOffset = self:getPipeOffset(self.combineToUnload) + if self:isPathfindingNeeded(self.vehicle, rendezvousWaypoint, xOffset, zOffset, 25) then + self:setNewOnFieldState(self.states.WAITING_FOR_PATHFINDER) + -- just in case, as the combine may give us a rendezvous waypoint + -- where it is full, make sure we are behind the combine + zOffset = - self:getCombinesMeasuredBackDistance() - 5 + self:debug('Start pathfinding to moving combine, %d m, ETE: %d s, meet combine at waypoint %d, xOffset = %.1f, zOffset = %.1f', + d, estimatedSecondsEnroute, rendezvousWaypointIx, xOffset, zOffset) + self:startPathfinding(rendezvousWaypoint, xOffset, zOffset, + PathfinderUtil.getFieldNumUnderVehicle(self.combineToUnload), + {self.combineToUnload}, self.onPathfindingDoneToMovingCombine) + else + self:debug('Rendezvous waypoint %d to moving combine too close, wait a bit', rendezvousWaypointIx) + self:startWaitingForCombine() + return + end + else + self:debug('can\'t find rendezvous waypoint to combine, waiting') + self:startWaitingForCombine() + return + end +end + ------------------------------------------------------------------------------------------------------------------------ -- Pathfinding to first unloader of a chopper. This is how the second unloader gets to the chopper. ------------------------------------------------------------------------------------------------------------------------ @@ -1397,7 +1465,7 @@ function CombineUnloadAIDriver:startPathfindingToTurnEnd(xOffset, zOffset) end function CombineUnloadAIDriver:onPathfindingDoneToTurnEnd(path, goalNodeInvalid) - if self:isPathFound(path, goalNodeInvalid, 'turn end') then + if self:isPathFound(path, goalNodeInvalid, 'turn end', true) then local driveToCombineCourse = Course(self.vehicle, courseGenerator.pointsToXzInPlace(path), true) self:startCourse(driveToCombineCourse, 1) self:setNewOnFieldState(self.states.FOLLOW_CHOPPER_THROUGH_TURN) @@ -1592,11 +1660,39 @@ function CombineUnloadAIDriver:changeToUnloadWhenFull() end return false end +------------------------------------------------------------------------------------------------------------------------ +-- Drive to stopped combine +------------------------------------------------------------------------------------------------------------------------ +function CombineUnloadAIDriver:driveToCombine() + + -- do not swerve for our combine towards the end of the course, + -- otherwise we won't be able to align with it when coming from + -- the wrong angle + if self.course:getDistanceToLastWaypoint(self.course:getCurrentWaypointIx()) < 20 then + self.doNotSwerveForVehicle:set(self.combineToUnload, 2000) + end + + courseplay:setInfoText(self.vehicle, "COURSEPLAY_DRIVE_TO_COMBINE"); + + self:setFieldSpeed() + + if self:isOkToStartUnloadingCombine() then + self:startUnloadingCombine() + elseif self:isOkToStartFollowingChopper() then + self:startFollowingChopper() + end +end ------------------------------------------------------------------------------------------------------------------------ -- Drive to moving combine ------------------------------------------------------------------------------------------------------------------------ function CombineUnloadAIDriver:driveToMovingCombine() + -- do not swerve for our combine towards the end of the course, + -- otherwise we won't be able to align with it when coming from + -- the wrong angle + if self.course:getDistanceToLastWaypoint(self.course:getCurrentWaypointIx()) < 20 then + self.doNotSwerveForVehicle:set(self.combineToUnload, 2000) + end courseplay:setInfoText(self.vehicle, "COURSEPLAY_DRIVE_TO_COMBINE"); @@ -1608,6 +1704,13 @@ function CombineUnloadAIDriver:driveToMovingCombine() elseif self:isOkToStartUnloadingCombine() then self:startUnloadingCombine() end + + if g_updateLoopIndex % 20 == 0 then + if self.combineToUnload.cp.driver:isWaitingForUnloadAfterPulledBack() then + self:debug('combine is now waiting for unload after pulled back, recalculate path') + self:startDrivingToCombine() + end + end end ------------------------------------------------------------------------------------------------------------------------ @@ -1683,14 +1786,9 @@ function CombineUnloadAIDriver:unloadMovingCombine() if self:changeToUnloadWhenFull() then return end - if self:canDriveBesideCombine(self.combineToUnload) or (self.combineToUnload.cp.driver and self.combineToUnload.cp.driver:isWaitingInPocket()) then - self:driveBesideCombine() - else - self:debugSparse('Can\'t drive beside combine as probably fruit under the pipe but ignore that for now and continue unloading.') + if self:canDriveBesideCombine(self.combineToUnload) or + (self.combineToUnload.cp.driver and self.combineToUnload.cp.driver:isWaitingInPocket()) then self:driveBesideCombine() - --self:releaseUnloader() - --self:startWaitingForCombine() - --return end --when the combine is empty, stop and wait for next combine @@ -1985,6 +2083,21 @@ function CombineUnloadAIDriver:followFirstUnloader() end end +------------------------------------------------------------------------------------------------------------------------ +-- We missed a rendezvous with the combine +------------------------------------------------------------------------------------------------------------------------ +---@param combineAIDriver CombineAIDriver +function CombineUnloadAIDriver:onMissedRendezvous(combineAIDriver) + self:debug('missed the rendezvous with %s', nameNum(combineAIDriver.vehicle)) + if self.state == self.states.ON_FIELD and self.onFieldState == self.states.DRIVE_TO_MOVING_COMBINE and + self.combineToUnload == combineAIDriver.vehicle then + -- re-evaluate situation + self:startWorking() + else + self:debug('ignore missed rendezvous, state %s, fieldwork state %s', self.state.name, self.onFieldState.name) + end +end + ------------------------------------------------------------------------------------------------------------------------ -- We are blocking another vehicle who wants us to move out of way ------------------------------------------------------------------------------------------------------------------------ diff --git a/CombineUnloadManager.lua b/CombineUnloadManager.lua index 355ef8ece..361806c9a 100644 --- a/CombineUnloadManager.lua +++ b/CombineUnloadManager.lua @@ -54,7 +54,7 @@ function CombineUnloadManager:addNewCombines() -- this isn't needed as combines will be added when an CombineAIDriver is created for them -- but we want to be able to reload this file on the fly when developing/troubleshooting for _, vehicle in pairs(g_currentMission.vehicles) do - if vehicle.cp.driver and vehicle.cp.driver.is_a and vehicle.cp.driver:is_a(CombineAIDriver) and not self.combines[vehicle] then + if vehicle.cp.driver and vehicle.cp.driver.isACombineAIDriver and not self.combines[vehicle] then self:addCombineToList(vehicle, vehicle.cp.driver) end end @@ -131,7 +131,7 @@ function CombineUnloadManager:releaseUnloaderFromCombine(unloader, combine, noEv self:debug('Released unloader %s from %s', nameNum(unloader), nameNum(combine)) table.remove(self.combines[combine].unloaders, ix) if not noEventSend then - UnloaderEvents:sendRelaseUnloaderEvent(unloader,combine) + UnloaderEvents:sendReleaseUnloaderEvent(unloader,combine) end end end @@ -145,7 +145,7 @@ function CombineUnloadManager:addUnloaderToCombine(unloader,combine,noEventSend) UnloaderEvents:sendAddUnloaderToCombine(unloader,combine) end else - self:debug('%s is already assigned to combine %s as #%d', nameNum(unloader), nameNum(combine), #self.combines[combine].unloaders) + self:debug('%s is already assigned to combine %s as number %d', nameNum(unloader), nameNum(combine), #self.combines[combine].unloaders) end end @@ -186,7 +186,7 @@ function CombineUnloadManager:giveMeACombineToUnload(unloader) end --then try to find a combine local combine = self:getCombineWithMostFillLevel(unloader) - self:debug('Combine with most fill level is %s', combine and combine:getName() or 'N/A') + self:debug('Combine with most fill level is %s', nameNum(combine)) local bestUnloader if combine ~= nil and combine.cp.driver:getFieldworkCourse() then if combine.cp.settings.combineWantsCourseplayer:is(true) then @@ -194,7 +194,13 @@ function CombineUnloadManager:giveMeACombineToUnload(unloader) combine.cp.settings.combineWantsCourseplayer:set(false) return combine end - local unloaders = self:getUnloaders(combine) + local num = self:getUnloadersNumber(unloader, combine) + if num then + -- awesome, we are on the list already. + self:debug('%s already assigned to %s as #%d', nameNum(unloader), nameNum(combine), num) + return combine + end + local unloaders = self:getIdleUnloaders(combine) if combine.cp.settings.driverPriorityUseFillLevel:is(true) then bestUnloader = self:getFullestUnloader(combine, unloaders) self:debug('Priority fill level, best unloader %s', bestUnloader and nameNum(bestUnloader) or 'N/A') @@ -203,13 +209,20 @@ function CombineUnloadManager:giveMeACombineToUnload(unloader) self:debug('Priority closest, best unloader %s', bestUnloader and nameNum(bestUnloader) or 'N/A') end if bestUnloader == unloader then - if self:getCombinesFillLevelPercent(combine) > unloader.cp.driver:getFillLevelThreshold() or combine.cp.driver:willWaitForUnloadToFinish() then - self:debug("%s: fill level %.1f, waiting for unload", nameNum(combine), self:getCombinesFillLevelPercent(combine)) + local combineFillLevelPercent = self:getCombinesFillLevelPercent(combine) + local willWait = combine.cp.driver:willWaitForUnloadToFinish() + if combineFillLevelPercent > unloader.cp.driver:getFillLevelThreshold() or willWait then + self:debug("%s: fill level %.1f (unloader threshold %.1f), waiting for unload", nameNum(combine), + combineFillLevelPercent, unloader.cp.driver:getFillLevelThreshold()) self:addUnloaderToCombine(unloader, combine) return combine else + self:debug("%s: fill level %.1f (unloader threshold %.1f), combine will wait %s (no unloader assigned)", + nameNum(combine), combineFillLevelPercent, unloader.cp.driver:getFillLevelThreshold(), tostring(willWait)) return nil, combine end + else + self:debug('Best unloader %s is not the one requesting (%s)', nameNum(bestUnloader), nameNum(unloader)) end end end @@ -232,14 +245,19 @@ end function CombineUnloadManager:getCombineWithMostFillLevel(unloader) local mostFillLevel = 0 local combineToReturn - for combine,_ in pairs(unloader.cp.driver:getAssignedCombines()) do + for combine, _ in pairs(unloader.cp.driver:getAssignedCombines()) do local data = self.combines[combine] -- if there is no unloader assigned or this unloader is already assigned as the first - if data and data.isCombine and (self:getNumUnloaders(combine) == 0 or self:getUnloaderIndex(unloader, combine) == 1) then + local numUnloaders = self:getNumUnloaders(combine) + local unloaderIndex = self:getUnloaderIndex(unloader, combine) + local fillLevelPct = combine.cp.driver:getFillLevelPercentage() + local combineReadyToUnload = combine.cp.driver:isReadyToUnload(unloader.cp.settings.useRealisticDriving:is(true)) + self:debug('For unloader %s: %s (fill level %.1f, ready to unload: %s) has %d unloaders, this unloader is # %d', + nameNum(unloader), nameNum(combine), fillLevelPct, tostring(combineReadyToUnload), numUnloaders, unloaderIndex or -1) + if data and data.isCombine and (numUnloaders == 0 or unloaderIndex == 1) and combineReadyToUnload then if combine.cp.settings.combineWantsCourseplayer:is(true) then return combine end - local fillLevelPct = combine.cp.driver:getFillLevelPercentage() if mostFillLevel < fillLevelPct then mostFillLevel = fillLevelPct combineToReturn = combine @@ -249,11 +267,12 @@ function CombineUnloadManager:getCombineWithMostFillLevel(unloader) return combineToReturn end -function CombineUnloadManager:getUnloaders(combine) +function CombineUnloadManager:getIdleUnloaders(combine) local unloaders = {} if g_currentMission then for _, vehicle in pairs(g_currentMission.vehicles) do - if vehicle.cp.driver and vehicle.cp.driver:is_a(CombineUnloadAIDriver) then + if vehicle.cp.driver and vehicle.cp.driver.isACombineUnloadAIDriver and + vehicle.cp.driver:isWaitingForAssignment() then -- TODO: refactor and move assignedCombines into the CombineUnloadAIDriver local assignedCombines = vehicle.cp.driver:getAssignedCombines() if assignedCombines[combine] then @@ -316,7 +335,7 @@ end function CombineUnloadManager:removeInactiveCombines() local vehiclesToRemove = {} for vehicle, _ in pairs (self.combines) do - if not vehicle.cp.driver or not vehicle.cp.driver:is_a(CombineAIDriver) then + if not vehicle.cp.driver or not vehicle.cp.driver.isACombineAIDriver then table.insert(vehiclesToRemove, vehicle) end end diff --git a/Events/UnloaderEvents.lua b/Events/UnloaderEvents.lua index a95ee5174..acf7e1ae5 100644 --- a/Events/UnloaderEvents.lua +++ b/Events/UnloaderEvents.lua @@ -58,7 +58,7 @@ function UnloaderEvents:run(connection) -- wir fuehren das empfangene event aus end; end -function UnloaderEvents:sendRelaseUnloaderEvent(unloader,combine) +function UnloaderEvents:sendReleaseUnloaderEvent(unloader,combine) if g_server ~= nil then -- Server have to broadcast to all clients and himself g_server:broadcastEvent(UnloaderEvents:new(unloader,combine,self.TYPE_REMOVE_FROM_COMBINE)) diff --git a/GlobalSettings.lua b/GlobalSettings.lua index 134650720..1fd871ff5 100644 --- a/GlobalSettings.lua +++ b/GlobalSettings.lua @@ -70,7 +70,7 @@ AutoRepairSetting.OFF = 0 function AutoRepairSetting:init() SettingList.init(self, 'autoRepair', 'COURSEPLAY_AUTOREPAIR', 'COURSEPLAY_AUTOREPAIR_TOOLTIP', nil, {AutoRepairSetting.OFF, 25, 70, 99}, - {'COURSEPLAY_AUTOREPAIR_OFF', '< 25%', '< 70%', 'COURSEPALY_AUTOREPAIR_ALWAYS'} + {'COURSEPLAY_AUTOREPAIR_OFF', '< 25%', '< 70%', 'COURSEPLAY_AUTOREPAIR_ALWAYS'} ) self:set(0) end diff --git a/TrafficCollision.lua b/TrafficCollision.lua index fb38d8460..f041a60b6 100644 --- a/TrafficCollision.lua +++ b/TrafficCollision.lua @@ -506,13 +506,15 @@ function TrafficConflictDetector:updateCollisionBoxes(course, ix, nominalSpeed, local positions if course then - positions = course:getPositionsOnCourse(nominalSpeed, ix, TrafficConflictDetector.boxDistance, TrafficConflictDetector.numTrafficCollisionTriggers) + positions = course:getPositionsOnCourse(nominalSpeed, ix, + TrafficConflictDetector.boxDistance, TrafficConflictDetector.numTrafficCollisionTriggers) + self:debug('updating collision boxes at waypoint %d, have %d positions', ix, #positions) else positions = self:getPositionsAtDirection(nominalSpeed, moveForwards, directionNode) + self:debug('updating collision boxes (no course), have %d positions', #positions) end local posIx = 1 local eta = 0 - self:debug('updating collision boxes at waypoint %d, have %d positions', ix, #positions) if #positions > 0 then for i, trigger in ipairs(self.trafficCollisionTriggers) do local d = (i - 1) * TrafficConflictDetector.boxDistance diff --git a/Waypoint.lua b/Waypoint.lua index 5f0d630b0..c047b613b 100644 --- a/Waypoint.lua +++ b/Waypoint.lua @@ -408,17 +408,19 @@ function Course:enrichWaypointData() self.waypoints[#self.waypoints].turnsToHere = self.totalTurns self.waypoints[#self.waypoints].calculatedRadius = self:calculateRadius(#self.waypoints) self.waypoints[#self.waypoints].reverseOffset = self:isReverseAt(#self.waypoints) - -- now add distance to next turn for the combines - local dToNextTurn, lNextRow = 0, 0 + -- now add some metadata for the combines + local dToNextTurn, lNextRow, nextRowStartIx = 0, 0, 0 local turnFound = false for i = #self.waypoints - 1, 1, -1 do if turnFound then dToNextTurn = dToNextTurn + self.waypoints[i].dToNext self.waypoints[i].dToNextTurn = dToNextTurn self.waypoints[i].lNextRow = lNextRow + self.waypoints[i].nextRowStartIx = nextRowStartIx end if self:isTurnStartAtIx(i) then lNextRow = dToNextTurn + nextRowStartIx = i + 1 dToNextTurn = 0 turnFound = true end @@ -861,7 +863,7 @@ end --- Get the index of the first waypoint from ix which is at least distance meters away ---@param backward boolean search backward if true ----@return numer, number index and exact distance +---@return number, number index and exact distance function Course:getNextWaypointIxWithinDistance(ix, distance, backward) local d = 0 local from, to, step = ix, #self.waypoints - 1, 1 @@ -1136,6 +1138,9 @@ function Course:isCloseToLastTurn(distance) return false end +--- Get the length of the up/down row where waypoint ix is located +--- @param ix number waypoint index in the row +--- @return number, number length of the current row and the index of the first waypoint of the row function Course:getRowLength(ix) for i = ix, 1, -1 do if self:isTurnEndAtIx(i) then @@ -1149,6 +1154,10 @@ function Course:getNextRowLength(ix) return self.waypoints[ix].lNextRow end +function Course:getNextRowStartIx(ix) + return self.waypoints[ix].nextRowStartIx +end + function Course:draw() for i = 1, math.max(#self.waypoints - 1, 1) do local x1, y1, z1 = self:getWaypointPosition(i) @@ -1470,8 +1479,9 @@ function Course:setPipeInFruitMap(pipeOffsetX, workWidth) pipeInFruitMapHelperWpNode:setToWaypoint(self, row.startIx) -- pipe's local position in the row start wp's system local lx, _, lz = worldToLocal(pipeInFruitMapHelperWpNode.node, x, y, z) - -- add 10 cm buffer to make sure turn end/start waypoints have correct data - if math.abs(lx) <= halfWorkWidth and lz >= 0.1 and lz <= row.length + 0.1 then + -- add 20 m buffer to account for non-perpendicular headlands where technically the pipe + -- would not be in the fruit around the end of the row + if math.abs(lx) <= halfWorkWidth and lz >= -20 and lz <= row.length + 20 then -- pipe is in the fruit at ix return true end @@ -1481,7 +1491,7 @@ function Course:setPipeInFruitMap(pipeOffsetX, workWidth) -- The idea here is that we walk backwards on the course, remembering each row and adding them -- to the list of unworked rows. This way, at any waypoint we have a list of rows the vehicle - -- wouldn't have finished if it was driving the course the right wa y (start to end). + -- wouldn't have finished if it was driving the course the right way (start to end). -- Now check if the pipe would be in any of these unworked rows local rowsNotDone = {} local totalNonHeadlandWps = 0 diff --git a/course-generator/HybridAStar.lua b/course-generator/HybridAStar.lua index 55d7ca829..7a8a15807 100644 --- a/course-generator/HybridAStar.lua +++ b/course-generator/HybridAStar.lua @@ -497,9 +497,6 @@ function HybridAStar:findPath(start, goal, turnRadius, allowReverse, constraints analyticPath[1]:setTrailerHeading(pred:getTrailerHeading()) State3D.calculateTrailerHeadings(analyticPath, hitchLength) if self:isPathValid(analyticPath) then - --TODO: figure out a possible debug channel, if needed - - --State3D.printPath(analyticPath, 'ANALYTIC') self:debug('Found collision free analytic path (%s) at iteration %d', pathType, self.iterations) -- remove first node of returned analytic path as it is the same as pred table.remove(analyticPath, 1) @@ -630,8 +627,8 @@ end --- 3 dimensional as we do not take the heading into account and we use a different set of motion primitives AStar = CpObject(HybridAStar) -function AStar:init(yieldAfter) - HybridAStar.init(self, yieldAfter) +function AStar:init(yieldAfter, maxIterations) + HybridAStar.init(self, yieldAfter, maxIterations) -- this needs to be small enough that no vehicle fit between the grid points (and remain undetected) self.deltaPos = 3 self.deltaPosGoal = self.deltaPos @@ -867,4 +864,4 @@ end function HybridAStarWithPathInTheMiddle:getAStar() return DummyAStar(self.path) -end \ No newline at end of file +end diff --git a/course-generator/PathfinderUtil.lua b/course-generator/PathfinderUtil.lua index ca081da6f..0b23a2813 100644 --- a/course-generator/PathfinderUtil.lua +++ b/course-generator/PathfinderUtil.lua @@ -549,6 +549,16 @@ function PathfinderConstraints:resetConstraints() self.context.parameters.maxFruitPercent = self.normalMaxFruitPercent end +---@param start State3D +---@param vehicleData PathfinderUtil.VehicleData +local function initializeTrailerHeading(start, vehicleData) + -- initialize the trailer's heading for the starting point + if vehicleData.trailer then + local _, _, yRot = PathfinderUtil.getNodePositionAndDirection(vehicleData.trailer.rootNode, 0, 0) + start:setTrailerHeading(courseGenerator.fromCpAngle(yRot)) + end +end + ---@param start State3D ---@param goal State3D local function startPathfindingFromVehicleToGoal(vehicle, start, goal, @@ -560,11 +570,7 @@ local function startPathfindingFromVehicleToGoal(vehicle, start, goal, offFieldPenalty or PathfinderUtil.defaultOffFieldPenalty) local vehicleData = PathfinderUtil.VehicleData(vehicle, true, 0.5) - -- initialize the trailer's heading for the starting point - if vehicleData.trailer then - local _, _, yRot = PathfinderUtil.getNodePositionAndDirection(vehicleData.trailer.rootNode, 0, 0) - start:setTrailerHeading(courseGenerator.fromCpAngle(yRot)) - end + initializeTrailerHeading(start, vehicleData) local context = PathfinderUtil.Context( vehicleData, @@ -746,6 +752,44 @@ function PathfinderUtil.startPathfindingFromVehicleToNode(vehicle, goalNode, vehiclesToIgnore, maxFruitPercent, offFieldPenalty, mustBeAccurate) end +------------------------------------------------------------------------------------------------------------------------ +--- Interface function to start a simple A* pathfinder in the game. The goal is a node +------------------------------------------------------------------------------------------------------------------------ +---@param vehicle table, will be used as the start location/heading, turn radius and size +---@param goalNode table The goal node +---@param xOffset number side offset of the goal from the goal node (> 0 is left) +---@param zOffset number length offset of the goal from the goal node (> 0 is front) +---@param fieldNum number if other than 0 or nil the pathfinding is restricted to the given field and its vicinity +---@param vehiclesToIgnore table[] list of vehicles to ignore for the collision detection (optional) +---@param maxFruitPercent number maximum percentage of fruit present before a node is marked as invalid (optional) +function PathfinderUtil.startAStarPathfindingFromVehicleToNode(vehicle, goalNode, + xOffset, zOffset, + fieldNum, vehiclesToIgnore, maxFruitPercent) + local x, z, yRot = PathfinderUtil.getNodePositionAndDirection(AIDriverUtil.getDirectionNode(vehicle)) + local start = State3D(x, -z, courseGenerator.fromCpAngle(yRot)) + x, z, yRot = PathfinderUtil.getNodePositionAndDirection(goalNode, xOffset, zOffset) + local goal = State3D(x, -z, courseGenerator.fromCpAngle(yRot)) + + local otherVehiclesCollisionData = PathfinderUtil.setUpVehicleCollisionData(vehicle, vehiclesToIgnore) + local parameters = PathfinderUtil.Parameters(maxFruitPercent or + (vehicle.cp.settings.useRealisticDriving:is(true) and 50 or math.huge), PathfinderUtil.defaultOffFieldPenalty) + local vehicleData = PathfinderUtil.VehicleData(vehicle, true, 0.5) + + initializeTrailerHeading(start, vehicleData) + + local context = PathfinderUtil.Context( + vehicleData, + fieldNum, + parameters, + vehiclesToIgnore, + otherVehiclesCollisionData) + + local pathfinder = AStar(100, 10000) + local done, path, goalNodeInvalid = pathfinder:start(start, goal, context.vehicleData.turnRadius, false, + PathfinderConstraints(context), context.vehicleData.trailerHitchLength) + return pathfinder, done, path, goalNodeInvalid +end + ------------------------------------------------------------------------------------------------------------------------ -- Debug stuff --------------------------------------------------------------------------------------------------------------------------- diff --git a/modDesc.xml b/modDesc.xml index d22e45428..21930678a 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -1,6 +1,6 @@ - 6.03.00019 + 6.03.00020 <!-- en=English de=German fr=French es=Spanish ru=Russian pl=Polish it=Italian br=Brazilian-Portuguese cs=Chinese(Simplified) ct=Chinese(Traditional) cz=Czech nl=Netherlands hu=Hungary jp=Japanese kr=Korean pt=Portuguese ro=Romanian tr=Turkish --> <en>CoursePlay SIX</en> diff --git a/settings.lua b/settings.lua index dcd9fc5d9..3ce230af2 100644 --- a/settings.lua +++ b/settings.lua @@ -2625,6 +2625,18 @@ function CombineWantsCourseplayerSetting:init(vehicle) self:set(false) end +---@class KeepUnloadingUntilEndOfRow : BooleanSetting +KeepUnloadingUntilEndOfRow = CpObject(BooleanSetting) +function KeepUnloadingUntilEndOfRow:init(vehicle) + BooleanSetting.init(self, 'keepUnloadingUntilEndOfRow', 'COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW', + 'COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP', vehicle) + self:set(false) +end + +function KeepUnloadingUntilEndOfRow:isDisabled() + return self.vehicle.cp.driver and not self.vehicle.cp.driver:is_a(CombineUnloadAIDriver) +end + ---@class SiloSelectedFillTypeSetting : LinkedListSetting SiloSelectedFillTypeSetting = CpObject(LinkedListSetting) SiloSelectedFillTypeSetting.NetworkTypes = {} @@ -3327,6 +3339,7 @@ function AssignedCombinesSetting:init(vehicle) Setting.init(self, 'assignedCombines','-', '-', vehicle) self.MAX_COMBINES_FOR_PAGE = 5 self.offsetHead = 0 + -- table has key (the combine's vehicle) with all combines assigned to this unloader in the HUD, value is true self.table = {} self.lastPossibleCombines = {} end @@ -3451,6 +3464,16 @@ function AssignedCombinesSetting:getData() return self.table end +function AssignedCombinesSetting:__tostring() + local ret = 'assigned combines: ' + local list = '' + for combine, _ in pairs(self.table) do + list = list .. nameNum(combine) .. ', ' + end + if list == '' then list = 'none' end + return ret .. list +end + function AssignedCombinesSetting:selectClosest() local dMin = math.huge local closestCombine diff --git a/translations/translation_br.xml b/translations/translation_br.xml index fa33c1dd6..881f16404 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -500,8 +500,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index 94d566688..506da4b7b 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -501,8 +501,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index c4c730ba5..eac6c4579 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -504,8 +504,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatická oprava" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Opravuje se automaticky na poli." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Neopravovat" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Udržovat v dobrém stavu" /> - - <!-- Replace marker, do not remove! --> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Udržovat v dobrém stavu" /> + + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> + <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_de.xml b/translations/translation_de.xml index b2b34b7ab..838cc472a 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -501,8 +501,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatisch reparieren" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repariert die Fahrzeuge automatisch." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Nie" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Immer" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Immer" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_en.xml b/translations/translation_en.xml index 0e2c7350a..4f5bb4205 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -499,8 +499,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> - + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_es.xml b/translations/translation_es.xml index 2a9fee197..d9119ef0a 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -499,8 +499,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index ba87162eb..0f86d484a 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -502,8 +502,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index 222f00ff6..f9ca2f41a 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -499,8 +499,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 94acaf9eb..7be59c1b9 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -501,8 +501,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> - </texts> +</texts> </l10n> diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index 38e5284bf..eb8b55b08 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -501,8 +501,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index c08f472d3..78108093c 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -499,8 +499,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n> diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 4c55b3545..68b209b9f 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -502,8 +502,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatyczna naprawa" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Naprawa jest przeprowadzana automatycznie na polu." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Nie naprawiaj" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Naprawiaj" /> - + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Naprawiaj" /> + + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> - </texts> +</texts> </l10n> diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index 866cc6f4b..186219669 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -499,8 +499,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> - </texts> +</texts> </l10n> diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index fdedb44af..ec5cdefea 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -503,8 +503,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Автоматический ремонт" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Автоматически ремонтировать на поле." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Не ремонтировать" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Поддерживать исправным" /> - + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Поддерживать исправным" /> + + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> - </texts> +</texts> </l10n> diff --git a/translations/translation_sl.xml b/translations/translation_sl.xml index 604f61aad..323e1e9e5 100644 --- a/translations/translation_sl.xml +++ b/translations/translation_sl.xml @@ -502,8 +502,10 @@ <text name="COURSEPLAY_AUTOREPAIR" text="Automatic repair" /> <text name="COURSEPLAY_AUTOREPAIR_TOOLTIP" text="Repairs automatically on the Field." /> <text name="COURSEPLAY_AUTOREPAIR_OFF" text="Don't repair" /> - <text name="COURSEPALY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_AUTOREPAIR_ALWAYS" text="Keep it healthy" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW" text="Keep unloading until end of row" /> + <text name="COURSEPLAY_KEEP_UNLOADING_UNTIL_END_OF_ROW_TOOLTIP" text="Don't stop unloading the combine when it is empty, follow it until the end of the row" /> <!-- Replace marker, do not remove! --> </texts> </l10n>