-------------------------------------------------------------------------------- -- info -------------------------------------------------------------------------------- function widget:GetInfo() return { name = "AutoScout", desc = "An automated scout", author = "SpliFF", date = "1 Oct, 2009", license = "GNU GPL, v2 or later", layer = 0, enabled = false -- loaded by default? } end -------------------------------------------------------------------------------- -- about -------------------------------------------------------------------------------- [[ This widget adds automated scouting behaviour. Scouts and idle intel units will spread out to cover important areas of the map ]] -------------------------------------------------------------------------------- -- settings -------------------------------------------------------------------------------- -- number of frames between updates. Set higher to reduce load on slower systems. local FRAMES_BETWEEN_FAST_UPDATES = 10 local FRAMES_BETWEEN_MEDIUM_UPDATES = 100 local FRAMES_BETWEEN_SLOW_UPDATES = 1000 -- area scale local AREA_SIZE_X = Game.mapSizeX / Game.mapX local AREA_SIZE_Z = Game.mapSizeZ / Game.mapZ local DEFAULT_AREA_SIZE = {AREA_SIZE_X, 0, AREA_SIZE_Z} -------------------------------------------------------------------------------- -- global vars -------------------------------------------------------------------------------- -- options for current mod local MOD = { -- how fast a unit must be capable of moving to be considered a scout minScoutSpeed = '7', minRadarRange = '3' } -- details of the map local MAP = { kms = {Game.mapX,Game.mapY}, size = {Game.mapSizeX,Game.mapSizeY} } -- for assigning unique area ids NEXT_AREA_ID = 1 -------------------------------------------------------------------------------- -- localised spring globals (for performance) -------------------------------------------------------------------------------- local CMD_GUARD = CMD.GUARD local CMD_MOVE = CMD.MOVE local band = math.bit_and local spGetMyTeamID = Spring.GetMyTeamID local spGetUnitBuildFacing = Spring.GetUnitBuildFacing local spGetUnitDefID = Spring.GetUnitDefID local spGetUnitGroup = Spring.GetUnitGroup local spGetUnitPosition = Spring.GetUnitPosition local spGetUnitCommands = Spring.GetUnitCommands local spGetUnitRadius = Spring.GetUnitRadius local spGiveOrderToUnit = Spring.GiveOrderToUnit local spSetUnitGroup = Spring.SetUnitGroup local spIsUnitInView = Spring.IsUnitInView local spGetUnitsInCylinder = Spring.GetUnitsInCylinder local spGetUnitsInRectangle = Spring.GetUnitsInRectangle local spGetTeamUnits = Spring.GetTeamUnits Spring.GetAllUnits --Spring.GetTeamUnitsCounts local echo = Spring.Echo -------------------------------------------------------------------------------- -- utility functions -------------------------------------------------------------------------------- -- To duplicate a table instead of simply pointing to it function table.copy(t) local t2 = {} for k,v in pairs(t) do t2[k] = v end return t2 end -- Copy table and sub-tables by value function table.deepcopy(t) local t2 = {} for k,v in pairs(t) do if type(v) == 'table' then t2[k] = table.deepcopy(v) else t2[k] = v end end return t2 end -- Based on distance calculation from Google Frog's Area Mex local function Dist(x1,z1,x2,z2) return sqrt((x1-x2)*(x1-x2)+(z1-z2)*(z1-z2)) end -- Distance between two points local function DistVec(p1,p2) local px1, py1, pz1 = unpack(p1) local px2, py2, pz2 = unpack(p2) return sqrt((px1-px2)*(px1-px2)+(pz1-pz2)*(pz1-pz2)) end -- returns a position vector with the y elevation set to ground level local function PosToGroundPos( pos ) return {pos.x, spGetGroundHeight(pos.x,pos.z), pos.z} end -- create a rectangle made of 4 points {tl, tr, br, bl} -- if anchor is the string 'center' then the area is created at that point otherwise the top left. local function NewRectAtPos(pos, size, anchor) local rect = {} local px, py, pz = unpack(pos) local sx, sy, sz = unpack(size) -- move offset from center to top left if anchor ~= nil and anchor == 'center' then px = px - math.floor(sx/2) pz = pz - math.floor(sx/2) end -- anchor rect over point return { PosToGroundPos({px, 0, pz}), -- top left PosToGroundPos({px+sx, 0, pz}), -- top right PosToGroundPos({px+sx, 0, pz+sz}), -- bottom right PosToGroundPos({px, 0, pz+sz}) -- bottom left } end -------------------------------------------------------------------------------- -- local functions -------------------------------------------------------------------------------- -- checks if a unitID can be put on automated duties local CanBeAutomated( unitID, unitDefID ) unitDefID = unitDefID or Spring.GetUnitDefID(unitID) -- skip invalid and dead units if (not Spring.ValidUnitID(unitID)) or Spring.GetUnitIsDead(unitID) then return false end -- don't automate units while the player has them selected if Spring.IsUnitSelected(unitID) then return false end -- make sure we aren't being transported if Spring.GetUnitTransporter(unitID) then return false end -- made it this far we passed return true end -- check if unit type can be used as scout local function CanScout( unitDef ) if UnitDefs[unitDef] then unitDef = UnitDefs[unitDef] end -- convert unitDefID return unitDef.speed > MOD.minScoutSpeed end -- check if unit type has radar local function HasRadar( unitDef ) if UnitDefs[unitDef] then unitDef = UnitDefs[unitDef] end -- convert unitDefID return unitDef.radarRadius > MOD.minRadarRadius end -- check if unit type has cloak device local function CanCloak( unitDef ) if UnitDefs[unitDef] then unitDef = UnitDefs[unitDef] end -- convert unitDefID return unitDef.canCloak end -- check whether scout is compatible with the area terrain local function CanPatrolArea(unitID, unitDefID, area) return true end -- initialise an area local function NewArea(pos, size, label) size = size or DEFAULT_AREA_SIZE label = label or local area = { -- id id = NEXT_AREA_ID, -- can be used to show area information label = label, -- center point pos = pos, -- width and height (x/y) size = size, -- border rect rect = NewRectAtPos(pos, size), -- unit ids in area units = {}, -- count of each unit type in area defs = {}, } -- update index for unique area ids NEXT_AREA_ID = NEXT_AREA_ID + 1 -- fill in missing values UpdateArea(area) -- return return area end -- periodic position updates. Adds intel and height data local function UpdatePos( pos ) pos = PosToGroundPos(area.pos) pos.intel = Spring.GetPositionLosState( unpack(area.pos) ) end -- periodic area updates local function UpdateArea( area ) -- update ground height at center UpdatePos( area.pos ) -- follow the border updating each corner for i, pos in ipairs(area.rect) do UpdatePos( area.pos ) end end -- order a unit to patrol an area local function DrawAreaOutline(unitID, unitDefID, area) local r = area.rect Spring.MarkerAddLine( r[0].x, 0, r[0].z, r[1].x, 0, r[1].z ) Spring.MarkerAddLine( r[1].x, 0, r[1].z, r[2].x, 0, r[2].z ) Spring.MarkerAddLine( r[2].x, 0, r[2].z, r[3].x, 0, r[3].z ) Spring.MarkerAddLine( r[3].x, 0, r[3].z, r[0].x, 0, r[0].z ) end -- order a unit to patrol an area local function PatrolArea(unitID, unitDefID, area) -- add to area units list area.units[unitID] = unitDefID -- show on map Spring.MarkerAddPoint(area.pos.x, area.pos.y, area.pos.z , 'patrol: '..unitDefID) DrawAreaOutline(area) return true end -- function to sort areas based on distance from a point (closest to furthest) local function GetAreasByDistanceFrom( pos ) -- return true if area 1 is closer than area 2 local function AreaIsCloser(a1, a2) return DistVec(a1.pos, pos) < DistVec(a2.pos, pos) end local sortedAreas = AREAS table.sort( sortedAreas, AreaIsCloser ) return sortedAreas end -- find nearest area with low visibility local function FindBestArea( unitID, unitDefID ) -- sort areas based on distance of area center from unit position (not accounting for terrain) local sortedAreas = GetAreasByDistanceFrom( Spring.GetUnitPosition(unitID) ) for i, area in ipairs(sortedAreas) do -- -- don't overcrowd an area if GetNumAreaScouts > MOD.maxAreaScouts and CanPatrolArea(unitID, unitDefID, area) then return area end end end -- scout nearest area with low visibility local function PatrolBestArea( unitID, unitDefID ) return PatrolArea( unitID, FindBestArea(unitID, unitDefID) ) end -------------------------------------------------------------------------------- -- callbacks -------------------------------------------------------------------------------- function widget:Initialize() -- only run if playing if Spring.GetSpectatingState() or Spring.IsReplay() then widgetHandler:RemoveWidget() return true end -- init end function widget:GameFrame(frame) -- this function is called a lot so it should be as optimised as much as possible -- one important optimisation is to closely monitor execution time and load -- and delay tasks that can wait CURRENT_FRAME = frame -- split tasks into different priority levels. if frame > NEXT_FAST_UPDATE then -- high priority updates LookForNearbyEnemies() elseif frame > NEXT_MEDIUM_UPDATE then -- medium priority updates elseif frame > NEXT_SLOW_UPDATE then -- low priority updates end end function widget:KeyPress(key, mods, isRepeat) -- catch end function widget:UnitIdle(unitID, unitDefID, unitTeam) local def = UnitDefs[unitDefID] -- set eligible units to scout automatically if unitTeam == myTeam and CanBeAutomated(unitID, unitDefID) and ( CanScout(def) or HasRadar(def) or CanCloak(def) ) then PatrolBestArea(unitID, unitDefID) end end -------------------------------------------------------------------------------- --------------------------------------------------------------------------------