@@ -7,248 +7,110 @@ function gadget:GetInfo()
77 author = " Doo, Beherith" ,
88 date = " April 2018" ,
99 license = " GNU GPL, v2 or later" ,
10- layer = 0 ,
11- enabled = true , -- When we will move on 105 :)
10+ layer = 0 , -- after game_dynamic_maxunits for accurate unit caps
11+ enabled = tonumber ( Engine . versionMajor ) >= 105 ,
1212 }
1313end
1414
1515if not gadgetHandler :IsSyncedCode () then
1616 return
1717end
1818
19+ local reaimTimeMax = 0.5 --- @type number Caps dynamic reaim times and filters unitDefs. In seconds.
20+ local spamRatingBase = 400 --- @type integer Each team ' s reaimtime increases by 1 each X units made.
21+ local spamRatingMax = 1000 --- @type integer Sets the minimum performance penalty for spammed units.
22+ local unitCapDefault = 2400 --- @type integer The per-team unit cap we assume in these spam ratings.
23+
24+ local math_floor = math.floor
25+ local math_max = math.max
26+ local math_clamp = math .clamp
27+
28+ local spGetTeamLuaAI = Spring .GetTeamLuaAI
29+ local spGetTeamMaxUnits = Spring .GetTeamMaxUnits
1930local spSetUnitWeaponState = Spring .SetUnitWeaponState
20- local tableCopy = table .copy
21-
22-
23- local convertedUnitsNames = {
24- -- value is reaimtime in frames, engine default is 15
25- [' armfav' ] = 3 ,
26- [' armbeamer' ] = 3 ,
27- [' armpw' ] = 2 ,
28- [' armpwt4' ] = 2 ,
29- [' armflea' ] = 2 ,
30- [' armrock' ] = 2 ,
31- [' armham' ] = 2 ,
32- [' armwar' ] = 6 ,
33- [' armjeth' ] = 2 ,
34- [' corfav' ] = 3 ,
35- [' corak' ] = 2 ,
36- [' corthud' ] = 2 ,
37- [' corstorm' ] = 2 ,
38- [' corcrash' ] = 5 ,
39- [' legkark' ] = 2 ,
40- [' corkark' ] = 2 ,
41- [' cordeadeye' ] = 2 ,
42- [' armsnipe' ] = 2 ,
43- [' armfido' ] = 3 ,
44- [' armfboy' ] = 2 ,
45- [' armfast' ] = 2 ,
46- [' armamph' ] = 3 ,
47- [' armmav' ] = 2 ,
48- [' armspid' ] = 3 ,
49- [' armsptk' ] = 5 ,
50- [' armzeus' ] = 3 ,
51- [' coramph' ] = 3 ,
52- [' corcan' ] = 2 ,
53- [' corhrk' ] = 5 ,
54- [' cormando' ] = 2 ,
55- [' cormort' ] = 2 ,
56- [' corpyro' ] = 2 ,
57- [' cortermite' ] = 2 ,
58- [' armraz' ] = 1 ,
59- [' armmar' ] = 3 ,
60- [' armbanth' ] = 1 ,
61- [' corkorg' ] = 1 ,
62- [' armvang' ] = 3 ,
63- [' armcrus' ] = 5 ,
64- [' corsala' ] = 6 ,
65- [' corsiegebreaker' ] = 5 ,
66- [' legerailtank' ] = 9 ,
67-
68- -- the following units get a faster reaimtime to counteract their turret acceleration
69- [' armthor' ] = 4 ,
70- [' armflash' ] = 6 ,
71- [' corgator' ] = 6 ,
72- [' armdecade' ] = 6 ,
73- [' coresupp' ] = 6 ,
74- [' corhlt' ] = 5 ,
75- [' corfhlt' ] = 5 ,
76- [' cordoom' ] = 5 ,
77- [' corshiva' ] = 5 ,
78- [' corcat' ] = 5 ,
79- [' corkarg' ] = 5 ,
80- [' corbhmth' ] = 5 ,
81- [' armguard' ] = 5 ,
82- [' armamb' ] = 5 ,
83- [' corpun' ] = 5 ,
84- [' cortoast' ] = 5 ,
85- [' corbats' ] = 5 ,
86- [' corblackhy' ] = 6 ,
87- [' corscreamer' ] = 5 ,
88- [' corcom' ] = 5 ,
89- [' armcom' ] = 5 ,
90- [' cordecom' ] = 5 ,
91- [' armdecom' ] = 5 ,
92- [' legcom' ] = 5 ,
93- [' legdecom' ] = 5 ,
94- [' legcomlvl2' ] = 5 ,
95- [' legcomlvl3' ] = 5 ,
96- [' legcomlvl4' ] = 5 ,
97- [' legcomlvl5' ] = 5 ,
98- [' legcomlvl6' ] = 5 ,
99- [' legcomlvl7' ] = 5 ,
100- [' legcomlvl8' ] = 5 ,
101- [' legcomlvl9' ] = 5 ,
102- [' legcomlvl10' ] = 5 ,
103- [' legah' ] = 5 ,
104- [' legbal' ] = 5 ,
105- [' legbastion' ] = 5 ,
106- [' legcen' ] = 3 ,
107- [' legfloat' ] = 5 ,
108- [' leggat' ] = 5 ,
109- [' leggob' ] = 5 ,
110- [' leginc' ] = 1 ,
111- [' cordemon' ] = 6 ,
112- [' corcrwh' ] = 7 ,
113- [' leglob' ] = 5 ,
114- [' legmos' ] = 5 ,
115- [' leghades' ] = 5 ,
116- [' leghelios' ] = 5 ,
117- [' legheavydrone' ] = 5 ,
118- [' legkeres' ] = 5 ,
119- [' legrail' ] = 5 ,
120- [' legbar' ] = 5 ,
121- [' legcomoff' ] = 5 ,
122- [' legcomt2off' ] = 5 ,
123- [' legcomt2com' ] = 5 ,
124- [' legstr' ] = 3 ,
125- [' legamph' ] = 4 ,
126- [' legbart' ] = 5 ,
127- [' legmrv' ] = 5 ,
128- [' legsco' ] = 5 ,
129- [' leegmech' ] = 5 ,
130- [' legionnaire' ] = 5 ,
131- [' legafigdef' ] = 5 ,
132- [' legvenator' ] = 5 ,
133- [' legmed' ] = 5 ,
134- [' legaskirmtank' ] = 5 ,
135- [' legaheattank' ] = 3 ,
136- [' legeheatraymech' ] = 1 ,
137- [' legtriariusdrone' ] = 1 ,
138- [' legnavydestro' ] = 4 ,
139- [' legeheatraymech_old' ] = 1 ,
140- [' legbunk' ] = 3 ,
141- [' legrwall' ] = 4 ,
142- [' legjav' ] = 1 ,
143- [' legeshotgunmech' ] = 3 ,
144- [' legehovertank' ] = 4 ,
145- [' armanavaldefturret' ] = 4 ,
146- }
147- -- add entries for scavboss
148- local scavengerBossV4Table = {' scavengerbossv4_veryeasy' , ' scavengerbossv4_easy' , ' scavengerbossv4_normal' , ' scavengerbossv4_hard' , ' scavengerbossv4_veryhard' , ' scavengerbossv4_epic' ,
149- ' scavengerbossv4_veryeasy_scav' , ' scavengerbossv4_easy_scav' , ' scavengerbossv4_normal_scav' , ' scavengerbossv4_hard_scav' , ' scavengerbossv4_veryhard_scav' , ' scavengerbossv4_epic_scav' }
150- for _ , name in pairs (scavengerBossV4Table ) do
151- convertedUnitsNames [name ] = 4
152- end
153- -- if Spring.GetModOptions().emprework then
154- -- convertedUnitsNames['armdfly'] = 50
155- -- end
156- -- convert unitname -> unitDefID
157- local convertedUnits = {}
158- for name , params in pairs (convertedUnitsNames ) do
159- if UnitDefNames [name ] then
160- convertedUnits [UnitDefNames [name ].id ] = params
31+
32+ local reaimFramesMax = math .round (reaimTimeMax * Game .gameSpeed )
33+ local unitCapReference = math_max (Spring .GetModOptions ().maxunits , unitCapDefault )
34+ local unitCapNonPlayer = math_max (6000 , unitCapReference ) -- for one team vs. many
35+
36+ local unitReaimTimes = {}
37+ local unitSpamRating = {}
38+ local unitWeaponCount = {}
39+
40+ -- Unit scripts have to manually check in script if it is at the desired heading.
41+ -- See: https://springrts.com/phpbb/viewtopic.php?t=36654
42+ for unitDefID , unitDef in pairs (UnitDefs ) do
43+ if # unitDef .weapons >= 1 then
44+ if tonumber (unitDef .customParams .continuous_aim_time ) then
45+ local reaimTime = tonumber (unitDef .customParams .continuous_aim_time )
46+ local reaimFrames = math_max (math .round (reaimTime * Game .gameSpeed ), 1 )
47+ if reaimFrames < reaimFramesMax then
48+ unitReaimTimes [unitDefID ] = reaimFrames
49+ unitWeaponCount [unitDefID ] = # unitDef .weapons
50+ end
51+ end
52+ local spamCount = tonumber (unitDef .customParams .continuous_aim_spam ) or spamRatingBase
53+ local spamScore = 1 / math .clamp (spamCount , 1 , spamRatingMax ) -- as reaimTime per unit
54+ unitSpamRating [unitDefID ] = spamScore * unitCapDefault -- as reaimTime/unit/max units
16155 end
16256end
163- convertedUnitsNames = nil
164-
165-
166- local spamUnitsTeamsNames = { -- {unitDefID = {teamID = totalcreated,...}}
167- [' armpw' ] = {},
168- [' armflea' ] = {},
169- [' armfav' ] = {},
170- [' corak' ] = {},
171- [' corfav' ] = {},
172- }
173- -- convert unitname -> unitDefID
174- local spamUnitsTeams = {}
175- for name , params in pairs (spamUnitsTeamsNames ) do
176- if UnitDefNames [name ] then
177- spamUnitsTeams [UnitDefNames [name ].id ] = params
178- end
57+
58+ local teamMaxUnits = {}
59+ local teamReaimTimes = {}
60+
61+ local function isNonPlayerEnemyTeam (teamID )
62+ local luaAI = spGetTeamLuaAI (teamID )
63+ return luaAI and (luaAI :find (" Scavenger" ) or luaAI :find (" Raptor" )) -- seems not-clean tbh
17964end
180- spamUnitsTeamsNames = nil
18165
66+ local function getTeamMaxUnits (teamID )
67+ local actual = spGetTeamMaxUnits (teamID )
68+ local reference = isNonPlayerEnemyTeam (teamID ) and unitCapNonPlayer or unitCapReference
69+ return actual and (math_max (actual , reference ) + reference ) * 0.5 or reference
70+ end
18271
183- local spamUnitsTeamsReaimTimes = {} -- {unitDefID = {teamID = currentReAimTime,...}}
72+ function gadget :MetaUnitAdded (unitID , unitDefID , unitTeam )
73+ local unitReaimTime = unitReaimTimes [unitDefID ]
18474
75+ if unitReaimTime then
76+ local teamReaimTime = teamReaimTimes [unitTeam ]
77+ local addSpamRating = unitSpamRating [unitDefID ] / teamMaxUnits [unitTeam ]
18578
186- -- for every spamThreshold'th spammable unit type built by this team, increase reaimtime by 1 for that team
187- local spamThreshold = 100
188- local maxReAimTime = 15
79+ teamReaimTime = teamReaimTime + addSpamRating
80+ teamReaimTimes [ unitTeam ] = teamReaimTime
81+ unitReaimTime = math_clamp ( math_floor ( unitReaimTime + teamReaimTime ), 1 , reaimFramesMax )
18982
190- -- add for scavengers copies
191- local convertedUnitsCopy = tableCopy ( convertedUnits )
192- for id , v in pairs ( convertedUnitsCopy ) do
193- if UnitDefNames [ UnitDefs [ id ]. name .. ' _scav ' ] then
194- convertedUnits [ UnitDefNames [ UnitDefs [ id ]. name .. ' _scav ' ]. id ] = v
83+ for weaponNum = 1 , unitWeaponCount [ unitDefID ] do
84+ -- NOTE: this will prevent unit from firing if it does not IMMEDIATELY return from AimWeapon (no sleeps, not wait for turns! )
85+ -- So you have to manually check in script if it is at the desired heading
86+ spSetUnitWeaponState ( unitID , weaponNum , " reaimTime " , unitReaimTime )
87+ end
19588 end
19689end
19790
198- local spamUnitsTeamsCopy = tableCopy (spamUnitsTeams )
199- for id ,v in pairs (spamUnitsTeamsCopy ) do
200- if UnitDefNames [UnitDefs [id ].name .. ' _scav' ] then
201- spamUnitsTeams [UnitDefNames [UnitDefs [id ].name .. ' _scav' ].id ] = {}
91+ function gadget :MetaUnitRemoved (unitID , unitDefID , teamID )
92+ if unitSpamRating [unitDefID ] then
93+ teamReaimTimes [teamID ] = teamReaimTimes [teamID ] - unitSpamRating [unitDefID ]
20294 end
20395end
20496
205- for unitDefID , _ in pairs (spamUnitsTeams ) do
206- spamUnitsTeamsReaimTimes [unitDefID ] = {}
207- end
208-
209- local unitWeapons = {}
210- for unitDefID , _ in pairs (convertedUnits ) do
211- local unitDef = UnitDefs [unitDefID ]
212- if unitDef then
213- local weapons = unitDef .weapons
214- if # weapons > 0 then
215- unitWeapons [unitDefID ] = {}
216- for id , _ in pairs (weapons ) do
217- unitWeapons [unitDefID ][id ] = true -- no need to store weapondefid
218- end
219- else
220- -- units with no weapons shouldnt even be here
221- convertedUnits [unitDefID ] = nil
222- end
97+ function gadget :TeamDied (deadTeamID )
98+ -- For now, this is our only source of TransferTeamMaxUnits after init.
99+ for _ , teamID in pairs (Spring .GetTeamList () or {}) do
100+ teamMaxUnits [teamID ] = getTeamMaxUnits (teamID )
223101 end
224102end
225103
226- function gadget :UnitCreated (unitID , unitDefID , teamID )
227- if convertedUnits [unitDefID ] then
228- local currentReaimTime = convertedUnits [unitDefID ]
229-
230- if spamUnitsTeams [unitDefID ] then
231- if not spamUnitsTeams [unitDefID ][teamID ] then
232- -- initialize for this team at base defaults
233- spamUnitsTeams [unitDefID ][teamID ] = 1
234- spamUnitsTeamsReaimTimes [unitDefID ][teamID ] = convertedUnits [unitDefID ]
235- else
236- local spamCount = spamUnitsTeams [unitDefID ][teamID ] + 1
237- spamUnitsTeams [unitDefID ][teamID ] = spamCount
238- currentReaimTime = spamUnitsTeamsReaimTimes [unitDefID ][teamID ]
239- if spamCount % spamThreshold == 0 and currentReaimTime < maxReAimTime then
240- spamUnitsTeamsReaimTimes [unitDefID ][teamID ] = currentReaimTime + 1
241- -- Spring.Echo("Unit type", unitDefID,'has been built', spamCount, 'times by team', teamID,'increasing reaimtime to ', currentReaimTime + 1)
242- end
243- end
244- end
245- if currentReaimTime < 15 then
246- for id , _ in pairs (unitWeapons [unitDefID ]) do
247- -- NOTE: this will prevent unit from firing if it does not IMMEDIATELY return from AimWeapon (no sleeps, not wait for turns!)
248- -- So you have to manually check in script if it is at the desired heading
249- -- https://springrts.com/phpbb/viewtopic.php?t=36654
250- spSetUnitWeaponState (unitID , id , " reaimTime" , currentReaimTime )
251- end
104+ function gadget :Initialize ()
105+ teamMaxUnits = {}
106+ teamReaimTimes = {}
107+
108+ for _ , teamID in pairs (Spring .GetTeamList () or {}) do
109+ teamMaxUnits [teamID ] = getTeamMaxUnits (teamID )
110+ teamReaimTimes [teamID ] = 0
111+
112+ for _ , unitID in pairs (Spring .GetTeamUnits (teamID ) or {}) do
113+ gadget :MetaUnitAdded (unitID , Spring .GetUnitDefID (unitID ), teamID )
252114 end
253115 end
254116end
0 commit comments