From 61b09d3c71dc91f5ddee73dbb7634e1484d3f3fd Mon Sep 17 00:00:00 2001 From: schwiti6190 <58079399+schwiti6190@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:01:46 +0200 Subject: [PATCH 1/7] WIP Pathfinder controller to unify the pathfinder handling --- scripts/ai/AIDriveStrategyCourse.lua | 6 +- scripts/ai/PathfinderController.lua | 364 +++++++++++++++++++++++++++ 2 files changed, 369 insertions(+), 1 deletion(-) create mode 100644 scripts/ai/PathfinderController.lua diff --git a/scripts/ai/AIDriveStrategyCourse.lua b/scripts/ai/AIDriveStrategyCourse.lua index f53cdbd49..6b467c1ec 100644 --- a/scripts/ai/AIDriveStrategyCourse.lua +++ b/scripts/ai/AIDriveStrategyCourse.lua @@ -111,6 +111,9 @@ function AIDriveStrategyCourse:setAIVehicle(vehicle, jobParameters) self:initializeImplementControllers(vehicle) self.ppc = PurePursuitController(vehicle) self.ppc:registerListeners(self, 'onWaypointPassed', 'onWaypointChange') + + self.pathfinderController = PathfinderController(vehicle) + self.storage = vehicle.spec_cpAIWorker self.settings = vehicle:getCpSettings() @@ -428,8 +431,9 @@ function AIDriveStrategyCourse:getCurrentCourse() return self.ppc:getCourse() or self.course end -function AIDriveStrategyCourse:update() +function AIDriveStrategyCourse:update(dt) self.ppc:update() + self.pathfinderController:update(dt) self:updatePathfinding() self:updateInfoTexts() end diff --git a/scripts/ai/PathfinderController.lua b/scripts/ai/PathfinderController.lua new file mode 100644 index 000000000..a85bed4e5 --- /dev/null +++ b/scripts/ai/PathfinderController.lua @@ -0,0 +1,364 @@ +--[[ +This file is part of Courseplay (https://github.com/Courseplay/Courseplay_FS22) +Copyright (C) 2023 Courseplay Dev Team +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +You should have received a copy of the GNU General Public License +along with this program. If not, see . +]] + +--[[ +PathfinderController for easy access to the pathfinder. +- Enables retrying with adjustable parameters compared to the last try, like fruit allowed and so.. +- Handles the pathfinder coroutines if needed. +- One callback when the path finding finished. + - Triggered if a valid path was found. + - Gets triggered if the goal node is invalid. + - Also gets triggered if no valid path was found and all retry attempts are used. +- Every time the path finding failed a callback gets triggered, if there are retry attempts left over. + - Enabled the changing of the pathfinder context and restart with the new context. + +PathfinderControllerContext implements all pathfinder parameters and the number of retries allowed. +- Other Context Classes could be derived for commonly used contexts. + + + +Example implementations: + +function Strategy:startPathfindingToGoal() + local numRetries = 2 + local context = PathfinderControllerContext(self.vehicle, numRetries) + context:set( + ... + ) + self.pathfinderController:setCallbacks(self, self.onPathfingFinished, self.onPathfindingFailed) + + self.pathfinderController:findPathToNode(context, ...) + +end + +function Strategy:onPathfingFinished(controller : PathfinderController, success : boolean, + path : table, goalNodeInvalid : boolean|nil) + if success then + // Path finding finished successfully + local course = self.pathfinderController:getTemporaryCourseFromPath(path) + ... + else + if goalNodeInvalid then + // Goal position can't be reached! + else + // Num retries reached without path! + end + end +end + +function Strategy:onPathfindingFailed(controller : PathfinderController, lastContext : PathfinderControllerContext, + wasLastRetry : boolean, currentRetryAttempt : number) + if currentRetryAttempt == 1 then + // Reduced fruit impact: + lastContext:ignoreFruit() + self.pathfinderController:findPathToNode(lastContext, ...) + else + // Something else ... + self.pathfinderController:findPathToNode(lastContext, ...) + end +end + +]] + +---@class PathfinderControllerContext +PathfinderControllerContext = CpObject() +PathfinderControllerContext.defaultNumRetries = 0 +function PathfinderControllerContext:init(vehicle, numRetries) + self.vehicle = vehicle + self.numRetries = numRetries or self.defaultNumRetries + --- Percentage of fruit allowed, math.hug means no fruit avoidance + self.maxFruitPercent = nil + --- Penalty sticking on the field and avoiding outside of the field + self.offFieldPenalty = nil + --- Should none owned field be avoid? + self.useFieldNum = false + --- A given area that has to be avoided. + self.areaToAvoid = nil + --- Is reverse driving allowed? + self.allowReverse = false + --- Vehicle Collisions that are ignored. + self.vehiclesToIgnore = nil + --- Is a accurate pathfinder goal position needed? + self.mustBeAccurate = false + self.areaToIgnoreFruit = nil +end + +--- Sets the Pathfinder context +---@param mustBeAccurate boolean|nil +---@param allowReverse boolean|nil +---@param maxFruitPercent number|nil +---@param offFieldPenalty number|nil +---@param useFieldNum boolean|nil +---@param areaToAvoid table|nil +---@param vehiclesToIgnore table|nil +---@param areaToIgnoreFruit table|nil +function PathfinderControllerContext:set( + mustBeAccurate, allowReverse, + maxFruitPercent, offFieldPenalty, useFieldNum, + areaToAvoid, vehiclesToIgnore, areaToIgnoreFruit) + self.maxFruitPercent = maxFruitPercent + self.offFieldPenalty = offFieldPenalty + self.useFieldNum = useFieldNum + self.areaToAvoid = areaToAvoid + self.allowReverse = allowReverse + self.vehiclesToIgnore = vehiclesToIgnore + self.mustBeAccurate = mustBeAccurate + self.areaToIgnoreFruit = areaToIgnoreFruit +end + +--- Disables the fruit avoidance +function PathfinderControllerContext:ignoreFruit() + self.maxFruitPercent = math.huge +end + +function PathfinderControllerContext:getNumRetriesAllowed() + return self.numRetries +end + +function PathfinderControllerContext:__tostring() + local str = "PathfinderControllerContext(vehicle name=%s, maxFruitPercent=%s, "+ + "offFieldPenalty=%s, useFieldNum=%s, areaToAvoid=%s, allowReverse=%s, "+ + "vehiclesToIgnore=%s, mustBeAccurate=%s, areaToIgnoreFruit=%s)" + return string.format(str, + CpUtil.getName(self.vehicle), + tostring(self.maxFruitPercent), + tostring(self.offFieldPenalty), + tostring(self.useFieldNum), + tostring(self.areaToAvoid), + tostring(self.allowReverse), + tostring(self.vehiclesToIgnore), + tostring(self.mustBeAccurate), + tostring(self.areaToIgnoreFruit)) +end + +---@class DefaultFieldPathfinderControllerContext : PathfinderControllerContext +DefaultFieldPathfinderControllerContext = CpObject(PathfinderControllerContext) + +function DefaultFieldPathfinderControllerContext:init(...) + PathfinderControllerContext.init(self, ...) +end + +---@class PathfinderController +PathfinderController= CpObject() + +PathfinderController.SUCCESS_FOUND_VALID_PATH = 0 +PathfinderController.ERROR_NO_PATH_FOUND = 1 +PathfinderController.ERROR_INVALID_GOAL_NODE = 2 +function PathfinderController:init(vehicle) + self.vehicle = vehicle + ---@type PathfinderInterface + self.pathfinder = nil + ---@type PathfinderControllerContext + self.lastContext = nil + self:reset() +end + +function PathfinderController:__tostring() + return string.format("PathfinderController(failCount=%d, numRetries=%s, active=%s)", + self.failCount, self.numRetries, tostring(self.pathfinder ~= nil)) +end + +function PathfinderController:reset() + self.numRetries = 0 + self.failCount = 0 + self.startedAt = 0 + self.timeTakenMs = 0 + self.callbackClass = nil + self.callbackSuccessFunction = nil + self.callbackRetryFunction = nil + self.lastContext = nil +end + +function PathfinderController:update(dt) + if self:isActive() then + --- Applies coroutine for path finding + local done, path, goalNodeInvalid = self.pathfinder:resume() + if done then + self:finished(path, goalNodeInvalid) + end + end +end + +function PathfinderController:isActive() + return self.pathfinder and self.pathfinder:isActive() +end + +---@return PathfinderControllerContext +function PathfinderController:getLastContext() + return self.lastContext +end + +--- Sets the callbacks which get called on the pathfinder finish. +---@param class table +---@param successFunc function func(PathfinderController, success, path, goalNodeInvalid) +---@param retryFunc function func(PathfinderController, last context, was last retry, retry attempt number) +function PathfinderController:setCallbacks(class, successFunc, retryFunc) + self.callbackClass = class + self.callbackSuccessFunction = successFunc + self.callbackRetryFunction = retryFunc +end + +--- Pathfinder was started +---@param context PathfinderControllerContext +function PathfinderController:started(context) + self:debug("Started pathfinding with context: %s.", tostring(context)) + self.startedAt = g_time + self.lastContext = context + self.numRetries = context:getNumRetriesAllowed() +end + +function PathfinderController:callCallback(callbackFunc, ...) + if self.callbackClass then + callbackFunc(self.callbackClass, self, ...) + else + callbackFunc(self, ...) + end +end + +--- Path finding has finished +---@param path table|nil +---@param goalNodeInvalid boolean|nil +function PathfinderController:finished(path, goalNodeInvalid) + self.pathfinder = nil + self.timeTakenMs = g_time - self.startedAt + local retValue = self:isValidPath(path, goalNodeInvalid) + if retValue == self.ERROR_NO_PATH_FOUND then + if self.callbackRetryFunction then + --- Retry is allowed, so check if any tries are leftover + if self.failCount < self.numRetries then + self:debug("Failed with try %d of %d.", self.failCount, self.numRetries) + --- Retrying the path finding + self.failCount = self.failCount + 1 + self:callCallback(self.callbackRetryFunction, self.lastContext, self.failCount == self.numRetries, self.failCount) + return + end + end + end + self:callCallback(self.callbackSuccessFunction, retValue == self.SUCCESS_FOUND_VALID_PATH, path, goalNodeInvalid) + self:reset() +end + +--- Is the path found and valid? +---@param path table|nil +---@param goalNodeInvalid boolean|nil +---@return integer +function PathfinderController:isValidPath(path, goalNodeInvalid) + if path and #path > 2 then + self:debug('Found a path (%d waypoints, after %d ms)', #path, self.timeTakenMs) + return self.SUCCESS_FOUND_VALID_PATH + end + if goalNodeInvalid then + self:error('No path found, goal node is invalid') + return self.ERROR_INVALID_GOAL_NODE + end + self:error("No path found after %d ms", self.timeTakenMs) + return self.ERROR_NO_PATH_FOUND +end + +--- Finds a path to given goal node +---@param context PathfinderControllerContext +---@param goalNode number +---@param xOffset number +---@param zOffset number +---@return boolean Was path finding started? +function PathfinderController:findPathToNode(context, + goalNode, xOffset, zOffset) + + if not self.callbackSuccessFunction then + self:error("No valid success callback was given!") + return false + end + self:started(context) + local pathfinder, done, path, goalNodeInvalid = PathfinderUtil.startPathfindingFromVehicleToNode( + context.vehicle, + goalNode, + xOffset, + zOffset, + context.allowReverse, + context.useFieldNum and 1, + context.vehiclesToIgnore, + context.maxFruitPercent, + context.offFieldPenalty, + context.areaToAvoid, + context.mustBeAccurate + ) + if done then + self:finished(path, goalNodeInvalid) + else + self:debug("Continuing as coroutine...") + self.pathfinder = pathfinder + end + return true +end + +--- Finds a path to a waypoint of a course. +---@param context PathfinderControllerContext +---@param course Course +---@param waypointIndex number +---@param xOffset number +---@param zOffset number +---@return boolean Was path finding started? +function PathfinderController:findPathToWaypoint(context, + course, waypointIndex, xOffset, zOffset) + + if not self.callbackSuccessFunction then + self:error("No valid success callback was given!") + return false + end + self:started(context) + local pathfinder, done, path, goalNodeInvalid = PathfinderUtil.startPathfindingFromVehicleToWaypoint( + context.vehicle, + course, + waypointIndex, + xOffset, + zOffset, + context.allowReverse, + context.useFieldNum and CpFieldUtil.getFieldNumUnderVehicle(context.vehicle), + context.vehiclesToIgnore, + context.maxFruitPercent, + context.offFieldPenalty, + context.areaToAvoid, + context.areaToIgnoreFruit) + if done then + self:finished(path, goalNodeInvalid) + else + self:debug("Continuing as coroutine...") + self.pathfinder = pathfinder + end +end + +function PathfinderController:getTemporaryCourseFromPath(path) + return Course(self.vehicle, CourseGenerator.pointsToXzInPlace(path), true) +end + +-------------------------------------------- +--- Debug functions +-------------------------------------------- + +function PathfinderController:debugStr(str, ...) + return "Pathfinder controller: " .. str, ... +end + +function PathfinderController:debug(...) + CpUtil.debugVehicle(CpDebug.DBG_PATHFINDER, self.vehicle, self:debugStr(...)) +end + +function PathfinderController:info(...) + CpUtil.infoVehicle(self.vehicle, self:debugStr(...)) +end + +function PathfinderController:error(...) + self:info(...) +end \ No newline at end of file From f3d7745f15230649cfd6dd09e442b60a7c58bb7c Mon Sep 17 00:00:00 2001 From: schwiti6190 <58079399+schwiti6190@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:43:42 +0200 Subject: [PATCH 2/7] PR adjustments --- modDesc.xml | 1 + scripts/CpObject.lua | 31 ++++ scripts/ai/PathfinderController.lua | 212 +++++++++++++------------- scripts/pathfinder/PathfinderUtil.lua | 4 +- 4 files changed, 140 insertions(+), 108 deletions(-) diff --git a/modDesc.xml b/modDesc.xml index 73f81840d..f8510e82c 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -276,6 +276,7 @@ Changelog 7.1.0.0: + diff --git a/scripts/CpObject.lua b/scripts/CpObject.lua index 64cda0a00..fab7e9dde 100644 --- a/scripts/CpObject.lua +++ b/scripts/CpObject.lua @@ -59,10 +59,41 @@ function CpObject(base, init) end return false end + c.__tostring = function (self) + -- Default tostring function for printing all attributes and assigned functions. + local str = '[ ' + for attribute, value in pairs(self) do + str = str .. string.format('%s: %s ', attribute, value) + end + str = str .. ']' + return str + end + setmetatable(c, mt) return c end +---@class CpObjectUtil +CpObjectUtil = {} + +--- Registers a builder api for a class. +--- The attributes are set as private variables with "_" before the variable name +--- and the builder functions are named like the attribute. +---@param class table +---@param attributesToDefault table +function CpObjectUtil.registerBuilderAPI(class, attributesToDefault) + for attributeName, default in pairs(attributesToDefault) do + --- Applies the default value to the private variable + class["_" .. attributeName] = default + --- Creates the builder functions/ setters with the public variable name + class[attributeName] = function(self, value) + self["_" .. attributeName] = value + return self + end + end +end + + --- Object that holds a value temporarily. You can tell when to set the value and how long it should keep that --- value, in milliseconds. Great for timers. ---@class CpTemporaryObject diff --git a/scripts/ai/PathfinderController.lua b/scripts/ai/PathfinderController.lua index a85bed4e5..91ff2af39 100644 --- a/scripts/ai/PathfinderController.lua +++ b/scripts/ai/PathfinderController.lua @@ -14,6 +14,11 @@ along with this program. If not, see . ]] --[[ + +-------------------------------------------- +--- Pathfinder controller +-------------------------------------------- + PathfinderController for easy access to the pathfinder. - Enables retrying with adjustable parameters compared to the last try, like fruit allowed and so.. - Handles the pathfinder coroutines if needed. @@ -24,11 +29,6 @@ PathfinderController for easy access to the pathfinder. - Every time the path finding failed a callback gets triggered, if there are retry attempts left over. - Enabled the changing of the pathfinder context and restart with the new context. -PathfinderControllerContext implements all pathfinder parameters and the number of retries allowed. -- Other Context Classes could be derived for commonly used contexts. - - - Example implementations: function Strategy:startPathfindingToGoal() @@ -44,10 +44,9 @@ function Strategy:startPathfindingToGoal() end function Strategy:onPathfingFinished(controller : PathfinderController, success : boolean, - path : table, goalNodeInvalid : boolean|nil) + course : Course, goalNodeInvalid : boolean|nil) if success then // Path finding finished successfully - local course = self.pathfinderController:getTemporaryCourseFromPath(path) ... else if goalNodeInvalid then @@ -70,77 +69,65 @@ function Strategy:onPathfindingFailed(controller : PathfinderController, lastCon end end +-------------------------------------------- +--- Pathfinder controller context +-------------------------------------------- + + +PathfinderControllerContext implements all pathfinder parameters and the number of retries allowed. +- Other Context Classes could be derived for commonly used contexts. +- Only the attributesToDefaultValue table has to be changed in the derived class. + +Example usage of the builder api: + +local context = PathfinderControllerContext():maxFruitPercent(100):useFieldNum(true):vehiclesToIgnore({vehicle}) + + ]] ---@class PathfinderControllerContext +---@field maxFruitPercent function +---@field offFieldPenalty function +---@field useFieldNum function +---@field areaToAvoid function +---@field allowReverse function +---@field vehiclesToIgnore function +---@field mustBeAccurate function +---@field areaToIgnoreFruit function +---@field _maxFruitPercent number +---@field _offFieldPenalty number +---@field _useFieldNum boolean +---@field _areaToAvoid PathfinderUtil.NodeArea +---@field _allowReverse boolean +---@field _vehiclesToIgnore table[] +---@field _mustBeAccurate boolean +---@field _areaToIgnoreFruit table[] PathfinderControllerContext = CpObject() PathfinderControllerContext.defaultNumRetries = 0 -function PathfinderControllerContext:init(vehicle, numRetries) - self.vehicle = vehicle - self.numRetries = numRetries or self.defaultNumRetries - --- Percentage of fruit allowed, math.hug means no fruit avoidance - self.maxFruitPercent = nil - --- Penalty sticking on the field and avoiding outside of the field - self.offFieldPenalty = nil - --- Should none owned field be avoid? - self.useFieldNum = false - --- A given area that has to be avoided. - self.areaToAvoid = nil - --- Is reverse driving allowed? - self.allowReverse = false - --- Vehicle Collisions that are ignored. - self.vehiclesToIgnore = nil - --- Is a accurate pathfinder goal position needed? - self.mustBeAccurate = false - self.areaToIgnoreFruit = nil -end +PathfinderControllerContext.attributesToDefaultValue = { + ["maxFruitPercent"] = 50, + ["offFieldPenalty"] = 50, + ["useFieldNum"] = false, + ["areaToAvoid"] = {}, + ["allowReverse"] = false, + ["vehiclesToIgnore"] = {}, + ["mustBeAccurate"] = false, + ["areaToIgnoreFruit"] = {} +} ---- Sets the Pathfinder context ----@param mustBeAccurate boolean|nil ----@param allowReverse boolean|nil ----@param maxFruitPercent number|nil ----@param offFieldPenalty number|nil ----@param useFieldNum boolean|nil ----@param areaToAvoid table|nil ----@param vehiclesToIgnore table|nil ----@param areaToIgnoreFruit table|nil -function PathfinderControllerContext:set( - mustBeAccurate, allowReverse, - maxFruitPercent, offFieldPenalty, useFieldNum, - areaToAvoid, vehiclesToIgnore, areaToIgnoreFruit) - self.maxFruitPercent = maxFruitPercent - self.offFieldPenalty = offFieldPenalty - self.useFieldNum = useFieldNum - self.areaToAvoid = areaToAvoid - self.allowReverse = allowReverse - self.vehiclesToIgnore = vehiclesToIgnore - self.mustBeAccurate = mustBeAccurate - self.areaToIgnoreFruit = areaToIgnoreFruit +function PathfinderControllerContext:init(vehicle, numRetries) + self._vehicle = vehicle + self._numRetries = numRetries or self.defaultNumRetries + CpObjectUtil.registerBuilderAPI(self, self.attributesToDefaultValue) end --- Disables the fruit avoidance function PathfinderControllerContext:ignoreFruit() - self.maxFruitPercent = math.huge + self._maxFruitPercent = math.huge end function PathfinderControllerContext:getNumRetriesAllowed() - return self.numRetries -end - -function PathfinderControllerContext:__tostring() - local str = "PathfinderControllerContext(vehicle name=%s, maxFruitPercent=%s, "+ - "offFieldPenalty=%s, useFieldNum=%s, areaToAvoid=%s, allowReverse=%s, "+ - "vehiclesToIgnore=%s, mustBeAccurate=%s, areaToIgnoreFruit=%s)" - return string.format(str, - CpUtil.getName(self.vehicle), - tostring(self.maxFruitPercent), - tostring(self.offFieldPenalty), - tostring(self.useFieldNum), - tostring(self.areaToAvoid), - tostring(self.allowReverse), - tostring(self.vehiclesToIgnore), - tostring(self.mustBeAccurate), - tostring(self.areaToIgnoreFruit)) + return self._numRetries end ---@class DefaultFieldPathfinderControllerContext : PathfinderControllerContext @@ -175,9 +162,6 @@ function PathfinderController:reset() self.failCount = 0 self.startedAt = 0 self.timeTakenMs = 0 - self.callbackClass = nil - self.callbackSuccessFunction = nil - self.callbackRetryFunction = nil self.lastContext = nil end @@ -186,11 +170,20 @@ function PathfinderController:update(dt) --- Applies coroutine for path finding local done, path, goalNodeInvalid = self.pathfinder:resume() if done then - self:finished(path, goalNodeInvalid) + self:onFinish(path, goalNodeInvalid) end end end +function PathfinderController:getDriveData() + local maxSpeed + if self:isActive() then + --- Pathfinder is active, so we stop the driver. + maxSpeed = 0 + end + return nil, nil, nil, maxSpeed +end + function PathfinderController:isActive() return self.pathfinder and self.pathfinder:isActive() end @@ -200,37 +193,30 @@ function PathfinderController:getLastContext() return self.lastContext end ---- Sets the callbacks which get called on the pathfinder finish. ----@param class table ----@param successFunc function func(PathfinderController, success, path, goalNodeInvalid) +--- Registers listeners for pathfinder success and failures. +--- TODO: Decide if multiple registered listeners are needed or not? +---@param object table +---@param successFunc function func(PathfinderController, success, Course, goalNodeInvalid) ---@param retryFunc function func(PathfinderController, last context, was last retry, retry attempt number) -function PathfinderController:setCallbacks(class, successFunc, retryFunc) - self.callbackClass = class +function PathfinderController:registerListeners(object, successFunc, retryFunc) + self.callbackObject = object self.callbackSuccessFunction = successFunc self.callbackRetryFunction = retryFunc end --- Pathfinder was started ---@param context PathfinderControllerContext -function PathfinderController:started(context) +function PathfinderController:onStart(context) self:debug("Started pathfinding with context: %s.", tostring(context)) self.startedAt = g_time self.lastContext = context self.numRetries = context:getNumRetriesAllowed() end -function PathfinderController:callCallback(callbackFunc, ...) - if self.callbackClass then - callbackFunc(self.callbackClass, self, ...) - else - callbackFunc(self, ...) - end -end - --- Path finding has finished ---@param path table|nil ---@param goalNodeInvalid boolean|nil -function PathfinderController:finished(path, goalNodeInvalid) +function PathfinderController:onFinish(path, goalNodeInvalid) self.pathfinder = nil self.timeTakenMs = g_time - self.startedAt local retValue = self:isValidPath(path, goalNodeInvalid) @@ -241,12 +227,17 @@ function PathfinderController:finished(path, goalNodeInvalid) self:debug("Failed with try %d of %d.", self.failCount, self.numRetries) --- Retrying the path finding self.failCount = self.failCount + 1 - self:callCallback(self.callbackRetryFunction, self.lastContext, self.failCount == self.numRetries, self.failCount) + self:callCallback(self.callbackRetryFunction, + self.lastContext, self.failCount == self.numRetries, self.failCount) return + elseif self.numRetries > 0 then + self:debug("Max number of retries already reached!") end end end - self:callCallback(self.callbackSuccessFunction, retValue == self.SUCCESS_FOUND_VALID_PATH, path, goalNodeInvalid) + self:callCallback(self.callbackSuccessFunction, + retValue == self.SUCCESS_FOUND_VALID_PATH, + self:getTemporaryCourseFromPath(path), goalNodeInvalid) self:reset() end @@ -267,6 +258,14 @@ function PathfinderController:isValidPath(path, goalNodeInvalid) return self.ERROR_NO_PATH_FOUND end +function PathfinderController:callCallback(callbackFunc, ...) + if self.callbackObject then + callbackFunc(self.callbackObject, self, ...) + else + callbackFunc(self, ...) + end +end + --- Finds a path to given goal node ---@param context PathfinderControllerContext ---@param goalNode number @@ -280,22 +279,22 @@ function PathfinderController:findPathToNode(context, self:error("No valid success callback was given!") return false end - self:started(context) + self:onStart(context) local pathfinder, done, path, goalNodeInvalid = PathfinderUtil.startPathfindingFromVehicleToNode( - context.vehicle, + context._vehicle, goalNode, xOffset, zOffset, - context.allowReverse, - context.useFieldNum and 1, - context.vehiclesToIgnore, - context.maxFruitPercent, - context.offFieldPenalty, - context.areaToAvoid, - context.mustBeAccurate + context._allowReverse, + context.useFieldNum and CpFieldUtil.getFieldNumUnderVehicle(context._vehicle) or nil, + context._vehiclesToIgnore, + context._maxFruitPercent, + context._offFieldPenalty, + context._areaToAvoid, + context._mustBeAccurate ) if done then - self:finished(path, goalNodeInvalid) + self:onFinish(path, goalNodeInvalid) else self:debug("Continuing as coroutine...") self.pathfinder = pathfinder @@ -317,26 +316,27 @@ function PathfinderController:findPathToWaypoint(context, self:error("No valid success callback was given!") return false end - self:started(context) + self:onStart(context) local pathfinder, done, path, goalNodeInvalid = PathfinderUtil.startPathfindingFromVehicleToWaypoint( - context.vehicle, + context._vehicle, course, waypointIndex, xOffset, zOffset, - context.allowReverse, - context.useFieldNum and CpFieldUtil.getFieldNumUnderVehicle(context.vehicle), - context.vehiclesToIgnore, - context.maxFruitPercent, - context.offFieldPenalty, - context.areaToAvoid, - context.areaToIgnoreFruit) + context._allowReverse, + context._useFieldNum and CpFieldUtil.getFieldNumUnderVehicle(context._vehicle) or nil, + context._vehiclesToIgnore, + context._maxFruitPercent, + context._offFieldPenalty, + context._areaToAvoid, + context._areaToIgnoreFruit) if done then - self:finished(path, goalNodeInvalid) + self:onFinish(path, goalNodeInvalid) else self:debug("Continuing as coroutine...") self.pathfinder = pathfinder end + return true end function PathfinderController:getTemporaryCourseFromPath(path) diff --git a/scripts/pathfinder/PathfinderUtil.lua b/scripts/pathfinder/PathfinderUtil.lua index 32b19e645..55ee89b12 100644 --- a/scripts/pathfinder/PathfinderUtil.lua +++ b/scripts/pathfinder/PathfinderUtil.lua @@ -938,8 +938,8 @@ end --- Interface function to start the pathfinder in the game. The goal is a point at sideOffset meters from the goal node --- (sideOffset > 0 is left) ------------------------------------------------------------------------------------------------------------------------ ----@param vehicle table, will be used as the start location/heading, turn radius and size ----@param goalNode table The goal node +---@param vehicle table will be used as the start location/heading, turn radius and size +---@param goalNode number The goal node ---@param xOffset number side offset of the goal from the goal node ---@param zOffset number length offset of the goal from the goal node ---@param allowReverse boolean allow reverse driving From 9a61c7698edc7084610baa93d86a1720bad3c54a Mon Sep 17 00:00:00 2001 From: Peter Vaiko Date: Fri, 13 Oct 2023 07:49:01 -0400 Subject: [PATCH 3/7] Pathfinder defaults added/documented. --- scripts/ai/PathfinderController.lua | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/scripts/ai/PathfinderController.lua b/scripts/ai/PathfinderController.lua index 91ff2af39..924103d4a 100644 --- a/scripts/ai/PathfinderController.lua +++ b/scripts/ai/PathfinderController.lua @@ -97,22 +97,36 @@ local context = PathfinderControllerContext():maxFruitPercent(100):useFieldNum(t ---@field _maxFruitPercent number ---@field _offFieldPenalty number ---@field _useFieldNum boolean ----@field _areaToAvoid PathfinderUtil.NodeArea +---@field _areaToAvoid PathfinderUtil.NodeArea|nil ---@field _allowReverse boolean ----@field _vehiclesToIgnore table[] +---@field _vehiclesToIgnore table[]|nil ---@field _mustBeAccurate boolean ---@field _areaToIgnoreFruit table[] PathfinderControllerContext = CpObject() PathfinderControllerContext.defaultNumRetries = 0 PathfinderControllerContext.attributesToDefaultValue = { + -- If an 4 x 4 m area around a pathfinder node has more than this fruit, a penalty of 0.5 * actual fruit + -- percentage will be applied to that node. + -- TODO: check if the fruitValue returned by FSDensityMapUtil.getFruitArea() really is a percentage ["maxFruitPercent"] = 50, - ["offFieldPenalty"] = 50, - ["useFieldNum"] = false, - ["areaToAvoid"] = {}, + -- This penalty is added to the cost of each step, which is about 12% of the turning radius when using + -- the hybrid A* and 3 with the simple A*. + -- Simple A* is used for long-range pathfinding, in that case we are willing to drive about 3 times longer + -- to stay on the field. Hybrid A* is more restrictive, TODO: review if these should be balanced + -- If useFieldNum true, fields that are not owned have a 20% more penalty. + ["offFieldPenalty"] = 7.5, + ["useFieldNum"] = false, + -- Pathfinder nodes in this area have a prohibitive penalty (2000) + ["areaToAvoid"] = nil, ["allowReverse"] = false, - ["vehiclesToIgnore"] = {}, + ["vehiclesToIgnore"] = nil, + -- If false, as we reach the maximum iterations, we relax our criteria to reach the goal: allow for arriving at + -- bigger angle differences, trading off accuracy for speed. This usually results in a direction at the goal + -- being less then 30º off which in many cases isn't a problem. + -- Otherwise, for example when a combine self unloading must accurately find the trailer, set this to true. ["mustBeAccurate"] = false, - ["areaToIgnoreFruit"] = {} + -- No fruit penalty in this area (e.g. when we know the goal is in fruit but want to avoid fruit all the way there) + ["areaToIgnoreFruit"] = nil } function PathfinderControllerContext:init(vehicle, numRetries) From f2700487c904ce457b956ef35c7adbaa75aa8837 Mon Sep 17 00:00:00 2001 From: schwiti6190 <58079399+schwiti6190@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:22:00 +0200 Subject: [PATCH 4/7] Added NIL alternative for Builder generation, as nil value are optimized away on declaration --- scripts/CpObject.lua | 8 +++++++- scripts/ai/PathfinderController.lua | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/CpObject.lua b/scripts/CpObject.lua index fab7e9dde..02f8dccf0 100644 --- a/scripts/CpObject.lua +++ b/scripts/CpObject.lua @@ -74,15 +74,21 @@ function CpObject(base, init) end ---@class CpObjectUtil -CpObjectUtil = {} +CpObjectUtil = { + BUILDER_API_NIL = "nil" +} --- Registers a builder api for a class. --- The attributes are set as private variables with "_" before the variable name --- and the builder functions are named like the attribute. +--- Nil values have to be replaced with CpObjectUtil.BUILDER_API_NIL !! ---@param class table ---@param attributesToDefault table function CpObjectUtil.registerBuilderAPI(class, attributesToDefault) for attributeName, default in pairs(attributesToDefault) do + if default == CpObjectUtil.BUILDER_API_NIL then + default = nil + end --- Applies the default value to the private variable class["_" .. attributeName] = default --- Creates the builder functions/ setters with the public variable name diff --git a/scripts/ai/PathfinderController.lua b/scripts/ai/PathfinderController.lua index 924103d4a..d7fc53668 100644 --- a/scripts/ai/PathfinderController.lua +++ b/scripts/ai/PathfinderController.lua @@ -117,16 +117,16 @@ PathfinderControllerContext.attributesToDefaultValue = { ["offFieldPenalty"] = 7.5, ["useFieldNum"] = false, -- Pathfinder nodes in this area have a prohibitive penalty (2000) - ["areaToAvoid"] = nil, + ["areaToAvoid"] = CpObjectUtil.BUILDER_API_NIL, ["allowReverse"] = false, - ["vehiclesToIgnore"] = nil, + ["vehiclesToIgnore"] = CpObjectUtil.BUILDER_API_NIL, -- If false, as we reach the maximum iterations, we relax our criteria to reach the goal: allow for arriving at -- bigger angle differences, trading off accuracy for speed. This usually results in a direction at the goal -- being less then 30º off which in many cases isn't a problem. -- Otherwise, for example when a combine self unloading must accurately find the trailer, set this to true. ["mustBeAccurate"] = false, -- No fruit penalty in this area (e.g. when we know the goal is in fruit but want to avoid fruit all the way there) - ["areaToIgnoreFruit"] = nil + ["areaToIgnoreFruit"] = CpObjectUtil.BUILDER_API_NIL } function PathfinderControllerContext:init(vehicle, numRetries) From 3550fde0f8b4ac24929472138f8f1682241c6150 Mon Sep 17 00:00:00 2001 From: schwiti6190 <58079399+schwiti6190@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:36:11 +0200 Subject: [PATCH 5/7] Changed useFieldNum to an INT --- scripts/ai/PathfinderController.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/ai/PathfinderController.lua b/scripts/ai/PathfinderController.lua index d7fc53668..8bb9ad5a5 100644 --- a/scripts/ai/PathfinderController.lua +++ b/scripts/ai/PathfinderController.lua @@ -96,7 +96,7 @@ local context = PathfinderControllerContext():maxFruitPercent(100):useFieldNum(t ---@field areaToIgnoreFruit function ---@field _maxFruitPercent number ---@field _offFieldPenalty number ----@field _useFieldNum boolean +---@field _useFieldNum number ---@field _areaToAvoid PathfinderUtil.NodeArea|nil ---@field _allowReverse boolean ---@field _vehiclesToIgnore table[]|nil @@ -113,9 +113,9 @@ PathfinderControllerContext.attributesToDefaultValue = { -- the hybrid A* and 3 with the simple A*. -- Simple A* is used for long-range pathfinding, in that case we are willing to drive about 3 times longer -- to stay on the field. Hybrid A* is more restrictive, TODO: review if these should be balanced - -- If useFieldNum true, fields that are not owned have a 20% more penalty. ["offFieldPenalty"] = 7.5, - ["useFieldNum"] = false, + -- If useFieldNum > 0, fields that are not owned have a 20% greater penalty. + ["useFieldNum"] = 0, -- Pathfinder nodes in this area have a prohibitive penalty (2000) ["areaToAvoid"] = CpObjectUtil.BUILDER_API_NIL, ["allowReverse"] = false, @@ -140,6 +140,11 @@ function PathfinderControllerContext:ignoreFruit() self._maxFruitPercent = math.huge end +--- Uses the field number of the vehicle to restrict path finding. +function PathfinderControllerContext:useVehicleFieldNumber() + self._useFieldNum = CpFieldUtil.getFieldNumUnderVehicle(self._vehicle) +end + function PathfinderControllerContext:getNumRetriesAllowed() return self._numRetries end @@ -300,7 +305,7 @@ function PathfinderController:findPathToNode(context, xOffset, zOffset, context._allowReverse, - context.useFieldNum and CpFieldUtil.getFieldNumUnderVehicle(context._vehicle) or nil, + context._useFieldNum, context._vehiclesToIgnore, context._maxFruitPercent, context._offFieldPenalty, @@ -338,7 +343,7 @@ function PathfinderController:findPathToWaypoint(context, xOffset, zOffset, context._allowReverse, - context._useFieldNum and CpFieldUtil.getFieldNumUnderVehicle(context._vehicle) or nil, + context._useFieldNum, context._vehiclesToIgnore, context._maxFruitPercent, context._offFieldPenalty, From fdc04400ebeae13455716763fbb6b6ead97c9e00 Mon Sep 17 00:00:00 2001 From: schwiti6190 <58079399+schwiti6190@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:36:48 +0200 Subject: [PATCH 6/7] Minor fix for build generation --- scripts/ai/PathfinderController.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/ai/PathfinderController.lua b/scripts/ai/PathfinderController.lua index 8bb9ad5a5..b3a874627 100644 --- a/scripts/ai/PathfinderController.lua +++ b/scripts/ai/PathfinderController.lua @@ -138,11 +138,13 @@ end --- Disables the fruit avoidance function PathfinderControllerContext:ignoreFruit() self._maxFruitPercent = math.huge + return self end --- Uses the field number of the vehicle to restrict path finding. function PathfinderControllerContext:useVehicleFieldNumber() self._useFieldNum = CpFieldUtil.getFieldNumUnderVehicle(self._vehicle) + return self end function PathfinderControllerContext:getNumRetriesAllowed() From cdf3c224c71f6956783c7faca3eb3760af605ace Mon Sep 17 00:00:00 2001 From: schwiti6190 <58079399+schwiti6190@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:37:14 +0200 Subject: [PATCH 7/7] Registered listeners to strategy --- scripts/ai/AIDriveStrategyCourse.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/ai/AIDriveStrategyCourse.lua b/scripts/ai/AIDriveStrategyCourse.lua index 6b467c1ec..1849d55d6 100644 --- a/scripts/ai/AIDriveStrategyCourse.lua +++ b/scripts/ai/AIDriveStrategyCourse.lua @@ -113,6 +113,7 @@ function AIDriveStrategyCourse:setAIVehicle(vehicle, jobParameters) self.ppc:registerListeners(self, 'onWaypointPassed', 'onWaypointChange') self.pathfinderController = PathfinderController(vehicle) + self.pathfinderController:registerListeners(self, self.onPathfindingFinished, self.onPathfindingRetry) self.storage = vehicle.spec_cpAIWorker @@ -583,6 +584,24 @@ end function AIDriveStrategyCourse:onWaypointPassed(ix, course) end +--- Pathfinding has finished +---@param controller PathfinderController +---@param success boolean +---@param course Course|nil +---@param goalNodeInvalid boolean|nil +function AIDriveStrategyCourse:onPathfindingFinished(controller, success, course, goalNodeInvalid) + -- override +end + +--- Pathfinding failed, but a retry attempt is leftover. +---@param controller PathfinderController +---@param lastContext PathfinderControllerContext +---@param wasLastRetry boolean +---@param currentRetryAttempt number +function AIDriveStrategyCourse:onPathfindingRetry(controller, lastContext, wasLastRetry, currentRetryAttempt) + -- override +end + ------------------------------------------------------------------------------------------------------------------------ --- Pathfinding ---------------------------------------------------------------------------------------------------------------------------