Snippet #97786

TTL: forever — WordwrapView raw

on 2022/10/05 14:08:38 (UTC) by Anonymous as Lua

  1. local widgetName = "Smarter Nanos"  -- used also in Echo() definition
  2.  
  3. function widget:GetInfo()
  4.     return {
  5.         name      = widgetName,
  6.         desc      = "Highly responsive automatic management for static builders (and Funnelweb).\nReplaces 'Auto Patrol Nanos', 'Auto Patrol Nanos v2', and 'Smart Nanos' widgets -- disable them to use this one.\nFirst priority: to avoid energy stalling; second priority: to avoid metal overflow; third priority: repair own units.\nDoes not assist allied buildings, but repairs allied units if there is nothing better to do.\nTakes into account building priority.\nIf a player issues manual command(s) to a builder, it returns to automatic management upon finishing them.\nAdds two new command buttons to the command panel: 'Enable Automatic Management' (green 'AI' letters) and 'Disable Automatic Management' (red 'AI' letters).\nBy default all the builders are automatically managed until the auto-management is disabled by the latter command.",
  7.         author    = "rollmops, using some ideas from 'Smart Nanos' by TheFatController and 'Auto Patrol Nanos v2' by trepan",
  8.         date      = "Oct 2022",
  9.         license   = "GNU GPL, v2 or later",
  10.         layer     = 0,
  11.         handler   = true, -- for adding custom commands into UI
  12.         enabled   = true,
  13.     }
  14. end
  15.  
  16.  
  17. -- TL;DR: all the core logic is in widget:GameFrame()
  18.  
  19. --------------------------------------------------------------------------------
  20. -- Speedups
  21. --------------------------------------------------------------------------------
  22.  
  23. local spGetTeamResources         = Spring.GetTeamResources
  24. local spEcho                     = Spring.Echo
  25. local spGetUnitPosition          = Spring.GetUnitPosition
  26. local spGetTeamUnits             = Spring.GetTeamUnits
  27. local spGetUnitDefID             = Spring.GetUnitDefID
  28. local spGetUnitsInCylinder       = Spring.GetUnitsInCylinder
  29. local spGetFeaturesInCylinder    = Spring.GetFeaturesInCylinder
  30. local spGetFeatureResources      = Spring.GetFeatureResources
  31. local spGiveOrderToUnit          = Spring.GiveOrderToUnit
  32. local spGetUnitCurrentCommand    = Spring.GetUnitCurrentCommand
  33. local spGetSelectedUnits         = Spring.GetSelectedUnits
  34. local spGetUnitHealth            = Spring.GetUnitHealth
  35. local spGetUnitAllyTeam          = Spring.GetUnitAllyTeam
  36. local spGetUnitRulesParam        = Spring.GetUnitRulesParam
  37. local spGetUnitFeatureSeparation = Spring.GetUnitFeatureSeparation
  38. local spGetSpectatingState       = Spring.GetSpectatingState
  39. local spIsReplay                 = Spring.IsReplay
  40.  
  41. local CMD_RECLAIM       = CMD.RECLAIM
  42. local CMD_REPAIR        = CMD.REPAIR
  43. local CMD_STOP          = CMD.STOP
  44.  
  45. local customCmds        = VFS.Include("LuaRules/Configs/customcmds.lua")
  46. local CMD_PRIORITY      = customCmds.PRIORITY
  47.  
  48. local Game_maxUnits     = Game.maxUnits
  49.  
  50.  
  51. --------------------------------------------------------------------------------
  52. -- Config
  53. --------------------------------------------------------------------------------
  54.  
  55. local gameFramesInterval = 10   -- defines the frequency ( = 30 / gameFramesInterval Hz) of the management actions
  56.  
  57. local debug = false -- change to true to enable console/log messages (see Echo() command definition in Globals section)
  58.  
  59. local buildersNames = { "staticcon", "striderhub", "striderfunnelweb" } -- these types of builders will be managed
  60.  
  61. local energyDefs = {    -- structures whose building will be prioritized in case of low energy
  62.     [ UnitDefNames.energywind.id     ] = { cost = UnitDefNames.energywind.cost     },
  63.     [ UnitDefNames.energysolar.id    ] = { cost = UnitDefNames.energysolar.cost    },
  64.     [ UnitDefNames.energygeo.id      ] = { cost = UnitDefNames.energygeo.cost      },
  65.     [ UnitDefNames.energyheavygeo.id ] = { cost = UnitDefNames.energyheavygeo.cost },
  66.     [ UnitDefNames.energyfusion.id   ] = { cost = UnitDefNames.energyfusion.cost   },
  67.     [ UnitDefNames.energysingu.id    ] = { cost = UnitDefNames.energysingu.cost    },
  68. }
  69.  
  70. local metalDefs = {     -- structures whose building will be prioritized in case of high metal
  71.     [ UnitDefNames.staticcon.id      ] = { cost = UnitDefNames.staticcon.cost      },
  72.     [ UnitDefNames.staticstorage.id  ] = { cost = UnitDefNames.staticstorage.cost  },
  73.     [ UnitDefNames.striderhub.id     ] = { cost = UnitDefNames.striderhub.cost     },
  74.     [ UnitDefNames.factoryamph.id    ] = { cost = UnitDefNames.factoryamph.cost    },
  75.     [ UnitDefNames.factorycloak.id   ] = { cost = UnitDefNames.factorycloak.cost   },
  76.     [ UnitDefNames.factorygunship.id ] = { cost = UnitDefNames.factorygunship.cost },
  77.     [ UnitDefNames.factoryhover.id   ] = { cost = UnitDefNames.factoryhover.cost   },
  78.     [ UnitDefNames.factoryjump.id    ] = { cost = UnitDefNames.factoryjump.cost    },
  79.     [ UnitDefNames.factoryplane.id   ] = { cost = UnitDefNames.factoryplane.cost   },
  80.     [ UnitDefNames.factoryshield.id  ] = { cost = UnitDefNames.factoryshield.cost  },
  81.     [ UnitDefNames.factoryship.id    ] = { cost = UnitDefNames.factoryship.cost    },
  82.     [ UnitDefNames.factoryspider.id  ] = { cost = UnitDefNames.factoryspider.cost  },
  83.     [ UnitDefNames.factorytank.id    ] = { cost = UnitDefNames.factorytank.cost    },
  84.     [ UnitDefNames.factoryveh.id     ] = { cost = UnitDefNames.factoryveh.cost     },
  85.     [ UnitDefNames.plateamph.id      ] = { cost = UnitDefNames.plateamph.cost      },
  86.     [ UnitDefNames.platecloak.id     ] = { cost = UnitDefNames.platecloak.cost     },
  87.     [ UnitDefNames.plategunship.id   ] = { cost = UnitDefNames.plategunship.cost   },
  88.     [ UnitDefNames.platehover.id     ] = { cost = UnitDefNames.platehover.cost     },
  89.     [ UnitDefNames.platejump.id      ] = { cost = UnitDefNames.platejump.cost      },
  90.     [ UnitDefNames.plateplane.id     ] = { cost = UnitDefNames.plateplane.cost     },
  91.     [ UnitDefNames.plateshield.id    ] = { cost = UnitDefNames.plateshield.cost    },
  92.     [ UnitDefNames.plateship.id      ] = { cost = UnitDefNames.plateship.cost      },
  93.     [ UnitDefNames.platespider.id    ] = { cost = UnitDefNames.platespider.cost    },
  94.     [ UnitDefNames.platetank.id      ] = { cost = UnitDefNames.platetank.cost      },
  95.     [ UnitDefNames.plateveh.id       ] = { cost = UnitDefNames.plateveh.cost       },
  96. }   -- This list was generated with the help of the following one-liner:
  97. -- Zero-K-master/units$ for f in $(ls | grep -P "(factory|plate)" | grep -oP '\w+(?=\.lua)'); do echo -e "\t[ UnitDefNames.$f.id\t] = { cost = UnitDefNames.$f.cost\t},"; done
  98.  
  99. -- define two custom commands ("enable/disable auto-management") to be added to each builder's command list
  100.  
  101. -- each command should have its number. for custom commands as these, put some random numbers, but not already taken in https://springrts.com/wiki/Lua_CMDs or in LuaRules/Configs/customcmds.lua
  102. local CMD_ENABLE_MANAGE_BUILDER  = 18247
  103. local CMD_DISABLE_MANAGE_BUILDER = 18248
  104.  
  105. -- define the two commands ( will be applied to selected builders by widget:CommandsChanged )
  106. local cmdEnableManageBuilder = {
  107.     id      = CMD_ENABLE_MANAGE_BUILDER,
  108.     type    = CMDTYPE.ICON,       -- expect 0 parameters in return
  109.     tooltip = 'Enable Automatic Management (Smarter Nanos widget)',
  110.     action  = 'smarter_nanos_on', --  "it can be binded to a key (eg: /bind f fight, will activate FIGHT when f is pressed" <-- LuaRules/Configs/customCmdTypes.lua
  111.     params  = {},
  112.     texture = 'LuaUI/Images/commands/states/ai_on.png', -- green 'AI' letters
  113. }
  114.  
  115. local cmdDisableManageBuilder = {
  116.     id      = CMD_DISABLE_MANAGE_BUILDER,
  117.     type    = CMDTYPE.ICON,
  118.     tooltip = 'Disable Automatic Management (Smarter Nanos widget)',
  119.     action  = 'smarter_nanos_off',
  120.     params  = {},
  121.     texture = 'LuaUI/Images/commands/states/ai_off.png',    -- red 'AI' letters
  122. }
  123.  
  124. --------------------------------------------------------------------------------
  125. -- Constants
  126. --------------------------------------------------------------------------------
  127.  
  128. local myTeamID     = Spring.GetMyTeamID()
  129. local myAllyTeamID = Spring.GetMyAllyTeamID()
  130.  
  131. VFS.Include("LuaRules/Configs/constants.lua") -- for HIDDEN_STORAGE which is 10,000 and should be subtracted from metalStorage and energyStorage provided by spGetTeamResources() to acquire the actual storage value.
  132.  
  133. --------------------------------------------------------------------------------
  134. -- Globals
  135. --------------------------------------------------------------------------------
  136.  
  137. local buildersDefID = {}    -- populated by the 'for' loop below, using buildersNames defined in Config section
  138.  
  139. for _,n in pairs( buildersNames ) do
  140.     local defID = UnitDefNames[n]
  141.     buildersDefID[ defID.id ] = { name = defID.humanName, range = defID.buildDistance, mobile = not defID.isImmobile }
  142. end
  143.  
  144. -- following are set and used in widget:GameFrame
  145. local energyState               -- current energy state, can be "low" or "fine"
  146. local metalState                -- current metal state, can be "low", "medium", or "high"
  147. local lastEnergyState = "fine"    -- what was the previous energy state
  148. local lastMetalState  = "high"  -- what was the previous metal state
  149.  
  150. local function Echo(...)
  151. -- if 'debug' (defined in Config section) is true,
  152. -- accepts any number of arguments, concatenates them to a space-delimited string, then spEcho() it.
  153.  
  154.     if not debug then return end
  155.  
  156.     local msg = widgetName..":"
  157.     for _, s in pairs{...} do
  158.         msg = msg .. " " .. tostring(s)
  159.     end
  160.     spEcho( msg )
  161. end
  162.  
  163.  
  164. --------------------------------------------------------------------------------
  165. -- Builder Class and its Methods
  166. --------------------------------------------------------------------------------
  167.  
  168. local builders = {}     -- holds builders' objects
  169.  
  170. local builderClass = {  -- each builder gets its own object derived from this class
  171.     id,
  172.     defid,
  173.     name,
  174.     range,
  175.     mobile,             -- Boolean
  176.     pos = {},
  177.     managed,            -- Boolean, whether the builder is under automated management;
  178.                         -- True by default, can be changed by the custom commands which this widget adds to builders' command panel.
  179.     manualCommand,      -- Boolean, whether the builder is currently doing manual-issued command(s),
  180.                         -- upon finishing which, will return to automated management.
  181.     myUnitsInRange,
  182.     currentCmdID,
  183.     currentCmdParam1,
  184. }
  185.  
  186. function builderClass:New( unitID, unitDefID )  -- the second argument is optional
  187.  
  188.     local o = {}
  189.  
  190.     setmetatable(o, self)
  191.     self.__index = self
  192.  
  193.     o.id     = unitID
  194.     o.defid  = unitDefID or spGetUnitDefID( unitID )
  195.     o.name   = buildersDefID[ o.defid ].name
  196.     o.range  = buildersDefID[ o.defid ].range
  197.     o.mobile = buildersDefID[ o.defid ].mobile
  198.  
  199.     local x,y,z = spGetUnitPosition( unitID )
  200.     o.pos   = { x = x, y = y, z = z }
  201.  
  202.     spGiveOrderToUnit( unitID, CMD_PRIORITY, 1, 0 ) -- set building priority to "normal"
  203.     o.managed       = true
  204.     o.manualCommand = false
  205.  
  206.     Echo( "added:", o.id, o.name, "mobile:", o.mobile, "range =", o.range, "located at", o.pos.x, o.pos.y, o.pos.z )
  207.  
  208.     return o
  209. end
  210.  
  211. function builderClass:Stop()
  212.     spGiveOrderToUnit( self.id, CMD_STOP, 0, 0 )
  213. end
  214.  
  215. -- each following builderClass function returns "true" if is resulted in an order given to a builder, "false" otherwise.
  216. -- in the latter case, another function (with lower priority) will be called (see widget:GameFrame).
  217.  
  218. -- there are three general functions: ReclaimFeatures(condition), AssistBuilding(arg),
  219. -- and RepairUnits(units); then there are quite a few specific functions which provide a specific task by supplying
  220. -- a relevant argument to one of the general functions, for example: RepairOwnUnits() calls RepairUnits(myUnitsInRange)
  221.  
  222. function builderClass:ReclaimFeatures( condition )
  223. -- "condition" is a boolean function of feature's reclaimable energy and metal, defining what to reclaim (e.g., "only metal", "prefer energy" etc.)
  224.  
  225.     local featuresInRange = spGetFeaturesInCylinder( self.pos.x, self.pos.z, self.range )
  226.     -- this functions returns also features that only their edge is in range, but their center is out of range, so they cannot be reclaimed. Hence additional checkup is needed (see the "if" statement below).
  227.     -- seems that the similar GetUnitsInCylinder() doesn't suffer from this issue.
  228.  
  229.     if not featuresInRange then return false end
  230.  
  231.     for _, featureID in pairs( featuresInRange ) do
  232.  
  233.         local RemainingMetal, _, RemainingEnergy = spGetFeatureResources( featureID )
  234.  
  235.         local target = featureID + Game_maxUnits -- convert featureID to absoluteID for GiveOrderToUnit()
  236.  
  237.         -- check that:
  238.         -- (1) the feature meets the condition requirement
  239.         -- (2) the feature is actually in builder's range (see comment about spGetFeaturesInCylinder above)
  240.         -- features also have "reclaimable" boolean value, but seems that there is no need to check it
  241.         if  condition( RemainingEnergy, RemainingMetal ) and
  242.             spGetUnitFeatureSeparation( self.id, featureID) < self.range then
  243.  
  244.             -- give order to reclaim unless the builder is already reclaiming this feature
  245.             if not ( self.currentCmdID and self.currentCmdID == CMD_RECLAIM and self.currentCmdParam1 and self.currentCmdParam1 == target ) then
  246.                 spGiveOrderToUnit( self.id, CMD_RECLAIM, target, 0 )
  247.             end
  248.  
  249.             return true
  250.         end
  251.     end
  252.     return false
  253. end
  254.  
  255. function builderClass:ReclaimEnergyOnly()
  256.     Echo( self.id, ": ReclaimEnergyOnly" )
  257.     return self:ReclaimFeatures( function( E, M ) return E > 1 and M < 1 end )
  258.     -- some features have some weird very low values of E or M (e.g. on Mescaline, San Pedro mushrooms have 0.001M),
  259.     -- so always compare to 1, not to 0.
  260. end
  261.  
  262. function builderClass:ReclaimEnergyMainly()
  263.     Echo( self.id, ": ReclaimEnergyMainly" )
  264.     return self:ReclaimFeatures( function( E, M ) return E > M end )
  265. end
  266.  
  267. function builderClass:ReclaimMetal()
  268.     Echo( self.id, ": ReclaimMetal" )
  269.     return self:ReclaimFeatures( function( E, M ) return M > 1 and E < 1 end )
  270. end
  271.  
  272. function builderClass:ReclaimAny()
  273.     Echo( self.id, ": ReclaimAny" )
  274.     return self:ReclaimFeatures( function( E, M ) return M > 1 or E > 1 end )
  275. end
  276.  
  277. function builderClass:AssistBuilding( arg )
  278. -- assist is always for own units only.
  279. -- if no list of DefIDs is provided, assist any own unit, otherwise only the specified kinds (DefIDs).
  280. -- assist only units with building priority no less than minPriority ("normal" if the argument isn't provided).
  281. -- choose a unit to assist by its building priority, then by how much metal remains to spend (see table.sort() below).
  282. -- since both arguments are optional, the function receives a table of named arguments ( DefIds and minPriority ).
  283.  
  284.     if not self.myUnitsInRange then return false end
  285.  
  286.     minPriority = arg.minPriority or 1 -- (0-low, 1-normal, 2-high)
  287.  
  288.     local unitsToAssist = {}    -- this table will be populated with all relevant units, then sorted to find the best.
  289.  
  290.     for _, unitID in pairs( self.myUnitsInRange ) do
  291.  
  292.         local unitDefID = spGetUnitDefID( unitID )
  293.  
  294.         local _,_,_,_,buildProgress = spGetUnitHealth( unitID )
  295.         local buildPriority = spGetUnitRulesParam(unitID, "buildpriority") or 1 -- (0-low, 1-normal, 2-high)
  296.  
  297.         -- check that:
  298.         -- (1) unit is not finished (needs building assist)
  299.         -- (2) list of DefIds wasn't provided (assist any), or unit's DefId is in the list
  300.         -- (3) buildPriority is equal or higher to minPriority
  301.         if  buildProgress and buildProgress < 1             and
  302.             ( not arg.DefIDs or arg.DefIDs[ unitDefID ] )   and
  303.             buildPriority >= minPriority                    then
  304.  
  305.             local unitCost = arg.DefIDs and arg.DefIDs[unitDefID].cost or UnitDefs[unitDefID].cost
  306.             local metalToInvest = unitCost * ( 1 - buildProgress )
  307.             unitsToAssist[ #unitsToAssist + 1 ] = { unitID = unitID, metalToInvest = metalToInvest, buildPriority = buildPriority }
  308.  
  309.         end
  310.     end
  311.  
  312.     if #unitsToAssist == 0 then return false end
  313.  
  314.     table.sort( unitsToAssist, function(a,b)
  315.     -- sort first by building priority (the higher, the better), then by metalToinvest (the less, the better)
  316.         return a.buildPriority ~= b.buildPriority and a.buildPriority > b.buildPriority or a.metalToInvest < b.metalToInvest end )
  317.  
  318.     local bestUnitToAssist = unitsToAssist[1].unitID
  319.     if not ( self.currentCmdID and self.currentCmdID == CMD_REPAIR and self.currentCmdParam1 and self.currentCmdParam1 == bestUnitToAssist ) then
  320.         spGiveOrderToUnit( self.id, CMD_REPAIR, bestUnitToAssist, 0 )
  321.     end
  322.     return true
  323. end
  324.  
  325. function builderClass:AssistEnergyStructures()
  326.     Echo( self.id, ": AssistEnergyStructures" )
  327.     return self:AssistBuilding{ DefIDs = energyDefs }
  328. end
  329.  
  330. function builderClass:AssistMetalSpending()
  331.     Echo( self.id, ": AssistMetalSpending" )
  332.     return self:AssistBuilding{ DefIDs = metalDefs }
  333. end
  334.  
  335. function builderClass:AssistAnyNormal()
  336.     Echo( self.id, ": AssistAnyNormal" )
  337.     return self:AssistBuilding{}
  338. end
  339.  
  340. function builderClass:AssistAnyLow()
  341.     Echo( self.id, ": AssistAnyLow" )
  342.     return self:AssistBuilding{ minPriority = 0 }
  343. end
  344.  
  345. function builderClass:RepairUnits( units )
  346. -- repair either own or allied units, depending on the argument (units list).
  347. -- choose the unit with the least HP deficiency (see table.sort() below)
  348.  
  349.     if not units then return false end
  350.  
  351.     local unitsToRepair = {}    -- this table will be populated with all relevant units, then sorted to find the best.
  352.  
  353.     for _, unitID in pairs( units ) do
  354.         local currentHP, maxHP, _, _, buildProgress = spGetUnitHealth( unitID )
  355.  
  356.         -- check that:
  357.         -- (1) unit is finished, because we want to repair, not to build
  358.         -- (2) unit needs repair
  359.         -- (3) not trying to repair itself
  360.         if buildProgress == 1 and currentHP < maxHP and unitID ~= self.id then
  361.             unitsToRepair[ #unitsToRepair + 1 ] = { unitID = unitID, HP_deficiency = maxHP - currentHP }
  362.         end
  363.     end
  364.     if #unitsToRepair == 0 then return false end
  365.  
  366.     table.sort( unitsToRepair, function(a,b) return a.HP_deficiency < b.HP_deficiency end )
  367.     local fastestRepairUnitID = unitsToRepair[1].unitID
  368.  
  369.     if not ( self.currentCmdID and self.currentCmdID == CMD_REPAIR and self.currentCmdParam1 and self.currentCmdParam1 == fastestRepairUnitID ) then
  370.         spGiveOrderToUnit( self.id, CMD_REPAIR, fastestRepairUnitID, 0 )
  371.     end
  372.     return true
  373. end
  374.  
  375. function builderClass:RepairOwnUnits()
  376.     Echo( self.id, ": RepairOwnUnits" )
  377.     return self:RepairUnits( self.myUnitsInRange )
  378. end
  379.  
  380. function builderClass:RepairAllyUnits()
  381.     Echo( self.id, ": RepairAllyUnits" )
  382.     local unitsInRange = spGetUnitsInCylinder( self.pos.x, self.pos.z, self.range )
  383.     if not unitsInRange then return false end
  384.     local allyUnitsInRange = {}
  385.     for _, unitID in pairs( unitsInRange ) do
  386.         if spGetUnitAllyTeam( unitID ) == myAllyTeamID then
  387.             allyUnitsInRange[ #allyUnitsInRange + 1 ] = unitID
  388.         end
  389.     end
  390.     return self:RepairUnits( allyUnitsInRange )
  391. end
  392.  
  393.  
  394. --------------------------------------------------------------------------------
  395. -- Callins
  396. --------------------------------------------------------------------------------
  397.  
  398. -- all the core logic is in widget:GameFrame
  399.  
  400. function widget:GameFrame(n)
  401.  
  402.     if n % gameFramesInterval ~= 1 then return end
  403.  
  404.     local metal, metalStorage = spGetTeamResources(myTeamID, "metal")
  405.  
  406.     metalStorage = metalStorage - HIDDEN_STORAGE        -- see explanation in Constants section
  407.  
  408.     local energy, energyStorage = spGetTeamResources(myTeamID, "energy")
  409.  
  410.     energyStorage = energyStorage - HIDDEN_STORAGE      -- see explanation in Constants section
  411.  
  412.     -- lastEnergyState adds some hysteresis to avoid fast switching back and forth around Energy States boundary;
  413.     -- in other words, moving from "low" to "fine" energy state requires more energy than moving from "fine" to "low",
  414.     energyState =
  415.         ( energy < 0.2 * energyStorage or lastEnergyState == "low" and energy < 0.3 * energyStorage ) and "low"
  416.         or "fine"
  417.  
  418.     -- lastMetalState plays a similar role to that of lastEnergyState above
  419.     metalState =
  420.         ( metal < 0.3 * metalStorage  or lastMetalState == "low"  and metal < 0.4 * metalStorage )  and "low"
  421.         or
  422.         ( metal > 0.75 * metalStorage or lastMetalState == "high" and metal > 0.65 * metalStorage ) and "high"
  423.         or "medium"
  424.  
  425.     lastEnergyState = energyState
  426.     lastMetalState  = metalState
  427.  
  428.     Echo( "Metal State:", metalState, "Energy State:", energyState )
  429.  
  430.     for _, builder in pairs( builders ) do
  431.  
  432.         if builder.managed then     -- if automatic management was disabled, there is nothing to do
  433.  
  434.             local cmdID, _, _, cmdParam1 = spGetUnitCurrentCommand( builder.id )
  435.  
  436.             Echo( builder.id, "cmd:", CMD[cmdID] or cmdID or "none", "managed:", builder.managed, "manualCommand:", builder.manualCommand )
  437.  
  438.             if not builder.manualCommand then
  439.             -- if the builder is performing manual-issued command(s), leave it alone
  440.             -- until it becomes idle (see 'elseif' clause of this level below)
  441.  
  442.                 builder.currentCmdID     = cmdID
  443.                 builder.currentCmdParam1 = cmdParam1
  444.  
  445.                 if builder.mobile then
  446.                     local x,y,z = spGetUnitPosition( builder.id )
  447.                     builder.pos   = { x = x, y = y, z = z }
  448.                 end
  449.  
  450.                 builder.myUnitsInRange = spGetUnitsInCylinder( builder.pos.x, builder.pos.z, builder.range, myTeamID )
  451.  
  452.                 if energyState == "low" then dummy =
  453.                 -- 'dummy = ...' is used to try each task (they are organized in priority-descending order) in turn;
  454.                 -- if a task was successful (an order was given to the builder), it returns 'true',
  455.                 -- so the attempts will stop here;
  456.                 -- if a task was unsuccessful (e.g., builder:ReclaimEnergyOnly() was called, but there are no
  457.                 -- pure-energy features in the builder's range), the task will return 'false', then
  458.                 -- the next function will be called.
  459.                 -- if all tasks fail, the Stop() command will be issued (always last in the chain).
  460.  
  461.                                             builder:ReclaimEnergyOnly()         or
  462.                     metalState == "low" and builder:ReclaimEnergyMainly()       or
  463.                                             builder:AssistEnergyStructures()    or
  464.                                             builder:ReclaimEnergyMainly()       or
  465.                                             builder:Stop()
  466.  
  467.                 elseif metalState == "high" then dummy =
  468.  
  469.                                             builder:AssistMetalSpending()       or
  470.                                             builder:AssistAnyNormal()           or
  471.                                             builder:AssistAnyLow()              or
  472.                                             builder:RepairOwnUnits()            or
  473.                                             builder:RepairAllyUnits()           or
  474.                                             builder:Stop()
  475.                 else dummy =
  476.                                             builder:RepairOwnUnits()            or
  477.                                             builder:ReclaimMetal()              or
  478.                                             builder:ReclaimAny()                or
  479.                                             builder:RepairAllyUnits()           or
  480.                                             builder:AssistAnyNormal()           or
  481.                                             builder:Stop()
  482.                 end
  483.  
  484.             elseif not cmdID then   -- builder was on manual-issued command(s) but idle now, therefore
  485.                                     -- returns to automated management.
  486.                                     -- in this gameFrame set its priority to "normal";
  487.                                     -- a building order will be issued in the next checked frame
  488.                                     -- to avoid interference.
  489.  
  490.                 builder.manualCommand = false
  491.                 spGiveOrderToUnit( builder.id, CMD_PRIORITY, 1, 0 )
  492.             end
  493.         end
  494.     end
  495. end
  496.  
  497. function widget:CommandNotify(cmdID, params, options)
  498. -- used to check if one of the two custom commands ("enable/disable auto-management") was issued,
  499. -- or, in case of any other command, switch the builder to "manual" state, since
  500. -- orders issued by the widget itself will not be caught by this callin
  501.  
  502.     Echo("CommandNotify:", CMD[cmdID] or cmdID )
  503.  
  504.     selectedUnits = spGetSelectedUnits()
  505.     if not selectedUnits then return end
  506.  
  507.     local returnValue -- return after "for" loop; "return true" discards the issued command
  508.  
  509.     for _, unitID in pairs( selectedUnits ) do
  510.         if builders[ unitID ] then
  511.             if cmdID == CMD_DISABLE_MANAGE_BUILDER then
  512.                 Echo( "management disabled for", unitID )
  513.                 builders[ unitID ].managed = false
  514.                 returnValue = true
  515.             elseif cmdID == CMD_ENABLE_MANAGE_BUILDER then
  516.                 Echo( "management enabled for", unitID )
  517.                 spGiveOrderToUnit( unitID, CMD_PRIORITY, 1, 0 )
  518.                 builders[ unitID ].managed = true
  519.                 returnValue = true
  520.             else
  521.                 builders[ unitID ].manualCommand = true
  522.                 returnValue = false
  523.             end
  524.         end
  525.     end
  526.  
  527.     return returnValue
  528. end
  529.  
  530. function widget:CommandsChanged()
  531. -- Called when the command descriptions (the "commands" panel) changed, i.e. when selecting or deselecting units of different types, to add the two custom commands, if any builder was selected.
  532.  
  533.     selectedUnits = spGetSelectedUnits()
  534.     if not selectedUnits then return end
  535.  
  536.     for _, unitID in pairs( selectedUnits ) do
  537.         if builders[ unitID ] then
  538.             local customCommands = widgetHandler.customCommands
  539.             customCommands[#customCommands+1] = cmdEnableManageBuilder
  540.             customCommands[#customCommands+1] = cmdDisableManageBuilder
  541.             return
  542.         end
  543.     end
  544. end
  545.  
  546. -- remaining is the usual boilerplate
  547.  
  548. function widget:UnitFinished( unitID, unitDefID, unitTeam )
  549.     if unitTeam == myTeamID and buildersDefID[ unitDefID ] and not builders[ unitID ] then
  550.         builders[ unitID ] = builderClass:New( unitID, unitDefID )
  551.     end
  552. end
  553.  
  554. function widget:UnitGiven(unitID, unitDefID, unitTeam)
  555.     local _,_,_,_,buildProgress = spGetUnitHealth ( unitID )
  556.     if buildProgress and buildProgress == 1 then
  557.         widget:UnitFinished(unitID, unitDefID, unitTeam)
  558.     end
  559. end
  560.  
  561. function widget:UnitDestroyed(unitID)
  562.     builders[ unitID ] = nil
  563. end
  564.  
  565. function widget:UnitTaken(unitID)
  566.     widget:UnitDestroyed(unitID)
  567. end
  568.  
  569. function widget:Initialize()
  570.     if spGetSpectatingState() or spIsReplay() then
  571.         widgetHandler:RemoveWidget(widget)
  572.     end
  573.     local myUnits = spGetTeamUnits( myTeamID )
  574.     if myUnits then
  575.         for _, unitID in pairs( myUnits ) do
  576.             widget:UnitGiven(unitID, spGetUnitDefID( unitID ), myTeamID)
  577.         end
  578.     end
  579. end
  580.  
  581. function widget:PlayerChanged (playerID)
  582.     if spGetSpectatingState() then
  583.         widgetHandler:RemoveWidget(widget)
  584.     end
  585. end

Recent Snippets