-
-
Notifications
You must be signed in to change notification settings - Fork 74
Expand file tree
/
Copy pathAIDriveStrategyUnloadCombine.lua
More file actions
3092 lines (2773 loc) · 157 KB
/
Copy pathAIDriveStrategyUnloadCombine.lua
File metadata and controls
3092 lines (2773 loc) · 157 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
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_FS25)
Copyright (C) 2022 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/>.
]]--
--[[
How do we make sure the unloader does not collide with the combine?
1. ProximitySensor
The ProximitySensor is a generic AIDriver feature.
The combine has a proximity sensor on the back and will slow down and stop
if something is in range.
The unloader has a proximity sensor on the front to prevent running into the combine
and to swerve other vehicles in case of a head on collision for example.
In some states, for instance when unloading choppers, the tractor disables the generic
speed control as it has to drive very close to the chopper.
There is an additional proximity sensor dedicated to following the chopper. This has
all controlling features disabled.
2. Turns
The combine stops when discharging during a turn, so at the end of a row or headland turn
it won't start the turn until it is empty.
3. Combine Ready For Unload
The unloader can also ask the combine if it is ready to unload (isReadyToUnload()), as we
expect the combine to know best when it is going to perform some maneuvers.
4. Cooperative Collision Avoidance Using the TrafficController
This is currently screwed up...
---------------------------------------------
--- Unload target possibilities
---------------------------------------------
1. Combines working on an field(no chopper!)
2. Silo loader picking up fill types dropped to the ground(heap) on the field or near the field.
---------------------------------------------
--- Unload the loaded fill volume possibilities
---------------------------------------------
1. Auger wagon can unload to nearby trailers
2. Trailer can be sent to unload either with Autodrive or Giants unloader.
3. Unloading on the field, which means dropping it on the ground to create an heap.
]]--
--- Strategy to unload combines or stationary silo loaders.
---@class AIDriveStrategyUnloadCombine : AIDriveStrategyCourse
---@field combineUnloadStates table
---@field trailerUnloadStates table
---@field fieldUnloadStates table
AIDriveStrategyUnloadCombine = CpObject(AIDriveStrategyCourse)
-- when moving out of way of another vehicle, move at least so many meters
AIDriveStrategyUnloadCombine.minDistanceWhenMovingOutOfWay = 5
-- when moving out of way of another vehicle, move at most so many meters
AIDriveStrategyUnloadCombine.maxDistanceWhenMovingOutOfWay = 25
AIDriveStrategyUnloadCombine.safeManeuveringDistance = 30 -- distance to keep from a combine not ready to unload
AIDriveStrategyUnloadCombine.pathfindingRange = 5 -- won't do pathfinding if target is closer than this
-- The normal limit to apply a penalty for the pathfinder. This is relatively low to keep the unloader further
-- away from the fruit.
AIDriveStrategyUnloadCombine.maxFruitPercent = 10
AIDriveStrategyUnloadCombine.proximitySensorRange = 15
AIDriveStrategyUnloadCombine.maxDirectionDifferenceDeg = 35 -- under this angle the unloader considers itself aligned with the combine
-- Add a short straight section to align with the combine's course in case it is late for the rendezvous
AIDriveStrategyUnloadCombine.driveToCombineCourseExtensionLength = 10
AIDriveStrategyUnloadCombine.targetDistanceBehindChopper = 1
-- Developer hack: to check the class of an object one should use the is_a() defined in CpObject.lua.
-- However, when we reload classes on the fly during the development, the is_a() calls in other modules still
-- have the old class definition (for example CombineUnloadManager.lua) of this class and thus, is_a() fails.
-- Therefore, use this instead, this is safe after a reload.
AIDriveStrategyUnloadCombine.isACombineUnloadAIDriver = true
-- when calculating a course to a trailer, do not end the course right at the target fill node, instead
-- unloadTargetOffset meters before that. This allows for a little distance to stop after the tractor
-- reaches the last waypoint, and the logic in unloadAugerWagon() will move the rig to the exact position anyway.
AIDriveStrategyUnloadCombine.unloadTargetOffset = 1.5
--- Offset to apply at the goal marker, so we don't crash with an empty unloader waiting there with the same position.
AIDriveStrategyUnloadCombine.invertedGoalPositionOffset = -4.5
--- Field unload constants
AIDriveStrategyUnloadCombine.siloAreaOffsetFieldUnload = 2
AIDriveStrategyUnloadCombine.unloadCourseLengthFieldUnload = 50
--- Unload modes
AIDriveStrategyUnloadCombine.UNLOAD_TYPES = {
COMBINE = 1,
SILO_LOADER = 2
}
---------------------------------------------
--- State properties
---------------------------------------------
--[[
fuelSaveAllowed : boolean
collisionAvoidanceEnabled : boolean
proximityControllerDisabled : boolean
openCoverAllowed : boolean
moveablePipeDisabled : boolean
vehicle : table|nil
holdCombine : boolean
]]
---------------------------------------------
--- Shared states
---------------------------------------------
AIDriveStrategyUnloadCombine.myStates = {
IDLE = { fuelSaveAllowed = true }, --- Only allow fuel save, if the unloader is waiting for a combine.
WAITING_FOR_PATHFINDER = {},
MOVING_BACK_BEFORE_PATHFINDING = { pathfinderController = nil, pathfinderContext = nil }, -- there is an obstacle ahead, move back a bit so the pathfinder can succeed
--- States to maneuver away from combines and so on.
--- No need to be assigned to a combine!
MOVING_BACK = { vehicle = nil, holdCombine = false, denyBackupRequest = true },
MOVING_BACK_WITH_TRAILER_FULL = { vehicle = nil, holdCombine = false, denyBackupRequest = true }, -- moving back from a combine we just unloaded (not assigned anymore)
BACKING_UP_FOR_REVERSING_COMBINE = { vehicle = nil, denyBackupRequest = true }, -- reversing as long as the combine is reversing
MOVING_BACK_FOR_HEADLAND_TURN = { vehicle = nil, holdCombine = false, denyBackupRequest = true }, -- making room for the harvester performing a headland turn
MOVING_AWAY_FROM_OTHER_VEHICLE = { vehicle = nil, denyBackupRequest = true }, -- moving until we have enough space between us and an other vehicle
WAITING_FOR_MANEUVERING_COMBINE = {},
DRIVING_BACK_TO_START_POSITION_WHEN_FULL = {}, -- Drives to the start position with a trailer attached and gives control to giants or AD there.
HANDLE_CHOPPER_180_TURN = { reversing = false, denyBackupRequest = true },
HANDLE_CHOPPER_HEADLAND_TURN = { reversing = false, denyBackupRequest = true },
FOLLOW_CHOPPER_THROUGH_TURN = {}
}
-------------------------------------------------
--- Unloading of a combine or silo loader states
--- Needs an assigned vehicle to work!
-------------------------------------------------
AIDriveStrategyUnloadCombine.myCombineUnloadStates = {
DRIVING_TO_COMBINE = { collisionAvoidanceEnabled = true },
DRIVING_TO_MOVING_COMBINE = { collisionAvoidanceEnabled = true },
UNLOADING_MOVING_COMBINE = { openCoverAllowed = true },
UNLOADING_STOPPED_COMBINE = { openCoverAllowed = true },
}
---------------------------------------------
--- Unloading into trailer states
---------------------------------------------
AIDriveStrategyUnloadCombine.myTrailerUnloadStates = {
DRIVING_TO_SELF_UNLOAD = { collisionAvoidanceEnabled = true },
WAITING_FOR_AUGER_PIPE_TO_OPEN = {},
UNLOADING_AUGER_WAGON = {},
MOVING_TO_NEXT_FILL_NODE = { moveablePipeDisabled = true },
MOVING_AWAY_FROM_UNLOAD_TRAILER = { moveablePipeDisabled = true },
}
---------------------------------------------
--- Field unload states states
---------------------------------------------
AIDriveStrategyUnloadCombine.myFieldUnloadStates = {
DRIVE_TO_FIELD_UNLOAD_POSITION = { collisionAvoidanceEnabled = true },
WAITING_UNTIL_FIELD_UNLOAD_IS_ALLOWED = {},
PREPARE_FOR_FIELD_UNLOAD = {},
DRIVE_TO_REVERSE_FIELD_UNLOAD_POSITION = {},
REVERSING_TO_THE_FIELD_UNLOAD_HEAP = {},
UNLOADING_ON_THE_FIELD = { proximityControllerDisabled = true },
DRIVE_TO_FIELD_UNLOAD_PARK_POSITION = {},
}
--- Register all active unloaders here to access them fast.
AIDriveStrategyUnloadCombine.activeUnloaders = {}
function AIDriveStrategyUnloadCombine:init(task, job)
AIDriveStrategyCourse.init(self, task, job)
self.combineUnloadStates = CpUtil.initStates(self.combineUnloadStates, AIDriveStrategyUnloadCombine.myCombineUnloadStates)
self.trailerUnloadStates = CpUtil.initStates(self.trailerUnloadStates, AIDriveStrategyUnloadCombine.myTrailerUnloadStates)
self.fieldUnloadStates = CpUtil.initStates(self.fieldUnloadStates, AIDriveStrategyUnloadCombine.myFieldUnloadStates)
self.states = CpUtil.initStates(self.states, AIDriveStrategyUnloadCombine.myStates)
--- Copies all references to the self.states table
self.states = CpUtil.copyStates(self.states, self.combineUnloadStates)
self.states = CpUtil.copyStates(self.states, self.trailerUnloadStates)
self.states = CpUtil.copyStates(self.states, self.fieldUnloadStates)
self.state = self.states.WAITING_FOR_FIELD_BOUNDARY_DETECTION
self.debugChannel = CpDebug.DBG_UNLOAD_COMBINE
---@type ImplementController[]
self.controllers = {}
self.followingCourseOffset = 0
self.distanceToCombine = math.huge
self.distanceToFront = 0
self.combineToUnloadReversing = 0
self.doNotSwerveForVehicle = CpTemporaryObject()
self.justFinishedPathfindingForDistance = CpTemporaryObject()
self.vehicleInFrontOfUS = CpTemporaryObject()
self.vehicleRequestingBackUp = CpTemporaryObject()
self.driveUnloadNowRequested = CpTemporaryObject(false)
self.movingAwayDelay = CpTemporaryObject(false)
self.checkForTrailerToUnloadTo = CpTemporaryObject(true)
self.unloadTargetType = self.UNLOAD_TYPES.COMBINE
--- Register all active unloaders here to access them fast.
AIDriveStrategyUnloadCombine.activeUnloaders[self] = self.vehicle
end
function AIDriveStrategyUnloadCombine:delete()
if self.fieldUnloadPositionNode then
CpUtil.destroyNode(self.fieldUnloadPositionNode)
CpUtil.destroyNode(self.fieldUnloadTurnStartNode)
CpUtil.destroyNode(self.fieldUnloadTurnEndNode)
end
if self.invertedStartPositionMarkerNode then
CpUtil.destroyNode(self.invertedStartPositionMarkerNode)
end
self:releaseCombine()
AIDriveStrategyUnloadCombine.activeUnloaders[self] = nil
AIDriveStrategyCourse.delete(self)
end
------------------------------------------------------------------------------------------------------------------------
-- Start and initialization
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:startWithoutCourse()
-- to always have a valid course (for the traffic conflict detector mainly)
self.course = Course.createStraightForwardCourse(self.vehicle, 25)
self:startCourse(self.course, 1)
self:info('Starting combine unload')
for _, implement in pairs(self.vehicle:getAttachedImplements()) do
self:info(' - %s', CpUtil.getName(implement.object))
end
end
function AIDriveStrategyUnloadCombine:getGeneratedCourse(jobParameters)
return nil
end
function AIDriveStrategyUnloadCombine:setJobParameterValues(jobParameters)
self.jobParameters = jobParameters
if jobParameters.useFieldUnload:getValue() and not jobParameters.useFieldUnload:getIsDisabled() then
local fieldUnloadPosition = jobParameters.fieldUnloadPosition
if fieldUnloadPosition ~= nil and fieldUnloadPosition.x ~= nil and fieldUnloadPosition.z ~= nil and fieldUnloadPosition.angle ~= nil then
--- Valid field unload position found and allowed.
self.fieldUnloadPositionNode = CpUtil.createNode("Field unload position", fieldUnloadPosition.x, fieldUnloadPosition.z, fieldUnloadPosition.angle, nil)
self.fieldUnloadTurnStartNode = CpUtil.createNode("Reverse field unload turn start position", fieldUnloadPosition.x, fieldUnloadPosition.z, fieldUnloadPosition.angle, nil)
self.fieldUnloadTurnEndNode = CpUtil.createNode("Reverse field unload turn end position", fieldUnloadPosition.x, fieldUnloadPosition.z, fieldUnloadPosition.angle, nil)
self.unloadTipSideID = jobParameters.unloadingTipSide:getValue()
end
end
--- Setup the unload target mode.
if jobParameters.unloadTarget:getValue() == CpCombineUnloaderJobParameters.UNLOAD_COMBINE then
self.unloadTargetType = self.UNLOAD_TYPES.COMBINE
self:debug("Unload target is a combine.")
else
self.unloadTargetType = self.UNLOAD_TYPES.SILO_LOADER
self:debug("Unload target is a silo loader.")
end
self.useUnloadOnField = jobParameters.useFieldUnload:getValue() and not jobParameters.useFieldUnload:getIsDisabled()
self.useGiantsUnload = jobParameters.useGiantsUnload:getValue() and not jobParameters.useGiantsUnload:getIsDisabled()
end
--- Gets the unload target drive strategy target.
function AIDriveStrategyUnloadCombine:getUnloadTargetType()
return self.unloadTargetType
end
function AIDriveStrategyUnloadCombine:setAIVehicle(vehicle, jobParameters)
AIDriveStrategyCourse.setAIVehicle(self, vehicle)
self:setJobParameterValues(jobParameters)
self.reverser = AIReverseDriver(self.vehicle, self.ppc)
self.collisionAvoidanceController = CollisionAvoidanceController(self.vehicle, self)
self.proximityController = ProximityController(self.vehicle, self:getProximitySensorWidth())
self.proximityController:registerIsSlowdownEnabledCallback(self, AIDriveStrategyUnloadCombine.isProximitySpeedControlEnabled)
self.proximityController:registerBlockingVehicleListener(self, AIDriveStrategyUnloadCombine.onBlockingVehicle)
self.proximityController:registerIgnoreObjectCallback(self, AIDriveStrategyUnloadCombine.ignoreProximityObject)
-- this is for following a chopper. The reason we are not using the proximityController's forward looking
-- sensor is that it may be too high, and does not see the header of the chopper. Alternatively, we could
-- lower the proximityController. Also, this is a little wider to catch the chopper during turns.
self.followModeProximitySensor = WideForwardLookingProximitySensorPack(
self.vehicle, Markers.getFrontMarkerNode(self.vehicle), 10, 0.5, self:getProximitySensorWidth(),
{ 20, 15, 8, 5, 0, -5, -8, -15, -20 })
-- remove any course already loaded (for instance to not to interfere with the fieldworker proximity controller)
vehicle:resetCpCourses()
--- Target nodes for unloading into the trailer.
self.trailerNodes = SelfUnloadHelper:getTrailersTargetNodes(vehicle)
end
function AIDriveStrategyUnloadCombine:initializeImplementControllers(vehicle)
self.augerWagon, self.pipeController = self:addImplementController(vehicle, PipeController, Pipe, {}, nil)
self:debug('Auger wagon found: %s', self.augerWagon ~= nil)
self.trailer, self.trailerController = self:addImplementController(vehicle, TrailerController, Trailer, {}, nil)
self:addImplementController(vehicle, MotorController, Motorized, {}, nil)
self:addImplementController(vehicle, WearableController, Wearable, {}, nil)
self:addImplementController(vehicle, CoverController, Cover, {}, nil)
self:addImplementController(vehicle, FoldableController, Foldable, {})
end
function AIDriveStrategyUnloadCombine:isProximitySpeedControlEnabled()
return not (self.state == self.states.UNLOADING_MOVING_COMBINE and self.combineToUnload:getCpDriveStrategy():hasAutoAimPipe())
end
function AIDriveStrategyUnloadCombine:ignoreProximityObject(object, vehicle, moveForwards, hitTerrain)
return (self.state == self.states.UNLOADING_ON_THE_FIELD and hitTerrain) or
-- these states handle the proximity by themselves
(self.state == self.states.UNLOADING_MOVING_COMBINE and vehicle == self.combineToUnload) or
(self.state == self.states.HANDLE_CHOPPER_HEADLAND_TURN and vehicle == self.combineToUnload)
end
function AIDriveStrategyUnloadCombine:checkCollisionWarning()
if self.state.properties.collisionAvoidanceEnabled and
self.collisionAvoidanceController:isCollisionWarningActive() then
self:debugSparse('Collision warning, waiting...')
self:setMaxSpeed(0)
end
end
------------------------------------------------------------------------------------------------------------------------
-- Main loop
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:getDriveData(dt, vX, vY, vZ)
self:updateLowFrequencyImplementControllers()
-- if applicable, calculate on which side of an auto aim pipe we should be driving, once every loop
self:calculateAutoAimPipeOffsetX(self.combineToUnload)
local moveForwards = not self.ppc:isReversing()
local gx, gz
----------------------------------------------------------------
if not moveForwards then
local maxSpeed
gx, gz, maxSpeed = self:getReverseDriveData()
self:setMaxSpeed(maxSpeed)
else
gx, _, gz = self.ppc:getGoalPointPosition()
end
-- make sure if we have a combine we stay registered
if self.combineToUnload and self.combineToUnload:getIsCpActive() then
local strategy = self.combineToUnload:getCpDriveStrategy()
if strategy then
if strategy.registerUnloader then
strategy:registerUnloader(self)
else
-- combine may have been stopped and restarted, so CP is active again but not yet the combine strategy,
-- for instance it is now driving to work start, so it can't accept a registration
self:debug('Lost my combine')
self:startWaitingForSomethingToDo()
end
end
end
if self.combineToUnload == nil or not self.combineToUnload:getIsCpActive() then
if CpUtil.isStateOneOf(self.state, self.combineUnloadStates) then
end
end
if self:hasToWaitForAssignedCombine() then
--- Safety check to make sure a combine is assigned, when needed.
self:setMaxSpeed(0)
self:debugSparse("Combine to unload lost during unload, waiting for something todo.")
if self:isDriveUnloadNowRequested() then
self:debug('Drive unload now requested')
self:startUnloadingTrailers()
end
elseif self.state == self.states.WAITING_FOR_FIELD_BOUNDARY_DETECTION then
self:setMaxSpeed(0)
if self:waitForFieldBoundary() then
self:debug('Have field boundary now, checking positions.')
if self:isPositionOk() then
self.state = self.states.INITIAL
end
end
elseif self.state == self.states.INITIAL then
if not self.startTimer then
--- Only create one instance of the timer and wait until it finishes.
self.startTimer = Timer.createOneshot(50, function()
--- Pipe measurement seems to be buggy with a few over loaders, like bergman RRW 500,
--- so a small delay of 50 ms is inserted here before unfolding starts.
self.vehicle:raiseAIEvent("onAIFieldWorkerStart", "onAIImplementStart")
self.state = self.states.IDLE
self.startTimer = nil
end)
end
self:setMaxSpeed(0)
elseif self.state == self.states.IDLE then
-- nothing to do right now, wait for one of the following:
-- - combine calls
-- - user sends us to unload the trailer
-- - a trailer appears where we can unload our auger wagon if full
self:setMaxSpeed(0)
if self:isDriveUnloadNowRequested() then
self:debug('Drive unload now requested')
self:startUnloadingTrailers()
elseif self.checkForTrailerToUnloadTo:get() and self:getAllTrailersFull(self.settings.fullThreshold:getValue()) then
-- every now and then check if should attempt to unload our trailer/auger wagon
self.checkForTrailerToUnloadTo:set(false, 10000)
self:debug('Trailers over %d fill level', self.settings.fullThreshold:getValue())
self:startUnloadingTrailers()
end
elseif self.state == self.states.WAITING_FOR_PATHFINDER then
-- just wait for the pathfinder to finish
self:setMaxSpeed(0)
elseif self.state == self.states.DRIVING_TO_COMBINE then
self:driveToCombine()
elseif self.state == self.states.DRIVING_TO_MOVING_COMBINE then
self:driveToMovingCombine()
elseif self.state == self.states.UNLOADING_STOPPED_COMBINE then
local x, z = self:unloadStoppedCombine()
-- if driveBesideCombine() has a better goal point, use that, instead of the offset course
if x ~= nil then
gx, gz = x, z
end
elseif self.state == self.states.UNLOADING_MOVING_COMBINE then
local x, z
if self.combineToUnload:getCpDriveStrategy():hasAutoAimPipe() then
x, z = self:unloadMovingChopper()
else
x, z = self:unloadMovingCombine(dt)
end
-- if driveBesideCombine()/followChopper() has a better goal point, use that, instead of the offset course
if x ~= nil then
gx, gz = x, z
end
elseif self.state == self.states.WAITING_FOR_MANEUVERING_COMBINE then
self:waitForManeuveringCombine()
elseif self.state == self.states.BACKING_UP_FOR_REVERSING_COMBINE then
-- reversing combine asking us to move
self:backUpForReversingCombine()
elseif self.state == self.states.MOVING_AWAY_FROM_OTHER_VEHICLE then
-- someone is blocking us or we are blocking someone
self:moveAwayFromOtherVehicle()
elseif self.state == self.states.MOVING_BACK_FOR_HEADLAND_TURN then
self:makeRoomForCombineTurningOnHeadland()
elseif self.state == self.states.MOVING_BACK_WITH_TRAILER_FULL then
self:setMaxSpeed(self.settings.reverseSpeed:getValue())
-- drive back to have some room for the pathfinder
local _, dx, dz = self:getDistanceFromCombine(self.state.properties.vehicle)
-- drive back more if we are close to the harvester
if dz > ((math.abs(dx) < self.turningRadius) and 0 or -3) then
self:startUnloadingTrailers()
end
elseif self.state == self.states.MOVING_BACK_BEFORE_PATHFINDING then
self:setMaxSpeed(self.settings.reverseSpeed:getValue())
elseif self.state == self.states.MOVING_BACK then
self:setMaxSpeed(self.settings.reverseSpeed:getValue())
if self.state.properties.holdCombine then
self:debugSparse('Holding combine while backing up')
self.combineToUnload:getCpDriveStrategy():hold(1000)
end
-- drive back until the combine is in front of us
local _, _, dz = self:getDistanceFromCombine(self.state.properties.vehicle)
if dz > 0 then
self:debug('Stop backing up')
self:startWaitingForSomethingToDo()
end
elseif self.state == self.states.DRIVING_TO_SELF_UNLOAD then
self:driveToSelfUnload()
elseif self.state == self.states.WAITING_FOR_AUGER_PIPE_TO_OPEN then
self:waitForAugerPipeToOpen()
elseif self.state == self.states.UNLOADING_AUGER_WAGON then
moveForwards = self:unloadAugerWagon()
elseif self.state == self.states.MOVING_TO_NEXT_FILL_NODE then
moveForwards = self:moveToNextFillNode()
elseif self.state == self.states.MOVING_AWAY_FROM_UNLOAD_TRAILER then
self:moveAwayFromUnloadTrailer()
elseif self.state == self.states.DRIVING_BACK_TO_START_POSITION_WHEN_FULL then
self:setMaxSpeed(self:getFieldSpeed())
---------------------------------------------
--- Unloading on the field
---------------------------------------------
elseif self.state == self.states.DRIVE_TO_FIELD_UNLOAD_POSITION then
self:setMaxSpeed(self:getFieldSpeed())
elseif self.state == self.states.WAITING_UNTIL_FIELD_UNLOAD_IS_ALLOWED then
self:waitingUntilFieldUnloadIsAllowed()
elseif self.state == self.states.PREPARE_FOR_FIELD_UNLOAD then
self:prepareForFieldUnload()
elseif self.state == self.states.UNLOADING_ON_THE_FIELD then
self:setMaxSpeed(self.settings.reverseSpeed:getValue())
elseif self.state == self.states.DRIVE_TO_REVERSE_FIELD_UNLOAD_POSITION then
self:setMaxSpeed(self:getFieldSpeed())
elseif self.state == self.states.REVERSING_TO_THE_FIELD_UNLOAD_HEAP then
self:driveToReverseFieldUnloadHeap()
elseif self.state == self.states.HANDLE_CHOPPER_180_TURN then
local x, z = self:handleChopper180Turn()
if x ~= nil then
gx, gz = x, z
end
elseif self.state == self.states.HANDLE_CHOPPER_HEADLAND_TURN then
self:handleChopperHeadlandTurn()
elseif self.state == self.states.FOLLOW_CHOPPER_THROUGH_TURN then
self:followChopperThroughTurn()
elseif self.state == self.states.DRIVE_TO_FIELD_UNLOAD_PARK_POSITION then
self:setMaxSpeed(self:getFieldSpeed())
end
self:checkProximitySensors(moveForwards)
self:checkCollisionWarning()
return gx, gz, moveForwards, self.maxSpeed, 100
end
function AIDriveStrategyUnloadCombine:hasToWaitForAssignedCombine()
if CpUtil.isStateOneOf(self.state, self.combineUnloadStates) then
return self.combineToUnload == nil or not self.combineToUnload:getIsCpActive() or self.combineToUnload:getCpDriveStrategy() == nil
end
return false
end
---@param combine table
---@param combineDriver AIDriveStrategyCombineCourse
function AIDriveStrategyUnloadCombine:areThereAnyCombinesOrLoaderLeftoverOnTheField(combine, combineDriver)
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
if vehicle ~= combine and AIDriveStrategyCombineCourse.isActiveCpCombine(vehicle) then
local x, _, z = getWorldTranslation(combine.rootNode)
if self:isServingPosition(x, z, 10) then
--- At least one more combine oder loader is working on this field.
return true
end
end
end
return false
end
function AIDriveStrategyUnloadCombine:startWaitingForSomethingToDo()
if self.state ~= self.states.IDLE then
self:releaseCombine()
self.course = Course.createStraightForwardCourse(self.vehicle, 25)
self:setNewState(self.states.IDLE)
end
end
---@return table|nil the best node (of all the fill nodes on all trailers) to use to unload a harvester
function AIDriveStrategyUnloadCombine:getBestTargetNode()
local function isValidNode(targetNode)
local fillType = self.combineToUnload:getCpDriveStrategy():getFillType()
-- for some harvesters (DeWulf), fill type is unknown until they start working
if fillType ~= FillType.UNKNOWN and not targetNode.trailer:getFillUnitAllowsFillType(targetNode.fillUnitIx, fillType) then
self:debugSparse("Fill node %d of trailer %s doesn't accept fillType %s!",
targetNode.fillUnitIx, targetNode.trailer, g_fillTypeManager:getFillTypeNameByIndex(fillType))
return false
end
if targetNode.trailer:getFillUnitFreeCapacity(targetNode.fillUnitIx) <= 0 then
self:debugSparse("Fill node %d of trailer %s is completely filled!",
targetNode.fillUnitIx, targetNode.trailer)
return false
end
return true
end
if not self.trailerNodes then
self:debugSparse("Warning no valid trailer nodes found!")
return
end
local bestTargetNode
for _, targetNode in pairs(self.trailerNodes) do
if isValidNode(targetNode) then
bestTargetNode = targetNode
break
end
end
return bestTargetNode
end
---@return number|nil find the best target node and its distance from the pipe, > 0 when behind the pipe, < 0 when in
--- front of the pipe
function AIDriveStrategyUnloadCombine:getBestTargetNodeDistanceFromPipe()
local bestTargetNode = self:getBestTargetNode()
if bestTargetNode == nil then
return
end
if CpUtil.isVehicleDebugActive(self.vehicle) and CpDebug:isChannelActive(self.debugChannel) then
DebugUtil.drawDebugNode(bestTargetNode.node, 'target')
end
local _, offsetZ = self:getPipeOffset(self.combineToUnload)
local _, _, dz = localToLocal(bestTargetNode.node, self.combineToUnload:getAIDirectionNode(), 0, 0, -offsetZ)
return -dz
end
---@return number | nil, number | nil gx, gz world coordinates to steer to, instead of the PPC determined goal point (which is
--- calculated from the offset harvester course).
--- This goal point is calculated from the harvester's position. It is on a straight line parallel to the harvester,
--- under the pipe and look ahead distance ahead of the unloader
--- driveBesideCombine() creates this goal when approaching the harvester to align with the pipe better and faster than
--- just using the offset course waypoints.
function AIDriveStrategyUnloadCombine:driveBesideCombine()
local dz = self:getBestTargetNodeDistanceFromPipe()
if dz == nil then
return
end
local strategy = self.combineToUnload:getCpDriveStrategy()
-- use a factor to make sure we reach the pipe fast, but be more gentle while discharging
local factor = strategy:isDischarging() and 0.75 or 2
local combineSpeed = self.combineToUnload.lastSpeedReal * 3600
local speed = combineSpeed + CpMathUtil.clamp(dz * factor, -10, 15)
if dz > 0 and speed < 2 then
-- Giants does not like speeds under 2, it just stops. So if we calculated a small speed
-- like when the combine is stopped, but not there yet, make sure we set a speed which
-- actually keeps the unloader moving, otherwise we will never get there.
speed = 2
end
-- slow down while the pipe is unfolding to avoid crashing onto it
if strategy:isPipeMoving() then
speed = (math.min(speed, self.combineToUnload:getLastSpeed() + 2))
end
self:setMaxSpeed(math.max(0, speed))
self:renderText(0, 0.02, "%s: driveBesideCombine: dz = %.1f, speed = %.1f, factor = %.1f",
CpUtil.getName(self.vehicle), dz, speed, factor)
local gx, gy, gz
-- Calculate an artificial goal point relative to the harvester to align better when starting to unload
if dz > 5 then
_, _, dz = localToLocal(self.vehicle:getAIDirectionNode(), self:getPipeOffsetReferenceNode(), 0, 0, 0)
gx, gy, gz = localToWorld(self:getPipeOffsetReferenceNode(),
-- straight line parallel to the harvester, under the pipe, look ahead distance from the unloader
self:getPipeOffset(self.combineToUnload), 0, dz + self.ppc:getLookaheadDistance())
if CpUtil.isVehicleDebugActive(self.vehicle) and CpDebug:isChannelActive(self.debugChannel) then
-- show the goal point
DebugUtil.drawDebugGizmoAtWorldPos(gx, gy + 3, gz, 1, 0, 1, 0, 1, 0, "Unloader goal", false)
end
end
return gx, gz
end
------------------------------------------------------------------------------------------------------------------------
-- Are we stuck?
-- For some reason, we are not moving but the chopper needs us
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:isInDeadlock()
if self.combineToUnload then
local combineStrategy = self.combineToUnload:getCpDriveStrategy()
if self.inDeadlock == nil then
self.inDeadlock = CpDelayedBoolean()
end
return self.inDeadlock:get(combineStrategy:isWaitingForUnload() and AIUtil.isStopped(self.vehicle), 10000)
else
return false
end
end
------------------------------------------------------------------------------------------------------------------------
-- Unload chopper (always moving)
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:unloadMovingChopper()
-- recalculate offset, just in case
self.followingCourseOffset = self:getFollowingCourseOffset(self.combineToUnload)
self.followCourse:setOffset(self.followingCourseOffset, 0)
if self:changeToUnloadWhenTrailerFull() then
return
end
if self:isInDeadlock() then
self:debug('Deadlock situation detected while unloading moving chopper.')
self:startMovingBackFromCombine(self.states.MOVING_BACK, self.combineToUnload)
return
end
local combineStrategy = self.combineToUnload:getCpDriveStrategy()
local gx, gz = self:followChopper()
if combineStrategy:isTurning() and not combineStrategy:isFinishingRow() then
self:startChopperTurn(combineStrategy)
end
return gx, gz
end
------------------------------------------------------------------------------------------------------------------------
-- Drive with the chopper, avoiding fruit and staying in the reach of the pipe.
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:followChopper()
-- self.autoAimPipeOffsetX is set in getPipeOffset() to where we should be. If we are on the wrong side, we can't just
-- move the goal point to the correct side, as we need to duck behind the chopper, that is, moving
-- the goal point back first so the tractor gets behind the choppers back and then to the correct
-- side, and then forward again.
-- Normally, when driving beside the harvester, align the direction nodes
local dx, _, dz = localToLocal(Markers.getFrontMarkerNode(self.vehicle), self.combineToUnload:getAIDirectionNode(), 0, 0, 0)
-- use both proximity sensors front as they are at different heights, one may see the header, but not the
-- choppers high back...
local dFollowProxy = self.followModeProximitySensor:getClosestObjectDistanceAndRootVehicle()
local dProxy = self.proximityController:checkBlockingVehicleFront()
local speed
-- adjust speed to the harvester's speed
local sameDirection = CpMathUtil.isSameDirection(self.vehicle:getAIDirectionNode(), self.combineToUnload:getAIDirectionNode(), 45)
if sameDirection then
if math.abs(dx - self:getAutoAimPipeOffsetX()) > 1 then
-- if the difference between the current and desired offset is big, slow down, our reference point is
-- the back of the harvester
dz = dz + self:getCombinesMeasuredBackDistance()
end
speed = self.combineToUnload.lastSpeedReal * 3600 + CpMathUtil.clamp(
math.min(-dz, dFollowProxy - self.targetDistanceBehindChopper, dProxy - self.targetDistanceBehindChopper) * 2,
-10, 15)
else
-- not aligned with the chopper, drive forward to get closer, regardless of dz
speed = CpMathUtil.clamp(
math.min(dFollowProxy - self.targetDistanceBehindChopper, dProxy - self.targetDistanceBehindChopper) * 2,
0, self.settings.turnSpeed:getValue())
end
self:setMaxSpeed(speed)
local _, _, dzGoal = localToLocal(self.vehicle:getAIDirectionNode(), self.combineToUnload:getAIDirectionNode(), 0, 0, 0)
local gx, gy, gz = localToWorld(self.combineToUnload:getAIDirectionNode(),
-- straight line parallel to the harvester, under the pipe, look ahead distance from the unloader
self:getAutoAimPipeOffsetX(), 0, dzGoal + self.ppc:getLookaheadDistance())
if CpUtil.isVehicleDebugActive(self.vehicle) and CpDebug:isChannelActive(self.debugChannel) then
-- show the goal point
DebugUtil.drawDebugGizmoAtWorldPos(gx, gy + 4, gz, 0, 0, 1, 0, 1, 0, "Virtual goal", false)
self:renderDebugTableFromLists(
{ 'dz', 'dProxy', 'dFollowProxy', 'speed', 'autoAimOffsetX' },
{ dz, dProxy, dFollowProxy, speed, self:getAutoAimPipeOffsetX() }
)
end
return gx, gz
end
------------------------------------------------------------------------------------------------------------------------
-- Chopper turns
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:startChopperTurn(combineStrategy)
if combineStrategy:isTurningOnHeadland() then
self:debug('Start chopper headland turn')
self:startCourse(self.followCourse, combineStrategy:getTurnStartWpIx())
self:setNewState(self.states.HANDLE_CHOPPER_HEADLAND_TURN)
else
self:debug('Start chopper 180 turn')
self:setNewState(self.states.HANDLE_CHOPPER_180_TURN)
end
end
------------------------------------------------------------------------------------------------------------------------
-- Chopper turn 180
-- The default strategy here is to stop before reaching the end of the row and then wait for the combine
-- to finish the 180 turn. After it finished the turn, we drive forward a bit to make sure we are behind the
-- chopper and then continue on the chopper's fieldwork course with the appropriate offset without pathfinding.
--
-- If the combine says that it won't reverse during the turn (for example performs a wide turn because the
-- next row to work on is not adjacent the current row), we switch to 'follow chopper through the turn' mode
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:handleChopper180Turn()
if self:changeToUnloadWhenTrailerFull() then
return
end
if self.combineToUnload:getCpDriveStrategy():isTurningButNotEndingTurn() then
if self.combineToUnload:getCpDriveStrategy():isTurnForwardOnly() then
---@type Course
local turnCourse = self.combineToUnload:getCpDriveStrategy():getTurnCourse()
if turnCourse then
self:debug('Follow chopper through the turn')
self:startCourse(turnCourse:copy(self.vehicle), 1)
self:setNewState(self.states.FOLLOW_CHOPPER_THROUGH_TURN)
return
else
self:debugSparse('Chopper said turn is forward only but has no turn course')
end
else
self:debugSparse('Chopper said turn is not forward only, following without turn course')
-- in a lands, or spiral pattern the row start may be very far from the row end, and we only have
-- the normal course loaded, no field work course. So while following the chopper through the turn,
-- we may get too far from the previous and next waypoints (row end/row start), triggering the
-- off-track detection in PPC, which would stop the helper.
-- To avoid this, we disable it temporarily
self.ppc:disableStopWhenOffTrack(2000)
end
else
local _, _, dz = self:getDistanceFromCombine()
self:setMaxSpeed(self.settings.turnSpeed:getValue())
-- start the chopper course (and thus, turning towards it) only after we are behind it
if dz < -3 then
self:debug('now behind chopper, continue following chopper')
self:setNewState(self.states.UNLOADING_MOVING_COMBINE)
return
end
end
local speed, isReversing = self:handleChopperTurn(self.combineToUnload)
self:setMaxSpeed(speed)
if not isReversing then
-- when driving forward, we still follow the virtual goal near the harvester (and not the course waypoints)
return self:followChopper()
end
end
------------------------------------------------------------------------------------------------------------------------
-- Chopper turn on headland
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:handleChopperHeadlandTurn()
if self:changeToUnloadWhenTrailerFull() then
return
end
local speed, _ = self:handleChopperTurn(self.combineToUnload)
self:setMaxSpeed(speed)
--when the turn is finished, return to follow chopper
if not self:getCombineIsTurning() then
self:debug('Combine stopped turning, resuming follow course')
-- resume course beside combine
-- skip over the turn start waypoint as it will throw the PPC off course
self:startCourse(self.followCourse, self.combineCourse:skipOverTurnStart(self.combineCourse:getCurrentWaypointIx()))
self:setNewState(self.states.UNLOADING_MOVING_COMBINE)
end
end
--- Handle both 180 and headland turns. Monitor the harvester and if it changes to reverse, set up a straight
--- reverse course for the unloader to use when backing up. If it changes back to forward, set the unloader back on
--- the follow course.
--- Calculate a speed (for both forward and reverse) that makes sure the harvester does not crash into the unloader.
---@param harvester table
---@return number speed to set to stay away (but not too far) from the maneuvering harvester
---@return boolean if true, the harvester is reversing (and we are on a straight reverse course)
function AIDriveStrategyUnloadCombine:handleChopperTurn(harvester)
-- since we are taking care of staying away, ask the chopper to ignore us
harvester:getCpDriveStrategy():requestToIgnoreProximity(self.vehicle)
local d, dx, dz = self:getDistanceFromCombine(harvester)
local combineSpeed = harvester.lastSpeedReal * 3600
local speed, dReference
--if the chopper is reversing, drive backwards, otherwise forwards, always keeping the distance from the chopper
if AIUtil.isReversing(harvester) then
if not self.state.properties.reversing then
self:debug('Harvester reversing')
self.state.properties.reversing = true
local reverseCourse = Course.createStraightReverseCourse(self.vehicle, 20)
self:startCourse(reverseCourse, 1)
end
local sameDirection = CpMathUtil.isSameDirection(self.vehicle:getAIDirectionNode(), harvester:getAIDirectionNode(), 45)
-- stay closer when still discharging
if sameDirection then
-- reverse speed is controlled around combine's speed
dReference = harvester:getCpDriveStrategy():isDischarging() and dz or dz - 3
speed = combineSpeed + CpMathUtil.clamp(self.targetDistanceBehindChopper - dReference, -combineSpeed,
self.settings.reverseSpeed:getValue() * 1.5)
else
-- reverse speed only depends on distance from the combine, stop when at working width
speed = CpMathUtil.clamp(harvester:getCpDriveStrategy():getWorkWidth() - d, 0,
self.settings.reverseSpeed:getValue() * 1.5)
end
else
if self.state.properties.reversing then
self:debug('Harvester driving forward')
self.state.properties.reversing = false
self:startCourse(self.followCourse, self.combineCourse:getCurrentWaypointIx())
end
local turnSpeed = self.settings.turnSpeed:getValue()
-- get closer to the chopper when beside it
dReference = (math.abs(dx) > 3) and dz or d
speed = combineSpeed +
(CpMathUtil.clamp(dReference - self.targetDistanceBehindChopper, -turnSpeed, turnSpeed))
end
self:renderDebugTableFromLists(
{ 'reversing', 'd', 'dx', 'dz', 'speed' },
{ self.state.properties.reversing, d, dx, dz, speed }
)
return speed, self.state.properties.reversing
end
------------------------------------------------------------------------------------------------------------------------
-- Follow chopper through turn
-- here we drive the chopper's turn course carefully keeping our distance from the combine.
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:followChopperThroughTurn()
if self:changeToUnloadWhenTrailerFull() then
return
end
local d = self:getDistanceFromCombine()
local turnCourse = self.combineToUnload:getCpDriveStrategy():getTurnCourse()
if self.combineToUnload:getCpDriveStrategy():isTurning() and turnCourse ~= nil then
-- making sure we are never ahead of the chopper on the course (we both drive the same course), this
-- prevents the unloader cutting in front of the chopper when for example the unloader is on the
-- right side of the chopper and the chopper reaches a right turn.
if self.course:getCurrentWaypointIx() > turnCourse:getCurrentWaypointIx() then
self:setMaxSpeed(0)
end
-- follow course, make sure we are keeping distance from the chopper
local combineSpeed = (self.combineToUnload.lastSpeedReal * 3600)
local speed = math.max(self.settings.turnSpeed:getValue(), combineSpeed)
self:setMaxSpeed(speed)
self:renderText(0, 0.7, 'd = %.1f, speed = %.1f', d, speed)
else
self:debug('chopper is ending/ended turn, return to follow mode')
self:startCourse(self.followCourse, self.combineCourse:getCurrentWaypointIx())
self:setNewState(self.states.UNLOADING_MOVING_COMBINE)
end
end
------------------------------------------------------------------------------------------------------------------------
--- Calculate a virtual pipe offset for the unloader to drive beside the chopper based on which
--- side of the chopper is already harvested, or behind it if both sides have fruit.
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:calculateAutoAimPipeOffsetX(harvester)
local strategy = harvester and harvester:getCpDriveStrategy()
if strategy and strategy.hasAutoAimPipe and strategy:hasAutoAimPipe() then
local fruitLeft, fruitRight = strategy:getFruitAtSides()
local targetOffsetX, distanceBetweenVehicles = 0, (AIUtil.getWidth(harvester) + AIUtil.getWidth(self.vehicle)) / 2 + 1
-- we use 20% of the average as a threshold for significant difference
local fruitThreshold = 0.2 * 0.5 * (fruitLeft + fruitRight)
if strategy:isOnHeadland(1) then
-- on the first headland always drive behind the chopper
targetOffsetX = 0
elseif math.abs(fruitRight - fruitLeft) < fruitThreshold then
-- about the same amount of fruit on both sides
targetOffsetX = 0
elseif fruitLeft > fruitRight then
-- significantly more fruit on the left, drive to the right
targetOffsetX = -distanceBetweenVehicles
else
-- significantly more fruit on the right, drive to the left
targetOffsetX = distanceBetweenVehicles
end
if not self.autoAimPipeOffsetX then
-- Side offset from a chopper. We don't want this to jump from one side to the other abruptly
self.autoAimPipeOffsetX = CpSlowChangingObject(targetOffsetX, 0)
else
self.autoAimPipeOffsetX:confirm(targetOffsetX, 3000, 0.2)
end
end
return self:getAutoAimPipeOffsetX()
end
function AIDriveStrategyUnloadCombine:onWaypointPassed(ix, course)
if course:isLastWaypointIx(ix) then
self:onLastWaypointPassed()
end
end
------------------------------------------------------------------------------------------------------------------------
-- On last waypoint
------------------------------------------------------------------------------------------------------------------------
function AIDriveStrategyUnloadCombine:onLastWaypointPassed()
self:debug('Last waypoint passed')
if self.state == self.states.DRIVING_TO_COMBINE then
if self:isOkToStartUnloadingCombine() then
-- Right behind the combine, aligned, go for the pipe
self:startUnloadingCombine()
else
self:startWaitingForSomethingToDo()
end