-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathunit_dynamic_avoidance_ex.lua
1762 lines (1667 loc) · 102 KB
/
unit_dynamic_avoidance_ex.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
local versionName = "v2.2"
--------------------------------------------------------------------------------
--
-- file: cmd_dynamic_Avoidance.lua
-- brief: a collision avoidance system
-- using: "non-Linear Dynamic system approach to modelling behavior" -SiomeGoldenstein, Edward Large, DimitrisMetaxas
-- code: Msafwan
--
-- Licensed under the terms of the GNU GPL, v2 or later.
--
--------------------------------------------------------------------------------
function widget:GetInfo()
return {
name = "Dynamic Avoidance System",
desc = versionName .. " Avoidance AI behaviour for constructor, cloakies, ground combat unit and gunships",
author = "msafwan",
date = "March 17, 2012",
license = "GNU GPL, v2 or later",
layer = 20,
enabled = false -- loaded by default?
}
end
--------------------------------------------------------------------------------
-- Functions:
local spGetTeamUnits = Spring.GetTeamUnits
local spGetGroundHeight = Spring.GetGroundHeight
local spGiveOrderToUnit =Spring.GiveOrderToUnit
local spGetMyTeamID = Spring.GetMyTeamID
local spIsUnitAllied = Spring.IsUnitAllied
local spGetUnitPosition =Spring.GetUnitPosition
local spGetUnitDefID = Spring.GetUnitDefID
local spGetUnitSeparation = Spring.GetUnitSeparation
local spGetUnitDirection =Spring.GetUnitDirection
local spGetUnitsInRectangle =Spring.GetUnitsInRectangle
local spGetVisibleUnits = Spring.GetVisibleUnits
local spGetCommandQueue = Spring.GetCommandQueue
local spGetUnitIsDead = Spring.GetUnitIsDead
local spGetGameSeconds = Spring.GetGameSeconds
local spGetFeaturePosition = Spring.GetFeaturePosition
local spValidFeatureID = Spring.ValidFeatureID
local spGetPlayerInfo = Spring.GetPlayerInfo
local spGetUnitStates = Spring.GetUnitStates
local spGetUnitTeam = Spring.GetUnitTeam
local spSendLuaUIMsg = Spring.SendLuaUIMsg
local spGetUnitLastAttacker = Spring.GetUnitLastAttacker
local spGetUnitHealth = Spring.GetUnitHealth
local spGetUnitWeaponState = Spring.GetUnitWeaponState
local spGetUnitShieldState = Spring.GetUnitShieldState
local spGetUnitIsStunned = Spring.GetUnitIsStunned
local spGetGameFrame = Spring.GetGameFrame
local spSendCommands = Spring.SendCommands
local CMD_STOP = CMD.STOP
local CMD_ATTACK = CMD.ATTACK
local CMD_GUARD = CMD.GUARD
local CMD_INSERT = CMD.INSERT
local CMD_REMOVE = CMD.REMOVE
local CMD_MOVE = CMD.MOVE
local CMD_OPT_INTERNAL = CMD.OPT_INTERNAL
local CMD_OPT_SHIFT = CMD.OPT_SHIFT
--local spRequestPath = Spring.RequestPath
local mathRandom = math.random
--local mathMax = math.max
--local spGetUnitSensorRadius = Spring.GetUnitSensorRadius
--------------------------------------------------------------------------------
-- Constant:
-- Switches:
local turnOnEcho =0 --Echo out all numbers for debugging the system (default = 0)
local activateAutoReverseG=1 --activate a one-time-reverse-command when unit is about to collide with an enemy (default = 0)
local activateImpatienceG=0 --auto disable auto-reverse & half the 'distanceCONSTANT' after 6 continuous auto-avoidance (3 second). In case the unit stuck (default = 0)
-- Graph constant:
local distanceCONSTANTunitG = 410 --increase obstacle awareness over distance. (default = 410 meter, ie: ZK's stardust range)
local safetyMarginCONSTANTunitG = 0.175 --obstacle graph offset (a "safety margin" constant). Offset the obstacle effect: to prefer avoid torward more left or right (default = 0.175 radian)
local smCONSTANTunitG = 0.175 -- obstacle graph offset (a "safety margin" constant). Offset the obstacle effect: to prefer avoid torward more left or right (default = 0.175 radian)
local aCONSTANTg = {math.pi/10 , math.pi/4} -- attractor graph; scale the attractor's strenght. Less equal to a lesser turning toward attraction(default = math.pi/10 radian (MOVE),math.pi/4 (GUARD & ATTACK)) (max value: math.pi/2).
local obsCONSTANTg = {math.pi/10, math.pi/4} -- obstacle graph; scale the obstacle's strenght. Less equal to a lesser turning away from avoidance(default = math.pi/10 radian (MOVE), math.pi/4 (GUARD & ATTACK)).
--aCONSTANTg Note: math.pi/4 is equal to about 45 degrees turning (left or right). aCONSTANTg is the maximum amount of turning toward target and the actual turning depend on unit's direction. Activated by 'graphCONSTANTtrigger[1]'
--an antagonist to aCONSTANg (obsCONSTANTg or obstacle graph) also use math.pi/4 (45 degree left or right) but actual maximum value varies depend on number of enemy, but already normalized. Activated by 'graphCONSTANTtrigger[2]'
local windowingFuncMultG = 1 --? (default = 1 multiplier)
local normalizeObsGraphG = false --// if 'true': normalize turn angle to a maximum of "obsCONSTANTg", if 'false': allow turn angle to grow as big as it can (depend on number of enemy, limited by "maximumTurnAngleG").
-- Obstacle/Target competetive interaction constant:
local cCONSTANT1g = {0.01,1,2} --attractor constant; effect the behaviour. ie: selection between 4 behaviour state. (default = 0.01x (All), 1x (Cloakies)) (behaviour:(MAINTAIN USER's COMMAND)|(IGNORE USER's COMMAND))
local cCONSTANT2g = {0.01,1,2} --repulsor constant; effect behaviour. (default = 0.01x (All), 1x (Cloakies)) (behaviour:(MAINTAIN USER's COMMAND)|(IGNORE USER's COMMAND))
local gammaCONSTANT2and1g = {0.05,0.05,0.05} -- balancing constant; effect behaviour. . (default = 0.05x (All), 0.05x (Cloakies))
local alphaCONSTANT1g = {500,0.4,0.4} -- balancing constant; effect behaviour. (default = 500x (All), 0.4x (Cloakies)) (behaviour:(MAINTAIN USER's COMMAND)|(IGNORE USER's COMMAND))
--Move Command constant:
local halfTargetBoxSize = {400, 0, 185, 50} --the distance from a target which widget should de-activate (default: MOVE = 400m (ie:800x800m box/2x constructor range), RECLAIM/RESSURECT=0 (always flee), REPAIR=185 (1x constructor's range), GUARD = 50 (arbitrary))
local cMD_DummyG = 248 --a fake command ID to flag an idle unit for pure avoidance. (arbitrary value, change if conflict with existing command)
local dummyIDg = "[]" --fake id for Lua Message to check lag (prevent processing of latest Command queue if server haven't process previous command yet; to avoid messy queue) (arbitrary value, change if conflict with other widget)
--Angle constant:
--http://en.wikipedia.org/wiki/File:Degree-Radian_Conversion.svg
local noiseAngleG =math.pi/36 --(default is pi/36 rad); add random angle (range from 0 to +-math.pi/36) to the new angle. To prevent a rare state that contribute to unit going straight toward enemy
local collisionAngleG=math.pi/36 --(default is pi/6 rad) a "field of vision" (range from 0 to +-math.pi/366) where auto-reverse will activate
local fleeingAngleG=math.pi/4 --(default is pi/4 rad) angle of enemy (range from 0 to +-math.pi/4) where fleeing enemy is considered (to de-activate avoidance to perform chase). Set to 0 to de-activate.
local maximumTurnAngleG = math.pi --safety measure. Prevent overturn (eg: 360+xx degree turn)
--pi is 180 degrees
--Update constant:
local doCalculation_then_gps_delayG = 0.25 --elapsed second (Wait) before gathering preliminary data for issuing command (default: 0.25 second)
local gps_then_DoCalculation_delayG = 0.25 --elapsed second (Wait) before issuing new command (default: 0.25 second)
-- Distance or velocity constant:
local timeToContactCONSTANTg= doCalculation_then_gps_delayG + gps_then_DoCalculation_delayG --time scale for move command; to calculate collision calculation & command lenght (default = 0.5 second). Will change based on user's Ping
local safetyDistanceCONSTANTg=205 --range toward an obstacle before unit auto-reverse (default = 205 meter, ie: half of ZK's stardust range) reference:80 is a size of BA's solar
local extraLOSRadiusCONSTANTg=205 --add additional distance for unit awareness over the default LOS. (default = +200 meter radius, ie: to 'see' radar blip)
local velocityScalingCONSTANTg=1 --scale command lenght. (default= 1 multiplier)
local velocityAddingCONSTANTg=10 --add or remove command lenght (default = 0 meter/second)
--Engine based wreckID correction constant:
local wreckageID_offset_multiplier = 0 --for Spring 0.82 this is 1500
local wreckageID_offset_initial = 32000 --for Spring 0.82 this is 4500
--curModID = upper(Game.modShortName)
--Weapon Reload and Shield constant:
local reloadableWeaponCriteriaG = 0.5 --second at which reload time is considered high enough to be a "reload-able". ie: 0.5second
local criticalShieldLevelG = 0.5 --percent at which shield is considered low and should activate avoidance. ie: 50%
local minimumRemainingReloadTimeG = 0.9 --seconds before actual reloading finish which avoidance should de-activate. ie: 0.9 second before finish
local secondPerGameFrameG = 0.5/15 --engine depended second-per-frame (for calculating remaining reload time). ie: 0.0333 second-per-frame or 0.5sec/15frame
--------------------------------------------------------------------------------
--Variables:
local unitInMotionG={} --store unitID
local skippingTimerG={0,0, echoTimestamp=0, networkDelay=0, sumOfAllNetworkDelay=0, sumCounter=0}
local commandIndexTableG= {} --store latest widget command for comparison
local myTeamID_gbl=-1
local myPlayerID=-1
local gaiaTeamID = Spring.GetGaiaTeamID()
local surroundingOfActiveUnitG={} --store value for transfer between function. Store obstacle separation, los, and ect.
local cycleG=1 --first execute "GetPreliminarySeparation()"
local wreckageID_offset=0
local roundTripComplete= true --variable for detecting network lag, prevent messy overlapping command queuing
local attackerG= {} --for recording last attacker
local commandTTL_G = {} --for recording command's age. To check for expiration
--------------------------------------------------------------------------------
--Methods:
---------------------------------Level 0
options_path = 'Game/Unit AI/Dynamic Avoidance' --//for use 'with gui_epicmenu.lua'
options_order = {'enableCons','enableCloaky','enableGround','enableGunship','enableReturnToBase'}
options = {
enableCons = {
name = 'Enable for constructors',
type = 'bool',
value = true,
desc = 'Enable constructor\'s avoidance feature. Constructor will return to base when given area-reclaim/area-ressurect, and partial avoidance while having build/repair/reclaim queue',
},
enableCloaky = {
name = 'Enable for cloakies',
type = 'bool',
value = true,
desc = 'Enable cloakies\' avoidance feature. Cloakable bots will avoid enemy when given move order',
},
enableGround = {
name = 'Enable for ground units',
type = 'bool',
value = true,
desc = 'Enable for ground units. All ground unit will avoid enemy while outside camera view OR when reloading, but units with hold position state is excluded',
},
enableGunship = {
name = 'Enable for gunships',
type = 'bool',
value = true,
desc = 'Enable gunship\'s avoidance feature. Gunship avoid enemy while outside camera view OR when reloading, but units with hold position state is excluded.',
},
-- enableAmphibious = {
-- name = 'Enable for amphibious',
-- type = 'bool',
-- value = true,
-- desc = 'Enable amphibious unit\'s avoidance feature (including Commander, and submarine). Unit avoid enemy while outside camera view OR when reloading, but units with hold position state is excluded..',
-- },
enableReturnToBase = {
name = "Find base",
type = 'bool',
value = true,
desc = "Allow constructor to return to base when having area-reclaim/area-ressurect command, else it will return to center of the circle when retreating. This function enabled and used \'Receive Indicator\' widget",
OnChange = function(self)
if self.value==true then
spSendCommands("luaui enablewidget Receive Units Indicator")
end
end,
},
}
function widget:Initialize()
skippingTimerG.echoTimestamp = spGetGameSeconds()
myPlayerID=Spring.GetMyPlayerID()
local _, _, spec = Spring.GetPlayerInfo(myPlayerID)
if spec then widgetHandler:RemoveWidget() return false end
myTeamID_gbl= spGetMyTeamID()
--count players to offset the ID of wreckage
local playerIDList= Spring.GetPlayerList()
local numberOfPlayers=#playerIDList
for i=1,numberOfPlayers do
local _,_,spectator,_,_,_,_,_,_=spGetPlayerInfo(playerIDList[i])
if spectator then numberOfPlayers=numberOfPlayers-1 end
end
wreckageID_offset=wreckageID_offset_initial+ (numberOfPlayers-2)*wreckageID_offset_multiplier
if (turnOnEcho == 1) then Spring.Echo("myTeamID_gbl(Initialize)" .. myTeamID_gbl) end
end
function widget:PlayerChanged(playerID)
if Spring.GetSpectatingState() then widgetHandler:RemoveWidget() end
--count players to offset the ID of wreckage
local playerIDList= Spring.GetPlayerList()
local numberOfPlayers=#playerIDList
for i=1,numberOfPlayers do
local _,_,spectator,_,_,_,_,_,_=spGetPlayerInfo(playerIDList[i])
if spectator then numberOfPlayers=numberOfPlayers-1 end
end
wreckageID_offset=wreckageID_offset_initial+ (numberOfPlayers-2)*wreckageID_offset_multiplier
end
--execute different function at different timescale
function widget:Update()
-------retrieve global table, localize global table
local commandIndexTable=commandIndexTableG
local unitInMotion = unitInMotionG
local surroundingOfActiveUnit=surroundingOfActiveUnitG
local cycle = cycleG
local skippingTimer = skippingTimerG
local attacker = attackerG
local commandTTL = commandTTL_G
-----
local now=spGetGameSeconds()
if (now >= skippingTimer[1]) then --wait until 'skippingTimer[1] second', then do "RefreshUnitList()"
if (turnOnEcho == 1) then Spring.Echo("-----------------------RefreshUnitList") end
unitInMotion, attacker, commandTTL=RefreshUnitList(attacker, commandTTL) --create unit list
local projectedDelay=ReportedNetworkDelay(myPlayerID, 1.1) --create list every 1.1 second, or every each second of lag.
skippingTimer[1]=now+projectedDelay --wait until next 'skippingTimer[1] second'
if (turnOnEcho == 1) then Spring.Echo("-----------------------RefreshUnitList") end
end
if (now >=skippingTimer[2] and cycle==1) and roundTripComplete then --wait until 'skippingTimer[2] second', and wait for 'LUA message received', and wait for 'cycle==1', then do "GetPreliminarySeparation()"
if (turnOnEcho == 1) then Spring.Echo("-----------------------GetPreliminarySeparation") end
surroundingOfActiveUnit,commandIndexTable=GetPreliminarySeparation(unitInMotion,commandIndexTable, attacker)
cycle=2 --set to 'cycle==2'
skippingTimer = CalculateNetworkDelay(1, skippingTimer, now) --update delay statistic. Record 'roundTripComplete'.
skippingTimer[2] = now+ gps_then_DoCalculation_delayG --wait until 'gps_then_DoCalculation_delayG'. The longer the better. The delay allow reliable unit direction to be derived from unit's motion
if (turnOnEcho == 1) then Spring.Echo("-----------------------GetPreliminarySeparation") end
end
if (now >=skippingTimer[2] and cycle==2) then --wait until 'skippingTimer[2] second', and wait for 'cycle==2', then do "DoCalculation()"
if (turnOnEcho == 1) then Spring.Echo("-----------------------DoCalculation") end
local networkDelay = CalculateNetworkDelay(0, skippingTimer, nil) --retrieve delay statistic
commandIndexTable, commandTTL =DoCalculation (surroundingOfActiveUnit,commandIndexTable, attacker, networkDelay, now, commandTTL) --initiate avoidance system
cycle=1 --set to 'cycle==1'
skippingTimer[2]=now+ doCalculation_then_gps_delayG --wait until 'doCalculation_then_gps_delayG'. Is arbitrarily set. Save CPU by setting longer wait.
skippingTimer = CalculateNetworkDelay(2, skippingTimer, now) --prepare delay statistic for new measurement
spSendLuaUIMsg(dummyIDg) --send ping to server. Wait for answer
roundTripComplete = false --Wait for 'LUA message Receive'.
if (turnOnEcho == 1) then Spring.Echo("-----------------------DoCalculation") end
end
if (turnOnEcho == 1) then
Spring.Echo("unitInMotion(Update):")
Spring.Echo(unitInMotion)
end
-------return global table
commandIndexTableG=commandIndexTable
unitInMotionG = unitInMotion
surroundingOfActiveUnitG=surroundingOfActiveUnit
cycleG = cycle
skippingTimerG = skippingTimer
attackerG = attacker
commandTTL_G = commandTTL
-----
end
---------------------------------Level 0 Top level
---------------------------------Level1 Lower level
-- return a refreshed unit list, else return nil
function RefreshUnitList(attacker, commandTTL)
local allMyUnits = spGetTeamUnits(myTeamID_gbl)
local arrayIndex=1
local relevantUnit={}
local metaForVisibleUnits = {}
local visibleUnits=spGetVisibleUnits(myTeamID_gbl)
for _, unitID in ipairs(visibleUnits) do --memorize units that is in view of camera
metaForVisibleUnits[unitID]="yes" --flag "yes" for visible unit (in view) and by default flag "nil" for out of view unit
end
for _, unitID in ipairs(allMyUnits) do
if unitID~=nil then --skip end of the table
-- additional hitchiking function: refresh attacker's list
attacker = RetrieveAttackerList (unitID, attacker)
-- additional hitchiking function: refresh WATCHDOG's list
commandTTL = RefreshWatchdogList (unitID, commandTTL)
--
local unitDefID = spGetUnitDefID(unitID)
local unitDef = UnitDefs[unitDefID]
local unitSpeed =unitDef["speed"]
local unitInView = metaForVisibleUnits[unitID] --transfer "yes" or "nil" from meta table into a local variable
if (unitSpeed>0) then
local unitType = 0 --// category that control WHEN avoidance is activated for each unit. eg: Category 2 only enabled when not in view & when guarding units. Used by 'GateKeeperOrCommandFilter()'
local fixedPointType = 1 --//category that control WHICH avoidance behaviour to use. eg: Category 2 priotize avoidance and prefer to ignore user's command when enemy is close. Used by 'CheckWhichFixedPointIsStable()'
if (unitDef["builder"] or unitDef["canCloak"]) and not unitDef.customParams.commtype then --include only constructor and cloakies, and not com
unitType =1 --//this unit type do avoidance even in camera view
if options.enableCons.value==false and unitDef["builder"] then --//if Cons epicmenu option is false and it is a constructor, then exclude Cons
unitType = 0
end
if unitDef["canCloak"] then --only cloakies
fixedPointType=2 --//use aggressive behaviour (avoid more)
if options.enableCloaky.value==false then --//if Cloaky option is false then exclude Cloaky
unitType = 0
end
end
--elseif not unitDef["canFly"] and not unitDef["canSubmerge"] then --include all ground unit, but excluding com & amphibious
elseif not unitDef["canFly"] then --include all ground unit, including com
unitType =2 --//this unit type only have avoidance outside camera view
if options.enableGround.value==false then --//if Ground unit epicmenu option is false then exclude Ground unit
unitType = 0
end
elseif (unitDef.hoverAttack== true) then --include gunships
unitType =3 --//this unit type only have avoidance outside camera view
if options.enableGunship.value==false then --//if Gunship epicmenu option is false then exclude Gunship
unitType = 0
end
-- elseif not unitDef["canFly"] and unitDef["canSubmerge"] then --include all amphibious unit & com
-- unitType =4 --//this unit type only have avoidance outside camera view
-- if options.enableAmphibious.value==false then --//if Gunship epicmenu option is false then exclude Gunship
-- unitType = 0
-- end
end
if (unitType>0) then
local unitShieldPower, reloadableWeaponIndex= -1, -1
unitShieldPower, reloadableWeaponIndex = CheckWeaponsAndShield(unitDef)
arrayIndex=arrayIndex+1
relevantUnit[arrayIndex]={unitID, unitType, unitSpeed, fixedPointType, isVisible = unitInView, unitShieldPower = unitShieldPower, reloadableWeaponIndex = reloadableWeaponIndex}
end
end
if (turnOnEcho == 1) then --for debugging
Spring.Echo("unitID(RefreshUnitList)" .. unitID)
Spring.Echo("unitDef[humanName](RefreshUnitList)" .. unitDef["humanName"])
Spring.Echo("((unitDef[builder] or unitDef[canCloak]) and unitDef[speed]>0)(RefreshUnitList):")
Spring.Echo((unitDef["builder"] or unitDef["canCloak"]) and unitDef["speed"]>0)
end
end
end
if arrayIndex>1 then relevantUnit[1]=arrayIndex -- store the array's lenght in the first row of the array
else relevantUnit[1] = nil end --send out nil if no unit is present
if (turnOnEcho == 1) then
Spring.Echo("allMyUnits(RefreshUnitList): ")
Spring.Echo(allMyUnits)
Spring.Echo("relevantUnit(RefreshUnitList): ")
Spring.Echo(relevantUnit)
end
return relevantUnit, attacker, commandTTL
end
-- detect initial enemy separation to detect "fleeing enemy" later
function GetPreliminarySeparation(unitInMotion,commandIndexTable, attacker)
local surroundingOfActiveUnit={}
if unitInMotion[1]~=nil then --don't execute if no unit present
local arrayIndex=1
for i=2, unitInMotion[1], 1 do --array index 1 contain the array's lenght, start from 2
local unitID= unitInMotion[i][1] --get unitID for commandqueue
if spGetUnitIsDead(unitID)==false then --prevent execution if unit died during transit
local cQueue = spGetCommandQueue(unitID)
local executionAllow, cQueueTemp = GateKeeperOrCommandFilter(unitID, cQueue, unitInMotion[i]) --filter/alter unwanted unit state by reading the command queue
if executionAllow then
cQueue = cQueueTemp --cQueueTemp has been altered for identification, copy it to cQueue for use (actual command is not yet issued)
--local boxSizeTrigger= unitInMotion[i][2]
local fixedPointCONSTANTtrigger = unitInMotion[i][4] --//using fixedPointType to trigger different fixed point constant for each unit type
local targetCoordinate, commandIndexTable, newCommand, boxSizeTrigger, graphCONSTANTtrigger, fixedPointCONSTANTtrigger=IdentifyTargetOnCommandQueue(cQueue, unitID, commandIndexTable,fixedPointCONSTANTtrigger) --check old or new command
local currentX,_,currentZ = spGetUnitPosition(unitID)
local lastPosition = {currentX, currentZ} --record current position for use to determine unit direction later.
local reachedTarget = TargetBoxReached(targetCoordinate, unitID, boxSizeTrigger, lastPosition) --check if widget should ignore command
local losRadius = GetUnitLOSRadius(unitID) --get LOS
local surroundingUnits = GetAllUnitsInRectangle(unitID, losRadius, attacker) --catalogue enemy
if (cQueue[1].id == CMD_MOVE and unitInMotion[i].isVisible ~= "yes") then --if unit has move Command and is outside user's view
reachedTarget = false --force unit to continue avoidance despite close to target (try to circle over target until seen by user)
end
if reachedTarget then --if reached target
commandIndexTable[unitID]=nil --empty the commandIndex (command history)
end
if surroundingUnits[1]~=nil and not reachedTarget then --execute when enemy exist and target not reached yet
--local unitType =unitInMotion[i][2]
--local unitSSeparation, losRadius = CatalogueMovingObject(surroundingUnits, unitID, lastPosition, unitType, losRadius) --detect initial enemy separation & alter losRadius when unit submerged
local unitSSeparation, losRadius = CatalogueMovingObject(surroundingUnits, unitID, lastPosition, losRadius) --detect initial enemy separation & alter losRadius when unit submerged
arrayIndex=arrayIndex+1 --// increment table index by 1, start at index 2; table lenght is stored at row 1
local unitSpeed = unitInMotion[i][3]
local impatienceTrigger,commandIndexTable = GetImpatience(newCommand,unitID, commandIndexTable)
surroundingOfActiveUnit[arrayIndex]={unitID, unitSSeparation, targetCoordinate, losRadius, cQueue, newCommand, unitSpeed,impatienceTrigger, lastPosition, graphCONSTANTtrigger, fixedPointCONSTANTtrigger} --store result for next execution
if (turnOnEcho == 1) then
Spring.Echo("unitsSeparation(GetPreliminarySeparation):")
Spring.Echo(unitsSeparation)
end
end
if (turnOnEcho == 1) then --debugging
Spring.Echo("i(GetPreliminarySeparation)" .. i)
Spring.Echo("unitID(GetPreliminarySeparation)" .. unitID)
Spring.Echo("losRadius(GetPreliminarySeparation)" .. losRadius)
Spring.Echo("surroundingUnits(GetPreliminarySeparatione): ")
Spring.Echo(surroundingUnits)
Spring.Echo("reachedTarget(GetPreliminarySeparation):")
Spring.Echo(reachedTarget)
Spring.Echo("surroundingUnits~=nil and cQueue[1].id==CMD_MOVE and not reachedTarget(GetPreliminarySeparation):")
Spring.Echo((surroundingUnits~=nil and cQueue[1].id==CMD_MOVE and not reachedTarget))
end
end --GateKeeperOrCommandFilter(cQueue, unitInMotion[i]) ==true
end --if spGetUnitIsDead(unitID)==false
end
if arrayIndex>1 then surroundingOfActiveUnit[1]=arrayIndex
else surroundingOfActiveUnit[1]=nil end
end --if unitInMotion[1]~=nil
return surroundingOfActiveUnit, commandIndexTable --send separation result away
end
--perform the actual collision avoidance calculation and send the appropriate command to unit
function DoCalculation (surroundingOfActiveUnit,commandIndexTable, attacker, networkDelay, now, commandTTL)
if surroundingOfActiveUnit[1]~=nil then --if flagged as nil then no stored content then this mean there's no relevant unit
for i=2,surroundingOfActiveUnit[1], 1 do --index 1 is for array's lenght
local unitID=surroundingOfActiveUnit[i][1]
if spGetUnitIsDead(unitID)==false then --prevent unit death from short circuiting the system
local unitSSeparation=surroundingOfActiveUnit[i][2]
local targetCoordinate=surroundingOfActiveUnit[i][3]
local losRadius=surroundingOfActiveUnit[i][4]
local cQueue=surroundingOfActiveUnit[i][5]
local newCommand=surroundingOfActiveUnit[i][6]
--do sync test. Ensure stored command not changed during last delay. eg: it change if user issued new command
local cQueueSyncTest = spGetCommandQueue(unitID)
local needToCancelOrder = false
if #cQueueSyncTest>=2 then --if new command is longer than or equal to 2 (1 any command + 1 stop command)
if #cQueueSyncTest~=#cQueue or --if command queue is not same as original, or
(cQueueSyncTest[1].params[1]~=cQueue[1].params[1] or cQueueSyncTest[1].params[3]~=cQueue[1].params[3]) or --or, if first queue has different content, or
cQueueSyncTest[1]==nil then --or, if unit has became idle
--newCommand=true
--cQueue=cQueueSyncTest
commandIndexTable[unitID]=nil --empty commandIndex (command history) for this unit
commandTTL[unitID][#commandTTL]=nil --delete latest watchdog's entry for this unit
needToCancelOrder = true --skip widget's command
end
end
if not needToCancelOrder then --//if unit receive new command then widget need to stop issuing command based on old information. This prevent user from being annoyed by widget overriding their command.
local unitSpeed= surroundingOfActiveUnit[i][7]
local impatienceTrigger= surroundingOfActiveUnit[i][8]
local lastPosition = surroundingOfActiveUnit[i][9]
local newSurroundingUnits = GetAllUnitsInRectangle(unitID, losRadius, attacker) --get the latest unit list (rather than using the preliminary list) to ensure reliable avoidance
local graphCONSTANTtrigger = surroundingOfActiveUnit[i][10] --//fetch information on which aCONSTANT and obsCONSTANT to use
local fixedPointCONSTANTtrigger = surroundingOfActiveUnit[i][11] --//fetch information on which fixedPoint constant to use
if (newSurroundingUnits[1] ~=nil) then --//check again if there's still any enemy to avoid. Submerged unit might return empty list if their enemy has no Sonar (their 'losRadius' became half the original value so that they don't detect/avoid unnecessarily).
local newX, newZ = AvoidanceCalculator(unitID, targetCoordinate,losRadius,newSurroundingUnits, unitSSeparation, unitSpeed, impatienceTrigger, lastPosition, graphCONSTANTtrigger, networkDelay, fixedPointCONSTANTtrigger, newCommand) --calculate move solution
local newY=spGetGroundHeight(newX,newZ)
commandIndexTable, commandTTL = InsertCommandQueue(cQueue, unitID, newX, newY, newZ, commandIndexTable, newCommand, now, commandTTL) --send move solution to unit
if (turnOnEcho == 1) then
Spring.Echo("newX(Update) " .. newX)
Spring.Echo("newZ(Update) " .. newZ)
end
end
end
end
end
end
return commandIndexTable, commandTTL
end
function widget:RecvLuaMsg(msg, playerID) --receive echo from server ('LUA message Receive')
if msg:sub(1,3) == dummyIDg and playerID == myPlayerID then
roundTripComplete = true --unlock system
end
end
function ReportedNetworkDelay(playerIDa, defaultDelay)
local _,_,_,_,_,totalDelay,_,_,_,_= Spring.GetPlayerInfo(playerIDa)
if totalDelay==nil or totalDelay<=defaultDelay then return defaultDelay --if ping is too low: set the minimum delay
else return totalDelay --take account for lag + wait a little bit for any command to properly update
end
end
function CalculateNetworkDelay(reportingIn, skippingTimer, now)
if reportingIn == 0 then --report known delay statistic
local delay = 0
local instantaneousDelay = skippingTimer.networkDelay --delay current lag
local averageDelay = skippingTimer.sumOfAllNetworkDelay/skippingTimer.sumCounter --average delay
if instantaneousDelay < averageDelay then --bound all delay to be > than average delay
delay = averageDelay
else
delay = instantaneousDelay
end
return delay
elseif reportingIn == 1 then --update delay statistic
skippingTimer.networkDelay = now - skippingTimer.echoTimestamp --get the delay between previous Command and the latest 'LUA message Receive'
skippingTimer.sumOfAllNetworkDelay=skippingTimer.sumOfAllNetworkDelay + skippingTimer.networkDelay --sum all the delay ever recorded
skippingTimer.sumCounter = skippingTimer.sumCounter + 1 --count all the delay ever recorded
return skippingTimer
elseif reportingIn == 2 then --update delay statistic
skippingTimer.echoTimestamp = now --remember the current time of sending ping
return skippingTimer
end
end
---------------------------------Level1
---------------------------------Level2 (level 1's call-in)
function RetrieveAttackerList (unitID, attacker)
local unitHealth,_,_,_,_ = spGetUnitHealth(unitID)
if attacker[unitID] == nil then --if attacker table is empty then fill with default value
attacker[unitID] = {id = nil, countDown = 0, myHealth = unitHealth}
end
if attacker[unitID].countDown >0 then attacker[unitID].countDown = attacker[unitID].countDown - 1 end --count-down until zero and stop
if unitHealth< attacker[unitID].myHealth then --if underattack then find out the attackerID
local attackerID = spGetUnitLastAttacker(unitID)
if attackerID~=nil then --if attackerID is found then mark the attackerID for avoidance
attacker[unitID].countDown = attacker[unitID].countDown + 3 --//add 3xUnitRefresh-rate (~3.3second) to attacker's TTL
attacker[unitID].id = attackerID
end
end
attacker[unitID].myHealth = unitHealth --refresh health data
return attacker
end
function RefreshWatchdogList (unitID, commandTTL)
if commandTTL[unitID] == nil then --if commandTTL table is empty then insert an empty table
commandTTL[unitID] = {}
else --//if commandTTL is not empty then perform check and update its content appropriately. Its not empty when widget has issued a new command
local cQueue = spGetCommandQueue(unitID, 1)
for i=#commandTTL[unitID], 1, -1 do
local firstParam, secondParam = 0, 0
if cQueue[1]~=nil then
firstParam, secondParam = cQueue[1].params[1], cQueue[1].params[3]
end
if (firstParam == commandTTL[unitID][i].widgetCommand[1]) and (secondParam == commandTTL[unitID][i].widgetCommand[2]) then --//if current command is similar to the one once issued by widget then countdown its TTL
if commandTTL[unitID][i].countDown >0 then
commandTTL[unitID][i].countDown = commandTTL[unitID][i].countDown - 1 --count-down until zero and stop
elseif commandTTL[unitID][i].countDown ==0 then --if commandTTL is found to reach ZERO then remove the command, assume a 'TIMEOUT', then remove *this* watchdog entry
spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[1].tag}, {} )
commandTTL[unitID][i] = nil
end
break --//exit the iteration, save the rest of the commandTTL for checking on next update/next cQueue. If later commandTTL is for cQueue[2] then it would be more appropriate to compare it later (currently we only check cQueue[1] for timeout/expiration).
else --//if current command is a NEW command (not similar to widget's issued), then delete *this* watchdog entry. Operator "#" will automatically register the 'nil' as 'end of table' for future updates
commandTTL[unitID][i] = nil
end
end
end
return commandTTL
end
function CheckWeaponsAndShield (unitDef)
--global variable
local reloadableWeaponCriteria = reloadableWeaponCriteriaG
----
local unitShieldPower, reloadableWeaponIndex =-1, -1 --assume unit has no shield and no reloadable/slow-loading weapons
local fastestReloadTime, fastWeaponIndex = 999, -1 --temporary variables
for currentWeaponIndex, weapons in ipairs(unitDef.weapons) do --reference: gui_contextmenu.lua by CarRepairer
local weaponsID = weapons.weaponDef
local weaponsDef = WeaponDefs[weaponsID]
if weaponsDef.name and not weaponsDef.name:find('fake') and not weaponsDef.name:find('noweapon') then --reference: gui_contextmenu.lua by CarRepairer
if weaponsDef.isShield then
unitShieldPower = weaponsDef.shieldPower --remember the shield power of this unit
else --if not shield then this is conventional weapon
local reloadTime = weaponsDef.reload
if reloadTime < fastestReloadTime then --find the weapon with the smallest reload time
fastestReloadTime = reloadTime
fastWeaponIndex = currentWeaponIndex-1 --remember the index of the fastest weapon. Somehow the weapon table actually start at "0", so minus 1 from actual value (ZK)
end
end
end
end
if fastestReloadTime > reloadableWeaponCriteria then --if the fastest reload cycle is greater than widget's update cycle, then:
reloadableWeaponIndex = fastWeaponIndex --remember the index of that fastest loading weapon
if (turnOnEcho == 1) then --debugging
Spring.Echo("reloadableWeaponIndex(CheckWeaponsAndShield):")
Spring.Echo(reloadableWeaponIndex)
Spring.Echo("fastestReloadTime(CheckWeaponsAndShield):")
Spring.Echo(fastestReloadTime)
end
end
return unitShieldPower, reloadableWeaponIndex
end
function GateKeeperOrCommandFilter (unitID, cQueue, unitInMotionSingleUnit)
local allowExecution = false
if cQueue~=nil then --prevent ?. Forgot...
local isReloading = CheckIfUnitIsReloading(unitInMotionSingleUnit) --check if unit is reloading/shieldCritical
local state=spGetUnitStates(unitID)
local holdPosition= (state.movestate == 0)
if ((unitInMotionSingleUnit.isVisible ~= "yes" or isReloading) and (cQueue[1] == nil or #cQueue == 1)) then --if unit is out of user's vision and currently idle/with-singular-mono-command (eg: widget's move order), or is reloading and currently idle/with-singular-mono-command (eg: auto-attack) then:
if (holdPosition== false) then --if not "hold position"
cQueue={{id = cMD_DummyG, params = {-1 ,-1,-1}, options = {}}, {id = CMD_STOP, params = {-1 ,-1,-1}, options = {}}, nil} --replace with a FAKE COMMAND. Will be used to initiate avoidance on idle unit & non-viewed unit
end
end
if cQueue[1]~=nil then --prevent idle unit from executing the system (prevent crash), but idle unit with FAKE COMMAND (cMD_DummyG) is allowed.
local isValidCommand = (cQueue[1].id == 40 or cQueue[1].id < 0 or cQueue[1].id == 90 or cQueue[1].id == CMD_MOVE or cQueue[1].id == 125 or cQueue[1].id == cMD_DummyG) -- ALLOW unit with command: repair (40), build (<0), reclaim (90), ressurect(125), move(10), or FAKE COMMAND
--local isValidUnitTypeOrIsNotVisible = (unitInMotionSingleUnit[2] == 1) or (unitInMotionSingleUnit.isVisible ~= "yes" and unitInMotionSingleUnit[2]~= 3)--ALLOW only unit of unitType=1 OR (all unitTypes that is outside player's vision except gunship)
local isValidUnitTypeOrIsNotVisible = (unitInMotionSingleUnit[2] == 1) or (unitInMotionSingleUnit.isVisible ~= "yes")--ALLOW only unit of unitType=1 OR (all unitTypes that is outside player's vision)
local _2ndAttackSignature = false --attack command signature
local _2ndGuardSignature = false --guard command signature
if #cQueue >=2 then --check if the command-queue is masked by widget's previous command, but the actual originality check will be performed by TargetBoxReached() later.
_2ndAttackSignature = (cQueue[1].id == CMD_MOVE and cQueue[2].id == CMD_ATTACK)
_2ndGuardSignature = (cQueue[1].id == CMD_MOVE and cQueue[2].id == CMD_GUARD)
end
local isReloadingState = (isReloading and (cQueue[1].id == CMD_ATTACK or cQueue[1].id == cMD_DummyG or _2ndAttackSignature)) --any unit with attack command or was idle that is Reloading
local isGuardState = (cQueue[1].id == CMD_GUARD or _2ndGuardSignature)
if (isValidCommand and isValidUnitTypeOrIsNotVisible) or (isReloadingState and not holdPosition) or (isGuardState) then --execute on: repair (40), build (<0), reclaim (90), ressurect(125), move(10), or FAKE idle COMMAND for: UnitType==1 or for: any unit outside visibility... or on any unit with any command which is reloading.
local isReloadAvoidance = (isReloadingState and not holdPosition)
if isReloadAvoidance or #cQueue>=2 then --check cQueue for lenght to prevent STOP command from short circuiting the system
if isReloadAvoidance or cQueue[2].id~=false then --prevent a spontaneous enemy engagement from short circuiting the system
allowExecution = true --allow execution
end --if cQueue[2].id~=false
if (turnOnEcho == 1) then Spring.Echo(cQueue[2].id) end --for debugging
end --if #cQueue>=2
end --if ((cQueue[1].id==40 or cQueue[1].id<0 or cQueue[1].id==90 or cQueue[1].id==10 or cQueue[1].id==125) and (unitInMotion[i][2]==1 or unitInMotion[i].isVisible == nil)
end --if cQueue[1]~=nil
end --if cQueue~=nil
return allowExecution, cQueue --disallow execution
end
--check if widget's command or user's command
function IdentifyTargetOnCommandQueue(cQueue, unitID,commandIndexTable, fixedPointCONSTANTtrigger) --//used by GetPreliminarySeparation()
local targetCoordinate = {nil,nil,nil}
local boxSizeTrigger=0
local graphCONSTANTtrigger = {}
local newCommand=true -- immediately assume user's command
if commandIndexTable[unitID]==nil then --memory was empty, so fill it with zeros
commandIndexTable[unitID]={widgetX=0, widgetZ=0 ,backupTargetX=0, backupTargetY=0, backupTargetZ=0, patienceIndexA=0}
else
local a = math.modf(dNil(cQueue[1].params[1])) --using math.modf to remove trailing decimal (only integer for matching). In case high resolution cause a fail matching with server's numbers... and use dNil incase wreckage suddenly disappear.
local c = math.modf(dNil(cQueue[1].params[3])) --dNil: if it is a reclaim or repair order (no z coordinate) then replace it with -1 (has similar effect to the "nil")
local b = math.modf(commandIndexTable[unitID]["widgetX"])
local d = math.modf(commandIndexTable[unitID]["widgetZ"])
newCommand= (a~= b and c~=d)--compare current command with in memory
if (turnOnEcho == 1) then --debugging
Spring.Echo("unitID(GetPreliminarySeparation)" .. unitID)
Spring.Echo("commandIndexTable[unitID][widgetX](IdentifyTargetOnCommandQueue):" .. commandIndexTable[unitID]["widgetX"])
Spring.Echo("commandIndexTable[unitID][widgetZ](IdentifyTargetOnCommandQueue):" .. commandIndexTable[unitID]["widgetZ"])
Spring.Echo("newCommand(IdentifyTargetOnCommandQueue):")
Spring.Echo(newCommand)
Spring.Echo("cQueue[1].params[1](IdentifyTargetOnCommandQueue):" .. cQueue[1].params[1])
Spring.Echo("cQueue[1].params[2](IdentifyTargetOnCommandQueue):" .. cQueue[1].params[2])
Spring.Echo("cQueue[1].params[3](IdentifyTargetOnCommandQueue):" .. cQueue[1].params[3])
if cQueue[2]~=nil then
Spring.Echo("cQueue[2].params[1](IdentifyTargetOnCommandQueue):")
Spring.Echo(cQueue[2].params[1])
Spring.Echo("cQueue[2].params[3](IdentifyTargetOnCommandQueue):")
Spring.Echo(cQueue[2].params[3])
end
end
end
if newCommand then --if user's new command
commandIndexTable, targetCoordinate, boxSizeTrigger, graphCONSTANTtrigger,fixedPointCONSTANTtrigger = ExtractTarget (1, unitID,cQueue,commandIndexTable,targetCoordinate,fixedPointCONSTANTtrigger)
commandIndexTable[unitID]["patienceIndexA"]=0 --//reset impatience counter
else --if widget's previous command
commandIndexTable, targetCoordinate, boxSizeTrigger, graphCONSTANTtrigger,fixedPointCONSTANTtrigger = ExtractTarget (2, unitID,cQueue,commandIndexTable,targetCoordinate,fixedPointCONSTANTtrigger)
end
return targetCoordinate, commandIndexTable, newCommand, boxSizeTrigger, graphCONSTANTtrigger, fixedPointCONSTANTtrigger --return target coordinate
end
--ignore command set on this box
function TargetBoxReached (targetCoordinate, unitID, boxSizeTrigger, lastPosition)
local currentX, currentZ = lastPosition[1], lastPosition[2]
local targetX = dNil(targetCoordinate[1]) -- use dNil if target asynchronously/spontaneously disappear: in that case it will replace "nil" with -1
local targetZ =targetCoordinate[3]
if targetX==-1 then return false end --if target is invalid (-1) then assume target not-yet-reached, return false (default state), and continue avoidance
local xDistanceToTarget = math.abs(currentX -targetX)
local zDistanceToTarget = math.abs(currentZ -targetZ)
if (turnOnEcho == 1) then
Spring.Echo("unitID(TargetBoxReached)" .. unitID)
Spring.Echo("currentX(TargetBoxReached)" .. currentX)
Spring.Echo("currentZ(TargetBoxReached)" .. currentZ)
Spring.Echo("cx(TargetBoxReached)" .. targetX)
Spring.Echo("cz(TargetBoxReached)" .. targetZ)
Spring.Echo("(xDistanceToTarget<=halfTargetBoxSize[boxSizeTrigger] and zDistanceToTarget<=halfTargetBoxSize[boxSizeTrigger])(TargetBoxReached):")
Spring.Echo((xDistanceToTarget<=halfTargetBoxSize[boxSizeTrigger] and zDistanceToTarget<=halfTargetBoxSize[boxSizeTrigger]))
end
return (xDistanceToTarget<=halfTargetBoxSize[boxSizeTrigger] and zDistanceToTarget<=halfTargetBoxSize[boxSizeTrigger]) --only command greater than this box return false
end
-- get LOS
function GetUnitLOSRadius(unitID)
local unitDefID= spGetUnitDefID(unitID)
local unitDef= UnitDefs[unitDefID]
local losRadius =550 --arbitrary (scout LOS)
if unitDef~=nil then --if unitDef is not empty then use the following LOS
losRadius= unitDef.losRadius*32 --for some reason it was times 32
end
return (losRadius + extraLOSRadiusCONSTANTg)
end
--return a table of surrounding enemy
function GetAllUnitsInRectangle(unitID, losRadius, attacker)
local x,y,z = spGetUnitPosition(unitID)
local unitDefID = spGetUnitDefID(unitID)
local unitDef = UnitDefs[unitDefID]
local iAmConstructor = unitDef["builder"]
local unitState = spGetUnitStates(unitID)
local iAmNotCloaked = not unitState["cloak"]
if (turnOnEcho == 1) then
Spring.Echo("spGetUnitIsDead(unitID)==false (GetAllUnitsInRectangle):")
Spring.Echo(spGetUnitIsDead(unitID)==false)
end
local unitsInRectangle = spGetUnitsInRectangle(x-losRadius, z-losRadius, x+losRadius, z+losRadius)
local relevantUnit={}
local arrayIndex=1
--add attackerID into enemy list
relevantUnit, arrayIndex = AddAttackerIDToEnemyList (unitID, losRadius, relevantUnit, arrayIndex, attacker)
--
for _, rectangleUnitID in ipairs(unitsInRectangle) do
local isAlly= spIsUnitAllied(rectangleUnitID)
if (rectangleUnitID ~= unitID) and not isAlly then --filter out ally units and self
local rectangleUnitTeamID = spGetUnitTeam(rectangleUnitID)
if (rectangleUnitTeamID ~= gaiaTeamID) then --filter out gaia (non aligned unit)
local recUnitDefID = spGetUnitDefID(rectangleUnitID)
local registerEnemy = false
if recUnitDefID~=nil and (iAmConstructor and iAmNotCloaked) then --if enemy is in LOS & I am a visible constructor: then
local recUnitDef = UnitDefs[recUnitDefID] --retrieve enemy definition
local enemyParalyzed,_,_ = spGetUnitIsStunned (rectangleUnitID)
if recUnitDef["weapons"][1]~=nil and not enemyParalyzed then -- check enemy for weapons and paralyze effect
registerEnemy = true --register the enemy only if it armed & wasn't paralyzed
end
else --if enemy is detected (in LOS or RADAR), and iAm a generic units OR any cloaked constructor then:
if iAmNotCloaked then --if I am not cloaked
local enemyParalyzed,_,_ = Spring.GetUnitIsStunned (rectangleUnitID)
if not enemyParalyzed then -- check for paralyze effect
registerEnemy = true --register enemy if it's not paralyzed
end
else --if I am cloaked (constructor or cloakies), then:
registerEnemy = true --register all enemy (avoid all unit)
end
end
if registerEnemy then
arrayIndex=arrayIndex+1
relevantUnit[arrayIndex]=rectangleUnitID --register enemy
end
end
end
end
if arrayIndex>1 then relevantUnit[1]=arrayIndex --fill index 1 with array lenght
else relevantUnit[1]=nil end
if (turnOnEcho == 1) then
Spring.Echo("relevantUnit(GetAllUnitsInRectangle): ")
Spring.Echo(relevantUnit)
end
return relevantUnit
end
--allow a unit to recognize fleeing enemy; so it doesn't need to avoid them
function CatalogueMovingObject(surroundingUnits, unitID, lastPosition, losRadius)
local unitsSeparation={}
if (surroundingUnits[1]~=nil) then --don't catalogue anything if no enemy exist
local unitDepth = 99
local sonarDetected = false
local halfLosRadius = losRadius/2
--if unitType == 4 then --//if unit is amphibious, then:
_,unitDepth,_ = spGetUnitPosition(unitID) --//get unit's y-axis. Less than 0 mean submerged.
--end
for i=2,surroundingUnits[1],1 do --//iterate over all enemy list.
local unitRectangleID=surroundingUnits[i]
if (unitRectangleID ~= nil) then
local relativeAngle = GetUnitRelativeAngle (unitID, unitRectangleID)
local unitDirection,_,_ = GetUnitDirection(unitID, lastPosition)
local unitSeparation = spGetUnitSeparation (unitID, unitRectangleID)
if math.abs(unitDirection- relativeAngle)< (collisionAngleG) then --unit inside the collision angle is catalogued with a value that is useful for comparision later
unitsSeparation[unitRectangleID]=unitSeparation
else --unit outside the collision angle is set to an arbitrary 999 which is not useful for comparision later
unitsSeparation[unitRectangleID]=999 --set saperation distance to 999 such that later comparison (which is always smaller than this) imply an approaching units
end
if unitDepth <0 then --//if unit is submerged, then:
--local enemySonarRadius = (spGetUnitSensorRadius(unitRectangleID,"sonar") or 0)
local enemyDefID = spGetUnitDefID(unitRectangleID)
local unitDefsSonarContent = 999 --//set to very large so that any un-identified contact is assumed as having sonar (as threat).
if UnitDefs[enemyDefID]~=nil then
unitDefsSonarContent = UnitDefs[enemyDefID].sonarRadius
end
local enemySonarRadius = (unitDefsSonarContent or 0)
if enemySonarRadius > halfLosRadius then --//check enemy for sonar
sonarDetected = true
end
end
end
end
if (not sonarDetected) and (unitDepth < 0) then --//if enemy doesn't have sonar but Iam still submerged, then:
losRadius = halfLosRadius --// halven the unit's 'avoidance' range. Don't need to avoid enemy if enemy are blind.
end
end
if (turnOnEcho == 1) then
Spring.Echo("unitSeparation(CatalogueMovingObject):")
Spring.Echo(unitsSeparation)
end
return unitsSeparation, losRadius
end
function GetImpatience(newCommand, unitID, commandIndexTable)
local impatienceTrigger=1 --zero will de-activate auto reverse
if commandIndexTable[unitID]["patienceIndexA"]>=6 then impatienceTrigger=0 end --//if impatience index level 6 (after 6 time avoidance) then trigger impatience. Impatience will deactivate/change some values downstream
if not newCommand and activateImpatienceG==1 then
commandIndexTable[unitID]["patienceIndexA"]=commandIndexTable[unitID]["patienceIndexA"]+1 --increase impatience index if impatience system is activate
end
if (turnOnEcho == 1) then Spring.Echo("commandIndexTable[unitID][patienceIndexA] (GetImpatienceLevel) " .. commandIndexTable[unitID]["patienceIndexA"]) end
return impatienceTrigger, commandIndexTable
end
function AvoidanceCalculator(unitID, targetCoordinate, losRadius, surroundingUnits, unitsSeparation, unitSpeed, impatienceTrigger, lastPosition, graphCONSTANTtrigger, networkDelay, fixedPointCONSTANTtrigger, newCommand)
if (unitID~=nil) and (targetCoordinate ~= nil) then --prevent idle/non-existent/ unit with invalid command from using collision avoidance
local aCONSTANT = aCONSTANTg --attractor constant (amplitude multiplier)
local unitDirection, _, usingLastPosition = GetUnitDirection(unitID, lastPosition) --get unit direction
local targetAngle = 0
local fTarget = 0
local fTargetSlope = 0
----
aCONSTANT = aCONSTANT[graphCONSTANTtrigger[1]] --//select which 'aCONSTANT' value
fTarget = aCONSTANT --//maximum value is aCONSTANT
fTargetSlope = 1 --//slope is negative or positive
if targetCoordinate[1]~=-1 then --if target coordinate contain -1 then disable target for pure avoidance
targetAngle = GetTargetAngleWithRespectToUnit(unitID, targetCoordinate) --get target angle
fTarget = GetFtarget (aCONSTANT, targetAngle, unitDirection)
fTargetSlope = GetFtargetSlope (aCONSTANT, targetAngle, unitDirection, fTarget)
--local targetSubtendedAngle = GetTargetSubtendedAngle(unitID, targetCoordinate) --get target 'size' as viewed by the unit
end
local wTotal=0
local fObstacleSum=0
local dFobstacle=0
local dSum=0
local nearestFrontObstacleRange =999
local normalizingFactor=0
--count every enemy unit and sum its contribution to the obstacle/repulsor variable
wTotal, dSum, fObstacleSum,dFobstacle,nearestFrontObstacleRange, normalizingFactor=SumAllUnitAroundUnitID (unitID, surroundingUnits, unitDirection, wTotal, dSum, fObstacleSum,dFobstacle,nearestFrontObstacleRange, unitsSeparation, impatienceTrigger, graphCONSTANTtrigger)
--calculate appropriate behaviour based on the constant and above summation value
local wTarget, wObstacle = CheckWhichFixedPointIsStable (fTargetSlope, dFobstacle, dSum, fTarget, fObstacleSum, wTotal, fixedPointCONSTANTtrigger)
--convert an angular command into a coordinate command
local newX, newZ= SendCommand(unitID, wTarget, wObstacle, fTarget, fObstacleSum, unitDirection, nearestFrontObstacleRange, losRadius, unitSpeed, impatienceTrigger, normalizingFactor, networkDelay, usingLastPosition, newCommand)
if (turnOnEcho == 1) then
Spring.Echo("unitID(AvoidanceCalculator)" .. unitID)
Spring.Echo("targetAngle(AvoidanceCalculator) " .. targetAngle)
Spring.Echo("unitDirection(AvoidanceCalculator) " .. unitDirection)
Spring.Echo("fTarget(AvoidanceCalculator) " .. fTarget)
Spring.Echo("fTargetSlope(AvoidanceCalculator) " .. fTargetSlope)
--Spring.Echo("targetSubtendedAngle(AvoidanceCalculator) " .. targetSubtendedAngle)
Spring.Echo("wTotal(AvoidanceCalculator) " .. wTotal)
Spring.Echo("dSum(AvoidanceCalculator) " .. dSum)
Spring.Echo("fObstacleSum(AvoidanceCalculator) " .. fObstacleSum)
Spring.Echo("dFobstacle(AvoidanceCalculator) " .. dFobstacle)
Spring.Echo("nearestFrontObstacleRange(AvoidanceCalculator) " .. nearestFrontObstacleRange)
Spring.Echo("wTarget(AvoidanceCalculator) " .. wTarget)
Spring.Echo("wObstacle(AvoidanceCalculator) " .. wObstacle)
Spring.Echo("newX(AvoidanceCalculator) " .. newX)
Spring.Echo("newZ(AvoidanceCalculator) " .. newZ)
end
return newX, newZ --return move coordinate
end
end
-- maintain the visibility of original command
-- reference: "unit_tactical_ai.lua" -ZeroK gadget by Google Frog
function InsertCommandQueue(cQueue, unitID,newX, newY, newZ, commandIndexTable, newCommand, now, commandTTL)
--Method 1: doesn't work online
--if not newCommand then spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[1].tag}, {} ) end --delete old command
-- spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, newX, newY, newZ}, {"alt"} ) --insert new command
----
--Method 2: doesn't work online
-- if not newCommand then spGiveOrderToUnit(unitID, CMD_MOVE, {cQueue[1].params[1],cQueue[1].params[2],cQueue[1].params[3]}, {"ctrl","shift"} ) end --delete old command
-- spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, newX, newY, newZ}, {"alt"} ) --insert new command
----
--Method 3.5: cause big movement noise
-- newX = Round(newX)
-- newY = Round(newY)
-- newZ = Round(newZ)
----
--Method 3: work online, but under rare circumstances doesn't work
-- spGiveOrderToUnit(unitID, CMD.STOP, {}, {})
-- spGiveOrderToUnit(unitID, CMD_MOVE, {newX, newY, newZ}, {} )
-- local arrayIndex=1
-- if not newCommand then arrayIndex=2 end --skip old widget command
-- if #cQueue>=2 then --try to identify unique signature of area reclaim/repair
-- if (cQueue[1].id==40 or cQueue[1].id==90 or cQueue[1].id==125) then
-- if cQueue[2].id==90 or cQueue[2].id==125 then
-- if (not Spring.ValidFeatureID(cQueue[2].params[1]-wreckageID_offset) or (not Spring.ValidFeatureID(cQueue[2].params[1]))) and not Spring.ValidUnitID(cQueue[2].params[1]) then --if it is an area command
-- spGiveOrderToUnit(unitID, CMD_MOVE, cQueue[2].params, {} ) --divert unit to the center of reclaim/repair command
-- arrayIndex=arrayIndex+1 --skip the target:wreck/units. Allow command reset
-- end
-- elseif cQueue[2].id==40 then
-- if (not Spring.ValidFeatureID(cQueue[2].params[1]-wreckageID_offset) or (not Spring.ValidFeatureID(cQueue[2].params[1]))) and not Spring.ValidUnitID(cQueue[2].params[1]) then --if it is an area command
-- arrayIndex=arrayIndex+1 --skip the target:units. Allow continuous command reset
-- end
-- end
-- end
-- end
-- for b = arrayIndex, #cQueue,1 do --re-do user's optional command
-- local options={"shift",nil,nil,nil}
-- local optionsIndex=2
-- if cQueue[b].options["alt"] then
-- options[optionsIndex]="alt"
-- end
-- if cQueue[b].options["ctrl"] then
-- optionsIndex=optionsIndex+1
-- options[optionsIndex]="ctrl"
-- end
-- if cQueue[b].options["right"] then
-- optionsIndex=optionsIndex+1
-- options[optionsIndex]="right"
-- end
-- spGiveOrderToUnit(unitID, cQueue[b].id, cQueue[b].params, options) --replace the rest of the command
-- end
--Method 4: with network delay detection won't do any problem
local queueIndex=1
if not newCommand then --if widget's command then delete it
spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[1].tag}, {} ) --delete previous widget command
queueIndex=2 --skip index 1 of stored command. Skip widget's command
commandTTL[unitID][#commandTTL[unitID]] = nil --//delete the latest watchdog entry (assuming commandTTL are insert/delete same command that widget is insert/delete).
end
if #cQueue>=queueIndex+1 then --reclaim,area reclaim,stop, or: move,reclaim, area reclaim,stop, or: area reclaim, stop, or:move, area reclaim, stop.
if (cQueue[queueIndex].id==40 or cQueue[queueIndex].id==90 or cQueue[queueIndex].id==125) then --if first (1) queue is reclaim/ressurect/repair
if cQueue[queueIndex+1].id==90 or cQueue[queueIndex+1].id==125 then --if second (2) queue is also reclaim/ressurect
--if (not Spring.ValidFeatureID(cQueue[queueIndex+1].params[1]-wreckageID_offset) or (not Spring.ValidFeatureID(cQueue[queueIndex+1].params[1]))) and not Spring.ValidUnitID(cQueue[queueIndex+1].params[1]) then --if it was an area command
if (cQueue[queueIndex+1].params[3]~=nil) then --second (2) queue is area reclaim. area command should has no "nil" on params 1,2,3, & 4
spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[queueIndex].tag}, {} ) --delete latest reclaiming/ressurecting command (skip the target:wreck/units). Allow command reset
local coordinate = (FindSafeHavenForCons(unitID, now)) or (cQueue[queueIndex+1])
spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, coordinate.params[1], coordinate.params[2], coordinate.params[3]}, {"alt"} ) --divert unit to the center of reclaim/repair command OR to any heavy concentration of ally (haven)
commandTTL[unitID][#commandTTL[unitID] +1] = {countDown = 15, widgetCommand= {coordinate.params[1], coordinate.params[3]}} --//remember this command on watchdog's commandTTL table. It has 15x*RefreshUnitUpdateRate* to expire
end
elseif cQueue[queueIndex+1].id==40 then --if second (2) queue is also repair
--if (not Spring.ValidFeatureID(cQueue[queueIndex+1].params[1]-wreckageID_offset) or (not Spring.ValidFeatureID(cQueue[queueIndex+1].params[1]))) and not Spring.ValidUnitID(cQueue[queueIndex+1].params[1]) then --if it was an area command
if (cQueue[queueIndex+1].params[3]~=nil) then --area command should has no "nil" on params 1,2,3, & 4
spGiveOrderToUnit(unitID, CMD_REMOVE, {cQueue[queueIndex].tag}, {} ) --delete current repair command, (skip the target:units). Reset the repair command
end
elseif (cQueue[queueIndex].params[3]~=nil) then --if first (1) queue is area reclaim (an area reclaim without any wreckage to reclaim). area command should has no "nil" on params 1,2,3, & 4
local coordinate = (FindSafeHavenForCons(unitID, now)) or (cQueue[queueIndex])
spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, coordinate.params[1], coordinate.params[2], coordinate.params[3]}, {"alt"} ) --divert unit to the center of reclaim/repair command
commandTTL[unitID][#commandTTL[unitID]+1] = {countDown = 4, widgetCommand= {coordinate.params[1], coordinate.params[3]}} --//remember this command on watchdog's commandTTL table. It has 4x*RefreshUnitUpdateRate* to expire
end
end
end
spGiveOrderToUnit(unitID, CMD_INSERT, {0, CMD_MOVE, CMD_OPT_INTERNAL, newX, newY, newZ}, {"alt"} ) --insert new command
commandTTL[unitID][#commandTTL[unitID]+1] = {countDown = 4, widgetCommand= {newX, newZ}} --//remember this command on watchdog's commandTTL table. It has 4x*RefreshUnitUpdateRate* to expire
----
commandIndexTable[unitID]["widgetX"]=newX --update the memory table. So that next update can use to check if unit has new or old (widget's) command
commandIndexTable[unitID]["widgetZ"]=newZ
if (turnOnEcho == 1) then
Spring.Echo("unitID(InsertCommandQueue)" .. unitID)
Spring.Echo("commandIndexTable[unitID][widgetX](InsertCommandQueue):" .. commandIndexTable[unitID]["widgetX"])
Spring.Echo("commandIndexTable[unitID][widgetZ](InsertCommandQueue):" .. commandIndexTable[unitID]["widgetZ"])
Spring.Echo("newCommand(InsertCommandQueue):")
Spring.Echo(newCommand)
Spring.Echo("cQueue[1].params[1](InsertCommandQueue):" .. cQueue[1].params[1])
Spring.Echo("cQueue[1].params[3](InsertCommandQueue):" .. cQueue[1].params[3])
if cQueue[2]~=nil then
Spring.Echo("cQueue[2].params[1](InsertCommandQueue):")
Spring.Echo(cQueue[2].params[1])
Spring.Echo("cQueue[2].params[3](InsertCommandQueue):")
Spring.Echo(cQueue[2].params[3])
end
end
return commandIndexTable, commandTTL --return updated memory tables. One for checking if new command is issued and another is to check for command's expiration age.
end
---------------------------------Level2
---------------------------------Level3 (low-level function)
--check if unit is vulnerable/reloading
function CheckIfUnitIsReloading(unitInMotionSingleUnitTable)
------
local criticalShieldLevel =criticalShieldLevelG --global constant
local minimumRemainingReloadTime =minimumRemainingReloadTimeG
local secondPerGameFrame =secondPerGameFrameG
------
--local unitType = unitInMotionSingleUnitTable[2] --retrieve stored unittype
local shieldIsCritical =false
local weaponIsEmpty = false
--if unitType ==2 or unitType == 1 then
local unitID = unitInMotionSingleUnitTable[1] --retrieve stored unitID
local unitShieldPower = unitInMotionSingleUnitTable.unitShieldPower --retrieve registered full shield power
if unitShieldPower ~= -1 then
local _, currentPower = spGetUnitShieldState(unitID)
if currentPower~=nil then
if currentPower/unitShieldPower <criticalShieldLevel then
shieldIsCritical = true
end
end
end
local unitFastestReloadableWeapon = unitInMotionSingleUnitTable.reloadableWeaponIndex --retrieve the quickest reloadable weapon index
if unitFastestReloadableWeapon ~= -1 then
local _, _, weaponReloadFrame, _, _ = spGetUnitWeaponState(unitID, unitFastestReloadableWeapon)
local currentFrame, _ = spGetGameFrame()
local remainingTime = (weaponReloadFrame - currentFrame)*secondPerGameFrame
weaponIsEmpty = (remainingTime> minimumRemainingReloadTime)
if (turnOnEcho == 1) then --debugging
Spring.Echo(unitFastestReloadableWeapon)
Spring.Echo(spGetUnitWeaponState(unitID, unitFastestReloadableWeapon, "range"))
end
end
--end
return (weaponIsEmpty or shieldIsCritical)
end
-- debugging method, used to quickly remove nil
function dNil(x)
if x==nil then
x=-1
end
return x
end
function ExtractTarget (queueIndex, unitID, cQueue, commandIndexTable, targetCoordinate, fixedPointCONSTANTtrigger) --//used by IdentifyTargetOnCommandQueue()