Skip to content

Commit

Permalink
refactor: the big field polygon refactoring
Browse files Browse the repository at this point in the history
Field polygon was all over the place before, now only
store it in the CourseGenerator spec.

Start detection asynchronously and prepare strategies
that need the field polygon to wait for the detection
finished.
  • Loading branch information
Peter Vaiko committed Jan 22, 2025
1 parent bac2057 commit 4b7fd0d
Show file tree
Hide file tree
Showing 15 changed files with 203 additions and 131 deletions.
2 changes: 2 additions & 0 deletions config/InfoTexts.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<InfoText name="ERROR_WRONG_MISSION_FRUIT_TYPE" text="errorWrongMissionFruitType" class="AIMessageErrorWrongMissionFruitType"/>
<InfoText name="ERROR_PALLETS_ARE_FULL" text="errorPalletsAreFull" class="pdlc_premiumExpansion.AIMessageErrorPalletsFull"/>
<InfoText name="ERROR_PALLETS_ARE_EMPTY" text="errorPalletsAreEmpty" class="pdlc_premiumExpansion.AIMessageErrorNoPalletsLoaded"/>
<InfoText name="ERROR_TOO_FAR_FROM_FIELD" text="errorTooFarFromField" class="AIMessageErrorTooFarFromField"/>

<InfoText name="IS_STUCK" text="isStuck"/>
<InfoText name="BLOCKED_BY_OBJECT" text="blockedByObject" class="AIMessageErrorBlockedByObject"/>
Expand All @@ -41,4 +42,5 @@
<InfoText name="WORK_FINISHED" text="workFinished" hasFinished="true" event="onCpFinished" class="AIMessageSuccessFinishedJob"/>
<InfoText name="DRIVING_TO_COMBINE" text="drivingToCombine"/>
<InfoText name="DRIVING_TO_SELF_UNLOAD" text="drivingToSelfUnload"/>
<InfoText name="WAITING_FOR_FIELD_BOUNDARY_DETECTION" text="waitingForFieldBoundaryDetection"/>
</InfoTexts>
12 changes: 8 additions & 4 deletions config/MasterTranslations.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@
<Text language="de"><![CDATA[Feldabladeposition ist zu weit vom Feld entfernt.]]></Text>
<Text language="en"><![CDATA[Field unload position is too far away from the field.]]></Text>
</Translation>
<Translation name="CP_error_vehicle_too_far_away_from_field">
<Text language="de"><![CDATA[Fahrzeug ist zu weit vom Feld entfernt.]]></Text>
<Text language="en"><![CDATA[Vehicle is too far away from the field.]]></Text>
</Translation>
<Translation name="CP_error_no_bunkerSilo_found">
<Text language="de"><![CDATA[Kein Silo gefunden.]]></Text>
<Text language="en"><![CDATA[No silo found.]]></Text>
Expand Down Expand Up @@ -389,6 +385,10 @@
<Text language="de"><![CDATA[Helfer %s hat die Arbeit gestoppt - Falsche Frucht für die Mission ausgewählt!]]></Text>
<Text language="en"><![CDATA[AI worker %s has stopped work unexpectedly - wrong fill type for mission selected!]]></Text>
</Translation>
<Translation name="CP_ai_messageErrorTooFarFromField">
<Text language="de"><![CDATA[Fahrzeug ist zu weit vom Feld entfernt.]]></Text>
<Text language="en"><![CDATA[Vehicle is too far away from the field.]]></Text>
</Translation>
</Category>
<Category name="AI fieldwork task descriptions">
<Translation name="CP_ai_taskDescriptionAttachHeader">
Expand Down Expand Up @@ -1724,6 +1724,10 @@ The course is saved automatically on closing of the editor and overrides the sel
<Text language="de"><![CDATA[ÜLW entladen]]></Text>
<Text language="en"><![CDATA[Driving to trailer]]></Text>
</Translation>
<Translation name="CP_infoTexts_waitingForFieldBoundaryDetection">
<Text language="de"><![CDATA[Felderkennung]]></Text>
<Text language="en"><![CDATA[Field detection]]></Text>
</Translation>
<Translation name="CP_infoTexts_outOfMoney">
<Text language="de"><![CDATA[Kein Geld mehr]]></Text>
<Text language="en"><![CDATA[Out of money]]></Text>
Expand Down
16 changes: 0 additions & 16 deletions scripts/Course.lua
Original file line number Diff line number Diff line change
Expand Up @@ -76,22 +76,6 @@ function Course:getVehicle()
return self.vehicle
end

function Course:setFieldPolygon(polygon)
self.fieldPolygon = polygon
end

