diff --git a/scripts/ai/controllers/BaleLoaderController.lua b/scripts/ai/controllers/BaleLoaderController.lua index 2d8bd996e..6126027a3 100644 --- a/scripts/ai/controllers/BaleLoaderController.lua +++ b/scripts/ai/controllers/BaleLoaderController.lua @@ -124,4 +124,8 @@ end --- This one is not used for the giants baleloaders function BaleLoaderController:isReadyToLoadNextBale() return false +end + +function BaleLoaderController:onPreFinished() + return self:canBeFolded() end \ No newline at end of file diff --git a/scripts/ai/controllers/BalerController.lua b/scripts/ai/controllers/BalerController.lua index 6fad87170..fdcf64243 100644 --- a/scripts/ai/controllers/BalerController.lua +++ b/scripts/ai/controllers/BalerController.lua @@ -26,6 +26,7 @@ function BalerController:init(vehicle, baler) self.slowDownStartSpeed = 20 self.balerSpec = self.baler.spec_baler self.baleWrapperSpec = self.baler.spec_baleWrapper + self.baleLoaderSpec = self.baler.spec_baleLoader self.lastDroppedBale = CpTemporaryObject() self:debug('Baler controller initialized') local additives = self.balerSpec.additives @@ -124,11 +125,20 @@ function BalerController:onStart() end end -function BalerController:onFinished(hasFinished) - -- TODO: not working, as this probably needs to be called, before the drive is released. - -- if hasFinished and not self.balerSpec.automaticDrop or not self.balerSpec.platformAutomaticDrop then - -- Baler.actionEventUnloading(self.implement) - -- end +function BalerController:onPreFinished(hasFinished) + if hasFinished then + Baler.actionEventUnloading(self.baler) + if self.balerSpec.platformDropInProgress then + return + end + if self.balerSpec.isBaleUnloading then + return + end + if self.balerSpec.unloadingState ~= Baler.UNLOADING_CLOSED then + return + end + end + return true end function BalerController:isThisMyBale(baleObject) diff --git a/scripts/ai/controllers/ImplementController.lua b/scripts/ai/controllers/ImplementController.lua index c49119bad..2f55a1a27 100644 --- a/scripts/ai/controllers/ImplementController.lua +++ b/scripts/ai/controllers/ImplementController.lua @@ -93,6 +93,10 @@ function ImplementController:onFinished(hasFinished) --- override end +function ImplementController:onPreFinished() + return true +end + function ImplementController:onFinishRow(isHeadlandTurn) end diff --git a/scripts/ai/jobs/CpAIJob.lua b/scripts/ai/jobs/CpAIJob.lua index 72fa0eca9..75baf25c6 100644 --- a/scripts/ai/jobs/CpAIJob.lua +++ b/scripts/ai/jobs/CpAIJob.lua @@ -58,14 +58,6 @@ function CpAIJob:setupCpJobParameters(jobParameters) self.cpJobParameters:validateSettings() end ---- Is the ai job allowed to finish ? ---- This entry point allowes us to catch giants stop conditions. ----@param message table Stop reason can be used to reverse engineer the cause. ----@return boolean -function CpAIJob:isFinishingAllowed(message) - return true -end - --- Gets the first task to start with. function CpAIJob:getStartTaskIndex() if self.currentTaskIndex ~= 0 or self.isDirectStart or self:isTargetReached() then @@ -137,12 +129,8 @@ function CpAIJob:stop(aiMessage) vehicle:deleteAgent() vehicle:aiJobFinished() vehicle:resetCpAllActiveInfoTexts() - local driveStrategy = vehicle:getCpDriveStrategy() if not aiMessage then self:debug("No valid ai message given!") - if driveStrategy then - driveStrategy:onFinished() - end AIJob.stop(self, aiMessage) return end @@ -160,9 +148,6 @@ function CpAIJob:stop(aiMessage) if event then SpecializationUtil.raiseEvent(vehicle, event) end - if driveStrategy then - driveStrategy:onFinished(hasFinished) - end g_messageCenter:unsubscribeAll(self) end diff --git a/scripts/ai/jobs/CpAIJobFieldWork.lua b/scripts/ai/jobs/CpAIJobFieldWork.lua index 91c1e86c7..778f4ae3c 100644 --- a/scripts/ai/jobs/CpAIJobFieldWork.lua +++ b/scripts/ai/jobs/CpAIJobFieldWork.lua @@ -47,29 +47,6 @@ function CpAIJobFieldWork:setupJobParameters() self:setupCpJobParameters(CpFieldWorkJobParameters(self)) end -function CpAIJobFieldWork:isFinishingAllowed(message) - local nextTaskIndex = self:getNextTaskIndex() - if message:isa(AIMessageErrorOutOfFill) then - --- At least one implement type needs to be refilled. - - local vehicle = self:getVehicle() - local setting = vehicle:getCpSettings().refillOnTheField - - if setting:getValue() == CpVehicleSettings.REFILL_ON_FIELD_DISABLED then - return true - elseif setting:getValue() == CpVehicleSettings.REFILL_ON_FIELD_WAITING then - if self.currentTaskIndex == self.fieldWorkTask.taskIndex then - self.fieldWorkTask:setWaitingForRefillingActive() - end - elseif setting:getValue() == CpVehicleSettings.REFILL_ON_FIELD_ACTIVE then - --- TODO_25 Add driving to trailer for refilling here and so on .. - self.fieldWorkTask:skip() - end - return false - end - return CpAIJob.isFinishingAllowed(self, message) -end - ---@param vehicle table ---@param mission table ---@param farmId number diff --git a/scripts/ai/strategies/AIDriveStrategyCourse.lua b/scripts/ai/strategies/AIDriveStrategyCourse.lua index c32119a14..6528ca5ed 100644 --- a/scripts/ai/strategies/AIDriveStrategyCourse.lua +++ b/scripts/ai/strategies/AIDriveStrategyCourse.lua @@ -26,12 +26,16 @@ AIDriveStrategyCourse.myStates = { INITIAL = {}, WAITING_FOR_PATHFINDER = {}, WAITING_FOR_FIELD_BOUNDARY_DETECTION = {}, + PRE_FINISHED = {}, + WAITING_FOR_FINISHED = {}, + FINISHED = {} } --- Implement controller events. --- TODO_25 a more generic implementation AIDriveStrategyCourse.onRaisingEvent = "onRaising" AIDriveStrategyCourse.onLoweringEvent = "onLowering" +AIDriveStrategyCourse.onPreFinishedEvent = "onPreFinished" AIDriveStrategyCourse.onFinishedEvent = "onFinished" AIDriveStrategyCourse.onStartEvent = "onStart" AIDriveStrategyCourse.onStartRefillingEvent = "onStartRefilling" @@ -59,6 +63,11 @@ function AIDriveStrategyCourse:init(task, job) self.currentTask = task self.job = job + self.stopRequestData = { + reason = nil, + waitForFolding = false, + prepareTimeout = CpTemporaryObject(true) + } end function AIDriveStrategyCourse:setCurrentTaskFinished() @@ -279,6 +288,10 @@ end --- Called in the low frequency function for the helper. function AIDriveStrategyCourse:updateLowFrequencyImplementControllers() + if self:hasFinished() then + --- Small hack so every ai drive strategy waits during finished. + self:setMaxSpeed(0) + end for _, controller in pairs(self.controllers) do ---@type ImplementController if controller:isEnabled() then @@ -435,8 +448,57 @@ function AIDriveStrategyCourse:update(dt) self.pathfinderController:update(dt) self:updatePathfinding() self:updateInfoTexts() + self:updateFinishing() +end + +function AIDriveStrategyCourse:updateFinishing() + local function finishStrategy() + if self.stopRequestData.stopReason then + g_currentMission.aiSystem:stopJob(self.job, self.stopRequestData.stopReason) + return + end + self.currentTask:skip() + end + if self.state == self.states.PRE_FINISHED then + --- Every implement controller gets the chance to + --- prepare for the driver release. + --- For example balers can unload their bales + --- before we can fold them and so on. + local finished = true + for _, controller in ipairs(self.controllers) do + finished = finished and controller:onPreFinished() + end + if finished then + self:debug("Precondition for stopping the strategy are reached.") + self.state = self.states.FINISHED + end + elseif self.state == self.states.FINISHED then + self:raiseControllerEvent(self.onFinishedEvent, self.stopRequestData.waitForFolding) + if self.stopRequestData.waitForFolding then + self:debug("Starting to fold implements and so on...") + self.vehicle:prepareForAIDriving() + self.stopRequestData.prepareTimeout:set(false, 15000) + self.state = self.states.WAITING_FOR_FINISHED + else + finishStrategy() + end + elseif self.state == self.states.WAITING_FOR_FINISHED then + if not self.vehicle:getIsAIPreparingToDrive() or self.stopRequestData.prepareTimeout:get() then + if self.stopRequestData.prepareTimeout:get() then + self:debug("Failed to prepare ai drive, aborting ..") + end + finishStrategy() + end + end end +--- Job has finished and we are now waiting to release the driver. +---@return boolean +function AIDriveStrategyCourse:hasFinished() + return self.state == self.states.PRE_FINISHED or + self.state == self.states.WAITING_FOR_FINISHED or + self.state == self.states.FINISHED +end function AIDriveStrategyCourse:getDriveData(dt, vX, vY, vZ) local moveForwards = not self.ppc:isReversing() @@ -664,15 +726,35 @@ function AIDriveStrategyCourse:isCloseToCourseStart(distance) return self.course:getDistanceFromFirstWaypoint(self.ppc:getCurrentWaypointIx()) < distance end +--- Possiblity to override the vehicle:stopCurrentAIJob(), +--- if for example refilling on the field is active. +function AIDriveStrategyCourse:handleFinishedRequest(stopReason) + --- override + return false +end + --- Event raised when the driver was stopped. ---@param hasFinished boolean|nil flag passed by the info text -function AIDriveStrategyCourse:onFinished(hasFinished) - self:raiseControllerEvent(self.onFinishedEvent, hasFinished) - if hasFinished and self.settings.foldImplementAtEnd:getValue() then - --- Folds implements at the end if the setting is active. - self:debug("Finished with folding implements of the implements.") - self.vehicle:prepareForAIDriving() +---@param stopReason table +function AIDriveStrategyCourse:onFinished(hasFinished, stopReason) + if self:handleFinishedRequest(stopReason) then + --- Stop request ignored + return + end + if self:hasFinished() then + --- Driver is already stopping and still waiting for folding and so on ... + return end + self:setFinished(hasFinished and self.settings.foldImplementAtEnd:getValue(), stopReason) +end + +--- Internal stop request +---@param waitForFolding boolean|nil wait until for the folding and so on .. +---@param stopReason table|nil if nil is given, then the task will be skipped +function AIDriveStrategyCourse:setFinished(waitForFolding, stopReason) + self.stopRequestData.stopReason = stopReason + self.stopRequestData.waitForFolding = waitForFolding + self.state = self.states.PRE_FINISHED end --- This is to set the offsets on the course at start, or update those values diff --git a/scripts/ai/strategies/AIDriveStrategyFieldWorkCourse.lua b/scripts/ai/strategies/AIDriveStrategyFieldWorkCourse.lua index 8e2e2501b..9cd2bf2ad 100644 --- a/scripts/ai/strategies/AIDriveStrategyFieldWorkCourse.lua +++ b/scripts/ai/strategies/AIDriveStrategyFieldWorkCourse.lua @@ -108,11 +108,28 @@ function AIDriveStrategyFieldWorkCourse:prepareForFieldWork() end --- Event raised when the driver has finished. -function AIDriveStrategyFieldWorkCourse:onFinished(hasFinished) - AIDriveStrategyCourse.onFinished(self, hasFinished) +function AIDriveStrategyFieldWorkCourse:onFinished(...) + AIDriveStrategyCourse.onFinished(self, ...) self.remainingTime:reset() end +function AIDriveStrategyFieldWorkCourse:handleFinishedRequest(stopReason) + --- TODO consolidate this logic in the future ... + if stopReason:isa(AIMessageErrorOutOfFill) then + --- At least one implement type needs to be refilled. + local setting = self.settings.refillOnTheField + if setting:getValue() == CpVehicleSettings.REFILL_ON_FIELD_WAITING then + self.currentTask:setWaitingForRefillingActive() + return true + elseif setting:getValue() == CpVehicleSettings.REFILL_ON_FIELD_ACTIVE then + --- TODO_25 Add driving to trailer for refilling here and so on .. + self:setFinished(true, nil) + return true + end + end + return false +end + function AIDriveStrategyFieldWorkCourse:update(dt) AIDriveStrategyCourse.update(self, dt) if CpDebug:isChannelActive(CpDebug.DBG_TURN, self.vehicle) then diff --git a/scripts/ai/strategies/AIDriveStrategyFindBales.lua b/scripts/ai/strategies/AIDriveStrategyFindBales.lua index f694e43f0..267f2872b 100644 --- a/scripts/ai/strategies/AIDriveStrategyFindBales.lua +++ b/scripts/ai/strategies/AIDriveStrategyFindBales.lua @@ -31,8 +31,7 @@ AIDriveStrategyFindBales.myStates = { WORKING_ON_BALE = {}, REVERSING_AFTER_PATHFINDER_FAILURE = {}, REVERSING_DUE_TO_OBSTACLE_AHEAD = {}, - DRIVING_TO_START_MARKER = {}, - WAITING_FOR_IMPLEMENTS_TO_FOLD = {} + DRIVING_TO_START_MARKER = {} } --- Offset to apply at the goal marker, so we don't crash with an empty unloader waiting there with the same position. AIDriveStrategyFindBales.invertedGoalPositionOffset = -4.5 @@ -85,7 +84,7 @@ function AIDriveStrategyFindBales:collectNextBale() self:findPathToNextBale() return end - self.state = self.states.WAITING_FOR_IMPLEMENTS_TO_FOLD + self.state = self.states.DRIVING_TO_START_MARKER end end @@ -314,29 +313,6 @@ function AIDriveStrategyFindBales:getBaleTarget(bale) return State3D(xb, -zb, CpMathUtil.angleFromGame(yRot)) end ---- Sets the driver as finished, so either a path ---- to the start marker as a park position can be used ---- or the driver stops directly. -function AIDriveStrategyFindBales:setFinished() - if not self:isReadyToFoldImplements() then - -- Waiting until the folding has finished.. - self:debugSparse("Waiting until an animation has finish, so the driver can be released ..") - return - end - self.vehicle:prepareForAIDriving() - if not self.vehicle:getIsAIReadyToDrive() then - -- Waiting until the folding has finished.. - self:debugSparse("Waiting until an animation has finish, so the driver can be released ..") - return - end - if self.invertedStartPositionMarkerNode then - self:debug("A valid start position is found, so the driver tries to finish at the inverted goal node") - self:startPathfindingToStartMarker() - else - self:finishJob() - end -end - --- Finishes the job with the correct stop reason, as --- the correct reason is needed for a possible AD takeover. function AIDriveStrategyFindBales:finishJob() @@ -566,9 +542,6 @@ function AIDriveStrategyFindBales:getDriveData(dt, vX, vY, vZ) self:setMaxSpeed(self.settings.reverseSpeed:getValue()) elseif self.state == self.states.DRIVING_TO_START_MARKER then self:setMaxSpeed(self.settings.fieldSpeed:getValue()) - elseif self.state == self.states.WAITING_FOR_IMPLEMENTS_TO_FOLD then - self:setFinished() - self:setMaxSpeed(0) --- folding end local moveForwards = not self.ppc:isReversing() @@ -685,10 +658,14 @@ function AIDriveStrategyFindBales:update(dt) self.ppc:getCourse():draw() end end - if self.state ~= self.states.DRIVING_TO_START_MARKER and - self.state ~= self.states.WAITING_FOR_IMPLEMENTS_TO_FOLD then + if self.state ~= self.states.DRIVING_TO_START_MARKER then if self:areBaleLoadersFull() then - self.state = self.states.WAITING_FOR_IMPLEMENTS_TO_FOLD + if self.invertedStartPositionMarkerNode then + self:debug("A valid start position is found, so the driver tries to finish at the inverted goal node") + self:startPathfindingToStartMarker() + else + self:finishJob() + end end end --- Ignores the loaded auto loader bales. diff --git a/scripts/ai/strategies/AIDriveStrategySiloLoader.lua b/scripts/ai/strategies/AIDriveStrategySiloLoader.lua index 2c831ffb6..57fed5cef 100644 --- a/scripts/ai/strategies/AIDriveStrategySiloLoader.lua +++ b/scripts/ai/strategies/AIDriveStrategySiloLoader.lua @@ -30,7 +30,7 @@ AIDriveStrategySiloLoader.myStates = { DRIVING_ALIGNMENT_COURSE = {}, WAITING_FOR_PREPARING = {}, WORKING = {}, - FINISHED = {} + FINISHED_WORKING = {} } AIDriveStrategySiloLoader.distanceOverFieldEdgeAllowed = 25 AIDriveStrategySiloLoader.siloAreaOffsetFieldUnload = 10 @@ -201,7 +201,7 @@ function AIDriveStrategySiloLoader:onWaypointPassed(ix, course) self.vehicle:raiseAIEvent("onAIFieldWorkerStart", "onAIImplementStart") self:lowerImplements() elseif self.state == self.states.WORKING then - self.state = self.states.FINISHED + self.state = self.states.FINISHED_WORKING end end end @@ -243,11 +243,11 @@ function AIDriveStrategySiloLoader:getDriveData(dt, vX, vY, vZ) local isEndReached, maxSpeed = self.bunkerSiloController:isEndReached(self.shovelController:getShovelNode(), 0) if self.silo:isTheSameSilo(closestObject) or isEndReached then self:debug("End wall detected or bunker silo end is reached.") - self.state = self.states.FINISHED + self.state = self.states.FINISHED_WORKING end end - elseif self.state == self.states.FINISHED then + elseif self.state == self.states.FINISHED_WORKING then self:setMaxSpeed(0) self:debugSparse("Waiting until the conveyor is empty.") if self.shovelController:isEmpty() then diff --git a/scripts/specializations/CpAIWorker.lua b/scripts/specializations/CpAIWorker.lua index c4bc4313d..8ad6c13e7 100644 --- a/scripts/specializations/CpAIWorker.lua +++ b/scripts/specializations/CpAIWorker.lua @@ -338,34 +338,34 @@ function CpAIWorker:stopCurrentAIJob(superFunc, message, ...) return superFunc(self, message, ...) end - local job = self:getJob() local wasCpActive = self:getIsCpActive() - if wasCpActive then - local driveStrategy = self:getCpDriveStrategy() - if driveStrategy then - -- TODO: this isn't needed if we do not return a 0 < maxSpeed < 0.5, should either be exactly 0 or greater than 0.5 - local maxSpeed = driveStrategy and driveStrategy:getMaxSpeed() - if message:isa(AIMessageErrorBlockedByObject) then - if self.spec_aiFieldWorker.didNotMoveTimer and self.spec_aiFieldWorker.didNotMoveTimer < 0 then - if maxSpeed and maxSpeed < 1 then - -- disable the Giants timeout which dismisses the AI worker if it does not move for 5 seconds - -- since we often stop for instance in convoy mode when waiting for another vehicle to turn - -- (when we do this, we set our maxSpeed to 0). So we also check our maxSpeed, this way the Giants timer will - -- fire if we are blocked (thus have a maxSpeed > 0 but not moving) - CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, 'Overriding the Giants did not move timer, with speed: %.2f', maxSpeed) - return - else - CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, 'Giants did not move timer triggered, with speed: %.2f!', maxSpeed) - end - end + if not wasCpActive then + return superFunc(self, message, ...) + end + ---@type AIDriveStrategyCourse + local driveStrategy = self:getCpDriveStrategy() + if not driveStrategy then + return superFunc(self, message, ...) + end + -- TODO: this isn't needed if we do not return a 0 < maxSpeed < 0.5, should either be exactly 0 or greater than 0.5 + local maxSpeed = driveStrategy and driveStrategy:getMaxSpeed() + if message:isa(AIMessageErrorBlockedByObject) then + if self.spec_aiFieldWorker.didNotMoveTimer and self.spec_aiFieldWorker.didNotMoveTimer < 0 then + if maxSpeed and maxSpeed < 1 then + -- disable the Giants timeout which dismisses the AI worker if it does not move for 5 seconds + -- since we often stop for instance in convoy mode when waiting for another vehicle to turn + -- (when we do this, we set our maxSpeed to 0). So we also check our maxSpeed, this way the Giants timer will + -- fire if we are blocked (thus have a maxSpeed > 0 but not moving) + CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, 'Overriding the Giants did not move timer, with speed: %.2f', maxSpeed) + return + else + CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, 'Giants did not move timer triggered, with speed: %.2f!', maxSpeed) end end - if not job:isFinishingAllowed(message) then - return - end end - CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, "stop message: %s", message:getI18NText()) - superFunc(self, message,...) + local releaseMessage, hasFinished, event, isOnlyShownOnPlayerStart = + g_infoTextManager:getInfoTextDataByAIMessage(message) + driveStrategy:onFinished(hasFinished, message) end --- Sets a stop flag to make the driver stop after the worker finished.