This repository has been archived by the owner on May 23, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 526
/
FieldworkAIDriver.lua
1494 lines (1347 loc) · 59.7 KB
/
FieldworkAIDriver.lua
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
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--[[
This file is part of Courseplay (https://github.com/Courseplay/courseplay)
Copyright (C) 2018 Peter Vajko
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/>.
]]
--[[
Fieldwork AI Driver
Can follow a fieldworking course, perform turn maneuvers, turn on/off and raise/lower implements,
add adjustment course if needed.
]]
---@class FieldworkAIDriver : AIDriver
FieldworkAIDriver = CpObject(AIDriver)
FieldworkAIDriver.myStates = {
-- main states
ON_FIELDWORK_COURSE = {},
ON_UNLOAD_OR_REFILL_COURSE = {},
RETURNING_TO_FIRST_POINT = {},
ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE = {},
-- ON_FIELDWORK_COURSE substates
WORKING = {},
UNLOAD_OR_REFILL_ON_FIELD = {},
WAITING_FOR_UNLOAD_OR_REFILL ={}, -- while on the field
ON_CONNECTING_TRACK = {},
WAITING_FOR_LOWER = {},
WAITING_FOR_LOWER_DELAYED = {},
WAITING_FOR_STOP = {},
WAITING_FOR_WEATHER = {},
TURNING = {},
TEMPORARY = {},
}
-- Our class implementation does not call the constructor of base classes
-- through multiple level of inheritances therefore we must explicitly call
-- the base class ctr.
function FieldworkAIDriver:init(vehicle)
courseplay.debugVehicle(courseplay.DBG_AI_DRIVER,vehicle,'FieldworkAIDriver:init()')
AIDriver.init(self, vehicle)
self:initStates(FieldworkAIDriver.myStates)
self.debugChannel = courseplay.DBG_MODE_4
-- waypoint index on main (fieldwork) course where we aborted the work before going on
-- an unload/refill course
self.aiDriverData.continueFieldworkAtWaypoint = 1
-- force stop for unload/refill, for example by a tractor, otherwise the same as stopping because full or empty
self.heldForUnloadRefill = false
self.heldForUnloadRefillTimestamp = 0
-- stop and raise implements while refilling/unloading on field
self.stopImplementsWhileUnloadOrRefillOnField = true
-- duration of the last turn maneuver. This is a default value and the driver will measure
-- the actual turn times. Used to calculate the remaining fieldwork time
self.turnDurationMs = 20000
self.convoyCurrentDistance = 0
self.convoyCurrentPosition = 0
self.convoyTotalMembers = 0
end
function FieldworkAIDriver:setHudContent()
AIDriver.setHudContent(self)
courseplay.hud:setFieldWorkAIDriverContent(self.vehicle)
end
function FieldworkAIDriver.register()
-- TODO: maybe just build a table with all specs we want to handle
local strawHarvestBaleCollectSpec
AIImplement.getCanImplementBeUsedForAI = Utils.overwrittenFunction(AIImplement.getCanImplementBeUsedForAI,
function(self, superFunc)
if self.customEnvironment ~= nil then
strawHarvestBaleCollectSpec = _G[self.customEnvironment].StrawHarvestBaleCollect
end
-- if we have the Straw Harvest add on we want to handle the bale collector
if SpecializationUtil.hasSpecialization(BaleLoader, self.specializations) then
return true
elseif SpecializationUtil.hasSpecialization(BaleWrapper, self.specializations) then
return true
elseif SpecializationUtil.hasSpecialization(Pickup, self.specializations) then
return true
elseif SpecializationUtil.hasSpecialization(Cutter, self.specializations) then
-- this is to make the straw pickup headers for choppers work as they are
-- not recognized as AI Implements since they don't have AI markers
-- so they don't unfold on the AI start line event
return true
elseif strawHarvestBaleCollectSpec and SpecializationUtil.hasSpecialization(strawHarvestBaleCollectSpec, self.specializations) then
return true
elseif superFunc ~= nil then
return superFunc(self)
end
end)
-- Make sure the Giants helper can't be hired for implements which have no Giants AI functionality
AIVehicle.getCanStartAIVehicle = Utils.overwrittenFunction(AIVehicle.getCanStartAIVehicle,
function(self, superFunc)
if self.customEnvironment ~= nil then
strawHarvestBaleCollectSpec = _G[self.customEnvironment].StrawHarvestBaleCollect
end
-- Only the courseplay helper can handle bale loaders.
if AIDriverUtil.hasAIImplementWithSpecialization(self, BaleLoader) or
AIDriverUtil.hasAIImplementWithSpecialization(self, BaleWrapper) or
AIDriverUtil.hasAIImplementWithSpecialization(self, Pickup) then
return false
end
if strawHarvestBaleCollectSpec and AIDriverUtil.hasAIImplementWithSpecialization(self, strawHarvestBaleCollectSpec) then
return false
end
if superFunc ~= nil then
return superFunc(self)
end
end)
BaleLoaderAIDriver.register()
Pickup.onAIImplementStartLine = Utils.overwrittenFunction(Pickup.onAIImplementStartLine,
function(self, superFunc)
if superFunc ~= nil then superFunc(self) end
self:setPickupState(true)
end)
Pickup.onAIImplementEndLine = Utils.overwrittenFunction(Pickup.onAIImplementEndLine,
function(self, superFunc)
if superFunc ~= nil then superFunc(self) end
self:setPickupState(false)
end)
-- TODO: move these to another dedicated class for implements?
local PickupRegisterEventListeners = function(vehicleType)
print('## Courseplay: Registering event listeners for loader wagons.')
SpecializationUtil.registerEventListener(vehicleType, "onAIImplementStartLine", Pickup)
SpecializationUtil.registerEventListener(vehicleType, "onAIImplementEndLine", Pickup)
end
print('## Courseplay: Appending event listener for loader wagons.')
Pickup.registerEventListeners = Utils.appendedFunction(Pickup.registerEventListeners, PickupRegisterEventListeners)
-- User option "keepCurrentSteering"
CrabSteering.onAIImplementStart = Utils.overwrittenFunction(CrabSteering.onAIImplementStart,
function(self, superFunc)
if superFunc ~= nil and self.cp.settings.keepCurrentSteering:is(false) then superFunc(self) end
end)
end
--- Enable speed control and swerve on unload/refill course and when returning to the first point
function FieldworkAIDriver:isProximitySpeedControlEnabled()
return self.state == self.states.ON_UNLOAD_OR_REFILL_COURSE or
self.state == self.states.RETURNING_TO_FIRST_POINT or
self.state == self.states.ON_FIELDWORK_COURSE
end
function FieldworkAIDriver:isProximitySwerveEnabled()
return self.state == self.states.ON_UNLOAD_OR_REFILL_COURSE or
self.state == self.states.RETURNING_TO_FIRST_POINT
end
function FieldworkAIDriver:shouldStopAtEndOfCourse()
return true
end
--- Start the course and turn on all implements when needed
--- @param startingPoint StartingPointSetting at which waypoint to start the course
function FieldworkAIDriver:start(startingPoint)
self:debug('Starting in mode %d', self.mode)
self.ppc:registerListeners(self, 'onWaypointPassed', 'onWaypointChange')
self:setMarkers()
self:beforeStart()
-- time to lower all implements
self:findLoweringDurationMs()
-- any offset imposed by the driver itself (tight turns, end of course, etc.), additional to any
-- tool offsets
self.aiDriverOffsetX = 0
self.aiDriverOffsetZ = 0
self.workWidth = self.courseGeneratorSettings.workWidth:getAutoWorkWidth()
self.ppc:setNormalLookaheadDistance()
self:setUpAndStart(startingPoint)
end
function FieldworkAIDriver:setUpAndStart(startingPoint)
self:setUpCourses()
-- now that we have our unload/refill and fieldwork courses set up, see where to start
local closestFieldworkIx, dClosestFieldwork, closestFieldworkIxRightDirection, dClosestFieldworkRightDirection =
self.fieldworkCourse:getNearestWaypoints(AIDriverUtil.getDirectionNode(self.vehicle))
-- default to math.huge in case there isn't an unload refill course to make sure it only the fieldwork Ix is used
local closestUnloadRefillIx, dClosestUnloadRefill, closestUnloadRefillIxRightDirection, dClosestUnloadRefillRightDirection =
0, math.huge, 0, math.huge
if self.unloadRefillCourse then
closestUnloadRefillIx, dClosestUnloadRefill, closestUnloadRefillIxRightDirection, dClosestUnloadRefillRightDirection =
self.unloadRefillCourse:getNearestWaypoints(AIDriverUtil.getDirectionNode(self.vehicle))
end
local ix = self.course and self.course:getCurrentWaypointIx()
local startWithFieldwork
if startingPoint:is(StartingPointSetting.START_AT_NEAREST_POINT) then
if dClosestFieldwork < dClosestUnloadRefill then
ix = closestFieldworkIx
startWithFieldwork = true
self:debug('Starting at nearest waypoint %d on fieldwork course', ix)
else
ix = closestUnloadRefillIx
startWithFieldwork = false
self:debug('Starting at nearest waypoint %d on unload/refill course', ix)
end
end
if startingPoint:is(StartingPointSetting.START_AT_NEXT_POINT) then
if dClosestFieldworkRightDirection < dClosestUnloadRefillRightDirection then
ix = closestFieldworkIxRightDirection
startWithFieldwork = true
self:debug('Starting at nearest waypoint %d (best direction) on fieldwork course', ix)
else
ix = closestUnloadRefillIxRightDirection
startWithFieldwork = false
self:debug('Starting at nearest waypoint %d (best direction) on unload/refill course', ix)
end
end
if startingPoint:is(StartingPointSetting.START_AT_FIRST_POINT) then
self:debug('Starting at first waypoint')
ix = 1
startWithFieldwork = true
end
if startingPoint:is(StartingPointSetting.START_AT_CURRENT_POINT) then
-- use last fieldwork waypoint if we have the same fieldwork course as before
if self.fieldworkCourseHash == self.aiDriverData.lastFieldworkCourseHash then
self:debug('Starting at current waypoint %d', self.aiDriverData.lastFieldworkWaypointIx)
ix = self.aiDriverData.lastFieldworkWaypointIx
else
self:debug('Current waypoint not found, starting at first')
ix = 1
end
startWithFieldwork = true
end
if startWithFieldwork then
self:startFieldworkWithPathfinding(ix)
else
self:changeToUnloadOrRefill()
self:startCourseWithAlignment(self.unloadRefillCourse, ix)
end
end
function FieldworkAIDriver:startFieldworkWithAlignment(ix)
if self:startCourseWithAlignment(self.fieldworkCourse, ix) then
self.state = self.states.ON_FIELDWORK_COURSE
self.fieldworkState = self.states.TEMPORARY
else
self:changeToFieldwork()
end
end
function FieldworkAIDriver:startFieldworkWithPathfinding(ix)
if self:startCourseWithPathfinding(self.fieldworkCourse, ix) then
self.state = self.states.ON_FIELDWORK_COURSE
self.fieldworkState = self.states.TEMPORARY
else
self:debug('No path to fieldwork start found.')
self:changeToFieldwork()
end
end
--- Start course with pathfinding
--- Will find a path on a field avoiding fruit as much as possible from the
--- current position to waypoint ix of course and start driving.
--- When waypoint ix of course reached, switch to the course and continue driving.
---
--- If no path found will use an alignment course to reach waypoint ix of course.
---@param course Course
---@param ix number
---@return boolean true when a pathfinding successfully started or an alignment course was added
function FieldworkAIDriver:startCourseWithPathfinding(course, ix)
-- if the implement is in the front, generate a path so the implement will be at the goal
local zOffset = self.frontMarkerDistance > 0 and - self.frontMarkerDistance or 0
return AIDriver.startCourseWithPathfinding(self, course, ix, zOffset,0)
end
function FieldworkAIDriver:stop(msgReference)
self:stopWork()
if self.fieldworkCourse then
-- persist last fieldwork waypoint data (for 'start at current waypoint')
self.aiDriverData.lastFieldworkCourseHash = self.fieldworkCourse:getHash()
self.aiDriverData.lastFieldworkWaypointIx = self.fieldworkCourse:getCurrentWaypointIx()
end
AIDriver.stop(self, msgReference)
end
function FieldworkAIDriver:setWorkFinished(msgReference)
self:stopWork()
AIDriver.setWorkFinished(self, msgReference)
end
function FieldworkAIDriver:writeUpdateStream(streamId, connection, dirtyMask)
if self.vehicle.cp.settings.convoyActive:is(true) then
streamWriteInt32(streamId,self.convoyCurrentDistance)
streamWriteIntN(streamId,self.convoyCurrentPosition,5)
streamWriteIntN(streamId,self.convoyTotalMembers,5)
end
if self.fieldworkState and self.fieldworkState ~= self.fieldworkStateSend then
streamWriteBool(streamId,true)
streamWriteString(streamId,self.fieldworkState.name)
self.fieldworkStateSend = self.fieldworkState
else
streamWriteBool(streamId,false)
end
if self.timeRemaining ~= nil and self.timeRemaining ~= self.timeRemainingSend then
streamWriteBool(streamId,true)
streamWriteFloat32(streamId,self.timeRemaining)
self.timeRemainingSend = self.timeRemaining
else
streamWriteBool(streamId,false)
end
AIDriver.writeUpdateStream(self,streamId, connection, dirtyMask)
end
function FieldworkAIDriver:readUpdateStream(streamId, timestamp, connection)
if self.vehicle.cp.settings.convoyActive:is(true) then
self.convoyCurrentDistance=streamReadInt32(streamId)
self.convoyCurrentPosition=streamReadIntN(streamId,5)
self.convoyTotalMembers=streamReadIntN(streamId,5)
end
if streamReadBool(streamId) then
local nameState = streamReadString(streamId)
self.fieldworkState = self.states[nameState]
end
if streamReadBool(streamId) then
self.timeRemaining = streamReadFloat32(streamId)
end
AIDriver.readUpdateStream(self,streamId, timestamp, connection)
end
function FieldworkAIDriver:onWriteStream(streamId)
if self.fieldworkState then
streamWriteBool(streamId,true)
streamWriteString(streamId,self.fieldworkState.name)
else
streamWriteBool(streamId,false)
end
if self.timeRemaining ~= nil then
streamWriteBool(streamId,true)
streamWriteFloat32(streamId,self.timeRemaining)
else
streamWriteBool(streamId,false)
end
AIDriver.onWriteStream(self,streamId)
end
function FieldworkAIDriver:onReadStream(streamId)
if streamReadBool(streamId) then
local nameState = streamReadString(streamId)
self.fieldworkState = self.states[nameState]
end
if streamReadBool(streamId) then
self.timeRemaining = streamReadFloat32(streamId)
end
AIDriver.onReadStream(self,streamId)
end
function FieldworkAIDriver:drive(dt)
-- TODO: this is also called in UnloadableFieldworkAIDriver
courseplay:updateFillLevelsAndCapacities(self.vehicle)
if self.state == self.states.ON_FIELDWORK_COURSE then
if self:driveFieldwork(dt) then
-- driveFieldwork is driving, no need for AIDriver
return
end
elseif self.state == self.states.ON_UNLOAD_OR_REFILL_COURSE then
local giveUpControl = self:driveUnloadOrRefill(dt)
if giveUpControl then
return
end
elseif self.state == self.states.RETURNING_TO_FIRST_POINT then
self:setSpeed(self:getFieldSpeed())
self.triggerHandler.fillableObject = nil
elseif self.state == self.states.ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE then
-- AutoDrive is driving, don't call AIDriver.drive()
return
end
self:setRidgeMarkers()
self:resetUnloadOrRefillHold()
AIDriver.drive(self, dt)
self:measureTurnTime()
end
-- Hold for unload (or refill) for example a combine can be asked by a an unloading tractor
-- to stop and wait. Must be called in every loop to keep waiting because it will automatically be
-- reset and the vehicle restarted. This way the users don't explicitly need to call resumeAfterUnloadOrRefill()
function FieldworkAIDriver:holdForUnloadOrRefill()
self.heldForUnloadRefill = true
self.heldForUnloadRefillTimestamp = g_updateLoopIndex
end
function FieldworkAIDriver:resumeAfterUnloadOrRefill()
self.heldForUnloadRefill = false
end
function FieldworkAIDriver:resetUnloadOrRefillHold()
if g_updateLoopIndex > self.heldForUnloadRefillTimestamp + 10 then
self:resumeAfterUnloadOrRefill()
end
end
--- Doing the fieldwork (headlands or up/down rows, including the turns)
---@return boolean true if driveFieldwork() is driving (no need to call ALDriver.drive())
function FieldworkAIDriver:driveFieldwork(dt)
local iAmDriving = false
self:updateFieldworkOffset()
if self.fieldworkState == self.states.WAITING_FOR_LOWER_DELAYED then
-- getCanAIVehicleContinueWork() seems to return false when the implement being lowered/raised (moving) but
-- true otherwise. Due to some timing issues it may return true just after we started lowering it, so this
-- here delays the check for another cycle.
self.fieldworkState = self.states.WAITING_FOR_LOWER
elseif self.fieldworkState == self.states.WAITING_FOR_LOWER then
if self.vehicle:getCanAIVehicleContinueWork() then
self:debug('all tools ready, start working')
self.fieldworkState = self.states.WORKING
self:setSpeed(self:getWorkSpeed())
else
self:debugSparse('waiting for all tools to lower')
self:setSpeed(0)
self:checkFillLevels()
end
elseif self.fieldworkState == self.states.WORKING then
self:work()
elseif self.fieldworkState == self.states.UNLOAD_OR_REFILL_ON_FIELD then
self:driveFieldworkUnloadOrRefill()
elseif self.fieldworkState == self.states.TEMPORARY then
self:setSpeed(self:getFieldSpeed())
elseif self.fieldworkState == self.states.ON_CONNECTING_TRACK then
self:setSpeed(self:getFieldSpeed())
elseif self.fieldworkState == self.states.WAITING_FOR_WEATHER then
self:setSpeed(0)
self:checkWeather()
elseif self.fieldworkState == self.states.TURNING and self.aiTurn then
iAmDriving = self.aiTurn:drive(dt)
end
return iAmDriving
end
--- Do the actual fieldwork. This is extracted here so derived classes can use the
--- existing start/end work mechanisms to lower/raise start/stop implements and only
--- need to implement the actual work themselves.
function FieldworkAIDriver:work()
self:setSpeed(self:getWorkSpeed())
self:manageConvoy()
self:checkWeather()
self:checkFillLevels()
end
function FieldworkAIDriver:getNominalSpeed()
if self.state == self.states.ON_FIELDWORK_COURSE then
return self:getWorkSpeed()
else
return self:getRecordedSpeed()
end
end
function FieldworkAIDriver:checkFillLevels(isWaitingForRefill)
if not self:allFillLevelsOk(isWaitingForRefill) or self.heldForUnloadRefill then
self:stopAndChangeToUnload()
end
end
function FieldworkAIDriver:stopAndChangeToUnload()
if self.unloadRefillCourse and not self.heldForUnloadRefill then
self:rememberWaypointToContinueFieldwork()
self:debug('at least one tool is empty/full, aborting work at waypoint %d.', self.aiDriverData.continueFieldworkAtWaypoint or -1)
self:changeToUnloadOrRefill()
self:startCourseWithPathfinding(self.unloadRefillCourse, 1)
else
if self.vehicle.spec_autodrive and self.vehicle.cp.settings.autoDriveMode:useForUnloadOrRefill() then
-- Switch to AutoDrive when enabled
self:rememberWaypointToContinueFieldwork()
self:stopWork()
self:foldImplements()
self.state = self.states.ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE
self:debug('passing the control to AutoDrive to run the unload/refill course.')
--- Make sure trigger handler is disabled, while autodrive is driving.
self.triggerHandler:disableFillTypeLoading()
self.triggerHandler:disableFuelLoading()
self.vehicle.spec_autodrive:StartDrivingWithPathFinder(self.vehicle, self.vehicle.ad.mapMarkerSelected, self.vehicle.ad.mapMarkerSelected_Unload, self, FieldworkAIDriver.onEndCourse, nil);
else
-- otherwise we'll
self:changeToFieldworkUnloadOrRefill()
end;
end
end
function FieldworkAIDriver:isFuelLevelOk()
if self.fieldworkState == self.states.WORKING and self:getFuelLevelPercentage() < 15 then
self:stopAndRefuel()
end
return AIDriver.isFuelLevelOk(self)
end
function FieldworkAIDriver:stopAndRefuel()
if self.vehicle.spec_autodrive and self.vehicle.cp.settings.autoDriveMode:useForUnloadOrRefill() and self.vehicle.spec_autodrive.getSetting("autoRefuel", self.vehicle) then
-- Switch to AutoDrive when enabled
self:rememberWaypointToContinueFieldwork()
self:stopWork()
self:foldImplements()
self.state = self.states.ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE
--- Make sure the trigger handler is deactivated, while autodrive is driving.
self.triggerHandler:disableFuelLoading()
self.triggerHandler:disableFillTypeLoading()
self:debug('passing the control to AutoDrive to run the fuel refill course.')
self.vehicle.spec_autodrive:StartDrivingWithPathFinder(self.vehicle, self.vehicle.ad.mapMarkerSelected, -2, self, FieldworkAIDriver.onEndCourse, nil);
end
end
---@return boolean true if unload took over the driving
function FieldworkAIDriver:driveUnloadOrRefill(dt)
if self.course:isTemporary() then
-- use the courseplay speed limit until we get to the actual unload course fields (on alignment/temporary)
self:setSpeed(self:getFieldSpeed())
else
-- just drive normally
self:setSpeed(self:getRecordedSpeed())
end
-- except when in reversing, then always use reverse speed
if self.ppc:isReversing() then
self:setSpeed(self.vehicle.cp.speeds.reverse or self.vehicle.cp.speeds.crawl)
end
return false
end
--- Full during fieldwork
function FieldworkAIDriver:changeToFieldworkUnloadOrRefill()
self.fieldworkState = self.states.UNLOAD_OR_REFILL_ON_FIELD
if self.stopImplementsWhileUnloadOrRefillOnField then
self.fieldworkUnloadOrRefillState = self.states.WAITING_FOR_STOP
else
self.fieldworkUnloadOrRefillState = self.states.WAITING_FOR_UNLOAD_OR_REFILL
end
end
--- Stop for unload/refill while driving the fieldwork course
function FieldworkAIDriver:driveFieldworkUnloadOrRefill()
-- don't move while empty
self:setSpeed(0)
if self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_STOP then
-- wait until we stopped before raising the implements
if self:isStopped() then
self:debug('implements raised, stop')
self:stopWork()
self.fieldworkUnloadOrRefillState = self.states.WAITING_FOR_UNLOAD_OR_REFILL
end
elseif self.fieldworkUnloadOrRefillState == self.states.WAITING_FOR_UNLOAD_OR_REFILL then
if g_updateLoopIndex % 5 == 0 then --small delay, to make sure no more fillLevel change is happening
if self:allFillLevelsOk(true) and not self.heldForUnloadRefill then
self:debug('unloaded, continue working')
-- not full/empty anymore, maybe because Refilling to a trailer, go back to work
self:clearInfoText(self:getFillLevelInfoText())
self:changeToFieldwork()
end
end
end
end
function FieldworkAIDriver:changeToFieldwork()
self:debug('change to fieldwork')
self.state = self.states.ON_FIELDWORK_COURSE
self.fieldworkState = self.states.WAITING_FOR_LOWER
self:startWork()
self:refreshHUD();
end
function FieldworkAIDriver:changeToUnloadOrRefill()
self:debug('changing to unload/refill course (%d waypoints)', self.unloadRefillCourse:getNumberOfWaypoints())
self:stopWork()
self:foldImplements()
self:enableCollisionDetection()
self.state = self.states.ON_UNLOAD_OR_REFILL_COURSE
end
function FieldworkAIDriver:onNextCourse()
if self.state == self.states.ON_FIELDWORK_COURSE then
if self.fieldworkState == self.states.TURNING then
self:debug('onNextCourse: a turn course ended')
-- a turn just ended, at this point all implements should already be lowered as the turn end course ended
-- but just in case, lower them now
self:lowerImplements()
self.fieldworkState = self.states.WORKING
else
self:debug('onNextCourse: not a turn course ended')
self:changeToFieldwork()
end
end
end
function FieldworkAIDriver:onEndCourse()
if self.state == self.states.ON_UNLOAD_OR_REFILL_COURSE or
self.state == self.states.ON_UNLOAD_OR_REFILL_WITH_AUTODRIVE then
-- unload/refill course ended, return to fieldwork
self.triggerHandler:enableFuelLoading()
self:debug('AI driver in mode %d continue fieldwork at %d/%d waypoints', self:getMode(), self.aiDriverData.continueFieldworkAtWaypoint, self.fieldworkCourse:getNumberOfWaypoints())
self:startFieldworkWithPathfinding(self.aiDriverData.continueFieldworkAtWaypoint)
elseif self.state == self.states.RETURNING_TO_FIRST_POINT then
AIDriver.onEndCourse(self)
else
self:debug('Fieldwork AI driver in mode %d ending fieldwork course', self:getMode())
if self:shouldReturnToFirstPoint() then
self:debug('Returning to first point')
if self:driveToPointWithPathfinding(self.fieldworkCourse:getWaypoint(1)) then
-- pathfinding was successful, drive back to first point
self.state = self.states.RETURNING_TO_FIRST_POINT
self:raiseImplements()
-- Should we fold Implement when work is done and we drive to start point?
if self.vehicle.cp.settings.foldImplementAtEnd:is(true) then
self:foldImplements()
end
else
-- no path or too short, stop here.
AIDriver.onEndCourse(self)
-- (onEndCourse calls stopWork which raises the implements, fold must be called after that)
-- TODO: add an option to enable fold implement on work end and make it part of stopWork()
self:foldImplements()
end
else
AIDriver.onEndCourse(self)
-- Should we fold Implement when work is done ?
if self.vehicle.cp.settings.foldImplementAtEnd:is(true) then
self:foldImplements()
end
end
end
end
--Check if we need to release the driver completely
function FieldworkAIDriver:onEndCourseFinished()
if self.vehicle.cp.settings.returnToFirstPoint:isReleaseDriverActive() then
self:debug("returnToFirstPoint => Stopped Driver completely")
self.shouldBeReleasedOnceEntered = true
end
AIDriver.onEndCourseFinished(self)
end
--release the driver completely once entered
function FieldworkAIDriver:shouldDriverBeReleased()
if (self.vehicle:getIsEntered() or self.vehicle:getIsControlled()) and self.shouldBeReleasedOnceEntered then
self.shouldBeReleasedOnceEntered = nil
courseplay.onStopCpAIDriver(self.vehicle,AIVehicle.STOP_REASON_REGULAR)
end
end
function FieldworkAIDriver:onWaypointPassed(ix)
if self.state == self.states.ON_FIELDWORK_COURSE then
if self.fieldworkState == self.states.WORKING then
-- check for transition to connecting track, make sure we've been on it for a few waypoints already
-- to avoid raising the implements too soon, this can be a problem with long implements not yet reached
-- the end of the headland track while the tractor is already on the connecting track
if self.course:isOnConnectingTrack(self.course:getCurrentWaypointIx()) and self.course:isOnConnectingTrack(ix) and self.course:isOnConnectingTrack(ix - 2) then
-- reached a connecting track (done with the headland, move to the up/down row or vice versa),
-- raise all implements while moving
self:debug('on a connecting track now, raising implements.')
self:raiseImplements()
self.fieldworkState = self.states.ON_CONNECTING_TRACK
end
end
if self.fieldworkState ~= self.states.TEMPORARY and self.course:isOnConnectingTrack(ix) then
-- passed a connecting track waypoint
-- check transition from connecting track to the up/down rows
-- we are close to the end of the connecting track, transition back to the up/down rows with
-- an alignment course
local d, firstUpDownWpIx = self.course:getDistanceToFirstUpDownRowWaypoint(ix)
self:debug('up/down rows start in %s meters.', tostring(d))
-- (no alignment if there is a turn generated here)
if d < self.settings.turnDiameter:get() * 2 and firstUpDownWpIx and not self.course:isTurnEndAtIx(firstUpDownWpIx) then
self:debug('End connecting track, start working on up/down rows (waypoint %d) with alignment course if needed.', firstUpDownWpIx)
self:startFieldworkWithAlignment(firstUpDownWpIx)
end
end
end
AIDriver.onWaypointPassed(self,ix)
end
function FieldworkAIDriver:onWaypointChange(ix)
self:debug('onWaypointChange %d, connecting: %s, temp: %s',
ix, tostring(self.course:isOnConnectingTrack(ix)), tostring(self.states == self.states.TEMPORARY))
if self.state == self.states.ON_FIELDWORK_COURSE then
self:updateRemainingTime(ix)
self:calculateTightTurnOffset()
self:debug('Tight turn offset = %.1f', self.tightTurnOffset)
self.aiDriverOffsetZ = 0
if self.fieldworkState == self.states.ON_CONNECTING_TRACK then
if not self.course:isOnConnectingTrack(ix) then
-- reached the end of the connecting track, back to work
self:debug('connecting track ended, back to work, first lowering implements.')
self:changeToFieldwork()
end
end
if self.fieldworkState == self.states.TEMPORARY then
-- band aid to make sure we have our implements lowered by the time we end the
-- temporary course
-- TODO: fix this and also PlowAIDriver:startWork()
if ix == self.course:getNumberOfWaypoints() then
self:debug('temporary (alignment) course is about to end, start work')
self:startWork()
end
-- towards the end of the field course make sure the implement reaches the last waypoint
-- TODO: this needs refactoring, for now don't do this for temporary courses like a turn as it messes up reversing
elseif ix > self.course:getNumberOfWaypoints() - 3 and not self.course:isTemporary() then
if self.frontMarkerDistance then
self:debug('adding offset (%.1f front marker) to make sure we do not miss anything when the course ends', self.frontMarkerDistance)
self.aiDriverOffsetZ = -self.frontMarkerDistance
end
elseif not self.course:isOnHeadland(ix) and self.course:isOnHeadland(ix + 1) then
self:debug('last row waypoint before switching to the headland')
self:finishRow(ix)
end
if self.fieldworkState ~= self.states.TURNING and self.course:isTurnStartAtIx(ix) then
self:startTurn(ix)
end
end
-- update the legacy waypoint counter on the HUD
if self.state == self.states.ON_FIELDWORK_COURSE or self.states.ON_UNLOAD_OR_REFILL_COURSE then
courseplay:setWaypointIndex(self.vehicle, self.ppc:getCurrentOriginalWaypointIx())
end
end
function FieldworkAIDriver:onTowedImplementPassedWaypoint(ix)
self:debug('Implement passed waypoint %d', ix)
end
--- Should we return to the first point of the course after we are done?
function FieldworkAIDriver:shouldReturnToFirstPoint()
-- TODO: implement and check setting in course or HUD
if self.vehicle.cp.settings.returnToFirstPoint:isReturnToStartActive() then
self:debug('Returning to first point.')
return true
else
self:debug('Not returning to first point.')
return false
end
end
--- Pass on self.speed set elsewhere to the AIDriver.
function FieldworkAIDriver:getSpeed()
local speed = AIDriver.getSpeed(self)
-- as long as other CP components mess with the cruise control we need to reset this, for example after
-- a turn
self.vehicle:setCruiseControlMaxSpeed(speed)
return speed
end
--- Start the actual work. Lower and turn on implements
function FieldworkAIDriver:startWork()
self:debug('Starting work: turn on and lower implements.')
StartStopWorkEvent:sendStartEvent(self.vehicle)
-- send the event first and _then_ lower otherwise it sometimes does not turn it on
self.vehicle:raiseAIEvent("onAIStart", "onAIImplementStart")
self.vehicle:requestActionEventUpdate()
self:startEngineIfNeeded()
self:lowerImplements(self.vehicle)
end
--- Stop working. Raise and stop implements
function FieldworkAIDriver:stopWork()
self:debug('Ending work: turn off and raise implements.')
StartStopWorkEvent:sendStopEvent(self.vehicle)
self:raiseImplements()
self.vehicle:raiseAIEvent("onAIEnd", "onAIImplementEnd")
self.vehicle:requestActionEventUpdate()
self:clearRemainingTime()
end
-- is the fill level ok to continue?
function FieldworkAIDriver:areFillLevelsOk()
-- implement specifics in the derived classes
return true
end
--- Set up the main (fieldwork) course and the unload/refill course and initial state
-- Currently, the legacy CP code just dumps all loaded courses to vehicle.Waypoints so
-- now we have to figure out which of that is the actual fieldwork course and which is the
-- refill/unload part.
-- This should better be handled by the course management though and should be refactored.
function FieldworkAIDriver:setUpCourses()
local nWaits = 0
local endFieldCourseIx = 0
for i, wp in ipairs(self.vehicle.Waypoints) do
if wp.wait then
nWaits = nWaits + 1
-- the second wp with the wait attribute is the end of the field course (assuming
-- the field course has been loaded first.
if nWaits == 2 then
endFieldCourseIx = i
break
end
end
end
if #self.vehicle.Waypoints > endFieldCourseIx and endFieldCourseIx ~= 0 then
self:debug('Course with %d waypoints set up, there seems to be an unload/refill course starting at waypoint %d',
#self.vehicle.Waypoints, endFieldCourseIx + 1)
---@type Course
self.fieldworkCourse = Course(self.vehicle, self.vehicle.Waypoints, false, 1, endFieldCourseIx)
---@type Course
if #self.vehicle.Waypoints - endFieldCourseIx > 2 then
self.unloadRefillCourse = Course(self.vehicle, self.vehicle.Waypoints, false, endFieldCourseIx + 1, #self.vehicle.Waypoints)
else
self:debug('Unload/refill course too short, ignoring')
end
else
self:debug('Course with %d waypoints set up, there seems to be no unload/refill course', #self.vehicle.Waypoints)
self.fieldworkCourse = Course(self.vehicle, self.vehicle.Waypoints, false, 1, #self.vehicle.Waypoints)
self.unloadRefillCourse = nil
end
-- get a signature of the course now, before offsetting it so can compare for the convoy
self.fieldworkCourseHash = self.fieldworkCourse:getHash()
-- apply the current tool offset to the fieldwork part (multitool offset is added by calculateOffsetCourse when needed)
self.fieldworkCourse:setOffset(self.vehicle.cp.settings.toolOffsetX:get(), self.vehicle.cp.settings.toolOffsetZ:get())
-- TODO: consolidate the working width calculation and usage, this is currently an ugly mess
self.fieldworkCourse:setWorkWidth(self.vehicle.cp.courseWorkWidth or self.courseGeneratorSettings.workWidth:getAutoWorkWidth())
local laneNumber = self.courseGeneratorSettings.multiTools.laneNumber:get()
if self.vehicle.cp.courseGeneratorSettings.multiTools:get() > 1 then
self:debug('Calculating offset course for position %d of %d', laneNumber,
self.vehicle.cp.courseGeneratorSettings.multiTools:get())
self.fieldworkCourse = self.fieldworkCourse:calculateOffsetCourse(
self.vehicle.cp.courseGeneratorSettings.multiTools:get(), laneNumber,
self.fieldworkCourse.workWidth / self.vehicle.cp.courseGeneratorSettings.multiTools:get(),
self.vehicle.cp.settings.symmetricLaneChange:is(true))
end
end
function FieldworkAIDriver:setRidgeMarkers()
-- no ridge markers with multitools to avoid collisions.
if self.vehicle.cp.settings.ridgeMarkersAutomatic:is(false) or
self.vehicle.cp.courseGeneratorSettings.multiTools:get() > 1 then return end
local active = self.state == self.states.ON_FIELDWORK_COURSE and self.fieldworkState ~= self.states.TURNING
for _, workTool in ipairs(self.vehicle.cp.workTools) do
-- yes, another Giants typo. And not sure why implements with no ridge markers even have the spec_ridgeMarker
if workTool.spec_ridgeMarker and workTool.spec_ridgeMarker.numRigdeMarkers > 0 then
local state = active and self.course:getRidgeMarkerState(self.ppc:getCurrentWaypointIx()) or 0
if workTool.spec_ridgeMarker.ridgeMarkerState ~= state then
self:debug('Setting ridge markers to %d', state)
workTool:setRidgeMarkerState(state)
end
end
end
end
--- We already set the offsets on the course at start, this is to update those values
-- if the user changed them during the run or the AI driver wants to add an offset
function FieldworkAIDriver:updateFieldworkOffset()
-- (as lua passes tables by reference, we can directly change self.fieldworkCourse even if we passed self.course
-- to the PPC to drive)
self.fieldworkCourse:setOffset(self.vehicle.cp.settings.toolOffsetX:get() + self.aiDriverOffsetX + (self.tightTurnOffset or 0),
self.vehicle.cp.settings.toolOffsetZ:get() + self.aiDriverOffsetZ)
end
function FieldworkAIDriver:hasSameCourse(otherVehicle)
if otherVehicle.cp.driver and otherVehicle.cp.driver.fieldworkCourseHash then
return self.fieldworkCourseHash == otherVehicle.cp.driver.fieldworkCourseHash
else
return false
end
end
function FieldworkAIDriver:getProgress()
return self.fieldworkCourse:getProgress()
end
--- When working in a group (convoy), do I have to hold so I don't get too close to the
-- other vehicles in front of me?
--- We can't just use the waypoint index as each vehicle in the convoy has its own course
--- generated and for instance on the headland the vehicles on the inside will have less
--- waypoints, so we operate with progress percentage
function FieldworkAIDriver:manageConvoy()
if not self.vehicle.cp.settings.convoyActive:is(true) then return false end
--get my position in convoy and look for the closest combine
local position = 1
local total = 1
local closestDistance = math.huge
for _, otherVehicle in pairs(CpManager.activeCoursePlayers) do
if otherVehicle ~= self.vehicle and otherVehicle.cp.settings.convoyActive:is(true) and self:hasSameCourse(otherVehicle) then
local myProgress, myWpIx = self:getProgress()
local length = self.fieldworkCourse:getLength()
local otherProgress, otherWpIx = otherVehicle.cp.driver:getProgress()
self:debugSparse(
'convoy: my progress at waypoint %d is %.3f%%, %s progress at waypoint %d is %.3f%%, 100%% %d m',
myWpIx, myProgress * 100, nameNum(otherVehicle), otherWpIx, otherProgress * 100, length)
total = total + 1
if myProgress < otherProgress then
position = position + 1
local distance = (otherProgress - myProgress) * length
if distance < closestDistance then
closestDistance = distance
end
self:debugSparse('convoy: my position %d, calculated distance from %s is %.3f m',
position, nameNum(otherVehicle), distance)
end
end
end
-- stop when I'm too close to the combine in front of me
if position > 1 then
if closestDistance < self.vehicle.cp.settings.convoyMinDistance:get() then
self:debugSparse('too close (%.1f m < %.1f) to other vehicles in convoy, holding.',
closestDistance, self.vehicle.cp.settings.convoyMinDistance:get())
self:setSpeed(0)
self:overrideAutoEngineStop()
end
else
closestDistance = 0
end
self.convoyCurrentDistance=closestDistance
self.convoyCurrentPosition=position
self.convoyTotalMembers=total
end
-- Although raising the AI start/stop events supposed to fold/unfold the implements, it does not always happen.
-- So use these to explicitly do so
function FieldworkAIDriver:unfoldImplements()
for _,workTool in pairs(self.vehicle.cp.workTools) do
if courseplay:isFoldable(workTool) then
local isFolding, isFolded, isUnfolded = courseplay:isFolding(workTool)
if not isUnfolded and workTool:getIsFoldAllowed(workTool.cp.realUnfoldDirection) then
self:debug('Unfolding %s', workTool:getName())
workTool:setFoldDirection(workTool.cp.realUnfoldDirection)
end
end
end
end
function FieldworkAIDriver:foldImplements()
for _,workTool in pairs(self.vehicle.cp.workTools) do
if courseplay:isFoldable(workTool) then
local isFolding, isFolded, isUnfolded = courseplay:isFolding(workTool)
if not isFolded and workTool:getIsFoldAllowed(-workTool.cp.realUnfoldDirection) then
self:debug('Folding %s', workTool:getName())
workTool:setFoldDirection(-workTool.cp.realUnfoldDirection)
end
end
end
end
function FieldworkAIDriver:clearRemainingTime()
self.timeRemaining = nil
end
function FieldworkAIDriver:getRemainingTime()
return self.timeRemaining
end
function FieldworkAIDriver:updateRemainingTime(ix)
if self.state == self.states.ON_FIELDWORK_COURSE then
local dist, turns = self.course:getRemainingDistanceAndTurnsFrom(ix)
local turnTime = turns * self.turnDurationMs / 1000
self.timeRemaining = math.max(0, dist / (self:getWorkSpeed() / 3.6) + turnTime)
self:debug('Distance to go: %.1f; Turns left: %d; Time left: %ds', dist, turns, self.timeRemaining)
else
self:clearRemainingTime()
end
end
function FieldworkAIDriver:measureTurnTime()
local isTurning = self.state == self.states.ON_FIELDWORK_COURSE and self.fieldworkState == self.states.TURNING
if self.wasTurning and not isTurning then
-- end of turn
if self.turnStartedAt then
-- use sliding average to smooth jumps
self.turnDurationMs = (self.turnDurationMs + self.vehicle.timer - self.turnStartedAt) / 2
self.realTurnDurationMs = self.vehicle.timer - self.turnStartedAt
self:debug('Measured turn duration is %.0f ms', self.turnDurationMs)
end
elseif not self.wasTurning and isTurning then
-- start of turn
self.turnStartedAt = self.vehicle.timer
end
self.wasTurning = isTurning
end
function FieldworkAIDriver:checkWeather()
if self.vehicle.getIsThreshingAllowed and not self.vehicle:getIsThreshingAllowed() then