-- The field polygon used to generate the course
function Course:getFieldPolygon()
local i = 1
while self.fieldPolygon == nil and i < self:getNumberOfWaypoints() do
CpUtil.debugVehicle(CpDebug.DBG_COURSES, self.vehicle, 'Field polygon not found, regenerating it (%d).', i)
local px, _, pz = self:getWaypointPosition(i)
self.fieldPolygon = CpFieldUtil.getFieldPolygonAtWorldPosition(px, pz)
i = i + 1
end
return self.fieldPolygon
end

function Course:getName()
return self.name
end
Expand Down
8 changes: 8 additions & 0 deletions scripts/ai/AIMessages.lua
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ function AIMessageErrorWrongMissionFruitType:getI18NText()
return g_i18n:getText("CP_ai_messageErrorWrongMissionFruitType")
end

---@class AIMessageErrorTooFarFromField
AIMessageErrorTooFarFromField = CpObject(AIMessage, AIMessage.new)
AIMessageErrorTooFarFromField.name = "CP_ERROR_TOO_FAR_FROM_FIELD"
function AIMessageErrorTooFarFromField:getI18NText()
return g_i18n:getText("CP_ai_messageErrorTooFarFromField")
end

CpAIMessages = {}
function CpAIMessages.register()
local function register(messageClass)
Expand All @@ -67,6 +74,7 @@ function CpAIMessages.register()
register(AIMessageErrorCutterNotSupported)
register(AIMessageErrorAutomaticCutterAttachNotActive)
register(AIMessageErrorWrongMissionFruitType)
register(AIMessageErrorTooFarFromField)
end

--- Another ugly hack, as the giants code to get the message index in mp isn't working ..
Expand Down
32 changes: 32 additions & 0 deletions scripts/ai/jobs/CpAIJob.lua
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,38 @@ function CpAIJob:validate(farmId)
return isValid, errorMessage
end

--- Start an asynchronous field boundary detection. Results are delivered by the callback
--- onFieldBoundaryDetectionFinished(vehicle, fieldPolygon, islandPolygons)
--- If the field position hasn't changed since the last call, the detection is skipped and this returns true.
--- In that case, the polygon from the previous run is still available from vehicle:cpGetFieldPolygon()
---@return boolean, string TODO
function CpAIJob:detectFieldBoundary()
local vehicle = self.vehicleParameter:getVehicle()

local tx, tz = self.cpJobParameters.fieldPosition:getPosition()
if tx == nil or tz == nil then
return false, g_i18n:getText("CP_error_not_on_field")
end
if vehicle:cpIsFieldBoundaryDetectionRunning() then
return false, g_i18n:getText("CP_error_field_detection_still_running")
end
local x, z = vehicle:cpGetFieldPosition()
if x == tx and z == tz then
self:debug('Field position still at %.1f/%.1f, do not detect field boundary again', tx, tz)
return true, ''
end
self:debug('Field position changed to %.1f/%.1f, start field boundary detection', tx, tz)
self.foundVines = nil

vehicle:cpDetectFieldBoundary(tx, tz, self, self.onFieldBoundaryDetectionFinished)
-- TODO: return false and nothing, as the detection is still running?
return true, g_i18n:getText('CP_error_field_detection_still_running')
end

function CpAIJob:onFieldBoundaryDetectionFinished(vehicle, fieldPolygon, islandPolygons)
-- override in the derived classes to handle the detected field boundary
end

function CpAIJob:getIsStartable(connection)

local vehicle = self.vehicleParameter:getVehicle()
Expand Down
38 changes: 2 additions & 36 deletions scripts/ai/jobs/CpAIJobBaleFinder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
CpAIJobBaleFinder = CpObject(CpAIJob)
CpAIJobBaleFinder.name = "BALE_FINDER_CP"
CpAIJobBaleFinder.jobName = "CP_job_baleCollect"
CpAIJobBaleFinder.minStartDistanceToField = 20
function CpAIJobBaleFinder:init(isServer)
CpAIJob.init(self, isServer)
self.selectedFieldPlot = FieldPlot(true)
Expand All @@ -28,10 +27,9 @@ function CpAIJobBaleFinder:getIsAvailableForVehicle(vehicle, cpJobsAllowed)
end

function CpAIJobBaleFinder:getCanStartJob()
return self:getFieldPolygon() ~= nil
return self:getVehicle():cpGetFieldPolygon() ~= nil
end


function CpAIJobBaleFinder:applyCurrentState(vehicle, mission, farmId, isDirectStart, isStartPositionInvalid)
CpAIJob.applyCurrentState(self, vehicle, mission, farmId, isDirectStart)
self.cpJobParameters:validateSettings()
Expand Down Expand Up @@ -64,43 +62,11 @@ function CpAIJobBaleFinder:validate(farmId)
--------------------------------------------------------------
--- Validate field setup
--------------------------------------------------------------

