Skip to content
This repository has been archived by the owner on May 23, 2023. It is now read-only.

Commit

Permalink
6.03.00020 Mode2/3 improvements
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
pvaiko committed Jan 3, 2021
1 parent ea86c6f commit 166a10f
Show file tree
Hide file tree
Showing 26 changed files with 564 additions and 219 deletions.
309 changes: 208 additions & 101 deletions CombineAIDriver.lua

Large diffs are not rendered by default.

231 changes: 172 additions & 59 deletions CombineUnloadAIDriver.lua

Large diffs are not rendered by default.

45 changes: 32 additions & 13 deletions CombineUnloadManager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -186,15 +186,21 @@ 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
self:addUnloaderToCombine(unloader,combine)
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')
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Events/UnloaderEvents.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion GlobalSettings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions TrafficCollision.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 16 additions & 6 deletions Waypoint.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 3 additions & 6 deletions course-generator/HybridAStar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -867,4 +864,4 @@ end

function HybridAStarWithPathInTheMiddle:getAStar()
return DummyAStar(self.path)
end
end
54 changes: 49 additions & 5 deletions course-generator/PathfinderUtil.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
---------------------------------------------------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion modDesc.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
<modDesc descVersion="47">
<version>6.03.00019</version>
<version>6.03.00020</version>
<author><![CDATA[Courseplay.devTeam]]></author>
<title><!-- 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>
Expand Down
23 changes: 23 additions & 0 deletions settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion translations/translation_br.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Loading

0 comments on commit 166a10f

Please sign in to comment.