Guest User

Untitled

a guest
Oct 21st, 2026
46
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 46.76 KB | None | 0 0
  1. --------------------------------------------------------------------------------
  2. --------------------------------------------------------------------------------
  3.  
  4. function widget:GetInfo()
  5. return {
  6. name = "Auto Retreat Plus",
  7. desc = "Extends the built-in auto retreat: retreating units path around enemy reach, stop when a constructor repairs them, " ..
  8. "can skip the walk back afterwards, resume their patrol, and idle engineers in retreat zones repair whoever is nearby.",
  9. author = "Thomas",
  10. date = "2026-10-21",
  11. license = "GNU GPL, v2 or later",
  12. layer = 0,
  13. enabled = true,
  14. }
  15. end
  16.  
  17. --------------------------------------------------------------------------------
  18. --------------------------------------------------------------------------------
  19. -- How it works
  20. --
  21. -- Auto retreat is the synced gadget LuaRules/Gadgets/cmd_retreat.lua, not a
  22. -- widget, and a widget cannot replace a gadget. This widget leaves the gadget
  23. -- in charge of when a unit retreats (the Retreat 30/65/99% button, the retreat
  24. -- zones, start states) and takes over how it gets there.
  25. --
  26. -- When the gadget starts a retreat it puts [RAW_MOVE to a point in the zone,
  27. -- WAIT] at the front of the queue, and if the unit was idle it appends a
  28. -- RAW_MOVE back to where the unit stood. It clears the WAIT at full health.
  29. -- The gadget checks every 20 frames at frame % 20 == 10, and this widget looks
  30. -- in the same frame, right after it, so it sees those orders the moment they
  31. -- are given:
  32. --
  33. -- * Enemy avoidance. Each visible enemy gets a danger disk of radius
  34. -- weapon range + speed * reach time + margin: where it could shoot us
  35. -- within that many seconds. A* over a coarse grid, with danger as extra
  36. -- cost, gives a route; it is pulled tight into a few waypoints and inserted
  37. -- in front of the gadget's WAIT. Replanned while the enemies move, three
  38. -- times a second while the unit is taking damage. Enemies that drop out
  39. -- of sight are remembered: structures until their spot is seen empty,
  40. -- mobile units for ten seconds.
  41. -- * Stop for repair. If an allied constructor is repairing the unit and the
  42. -- unit is out of every danger disk and not taking damage, its moves are
  43. -- removed so it halts on the gadget's WAIT. They come back when the repair
  44. -- stops.
  45. -- * No walk back. The return RAW_MOVE is removed.
  46. -- * Patrol. The unit's patrol points are recorded when the retreat starts. If
  47. -- none are left in the queue when it ends, the unit walks to the first one
  48. -- and patrols the same loop again.
  49. -- * Retreat zone engineers. Constructors that are idle inside a zone, or
  50. -- sitting on the retreat WAIT there, repair damaged allies in reach,
  51. -- retreating units first.
  52. --
  53. -- Our moves are recognised by their coordinates, as the Lotus widget does
  54. -- with its aim points: an order we send has no tag until it arrives.
  55. --------------------------------------------------------------------------------
  56.  
  57. local spGetUnitPosition = Spring.GetUnitPosition
  58. local spGetUnitDefID = Spring.GetUnitDefID
  59. local spGetUnitCommands = Spring.GetUnitCommands
  60. local spGetUnitCommandCount = Spring.GetUnitCommandCount
  61. local spGetUnitRulesParam = Spring.GetUnitRulesParam
  62. local spGetUnitsInCylinder = Spring.GetUnitsInCylinder
  63. local spGetUnitsInRectangle = Spring.GetUnitsInRectangle
  64. local spGetUnitIsBuilding = Spring.GetUnitIsBuilding
  65. local spGetUnitIsBeingBuilt = Spring.GetUnitIsBeingBuilt
  66. local spGetUnitIsStunned = Spring.GetUnitIsStunned
  67. local spGetUnitHealth = Spring.GetUnitHealth
  68. local spGetUnitRadius = Spring.GetUnitRadius
  69. local spGetGroundHeight = Spring.GetGroundHeight
  70. local spGetGroundNormal = Spring.GetGroundNormal
  71. local spGetTeamRulesParam = Spring.GetTeamRulesParam
  72. local spGetGameRulesParam = Spring.GetGameRulesParam
  73. local spGetTeamList = Spring.GetTeamList
  74. local spGetTeamUnits = Spring.GetTeamUnits
  75. local spGiveOrderToUnit = Spring.GiveOrderToUnit
  76. local spGiveOrderArrayToUnitArray = Spring.GiveOrderArrayToUnitArray
  77. local spValidUnitID = Spring.ValidUnitID
  78. local spIsUnitSelected = Spring.IsUnitSelected
  79. local spGetGameFrame = Spring.GetGameFrame
  80. local spIsPosInLos = Spring.IsPosInLos
  81.  
  82. local sqrt, floor, ceil, min, max, abs = math.sqrt, math.floor, math.ceil, math.min, math.max, math.abs
  83. local cos, rad = math.cos, math.rad
  84.  
  85. local CUSTOM = (Spring.Utilities and Spring.Utilities.CMD) or {}
  86. local CMD_RAW_MOVE = CUSTOM.RAW_MOVE or 31109
  87. local CMD_REARM = CUSTOM.REARM or 33410
  88. local CMD_WAIT = CMD.WAIT
  89. local CMD_PATROL = CMD.PATROL
  90. local CMD_REPAIR = CMD.REPAIR
  91. local CMD_INSERT = CMD.INSERT
  92. local CMD_REMOVE = CMD.REMOVE
  93. local CMD_OPT_SHIFT = CMD.OPT_SHIFT
  94. local ENEMY_UNITS = Spring.ENEMY_UNITS
  95. local ALLY_UNITS = Spring.ALLY_UNITS
  96.  
  97. local MAP_X, MAP_Z = Game.mapSizeX, Game.mapSizeZ
  98.  
  99. -- Danger grid. Coarse on purpose: it only decides the waypoints, and the
  100. -- engine pathfinder still walks between them.
  101. local CELL = max(96, ceil(max(MAP_X, MAP_Z) / 72))
  102. local NX, NZ = ceil(MAP_X / CELL), ceil(MAP_Z / CELL)
  103.  
  104. local MAX_CHASE = 600 -- elmos; caps speed * reach time for fast raiders and bombers
  105. -- A* costs about 10 us a node in plain Lua 5.1. Plans run in a coroutine that
  106. -- stops for the frame once it has spent NODES_PER_FRAME, so one plan may span
  107. -- several frames but no frame pays more than a millisecond or two. A plan that
  108. -- hits MAX_EXPAND heads for the explored cell nearest the zone, and the next
  109. -- replan carries on from there.
  110. local MAX_EXPAND = 1500 -- A* nodes per plan
  111. local NODES_PER_FRAME = 150
  112. local HEURISTIC_WEIGHT = 1.6
  113. local MAX_WAYPOINTS = 10
  114. local RADAR_THREAT = {range = 300, speed = 60, w = 0.6}
  115. local MOBILE_MEMORY = 300 -- frames a mobile enemy is still avoided after it leaves sight
  116. local RETREAT_RADIUS = 160 -- read from the gadget in Initialize
  117.  
  118. --------------------------------------------------------------------------------
  119. -- Options
  120.  
  121. options_path = 'Settings/Unit Behaviour/Auto Retreat Plus'
  122.  
  123. local function V(key)
  124. return options[key].value
  125. end
  126.  
  127. options_order = {
  128. 'lbl_avoid',
  129. 'avoidEnemies', 'reachTime', 'safetyMargin', 'avoidStrength', 'maxThreatRange', 'avoidRadar', 'replanInterval',
  130.  
  131. 'lbl_repair',
  132. 'stopForRepair', 'repairResumeDelay', 'havenEngineers', 'engineerReach',
  133.  
  134. 'lbl_after',
  135. 'returnToPosition', 'resumePatrol',
  136.  
  137. 'lbl_draw',
  138. 'drawRoutes', 'drawThreats',
  139. }
  140.  
  141. options = {
  142. lbl_avoid = {name = 'Enemy avoidance', type = 'label', noSave = true},
  143.  
  144. avoidEnemies = {
  145. name = 'Path around enemies while retreating',
  146. desc = 'Retreating units take a route around the estimated reach of visible enemies instead of the straight line to the retreat zone.',
  147. type = 'bool', value = true,
  148. },
  149. reachTime = {
  150. name = 'Enemy reach time (s)',
  151. desc = 'Each enemy is avoided by its weapon range plus how far it can move in this many seconds. ' ..
  152. 'Higher keeps a wider berth from fast units. Static defences only count their range.',
  153. type = 'number', value = 2, min = 0, max = 6, step = 0.5,
  154. },
  155. safetyMargin = {
  156. name = 'Extra safety margin (elmos)',
  157. type = 'number', value = 60, min = 0, max = 300, step = 10,
  158. },
  159. avoidStrength = {
  160. name = 'Detour willingness',
  161. desc = 'How much longer a route may be to stay out of danger. At 6, an elmo inside the deepest part of an enemy zone costs as much as about seven elmos of safe ground.',
  162. type = 'number', value = 6, min = 1, max = 20, step = 1,
  163. },
  164. maxThreatRange = {
  165. name = 'Ignore enemies with more range than',
  166. desc = 'Long-range artillery and superweapons cannot be walked around. They are left out so they do not cover the whole map.',
  167. type = 'number', value = 1400, min = 400, max = 4000, step = 100,
  168. },
  169. avoidRadar = {
  170. name = 'Avoid unidentified radar dots',
  171. desc = 'Treat radar contacts as a small generic threat.',
  172. type = 'bool', value = true,
  173. },
  174. replanInterval = {
  175. name = 'Replan every (s)',
  176. type = 'number', value = 1.5, min = 0.5, max = 5, step = 0.5,
  177. },
  178.  
  179. lbl_repair = {name = 'Repair', type = 'label', noSave = true},
  180.  
  181. stopForRepair = {
  182. name = 'Stop retreating while being repaired',
  183. desc = 'A retreating unit that an allied constructor is repairing waits where it is, unless it is inside enemy reach or taking damage. It moves on if the repair stops.',
  184. type = 'bool', value = true,
  185. },
  186. repairResumeDelay = {
  187. name = 'Move on after repair stops for (s)',
  188. type = 'number', value = 1.5, min = 0.5, max = 6, step = 0.5,
  189. },
  190. havenEngineers = {
  191. name = 'Engineers in retreat zones repair nearby units',
  192. desc = 'Idle constructors inside a retreat zone, including ones that retreated there, repair damaged allied units in reach. Retreating units go first, then the most damaged.',
  193. type = 'bool', value = true,
  194. },
  195. engineerReach = {
  196. name = 'Engineers walk up to (elmos)',
  197. desc = 'How far past its build range a zone engineer may walk to reach a damaged unit. It walks back afterwards.',
  198. type = 'number', value = 150, min = 0, max = 500, step = 25,
  199. },
  200.  
  201. lbl_after = {name = 'After the retreat', type = 'label', noSave = true},
  202.  
  203. returnToPosition = {
  204. name = 'Go back to where it was',
  205. desc = 'At full health, a unit that was idle when it retreated walks back to where it stood (the built-in behaviour). Turn off to leave it in the retreat zone.',
  206. type = 'bool', value = true,
  207. },
  208. resumePatrol = {
  209. name = 'Return to patrol route',
  210. desc = 'A unit that was patrolling when it retreated goes back to the same patrol loop at full health, if its patrol orders were lost.',
  211. type = 'bool', value = true,
  212. },
  213.  
  214. lbl_draw = {name = 'Display', type = 'label', noSave = true},
  215.  
  216. drawRoutes = {
  217. name = 'Draw retreat routes of selected units',
  218. type = 'bool', value = true,
  219. },
  220. drawThreats = {
  221. name = 'Draw enemy reach estimates near selected retreating units',
  222. type = 'bool', value = false,
  223. },
  224. }
  225.  
  226. --------------------------------------------------------------------------------
  227. -- Unit data
  228.  
  229. local function Truthy(v)
  230. return v and v ~= "0" and v ~= "false" and v ~= 0 and v ~= false
  231. end
  232.  
  233. local moverDef = {} -- defID -> true: has the retreat command
  234. local builderDef = {} -- defID -> build distance, for anything that can repair
  235. local mobileDef = {} -- defID -> true for builders that can walk to a target
  236. local ignoreDef = {} -- terraform markers and the like
  237. local defCats = {} -- defID -> {set = {cat = true}, key = "a b c"}
  238. local defMove = {} -- defID -> passability key, false for flyers
  239. local maxBuildDist = 0
  240.  
  241. for defID = 1, #UnitDefs do
  242. local ud = UnitDefs[defID]
  243. local cp = ud.customParams or {}
  244. if Truthy(cp.dontcount) then
  245. ignoreDef[defID] = true
  246. end
  247. if ud.canMove and (ud.speed or 0) > 0 then
  248. moverDef[defID] = true
  249. end
  250. local bd = ud.buildDistance or 0
  251. if ud.isBuilder and ud.canRepair and not ud.isFactory and bd > 0 then
  252. builderDef[defID] = bd
  253. maxBuildDist = max(maxBuildDist, bd)
  254. if ud.canMove and (ud.speed or 0) > 0 then
  255. mobileDef[defID] = true
  256. end
  257. end
  258. end
  259. maxBuildDist = min(maxBuildDist, 1000)
  260.  
  261. local function Cats(defID)
  262. local c = defCats[defID]
  263. if c then
  264. return c
  265. end
  266. local set, list = {}, {}
  267. local mc = UnitDefs[defID].modCategories or {}
  268. for cat in pairs(mc) do
  269. local lc = string.lower(cat)
  270. set[lc] = true
  271. list[#list + 1] = lc
  272. end
  273. table.sort(list)
  274. c = {set = set, key = table.concat(list, " "), fly = UnitDefs[defID].canFly and true or false}
  275. defCats[defID] = c
  276. return c
  277. end
  278.  
  279. -- What one enemy type can do to one kind of target. Same reading of the weapon
  280. -- tables as the Combat Advisor: stats_damage, and mount-level onlyTargets keyed
  281. -- by lowercase category, where an empty table hits everything.
  282. local profiles = {} -- catKey -> defID -> profile or false
  283.  
  284. local function Profile(defID, cats)
  285. local byKey = profiles[cats.key]
  286. if not byKey then
  287. byKey = {}
  288. profiles[cats.key] = byKey
  289. end
  290. local p = byKey[defID]
  291. if p ~= nil then
  292. return p
  293. end
  294. local ud = UnitDefs[defID]
  295. local range, dps = 0, 0
  296. local weapons = ud.weapons or {}
  297. for i = 1, #weapons do
  298. local wpn = weapons[i]
  299. local wd = wpn.weaponDef and WeaponDefs[wpn.weaponDef]
  300. if wd and not wd.isShield and not (cats.fly and wd.type == "TorpedoLauncher") then
  301. local wcp = wd.customParams or {}
  302. local dmg = tonumber(wcp.stats_damage) or (wd.damages and wd.damages[0]) or 0
  303. if wd.paralyzer then
  304. dmg = dmg * 0.5
  305. end
  306. local reload = wd.reload or 0
  307. if reload <= 0 then
  308. reload = 1
  309. end
  310. local wdps = dmg * (wd.salvoSize or 1) * (wd.projectiles or 1) / reload
  311. if wdps > 1 then
  312. local ot = wpn.onlyTargets
  313. local hits = (not ot) or (next(ot) == nil)
  314. if not hits then
  315. for cat in pairs(ot) do
  316. if cats.set[string.lower(cat)] then
  317. hits = true
  318. break
  319. end
  320. end
  321. end
  322. if hits then
  323. range = max(range, wd.range or 0)
  324. dps = dps + wdps
  325. end
  326. end
  327. end
  328. end
  329. if range <= 0 then
  330. p = false
  331. else
  332. p = {range = range, speed = ud.speed or 0, w = min(max(sqrt(dps / 150), 0.5), 3)}
  333. end
  334. byKey[defID] = p
  335. return p
  336. end
  337.  
  338. --------------------------------------------------------------------------------
  339. -- State
  340.  
  341. local retreaters = {} -- unitID -> retreat record, while the gadget has it retreating
  342. local myMovers = {} -- unitID -> defID
  343. local myBuilders = {} -- unitID -> defID
  344. local patrolJobs = {} -- unitID -> patrol restore in progress
  345. local havens = {} -- {x, z} of every allied retreat zone
  346. local myHavens = {} -- own team's only, where our units retreat to
  347.  
  348. local myTeam, myAllyTeam, spectating
  349.  
  350. local threatList, threatFrame = {}, -1000
  351. local enemyMemory = {} -- unitID -> {x, z, defID, frame}: last sighting, kept after the enemy leaves sight
  352. local fields = {} -- catKey -> {frame, d = {cell = danger}, threats = {...}}
  353. local passCache = {} -- move key -> {cell = bool}
  354.  
  355. local planQueue = {} -- unitIDs waiting for the planner, most urgent first
  356. local planner -- the planning coroutine
  357. local planSpent = 0 -- work units spent this frame (one per A* node)
  358.  
  359. -- Called from inside planning work: pauses the planner until next frame once
  360. -- the frame's budget is gone. Outside the coroutine it only counts.
  361. local function Spend(k)
  362. planSpent = planSpent + k
  363. if planSpent >= NODES_PER_FRAME and coroutine.running() then
  364. coroutine.yield()
  365. end
  366. end
  367.  
  368. --------------------------------------------------------------------------------
  369. -- Grid helpers
  370.  
  371. local INV_CELL = 1 / CELL
  372.  
  373. local function CellOf(x, z)
  374. local cx, cz = floor(x * INV_CELL), floor(z * INV_CELL)
  375. if cx < 0 then cx = 0 elseif cx >= NX then cx = NX - 1 end
  376. if cz < 0 then cz = 0 elseif cz >= NZ then cz = NZ - 1 end
  377. return cz * NX + cx + 1, cx, cz
  378. end
  379.  
  380. local function CellCenter(i)
  381. local j = i - 1
  382. local cx = j % NX
  383. local cz = (j - cx) / NX
  384. return (cx + 0.5) * CELL, (cz + 0.5) * CELL
  385. end
  386.  
  387. local function MoveKey(defID)
  388. local k = defMove[defID]
  389. if k ~= nil then
  390. return k
  391. end
  392. local ud = UnitDefs[defID]
  393. if ud.canFly or not ud.moveDef then
  394. k = false
  395. else
  396. k = tostring(ud.moveDef.name or ud.moveDef.id or defID)
  397. end
  398. defMove[defID] = k
  399. return k
  400. end
  401.  
  402. -- moveDef.maxSlope is stored as 1 - cos(angle), the same scale as 1 - normal.y.
  403. local function SlopeLimit(md)
  404. local s = md.maxSlope
  405. if not s then
  406. return 1
  407. end
  408. if s > 1 then -- degrees, if an engine ever hands those out
  409. return 1 - cos(rad(s))
  410. end
  411. return s
  412. end
  413.  
  414. local function PointPassable(ud, md, family, x, z)
  415. local h = spGetGroundHeight(x, z)
  416. if family == "ship" or (ud.minWaterDepth or 0) > 0 then
  417. return h < -(ud.minWaterDepth or 5)
  418. end
  419. if h < 0 then
  420. if family == "hover" then
  421. return true
  422. end
  423. local depth = md.depth or ud.maxWaterDepth or 22
  424. if -h > depth then
  425. return false
  426. end
  427. end
  428. local _, ny = spGetGroundNormal(x, z)
  429. if ny and (1 - ny) > SlopeLimit(md) then
  430. return false
  431. end
  432. return true
  433. end
  434.  
  435. -- The per-cell cache for this unit's movement class, nil for flyers.
  436. local function PassTable(defID)
  437. local key = MoveKey(defID)
  438. if not key then
  439. return nil
  440. end
  441. local cache = passCache[key]
  442. if not cache then
  443. cache = {}
  444. passCache[key] = cache
  445. end
  446. return cache
  447. end
  448.  
  449. local function Passable(defID, i)
  450. local cache = PassTable(defID)
  451. if not cache then
  452. return true
  453. end
  454. local p = cache[i]
  455. if p == nil then
  456. local ud = UnitDefs[defID]
  457. local md = ud.moveDef
  458. local family = md.family or md.type
  459. local x, z = CellCenter(i)
  460. local o = CELL * 0.3
  461. local ok = 0
  462. if PointPassable(ud, md, family, x, z) then ok = ok + 1 end
  463. if PointPassable(ud, md, family, x - o, z - o) then ok = ok + 1 end
  464. if PointPassable(ud, md, family, x + o, z - o) then ok = ok + 1 end
  465. if PointPassable(ud, md, family, x - o, z + o) then ok = ok + 1 end
  466. if PointPassable(ud, md, family, x + o, z + o) then ok = ok + 1 end
  467. p = ok >= 3
  468. cache[i] = p
  469. end
  470. return p
  471. end
  472.  
  473. --------------------------------------------------------------------------------
  474. -- Enemy reach estimate
  475.  
  476. -- Enemies that left sight are still avoided: a structure until the spot is back
  477. -- in LOS without it, a mobile unit for MOBILE_MEMORY frames, its reach growing
  478. -- with the time since it was seen. Without this a unit that retreats out of
  479. -- sight forgets the turrets it just ran from and plans straight back past them.
  480. local function CollectThreats(frame)
  481. if frame - threatFrame < 15 then
  482. return threatList
  483. end
  484. threatFrame = frame
  485. local units = spGetUnitsInRectangle(0, 0, MAP_X, MAP_Z, ENEMY_UNITS) or {}
  486. for k = 1, #units do
  487. local u = units[k]
  488. local x, _, z = spGetUnitPosition(u)
  489. if x then
  490. local m = enemyMemory[u]
  491. local defID = spGetUnitDefID(u) or (m and m.defID) -- a radar dot keeps its identity
  492. if defID and (ignoreDef[defID] or (spGetUnitIsBeingBuilt and spGetUnitIsBeingBuilt(u))) then
  493. enemyMemory[u] = nil
  494. elseif m then
  495. m.x, m.z, m.defID, m.frame = x, z, defID, frame
  496. else
  497. enemyMemory[u] = {x = x, z = z, defID = defID, frame = frame}
  498. end
  499. end
  500. end
  501. local list = {}
  502. local radar = V('avoidRadar')
  503. for u, m in pairs(enemyMemory) do
  504. local age = frame - m.frame
  505. local keep = true
  506. if age > 0 then
  507. local static = m.defID and not moverDef[m.defID]
  508. if not static then
  509. keep = age <= MOBILE_MEMORY and m.defID ~= nil
  510. elseif spIsPosInLos and spIsPosInLos(m.x, spGetGroundHeight(m.x, m.z), m.z, myAllyTeam) then
  511. keep = false -- we can see the spot and it is not there
  512. end
  513. end
  514. if not keep then
  515. enemyMemory[u] = nil
  516. elseif m.defID or radar then
  517. list[#list + 1] = {x = m.x, z = m.z, defID = m.defID, age = age}
  518. end
  519. end
  520. threatList = list
  521. return list
  522. end
  523.  
  524. local function Stamp(d, x, z, R, w)
  525. local cx0, cx1 = max(floor((x - R) / CELL), 0), min(floor((x + R) / CELL), NX - 1)
  526. local cz0, cz1 = max(floor((z - R) / CELL), 0), min(floor((z + R) / CELL), NZ - 1)
  527. for cz = cz0, cz1 do
  528. local pz = (cz + 0.5) * CELL - z
  529. for cx = cx0, cx1 do
  530. local px = (cx + 0.5) * CELL - x
  531. local dd = sqrt(px*px + pz*pz)
  532. if dd < R then
  533. local i = cz * NX + cx + 1
  534. d[i] = (d[i] or 0) + w * (0.4 + 0.6 * (1 - dd / R))
  535. end
  536. end
  537. end
  538. end
  539.  
  540. local function GetField(defID, frame)
  541. local cats = Cats(defID)
  542. local f = fields[cats.key]
  543. if f and frame - f.frame < 30 then
  544. return f
  545. end
  546. local d, drawn = {}, {}
  547. local T, margin, maxRange = V('reachTime'), V('safetyMargin'), V('maxThreatRange')
  548. local list = CollectThreats(frame)
  549. for k = 1, #list do
  550. local t = list[k]
  551. local p
  552. if t.defID then
  553. p = Profile(t.defID, cats)
  554. else
  555. p = RADAR_THREAT
  556. end
  557. if p and p.range <= maxRange then
  558. local R = p.range + min(p.speed * (T + t.age / 30), MAX_CHASE) + margin
  559. Stamp(d, t.x, t.z, R, p.w)
  560. drawn[#drawn + 1] = {t.x, t.z, R}
  561. end
  562. end
  563. f = {frame = frame, d = d, threats = drawn}
  564. fields[cats.key] = f
  565. return f
  566. end
  567.  
  568. local function DangerAt(f, x, z)
  569. return f.d[(CellOf(x, z))] or 0
  570. end
  571.  
  572. --------------------------------------------------------------------------------
  573. -- Route planning
  574.  
  575. -- Binary heap on parallel arrays.
  576. local function HeapPush(hf, hi, n, f, i)
  577. n = n + 1
  578. local c = n
  579. while c > 1 do
  580. local p = floor(c / 2)
  581. if hf[p] <= f then
  582. break
  583. end
  584. hf[c], hi[c] = hf[p], hi[p]
  585. c = p
  586. end
  587. hf[c], hi[c] = f, i
  588. return n
  589. end
  590.  
  591. local function HeapPop(hf, hi, n)
  592. local topI = hi[1]
  593. local lf, li = hf[n], hi[n]
  594. hf[n], hi[n] = nil, nil
  595. n = n - 1
  596. if n > 0 then
  597. local c = 1
  598. while true do
  599. local l = c * 2
  600. if l > n then
  601. break
  602. end
  603. if l < n and hf[l + 1] < hf[l] then
  604. l = l + 1
  605. end
  606. if hf[l] >= lf then
  607. break
  608. end
  609. hf[c], hi[c] = hf[l], hi[l]
  610. c = l
  611. end
  612. hf[c], hi[c] = lf, li
  613. end
  614. return topI, n
  615. end
  616.  
  617. local NB_DX = {1, -1, 0, 0, 1, 1, -1, -1}
  618. local NB_DZ = {0, 0, 1, -1, 1, -1, 1, -1}
  619. local NB_LEN = {1, 1, 1, 1, 1.41421, 1.41421, 1.41421, 1.41421}
  620.  
  621. -- Returns the cell path, whether it stops short of the goal, and the nodes spent.
  622. -- The inner loop is inlined by hand: this runs in plain Lua 5.1.
  623. local function AStar(defID, f, sx, sz, gx, gz, budget)
  624. local d = f.d
  625. local halfK = V('avoidStrength') * 0.5
  626. local s, scx, scz = CellOf(sx, sz)
  627. local g, gcx, gcz = CellOf(gx, gz)
  628. if s == g then
  629. return {s}, false, 0
  630. end
  631. local span = max(abs(gcx - scx), abs(gcz - scz))
  632. local pad = max(12, floor(span * 0.5))
  633. local bx0, bx1 = max(min(scx, gcx) - pad, 0), min(max(scx, gcx) + pad, NX - 1)
  634. local bz0, bz1 = max(min(scz, gcz) - pad, 0), min(max(scz, gcz) + pad, NZ - 1)
  635. local pt = PassTable(defID) -- nil for flyers
  636.  
  637. -- Octile distance, inflated: a slightly longer route is fine, and a greedier
  638. -- search expands a fraction of the nodes when danger covers much of the map.
  639. local hw = CELL * HEURISTIC_WEIGHT
  640. local hf, hi, n = {}, {}, 0
  641. local gScore, came, closed = {[s] = 0}, {}, {}
  642.  
  643. local function PathTo(i)
  644. local path = {i}
  645. while came[path[1]] do
  646. table.insert(path, 1, came[path[1]])
  647. end
  648. return path
  649. end
  650.  
  651. local dx0, dz0 = abs(scx - gcx), abs(scz - gcz)
  652. local bestI = s
  653. local bestH = hw * (max(dx0, dz0) + 0.41421 * min(dx0, dz0))
  654. n = HeapPush(hf, hi, n, bestH, s)
  655. local expanded = 0
  656. while n > 0 do
  657. local cur
  658. cur, n = HeapPop(hf, hi, n)
  659. if cur == g then
  660. return PathTo(g), false, expanded
  661. end
  662. if not closed[cur] then
  663. closed[cur] = true
  664. expanded = expanded + 1
  665. if expanded % 25 == 0 then
  666. Spend(25)
  667. end
  668. if expanded > budget then
  669. -- Out of budget: head for the explored cell nearest the goal.
  670. if bestI ~= s then
  671. return PathTo(bestI), true, expanded
  672. end
  673. return nil, false, expanded
  674. end
  675. local j = cur - 1
  676. local cx = j % NX
  677. local cz = (j - cx) / NX
  678. local dCur = d[cur] or 0
  679. local gCur = gScore[cur]
  680. for k = 1, 8 do
  681. local nx, nz = cx + NB_DX[k], cz + NB_DZ[k]
  682. if nx >= bx0 and nx <= bx1 and nz >= bz0 and nz <= bz1 then
  683. local ni = nz * NX + nx + 1
  684. if not closed[ni] then
  685. local ok = true
  686. if pt and ni ~= g then
  687. ok = pt[ni]
  688. if ok == nil then
  689. ok = Passable(defID, ni)
  690. end
  691. end
  692. if ok then
  693. local ng = gCur + CELL * NB_LEN[k] * (1 + halfK * (dCur + (d[ni] or 0)))
  694. local old = gScore[ni]
  695. if not old or ng < old then
  696. gScore[ni] = ng
  697. came[ni] = cur
  698. local hx, hz = nx - gcx, nz - gcz
  699. if hx < 0 then hx = -hx end
  700. if hz < 0 then hz = -hz end
  701. local h
  702. if hx > hz then
  703. h = hw * (hx + 0.41421 * hz)
  704. else
  705. h = hw * (hz + 0.41421 * hx)
  706. end
  707. if h < bestH then
  708. bestI, bestH = ni, h
  709. end
  710. n = HeapPush(hf, hi, n, ng + h, ni)
  711. end
  712. end
  713. end
  714. end
  715. end
  716. end
  717. end
  718. return nil, false, expanded
  719. end
  720.  
  721. -- Can the unit walk straight from cell a to cell b without more danger than the
  722. -- grid path between them had?
  723. local function SegmentClear(defID, f, ax, az, bx, bz, limit)
  724. local d = f.d
  725. local pt = PassTable(defID)
  726. local dx, dz = bx - ax, bz - az
  727. local len = sqrt(dx*dx + dz*dz)
  728. local steps = max(1, ceil(len / (CELL * 0.5)))
  729. Spend(steps * 0.1)
  730. limit = limit + 0.01
  731. for k = 1, steps - 1 do
  732. local t = k / steps
  733. local i = CellOf(ax + dx * t, az + dz * t)
  734. if (d[i] or 0) > limit then
  735. return false
  736. end
  737. if pt then
  738. local p = pt[i]
  739. if p == nil then
  740. p = Passable(defID, i)
  741. end
  742. if not p then
  743. return false
  744. end
  745. end
  746. end
  747. return true
  748. end
  749.  
  750. -- Danger-weighted length of a straight segment.
  751. local function SegCost(f, ax, az, bx, bz, K)
  752. local d = f.d
  753. local dx, dz = bx - ax, bz - az
  754. local len = sqrt(dx*dx + dz*dz)
  755. local steps = max(1, ceil(len / (CELL * 0.5)))
  756. Spend(steps * 0.1)
  757. local step, cost = len / steps, 0
  758. for s = 1, steps do
  759. local t = (s - 0.5) / steps
  760. cost = cost + step * (1 + K * (d[(CellOf(ax + dx * t, az + dz * t))] or 0))
  761. end
  762. return cost
  763. end
  764.  
  765. -- Pull the grid path tight. Only the cells where the path turns are candidate
  766. -- waypoints, and a shortcut must not cost more, danger included, than the
  767. -- stretch of grid path it replaces. Comparing only the worst cell let a unit
  768. -- starting deep in enemy reach cut diagonally across it instead of backing out
  769. -- the way the grid path did.
  770. local function Smooth(defID, f, path, sx, sz)
  771. local K = V('avoidStrength')
  772. local m = #path
  773. local corners = {1}
  774. for k = 2, m - 1 do
  775. if path[k] - path[k - 1] ~= path[k + 1] - path[k] then
  776. corners[#corners + 1] = k
  777. end
  778. end
  779. if m > 1 then
  780. corners[#corners + 1] = m
  781. end
  782. local out = {}
  783. local a = 1 -- index into corners
  784. local ax, az = sx, sz
  785. local nc = #corners
  786. while a < nc do
  787. local b = a + 1
  788. -- Cost of following the grid from the anchor to corner b.
  789. local px, pz = ax, az
  790. local grid = 0
  791. for k = corners[a] + 1, corners[b] do
  792. local cx, cz = CellCenter(path[k])
  793. grid = grid + SegCost(f, px, pz, cx, cz, K)
  794. px, pz = cx, cz
  795. end
  796. while b < nc do
  797. local nextGrid = grid
  798. for k = corners[b] + 1, corners[b + 1] do
  799. local cx, cz = CellCenter(path[k])
  800. nextGrid = nextGrid + SegCost(f, px, pz, cx, cz, K)
  801. px, pz = cx, cz
  802. end
  803. local qx, qz = CellCenter(path[corners[b + 1]])
  804. if SegCost(f, ax, az, qx, qz, K) <= nextGrid + 1 and SegmentClear(defID, f, ax, az, qx, qz, math.huge) then
  805. b = b + 1
  806. grid = nextGrid
  807. else
  808. break
  809. end
  810. end
  811. ax, az = CellCenter(path[corners[b]])
  812. out[#out + 1] = {ax, az}
  813. a = b
  814. end
  815. return out
  816. end
  817.  
  818. local function RoundPoint(x, z)
  819. x = floor(min(max(x, 8), MAP_X - 8) + 0.5)
  820. z = floor(min(max(z, 8), MAP_Z - 8) + 0.5)
  821. return {x, spGetGroundHeight(x, z), z}
  822. end
  823.  
  824. -- Waypoints to dest, ending with dest itself, and the A* nodes spent. nil if no
  825. -- plan could be made.
  826. local function PlanRoute(unitID, r, frame, budget)
  827. local x, _, z = spGetUnitPosition(unitID)
  828. if not x then
  829. return nil, 0
  830. end
  831. local dest = r.dest
  832. if not V('avoidEnemies') then
  833. return {dest}, 0
  834. end
  835. local f = GetField(r.defID, frame)
  836. local path, partial, used = AStar(r.defID, f, x, z, dest[1], dest[3], budget or MAX_EXPAND)
  837. if not path then
  838. return nil, used
  839. end
  840. local pts = {}
  841. local smooth = Smooth(r.defID, f, path, x, z)
  842. -- The last cell is dest's own, unless the search stopped short of it.
  843. for k = 1, #smooth - (partial and 0 or 1) do
  844. local p = smooth[k]
  845. local dx, dz = p[1] - x, p[2] - z
  846. if dx*dx + dz*dz > (CELL * 0.75)^2 then
  847. pts[#pts + 1] = RoundPoint(p[1], p[2])
  848. end
  849. end
  850. while #pts > MAX_WAYPOINTS - 1 do
  851. table.remove(pts, #pts)
  852. end
  853. pts[#pts + 1] = dest
  854. return pts, used
  855. end
  856.  
  857. -- Danger-weighted length of a polyline, and whether any of it is in danger.
  858. local function PolyCost(f, x, z, pts)
  859. local K = V('avoidStrength')
  860. local cost, risky = 0, false
  861. local px, pz = x, z
  862. for k = 1, #pts do
  863. local qx, qz = pts[k][1], pts[k][3]
  864. local dx, dz = qx - px, qz - pz
  865. local len = sqrt(dx*dx + dz*dz)
  866. local steps = max(1, ceil(len / (CELL * 0.5)))
  867. local step = len / steps
  868. for s = 1, steps do
  869. local t = (s - 0.5) / steps
  870. local danger = DangerAt(f, px + dx * t, pz + dz * t)
  871. if danger > 0 then
  872. risky = true
  873. end
  874. cost = cost + step * (1 + K * danger)
  875. end
  876. px, pz = qx, qz
  877. end
  878. return cost, risky
  879. end
  880.  
  881. --------------------------------------------------------------------------------
  882. -- Queue helpers
  883.  
  884. local function Near(p, x, z, tol)
  885. return p and abs(p[1] - x) < tol and abs(p[3] - z) < tol
  886. end
  887.  
  888. local function HasTag(unitID, tag)
  889. if not tag then
  890. return false
  891. end
  892. local q = spGetUnitCommands(unitID, -1) or {}
  893. for k = 1, #q do
  894. if q[k].tag == tag then
  895. return true
  896. end
  897. end
  898. return false
  899. end
  900.  
  901. -- Tags and points of the route moves still queued in front of the WAIT.
  902. local function RouteInQueue(unitID, r)
  903. local tags, pts = {}, {}
  904. local q = spGetUnitCommands(unitID, 16) or {}
  905. for k = 1, #q do
  906. local c = q[k]
  907. if c.tag == r.waitTag then
  908. break
  909. end
  910. if c.id == CMD_RAW_MOVE and c.params then
  911. local cx, cz = c.params[1], c.params[3]
  912. for j = 1, #r.route do
  913. if Near(r.route[j], cx, cz, 1.5) then
  914. tags[#tags + 1] = c.tag
  915. pts[#pts + 1] = r.route[j]
  916. break
  917. end
  918. end
  919. end
  920. end
  921. return tags, pts
  922. end
  923.  
  924. local function Give(unitID, orders, r, frame)
  925. if #orders > 0 then
  926. spGiveOrderArrayToUnitArray({unitID}, orders)
  927. if r then
  928. r.pendingUntil = frame + 12
  929. end
  930. end
  931. end
  932.  
  933. local function InsertRouteOrders(orders, pts, skipLast)
  934. local last = skipLast and (#pts - 1) or #pts
  935. for k = 1, last do
  936. local p = pts[k]
  937. orders[#orders + 1] = {CMD_INSERT, {k - 1, CMD_RAW_MOVE, CMD_OPT_SHIFT, p[1], p[2], p[3]}, {"alt"}}
  938. end
  939. end
  940.  
  941. --------------------------------------------------------------------------------
  942. -- Repair detection
  943.  
  944. local function IsBeingRepaired(unitID, x, z)
  945. if not spGetUnitIsBuilding then
  946. return false
  947. end
  948. local near = spGetUnitsInCylinder(x, z, maxBuildDist + 60, ALLY_UNITS) or {}
  949. for k = 1, #near do
  950. local b = near[k]
  951. if b ~= unitID then
  952. local defID = spGetUnitDefID(b)
  953. if defID and builderDef[defID] and spGetUnitIsBuilding(b) == unitID then
  954. return true
  955. end
  956. end
  957. end
  958. return false
  959. end
  960.  
  961. local function IsSafe(unitID, r, frame, x, z)
  962. if frame - (r.lastDamage or -1000) < 45 then
  963. return false
  964. end
  965. return DangerAt(GetField(r.defID, frame), x, z) <= 0
  966. end
  967.  
  968. --------------------------------------------------------------------------------
  969. -- Retreat lifecycle
  970.  
  971. local function Adopt(unitID, defID, frame)
  972. local q = spGetUnitCommands(unitID, 4) or {}
  973. local w
  974. for k = 2, min(3, #q) do
  975. if q[k].id == CMD_WAIT then
  976. w = k
  977. break
  978. end
  979. end
  980. if not w or q[w - 1].id ~= CMD_RAW_MOVE then
  981. return -- not the gadget's move-and-wait: planes rearming, units already in a zone
  982. end
  983. local hasRearm = (w == 3)
  984. if hasRearm and q[1].id ~= CMD_REARM then
  985. return
  986. end
  987. local mp = q[w - 1].params
  988. local r = {
  989. defID = defID,
  990. dest = {mp[1], mp[2], mp[3]},
  991. waitTag = q[w].tag,
  992. hasRearm = hasRearm,
  993. state = 'moving',
  994. pendingUntil = 0,
  995. nextPlan = frame,
  996. }
  997. -- Gunships heading for an airpad keep the gadget's REARM; we only add waypoints.
  998. r.route = hasRearm and {} or {r.dest}
  999.  
  1000. -- The gadget's walk back: a RAW_MOVE to where the unit stood, last in the queue.
  1001. local ret = q[w + 1]
  1002. if ret and ret.id == CMD_RAW_MOVE and spGetUnitCommandCount(unitID) == w + 1 then
  1003. local x, _, z = spGetUnitPosition(unitID)
  1004. local tol = 24 + (UnitDefs[defID].speed or 0) * 0.75
  1005. if x and Near(ret.params, x, z, tol) then
  1006. r.returnTag = ret.tag
  1007. end
  1008. end
  1009.  
  1010. local full = spGetUnitCommands(unitID, -1) or {}
  1011. for k = 1, #full do
  1012. local c = full[k]
  1013. if c.id == CMD_PATROL and c.params and c.params[3] then
  1014. r.patrol = r.patrol or {}
  1015. r.patrol[#r.patrol + 1] = {c.params[1], c.params[2], c.params[3]}
  1016. end
  1017. end
  1018.  
  1019. retreaters[unitID] = r
  1020. patrolJobs[unitID] = nil
  1021.  
  1022. if r.returnTag and not V('returnToPosition') then
  1023. Give(unitID, {{CMD_REMOVE, {r.returnTag}, 0}}, r, frame)
  1024. r.returnTag = nil
  1025. end
  1026. end
  1027.  
  1028. local function Finish(unitID, r, frame)
  1029. retreaters[unitID] = nil
  1030. local orders = {}
  1031. local tags = RouteInQueue(unitID, r)
  1032. if #tags > 0 then
  1033. orders[#orders + 1] = {CMD_REMOVE, tags, 0}
  1034. end
  1035. if r.returnTag and not V('returnToPosition') and HasTag(unitID, r.returnTag) then
  1036. orders[#orders + 1] = {CMD_REMOVE, {r.returnTag}, 0}
  1037. end
  1038. Give(unitID, orders, nil, frame)
  1039. if r.patrol and V('resumePatrol') then
  1040. patrolJobs[unitID] = {pts = r.patrol, phase = 0, checkAt = frame + 10, giveUpAt = frame + 30 * 90}
  1041. end
  1042. end
  1043.  
  1044. local function CheckRetreaters(frame)
  1045. for unitID, defID in pairs(myMovers) do
  1046. local want = spGetUnitRulesParam(unitID, "retreat") == 1
  1047. local r = retreaters[unitID]
  1048. if r then
  1049. if not want then
  1050. Finish(unitID, r, frame)
  1051. elseif not HasTag(unitID, r.waitTag) then
  1052. -- A new order replaced the retreat. The gadget sends the unit
  1053. -- again once it goes idle, and we pick it up then.
  1054. retreaters[unitID] = nil
  1055. end
  1056. elseif want then
  1057. Adopt(unitID, defID, frame)
  1058. end
  1059. end
  1060. end
  1061.  
  1062. -- What is left of the current route, ending at dest.
  1063. local function RemainingRoute(r, remaining)
  1064. local old = {}
  1065. for k = 1, #remaining do
  1066. old[k] = remaining[k]
  1067. end
  1068. if #old == 0 or old[#old] ~= r.dest then
  1069. old[#old + 1] = r.dest
  1070. end
  1071. return old
  1072. end
  1073.  
  1074. -- A finished plan replaces the queued route if it is clearly safer. The queue is
  1075. -- read again here because the plan may have taken a few frames.
  1076. local function ApplyPlan(unitID, r, pts)
  1077. local frame = spGetGameFrame()
  1078. if retreaters[unitID] ~= r or r.state ~= 'moving' then
  1079. return
  1080. end
  1081. if frame < r.pendingUntil then
  1082. r.nextPlan = r.pendingUntil
  1083. return
  1084. end
  1085. r.planned = true
  1086. if not pts then
  1087. return
  1088. end
  1089. local tags, remaining = RouteInQueue(unitID, r)
  1090. if #tags == 0 and not r.hasRearm then
  1091. return -- arrived meanwhile
  1092. end
  1093. local x, _, z = spGetUnitPosition(unitID)
  1094. local f = GetField(r.defID, frame)
  1095. local oldCost = PolyCost(f, x, z, RemainingRoute(r, remaining))
  1096. local newCost = PolyCost(f, x, z, pts)
  1097. if newCost >= oldCost * 0.85 - 1 then
  1098. return
  1099. end
  1100. local orders = {}
  1101. if #tags > 0 then
  1102. orders[1] = {CMD_REMOVE, tags, 0}
  1103. end
  1104. InsertRouteOrders(orders, pts, r.hasRearm)
  1105. if r.hasRearm then
  1106. pts[#pts] = nil
  1107. end
  1108. r.route = pts
  1109. Give(unitID, orders, r, frame)
  1110. end
  1111.  
  1112. local function PlannerLoop()
  1113. while true do
  1114. local unitID = table.remove(planQueue, 1)
  1115. if not unitID then
  1116. coroutine.yield()
  1117. else
  1118. local r = retreaters[unitID]
  1119. if r then
  1120. r.planQueued = nil
  1121. if r.state == 'moving' and spValidUnitID(unitID) then
  1122. -- GetGameFrame returns a second value (days), which would land in budget.
  1123. local pts = PlanRoute(unitID, r, (spGetGameFrame()))
  1124. ApplyPlan(unitID, r, pts)
  1125. end
  1126. end
  1127. end
  1128. end
  1129. end
  1130.  
  1131. local function RequestPlan(unitID, r)
  1132. if r.planQueued then
  1133. return
  1134. end
  1135. r.planQueued = true
  1136. if r.planned then
  1137. planQueue[#planQueue + 1] = unitID
  1138. else
  1139. -- Units still on the gadget's straight line go first.
  1140. local k = 1
  1141. while planQueue[k] and retreaters[planQueue[k]] and not retreaters[planQueue[k]].planned do
  1142. k = k + 1
  1143. end
  1144. table.insert(planQueue, k, unitID)
  1145. end
  1146. end
  1147.  
  1148. local function RunPlanner()
  1149. if #planQueue == 0 then
  1150. return
  1151. end
  1152. if not planner or coroutine.status(planner) == "dead" then
  1153. planner = coroutine.create(PlannerLoop)
  1154. end
  1155. planSpent = 0
  1156. local ok, err = coroutine.resume(planner)
  1157. if not ok then
  1158. Spring.Echo("Auto Retreat Plus: planner error: " .. tostring(err))
  1159. planner = nil
  1160. end
  1161. end
  1162.  
  1163. -- Back on the move after a repair: straight to the zone at once, and a proper
  1164. -- plan as soon as the planner gets to it.
  1165. local function Resume(unitID, r, frame)
  1166. local orders = {}
  1167. InsertRouteOrders(orders, {r.dest}, false)
  1168. r.route = {r.dest}
  1169. r.state = 'moving'
  1170. r.planned = false
  1171. Give(unitID, orders, r, frame)
  1172. r.nextPlan = r.pendingUntil
  1173. end
  1174.  
  1175. local function UpdateRetreater(unitID, r, frame)
  1176. if frame < r.pendingUntil then
  1177. return
  1178. end
  1179. local x, _, z = spGetUnitPosition(unitID)
  1180. if not x then
  1181. return
  1182. end
  1183. if r.state == 'moving' then
  1184. local tags, remaining = RouteInQueue(unitID, r)
  1185. -- A gunship bound for an airpad starts with no moves of ours, so it has
  1186. -- not arrived until it had its chance at a plan.
  1187. if #tags == 0 and (not r.hasRearm or r.planned or not V('avoidEnemies')) then
  1188. r.state = 'arrived'
  1189. return
  1190. end
  1191. if V('stopForRepair') and not r.hasRearm and IsBeingRepaired(unitID, x, z) and IsSafe(unitID, r, frame, x, z) then
  1192. r.state = 'held'
  1193. r.lastRepair = frame
  1194. Give(unitID, {{CMD_REMOVE, tags, 0}}, r, frame)
  1195. return
  1196. end
  1197. if V('avoidEnemies') and frame >= r.nextPlan then
  1198. -- A route that touches no enemy reach cannot be improved on. Most
  1199. -- checks end here, which keeps the A* cost to units actually at risk.
  1200. -- Under fire, every second counts: replan about three times a second.
  1201. if frame - (r.lastDamage or -1000) < 30 then
  1202. r.nextPlan = frame + 9
  1203. else
  1204. r.nextPlan = frame + V('replanInterval') * 30
  1205. end
  1206. if r.planned then
  1207. local _, risky = PolyCost(GetField(r.defID, frame), x, z, RemainingRoute(r, remaining))
  1208. if not risky then
  1209. return
  1210. end
  1211. end
  1212. RequestPlan(unitID, r)
  1213. end
  1214. elseif r.state == 'held' then
  1215. if IsBeingRepaired(unitID, x, z) then
  1216. r.lastRepair = frame
  1217. end
  1218. if not V('stopForRepair') or not IsSafe(unitID, r, frame, x, z)
  1219. or frame - r.lastRepair >= V('repairResumeDelay') * 30 then
  1220. Resume(unitID, r, frame)
  1221. end
  1222. end
  1223. end
  1224.  
  1225. --------------------------------------------------------------------------------
  1226. -- Patrol restore
  1227.  
  1228. local function UpdatePatrolJob(unitID, job, frame)
  1229. if frame < job.checkAt then
  1230. return
  1231. end
  1232. job.checkAt = frame + 15
  1233. if frame > job.giveUpAt or not spValidUnitID(unitID) then
  1234. patrolJobs[unitID] = nil
  1235. return
  1236. end
  1237. local pts = job.pts
  1238. local q = spGetUnitCommands(unitID, -1) or {}
  1239. if job.phase == 0 then
  1240. for k = 1, #q do
  1241. if q[k].id == CMD_PATROL then
  1242. patrolJobs[unitID] = nil -- the loop survived the retreat
  1243. return
  1244. end
  1245. end
  1246. local p = pts[1]
  1247. local opts = (#q > 0) and CMD_OPT_SHIFT or 0
  1248. if #pts == 1 then
  1249. spGiveOrderToUnit(unitID, CMD_PATROL, p, opts)
  1250. patrolJobs[unitID] = nil
  1251. return
  1252. end
  1253. -- Walk onto the loop first. A patrol order starts from where the unit
  1254. -- stands, so ordering it from the retreat zone would add the zone to it.
  1255. spGiveOrderToUnit(unitID, CMD_RAW_MOVE, p, opts)
  1256. job.phase = 1
  1257. job.checkAt = frame + 30
  1258. return
  1259. end
  1260. local p = pts[1]
  1261. if #q > 0 then
  1262. for k = 1, #q do
  1263. if q[k].id == CMD_RAW_MOVE and Near(q[k].params, p[1], p[3], 1.5) then
  1264. return -- still walking there
  1265. end
  1266. end
  1267. patrolJobs[unitID] = nil -- given other orders meanwhile
  1268. return
  1269. end
  1270. -- Idle: arrived, or the move gave up short of the point. Either way patrol
  1271. -- the loop from here; in the second case the loop gains this spot.
  1272. local x, _, z = spGetUnitPosition(unitID)
  1273. local first = (x and Near(p, x, z, 120)) and 2 or 1
  1274. local orders = {}
  1275. for k = first, #pts do
  1276. orders[#orders + 1] = {CMD_PATROL, pts[k], (k > first) and CMD_OPT_SHIFT or 0}
  1277. end
  1278. spGiveOrderArrayToUnitArray({unitID}, orders)
  1279. patrolJobs[unitID] = nil
  1280. end
  1281.  
  1282. --------------------------------------------------------------------------------
  1283. -- Retreat zone engineers
  1284.  
  1285. local function UpdateHavens()
  1286. RETREAT_RADIUS = spGetGameRulesParam('retreatZoneRadius') or RETREAT_RADIUS
  1287. havens, myHavens = {}, {}
  1288. if not myAllyTeam then
  1289. return
  1290. end
  1291. for _, teamID in ipairs(spGetTeamList(myAllyTeam) or {}) do
  1292. local count = spGetTeamRulesParam(teamID, "haven_count") or 0
  1293. for i = 1, count do
  1294. local hx, hz = spGetTeamRulesParam(teamID, "haven_x" .. i), spGetTeamRulesParam(teamID, "haven_z" .. i)
  1295. if hx and hz then
  1296. havens[#havens + 1] = {hx, hz}
  1297. if teamID == myTeam then
  1298. myHavens[#myHavens + 1] = {hx, hz}
  1299. end
  1300. end
  1301. end
  1302. end
  1303. end
  1304.  
  1305. local function InHaven(x, z)
  1306. local r2 = (RETREAT_RADIUS + 24)^2
  1307. for k = 1, #havens do
  1308. local h = havens[k]
  1309. local dx, dz = x - h[1], z - h[2]
  1310. if dx*dx + dz*dz <= r2 then
  1311. return h
  1312. end
  1313. end
  1314. return nil
  1315. end
  1316.  
  1317. local function IsBuilt(unitID)
  1318. if spGetUnitIsBeingBuilt then
  1319. return not spGetUnitIsBeingBuilt(unitID)
  1320. end
  1321. local _, _, _, _, bp = spGetUnitHealth(unitID)
  1322. return bp and bp >= 1
  1323. end
  1324.  
  1325. local function PickRepairTarget(builderID, defID, x, z)
  1326. local reach = builderDef[defID] + (mobileDef[defID] and V('engineerReach') or 0)
  1327. local near = spGetUnitsInCylinder(x, z, reach + 60, ALLY_UNITS) or {}
  1328. local best, bestRank, bestDist
  1329. for k = 1, #near do
  1330. local u = near[k]
  1331. if u ~= builderID then
  1332. local uDef = spGetUnitDefID(u)
  1333. local hp, maxHp = spGetUnitHealth(u)
  1334. if uDef and not ignoreDef[uDef] and hp and maxHp and hp < maxHp - 0.5 and IsBuilt(u) then
  1335. local ux, _, uz = spGetUnitPosition(u)
  1336. local dx, dz = ux - x, uz - z
  1337. local dist = sqrt(dx*dx + dz*dz) - (spGetUnitRadius(u) or 0)
  1338. if dist <= reach then
  1339. -- Retreating units first, then by health fraction.
  1340. local rank = hp / maxHp + ((spGetUnitRulesParam(u, "retreat") == 1) and 0 or 1)
  1341. if not best or rank < bestRank - 0.001 or (abs(rank - bestRank) <= 0.001 and dist < bestDist) then
  1342. best, bestRank, bestDist = u, rank, dist
  1343. end
  1344. end
  1345. end
  1346. end
  1347. end
  1348. return best, bestDist
  1349. end
  1350.  
  1351. local function RunEngineers(frame)
  1352. if not V('havenEngineers') or #havens == 0 then
  1353. return
  1354. end
  1355. for b, defID in pairs(myBuilders) do
  1356. if IsBuilt(b) and not spGetUnitIsStunned(b) then
  1357. local x, y, z = spGetUnitPosition(b)
  1358. if x and InHaven(x, z) then
  1359. local count = spGetUnitCommandCount(b) or 0
  1360. local waiting = false
  1361. if count > 0 and spGetUnitRulesParam(b, "retreat") == 1 then
  1362. local q = spGetUnitCommands(b, 1)
  1363. waiting = q and q[1] and q[1].id == CMD_WAIT
  1364. end
  1365. if count == 0 or waiting then
  1366. local target, dist = PickRepairTarget(b, defID, x, z)
  1367. if target then
  1368. if waiting then
  1369. spGiveOrderToUnit(b, CMD_INSERT, {0, CMD_REPAIR, 0, target}, {"alt"})
  1370. else
  1371. local orders = {{CMD_REPAIR, {target}, 0}}
  1372. if mobileDef[defID] and dist > builderDef[defID] then
  1373. local p = RoundPoint(x, z)
  1374. orders[2] = {CMD_RAW_MOVE, p, CMD_OPT_SHIFT}
  1375. end
  1376. spGiveOrderArrayToUnitArray({b}, orders)
  1377. end
  1378. end
  1379. end
  1380. end
  1381. end
  1382. end
  1383. end
  1384.  
  1385. --------------------------------------------------------------------------------
  1386. -- Main loop
  1387.  
  1388. function widget:GameFrame(n)
  1389. if spectating then
  1390. return
  1391. end
  1392. if n % 15 == 0 then
  1393. CollectThreats(n) -- keeps enemyMemory current while nothing is retreating
  1394. end
  1395. if n % 30 == 15 then
  1396. UpdateHavens()
  1397. RunEngineers(n)
  1398. end
  1399. -- The gadget checks at n % 20 == 10, so this sees its orders the frame they appear.
  1400. if n % 20 == 10 then
  1401. CheckRetreaters(n)
  1402. end
  1403. if n % 6 == 0 then
  1404. for unitID, r in pairs(retreaters) do
  1405. if spValidUnitID(unitID) then
  1406. UpdateRetreater(unitID, r, n)
  1407. else
  1408. retreaters[unitID] = nil
  1409. end
  1410. end
  1411. for unitID, job in pairs(patrolJobs) do
  1412. UpdatePatrolJob(unitID, job, n)
  1413. end
  1414. end
  1415. RunPlanner()
  1416. if n % 1800 == 0 then
  1417. passCache = {} -- terraform
  1418. end
  1419. end
  1420.  
  1421. --------------------------------------------------------------------------------
  1422. -- Callins
  1423.  
  1424. local function AddUnit(unitID, unitDefID, unitTeam)
  1425. if unitTeam ~= myTeam or not unitDefID then
  1426. return
  1427. end
  1428. if moverDef[unitDefID] then
  1429. myMovers[unitID] = unitDefID
  1430. end
  1431. if builderDef[unitDefID] then
  1432. myBuilders[unitID] = unitDefID
  1433. end
  1434. end
  1435.  
  1436. local function RemoveUnit(unitID)
  1437. myMovers[unitID] = nil
  1438. myBuilders[unitID] = nil
  1439. retreaters[unitID] = nil
  1440. patrolJobs[unitID] = nil
  1441. end
  1442.  
  1443. function widget:UnitCreated(unitID, unitDefID, unitTeam)
  1444. AddUnit(unitID, unitDefID, unitTeam)
  1445. end
  1446.  
  1447. function widget:UnitGiven(unitID, unitDefID, newTeam)
  1448. AddUnit(unitID, unitDefID, newTeam)
  1449. end
  1450.  
  1451. function widget:UnitTaken(unitID, unitDefID, oldTeam, newTeam)
  1452. if newTeam ~= myTeam then
  1453. RemoveUnit(unitID)
  1454. end
  1455. end
  1456.  
  1457. function widget:UnitDestroyed(unitID)
  1458. RemoveUnit(unitID)
  1459. enemyMemory[unitID] = nil
  1460. end
  1461.  
  1462. function widget:UnitDamaged(unitID, unitDefID, unitTeam, damage)
  1463. local r = retreaters[unitID]
  1464. if r and (damage or 0) > 0 then
  1465. r.lastDamage = spGetGameFrame()
  1466. end
  1467. end
  1468.  
  1469. local function Reinitialize()
  1470. retreaters, myMovers, myBuilders, patrolJobs = {}, {}, {}, {}
  1471. fields, threatList, threatFrame, enemyMemory = {}, {}, -1000, {}
  1472. planQueue, planner = {}, nil
  1473. myTeam = Spring.GetMyTeamID()
  1474. myAllyTeam = Spring.GetMyAllyTeamID()
  1475. spectating = Spring.GetSpectatingState()
  1476. if spectating then
  1477. return
  1478. end
  1479. for _, unitID in ipairs(spGetTeamUnits(myTeam) or {}) do
  1480. AddUnit(unitID, spGetUnitDefID(unitID), myTeam)
  1481. end
  1482. UpdateHavens()
  1483. end
  1484.  
  1485. function widget:PlayerChanged()
  1486. if Spring.GetMyTeamID() ~= myTeam or Spring.GetSpectatingState() ~= spectating then
  1487. Reinitialize()
  1488. end
  1489. end
  1490.  
  1491. function widget:Initialize()
  1492. Reinitialize()
  1493. end
  1494.  
  1495. --------------------------------------------------------------------------------
  1496. -- Drawing
  1497.  
  1498. local glColor, glVertex, glLineWidth = gl.Color, gl.Vertex, gl.LineWidth
  1499. local glBeginEnd, glDrawGroundCircle, glDepthTest = gl.BeginEnd, gl.DrawGroundCircle, gl.DepthTest
  1500. local GL_LINE_STRIP = GL.LINE_STRIP
  1501.  
  1502. local stripPts, stripN = {}, 0
  1503. local function DrawStrip()
  1504. for k = 1, stripN do
  1505. local p = stripPts[k]
  1506. glVertex(p[1], p[2], p[3])
  1507. end
  1508. end
  1509.  
  1510. function widget:DrawWorld()
  1511. if spectating or not next(retreaters) then
  1512. return
  1513. end
  1514. local routes, threats = V('drawRoutes'), V('drawThreats')
  1515. if not routes and not threats then
  1516. return
  1517. end
  1518. glDepthTest(true)
  1519. glLineWidth(2)
  1520. local frame = spGetGameFrame()
  1521. local shown = {}
  1522. for unitID, r in pairs(retreaters) do
  1523. if spIsUnitSelected(unitID) then
  1524. local x, y, z = spGetUnitPosition(unitID)
  1525. if x then
  1526. if routes then
  1527. if r.state == 'held' then
  1528. glColor(0.3, 1, 0.4, 0.8)
  1529. glDrawGroundCircle(x, y, z, 40, 20)
  1530. elseif r.state == 'moving' then
  1531. local _, remaining = RouteInQueue(unitID, r)
  1532. stripN = 1
  1533. stripPts[1] = {x, y + 6, z}
  1534. for k = 1, #remaining do
  1535. local p = remaining[k]
  1536. stripN = stripN + 1
  1537. stripPts[stripN] = {p[1], spGetGroundHeight(p[1], p[3]) + 6, p[3]}
  1538. end
  1539. glColor(1, 0.8, 0.2, 0.8)
  1540. glBeginEnd(GL_LINE_STRIP, DrawStrip)
  1541. end
  1542. end
  1543. if threats then
  1544. local f = fields[Cats(r.defID).key]
  1545. if f and not shown[f] then
  1546. shown[f] = true
  1547. glColor(1, 0.25, 0.2, 0.35)
  1548. for k = 1, #f.threats do
  1549. local t = f.threats[k]
  1550. local dx, dz = t[1] - x, t[2] - z
  1551. if dx*dx + dz*dz < (t[3] + 1500)^2 then
  1552. glDrawGroundCircle(t[1], spGetGroundHeight(t[1], t[2]), t[2], t[3], 40)
  1553. end
  1554. end
  1555. end
  1556. end
  1557. end
  1558. end
  1559. end
  1560. glColor(1, 1, 1, 1)
  1561. glLineWidth(1)
  1562. glDepthTest(false)
  1563. end
  1564.  
Advertisement
Comments
  • User was banned
Add Comment
Please, Sign In to add comment