isValid, errorMessage = self:validateFieldPosition(isValid, errorMessage)
local fieldPolygon = self:getFieldPolygon()
--------------------------------------------------------------
--- Validate start distance to field, if started with the hud
--------------------------------------------------------------
if isValid and self.isDirectStart and fieldPolygon then
--- Checks the distance for starting with the hud, as a safety check.
--- Firstly check, if the vehicle is near the field.
local x, _, z = getWorldTranslation(vehicle.rootNode)
isValid = CpMathUtil.isPointInPolygon(fieldPolygon, x, z) or
CpMathUtil.getClosestDistanceToPolygonEdge(fieldPolygon, x, z) < self.minStartDistanceToField
if not isValid then
return false, g_i18n:getText("CP_error_vehicle_too_far_away_from_field")
end
end

isValid, errorMessage = self:detectFieldBoundary(isValid, errorMessage)

return isValid, errorMessage
end

function CpAIJobBaleFinder:validateFieldPosition(isValid, errorMessage)
local tx, tz = self.cpJobParameters.fieldPosition:getPosition()
if tx == nil or tz == nil then
return false, g_i18n:getText("CP_error_not_on_field")
end
local fieldPolygon, _ = CpFieldUtil.getFieldPolygonAtWorldPosition(tx, tz)
self:setFieldPolygon(fieldPolygon)
if fieldPolygon then
self.selectedFieldPlot:setWaypoints(fieldPolygon)
self.selectedFieldPlot:setVisible(true)
else
return false, g_i18n:getText("CP_error_not_on_field")
end
return isValid, errorMessage
end

function CpAIJobBaleFinder:draw(map, isOverviewMap)
CpAIJob.draw(self, map, isOverviewMap)
if not isOverviewMap then
Expand Down
43 changes: 9 additions & 34 deletions scripts/ai/jobs/CpAIJobFieldWork.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ CpAIJobFieldWork.jobName = "CP_job_fieldWork"
CpAIJobFieldWork.GenerateButton = "FIELDWORK_BUTTON"
function CpAIJobFieldWork:init(isServer)
CpAIJob.init(self, isServer)
self.logger = Logger('CpAIJobFieldWork', nil, CpDebug.DBG_FIELDWORK)
self.hasValidPosition = false
self.foundVines = nil
self.selectedFieldPlot = FieldPlot(true)
self.selectedFieldPlot:setVisible(false)
self.selectedFieldPlot:setBrightColor(true)
self.courseGeneratorInterface = CourseGeneratorInterface()
end

