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