-
Notifications
You must be signed in to change notification settings - Fork 251
Expand file tree
/
Copy pathSimUtils.lua
More file actions
1571 lines (1358 loc) · 56.6 KB
/
SimUtils.lua
File metadata and controls
1571 lines (1358 loc) · 56.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
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
-- Copyright © 2005 Gas Powered Games, Inc. All rights reserved.
--
-- General Sim scripts
-- upvalues for performance
local ArmyBrains = ArmyBrains
local GetCurrentCommandSource = GetCurrentCommandSource
------------------------------------------------------------------------------------------------------------------------
--#region General Unit Transfer Scripts
local CreateWreckage = import("/lua/wreckage.lua").CreateWreckage
local transferUnbuiltCategory = categories.ALLUNITS
local transferUnitsCategory = categories.ALLUNITS - categories.INSIGNIFICANTUNIT
local buildersCategory = categories.ALLUNITS - categories.CONSTRUCTION - categories.ENGINEER
---@class FactoryRebuildData
---@field FacRebuild_Progress number # progress -- save current progress for some later checks
---@field FacRebuild_BuildTime number # progress * bp.Economy.BuildTime
---@field FacRebuild_Health number # unitBeingBuilt:GetHealth()
---@field FacRebuild_OldBuildRate? number
---@alias FactoryRebuildDataTable table<UnitId, (FactoryUnit | FactoryRebuildData)[]>
--- Clear data for a factory so transferring it doesn't try to rebuild units again
---@param factory FactoryUnit | FactoryRebuildData
local function clearFactoryRebuildData(factory)
factory.FacRebuild_UnitId = nil
factory.FacRebuild_Progress = nil
factory.FacRebuild_BuildTime = nil
factory.FacRebuild_Health = nil
factory.FacRebuild_OldBuildRate = nil
end
---@param factoryRebuildDataTable FactoryRebuildDataTable
function FactoryRebuildUnits(factoryRebuildDataTable)
for buildUnitId, factories in factoryRebuildDataTable do
-- Remove support factories that can't build their unit due to lacking an HQ
local noFactories = false
for i, factory in factories do
if not factory:CanBuild(buildUnitId) then
clearFactoryRebuildData(factory)
factories[i] = nil
if table.empty(factories) then
factoryRebuildDataTable[buildUnitId] = nil
noFactories = true
end
continue
end
end
if noFactories then continue end
IssueClearCommands(factories)
IssueBuildFactory(factories, buildUnitId, 1)
end
-- wait for build order to start and then rebuild the units for free
WaitTicks(1)
for k, factories in factoryRebuildDataTable do
for i, factory in factories do
if factory.Dead then
factories[i] = nil
if table.empty(factories) then
factoryRebuildDataTable[k] = nil
end
continue
end
factory.FacRebuild_OldBuildRate = factory:GetBuildRate()
factory:SetBuildRate(factory.FacRebuild_BuildTime * 10)
factory:SetConsumptionPerSecondEnergy(0)
factory:SetConsumptionPerSecondMass(0)
end
end
-- wait for buildpower to apply then return the factories to normal and pause them
WaitTicks(1)
for k, factories in factoryRebuildDataTable do
for i, factory in factories do
if factory.Dead then
factories[i] = nil
if table.empty(factories) then
factoryRebuildDataTable[k] = nil
end
continue
end
factory:SetBuildRate(factory.FacRebuild_OldBuildRate)
-- consumption values will update back to normal through `Unit:OnPaused`
factory:SetPaused(true)
-- A hack to make the UI show the pause icon over the base unit.
-- I hope nobody else uses `Unit.Parent` in any other way. `GetParent` for exfacs doesn't return the base unit.
-- TODO: Add a SetPaused hook into all the exfac class units (the class hierarchy is ambiguous) so this isn't necessary.
local parent = factory--[[@as ExternalFactoryUnit]].Parent
if parent then
parent:SetPaused(true)
end
-- First make sure rebuilding went correctly
local rebuiltUnit = factory.UnitBeingBuilt
if not rebuiltUnit or math.abs(rebuiltUnit:GetFractionComplete() - factory.FacRebuild_Progress) > 0.001 then
if rebuiltUnit then
rebuiltUnit:Destroy()
rebuiltUnit = nil
end
IssueClearCommands({ factory })
factory:SetPaused(false)
WARN(string.format(
[[FactoryRebuildUnits failed to rebuild correctly for factory %s (entity ID %d).
Rebuild data:
Progress: %f
BuildTime: %f
Health: %f
%s]]
, factory.UnitId
, factory.EntityId
, factory.FacRebuild_Progress
, factory.FacRebuild_BuildTime
, factory.FacRebuild_Health
, factory.FacRebuild_OldBuildRate
, debug.traceback()
))
end
if rebuiltUnit then
-- Set correct health for the rebuilt unit in case it was damaged in the factory
rebuiltUnit:SetHealth(nil, factory.FacRebuild_Health)
end
clearFactoryRebuildData(factory)
end
end
end
--- Pauses all drones in `kennels`
---@param kennels TPodTowerUnit[]
function PauseTransferredKennels(kennels)
-- wait for drones to spawn
WaitTicks(1)
for _, unit in kennels do
unit:SetPaused(true)
local podData = unit.PodData
if podData then
for _, pod in podData do
local podHandle = pod.PodHandle
if podHandle then
podHandle:SetPaused(true)
end
end
end
end
end
--- Upgrades `kennels` to their `TargetUpgradeBuildTime` value, allowing for drones to spawn and get paused
---@param kennels TPodTowerUnit[]
function UpgradeTransferredKennels(kennels)
WaitTicks(1) -- spawn drones
for _, unit in kennels do
if not unit:BeenDestroyed() then
for _, pod in unit.PodData or {} do -- pause Kennels drones
local podHandle = pod.PodHandle
if podHandle then
podHandle:SetPaused(true)
end
end
IssueUpgrade({ unit }, unit.UpgradesTo)
end
end
WaitTicks(3)
for _, unit in kennels do
if not unit:BeenDestroyed() then
unit:SetBuildRate(unit.TargetUpgradeBuildTime * 10)
unit:SetConsumptionPerSecondMass(0)
unit:SetConsumptionPerSecondEnergy(0)
end
end
WaitTicks(1)
for _, unit in kennels do
if not unit:BeenDestroyed() then
unit:SetBuildRate(unit.DefaultBuildRate)
unit:SetPaused(true) -- `SetPaused` updates ConsumptionPerSecond values
unit.TargetUpgradeBuildTime = nil
unit.DefaultBuildRate = nil
end
end
end
--- Upgrades `units` to `UpgradesTo` at their `TargetUpgradeBuildTime` values (defaulting to
--- `UpgradeBuildTime`, i.e. completion) and resets the build rate to `DefaultBuildRate` (defaulting
--- to the build rate at the start)
---@param units Unit[]
function UpgradeUnits(units)
for _, unit in units do
IssueUpgrade({ unit }, unit.UpgradesTo)
if not unit.DefaultBuildRate then
unit.DefaultBuildRate = unit:GetBuildRate()
end
unit:SetBuildRate(0)
end
WaitTicks(3)
for _, unit in units do
if not unit:BeenDestroyed() then
local targetUpgradeBuildTime = unit.TargetUpgradeBuildTime or unit.UpgradeBuildTime
unit:SetBuildRate(targetUpgradeBuildTime * 10)
unit:SetConsumptionPerSecondMass(0)
unit:SetConsumptionPerSecondEnergy(0)
end
end
WaitTicks(1)
for _, unit in units do
if not unit:BeenDestroyed() then
unit:SetBuildRate(unit.DefaultBuildRate)
unit:SetPaused(true) -- `SetPaused` updates ConsumptionPerSecond values
unit.TargetUpgradeBuildTime = nil
unit.DefaultBuildRate = nil
end
end
end
-- used to make more expensive units transfer first, in case there's a unit cap issue
local function TransferUnitsOwnershipComparator(a, b)
a = a.Blueprint or a:GetBlueprint()
b = b.Blueprint or b:GetBlueprint()
return a.Economy.BuildCostMass > b.Economy.BuildCostMass
end
local sharedUnits = {}
--- Transfers units to an army, returning the new units (since changing the army
--- replaces the units with new ones)
---@param units Unit[]
---@param toArmy integer
---@param captured? boolean
---@param noRestrictions? boolean
---@return Unit[]?
function TransferUnitsOwnership(units, toArmy, captured, noRestrictions)
local toBrain = ArmyBrains[toArmy]
if not toBrain or (not noRestrictions and toBrain:IsDefeated())
or table.empty(units)
then
return
end
local categoriesENGINEERSTATION = categories.ENGINEERSTATION
local shareUpgrades = ScenarioInfo.Options.Share ~= 'ShareUntilDeath'
-- do not gift insignificant units
units = EntityCategoryFilterDown(transferUnitsCategory, units)
-- gift most valuable units first
table.sort(units, TransferUnitsOwnershipComparator)
local newUnitCount = 0
local newUnits = {}
local upgradeUnitCount = 0
local upgradeUnits = {}
local pauseKennelCount = 0
local pauseKennels = {}
local upgradeKennelCount = 0
local upgradeKennels = {}
---@type FactoryRebuildDataTable
local factoryRebuildDataTable = {}
for _, unit in units do
local owner = unit.Army
-- Only allow units not attached to be given. This is because units will give all of its
-- children over as well, so we only want the top level units to be given.
-- Units currently being captured are also denied
if owner == toArmy or
unit:GetParent() ~= unit or (unit.Parent and unit.Parent ~= unit) or
unit.CaptureProgress > 0 or
unit:GetFractionComplete() < 1.0
then
continue
end
local bp = unit.Blueprint
local bpPhysics = bp.Physics
local categoriesHash = bp.CategoriesHash
-- B E F O R E
local orientation = unit:GetOrientation()
local siloWorkProgress = unit:IsUnitState("SiloBuildingAmmo") and unit:GetWorkProgress() or 0
local numNukes = unit:GetNukeSiloAmmoCount() -- nuclear missiles; SML or SMD
local numTacMsl = unit:GetTacticalSiloAmmoCount()
local massKilled = unit.VetExperience
local unitHealth = unit:GetHealth()
local tarmacs = unit--[[@as StructureUnit]].TarmacBag
local shieldIsOn = false
local shieldHealth = 0
local hasFuel = false
local fuelRatio = 0
local activeEnhancements
local oldowner = unit.oldowner
local LastTickDamaged = unit--[[@as ACUUnit]].LastTickDamaged
local upgradesTo = unit.UpgradesTo
local defaultBuildRate
local upgradeBuildTimeComplete
local exclude
local FacRebuild_UnitId = unit.FacRebuild_UnitId
local FacRebuild_Progress = unit.FacRebuild_Progress
local FacRebuild_BuildTime = unit.FacRebuild_BuildTime
local FacRebuild_Health = unit.FacRebuild_Health
local shield = unit.MyShield
if shield then
shieldIsOn = unit:ShieldIsOn()
shieldHealth = shield:GetHealth()
end
local fuelUseTime = bpPhysics.FuelUseTime
if fuelUseTime and fuelUseTime > 0 then -- going through the BP to check for fuel
fuelRatio = unit:GetFuelRatio() -- usage is more reliable then unit.HasFuel
hasFuel = true -- cause some buildings say they use fuel
end
local enhancements = bp.Enhancements
if enhancements then
local unitEnh = SimUnitEnhancements[unit.EntityId]
if unitEnh then
activeEnhancements = {}
for i, enh in unitEnh do
activeEnhancements[i] = enh
end
if not activeEnhancements[1] then
activeEnhancements = nil
end
end
end
if categoriesHash['ENGINEERSTATION'] and categoriesHash['UEF'] then
-- We have to kill drones which are idling inside Kennel at the moment of transfer
-- otherwise additional dummy drone will appear after transfer
for _, drone in unit:GetCargo() do
drone:Destroy()
end
end
if unit.TransferUpgradeProgress and shareUpgrades then
local progress = unit:GetWorkProgress()
local upgradeBuildTime = unit.UpgradeBuildTime
defaultBuildRate = unit:GetBuildRate()
if progress > 0.05 then --5%. EcoManager & auto-paused mexes etc.
upgradeBuildTimeComplete = upgradeBuildTime * progress
end
end
unit.IsBeingTransferred = true
-- If this unit is a factory building a unit (parent of the unit being built is our unit)
-- then store data to rebuild the factory progress after transfer
local unitExternalFactory = unit.ExternalFactory
local factoryUnit = unitExternalFactory or unit
local unitBeingBuilt = factoryUnit.UnitBeingBuilt
if unitBeingBuilt
and not unitBeingBuilt.Dead
and not unitBeingBuilt.isFinishedUnit
-- In external factories, the units are parented to the base unit instead of the exfac.
-- Checking the parent also excludes upgrading factories (the upgrade's parent is the upgrade itself)
and unitBeingBuilt:GetParent() == unit
then
local bpBeingBuilt = unitBeingBuilt.Blueprint
FacRebuild_UnitId = unitBeingBuilt.UnitId
FacRebuild_Progress = unitBeingBuilt:GetFractionComplete()
FacRebuild_BuildTime = FacRebuild_Progress * bpBeingBuilt.Economy.BuildTime
FacRebuild_Health = unitBeingBuilt:GetHealth()
-- For external factories, destroy the unit being built since otherwise it will be transferred as a built unit because it is attached indirectly
if unitExternalFactory then
unitBeingBuilt:Destroy()
end
end
-- changing owner
local newUnit = ChangeUnitArmy(unit, toArmy, noRestrictions or false)
if not newUnit then
continue
end
newUnitCount = newUnitCount + 1
newUnits[newUnitCount] = newUnit
if IsAlly(owner, toArmy) then
if not oldowner then
oldowner = owner
end
local sharedUnitsTable = sharedUnits[oldowner]
if not sharedUnitsTable then
sharedUnitsTable = {}
sharedUnits[oldowner] = sharedUnitsTable
end
table.insert(sharedUnitsTable, newUnit)
end
newUnit.oldowner = oldowner
-- A F T E R
-- for the disconnect ACU share option
if LastTickDamaged then
newUnit.LastTickDamaged = LastTickDamaged
end
newUnit:SetOrientation(orientation, true)
if massKilled and massKilled > 0 then
newUnit:CalculateVeterancyLevelAfterTransfer(massKilled, true)
end
if activeEnhancements then
for _, enh in activeEnhancements do
newUnit:CreateEnhancement(enh)
end
end
local maxHealth = newUnit:GetMaxHealth()
if unitHealth > maxHealth then
unitHealth = maxHealth
end
newUnit:SetHealth(newUnit, unitHealth)
if hasFuel then
newUnit:SetFuelRatio(fuelRatio)
end
if tarmacs then
newUnit.TarmacBag = tarmacs
end
if numNukes and numNukes > 0 then
newUnit:GiveNukeSiloAmmo(numNukes - newUnit:GetNukeSiloAmmoCount())
end
if numTacMsl and numTacMsl > 0 then
newUnit:GiveTacticalSiloAmmo(numTacMsl - newUnit:GetTacticalSiloAmmoCount())
end
if newUnit.Blueprint.CategoriesHash["SILO"] then
newUnit:GiveNukeSiloBlocks(siloWorkProgress)
end
local newShield = newUnit.MyShield
if newShield then
newShield:SetHealth(newUnit, shieldHealth)
if shieldIsOn then
newUnit:EnableShield()
else
newUnit:DisableShield()
end
end
if EntityCategoryContains(categoriesENGINEERSTATION, newUnit) then
if not upgradeBuildTimeComplete or not shareUpgrades then
if categoriesHash['UEF'] then
-- use special thread for UEF Kennels
-- Give them 1 tick to spawn their drones and then pause both station and drone
pauseKennelCount = pauseKennelCount + 1
pauseKennels[pauseKennelCount] = newUnit
else -- pause cybran hives immediately
newUnit:SetPaused(true)
end
elseif categoriesHash['UEF'] then
newUnit.UpgradesTo = upgradesTo
newUnit.DefaultBuildRate = defaultBuildRate
newUnit.TargetUpgradeBuildTime = upgradeBuildTimeComplete
upgradeKennelCount = upgradeKennelCount + 1
upgradeKennels[upgradeKennelCount] = newUnit
exclude = true
end
end
if upgradeBuildTimeComplete and not exclude then
newUnit.UpgradesTo = upgradesTo
newUnit.DefaultBuildRate = defaultBuildRate
newUnit.TargetUpgradeBuildTime = upgradeBuildTimeComplete
upgradeUnitCount = upgradeUnitCount + 1
upgradeUnits[upgradeUnitCount] = newUnit
end
if FacRebuild_UnitId then
local newFactoryUnit = newUnit--[[@as Unit | ExternalFactoryComponent]].ExternalFactory or newUnit
local data = factoryRebuildDataTable[FacRebuild_UnitId]
if not data then
factoryRebuildDataTable[FacRebuild_UnitId] = { newFactoryUnit }
else
table.insert(data, newFactoryUnit)
end
-- store data for rebuilding
-- unit id is not needed during rebuild but is needed if transferred again in the middle of rebuild
newFactoryUnit.FacRebuild_UnitId = FacRebuild_UnitId
newFactoryUnit.FacRebuild_Progress = FacRebuild_Progress
newFactoryUnit.FacRebuild_BuildTime = FacRebuild_BuildTime
newFactoryUnit.FacRebuild_Health = FacRebuild_Health
end
unit.IsBeingTransferred = nil
if unit.OnGiven then
unit:OnGiven(newUnit)
end
end
if not captured then
if not table.empty(upgradeUnits) then
ForkThread(UpgradeUnits, upgradeUnits)
end
if not table.empty(pauseKennels) then
ForkThread(PauseTransferredKennels, pauseKennels)
end
if not table.empty(upgradeKennels) then
ForkThread(UpgradeTransferredKennels, upgradeKennels)
end
if not table.empty(factoryRebuildDataTable) then
ForkThread(FactoryRebuildUnits, factoryRebuildDataTable)
end
end
return newUnits
end
---@class RebuildTracker
---@field CanCreateWreck boolean
---@field Success boolean
---@field TargetBuildTime number
---@field UnitBlueprint UnitBlueprint
---@field UnitBlueprintID string
---@field UnitHealth number
---@field UnitID string
---@field UnitOrientation Quaternion
---@field UnitPos Vector
---@field UnitProgress number
---@alias RevertibleCollisionShapeEntity Prop | Unit
--- Initializes the rebuild process for a `unit`. It is destroyed in this method and replaced
--- with a tracker. Any possible entities that could block construction have their collision
--- shapes disabled and are placed into `blockingEntities` to be reverted later. A unit can be
--- tagged with `TargetFractionComplete` to be rebuilt with a different build progress.
---@param unit Unit
---@param blockingEntities RevertibleCollisionShapeEntity[]
---@return RebuildTracker tracker
function CreateRebuildTracker(unit, blockingEntities)
local bp = unit.Blueprint
local blueprintID = bp.BlueprintId
local buildTime = bp.Economy.BuildTime
local health = unit:GetHealth()
local pos = unit:GetPosition()
local progress = unit.TargetFractionComplete or unit:GetFractionComplete()
---@type RebuildTracker
local tracker = {
-- save all important data because the unit will be destroyed
UnitHealth = health,
UnitPos = pos,
UnitID = unit.EntityId,
UnitOrientation = unit:GetOrientation(),
UnitBlueprint = bp,
UnitBlueprintID = blueprintID,
UnitProgress = progress, -- save current progress for some later checks
CanCreateWreck = progress > 0.5, -- if rebuilding fails, we have to create a wreck manually
TargetBuildTime = progress * buildTime,
Success = false,
}
-- wrecks can prevent drone from starting construction
local wrecks = GetReclaimablesInRect(unit:GetSkirtRect()) --[[@as ReclaimObject[] | Wreckage[] ]]
if wrecks then
for _, reclaim in wrecks do
if reclaim.IsWreckage then
-- collision shape to none to prevent it from blocking, keep track to revert later
reclaim:CacheAndRemoveCollisionExtents()
table.insert(blockingEntities, reclaim)
end
end
end
-- units can prevent drone from starting construction
local nearbyUnits = GetUnitsInRect(unit:GetSkirtRect())
if nearbyUnits then
for _, nearbyUnit in nearbyUnits do
nearbyUnit:SetCollisionShape('None')
table.insert(blockingEntities, nearbyUnit)
end
end
unit:Destroy()
return tracker
end
--- Attempts to rebuild `units` for an `army`, returning the resulting rebuild trackers
--- and any entities needing their collision shape reverted
---@param units Unit[]
---@param trackers? RebuildTracker[]
---@param blockingEntities? RevertibleCollisionShapeEntity[]
---@return RebuildTracker[] blockingEntities
---@return RevertibleCollisionShapeEntity[] blockingEntities
function StartRebuildUnits(units, trackers, blockingEntities)
trackers = trackers or {}
blockingEntities = blockingEntities or {}
for i, unit in units do
trackers[i] = CreateRebuildTracker(unit, blockingEntities)
end
return trackers, blockingEntities
end
--- Attempts to rebuild units for an `army`, using `trackers`
---@param trackers RebuildTracker[]
---@param army Army
function TryRebuildUnits(trackers, army)
local rebuilders = {}
for k, tracker in trackers do
if tracker.Success then
continue
end
-- create invisible drone which belongs to allied army. BuildRange = 10000
local rebuilder = CreateUnitHPR('ZXA0001', army, 5, 20, 5, 0, 0, 0)
rebuilder.TargetBuildTime = tracker.TargetBuildTime
rebuilders[k] = rebuilder
IssueBuildMobile({ rebuilder }, tracker.UnitPos, tracker.UnitBlueprintID, {})
end
WaitTicks(3) -- wait some ticks (3 is minimum), IssueBuildMobile() is not instant
for _, rebuilder in rebuilders do
rebuilder:SetBuildRate(rebuilder.TargetBuildTime * 10) -- set crazy build rate and consumption = 0
rebuilder:SetConsumptionPerSecondMass(0)
rebuilder:SetConsumptionPerSecondEnergy(0)
end
WaitTicks(1)
for k, rebuilder in rebuilders do
local tracker = trackers[k]
local newUnit = rebuilder:GetFocusUnit()
local progressDif = rebuilder:GetWorkProgress() - tracker.UnitProgress
if newUnit and math.abs(progressDif) < 0.001 then
newUnit:SetHealth(newUnit, tracker.UnitHealth)
tracker.Success = true
end
rebuilder:Destroy()
end
end
--- Finalizes the unit rebuilding process. Any failed rebuilding attempts are replaced with
--- wreckage and all blocking entities have their collision shapes reverted.
---@param trackers RebuildTracker[]
---@param blockingEntities RevertibleCollisionShapeEntity[]
function FinalizeRebuiltUnits(trackers, blockingEntities)
for _, tracker in trackers do
if not tracker.Success and tracker.CanCreateWreck then
local bp = tracker.UnitBlueprint
local pos = tracker.UnitPos
local orientation = tracker.UnitOrientation
-- Refund exactly how much mass was put into the unit
local completionFactor = tracker.TargetBuildTime / bp.Economy.BuildTime
local mass = bp.Economy.BuildCostMass * completionFactor
-- Don't refund energy because it would be counterintuitive for wreckage
local energy = 0
-- global 2x time multiplier for unit wrecks, see `Unit:CreateWreckageProp`
local time = (bp.Wreckage.ReclaimTimeMultiplier or 1) * 2
CreateWreckage(bp, pos, orientation, mass, energy, time)
end
end
-- revert collision shapes of any blocking units or wreckage
for _, entity in blockingEntities do
if not entity:BeenDestroyed() then
if entity.IsProp then
entity:ApplyCachedCollisionExtents()
else
entity:RevertCollisionShape()
end
end
end
end
--- Rebuilds `units`, giving a try for each army (in order) in case they can't for unit cap
--- reasons. If a unit cannot be rebuilt at all, a wreckage is placed instead. Each unit can
--- be tagged with `TargetFractionComplete` to be rebuilt with a different build progress.
---@see AddConstructionProgress # doesn't destroy and rebuild the unit
---@param units Unit[]
---@param armies Army[]
function RebuildUnits(units, armies)
local trackers, blockingEntities = StartRebuildUnits(units)
for _, army in armies do
TryRebuildUnits(trackers, army)
end
FinalizeRebuiltUnits(trackers, blockingEntities)
end
--- Takes the units and tries to rebuild them for each army (in order).
---@param units Unit[]
---@param armies Army[]
function TransferUnfinishedUnitsAfterDeath(units, armies)
local unbuiltUnits = {}
local unbuiltUnitCount = 0
for _, unit in EntityCategoryFilterDown(transferUnbuiltCategory, units) do
if unit:IsBeingBuilt()
-- Check if a unit is an upgrade to prevent duplicating it along with `UpgradeUnits`
and not unit.IsUpgrade
-- Make sure units are parents of themselves to avoid units being built in factories,
-- since they are awkward to finish building and they can even block factories.
-- `FactoryRebuildUnits` handles units inside factories correctly.
and unit == unit:GetParent()
then
unbuiltUnitCount = unbuiltUnitCount + 1
unbuiltUnits[unbuiltUnitCount] = unit
end
end
if not (unbuiltUnits[1] and armies[1]) then
return
end
RebuildUnits(unbuiltUnits, armies)
end
---@param data {To: integer}
---@param units? Unit[]
function GiveUnitsToPlayer(data, units)
local manualShare = ScenarioInfo.Options.ManualUnitShare
if manualShare == 'none' or table.empty(units) then
return
end
local toArmy = data.To
local owner = units[1].Army
if OkayToMessWithArmy(owner) and IsAlly(owner, toArmy) then
if manualShare == 'no_builders' then
local unitsBefore = table.getsize(units)
units = EntityCategoryFilterDown(buildersCategory, units)
local unitsAfter = table.getsize(units)
if unitsAfter ~= unitsBefore then
-- Maybe spawn an UI dialog instead?
print((unitsBefore - unitsAfter) .. " engineers/factories could not be transferred due to manual share rules")
end
end
TransferUnitsOwnership(units, toArmy)
end
end
--#endregion
------------------------------------------------------------------------------------------------------------------------
--#region Army Death Unit Transfer
--- Functions related to dealing with unit ownership when an army dies based on share conditions.
local CalculateBrainScore = import("/lua/sim/score.lua").CalculateBrainScore
local FakeTeleportUnits = import("/lua/scenarioframework.lua").FakeTeleportUnits
local defaultTransferCategory = categories.ALLUNITS - categories.WALL - categories.COMMAND
-- only units in this category will be shared under partial share rules
local partialShareCategory = categories.STRUCTURE + categories.ENGINEER
---@param owner integer
---@param categoriesToKill? EntityCategory defaults to all categories
function KillSharedUnits(owner, categoriesToKill)
local sharedUnitOwner = sharedUnits[owner]
if table.empty(sharedUnitOwner) then
return
end
for i = table.getn(sharedUnitOwner), 1, -1 do
local unit = sharedUnitOwner[i]
if unit.Dead then
table.remove(sharedUnitOwner, i) -- don't let them keep clogging our list!
elseif unit.oldowner == owner and
(not categoriesToKill or EntityCategoryContains(categoriesToKill, unit))
then
table.remove(sharedUnitOwner, i)
unit:Kill()
end
end
end
--- Given that `deadArmy` just died, redistributes their unit cap based on the scenario options
---@param deadArmy integer
function UpdateUnitCap(deadArmy)
local shareCapOption = ScenarioInfo.Options.ShareUnitCap
if not shareCapOption or shareCapOption == 'none' then
return
end
if not ArmyBrains[deadArmy]:IsDefeated() then
-- this is gonna give everyone some unit cap
WARN("Error while updating unit cap: dead army isn't defeated")
end
local shareToAll = false
if shareCapOption == "all" then
shareToAll = true
elseif shareCapOption ~= "allies" then
WARN("Unknown share unit cap mode: " .. tostring(shareCapOption))
end
local aliveCount = 0
---@type AIBrain[]
local alive = {}
for index, brain in ArmyBrains do
if not ArmyIsCivilian(index) and not brain:IsDefeated() and
(shareToAll or IsAlly(deadArmy, index))
then
aliveCount = aliveCount + 1
alive[aliveCount] = brain
end
end
if aliveCount > 0 then
local capChng = GetArmyUnitCap(deadArmy) / aliveCount
for _, brain in alive do
SetArmyUnitCap(brain.Army, GetArmyUnitCap(brain.Army) + capChng)
end
end
end
--- Transfer a brain's units to other brains.
---@param self AIBrain
---@param brains AIBrain[]
---@param transferUnfinishedUnits boolean
---@param categoriesToTransfer? EntityCategory # Defaults to ALLUNITS - WALL - COMMAND
---@param reason? string # Defaults to "FullShare"
---@return Unit[]?
function TransferUnitsToBrain(self, brains, transferUnfinishedUnits, categoriesToTransfer, reason)
if table.empty(brains) then
return
end
categoriesToTransfer = categoriesToTransfer or defaultTransferCategory
if transferUnfinishedUnits then
local indexes = {}
for _, brain in brains do
table.insert(indexes, brain.Army)
end
local units = self:GetListOfUnits(categoriesToTransfer, false)
TransferUnfinishedUnitsAfterDeath(units, indexes)
end
local totalNewUnits = {}
for _, brain in brains do
local units = self:GetListOfUnits(categoriesToTransfer, false)
if not table.empty(units) then
local newUnits = TransferUnitsOwnership(units, brain.Army, false, true)
-- we might not transfer any newUnits
if not table.empty(newUnits) then
table.destructiveCat(totalNewUnits, newUnits)
Sync.ArmyTransfer = { {
from = self.Army,
to = brain.Army,
reason = reason or "FullShare"
} }
end
-- Prevent giving the same units to multiple armies
WaitSeconds(1)
end
end
return totalNewUnits
end
--- Returns a table of the allies and enemies of a brain, and civilians.
---@param armyIndex integer
---@return { Civilians: AIBrain[], Enemies: AIBrain[], Allies: AIBrain[] } brainCategories
function GetAllegianceCategories(armyIndex)
local brainCategories = { Enemies = {}, Civilians = {}, Allies = {} }
for index, brain in ArmyBrains do
if not brain:IsDefeated() and armyIndex ~= index then
if ArmyIsCivilian(index) then
table.insert(brainCategories.Civilians, brain)
elseif IsEnemy(armyIndex, brain.Army) then
table.insert(brainCategories.Enemies, brain)
else
table.insert(brainCategories.Allies, brain)
end
end
end
return brainCategories
end
--- Transfer a brain's units to other brains, sorted by positive rating and then score.
---@param self AIBrain
---@param brains AIBrain[]
---@param transferUnfinishedUnits boolean
---@param categoriesToTransfer? EntityCategory # Defaults to ALLUNITS - WALL - COMMAND
---@param reason? string Usually 'FullShare'
---@return Unit[]?
function TransferUnitsToHighestBrain(self, brains, transferUnfinishedUnits, categoriesToTransfer, reason)
if table.empty(brains) then
return
end
local ratings = ScenarioInfo.Options.Ratings
---@type table<AIBrain, number>
local brainRatings = {}
for _, brain in brains do
-- AI can have a rating set in the lobby
if brain.BrainType == "Human" and ratings[brain.Nickname] then
brainRatings[brain] = ratings[brain.Nickname]
else
-- if there is no rating, create a fake negative rating based on score
-- leave -1000 rating for negative rated players
brainRatings[brain] = -1000 - 1 / CalculateBrainScore(brain)
end
end
-- sort brains by rating
table.sort(brains, function(a, b) return brainRatings[a] > brainRatings[b] end)
return TransferUnitsToBrain(self, brains, transferUnfinishedUnits, categoriesToTransfer, reason)
end
--local helper functions for KillArmy
-- Seconds to wait after an army has been defeated before we begin processing
-- what to do with the rest of its units. This is so that, if the game ends
-- shortly thereafter due to the army being defeated (as is common), we then
-- have an opportunity to see the final game state as observers before everything
-- would have blown up.
EndGameGracePeriod = 10
--- Kills all given units, if not already dead
---@param toKill Entity[]
local function KillUnits(toKill)
if not table.empty(toKill) then
for _, unit in toKill do
if not IsDestroyed(unit) then
unit:Kill()
end
end
end
end
---@param self AIBrain
local function KillWalls(self)
KillUnits(self:GetListOfUnits(categories.WALL, false))
end
---@param self AIBrain
local function KillRemaining(self)
KillUnits(self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false))
end
--- Remove the borrowed status from units we lent to a set of `brains`.
---@param brains AIBrain[] Usually our allies
---@param selfIndex number
local function TransferOwnershipOfBorrowedUnits(brains, selfIndex)
for _, brain in brains do
local units = brain:GetListOfUnits(categories.ALLUNITS, false)
if not table.empty(units) then
for _, unit in units do
if unit.oldowner == selfIndex then
unit.oldowner = nil
end
end
end
end
end
--- Return units transferred to me to their original owner (if alive)
---@param self AIBrain
local function ReturnBorrowedUnits(self)
local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false)
local borrowed = {}
for _, unit in units do
local oldowner = unit.oldowner
if oldowner and oldowner ~= self.Army and not ArmyBrains[oldowner]:IsDefeated() then
if not borrowed[oldowner] then
borrowed[oldowner] = {}
end
table.insert(borrowed[oldowner], unit)
end
end
for owner, units in borrowed do
TransferUnitsOwnership(units, owner, false, true)
end
WaitSeconds(1)
end