Expand Down Expand Up @@ -77,7 +76,6 @@ end
---@param isDirectStart boolean disables the drive to by giants
---@param resetToVehiclePosition boolean resets the drive to target position by giants and the field position to the vehicle position.
function CpAIJobFieldWork:applyCurrentState(vehicle, mission, farmId, isDirectStart, resetToVehiclePosition)
print('********************** Apply current state **********************')
CpAIJob.applyCurrentState(self, vehicle, mission, farmId, isDirectStart)
if resetToVehiclePosition then
-- set the start and the field position to the vehicle's position (
Expand All @@ -97,39 +95,16 @@ function CpAIJobFieldWork:applyCurrentState(vehicle, mission, farmId, isDirectSt
self.cpJobParameters.fieldPosition:setPosition(x, z)
end
end
end

--- Checks the field position setting.
function CpAIJobFieldWork:validateFieldSetup()
print('********************** Validate field setup **********************')
local vehicle = self.vehicleParameter:getVehicle()

-- everything else is valid, now find the field
local tx, tz = self.cpJobParameters.fieldPosition:getPosition()
if tx == nil or tz == nil then
return false, g_i18n:getText("CP_error_not_on_field")
end
if vehicle:cpIsFieldBoundaryDetectionRunning() then
return false, g_i18n:getText("CP_error_field_detection_still_running")
end
local x, z = vehicle:cpGetFieldPosition()
if x == tx and z == tz then
self.logger:debug(vehicle, 'Field position still at %.1f/%.1f, do not detect field boundary again', tx, tz)
return true, ''
local fieldPolygon = vehicle:cpGetFieldPolygon()
if fieldPolygon then
-- if we already have a field polygon, show it
self.selectedFieldPlot:setWaypoints(fieldPolygon)
self.selectedFieldPlot:setVisible(true)
end
self.logger:debug(vehicle, 'Field position changed to %.1f/%.1f, start field boundary detection', tx, tz)
self.hasValidPosition = false
self.foundVines = nil

vehicle:cpDetectFieldBoundary(tx, tz, self, CpAIJobFieldWork.onFieldBoundaryDetectionFinished)
-- TODO: return false and nothing, as the detection is still running?
end

function CpAIJobFieldWork:onFieldBoundaryDetectionFinished(vehicle, fieldPolygon, islandPolygons)

self:setFieldPolygon(fieldPolygon)
if fieldPolygon then
self.hasValidPosition = true
local x, z = vehicle:cpGetFieldPosition()
self.foundVines = g_vineScanner:findVineNodesInField(fieldPolygon, x, z, self.customField ~= nil)
if self.foundVines then
Expand All @@ -138,7 +113,6 @@ function CpAIJobFieldWork:onFieldBoundaryDetectionFinished(vehicle, fieldPolygon
end
self.selectedFieldPlot:setWaypoints(fieldPolygon)
self.selectedFieldPlot:setVisible(true)
self.selectedFieldPlot:setBrightColor(true)
else
self.selectedFieldPlot:setVisible(false)
-- TODO: here we need to tell somehow the frame about the detection success/failure
Expand Down Expand Up @@ -167,7 +141,7 @@ function CpAIJobFieldWork:validate(farmId)

--- Only check the valid field position in the in game menu.
if not self.isDirectStart then
isValid, errorMessage = self:validateFieldSetup()
isValid, errorMessage = self:detectFieldBoundary()
if not isValid then
return isValid, errorMessage
end
Expand All @@ -190,7 +164,8 @@ function CpAIJobFieldWork:draw(map, isOverviewMap)
end

function CpAIJobFieldWork:getCanGenerateFieldWorkCourse()
return self.hasValidPosition
local vehicle = self:getVehicle()
return vehicle and vehicle:cpGetFieldPolygon() ~= nil and not vehicle:cpIsFieldBoundaryDetectionRunning()
end

-- To pass an alignment course from the drive to fieldwork start to the fieldwork, so the
Expand Down
8 changes: 8 additions & 0 deletions scripts/ai/strategies/AIDriveStrategyCombineCourse.lua
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ function AIDriveStrategyCombineCourse:getStateAsString()
return s
end

--- Combine needs a field polygon for the self-unload to work. Although it is a user setting, but it can be
--- changed during the work, so we always require the field polygon, regardless of the setting, as it is checked
--- only after starting the CP driver.
---@return boolean true if the strategy needs the field polygon to work
function AIDriveStrategyCombineCourse:needsFieldPolygon()
return true
end

-----------------------------------------------------------------------------------------------------------------------
--- Initialization
-----------------------------------------------------------------------------------------------------------------------
Expand Down
31 changes: 30 additions & 1 deletion scripts/ai/strategies/AIDriveStrategyCourse.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ AIDriveStrategyCourse = CpObject()
AIDriveStrategyCourse.myStates = {
INITIAL = {},
WAITING_FOR_PATHFINDER = {},
WAITING_FOR_FIELD_BOUNDARY_DETECTION = {},
}

--- Implement controller events.
Expand Down Expand Up @@ -703,4 +704,32 @@ function AIDriveStrategyCourse:updateInfoTexts()
self:clearInfoText(infoText)
end
end
end
end

------------------------------------------------------------------------------------------------------------------------
--- Field boundary detection
---------------------------------------------------------------------------------------------------------------------------
--- Some strategies need to know the field boundaries. Bale finder must search for bales on the field, combine
--- unload will look for harvesters on the field, self-unload will look for trailers around the field. When
--- these strategies are started directly from the HUD or by a shortcut, they won't necessarily have a the
--- field boundary yet, as detection is an asynchronous process. Once the detection is done, the field polygon is
--- available in the CpCourseGenerator specialization by calling cpGetFieldPolygon().
---
--- Strategies that need the boundary should set the state WAITING_FOR_FIELD_BOUNDARY_DETECTION and call this on
--- until it returns true and only then transition to the INITIAL state.
---
---@return boolean true if the field boundary is already available
function AIDriveStrategyCourse:haveFieldPolygon()
if self.fieldPolygon == nil then
if self.vehicle:cpGetFieldPolygon() then
self:clearInfoText(InfoTextManager.WAITING_FOR_FIELD_BOUNDARY_DETECTION)
self.fieldPolygon = self.vehicle:cpGetFieldPolygon()
return true
else
self:setInfoText(InfoTextManager.WAITING_FOR_FIELD_BOUNDARY_DETECTION)
return false
end
else
return true
end
end
Loading

0 comments on commit 4b7fd0d

Please sign in to comment.