-
-
Notifications
You must be signed in to change notification settings - Fork 69
Expand file tree
/
Copy pathAIDriveStrategyCourse.lua
More file actions
812 lines (714 loc) · 32.6 KB
/
AIDriveStrategyCourse.lua
File metadata and controls
812 lines (714 loc) · 32.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
--[[
This file is part of Courseplay (https://github.com/Courseplay/Courseplay_FS25)
Copyright (C) 2021 Peter Vaiko
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 <http://www.gnu.org/licenses/>.
Base class for all Courseplay drive strategies
]]
---@class AIDriveStrategyCourse
AIDriveStrategyCourse = CpObject()
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"
AIDriveStrategyCourse.onStopRefillingEvent = "onStopRefilling"
AIDriveStrategyCourse.onUpdateRefillingEvent = "onUpdateRefilling"
AIDriveStrategyCourse.updateEvent = "update"
AIDriveStrategyCourse.deleteEvent = "delete"
--- A row has just been finished, implements are being raised and about to start the actual turn
AIDriveStrategyCourse.onFinishRowEvent = "onFinishRow"
--- The actual turn is done, now we are starting into the row and will lower the implements when
--- they reach the start of the row
AIDriveStrategyCourse.onTurnEndProgressEvent = "onTurnEndProgress"
---@param task CpAITask
---@param job CpAIJob
function AIDriveStrategyCourse:init(task, job)
self.debugChannel = CpDebug.DBG_AI_DRIVER
self:initStates(AIDriveStrategyCourse.myStates)
---@type ImplementController[]
self.controllers = {}
self.registeredInfoTexts = {}
--- To temporary hold a vehicle (will force speed to 0)
self.held = CpTemporaryObject()
self.fuelSaveActiveWhileHeld = false
self.currentTask = task
self.job = job
self.stopRequestData = {
reason = nil,
waitForFolding = false,
prepareTimeout = CpTemporaryObject(true)
}
end
function AIDriveStrategyCourse:setCurrentTaskFinished()
self.currentTask:skip()
end
--- Aggregation of states from this and all descendant classes
function AIDriveStrategyCourse:initStates(newStates)
self.states = CpUtil.initStates(self.states, newStates)
end
function AIDriveStrategyCourse:getStateAsString()
return self.state.name
end
function AIDriveStrategyCourse:getName()
return CpUtil.getName(self.vehicle)
end
function AIDriveStrategyCourse:debug(...)
CpUtil.debugVehicle(self.debugChannel, self.vehicle, self:getStateAsString() .. ': ' .. string.format(...))
end
function AIDriveStrategyCourse:debugSparse(...)
local nowSecs = math.floor(g_time / 1000)
-- report every 5 seconds
-- TODO: make this a parameter in seconds?
if not self.lastLogSecs or (nowSecs > self.lastLogSecs and nowSecs % 5 == 0) then
self:debug(...)
self.lastLogSecs = nowSecs
end
end
function AIDriveStrategyCourse:info(...)
CpUtil.infoVehicle(self.vehicle, self:getStateAsString() .. ': ' .. string.format(...))
end
function AIDriveStrategyCourse:error(...)
CpUtil.infoVehicle(self.vehicle, self:getStateAsString() .. ': ' .. string.format(...))
end
--- Sets an info text
---@param text CpInfoTextElement
function AIDriveStrategyCourse:setInfoText(text)
self.vehicle:setCpInfoTextActive(text)
end
--- @param text CpInfoTextElement
function AIDriveStrategyCourse:clearInfoText(text)
if text then
self.vehicle:resetCpActiveInfoText(text)
end
end
function AIDriveStrategyCourse:setAIVehicle(vehicle, jobParameters)
self.vehicle = vehicle
--self:fixTurnOnEvent()
self.jobParameters = jobParameters
self:initializeImplementControllers(vehicle)
self.ppc = PurePursuitController(vehicle)
self.ppc:registerListeners(self, 'onWaypointPassed', 'onWaypointChange')
self.storage = vehicle.spec_cpAIWorker
self.settings = vehicle:getCpSettings()
self.courseGeneratorSettings = vehicle:getCourseGeneratorSettings()
-- for now, pathfinding generated courses can't be driven by towed tools
self.allowReversePathfinding = AIUtil.getFirstReversingImplementWithWheels(self.vehicle) == nil
self.turningRadius = AIUtil.getTurningRadius(vehicle)
self.pathfinderController = PathfinderController(vehicle, self.turningRadius)
self.pathfinderController:registerListeners(self, self.onPathfindingFinished, self.onPathfindingFailed)
self:setAllStaticParameters()
-- TODO: this may or may not be the course we need for the strategy
local course = self:isGeneratedCourseNeeded() and self:getGeneratedCourse(jobParameters)
if course then
self:debug('Vehicle has a fieldwork course, figure out where to start')
if course:wasEditedByCourseEditor() then
self:info('The fieldwork course was edited by the course editor.')
end
local startIx = self:getStartingPointWaypointIx(course, jobParameters.startAt:getValue())
self:start(course, startIx, jobParameters)
else
-- some strategies do not need a recorded or generated course to work, they
-- will create the courses on the fly.
self:debug('Vehicle has no course, start work without it.')
self:startWithoutCourse(jobParameters)
end
self:raiseControllerEvent(self.onStartEvent)
end
function AIDriveStrategyCourse:fixTurnOnEvent()
for _, action in ipairs(self.vehicle.actionController.actions) do
for _, listener in ipairs(action.aiEventListener) do
if listener.eventName == 'onAIImplementStart' then
listener.direction = 1
end
end
end
end
--- Does the strategy need the current assigned course?
function AIDriveStrategyCourse:isGeneratedCourseNeeded()
return true
end
function AIDriveStrategyCourse:delete()
self:raiseControllerEvent(self.deleteEvent)
end
function AIDriveStrategyCourse:getGeneratedCourse(jobParameters)
return self.vehicle:getFieldWorkCourse()
end
function AIDriveStrategyCourse:getStartingPointWaypointIx(course, startAt)
if startAt == CpFieldWorkJobParameters.START_AT_NEAREST_POINT then
local _, _, ixClosestRightDirection, _ = course:getNearestWaypoints(self.vehicle:getAIDirectionNode())
self:debug('Starting course at the closest waypoint in the right direction %d', ixClosestRightDirection)
return ixClosestRightDirection
elseif startAt == CpFieldWorkJobParameters.START_AT_LAST_POINT then
local lastWpIx = self.vehicle:getCpLastRememberedWaypointIx()
if lastWpIx then
self:debug('Starting course at the last waypoint %d', lastWpIx)
return lastWpIx
end
end
self:debug('Starting course at the first waypoint')
return 1
end
function AIDriveStrategyCourse:start(course, startIx, jobParameters)
self:startCourse(course, startIx)
self.state = self.states.INITIAL
end
function AIDriveStrategyCourse:startWithoutCourse(jobParameters)
end
function AIDriveStrategyCourse:updateCpStatus(status)
--- override
end
-----------------------------------------------------------------------------------------------------------------------
--- Implement handling
-----------------------------------------------------------------------------------------------------------------------
--- Adds implement controllers for every implement, that has the given specialization.
---@param vehicle table
---@param class ImplementController
---@param spec table|nil
---@param states table|nil
---@param specReference string|nil
---@return table last implement found.
---@return table last implement controller
function AIDriveStrategyCourse:addImplementController(vehicle, class, spec, states, specReference)
--- If multiple implements have this spec, then add a controller for each implement.
local lastImplement, lastController
for _, childVehicle in pairs(AIUtil.getAllChildVehiclesWithSpecialization(vehicle, spec, specReference)) do
local controller = class(vehicle, childVehicle)
self:appendImplementController(controller, states)
lastImplement, lastController = childVehicle, controller
end
return lastImplement, lastController
end
--- Appends an implement controller
---@param controller ImplementController
---@param disabledStates table|nil
function AIDriveStrategyCourse:appendImplementController(controller, disabledStates)
if disabledStates then
controller:setDisabledStates(disabledStates)
end
controller:setDriveStrategy(self)
table.insert(self.controllers, controller)
end
--- Gets all registered implement controller of a given type.
---@param controllerClass ImplementController
---@return boolean found?
---@return table all found implement controllers
function AIDriveStrategyCourse:getRegisteredImplementControllersByClass(controllerClass)
local foundControllers = {}
for _, controller in pairs(self.controllers) do
if controller:is_a(controllerClass) then
table.insert(foundControllers, controller)
end
end
return #foundControllers > 0, foundControllers
end
--- Gets the first found registered implement controller and it's implement.
---@param controllerClass ImplementController
---@return ImplementController|nil found implement controller
---@return table|nil found implement
function AIDriveStrategyCourse:getFirstRegisteredImplementControllerByClass(controllerClass)
local found, controllers = self:getRegisteredImplementControllersByClass(controllerClass)
if found then
return controllers[1], controllers[1]:getImplement()
end
end
--- Checks if any controller disables fuel save, for example a round baler that is dropping a bale.
function AIDriveStrategyCourse:isFuelSaveAllowed()
return self.fuelSaveActiveWhileHeld and self:isBeingHeld()
end
function AIDriveStrategyCourse:initializeImplementControllers(vehicle)
--- override
end
--- Normal update function called every frame.
--- For releasing the helper in the controller, use this one.
function AIDriveStrategyCourse:updateImplementControllers(dt)
self:raiseControllerEvent(self.updateEvent, dt)
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
-- we don't know yet if we even need anything from the controller other than the speed.
local _, _, _, maxSpeed = controller:getDriveData()
if maxSpeed then
self:setMaxSpeed(maxSpeed)
end
end
end
end
function AIDriveStrategyCourse:updateLowFrequencyPathfinder()
local _, _, _, maxSpeed = self.pathfinderController:getDriveData()
if maxSpeed then
self:setMaxSpeed(maxSpeed)
end
end
--- Raises a event for the controllers.
function AIDriveStrategyCourse:raiseControllerEvent(eventName, ...)
self:raiseControllerEventWithLambda(eventName, function () end, ...)
end
function AIDriveStrategyCourse:raiseControllerEventWithLambda(eventName, lambda, ...)
for _, controller in pairs(self.controllers) do
---@type ImplementController
if controller:isEnabled() then
if controller[eventName] then
lambda(controller[eventName](controller, ...))
end
end
end
end
function AIDriveStrategyCourse:raiseImplements()
--- Raises all implements, that are available for the giants field worker.
for _, implement in pairs(self.vehicle:getAttachedAIImplements()) do
implement.object:aiImplementEndLine()
end
self.vehicle:raiseStateChange(VehicleStateChange.AI_END_LINE)
--- Raises implements, that are not covered by giants.
self:raiseControllerEvent(self.onRaisingEvent)
end
function AIDriveStrategyCourse:lowerImplements()
self:debug('Lowering all implements')
--- Lowers all implements, that are available for the giants field worker.
for _, implement in pairs(self.vehicle:getAttachedAIImplements()) do
CpUtil.debugImplement(CpDebug.DBG_IMPLEMENTS, implement.object,'Lowering implement')
implement.object:aiImplementStartLine()
end
self.vehicle:raiseStateChange(VehicleStateChange.AI_START_LINE)
--- Lowers implements, that are not covered by giants.
self:raiseControllerEvent(self.onLoweringEvent)
end
--- Can the ai worker continue working?
---@return boolean
function AIDriveStrategyCourse:getCanContinueWork()
--- Not every implement, for example balers or bale wrapper are handled by the giants function.
for _, controller in pairs(self.controllers) do
---@type ImplementController
if not controller:canContinueWork() then
return false
end
end
return self.vehicle:getCanAIFieldWorkerContinueWork()
end
-----------------------------------------------------------------------------------------------------------------------
--- Static parameters (won't change while driving)
-----------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyCourse:setAllStaticParameters()
self.workWidth = self.vehicle:getCourseGeneratorSettings().workWidth:getValue()
self.reverser = AIReverseDriver(self.vehicle, self.ppc)
self.proximityController = ProximityController(self.vehicle, self:getProximitySensorWidth())
self.proximityController:registerIgnoreObjectCallback(self, self.ignoreBaleInFrontWithBalePusher)
-- let all controllers register an ignore object callback if they want
for _, controller in pairs(self.controllers) do
controller:registerIgnoreProximityObjectCallback(self.proximityController)
end
end
--- Find the foremost and rearmost AI marker
function AIDriveStrategyCourse:setFrontAndBackMarkers()
local markers = {}
local addMarkers = function(object, referenceNode)
self:debug('Finding AI markers of %s', CpUtil.getName(object))
local aiLeftMarker, aiRightMarker, aiBackMarker = WorkWidthUtil.getAIMarkers(object)
if aiLeftMarker and aiBackMarker and aiRightMarker then
local leftMarkerDistance = ImplementUtil.getDistanceToImplementNode(referenceNode, object, aiLeftMarker)
local rightMarkerDistance = ImplementUtil.getDistanceToImplementNode(referenceNode, object, aiRightMarker)
local backMarkerDistance = ImplementUtil.getDistanceToImplementNode(referenceNode, object, aiBackMarker)
table.insert(markers, leftMarkerDistance)
table.insert(markers, rightMarkerDistance)
table.insert(markers, backMarkerDistance)
self:debug('%s: left = %.1f, right = %.1f, back = %.1f', CpUtil.getName(object), leftMarkerDistance, rightMarkerDistance, backMarkerDistance)
end
end
local referenceNode = self.vehicle:getAIDirectionNode()
-- now go ahead and try to find the real markers
-- work areas of the vehicle itself
addMarkers(self.vehicle, referenceNode)
-- and then the work areas of all the implements
for _, implement in pairs(AIUtil.getAllAIImplements(self.vehicle)) do
addMarkers(implement.object, referenceNode)
end
if #markers == 0 then
-- make sure we always have a default front/back marker, placed on the direction node if nothing else found
table.insert(markers, 0)
table.insert(markers, 3)
end
-- now that we have all, find the foremost and the last
self.frontMarkerDistance, self.backMarkerDistance = 0, 0
local frontMarkerDistance, backMarkerDistance = -math.huge, math.huge
for _, d in pairs(markers) do
if d > frontMarkerDistance then
frontMarkerDistance = d
end
if d < backMarkerDistance then
backMarkerDistance = d
end
end
self.frontMarkerDistance = frontMarkerDistance
self.backMarkerDistance = backMarkerDistance
self:debug('front marker: %.1f, back marker: %.1f', frontMarkerDistance, backMarkerDistance)
end
--- Gets the front and back marker offset relative to the direction node. These markers define the front
--- and the back of the work area. When negative, they are behind the direction node, when positive, in front of it.
---@return number distance of the foremost work area from the direction node, negative when behind the direction node
---@return number distance of the rearmost work area from the direction node, negative when behind the direction node
function AIDriveStrategyCourse:getFrontAndBackMarkers()
if not self.frontMarkerDistance then
self:setFrontAndBackMarkers()
end
return self.frontMarkerDistance, self.backMarkerDistance
end
function AIDriveStrategyCourse:getWorkWidth()
return self.workWidth
end
--- Get the currently active course (temporary or not)
function AIDriveStrategyCourse:getCurrentCourse()
return self.ppc:getCourse() or self.course
end
function AIDriveStrategyCourse:update(dt)
self.ppc:update()
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()
local gx, _, gz = self.ppc:getGoalPointPosition()
return gx, gz, moveForwards, self.maxSpeed, 100
end
function AIDriveStrategyCourse:getReverseDriveData()
local gx, gz, _, maxSpeed = self.reverser:getDriveData()
if not gx then
-- simple reverse (not towing anything), just use PPC
gx, _, gz = self.ppc:getGoalPointPosition()
maxSpeed = self.settings.reverseSpeed:getValue()
end
return gx, gz, maxSpeed
end
-----------------------------------------------------------------------------------------------------------------------
--- Proximity
-----------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyCourse:getProximitySensorWidth()
-- a bit less as size.width always has plenty of buffer
return self.vehicle.size.width - 0.5
end
function AIDriveStrategyCourse:checkProximitySensors(moveForwards)
local _, _, _, maxSpeed = self.proximityController:getDriveData(self:getMaxSpeed(), moveForwards)
self:setMaxSpeed(maxSpeed)
end
--- Is vehicle close to the front or rear proximity sensors?
---@param vehicle table
---@return boolean, number true if vehicle is in proximity, distance of vehicle
function AIDriveStrategyCourse:isVehicleInProximity(vehicle)
return self.proximityController:isVehicleInRange(vehicle)
end
--- Ignoring bales in front when configured, bales back when not yet dropped from the baler
function AIDriveStrategyCourse:ignoreBaleInFrontWithBalePusher(object, vehicle, moveForwards)
if not object then
return
end
if object.isa and object:isa(Bale) and object.nodeId and entityExists(object.nodeId) then
-- this is a bale
if moveForwards and g_vehicleConfigurations:getRecursively(self.vehicle, 'ignoreBaleCollisionForward') then
-- when configured, ignore bales in front, for instance using a bale pusher
self:debugSparse('ignoring forward collision with bale')
return true
end
end
return false
end
-----------------------------------------------------------------------------------------------------------------------
--- Speed control
-----------------------------------------------------------------------------------------------------------------------
--- Set the maximum speed. The idea is that self.maxSpeed is reset at the beginning of every loop and
-- every function calls setMaxSpeed() and the speed will be set to the minimum
-- speed set in this loop.
function AIDriveStrategyCourse:setMaxSpeed(speed)
if self.maxSpeedUpdatedLoopIndex == nil or self.maxSpeedUpdatedLoopIndex ~= g_updateLoopIndex then
-- new loop, reset max speed. Always 0 if frozen
self.maxSpeed = (self.frozen or self:isBeingHeld()) and 0 or self.vehicle:getSpeedLimit(true)
self.maxSpeedUpdatedLoopIndex = g_updateLoopIndex
end
self.maxSpeed = math.min(self.maxSpeed, speed)
end
function AIDriveStrategyCourse:getMaxSpeed()
return self.maxSpeed or self.vehicle:getSpeedLimit(true)
end
--- Hold the vehicle (set speed to 0) temporary. This is meant to be used for other vehicles to coordinate movements,
--- for instance tell a vehicle it should not move as the other vehicle is driving around it.
---@param milliseconds number milliseconds to hold
---@param fuelSaveAllowed boolean enables the fuel save, while the vehicle is being held.
function AIDriveStrategyCourse:hold(milliseconds, fuelSaveAllowed)
if not self.held:get() then
self:debug('Hold requested for %.1f seconds', milliseconds / 1000)
end
self.held:set(true, milliseconds)
self.fuelSaveActiveWhileHeld = fuelSaveAllowed
end
--- Release a hold anytime, even before it is released automatically after the time given at hold()
function AIDriveStrategyCourse:unhold()
if self.held:get() then
self:debug("Hold reset")
end
self.held:reset()
self.fuelSaveActiveWhileHeld = false
end
--- Are we currently being held?
function AIDriveStrategyCourse:isBeingHeld()
return self.held:get()
end
--- Freeze (force speed to 0), but keep everything up and running otherwise, showing all debug
--- drawings, etc. This is for troubleshooting only. Unlike pausing the game, this still calls update() and
--- getDriveData() so all debug drawings remain visible during the freeze.
function AIDriveStrategyCourse:freeze()
self.frozen = true
end
function AIDriveStrategyCourse:unfreeze()
self.frozen = false
end
--- Slow down a bit towards the end of course or near direction changes, and later maybe where the turn radius is
--- small, unless we are reversing, as then (hopefully) we already have a slow speed set
function AIDriveStrategyCourse:limitSpeed()
if self.maxSpeed > self.settings.turnSpeed:getValue() and
not self.ppc:isReversing() and
(self.ppc:getCourse():isCloseToLastWaypoint(15) or
self.ppc:getCourse():isCloseToNextDirectionChange(15)) then
local maxSpeed = self.maxSpeed
self:setMaxSpeed(self.settings.turnSpeed:getValue())
self:debugSparse('speed %.1f limited to turn speed %.1f', maxSpeed, self.maxSpeed)
else
self:debugSparse('speed %.1f', self.maxSpeed)
end
end
--- Start a course at waypoint ix
---@param course Course
---@param ix number
function AIDriveStrategyCourse:startCourse(course, ix)
self:debug('Starting a course, at waypoint %d (of %d).', ix, course:getNumberOfWaypoints())
self.course = course
self.ppc:setCourse(self.course)
self.ppc:initialize(ix)
end
function AIDriveStrategyCourse:getFillLevelInfoText()
return InfoTextManager.NEEDS_UNLOADING
end
-----------------------------------------------------------------------------------------------------------------------
--- Event listeners
-----------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyCourse:onWaypointChange(ix, course)
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 PathfinderContext
---@param wasLastRetry boolean
---@param currentRetryAttempt number
function AIDriveStrategyCourse:onPathfindingFailed(controller, lastContext, wasLastRetry, currentRetryAttempt)
-- override
end
------------------------------------------------------------------------------------------------------------------------
--- Pathfinding
---------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyCourse:getAllowReversePathfinding()
return self.allowReversePathfinding and self.settings.allowReversePathfinding:getValue()
end
function AIDriveStrategyCourse:setPathfindingDoneCallback(object, func)
self.pathfindingDoneObject = object
self.pathfindingDoneCallbackFunc = func
end
function AIDriveStrategyCourse:updatePathfinding()
if self.pathfinder and self.pathfinder:isActive() then
self:setMaxSpeed(0)
local result = self.pathfinder:resume()
if result.done then
self.pathfindingDoneCallbackFunc(self.pathfindingDoneObject, result.path)
end
end
end
--- Create an alignment course between the current vehicle position and waypoint endIx of the course
---@param course Course the course to start
---@param ix number the waypoint where start the course
function AIDriveStrategyCourse:createAlignmentCourse(course, ix)
self:debug('Generate alignment course to waypoint %d', ix)
local alignmentCourse = AlignmentCourse(self.vehicle, self.vehicle:getAIDirectionNode(), self.turningRadius,
course, ix, math.min(-self.frontMarkerDistance, -1)):getCourse()
return alignmentCourse
end
-- remember a course to start
function AIDriveStrategyCourse:rememberCourse(course, ix)
self.rememberedCourse = course
self.rememberedCourseStartIx = ix
end
-- start a remembered course
function AIDriveStrategyCourse:startRememberedCourse()
self:startCourse(self.rememberedCourse, self.rememberedCourseStartIx)
end
function AIDriveStrategyCourse:getRememberedCourseAndIx()
return self.rememberedCourse, self.rememberedCourseStartIx
end
------------------------------------------------------------------------------------------------------------------------
--- Course helpers
---------------------------------------------------------------------------------------------------------------------------
--- Are we within distance meters of the last waypoint (measured on the course, not direct path)?
function AIDriveStrategyCourse:isCloseToCourseEnd(distance)
return self.course:getDistanceToLastWaypoint(self.ppc:getCurrentWaypointIx()) < distance
end
--- Are we within distance meters of the first waypoint (measured on the course, not direct path)?
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
---@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
--- if the user changed them during the run or the AI driver wants to add an offset
function AIDriveStrategyCourse:updateFieldworkOffset(course)
course:setOffset(self.settings.toolOffsetX:getValue() + (self.aiOffsetX or 0) + (self.tightTurnOffset or 0),
(self.aiOffsetZ or 0))
end
------------------------------------------------------------------------------------------------------------------------
--- Info texts
---------------------------------------------------------------------------------------------------------------------------
--- Registers info texts for specific states.
---@param infoText CpInfoTextElement
---@param states table
function AIDriveStrategyCourse:registerInfoTextForStates(infoText, states)
if self.registeredInfoTexts[infoText] == nil then
self.registeredInfoTexts[infoText] = states
end
end
--- Enables/disables based on the state.
function AIDriveStrategyCourse:updateInfoTexts()
for infoText, states in pairs(self.registeredInfoTexts) do
if states[self.state] then
self:setInfoText(infoText)
else
self:clearInfoText(infoText)
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:waitForFieldBoundary()
if self.vehicle:cpGetFieldPolygon() then
self:clearInfoText(InfoTextManager.WAITING_FOR_FIELD_BOUNDARY_DETECTION)
return true
else
self:setInfoText(InfoTextManager.WAITING_FOR_FIELD_BOUNDARY_DETECTION)
return false
end
end