Skip to content

Commit a4a6594

Browse files
authored
Merge pull request #721 from Courseplay/little-improvements
refactor: pathfinder code quality improvements
2 parents ad0a18e + a75888a commit a4a6594

6 files changed

Lines changed: 238 additions & 218 deletions

File tree

modDesc.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ Changelog 8.0.0.0:
164164
<sourceFile filename="scripts/pathfinder/HybridAStar.lua"/>
165165
<sourceFile filename="scripts/pathfinder/AStar.lua"/>
166166
<sourceFile filename="scripts/pathfinder/HybridAStarWithAStarInTheMiddle.lua"/>
167+
<sourceFile filename="scripts/pathfinder/PathfinderCollisionDetector.lua"/>
167168
<sourceFile filename="scripts/pathfinder/PathfinderConstraints.lua"/>
168169
<sourceFile filename="scripts/pathfinder/PathfinderContext.lua"/>
169170
<sourceFile filename="scripts/pathfinder/PathfinderUtil.lua"/>

scripts/Course.lua

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -960,9 +960,7 @@ function Course.createFromNode(vehicle, referenceNode, xOffset, from, to, step,
960960
local x, _, z = localToWorld(referenceNode, xOffset, 0, dz + i * dBetweenPoints)
961961
table.insert(waypoints, { x = x, z = z, rev = reverse })
962962
end
963-
local course = Course(vehicle, waypoints, true)
964-
course:enrichWaypointData()
965-
return course
963+
return Course(vehicle, waypoints, true)
966964
end
967965

968966
--- Create a straight, forward course for the vehicle.

scripts/ai/strategies/AIDriveStrategyShovelSiloLoader.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -752,15 +752,15 @@ end
752752
local function addMixerWagonTriggers(mixerWagon)
753753
local spec = mixerWagon.spec_mixerWagon
754754
if spec.hudTrigger then
755-
PathfinderUtil.CollisionDetector.addNodeToIgnore(spec.hudTrigger)
755+
PathfinderCollisionDetector.addNodeToIgnore(spec.hudTrigger)
756756
end
757757
end
758758
MixerWagon.onLoad = Utils.appendedFunction(MixerWagon.onLoad, addMixerWagonTriggers)
759759

760760
local function deleteMixerWagonTrigger(mixerWagon)
761761
local spec = mixerWagon.spec_mixerWagon
762762
if spec.hudTrigger then
763-
PathfinderUtil.CollisionDetector.removeNodeToIgnore(spec.hudTrigger)
763+
PathfinderCollisionDetector.removeNodeToIgnore(spec.hudTrigger)
764764
end
765765
end
766766
MixerWagon.onDelete = Utils.prependedFunction(MixerWagon.onDelete, deleteMixerWagonTrigger)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
--[[
2+
This file is part of Courseplay (https://github.com/Courseplay/Courseplay_FS25)
3+
Copyright (C) 2024 Courseplay Dev Team
4+
5+
This program is free software: you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published by
7+
the Free Software Foundation, either version 3 of the License, or
8+
(at your option) any later version.
9+
10+
This program is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
GNU General Public License for more details.
14+
15+
You should have received a copy of the GNU General Public License
16+
along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
]]
18+
19+
------------------------------------------------------------------------------------------------------------------------
20+
-- A collision detector used by the pathfinder
21+
---------------------------------------------------------------------------------------------------------------------------
22+
---@class PathfinderCollisionDetector
23+
PathfinderCollisionDetector = CpObject()
24+
25+
--- Nodes of a trigger for example, that will be ignored as collision.
26+
PathfinderCollisionDetector.NODES_TO_IGNORE = {}
27+
28+
function PathfinderCollisionDetector:init(vehicle, vehiclesToIgnore, objectsToIgnore, ignoreFruitHeaps, collisionMask)
29+
self.logger = Logger('PathfinderCollisionDetector', Logger.level.debug, CpDebug.DBG_PATHFINDER)
30+
self.vehicle = vehicle
31+
self.vehiclesToIgnore = vehiclesToIgnore or {}
32+
self.objectsToIgnore = objectsToIgnore or {}
33+
self.ignoreFruitHeaps = ignoreFruitHeaps
34+
self.collidingShapes = 0
35+
self.collisionMask = collisionMask or CpUtil.getDefaultCollisionFlags()
36+
end
37+
38+
--- Adds a node which collision will be ignored global for every pathfinder.
39+
---@param node number
40+
function PathfinderCollisionDetector.addNodeToIgnore(node)
41+
PathfinderCollisionDetector.NODES_TO_IGNORE[node] = true
42+
end
43+
44+
--- Removes a node, so it's collision is no longer applied.
45+
---@param node number
46+
function PathfinderCollisionDetector.removeNodeToIgnore(node)
47+
PathfinderCollisionDetector.NODES_TO_IGNORE[node] = nil
48+
end
49+
50+
function PathfinderCollisionDetector:_overlapBoxCallback(transformId)
51+
if PathfinderCollisionDetector.NODES_TO_IGNORE[transformId] then
52+
--- Global node, that needs to be ignored
53+
return
54+
end
55+
56+
local collidingObject = g_currentMission.nodeToObject[transformId]
57+
if collidingObject and PathfinderUtil.elementOf(self.objectsToIgnore, collidingObject) then
58+
-- an object we want to ignore
59+
return
60+
end
61+
local text, rootVehicle
62+
if collidingObject then
63+
if collidingObject.getRootVehicle then
64+
rootVehicle = collidingObject:getRootVehicle()
65+
elseif collidingObject:isa(Bale) and collidingObject.mountObject then
66+
rootVehicle = collidingObject.mountObject:getRootVehicle()
67+
end
68+
if rootVehicle == self.vehicle:getRootVehicle() or
69+
PathfinderUtil.elementOf(self.vehiclesToIgnore, rootVehicle) then
70+
-- just bumped into myself or a vehicle we want to ignore
71+
return
72+
end
73+
if collidingObject:isa(Bale) then
74+
text = string.format('bale %d', collidingObject.id)
75+
else
76+
text = CpUtil.getName(collidingObject)
77+
end
78+
end
79+
if getHasClassId(transformId, ClassIds.TERRAIN_TRANSFORM_GROUP) then
80+
81+
local x, y, z = unpack(self.currentOverlapBoxPosition.pos)
82+
local dirX, dirZ = unpack(self.currentOverlapBoxPosition.direction)
83+
local size = self.currentOverlapBoxPosition.size
84+
--- Roughly checks the overlap box for any dropped fill type to the ground.
85+
--- TODO: DensityMapHeightUtil.getFillTypeAtArea() would be better.
86+
local fillType = DensityMapHeightUtil.getFillTypeAtLine(x, y, z, x + dirX * size, y, z + dirZ * size, size)
87+
if not self.ignoreFruitHeaps and fillType and fillType ~= FillType.UNKNOWN then
88+
text = string.format('terrain and fillType: %s.',
89+
g_fillTypeManager:getFillTypeByIndex(fillType).title)
90+
else
91+
--- Ignore terrain hits, if no fillType is dropped to the ground was detected.
92+
return
93+
end
94+
end
95+
96+
if text == nil then
97+
text = transformId .. ':'
98+
for key, classId in pairs(ClassIds) do
99+
if getHasClassId(transformId, classId) then
100+
text = text .. ' ' .. key
101+
end
102+
end
103+
end
104+
self.collidingShapesText = text
105+
self.collidingShapes = self.collidingShapes + 1
106+
end
107+
108+
function PathfinderCollisionDetector:findCollidingShapes(node, vehicleToLog, overlapBoxParams)
109+
local xRot, yRot, zRot = getWorldRotation(node)
110+
local x, y, z = localToWorld(node, overlapBoxParams.xOffset, 1, overlapBoxParams.zOffset)
111+
local dirX, dirZ = MathUtil.getDirectionFromYRotation(yRot)
112+
--- Save these for the overlap box callback.
113+
self.currentOverlapBoxPosition = {
114+
pos = { x, y, z },
115+
direction = { dirX, dirZ },
116+
size = math.max(overlapBoxParams.width, overlapBoxParams.length)
117+
}
118+
self.collidingShapes = 0
119+
self.collidingShapesText = 'unknown'
120+
121+
overlapBox(x, y + 0.2, z, xRot, yRot, zRot, overlapBoxParams.width, 1, overlapBoxParams.length, '_overlapBoxCallback',
122+
self, self.collisionMask, true, true, true, true)
123+
124+
if true and self.collidingShapes > 0 then
125+
table.insert(PathfinderUtil.overlapBoxes,
126+
{ x = x, y = y + 0.2, z = z, xRot = xRot, yRot = yRot, zRot = zRot, width = overlapBoxParams.width,
127+
length = overlapBoxParams.length })
128+
self.logger:debug(self.vehicle,'my %s (%.1fx%.1f) is colliding with %s at x = %.1f, z = %.1f, yRot = %d',
129+
CpUtil.getName(vehicleToLog), 2 * overlapBoxParams.width, 2 * overlapBoxParams.length, self.collidingShapesText, x, z, math.deg(yRot))
130+
end
131+
132+
return self.collidingShapes
133+
end

scripts/pathfinder/PathfinderConstraints.lua

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ PathfinderConstraints = CpObject(PathfinderConstraintInterface)
5454

5555
---@param context PathfinderContext
5656
function PathfinderConstraints:init(context)
57+
self.logger = Logger('PathfinderConstraints', Logger.level.debug, CpDebug.DBG_PATHFINDER)
58+
self.vehicle = context._vehicle
59+
self.turnRadius = AIUtil.getTurningRadius(context._vehicle) or 10
5760
self.vehicleData = PathfinderUtil.VehicleData(context._vehicle, true, 0.25)
5861
self.trailerHitchLength = AIUtil.getTowBarLength(context._vehicle) or 3
59-
self.turnRadius = AIUtil.getTurningRadius(context._vehicle) or 10
60-
self.objectsToIgnore = context._objectsToIgnore or {}
61-
self.vehiclesToIgnore = context._vehiclesToIgnore or {}
62-
62+
self.collisionDetector = PathfinderCollisionDetector(context._vehicle, context._vehiclesToIgnore, context._objectsToIgnore, context._collisionMask)
6363
self.maxFruitPercent = context._maxFruitPercent
6464
self.offFieldPenalty = context._offFieldPenalty
6565
self.fieldNum = context._useFieldNum
@@ -72,12 +72,11 @@ function PathfinderConstraints:init(context)
7272
self.ignoreTrailerAtStartRange = context._ignoreTrailerAtStartRange or 0
7373
self.initialMaxFruitPercent = self.maxFruitPercent
7474
self.initialOffFieldPenalty = self.offFieldPenalty
75-
self.collisionMask = context._collisionMask
7675
self.strictMode = false
7776
self:resetCounts()
7877
local areaToAvoidText = self.areaToAvoid and
7978
string.format('are to avoid %.1f x %.1f m', self.areaToAvoid.length, self.areaToAvoid.width) or 'none'
80-
self:debug('Pathfinder constraints: off field penalty %.1f, max fruit percent: %.1f, field number %d, %s, ignore fruit %s, ignore off-field penalty %s',
79+
self.logger:debug('off field penalty %.1f, max fruit percent: %.1f, field number %d, %s, ignore fruit %s, ignore off-field penalty %s',
8180
self.offFieldPenalty, self.maxFruitPercent, self.fieldNum, areaToAvoidText,
8281
self.areaToIgnoreFruit or 'none', self.areaToIgnoreOffFieldPenalty or 'none')
8382
end
@@ -173,7 +172,7 @@ function PathfinderConstraints:isValidAnalyticSolutionNode(node, log)
173172
local analyticLimit = self.maxFruitPercent * 2
174173
if hasFruit and fruitValue > analyticLimit then
175174
if log then
176-
self:debug('isValidAnalyticSolutionNode: fruitValue %.1f, max %.1f @ %.1f, %.1f',
175+
self.logger:debug('isValidAnalyticSolutionNode: fruitValue %.1f, max %.1f @ %.1f, %.1f',
177176
fruitValue, analyticLimit, node.x, -node.y)
178177
end
179178
return false
@@ -205,20 +204,19 @@ function PathfinderConstraints:isValidNode(node, ignoreTrailer, offFieldValid)
205204
node.x, -node.y, CpMathUtil.angleToGame(node.t), 0.5)
206205

207206
-- for debug purposes only, store validity info on node
208-
node.collidingShapes = PathfinderUtil.collisionDetector:findCollidingShapes(PathfinderUtil.helperNode,
209-
self.vehicleData, self.vehiclesToIgnore, self.objectsToIgnore, self.ignoreFruitHeaps, self.collisionMask)
207+
node.collidingShapes = self.collisionDetector:findCollidingShapes(PathfinderUtil.helperNode,
208+
self.vehicleData:getVehicle(), self.vehicleData:getVehicleOverlapBoxParams())
210209
ignoreTrailer = ignoreTrailer or node.d < self.ignoreTrailerAtStartRange
211-
if self.vehicleData.trailer and not ignoreTrailer then
210+
if self.vehicleData:getTowedImplement() and not ignoreTrailer then
212211
-- now check the trailer or towed implement
213212
-- move the node to the rear of the vehicle (where approximately the trailer is attached)
214-
local x, y, z = localToWorld(PathfinderUtil.helperNode, 0, 0, self.vehicleData.trailerHitchOffset)
213+
local x, y, z = localToWorld(PathfinderUtil.helperNode, 0, 0, self.vehicleData:getHitchOffset())
215214

216215
PathfinderUtil.setWorldPositionAndRotationOnTerrain(PathfinderUtil.helperNode, x, z,
217216
CpMathUtil.angleToGame(node.tTrailer), 0.5)
218217

219-
node.collidingShapes = node.collidingShapes + PathfinderUtil.collisionDetector:findCollidingShapes(
220-
PathfinderUtil.helperNode, self.vehicleData.trailerRectangle, self.vehiclesToIgnore,
221-
self.objectsToIgnore, self.ignoreFruitHeaps, self.collisionMask)
218+
node.collidingShapes = node.collidingShapes + self.collisionDetector:findCollidingShapes(PathfinderUtil.helperNode,
219+
self.vehicleData:getTowedImplement(), self.vehicleData:getTowedImplementOverlapBoxParams())
222220
if node.collidingShapes > 0 then
223221
self.trailerCollisionNodeCount = self.trailerCollisionNodeCount + 1
224222
end
@@ -241,10 +239,10 @@ function PathfinderConstraints:resetStrictMode()
241239
end
242240

243241
function PathfinderConstraints:showStatistics()
244-
self:debug('Nodes: %d, Penalties: fruit: %d, off-field: %d, not owned field: %d, collisions: %d, trailer collisions: %d, area to avoid: %d, preferred path: %d',
242+
self.logger:debug('Nodes: %d, Penalties: fruit: %d, off-field: %d, not owned field: %d, collisions: %d, trailer collisions: %d, area to avoid: %d, preferred path: %d',
245243
self.totalNodeCount, self.fruitPenaltyNodeCount, self.offFieldPenaltyNodeCount, self.notOwnedFieldPenaltyNodeCount,
246244
self.collisionNodeCount, self.trailerCollisionNodeCount, self.areaToAvoidPenaltyCount, self.preferredPathPenaltyCount)
247-
self:debug(' max fruit %.1f %%, off-field penalty: %.1f',
245+
self.logger:debug(' max fruit %.1f %%, off-field penalty: %.1f',
248246
self.maxFruitPercent, self.offFieldPenalty)
249247
end
250248

@@ -259,7 +257,3 @@ end
259257
function PathfinderConstraints:getOffFieldPenaltyNodePercent()
260258
return self.totalNodeCount > 0 and (self.offFieldPenaltyNodeCount / self.totalNodeCount) or 0
261259
end
262-
263-
function PathfinderConstraints:debug(...)
264-
self.vehicleData:debug(...)
265-
end

0 commit comments

Comments
 (0)